@event4u/agent-config 16.1.0 → 16.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 (217) 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 +237 -248
  5. package/dist/agent-src/commands/roadmap/process-full.md +1 -1
  6. package/dist/agent-src/commands/work.md +4 -0
  7. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +4 -8
  8. package/dist/agent-src/rules/ui-audit-gate.md +1 -2
  9. package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
  10. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
  11. package/dist/agent-src/skills/design-review/SKILL.md +30 -4
  12. package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
  13. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +16 -1
  14. package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
  15. package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
  16. package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
  17. package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
  18. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  19. package/dist/agent-src/templates/roadmaps.md +30 -0
  20. package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  21. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  22. package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  23. package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  24. package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
  25. package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  26. package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  27. package/dist/cli/python/knowledge_ingest.js +1 -1
  28. package/dist/cli/python/workspace_drive.js +1 -1
  29. package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
  30. package/dist/cli-delegate/{chunk-HDVVGPQW.js → chunk-7XGBX6ZH.js} +471 -387
  31. package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-FP36SWJD.js} +432 -144
  32. package/dist/cli-delegate/{chunk-UNCRJBT3.js → chunk-IGD6ERX6.js} +31 -0
  33. package/dist/cli-delegate/{chunk-FXYUYU4N.js → chunk-JNMVLWLR.js} +42 -1
  34. package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
  35. package/dist/cli-delegate/cmd_conformance.js +902 -782
  36. package/dist/cli-delegate/cmd_doctor.js +2 -2
  37. package/dist/cli-delegate/cmd_export.js +2 -2
  38. package/dist/cli-delegate/cmd_handoff.js +4 -4
  39. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  40. package/dist/cli-delegate/cmd_prune.js +2 -2
  41. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  42. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  43. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  44. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  45. package/dist/cli-delegate/cmd_sync.js +2 -2
  46. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  47. package/dist/cli-delegate/cmd_validate.js +2 -2
  48. package/dist/discovery/deprecation-report.md +1 -1
  49. package/dist/discovery/discovery-manifest.json +10 -10
  50. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  51. package/dist/discovery/discovery-manifest.summary.md +1 -1
  52. package/dist/discovery/dormancy-report.md +1 -1
  53. package/dist/discovery/orphan-report.md +1 -1
  54. package/dist/discovery/packs.json +2 -2
  55. package/dist/discovery/trust-report.md +1 -1
  56. package/dist/discovery/workspaces.json +2 -2
  57. package/dist/hooks/dispatch.js +6666 -2581
  58. package/dist/hooks/dispatch.sha256 +1 -0
  59. package/dist/install/install.mjs +848 -733
  60. package/dist/mcp/registry-manifest.json +2 -2
  61. package/dist/mcp/server.json +2 -2
  62. package/dist/mcp/server.mjs +2747 -1510
  63. package/dist/server/schemas/settings.js +23 -0
  64. package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
  65. package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
  66. package/dist/ui/index.html +2 -2
  67. package/docs/contracts/settings-classes.md +6 -4
  68. package/package.json +4 -4
  69. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  70. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  71. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  72. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  73. package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
  74. package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  75. package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  76. package/src/config/agent-settings.template.yml +29 -0
  77. package/src/config/ci-local-parity.yml +18 -0
  78. package/src/config/continuity-surface.json +2 -2
  79. package/src/config/gate-coverage.yml +234 -3
  80. package/src/config/gate-violation-baselines.json +7 -3
  81. package/src/config/hook-latency-budget.json +21 -0
  82. package/src/config/hook-token-budget.json +2 -2
  83. package/src/config/metric-registry.yml +114 -0
  84. package/src/config/placeholder-drift-budget.json +10 -0
  85. package/src/config/routing-coverage-seed.json +18 -4
  86. package/src/scripts/_cli/cmd_doctor.ts +5 -19
  87. package/src/scripts/_cli/doctor_execution.ts +100 -17
  88. package/src/scripts/_cli/doctor_network_posture.ts +226 -0
  89. package/src/scripts/_lib/body_portable.ts +102 -0
  90. package/src/scripts/_lib/cc_transcript.ts +132 -1
  91. package/src/scripts/_lib/concern_sla_window.ts +200 -0
  92. package/src/scripts/_lib/forge_protection.ts +114 -0
  93. package/src/scripts/_lib/forge_reader.ts +558 -0
  94. package/src/scripts/_lib/host_env_write.ts +111 -0
  95. package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
  96. package/src/scripts/_lib/obligations.ts +119 -8
  97. package/src/scripts/_lib/probe_inputs.ts +271 -0
  98. package/src/scripts/_lib/read_surface_scan.ts +322 -0
  99. package/src/scripts/_lib/release_highlights.ts +2 -1
  100. package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
  101. package/src/scripts/_lib/shell_write_shapes.ts +108 -0
  102. package/src/scripts/_lib/structural_hiding.ts +622 -0
  103. package/src/scripts/_lib/turn_end_refusals.ts +525 -4
  104. package/src/scripts/_lib/turn_end_transcript.ts +216 -0
  105. package/src/scripts/_lib/verification_command.ts +324 -0
  106. package/src/scripts/_lib/verification_evidence.ts +647 -0
  107. package/src/scripts/_lib/verify_clause.ts +280 -0
  108. package/src/scripts/ai_team/team_dispatch.ts +4 -0
  109. package/src/scripts/before_complete_hook.ts +340 -23
  110. package/src/scripts/bench_hook_latency.ts +412 -5
  111. package/src/scripts/cache_realization_report.ts +61 -1
  112. package/src/scripts/check_argument_hint.ts +287 -0
  113. package/src/scripts/check_claims.ts +125 -1
  114. package/src/scripts/check_enforcement_coverage.ts +38 -9
  115. package/src/scripts/check_enforcement_matrix.ts +245 -3
  116. package/src/scripts/check_finding_dispositions.ts +22 -3
  117. package/src/scripts/check_generator_sync.ts +71 -0
  118. package/src/scripts/check_host_docs_digest.ts +786 -0
  119. package/src/scripts/check_host_format_column.ts +559 -0
  120. package/src/scripts/check_input_references.ts +228 -0
  121. package/src/scripts/check_inputs_parity.ts +235 -0
  122. package/src/scripts/check_kernel_edit_ratified.ts +47 -5
  123. package/src/scripts/check_kill_switch_table.ts +294 -0
  124. package/src/scripts/check_placeholder_drift.ts +221 -0
  125. package/src/scripts/check_read_surface_coverage.ts +515 -0
  126. package/src/scripts/check_release_holds.ts +2 -1
  127. package/src/scripts/check_routing_coverage.ts +378 -11
  128. package/src/scripts/check_static_parity.ts +17 -0
  129. package/src/scripts/ci_settle.ts +145 -19
  130. package/src/scripts/closure_scan.ts +115 -1
  131. package/src/scripts/compile_hook_manifest.ts +60 -16
  132. package/src/scripts/external_sources_denylist.json +29 -4
  133. package/src/scripts/hook_manifest.json +1 -1
  134. package/src/scripts/hook_manifest.yaml +42 -5
  135. package/src/scripts/hooks/block_config_weakening.ts +229 -4
  136. package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
  137. package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
  138. package/src/scripts/hooks/bundle_integrity.ts +291 -0
  139. package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
  140. package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
  141. package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
  142. package/src/scripts/hooks/concern_registry.ts +2 -0
  143. package/src/scripts/hooks/concern_timings.ts +94 -0
  144. package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
  145. package/src/scripts/hooks/design_pass_hook.ts +100 -9
  146. package/src/scripts/hooks/design_slop_hook.ts +1 -2
  147. package/src/scripts/hooks/dispatch_hook.ts +25 -19
  148. package/src/scripts/hooks/edit_shape_hook.ts +1 -2
  149. package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
  150. package/src/scripts/hooks/evidence_independence.ts +1 -2
  151. package/src/scripts/hooks/exit_codes.ts +128 -0
  152. package/src/scripts/hooks/host_lowering.json +1 -0
  153. package/src/scripts/hooks/host_lowering.ts +128 -3
  154. package/src/scripts/hooks/host_lowering.yaml +262 -46
  155. package/src/scripts/hooks/host_semantics.ts +30 -2
  156. package/src/scripts/hooks/injection_budget.ts +9 -2
  157. package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
  158. package/src/scripts/hooks/journal_record_hook.ts +1 -2
  159. package/src/scripts/hooks/memory_recall_hook.ts +1 -2
  160. package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
  161. package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
  162. package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
  163. package/src/scripts/hooks/permission_gate.ts +1 -2
  164. package/src/scripts/hooks/push_settle_hook.ts +1 -3
  165. package/src/scripts/hooks/replay_hook.ts +2 -1
  166. package/src/scripts/hooks/reread_guard_hook.ts +1 -2
  167. package/src/scripts/hooks/review_baseline_hook.ts +1 -2
  168. package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
  169. package/src/scripts/hooks/rule_inject_hook.ts +8 -5
  170. package/src/scripts/hooks/run_continuation_hook.ts +13 -15
  171. package/src/scripts/hooks/skill_route_hook.ts +1 -2
  172. package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
  173. package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
  174. package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
  175. package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
  176. package/src/scripts/hooks/table_fingerprint.ts +49 -0
  177. package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
  178. package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
  179. package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
  180. package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
  181. package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
  182. package/src/scripts/install.ts +3 -4
  183. package/src/scripts/lint_deny_text.ts +419 -0
  184. package/src/scripts/lint_exit_codes.ts +262 -0
  185. package/src/scripts/lint_hook_manifest.ts +55 -0
  186. package/src/scripts/lint_roadmap_blockers.ts +78 -12
  187. package/src/scripts/lint_skill_descriptions.ts +1 -1
  188. package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
  189. package/src/scripts/mcp_server/prompts.ts +142 -1
  190. package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
  191. package/src/scripts/measure_turn_end_gate.ts +193 -0
  192. package/src/scripts/memory_lookup.ts +1 -1
  193. package/src/scripts/prepack-check.mjs +53 -0
  194. package/src/scripts/release_publication.ts +13 -1
  195. package/src/scripts/report_invocation_surface.ts +383 -0
  196. package/src/scripts/report_obligation_carriers.ts +699 -0
  197. package/src/scripts/report_obligation_writer_split.ts +236 -0
  198. package/src/scripts/report_skill_menu_census.ts +208 -0
  199. package/src/scripts/report_standing_payload_by_host.ts +1 -1
  200. package/src/scripts/roadmap_verify_share.ts +213 -0
  201. package/src/scripts/schemas/command.schema.json +44 -0
  202. package/src/scripts/schemas/review-findings.schema.json +4 -0
  203. package/src/scripts/schemas/skill.schema.json +44 -0
  204. package/src/scripts/self_review_gate.ts +359 -10
  205. package/src/scripts/semantic_parity_census.ts +314 -0
  206. package/src/scripts/session_eol_report.ts +54 -1
  207. package/src/scripts/sweep_dead_scan_roots.ts +1 -1
  208. package/src/scripts/trigger_eval_grandfather.json +0 -2
  209. package/src/scripts/trigger_eval_rotation.ts +122 -27
  210. package/src/scripts/ui_conformance_probe.ts +156 -4
  211. package/src/scripts/update_prices.ts +85 -3
  212. package/src/scripts/write_bundle_digest.mjs +74 -0
  213. package/src/server/schemas/settings.ts +31 -0
  214. package/src/templates/consumer-settings/ONBOARDING.md +1 -0
  215. package/dist/ui/assets/index-DH1xc62v.js +0 -29
  216. package/dist/ui/assets/index-jSfMW8_t.css +0 -1
  217. /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
