@event4u/agent-config 14.18.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 (146) 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 +169 -130
  5. package/README.md +71 -149
  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/review/changes.md +19 -19
  9. package/dist/agent-src/commands/review.md +2 -2
  10. package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
  11. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  12. package/dist/agent-src/rules/git-history-discipline.md +2 -2
  13. package/dist/agent-src/rules/guidelines.md +1 -1
  14. package/dist/agent-src/rules/session-canary.md +2 -2
  15. package/dist/agent-src/rules/token-efficiency.md +1 -1
  16. package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
  17. package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
  18. package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
  19. package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
  20. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
  21. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
  22. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
  23. package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
  24. package/dist/agent-src/skills/design-review/SKILL.md +1 -1
  25. package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
  26. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  27. package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
  28. package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
  29. package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
  30. package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
  31. package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
  32. package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
  33. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  34. package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
  35. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  36. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
  37. package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
  38. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
  39. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  40. package/dist/cli/commands/recordTriggerEval.js +2 -2
  41. package/dist/cli/main.js +1 -1
  42. package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
  43. package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
  44. package/dist/cli-delegate/cmd_adr_effective.js +318 -81
  45. package/dist/cli-delegate/cmd_conformance.js +420 -16
  46. package/dist/cli-delegate/cmd_doctor.js +1 -1
  47. package/dist/cli-delegate/cmd_export.js +1 -1
  48. package/dist/cli-delegate/cmd_handoff.js +1 -1
  49. package/dist/cli-delegate/cmd_prune.js +1 -1
  50. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  51. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  52. package/dist/cli-delegate/cmd_sync.js +1 -1
  53. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  54. package/dist/cli-delegate/cmd_validate.js +1 -1
  55. package/dist/discovery/deprecation-report.md +1 -1
  56. package/dist/discovery/discovery-manifest.json +28 -27
  57. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  58. package/dist/discovery/discovery-manifest.summary.md +1 -1
  59. package/dist/discovery/dormancy-report.md +1 -1
  60. package/dist/discovery/orphan-report.md +1 -1
  61. package/dist/discovery/packs.json +2 -2
  62. package/dist/discovery/trust-report.md +1 -1
  63. package/dist/discovery/workspaces.json +2 -2
  64. package/dist/hooks/dispatch.js +2132 -1462
  65. package/dist/install/install.mjs +588 -527
  66. package/dist/mcp/registry-manifest.json +1 -1
  67. package/dist/mcp/server.json +2 -2
  68. package/dist/mcp/server.mjs +1619 -1045
  69. package/dist/scripts/_lib/model_tier.js +1 -1
  70. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
  71. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  72. package/docs/guidelines/design-antipatterns.md +87 -64
  73. package/package.json +1 -1
  74. package/src/agent-src/README.md +1 -1
  75. package/src/config/ci-local-parity.yml +6 -0
  76. package/src/config/gate-coverage.yml +170 -0
  77. package/src/config/gate-violation-baselines.json +3 -3
  78. package/src/config/release-gate-locality.yml +12 -0
  79. package/src/scripts/_cli/cmd_doctor.ts +2 -2
  80. package/src/scripts/_cli/cmd_handoff.ts +1 -1
  81. package/src/scripts/_cli/cmd_update.ts +1 -1
  82. package/src/scripts/_cli/cmd_versions.ts +1 -1
  83. package/src/scripts/_lib/adr_frontmatter.ts +44 -0
  84. package/src/scripts/_lib/consequence_objects.ts +152 -0
  85. package/src/scripts/_lib/exec_evidence.ts +7 -0
  86. package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
  87. package/src/scripts/_lib/host_permission_checks.ts +251 -0
  88. package/src/scripts/_lib/model_tier.ts +1 -1
  89. package/src/scripts/_lib/obligation_frequency.ts +2 -2
  90. package/src/scripts/_lib/release_highlights.ts +93 -0
  91. package/src/scripts/_lib/release_material.ts +64 -1
  92. package/src/scripts/_lib/runtime_journal.ts +26 -3
  93. package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
  94. package/src/scripts/_lib/session_register.ts +11 -6
  95. package/src/scripts/_lib/sqlite_schema.ts +22 -0
  96. package/src/scripts/_lib/surface.ts +86 -0
  97. package/src/scripts/_lib/ui_authority.ts +116 -0
  98. package/src/scripts/adr/regenerate_index.ts +54 -1
  99. package/src/scripts/adr_cite_check.ts +71 -15
  100. package/src/scripts/ai_council/clients.ts +1 -1
  101. package/src/scripts/autonomy_friction_corpus.ts +229 -0
  102. package/src/scripts/build_proof.ts +15 -2
  103. package/src/scripts/check_beta_review_markers.ts +141 -17
  104. package/src/scripts/check_enforcement_coverage.ts +1 -1
  105. package/src/scripts/check_enforcement_denominator.ts +121 -11
  106. package/src/scripts/check_finding_dispositions.ts +66 -9
  107. package/src/scripts/check_gate_coverage.ts +96 -0
  108. package/src/scripts/check_release_highlights.ts +74 -1
  109. package/src/scripts/check_skill_activation_claim.ts +179 -0
  110. package/src/scripts/check_trigger_evals.ts +1 -1
  111. package/src/scripts/conformance_scan.ts +44 -3
  112. package/src/scripts/explain_run.ts +1 -1
  113. package/src/scripts/generate_capability_matrix.ts +1 -1
  114. package/src/scripts/git_authorization_hook.ts +75 -1
  115. package/src/scripts/hook_manifest.json +1 -1
  116. package/src/scripts/hook_manifest.yaml +14 -11
  117. package/src/scripts/hooks/augment-dispatcher.sh +1 -1
  118. package/src/scripts/hooks/category_a.ts +291 -0
  119. package/src/scripts/hooks/cline-dispatcher.sh +1 -1
  120. package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
  121. package/src/scripts/hooks/dispatch_hook.ts +17 -22
  122. package/src/scripts/hooks/git_command_classifier.ts +156 -1
  123. package/src/scripts/hooks/host_lowering.ts +215 -0
  124. package/src/scripts/hooks/host_lowering.yaml +185 -0
  125. package/src/scripts/hooks/host_semantics.ts +105 -37
  126. package/src/scripts/hooks/journal_record_hook.ts +10 -0
  127. package/src/scripts/hooks/permission_gate.ts +117 -0
  128. package/src/scripts/hooks_status.ts +12 -10
  129. package/src/scripts/install.ts +53 -82
  130. package/src/scripts/install_anthropic_key.sh +2 -2
  131. package/src/scripts/lint_design_antipattern_parity.ts +80 -0
  132. package/src/scripts/lint_evidence_artifacts.ts +45 -1
  133. package/src/scripts/lint_hook_manifest.ts +87 -3
  134. package/src/scripts/lint_motion_authority_drift.ts +731 -0
  135. package/src/scripts/lint_readme_size.ts +158 -12
  136. package/src/scripts/lint_reasoning_fixtures.ts +216 -0
  137. package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
  138. package/src/scripts/measure_sibling_naming.ts +150 -0
  139. package/src/scripts/redact_hook_capture.ts +1 -1
  140. package/src/scripts/report_skill_activation.ts +319 -7
  141. package/src/scripts/schemas/review-findings.schema.json +5 -0
  142. package/src/scripts/schemas/skill.schema.json +2 -2
  143. package/src/scripts/session_canary_hook.ts +1 -1
  144. package/src/scripts/session_register_hook.ts +4 -3
  145. package/src/scripts/trigger_eval_grandfather.json +5 -1
  146. package/src/scripts/validate_agent_settings.ts +1 -1
