Skip to content

<project>/ISA.md never reaches work.json: ISASync and CheckpointPerISC watch only MEMORY/WORK #1807

Description

@mhaisham

<project>/ISA.md never reaches work.json

Affected: v7.28.3 (verified against 36c6f01e9c2c515aaaf475b868aca7f3ffdf9c43)
Component: hooks/ISASync.hook.ts, hooks/CheckpointPerISC.hook.ts
Impact: Visibility only. No work is lost and no claim is affected, but on this install the run surfaces are blind to 21 of the 21 project ISAs, and have been for as long as project ISAs have existed.

Summary

The Algorithm sanctions two ISA homes (ALGORITHM/v8.17.3.md, "A run is complete when", point 2):

an ISA at the correct home (<project>/ISA.md for persistent things, MEMORY/WORK/{slug}/ISA.md for tasks)

ISASync.hook.ts:52 watches the second and exits on the first:

// Only trigger for ISA.md (or legacy PRD.md) files in MEMORY/WORK/.
if (!filePath.includes('MEMORY/WORK/')) return;

CheckpointPerISC.hook.ts:187 carries the same line. Since work.json is the registry every run surface reads, a project can be scaffolded, climbed for weeks and closed without ever appearing anywhere. The ISA file itself is fine, which is exactly why nobody notices: the state of record is current while every dashboard reports the project idle.

I would have filed this as "the run registry is task-scoped by design, and that is defensible" except for two things upstream already ships that say otherwise. Details below, and I would rather be corrected on that reading than have it go unsaid.

Reproduction (no install state touched)

mkdir -p /tmp/demo-project
cat > /tmp/demo-project/ISA.md <<'EOF'
---
task: "demo project"
slug: zz-demo-project
phase: build
progress: 2/7
---
# Demo
EOF

WJ=~/.claude/LIFEOS/MEMORY/STATE/work.json
sha256sum "$WJ"
echo '{"session_id":"repro","tool_name":"Write","tool_input":{"file_path":"/tmp/demo-project/ISA.md"}}' \
  | bun ~/.claude/hooks/ISASync.hook.ts
sha256sum "$WJ"
grep -c zz-demo-project "$WJ"

The hook returns {"continue":true}, both hashes match, the grep returns 0. Move the same file under MEMORY/WORK/<slug>/ and the row appears. So this is the path guard, not a frontmatter or parse problem.

What it costs

Surface Effect
MEMORY/STATE/work.json the project run is never a row
Session-start "ACTIVE WORK" list a project mid-climb reads as idle
Pulse work board, statusline, terminal tab deriveAscent() reads data that was never written
ISA HTML mirror never rendered for a project ISA

The silence is the expensive part. A project ISA at progress: 200/267 and one abandoned in March are indistinguishable from every board in the system, and the absence looks like an answer rather than a gap.

The docs say this shipped

ISAFormat.md disagrees with itself in one file:

  • :37: "v6.0.x mechanics for <project>/ISA.md parser support ... and project-ISA seeding migration are forthcoming patches."
  • :81: "v6.0.x mechanics (SHIPPED): ISASync.hook.ts, CheckpointPerISC.hook.ts, and hooks/lib/isa-utils.ts discover <project>/ISA.md alongside MEMORY/WORK/ paths; Pulse renders both homes"

The code matches :37. Line 81 names the two hooks in this issue by filename and describes behaviour neither has.

It also strands a feature that already merged

This is the part I would lead with if I could only keep one section.

syncToWorkJson stores the artifact path relative to the install (hooks/lib/isa-utils.ts:906, isa: relativeIsa), and relativeIsa is the ISA path with the ~/.claude/ prefix stripped (:698). A task ISA lives inside the install, so its stored path is always relative. Only an ISA outside the install can survive that strip as an absolute path. In other words, isa starting with / is a value that only a project ISA can ever produce.

