@event4u/agent-config 17.0.0 → 17.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +157 -468
  5. package/README.md +4 -3
  6. package/dist/agent-src/commands/project/analyze.md +9 -6
  7. package/dist/agent-src/commands/project/map.md +58 -0
  8. package/dist/agent-src/commands/project.md +5 -3
  9. package/dist/agent-src/contexts/execution/host-capability-manifest.md +5 -3
  10. package/dist/agent-src/skills/code-intelligence/SKILL.md +6 -0
  11. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  12. package/dist/agent-src/templates/roadmaps.md +5 -1
  13. package/dist/cli/registry.js +1 -1
  14. package/dist/cli-delegate/{chunk-IROAQ77A.js → chunk-3PK3VZHB.js} +107 -9
  15. package/dist/cli-delegate/{chunk-2SFOOFJJ.js → chunk-BZJ4Y3CI.js} +1 -1
  16. package/dist/cli-delegate/{chunk-CAL76ADI.js → chunk-CMZEKSNN.js} +52 -10
  17. package/dist/cli-delegate/{chunk-ESUNQ5XR.js → chunk-E3CFULNX.js} +22 -4
  18. package/dist/cli-delegate/{chunk-GOMILC3O.js → chunk-QBHEI5N4.js} +4 -3
  19. package/dist/cli-delegate/{chunk-SIOMDAKL.js → chunk-VYZGJLCA.js} +29 -39
  20. package/dist/cli-delegate/chunk-W5JXMF2T.js +77 -0
  21. package/dist/cli-delegate/{chunk-4SHNERBL.js → chunk-XIAPY44N.js} +1 -1
  22. package/dist/cli-delegate/{chunk-JHMMWNJ2.js → chunk-YHAPONWV.js} +1 -1
  23. package/dist/cli-delegate/cmd_conformance.js +32 -47
  24. package/dist/cli-delegate/cmd_doctor.js +4 -4
  25. package/dist/cli-delegate/cmd_doctor_neighbours.js +2 -2
  26. package/dist/cli-delegate/cmd_explain.js +1 -1
  27. package/dist/cli-delegate/cmd_export.js +6 -6
  28. package/dist/cli-delegate/cmd_fleet.js +3 -3
  29. package/dist/cli-delegate/cmd_preflight.js +3 -3
  30. package/dist/cli-delegate/cmd_prune.js +6 -6
  31. package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
  32. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  33. package/dist/cli-delegate/cmd_sync.js +6 -6
  34. package/dist/cli-delegate/cmd_uninstall.js +6 -6
  35. package/dist/cli-delegate/cmd_update.js +9 -13
  36. package/dist/cli-delegate/cmd_validate.js +6 -6
  37. package/dist/discovery/deprecation-report.md +1 -1
  38. package/dist/discovery/discovery-manifest.json +41 -15
  39. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  40. package/dist/discovery/discovery-manifest.summary.md +3 -3
  41. package/dist/discovery/dormancy-report.md +1 -1
  42. package/dist/discovery/orphan-report.md +1 -1
  43. package/dist/discovery/packs.json +6 -5
  44. package/dist/discovery/trust-report.md +2 -2
  45. package/dist/discovery/workspaces.json +5 -4
  46. package/dist/hooks/dispatch.js +163 -148
  47. package/dist/hooks/dispatch.sha256 +1 -1
  48. package/dist/hooks/instructions_observer.chunk.cjs +301 -0
  49. package/dist/hooks/instructions_observer.chunk.cjs.sha256 +1 -0
  50. package/dist/install/claudeRuleRewrite.js +10 -3
  51. package/dist/install/conflict.js +24 -2
  52. package/dist/install/conflictTracker.js +14 -3
  53. package/dist/install/install.mjs +222 -117
  54. package/dist/install/installThinLayer.js +1 -0
  55. package/dist/install/loggedWrites.js +142 -0
  56. package/dist/install/paths.js +12 -2
  57. package/dist/install/preserve.js +25 -4
  58. package/dist/install/txlog.js +10 -6
  59. package/dist/mcp/registry-manifest.json +3 -3
  60. package/dist/mcp/server.json +2 -2
  61. package/dist/mcp/server.mjs +33595 -22916
  62. package/dist/scripts/_lib/egress_rules.js +4 -1
  63. package/dist/scripts/_lib/identifier_redaction.js +35 -2
  64. package/dist/scripts/ai_council/seat_policy.js +7 -1
  65. package/dist/server/schemas/settings.js +1 -1
  66. package/package.json +4 -1
  67. package/src/config/ci-local-parity.yml +1 -1
  68. package/src/config/continuity-surface.json +7 -0
  69. package/src/config/evaluator-budgets.json +4 -4
  70. package/src/config/gate-coverage.yml +27 -7
  71. package/src/config/gate-violation-baselines.json +9 -3
  72. package/src/config/gitignore-block.txt +6 -0
  73. package/src/config/hook-bundle-budget.json +19 -4
  74. package/src/config/hook-token-budget.json +1 -1
  75. package/src/config/packed-binary-manifest.json +27 -0
  76. package/src/config/production-entries.json +18 -0
  77. package/src/config/release-gate-locality.yml +0 -7
  78. package/src/config/skill-body-budget.json +7 -1
  79. package/src/install/claudeRuleRewrite.ts +10 -1
  80. package/src/install/conflict.ts +24 -1
  81. package/src/install/conflictTracker.ts +18 -2
  82. package/src/install/installThinLayer.ts +6 -0
  83. package/src/install/loggedWrites.ts +148 -0
  84. package/src/install/paths.ts +11 -2
  85. package/src/install/preserve.ts +40 -4
  86. package/src/install/txlog.ts +10 -6
  87. package/src/scripts/_cli/cmd_conformance.ts +3 -2
  88. package/src/scripts/_cli/cmd_update.ts +15 -17
  89. package/src/scripts/_lib/bench_ab_complexity.ts +14 -2
  90. package/src/scripts/_lib/collector_record.ts +1 -0
  91. package/src/scripts/_lib/council_settings_block.ts +9 -2
  92. package/src/scripts/_lib/egress_rules.ts +4 -1
  93. package/src/scripts/_lib/host_capability.ts +10 -5
  94. package/src/scripts/_lib/identifier_redaction.ts +38 -2
  95. package/src/scripts/_lib/price_freshness.ts +112 -0
  96. package/src/scripts/_lib/production_reach.ts +194 -0
  97. package/src/scripts/_lib/quota_parked.ts +17 -28
  98. package/src/scripts/_lib/release_findings_ingest.ts +13 -335
  99. package/src/scripts/_lib/runtime_journal.ts +7 -7
  100. package/src/scripts/_lib/tier_downshift_verdict.ts +81 -35
  101. package/src/scripts/ai_council/chairman.ts +5 -2
  102. package/src/scripts/ai_council/clients.ts +4 -3
  103. package/src/scripts/ai_council/free_hand_shadow.ts +24 -12
  104. package/src/scripts/ai_council/free_route_catalog.ts +142 -5
  105. package/src/scripts/ai_council/free_route_client.ts +4 -0
  106. package/src/scripts/ai_council/hand_payload.ts +115 -7
  107. package/src/scripts/ai_council/orchestrator.ts +7 -10
  108. package/src/scripts/ai_council/route_chooser.ts +1 -1
  109. package/src/scripts/ai_council/route_member.ts +66 -4
  110. package/src/scripts/ai_council/seat_policy.ts +8 -1
  111. package/src/scripts/ai_council/self_hosted_probe.ts +327 -0
  112. package/src/scripts/ai_council/status_surface.ts +2 -0
  113. package/src/scripts/ask_transport.ts +137 -15
  114. package/src/scripts/check_finding_dispositions.ts +25 -71
  115. package/src/scripts/check_release_pr_shape.ts +3 -3
  116. package/src/scripts/check_step_production_reach.ts +257 -0
  117. package/src/scripts/code_graph/build.ts +210 -10
  118. package/src/scripts/code_graph/cli.ts +168 -46
  119. package/src/scripts/code_graph/detect.ts +5 -4
  120. package/src/scripts/code_graph/docs_layer.ts +57 -0
  121. package/src/scripts/code_graph/extract.ts +58 -13
  122. package/src/scripts/code_graph/extract_more.ts +314 -0
  123. package/src/scripts/code_graph/laravel_overlay.ts +454 -0
  124. package/src/scripts/code_graph/map.ts +846 -0
  125. package/src/scripts/code_graph/map_model.ts +977 -0
  126. package/src/scripts/code_graph/map_verify.ts +140 -0
  127. package/src/scripts/code_graph/map_viewer/viewer.css +278 -0
  128. package/src/scripts/code_graph/map_viewer/viewer.js +756 -0
  129. package/src/scripts/code_graph/scip_reader.ts +201 -0
  130. package/src/scripts/code_graph/sqlite_store.ts +8 -1
  131. package/src/scripts/code_graph/types.ts +81 -15
  132. package/src/scripts/code_graph/validate.ts +16 -26
  133. package/src/scripts/code_graph/verbs.ts +7 -0
  134. package/src/scripts/council_cli.ts +10 -8
  135. package/src/scripts/hook_chunks.mjs +7 -0
  136. package/src/scripts/hook_manifest.json +1 -1
  137. package/src/scripts/hook_manifest.yaml +23 -0
  138. package/src/scripts/hooks/dispatch_hook.ts +4 -4
  139. package/src/scripts/hooks/instructions_observer_hook.ts +25 -6
  140. package/src/scripts/hooks/lazy_concern.ts +4 -0
  141. package/src/scripts/hooks/payload_stub.ts +27 -0
  142. package/src/scripts/hooks/spawn_guard_shadow_hook.ts +29 -70
  143. package/src/scripts/hooks/subagent_ledger_hook.ts +80 -0
  144. package/src/scripts/install-hooks.sh +5 -0
  145. package/src/scripts/install.ts +2 -2
  146. package/src/scripts/lint_agents_layout.ts +2 -0
  147. package/src/scripts/lint_decision_classes.ts +34 -0
  148. package/src/scripts/lint_hook_manifest.ts +25 -0
  149. package/src/scripts/lint_roadmap_blockers.ts +52 -3
  150. package/src/scripts/mcp_server/consumer_tool_catalog.json +53 -6
  151. package/src/scripts/mcp_server/graph_tools.ts +73 -1
  152. package/src/scripts/mcp_server/tools.ts +19 -36
  153. package/src/scripts/release.ts +5 -9
  154. package/src/scripts/release_drill.ts +11 -165
  155. package/src/scripts/release_publication.ts +0 -9
  156. package/src/scripts/report_production_reach.ts +35 -0
  157. package/src/scripts/worker_reuse_shadow_report.ts +399 -1
  158. package/src/server/schemas/settings.ts +1 -1
  159. package/src/vendor/grammars/README.md +10 -5
  160. package/src/vendor/grammars/tree-sitter-go.wasm +0 -0
  161. package/src/vendor/grammars/tree-sitter-java.wasm +0 -0
  162. package/src/vendor/grammars/tree-sitter-python.wasm +0 -0
  163. package/dist/cli-delegate/chunk-A4QJ6ENT.js +0 -50
  164. /package/dist/cli-delegate/{chunk-E6XNBVE4.js → chunk-KR5ZIFPO.js} +0 -0
