Context
Grace surfaced the missing producer while reviewing the #14463 / #13751 hook-read consumer: lifecycle-state.json now has multiple readers or near-term consumers, but no writer exists. The current #13623 parent remains not-code-ready for AC4 ratio/re-tread/operator-scope observability because those fields still lack source/window semantics. Phase 1 does not need to wait for that: the sources for {openPRs, unreadCount, goldenPathDirection} are already live and can unblock the existing hook board consumers.
Live latest-open sweep: checked the latest 20 open issues on 2026-07-02T10:39Z; no equivalent standalone shared-writer ticket found. A2A in-flight sweep: checked latest 30 all-status messages on 2026-07-02T10:39Z; Grace's #13623 co-scope ask exists, but no competing ticket-creation claim exists.
The Problem
.claude/hooks/laneStateStopHook.mjs already reads .claude/logs/lifecycle-state.json as a fail-open enrichment for the no-hold block directive. The comment says the orchestrator/wake daemon writes the file, but the repository currently only has the read side. That leaves three surfaces dormant or partially dormant:
- #13678's live-board fields:
openPRs and unreadCount.
- #14463 / #13751's hook-read consumer:
goldenPathDirection.
- #13623 AC4's future observability fields, which should be additive later but are not source-ready today.
A single shared writer prevents multiple hook-adjacent lanes from inventing separate files, races, or duplicated GitHub/mailbox reads.
The Architectural Reality
Source anchors checked before filing:
.claude/hooks/laneStateStopHook.mjs:154 names the daemon-write side and .claude/logs/lifecycle-state.json; readLifecycleState() is fail-open and formatLifecycleBoard() currently renders only openPRs and unreadCount.
ai/services/graph/GoldenPathSynthesizer.mjs:501 already fetches live open PR data for the Active PR Cycle State section.
ai/services/graph/GoldenPathSynthesizer.mjs:1021 renders Active PR Cycle State during the same synthesis pass that writes sandman_handoff.md at ai/services/graph/GoldenPathSynthesizer.mjs:1085.
ai/daemons/orchestrator/scheduling/pipeline.mjs:557 runs the Golden Path task via services.goldenPathSynthesizer.synthesizeGoldenPath(); the orchestrator already treats successful handoff writing as a task outcome.
ai/services/memory-core/MailboxService.mjs:2095 has a direct countMessages({box:'inbox', status:'unread'}) path for unreadCount.
The home call: do not put this in ai/scripts/runners/runSandman.mjs; that file is a thin CLI wrapper over DreamService.executeRemCycle(). The writer should live with the synthesized state owner, preferably as a small helper/service invoked from the Golden Path synthesis/orchestrator golden-path task after the data is computed. The hook remains a fail-open reader only.
The Fix
Create one lifecycle-state writer for Phase 1 fields:
generatedAt: ISO timestamp for freshness display.
openPRs: bounded array of the current agent's open PR board entries suitable for the hook formatter.
unreadCount: current bound-agent unread A2A count.
goldenPathDirection: compact top-route / degraded-route directive for #14463's consumer, sourced from the same Golden Path pass that writes sandman_handoff.md.
The writer should write exactly the file the hook reads: .claude/logs/lifecycle-state.json. The write must be atomic enough for hook-read safety: write temp + rename or equivalent. Schema/read failures stay fail-open in the hook.
Recommended substrate: a dedicated lifecycle-state helper under the graph/orchestrator service boundary, called from the Golden Path synthesis/orchestrator golden-path task. Keep the CLI runner passive.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
.claude/logs/lifecycle-state.json |
.claude/hooks/laneStateStopHook.mjs read side + #13623 co-scope comment IC-4864771245 |
Single writer emits {generatedAt, openPRs, unreadCount, goldenPathDirection}. |
Missing/malformed file remains fail-open; hook injects bare reminder. |
JSDoc on writer helper and hook schema comment. |
Unit test for valid write shape and malformed/fail-open read behavior. |
openPRs |
GoldenPathSynthesizer live gh pr list enrichment |
Reuse/derive from the existing open PR fetch path; bound to the active agent where needed by the hook board. |
If GitHub fetch fails, omit openPRs and preserve other fields. |
JSDoc field contract. |
Unit fixture for populated, empty, and degraded PR fetch. |
unreadCount |
MailboxService countMessages unread path |
Emit integer unread count for the current bound agent identity. |
If identity/mailbox unavailable, omit or set 0 only when that state is verified; do not fabricate from errors. |
JSDoc field contract. |
Unit fixture for count success and mailbox failure. |
goldenPathDirection |
Golden Path synthesis result / #13751 consumer contract |
Emit compact directional data from the routed top node or degraded-route state. |
If Golden Path route is degraded, emit an explicit degraded/null state rather than a fake recommendation. |
JSDoc field contract; no new ADR unless direction semantics change. |
Unit fixture for routed, empty, contradiction, and degraded route states. |
Decision Record impact
Aligned with the existing Golden Path / Dream Pipeline architecture and ADR-0019-style SSOT discipline: one writer, one file, fail-open reader. No ADR required if this stays a hook-board enrichment and does not change Stop-hook admission semantics.
Acceptance Criteria
Out of Scope
- #13623 AC4 ratio-over-window, re-tread, or
forcedContinuationNoCompliantLane observability fields.
- COP rendering under #13444.
- Stop-hook admission, lane-state validator semantics, or any new permission-to-stop path.
- Multiple writers or per-harness divergent files.
- Changing health/check economics or Golden Path ranking semantics.
Avoided Traps
- Do not bury this in
runSandman.mjs. That runner is intentionally a thin CLI wrapper; the writer belongs with the synthesized state owner.
- Do not keep this inside #13623 AC4. Phase 1 has resolved sources and unblocks existing consumers; AC4 Phase 2 remains source-unresolved and should not hold the writer hostage.
- Do not create a second hook-state file for
goldenPathDirection. The hook needs one fail-open board file, not competing substrates.
Related
Related: #13623
Related: #13678
Related: #13751
Related: #14463
Related: #13652
Origin Session ID: 8facbc96-c346-4633-9141-79a968ca1c5d
Retrieval Hint: lifecycle-state.json writer GoldenPathSynthesizer openPRs unreadCount goldenPathDirection #13623 IC-4864771245
Context
Grace surfaced the missing producer while reviewing the #14463 / #13751 hook-read consumer:
lifecycle-state.jsonnow has multiple readers or near-term consumers, but no writer exists. The current #13623 parent remainsnot-code-readyfor AC4 ratio/re-tread/operator-scope observability because those fields still lack source/window semantics. Phase 1 does not need to wait for that: the sources for{openPRs, unreadCount, goldenPathDirection}are already live and can unblock the existing hook board consumers.Live latest-open sweep: checked the latest 20 open issues on 2026-07-02T10:39Z; no equivalent standalone shared-writer ticket found. A2A in-flight sweep: checked latest 30 all-status messages on 2026-07-02T10:39Z; Grace's #13623 co-scope ask exists, but no competing ticket-creation claim exists.
The Problem
.claude/hooks/laneStateStopHook.mjsalready reads.claude/logs/lifecycle-state.jsonas a fail-open enrichment for the no-hold block directive. The comment says the orchestrator/wake daemon writes the file, but the repository currently only has the read side. That leaves three surfaces dormant or partially dormant:openPRsandunreadCount.goldenPathDirection.A single shared writer prevents multiple hook-adjacent lanes from inventing separate files, races, or duplicated GitHub/mailbox reads.
The Architectural Reality
Source anchors checked before filing:
.claude/hooks/laneStateStopHook.mjs:154names the daemon-write side and.claude/logs/lifecycle-state.json;readLifecycleState()is fail-open andformatLifecycleBoard()currently renders onlyopenPRsandunreadCount.ai/services/graph/GoldenPathSynthesizer.mjs:501already fetches live open PR data for the Active PR Cycle State section.ai/services/graph/GoldenPathSynthesizer.mjs:1021renders Active PR Cycle State during the same synthesis pass that writessandman_handoff.mdatai/services/graph/GoldenPathSynthesizer.mjs:1085.ai/daemons/orchestrator/scheduling/pipeline.mjs:557runs the Golden Path task viaservices.goldenPathSynthesizer.synthesizeGoldenPath(); the orchestrator already treats successful handoff writing as a task outcome.ai/services/memory-core/MailboxService.mjs:2095has a directcountMessages({box:'inbox', status:'unread'})path forunreadCount.The home call: do not put this in
ai/scripts/runners/runSandman.mjs; that file is a thin CLI wrapper overDreamService.executeRemCycle(). The writer should live with the synthesized state owner, preferably as a small helper/service invoked from the Golden Path synthesis/orchestrator golden-path task after the data is computed. The hook remains a fail-open reader only.The Fix
Create one lifecycle-state writer for Phase 1 fields:
generatedAt: ISO timestamp for freshness display.openPRs: bounded array of the current agent's open PR board entries suitable for the hook formatter.unreadCount: current bound-agent unread A2A count.goldenPathDirection: compact top-route / degraded-route directive for #14463's consumer, sourced from the same Golden Path pass that writessandman_handoff.md.The writer should write exactly the file the hook reads:
.claude/logs/lifecycle-state.json. The write must be atomic enough for hook-read safety: write temp + rename or equivalent. Schema/read failures stay fail-open in the hook.Recommended substrate: a dedicated lifecycle-state helper under the graph/orchestrator service boundary, called from the Golden Path synthesis/orchestrator golden-path task. Keep the CLI runner passive.
Contract Ledger Matrix
.claude/logs/lifecycle-state.json.claude/hooks/laneStateStopHook.mjsread side + #13623 co-scope comment IC-4864771245{generatedAt, openPRs, unreadCount, goldenPathDirection}.openPRsgh pr listenrichmentopenPRsand preserve other fields.unreadCountcountMessagesunread path0only when that state is verified; do not fabricate from errors.goldenPathDirectionDecision Record impact
Aligned with the existing Golden Path / Dream Pipeline architecture and ADR-0019-style SSOT discipline: one writer, one file, fail-open reader. No ADR required if this stays a hook-board enrichment and does not change Stop-hook admission semantics.
Acceptance Criteria
.claude/logs/lifecycle-state.jsonwithgeneratedAt,openPRs,unreadCount, andgoldenPathDirection.Out of Scope
forcedContinuationNoCompliantLaneobservability fields.Avoided Traps
runSandman.mjs. That runner is intentionally a thin CLI wrapper; the writer belongs with the synthesized state owner.goldenPathDirection. The hook needs one fail-open board file, not competing substrates.Related
Related: #13623 Related: #13678 Related: #13751 Related: #14463 Related: #13652
Origin Session ID: 8facbc96-c346-4633-9141-79a968ca1c5d
Retrieval Hint:
lifecycle-state.json writer GoldenPathSynthesizer openPRs unreadCount goldenPathDirection #13623 IC-4864771245