@@ -113,6 +113,21 @@ export interface GateSpec {
113
113
  * `pending` is reported instead of hidden.
114
114
  */
115
115
  unavailable_exit?: number;
116
+ /**
117
+ * Repo-relative workflow file this row's `argv` is pinned to.
118
+ *
119
+ * Rule 2 of this manifest — CI-IDENTICAL INVOCATION — was an instruction to
120
+ * the author and nothing else: the guard runs whatever `argv` says and never
121
+ * looked at how CI calls the gate, so a row could drift from its workflow, or
122
+ * the workflow step could be deleted outright, and this file would keep
123
+ * reporting healthy coverage for a gate CI no longer runs.
124
+ *
125
+ * Opt-in per row, because most gates here have several CI invocations with
126
+ * different arguments and one `argv` cannot be identical to all of them. Where
127
+ * a row DOES name a workflow, that workflow must contain an invocation of the
128
+ * gate whose argument list equals `argv` exactly.
129
+ */
130
+ ci_invocation?: string;
116
131
  note?: string;
117
132
  }
118
133
 
@@ -191,6 +206,9 @@ export function load_manifest(file = MANIFEST): GateSpec[] {
191
206
  ...(e['unavailable_exit'] === undefined
192
207
  ? {}
193
208
  : { unavailable_exit: _require_int(e['unavailable_exit'], id, i) }),
209
+ ...(e['ci_invocation'] === undefined
210
+ ? {}
211
+ : { ci_invocation: _require_str(e['ci_invocation'], 'ci_invocation', id, i) }),
194
212
  ...(e['note'] === undefined ? {} : { note: String(e['note']) }),
195
213
  ...(e['canary'] === undefined ? {} : { canary: _require_canary(e['canary'], id, i) }),
196
214
  ...(e['no_canary_reason'] === undefined
@@ -405,6 +423,73 @@ function report_negative_control_inventory(specs: readonly GateSpec[]): void {
405
423
  }
406
424
  }
407
425
 
426
+ /**
427
+ * Invocations of `src/scripts/<id>` in a workflow, each as its argument list.
428
+ *
429
+ * Deliberately a text scan rather than a YAML walk: an invocation lives inside a
430
+ * `run:` block scalar, so parsing the workflow buys nothing and costs the ability
431
+ * to see a call split across a line continuation, which is how the release job
432
+ * writes this very gate. Backslash-newline joins are folded before matching for
433
+ * that reason.
434
+ */
435
+ export function ci_invocations(workflowText: string, id: string): string[][] {
436
+ const folded = workflowText.replace(/\\\n\s*/gu, ' ');
437
+ const out: string[][] = [];
438
+ const re = new RegExp(String.raw`src/scripts/${id}(?![A-Za-z0-9_])([^\n]*)`, 'gu');
439
+ for (const m of folded.matchAll(re)) {
440
+ out.push(
441
+ (m[1] ?? '')
442
+ .trim()
443
+ .split(/\s+/u)
444
+ .filter((t) => t !== ''),
445
+ );
446
+ }
447
+ return out;
448
+ }
449
+
450
+ /**
451
+ * The row's `argv` must be reproducible from the workflow it pins.
452
+ *
453
+ * Both directions are failures and they are different defects: no matching
454
+ * invocation means the manifest probes the gate in a way CI does not (or CI
455
+ * stopped calling it at all), which is the drift rule 2 asks for and nothing
456
+ * enforced.
457
+ */
458
+ export function ci_invocation_problem(
459
+ workflowText: string,
460
+ id: string,
461
+ argv: readonly string[],
462
+ ): string | null {
463
+ const found = ci_invocations(workflowText, id);
464
+ if (found.length === 0) {
465
+ return `no invocation of src/scripts/${id} — the workflow does not call this gate`;
466
+ }
467
+ const want = argv.join(' ');
468
+ if (found.some((a) => a.join(' ') === want)) {
469
+ return null;
470
+ }
471
+ return (
472
+ `argv [${want}] matches no invocation there; the workflow calls it as ` +
473
+ found.map((a) => `[${a.join(' ')}]`).join(', ')
474
+ );
475
+ }
476
+
477
+ /** Rows whose `ci_invocation` no longer describes their workflow. */
478
+ export function ci_invocation_drift(specs: readonly GateSpec[], repoRoot = REPO_ROOT): string[] {
479
+ const problems: string[] = [];
480
+ for (const s of specs) {
481
+ if (s.ci_invocation === undefined) continue;
482
+ const file = path.join(repoRoot, s.ci_invocation);
483
+ if (!fs.existsSync(file)) {
484
+ problems.push(`${s.id}: ci_invocation ${s.ci_invocation} does not exist`);
485
+ continue;
486
+ }
487
+ const problem = ci_invocation_problem(fs.readFileSync(file, 'utf-8'), s.id, s.argv);
488
+ if (problem !== null) problems.push(`${s.id}: ${s.ci_invocation} — ${problem}`);
489
+ }
490
+ return problems;
491
+ }
492
+
408
493
  export function main(argv: readonly string[]): number {
409
494
  const wantJson = argv.includes('--format') && argv[argv.indexOf('--format') + 1] === 'json';
410
495
  const listOnly = argv.includes('--list');
@@ -523,12 +608,23 @@ export function main(argv: readonly string[]): number {
523
608
  );
524
609
  }
525
610
  report_negative_control_inventory(specs);
611
+ const drift = ci_invocation_drift(specs);
612
+ for (const d of drift) {
613
+ process.stdout.write(` ❌ ${d}\n`);
614
+ }
526
615
  const hardening = report_hardening_ratchet();
527
616
  const selfTest = report_self_test_ratchet();
528
617
  if (failed.length > 0) {
529
618
  process.stdout.write(`❌ ${String(failed.length)} gate(s) failed the coverage floor.\n`);
530
619
  return 1;
531
620
  }
621
+ if (drift.length > 0) {
622
+ process.stdout.write(
623
+ `❌ ${String(drift.length)} row(s) declare a ci_invocation their workflow no longer matches — ` +
624
+ `the manifest probes the gate differently from CI, or CI stopped calling it.\n`,
625
+ );
626
+ return 1;
627
+ }
532
628
  if (hardening !== 0) {
533
629
  return hardening;
534
630
  }
@@ -72,13 +72,17 @@ import {
72
72
  derive_categories,
73
73
  parse_git_log,
74
74
  mix_response_blockers,
75
+ previous_promise,
75
76
  previous_release_tag,
77
+ promise_readback_blockers,
76
78
  stale_draft_labels,
77
79
  } from './_lib/release_highlights.js';
78
80
  import { loadTaxonomy, measureRange } from './measure_release_mix.js';
79
81
  import {
80
82
  CURATED_HEAD_INSTRUCTION,
83
+ PROMISE_READBACK_MARKER,
81
84
  extract_changelog_section,
85
+ previous_changelog_version,
82
86
  } from './_lib/release_material.js';
83
87
  import { assertScanned, assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
84
88
 
@@ -207,7 +211,8 @@ export function main(argv: readonly string[]): number {
207
211
  }
208
212
  throw err;
209
213
  }
210
- const section = extract_changelog_section(fs.readFileSync(changelogPath, 'utf-8'), version);
214
+ const changelogText = fs.readFileSync(changelogPath, 'utf-8');
215
+ const section = extract_changelog_section(changelogText, version);
211
216
  if (!section) {
212
217
  process.stderr.write(`CHANGELOG carries no section for ${version}\n`);
213
218
  return 2;
@@ -251,6 +256,11 @@ export function main(argv: readonly string[]): number {
251
256
  return mix;
252
257
  }
253
258
 
259
+ const readback = check_previous_promise_readback(changelogText, section.body, version);
260
+ if (readback !== 0) {
261
+ return readback;
262
+ }
263
+
254
264
  const curated = parse_curated_head(section.body);
255
265
  if (!curated) {
256
266
  process.stdout.write(`ℹ️ no curated head in the ${version} section — nothing to check\n`);
@@ -417,6 +427,69 @@ export function check_governance_mix_response(
417
427
  return 1;
418
428
  }
419
429
 
430
+ /**
431
+ * The previous release's promise, read back at the next release.
432
+ *
433
+ * `_lib/release_material.ts` generates `Next cycle ships …` and, until this
434
+ * check existed, nothing ever asked what became of it: 14.18.0 promised the MCP
435
+ * bridge repair, and no gate in the tree would have noticed the same promise
436
+ * being reprinted in 14.19.0 and 14.20.0. A commitment nothing reads back is
437
+ * free to restate, and one that is free to restate is not a commitment.
438
+ *
439
+ * Only the section under release is read, and only the previous section's
440
+ * governance-response block — the same scope every other check here uses, so a
441
+ * historical section cannot red a future release.
442
+ *
443
+ * The check is about ANSWERING, never about having kept the promise. A gate
444
+ * that refused an unkept promise would price honesty above silence and teach
445
+ * the next author to promise nothing.
446
+ */
447
+ export function check_previous_promise_readback(
448
+ changelogText: string,
449
+ body: string,
450
+ version: string,
451
+ ): number {
452
+ const previousVersion = previous_changelog_version(changelogText, version);
453
+ if (previousVersion === null) {
454
+ process.stdout.write(`✅ no previous CHANGELOG section before ${version} — no promise to read back\n`);
455
+ return 0;
456
+ }
457
+ const promise = previous_promise(changelogText, previousVersion);
458
+ if (promise === null) {
459
+ process.stdout.write(
460
+ `✅ the ${previousVersion} section made no next-cycle promise — nothing owed by ${version}\n`,
461
+ );
462
+ return 0;
463
+ }
464
+ const blockers = promise_readback_blockers(
465
+ body,
466
+ version,
467
+ previousVersion,
468
+ promise,
469
+ `\`release/${version}\``,
470
+ );
471
+ if (blockers.length === 0) {
472
+ process.stdout.write(`✅ ${version} answers the ${previousVersion} next-cycle promise\n`);
473
+ return 0;
474
+ }
475
+ process.stderr.write(
476
+ `❌ the ${version} section owes an answer to the ${previousVersion} next-cycle promise.\n` +
477
+ blockers.map((b) => ` - ${b}\n`).join('') +
478
+ ` The ${previousVersion} promise reads:\n` +
479
+ promise
480
+ .split('\n')
481
+ .map((l) => ` ${l}\n`)
482
+ .join('') +
483
+ ` Answering it is one blockquote line under the curated head, e.g.\n` +
484
+ ` > ${PROMISE_READBACK_MARKER} the <promise> did not ship; <what happened>.\n` +
485
+ ' Shipped, did not ship, or withdrawn with a reason — the check refuses a MISSING\n' +
486
+ ' answer, never a particular outcome. An unkept promise answered honestly passes.\n' +
487
+ ' Reproduce locally, before any push:\n' +
488
+ ` ./scripts-run src/scripts/check_release_highlights --version ${version}\n`,
489
+ );
490
+ return 1;
491
+ }
492
+
420
493
  const _isMain = (() => {
421
494
  const entry = process.argv[1];
422
495
  if (!entry) return false;
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * The published skill-activation figure agrees with the record it came from.
4
+ *
5
+ * WHY THIS IS A SEPARATE SCRIPT FROM THE CENSUS
6
+ * ---------------------------------------------
7
+ * `report_skill_activation` is a REPORT: it gates on nothing, has no threshold,
8
+ * and its own docstring says it must never acquire one. This is a GATE, and the
9
+ * two must not share an entry point — `src/config/gate-coverage.yml` is a
10
+ * manifest of gates, and `_lib/gate_population.ts` classifies a script by its
11
+ * prefix precisely so that "is this a gate" is answered structurally rather
12
+ * than by whoever last edited a config. Registering a `report_*` id there forks
13
+ * that population, which `tests/scripts/gate_population.test.ts` refuses.
14
+ *
15
+ * So the split is not cosmetic. The report keeps its exit-0 contract; the
16
+ * assertion that CAN fail lives here, where a floor and a self-test belong.
17
+ *
18
+ * WHAT IT CANNOT DO, STATED SO NOBODY READS IT AS COVERED
19
+ * ------------------------------------------------------
20
+ * It compares two COMMITTED files — the census record against the sentence
21
+ * `docs/CLAIMS.md` publishes. It cannot tell whether the record still describes
22
+ * the world: the transcript store it was taken from is one machine's, keyed on
23
+ * the directory the census ran in, and absent from CI. Re-taking the
24
+ * measurement is `report_skill_activation --emit`, which refuses to write from
25
+ * an empty store. This gate only forbids the published sentence and the record
26
+ * from telling two different stories.
27
+ *
28
+ * Exit: 0 agree · 1 disagree or a file is missing.
29
+ */
30
+
31
+ import * as fs from 'node:fs';
32
+ import * as os from 'node:os';
33
+ import * as path from 'node:path';
34
+ import { fileURLToPath, pathToFileURL } from 'node:url';
35
+
36
+ import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
37
+ import { reportScanned } from './_lib/scan_scope.js';
38
+ import {
39
+ CLAIMS_PATH,
40
+ CLAIM_ID,
41
+ RECORD_PATH,
42
+ claimBlock,
43
+ claimProblems,
44
+ parseClaimFigures,
45
+ readRecord,
46
+ } from './report_skill_activation.js';
47
+
48
+ const _HERE = fileURLToPath(import.meta.url);
49
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
50
+
51
+ export function check(root: string): number {
52
+ const rec = readRecord(root);
53
+ if (rec === null) {
54
+ process.stderr.write(`❌ ${RECORD_PATH} is missing — the published figure has no record behind it.\n`);
55
+ return 1;
56
+ }
57
+ const claimsFile = path.join(root, CLAIMS_PATH);
58
+ if (!fs.existsSync(claimsFile)) {
59
+ process.stderr.write(`❌ ${CLAIMS_PATH} is missing.\n`);
60
+ return 1;
61
+ }
62
+ const block = claimBlock(fs.readFileSync(claimsFile, 'utf8'), CLAIM_ID);
63
+ // Emitted before the verdict on both paths: a gate that reports what it
64
+ // inspected only when it passes leaves the coverage census blind exactly when
65
+ // it matters.
66
+ reportScanned({
67
+ gate: 'check_skill_activation_claim',
68
+ scanned: block === null ? 0 : 1,
69
+ units: 'published claim block(s)',
70
+ roots: [CLAIMS_PATH],
71
+ });
72
+ if (block === null) {
73
+ process.stderr.write(`❌ claim \`${CLAIM_ID}\` is not in ${CLAIMS_PATH}.\n`);
74
+ return 1;
75
+ }
76
+ const problems = claimProblems(rec, parseClaimFigures(block));
77
+ if (problems.length > 0) {
78
+ process.stderr.write('❌ the published census claim and its record disagree:\n');
79
+ for (const p of problems) {
80
+ process.stderr.write(` · ${p}\n`);
81
+ }
82
+ process.stderr.write(
83
+ `\nRe-take the measurement (report_skill_activation --emit) and update the claim from ${RECORD_PATH}.\n`,
84
+ );
85
+ return 1;
86
+ }
87
+ process.stdout.write(`✅ census claim matches ${RECORD_PATH} (measured ${rec.measured_at}).\n`);
88
+ return 0;
89
+ }
90
+
91
+ /**
92
+ * The gate proving it still discriminates.
93
+ *
94
+ * The third case is the one a value comparison alone would miss: a claim that
95
+ * silently stops stating a figure would otherwise pass every equality test it
96
+ * no longer participates in.
97
+ */
98
+ function selfTest(): number {
99
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'activation-claim-selftest-'));
100
+ const record = {
101
+ schema_version: 1,
102
+ measured_at: '2026-01-02',
103
+ skills_shipped: 300,
104
+ with_trigger_key: 10,
105
+ with_trigger_corpus: 100,
106
+ trigger_key_and_corpus: 5,
107
+ human_named_only: 195,
108
+ sessions: 7,
109
+ assistant_turns: 1234,
110
+ invocations: 0,
111
+ distinct_skills_invoked: 0,
112
+ stores: ['fixture'],
113
+ };
114
+ const agreeing =
115
+ '`report_skill_activation` over 7 sessions and 1,234 assistant turns records 0 Skill ' +
116
+ 'invocations and 0 of 300 distinct skills (measured 2026-01-02). 10 declare a ' +
117
+ 'machine-matchable trigger key in frontmatter, 100 carry an `evals/triggers.json` corpus, ' +
118
+ '5 do both, and the remaining 195 are reachable only by a human naming them.';
119
+ const write = (name: string, body: string): string => {
120
+ const root = path.join(tmp, name);
121
+ fs.mkdirSync(path.join(root, path.dirname(RECORD_PATH)), { recursive: true });
122
+ fs.mkdirSync(path.join(root, path.dirname(CLAIMS_PATH)), { recursive: true });
123
+ fs.writeFileSync(path.join(root, RECORD_PATH), JSON.stringify(record));
124
+ fs.writeFileSync(
125
+ path.join(root, CLAIMS_PATH),
126
+ `# Claims\n\n### claim: ${CLAIM_ID}\n- claim: ${body}\n- kind: quant\n- status: backed\n- last_verified: 2026-01-02\n\n### claim: unrelated\n- claim: x\n`,
127
+ );
128
+ return root;
129
+ };
130
+ const run = (root: string): number =>
131
+ runGateCli(REPO_ROOT, 'src/scripts/check_skill_activation_claim.ts', ['--root', root], root);
132
+ try {
133
+ return runSelfTest({
134
+ gate: 'check_skill_activation_claim',
135
+ minCases: 4,
136
+ minRejectCases: 3,
137
+ cases: [
138
+ {
139
+ name: 'a turn count that moved away from the record is rejected',
140
+ expect: 'reject',
141
+ run: () => run(write('turns', agreeing.replace('1,234 assistant', '9,999 assistant'))),
142
+ },
143
+ {
144
+ name: 'a population redrawn to enlarge the human-named remainder is rejected',
145
+ expect: 'reject',
146
+ run: () => run(write('population', agreeing.replace('remaining 195 are', 'remaining 290 are'))),
147
+ },
148
+ {
149
+ name: 'a claim that stops stating the population split is rejected',
150
+ expect: 'reject',
151
+ run: () => run(write('dropped', agreeing.slice(0, agreeing.indexOf('. 10 declare') + 1))),
152
+ },
153
+ {
154
+ name: 'a claim reproducing every recorded figure passes',
155
+ expect: 'accept',
156
+ run: () => run(write('agreeing', agreeing)),
157
+ },
158
+ ],
159
+ });
160
+ } finally {
161
+ fs.rmSync(tmp, { recursive: true, force: true });
162
+ }
163
+ }
164
+
165
+ export function main(argv: readonly string[]): number {
166
+ if (argv.includes('--help') || argv.includes('-h')) {
167
+ process.stdout.write('usage: check_skill_activation_claim [--root DIR] [--self-test]\n');
168
+ return 0;
169
+ }
170
+ if (argv.includes('--self-test')) {
171
+ return selfTest();
172
+ }
173
+ const i = argv.indexOf('--root');
174
+ return check(i === -1 ? REPO_ROOT : (argv[i + 1] ?? REPO_ROOT));
175
+ }
176
+
177
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
178
+ process.exit(main(process.argv.slice(2)));
179
+ }
@@ -150,7 +150,7 @@ function _check_one(p: string, today: PyDate): string[] {
150
150
  if (age > MAX_AGE_DAYS) {
151
151
  errors.push(
152
152
  `${rel}: \`last_eval\` ${raw as string} is ${age}d old (> ${MAX_AGE_DAYS}d) ` +
153
- '— re-run skill_trigger_eval.py and bump it',
153
+ '— re-run skill_trigger_eval and bump it',
154
154
  );
155
155
  }
156
156
  }
@@ -53,8 +53,13 @@ import { entryText, isSidechain } from "./_lib/transcript_entry.js";
53
53
 
54
54
  import { classify } from "./language_mirror_hook.js";
55
55
  import { isSyntheticPrompt } from "./_lib/prompt_shape.js";
56
- import { classifyAuthorization, type GitOp } from "./git_authorization_hook.js";
57
- import { BLOCK_OPS, commandOp } from "./hooks/git_command_classifier.js";
56
+ import {
57
+ classifyAuthorization,
58
+ extractObjects,
59
+ OBJECT_REQUIRED_OPS,
60
+ type GitOp,
61
+ } from "./git_authorization_hook.js";
62
+ import { BLOCK_OPS, commandObject, commandOp } from "./hooks/git_command_classifier.js";
58
63
  import { isVacuousOutput, isCiPoll, pendingCount } from "./before_complete_hook.js";
59
64
  // Round 7 § 1.5 — the SAME predicate `turn-end-gate` refuses on, imported rather
60
65
  // than reimplemented, so the measured rate and the gate cannot disagree.
@@ -579,6 +584,8 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
579
584
  let pinned: "de" | "en" | null = null;
580
585
  let pendingPoll: { at: string; cmd: string } | null = null;
581
586
  let authorized = new Set<GitOp>();
587
+ // Per-op objects the current turn's prompt named. Reset with `authorized`.
588
+ let authorizedObjects = new Map<GitOp, Set<string>>();
582
589
  let sawPending = false;
583
590
  let evaluationsThisTurn = 0;
584
591
  // Round-5 provenance for language violations. The round-5 audit could only
@@ -723,7 +730,15 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
723
730
  if (c.language !== "und") {
724
731
  pinned = c.language;
725
732
  }
726
- authorized = new Set(classifyAuthorization(ut).authorized);
733
+ const classified = classifyAuthorization(ut);
734
+ authorized = new Set(classified.authorized);
735
+ // Step 2.2 — the objects the turn NAMED, per operation. Rebuilt on every
736
+ // counted user turn for the same reason `authorized` is: an authorization
737
+ // for one table does not carry into the next prompt any more than the
738
+ // operation does.
739
+ authorizedObjects = new Map(
740
+ classified.authorized.map((op) => [op, new Set(extractObjects(op, ut))]),
741
+ );
727
742
  sawPending = false;
728
743
  evaluationsThisTurn = 0;
729
744
  pendingPoll = null;
@@ -811,6 +826,32 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
811
826
  at,
812
827
  detail: `irreversible \`${op}\` with no authorization in the turn's prompt: ${cmd.slice(0, 110)}`,
813
828
  });