@@ -60,8 +60,7 @@ import { derive_session_tag } from '../chat_history.js';
60
60
  import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
61
61
  import { readHookStdin } from './hook_stdin.js';
62
62
  import { HANDBACK, finalParagraph, readTranscriptTail } from './turn_end_gate_hook.js';
63
-
64
- const EXIT_OK = 0;
63
+ import { EXIT_ALLOW as EXIT_OK } from './exit_codes.js';
65
64
 
66
65
  /** Same cap the turn-end gate reads its tail under. */
67
66
  const TRANSCRIPT_READ_MAX_BYTES = 2 * 1024 * 1024;
@@ -102,8 +102,7 @@ import {
102
102
  } from '../_lib/runtime_journal.js';
103
103
  import { readHookStdin } from './hook_stdin.js';
104
104
  import { is_replay_mode } from './state_io.js';
105
-
106
- const EXIT_ALLOW = 0;
105
+ import { EXIT_ALLOW } from './exit_codes.js';
107
106
 
108
107
  /** The `.agent-settings.yml` section under `hooks:` that arms this concern. */
109
108
  export const SETTINGS_SECTION = 'runtime_journal';
@@ -68,13 +68,12 @@ import { COMMAND_TOOLS } from '../before_complete_hook.js';
68
68
  import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
