@event4u/agent-config 14.17.0 → 14.19.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 (184) 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 +207 -77
  5. package/README.md +72 -150
  6. package/dist/agent-src/README.md +1 -1
  7. package/dist/agent-src/commands/agent-handoff.md +2 -2
  8. package/dist/agent-src/commands/pr/merge.md +35 -44
  9. package/dist/agent-src/commands/review/changes.md +19 -19
  10. package/dist/agent-src/commands/review.md +2 -2
  11. package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
  12. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  13. package/dist/agent-src/rules/git-history-discipline.md +2 -2
  14. package/dist/agent-src/rules/guidelines.md +1 -1
  15. package/dist/agent-src/rules/session-canary.md +2 -2
  16. package/dist/agent-src/rules/token-efficiency.md +1 -1
  17. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
  18. package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
  19. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
  20. package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
  21. package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
  22. package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
  23. package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
  24. package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
  25. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
  26. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
  27. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
  28. package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
  29. package/dist/agent-src/skills/design-review/SKILL.md +3 -1
  30. package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
  31. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  32. package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
  33. package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
  34. package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
  35. package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
  36. package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
  37. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
  38. package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
  39. package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
  40. package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
  41. package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
  42. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  43. package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
  44. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  45. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
  46. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
  47. package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
  48. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
  49. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
  50. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
  51. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  52. package/dist/cli/commands/recordTriggerEval.js +2 -2
  53. package/dist/cli/main.js +1 -1
  54. package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
  55. package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
  56. package/dist/cli-delegate/cmd_adr_effective.js +318 -81
  57. package/dist/cli-delegate/cmd_conformance.js +749 -669
  58. package/dist/cli-delegate/cmd_doctor.js +1 -1
  59. package/dist/cli-delegate/cmd_export.js +1 -1
  60. package/dist/cli-delegate/cmd_handoff.js +1 -1
  61. package/dist/cli-delegate/cmd_prune.js +1 -1
  62. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  63. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  64. package/dist/cli-delegate/cmd_sync.js +1 -1
  65. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  66. package/dist/cli-delegate/cmd_validate.js +1 -1
  67. package/dist/discovery/deprecation-report.md +1 -1
  68. package/dist/discovery/discovery-manifest.json +39 -38
  69. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  70. package/dist/discovery/discovery-manifest.summary.md +1 -1
  71. package/dist/discovery/dormancy-report.md +1 -1
  72. package/dist/discovery/orphan-report.md +1 -1
  73. package/dist/discovery/packs.json +2 -2
  74. package/dist/discovery/trust-report.md +1 -1
  75. package/dist/discovery/workspaces.json +2 -2
  76. package/dist/hooks/dispatch.js +2764 -2439
  77. package/dist/install/install.mjs +588 -527
  78. package/dist/mcp/registry-manifest.json +2 -2
  79. package/dist/mcp/server.json +2 -2
  80. package/dist/mcp/server.mjs +1381 -1138
  81. package/dist/scripts/_lib/model_tier.js +1 -1
  82. package/dist/shared/settingsConsent.js +1 -1
  83. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
  84. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  85. package/docs/guidelines/design-antipatterns.md +87 -64
  86. package/package.json +1 -1
  87. package/src/agent-src/README.md +1 -1
  88. package/src/config/ci-local-parity.yml +6 -0
  89. package/src/config/conformance-claim-baseline.json +9 -0
  90. package/src/config/gate-coverage.yml +170 -0
  91. package/src/config/gate-violation-baselines.json +4 -4
  92. package/src/config/release-gate-locality.yml +141 -0
  93. package/src/scripts/_cli/cmd_doctor.ts +2 -2
  94. package/src/scripts/_cli/cmd_handoff.ts +1 -1
  95. package/src/scripts/_cli/cmd_update.ts +1 -1
  96. package/src/scripts/_cli/cmd_versions.ts +1 -1
  97. package/src/scripts/_lib/adr_frontmatter.ts +44 -0
  98. package/src/scripts/_lib/consequence_objects.ts +152 -0
  99. package/src/scripts/_lib/exec_evidence.ts +7 -0
  100. package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
  101. package/src/scripts/_lib/host_permission_checks.ts +251 -0
  102. package/src/scripts/_lib/model_tier.ts +1 -1
  103. package/src/scripts/_lib/obligation_frequency.ts +2 -2
  104. package/src/scripts/_lib/release_highlights.ts +265 -1
  105. package/src/scripts/_lib/release_material.ts +129 -0
  106. package/src/scripts/_lib/repeated_run.ts +77 -0
  107. package/src/scripts/_lib/runtime_journal.ts +26 -3
  108. package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
  109. package/src/scripts/_lib/session_register.ts +11 -6
  110. package/src/scripts/_lib/sqlite_schema.ts +22 -0
  111. package/src/scripts/_lib/surface.ts +86 -0
  112. package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
  113. package/src/scripts/_lib/ui_authority.ts +116 -0
  114. package/src/scripts/_lib/unified_diff.ts +121 -0
  115. package/src/scripts/adr/regenerate_index.ts +54 -1
  116. package/src/scripts/adr_cite_check.ts +71 -15
  117. package/src/scripts/ai_council/clients.ts +1 -1
  118. package/src/scripts/autonomy_friction_corpus.ts +229 -0
  119. package/src/scripts/build_proof.ts +15 -2
  120. package/src/scripts/check_beta_review_markers.ts +141 -17
  121. package/src/scripts/check_enforcement_coverage.ts +1 -1
  122. package/src/scripts/check_enforcement_denominator.ts +121 -11
  123. package/src/scripts/check_finding_dispositions.ts +239 -6
  124. package/src/scripts/check_gate_coverage.ts +96 -0
  125. package/src/scripts/check_installed_hooks_fresh.ts +381 -0
  126. package/src/scripts/check_release_highlights.ts +99 -14
  127. package/src/scripts/check_skill_activation_claim.ts +179 -0
  128. package/src/scripts/check_suppression_hygiene.ts +11 -0
  129. package/src/scripts/check_trigger_evals.ts +1 -1
  130. package/src/scripts/code_graph/build.ts +230 -33
  131. package/src/scripts/code_graph/extract.ts +168 -33
  132. package/src/scripts/code_graph/types.ts +37 -2
  133. package/src/scripts/code_graph/validate.ts +12 -1
  134. package/src/scripts/conformance_scan.ts +45 -4
  135. package/src/scripts/detect_silent_catch.ts +403 -0
  136. package/src/scripts/detect_verification_tampering.ts +538 -0
  137. package/src/scripts/explain_run.ts +1 -1
  138. package/src/scripts/generate_capability_matrix.ts +1 -1
  139. package/src/scripts/git_authorization_hook.ts +228 -18
  140. package/src/scripts/hook_manifest.json +1 -1
  141. package/src/scripts/hook_manifest.yaml +47 -30
  142. package/src/scripts/hooks/augment-dispatcher.sh +1 -1
  143. package/src/scripts/hooks/block_no_verify.ts +3 -3
  144. package/src/scripts/hooks/category_a.ts +291 -0
  145. package/src/scripts/hooks/cline-dispatcher.sh +1 -1
  146. package/src/scripts/hooks/concern_registry.ts +2 -2
  147. package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
  148. package/src/scripts/hooks/dispatch_hook.ts +17 -22
  149. package/src/scripts/hooks/evidence_independence.ts +21 -2
  150. package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
  151. package/src/scripts/hooks/host_lowering.ts +215 -0
  152. package/src/scripts/hooks/host_lowering.yaml +185 -0
  153. package/src/scripts/hooks/host_semantics.ts +105 -37
  154. package/src/scripts/hooks/journal_record_hook.ts +10 -0
  155. package/src/scripts/hooks/permission_gate.ts +117 -0
  156. package/src/scripts/hooks/push_settle_hook.ts +250 -0
  157. package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
  158. package/src/scripts/hooks_status.ts +12 -10
  159. package/src/scripts/install-hooks.sh +134 -1
  160. package/src/scripts/install.ts +53 -82
  161. package/src/scripts/install_anthropic_key.sh +2 -2
  162. package/src/scripts/language_mirror_hook.ts +27 -5
  163. package/src/scripts/lint_design_antipattern_parity.ts +80 -0
  164. package/src/scripts/lint_evidence_artifacts.ts +45 -1
  165. package/src/scripts/lint_hook_manifest.ts +87 -3
  166. package/src/scripts/lint_motion_authority_drift.ts +731 -0
  167. package/src/scripts/lint_readme_size.ts +158 -12
  168. package/src/scripts/lint_reasoning_fixtures.ts +216 -0
  169. package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
  170. package/src/scripts/measure_sibling_naming.ts +150 -0
  171. package/src/scripts/redact_hook_capture.ts +1 -1
  172. package/src/scripts/release.ts +31 -143
  173. package/src/scripts/release_drill.ts +71 -1
  174. package/src/scripts/release_publication.ts +243 -3
  175. package/src/scripts/release_verify.ts +220 -0
  176. package/src/scripts/report_skill_activation.ts +319 -7
  177. package/src/scripts/schemas/review-findings.schema.json +6 -1
  178. package/src/scripts/schemas/skill.schema.json +12 -2
  179. package/src/scripts/self_review_gate.ts +10 -1
  180. package/src/scripts/session_canary_hook.ts +1 -1
  181. package/src/scripts/session_register_hook.ts +4 -3
  182. package/src/scripts/trigger_eval_grandfather.json +5 -1
  183. package/src/scripts/validate_agent_settings.ts +1 -1
  184. package/src/shared/settingsConsent.ts +1 -1