829
+ } else if (op !== null && authorized.has(op) && OBJECT_REQUIRED_OPS.has(op)) {
830
+ // Step 2.2 — the operation WAS authorized and ran against something
831
+ // the turn never named. An authorization for one table is not an
832
+ // authorization for another, and until the ledger could carry an
833
+ // object this was indistinguishable from a clean turn.
834
+ //
835
+ // This REPORTS and refuses nothing — ADR-254 removed the enforcing
836
+ // reader, and the shape it preserved is a count after the fact.
837
+ //
838
+ // A command naming NO object produces no finding: `commandObject`
839
+ // returns null there, and a guessed object would manufacture a
840
+ // violation out of a parse failure. Under-reporting is the safe
841
+ // direction for a measurement.
842
+ const object = commandObject(op, cmd);
843
+ const named = authorizedObjects.get(op);
844
+ if (object !== null && !(named?.has(object) ?? false)) {
845
+ report.violations.push({
846
+ check: "git-authorization",
847
+ session: sessionId,
848
+ at,
849
+ detail:
850
+ `\`${op}\` authorized, but its object \`${object}\` is not one the ` +
851
+ `turn's prompt named (${named && named.size > 0 ? [...named].join(", ") : "no object named"}): ` +
852
+ cmd.slice(0, 110),
853
+ });
854
+ }
814
855
  }
