@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
@@ -25,7 +25,7 @@
25
25
  * (measured: ADR-035 still asserts a rejection ADR-232 reopened).
26
26
  * - **review_trigger** — verbatim, plus a state.
27
27
  *
28
- * `indeterminate` is a FIRST-CLASS trigger result, not a failure. Every
28
+ * `indeterminate` is a FIRST-CLASS trigger result, not a failure. Almost every
29
29
  * `review_trigger` in the corpus is a semantic condition ("reopen when the
30
30
  * capacity premise changes", "if a fifth band appears"), so forcing them to a
31
31
  * boolean would convert uncertainty into either permission or blockage. Both
@@ -33,6 +33,29 @@
33
33
  * trigger grammar as a prerequisite for exactly this reason. An `indeterminate`
34
34
  * result means: this may not be presented as an unqualified lock — route it.
35
35
  *
36
+ * THE DATED SUB-CLASS, AND WHY THE 2026-08-19 REJECTION DOES NOT REACH IT
37
+ * ----------------------------------------------------------------------
38
+ * The sentence above used to open "Every `review_trigger` in the corpus is a
39
+ * semantic condition"; it is weakened rather than deleted because the claim was
40
+ * true when written and one record has since falsified it.
41
+ *
42
+ * What those two seats rejected was forcing SEMANTIC conditions to a boolean.
43
+ * A trigger that OPENS with a calendar date is not one: `fired` / `not-fired`
44
+ * is decided by comparing that date to `asOf()`, which is arithmetic and
45
+ * carries no interpretation of the tree, so a second reader of the same commit
46
+ * gets the same answer. That is the entire carve-out — a leading
47
+ * `Expiry YYYY-MM-DD`, or a bare leading ISO date, and nothing else. A
48
+ * condition that merely MENTIONS a date somewhere in its prose ("revisit at the
49
+ * 2026-11-10 review date, or earlier on either observable event") is a semantic
50
+ * condition and stays `indeterminate`, which is what keeps this from being the
51
+ * rejected grammar by increments.
52
+ *
53
+ * `dated-unparsed` exists so the carve-out cannot fail silently. A trigger that
54
+ * announces itself as dated and whose date this parser cannot read is a defect
55
+ * in the record or in the parser; returning `indeterminate` for it would hide
56
+ * that defect behind the exact state every trigger had before the carve-out
57
+ * existed, where no test could ever see it as a regression.
58
+ *
36
59
  * It also prints the two descriptive axes (`provenance`, `evidence`) and
37
60
  * `authority_basis`, and for an accepted E0/E1 record that does NOT carry
38
61
  * `authority_basis: owner_intent` it prints
@@ -73,12 +96,16 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
73
96
  import {
74
97
  type AdrFrontmatter,
75
98
  authorityBasisOf,
99
+ dated_trigger_day,
100
+ DATED_TRIGGER_PROBE,
101
+ iso_day,
76
102
  evidenceOf,
77
103
  isLowEvidenceAccepted,
78
104
  provenanceOf,
79
105
  readAdrFrontmatter,
80
106
  readAdrFrontmatterScalars,
81
107
  } from './_lib/adr_frontmatter.js';
108
+ import { asOf } from './_lib/as_of.js';
82
109
  import { GateLedger } from './_lib/gate_ledger.js';
83
110
  import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
84
111
  import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
@@ -149,8 +176,12 @@ export const SURFACES_NOT_SCANNED = [
149
176
  */
150
177
  export const PARTIAL_COVERAGE: readonly string[] = [];
151
178
 
152
- /** A trigger state. `none` means the ADR never recorded a reopen condition. */
153
- export type TriggerState = 'none' | 'indeterminate' | 'fired' | 'not-fired';
179
+ /**
180
+ * A trigger state. `none` means the ADR never recorded a reopen condition;
181
+ * `dated-unparsed` means it recorded one that announces a date this parser
182
+ * could not read, which is a defect and not an answer.
183
+ */
184
+ export type TriggerState = 'none' | 'indeterminate' | 'fired' | 'not-fired' | 'dated-unparsed';
154
185
 
155
186
  export interface CiteResult {
156
187
  ref: string;
@@ -469,12 +500,21 @@ export function amendment_blocks(body: string): string[] {
469
500
  /**
470
501
  * A trigger's state, from what is mechanically decidable and nothing else.
471
502
  *
472
- * There is no clause that returns `fired` or `not-fired` from prose, and that
473
- * absence is the design: every trigger in the corpus is semantic. The states
474
- * exist in the type because a future structured trigger can carry them; today
475
- * the honest answers are `none` and `indeterminate`.
503
+ * Two clauses return a decided answer, and both read a leading calendar date
504
+ * and nothing else — see `DATED_TRIGGER_RE` for why the anchor is the safety
505
+ * property. Every semantic trigger still returns `indeterminate`, which is a
506
+ * first-class result here and not a failure.
507
+ *
508
+ * The clock is read ONLY when a dated trigger is actually present — `asOf()`
509
+ * warns on stderr for an unpinned run, and a corpus with no dated trigger has
510
+ * no verdict that depends on the hour, so announcing one there would be noise
511
+ * on a stream other tools read beside the payload.
512
+ *
513
+ * @param asOfDate the "now" to compare against; defaults to the `asOf()` seam,
514
+ * so a CI run compares against the commit date and is
515
+ * reproducible from the commit alone.
476
516
  */
477
- export function trigger_state(fm: Record<string, string>): TriggerState {
517
+ export function trigger_state(fm: Record<string, string>, asOfDate?: Date): TriggerState {
478
518
  const t = (fm['review_trigger'] ?? '').trim();
479
519
  if (t === '') return 'none';
480
520
  // The transitional migration value records that no condition has been
@@ -485,6 +525,9 @@ export function trigger_state(fm: Record<string, string>): TriggerState {
485
525
  // nobody has evaluated. Found in completion review; the field did not
486
526
  // exist in this file's corpus when the mapping was written.
487
527
  if (t.toLowerCase() === 'unclassified') return 'none';
528
+ const day = dated_trigger_day(t);
529
+ if (day !== null) return iso_day(asOfDate ?? asOf()) >= day ? 'fired' : 'not-fired';
530
+ if (DATED_TRIGGER_PROBE.test(t)) return 'dated-unparsed';
488
531
  return 'indeterminate';
489
532
  }
490
533
 
@@ -530,6 +573,15 @@ export function verdict_for(r: Omit<CiteResult, 'verdict'>): string {
530
573
  if (r.trigger_state === 'none') {
531
574
  return 'LIVE, NO REOPEN CONDITION — the ADR records no `review_trigger`, so nothing would ever reopen it on its own. Treat that as a defect in the ADR, not as strength of the lock.';
532
575
  }
576
+ if (r.trigger_state === 'fired') {
577
+ return 'LIVE, TRIGGER FIRED — the reopen condition is a date and that date has passed. The record still binds until someone acts on it, and citing it without naming the fired trigger presents a lapsed review as a current one.';
578
+ }
579
+ if (r.trigger_state === 'not-fired') {
580
+ return 'LIVE, TRIGGER NOT YET FIRED — the reopen condition is a date still in the future. This is the one state in which a trigger genuinely adds nothing to the citation, and it stops being true on a known day.';
581
+ }
582
+ if (r.trigger_state === 'dated-unparsed') {
583
+ return 'LIVE, DATED TRIGGER UNREADABLE — the reopen condition announces a date this tool could not parse. That is a defect in the record or in the parser, not a verdict: fix the trigger to open with `Expiry YYYY-MM-DD` rather than reading this as "unknown".';
584
+ }
533
585
  return 'LIVE, TRIGGER INDETERMINATE — the reopen condition is semantic and this tool cannot decide it. Not an unqualified lock: evaluate the condition against the current tree and route the result.';
534
586
  }
535
587
 
@@ -823,21 +875,25 @@ export interface CorpusRow {
823
875
  /** Does the record carry a `review_trigger` at all? */
824
876
  has_trigger: boolean;
825
877
  /** Only meaningful when `has_trigger`; see `TRIGGER_VERDICTS`. */
826
- trigger: 'fired' | 'not-fired' | 'indeterminate' | 'none';
878
+ trigger: TriggerState;
827
879
  reopen_policy: string;
828
880
  /** Cited anywhere OUTSIDE `docs/decisions/`. */
829
881
  cited_outside: boolean;
830
882
  }
831
883
 
832
884
  /**
833
- * The three states 2.2 requires, and why a fourth exists beside them.
885
+ * The states a record CARRYING a trigger can be in, and why `none` is not one.
886
+ *
887
+ * `none` is outside the denominator, not a bucket inside it: the survey asks
888
+ * that these sum to "the number of ADRs carrying a trigger", and a record with
889
+ * no trigger carries none.
834
890
  *
835
- * `none` is NOT one of the three. The step asks that fired + not-fired +
836
- * indeterminate sum to "the number of ADRs carrying a trigger", so a record
837
- * with no trigger is outside the denominator rather than a fourth bucket
838
- * inside it.
891
+ * `dated-unparsed` was added with the dated sub-class and belongs in the sum
892
+ * for the same reason `none` does not — a record in that state DOES carry a
893
+ * trigger. Leaving it out would have made the sum line report a mismatch, i.e.
894
+ * announced a defect in the survey where the defect is in the ADR.
839
895
  */
840
- export const TRIGGER_VERDICTS = ['fired', 'not-fired', 'indeterminate'] as const;
896
+ export const TRIGGER_VERDICTS = ['fired', 'not-fired', 'indeterminate', 'dated-unparsed'] as const;
841
897
 
842
898
  /** Every ADR file, surveyed. */
843
899
  export function corpus_survey(repo_root: string = REPO_ROOT): CorpusRow[] {
@@ -1,7 +1,7 @@
1
1
  // External-AI clients for the council — ported from the retired Python
2
2
  // `src/scripts/ai_council/clients.py` (ADR-200).
3
3
  //
4
- // Mirrors the contract from `scripts/skill_trigger_eval.py`:
4
+ // Mirrors the contract from `src/scripts/skill_trigger_eval.ts`:
5
5
  // - Tokens come exclusively from `~/.event4u/agent-config/<provider>.key`
6
6
  // (legacy `~/.config/agent-config/<provider>.key` is read as a fallback so
7
7
  // pre-2.4 installs keep working until the user moves the files into the new
@@ -0,0 +1,229 @@
1
+ /**
2
+ * autonomy_friction_corpus — how many host confirmations each payload class
3
+ * costs, measured through the real dispatcher.
4
+ *
5
+ * WHY A CONFIRMATION COUNT AND NOT A GATE COUNT. "Three gates fire" says
6
+ * nothing about what the operator experiences; "this call costs one
7
+ * confirmation" is the quantity the round is about. A corpus counting gates
8
+ * would go green on a change that fired fewer gates and prompted just as often.
9
+ *
10
+ * WHAT IT MEASURES, STATED NARROWLY. The number of confirmations THIS PACKAGE
11
+ * leaves on the table for a call — 0 when the dispatcher hands the host an
12
+ * explicit `permissionDecision: allow`, 1 when it emits no permission field and
13
+ * the host's own machinery decides. It is NOT a measurement of what the host
14
+ * finally does: a consumer whose settings already allow the call is not
15
+ * prompted either way, and a confined session ignores our allow entirely
16
+ * (`permissionDecision=allow ignored: a confined session takes grants only
17
+ * from its command line`). Reading this as a host-behaviour measurement is the
18
+ * fact-about-our-plumbing-written-as-a-fact-about-the-host collapse the hook
19
+ * contract warns about three times.
20
+ *
21
+ * A REFUSAL IS NOT A CONFIRMATION. A case the dispatcher blocks (exit 2) is
22
+ * reported as `refused` and scores no confirmations — counting a refusal as
23
+ * friction would reward removing a gate, which is the opposite of the point.
24
+ *
25
+ * Usage:
26
+ * ./scripts-run src/scripts/autonomy_friction_corpus [--json]
27
+ * ./agent-config … (no verb — this is a package-internal measurement)
28
+ */
29
+ import { spawnSync } from "node:child_process";
30
+ import * as fs from "node:fs";
31
+ import * as os from "node:os";
32
+ import * as path from "node:path";
33
+ import process from "node:process";
34
+ import { fileURLToPath } from "node:url";
35
+
36
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
37
+ const REPO_ROOT = path.resolve(HERE, "..", "..");
38
+ const ENTRY = path.join(REPO_ROOT, "src", "scripts", "hooks", "dispatch_entry.ts");
39
+ const TSX_BIN = path.join(
40
+ REPO_ROOT,
41
+ "node_modules",
42
+ ".bin",
43
+ process.platform === "win32" ? "tsx.cmd" : "tsx",
44
+ );
45
+
46
+ /** One payload class, with the confirmations it is expected to cost. */
47
+ export interface FrictionCase {
48
+ id: string;
49
+ /** The class the owner named, in their words. */
50
+ klass: string;
51
+ tool: string;
52
+ input: Record<string, unknown>;
53
+ /** Confirmations this call SHOULD cost. */
54
+ expected: number;
55
+ /** Why that number, in one line. */
56
+ because: string;
57
+ }
58
+
59
+ /** One measured result. */
60
+ export interface FrictionResult extends FrictionCase {
61
+ observed: number;
62
+ refused: boolean;
63
+ pass: boolean;
64
+ }
65
+
66
+ /**
67
+ * The six classes the round named, plus the directory-flag form.
68
+ *
69
+ * The seventh is not padding: cases 1 and 2 are the shape the canon stopped
70
+ * teaching, and without the shape that replaced them the corpus would record
71
+ * the cost and not the fix.
72
+ */
73
+ export const CASES: readonly FrictionCase[] = [
74
+ {
75
+ id: "directory-change",
76
+ klass: "directory change",
77
+ tool: "Bash",
78
+ input: { command: "cd sub" },
79
+ expected: 1,
80
+ because:
81
+ "a bare `cd` is not category A — the canon's answer is a directory flag, " +
82
+ "not an allow for changing directory",
83
+ },
84
+ {
85
+ id: "directory-change-compounded",
86
+ klass: "directory change compounded with a status read",
87
+ tool: "Bash",
88
+ input: { command: "cd sub && git status" },
89
+ expected: 1,
90
+ because:
91
+ "a compound command is refused before its argv is read — a safe head " +
92
+ "token must not be able to carry a second command",
93
+ },
94
+ {
95
+ id: "directory-flag-status",
96
+ klass: "the directory-flag form of the case above",
97
+ tool: "Bash",
98
+ input: { command: "git -C sub status" },
99
+ expected: 0,
100
+ because: "one simple command, a read-only git subcommand, inside the working tree",
101
+ },
102
+ {
103
+ id: "test-runner-in-subdirectory",
104
+ klass: "a test runner invoked from a subdirectory",
105
+ tool: "Bash",
106
+ input: { command: "vitest run sub" },
107
+ expected: 0,
108
+ because: "a test runner admitted by name — the name IS the operation",
109
+ },
110
+ {
111
+ id: "workspace-package-manager",
112
+ klass: "a workspace package manager",
113
+ tool: "Bash",
114
+ input: { command: "npm run test" },
115
+ expected: 0,
116
+ because: "a declared subcommand with a declared script name",
117
+ },
118
+ {
119
+ id: "consequence-authorized",
120
+ klass: "a consequence operation with the authorizing phrase present",
121
+ tool: "Bash",
122
+ input: { command: "terraform apply" },
123
+ expected: 1,
124
+ because:
125
+ "the phrase changes the RECORD, never the prompt — after ADR-254 the " +
126
+ "Hard Floor is model-held and this package grants nothing",
127
+ },
128
+ {
129
+ id: "consequence-unauthorized",
130
+ klass: "the same operation with the authorizing phrase absent",
131
+ tool: "Bash",
132
+ input: { command: "terraform apply" },
133
+ expected: 1,
134
+ because:
135
+ "identical to the case above BY DESIGN — that the two are the same " +
136
+ "number is the finding, not a defect in the corpus",
137
+ },
138
+ ];
139
+
140
+ /** Run one case through the real dispatcher and count its confirmations. */
141
+ export function measure(c: FrictionCase, workspace: string): FrictionResult {
142
+ const payload = JSON.stringify({
143
+ session_id: "autonomy-friction-corpus",
144
+ cwd: workspace,
145
+ hook_event_name: "PreToolUse",
146
+ tool_name: c.tool,
147
+ tool_input: c.input,
148
+ });
149
+ const r = spawnSync(
150
+ TSX_BIN,
151
+ [ENTRY, "--platform", "claude", "--event", "pre_tool_use", "--project-dir", workspace],
152
+ {
153
+ encoding: "utf-8",
154
+ cwd: workspace,
155
+ input: payload,
156
+ timeout: 120_000,
157
+ env: { ...process.env, AGENT_CONFIG_PACKAGE_ROOT: REPO_ROOT },
158
+ },
159
+ );
160
+ const refused = r.status === 2;
161
+ const allowed = !refused && (r.stdout ?? "").includes('"permissionDecision":"allow"');
162
+ const observed = refused || allowed ? 0 : 1;
163
+ return { ...c, observed, refused, pass: refused || observed === c.expected };
164
+ }
165
+
166
+ export function runCorpus(): FrictionResult[] {
167
+ const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "ac-friction-"));
168
+ fs.mkdirSync(path.join(workspace, "sub"), { recursive: true });
169
+ try {
170
+ return CASES.map((c) => measure(c, workspace));
171
+ } finally {
172
+ fs.rmSync(workspace, { recursive: true, force: true });
173
+ }
174
+ }
175
+
176
+ export function render(results: readonly FrictionResult[]): string {
177
+ const out: string[] = [];
178
+ out.push("autonomy_friction_corpus — confirmations per payload class");
179
+ out.push("");
180
+ out.push(" measured through the real dispatcher; a confirmation is one this");
181
+ out.push(" package leaves to the host, NOT one the host necessarily shows.");
182
+ out.push("");
183
+ for (const r of results) {
184
+ const mark = r.pass ? "✅" : "❌";
185
+ const obs = r.refused ? "refused" : String(r.observed);
186
+ out.push(` ${mark} ${r.id.padEnd(30)} expected ${r.expected} observed ${obs}`);
187
+ out.push(` ${r.klass}`);
188
+ out.push(` ${r.because}`);
189
+ }
190
+ const failures = results.filter((r) => !r.pass);
191
+ out.push("");
192
+ out.push(
193
+ failures.length === 0
194
+ ? ` ${results.length} case(s), every observation matches its expectation`
195
+ : ` ${failures.length} of ${results.length} case(s) miss their expectation: ` +
196
+ failures.map((f) => f.id).join(", "),
197
+ );
198
+ return out.join("\n") + "\n";
199
+ }
200
+
201
+ export function main(argv: string[] = process.argv.slice(2)): number {
202
+ const json = argv.includes("--json");
203
+ if (argv.includes("--help") || argv.includes("-h")) {
204
+ process.stdout.write(
205
+ "usage: autonomy_friction_corpus [--json]\n" +
206
+ " Reports the confirmations each payload class costs, measured\n" +
207
+ " through the real dispatcher. Exit 1 when an observation misses\n" +
208
+ " its expectation.\n",
209
+ );
210
+ return 0;
211
+ }
212
+ const results = runCorpus();
213
+ process.stdout.write(json ? `${JSON.stringify(results, null, 2)}\n` : render(results));
214
+ return results.every((r) => r.pass) ? 0 : 1;
215
+ }
216
+
217
+ function _isCliEntry(): boolean {
218
+ const argv1 = process.argv[1];
219
+ if (argv1 === undefined) return false;
220
+ try {
221
+ return fs.realpathSync(argv1) === fs.realpathSync(fileURLToPath(import.meta.url));
222
+ } catch {
223
+ return false;
224
+ }
225
+ }
226
+
227
+ if (_isCliEntry()) {
228
+ process.exit(main());
229
+ }
@@ -413,10 +413,20 @@ function render(): string {
413
413
  {
414
414
  const rows = collectEnforcement();
415
415
  const s = summariseEnforcement(rows);
416
+ // The split ships WITH the figure, and the word `yet` travels only with
417
+ // the half it is true of. `kernel_denied` rules cannot carry an
418
+ // `enforced_by` field at all — `block_kernel_rule_writes` refuses the
419
+ // write with no agent-accessible override — so publishing them as "not
420
+ // declared yet" promises a future that does not exist for them. They
421
+ // still count as uncovered — the counting policy is unchanged; only the
422
+ // claim about their future is.
416
423
  L.push(
417
424
  `**Axis 1 — enforcement level per rule.** ${s.total} rules · ` +
418
425
  `${s.blocking} blocking (${s.blocking_pct}%) · ${s.observer} observer · ` +
419
- `${s.local_only} local-only · ${s.undeclared} undeclared (no \`enforced_by\` yet).`,
426
+ `${s.local_only} local-only · ${s.undeclared} undeclared — of which ` +
427
+ `${s.kernel_denied} kernel-denied (\`block_kernel_rule_writes\` refuses an ` +
428
+ `\`enforced_by\` write on a kernel rule, so no declaration is reachable ` +
429
+ `for them at all) and ${s.undeclared - s.kernel_denied} not declared yet.`,
420
430
  );
421
431
  L.push('');
422
432
  // The denominator, WITH the frame that produced it. Until 2026-08-23 the
@@ -436,7 +446,10 @@ function render(): string {
436
446
  L.push(`| \`${r.id}\` | ${r.effective} | ${decl} |`);
437
447
  }
438
448
  L.push('');
439
- L.push(`Undeclared rules (${s.undeclared}) carry no row — an honest gap beats a false claim.`);
449
+ L.push(
450
+ `Undeclared rules (${s.undeclared}, of which ${s.kernel_denied} kernel-denied) ` +
451
+ 'carry no row — an honest gap beats a false claim.',
452
+ );
440
453
  }
441
454
  L.push('');
442
455
  {