Now the fix for #1498 (@simeonzickert, "ContextSearch never searches <project>/ISA.md"), which shipped and is running today at skills/ContextSearch/Tools/ContextSearch.ts:441:

if (typeof e?.isa === "string" && e.isa.startsWith("/") && existsSync(e.isa)) candidates.add(e.isa);

That guard selects project ISAs and nothing else. It was written expecting rows this hook never writes. On this install it matches 0 of 27 rows, and it cannot match anything on any install, because no writer produces an absolute isa value. ContextSearch still finds project ISAs through its other source (cwds recovered from transcripts), so the feature works, but half of it is unreachable.

So the intent seems settled: both homes were meant to register, the sync half never landed, and something downstream was built on the assumption that it had. If that reading is wrong and the registry really is task-only by design, then the doc line and this guard are the defects instead, and I would still want to know which.

Why relaxing the guard is not enough

I tried to write this as a one-line fix and it does not survive contact. Four things break, measured on a real install with 21 project ISAs:

1. Most project ISAs would still be skipped. syncToWorkJson returns immediately without fm.slug (isa-utils.ts:696), and the only slug fallback derives from a MEMORY/WORK/ path (ISASync.hook.ts:81). 8 of 21 here carry no slug:, which the format sanctions ("slug derives from the directory"). Without a project-directory fallback they stay invisible after the guard is opened, which is the worst outcome: fixed on paper, still silent.

2. Slug lookups resolve to the wrong file. findArtifactPath(slug) rebuilds WORK_DIR/<slug>/ISA.md (isa-utils.ts:82) and Pulse repeats that join independently (LIFEOS/PULSE/modules/work.ts:163). A registered project row would render and never resolve back to its file. The registry already stores the path; both sites should read it rather than reconstruct one.

3. The slug namespace is already shared. Delegate progress tools write event trails to MEMORY/WORK/<slug>/ keyed by the project's slug, so two project slugs here already name existing MEMORY/WORK/ directories. Neither holds an ISA.md, so nothing is corrupted today, but registering by bare slug puts two different kinds of artifact in one keyspace, and a future ISA.md in one of those directories would silently contend for the row.

4. Registration would start mutating long-lived state of record. 11 of the 21 sit at phase: complete, which is the normal resting state for a project ISA between pushes. Resume-After-Complete then rewrites frontmatter to learn, increments iteration and appends a Decisions row on the next edit (isa-utils.ts:738-756), and ISARender writes an ISA.html beside the source (LIFEOS/TOOLS/ISARender.ts:614), inside what is usually a git repo. That behaviour is correct for a task ISA and wrong for a project one, which ISASystem.md:232 describes as never completed, only extended. Whatever else changes, a project ISA should register without being pulled into the task lifecycle.

Patch

The first two are small, and I am happy to open a PR for them plus the doc line if that is useful. Items 3 and 4 are design calls that belong to you, not to me.

// ISASync.hook.ts / CheckpointPerISC.hook.ts: accept both sanctioned homes.
const inWorkDir = filePath.includes('MEMORY/WORK/');
const isProjectIsa = !inWorkDir && filePath.endsWith('/ISA.md');
if (!inWorkDir && !isProjectIsa) return;
// ISASync.hook.ts: the slug already derives from the directory, so let it do so in both homes.
if (!fm.slug) {
  fm.slug = filePath.match(/MEMORY\/WORK\/([^/]+)\//)?.[1] ?? basename(dirname(filePath));
}

Then: prefer the stored isa path over rebuilding it in findArtifactPath and in Pulse's work module, decide how a persistent ISA opts out of rewind and of the HTML mirror (a home check, or a frontmatter flag, frozen: true already exists and may be enough), and reconcile ISAFormat.md:81 with :37.

Environment

  • LifeOS v7.28.3, Algorithm v8.17.3, Linux, bun, hooks registered and firing.
  • Every file named above is byte-identical to the release tag on this install, so none of this is local drift.

Investigated and drafted by my LifeOS DA on my install; I reviewed it before filing and the claims are mine.

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