69
69
  import { readHookStdin } from './hook_stdin.js';
70
70
  import { atomic_write_json } from './state_io.js';
71
+ import { EXIT_ALLOW, EXIT_WARN } from './exit_codes.js';
71
72
 
72
- const EXIT_ALLOW = 0;
73
73
  /**
74
74
  * Severity comes from the exit code, not from the `decision` field. See the
75
75
  * header: this never blocks, and never returns 1 on any path.
76
76
  */
77
- const EXIT_WARN = 2;
78
77
 
79
78
  /** Hard ceiling on `reason` + `additional_context`, matching the registered row. */
80
79
  export const CAP_BYTES = 768;
@@ -41,13 +41,19 @@
41
41
  import path from 'node:path';
42
42
  import process from 'node:process';
43
43
 
44
+ import { readHookStdin } from './hook_stdin.js';
45
+ // The WRITER's own root resolver, imported rather than re-derived. A second
46
+ // spelling of "which tree is this" is how a reader comes to address a ledger
47
+ // the writer never wrote — which is the defect this whole file is repairing,
48
+ // one field over.
49
+ import { workspaceRoot as writerWorkspaceRoot } from './rule_inject_hook.js';
50
+
44
51
  import { may_refuse_on } from '../_lib/obligation_frequency.js';
45
52
  import { appendShadow, readDelivered, readDischarged, stamp } from '../_lib/obligations.js';
46
53
  import { loadRouter, matchTierRules } from '../_lib/rule_injection.js';
47
54
  import { gitNumstatRows, isDocPath, untrackedNonDocFiles } from './end_review_nudge_hook.js';
48
55
  import { openRecordStats } from './subagent_ledger_hook.js';
49
-
50
- const EXIT_ALLOW = 0;
56
+ import { EXIT_ALLOW } from './exit_codes.js';
51
57
 
