@event4u/agent-config 12.1.0 → 14.0.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 (185) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +164 -186
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/cost/report.md +23 -0
  5. package/dist/agent-src/commands/evals/roadmap.json +8 -0
  6. package/dist/agent-src/commands/fix/ci.md +39 -1
  7. package/dist/agent-src/commands/pr/create.md +65 -4
  8. package/dist/agent-src/commands/roadmap/next.md +28 -5
  9. package/dist/agent-src/commands/work.md +16 -0
  10. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
  11. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
  12. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
  13. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
  14. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
  15. package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
  16. package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
  17. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
  18. package/dist/agent-src/contexts/subagent-configuration.md +69 -7
  19. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  20. package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
  21. package/dist/agent-src/rules/design-fidelity.md +1 -1
  22. package/dist/agent-src/rules/design-review-after-ui-write.md +36 -14
  23. package/dist/agent-src/rules/evaluator-independence.md +27 -5
  24. package/dist/agent-src/rules/git-history-discipline.md +5 -1
  25. package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
  26. package/dist/agent-src/rules/ui-audit-gate.md +34 -12
  27. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  28. package/dist/agent-src/scripts/gate_execute.ts +395 -0
  29. package/dist/agent-src/scripts/resume_probe.ts +422 -0
  30. package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
  31. package/dist/agent-src/scripts/update_roadmap_progress.ts +70 -4
  32. package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
  33. package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
  34. package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
  35. package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
  36. package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
  37. package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
  38. package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
  39. package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
  40. package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
  41. package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
  42. package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
  43. package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
  44. package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
  45. package/dist/agent-src/skills/roadmap-writing/SKILL.md +5 -5
  46. package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
  47. package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
  48. package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
  49. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  50. package/dist/agent-src/templates/roadmaps.md +43 -3
  51. package/dist/cli/registry.js +1 -0
  52. package/dist/cli/registry.js.map +1 -1
  53. package/dist/cli-delegate/{chunk-K34QTSD5.js → chunk-7PZ4FSZB.js} +368 -191
  54. package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
  55. package/dist/cli-delegate/{chunk-O52B4F5S.js → chunk-F3YUF7SA.js} +1 -1
  56. package/dist/cli-delegate/{chunk-LKZ5DOYW.js → chunk-XURJ4AQC.js} +11 -3
  57. package/dist/cli-delegate/cmd_conformance.js +224 -104
  58. package/dist/cli-delegate/cmd_doctor.js +2 -2
  59. package/dist/cli-delegate/cmd_export.js +2 -2
  60. package/dist/cli-delegate/cmd_handoff.js +1 -1
  61. package/dist/cli-delegate/cmd_prune.js +2 -2
  62. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  63. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  64. package/dist/cli-delegate/cmd_sync.js +2 -2
  65. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  66. package/dist/cli-delegate/cmd_validate.js +2 -2
  67. package/dist/discovery/deprecation-report.md +1 -1
  68. package/dist/discovery/discovery-manifest.json +73 -30
  69. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  70. package/dist/discovery/discovery-manifest.summary.md +3 -3
  71. package/dist/discovery/dormancy-report.md +1 -1
  72. package/dist/discovery/orphan-report.md +1 -1
  73. package/dist/discovery/packs.json +7 -5
  74. package/dist/discovery/trust-report.md +3 -3
  75. package/dist/discovery/workspaces.json +6 -4
  76. package/dist/hooks/dispatch.js +2653 -1365
  77. package/dist/install/install.mjs +12 -4
  78. package/dist/mcp/registry-manifest.json +3 -3
  79. package/dist/mcp/server.json +2 -2
  80. package/dist/mcp/server.mjs +2679 -887
  81. package/dist/router.json +1 -1
  82. package/dist/scripts/condense.js +1 -1
  83. package/dist/scripts/condense.js.map +1 -1
  84. package/dist/server/schemas/settings.js +2 -2
  85. package/dist/server/schemas/settings.js.map +1 -1
  86. package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
  87. package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
  88. package/dist/ui/index.html +1 -1
  89. package/docs/guidelines/design-fidelity-mechanics.md +21 -0
  90. package/docs/guidelines/design-handover-extraction.md +87 -0
  91. package/docs/guidelines/prompt-templates.md +24 -1
  92. package/package.json +1 -1
  93. package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  94. package/src/agent-src/scripts/gate_execute.ts +395 -0
  95. package/src/agent-src/scripts/resume_probe.ts +422 -0
  96. package/src/agent-src/scripts/roadmap_gates.ts +138 -4
  97. package/src/agent-src/scripts/update_roadmap_progress.ts +70 -4
  98. package/src/config/ci-local-parity.yml +13 -0
  99. package/src/config/evaluator-budgets.json +9 -8
  100. package/src/config/gate-coverage.yml +140 -12
  101. package/src/config/gate-violation-baselines.json +9 -3
  102. package/src/config/hook-token-budget.json +8 -1
  103. package/src/config/rule-activation-census.json +41 -0
  104. package/src/config/rule-stub-ceilings.json +52 -0
  105. package/src/scripts/_dispatch.bash +4 -0
  106. package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
  107. package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
  108. package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
  109. package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
  110. package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
  111. package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
  112. package/src/scripts/_lib/evaluator_contract.ts +159 -0
  113. package/src/scripts/_lib/gh_transient.ts +80 -0
  114. package/src/scripts/_lib/release_material.ts +16 -5
  115. package/src/scripts/_lib/review_independence.ts +111 -0
  116. package/src/scripts/_lib/session_register.ts +14 -0
  117. package/src/scripts/_lib/skill_catalogue.ts +278 -5
  118. package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
  119. package/src/scripts/_lib/tier_budget_routing.ts +31 -331
  120. package/src/scripts/_lib/turn_end_refusals.ts +482 -0
  121. package/src/scripts/ai_council/_py_json.ts +96 -0
  122. package/src/scripts/ai_council/cli_call_budget.ts +357 -0
  123. package/src/scripts/ai_council/clients.ts +101 -164
  124. package/src/scripts/ai_council/config.ts +25 -11
  125. package/src/scripts/ai_council/probe_store.ts +196 -0
  126. package/src/scripts/ai_council/qualification.ts +425 -0
  127. package/src/scripts/ai_council/qualification_wiring.ts +243 -0
  128. package/src/scripts/ai_council/quorum_wiring.ts +211 -0
  129. package/src/scripts/ai_team/team_dispatch.ts +40 -6
  130. package/src/scripts/bench_ab_v2_complexity.ts +252 -0
  131. package/src/scripts/bench_ab_v2_run.ts +3 -42
  132. package/src/scripts/bench_ab_v2_safety.ts +296 -0
  133. package/src/scripts/bench_ab_v2_search.ts +343 -0
  134. package/src/scripts/bench_ab_v2_stats.ts +191 -0
  135. package/src/scripts/build_archive_index.ts +383 -0
  136. package/src/scripts/capture_skill_catalogue.ts +369 -4
  137. package/src/scripts/check_evaluator_schema.ts +249 -0
  138. package/src/scripts/check_pr_ci_current.ts +409 -0
  139. package/src/scripts/check_review_schema.ts +195 -0
  140. package/src/scripts/check_rule_activation_census.ts +451 -0
  141. package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
  142. package/src/scripts/check_source_size_budget.ts +428 -0
  143. package/src/scripts/check_suppression_hygiene.ts +5 -1
  144. package/src/scripts/condense.ts +1 -1
  145. package/src/scripts/cost/backfill_rates.mjs +298 -0
  146. package/src/scripts/cost/budget.mjs +12 -32
  147. package/src/scripts/cost_summary.ts +25 -0
  148. package/src/scripts/council_cli.ts +141 -191
  149. package/src/scripts/dispatch_r2_reviewer.ts +8 -1
  150. package/src/scripts/external_sources_denylist.json +1 -0
  151. package/src/scripts/hook_manifest.yaml +99 -6
  152. package/src/scripts/hooks/concern_registry.ts +6 -0
  153. package/src/scripts/hooks/dispatch_hook.ts +43 -1
  154. package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
  155. package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
  156. package/src/scripts/hooks/skill_route_hook.ts +226 -0
  157. package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
  158. package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
  159. package/src/scripts/hooks_doctor.ts +112 -0
  160. package/src/scripts/interruption_report.ts +443 -0
  161. package/src/scripts/lint_design_antipattern_parity.ts +40 -6
  162. package/src/scripts/lint_evidence_artifacts.ts +359 -0
  163. package/src/scripts/lint_harvest_provenance.ts +35 -1
  164. package/src/scripts/lint_roadmap_blockers.ts +65 -0
  165. package/src/scripts/lint_roadmap_complexity.ts +7 -1
  166. package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
  167. package/src/scripts/lint_skill_router_head.ts +21 -0
  168. package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
  169. package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
  170. package/src/scripts/mcp_server/tools.ts +87 -0
  171. package/src/scripts/release.ts +43 -58
  172. package/src/scripts/routing_doctor.ts +50 -13
  173. package/src/scripts/rule_activation_census.ts +292 -0
  174. package/src/scripts/schemas/command.schema.json +1 -1
  175. package/src/scripts/schemas/evaluator-output.schema.json +60 -0
  176. package/src/scripts/schemas/review-findings.schema.json +31 -0
  177. package/src/scripts/schemas/skill.schema.json +18 -1
  178. package/src/scripts/self_review_gate.ts +6 -0
  179. package/src/scripts/session_register_hook.ts +30 -1
  180. package/src/scripts/sync_pr_branch.ts +271 -0
  181. package/src/scripts/test_council_qualification.ts +213 -0
  182. package/src/scripts/trigger_coverage.ts +224 -3
  183. package/src/server/schemas/settings.ts +2 -2
  184. package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
  185. package/src/config/budget-routing.json +0 -7