@@ -1,11 +1,28 @@
1
- #!/usr/bin/env node
2
1
  /**
3
- * Unauthorized-git gate — `pre_tool_use` concern.
4
- *
5
- * Checks a git / GitHub / npm operation against the authorization ledger that
6
- * `git_authorization_hook.ts` writes on `user_prompt_submit`, so the question
7
- * "did the user authorize this THIS turn?" is answered from a recorded fact
8
- * rather than from the model's recollection.
2
+ * Git / GitHub / npm command classifier — a pure library, not a gate.
3
+ *
4
+ * WHAT THIS IS NOT, AND WHY. Until 2026-09-04 this file was the
5
+ * `block-unauthorized-git` `pre_tool_use` concern: it read the authorization
6
+ * ledger and REFUSED an operation the turn had not authorized. The owner
7
+ * removed that enforcement (ADR-254) after it was measured to refuse
8
+ * authorizations the owner had in fact given — a guardrail sentence inside an
9
+ * authorization ("never merge empty PRs") read as a withdrawal and erased the
10
+ * whole ledger, and an answer given as an option number ("1") matched no
11
+ * authorizing pattern at all.
12
+ *
13
+ * What survives is the half that never decided anything: the command parser and
14
+ * the operation vocabulary. Two callers depend on it and neither gates git —
15
+ * `block_no_verify.ts` needs `substitutionPayloads` so `git $(echo --no-verify)`
16
+ * cannot slip past the hook-bypass guard, and `conformance_scan.ts` needs
17
+ * `commandOp` / `BLOCK_OPS` to MEASURE how often an irreversible operation ran
18
+ * on an unauthorized turn. Measurement is all that is left here.
19
+ *
20
+ * `BLOCK_OPS` and `WARN_OPS` keep their names because they name the
21
+ * reversibility split `non-destructive-by-default` draws, and that split is
22
+ * still true. They no longer name an action this file takes.
23
+ *
24
+ * SEVERITY SPLIT — by reversibility, not by confidence. Retained below because
25
+ * it is what the two sets still mean.
9
26
  *
10
27
  * SEVERITY SPLIT — by reversibility, not by confidence.
11
28
  *
@@ -56,33 +73,7 @@
56
73
  * substitution in every position tried, `sh -c` and `eval` payloads, env
57
74
  * assignment prefixes, and quotes inside the command word.
58
75
  */
