@event4u/agent-config 9.7.0 → 9.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +144 -0
  3. package/README.md +5 -5
  4. package/dist/agent-src/commands/commit/in-chunks.md +8 -0
  5. package/dist/agent-src/commands/commit.md +17 -0
  6. package/dist/agent-src/commands/pr/create.md +4 -0
  7. package/dist/agent-src/contexts/override-system.md +75 -5
  8. package/dist/agent-src/rules/context-hygiene.md +2 -0
  9. package/dist/agent-src/rules/doc-screenshot-hygiene.md +97 -0
  10. package/dist/agent-src/rules/domain-safety-pii.md +1 -0
  11. package/dist/agent-src/rules/external-code-graph-interop.md +13 -3
  12. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
  13. package/dist/agent-src/rules/git-history-discipline.md +2 -0
  14. package/dist/agent-src/rules/language-and-tone.md +2 -0
  15. package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
  16. package/dist/agent-src/rules/media-governance-routing.md +2 -0
  17. package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
  18. package/dist/agent-src/rules/no-roadmap-references.md +3 -0
  19. package/dist/agent-src/rules/non-destructive-by-default.md +20 -0
  20. package/dist/agent-src/rules/onboarding-gate.md +2 -0
  21. package/dist/agent-src/rules/output-discipline.md +2 -0
  22. package/dist/agent-src/rules/persona-governance.md +2 -0
  23. package/dist/agent-src/rules/preservation-guard.md +3 -0
  24. package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
  25. package/dist/agent-src/rules/secret-vcs-guard.md +106 -0
  26. package/dist/agent-src/rules/security-sensitive-stop.md +1 -1
  27. package/dist/agent-src/rules/skill-quality.md +2 -0
  28. package/dist/agent-src/rules/source-confidentiality.md +2 -0
  29. package/dist/agent-src/rules/source-of-truth.md +2 -0
  30. package/dist/agent-src/rules/telegraph-speak.md +2 -0
  31. package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
  32. package/dist/agent-src/rules/tool-safety.md +1 -0
  33. package/dist/agent-src/rules/verify-before-complete.md +2 -0
  34. package/dist/agent-src/skills/adr-create/SKILL.md +17 -1
  35. package/dist/agent-src/skills/code-intelligence/SKILL.md +94 -0
  36. package/dist/agent-src/skills/gated-reach/SKILL.md +185 -0
  37. package/dist/agent-src/skills/gated-reach/evals/triggers.json +72 -0
  38. package/dist/agent-src/skills/image-analyser/SKILL.md +14 -12
  39. package/dist/agent-src/skills/image-editing/SKILL.md +1 -0
  40. package/dist/agent-src/skills/readme-writing/SKILL.md +6 -1
  41. package/dist/agent-src/skills/screenshot-hygiene/SKILL.md +171 -0
  42. package/dist/agent-src/skills/secrets-management/SKILL.md +44 -1
  43. package/dist/agent-src/skills/subagent-orchestration/prompts/adversarial-verification-council.md +29 -0
  44. package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge-two-stage.md +29 -0
  45. package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge.md +29 -0
  46. package/dist/agent-src/skills/subagent-orchestration/prompts/do-competitively.md +29 -0
  47. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-parallel.md +29 -0
  48. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-steps.md +29 -0
  49. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-worktrees.md +29 -0
  50. package/dist/agent-src/skills/subagent-orchestration/prompts/judge-with-debate.md +29 -0
  51. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +4 -0
  52. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  53. package/dist/agent-src/templates/github-workflows/secret-scan.yml +65 -0
  54. package/dist/cli/configRoot.js +112 -0
  55. package/dist/cli/configRoot.js.map +1 -0
  56. package/dist/cli/configRoot.test.js +97 -0
  57. package/dist/cli/configRoot.test.js.map +1 -0
  58. package/dist/cli/help.js +4 -1
  59. package/dist/cli/help.js.map +1 -1
  60. package/dist/cli/main.js +19 -2
  61. package/dist/cli/main.js.map +1 -1
  62. package/dist/cli/registry.js +1 -0
  63. package/dist/cli/registry.js.map +1 -1
  64. package/dist/discovery/deprecation-report.md +1 -1
  65. package/dist/discovery/discovery-manifest.json +161 -44
  66. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  67. package/dist/discovery/discovery-manifest.summary.md +4 -4
  68. package/dist/discovery/orphan-report.md +1 -1
  69. package/dist/discovery/packs.json +13 -8
  70. package/dist/discovery/trust-report.md +11 -11
  71. package/dist/discovery/workspaces.json +11 -6
  72. package/dist/install/install.mjs +16 -0
  73. package/dist/mcp/registry-manifest.json +3 -3
  74. package/dist/router.json +1 -1
  75. package/dist/server/app.js +10 -0
  76. package/dist/server/app.js.map +1 -1
  77. package/dist/server/routes/ping.js +18 -0
  78. package/dist/server/routes/ping.js.map +1 -1
  79. package/dist/server/schemas/settings.js +8 -0
  80. package/dist/server/schemas/settings.js.map +1 -1
  81. package/dist/server/serverInfo.js +6 -2
  82. package/dist/server/serverInfo.js.map +1 -1
  83. package/dist/server/token.js +11 -4
  84. package/dist/server/token.js.map +1 -1
  85. package/dist/server/writeRoot.js +15 -1
  86. package/dist/server/writeRoot.js.map +1 -1
  87. package/dist/shared/capabilities.js +33 -0
  88. package/dist/shared/capabilities.js.map +1 -0
  89. package/dist/shared/capabilities.test.js +39 -0
  90. package/dist/shared/capabilities.test.js.map +1 -0
  91. package/dist/ui/assets/{index-BiEZiZ31.js → index-CrmZ34Rg.js} +16 -16
  92. package/dist/ui/assets/index-CrmZ34Rg.js.map +1 -0
  93. package/dist/ui/index.html +14 -5
  94. package/docs/CLAIMS.md +62 -37
  95. package/docs/architecture.md +2 -2
  96. package/docs/benchmark.md +137 -0
  97. package/docs/catalog.md +8 -3
  98. package/docs/comparison.yaml +44 -2
  99. package/docs/contracts/local-server-ports.md +53 -0
  100. package/docs/contracts/no-runtime-boundary.md +1 -1
  101. package/docs/contracts/subagent-boundary.md +15 -0
  102. package/docs/customization.md +14 -3
  103. package/docs/decisions/ADR-016-installer-architecture.md +2 -0
  104. package/docs/decisions/ADR-088-no-external-runtime-federation.md +1 -1
  105. package/docs/decisions/ADR-094-agent-memory-layer-removal.md +1 -1
  106. package/docs/decisions/ADR-109-subagent-v1-contract.md +2 -0
  107. package/docs/decisions/ADR-110-discipline-profile-resolution-locus.md +2 -0
  108. package/docs/decisions/ADR-113-ci-native-release-label-trigger.md +11 -3
  109. package/docs/decisions/ADR-124-embedded-engine-doctrine.md +255 -0
  110. package/docs/decisions/ADR-125-doc-screenshot-anonymization.md +131 -0
  111. package/docs/decisions/ADR-126-internet-reach-operator-tooling.md +254 -0
  112. package/docs/decisions/ADR-127-enforcement-claims-must-resolve.md +150 -0
  113. package/docs/decisions/ADR-128-executable-evidence-and-measured-escalation.md +201 -0
  114. package/docs/decisions/INDEX.md +21 -0
  115. package/docs/decisions/engine-reclassification-2026-07.md +104 -0
  116. package/docs/distribution/awesome-list-submission.md +1 -1
  117. package/docs/distribution/registries.md +36 -27
  118. package/docs/enforcement-by-host.md +7 -0
  119. package/docs/featured-skills.md +2 -2
  120. package/docs/getting-started-by-role.md +1 -1
  121. package/docs/getting-started.md +1 -1
  122. package/docs/governance-advantage.md +1 -1
  123. package/docs/guides/gated-platform-reads.md +221 -0
  124. package/docs/positioning-evidence.md +1 -1
  125. package/docs/proof.md +92 -16
  126. package/docs/threat-model.md +1 -0
  127. package/package.json +5 -2
  128. package/src/config/agent-settings.template.yml +43 -0
  129. package/src/config/reach-channels.yml +217 -0
  130. package/src/config/reach-prescriptions-intake.yml +200 -0
  131. package/src/scripts/_dispatch.bash +15 -0
  132. package/src/scripts/_lib/exec_evidence.ts +251 -0
  133. package/src/scripts/_lib/fixtures/secret-detector/negatives.txt +10 -0
  134. package/src/scripts/_lib/fixtures/secret-detector/positives.txt +11 -0
  135. package/src/scripts/_lib/kernel_rules.ts +46 -0
  136. package/src/scripts/_lib/reddit_thread_parse.ts +598 -0
  137. package/src/scripts/_lib/risk_paths.ts +160 -0
  138. package/src/scripts/_lib/secret_detector.test.ts +103 -0
  139. package/src/scripts/_lib/secret_detector.ts +319 -0
  140. package/src/scripts/_lib/tool_probe.ts +359 -0
  141. package/src/scripts/adr/regenerate_index.ts +81 -6
  142. package/src/scripts/audit_adr_coverage.ts +33 -0
  143. package/src/scripts/build_proof.ts +162 -0
  144. package/src/scripts/check_adr_frontmatter.ts +195 -0
  145. package/src/scripts/check_artefact_count_messaging.ts +4 -0
  146. package/src/scripts/check_backstop_debt.ts +200 -0
  147. package/src/scripts/check_claims.ts +188 -8
  148. package/src/scripts/check_comparison.ts +12 -0
  149. package/src/scripts/check_enforcement_coverage.ts +592 -0
  150. package/src/scripts/check_kernel_rule_bundle.ts +16 -12
  151. package/src/scripts/check_reach_channels.ts +407 -0
  152. package/src/scripts/check_reach_staleness.ts +364 -0
  153. package/src/scripts/check_secret_leak.test.ts +47 -0
  154. package/src/scripts/check_secret_leak.ts +290 -0
  155. package/src/scripts/code_graph/build.ts +0 -0
  156. package/src/scripts/code_graph/cli.ts +207 -0
  157. package/src/scripts/code_graph/detect.ts +134 -0
  158. package/src/scripts/code_graph/extract.ts +386 -0
  159. package/src/scripts/code_graph/loader.ts +123 -0
  160. package/src/scripts/code_graph/query.ts +176 -0
  161. package/src/scripts/code_graph/sanitize.ts +30 -0
  162. package/src/scripts/code_graph/types.ts +83 -0
  163. package/src/scripts/code_graph/validate.ts +63 -0
  164. package/src/scripts/external_sources_denylist.json +3 -1
  165. package/src/scripts/generate_subagent_floor.ts +194 -0
  166. package/src/scripts/hook_manifest.yaml +13 -3
  167. package/src/scripts/hooks/code_graph_nudge_hook.ts +184 -0
  168. package/src/scripts/iron_law_sha.ts +3 -12
  169. package/src/scripts/lint_override_kernel_guard.ts +219 -0
  170. package/src/scripts/measure_rule_budget.ts +3 -12
  171. package/src/scripts/postinstall_gui.ts +129 -0
  172. package/src/scripts/reach_doctor.ts +1651 -0
  173. package/src/scripts/release.ts +24 -3
  174. package/src/scripts/risk_escalation_shadow.ts +160 -0
  175. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  176. package/src/scripts/schemas/reach-channels.schema.json +134 -0
  177. package/src/scripts/schemas/reach-doctor-payload.schema.json +220 -0
  178. package/src/scripts/schemas/rule.schema.json +10 -0
  179. package/src/scripts/skill_linter.ts +55 -0
  180. package/src/scripts/validate_reach_prescriptions.ts +751 -0
  181. package/src/server/schemas/settings.ts +16 -0
  182. package/src/shared/capabilities.test.ts +43 -0
  183. package/src/shared/capabilities.ts +75 -0
  184. package/src/templates/minimal/agents-overrides-readme.md +20 -0
  185. package/dist/ui/assets/index-BiEZiZ31.js.map +0 -1