815
856
  if (isCiPoll(cmd)) {
816
857
  // The RESULT of this call arrives on the NEXT entry, and that entry is
@@ -690,7 +690,7 @@ function renderHygieneSection(hygiene: HygieneReadResult): string[] {
690
690
  out.push(`_Source: tried ${hygiene.checkedPaths.map((p) => `\`${p}\``).join(', then ')}_`);
691
691
  out.push('');
692
692
  if (hygiene.state === null) {
693
- out.push('no data — none of the candidate paths exist yet (no PostToolUse hook has run this session, or this host has no hook surface for it)');
693
+ out.push('no data — none of the candidate paths exist yet (no PostToolUse hook has run this session, or no PostToolUse hook is bound on this host)');
694
694
  return out;
695
695
  }
696
696
  const s = hygiene.state;
@@ -136,7 +136,7 @@ const _FN_SPEC: Record<string, FnSpec> = {
136
136
  // Surfaces the INSTALLER provides outside generate_tools() (so the derivation
137
137
  // guard stays pure). Marked with a † footnote in the rendered matrix.
138
138
  const _INSTALL_TIME_CELLS: Record<string, Record<string, string>> = {
139
- rules: { copilot: 'adapter' }, // .github/copilot-instructions.md (aggregated, install.py)
139
+ rules: { copilot: 'adapter' }, // .github/copilot-instructions.md (aggregated, the installer)
140
140
  };
141
141
 
142
142
  // Python: re.compile(r"\b(generate_[A-Za-z0-9_]+)\s*\(").
@@ -61,6 +61,10 @@ import { fileURLToPath, pathToFileURL } from "node:url";
61
61
 
62
62
  import { humanTypedThisTurn as _humanTypedThisTurn } from "./_lib/machine_wake.js";
63
63
 
64
+ import {
65
+ CONSEQUENCE_PHRASES,
66
+ extractObjects,
67
+ } from "./_lib/consequence_objects.js";
64
68
  import { atomic_write_json } from "./hooks/state_io.js";
65
69
  import { readHookStdin } from "./hooks/hook_stdin.js";
66
70
 
@@ -235,7 +239,36 @@ export type GitOp =
235
239
  /** Deleting a branch, locally or on the remote. */
236
240
  | "branch-delete"
237
241
  /** Closing a pull request or an issue. */
238
- | "close";
242
+ | "close"
243
+
244
+ // ── Consequence operations, added 2026-09-06
245
+ // (`road-to-authorization-that-reaches-further` 2.1).
246
+ //
247
+ // The boundaries the owner named had NO vocabulary at all, so an
248
+ // authorization the user gave in plain words was invisible to the record and
249
+ // the matching operation was invisible to the count. These seven are NOT git
250
+ // operations, and the union's name is now narrower than its contents — a
251
+ // rename touches every consumer and buys nothing the doc comments do not.
252
+ //
253
+ // RECORDING AN AUTHORIZATION IS NOT GRANTING ONE. After ADR-254 there is no
254
+ // grantor left in this package: the ledger concern is advisory by manifest
255
+ // and the only consumer that acts on this vocabulary is the after-the-fact
256
+ // count in `conformance_scan`. Nothing here refuses, and nothing here may be
257
+ // read as a step toward refusing.
258
+ /** Deploying to a production environment. */
259
+ | "prod-deploy"
260
+ /** Destroying production data — a table, a bucket, a collection, rows. */
261
+ | "prod-data-destroy"
262
+ /** Changing production infrastructure — cluster, IAM, DNS, network. */
263
+ | "prod-infra-change"
264
+ /** Sending something outward that a third party receives. */
265
+ | "external-send"
266
+ /** Moving money — a charge, a refund, a payout, a transfer. */
267
+ | "money-movement"
268
+ /** Force-pushing a shared trunk, as distinct from a feature branch. */
269
+ | "trunk-force-push"
270
+ /** Destroying work outside the task's own scope. */
271
+ | "out-of-scope-destruction";
239
272
 
240
273
  export const ALL_OPS: readonly GitOp[] = [
241
274
  "commit",
@@ -264,6 +297,13 @@ export const ALL_OPS: readonly GitOp[] = [
264
297
  "stash-drop",
265
298
  "branch-delete",
266
299
  "close",
300
+ "prod-deploy",
301
+ "prod-data-destroy",
302
+ "prod-infra-change",
303
+ "external-send",
304
+ "money-movement",
305
+ "trunk-force-push",
306
+ "out-of-scope-destruction",
267
307
  ];
268
308
 
269
309
  /**
@@ -432,6 +472,8 @@ const PHRASES: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
432
472
  op: "pr-merge-auto",
433
473
  re: /\bauto[-\s]?merge\b|\bmerge\b[^.\n]{0,20}\bautomatisch\b|\bautomatisch\b[^.\n]{0,20}\bmerg(e|en)\b/i,
434
474
  },
475
+
476
+ ...CONSEQUENCE_PHRASES,
435
477
  ];
436
478
 
437
479
  /**
@@ -615,6 +657,25 @@ export interface Ledger extends JsonObject {
615
657
  * as "no grants", which is the pre-ADR-252 behaviour exactly.
616
658
  */
617
659
  grants?: MergeGrant[];
660
+ /**
661
+ * The objects a consequence authorization named, per operation.
662
+ *
663
+ * An authorization for one table, one environment or one recipient class is
664
+ * not an authorization for another, and until now the record could not
665
+ * express the difference for anything but pull-request numbers
666
+ * (`MergeGrant.targets`). This is that distinction generalised to the
667
+ * consequence vocabulary.
668
+ *
669
+ * IT IS A RECORDED FIELD, NOT A MATCHING CONDITION. Nothing consults it to
670
+ * refuse anything; the one consumer is `conformance_scan`, which counts
671
+ * after the fact when an executed operation's object is absent here. Making
672
+ * it a matching condition is the ledger item ADR-254 left to the owner, and
673
+ * it is deliberately not taken here.
674
+ *
675
+ * Optional, so a ledger written by an older build still parses. Absent reads
676
+ * as "no objects named", which is exactly the pre-2.2 behaviour.
677
+ */
678
+ objects?: { [op: string]: string[] };
618
679
  }
619
680
 
620
681
  /**
@@ -1281,6 +1342,7 @@ export function takePending(
1281
1342
  * file for the discriminator and the captured evidence behind it.
1282
1343
  */
1283
1344
  export { humanTypedThisTurn } from "./_lib/machine_wake.js";
1345
+ export { extractObjects, OBJECT_REQUIRED_OPS } from "./_lib/consequence_objects.js";
1284
1346
 
1285
1347
  export function run(stdin_text: string, options: { consumer_root: string }): number {
1286
1348
  let envelope: JsonObject = {};
@@ -1332,6 +1394,17 @@ export function run(stdin_text: string, options: { consumer_root: string }): num
1332
1394
  `"${prompt.trim().slice(0, 40)}"`;
1333
1395
  }
1334
1396
 
1397
+ // The objects the prose named, per authorized operation. Recorded for every
1398
+ // consequence op that names one; `OBJECT_REQUIRED_OPS` is the subset whose
1399
+ // ABSENCE the conformance count reads as a finding, and that reading lives in
1400
+ // the scan rather than here — this side records what was said, and never
1401
+ // decides what it permits.
1402
+ const objects: { [op: string]: string[] } = {};
1403
+ for (const op of authorized) {
1404
+ const named = extractObjects(op, prompt);
1405
+ if (named.length > 0) objects[op] = named;
1406
+ }
1407
+
1335
1408
  const now = new Date();
1336
1409
  const ledger: Ledger = {
1337
1410
  session_id: typeof envelope["session_id"] === "string" ? envelope["session_id"] : "",
@@ -1339,6 +1412,7 @@ export function run(stdin_text: string, options: { consumer_root: string }): num
1339
1412
  authorized,
1340
1413
  evidence,
1341
1414
  prompt_chars: prompt.length,
1415
+ objects,
1342
1416
  // Grants survive this write; `authorized` does not. That asymmetry IS the
1343
1417
  // decision. A bare operation name stays one-shot exactly as `commit-policy`
1344
1418
  // requires, and only an authorization that also froze its objects earns a