59
- import fs from "node:fs";
60
- import path from "node:path";
61
- import { fileURLToPath, pathToFileURL } from "node:url";
62
-
63
- import { readHookStdin } from "./hook_stdin.js";
64
- import { analyseMergeImpact, describeImpact } from "./merge_impact.js";
65
- import { atomic_write_json } from "./state_io.js";
66
- import {
67
- consumeGrantTarget,
68
- ledgerFileFor,
69
- pendingFileFor,
70
- PR_NUMBER_MAX,
71
- readGrants,
72
- STATE_FILE,
73
- type GitOp,
74
- type MergeGrant,
75
- } from "../git_authorization_hook.js";
76
-
77
- const EXIT_ALLOW = 0;
78
- // MUST equal dispatch_hook.EXIT_BLOCK. The dispatcher's internal ladder is
79
- // 0 allow / 1 block / 2 warn — NOT the 2-means-block shape a PreToolUse guard
80
- // reads naturally from Claude's own native contract. This constant was 2 when
81
- // this gate first shipped, so the dispatcher reduced every refusal to a WARN
82
- // and the gate emitted advisory context while the operation went through.
83
- // Pinned against the dispatcher's export by
84
- // tests/hooks/concern_block_exit_parity.test.ts.
85
- const EXIT_BLOCK = 1;
76
+ import { PR_NUMBER_MAX, type GitOp } from "../git_authorization_hook.js";
86
77
 
87
78
  type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
88
79
  type JsonObject = { [k: string]: JsonValue };
@@ -125,6 +116,22 @@ export const BLOCK_OPS: ReadonlySet<GitOp> = new Set<GitOp>([
125
116
  "worktree-remove",
126
117
  /** `-x` is the flag that takes `.env` and local certificates. */
127
118
  "clean-ignored",
119
+
120
+ // ── The consequence vocabulary, added 2026-09-06. Every one is already
121
+ // never-autonomous in the `non-destructive-by-default` prose.
122
+ //
123
+ // MEMBERSHIP HERE IS A COUNTING DECISION, NOT A REFUSAL. The set's name
124
+ // predates ADR-254, which deleted the reader that blocked: the one consumer
125
+ // that acts on it now is `conformance_scan`'s after-the-fact tally, so
126
+ // "blocked" describes what the Hard Floor requires of the model, never what
127
+ // this package does at tool-call time.
128
+ "prod-deploy",
129
+ "prod-data-destroy",
130
+ "prod-infra-change",
131
+ "external-send",
132
+ "money-movement",
133
+ "trunk-force-push",
134
+ "out-of-scope-destruction",
128
135
  ]);
129
136
 
130
137
  /** Recoverable operations — warned without a this-turn authorization. */
@@ -163,6 +170,17 @@ const G = "(?:-[A-Za-z-]+(?:=\\S+)?(?:\\s+\\S+)?\\s+)*";
163
170
  /** Optional absolute/relative path in front of the tool word: `/usr/bin/npm`. */
164
171
  const P = "^(?:\\S*\\/)?";
165
172
 
