Context
PR #14469 implemented the writer requested by #14466, but team review found that the ticket described the wrong consumer path. The implementation is useful salvage, yet the default write target is dead-on-arrival against current hook readers: it writes the stale .claude/logs/lifecycle-state.json contract from #14466 while the live readers resolve state under .neo-ai-data.
This ticket supersedes #14466 instead of trying to patch PR #14469 in place. Tobias challenged the PR to drop/supersede, @neo-gpt accepted the author-side disposition, @neo-opus-vega and @neo-fable-clio posted peer-role concurrence with falsifying evidence, and @neo-opus-grace released her redundant peer-role claim after Vega resolved the constant evidence.
Live latest-open sweep: checked latest 20 open issues at 2026-07-02T14:24:34Z; #14466 is the stale superseded source, and no separate replacement-path contract ticket was present. A2A in-flight sweep: checked latest 30 all-status messages at 2026-07-02T14:24:34Z; saw Vega and Clio peer-role posts, Grace claim release, and Vega topology correction, but no competing ticket-creation claim. KB ticket sweep for lifecycle-state hook live board .neo-ai-data harness-state duplicate returned unrelated historical board material and no equivalent ticket.
The Problem
#14466 transcribed the hook consumer as .claude/logs/lifecycle-state.json. That was stale against the current hook source. Implementing it faithfully creates a writer and consumer that do not meet:
- .claude/hooks/laneStateStopHook.mjs:64 sets LOG_DIR to process.env.NEO_AI_DAEMON_DIR || path.join(os.homedir(), .neo-ai-data, lane-state-hook); line 158 reads lifecycle-state.json under that directory.
- .codex/hooks/codex-lane-state-stop.mjs:30 mirrors the neutral .neo-ai-data daemon-dir pattern.
- #14466 still says the writer should produce .claude/logs/lifecycle-state.json.
The old PR body, tests, ticket close target, and default constant all encode the stale path. A rename patch would still leave the larger contract implicit: which .neo-ai-data home, whether the file is per-agent or shared, how hooks discover it without network calls, and which module owns the path resolver.
The Architectural Reality
Source anchors checked before filing:
- .claude/hooks/laneStateStopHook.mjs:154-158 says the hook reads daemon-written current-agent lane state, cheaply and fail-open, from path.join(LOG_DIR, lifecycle-state.json).
- .codex/hooks/codex-lane-state-stop.mjs:27-31 establishes the Codex family adapter uses the same neutral daemon-dir family, not .claude.
- .gitignore:100-103 makes .neo-ai-data the ignored Agent OS local data home while preserving the tracked .neo-ai-data/concepts/ exception.
- package.json:72 uses ./.neo-ai-data/chroma/unified for the local Chroma server.
- ai/scripts/migrations/bootstrapWorktree.mjs:48-67 documents why the .neo-ai-data boundary is load-bearing: the parent directory must not be symlinked wholesale, while gitignored child substrate dirs can be linked to unify Memory Core, A2A, wake, and logs without clobbering tracked concepts.
- ai/services/graph/GoldenPathSynthesizer.mjs already owns Golden Path synthesis, Active PR Cycle sections, and handoff writing; ai/daemons/orchestrator/scheduling/pipeline.mjs:557-561 invokes it from the golden-path cadence.
Pre-Flight (structural fast-path): a shared lifecycle-state path resolver under ai/scripts/lifecycle/ matches the sibling pattern of ai/scripts/lifecycle/stopHookDecision.mjs and ai/scripts/lifecycle/validateLaneStateTerminal.mjs; family hook files remain thin adapters, writer code imports the same resolver, and no novel directory choice is needed. Map-maintenance: not needed because this is another lifecycle helper under an existing directory role.
The Fix
Replace #14466 with a resolver-first implementation:
- Define one shared lifecycle-state path resolver used by every reader and writer. It must honor NEO_AI_DAEMON_DIR and default to a neutral .neo-ai-data hook-state home, never a family-local .claude path.
- Update the Claude and Codex hook readers to share the same resolver contract instead of rebuilding separate default paths.
- Port the useful PR #14469 writer pieces: payload builder, generated timestamp, own-PR board, unread-count honesty, Golden Path direction projection, same-directory temp + rename write, and degrade-by-omission behavior.
- Decide and implement the topology explicitly. Vega follow-up probe falsified the earlier unkeyed per-clone lean because harness-state is symlinked to canonical. The preferred shape is now a central/canonical home with per-agent-keyed files, for example lifecycle-state/.json, unless implementation V-B-A proves a better equivalent. A single unkeyed file is not acceptable for multi-peer operation.
- Keep hook relocation to ai/agent/hooks out of this ticket. The replacement may prepare shared logic under ai/scripts/lifecycle/, but the v13.2 hook-home move belongs to the broader harness/Agent OS architecture lane.
- Close PR #14469 unmerged after the replacement ticket exists; the code should be ported, not merged from that branch as-is.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
| Lifecycle-state path resolver |
laneStateStopHook.mjs:64/:158, codex-lane-state-stop.mjs:30, structural fast-path to ai/scripts/lifecycle/ |
One resolver returns the exact path for both readers and writers, honoring NEO_AI_DAEMON_DIR and a neutral .neo-ai-data default. |
Missing env var uses documented default; invalid identity/path inputs fail closed at write time and fail open at hook read time. |
JSDoc on resolver naming locality, identity keying, and env override semantics. |
Unit test proving Claude reader, Codex reader, and writer resolve the same path for default and env override cases. |
| Per-agent lifecycle-state payload |
Hook read-side docblock and peer-role topology correction on PR #14469 |
Store agent-specific state under a keyed path, not one clobber-prone shared JSON file. |
Missing keyed file returns null; hook injects bare reminder. |
Writer/helper JSDoc names the key source and scope. |
Unit test with two agent identities proving no overwrite or clobber. |
| Writer cadence |
GoldenPathSynthesizer plus orchestrator golden-path task |
Golden Path/orchestrator cadence writes the resolved file after source data is computed. |
Writer failure warns and does not break handoff synthesis. |
JSDoc on writer owner contract. |
Focused unit coverage for success, partial-source omission, and write failure isolation. |
| Hook readers |
Claude and Codex family hook adapters |
Hooks remain cheap fail-open readers; they do not fetch network data or make admission decisions from the live board. |
Missing/malformed/unreadable state yields no board enrichment. |
Existing hook schema comments updated to point at shared resolver. |
Existing hook tests plus new resolver-agreement regression. |
| Migration posture |
PR #14469 peer-role convergence |
.claude/logs/lifecycle-state.json is legacy at most; no canonical write targets it. |
Optional best-effort read of legacy file may be allowed only if it cannot mask the resolved canonical file. |
PR body must state migration behavior explicitly. |
Test or code comment proving canonical path wins. |
Decision Record impact
Aligned with ADR-0019-style SSOT discipline: path/default semantics should live in one resolver rather than being re-derived in each reader/writer. No new ADR is required if this stays a lifecycle hook enrichment and does not change Stop-hook admission semantics. If implementation discovers that cloud/local state-home discovery needs a broader config-plane precedent, route that delta to the existing Agent OS/config-plane discussion before expanding this ticket.
Acceptance Criteria
Out of Scope
- Moving family hook definitions into ai/agent/hooks.
- Changing Stop-hook admission, lane-state validation, or no-hold semantics.
- Adding #13623 AC4 ratio/re-tread/operator-scope fields.
- Broad cloud deployment state discovery beyond the minimum local/cloud-safe path contract needed here.
- Reusing .claude/logs as a canonical state home.
Avoided Traps
- Resolved constants beat ticket transcription. Future review must compare reader and writer resolved defaults, not only field shapes or issue prose.
- Do not merge a functionally dead writer. Green tests that inject paths do not prove production reader/writer rendezvous.
- Do not use one unkeyed file for many peers. The board is current-agent state; clobbering across agent identities violates the consumer contract.
- Do not symlink parent .neo-ai-data. Follow the child-link discipline from bootstrapWorktree.mjs so tracked concepts remains safe.
Related
Supersedes #14466
Related: #14469
Related: #13623
Related: #13678
Related: #13751
Related: #14463
Related: #13012
Origin Session ID: c5938a7c-42e6-4f94-ac19-1a874529dfb4
Retrieval Hint: PR 14469 lifecycle-state writer drop supersede .neo-ai-data resolver per-agent keyed live board
Context
PR #14469 implemented the writer requested by #14466, but team review found that the ticket described the wrong consumer path. The implementation is useful salvage, yet the default write target is dead-on-arrival against current hook readers: it writes the stale .claude/logs/lifecycle-state.json contract from #14466 while the live readers resolve state under .neo-ai-data.
This ticket supersedes #14466 instead of trying to patch PR #14469 in place. Tobias challenged the PR to drop/supersede, @neo-gpt accepted the author-side disposition, @neo-opus-vega and @neo-fable-clio posted peer-role concurrence with falsifying evidence, and @neo-opus-grace released her redundant peer-role claim after Vega resolved the constant evidence.
Live latest-open sweep: checked latest 20 open issues at 2026-07-02T14:24:34Z; #14466 is the stale superseded source, and no separate replacement-path contract ticket was present. A2A in-flight sweep: checked latest 30 all-status messages at 2026-07-02T14:24:34Z; saw Vega and Clio peer-role posts, Grace claim release, and Vega topology correction, but no competing ticket-creation claim. KB ticket sweep for lifecycle-state hook live board .neo-ai-data harness-state duplicate returned unrelated historical board material and no equivalent ticket.
The Problem
#14466 transcribed the hook consumer as .claude/logs/lifecycle-state.json. That was stale against the current hook source. Implementing it faithfully creates a writer and consumer that do not meet:
The old PR body, tests, ticket close target, and default constant all encode the stale path. A rename patch would still leave the larger contract implicit: which .neo-ai-data home, whether the file is per-agent or shared, how hooks discover it without network calls, and which module owns the path resolver.
The Architectural Reality
Source anchors checked before filing:
Pre-Flight (structural fast-path): a shared lifecycle-state path resolver under ai/scripts/lifecycle/ matches the sibling pattern of ai/scripts/lifecycle/stopHookDecision.mjs and ai/scripts/lifecycle/validateLaneStateTerminal.mjs; family hook files remain thin adapters, writer code imports the same resolver, and no novel directory choice is needed. Map-maintenance: not needed because this is another lifecycle helper under an existing directory role.
The Fix
Replace #14466 with a resolver-first implementation:
Contract Ledger Matrix
Decision Record impact
Aligned with ADR-0019-style SSOT discipline: path/default semantics should live in one resolver rather than being re-derived in each reader/writer. No new ADR is required if this stays a lifecycle hook enrichment and does not change Stop-hook admission semantics. If implementation discovers that cloud/local state-home discovery needs a broader config-plane precedent, route that delta to the existing Agent OS/config-plane discussion before expanding this ticket.
Acceptance Criteria
Out of Scope
Avoided Traps
Related
Supersedes #14466 Related: #14469 Related: #13623 Related: #13678 Related: #13751 Related: #14463 Related: #13012
Origin Session ID: c5938a7c-42e6-4f94-ac19-1a874529dfb4
Retrieval Hint: PR 14469 lifecycle-state writer drop supersede .neo-ai-data resolver per-agent keyed live board