@@ -74,6 +74,21 @@ concerns:
74
74
  # Phase 5 — Tier-1 hook for the minimal-safe-diff rule. Counts unique
75
75
  # files touched per turn into agents/runtime/state/minimal-safe-diff.json and
76
76
  # flips warning=true past hooks.minimal_safe_diff.threshold.
77
+ # road-to-metric-loop-and-review-integrity Phase 4 — shipping-intent gate on
78
+ # diff VOLUME, complementary to `minimal-safe-diff` rather than a duplicate:
79
+ # that concern counts unique files touched PER TURN, this one counts changed
80
+ # lines on the branch AT A SHIP VERB. Threshold 1695 is the corrected p90 of
81
+ # this repository's own last 40 merged PRs, and the exclusion set (review-input
82
+ # snapshots, generated indexes, projection trees) is part of the metric's
83
+ # definition, not a tuning knob — the replay measured the naive metric
84
+ # double-counting the diff it was measuring. Warn-level first; escalation waits
85
+ # for one release of recorded firings. Evidence:
86
+ # agents/evidence/eval-findings/metric-loop-s04.md
87
+ ship-diff-volume:
88
+ script: src/scripts/hooks/ship_diff_volume_hook.ts
89
+ args: []
90
+ fail_closed: false
91
+ severity: advisory
77
92
  minimal-safe-diff:
78
93
  script: src/scripts/minimal_safe_diff_hook.ts
79
94
  args: []
@@ -434,6 +449,28 @@ concerns:
434
449
  args: []
435
450
  fail_closed: false
436
451
  severity: advisory
452
+ # road-to-inbox-harvest-2026-08-d-runtime-skill-routing Phase 2 — binds the
453
+ # deterministic task→skill ranker, which had shipped with ZERO production
454
+ # callers: `score_skill_relevance.rank` and its `suggest_skill_for_task`
455
+ # wrapper were reachable only from their CLIs and from `routing:doctor`.
456
+ # Ranks the SUBMITTED prompt over the installed skill catalogue and injects
457
+ # at most one line naming the top-3 skill ids and their scores — POINTERS,
458
+ # never bodies, never a load instruction. Same delivery as `delegation-nudge`
459
+ # above: `{decision:"warn", reason, additional_context}` at exit 2, reduced
460
+ # to a real exit of 0 by `host_semantics.emitFor`'s warn branch, so it never
461
+ # blocks a turn.
462
+ #
463
+ # Silence is the default and its floor is MEASURED, not chosen: `rank`
464
+ # returns something on nearly every real prompt, so the trigger is a top-1
465
+ # score at or above 30/100 — the p90 of the routing-matrix corpus (496 prompt
466
+ # lines; median 18, p75 28, max 70), firing on 13.9% of them. A median floor
467
+ # would speak on half of all turns, which is the per-turn-reminder shape this
468
+ # estate has already measured failing. fail_closed: false.
469
+ skill-route:
470
+ script: src/scripts/hooks/skill_route_hook.ts
471
+ args: []
472
+ fail_closed: false
473
+ severity: advisory
437
474
  # road-to-orchestrator-discipline-carriers Phase 5 (F4-lite) — end-review
438
475
  # nudge. Fires ONLY when the session mutated tracked+untracked, non-doc
439
476
  # files beyond MUTATION_LINE_THRESHOLD (50 lines; untracked files added
@@ -452,6 +489,16 @@ concerns:
452
489
  args: []
453
490
  fail_closed: false
454
491
  severity: advisory
492
+ # road-to-stop-gate-honesty step 3.1 — skipped on a refusal RETRY.
493
+ # ARGUMENT, per concern as that step requires: the F2 fire gate is a
494
+ # once-per-session marker, and this concern's own header records that a
495
+ # second run "reads the marker and returns silently, without re-running the
496
+ # transcript scan". The retry therefore cannot change its artefact — the
497
+ # telemetry line was already written, or already suppressed, on the first
498
+ # Stop. Idempotence is the concern's own construction, not our assumption
499
+ # about it. The saving is the module load rather than a large read, which is
500
+ # the smaller of the two flags below it and is stated as such.
501
+ skip_on_refusal_retry: true
455
502
  # road-to-conformance-round5 Phase 3 — the FIRST concern that can refuse a
456
503
  # turn-END. Round 5 measured the reason: both blocking carriers reached zero
457
504
  # violations, neither advisory carrier did, and 19 language violations
@@ -535,6 +582,43 @@ concerns:
535
582
  args: []
536
583
  fail_closed: false
537
584
  severity: advisory