173
+ /**
174
+ * Tool words the consequence rows classify, as a head anchor.
175
+ *
176
+ * Kept as one constant rather than folded into each row: `commandOp` filters
177
+ * segments by head BEFORE consulting the table, so a row whose tool word is
178
+ * missing here never runs — a silent no-op that looks exactly like a pattern
179
+ * that did not match.
180
+ */
181
+ const CONSEQUENCE_TOOL_HEAD =
182
+ /^(?:\S*\/)?(terraform|pulumi|kubectl|helm|aws|gcloud|az|vercel|netlify|flyctl|fly|wrangler|serverless|sls|eb|heroku|cap|psql|mysql|mariadb|sqlite3|mongosh|mongo|redis-cli|sendmail|msmtp|mail|mailx|curl|stripe|braintree|paypal|rm)\b/i;
183
+
166
184
  /**
167
185
  * A `gh api` call that WRITES. Read-only `gh api` is the standard way to inspect
168
186
  * GitHub state and must never be refused.
@@ -276,6 +294,13 @@ const COMMAND_OPS: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
276
294
  // Above `push`, and both spellings: `--force` and `--force-with-lease`. The
277
295
  // "safe force" is in the same class here because what it is safe against is a
278
296
  // stale local view, not a collaborator who pushed after your fetch.
297
+ {
298
+ op: "trunk-force-push",
299
+ re: new RegExp(
300
+ `${P}git\\s+${G}push\\b[^\\n;|&]*(--force\\b|--force-with-lease\\b|\\s-f\\b)[^\\n;|&]*\\b(main|master|trunk|production|prod)\\b`,
301
+ "i",
302
+ ),
303
+ },
279
304
  {
280
305
  op: "force-push",
281
306
  re: new RegExp(`${P}git\\s+${G}push\\b[^\\n;|&]*(--force\\b|--force-with-lease\\b|\\s-f\\b)`, "i"),
@@ -305,6 +330,73 @@ const COMMAND_OPS: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
305
330
  },
306
331
  { op: "close", re: new RegExp(`${P}gh\\s+(pr|issue)\\s+close\\b`, "i") },
307
332
 
333
+ // ── Consequence operations, added 2026-09-06
334
+ // (`road-to-authorization-that-reaches-further` 2.1).
335
+ //
336
+ // ORDER: above the generic `push`/`commit` fallbacks, because
337
+ // `trunk-force-push` is a `git push` and the most specific pattern must win —
338
+ // the rule this table states at its head.
339
+ //
340
+ // These classify commands the count could not see before. They add NOTHING to
341
+ // any refusal path: `commandOp` and `BLOCK_OPS` have exactly one consumer
342
+ // that acts on them, the after-the-fact tally in `conformance_scan`, and this
343
+ // change does not add a second.
344
+ {
345
+ op: "prod-data-destroy",
346
+ re: new RegExp(
347
+ `${P}(psql|mysql|mariadb|sqlite3|mongosh?)\\b[^\\n]*\\b(drop\\s+(table|database|schema)|truncate|delete\\s+from)\\b` +
348
+ `|${P}aws\\s+s3\\s+(rm|rb)\\b` +
349
+ `|${P}aws\\s+(dynamodb|rds)\\s+delete-\\S+` +
350
+ `|${P}redis-cli\\b[^\\n]*\\bflush(all|db)\\b`,
351
+ "i",
352
+ ),
353
+ },
354
+ {
355
+ op: "prod-infra-change",
356
+ re: new RegExp(
357
+ `${P}terraform\\s+(apply|destroy)\\b` +
358
+ `|${P}pulumi\\s+(up|destroy)\\b` +
359
+ `|${P}kubectl\\s+(apply|delete|scale|drain)\\b` +
360
+ `|${P}helm\\s+(install|upgrade|uninstall|rollback)\\b` +
361
+ `|${P}aws\\s+(iam|ec2|route53|elbv2)\\s+(create|delete|put|update|modify|attach|detach)-\\S+` +
362
+ `|${P}(gcloud|az)\\s+\\S+\\s+(create|delete|update)\\b`,
363
+ "i",
364
+ ),
365
+ },
366
+ {
367
+ op: "prod-deploy",
368
+ re: new RegExp(
369
+ `${P}(vercel|netlify|flyctl|fly|wrangler|serverless|sls|eb|heroku)\\s+${G}deploy\\b` +
370
+ `|${P}(vercel|netlify)\\b[^\\n]*--prod\\b` +
371
+ `|${P}(npm|pnpm|yarn)\\s+run\\s+deploy\\b` +
372
+ `|${P}cap\\s+production\\s+deploy\\b`,
373
+ "i",
374
+ ),
375
+ },
376
+ {
377
+ op: "external-send",
378
+ re: new RegExp(
379
+ `${P}(sendmail|msmtp|mailx?)\\b` +
380
+ `|${P}curl\\b(?=[^\\n]*(?:-X|--request)\\s+(?:POST|PUT|PATCH)\\b)(?=[^\\n]*https?:\\/\\/)`,
381
+ "i",
382
+ ),
383
+ },
384
+ {
385
+ op: "money-movement",
386
+ re: new RegExp(
387
+ `${P}(stripe|braintree|paypal)\\s+\\S*(charge|refund|payout|transfer|payment)\\S*\\b`,
388
+ "i",
389
+ ),
390
+ },
391
+ {
392
+ // `rm -rf` on an ABSOLUTE path or a home-relative one. Deliberately not
393
+ // "outside the working tree" — this function receives a command string and
394
+ // no cwd, so that reading is not decidable here and a classifier that
395
+ // guesses is worse than one that under-reports.
396
+ op: "out-of-scope-destruction",
397
+ re: new RegExp(`${P}rm\\s+(-\\S+\\s+)*-\\S*[rR]\\S*\\s+(-\\S+\\s+)*(~|/)\\S*`, "i"),
398
+ },
399
+
308
400
  { op: "push", re: new RegExp(`${P}git\\s+${G}push\\b`, "i") },
309
401
  { op: "commit", re: new RegExp(`${P}git\\s+${G}commit\\b`, "i") },
310
402
  { op: "branch", re: new RegExp(`${P}git\\s+${G}(checkout\\s+-b|switch\\s+-c)\\b`, "i") },
@@ -601,7 +693,14 @@ export function commandOp(command: string): GitOp | null {
601
693
  // unchecked once `publish` was authorized.
602
694
  let found: GitOp | null = null;
603
695
  for (const seg of invokedSegments(command)) {
604
- if (!/^(?:\S*\/)?(git|gh|npm|pnpm|yarn)\b/.test(seg)) {
696
+ // Widened 2026-09-06 for the consequence vocabulary. Before this the gate
697
+ // admitted five tool words, so `terraform apply` and `aws s3 rm` were not
698
+ // merely unclassified — they could not reach the table at all, and the
699
+ // count read them as nothing rather than as unmeasured.
700
+ if (
701
+ !/^(?:\S*\/)?(git|gh|npm|pnpm|yarn)\b/.test(seg) &&
702
+ !CONSEQUENCE_TOOL_HEAD.test(seg)
703
+ ) {
605
704
  continue;
606
705
  }
607
706
  for (const { op, re } of COMMAND_OPS) {
@@ -643,343 +742,55 @@ export function mergeTargetOf(command: string): number | null {
643
742
  return api ? _boundedTarget(api[1] as string) : null;
644
743
  }
645
744
 
646
- /** A target only counts inside the API's own range; anything else is no target. */
647
- function _boundedTarget(digits: string): number | null {
648
- const n = Number.parseInt(digits, 10);
649
- return Number.isSafeInteger(n) && n > 0 && n <= PR_NUMBER_MAX ? n : null;
650
- }
651
-
652
745
  /**
653
- * Does a standing grant cover this exact merge?
746
+ * The object a consequence command acts on, or `null` when the command does not
747
+ * name one.
654
748
  *
655
- * Three conditions, all necessary: the grant is for this op, the command named
656
- * a target, and that target sits in the grant's frozen set unspent.
749
+ * The sibling of `mergeTargetOf`, generalised past pull-request numbers, and it
750
+ * inherits that function's fail-closed stance verbatim: a command that names no
751
+ * object matches no record, so the caller reports nothing rather than guessing.
752
+ * Under-reporting is the safe direction for a measurement; a guessed object
753
+ * would manufacture a violation out of a parse failure.
657
754
  *
658
- * No clock is consulted here, and that is the whole of ADR-252. `authorized`
659
- * answers "did the user name this operation recently"; a grant answers "did the
660
- * user name this operation AND this pull request". The second question does not
661
- * decay with time, because the object it names cannot drift into a different
662
- * object — a force-push changes the SHA, but the grant is spent on first use, so
663
- * a merge that already happened can never be replayed against a moved head.
755
+ * Normalised to lower case with quotes stripped, so it compares against the
756
+ * prose side's `extractObjects` without either end re-deriving the rule.
664
757
  */