@@ -4,8 +4,8 @@
4
4
  "registered_at": "2026-10-01",
5
5
  "owner": "maintainer",
6
6
  "review_by": "2027-10-01",
7
- "max_bytes": 1550000,
8
- "max_bytes_derivation": "Measured 1,514,715 bytes on origin/main at 213d87055 (the gate's own reading, APFS-cloned node_modules so no worktree path artefacts). The ceiling is that reading plus ~2.3 % of headroom, rounded to 1,550,000, on the same doctrine as the 2026-10-01 registration: a bundle is rebuilt from source on every run, so an esbuild minor that renames symbols moves the figure by hundreds of bytes with no change to this tree, and a ceiling one byte above the reading would red on that. Composition is healthy at this reading and was checked rather than assumed: one YAML package (258,833 bytes) and 1,238,525 bytes of own source, so the growth is own code, not a re-entered dependency. The 1,564,211-byte pre-swap regression this roadmap closed still cannot reappear under it.",
7
+ "max_bytes": 1550500,
8
+ "max_bytes_derivation": "Measured 1,514,715 bytes on origin/main at 213d87055 (the gate's own reading, APFS-cloned node_modules so no worktree path artefacts). The ceiling is that reading plus ~2.3 % of headroom, rounded to 1,550,000, on the same doctrine as the 2026-10-01 registration: a bundle is rebuilt from source on every run, so an esbuild minor that renames symbols moves the figure by hundreds of bytes with no change to this tree, and a ceiling one byte above the reading would red on that. Composition is healthy at this reading and was checked rather than assumed: one YAML package (258,833 bytes) and 1,238,525 bytes of own source, so the growth is own code, not a re-entered dependency. The 1,564,211-byte pre-swap regression this roadmap closed still cannot reappear under it. RAISED to 1,550,500 on 2026-10-23 for the InstructionsLoaded binding (237 bytes against 51 of headroom); the reasoning and the measured alternative are the last `raise_log` entry, so this paragraph describes the 1,550,000 registration and the raise is recorded there rather than restated.",
9
9
  "shrink_only": true,