585
+ # road-to-user-out-of-the-loop Phase 0 Step 1 — the interruption ledger.
586
+ # Every Stop, classifies whether the turn that just ended put a synchronous
587
+ # contact to the user and appends one `{run_id, turn, kind, class, roadmap}`
588
+ # line to gitignored runtime state (agents/runtime/state/interruptions.jsonl,
589
+ # file shape after gate-metrics.jsonl). Three classes, not two: a hand-back
590
+ # ("das entscheidest Du", "your call") ends the turn and waits for the user
591
+ # exactly as a question does, so counting only `?` would report this
592
+ # package's own preferred shape as zero contacts.
593
+ #
594
+ # CAPTURE ONLY, and that is the point rather than caution: Phase 0's two
595
+ # pre-registered claims are baselines, and a baseline can only be read off a
596
+ # window no mechanism reacted to. It emits nothing, never warns, and always
597
+ # returns 0 — so it carries no src/config/hook-token-budget.json row, the
598
+ # same as subagent-ledger and orchestration-record.
599
+ #
600
+ # Reply-shape detection is IMPORTED from turn-end-gate (readTranscriptTail,
601
+ # finalParagraph, HANDBACK) rather than copied — a second copy would drift
602
+ # from the measured corpus's detector and the drift would land silently in
603
+ # the baseline. Dedupe on the turn ordinal, the same identity that hook
604
+ # derives, because a host may fire Stop twice for one turn. fail_closed: false.
605
+ interruption-ledger:
606
+ script: src/scripts/hooks/interruption_ledger_hook.ts
607
+ args: []
608
+ fail_closed: false
609
+ severity: advisory
610
+ # road-to-stop-gate-honesty step 3.1 — skipped on a refusal RETRY, and this
611
+ # is the flag with the real saving.
612
+ # ARGUMENT, per concern: `alreadyRecorded(ledgerPath, run_id, turn)` dedupes
613
+ # on the TURN ORDINAL, and a refusal retry is the same turn by construction
614
+ # — the gate's layer-2 marker is keyed on that same ordinal. So the ledger
615
+ # already writes nothing on the retry and the artefact is provably identical.
616
+ # What it does NOT already avoid is the cost: `main()` calls
617
+ # `readTranscriptTail` (up to 8 MB, walking the whole file) BEFORE reaching
618
+ # `alreadyRecorded`, so today every refused Stop pays a full transcript walk
619
+ # and then discards the result. That is the "pure duplicate cost" § D-3
620
+ # names, measured in the source rather than assumed.
621
+ skip_on_refusal_retry: true
538
622
  # road-to-subagent-lifecycle-integrity Phase 1 (Steps 2+3) — the subagent
539
623
  # lifecycle ledger. Binds ONLY the two new events; on every other slot it is
540
624
  # not listed at all. Appends `subagent_start` / `subagent_stop` lines to
@@ -639,22 +723,31 @@ roles:
639
723
  # Orchestrator-only concerns (Phase 2.3): a worker never talks to the
640
724
  # user, never dispatches sub-workers, never runs the council, and its
641
725
  # complaints route via the orchestrator.
642
- drop: [delegation-nudge, end-review-nudge, council-availability, team-review-gate, self-repair, session-eol]
726
+ # `skill-route` joins the drop list for the same reason `delegation-nudge`
727
+ # is on it: a worker was handed a bounded slice by an orchestrator that
728
+ # already chose the approach, so a routing pointer there advises nobody who
729
+ # can act on it and costs the dispatch anyway.
730
+ # `interruption-ledger` joins it on the first clause verbatim — "a worker
731
+ # never talks to the user". Its whole measurement is synchronous user
732
+ # contacts, so a worker turn that ends in a question to its orchestrator
733
+ # would be recorded as a contact that never reached a human, inflating the
734
+ # exact baseline Phase 0 pre-registers.
735
+ drop: [delegation-nudge, skill-route, end-review-nudge, council-availability, team-review-gate, self-repair, session-eol, interruption-ledger]
643
736
 
644
737
  platforms:
645
738
  augment:
646
739
  session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
647
740
  session_end: [chat-history, memory-learn, session-register]
648
741
  stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
649
- pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, reread-guard]
742
+ pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, ship-diff-volume, reread-guard]
650
743
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, tool-result-bytes, edit-shape]
651
744
 
652
745
  claude:
653
746
  session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
654
747
  session_end: [chat-history, memory-learn, session-register]
655
- stop: [chat-history, hot-context, verify-before-complete, team-review-gate, end-review-nudge, turn-end-gate, self-repair, session-register, session-eol]
656
- user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, delegation-nudge, git-authorization, session-canary, self-repair, session-register]
657
- pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, reread-guard, spawn-guard-shadow]
748
+ stop: [chat-history, hot-context, verify-before-complete, team-review-gate, end-review-nudge, turn-end-gate, self-repair, session-register, session-eol, interruption-ledger]
749
+ user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, delegation-nudge, skill-route, git-authorization, session-canary, self-repair, session-register]
750
+ pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, ship-diff-volume, reread-guard, spawn-guard-shadow]
658
751
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, tool-result-bytes, edit-shape, language-mirror]
659
752
  # `language-mirror` — round-5 § 6.1. `pre_compact` sets a pin-lost marker;
660
753
  # the `post_tool_use` entry above re-emits the pin exactly once while that
@@ -708,7 +801,7 @@ platforms:
708
801
  # still costs the dispatch, and its entire output is thrown away.
709
802
  stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
710
803
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
711
- pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, reread-guard, spawn-guard-shadow]
804
+ pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, ship-diff-volume, reread-guard, spawn-guard-shadow]
712
805
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, tool-result-bytes, edit-shape]
713
806
  # subagent-ledger IS bound here, unlike the two nudges above, and the
714
807
  # difference is the delivery channel rather than the platform: the nudges
