Skip to content

fix(pulse): /api/life/home reads per-topic TELOS files with no fallback — Life Home renders empty on a stock install #1609

Description

@xmasyx

Summary

GET /api/life/home (Pulse Observability) reads four per-topic TELOS files directly, with no fallback to the unified TELOS.md. Three of those four files are not shipped by the installer, and the fourth (GOALS.md) is the legacy form that TELOS.md superseded on 2026-05-01. The result is a Life Home that renders entirely empty on a stock install, with no error anywhere.

Two sibling call sites in the same repo already implement the fallback this endpoint is missing, so this is an inconsistency inside the codebase rather than a missing feature.

Reproduction

On an install where TELOS lives in the unified USER/TELOS/TELOS.md (the documented single source of truth):

curl -s http://localhost:31337/api/life/home

Observed:

{"oneSentence":"Unknown, Unknown energy. Focused on: Unknown.",
 "current":{},"topGoals":[],"nextActions":[],
 "spark":null,"sparkCount":0,"timelineBlockCount":0,"topIntent":null}

Root cause

LIFEOS/PULSE/Observability/observability.ts, handleLifeHome() (around line 1666):

const current      = readMd(join(TELOS_DIR, "CURRENT.md"))
const goalsRaw     = readMd(join(TELOS_DIR, "GOALS.md"))
const sparksRaw    = readMd(join(TELOS_DIR, "SPARKS.md"))
const timelineRaw  = readMd(join(TELOS_DIR, "2036.md"))

Checked against the files the installer actually creates in LifeOS/install/USER/TELOS/:

File read Shipped by the installer? Notes
CURRENT.md no no template ships it
GOALS.md yes legacy per-topic file, superseded by TELOS.md H2 sections
SPARKS.md no no template ships it
2036.md no no template ships it

So on a fresh install three of the four reads are guaranteed empty, and the fourth goes empty as soon as the user follows the documented unified-TELOS.md layout. readMd returns empty rather than throwing, so every downstream parse yields []/Unknown and the endpoint reports success.

The same pattern appears again at handleLifeDashboard() (around lines 2188 and 2242), which reads CURRENT.md and 2036.md the same way.

Why this is inconsistent with the rest of the repo

Two other call sites already treat the per-topic files as legacy and fall back to the unified document:

  • skills/Daemon/Tools/DaemonAggregator.ts:100-132readTelosSection(), with the comment: "TELOS.md is the single source of truth; the legacy per-topic files (MISSION.md, GOALS.md, WISDOM.md) no longer exist on TELOS.md-only setups, so the daemon read them as empty."
  • LIFEOS/PULSE/checks/life-morning-brief.ts:24-31 — legacy GOALS.md first, then TELOS.md's ## GOALS section.

handleLifeHome() never received the same treatment.

Proposed fix

Reuse the established pattern: try the legacy per-topic file, then fall back to the matching H2 section of TELOS.md.

// Legacy per-topic files were superseded by the unified TELOS.md (H2 sections) on
// 2026-05-01, and CURRENT.md / SPARKS.md / 2036.md are not shipped by the installer
// at all. Falling back to the matching section keeps Life Home populated on a stock
// install instead of silently rendering "Unknown".
function readTelosSection(heading: string): string {
  const telos = readMd(join(TELOS_DIR, "TELOS.md"))
  if (!telos) return ""
  const re = new RegExp(`^##\\s+${heading}\\s*$([\\s\\S]*?)(?=^##\\s|\\Z)`, "im")
  return telos.match(re)?.[1]?.trim() ?? ""
}

const current     = readMd(join(TELOS_DIR, "CURRENT.md"))  || readTelosSection("Current State")
const goalsRaw    = readMd(join(TELOS_DIR, "GOALS.md"))    || readTelosSection("GOALS")
const sparksRaw   = readMd(join(TELOS_DIR, "SPARKS.md"))   || readTelosSection("Sparks")
const timelineRaw = readMd(join(TELOS_DIR, "2036.md"))     || readTelosSection("Timeline")

Applying the same fallback at handleLifeDashboard() (lines ~2188, ~2242) would close the second occurrence.

Two smaller points worth considering alongside it:

  1. Fail loud rather than silently empty. An endpoint that finds no TELOS source at all currently returns 200 with "Unknown". Surfacing a sources: [] / degraded flag in the payload would make the empty state visible to the dashboard rather than indistinguishable from a user who genuinely has no goals.
  2. Spark parsing expects ### headings. A SPARKS.md written with ## headings yields sparkCount: 0 even when the file exists, so a heading-level-tolerant parse would help here too.

Happy to open a PR with the fallback if the approach looks right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions