@event4u/agent-config 9.6.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 (204) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +207 -942
  3. package/README.md +21 -21
  4. package/dist/agent-src/README.md +2 -2
  5. package/dist/agent-src/commands/commit/in-chunks.md +8 -0
  6. package/dist/agent-src/commands/commit.md +17 -0
  7. package/dist/agent-src/commands/pr/create.md +4 -0
  8. package/dist/agent-src/contexts/override-system.md +75 -5
  9. package/dist/agent-src/rules/context-hygiene.md +2 -0
  10. package/dist/agent-src/rules/doc-screenshot-hygiene.md +97 -0
  11. package/dist/agent-src/rules/domain-safety-pii.md +1 -0
  12. package/dist/agent-src/rules/downstream-changes.md +31 -1
  13. package/dist/agent-src/rules/external-code-graph-interop.md +13 -3
  14. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
  15. package/dist/agent-src/rules/git-history-discipline.md +2 -0
  16. package/dist/agent-src/rules/language-and-tone.md +2 -0
  17. package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
  18. package/dist/agent-src/rules/media-governance-routing.md +2 -0
  19. package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
  20. package/dist/agent-src/rules/no-roadmap-references.md +3 -0
  21. package/dist/agent-src/rules/non-destructive-by-default.md +20 -0
  22. package/dist/agent-src/rules/onboarding-gate.md +2 -0
  23. package/dist/agent-src/rules/output-discipline.md +2 -0
  24. package/dist/agent-src/rules/persona-governance.md +2 -0
  25. package/dist/agent-src/rules/preservation-guard.md +3 -0
  26. package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
  27. package/dist/agent-src/rules/secret-vcs-guard.md +106 -0
  28. package/dist/agent-src/rules/security-sensitive-stop.md +1 -1
  29. package/dist/agent-src/rules/skill-quality.md +2 -0
  30. package/dist/agent-src/rules/source-confidentiality.md +2 -0
  31. package/dist/agent-src/rules/source-of-truth.md +2 -0
  32. package/dist/agent-src/rules/telegraph-speak.md +2 -0
  33. package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
  34. package/dist/agent-src/rules/tool-safety.md +1 -0
  35. package/dist/agent-src/rules/verify-before-complete.md +2 -0
  36. package/dist/agent-src/skills/adr-create/SKILL.md +17 -1
  37. package/dist/agent-src/skills/agent-docs-writing/SKILL.md +34 -16
  38. package/dist/agent-src/skills/agents-md-thin-root/SKILL.md +9 -9
  39. package/dist/agent-src/skills/code-intelligence/SKILL.md +94 -0
  40. package/dist/agent-src/skills/gated-reach/SKILL.md +185 -0
  41. package/dist/agent-src/skills/gated-reach/evals/triggers.json +72 -0
  42. package/dist/agent-src/skills/image-analyser/SKILL.md +14 -12
  43. package/dist/agent-src/skills/image-editing/SKILL.md +1 -0
  44. package/dist/agent-src/skills/readme-writing/SKILL.md +6 -1
  45. package/dist/agent-src/skills/screenshot-hygiene/SKILL.md +171 -0
  46. package/dist/agent-src/skills/secrets-management/SKILL.md +44 -1
  47. package/dist/agent-src/skills/subagent-orchestration/prompts/adversarial-verification-council.md +29 -0
  48. package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge-two-stage.md +29 -0
  49. package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge.md +29 -0
  50. package/dist/agent-src/skills/subagent-orchestration/prompts/do-competitively.md +29 -0
  51. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-parallel.md +29 -0
  52. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-steps.md +29 -0
  53. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-worktrees.md +29 -0
  54. package/dist/agent-src/skills/subagent-orchestration/prompts/judge-with-debate.md +29 -0
  55. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +4 -0
  56. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  57. package/dist/agent-src/templates/github-workflows/doc-impact.yml +101 -0
  58. package/dist/agent-src/templates/github-workflows/secret-scan.yml +65 -0
  59. package/dist/cli/configRoot.js +112 -0
  60. package/dist/cli/configRoot.js.map +1 -0
  61. package/dist/cli/configRoot.test.js +97 -0
  62. package/dist/cli/configRoot.test.js.map +1 -0
  63. package/dist/cli/help.js +4 -1
  64. package/dist/cli/help.js.map +1 -1
  65. package/dist/cli/main.js +19 -2
  66. package/dist/cli/main.js.map +1 -1
  67. package/dist/cli/registry.js +1 -0
  68. package/dist/cli/registry.js.map +1 -1
  69. package/dist/discovery/deprecation-report.md +1 -1
  70. package/dist/discovery/discovery-manifest.json +164 -47
  71. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  72. package/dist/discovery/discovery-manifest.summary.md +4 -4
  73. package/dist/discovery/orphan-report.md +1 -1
  74. package/dist/discovery/packs.json +13 -8
  75. package/dist/discovery/trust-report.md +11 -11
  76. package/dist/discovery/workspaces.json +11 -6
  77. package/dist/install/install.mjs +16 -0
  78. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  79. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
  80. package/dist/mcp/registry-manifest.json +4 -4
  81. package/dist/router.json +1 -1
  82. package/dist/server/app.js +10 -0
  83. package/dist/server/app.js.map +1 -1
  84. package/dist/server/routes/ping.js +18 -0
  85. package/dist/server/routes/ping.js.map +1 -1
  86. package/dist/server/schemas/settings.js +8 -0
  87. package/dist/server/schemas/settings.js.map +1 -1
  88. package/dist/server/serverInfo.js +6 -2
  89. package/dist/server/serverInfo.js.map +1 -1
  90. package/dist/server/token.js +11 -4
  91. package/dist/server/token.js.map +1 -1
  92. package/dist/server/writeRoot.js +15 -1
  93. package/dist/server/writeRoot.js.map +1 -1
  94. package/dist/shared/capabilities.js +33 -0
  95. package/dist/shared/capabilities.js.map +1 -0
  96. package/dist/shared/capabilities.test.js +39 -0
  97. package/dist/shared/capabilities.test.js.map +1 -0
  98. package/dist/ui/assets/{index-BiEZiZ31.js → index-CrmZ34Rg.js} +16 -16
  99. package/dist/ui/assets/index-CrmZ34Rg.js.map +1 -0
  100. package/dist/ui/index.html +14 -5
  101. package/docs/CLAIMS.md +64 -38
  102. package/docs/architecture.md +2 -2
  103. package/docs/archive/CHANGELOG-6.0.0-overview.md +953 -0
  104. package/docs/benchmark.md +170 -9
  105. package/docs/catalog.md +8 -3
  106. package/docs/comparison.yaml +44 -2
  107. package/docs/contracts/local-server-ports.md +53 -0
  108. package/docs/contracts/no-runtime-boundary.md +1 -1
  109. package/docs/contracts/subagent-boundary.md +15 -0
  110. package/docs/customization.md +14 -3
  111. package/docs/decisions/ADR-016-installer-architecture.md +2 -0
  112. package/docs/decisions/ADR-088-no-external-runtime-federation.md +1 -1
  113. package/docs/decisions/ADR-094-agent-memory-layer-removal.md +1 -1
  114. package/docs/decisions/ADR-109-subagent-v1-contract.md +2 -0
  115. package/docs/decisions/ADR-110-discipline-profile-resolution-locus.md +2 -0
  116. package/docs/decisions/ADR-113-ci-native-release-label-trigger.md +11 -3
  117. package/docs/decisions/ADR-122-adversarial-verification-council.md +30 -0
  118. package/docs/decisions/ADR-123-runtime-security-scope-and-spawn-hardening.md +31 -0
  119. package/docs/decisions/ADR-124-embedded-engine-doctrine.md +255 -0
  120. package/docs/decisions/ADR-125-doc-screenshot-anonymization.md +131 -0
  121. package/docs/decisions/ADR-126-internet-reach-operator-tooling.md +254 -0
  122. package/docs/decisions/ADR-127-enforcement-claims-must-resolve.md +150 -0
  123. package/docs/decisions/ADR-128-executable-evidence-and-measured-escalation.md +201 -0
  124. package/docs/decisions/INDEX.md +21 -0
  125. package/docs/decisions/engine-reclassification-2026-07.md +104 -0
  126. package/docs/distribution/awesome-list-submission.md +1 -1
  127. package/docs/distribution/registries.md +36 -27
  128. package/docs/enforcement-by-host.md +7 -0
  129. package/docs/featured-skills.md +2 -2
  130. package/docs/getting-started-by-role.md +1 -1
  131. package/docs/getting-started.md +1 -1
  132. package/docs/governance-advantage.md +1 -1
  133. package/docs/guides/gated-platform-reads.md +221 -0
  134. package/docs/positioning-evidence.md +1 -1
  135. package/docs/proof.md +92 -16
  136. package/docs/spawn-site-policy.md +72 -0
  137. package/docs/threat-model.md +2 -1
  138. package/package.json +6 -3
  139. package/src/agent-src/README.md +2 -2
  140. package/src/config/agent-settings.template.yml +43 -0
  141. package/src/config/reach-channels.yml +217 -0
  142. package/src/config/reach-prescriptions-intake.yml +200 -0
  143. package/src/scripts/_dispatch.bash +15 -0
  144. package/src/scripts/_lib/adversarial_bench_score.ts +106 -0
  145. package/src/scripts/_lib/exec_evidence.ts +251 -0
  146. package/src/scripts/_lib/fixtures/secret-detector/negatives.txt +10 -0
  147. package/src/scripts/_lib/fixtures/secret-detector/positives.txt +11 -0
  148. package/src/scripts/_lib/kernel_rules.ts +46 -0
  149. package/src/scripts/_lib/reddit_thread_parse.ts +598 -0
  150. package/src/scripts/_lib/risk_paths.ts +160 -0
  151. package/src/scripts/_lib/secret_detector.test.ts +103 -0
  152. package/src/scripts/_lib/secret_detector.ts +319 -0
  153. package/src/scripts/_lib/spawn_env.ts +24 -4
  154. package/src/scripts/_lib/tool_probe.ts +359 -0
  155. package/src/scripts/adr/regenerate_index.ts +81 -6
  156. package/src/scripts/audit_adr_coverage.ts +33 -0
  157. package/src/scripts/bench_adversarial_council.ts +292 -0
  158. package/src/scripts/build_proof.ts +162 -0
  159. package/src/scripts/check_adr_frontmatter.ts +195 -0
  160. package/src/scripts/check_artefact_count_messaging.ts +4 -0
  161. package/src/scripts/check_backstop_debt.ts +200 -0
  162. package/src/scripts/check_claims.ts +188 -8
  163. package/src/scripts/check_comparison.ts +12 -0
  164. package/src/scripts/check_enforcement_coverage.ts +592 -0
  165. package/src/scripts/check_kernel_rule_bundle.ts +16 -12
  166. package/src/scripts/check_no_new_legacy_path.ts +2 -0
  167. package/src/scripts/check_reach_channels.ts +407 -0
  168. package/src/scripts/check_reach_staleness.ts +364 -0
  169. package/src/scripts/check_secret_leak.test.ts +47 -0
  170. package/src/scripts/check_secret_leak.ts +290 -0
  171. package/src/scripts/check_source_pointer_freshness.ts +165 -0
  172. package/src/scripts/code_graph/build.ts +0 -0
  173. package/src/scripts/code_graph/cli.ts +207 -0
  174. package/src/scripts/code_graph/detect.ts +134 -0
  175. package/src/scripts/code_graph/extract.ts +386 -0
  176. package/src/scripts/code_graph/loader.ts +123 -0
  177. package/src/scripts/code_graph/query.ts +176 -0
  178. package/src/scripts/code_graph/sanitize.ts +30 -0
  179. package/src/scripts/code_graph/types.ts +83 -0
  180. package/src/scripts/code_graph/validate.ts +63 -0
  181. package/src/scripts/external_sources_denylist.json +3 -1
  182. package/src/scripts/generate_subagent_floor.ts +194 -0
  183. package/src/scripts/hook_manifest.yaml +13 -3
  184. package/src/scripts/hooks/code_graph_nudge_hook.ts +184 -0
  185. package/src/scripts/hooks/dispatch_hook.ts +5 -1
  186. package/src/scripts/iron_law_sha.ts +3 -12
  187. package/src/scripts/lint_override_kernel_guard.ts +219 -0
  188. package/src/scripts/lint_readme_serial_comma.ts +169 -0
  189. package/src/scripts/measure_rule_budget.ts +3 -12
  190. package/src/scripts/postinstall_gui.ts +129 -0
  191. package/src/scripts/reach_doctor.ts +1651 -0
  192. package/src/scripts/release.ts +24 -3
  193. package/src/scripts/risk_escalation_shadow.ts +160 -0
  194. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  195. package/src/scripts/schemas/reach-channels.schema.json +134 -0
  196. package/src/scripts/schemas/reach-doctor-payload.schema.json +220 -0
  197. package/src/scripts/schemas/rule.schema.json +10 -0
  198. package/src/scripts/skill_linter.ts +55 -0
  199. package/src/scripts/validate_reach_prescriptions.ts +751 -0
  200. package/src/server/schemas/settings.ts +16 -0
  201. package/src/shared/capabilities.test.ts +43 -0
  202. package/src/shared/capabilities.ts +75 -0
  203. package/src/templates/minimal/agents-overrides-readme.md +20 -0
  204. package/dist/ui/assets/index-BiEZiZ31.js.map +0 -1
@@ -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
+ }
@@ -15,10 +15,17 @@
15
15
  * Ledger entries with `status: unbacked` are inventory (documented debt) and do
16
16
  * NOT fail the build — but marking such an entry's claim in prose does.
17
17
  *
18
- * Evidence-pointer grammar (v1):
18
+ * Evidence-pointer grammar (v2):
19
19
  * <repo-path>[:line] → the repo file exists (line advisory).
20
20
  * <repo-path>#<substring> → the file exists AND contains <substring>.
21
21
  * https://… (YYYY-MM-DD) → external cite with a dated stamp (not fetched).
22
+ * exec:<command> -> <code> → the command RE-RUNS and its exit code matches.
23
+ *
24
+ * The first three are existence checks and cannot tell a live claim from a
25
+ * stale one: a pointer at a report nobody regenerated resolves forever. The
26
+ * fourth re-derives the claim. It runs in CI only — locally it reports
27
+ * UNVERIFIED rather than executing anything, because a consumer's checkout has
28
+ * no business re-running this package's evidence commands.
22
29
  *
23
30
  * Exit codes: 0 = clean · 2 = unbacked/dangling/missing-entry finding OR usage
24
31
  * error. Success prints to stdout; findings print to stderr.
@@ -26,10 +33,17 @@
26
33
  * Usage:
27
34
  * ./scripts-run src/scripts/check_claims
28
35
  * ./scripts-run src/scripts/check_claims --quiet
36
+ * CI=true ./scripts-run src/scripts/check_claims # re-runs exec: claims
29
37
  */
30
38
  import * as fs from 'node:fs';
31
39
  import * as path from 'node:path';
32
40
  import { fileURLToPath, pathToFileURL } from 'node:url';
41
+ import {
42
+ exec_allowed_here,
43
+ exec_static_error,
44
+ parse_exec_pointer,
45
+ run_exec_evidence,
46
+ } from './_lib/exec_evidence.js';
33
47
 
34
48
  const _FILE = fileURLToPath(import.meta.url);
35
49
  const _HERE = path.dirname(_FILE);
@@ -46,8 +60,47 @@ const URL_DATED = /^https?:\/\/\S+\(\d{4}-\d{2}-\d{2}\)\s*$/;
46
60
  * not appear as a capability claim. docs/proof.md and docs/comparison.yaml
47
61
  * are structurally witnessed (pointer columns + their own linters). */
48
62
  const WITNESS_SURFACES = ['README.md', 'CAPABILITIES.yaml'];
49
- /** A percentage or multiplier reading as a measured capability figure. */
50
- const QUANTIFIED_CLAIM = /\b\d+(?:\.\d+)?\s*(?:%|[x×](?![A-Za-z0-9]))/;
63
+
64
+ /**
65
+ * Shapes that read as a measured capability figure.
66
+ *
67
+ * The ratio pattern was written against the "84.8%-style headline" anti-lesson
68
+ * and catches exactly that. It missed the shape that actually bit us: the README
69
+ * shipped "compiled into 7+ host agents" while the real, test-pinned number was
70
+ * 23 detected / 20 emitted — understating coverage by 3x, for months, on the one
71
+ * surface this sweep already watched. A bare integer carries no `%` and no `x`,
72
+ * so no ratio pattern could see it and no unit allowlist would either.
73
+ *
74
+ * Three narrow classes:
75
+ * - RATIO — percentages and multipliers (the original).
76
+ * - MAGNITUDE — a number carrying a measurement unit. Unit-gated, not
77
+ * digit-gated: a thousands-separator heuristic would catch
78
+ * `22,077` and also every year and large ordinal.
79
+ * - SELF_COUNT — a count-shaped assertion about the package's own reach.
80
+ * This is the class that shipped wrong.
81
+ *
82
+ * Excluded from MAGNITUDE in v1: time units. "wait 30 seconds" is an
83
+ * instruction, not a claim, and a gate that false-positives is a gate that gets
84
+ * bypassed (`narrow > recall`, as with the credential floor).
85
+ *
86
+ * Deliberately NOT widened to more surfaces, and this was measured rather than
87
+ * assumed: `docs/benchmark.md` alone matches the ratio pattern on 54 lines — it
88
+ * is a methodology document whose job is to be full of statistics, and sweeping
89
+ * it would produce exactly the flood that teaches a maintainer to bypass the
90
+ * gate. `docs/proof.md` and `docs/comparison.yaml` are pointer-enforced by their
91
+ * own linters, so an unmarkered figure cannot originate there. The gap was never
92
+ * the surface list; it was the pattern.
93
+ */
94
+ const RATIO = /\b\d+(?:\.\d+)?\s*(?:%|[x×](?![A-Za-z0-9]))/;
95
+ // The optional `<word>-` allows a qualified unit: `13,881 GPT-tokens` is the
96
+ // same claim shape as `13,881 tokens` and was missed without it.
97
+ const MAGNITUDE = /\b\d[\d,._]*\s*(?:[A-Za-z]+-)?(?:tokens?|ms|USD|KB|MB|GB|chars?)\b/i;
98
+ const SELF_COUNT = /\b\d+\+?\s+(?:host agents?|hosts|supported (?:agents?|hosts))\b/i;
99
+
100
+ /** True when a line carries any figure shape that must bind to a claim. */
101
+ export function is_quantified_claim(line: string): boolean {
102
+ return RATIO.test(line) || MAGNITUDE.test(line) || SELF_COUNT.test(line);
103
+ }
51
104
 
