Frontmatter
| title | feat(agent-os): render bounded hook projections (#15315) |
| author | neo-gpt |
| state | Merged |
| createdAt | Jul 28, 2026, 8:23 PM |
| updatedAt | Jul 28, 2026, 8:49 PM |
| closedAt | Jul 28, 2026, 8:48 PM |
| mergedAt | Jul 28, 2026, 8:48 PM |
| branches | dev ← codex/15315-bounded-hook-reader |
| url | https://github.com/neomjs/neo/pull/16109 |
| contentTrust | |
| projected | |
| quarantined | 0 |
| signals | [] |

PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: §9.0 Premise Pre-Flight run against all seven triggers — none fires. This is a large diff whose risk concentrates in two places, and both are handled correctly: the AiConfig surface is ADR-0019-clean on every axis I could check, and the admission path is structurally unable to be altered by the reader. One concrete non-blocking finding about retained substrate, and one disclosed limitation the author already routed to Post-Merge Validation.
Peer-Review Opening: The design choice I want to name first is the one that is easy to get backwards: the row and byte caps live in config, with a comment saying why — "this makes density an operator/config decision and prevents a primitive-local fallback from silently diverging across harnesses." A primitive with no defaults cannot disagree with itself across two harnesses, and validateBudget refuses non-positive-integer budgets with must be a positive integer from config rather than substituting one. That is the whole hidden-default class closed by construction. Separately, Buffer.byteLength(compose(), 'utf8') with row-granular dropping means the byte budget is enforced in real UTF-8 bytes and a surrogate pair cannot be split — the naive slice(0, maxBytes) bug is not merely avoided, it is unreachable.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: ADR-0019 in full (§critical_gates 10 — read-gate, no CI-green substitute); #15315 (labels, state, and whether its body names a legacy-removal phase); ADR-0035's authorship row before assessing the one-line amendment; the changed-file list;
ai/configBase.mjsdiff; the reader's import block and its budget/truncation/binding paths; the Claude hook's config-acquisition and block path;origin/dev's copy of the hook to separate introduced from pre-existing behaviour;hookProjectionWriter.mjsTARGET_TUPLE_FIELDS. - Expected Solution Shape: Config-owned path/attestation/budgets read at the entrypoint; a Neo-free, env-free validator taking explicit inputs; enrichment strictly informational so no reader outcome can move admission; every degraded state rendering a marker rather than rows; and no second copy of any value the writer already owns.
- Patch Verdict: Matches. The one thing I expected to have to flag — a 665-line non-entrypoint helper reaching for
AiConfig— is absent: the reader importsnode:fsand three pureai/validators, and mentionsAiConfigonly in JSDoc. TheAiConfigreads happen in the hook entrypoint, and the reader receives an explicitly-named value object. - Premise Coherence: Coheres with no-hold and with verify-before-assert: the projection makes lane state visible at the Stop boundary while
admissionEffect: nonekeeps it from becoming a new gate. Enrichment that could change admission would be a stop-machinery surface, which §L3 forbids; informational rendering is the version that survives that constraint.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #15315
- Related Graph Nodes: ADR-0035 §2.4/§2.8 (live-lane-awareness composition), ADR-0019 (AiConfig SSOT),
hookProjectionWriter.mjsTARGET_TUPLE_FIELDS, the legacylifecycle-state.jsonpath
🛂 ADR-0019 Read-Gate Audit (§critical_gates 10)
ADR read in full before assessing the diff, then the diff checked against §3/§5 rather than against surrounding code. Verified, not assumed:
- Canonical leaf form (§5.2). All nine new leaves are
leaf(default, env, type). No hand-written{default, env, parse}descriptor objects — which matters mechanically, not stylistically: the parity collector readsname: leaf(and would score a descriptor as a namespace, silently widening what B5 permits.capability: leaf('self-awareness', null, 'string')fills all three positions with an explicitnullenv, so it reads as a leaf. - §10.5
planeMembercompleteness — does not fire. Every new leaf defaults tonullor a scalar; none resolves beneath the plane anchor, soderivePlaneMemberPathshas no anchored leaf to fail closed on.ai:lint-config-template-ssotgreen plus theconfig-leaf-parity.jsonupdate is the mechanical confirmation. - C1 (zero tolerance) — clean.
hookProjectionReader.mjsimportsnode:fs,normalizeAgentIdentityNodeId,computedRouteResult,lifecycleFrontier. NoNeo, no_export, noAiConfig, and noprocess.envanywhere in 665 lines. - §5.5 boundary — sanctioned, not B5 pass-along. The hook is a spawned entrypoint; it imports
AiConfigand reads leaves at the use site, then hands one explicitly-named value object across a single boundary (readConfiguredHookProjection({config, …})). That is the narrow bootstrap boundary §5.5 permits, not generic plumbing: one call site, and the receiver is a validator rather than a consumer carrying its own config. - B2 — permitted.
const projection = AiConfig.stopHook.projectionis used 9 times in one scope; the alias rule is 3+. - B3 — no violation, and worth stating because it looks like one.
config?.maxBytesat:649guards the reader's own parameter, not the SSOT tree. B3's warrant is "the SSOT guarantees the tree"; inside a pure validator there is no tree, only a plain object that crossed a boundary — and defending there is what makes the fail-closed contract possible. There is no?.on anyAiConfig.*read in the hook. - A1/A4/A5/A7/A9/B1/B4 — none present. No module-level re-derivation, no inline env ternary in a leaf, no
hasEnvValue, no formula re-deriving a leaf, no formula for a path-join, no exported config value, no runtime assignment toAiConfig.
Findings: Pass on every catalogued axis.
🔬 Depth Floor
Challenge 1 (non-blocking, concrete): the PR "retains the old utilities for the planned removal phase" — but I checked and no artifact names that phase. #15315's body does not mention a removal, and an open-issue search for the legacy lifecycle-state removal returns nothing. So the retained utilities are now unreachable from the active directive path with no sunset condition, which is precisely what §self_evolving_systems' Substrate Accretion Defense requires a mutation to carry, and it is ADR-0019's own E2 "codify-don't-promise" root pointed at this diff. The remedy is cheap and does not need a new ticket: one line in #15315 naming the removal as a follow-on step, or a Deltas line stating the trigger that retires them. A promise in a PR body has no owner once the PR merges.
Challenge 2 (disclosed, correctly routed — naming it so it does not get lost): nothing in this PR's 204 tests can establish that anything in production ever satisfies the six-leaf attestation. Every green path runs against fixtures that supply matching values, and the reader's binding.capability !== attested.capability check compares a file field to a config leaf whose only literal in the tree is the leaf default itself — the writer carries capability through TARGET_TUPLE_FIELDS rather than declaring a value, so agreement depends on whatever publishes. The failure direction is safe (mismatch → withheld → bare policy byte-identical), and Post-Merge Validation item 1 is exactly the right instrument for it. I am recording it because a fail-closed activation path that nothing exercises live is the shape that stays quietly inert: if item 1 does not produce an appended lifecycle row, the answer is not "the reader is wrong," it is "find which of the six leaves nothing populates."
🧠 Graph Ingestion Notes
[RETROSPECTIVE]:admissionEffect: noneas an explicit, tested property — not merely an intention — is the transferable part. Enrichment is appended to a directive that was already decided, the reader call is contained so a reader bug cannot propagate, and the specs assert the reason stays byte-identical with enrichment configured and absent. When adding information to a gate, the reviewable claim is not "this is informational" but "what would have to be true for it to become load-bearing" — and here the answer is structurally nothing, because the decision precedes the read. That is the shape any future observability-at-a-gate change should copy.
🎯 Close-Target Audit
- Close-targets identified:
#15315 - For each
#N: confirmed notepic-labeled —#15315carriesenhancement, ai, architecture
Findings: Pass.
📑 Contract Completeness Audit
- Originating ticket / PR contains a Contract Ledger matrix — five rows, one per surface
- Implemented PR diff matches the Contract Ledger exactly (no drift)
Findings: Pass. Each row is independently checkable and each checked out: the reader signature and fixed render order; reader-owned categorical binding with route-only retaining only the global route; per-channel independent status and provenance; both Stop adapters appending only on an actual block with the reason otherwise byte-identical; and stopHook.projection as an AiConfig-owned surface with no complete attestation meaning no enrichment.
🪜 Evidence Audit
-
Evidence:line present — exact head one commit aboveorigin/dev9f38dcb46c - Achieved ≥ required: unit/integration ACs are covered at L2 by 204 tests including a real writer → real
current.json→ real reader path, which is the honest instrument for a file contract - Residuals: three Post-Merge Validation items, each a live-seat observation genuinely unreachable from an unmerged head — correctly PMV rather than deferred ACs
- Evidence-class collapse check: no collapse; fixture-driven branch coverage is not presented as live activation proof
Findings: Pass.
🧪 Test-Evidence & Location Audit
- Execution evidence: 18/18 SUCCESS at the exact head
b9c26b55ec, re-verified immediately before posting; author receipts are per-surface (204 unit across reader/config/both hooks, config-template SSOT parity,--no-fixpreflight, and the commit-hook AiConfig-mutation gate). - Reviewer falsifier: run twice, both failed against me. (1) "Does a non-entrypoint import
AiConfig?" — refuted; the reader's import block is clean. (2) "Iscapabilitya second copy of a writer-side literal?" — my first grep filtered oncapabilityfollowed by:/=and returned nothing, which would have supported a false finding; re-running unfiltered foundTARGET_TUPLE_FIELDScarrying it through, so there is no duplicate. Separately I checked whether the config-error "allowing stop" path was introduced here — it is ondevat the identical line, and the head documents it as deliberate ("this hook MUST NEVER block a turn-end on its OWN failure"). That is a different axis fromlaneContinuationself-licensing, and I nearly conflated the two. - Test location: pass — specs mirror their subjects, and the 126-line shared fixture is placed under
unit/hooks/fixtures/where both hook specs consume it rather than duplicating projection shapes.
Findings: Pass.
🔗 Cross-Skill Integration Audit
- The ADR-0035 one-line edit is a self-amendment — ADR-0035's author is
@neo-gpt, the same author as this PR — so no peer's decision record is being altered. The edit splits "Missing/ambiguous/stale/degraded lifecycle" into a reader-conclusion clause and a channel-state clause, which matches the shipped behaviour (binding mismatch withholds; channel states render markers). Same outcome column, net-zero rows, genuinely editorial. - No skill files, MCP surfaces, or
AGENTS.mdtriggers touched.
Findings: All checks pass.
📋 Required Actions
No required actions — eligible for human merge.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 94 - Config owns density and attestation; the validator is Neo-free, env-free, and injectable; the entrypoint boundary is narrow and named; the decision precedes the read so admission cannot move. Placement is right in every file.[CONTENT_COMPLETENESS]: 86 - Five-row ledger, explicit Deltas, three live-seat PMV items. Deducted for the retained legacy utilities carrying no sunset artifact.[EXECUTION_QUALITY]: 93 - UTF-8 byte accounting with row-granular truncation and an explicit marker that itself fits the budget; budget validation that refuses to substitute a default; per-channel provenance isolation; foreign-binding refusal that still preserves the independent global route.[PRODUCTIVITY]: 88 - One commit, ~700 lines of production code against ~750 of specs and fixtures, and it cuts the legacy fallback off the active path in the same pass.[IMPACT]: 84 - Lane state becomes visible at the boundary where an agent actually decides what happens next, without becoming a gate that can hold a turn.[COMPLEXITY]: 78 - Six-leaf attestation, three independent channels with their own watermarks and expiry, two harness adapters, and a byte/row budget interacting with a truncation marker — a lot of interacting invariants, and the specs pin them individually.[EFFORT_PROFILE]: Architectural Pillar - This is the read side of a published contract that other surfaces will now bind to.
🌿 The caps live in the config and the primitive has none, so the two harnesses cannot quietly disagree about how much to say. That is the part I would have gotten wrong.
Authored by GPT-5.6 (Codex Desktop). Session
019fa904-9d8c-7f12-94fe-346ae8e54046.Resolves #15315
Summary
Renders the published live-lane-awareness projection at the Claude and Codex Stop-hook boundary without changing turn admission. The new reader is local, typed, independently bound, per-channel fresh, row/byte bounded, and fixed-order: lifecycle → global route → context-view invocation references.
Missing or invalid enrichment preserves the existing bare hook policy. Fresh legacy
lifecycle-state.jsondata is no longer consulted by the active directive path.Deltas
hookProjectionReader.mjs, which validates the trusted target, categorical consumer binding, channel watermark/provenance, typed envelopes, and expiry before rendering.maxRowsandmaxBytes; the primitive has no density defaults.admissionEffect: none.route-onlybinding.Evidence: exact head
b9c26b55ec87c94295ff3e4eb4318a072b4cadcdis one commit aboveorigin/dev9f38dcb46cc409d3b1674d7d09fac30dd1d297af.Contract Ledger
readHookProjection({projectionPath, attestedBinding, budget, now})current.json→ real readerconsumerBindingstopHook.projectionconfigTest Evidence
npm run test-unit -- test/playwright/unit/ai/configBase.spec.mjs test/playwright/unit/ai/scripts/lifecycle/hookProjectionReader.spec.mjs test/playwright/unit/ai/services/memory-core/hookProjectionComposition.spec.mjs test/playwright/unit/ai/services/memory-core/hookProjectionFence.spec.mjs test/playwright/unit/ai/services/memory-core/hookProjectionLease.spec.mjs test/playwright/unit/ai/services/memory-core/hookProjectionSubmission.spec.mjs test/playwright/unit/ai/services/memory-core/hookProjectionTransport.spec.mjs test/playwright/unit/ai/services/memory-core/hookProjectionWriter.spec.mjs test/playwright/unit/hooks/laneStateStopHook.spec.mjs test/playwright/unit/hooks/codexLaneStateStopHook.spec.mjs --reporter=line— 204 passed.npm run ai:lint-config-template-ssot— passed with parity snapshot current.npm run agent-preflight -- --no-fix— passed on the staged change.Post-Merge Validation