10
10
  "shrink_only_note": "A RAISE needs a recorded reason in `raise_log` below naming what was added and why it could not be avoided. Enforced since road-to-a-ratification-fence-that-follows-its-imports 3.2: the gate reads `max_bytes` at the base revision, and a working value above it fails unless `raise_log` gained an entry whose `to` equals the new value. A lowering needs no entry. This file is also on the ratification gate's watched set, so a raise carries a record as well as a log entry.",
11
11
  "raise_log": [
@@ -24,9 +24,18 @@
24
24
  "from": 28000,
25
25
  "to": 32000,
26
26
  "by": "agent, during the feat/subagent-system-2583 run",
27
- "what_was_added": "The queued engine work the 28,000 ceiling was sized for, landed together: the validated identifier rows of one-detector 2.3 (IBAN, card, e-mail with a keyed-digest allowlist, three phone shapes, German tax id, US SSN), the invisible-character and lookalike-letter normalisation of 2.5, and three free-tier credential rows (Hugging Face, NVIDIA, Replicate) of free-hands 1.2. Measured 30,445 bytes when the raise landed; 30,493 bytes at the ratified revision, after the round-6 IBAN rows.",
28
- "why_it_could_not_be_avoided": "Each addition is a detection row or the normalisation that keeps one from being hidden; the 2026-10-18 derivation already counted them as the chunk's planned growth and underestimated their sum by about 2.4 KB. The chunk is loaded only when a caller needs detection, so no dispatch pays these bytes; the dispatch bundle itself is unchanged.",
27
+ "what_was_added": "The queued engine work the 28,000 ceiling was sized for, landed together: the validated identifier rows of one-detector 2.3 (IBAN, card, e-mail with a keyed-digest allowlist, three phone shapes, German tax id, US SSN), and three free-tier credential rows (Hugging Face, NVIDIA, Replicate) of free-hands 1.2. The invisible-character normalisation of one-detector 2.5 (`egress_normalize.ts`) is not in this chunk: the engine does not import it (corrected 2026-10-23, road-to-a-tick-the-shipped-path-earns 1.4). Measured 30,445 bytes when the raise landed; 30,493 bytes at the ratified revision, after the round-6 IBAN rows.",
28
+ "why_it_could_not_be_avoided": "Each addition is an identifier or credential detection row; the 2026-10-18 derivation already counted them as the chunk's planned growth and underestimated their sum by about 2.4 KB. The chunk is loaded only when a caller needs detection, so no dispatch pays these bytes; the dispatch bundle itself is unchanged.",
29
29
  "what_would_have_caught_it": "Re-measuring the queued items' sum when the ceiling was set, rather than estimating each."
30
+ },
31
+ {
32
+ "date": "2026-10-23",
33
+ "from": 1550000,
34
+ "to": 1550500,
35
+ "by": "agent, feat/instructions-loaded-binding (road-to-a-setup-that-decides-itself 1.1)",
36
+ "what_was_added": "The `InstructionsLoaded` binding: one `LAZY_CONCERNS` row for the observer (its 11,097-byte body ships in its own chunk, never in this bundle) and the event name in the three vocabularies the bundle carries (dispatcher, collector, journal). Measured 1,550,186 bytes with the binding by the gate; the same esbuild invocation on the base and on this branch differs by 237 bytes, so the base sat at 1,549,949 — 51 bytes of headroom.",
37
+ "why_it_could_not_be_avoided": "D8 (council, 2026-10-18) asked for room before a raise. Room was tried and measured: moving `session-eol` (27,708 B in-bundle, `stop` alone) into a chunk freed 29,948 B (same esbuild invocation, with and without the move) but produced a 259,330-byte chunk (`yaml` external) required on every `stop` — about 8.5 times the bytes that event parses today, to buy 186. The observer itself was already moved out. What remains in the bundle is the row and three event-name strings, which no chunk can carry.",
38
+ "what_would_have_caught_it": "Nothing earlier: the base sat 51 bytes under the ceiling, so any new event in the vocabulary would have tripped it. Sized to this binding plus about 300 bytes; step 3.3's promotion is sized when it is built."
30
39
  }
31
40
  ],
32
41
  "chunks": [
@@ -47,6 +56,12 @@
47
56
  "max_bytes": 232000,
48
57
  "registered_at": "2026-10-20",
49
58
  "derivation": "Measured 209,890 bytes on 2026-10-20, with `yaml` external (inlined it is 472,201 \u2014 a second copy of the parser the dispatch bundle already carries). The ceiling is that reading plus about 10 %, rounded, because this chunk carries a 2,300-line concern plus the memory lookup and FTS index it reads, all of which still grow.\n\nWHY IT WAS MOVED \u2014 AND THE REASON FIRST GIVEN WAS FALSE, which is recorded here rather than corrected silently. This entry said `chat-history` is bound on `session_start` and on no other event, and called it the largest narrow-event module the metafile ranks. An independent provider review of 2026-10-20 checked the manifest and refused the change on it: `chat-history` is bound on FIVE events \u2014 `session_start`, `session_end`, `stop`, `post_tool_use` and `user_prompt_submit` \u2014 across seven hosts. It is close to the widest-bound concern in the tree, not the narrowest. The claim came from reading the `session_start:` rows of the manifest and never the others.\n\nWHAT SURVIVES THE CORRECTION, stated as narrowly as the evidence allows: the move takes 120,586 bytes out of a bundle every hook event parses (1,549,961 -> 1,429,414 B) and pays for them with one `require` of a 209,890-byte CJS file on the events that actually need the concern. Whether that is a latency WIN is UNMEASURED, and the 2026-10-18 council that chose this mechanism asked for exactly that proof inside `src/config/hook-latency-budget.json`. Until it exists this chunk is a byte-budget move with an unproven wall-clock effect, and the honest narrow-event candidates the same metafile ranks are recorded for whoever re-does it: `turn-end-gate` 71,928 B on `stop` alone, `injection-scan` 46,660 B on `post_tool_use` alone, `skill-route` 31,183 B and `git-authorization` 30,761 B on `user_prompt_submit` alone, `session-eol` 27,708 B on `stop` alone, `handoff-context` 25,220 B on `session_start` alone.\n\nUNLIKE THE OTHER TWO CHUNKS this one carries a CONCERN, so there is a correct path behind the fast one: `_run_concern` tries the static registry, then `loadLazyConcern`, then the historical spawn-per-concern path. A missing or mis-digested chunk costs latency, never the concern's effect \u2014 which the same review confirmed, noting it holds because `src/scripts/` ships in the package."
59
+ },
60
+ {
61
+ "entry": "src/scripts/hooks/instructions_observer_hook.ts",
62
+ "max_bytes": 12000,
63
+ "registered_at": "2026-10-23",
64
+ "derivation": "Measured 11,097 bytes on 2026-10-23 (the observe-only `InstructionsLoaded` capture, its classifier, and the CLI entry the spawn fallback needs). The ceiling is that reading plus about 10 %, rounded, with no growth queued. Bound on `instructions_loaded` alone, so a chunk keeps it off every other dispatch's parse; the observer body ships here, never in the dispatch bundle. The binding that reaches it (one registry row and the event name in three vocabularies, 237 bytes) does sit in the bundle and is the 2026-10-23 raise of `max_bytes` recorded in `raise_log`. Like `chat_history` it carries a concern, so a missing chunk falls back to the spawn path and costs latency, never the capture."
50
65
  }
