Node memory helper and repo feeds
Epic: Centralized project memory for runners, behind a per-task switch. This child is C6b. It covers:
- the node-side helper `tools/project_memory_node.py` (all modes): anchor verification, canon digest, generated code map, Stop delta, spool shipping, PR-citation scan;
- its MANIFEST entry;
- jaahd launch step 4 (the supervised helper) and orphan-spool shipping on slot release.
Design sources: the BA PRD (Task 5403, `prd`) and the settled architecture (Task 5404, `architecture`), via `task_read_artifact`. It depends on:
- C3: the lease `memory` key, the `<ref_key>.memory.json` bundle file, the three env exports, and the launch seam for step 4;
- C6a: the `memory.ingest` and `memory.report` routes/verbs, the claim protocol, and the verdict CAS.
C5 already wired the hook commands that call this script (fail-open), and owns the spool format and the hook settings files.
## Epic requirement (verbatim)
> Runners re-derive what other runners already learned: … Build a centralized project-memory layer per the attached architecture ("Jaah Project Memory") and its companion comparison, which lists seven amendments to fold in (…, generated repo wiki feed, …). The attachments are research input: the BA and Architect own the design and may depart from them with reasons.
> 2. Capture mostly without runner effort: … ship the in-session OMC memory delta at Stop …
> 4. Curation runs in the CP: … staleness verification against the integration branch …
## Settled requirements (verbatim)
- R-CAP-3 Stop delta: at session Stop of an ON task, the in-session OMC memory delta (OMC wiki pages / project-memory changed during the session) is shipped to the CP as candidates. No OMC state is moved to shared storage (N-1).
- R-CAP-6 Generated repo wiki feed (A3): on integration-branch merges of a project, the CP regenerates a per-project code map (module purposes, entry points, test map) and indexes it as `codemap` units anchored to the merge sha. It never lands in the repo (generated, not canon).
- R-CUR-4 Staleness: path-anchored units are verified at least daily against the integration-branch head; a unit whose path is gone or whose anchored content changed beyond threshold becomes `stale` …
- R-SC-4 No secret ever persisted — in … node cache, logs …
## Approach (architecture, binding)
- **Shape.** A stdlib-only script, added to the `tools/build_jaahd_bundle.py` MANIFEST. It reads the bundle file and the env, and talks to the CP only through `jaah-call` verbs (`memory.ingest`, `memory.report`).
- **Merge signal.** "On integration-branch merges" = the first ON launch after the integration head moved (the CP has no merge signal, AD-2).
- **Modes:**
- `--background`, started by jaahd at launch (step 4: a supervised child of the slot, killed when the slot is released, its claims released by expiry):
- `git fetch origin <integration_branch>` (the lease fetches only the task base).
- Read git objects ONLY by sha (`git ls-tree -r <sha>`, `git show <sha>:<path>`, `git diff --numstat <a> <b>`), never the working tree.
- (1) **verify**: claim (project, head, "verify"); the granted response carries the work-list. Report `stale` when a path is gone, or when `git diff --numstat <anchor_sha> <head> -- <path>` changed >40% of the anchored file's lines; else `fresh` (with blob shas when first anchoring) → `memory.report verdicts`.
- (2) **canon digest** when `canon_sha ≠ head`: every directive block in `.claude/context-routes.json` at head (`route_id`, doc path, triggers, text) → `memory.ingest canon_digest`.
- (3) **codemap**: claim (project, head, "codemap") → regenerate per touched directory, whole directory each time. Inputs: `tools/file_footprint.py` outline, the first docstring/comment line per file, entry points (`main`, route decorators, CLI `__main__`), and the test map by stem. One `codemap` unit per directory, split at the body cap → `memory.ingest codemap` with `anchor_sha` = head.
- Every submission carries `{sha, prev_sha, is_descendant}`, where `is_descendant` = `git merge-base --is-ancestor <prev_sha> <sha>`.
- `--session-start` (SessionStart hook): when `source == "startup"`, snapshot `.omc/` page hashes into the node cache. Prints nothing.
- `--stop` (Stop hook): ship the spool via `memory.report events`, and ship the `.omc/wiki/*.md` and `.omc/project-memory.json` delta vs the startup snapshot via `memory.ingest stop_delta`. ≤5 s. Prints nothing.
- `--settle` (PreToolUse `mcp__plugin_jaah_control__task_settle`): flush the spool. If `tool_input.pr_url` is set, run `jaah-forge pr view <url> --json`, find the `mem-N` tokens in the body, and send `memory.report events kind=cited channel=pr` (provisional until `merged_at`). ≤10 s. Prints nothing. Always exits 0.
- **Spool.** `<jaah cache>/project-memory/spool-<uid>-<session_id>.jsonl`, shipped by rename → send → delete. jaahd ships any orphaned spool through `memory.report` as that task on slot release.
- **Gating.** Every mode exits 0 silently when `JAAH_PROJECT_MEMORY` is unset.
- **Node cache.** The node cache stores hashes and ids only, never unit or candidate text beyond the in-flight send.
## Acceptance criteria (BA, verbatim — this child's legs)
- AC-11 Stop delta. An ON session that wrote an OMC wiki page ships it as a candidate at Stop; an OFF session ships nothing. *Verify:* hook test with a fixture `.omc/`. (The live transcript check is C9.)
- AC-13 Generated code map. A merge to a project's integration branch produces/refreshes `codemap` units anchored to that sha; no file appears in the repo. *Verify:* job test; `git status` clean in the repo.
- AC-17 (helper leg) A unit anchored to a path deleted on the integration branch gets a `stale` verdict from the helper. *Verify:* test against a fixture git repo.
- AC-15 (node-cache leg) A secret fixture in an `.omc/` page never appears in the node cache, spool or helper logs. *Verify:* test grepping those sinks.
- Quality bar: codemap bodies are useful orientation, naming each module's purpose, its entry points and its tests. A directory listing alone is not enough. Prove it on a fixture dir in the test.
Out of scope: Epic (verbatim): "moving OMC state to shared storage (ADR-0099); changing canon docs or the router's existing deterministic routes; org-wide (cross-project) memory beyond a scope field reserved for it; adopting an external memory SaaS." BA fences:
- N-1: `.omc/` is read-only input, never moved or shared.
- N-8: no unit text, candidate or bundle written into the repo or a tracked worktree file.
- N-10: never blocks or delays settle, dispatch or hooks.
Also binding:
- Do NOT edit hook settings files or `apps/jaahd/tests/golden/*` (C5 owns them).
- No CP routes (C6a).
- No router changes (C5).
- Never run `gh`; forge reads go via `jaah-forge` only.
- Never read the working tree for verification.
Done means:
- AC-11: a hook test with a fixture `.omc/` — ON ships one `stop_delta` candidate; with the env unset nothing is sent and stdout/stderr are empty.
- AC-13: a fixture-repo test — a head move ⇒ claim + codemap ingest anchored to the head sha, and `git status --porcelain` is empty afterwards.
- AC-17 helper leg: deleted-path ⇒ stale, >40% change ⇒ stale, unchanged ⇒ fresh, and `is_descendant` computed correctly.
- AC-15 node-cache grep is clean.
- `--settle` extracts `mem-N` from a mocked `jaah-forge` JSON and always exits 0 within budget.
- jaahd launch test: helper started only with `memory`, killed on release, orphan spool shipped.
- The MANIFEST includes the script.
- `./dc check full` green.
Required checks: gate
Outcome
Built tools/project_memory_node.py (verify, canon digest, code map, Stop delta, spool, PR citations) + MANIFEST, jaahd launch step 4 + orphan spool, jaah-forge pr view --body. PR #4757 merged; CI green; Jaah Deploy (dev) succeeded. Local full gate: one pre-existing host atime flake. CP follow-ups named.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)