@event4u/agent-config 14.7.0 → 14.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 (138) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +245 -388
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
  5. package/dist/agent-src/commands/evals/roadmap.json +17 -1
  6. package/dist/agent-src/commands/pr/create.md +14 -2
  7. package/dist/agent-src/commands/pr/merge.md +379 -0
  8. package/dist/agent-src/commands/roadmap/process-full.md +157 -3
  9. package/dist/agent-src/commands/roadmap.md +1 -1
  10. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
  11. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
  12. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +64 -2
  13. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +154 -2
  14. package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
  15. package/dist/agent-src/rules/decision-revisit-gate.md +65 -33
  16. package/dist/agent-src/rules/recurring-criticism.md +1 -1
  17. package/dist/agent-src/rules/roadmap-progress-sync.md +19 -0
  18. package/dist/agent-src/rules/source-confidentiality.md +1 -1
  19. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
  20. package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
  21. package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -48
  22. package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
  23. package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
  24. package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
  25. package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
  26. package/dist/agent-src/skills/security/SKILL.md +8 -0
  27. package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
  28. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  29. package/dist/agent-src/templates/roadmaps.md +20 -2
  30. package/dist/cli/registry.js +1 -0
  31. package/dist/cli/registry.js.map +1 -1
  32. package/dist/cli-delegate/{chunk-JZ4NR2TW.js → chunk-7PRE6ZV2.js} +1 -1
  33. package/dist/cli-delegate/{chunk-7F3VYSM2.js → chunk-EEJUMCDZ.js} +14 -14
  34. package/dist/cli-delegate/{chunk-74E3TKWV.js → chunk-H4VIVD4C.js} +5 -5
  35. package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
  36. package/dist/cli-delegate/{chunk-4VR4ORQ6.js → chunk-LPPWO5CE.js} +4 -3
  37. package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-N7CDDGI3.js} +4 -23
  38. package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
  39. package/dist/cli-delegate/cmd_conformance.js +14 -14
  40. package/dist/cli-delegate/cmd_converge.js +3 -3
  41. package/dist/cli-delegate/cmd_doctor.js +7 -7
  42. package/dist/cli-delegate/cmd_export.js +6 -6
  43. package/dist/cli-delegate/cmd_handoff.js +7 -6
  44. package/dist/cli-delegate/cmd_memory_get.js +2 -1
  45. package/dist/cli-delegate/cmd_migrate.js +2 -2
  46. package/dist/cli-delegate/cmd_preflight.js +5 -5
  47. package/dist/cli-delegate/cmd_prune.js +6 -6
  48. package/dist/cli-delegate/cmd_route_audit.js +6 -5
  49. package/dist/cli-delegate/cmd_route_explain.js +3 -3
  50. package/dist/cli-delegate/cmd_session_recycle.js +3 -3
  51. package/dist/cli-delegate/cmd_settings_get.js +3 -3
  52. package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
  53. package/dist/cli-delegate/cmd_sync.js +6 -6
  54. package/dist/cli-delegate/cmd_uninstall.js +6 -6
  55. package/dist/cli-delegate/cmd_update.js +3 -3
  56. package/dist/cli-delegate/cmd_upgrade.js +3 -3
  57. package/dist/cli-delegate/cmd_validate.js +6 -6
  58. package/dist/cli-delegate/cmd_versions.js +3 -3
  59. package/dist/discovery/deprecation-report.md +1 -1
  60. package/dist/discovery/discovery-manifest.json +53 -22
  61. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  62. package/dist/discovery/discovery-manifest.summary.md +3 -3
  63. package/dist/discovery/dormancy-report.md +1 -1
  64. package/dist/discovery/orphan-report.md +1 -1
  65. package/dist/discovery/packs.json +7 -6
  66. package/dist/discovery/trust-report.md +2 -2
  67. package/dist/discovery/workspaces.json +6 -5
  68. package/dist/hooks/dispatch.js +806 -681
  69. package/dist/install/globalRuleLayers.js +169 -0
  70. package/dist/install/globalRuleLayers.js.map +1 -0
  71. package/dist/install/partitionEligibility.js +156 -0
  72. package/dist/install/partitionEligibility.js.map +1 -1
  73. package/dist/install/ruleLayerPartition.js +157 -0
  74. package/dist/install/ruleLayerPartition.js.map +1 -0
  75. package/dist/mcp/registry-manifest.json +3 -3
  76. package/dist/mcp/server.json +2 -2
  77. package/dist/mcp/server.mjs +654 -578
  78. package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
  79. package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
  80. package/dist/scripts/condense.js +52 -89
  81. package/dist/scripts/condense.js.map +1 -1
  82. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
  83. package/docs/guidelines/agent-infra/false-green.md +47 -1
  84. package/package.json +1 -1
  85. package/src/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
  86. package/src/agent-src/scripts/dashboard_mode.ts +212 -0
  87. package/src/agent-src/scripts/update_roadmap_progress.ts +47 -48
  88. package/src/config/ci-local-parity.yml +21 -0
  89. package/src/config/estate-count-budget.json +275 -2
  90. package/src/config/evaluator-budgets.json +3 -3
  91. package/src/config/gate-coverage.yml +220 -12
  92. package/src/config/gate-violation-baselines.json +73 -63
  93. package/src/config/gitignore-block.txt +15 -2
  94. package/src/config/hook-latency-budget.json +5 -0
  95. package/src/config/rule-stub-ceilings.json +17 -3
  96. package/src/install/globalRuleLayers.ts +197 -0
  97. package/src/install/partitionEligibility.ts +177 -0
  98. package/src/install/ruleLayerPartition.ts +212 -0
  99. package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
  100. package/src/scripts/_dispatch.bash +11 -0
  101. package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
  102. package/src/scripts/_lib/adr_frontmatter.ts +459 -0
  103. package/src/scripts/_lib/envelope_grounding.ts +11 -1
  104. package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
  105. package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
  106. package/src/scripts/adr/evidence_census.ts +899 -0
  107. package/src/scripts/adr/regenerate_index.ts +12 -32
  108. package/src/scripts/adr_cite_check.ts +540 -39
  109. package/src/scripts/audit_adr_coverage.ts +103 -17
  110. package/src/scripts/bench_hook_latency.ts +204 -0
  111. package/src/scripts/build_archive_index.ts +34 -0
  112. package/src/scripts/check_adr_frontmatter.ts +574 -39
  113. package/src/scripts/check_hook_bundle_content.ts +219 -0
  114. package/src/scripts/check_new_adr_evidence.ts +529 -0
  115. package/src/scripts/check_references.ts +18 -0
  116. package/src/scripts/check_rule_layer_partition.ts +531 -0
  117. package/src/scripts/check_single_delivery.ts +137 -4
  118. package/src/scripts/check_standing_rule_delivery.ts +8 -2
  119. package/src/scripts/condense.ts +61 -91
  120. package/src/scripts/external_sources_denylist.json +4 -1
  121. package/src/scripts/hooks/block_unauthorized_git.ts +22 -2
  122. package/src/scripts/hooks/dispatch_hook.ts +154 -187
  123. package/src/scripts/hooks/dispatch_issues.ts +38 -19
  124. package/src/scripts/hooks/fallback_yaml.ts +66 -0
  125. package/src/scripts/hooks/host_semantics.ts +21 -0
  126. package/src/scripts/hooks/injection_budget.ts +13 -3
  127. package/src/scripts/hooks/py_json_dumps.ts +103 -0
  128. package/src/scripts/hooks/state_io.ts +88 -1
  129. package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
  130. package/src/scripts/injection_scan_hook.ts +137 -5
  131. package/src/scripts/lint_hook_manifest.ts +49 -0
  132. package/src/scripts/lint_provenance_vocabulary.ts +472 -4
  133. package/src/scripts/routing_doctor.ts +5 -1
  134. package/src/scripts/sync_pr_branch.ts +163 -20
  135. package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
  136. package/dist/cli-delegate/{chunk-6EOCNQAM.js → chunk-E4XIZDIB.js} +0 -0
  137. package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-MMSGVZKV.js} +3 -3
  138. package/dist/cli-delegate/{chunk-GJ5BVHXM.js → chunk-PMEKGDED.js} +3 -3
@@ -0,0 +1,529 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * A NEW accepted ADR must disclose its evidence — the half of AC-5 that had no
4
+ * mechanism.
5
+ *
6
+ * AC-5 of `road-to-evidence-based-adr-governance` claims that "a newly added
7
+ * accepted ADR cannot pass CI without an Evidence section, a substantive
8
+ * `review_trigger`, and no unscoped permanence language". Two of those three
9
+ * were enforced by `check_adr_frontmatter`. The first was not, and that gate's
10
+ * own docstring said so: `check_descriptive_axes` validates the SHAPE of an axis
11
+ * that is PRESENT, so a fresh record with no `provenance`, no `evidence` and no
12
+ * `## Evidence` section passed every gate in this tree.
13
+ *
14
+ * The blocker was real and structural rather than an omission: requiring the
15
+ * axes needs a notion of NEW, and a single-file linter has none — it sees a
16
+ * `date:` the author typed, not whether the record is new to the repository, and
17
+ * a backdated record slips straight through that proxy. The fix named there was
18
+ * "a two-ref diff, which is buildable and not built". This is that diff.
19
+ *
20
+ * ## What it requires, and of what
21
+ *
22
+ * Scope: records **ADDED** relative to a base ref (`git diff --diff-filter=A`),
23
+ * under `docs/decisions/ADR-*.md` and `docs/adrs/<area>/NNNN-*.md`. Records that
24
+ * already existed are untouched — the 147 accepted records in the corpus predate
25
+ * the axes, and retrofitting them is a migration event this gate deliberately is
26
+ * not. Same scope decision, same reasoning, as `lint_evidence_artifacts`.
27
+ *
28
+ * Of an added record whose `status:` is `accepted`, all three:
29
+ *
30
+ * 1. a `provenance:` map carrying a `kind:`
31
+ * 2. an `evidence:` map carrying a `strength:`
32
+ * 3. an `## Evidence` section in the body (heading at any level)
33
+ *
34
+ * `authority_basis: owner_intent` does NOT excuse the section. That field
35
+ * governs where a record's AUTHORITY comes from — an owner purpose statement is
36
+ * legitimately E0 and its alternatives are foreclosed by ownership — and it says
37
+ * nothing about whether the record discloses what it rests on. Reading it as a
38
+ * disclosure exemption would turn the one honest escape into the hole.
39
+ *
40
+ * Non-`accepted` added records (`proposed`, `superseded`, …) are OUT OF SCOPE
41
+ * and are reported as such, per file, rather than silently skipped: a record can
42
+ * legitimately ship `proposed` precisely so its acceptance is separately
43
+ * reviewable (ADR-239 does), and a gate that prints nothing about them is
44
+ * indistinguishable from a gate that failed to see them.
45
+ *
46
+ * ## What it does NOT do
47
+ *
48
+ * It checks the axes are PRESENT, never that their values are valid. Vocabulary
49
+ * validation (`kind` ∈ {human, agentic, mixed, unknown}, `strength` ∈ E0…E4,
50
+ * the `review_trigger` staging, permanence language) is
51
+ * `check_adr_frontmatter`'s and stays there — one axis, one owner. Nor does it
52
+ * judge whether the `## Evidence` section says anything true; no gate can, and
53
+ * that stays a human read at review time.
54
+ *
55
+ * It reads frontmatter through the SHARED reader `_lib/adr_frontmatter.ts`. The
56
+ * tree carried three divergent ADR parsers until that extraction; a fourth
57
+ * would have re-created the exact divergence — and the nested axes are the
58
+ * shapes the old regex reader silently read as absent.
59
+ *
60
+ * Usage:
61
+ * ./scripts-run src/scripts/check_new_adr_evidence # gate
62
+ * ./scripts-run src/scripts/check_new_adr_evidence --base REF
63
+ * ./scripts-run src/scripts/check_new_adr_evidence --self-test
64
+ *
65
+ * Exit codes: 0 clean (including "nothing added") · 1 violation · 2 usage/env.
66
+ */
67
+
68
+ import { execFileSync } from 'node:child_process';
69
+ import * as fs from 'node:fs';
70
+ import * as os from 'node:os';
71
+ import * as path from 'node:path';
72
+ import process from 'node:process';
73
+ import { fileURLToPath, pathToFileURL } from 'node:url';
74
+
75
+ import { evidenceOf, provenanceOf, readAdrFrontmatter } from './_lib/adr_frontmatter.js';
76
+ import { GateLedger } from './_lib/gate_ledger.js';
77
+ import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
78
+ import { resolveBaseRef } from './_lib/ratchet_base_ref.js';
79
+ import { reportScanned } from './_lib/scan_scope.js';
80
+
81
+ const _HERE = path.dirname(fileURLToPath(import.meta.url));
82
+ const REPO_ROOT = path.resolve(_HERE, '..', '..');
83
+
84
+ /** The two decision surfaces that carry numbered records. */
85
+ export const ADR_PATHSPECS = [
86
+ 'docs/decisions/ADR-*.md',
87
+ 'docs/adrs/*/[0-9][0-9][0-9][0-9]-*.md',
88
+ ] as const;
89
+
90
+ /**
91
+ * The same two surfaces as anchored regexes.
92
+ *
93
+ * Git's default pathspec wildcards match `/`, so `docs/adrs/*​/NNNN-*.md` can
94
+ * over-match a deeper path. The pathspecs narrow what git has to walk; these
95
+ * decide what is in scope. Belt and braces, because an over-match here is a
96
+ * false red on somebody else's file.
97
+ */
98
+ const ADR_PATH_RES = [
99
+ /^docs\/decisions\/ADR-[^/]*\.md$/,
100
+ /^docs\/adrs\/[^/]+\/\d{4}-[^/]*\.md$/,
101
+ ] as const;
102
+
103
+ /** Fenced blocks, so a quoted `## Evidence` inside one is not a declaration. */
104
+ const FENCE_RE = /^(?:```|~~~)/;
105
+
106
+ /** An `Evidence` heading at any level. The heading, never a mention of the word. */
107
+ const EVIDENCE_HEADING_RE = /^#{1,6}[ \t]+Evidence\b/;
108
+
109
+ /**
110
+ * Does the body carry an `## Evidence` section?
111
+ *
112
+ * Fenced blocks are stripped first. An ADR that quotes a template — and several
113
+ * in this corpus do — would otherwise satisfy the check by showing what the
114
+ * section looks like instead of by having one.
115
+ *
116
+ * A heading is accepted when its text STARTS with `Evidence`, so
117
+ * `## Evidence and assumptions` counts. The alternative (exact match) would
118
+ * reject a legitimate compound heading, and the failure mode of this check is
119
+ * the one worth minimising: it is a disclosure floor, not a style rule.
120
+ */
121
+ export function hasEvidenceSection(text: string): boolean {
122
+ let inFence = false;
123
+ for (const raw of text.split('\n')) {
124
+ const line = raw.trim();
125
+ if (FENCE_RE.test(line)) {
126
+ inFence = !inFence;
127
+ continue;
128
+ }
129
+ if (inFence) continue;
130
+ if (EVIDENCE_HEADING_RE.test(line)) return true;
131
+ }
132
+ return false;
133
+ }
134
+
135
+ function _git(root: string, args: readonly string[]): string {
136
+ // GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE are inherited inside a git hook
137
+ // and point at the INVOKING repository, so a `cwd`-scoped call silently
138
+ // reads the wrong tree. This repo has already recorded that failure once.
139
+ const env = { ...process.env };
140
+ delete env['GIT_DIR'];
141
+ delete env['GIT_WORK_TREE'];
142
+ delete env['GIT_INDEX_FILE'];
143
+ return execFileSync('git', [...args], {
144
+ cwd: root,
145
+ encoding: 'utf8',
146
+ env,
147
+ maxBuffer: 64 * 1024 * 1024,
148
+ });
149
+ }
150
+
151
+ /** Decision records ADDED relative to `baseRef`. */
152
+ export function addedAdrRecords(root: string, baseRef: string): string[] {
153
+ const out = _git(root, [
154
+ 'diff',
155
+ '--name-only',
156
+ '--diff-filter=A',
157
+ `${baseRef}...HEAD`,
158
+ '--',
159
+ ...ADR_PATHSPECS,
160
+ ]);
161
+ return out
162
+ .split('\n')
163
+ .map((s) => s.trim())
164
+ .filter((s) => s !== '' && ADR_PATH_RES.some((re) => re.test(s)))
165
+ .sort();
166
+ }
167
+
168
+ export interface Finding {
169
+ readonly file: string;
170
+ readonly reason: string;
171
+ }
172
+
173
+ /** Records considered, split by what the gate did with each. */
174
+ export interface CheckResult {
175
+ readonly findings: Finding[];
176
+ /** Added records whose status is not `accepted` — named, not hidden. */
177
+ readonly outOfScope: { readonly file: string; readonly status: string }[];
178
+ readonly checked: string[];
179
+ }
180
+
181
+ const REMEDY =
182
+ 'add `provenance:` (with `kind:`), `evidence:` (with `strength:`) and an ' +
183
+ '`## Evidence` section — see docs/contracts/adr-layout.md and ADR-239. ' +
184
+ '`authority_basis: owner_intent` governs authority, never disclosure, and ' +
185
+ 'does not exempt any of the three.';
186
+
187
+ export function checkRecords(root: string, files: readonly string[], ledger?: GateLedger): CheckResult {
188
+ const findings: Finding[] = [];
189
+ const outOfScope: { file: string; status: string }[] = [];
190
+ const checked: string[] = [];
191
+ ledger?.plan([...files]);
192
+
193
+ for (const rel of files) {
194
+ let text: string;
195
+ try {
196
+ text = fs.readFileSync(path.join(root, rel), 'utf8');
197
+ } catch {
198
+ // Listed by git, absent on disk: an added-then-moved path in a dirty
199
+ // tree hits this and is somebody else's gate, not a finding here.
200
+ ledger?.outOfScope(rel, 'no_applicable_files');
201
+ continue;
202
+ }
203
+
204
+ const fm = readAdrFrontmatter(text);
205
+ if (fm === null) {
206
+ // A NEW record with no YAML block cannot be classified at all — its
207
+ // status is unreadable, so "is it accepted" has no answer. Failing is
208
+ // the honest direction: the alternative is to let an unreadable
209
+ // record through the one gate that exists to read it.
210
+ ledger?.fail(rel, 'no YAML frontmatter');
211
+ findings.push({
212
+ file: rel,
213
+ reason: 'no YAML frontmatter — a new decision record carries one, so its status and axes can be read',
214
+ });
215
+ continue;
216
+ }
217
+
218
+ const status = (fm.scalars['status'] ?? '').trim();
219
+ if (status !== 'accepted') {
220
+ outOfScope.push({ file: rel, status: status === '' ? '(unset)' : status });
221
+ ledger?.outOfScope(rel, 'not_applicable_kind');
222
+ continue;
223
+ }
224
+
225
+ const missing: string[] = [];
226
+ if (provenanceOf(fm)?.kind == null) missing.push('`provenance.kind`');
227
+ if (evidenceOf(fm)?.strength == null) missing.push('`evidence.strength`');
228
+ if (!hasEvidenceSection(text)) missing.push('an `## Evidence` section');
229
+
230
+ if (missing.length > 0) {
231
+ ledger?.fail(rel, `missing ${missing.join(', ')}`);
232
+ findings.push({
233
+ file: rel,
234
+ reason: `accepted on arrival and missing ${missing.join(', ')} — ${REMEDY}`,
235
+ });
236
+ continue;
237
+ }
238
+ checked.push(rel);
239
+ ledger?.complete(rel);
240
+ }
241
+ return { findings, outOfScope, checked };
242
+ }
243
+
244
+ function _argValue(argv: readonly string[], flag: string): string | null {
245
+ const i = argv.indexOf(flag);
246
+ if (i === -1) return null;
247
+ const v = argv[i + 1];
248
+ return v === undefined || v.startsWith('--') ? null : v;
249
+ }
250
+
251
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
252
+ if (argv.includes('--self-test')) return selfTest();
253
+
254
+ const root = process.cwd();
255
+
256
+ // `resolveBaseRef` rather than a hardcoded `origin/main`: `actions/checkout`
257
+ // performs a shallow PR-merge fetch, so a PR build frequently has no
258
+ // `origin/main` remote-tracking ref at all. That ladder is the repo's one
259
+ // answer to this question and it needs no network on those builds.
260
+ const baseRef = _argValue(argv, '--base') ?? resolveBaseRef(root);
261
+ if (baseRef === null) {
262
+ // Fail rather than compare against an assumed-empty base. An
263
+ // unresolvable base makes every record look un-added, and the gate would
264
+ // then pass by scanning nothing — the precise false green it exists to
265
+ // remove from AC-5.
266
+ process.stderr.write(
267
+ 'check_new_adr_evidence: no base ref resolved — pass --base <ref> or set RATCHET_BASE_REF\n',
268
+ );
269
+ return 2;
270
+ }
271
+
272
+ let files: string[];
273
+ try {
274
+ files = addedAdrRecords(root, baseRef);
275
+ } catch (exc) {
276
+ process.stderr.write(`check_new_adr_evidence: cannot diff against ${baseRef} — ${String(exc)}\n`);
277
+ return 2;
278
+ }
279
+
280
+ const ledger = new GateLedger('check_new_adr_evidence');
281
+ const result = checkRecords(root, files, ledger);
282
+ ledger.report();
283
+
284
+ // Zero added records is the NORMAL case, so it must exit green — and say so.
285
+ // `assertScanned` throws on a zero count unless a justified `allowEmpty`
286
+ // reason is supplied, and the reason here is `EMPTY_VALID` rather than
287
+ // `OPTIONAL_INPUT`: the scope is a DIFF, not a corpus, so "this branch added
288
+ // no decision record" genuinely is the pass, and stays true even if
289
+ // `docs/decisions/` were deleted outright. The line is still emitted, because
290
+ // a silent green over an empty set is how a gate stops measuring anything
291
+ // without anyone noticing.
292
+ //
293
+ // The honest cost, named rather than implied: an unconditional `allowEmpty`
294
+ // means the scope assertion can never fire for this gate, so it buys no
295
+ // dead-root protection. What replaces it is `--self-test`, which proves the
296
+ // verdict still moves — the property a `scanned:` floor cannot establish for
297
+ // a gate whose usual reading is zero.
298
+ reportScanned({
299
+ gate: 'check_new_adr_evidence',
300
+ scanned: files.length,
301
+ units: 'added decision record(s)',
302
+ roots: ['docs/decisions', 'docs/adrs'],
303
+ allowEmpty:
304
+ 'EMPTY_VALID: the scope is the set of records ADDED since the base ref; ' +
305
+ 'a branch that adds none has nothing to disclose, and zero is the pass.',
306
+ });
307
+
308
+ if (files.length === 0) {
309
+ process.stdout.write(
310
+ `check_new_adr_evidence: ✅ no decision record added since ${baseRef} — nothing to check\n`,
311
+ );
312
+ return 0;
313
+ }
314
+
315
+ for (const row of result.outOfScope) {
316
+ process.stdout.write(
317
+ ` ⚪ ${row.file}: status ${row.status} — out of scope; this gate requires disclosure of \`accepted\` records only\n`,
318
+ );
319
+ }
320
+
321
+ process.stdout.write(
322
+ `check_new_adr_evidence: ${String(files.length)} added record(s) since ${baseRef} · ` +
323
+ `${String(result.checked.length)} accepted and disclosing · ` +
324
+ `${String(result.outOfScope.length)} not accepted · ` +
325
+ `${String(result.findings.length)} violation(s)\n`,
326
+ );
327
+
328
+ if (result.findings.length > 0) {
329
+ for (const f of result.findings) {
330
+ process.stderr.write(` ❌ ${f.file}: ${f.reason}\n`);
331
+ }
332
+ process.stderr.write(
333
+ `\n❌ check_new_adr_evidence: ${String(result.findings.length)} new record(s) claim ` +
334
+ 'accepted status without disclosing what they rest on.\n',
335
+ );
336
+ return 1;
337
+ }
338
+ process.stdout.write('✅ check_new_adr_evidence: no violations\n');
339
+ return 0;
340
+ }
341
+
342
+ // ---------------------------------------------------------------------------
343
+ // --self-test — drives the REAL binary over synthetic repositories.
344
+ // ---------------------------------------------------------------------------
345
+
346
+ /**
347
+ * Why this gate needs a self-test more than most.
348
+ *
349
+ * Its normal reading is ZERO added records, and that is exactly the state where
350
+ * a working gate and a completely broken one emit the same output: `scanned: 0`,
351
+ * green. A `min_scanned` floor cannot separate them — a floor of 0 can never
352
+ * trip, and any higher floor would red every branch that adds no ADR. Only
353
+ * feeding it a synthetic added-accepted-record-without-evidence and watching the
354
+ * verdict move proves the reading still changes the answer.
355
+ *
356
+ * Each case builds a throwaway git repository, commits a base, then commits the
357
+ * record on top, and runs the real CLI with `--base <base-sha>` — so the diff
358
+ * path, the argv parsing and the entry guard are all under test rather than an
359
+ * imported function.
360
+ */
361
+ interface FixtureOpts {
362
+ /** Frontmatter body between the `---` markers. */
363
+ readonly frontmatter: string;
364
+ /** Markdown body after the frontmatter. */
365
+ readonly body: string;
366
+ /** Repo-relative path for the record; defaults to a flat ADR. */
367
+ readonly rel?: string;
368
+ }
369
+
370
+ function _gitQuiet(root: string, args: readonly string[]): void {
371
+ const env = { ...process.env };
372
+ delete env['GIT_DIR'];
373
+ delete env['GIT_WORK_TREE'];
374
+ delete env['GIT_INDEX_FILE'];
375
+ execFileSync(
376
+ 'git',
377
+ [
378
+ '-c',
379
+ 'user.email=selftest@example.invalid',
380
+ '-c',
381
+ 'user.name=self-test',
382
+ '-c',
383
+ 'commit.gpgsign=false',
384
+ '-c',
385
+ 'core.hooksPath=',
386
+ ...args,
387
+ ],
388
+ { cwd: root, encoding: 'utf8', env, stdio: 'pipe' },
389
+ );
390
+ }
391
+
392
+ function _write(root: string, rel: string, contents: string): void {
393
+ const abs = path.join(root, rel);
394
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
395
+ fs.writeFileSync(abs, contents, 'utf8');
396
+ }
397
+
398
+ /** A repo whose HEAD adds `record` (or nothing) on top of a committed base. */
399
+ function _repo(parent: string, record: FixtureOpts | null): { root: string; base: string } {
400
+ const root = fs.mkdtempSync(path.join(parent, 'repo-'));
401
+ _gitQuiet(root, ['init', '-q']);
402
+ _write(root, 'README.md', '# base\n');
403
+ _gitQuiet(root, ['add', '-A']);
404
+ _gitQuiet(root, ['commit', '-q', '-m', 'base']);
405
+ const base = _git(root, ['rev-parse', 'HEAD']).trim();
406
+ if (record !== null) {
407
+ _write(
408
+ root,
409
+ record.rel ?? 'docs/decisions/ADR-901-probe.md',
410
+ `---\n${record.frontmatter}\n---\n\n# ADR-901 — probe\n\n${record.body}\n`,
411
+ );
412
+ _gitQuiet(root, ['add', '-A']);
413
+ _gitQuiet(root, ['commit', '-q', '-m', 'add record']);
414
+ }
415
+ return { root, base };
416
+ }
417
+
418
+ const FULL_FRONTMATTER = [
419
+ 'adr: 901',
420
+ 'status: accepted',
421
+ 'date: 2026-08-21',
422
+ 'decision: probe',
423
+ 'review_trigger: Reopen when the probe stops probing.',
424
+ 'provenance:',
425
+ ' kind: mixed',
426
+ ' decision_makers: [owner]',
427
+ 'evidence:',
428
+ ' strength: E3',
429
+ ' discovery: complete',
430
+ ].join('\n');
431
+
432
+ export function selfTest(): number {
433
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cnae-'));
434
+
435
+ const run = (record: FixtureOpts | null): number => {
436
+ const { root, base } = _repo(tmp, record);
437
+ return runGateCli(REPO_ROOT, 'src/scripts/check_new_adr_evidence.ts', ['--base', base], root);
438
+ };
439
+
440
+ const withoutLines = (drop: RegExp): string =>
441
+ FULL_FRONTMATTER.split('\n')
442
+ .filter((l) => !drop.test(l))
443
+ .join('\n');
444
+
445
+ try {
446
+ return runSelfTest({
447
+ gate: 'check_new_adr_evidence',
448
+ minCases: 7,
449
+ minRejectCases: 4,
450
+ cases: [
451
+ {
452
+ name: 'a branch that adds no record passes — the normal case must stay green',
453
+ expect: 'accept',
454
+ run: () => run(null),
455
+ },
456
+ {
457
+ name: 'an added accepted record with both axes and an `## Evidence` section passes',
458
+ expect: 'accept',
459
+ run: () =>
460
+ run({ frontmatter: FULL_FRONTMATTER, body: '## Evidence\n\nMeasured.\n' }),
461
+ },
462
+ {
463
+ name: 'an added accepted record with NO `## Evidence` section is rejected — the AC-5 gap itself',
464
+ expect: 'reject',
465
+ run: () => run({ frontmatter: FULL_FRONTMATTER, body: '## Context\n\nNothing.\n' }),
466
+ },
467
+ {
468
+ name: 'an added accepted record with no `evidence.strength` is rejected',
469
+ expect: 'reject',
470
+ run: () =>
471
+ run({
472
+ frontmatter: withoutLines(/^(evidence:|\s+strength:|\s+discovery:)/),
473
+ body: '## Evidence\n\nMeasured.\n',
474
+ }),
475
+ },
476
+ {
477
+ name: 'an added accepted record with no `provenance.kind` is rejected',
478
+ expect: 'reject',
479
+ run: () =>
480
+ run({
481
+ frontmatter: withoutLines(/^(provenance:|\s+kind:|\s+decision_makers:)/),
482
+ body: '## Evidence\n\nMeasured.\n',
483
+ }),
484
+ },
485
+ {
486
+ name: '`authority_basis: owner_intent` does not excuse the section — authority is not disclosure',
487
+ expect: 'reject',
488
+ run: () =>
489
+ run({
490
+ frontmatter: `${FULL_FRONTMATTER}\nauthority_basis: owner_intent`,
491
+ body: '## Context\n\nOwner says so.\n',
492
+ }),
493
+ },
494
+ {
495
+ name: 'a non-accepted added record is out of scope, not a violation',
496
+ expect: 'accept',
497
+ run: () =>
498
+ run({
499
+ frontmatter: FULL_FRONTMATTER.replace('status: accepted', 'status: proposed')
500
+ .split('\n')
501
+ .filter((l) => !/^(provenance:|evidence:|\s{2}\w+:)/.test(l))
502
+ .join('\n'),
503
+ body: '## Context\n\nStill being argued.\n',
504
+ }),
505
+ },
506
+ ],
507
+ });
508
+ } finally {
509
+ fs.rmSync(tmp, { recursive: true, force: true });
510
+ }
511
+ }
512
+
513
+ // Main-guard (realpath-compared, mirrors the repo convention).
514
+ function _isCliEntry(): boolean {
515
+ if (process.argv[1] === undefined) return false;
516
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
517
+ if (import.meta.url === argvUrl) return true;
518
+ try {
519
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
520
+ const argv1 = fs.realpathSync(path.resolve(process.argv[1]));
521
+ return here === argv1;
522
+ } catch {
523
+ return false;
524
+ }
525
+ }
526
+
527
+ if (_isCliEntry()) {
528
+ process.exit(main());
529
+ }
@@ -97,6 +97,13 @@ const FILE_SKIP_MARKER = '<!-- check-refs: skip -->';
97
97
  // fully-checked documents.
98
98
  const LINE_IGNORE_MARKER = '<!-- ref-ignore -->';
99
99
 
100
+ /**
101
+ * The one generated artefact this repository intentionally does not track, so
102
+ * no clone carries it and an existence check cannot answer for it. Referencing
103
+ * it in prose is legitimate; the path simply is not a file in CI.
104
+ */
105
+ const UNTRACKED_GENERATED_DASHBOARD = 'agents/roadmaps-progress.md';
106
+
100
107
  // YAML memory files (engineering-memory layer) live under `agents/memory/`.
101
108
  // Each entry may reference skills, ADR paths, or local files via
102
109
  // `source:` / `enforcement:` / `skill:`. We validate those paths so a
@@ -715,6 +722,16 @@ function check_file(
715
722
  // check above already swallows refs into `agents/runtime/`,
716
723
  // so no extra carve-out is needed.
717
724
  }
725
+ if (!resolved && rawRef === UNTRACKED_GENERATED_DASHBOARD) {
726
+ // A generated LOCAL artefact this repository deliberately does
727
+ // not commit (2026-08-21: it was the #1 merge-conflict path).
728
+ // It exists on a developer's disk and in no clone, so an
729
+ // existence check is the wrong question for it — same shape as
730
+ // the gitignored `agents/runtime/` carve-out above. Exactly one
731
+ // path, never a prefix: a sibling like `<path>.bak` is still a
732
+ // real broken reference.
733
+ resolved = true;
734
+ }
718
735
  if (
719
736
  !resolved &&
720
737
  _is_frozen_record_source(_relPosix(root, filepath)) &&
@@ -1168,6 +1185,7 @@ export {
1168
1185
  SKIP_NAMES,
1169
1186
  EXAMPLE_PATH_PATTERNS,
1170
1187
  ALLOWLIST_PATTERNS,
1188
+ UNTRACKED_GENERATED_DASHBOARD,
1171
1189
  type AllowlistPattern,
1172
1190
  _is_allowlisted,
1173
1191
  collect_artifacts,