@@ -32,6 +32,7 @@ import { main as onboardingGateMain } from '../onboarding_gate_hook.js';
32
32
  import { main as contextHygieneMain } from '../context_hygiene_hook.js';
33
33
  import { main as verifyBeforeCompleteMain } from '../before_complete_hook.js';
34
34
  import { main as minimalSafeDiffMain } from '../minimal_safe_diff_hook.js';
35
+ import { main as shipDiffVolumeMain } from './ship_diff_volume_hook.js';
35
36
  import { main as injectionScanMain } from '../injection_scan_hook.js';
36
37
  import { main as memoryLearnMain } from '../memory_learn_hook.js';
37
38
  import { main as firstRunGateMain } from '../first_run_gate_hook.js';
@@ -57,6 +58,7 @@ import { main as codeGraphNudgeMain } from './code_graph_nudge_hook.js';
57
58
  import { main as uiRouteNudgeMain } from './ui_route_nudge_hook.js';
58
59
  import { main as orchestrationRecordMain } from './orchestration_record_hook.js';
59
60
  import { main as delegationNudgeMain } from './delegation_nudge_hook.js';
61
+ import { main as skillRouteMain } from './skill_route_hook.js';
60
62
  import { main as endReviewNudgeMain } from './end_review_nudge_hook.js';
61
63
  import { main as turnEndGateMain } from './turn_end_gate_hook.js';
62
64
  import { main as editShapeMain } from './edit_shape_hook.js';
@@ -65,6 +67,7 @@ import { main as sessionEolMain } from './session_eol_hook.js';
65
67
  import { main as subagentLedgerMain } from './subagent_ledger_hook.js';
66
68
  import { main as toolResultBytesMain } from './tool_result_bytes_hook.js';
67
69
  import { main as spawnGuardShadowMain } from './spawn_guard_shadow_hook.js';
70
+ import { main as interruptionLedgerMain } from './interruption_ledger_hook.js';
68
71
 
69
72
  /** A concern `main` — argv-taking or not; both shapes exist. */
70
73
  export type ConcernMain = (argv?: string[]) => number | undefined | void;
@@ -79,6 +82,7 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
79
82
  'src/scripts/context_hygiene_hook.ts': contextHygieneMain as ConcernMain,
80
83
  'src/scripts/before_complete_hook.ts': verifyBeforeCompleteMain as ConcernMain,
81
84
  'src/scripts/minimal_safe_diff_hook.ts': minimalSafeDiffMain as ConcernMain,
85
+ 'src/scripts/hooks/ship_diff_volume_hook.ts': shipDiffVolumeMain as ConcernMain,
82
86
  'src/scripts/injection_scan_hook.ts': injectionScanMain as ConcernMain,
83
87
  'src/scripts/memory_learn_hook.ts': memoryLearnMain as ConcernMain,
84
88
  'src/scripts/first_run_gate_hook.ts': firstRunGateMain as ConcernMain,
@@ -104,6 +108,7 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
104
108
  'src/scripts/hooks/ui_route_nudge_hook.ts': uiRouteNudgeMain as ConcernMain,
105
109
  'src/scripts/hooks/orchestration_record_hook.ts': orchestrationRecordMain as ConcernMain,
106
110
  'src/scripts/hooks/delegation_nudge_hook.ts': delegationNudgeMain as ConcernMain,
111
+ 'src/scripts/hooks/skill_route_hook.ts': skillRouteMain as ConcernMain,
107
112
  'src/scripts/hooks/end_review_nudge_hook.ts': endReviewNudgeMain as ConcernMain,
108
113
  'src/scripts/hooks/turn_end_gate_hook.ts': turnEndGateMain as ConcernMain,
109
114
  'src/scripts/hooks/edit_shape_hook.ts': editShapeMain as ConcernMain,
@@ -112,4 +117,5 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
112
117
  'src/scripts/hooks/subagent_ledger_hook.ts': subagentLedgerMain as ConcernMain,
113
118
  'src/scripts/hooks/tool_result_bytes_hook.ts': toolResultBytesMain as ConcernMain,
114
119
  'src/scripts/hooks/spawn_guard_shadow_hook.ts': spawnGuardShadowMain as ConcernMain,
120
+ 'src/scripts/hooks/interruption_ledger_hook.ts': interruptionLedgerMain as ConcernMain,
115
121
  };
@@ -342,16 +342,51 @@ export function _role_drop_set(
342
342
  return new Set(drop.filter((d): d is string => typeof d === "string"));
343
343
  }
344
344
 
345
+ /**
346
+ * True when this `stop` is the RETRY of a turn a stop-hook already refused.
347
+ *
348
+ * The host sets `stop_hook_active` on exactly that Stop; `turn_end_gate_hook`
349
+ * reads the same field as its layer-1 re-entrancy guard, so this is the host's
350
+ * own answer rather than an inference of ours. Anything unparseable is `false`,
351
+ * which is the full chain — the safe direction, since the cost of running a
352
+ * concern twice is duplicate work and the cost of skipping one wrongly is a lost
353
+ * write.
354
+ */
355
+ export function _is_refusal_retry(event: string, payload_text: string): boolean {
356
+ if (event !== "stop") return false;
357
+ try {
358
+ const parsed: unknown = JSON.parse(payload_text);
359
+ if (!_isObject(parsed)) return false;
360
+ const payload = parsed["payload"];
361
+ const source = _isObject(payload) ? payload : parsed;
362
+ return source["stop_hook_active"] === true;
363
+ } catch {
364
+ return false;
365
+ }
366
+ }
367
+
345
368
  /**
346
369
  * Return the ordered concern definitions for (platform, event) under the
347
370
  * given session role (default `orchestrator` — byte-identical to the
348
371
  * pre-role-axis behaviour).
372
+ *
373
+ * `opts.refusal_retry` drops concerns that opted in with
374
+ * `skip_on_refusal_retry: true` — `road-to-stop-gate-honesty` step 3.1. Stop is
375
+ * the heaviest slot on claude and **every refused Stop runs the whole slot again
376
+ * on the retry** (§ D-3), so the concerns whose second run provably produces the
377
+ * identical artefact are pure duplicate cost.
378
+ *
379
+ * Opt-in per concern, default off, never a blanket skip: Risk 3 of that roadmap
380
+ * is that a concern skipped on the retry might have been the one that needed the
381
+ * second pass, and the loss would be silent. The manifest carries the per-concern
382
+ * argument beside each flag.
349
383
  */
350
384
  export function _resolve_concerns(
351
385
  manifest: JsonObject,
352
386
  platform: string,
353
387
  event: string,
354
388
  role: SessionRole = "orchestrator",
389
+ opts: { refusal_retry?: boolean } = {},
355
390
  ): ConcernDef[] {
356
391
  const platformsRaw = manifest["platforms"];
357
392
  const platforms = _isObject(platformsRaw) ? platformsRaw : {};
@@ -381,6 +416,9 @@ export function _resolve_concerns(
381
416
  );
382
417
  continue;
383
418
  }
419
+ if (opts.refusal_retry && spec["skip_on_refusal_retry"] === true) {
420
+ continue;
421
+ }
384
422
  out.push({ name: String(name), ...spec });
385
423
  }
386
424
  return out;