51
66
  ],
52
67
  "chunks_note": "A chunk is loaded only when a caller needs it, so its bytes are not paid by every dispatch; its ceiling exists so that the move out of dispatch.js stays a measured place rather than an unmeasured one. The gate builds each chunk with the exact esbuild segment `build:hook-chunks` uses for its entry.",
@@ -44,7 +44,7 @@
44
44
  "_comment": "Cross-concern per-fire sum per slot (roadmap 3.1). session_start is one-time and may carry the restore payloads; per-turn slots stay tight.",
45
45
  "session_start": 16384,
46
46
  "user_prompt_submit": 16384,
47
- "user_prompt_submit_reason": "RAISED 4096 -> 16384 on 2026-09-07 by road-to-delivery-for-every-host 3.2, under owner ruling E2, which assigns this move to the run that flips `lean_projection.mode: delivery` and is the activation charge the `rule-inject` row below defers to it. THE NUMBER IS MEASURED, NOT CHOSEN: 16,384 is the p90 gate-open fire size of the delivery concern over the frozen `tests/eval/routing-matrix` corpus rounded UP to 512 bytes, exactly as E2 specifies. Distribution over 318 gate-open fires: p50 6,674 B, p90 16,188 B, max 20,406 B. Reproduce it from `_lib/rule_injection.ts::matchTierRules` + `selectForInjection` at `hooks/rule_inject_hook.ts::CAP_BYTES` over that corpus. NOT config weakening, and the guard agrees by its own classification: `hooks/block_config_weakening.ts:96-98` classifies `*-budget.json` as `advisory`, so its decide() path returns `warn` and never a block — treat that warning as expected and documented. `pre_tool_use` is deliberately NOT raised alongside it: the concern's binding on that slot was REMOVED in the same change, so nothing new emits there and its 2,048 stands.",
47
+ "user_prompt_submit_reason": "RAISED 4096 -> 16384 on 2026-09-07 by road-to-delivery-for-every-host 3.2, under owner ruling E2, which assigns this move to the run that flipped the rule layer to its delivery form (then a settings key, removed by road-to-a-setup-that-decides-itself 4.4) and is the activation charge the `rule-inject` row below defers to it. THE NUMBER IS MEASURED, NOT CHOSEN: 16,384 is the p90 gate-open fire size of the delivery concern over the frozen `tests/eval/routing-matrix` corpus rounded UP to 512 bytes, exactly as E2 specifies. Distribution over 318 gate-open fires: p50 6,674 B, p90 16,188 B, max 20,406 B. Reproduce it from `_lib/rule_injection.ts::matchTierRules` + `selectForInjection` at `hooks/rule_inject_hook.ts::CAP_BYTES` over that corpus. NOT config weakening, and the guard agrees by its own classification: `hooks/block_config_weakening.ts:96-98` classifies `*-budget.json` as `advisory`, so its decide() path returns `warn` and never a block — treat that warning as expected and documented. `pre_tool_use` is deliberately NOT raised alongside it: the concern's binding on that slot was REMOVED in the same change, so nothing new emits there and its 2,048 stands.",
48
48
  "pre_tool_use": 2048,
49
49
  "post_tool_use": 2048,
50
50
  "stop": 2048,
@@ -28,6 +28,33 @@
28
28
  "grammar_id": "javascript",
29
29
  "abi": 14,
30
30
  "why": "The JavaScript grammar. Same reason as the php entry."
31
+ },
32
+ {
33
+ "path": "src/vendor/grammars/tree-sitter-python.wasm",
34
+ "sha256": "9056d0fb0c337810d019fae350e8167786119da98f0f282aceae7ab89ee8253b",
35
+ "size": 476105,
36
+ "kind": "tree-sitter-wasm-grammar",
37
+ "grammar_id": "python",
38
+ "abi": 14,
39
+ "why": "The Python grammar (road-to-a-map-that-draws-itself 6.1). Same reason as the php entry: the engine indexes .py files in a consumer install with no manual parser step."
40
+ },
41
+ {
42
+ "path": "src/vendor/grammars/tree-sitter-go.wasm",
43
+ "sha256": "9963ca89b616eaf04b08a43bc1fb0f07b85395bec313330851f1f1ead2f755b6",
44
+ "size": 235957,
45
+ "kind": "tree-sitter-wasm-grammar",
46
+ "grammar_id": "go",
47
+ "abi": 14,
48
+ "why": "The Go grammar (road-to-a-map-that-draws-itself 6.1). Same reason as the php entry: the engine indexes .go files in a consumer install with no manual parser step."
49
+ },
50
+ {
51
+ "path": "src/vendor/grammars/tree-sitter-java.wasm",
52
+ "sha256": "637aac4415fb39a211a4f4292d63c66b5ce9c32fa2cd35464af4f681d91b9a1f",
53
+ "size": 430239,
54
+ "kind": "tree-sitter-wasm-grammar",
55
+ "grammar_id": "java",
56
+ "abi": 14,
57
+ "why": "The Java grammar (road-to-a-map-that-draws-itself 6.1). Same reason as the php entry: the engine indexes .java files in a consumer install with no manual parser step."
31
58
  }
32
59
  ]
33
60
  }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$comment": "Production entries for check_step_production_reach (road-to-a-tick-the-shipped-path-earns 3.1). `entries` are modules a host or a consumer runs directly. `entry_sources` are files that name runnable scripts by path (`src/scripts/<name>`, with or without `.ts`); every script they name is an entry too. Projected subagent definitions are markdown read by the host, not imported, so they are not entries here: a step about one names its generator with `production entry: <path>`.",
3
+ "entries": [
4
+ "src/scripts/hooks/dispatch_hook.ts",
5
+ "src/cli/agent-config.ts",
6
+ "src/server/app.ts"
7
+ ],
8
+ "entry_sources": [
9
+ "src/scripts/_dispatch.bash",
10
+ "Taskfile.yml",
11
+ "taskfiles/*.yml",
12
+ "hooks/hooks.json",
13
+ ".github/workflows/*.yml",
14
+ "src/agent-src/commands/**/*.md",
15
+ "src/domains/**/*.md",
16
+ "src/skills/**/SKILL.md"
17
+ ]
18
+ }
@@ -106,13 +106,6 @@ jobs:
106
106
  script: src/scripts/check_template_pin_drift.ts