52
58
  /**
53
59
  * Every non-doc path this turn wrote, tracked or not.
@@ -142,9 +148,144 @@ export function shouldContinue(attempt: number): boolean {
142
148
  return attempt <= 1;
143
149
  }
144
150
 
145
- export function main(): number {
146
- const root = process.cwd();
147
- const session = (process.env['CLAUDE_CODE_SESSION_ID'] ?? '').trim();
151
+ /**
152
+ * The session and root this reading is about, resolved the way the WRITER does.
153
+ *
154
+ * THE DEFECT THIS EXISTS TO CLOSE. This hook used to read
155
+ * `CLAUDE_CODE_SESSION_ID` out of the process environment and return allow when
156
+ * it was empty. The dispatcher sets no such variable — it hands each concern
157
+ * the envelope on stdin and `AGENT_CONFIG_PACKAGE_ROOT` in the environment,
158
+ * nothing else — so on every dispatched stop event the reader looked for a
159
+ * ledger under a key the writer never used. Measured 2026-09-29 on this
160
+ * machine's own ledgers: 187 delivered rows, 0 shadow rows, across 8 sessions.
161
+ *
162
+ * WHY THE ORDER IS ENVELOPE-THEN-ENVIRONMENT AND NOT THE REVERSE. The envelope
163
+ * id is the key `rule_inject_hook.ts` wrote the delivered rows under, so it is
164
+ * not merely the better source — it is the ONLY one that can find them. The
165
+ * environment fallback survives for a raw invocation that carries no envelope
166
+ * at all (a maintainer piping nothing, a host shim that skips the dispatcher),
167
+ * which is a case where guessing wrong costs a missing reading rather than a
168
+ * wrong one.
169
+ *
170
+ * THE ROOT IS PART OF THE SAME JOIN, not a second change riding along. A ledger
171
+ * is addressed by root AND session; reading the session from the envelope while
172
+ * taking the root from `process.cwd()` would leave the pair half-joined, and the
173
+ * two disagree exactly on the hosts whose shim does not chdir. `cwd` stays the
174
+ * fallback because that is what the dispatcher sets it to.
175
+ *
176
+ * Both spellings of each key are accepted because the injector accepts both.
177
+ */
178
+ export function resolveSettleContext(
179
+ envelope: Record<string, unknown>,
180
+ payload: Record<string, unknown>,
181
+ env: Record<string, string | undefined>,
182
+ ): { root: string; session: string } {
183
+ const str = (...values: unknown[]): string => {
184
+ for (const v of values) {
185
+ if (typeof v === 'string' && v.trim() !== '') return v.trim();
186
+ }
187
+ return '';
188
+ };
189
+ return {
190
+ // `workspaceRoot` is the INJECTOR's function, not a copy of it. It reads
191
+ // `workspace` / `cwd` / `project_dir`; this file used to read
192
+ // `workspace_root` / `workspace` / `cwd`, so the two key sets overlapped
193
+ // without matching and the pair was joined on the session only. It
194
+ // happened to agree on both real envelope shapes — the dispatcher sets
195
+ // `workspace_root` and chdirs concerns to it, a native wiring passes
196
+ // `cwd` — which made the asymmetry a property of today's envelopes
197
+ // rather than of the code. `workspace_root` is appended ahead of the
198
+ // shared resolver because the dispatcher is the only producer that sets
199
+ // it and nothing else reads it.
200
+ root:
201
+ str(envelope['workspace_root'])
202
+ || writerWorkspaceRoot(envelope as Parameters<typeof writerWorkspaceRoot>[0]),
203
+ session: str(
204
+ envelope['session_id'],
205
+ envelope['sessionId'],
206
+ payload['session_id'],
207
+ payload['sessionId'],
208
+ // The one pre-existing source, kept as the last resort and NOT
209
+ // joined by siblings: adding a name the writer never keys on
210
+ // would address a ledger that cannot exist.
211
+ env['CLAUDE_CODE_SESSION_ID'],
212
+ ),
213
+ };
214
+ }
215
+
216
+ /**
217
+ * The session id the WRITER would have used, for the ledger nobody reads.
218
+ *
219
+ * `rule_inject_hook.ts` defaults an unnamed session to the literal `unknown`
220
+ * and writes delivered rows there; this reader used to return `''` and allow, so
221
+ * that population was invisible to every reading of the bar. Reported rather
222
+ * than silently joined: a shared `unknown` bucket aggregates unrelated sessions,
223
+ * so reading it as one session's turn would be worse than not reading it. The
224
+ * constant is exported so a future census can count what is sitting there.
225
+ */
226
+ export const WRITER_UNNAMED_SESSION = 'unknown';
227
+
228
+ /** The envelope on stdin, or an empty object when there is none to read. */
229
+ function readEnvelope(): { envelope: Record<string, unknown>; payload: Record<string, unknown> } {
230
+ let envelope: Record<string, unknown> = {};
231
+ try {
232
+ const raw = readHookStdin();
233
+ const parsed: unknown = raw.trim() === '' ? {} : JSON.parse(raw);
234
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
235
+ envelope = parsed as Record<string, unknown>;
236
+ }
237
+ } catch {
238
+ // A malformed envelope is not a reason to refuse, and this concern
239
+ // refuses nothing anyway. It is a reason to read nothing from it.
240
+ }
241
+ const nested = envelope['payload'];
242
+ const payload =
243
+ typeof nested === 'object' && nested !== null && !Array.isArray(nested)
244
+ ? (nested as Record<string, unknown>)
245
+ : envelope;
246
+ return { envelope, payload };
247
+ }
248
+
249
+ /**
250
+ * The resolver as it stood BEFORE the join fix, kept reachable for one test.
251
+ *
252
+ * A reproduction that cannot run the old code is not a reproduction — it is an
253
+ * assertion about the new function's behavior on inputs nobody sends, and
254
+ * `tests/hooks/obligation_settle.test.ts` carried exactly that until an
255
+ * independent review named it. This function is dead on every production path
256
+ * (nothing but the fixture calls it) and exists so the defect stays executable:
257
+ * environment-only resolution, allow when empty, root from the cwd.
258
+ */
259
+ export function resolveSettleContextPreFix(
260
+ _envelope: Record<string, unknown>,
261
+ _payload: Record<string, unknown>,
262
+ env: Record<string, string | undefined>,
263
+ ): { root: string; session: string } {
264
+ // Same arity as the live resolver ON PURPOSE. A narrower signature would let
265
+ // the fixture pass the envelope in as `env`, where the lookup misses for the
266
+ // wrong reason and the test goes green without exercising the defect.
267
+ return { root: process.cwd(), session: (env['CLAUDE_CODE_SESSION_ID'] ?? '').trim() };
268
+ }
269
+
270
+ /**
271
+ * Which context resolver this reading uses — the seam the pre-fix fixture needs.
272
+ *
273
+ * It is NOT a parameter of `main()`, and that is a contract rather than a
274
+ * preference: `concern_main_signature` requires every concern's `main()` to take
275
+ * argv first or nothing, because the dispatcher calls `main(argv)` — so a
276
+ * resolver in first position would receive an argv array at runtime and resolve
277
+ * a session from it. Caught by that test, which is the reason this indirection
278
+ * exists instead of the obvious default parameter.
279
+ */
280
+ export type SettleResolver = (
281
+ envelope: Record<string, unknown>,
282
+ payload: Record<string, unknown>,
283
+ env: Record<string, string | undefined>,
284
+ ) => { root: string; session: string };
285
+
286
+ export function runSettle(resolve: SettleResolver = resolveSettleContext): number {
287
+ const { envelope, payload } = readEnvelope();
288
+ const { root, session } = resolve(envelope, payload, process.env);
148
289
  if (session === '') return EXIT_ALLOW;
149
290
 
150
291
  let verdict: SettleVerdict;
@@ -176,6 +317,11 @@ export function main(): number {
176
317
  return EXIT_ALLOW;
177
318
  }
178
319
 
320
+ /** The dispatcher entry point. Argv-shaped, per `concern_main_signature`. */
321
+ export function main(_argv: readonly string[] = process.argv.slice(2)): number {
322
+ return runSettle();
323
+ }
324
+
179
325
  // Bundle-safety: never auto-run when inlined into an esbuild bundle.
180
326
  if (process.argv[1] !== undefined && path.basename(process.argv[1]).startsWith('obligation_settle')) {
181
327
  process.exit(main());
@@ -16,10 +16,31 @@
16
16
  *
17
17
  * WHAT IT SEES, honestly. `_lib/structured_ask.ts` carries no observed
18
18
  * per-host tool, so this guard matches on a name SHAPE and is filtered by the
19
- * manifest's per-concern `tools:` key to the same candidate names. On every
20
- * host measured today it therefore fires on nothing — the concern exists so
21
- * that the first host to ship a picker meets the rule already in force, not
22
- * because a violation has been observed.
19
+ * manifest's per-concern `tools:` key to the same candidate names. No host's
20
+ * delivered surface has been OBSERVED in this tree carrying a question-picker
21
+ * tool, which is why `STRUCTURED_ASK_SHAPES` is empty and no per-host shape row
22
+ * exists — the same sentence `_lib/structured_ask.ts` already uses about itself.
23
+ *
24
+ * That is NARROWER than the claim this header used to make. The earlier
25
+ * sentence asserted that on every host measured today the concern therefore
26
+ * matches nothing at all, and the two are different claims with only the first
27
+ * supported. The old wording is described here rather than quoted, because
28
+ * 1.1's drift check is a grep and a grep cannot tell a refuted quotation from
29
+ * a live assertion. The measurement base is ONE row:
30
+ * `_lib/host_capability.ts`'s registry records `structured_ask: false` for
31
+ * `claude` alone, observed-absent on Claude Code 2.1.263 on 2026-09-07, and the
32
+ * other eight hosts in `docs/enforcement-by-host.md` have no row at all — never
33
+ * looked, not measured. "Every host measured" read against that table therefore
34
+ * describes one host, and a present-tense claim about what the concern matches
35
+ * turns one dated single-version reading into a standing property, which the registry's own
36
+ * comment refuses in the same breath: the observation "is not a claim that the
37
+ * vendor ships no such tool anywhere, and a later session observing one writes
38
+ * the `true` row over it".
39
+ *
40
+ * So the honest statement is about what has been looked at, not about what
41
+ * fires. The concern exists so the first host whose delivered surface carries a
42
+ * picker meets the rule already in force, not because a violation has been
43
+ * observed.
23
44
  *
24
45
  * ENFORCEMENT REACH, also honestly. `fail_closed: false` and the exit-1 deny
25
46
  * are only a deny where the host both binds `pre_tool_use` AND honours the
@@ -72,8 +72,7 @@ import { buildOrchestrationLine, type RecordInput } from '../_lib/orchestration_
72
72
  import { DEFAULT_DIR } from '../orchestration_record.js';
73
73
  import { is_replay_mode } from './state_io.js';
74
74
  import { readHookStdin } from './hook_stdin.js';
75
-
76
- const EXIT_ALLOW = 0;
75
+ import { EXIT_ALLOW } from './exit_codes.js';
77
76
 
78
77
  type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
79
78
  type JsonObject = { [k: string]: JsonValue };
@@ -20,6 +20,7 @@ import {
20
20
  type PermissionDecision,
21
21
  type PermissionEmission,
22
22
  } from "./host_semantics.js";
23
+ import { EXIT_ALLOW, EXIT_BLOCK } from './exit_codes.js';
23
24
 
24
25
  type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
25
26
  type JsonObject = { [k: string]: JsonValue };
@@ -29,8 +30,6 @@ function _isObject(v: unknown): v is JsonObject {
29
30
  }
30
31
 
31
32
  /** Mirrors `dispatch_hook.EXIT_ALLOW` / `EXIT_BLOCK`; imported would be a cycle. */
32
- const EXIT_ALLOW = 0;
33
- const EXIT_BLOCK = 1;
34
33
 
35
34
  /** The host's `tool_name`, or `""` when the event carries none. */
36
35
  export function _payload_tool_name(envelope: JsonObject): string {
@@ -54,9 +54,7 @@ import path from "node:path";
54
54
  import { execFileSync } from "node:child_process";
55
55
  import { fileURLToPath, pathToFileURL } from "node:url";
56
56
  import { readHookStdin } from "./hook_stdin.js";
57
-
58
- const EXIT_ALLOW = 0;
59
- const EXIT_WARN = 2;
57
+ import { EXIT_ALLOW, EXIT_WARN } from './exit_codes.js';
60
58
 
61
59
  type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
62
60
  type JsonObject = { [k: string]: JsonValue };
@@ -36,6 +36,7 @@ import { spawnSync } from "node:child_process";
36
36
  import fs from "node:fs";
37
37
  import path from "node:path";
38
38
  import { fileURLToPath, pathToFileURL } from "node:url";
39
+ import { EXIT_USAGE } from './exit_codes.js';
39
40
 
40
41
  // src/scripts/hooks/replay_hook.ts → parents[3] is the repo root.
41
42
  const REPO_ROOT = path.resolve(
@@ -175,7 +176,7 @@ function _parse_args(argv: string[]): ReplayArgs {
175
176
  process.stderr.write(
176
177
  `replay_hook: the following arguments are required: ${missing.join(", ")}\n`,
177
178
  );
178
- process.exit(2);
179
+ process.exit(EXIT_USAGE);
179
180
  }
180
181
  return args;
181
182
  }
@@ -69,12 +69,11 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
69
69
  import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
70
70
  import { readHookStdin } from './hook_stdin.js';
71
71
  import { atomic_write_json } from './state_io.js';
72
+ import { EXIT_ALLOW, EXIT_WARN } from './exit_codes.js';
72
73
 
73
- const EXIT_ALLOW = 0;
74
74
  // Severity is taken from the EXIT CODE, not from the `decision` field — see
75
75
  // the file header for why this never blocks on `claude`. This concern never
76
76
  // returns exit 1 on any path.
77
- const EXIT_WARN = 2;
78
77
 
79
78
  /**
80
79
  * Committed exemption list — deliberately duplicated from
@@ -49,8 +49,7 @@ import { deriveSessionKey, totalNonDocMutatedLinesWithMeasure } from './end_revi
49
49
  import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
50
50
  import { readHookStdin } from './hook_stdin.js';
51
51
  import { atomic_write_json } from './state_io.js';
52
-
53
- const EXIT_OK = 0;
52
+ import { EXIT_ALLOW as EXIT_OK } from './exit_codes.js';
54
53
 
55
54
  export function buildBaseline(workspaceRoot: string, now: Date = new Date()): ReviewBaseline {
56
55
  const measured = totalNonDocMutatedLinesWithMeasure(workspaceRoot);
@@ -63,10 +63,9 @@ import {
63
63
  } from "./block_no_verify.js";
64
64
  import { readHookStdin } from "./hook_stdin.js";
65
65
  import { detectRtkCached } from "../../install/rtkDetection.js";
66
+ import { EXIT_ALLOW, EXIT_WARN } from './exit_codes.js';
66
67
 
67
68
  const SETTINGS_FILE = ".agent-settings.yml";
68
- const EXIT_ALLOW = 0;
69
- const EXIT_WARN = 2;
70
69
 
71
70
  type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
72
71
  type JsonObject = { [k: string]: JsonValue };
@@ -102,7 +102,12 @@ import {
102
102
  resolveLeanProjectionHosts,
103
103
  } from '../_lib/lean_projection_mode.js';
104
104
  import { enforcement_class_from_frontmatter } from '../_lib/obligation_frequency.js';
105
- import { appendDelivered, stamp, type DeliveredRow } from '../_lib/obligations.js';
105
+ import {
106
+ appendDelivered,
107
+ stamp,
108
+ type DeliveredRow,
109
+ type WriterInput,
110
+ } from '../_lib/obligations.js';
106
111
  import {
107
112
  loadRuleBody,
108
113
  loadRouter,
@@ -110,9 +115,7 @@ import {
110
115
  selectForInjection,
111
116
  } from '../_lib/rule_injection.js';
112
117
  import { readHookStdin } from './hook_stdin.js';
113
-
114
- const EXIT_ALLOW = 0;
115
- const EXIT_WARN = 2;
118
+ import { EXIT_ALLOW, EXIT_WARN } from './exit_codes.js';
116
119
 
117
120
  /**
118
121
  * Per-prompt injection ceiling, in UTF-8 BYTES.
@@ -268,7 +271,7 @@ export interface Injection {
268
271
  export function recordDelivered(root: string, session: string, ruleIds: string[]): number {
269
272
  try {
270
273
  const now = stamp();
271
- const rows: DeliveredRow[] = ruleIds.map((id) => {
274
+ const rows: WriterInput<DeliveredRow>[] = ruleIds.map((id) => {
272
275
  const body = loadRuleBody(root, id);
273
276
  return {
274
277
  rule: id,
@@ -152,10 +152,9 @@ import {
152
152
  sessionRefusalFile,
153
153
  } from '../_lib/turn_end_refusals.js';
154
154
  import { phaseLines } from '../_lib/roadmap_checkboxes.js';
155
+ import { commandBearing, renderVerifyLine, type VerifyClause } from '../_lib/verify_clause.js';
155
156
 
156
- const EXIT_ALLOW = 0;
157
157
  /** Dispatcher-internal block code; the dispatcher maps stop-slot 1 → host 2. */
158
- const EXIT_BLOCK = 1;
159
158
 
160
159
  /**
161
160
  * The gate's cap, IMPORTED rather than restated — the two must agree by
@@ -194,6 +193,7 @@ import {
194
193
  WALL_CLOCK_CAP_MS,
195
194
  type LadderAction,
196
195
  } from '../_lib/continuation_ladder.js';
196
+ import { EXIT_ALLOW, EXIT_BLOCK } from './exit_codes.js';
197
197
 
198
198
  /**
199
199
  * DECLARED in `_lib/continuation_ladder.ts`, re-exported here: one declaration,
@@ -375,8 +375,8 @@ export function isDuplicateFire(
375
375
  export interface NextStep {
376
376
  /** The step's own line, checkbox stripped, truncated for the message. */
377
377
  text: string;
378
- /** The step's `verify:` command, when its line carries one. */
379
- verify: string | null;
378
+ /** The step's `verify:` clause — command plus optional expectation. */
379
+ verify: VerifyClause | null;
380
380
  }
381
381
 
382
382
  export interface ScanResult {
@@ -481,15 +481,15 @@ export function scanOpenSteps(text: string): ScanResult {
481
481
  * The comment form wins when both are present: it is the machine-readable one,
482
482
  * and a step carrying both has a human-facing line and a tooling-facing line
483
483
  * rather than two commands.
484
+ *
485
+ * The hook's narrowing only — a prose clause is not a command, and the
486
+ * continuation message renders commands. It parses nothing itself: a second
487
+ * copy of the arrow grammar is how this hook and `closure_scan` would drift,
488
+ * and a drifted grammar stops reading expectations without failing anything.
489
+ * `_lib/verify_clause.ts` owns the grammar and the sweep that keeps it single.
484
490
  */
485
- export function extractVerify(stepText: string): string | null {
486
- const html = /<!--\s*verify:\s*(.*?)\s*-->/.exec(stepText);
487
- if (html !== null) return (html[1] as string).trim() || null;
488
- // `verify:` then the command, both backticked. The label's own backticks
489
- // are optional because the tree carries `verify:` and `` `verify:` ``.
490
- const backticked = /`?verify:`?\s*`([^`]+)`/.exec(stepText);
491
- if (backticked !== null) return (backticked[1] as string).trim() || null;
492
- return null;
491
+ export function extractVerify(stepText: string): VerifyClause | null {
492
+ return commandBearing(stepText);
493
493
  }
494
494
 
495
495
  /**
@@ -1259,7 +1259,6 @@ export function main(): number {
1259
1259
  return EXIT_ALLOW;
1260
1260
  }
1261
1261
 
1262
-
1263
1262
  // The two-tree provenance, carried on every event this run emits. See
1264
1263
  // `provenance()` for the six fields and why none of them is a boolean.
1265
1264
  //
@@ -1487,8 +1486,7 @@ export function main(): number {
1487
1486
 
1488
1487
  function _continuationText(slug: string, scan: ScanResult, iteration: number): string {
1489
1488
  const stepLine = scan.next
1490
- ? `Next step: ${scan.next.text}` +
1491
- (scan.next.verify ? `\n verify: ${scan.next.verify}` : '')
1489
+ ? `Next step: ${scan.next.text}${renderVerifyLine(scan.next.verify)}`
1492
1490
  : 'Next step: (first open checkbox in the roadmap)';
1493
1491
  return (
1494
1492
  `run-continuation: the contracted roadmap \`${slug}\` has ${scan.open} open ` +
@@ -143,10 +143,9 @@ import { isSyntheticPrompt } from "../_lib/prompt_shape.js";
143
143
  import { OBSERVATION_LOG, readObservationLog, resolveSkillCatalogueRoots } from "../_lib/skill_catalogue.js";
144
144
  import { knownBareNames } from "../_lib/skill_catalogue_series.js";
145
145
  import { _tokenize, rank, type RankRow } from "../skill_tools/score_skill_relevance.js";
146
+ import { EXIT_ALLOW, EXIT_WARN } from './exit_codes.js';
146
147
 
147
- const EXIT_ALLOW = 0;
148
148
  /** See the header: reduced to a real exit of 0 by `emitFor`'s warn branch. */
149
- const EXIT_WARN = 2;
150
149
 
151
150
  /**
152
151
  * Minimum top-1 ranker score before this concern speaks.
@@ -95,8 +95,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
95
95
  import { is_replay_mode } from './state_io.js';
96
96
  import { readHookStdin } from './hook_stdin.js';
97
97
  import { resolveConsumerRoot } from './subagent_ledger_hook.js';
98
-
99
- const EXIT_ALLOW = 0;
98
+ import { EXIT_ALLOW } from './exit_codes.js';
100
99
 
101
100
  /** Would-warns per session before the valve goes silent — `MAX_NUDGES` shape. */
102
101
  export const MAX_SHADOW_WARNS = 2;
@@ -55,8 +55,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
55
55
 
56
56
  import { appendLedgerLine, openRecordStats, resolveConsumerRoot } from './subagent_ledger_hook.js';
57
57
  import { readHookStdin } from './hook_stdin.js';
58
-
59
- const EXIT_ALLOW = 0;
58
+ import { EXIT_ALLOW } from './exit_codes.js';
60
59
 
61
60
  type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
62
61
  type JsonObject = { [k: string]: JsonValue };
@@ -115,8 +115,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
115
115
  import { countContractFields, validateResponse } from '../_lib/subagent_response.js';
116
116
  import { atomic_write_json, is_replay_mode } from './state_io.js';
117
117
  import { readHookStdin } from './hook_stdin.js';
118
-
119
- const EXIT_ALLOW = 0;
118
+ import { EXIT_ALLOW } from './exit_codes.js';
120
119
 
121
120
  type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
122
121
  type JsonObject = { [k: string]: JsonValue };
@@ -51,8 +51,8 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
51
51
  import { unwrap } from './envelope.js';
52
52
  import { readHookStdin } from './hook_stdin.js';
53
53
  import { humanTypedThisTurn } from '../_lib/machine_wake.js';
54
+ import { EXIT_ALLOW } from './exit_codes.js';
54
55
 
55
- const EXIT_ALLOW = 0;
56
56
  const SETTINGS_FILE = '.agent-settings.yml';
57
57
  const SINK_REL = path.join('agents', 'runtime', 'state', 'audit', 'suggestion-capture.jsonl');
58
58
  const LATCH_REL = path.join('agents', 'runtime', 'state', 'suggestion-latch.json');
@@ -0,0 +1,49 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ /**
4
+ * table_fingerprint — the content fingerprint a compiled plumbing table is
5
+ * validated against.
6
+ *
7
+ * Extracted from `dispatch_hook._manifest_fingerprint` so a SECOND table
8
+ * (`host_lowering.yaml`) can use the same helper without importing the
9
+ * dispatcher: `dispatch_hook` → `host_semantics` → `host_lowering` is a live
10
+ * import chain, so the reverse edge would be a cycle.
11
+ *
12
+ * DELIBERATELY CONTENT-DERIVED, NOT MTIME. The first version of the manifest
13
+ * fast path compared mtimes and that was a measured defect, not a theoretical
14
+ * one: on a fresh `actions/checkout` both files carry the checkout timestamp in
15
+ * whatever order git wrote them, so whether the optimisation applied at all was
16
+ * a coin flip — it won on a PR run (p95 129 ms) and lost on the trunk
17
+ * (p95 186 ms) for the same commit. A fingerprint is deterministic wherever the
18
+ * tree came from.
19
+ *
20
+ * SHA-256 over the UTF-8 bytes, plus the length. This replaced an FNV-1a loop
21
+ * in step 3.1 of `road-to-a-kernel-that-guards-its-plumbing`, and the reason
22
+ * the old one existed no longer holds. Its header said a cryptographic hash was
23
+ * refused because `require('node:crypto')` costs 8 ms of process start, which
24
+ * is most of what the precompiled table saves. Both halves were re-measured
25
+ * against the tree as it now stands, and both had gone stale:
26
+ *
27
+ * - The module is ALREADY LOADED. `dist/hooks/dispatch.js` carries 23
28
+ * top-level `import … from "node:crypto"` statements from other parts of the
29
+ * graph, so the 8 ms is paid on every dispatch whether this function uses it
30
+ * or not. There is no startup cost left to avoid.
31
+ * - SHA-256 is FASTER here, not slower: 0.106 ms against 0.113 ms over the
32
+ * 99,792-byte manifest, because Node's digest is native and the FNV-1a loop
33
+ * was 99,792 interpreted `charCodeAt` calls.
34
+ *
35
+ * The change is therefore free, and it buys the property the old comment had to
36
+ * disclaim: this now guards against a CRAFTED sibling as well as a stale one,
37
+ * which is what a file called a fingerprint should mean when the roadmap around
38
+ * it is about integrity.
39
+ *
40
+ * Producer and both consumers import THIS function — `compile_hook_manifest`
41
+ * writes the value, `dispatch_hook` and `host_lowering` check it — so the
42
+ * algorithm can never drift between the two sides. A mismatch is not a failure
43
+ * either way: the reader falls through to parsing the YAML source, which is
44
+ * correct and merely slower.
45
+ */
46
+ export function tableFingerprint(text: string): string {
47
+ const digest = createHash('sha256').update(text, 'utf-8').digest('hex');
48
+ return `${digest}:${String(text.length)}`;
49
+ }
@@ -45,8 +45,7 @@ import {
45
45
  import { is_replay_mode } from './state_io.js';
46
46
  import { readHookStdin } from './hook_stdin.js';
47
47
  import { _resolveRoot, readSettingsFor } from './telemetry_usage_hook.js';
48
-
49
- const EXIT_ALLOW = 0;
48
+ import { EXIT_ALLOW } from './exit_codes.js';
50
49
 
51
50
  /**
52
51
  * Where `flush_sender.mjs` lives, relative to this module.
@@ -57,8 +57,7 @@ import {
57
57
  } from '../../agent-src/templates/scripts/telemetry/transport.js';
58
58
  import { is_replay_mode } from './state_io.js';
59
59
  import { readHookStdin } from './hook_stdin.js';
60
-
61
- const EXIT_ALLOW = 0;
60
+ import { EXIT_ALLOW } from './exit_codes.js';
62
61
 
63
62
  /** The tool name Phase 0 spike 1 confirmed for a skill invocation. */
64
63
  export const SKILL_TOOL_NAME = 'Skill';