@@ -1081,13 +1119,17 @@ export function main(argv?: string[]): number {
1081
1119
  const payload_text = process.stdin.isTTY ? "" : _readStdin();
1082
1120
  _maybe_capture_payload(args, payload_text);
1083
1121
  const session_role = resolveSessionRole(process.env);
1084
- const concerns = _resolve_concerns(manifest, args.platform, args.event, session_role);
1122
+ const refusal_retry = _is_refusal_retry(args.event, payload_text);
1123
+ const concerns = _resolve_concerns(manifest, args.platform, args.event, session_role, {
1124
+ refusal_retry,
1125
+ });
1085
1126
 
1086
1127
  if (args.dry_run) {
1087
1128
  const plan = {
1088
1129
  platform: args.platform,
1089
1130
  event: args.event,
1090
1131
  role: session_role,
1132
+ refusal_retry,
1091
1133
  concerns: concerns.map((c) => c["name"]),
1092
1134
  };
1093
1135
  process.stdout.write(_py_json_dumps(plan, 2) + "\n");
@@ -0,0 +1,256 @@
1
+ /**
2
+ * interruption_ledger — road-to-user-out-of-the-loop Phase 0 Step 1.
3
+ *
4
+ * CAPTURE ONLY. Every `stop`, classifies whether the turn that just ended put a
5
+ * synchronous contact to the user, and appends one line to
6
+ * `agents/runtime/state/interruptions.jsonl`. It never emits context, never
7
+ * warns, and always returns 0 — the whole point of Phase 0 is a window that no
8
+ * mechanism reacted to, so a ledger that could change a run would corrupt the
9
+ * baseline it exists to measure.
10
+ *
11
+ * WHY THIS EXISTS. The roadmap's Goal names two axes — synchronous contacts per
12
+ * delivered roadmap, and wall-clock to open PR — and pre-registers both as
13
+ * claims. Neither is derivable today: nothing in the tree records that a turn
14
+ * ended by asking the user something. `check_enforcement_coverage` counts
15
+ * carriers, `gate-metrics.jsonl` counts gate verdicts, and the conformance suite
16
+ * scores reply SHAPE without ever asking whether the reply stopped the run.
17
+ *
18
+ * REUSE, NOT REIMPLEMENTATION. Reply-shape detection is `turn_end_gate_hook`'s,
19
+ * imported rather than copied: `readTranscriptTail` for the last assistant text,
20
+ * `finalParagraph` for the closing paragraph, and its `HANDBACK` list for the
21
+ * yields-without-asking shape. That file is the measured corpus's detector and
22
+ * has an R2 history behind each regex; a second copy here would drift from it
23
+ * silently and the drift would land in the baseline, not in a test.
24
+ *
25
+ * WHAT COUNTS AS A CONTACT, and why it is three classes rather than two. A
26
+ * question mark is the obvious one. It is not the only one: `scope-control`
27
+ * requires a hand-back — "das entscheidest Du", "your call" — which ends the
28
+ * turn and waits for the user just as hard as a question does, with no `?`
29
+ * anywhere. Counting only questions would report the package's own preferred
30
+ * hand-back shape as zero contacts, which is precisely the metric-that-flatters-
31
+ * the-design failure the roadmap's Risk 6 names.
32
+ *
33
+ * ask — the closing paragraph ends in a question mark.
34
+ * handback — no question, but the closing paragraph yields the decision.
35
+ * none — the turn ended without putting anything to the user.
36
+ *
37
+ * `class` refines an `ask` by the shape the user has to answer in:
38
+ * `numbered-options` when the reply carries a numbered block (the
39
+ * `user-interaction` shape — one decision point, cheap to answer), or
40
+ * `open-question` when it does not (free text, the expensive shape). The split
41
+ * exists because the two are not equal contacts and the report separates them.
42
+ *
43
+ * DEDUPE is on the turn ordinal, the same identity `turn_end_gate_hook` derives
44
+ * and for the same reason: a host may fire `stop` more than once for one turn,
45
+ * and a per-turn ledger that double-counts inflates exactly the number the
46
+ * baseline is pre-registered on.
47
+ *
48
+ * The roadmap slug comes from this session's own claim file — the one
49
+ * `agent-config sessions:claim` writes — so a contact is attributable to a
50
+ * delivered roadmap without this hook knowing anything about roadmaps. No claim
51
+ * means `roadmap: null`, which is a real and common state (any session that is
52
+ * not a roadmap run) and never a reason to skip the line.
53
+ */
54
+
55
+ import * as fs from 'node:fs';
56
+ import * as path from 'node:path';
57
+ import { fileURLToPath, pathToFileURL } from 'node:url';
58
+
59
+ import { derive_session_tag } from '../chat_history.js';
60
+ import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
61
+ import { readHookStdin } from './hook_stdin.js';
62
+ import { HANDBACK, finalParagraph, readTranscriptTail } from './turn_end_gate_hook.js';
63
+
64
+ const EXIT_OK = 0;
65
+
66
+ /** Same cap the turn-end gate reads its tail under. */
67
+ const TRANSCRIPT_READ_MAX_BYTES = 2 * 1024 * 1024;
68
+
69
+ export const LEDGER_RELPATH = path.join('agents', 'runtime', 'state', 'interruptions.jsonl');
70
+
71
+ export type ContactKind = 'ask' | 'handback' | 'none';
72
+ export type ContactClass = 'numbered-options' | 'open-question' | 'handback' | 'none';
73
+
74
+ export interface InterruptionRecord {
75
+ run_id: string;
76
+ turn: number;
77
+ kind: ContactKind;
78
+ class: ContactClass;
79
+ roadmap: string | null;
80
+ at: string;
81
+ }
82
+
83
+ /**
84
+ * A numbered-options block: two or more numbered lines. One numbered line is an
85
+ * ordinary enumeration inside prose and is not a decision surface — requiring
86
+ * two is what keeps "1. read the file" from reading as an ask.
87
+ */
88
+ export function hasNumberedOptions(reply: string): boolean {
89
+ let count = 0;
90
+ for (const line of reply.split('\n')) {
91
+ if (/^[ \t]{0,3}\d+[.)]\s+\S/.test(line)) count += 1;
92
+ if (count >= 2) return true;
93
+ }
94
+ return false;
95
+ }
96
+
97
+ /**
98
+ * Classify the reply. The question test is `turn_end_gate_hook`'s exactly — a
99
+ * question that ENDS the paragraph, never `includes('?')`, because a rhetorical
100
+ * or quoted question mid-reply is not a contact and counting it would inflate
101
+ * the baseline in the direction that makes the roadmap look successful later.
102
+ */
103
+ export function classifyReply(reply: string): { kind: ContactKind; class: ContactClass } {
104
+ const tail = finalParagraph(reply);
105
+ if (!tail) return { kind: 'none', class: 'none' };
106
+
107
+ if (/\?["'’)\]]*\s*$/.test(tail)) {
108
+ return {
109
+ kind: 'ask',
110
+ class: hasNumberedOptions(reply) ? 'numbered-options' : 'open-question',
111
+ };
112
+ }
113
+ if (HANDBACK.some((re) => re.test(tail))) return { kind: 'handback', class: 'handback' };
114
+ return { kind: 'none', class: 'none' };
115
+ }
116
+
117
+ /** The roadmap this session claimed, or null. Never throws. */
118
+ export function readClaimedRoadmap(workspaceRoot: string, sessionId: string): string | null {
119
+ if (!sessionId) return null;
120
+ try {
121
+ const file = path.join(
122
+ workspaceRoot,
123
+ 'agents',
124
+ 'runtime',
125
+ 'state',
126
+ `roadmap-claim-${sessionId}.json`,
127
+ );
128
+ const parsed: unknown = JSON.parse(fs.readFileSync(file, 'utf8'));
129
+ if (parsed === null || typeof parsed !== 'object') return null;
130
+ const slug = (parsed as Record<string, unknown>)['slug'];
131
+ return typeof slug === 'string' && slug.length > 0 ? slug : null;
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * True when this run has already recorded this turn. Reads only the tail of the
139
+ * ledger: the duplicate a repeated `stop` produces is always the most recent
140
+ * line, so scanning the whole file would cost more on every turn to catch
141
+ * nothing extra.
142
+ */
143
+ export function alreadyRecorded(ledgerPath: string, runId: string, turn: number): boolean {
144
+ let raw: string;
145
+ try {
146
+ raw = fs.readFileSync(ledgerPath, 'utf8');
147
+ } catch {
148
+ return false;
149
+ }
150
+ const lines = raw.split('\n').filter((l) => l.trim().length > 0);
151
+ for (const line of lines.slice(-20)) {
152
+ try {
153
+ const rec: unknown = JSON.parse(line);
154
+ if (rec === null || typeof rec !== 'object') continue;
155
+ const r = rec as Record<string, unknown>;
156
+ if (r['run_id'] === runId && r['turn'] === turn) return true;
157
+ } catch {
158
+ // A corrupt line is not a reason to skip the write.
159
+ }
160
+ }
161
+ return false;
162
+ }
163
+
164
+ function str(v: JsonValue | undefined): string {
165
+ return typeof v === 'string' ? v : '';
166
+ }
167
+
168
+ export function main(): number {
169
+ let envelope: JsonObject;
170
+ let payload: JsonObject;
171
+ try {
172
+ [envelope, payload] = unwrap(readHookStdin(), 'claude');
173
+ } catch {
174
+ return EXIT_OK;
175
+ }
176
+
177
+ const workspaceRoot = str(envelope['workspace_root'] as JsonValue | undefined) || process.cwd();
178
+ const transcriptPath = str(
179
+ (payload['transcript_path'] ?? payload['transcriptPath']) as JsonValue | undefined,
180
+ );
181
+ if (!transcriptPath) return EXIT_OK;
182
+
183
+ let lastAssistant = '';
184
+ let turnOrdinal = 0;
185
+ try {
186
+ const tail = readTranscriptTail(transcriptPath, { maxBytes: TRANSCRIPT_READ_MAX_BYTES });
187
+ lastAssistant = tail.lastAssistant;
188
+ turnOrdinal = tail.turnOrdinal;
189
+ } catch {
190
+ return EXIT_OK;
191
+ }
192
+ if (!lastAssistant) return EXIT_OK;
193
+
194
+ const sessionId =
195
+ str(payload['session_id'] as JsonValue | undefined) ||
196
+ str(envelope['session_id'] as JsonValue | undefined);
197
+ // `derive_session_tag`, NOT the turn-end gate's `deriveSessionKey`. Both
198
+ // hash the session id, and only this one produces the tag `chat_history`
199
+ // already writes as `s`. The wall-clock axis is a join between this ledger
200
+ // and those timestamps, so a 32-char key from the other derivation would
201
+ // have made the roadmap's second pre-registered claim underivable — while
202
+ // looking correct in every test that never crossed the two files.
203
+ const runId = sessionId ? derive_session_tag(sessionId) : 'unknown';
204
+
205
+ const ledgerPath = path.join(workspaceRoot, LEDGER_RELPATH);
206
+ if (alreadyRecorded(ledgerPath, runId, turnOrdinal)) return EXIT_OK;
207
+
208
+ const verdict = classifyReply(lastAssistant);
209
+ const record: InterruptionRecord = {
210
+ run_id: runId,
211
+ turn: turnOrdinal,
212
+ kind: verdict.kind,
213
+ class: verdict.class,
214
+ roadmap: readClaimedRoadmap(workspaceRoot, sessionId),
215
+ at: new Date().toISOString(),
216
+ };
217
+
218
+ try {
219
+ fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });
220
+ fs.appendFileSync(ledgerPath, `${JSON.stringify(record)}\n`, 'utf8');
221
+ } catch {
222
+ // A ledger that cannot be written is a lost measurement, never a
223
+ // reason to affect the turn. Phase 0 is advisory by construction.
224
+ }
225
+ return EXIT_OK;
226
+ }
227
+
228
+ /**
229
+ * The concern runs in-process via `CONCERN_REGISTRY`, but the manifest's
230
+ * `script:` path is also the SPAWN FALLBACK — and without this guard that path
231
+ * imports the module, calls nothing, and exits 0. A silent no-op on a
232
+ * capture-only concern is invisible by construction: the ledger simply stays
233
+ * empty and the baseline reads as "no contacts". Caught by running the hook
234
+ * against a real envelope rather than by any unit test, which is why the probe
235
+ * is worth the two minutes.
236
+ */
237
+ function _isCliEntry(): boolean {
238
+ if (process.argv[1] === undefined) {
239
+ return false;
240
+ }
241
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
242
+ if (import.meta.url === argvUrl) {
243
+ return true;
244
+ }
245
+ try {
246
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
247
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
248
+ return here === argv;
249
+ } catch {
250
+ return false;
251
+ }
252
+ }
253
+
254
+ if (_isCliEntry()) {
255
+ process.exit(main());
256
+ }