From 0bfbc29af483f01907a9035c1712a816c5d3f451 Mon Sep 17 00:00:00 2001 From: Matt <174058705+asdf8675309@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:37:25 -0400 Subject: [PATCH] telos: make TelosFreshness and InterviewScan split-file aware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What LifeOS supports a split-file TELOS: TELOS.md is a thin index, and each section lives in its own ALLCAPS file (MISSION.md, GOALS.md, ...). GenerateTelosSummary already reads those, but TelosFreshness and InterviewScan only ever looked at TELOS.md's H2 headings. So on a split-file setup, freshness misses every section and the interview report scores them as "does not exist." ## Change (two files, one concern) - TelosFreshness.ts: adds SPLIT_FILE_BY_SLUG (the inverse of GenerateTelosSummary's LEGACY_FILE_TO_SECTION) and an exported splitFilePathForSlug(). readTelosFreshness now surfaces each split file as its own section, and the freshness bump writes the split file. One shared extractPreview() helper feeds both the H2 scan and the split scan. - InterviewScan.ts: extractSectionBody reads the split file's body as the real source. It imports from TelosFreshness, which is why this is one PR. When a section shows up as both a TELOS.md H2 and a split file, the split file wins — same as GenerateTelosSummary, and same as what TELOS.md itself says. In a normal thin-index setup nothing lives in both, so this only settles the odd overlap case. ## Why it's safe For a plain unified TELOS.md there's no split file, so none of this runs and nothing changes. The split path only kicks in when the sibling file exists. A populated split file wins; an empty one falls back to the H2, so a half-made file can't hide real content. Nothing gets counted twice. The only write is the existing freshness bump, now pointed at the right file. ## Verification Tested on macOS, Bun 1.3.14, against a real split-file TELOS (thin index plus 21 ALLCAPS files). | TELOS shape | TelosFreshness sections | InterviewScan on Mission | |-------------|-------------------------|--------------------------| | unified (H2 per section) | all H2 sections (unchanged) | reads H2 body (unchanged) | | split, BEFORE this change | 3 (only the index H2s) | 0% "section does not exist" | | split, AFTER this change | 24 (3 H2 + 21 split, real per-file freshness) | 100% (reads MISSION.md body) | | split, sibling genuinely absent | not surfaced | 0% "section does not exist" (correct) | ## Known follow-up SPLIT_FILE_BY_SLUG and LEGACY_FILE_TO_SECTION are the same mapping in two directions, kept by hand in two different tools — add a TELOS concept and you have to update both. A single shared map would kill the drift risk. Left it out here to keep this to one concern. ## Scope TelosFreshness.ts + InterviewScan.ts. No new dependency. Unified-TELOS users see no change. Co-authored-by: Claude --- LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts | 14 ++- LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts | 87 +++++++++++++++++-- 2 files changed, 90 insertions(+), 11 deletions(-) diff --git a/LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts b/LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts index 86546bf4c3..9ec95aeda7 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts @@ -25,7 +25,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { readFileSync, existsSync } from "fs"; import { join } from "path"; -import { readTelosFreshness, sectionSlug, type SectionFreshness } from "./TelosFreshness"; +import { readTelosFreshness, sectionSlug, splitFilePathForSlug, parseFrontmatter, type SectionFreshness } from "./TelosFreshness"; // Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404) for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { @@ -274,7 +274,17 @@ function parseTelosSections(content: string): Map { function extractSectionBody(target: RegistryTarget, sections: Map): string | null { if (!target.path.startsWith(TELOS_SECTION_PREFIX)) return null; - return sections.get(sectionSlug(target.name)) ?? null; + const slug = sectionSlug(target.name); + // Split-file mode wins: TELOS.md is a thin index, so the per-concept file is + // authoritative when it has content — matching GenerateTelosSummary and TELOS.md's + // own "split file wins" rule. Strip frontmatter so scoring matches how TELOS.md H2 + // bodies are measured (and a prior last_updated bump can't inflate the body). + const splitPath = splitFilePathForSlug(slug, TELOS_PATH); + if (splitPath) { + const body = parseFrontmatter(readFileSync(splitPath, "utf-8")).rest; + if (body != null && body.trim().length > 0) return body; + } + return sections.get(slug) ?? null; } function freshnessForSection(target: RegistryTarget): SectionFreshness | null { diff --git a/LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts b/LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts index 7623a85c7c..63ee839520 100755 --- a/LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts +++ b/LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts @@ -32,7 +32,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { readFileSync, writeFileSync, existsSync } from "fs"; import { getDAName } from "../../hooks/lib/identity" -import { basename, join } from "path"; +import { basename, dirname, join } from "path"; // Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404) for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { @@ -115,6 +115,50 @@ export const STALENESS_THRESHOLDS: Record = { const DEFAULT_THRESHOLD_DAYS = 180; +// ─── Split-file mode support ──────────────────────────────────────────────── +// +// LifeOS schema "one concept → one file": TELOS.md can be a thin index while +// each section's content lives in its own ALLCAPS sibling file (MISSION.md, +// GOALS.md, …). This map is the section-slug → split-filename source of truth, +// consumed by readTelosFreshness (below) and InterviewScan.extractSectionBody +// so both read split content when the TELOS.md H2 is absent. Irregular pairs +// (food→FOOD_PREFERENCES, learning_interests→LEARNING) are explicit; consistent +// with GenerateTelosSummary's LEGACY_FILE_TO_SECTION inverse. +export const SPLIT_FILE_BY_SLUG: Record = { + mission: "MISSION.md", goals: "GOALS.md", problems: "PROBLEMS.md", + strategies: "STRATEGIES.md", challenges: "CHALLENGES.md", narratives: "NARRATIVES.md", + beliefs: "BELIEFS.md", models: "MODELS.md", frames: "FRAMES.md", traumas: "TRAUMAS.md", + wrong: "WRONG.md", wisdom: "WISDOM.md", predictions: "PREDICTIONS.md", ideas: "IDEAS.md", + sparks: "SPARKS.md", books: "BOOKS.md", authors: "AUTHORS.md", bands: "BANDS.md", + movies: "MOVIES.md", restaurants: "RESTAURANTS.md", food: "FOOD_PREFERENCES.md", + meetups: "MEETUPS.md", civic: "CIVIC.md", learning_interests: "LEARNING.md", team: "TEAM.md", +}; + +/** Absolute path to a section's split file if it exists as a TELOS-dir sibling, else null. */ +export function splitFilePathForSlug(slug: string, telosPath: string = TELOS_PATH): string | null { + const filename = SPLIT_FILE_BY_SLUG[slug]; + if (!filename) return null; + const p = join(dirname(telosPath), filename); + return existsSync(p) ? p : null; +} + +function prettifySlug(slug: string): string { + return slug.split("_").map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w)).join(" "); +} + +// First substantive body line, trimmed of markup and capped, for a section preview. +// Shared by the H2-section scan and the split-file scan. skipHeadings drops '#' +// lines (a split file body can carry its own headings; an H2 section body cannot). +function extractPreview(lines: string[], skipHeadings = false): string { + for (const line of lines) { + const t = line.trim(); + if (!t || t.startsWith("