665
- export function grantCovers(
666
- grants: MergeGrant[],
667
- op: GitOp,
668
- target: number | null,
669
- ): MergeGrant | null {
670
- if (op !== "pr-merge" || target === null) {
671
- return null;
672
- }
673
- return (
674
- grants.find(
675
- (g) => g.op === "pr-merge" && g.targets.includes(target) && !g.consumed.includes(target),
676
- ) ?? null
677
- );
678
- }
679
-
680
- /**
681
- * A ledger older than this is not "this turn" under any reading.
682
- *
683
- * **Widening this constant for a long run is forbidden practice.** On
684
- * 2026-08-21 it was sed-patched to twelve times this value — six hours —
685
- * behind a marker promising a revert that never came, so an autonomous PR-drain could
686
- * merge past the 30-minute bound. The widening was committed to the trunk and
687
- * left there. It is a twelvefold expansion of the authorization lifetime on
688
- * the guard that gates `pr-merge`, which is a `BLOCK_OPS` member precisely
689
- * because it is irreversible.
690
- *
691
- * Neither the widened expression nor the marker text is written out here: the
692
- * obvious regression check for either is a grep, and a comment reciting the
693
- * literal makes that grep match the guard's own prose on a clean tree.
694
- *
695
- * The supported answer to "my run is longer than the window" is that the run
696
- * STOPS and REPORTS at expiry and the user re-authorizes — never that the
697
- * window grows. The agent never edits this constant, this file, or the built
698
- * bundle; `dist/hooks/dispatch.js` is what actually executes, and a source
699
- * edit without a rebuild is silently inert.
700
- *
701
- * **ADR-251 (2026-08-30) decided this question and KEPT the value.** An AI
702
- * council converged 2/2 on a different authorization SHAPE rather than a wider
703
- * window, against the measured pressure that 7 of the 10 most recent sessions
704
- * of this package outlive a 30-minute window (median span 3.1 h) and that the
705
- * constant has been hand-widened twice. `LEDGER_MAX_AGE_MS` is unchanged at 30
706
- * minutes and raising it remains forbidden — that transition is owner-reserved,
707
- * and this record did not take it.
708
- *
709
- * What the record DID change is the behaviour at expiry: the run PAUSES,
710
- * reports the remaining work, and ASKS for re-authorization, instead of ending.
711
- * The user's reply renews this ledger through the path that already exists —
712
- * `git_authorization_hook.ts` rewrites the session's ledger with a fresh
713
- * `detected_at` on every human-typed prompt (`:512-537`), so a mid-run reply
714
- * carrying an authorization phrase resets the age this constant is compared
715
- * against. No new machinery, and the signal stays agent-unforgeable.
716
- *
717
- * See `docs/decisions/ADR-251-authorization-window-shape-not-width.md`.
718
- */
719
- export const LEDGER_MAX_AGE_MS = 30 * 60 * 1000;
720
-
721
- /**
722
- * Load the ledger, and refuse one that does not belong to THIS session and
723
- * THIS turn.
724
- *
725
- * The header claims the gate answers "did the user authorize this THIS turn?".
726
- * Before this check it answered "did anyone, in any session, ever authorize
727
- * it?" — the file records `session_id` and `detected_at` and neither was read.
728
- * Two concurrent worktrees share one state root, so session A typing
729
- * `mach den release` authorized session B.
730
- */
731
- function _loadLedger(
732
- consumer_root: string,
733
- session_id: string,
734
- now: number,
735
- ): { authorized: Set<GitOp>; present: boolean; grants: MergeGrant[] } {
736
- // Grants are read on their OWN path, and the separation is load-bearing.
737
- // `_readLedgerFile` below discards the entire ledger once `detected_at` is
738
- // older than `LEDGER_MAX_AGE_MS`, so folding grants into its return value
739
- // would let the clock silently kill them too — the change would ship, look
740
- // correct, and do nothing after thirty minutes, with no test going red.
741
- // `readGrants` performs the session check and no age check, which is exactly
742
- // the contract: object identity, not recency (ADR-252).
743
- const grants = readGrants(consumer_root, session_id);
744
- // This session's own ledger first; the flat legacy file only as a fallback,
745
- // so a ledger written by an older build still counts. Reading the flat file
746
- // FIRST was the defect: any concurrent session in the repo overwrites it, and
747
- // the session check below then discarded a real authorization as foreign.
748
- const candidates = [ledgerFileFor(session_id)];
749
- if (!candidates.includes(STATE_FILE)) {
750
- candidates.push(STATE_FILE);
751
- }
752
- for (const rel of candidates) {
753
- const found = _readLedgerFile(path.join(consumer_root, rel), session_id, now);
754
- if (found !== null) {
755
- return { ...found, grants };
756
- }
757
- }
758
- return { authorized: new Set<GitOp>(), present: false, grants };
759
- }
760
-
761
- /**
762
- * Read one ledger file. `null` means "this file does not answer the question" —
763
- * absent, malformed, foreign session, or expired — so the caller may try the
764
- * next candidate. A parsed, in-session, fresh ledger answers definitively.
765
- */
766
- function _readLedgerFile(
767
- file: string,
768
- session_id: string,
769
- now: number,
770
- ): { authorized: Set<GitOp>; present: boolean } | null {
771
- try {
772
- const raw = fs.readFileSync(file, "utf8");
773
- const decoded = JSON.parse(raw) as unknown;
774
- if (_isObject(decoded) && Array.isArray(decoded["authorized"])) {
775
- const ledgerSession = typeof decoded["session_id"] === "string" ? decoded["session_id"] : "";
776
- // A ledger from a different session is another conversation's consent.
777
- if (session_id && ledgerSession && ledgerSession !== session_id) {
778
- return null;
779
- }
780
- const at = Date.parse(String(decoded["detected_at"] ?? ""));
781
- if (Number.isFinite(at) && now - at > LEDGER_MAX_AGE_MS) {
782
- return null;
783
- }
784
- return {
785
- authorized: new Set((decoded["authorized"] as string[]).filter(Boolean) as GitOp[]),
786
- present: true,
787
- };
758
+ export function commandObject(op: GitOp, command: string): string | null {
759
+ const patterns = COMMAND_OBJECTS[op];
760
+ if (!patterns) return null;
761
+ for (const re of patterns) {
762
+ const m = new RegExp(re.source, re.flags.replace("g", "")).exec(command);
763
+ const raw = m?.[1];
764
+ if (raw) {
765
+ const value = raw.trim().replace(/^["'`]+|["'`]+$/g, "").replace(/[.,;:]+$/, "").toLowerCase();
766
+ if (value) return value;
788
767
  }
789
- } catch {
790
- /* fall through */
791
768
  }
792
769
  return null;
793
770
  }
794
771
 
795
- export interface Decision {
796
- exit: number;
797
- stdout: string;
798
- stderr: string;
799
- /**
800
- * The op this decision refused, when it refused one.
801
- *
802
- * `decide` stays pure; `run` turns this into the pending record that lets the
803
- * user's answer to the refusal count as authorization for that one op. See
804
- * `git_authorization_hook.PENDING_FILE` for why that record exists at all.
805
- */
806
- blockedOp?: GitOp;
807
- /**
808
- * The grant target this decision spent, when a standing grant is what allowed
809
- * it.
810
- *
811
- * `decide` stays pure; `run` turns this into the ledger write that marks the
812
- * target used. Single use is the property that keeps a grant from being a
813
- * standing licence: the user authorized merging PR #1499, once.
814
- */
815
- consumedTarget?: number;
816
- }
817
-
818
- export function decide(
819
- command: string | null,
820
- ledger: { authorized: Set<GitOp>; present: boolean; grants?: MergeGrant[] },
821
- impact?: string,
822
- ): Decision {
823
- if (command === null) {
824
- return { exit: EXIT_ALLOW, stdout: "", stderr: "" };
825
- }
826
- const op = commandOp(command);
827
- if (op === null || ledger.authorized.has(op)) {
828
- return { exit: EXIT_ALLOW, stdout: "", stderr: "" };
829
- }
830
-
831
- // A standing grant is the SECOND way an operation can be authorized, and it
832
- // is strictly narrower than the first rather than an escape from it. Only
833
- // `pr-merge` can hold one, the command must name its pull request, and that
834
- // number must sit unspent in a set the user's own sentence froze. An unnamed
835
- // target matches nothing and a spent target matches nothing, so every path
836
- // that is not exactly "the user named this PR and it has not been merged yet"
837
- // falls through to the clock-bound decision below unchanged (ADR-252).
838
- const target = mergeTargetOf(command);
839
- if (grantCovers(ledger.grants ?? [], op, target) && target !== null) {
840
- return { exit: EXIT_ALLOW, stdout: "", stderr: "", consumedTarget: target };
841
- }
842
-
843
- const shown = command.length > 120 ? `${command.slice(0, 117)}…` : command;
844
-
845
- if (BLOCK_OPS.has(op)) {
846
- // STAGE 2. The refusal stands either way — what the scan changes is whether
847
- // the user has to go read the diff to answer it. `impact` is injected by
848
- // `run`, never computed here, so `decide` stays pure and testable without a
849
- // repository.
850
- const scan = op === "pr-merge" && target !== null && impact ? ` ${impact}` : "";
851
- const reason =
852
- `Blocked: \`${op}\` with no authorization in this turn's prompt. ` +
853
- `This operation is irreversible and non-destructive-by-default already declares it ` +
854
- `never-autonomous. Command: ${shown}.${scan} ` +
855
- `Ask the user, in one numbered-options block, and run it only after they answer this turn.`;
856
- return { exit: EXIT_BLOCK, stdout: "", stderr: `${reason}\n`, blockedOp: op };
857
- }
858
-
859
- if (WARN_OPS.has(op)) {
860
- const reason =
861
- `No \`${op}\` authorization found in this turn's prompt (commit-policy: a one-shot ` +
862
- `authorization is spent on the operation it named). If the user did authorize it this ` +
863
- `turn, proceed; otherwise stop and ask. Command: ${shown}`;
864
- return {
865
- exit: EXIT_ALLOW,
866
- stdout: `${JSON.stringify({ decision: "warn", reason })}\n`,
867
- stderr: "",
868
- };
869
- }
772
+ /** Per-operation object patterns on the COMMAND side. */
773
+ const COMMAND_OBJECTS: Readonly<Record<string, readonly RegExp[]>> = {
774
+ "prod-data-destroy": [
775
+ /\bdrop\s+(?:table|database|schema|collection)\s+(?:if\s+exists\s+)?["'`]?([\w.$-]+)/i,
776
+ /\btruncate\s+(?:table\s+)?["'`]?([\w.$-]+)/i,
777
+ /\bdelete\s+from\s+["'`]?([\w.$-]+)/i,
778
+ /\bs3:\/\/([\w.-]+)/i,
779
+ /--table-name[= ]["'`]?([\w.-]+)/i,
780
+ ],
781
+ "external-send": [
782
+ /\bhttps?:\/\/([^\s/"'`]+)/i,
783
+ /([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/,
784
+ ],
785
+ "money-movement": [
786
+ /--customer[= ]["'`]?([\w.-]+)/i,
787
+ /--amount[= ]["'`]?([\d.,]+)/i,
788
+ /\b(\d[\d.,]*)\s*(?:eur|usd)\b/i,
789
+ ],
790
+ };
870
791
 
871
- return { exit: EXIT_ALLOW, stdout: "", stderr: "" };
872
- }
873
-
874
- export function run(stdin_text: string, options: { consumer_root: string }): number {
875
- let envelope: JsonObject = {};
876
- if (stdin_text.trim()) {
877
- try {
878
- const decoded = JSON.parse(stdin_text) as unknown;
879
- if (_isObject(decoded)) {
880
- envelope = decoded;
881
- }
882
- } catch {
883
- return EXIT_ALLOW;
884
- }
885
- }
886
- const session_id = typeof envelope["session_id"] === "string" ? envelope["session_id"] : "";
887
- const command = extractCommand(envelope);
888
- const ledger = _loadLedger(options.consumer_root, session_id, Date.now());
889
-
890
- // STAGE 2 runs only on the path where it can change something: a merge that is
891
- // ABOUT to be refused. Not on an allowed merge (nothing to tell the user), not
892
- // on an op whose target the command does not name, and never on publish or
893
- // release — a scan costs a diff read, and spending one where it cannot change
894
- // the outcome is latency in the user's critical path for nothing.
895
- let impact: string | undefined;
896
- const stage1 = command === null ? null : commandOp(command);
897
- const stage2Target = command === null ? null : mergeTargetOf(command);
898
- if (
899
- stage1 === "pr-merge" &&
900
- stage2Target !== null &&
901
- !ledger.authorized.has("pr-merge") &&
902
- !grantCovers(ledger.grants ?? [], "pr-merge", stage2Target)
903
- ) {
904
- try {
905
- impact = describeImpact(
906
- stage2Target,
907
- analyseMergeImpact(stage2Target, { cwd: options.consumer_root }),
908
- );
909
- } catch {
910
- // The scan is an enrichment, never a gate. A refusal without it is the
911
- // pre-stage-2 message, which is still correct.
912
- }
913
- }
914
-
915
- const decision = decide(command, ledger, impact);
916
- if (decision.consumedTarget !== undefined) {
917
- // Spend the target at the moment the merge is actually let through, never
918
- // when the grant was minted. A grant the run never reached stays whole, and
919
- // a pull request that merged can never be replayed — which is what makes a
920
- // force-push back to an already-merged head harmless here.
921
- consumeGrantTarget(options.consumer_root, session_id, decision.consumedTarget);
922
- }
923
- if (decision.blockedOp) {
924
- // Record WHAT was refused, so the user's answer to the question this
925
- // refusal just told the agent to ask can authorize that one operation.
926
- // Best-effort: a failed write degrades to the previous behaviour, where a
927
- // numbered answer is unrecordable and the refusal simply repeats.
928
- try {
929
- atomic_write_json(path.join(options.consumer_root, pendingFileFor(session_id)), {
930
- op: decision.blockedOp,
931
- session_id,
932
- refused_at: new Date().toISOString(),
933
- });
934
- } catch {
935
- /* observability only — see above */
936
- }
937
- }
938
- if (decision.stdout) {
939
- process.stdout.write(decision.stdout);
940
- }
941
- if (decision.stderr) {
942
- process.stderr.write(decision.stderr);
943
- }
944
- return decision.exit;
945
- }
946
-
947
- export function main(argv?: string[]): number {
948
- const args = argv ?? process.argv.slice(2);
949
- let consumer_root = process.cwd();
950
- for (let i = 0; i < args.length; i += 1) {
951
- const a = args[i];
952
- if (a === "--project-dir" && args[i + 1] !== undefined) {
953
- consumer_root = args[i + 1] as string;
954
- i += 1;
955
- } else if (a !== undefined && a.startsWith("--project-dir=")) {
956
- consumer_root = a.slice("--project-dir=".length);
957
- }
958
- }
959
- return run(readHookStdin(), { consumer_root });
960
- }
961
-
962
- declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
963
- function _isCliEntry(): boolean {
964
- if (typeof __AGENT_CONFIG_BUNDLE__ !== "undefined" && __AGENT_CONFIG_BUNDLE__) {
965
- return false;
966
- }
967
- if (process.argv[1] === undefined) {
968
- return false;
969
- }
970
- const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
971
- if (import.meta.url === argvUrl) {
972
- return true;
973
- }
974
- try {
975
- const here = fs.realpathSync(fileURLToPath(import.meta.url));
976
- const argv = fs.realpathSync(path.resolve(process.argv[1]));
977
- return here === argv;
978
- } catch {
979
- return false;
980
- }
981
- }
982
-
983
- if (_isCliEntry()) {
984
- process.exit(main());
792
+ /** A target only counts inside the API's own range; anything else is no target. */
793
+ function _boundedTarget(digits: string): number | null {
794
+ const n = Number.parseInt(digits, 10);
795
+ return Number.isSafeInteger(n) && n > 0 && n <= PR_NUMBER_MAX ? n : null;
985
796
  }