@@ -41,6 +41,105 @@ class ExitCode extends Error {
41
41
  }
42
42
  }
43
43
 
44
+ /**
45
+ * The prevented-failure table.
46
+ *
47
+ * Every row names a **failure mode that is prevented**, never a feature. That
48
+ * constraint is the whole discipline: "cross-platform" is a feature and says
49
+ * nothing; "the same rules do not silently differ between the agent you run
50
+ * today and the one you switch to" names what goes wrong without it.
51
+ *
52
+ * Every row cites a ledger claim, and `backs` is not decoration — a cited id
53
+ * that is missing or not `backed` throws, so the table cannot outlive its
54
+ * evidence. This is the guard against the failure it was modelled on: the
55
+ * reference ships an equivalent table whose headline efficiency figure appears
56
+ * only there, an order of magnitude below the cheapest scenario in its own
57
+ * generated benchmark. A number with nowhere to resolve is exactly what this
58
+ * ledger exists to catch, so the table is held to the ledger's own bar.
59
+ *
60
+ * A row with no resolving claim ships WITHOUT a number rather than with an
61
+ * approximate one.
62
+ */
63
+ export const PREVENTED_FAILURES: { failure: string; mechanism: string; backs: string }[] = [
64
+ {
65
+ failure: 'A rule says MUST, and nothing anywhere enforces it — so the rule reads as a guarantee while being honour-system.',
66
+ mechanism:
67
+ 'Each rule declares `enforced_by:`, and the check **resolves** it: a validator reachable from no taskfile, workflow, or hook manifest counts as unwired, not as covered. Undeclared counts as uncovered.',
68
+ backs: 'enforcement-coverage-resolved',
69
+ },
70
+ {
71
+ failure:
72
+ 'A shipped artefact carries a hidden-Unicode or instruction-smuggling payload, and reaches consumers because only the source tree was scanned.',
73
+ mechanism:
74
+ 'Source **and** the condensed projection are scanned in CI; a finding blocks the release before publish, not merely the merge.',
75
+ backs: 'shipped-artifacts-hidden-instruction-scanned',
76
+ },
77
+ {
78
+ failure:
79
+ 'A count in public prose drifts from the source, and the marketing number quietly stops being true.',
80
+ mechanism:
81
+ 'Counts are generated from source and drift-checked; the build fails on a count-shaped prose mention that disagrees, or on two different numbers for the same artefact kind.',
82
+ backs: 'skill-count',
83
+ },
84
+ {
85
+ failure:
86
+ 'A host-coverage claim understates or overstates what actually works — for months, on the one surface everybody reads.',
87
+ mechanism: 'The number is pinned by a test over real detection, and the test is re-run as the claim evidence.',
88
+ backs: 'host-agent-count',
89
+ },
90
+ {
91
+ failure:
92
+ 'A non-coding domain skill implies proven correctness it never had, because it was forged on a different domain and never checked against one.',
93
+ mechanism:
94
+ 'Domain skills are labelled unvalidated until they pass a sourced domain-truth fixture, and the validated count is ratcheted so it cannot quietly fall.',
95
+ backs: 'domain-soundness-scoped',
96
+ },
97
+ {
98
+ failure:
99
+ 'Behavioural-eval coverage regresses as skills are added, and the suite looks healthy because only the absolute number is reported.',
100
+ mechanism: 'Coverage is measured per tier and ratcheted in CI, so it can only rise; the gap is published rather than implied away.',
101
+ backs: 'eval-coverage-ratcheted',
102
+ },
103
+ ];
104
+
105
+ export function renderPreventedFailures(ledger: ReturnType<typeof load_ledger>): string {
106
+ const L: string[] = [];
107
+ L.push('## What this prevents');
108
+ L.push('');
109
+ L.push('Each row is a failure mode, not a feature — and each cites the ledger');
110
+ L.push('claim that backs it. A row whose claim stopped resolving would fail this');
111
+ L.push('page\'s own generator, so the table cannot outlive its evidence.');
112
+ L.push('');
113
+ L.push('| Failure this prevents | How | Backed by |');
114
+ L.push('|---|---|---|');
115
+ for (const row of PREVENTED_FAILURES) {
116
+ const entry = ledger.get(row.backs);
117
+ if (!entry) {
118
+ throw new Error(
119
+ `build_proof: prevented-failure row cites claim '${row.backs}', which has no ledger entry. ` +
120
+ 'Bind it in docs/CLAIMS.md or drop the row — the table may not carry an unbacked cell.',
121
+ );
122
+ }
123
+ if (entry.status !== 'backed') {
124
+ throw new Error(
125
+ `build_proof: prevented-failure row cites claim '${row.backs}', whose ledger status is ` +
126
+ `'${entry.status}', not 'backed'. The table may not carry an unbacked cell.`,
127
+ );
128
+ }
129
+ const reverifies = entry.evidence.trim().startsWith('exec:') ? ' ⟳' : '';
130
+ L.push(
131
+ `| ${row.failure.replace(/\|/g, '\\|')} | ${row.mechanism.replace(/\|/g, '\\|')} | ` +
132
+ `[\`${row.backs}\`](CLAIMS.md)${reverifies} |`,
133
+ );
134
+ }
135
+ L.push('');
136
+ L.push('⟳ — the claim carries `exec:` evidence: CI re-runs the command and');
137
+ L.push('compares its exit code, so a stale row turns the build red rather than');
138
+ L.push('ageing quietly into marketing.');
139
+ L.push('');
140
+ return L.join('\n');
141
+ }
142
+
44
143
  function render(): string {
45
144
  const ledger = load_ledger();
46
145
  const entries = [...ledger.values()].sort((a, b) => a.id.localeCompare(b.id));
@@ -59,6 +158,7 @@ function render(): string {
59
158
  L.push('every check below on a fresh checkout. This page is itself generated');
60
159
  L.push('from those sources and fails CI if it drifts.');
61
160
  L.push('');
161
+ L.push(renderPreventedFailures(ledger));
62
162
  L.push('## See it run (< 60s, real output)');
63
163
  L.push('');
64
164
  L.push('![The trust surface running green — every "verify it yourself" command below, recorded from a real run](media/proof-demo.gif)');
@@ -86,6 +186,39 @@ function render(): string {
86
186
  L.push('');
87
187
  L.push(`**${backed.length} backed claim(s)** — all evidence pointers resolve in CI.`);
88
188
  L.push('');
189
+
190
+ // Re-verifiable vs. merely-resolving. The distinction is the point: a
191
+ // pointer proves an artefact exists, not that the claim is still true. Any
192
+ // page that reported only the backed total would hide exactly the gap this
193
+ // ledger was built to expose.
194
+ const execBacked = backed.filter((e) => e.evidence.trim().startsWith('exec:'));
195
+ const pointerOnly = backed.filter((e) => !e.evidence.trim().startsWith('exec:'));
196
+ L.push('### How many of those re-derive themselves');
197
+ L.push('');
198
+ L.push(`**${execBacked.length} of ${backed.length}** backed claims carry \`exec:\` evidence —`);
199
+ L.push('CI re-runs the command and compares its exit code to the claim, so a');
200
+ L.push('stale one turns the build red. The other');
201
+ L.push(`**${pointerOnly.length}** rest on a pointer: CI checks that the artefact`);
202
+ L.push('exists and contains what it should, which cannot distinguish a live');
203
+ L.push('claim from one whose producer nobody has run in months.');
204
+ L.push('');
205
+ L.push('That residue is not an oversight, and it is listed rather than rounded');
206
+ L.push('away. A claim can only re-derive itself when a deterministic command');
207
+ L.push("carries the verdict in its exit code. These cannot, for the reasons");
208
+ L.push('given:');
209
+ L.push('');
210
+ L.push('| Claim | Why it cannot re-execute |');
211
+ L.push('|---|---|');
212
+ for (const e of pointerOnly) {
213
+ const ev = e.evidence.trim();
214
+ const why = /^https?:\/\//.test(ev)
215
+ ? 'external cite — CI does not fetch the network'
216
+ : /benchmark|bench\/|results-/.test(ev)
217
+ ? 'benchmark output — regenerating it needs paid or stochastic model calls no CI job can re-derive'
218
+ : 'prose or contract artefact — no exit code carries the verdict';
219
+ L.push(`| ${e.claim.replace(/\|/g, '\\|').slice(0, 110)} | ${why} |`);
220
+ }
221
+ L.push('');
89
222
  L.push('Artefact counts in public prose (skills, commands, governed rules,');
90
223
  L.push('guidelines, personas) are **generated from source and CI-drift-checked**:');
91
224
  L.push('`update_counts.ts` writes the numbers, `check_artefact_count_messaging.ts`');
@@ -224,6 +357,35 @@ function render(): string {
224
357
  L.push(`| ${claim} | ${ours} | ${theirs} | ${r.checkable ? '✅' : '—'} |`);
225
358
  }
226
359
  }
360
+
361
+ // Second lens on the SAME rows: what each one prevents, for a reader who has
362
+ // not asked "compared to what". Membership is a data property — a row appears
363
+ // here iff it carries a `failure_mode` — so nobody curates a short-list that
364
+ // then drifts from the table above. The pointer is the same pointer, resolved
365
+ // by the same gate, so no cell reaches this view unbound.
366
+ const fmRows = cmpRows.filter((r) => (r.failure_mode ?? '').trim() !== '');
367
+ if (fmRows.length > 0) {
368
+ L.push('');
369
+ L.push('### What each one prevents');
370
+ L.push('');
371
+ L.push('The same rows, read as failure modes rather than as comparisons. Each names');
372
+ L.push('something that goes wrong without the control and carries the identical');
373
+ L.push('resolvable pointer — a projection of the table above, not a second list to');
374
+ L.push('keep in sync.');
375
+ L.push('');
376
+ L.push('| Without it | The control | Evidence |');
377
+ L.push('|---|---|---|');
378
+ for (const r of fmRows) {
379
+ const fm = (r.failure_mode ?? '').replace(/\|/g, '\\|').trim();
380
+ const claim = r.claim.replace(/\|/g, '\\|');
381
+ const filePart = (r.our_evidence.split('#')[0] ?? r.our_evidence).split(':')[0] ?? r.our_evidence;
382
+ const ours = pointer_unresolved(r.our_evidence) === null
383
+ ? `[\`${r.our_evidence}\`](../${filePart})`
384
+ : `\`${r.our_evidence}\``;
385
+ L.push(`| ${fm} | ${claim} | ${ours} |`);
386
+ }
387
+ }
388
+
227
389
  L.push('');
228
390
  L.push('## 5. Verify it yourself');
229
391
  L.push('');
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * ADR frontmatter gate — and the `review_trigger` field that makes a decision
4
+ * revisitable on purpose rather than by archaeology.
5
+ *
6
+ * Two findings drive this. First, ADR frontmatter was **entirely unvalidated**:
7
+ * `validate_frontmatter.ts` covers skills, rules, commands, and personas, and
8
+ * nothing covered `docs/decisions/`. That is why `review_date` drifted into 11
9
+ * ADRs and back out again with nobody noticing.
10
+ *
11
+ * Second, of 128 ADRs, zero named the condition under which they should be
12
+ * reconsidered — while `decision-record` SKILL § step 4 already *mandates* a
13
+ * "Revisit-if" line and `decision-revisit-gate` already treats an ADR as a lock
14
+ * that must be re-openable. The doctrine was canon; the template just never
15
+ * emitted a slot for it, so 33 authors wrote it in prose and it stayed
16
+ * unfindable. This is a missing field, not a missing idea.
17
+ *
18
+ * `review_trigger` is a named CONDITION, never a date. "Review annually" is
19
+ * ignored by everyone and rots into ceremony; "when a second consumer reports a
20
+ * preservation surprise" fires exactly when it should.
21
+ *
22
+ * Grandfathering is by authorship date, not an allowlist: ADRs dated before the
23
+ * switch warn, ADRs dated on or after it fail. No list to maintain and no
24
+ * exemption to forget — a retrofit of 128 files would be busywork that teaches
25
+ * nobody anything.
26
+ *
27
+ * Usage:
28
+ * ./scripts-run src/scripts/check_adr_frontmatter # gate
29
+ * ./scripts-run src/scripts/check_adr_frontmatter --json
30
+ *
31
+ * Exit codes: 0 ok (warnings allowed) · 1 violation · 2 usage/env error.
32
+ */
33
+
34
+ import * as fs from 'node:fs';
35
+ import * as path from 'node:path';
36
+ import { fileURLToPath, pathToFileURL } from 'node:url';
37
+
38
+ const _HERE = path.dirname(fileURLToPath(import.meta.url));
39
+ const REPO_ROOT = path.resolve(_HERE, '..', '..');
40
+ const ADR_DIR = path.join(REPO_ROOT, 'docs', 'decisions');
41
+
42
+ /**
43
+ * Grandfathering switch. ADRs dated before this warn on a missing
44
+ * `review_trigger`; ADRs dated on or after it fail.
45
+ */
46
+ export const REVIEW_TRIGGER_SINCE = '2026-07-25';
47
+
48
+ /** Fields every ADR must carry — the shape that already exists in practice. */
49
+ const REQUIRED = ['adr', 'status', 'date', 'decision'] as const;
50
+
51
+ const ALLOWED_STATUS = new Set([
52
+ 'accepted',
53
+ 'proposed',
54
+ 'superseded',
55
+ 'rejected',
56
+ 'deprecated',
57
+ 'draft',
58
+ ]);
59
+
60
+ export interface AdrFinding {
61
+ file: string;
62
+ level: 'error' | 'warn';
63
+ message: string;
64
+ }
65
+
66
+ export function parse_frontmatter(text: string): Record<string, string> | null {
67
+ if (!text.startsWith('---\n')) return null;
68
+ const end = text.indexOf('\n---\n', 4);
69
+ if (end === -1) return null;
70
+ const out: Record<string, string> = {};
71
+ let key: string | null = null;
72
+ for (const raw of text.slice(4, end).split('\n')) {
73
+ const line = raw.replace(/\s+$/, '');
74
+ if (!line || line.trimStart().startsWith('#')) continue;
75
+ // Continuation of a folded/indented value.
76
+ if (/^\s/.test(line) && key) {
77
+ out[key] = `${out[key]} ${line.trim()}`.trim();
78
+ continue;
79
+ }
80
+ const idx = line.indexOf(':');
81
+ if (idx === -1) continue;
82
+ key = line.slice(0, idx).trim();
83
+ out[key] = line
84
+ .slice(idx + 1)
85
+ .trim()
86
+ .replace(/^["'](.*)["']$/, '$1');
87
+ }
88
+ return out;
89
+ }
90
+
91
+ /** A trigger must name a condition. A bare cadence is the failure mode. */
92
+ export function trigger_is_meaningful(value: string): boolean {
93
+ const v = value.trim();
94
+ if (v.length < 20) return false;
95
+ // "annually" / "every 6 months" / a bare date is a calendar, not a condition.
96
+ if (/^(annually|yearly|quarterly|monthly|every\s+\d+\s+\w+|\d{4}-\d{2}-\d{2})\.?$/i.test(v)) {
97
+ return false;
98
+ }
99
+ return true;
100
+ }
101
+
102
+ export function check(dir: string = ADR_DIR): AdrFinding[] {
103
+ const findings: AdrFinding[] = [];
104
+ if (!fs.existsSync(dir)) return findings;
105
+
106
+ for (const name of fs.readdirSync(dir).sort()) {
107
+ if (!/^ADR-.*\.md$/.test(name)) continue;
108
+ const rel = path.relative(REPO_ROOT, path.join(dir, name));
109
+ const fm = parse_frontmatter(fs.readFileSync(path.join(dir, name), 'utf-8'));
110
+
111
+ if (fm === null) {
112
+ findings.push({ file: rel, level: 'error', message: 'no YAML frontmatter block' });
113
+ continue;
114
+ }
115
+ for (const req of REQUIRED) {
116
+ if (!fm[req]) findings.push({ file: rel, level: 'error', message: `missing \`${req}\`` });
117
+ }
118
+ if (fm['status'] && !ALLOWED_STATUS.has(fm['status'])) {
119
+ findings.push({
120
+ file: rel,
121
+ level: 'error',
122
+ message: `status \`${fm['status']}\` is not one of: ${[...ALLOWED_STATUS].sort().join(', ')}`,
123
+ });
124
+ }
125
+
126
+ const date = fm['date'] ?? '';
127
+ const grandfathered = date !== '' && date < REVIEW_TRIGGER_SINCE;
128
+ const trigger = fm['review_trigger'];
129
+
130
+ if (!trigger) {
131
+ findings.push({
132
+ file: rel,
133
+ level: grandfathered ? 'warn' : 'error',
134
+ message: grandfathered
135
+ ? `no \`review_trigger\` (grandfathered — dated ${date}, before ${REVIEW_TRIGGER_SINCE})`
136
+ : `missing \`review_trigger\` — name the CONDITION that would reopen this decision`,
137
+ });
138
+ } else if (!trigger_is_meaningful(trigger)) {
139
+ findings.push({
140
+ file: rel,
141
+ level: grandfathered ? 'warn' : 'error',
142
+ message: `\`review_trigger\` is a cadence, not a condition: "${trigger}". A calendar review is ignored; an event fires.`,
143
+ });
144
+ }
145
+ }
146
+ return findings;
147
+ }
148
+
149
+ function main(argv: string[]): number {
150
+ const as_json = argv.includes('--json');
151
+ const findings = check();
152
+
153
+ if (as_json) {
154
+ process.stdout.write(JSON.stringify({ findings }, null, 2) + '\n');
155
+ return 0;
156
+ }
157
+
158
+ const errors = findings.filter((f) => f.level === 'error');
159
+ const warns = findings.filter((f) => f.level === 'warn');
160
+ const total = fs.existsSync(ADR_DIR)
161
+ ? fs.readdirSync(ADR_DIR).filter((f) => /^ADR-.*\.md$/.test(f)).length
162
+ : 0;
163
+
164
+ process.stdout.write(
165
+ `ADR frontmatter: ${total} ADR(s) · ${errors.length} error(s) · ` +
166
+ `${warns.length} grandfathered without a revisit condition\n`,
167
+ );
168
+ for (const e of errors) process.stderr.write(` ❌ ${e.file}: ${e.message}\n`);
169
+
170
+ if (errors.length > 0) {
171
+ process.stderr.write(
172
+ `\n❌ check_adr_frontmatter: ${errors.length} error(s). ` +
173
+ `ADRs dated ${REVIEW_TRIGGER_SINCE} or later must name a \`review_trigger\`.\n`,
174
+ );
175
+ return 1;
176
+ }
177
+ process.stdout.write('✅ check_adr_frontmatter: no errors\n');
178
+ return 0;
179
+ }
180
+
181
+ // Main-guard (realpath-compared, mirrors the repo convention).
182
+ if (process.argv[1] !== undefined) {
183
+ try {
184
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
185
+ const argv1 = fs.realpathSync(path.resolve(process.argv[1]));
186
+ if (here === argv1) {
187
+ process.exit(main(process.argv.slice(2)));
188
+ }
189
+ } catch {
190
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
191
+ if (import.meta.url === argvUrl) {
192
+ process.exit(main(process.argv.slice(2)));
193
+ }
194
+ }
195
+ }
@@ -53,6 +53,10 @@ export const SURFACES: readonly string[] = [
53
53
  'docs/featured-skills.md',
54
54
  'docs/featured-commands.md',
55
55
  'docs/proof.md',
56
+ // Public submission text: the block here described version 3.2.0 with a
57
+ // "4929 tests" figure while the package reached 9.7.0, because nothing
58
+ // watched this file. Added 2026-07-25 so a hand-typed count here fails CI.
59
+ 'docs/distribution/registries.md',
56
60
  'site/src/content/docs/index.mdx',
57
61
  'site/src/content/docs/claims.md',
58
62
  ];
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Debt ratchet for rule backstops that are red on arrival.
4
+ *
5
+ * Wiring the `enforced_by:` validators into CI (`rule-backstops.yml`) surfaced
6
+ * something the coverage number could not: **five of them were already failing**,
7
+ * and had been, invisibly, because no workflow ran them. A rule declared a
8
+ * backstop; the backstop existed; the backstop was red; nothing said so.
9
+ *
10
+ * Those failures are pre-existing debt, not regressions from the wiring change,
11
+ * and fixing eleven framework-leakage spots plus eighteen source-confidentiality
12
+ * hits is a different piece of work with a different reviewer. Folding them in
13
+ * would make one PR unreviewable and would mix "make the gate run" with "clean up
14
+ * what the gate found".
15
+ *
16
+ * So the established pattern applies — the one the decision-homing work already
17
+ * uses and the council endorsed: **baseline the legacy, enforce the boundary.**
18
+ * Each gate's current finding count is committed. The count may fall freely; it
19
+ * may not rise. New violations fail, existing debt is visible and counted, and
20
+ * nothing is silently tolerated.
21
+ *
22
+ * What this deliberately is NOT: a `continue-on-error` step. That is a WARN
23
+ * wearing a gate's clothes, and removing exactly that confusion is why this
24
+ * workflow exists.
25
+ *
26
+ * Usage:
27
+ * ./scripts-run src/scripts/check_backstop_debt # ratchet (CI)
28
+ * ./scripts-run src/scripts/check_backstop_debt --write-baseline
29
+ * ./scripts-run src/scripts/check_backstop_debt --json
30
+ *
31
+ * Exit codes: 0 held or improved · 1 a count rose · 2 usage/env error.
32
+ */
33
+
34
+ import { spawnSync } from 'node:child_process';
35
+ import * as fs from 'node:fs';
36
+ import * as path from 'node:path';
37
+ import { fileURLToPath, pathToFileURL } from 'node:url';
38
+
39
+ const _HERE = path.dirname(fileURLToPath(import.meta.url));
40
+ const REPO_ROOT = path.resolve(_HERE, '..', '..');
41
+ const BASELINE = path.join(REPO_ROOT, 'internal', 'reports', 'rule-backstop-debt.json');
42
+
43
+ export interface Gate {
44
+ /** Script stem under `src/scripts/`. */
45
+ script: string;
46
+ /** The rule whose `enforced_by:` names it — so a reader knows what is unguarded. */
47
+ rule: string;
48
+ /**
49
+ * Extracts the finding count from the gate's own output.
50
+ *
51
+ * Explicit per gate rather than a shared heuristic: these five print their
52
+ * totals in five different shapes, and a regex that guesses would silently
53
+ * read 0 the day a gate changes its wording — turning a rising count into a
54
+ * passing build, which is the failure mode this file exists to prevent.
55
+ */
56
+ count: RegExp;
57
+ }
58
+
59
+ export const GATES: readonly Gate[] = [
60
+ { script: 'lint_framework_leakage', rule: 'framework-neutrality-in-generic-skills', count: /^(\d+) hits across \d+ files/m },
61
+ { script: 'check_no_roadmap_refs', rule: 'no-roadmap-references', count: /Found (\d+) roadmap reference\(s\)/ },
62
+ { script: 'check_council_references', rule: 'no-roadmap-references', count: /(\d+) forbidden council reference\(s\)/ },
63
+ { script: 'check_no_external_sources', rule: 'source-confidentiality', count: /(\d+) external-source reference\(s\)/ },
64
+ { script: 'check_token_optimizer_freshness', rule: 'token-optimizer-maintenance', count: /(\d+) drift signal\(s\)/ },
65
+ ];
66
+
67
+ export interface Measured {
68
+ script: string;
69
+ rule: string;
70
+ findings: number;
71
+ exit_code: number;
72
+ /** True when the gate ran but its count pattern did not match — never read as 0. */
73
+ unparsed: boolean;
74
+ }
75
+
76
+ export function measure(gate: Gate, repo: string = REPO_ROOT): Measured {
77
+ const r = spawnSync('./scripts-run', [`src/scripts/${gate.script}`], {
78
+ cwd: repo,
79
+ encoding: 'utf-8',
80
+ env: { ...process.env, NO_COLOR: '1' },
81
+ });
82
+ const out = `${r.stdout ?? ''}\n${r.stderr ?? ''}`;
83
+ const m = gate.count.exec(out);
84
+ // A clean gate legitimately prints no total. Only treat a MISSING count as
85
+ // unparsed when the gate actually failed — otherwise a green run reads as
86
+ // broken instrumentation.
87
+ const failed = (r.status ?? 0) !== 0;
88
+ return {
89
+ script: gate.script,
90
+ rule: gate.rule,
91
+ findings: m ? Number(m[1]) : 0,
92
+ exit_code: r.status ?? 0,
93
+ unparsed: failed && m === null,
94
+ };
95
+ }
96
+
97
+ function main(argv: string[]): number {
98
+ const as_json = argv.includes('--json');
99
+ const write = argv.includes('--write-baseline');
100
+
101
+ const measured = GATES.map((g) => measure(g));
102
+
103
+ if (as_json) {
104
+ process.stdout.write(JSON.stringify({ gates: measured }, null, 2) + '\n');
105
+ return 0;
106
+ }
107
+
108
+ if (write) {
109
+ fs.mkdirSync(path.dirname(BASELINE), { recursive: true });
110
+ fs.writeFileSync(
111
+ BASELINE,
112
+ JSON.stringify(
113
+ {
114
+ _doc:
115
+ 'Pre-existing findings in rule backstops that were red when they were first ' +
116
+ 'wired into CI — they had been failing invisibly because no workflow ran them. ' +
117
+ 'Counts may FALL freely; a rise fails the build. This is debt made visible, ' +
118
+ 'not debt tolerated: every number here is a rule whose declared backstop is ' +
119
+ 'currently not clean. Regenerate with --write-baseline only when the change ' +
120
+ 'is deliberate.',
121
+ gates: Object.fromEntries(measured.map((m) => [m.script, { rule: m.rule, findings: m.findings }])),
122
+ total: measured.reduce((a, m) => a + m.findings, 0),
123
+ },
124
+ null,
125
+ 2,
126
+ ) + '\n',
127
+ );
128
+ process.stdout.write(`✅ wrote baseline → ${path.relative(REPO_ROOT, BASELINE)}\n`);
129
+ return 0;
130
+ }
131
+
132
+ if (!fs.existsSync(BASELINE)) {
133
+ process.stderr.write(
134
+ `❌ check_backstop_debt: no baseline at ${path.relative(REPO_ROOT, BASELINE)}; run --write-baseline\n`,
135
+ );
136
+ return 2;
137
+ }
138
+ const base = JSON.parse(fs.readFileSync(BASELINE, 'utf-8')).gates as Record<string, { findings: number }>;
139
+
140
+ const regressions: string[] = [];
141
+ const improvements: string[] = [];
142
+ for (const m of measured) {
143
+ if (m.unparsed) {
144
+ regressions.push(
145
+ `${m.script}: failed (exit ${m.exit_code}) but its finding count could not be read — ` +
146
+ `the count pattern no longer matches its output. Fix the pattern; do not assume 0.`,
147
+ );
148
+ continue;
149
+ }
150
+ const was = base[m.script]?.findings;
151
+ if (was === undefined) {
152
+ regressions.push(`${m.script}: not in the baseline — add it with --write-baseline`);
153
+ continue;
154
+ }
155
+ if (m.findings > was) {
156
+ regressions.push(`${m.script} (${m.rule}): findings rose ${was} → ${m.findings}`);
157
+ } else if (m.findings < was) {
158
+ improvements.push(`${m.script} (${m.rule}): ${was} → ${m.findings}`);
159
+ }
160
+ }
161
+
162
+ const total = measured.reduce((a, m) => a + m.findings, 0);
163
+ process.stdout.write(
164
+ `rule-backstop debt · ${total} finding(s) across ${measured.filter((m) => m.findings > 0).length} red gate(s)\n`,
165
+ );
166
+ for (const m of measured) {
167
+ if (m.findings > 0) process.stdout.write(` · ${m.script} (${m.rule}): ${m.findings}\n`);
168
+ }
169
+ for (const i of improvements) process.stdout.write(` ✅ improved — ${i}\n`);
170
+
171
+ if (regressions.length > 0) {
172
+ process.stderr.write('❌ rule-backstop debt ratchet:\n');
173
+ for (const r of regressions) process.stderr.write(` · ${r}\n`);
174
+ process.stderr.write(
175
+ ' These gates are already failing on pre-existing debt; the ratchet only forbids ADDING to it.\n',
176
+ );
177
+ return 1;
178
+ }
179
+ if (improvements.length > 0) {
180
+ process.stdout.write(' Baseline is now loose — regenerate with --write-baseline to lock the improvement.\n');
181
+ }
182
+ process.stdout.write('✅ rule-backstop debt ratchet holds\n');
183
+ return 0;
184
+ }
185
+
186
+ // Main-guard (realpath-compared, mirrors the repo convention).
187
+ if (process.argv[1] !== undefined) {
188
+ try {
189
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
190
+ const argv1 = fs.realpathSync(path.resolve(process.argv[1]));
191
+ if (here === argv1) {
192
+ process.exit(main(process.argv.slice(2)));
193
+ }
194
+ } catch {
195
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
196
+ if (import.meta.url === argvUrl) {
197
+ process.exit(main(process.argv.slice(2)));
198
+ }
199
+ }
200
+ }