107
107
  guard: ~
108
108
 
109
- finding-dispositions:
110
- local: './scripts-run src/scripts/check_finding_dispositions --release ${VERSION}'
111
- verify: false
112
- reason: 'NEEDS_PR: the unrecorded-findings half reads live PR review comments. The missing-dispositions half does run locally and is what the `local` command covers.'
113
- script: src/scripts/check_finding_dispositions.ts
114
- guard: ~
115
-
116
109
  audit-gate:
117
110
  local: 'npm audit --omit=dev --audit-level=high'
118
111
  verify: true
@@ -38,7 +38,7 @@
38
38
  "character-consistency": 8555,
39
39
  "check-refs": 4188,
40
40
  "churn-prevention": 8014,
41
- "code-intelligence": 16492,
41
+ "code-intelligence": 16998,
42
42
  "code-refactoring": 13717,
43
43
  "code-review": 16261,
44
44
  "command-routing": 6040,
@@ -681,6 +681,12 @@
681
681
  "old": 7434,
682
682
  "new": 8465,
683
683
  "reason": "A § Free tiers table (where a free tier's quota, data-use and region terms are read live, plus the never-send-project-data-to-a-training-tier line) so the skill answers 'set up a free LLM API' without copying any model id or limit. road-to-free-hands-a-paid-head-checks 7.1."
684
+ },
685
+ {
686
+ "skill": "code-intelligence",
687
+ "old": 16492,
688
+ "new": 16998,
689
+ "reason": "road-to-a-map-that-draws-itself (ADR-297): step 0 sends an orientation question to agents/map/MAP.md before any graph query, and the MCP table gains rows for graph_map_status and graph_area. Both are the skill's routing surface for the new map; trimming them would hide the tools the roadmap added."
684
690
  }
685
691
  ]
686
692
  }
@@ -48,6 +48,9 @@ import * as fs from 'node:fs';
48
48
  import * as path from 'node:path';
49
49
 
50
50
  import { _claude_paths_plan } from './claudePathsPlan.js';
51
+ // Re-exported for `install.ts`, which sits past the source-size ceiling and so
52
+ // reaches it through the namespace it already imports instead of a new line.
53
+ export { onlyDeployed } from './loggedWrites.js';
51
54
  import { parseFrontmatter } from './ruleInScope.js';
52
55
 
53
56
  /**
@@ -134,8 +137,9 @@ export function rewriteAndReport(
134
137
  warn: (m: string) => void,
135
138
  wrappers?: { wrapped: readonly string[]; reserved: readonly string[]; foreign?: readonly string[] },
136
139
  preserved: ReadonlySet<string> = new Set(),
140
+ onWrite?: (target: string) => void,
137
141
  ): ClaudeRuleRewriteResult {
138
- const result = rewriteClaudeRules(rulesDir, preserved);
142
+ const result = rewriteClaudeRules(rulesDir, preserved, onWrite);
139
143
  // The flat-command wrapper report rides along rather than sitting at the
140
144
  // call site: `install.ts` is thousands of lines past
141
145
  // `check_source_size_budget`'s ceiling, and this module is the one place
@@ -248,10 +252,14 @@ export function renderClaudeRule(sourceText: string): {
248
252
  * disappearing from the frontmatter.
249
253
  *
250
254
  * Returns counts rather than printing. The caller owns the reporting surface.
255
+ * `onWrite` receives each rewritten path, so the installer can log it: the
256
+ * transaction log is read back as proof the bytes on disk are ours, and a
257
+ * write it never saw would leave every rewritten rule unprovable.
251
258
  */
252
259
  export function rewriteClaudeRules(
253
260
  rulesDir: string,
254
261
  preserved: ReadonlySet<string> = new Set(),
262
+ onWrite?: (target: string) => void,
255
263
  ): ClaudeRuleRewriteResult {
256
264
  const result: ClaudeRuleRewriteResult = { rewritten: 0, scoped: [], dropped: [], failed: [] };
257
265
  let entries: string[];
@@ -299,6 +307,7 @@ export function rewriteClaudeRules(
299
307
  });
300
308
  continue;
301
309
  }
310
+ onWrite?.(full);
302
311
  result.rewritten += 1;
303
312
  }
304
313
  const stem = name.slice(0, -3);
@@ -18,7 +18,8 @@
18
18
  import { existsSync } from 'node:fs';
19
19
 
20
20
  import { sha256File } from './plan.js';