52
105
  class ExitCode extends Error {
53
106
  code: number;
@@ -57,7 +110,7 @@ class ExitCode extends Error {
57
110
  }
58
111
  }
59
112
 
60
- interface LedgerEntry {
113
+ export interface LedgerEntry {
61
114
  id: string;
62
115
  claim: string;
63
116
  kind: string;
@@ -170,10 +223,24 @@ function scan_markers(): { id: string; file: string }[] {
170
223
  return found;
171
224
  }
172
225
 
173
- /** Does an evidence pointer resolve? Returns null on success, else a reason. */
226
+ /**
227
+ * Does an evidence pointer resolve? Returns null on success, else a reason.
228
+ *
229
+ * For `exec:` this is the STATIC half only — is the pointer well-formed and is
230
+ * the command allowlisted. A malformed or non-allowlisted command fails
231
+ * everywhere, including locally, because that is a defect in the ledger rather
232
+ * than a property of the environment. Re-execution is the separate CI-gated
233
+ * pass in `main`, so a laptop never runs an evidence command.
234
+ */
174
235
  function pointer_unresolved(evidence: string): string | null {
175
236
  const ev = evidence.trim();
176
237
  if (!ev) return 'empty evidence pointer';
238
+
239
+ const exec = parse_exec_pointer(ev);
240
+ if (exec !== null) {
241
+ return 'error' in exec ? exec.error : exec_static_error(exec);
242
+ }
243
+
177
244
  if (/^https?:\/\//.test(ev)) {
178
245
  return URL_DATED.test(ev) ? null : 'external URL missing a (YYYY-MM-DD) stamp';
179
246
  }
@@ -254,16 +321,100 @@ function main(argv: string[] = process.argv.slice(2)): number {
254
321
  continue;
255
322
  }
256
323
  if (inFence) continue;
257
- if (!QUANTIFIED_CLAIM.test(line)) continue;
258
- if (/<!--\s*claim:/.test(line) || /unverified/i.test(line)) continue;
324
+ if (!is_quantified_claim(line)) continue;
325
+ if (/unverified/i.test(line)) continue;
326
+
327
+ // A marker on the line is not enough — it must be a marker that can
328
+ // LICENSE A NUMBER. This is how "7+ host agents" survived: the line
329
+ // already carried `claim:no-runtime-daemon`, a `kind: qual` claim
330
+ // about having no daemon, and any-marker-exempts-the-line let the
331
+ // unrelated figure ride along on it. A qualitative claim says nothing
332
+ // about a quantity, so only a `kind: quant` entry clears a figure.
333
+ const markers = [...line.matchAll(/<!--\s*claim:([A-Za-z0-9._-]+)\s*-->/g)].map((m) => m[1] as string);
334
+ const licensed = markers.some((id) => ledger.get(id)?.kind === 'quant');
335
+ if (licensed) continue;
336
+
337
+ const why =
338
+ markers.length > 0
339
+ ? `figure carried only by non-quantitative claim marker(s) [${markers.join(', ')}] — a \`kind: qual\` claim cannot license a number`
340
+ : 'quantified claim without a claim marker or \'unverified\' annotation';
259
341
  findings.push({
260
342
  id: '(unmarkered)',
261
343
  file: `${rel}:${i + 1}`,
262
- reason: `quantified claim without a claim marker or 'unverified' annotation — ${line.trim().slice(0, 70)}`,
344
+ reason: `${why} — ${line.trim().slice(0, 70)}`,
263
345
  });
264
346
  }
265
347
  }
266
348
 
349
+ // A published denominator must match the live ledger.
350
+ //
351
+ // `exec-evidence-feasibility.json` records how many backed claims the
352
+ // feasibility measurement was taken over. That number was hand-written and
353
+ // drifted twice inside two days — 25 when the ledger held 26, then 26 when it
354
+ // held 27 — and CI stayed green both times, because the claim's evidence
355
+ // pointer resolved. Pointer-resolution is not truth: that is the very thing
356
+ // `ledger-exec-verifiability` says about the ledger, demonstrated by its own
357
+ // entry. The classification in that file is a human judgment and stays one;
358
+ // the denominator is mechanical, so it gets checked.
359
+ {
360
+ const rel = 'internal/reports/exec-evidence-feasibility.json';
361
+ const abs = path.join(REPO, rel);
362
+ if (fs.existsSync(abs)) {
363
+ try {
364
+ const stored = JSON.parse(fs.readFileSync(abs, 'utf8')) as { backed_claims?: number };
365
+ const live = [...ledger.values()].filter((e) => e.status === 'backed').length;
366
+ if (typeof stored.backed_claims === 'number' && stored.backed_claims !== live) {
367
+ findings.push({
368
+ id: '(derived-count)',
369
+ file: rel,
370
+ reason:
371
+ `backed_claims is ${stored.backed_claims} but the ledger holds ${live} — ` +
372
+ `a published denominator drifted from its source. Re-measure and update the report.`,
373
+ });
374
+ }
375
+ } catch {
376
+ findings.push({
377
+ id: '(derived-count)',
378
+ file: rel,
379
+ reason: 'unparseable JSON — the derived-count check cannot verify the published denominator',
380
+ });
381
+ }
382
+ }
383
+ }
384
+
385
+ // 4. Re-execution pass (`exec:` evidence). CI only — locally every exec
386
+ // claim reports UNVERIFIED and nothing runs. A mismatch is a finding: the
387
+ // command ran and disagreed with the claim. A skip is NOT a finding —
388
+ // collapsing "could not run" into "failed" is how a verifier degrades
389
+ // into a rubber stamp in the other direction.
390
+ const execEntries = [...ledger.values()]
391
+ .filter((e) => e.status === 'backed')
392
+ .map((e) => ({ entry: e, ptr: parse_exec_pointer(e.evidence) }))
393
+ .filter((x): x is { entry: LedgerEntry; ptr: { command: string; expected: number } } =>
394
+ x.ptr !== null && !('error' in x.ptr),
395
+ );
396
+
397
+ const execSkipped: string[] = [];
398
+ const execVerified: string[] = [];
399
+
400
+ if (execEntries.length > 0) {
401
+ const allowed = exec_allowed_here();
402
+ for (const { entry, ptr } of execEntries) {
403
+ if (!allowed) {
404
+ execSkipped.push(entry.id);
405
+ continue;
406
+ }
407
+ const outcome = run_exec_evidence(ptr, REPO);
408
+ if (outcome.mismatch) {
409
+ findings.push({ id: entry.id, file: LEDGER_REL, reason: outcome.reason });
410
+ } else if (outcome.verified) {
411
+ execVerified.push(entry.id);
412
+ } else {
413
+ execSkipped.push(entry.id);
414
+ }
415
+ }
416
+ }
417
+
267
418
  if (findings.length > 0) {
268
419
  process.stderr.write(`❌ check_claims: ${findings.length} unbacked/dangling claim(s):\n`);
269
420
  for (const f of findings) {
@@ -279,6 +430,14 @@ function main(argv: string[] = process.argv.slice(2)): number {
279
430
  process.stdout.write(
280
431
  `✅ check_claims: ${markers.length} markered claim(s) bound · ledger ${ledger.size} entries (${backed} backed, ${unbacked} unbacked inventory)\n`,
281
432
  );
433
+ if (execEntries.length > 0) {
434
+ const detail = execVerified.length > 0 ? ` (${execVerified.length} re-verified)` : '';
435
+ process.stdout.write(
436
+ execSkipped.length === execEntries.length
437
+ ? ` exec: ${execEntries.length} claim(s) UNVERIFIED — re-execution is CI-only, skipped locally\n`
438
+ : ` exec: ${execEntries.length} claim(s)${detail}, ${execSkipped.length} skipped\n`,
439
+ );
440
+ }
282
441
  }
283
442
  return 0;
284
443
  }
@@ -308,3 +467,24 @@ if (_isCliEntry()) {
308
467
  }
309
468
 
310
469
  export { REPO, LEDGER_REL, main, parse_args, load_ledger, scan_markers, pointer_unresolved, ExitCode };
470
+
471
+ /**
472
+ * Live count of `status: backed` ledger entries.
473
+ *
474
+ * Exists so a published denominator can be DERIVED instead of typed. The
475
+ * `ledger-exec-verifiability` entry hard-coded this number twice and drifted
476
+ * within a day both times — first 25 when the ledger held 26, then 26 when it
477
+ * held 27 — while `check_claims` stayed green because its evidence pointer
478
+ * resolved. A number a human retypes on every ledger edit will drift; the only
479
+ * fix is to stop retyping it.
480
+ *
481
+ * Note this is not the same as `grep -c '^- status: backed'`, which also counts
482
+ * the entry-schema template in the document header.
483
+ */
484
+ export function count_backed(): number {
485
+ let n = 0;
486
+ for (const e of load_ledger().values()) {
487
+ if (e.status === 'backed') n += 1;
488
+ }
489
+ return n;
490
+ }
@@ -29,6 +29,18 @@ export interface Row {
29
29
  our_evidence: string;
30
30
  their_evidence: string;
31
31
  checkable: boolean;
32
+ /**
33
+ * Optional — the failure mode this row's claim prevents, in adopter-facing
34
+ * voice. Populated rows additionally render as a second, non-comparative
35
+ * view; an empty field is the honest signal that a row is comparative only.
36
+ *
37
+ * The field exists so that "which rows appear where" is a **data property**
38
+ * rather than a curatorial decision. Hand-picking rows for a second surface
39
+ * would be manual curation with weaker enforcement than the projection this
40
+ * repo already runs — and either way the row's `our_evidence` pointer keeps
41
+ * being resolved, so no cell escapes the gate by appearing in the new view.
42
+ */
43
+ failure_mode?: string;
32
44
  }
33
45
 
34
46
  export function loadRows(repo: string = REPO): Row[] {