21
- import { isLedgerMigration } from './preserve.js';
21
+ import { loggedWriteDigests, wroteTheseBytes, type LoggedWrites } from './loggedWrites.js';
22
+ import { isLedgerMigration, logProofApplies, sidecarPathFor } from './preserve.js';
22
23
  import {
23
24
  classifyOwnership,
24
25
  ledgerListedPaths,
@@ -200,10 +201,13 @@ export const CONFLICT_BATCH_THRESHOLD = 5;
200
201
  *
201
202
  * `recorded` is injectable for tests; by default it is read from the
202
203
  * manifest at `plan.root`, with the user-scoped ownership ledger laid over it.
204
+ * `logged` likewise; by default it is the install transaction log, opened only
205
+ * for an entry `logProofApplies` to.
203
206
  */
204
207
  export function computeConflicts(
205
208
  plan: InstallPlan,
206
209
  recordedOverride?: RecordedHashes,
210
+ loggedOverride?: LoggedWrites,
207
211
  ): readonly ConflictEntry[] {
208
212
  // Force returns before the manifest is read. As a default parameter the
209
213
  // map was built at call entry and discarded on the next line, so every
@@ -214,6 +218,7 @@ export function computeConflicts(
214
218
  // digests never reaches the branch that needs this, and an unconditional
215
219
  // read would charge every plan for an answer most of them discard.
216
220
  let ledgerFiles: ReadonlySet<string> | null = null;
221
+ let logged: LoggedWrites | undefined;
217
222
  const ledgerLists = (p: string): boolean => {
218
223
  ledgerFiles ??= ledgerListedPaths();
219
224
  return ledgerFiles.has(p);
@@ -234,6 +239,24 @@ export function computeConflicts(
234
239
  const onDisk = sha256File(entry.path);
235
240
  const ownership = classifyOwnership(recorded.get(entry.path), onDisk);
236
241
  if (ownership === 'recorded-unchanged') continue;
242
+ // The writer's transaction-log proof, under the writer's own
243
+ // condition (`logProofApplies`): where it holds and the log records
244
+ // these bytes, the writer replaces the file, so the preview must
245
+ // not name it.
246
+ const recordedSha = recorded.get(entry.path);
247
+ const unproven = {
248
+ exists: true,
249
+ recordedSha256: recordedSha,
250
+ onDiskSha256: onDisk,
251
+ force: false,
252
+ ledgerListed: ledgerLists(entry.path),
253
+ recordedIsStaged:
254
+ typeof recordedSha === 'string' && recordedSha === recorded.get(sidecarPathFor(entry.path)),
255
+ };
256
+ if (logProofApplies(unproven)) {
257
+ logged ??= loggedOverride ?? loggedWriteDigests();
258
+ if (wroteTheseBytes(entry.path, onDisk as string, logged)) continue;
259
+ }
237
260
  if (ownership === 'unknown' && plan.policy.knownPaths.has(entry.path)) continue;
238
261
  // CANDIDACY, not the verdict, and it is computed BEFORE the
239
262
  // equality short-circuit below rather than after. The migration
@@ -28,11 +28,13 @@ import {
28
28
  decideDeployWrite,
29
29
  foreignSidecarMessage,
30
30
  isLedgerMigration,
31
+ logProofApplies,
31
32
  migrationPreservedMessage,
32
33
  preservedFileMessage,
33
34
  sidecarPathFor,
34
35
  type DeployWriteDecision,
35
36
  } from './preserve.js';
37
+ import { loggedWriteDigests, wroteTheseBytes, type LoggedWrites } from './loggedWrites.js';
36
38
  import { ledgerListedPaths, recordedHashesWithLedger, type RecordedHashes } from './recordedOwnership.js';
37
39
 
38
40
  /**
@@ -89,6 +91,8 @@ export interface ConflictState {
89
91
  * compare a file against a listing this very run had just produced.
90
92
  */
91
93
  ledgerFiles: ReadonlySet<string> | null;
94
+ /** The transaction log's writes, read lazily once per run for the same reason. */
95
+ logged: LoggedWrites | null;
92
96
  preserved: string[];
93
97
  /**
94
98
  * Targets this run preserved on the migration branch.
@@ -148,6 +152,7 @@ export function createConflictTracker(host: ConflictTrackerHost): ConflictTracke
148
152
  root: null,
149
153
  recorded: null,
150
154
  ledgerFiles: null,
155
+ logged: null,
151
156
  preserved: [],
152
157
  migrated: new Set(),
153
158
  staged: new Map(),
@@ -331,6 +336,7 @@ export function createConflictTracker(host: ConflictTrackerHost): ConflictTracke
331
336
  conflictState.root = root;
332
337
  conflictState.recorded = null;
333
338
  conflictState.ledgerFiles = null;
339
+ conflictState.logged = null;
334
340
  conflictState.preserved = [];
335
341
  conflictState.migrated = new Set();
336
342
  conflictState.staged = new Map();
@@ -344,13 +350,23 @@ export function createConflictTracker(host: ConflictTrackerHost): ConflictTracke
344
350
  */
345
351
  resolve(target: string, force: boolean, would: WouldWrite): DeployWriteDecision {
346
352
  const exists = pathExists(target);
347
- const base = {
353
+ const recordedSha256 = exists ? recordedHashFor(target) : undefined;
354
+ const unproven = {
348
355
  exists,
349
- recordedSha256: exists ? recordedHashFor(target) : undefined,
356
+ recordedSha256,
350
357
  onDiskSha256: exists ? sha256OfFile(target) : null,
351
358
  force,
352
359
  ledgerListed: exists && ledgerLists(target),
360
+ recordedIsStaged:
361
+ typeof recordedSha256 === 'string' && recordedSha256 === recordedHashFor(sidecarPathFor(target)),
353
362
  };
363
+ // The log is opened only where the record alone would preserve
364
+ // without cause, and read once per run: this run's own appends
365
+ // cannot prove a file it has not reached yet.
366
+ const loggedWrite =
367
+ logProofApplies(unproven) &&
368
+ wroteTheseBytes(target, unproven.onDiskSha256 as string, (conflictState.logged ??= loggedWriteDigests()));
369
+ const base = { ...unproven, loggedWrite };
354
370
  // The packaged digest costs a temp write and a tag, so it is
355
371
  // computed only for the targets that can consume it. On a tree
356
372
  // where every ledger-listed path carries a digest — which is every
@@ -109,6 +109,11 @@ export interface ThinInstalledLayerOptions {
109
109
  * the staged runtime, which outlives a cleared package-manager cache.
110
110
  */
111
111
  readonly bodyLinkRoot?: string | null;
112
+ /**
113
+ * Called with each path this pass rewrote — the installer logs it, because
114
+ * the transaction log is read back as proof the bytes on disk are ours.
115
+ */
116
+ readonly onWrite?: (target: string) => void;
112
117
  }
113
118
 
114
119
  /**
@@ -212,6 +217,7 @@ export function thinInstalledRuleLayer(opts: ThinInstalledLayerOptions): ThinIns
212
217
  try {
213
218
  if (next !== installed) {
214
219
  fs.writeFileSync(target, next, 'utf-8');
220
+ opts.onWrite?.(target);
215
221
  rewritten += 1;
216
222
  }
217
223
  thinned += 1;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * The install transaction log read as an ownership record.
3
+ *
4
+ * WHY. The writer preserves any managed file it cannot prove is still the bytes
5
+ * it wrote. For a file deployed before the ownership ledger recorded digests,
6
+ * the ledger has nothing to compare against, so EVERY such file was preserved
7
+ * and a sidecar staged beside it — thousands of them on one upgrade, all
8
+ * untouched. Worse, the ledger then records the staged digest for each target,
9
+ * so the next run calls the same untouched file "user-modified".
10
+ *
11
+ * The proof was on disk the whole time. `src/scripts/install.ts` appends a
12
+ * `write` entry carrying the SHA-256 of the target's bytes immediately after
13
+ * every copy. A file whose bytes equal a digest the log records this installer
14
+ * writing to that exact path is the installer's own output, unedited. A path
15
+ * with no matching record keeps the existing answer. WHEN that proof may decide
16
+ * is narrower than "whenever it matches" — see `decideDeployWrite`.
17
+ *
18
+ * WHAT IS READ. The active log and every rotated sibling, plain or gzipped
19
+ * (`txlog.ts` rotates at 10 MB or 30 days, and the gzip step can leave both
20
+ * forms behind). An unreadable or truncated file contributes nothing — the
21
+ * failure direction is "no proof", which preserves.
22
+ *
23
+ * COST. Parsed per FILE and cached on that file's size and mtime. A rotated
24
+ * archive never changes, so only the active log is re-read when it grows — and
25
+ * an install appends to it after every file it touches, so a cache keyed on the
26
+ * whole set would re-parse the entire history once per candidate.
27
+ */
28
+
29
+ import { gunzipSync } from 'node:zlib';
30
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
31
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
32
+
33
+ import { getLogPath } from './paths.js';
34
+
35
+ /** Absolute target path → every digest the log records this installer writing there. */
36
+ export type LoggedWrites = ReadonlyMap<string, ReadonlySet<string>>;
37
+
38
+ const WRITE_MARKER = '"kind":"write"';
39
+
40
+ const fileCache = new Map<string, { readonly stamp: string; readonly entries: ReadonlyArray<readonly [string, string]> }>();
41
+ const dirRealpaths = new Map<string, string>();
42
+
43
+ /**
44
+ * One canonical spelling for a path: its directory realpath'd, its basename
45
+ * kept. Both the log keys and the queries go through it, so `/var/…` and its
46
+ * macOS realpath twin meet. The directory is realpath'd rather than the file
47
+ * because the file may not exist, and is memoised because a log names a few
48
+ * hundred directories across tens of thousands of entries.
49
+ */
50
+ export function canonicalPath(p: string): string {
51
+ const abs = resolve(p);
52
+ const dir = dirname(abs);
53
+ let real = dirRealpaths.get(dir);
54
+ if (real === undefined) {
55
+ try {
56
+ real = realpathSync(dir);
57
+ } catch {
58
+ real = dir;
59
+ }
60
+ dirRealpaths.set(dir, real);
61
+ }
62
+ return join(real, basename(abs));
63
+ }
64
+
65
+ /** The active log plus its rotated siblings, sorted by name. */
66
+ function logFiles(logPath: string): string[] {
67
+ const dir = dirname(logPath);
68
+ if (!existsSync(dir)) return [];
69
+ const stem = basename(logPath).replace(/\.jsonl$/, '');
70
+ try {
71
+ return readdirSync(dir)
72
+ .filter((n) => n.startsWith(stem) && (n.endsWith('.jsonl') || n.endsWith('.jsonl.gz')))
73
+ .map((n) => join(dir, n))
74
+ .sort();
75
+ } catch {
76
+ return [];
77
+ }
78
+ }
79
+
80
+ function writesIn(file: string): ReadonlyArray<readonly [string, string]> {
81
+ let stamp: string;
82
+ try {
83
+ const st = statSync(file);
84
+ stamp = `${st.size}:${Math.trunc(st.mtimeMs)}`;
85
+ } catch {
86
+ return [];
87
+ }
88
+ const cached = fileCache.get(file);
89
+ if (cached !== undefined && cached.stamp === stamp) return cached.entries;
90
+
91
+ let text = '';
92
+ try {
93
+ const raw = readFileSync(file);
94
+ text = (file.endsWith('.gz') ? gunzipSync(raw) : raw).toString('utf8');
95
+ } catch {
96
+ text = '';
97
+ }
98
+ const entries: Array<readonly [string, string]> = [];
99
+ for (const line of text.split('\n')) {
100
+ if (!line.includes(WRITE_MARKER)) continue;
101
+ let entry: { path?: unknown; sha256?: unknown };
102
+ try {
103
+ entry = JSON.parse(line) as { path?: unknown; sha256?: unknown };
104
+ } catch {
105
+ continue;
106
+ }
107
+ if (typeof entry.path !== 'string' || typeof entry.sha256 !== 'string' || entry.sha256 === '') continue;
108
+ entries.push([canonicalPath(entry.path), entry.sha256]);
109
+ }
110
+ fileCache.set(file, { stamp, entries });
111
+ return entries;
112
+ }
113
+
114
+ /** Every `write` the transaction log records, keyed by canonical path. */
115
+ export function loggedWriteDigests(logPath: string = getLogPath()): LoggedWrites {
116
+ const writes = new Map<string, Set<string>>();
117
+ for (const file of logFiles(logPath)) {
118
+ for (const [target, sha] of writesIn(file)) {
119
+ let set = writes.get(target);
120
+ if (set === undefined) writes.set(target, (set = new Set()));
121
+ set.add(sha);
122
+ }
123
+ }
124
+ return writes;
125
+ }
126
+
127
+ /**
128
+ * Wrap a log writer so it records only paths this run's copy deploys.
129
+ *
130
+ * The claude-code rewrite and thinning passes walk the whole rule directory,
131
+ * including rules the user authored there; logging those rewrites would make
132
+ * the log vouch for content that was never the package's.
133
+ */
134
+ export function onlyDeployed(
135
+ anchor: string,
136
+ currentFiles: ReadonlySet<string>,
137
+ log: (target: string) => void,
138
+ ): (target: string) => void {
139
+ return (target) => {
140
+ const rel = relative(resolve(anchor), resolve(target)).split(sep).join('/');
141
+ if (currentFiles.has(rel)) log(target);
142
+ };
143
+ }
144
+
145
+ /** Did this installer write exactly `onDiskSha256` to `target`? */
146
+ export function wroteTheseBytes(target: string, onDiskSha256: string, writes: LoggedWrites): boolean {
147
+ return writes.get(canonicalPath(target))?.has(onDiskSha256) === true;
148
+ }
@@ -69,12 +69,21 @@ export function getInstallRoot(home?: string): string {
69
69
  }
70
70
 
71
71
  /**
72
- * Absolute path to the active transaction log:
73
- * `~/.event4u/agent-config/install-log.jsonl`.
72
+ * Absolute path to the active transaction log: `install-log.jsonl` in
73
+ * `$EVENT4U_CONFIG_HOME` when that is set and no explicit `home` is given,
74
+ * otherwise in `~/.event4u/agent-config/`.
74
75
  *
75
76
  * Phase A4 writers append here on every successful atomic rename.
77
+ *
78
+ * Without the variable, the hermetic test environment — which sets only that
79
+ * variable — appended every test install to the real user's log, and the log
80
+ * is now read back as ownership proof. A history written under the other root
81
+ * before a user switched roots is not read; the failure direction is "no
82
+ * proof", which preserves.
76
83
  */
77
84
  export function getLogPath(home?: string): string {
85
+ const configHome = home ? undefined : process.env['EVENT4U_CONFIG_HOME'];
86
+ if (configHome) return join(configHome, INSTALL_LOG_FILENAME);
78
87
  return join(getInstallRoot(home), INSTALL_LOG_FILENAME);
79
88
  }
80
89
 
@@ -109,6 +109,23 @@ export interface DeployWriteInputs {
109
109
  * `null` when the file could not be read.
110
110
  */
111
111
  readonly onDiskSha256: string | null;
112
+ /**
113
+ * True when the install transaction log records this installer writing
114
+ * exactly the bytes now on disk to this path (`./loggedWrites.ts`).
115
+ * Absent means false. Decides only where {@link logProofApplies} says so.
116
+ */
117
+ readonly loggedWrite?: boolean;
118
+ /**
119
+ * True when the recorded digest is the one this installer STAGED beside the
120
+ * target as a sidecar, rather than bytes it wrote to the target itself.
121
+ *
122
+ * The ledger records a preserved target under the staged digest, so such a
123
+ * target reads `recorded-modified` without anyone having edited it. Only
124
+ * then may the log override the record: a recorded digest of bytes we DID
125
+ * write diverging from disk is a real edit — including a user restoring an
126
+ * older shipped version on purpose, which the log would also match.
127
+ */
128
+ readonly recordedIsStaged?: boolean;
112
129
  /** True when the run carries `--force`. */
113
130
  readonly force: boolean;
114
131
  }
@@ -135,18 +152,36 @@ export interface DeployWriteInputs {
135
152
  * behaviour stands rather than being changed on a case nobody reasoned about.
136
153
  */
137
154
  export function isLedgerMigration(inputs: Omit<DeployWriteInputs, 'packagedSha256'>): boolean {
138
- if (!inputs.exists || inputs.force) return false;
155
+ if (!inputs.exists || inputs.force || inputs.loggedWrite === true) return false;
139
156
  if (inputs.ledgerListed !== true) return false;
140
157
  if (inputs.onDiskSha256 === null) return false;
141
158
  return classifyOwnership(inputs.recordedSha256, inputs.onDiskSha256) === 'unknown';
142
159
  }
143
160
 
161
+ /**
162
+ * May the transaction-log proof decide this target? Only where the verdict
163
+ * would otherwise be a preservation the record cannot justify: a ledger-listed
164
+ * file with no digest (the migration branch), or a `recorded-modified` file
165
+ * whose recorded digest is a staged one. Everywhere else the log is not read —
166
+ * the ledger alone already decides, and a genuine edit must stay preserved.
167
+ */
168
+ export function logProofApplies(inputs: Omit<DeployWriteInputs, 'packagedSha256' | 'loggedWrite'>): boolean {
169
+ if (!inputs.exists || inputs.force || inputs.onDiskSha256 === null) return false;
170
+ const ownership = classifyOwnership(inputs.recordedSha256, inputs.onDiskSha256);
171
+ if (ownership === 'recorded-modified') return inputs.recordedIsStaged === true;
172
+ // `loggedWrite` is cleared because `isLedgerMigration` excludes a proven
173
+ // target, and this asks whether the target is a candidate BEFORE the proof.
174
+ return ownership === 'unknown' && isLedgerMigration({ ...inputs, loggedWrite: false });
175
+ }
176
+
144
177
  /**
145
178
  * Decide whether the writer may replace one managed file.
146
179
  *
147
- * A target that does not exist is written. A forced run writes. Otherwise the
148
- * recorded-ownership verdict decides: `recorded-modified` preserves, and
149
- * `recorded-unchanged` writes.
180
+ * A target that does not exist is written. A forced run writes. A target whose
181
+ * bytes the transaction log records this installer writing there is written
182
+ * where {@link logProofApplies} — there is no edit to lose. Otherwise the
183
+ * recorded-ownership verdict decides:
184
+ * `recorded-modified` preserves, and `recorded-unchanged` writes.
150
185
  *
151
186
  * The `unknown` arm is the one worth stating out loud. No recorded digest
152
187
  * means no divergence to measure — a tree with no manifest, a global anchor
@@ -169,6 +204,7 @@ export function isLedgerMigration(inputs: Omit<DeployWriteInputs, 'packagedSha25
169
204
  export function decideDeployWrite(inputs: DeployWriteInputs): DeployWriteDecision {
170
205
  if (!inputs.exists) return 'write';
171
206
  if (inputs.force) return 'write';
207
+ if (inputs.loggedWrite === true && logProofApplies(inputs)) return 'write';
172
208
  const ownership = classifyOwnership(inputs.recordedSha256, inputs.onDiskSha256);
173
209
  if (ownership === 'recorded-modified') return 'preserve';
174
210
  if (ownership === 'unknown' && isLedgerMigration(inputs)) {
@@ -6,14 +6,18 @@
6
6
  * nothing is un-written automatically. No PID lockfile (per Phase-A4
7
7
  * decision 8).
8
8
  *
9
- * WHAT ACTUALLY WRITES THIS FILE, as of 2026-09-15: two call sites.
10
- * `src/server/routes/install.ts`'s recovery-dismiss handler appends a
11
- * `rollback` marker with an empty path and a null hash; `src/scripts/install.ts`
12
- * — the surviving headless writer, per-file, through `_log_tx_entry` — appends
13
- * a `write` or `skip` entry per copied/skipped path during
14
- * `_copy_dir_dereferencing_symlinks`. Both call `appendTxLog` directly, so the
9
+ * WHAT ACTUALLY WRITES THIS FILE: `src/server/routes/install.ts`'s
10
+ * recovery-dismiss handler appends a `rollback` marker with an empty path and a
11
+ * null hash; `src/scripts/install.ts`, through `_log_tx_entry`, appends a
12
+ * `write` or `skip` entry per path it copies, preserves or stages a sidecar
13
+ * for, and a `write` entry per deployed Claude rule the host-form rewrite and
14
+ * the thinning pass rewrite after the copy. All call `appendTxLog`, so the
15
15
  * entry shape is identical by construction rather than by convention.
16
16
  *
17
+ * WHAT READS IT: recovery (the active log only) and `./loggedWrites.ts`, which
18
+ * reads every `write` digest in the active log AND its rotated siblings as
19
+ * proof that bytes on disk are the installer's own output.
20
+ *
17
21
  * Rotation: 10 MB OR 30 days, whichever first. Rotated copies move to
18
22
  * `install-log.<ISO>.jsonl.gz` siblings; recovery never scans them.
19
23
  *
@@ -39,6 +39,7 @@ import * as YAML from 'yaml';
39
39
 
40
40
  import * as installed_tools from '../_lib/installed_tools.js';
41
41
  import { atomicAppendLine } from '../../install/atomic.js';
42
+ import { getLogPath } from '../../install/paths.js';
42
43
  import { readRecentEntries } from '../../install/txlog.js';
43
44
  import { detectToolPresence } from '../../install/detect.js';
44
45
  import { resolvePackageRoot } from '../_lib/package_root.js';
@@ -120,13 +121,13 @@ export function conformanceLogPath(): string {
120
121
  return path.join(os.homedir(), '.event4u', 'agent-config', 'conformance-log.jsonl');
121
122
  }
122
123
 
123
- /** Default install txlog path (kept in sync with `src/install/txlog.ts` docs). */
124
+ /** Install txlog path — the one the installer writes, via `getLogPath()`. */
124
125
  export function installLogPath(): string {
125
126
  const override = process.env['AGENT_CONFIG_INSTALL_LOG'];
126
127
  if (override && override.trim().length > 0) {
127
128
  return override;
128
129
  }
129
- return path.join(os.homedir(), '.event4u', 'agent-config', 'install-log.jsonl');
130
+ return getLogPath();
130
131
  }
131
132
 
132
133
  // ---------------------------------------------------------------------------