@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,459 @@
1
+ /**
2
+ * One ADR frontmatter reader, for the three call sites that each had their own.
3
+ *
4
+ * Before this file the tree carried three independent parsers:
5
+ * `check_adr_frontmatter.parse_frontmatter` (folded indented continuations into
6
+ * the parent scalar), `adr_cite_check.parse_frontmatter` (the same, plus
7
+ * stripping a bare `>-` fold marker), and `adr/regenerate_index.fm` (a
8
+ * `^([a-z_]+):[ \t]*(.+?)[ \t]*$` regex, so a key with an empty value matched
9
+ * nothing and an indented line matched nothing either).
10
+ *
11
+ * Three readers were survivable while every field was a scalar. `provenance`
12
+ * and `evidence` are nested and carry lists, so the divergence stops being
13
+ * cosmetic: the regex reader would have read both as absent — silently, on the
14
+ * surface that renders the public index — while the other two would have folded
15
+ * them into one unusable string. Adding an axis to three parsers also pays for
16
+ * it three times, which is the reason this extraction lands BEFORE the axes
17
+ * rather than after.
18
+ *
19
+ * ## Scalar compatibility is the contract
20
+ *
21
+ * `scalars` reproduces the previous behaviour key-for-key, including the fold
22
+ * of an indented continuation into its parent and the `>-` strip. That is
23
+ * deliberate: the three call sites read `fm.status`, `fm.review_trigger`,
24
+ * `fm.superseded_by` and friends, and a reader that changed any of those
25
+ * values would be a behaviour change wearing a refactor's clothes.
26
+ *
27
+ * The one intentional difference: a key whose own value is empty and whose
28
+ * indented children look like `key: value` (or `- item`) becomes a **nested**
29
+ * entry and is kept out of `scalars` entirely, rather than folded into the
30
+ * garbage string the old readers produced. Nothing read those strings.
31
+ */
32
+
33
+ /** A frontmatter value that is not a scalar. */
34
+ export type AdrFrontmatterNode = string | string[] | { [key: string]: AdrFrontmatterNode };
35
+
36
+ export interface AdrFrontmatter {
37
+ /**
38
+ * Scalar keys, byte-identical to what the three previous parsers produced
39
+ * for every shape that existed before the nested axes.
40
+ */
41
+ scalars: Record<string, string>;
42
+ /** Keys whose value is a nested map or a list. */
43
+ nested: Record<string, AdrFrontmatterNode>;
44
+ /** Source order of every top-level key, for diagnostics. */
45
+ keyOrder: string[];
46
+ }
47
+
48
+ /** Fold indicators: the value is a multi-line scalar, never a nested map. */
49
+ const FOLD_MARKERS = new Set(['>', '>-', '>+', '|', '|-', '|+']);
50
+
51
+ /**
52
+ * Consume an indented block as prose, with no map/list discrimination.
53
+ *
54
+ * Used whenever the parent carried a fold marker. Routing a folded block
55
+ * through `parseBlock` looked equivalent and was not: real `review_trigger`
56
+ * prose contains colons ("Reopen when an `exec:` claim is found green"), so the
57
+ * map branch claimed the first line and then stopped at the first line without
58
+ * one — truncating four ADRs' triggers to their opening clause. The corpus
59
+ * equivalence test is what caught it; a single-line fixture round-tripped
60
+ * correctly and hid the bug.
61
+ */
62
+ function consumeFolded(lines: Line[], start: number, parentIndent: number): [string, number] {
63
+ const parts: string[] = [];
64
+ let i = start;
65
+ while (i < lines.length) {
66
+ const line = lines[i] as Line;
67
+ if (line.indent <= parentIndent) break;
68
+ parts.push(line.text);
69
+ i += 1;
70
+ }
71
+ return [parts.join(' ').trim(), i];
72
+ }
73
+
74
+ const QUOTED = /^["'](.*)["']$/;
75
+
76
+ function stripQuotes(value: string): string {
77
+ return value.replace(QUOTED, '$1');
78
+ }
79
+
80
+ /** `[a, b, c]` → `['a','b','c']`; a bare `[]` → `[]`. */
81
+ function parseInlineList(raw: string): string[] | null {
82
+ const trimmed = raw.trim();
83
+ if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) return null;
84
+ const inner = trimmed.slice(1, -1).trim();
85
+ if (inner === '') return [];
86
+ return inner
87
+ .split(',')
88
+ .map((part) => stripQuotes(part.trim()))
89
+ .filter((part) => part !== '');
90
+ }
91
+
92
+ interface Line {
93
+ indent: number;
94
+ text: string;
95
+ }
96
+
97
+ /** Split the `---` block into significant lines, dropping blanks and comments. */
98
+ function significantLines(block: string): Line[] {
99
+ const out: Line[] = [];
100
+ for (const raw of block.split('\n')) {
101
+ const line = raw.replace(/\s+$/, '');
102
+ if (line === '') continue;
103
+ const trimmed = line.trimStart();
104
+ if (trimmed.startsWith('#')) continue;
105
+ out.push({ indent: line.length - trimmed.length, text: trimmed });
106
+ }
107
+ return out;
108
+ }
109
+
110
+ /**
111
+ * Parse one indented block into a map, a list, or a folded scalar.
112
+ *
113
+ * The discriminator is the block's own first line, not the parent key: `- x`
114
+ * opens a list, `k: v` opens a map, anything else is prose to fold. Keying on
115
+ * the parent name would have meant a hard-coded field list, which is the thing
116
+ * three divergent parsers already demonstrated does not survive a new axis.
117
+ */
118
+ function parseBlock(lines: Line[], start: number, parentIndent: number): [AdrFrontmatterNode, number] {
119
+ const first = lines[start];
120
+ if (first === undefined) return ['', start];
121
+
122
+ // A list.
123
+ if (first.text.startsWith('- ') || first.text === '-') {
124
+ const items: string[] = [];
125
+ let i = start;
126
+ while (i < lines.length) {
127
+ const line = lines[i] as Line;
128
+ if (line.indent <= parentIndent || !line.text.startsWith('-')) break;
129
+ items.push(stripQuotes(line.text.replace(/^-\s*/, '').trim()));
130
+ i += 1;
131
+ }
132
+ return [items, i];
133
+ }
134
+
135
+ // A map — every child line at this indent carries a `key:`.
136
+ const colon = first.text.indexOf(':');
137
+ if (colon > 0) {
138
+ const map: Record<string, AdrFrontmatterNode> = {};
139
+ let i = start;
140
+ while (i < lines.length) {
141
+ const line = lines[i] as Line;
142
+ if (line.indent <= parentIndent) break;
143
+ const idx = line.text.indexOf(':');
144
+ if (idx <= 0) break;
145
+ const key = line.text.slice(0, idx).trim();
146
+ const rest = line.text.slice(idx + 1).trim();
147
+ i += 1;
148
+ if (rest === '' || FOLD_MARKERS.has(rest)) {
149
+ const next = lines[i];
150
+ if (next !== undefined && next.indent > line.indent) {
151
+ if (FOLD_MARKERS.has(rest)) {
152
+ const [folded, consumed] = consumeFolded(lines, i, line.indent);
153
+ map[key] = folded;
154
+ i = consumed;
155
+ continue;
156
+ }
157
+ const [child, consumed] = parseBlock(lines, i, line.indent);
158
+ map[key] = child;
159
+ i = consumed;
160
+ continue;
161
+ }
162
+ map[key] = '';
163
+ continue;
164
+ }
165
+ const inline = parseInlineList(rest);
166
+ map[key] = inline ?? stripQuotes(rest);
167
+ }
168
+ return [map, i];
169
+ }
170
+
171
+ // Prose — fold it.
172
+ const parts: string[] = [];
173
+ let i = start;
174
+ while (i < lines.length) {
175
+ const line = lines[i] as Line;
176
+ if (line.indent <= parentIndent) break;
177
+ parts.push(line.text);
178
+ i += 1;
179
+ }
180
+ return [parts.join(' ').trim(), i];
181
+ }
182
+
183
+ /**
184
+ * Flatten a node back to the single string the old fold produced.
185
+ *
186
+ * A list renders as its inline literal (`[a, b]`) rather than as joined items,
187
+ * because that is what the previous parsers put in the scalar map for
188
+ * `protected_dimensions: [purpose]` and the corpus-equivalence test holds this
189
+ * reader to that byte-for-byte.
190
+ */
191
+ function foldToString(node: AdrFrontmatterNode): string {
192
+ if (typeof node === 'string') return node;
193
+ if (Array.isArray(node)) return `[${node.join(', ')}]`;
194
+ return Object.entries(node)
195
+ .map(([k, v]) => `${k}: ${foldToString(v)}`)
196
+ .join(' ')
197
+ .trim();
198
+ }
199
+
200
+ /**
201
+ * Read the leading `---` block of an ADR.
202
+ *
203
+ * Returns `null` when there is no frontmatter at all — which is the honest
204
+ * answer for the seven per-area records, whose metadata lives in a blockquote
205
+ * line instead. `adr_cite_check` already declares them partial coverage for
206
+ * exactly this reason; this reader does not paper over it.
207
+ */
208
+ export function readAdrFrontmatter(text: string): AdrFrontmatter | null {
209
+ if (!text.startsWith('---\n')) return null;
210
+ const end = text.indexOf('\n---\n', 4);
211
+ if (end === -1) return null;
212
+
213
+ const lines = significantLines(text.slice(4, end));
214
+ const scalars: Record<string, string> = {};
215
+ const nested: Record<string, AdrFrontmatterNode> = {};
216
+ const keyOrder: string[] = [];
217
+
218
+ let i = 0;
219
+ while (i < lines.length) {
220
+ const line = lines[i] as Line;
221
+ if (line.indent > 0) {
222
+ // Orphan indented line with no parent — the old readers dropped
223
+ // these too (no key to fold into). Skip rather than invent one.
224
+ i += 1;
225
+ continue;
226
+ }
227
+ const idx = line.text.indexOf(':');
228
+ if (idx <= 0) {
229
+ i += 1;
230
+ continue;
231
+ }
232
+ const key = line.text.slice(0, idx).trim();
233
+ const rest = line.text.slice(idx + 1).trim();
234
+ keyOrder.push(key);
235
+ i += 1;
236
+
237
+ const next = lines[i];
238
+ const hasBlock = next !== undefined && next.indent > 0;
239
+
240
+ if (FOLD_MARKERS.has(rest) && hasBlock) {
241
+ const [folded, consumed] = consumeFolded(lines, i, 0);
242
+ scalars[key] = folded;
243
+ i = consumed;
244
+ continue;
245
+ }
246
+
247
+ if (rest === '' && hasBlock) {
248
+ const [child, consumed] = parseBlock(lines, i, 0);
249
+ i = consumed;
250
+ if (typeof child === 'string') {
251
+ scalars[key] = child;
252
+ } else {
253
+ nested[key] = child;
254
+ }
255
+ continue;
256
+ }
257
+
258
+ const inline = parseInlineList(rest);
259
+ if (inline !== null) {
260
+ nested[key] = inline;
261
+ continue;
262
+ }
263
+
264
+ // A plain scalar with continuation lines — prose, as the old readers
265
+ // treated it. The same colon hazard applies here.
266
+ if (hasBlock) {
267
+ const [folded, consumed] = consumeFolded(lines, i, 0);
268
+ scalars[key] = `${stripQuotes(rest)} ${folded}`.trim();
269
+ i = consumed;
270
+ continue;
271
+ }
272
+ scalars[key] = stripQuotes(rest).replace(/^>-?$/, '');
273
+ }
274
+
275
+ return { scalars, nested, keyOrder };
276
+ }
277
+
278
+ /**
279
+ * The scalar-only view, for the call sites that want exactly what they had.
280
+ *
281
+ * A nested key is rendered back into its folded string here so a caller that
282
+ * only knows about scalars still sees *something* for `provenance` rather than
283
+ * `undefined` — which matters for the index reader, where an absent key and an
284
+ * empty key print differently.
285
+ */
286
+ export function readAdrFrontmatterScalars(text: string): Record<string, string> | null {
287
+ const parsed = readAdrFrontmatter(text);
288
+ if (parsed === null) return null;
289
+ const out: Record<string, string> = { ...parsed.scalars };
290
+ for (const [key, node] of Object.entries(parsed.nested)) {
291
+ out[key] = foldToString(node);
292
+ }
293
+ return out;
294
+ }
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // Typed accessors for the two axes. Deliberately tolerant: a census proposal
298
+ // and a hand-authored record both flow through here, and a malformed value has
299
+ // to be reportable rather than fatal.
300
+ // ---------------------------------------------------------------------------
301
+
302
+ export const PROVENANCE_KINDS = ['human', 'agentic', 'mixed', 'unknown'] as const;
303
+ export type ProvenanceKind = (typeof PROVENANCE_KINDS)[number];
304
+
305
+ export const AGENTIC_MODES = ['single', 'council', 'delegated'] as const;
306
+ export const EVIDENCE_STRENGTHS = ['E0', 'E1', 'E2', 'E3', 'E4'] as const;
307
+ export type EvidenceStrength = (typeof EVIDENCE_STRENGTHS)[number];
308
+
309
+ export const DISCOVERY_STATES = ['complete', 'incomplete'] as const;
310
+ export const AUTHORITY_BASES = ['evidence', 'owner_intent'] as const;
311
+
312
+ export interface AdrProvenance {
313
+ kind: string | null;
314
+ decisionMakers: string[];
315
+ humanDirected: string | null;
316
+ agenticMode: string | null;
317
+ }
318
+
319
+ export interface AdrEvidence {
320
+ strength: string | null;
321
+ discovery: string | null;
322
+ basis: string[];
323
+ }
324
+
325
+ function asString(node: AdrFrontmatterNode | undefined): string | null {
326
+ if (typeof node === 'string') return node === '' ? null : node;
327
+ return null;
328
+ }
329
+
330
+ function asList(node: AdrFrontmatterNode | undefined): string[] {
331
+ if (Array.isArray(node)) return node;
332
+ if (typeof node === 'string' && node !== '') return [node];
333
+ return [];
334
+ }
335
+
336
+ export function provenanceOf(fm: AdrFrontmatter): AdrProvenance | null {
337
+ const node = fm.nested.provenance;
338
+ if (node === undefined || typeof node === 'string' || Array.isArray(node)) return null;
339
+ return {
340
+ kind: asString(node.kind),
341
+ decisionMakers: asList(node.decision_makers),
342
+ humanDirected: asString(node.human_directed),
343
+ agenticMode: asString(node.agentic_mode),
344
+ };
345
+ }
346
+
347
+ export function evidenceOf(fm: AdrFrontmatter): AdrEvidence | null {
348
+ const node = fm.nested.evidence;
349
+ if (node === undefined || typeof node === 'string' || Array.isArray(node)) return null;
350
+ return {
351
+ strength: asString(node.strength),
352
+ discovery: asString(node.discovery),
353
+ basis: asList(node.basis),
354
+ };
355
+ }
356
+
357
+ export function authorityBasisOf(fm: AdrFrontmatter): string | null {
358
+ return fm.scalars.authority_basis ?? null;
359
+ }
360
+
361
+ /**
362
+ * Does this record's own metadata say a cite-time reader must NOT treat it as
363
+ * establishing that the alternatives remain invalid?
364
+ *
365
+ * True for an accepted record graded E0 or E1, whatever its provenance.
366
+ *
367
+ * **Provenance was in this predicate and has been removed** (neutral review,
368
+ * 2026-08-21). It required `kind === 'agentic'`, so an identical E0 record made
369
+ * by a human printed no notice at all. That was wrong on the claim's own terms:
370
+ * "this record does not by itself establish that alternatives remain invalid"
371
+ * is a statement about the strength of the EVIDENCE, and a human snapshot is
372
+ * exactly as thin as an agent snapshot. Gating it on who decided also sat one
373
+ * step sideways from `adr-layout`'s own line that a record's historical
374
+ * decision-maker does not determine how it is treated on citation.
375
+ *
376
+ * `authority_basis: owner_intent` is the one exemption, and it is the RIGHT
377
+ * discriminator where provenance was the wrong one. An owner purpose statement
378
+ * is legitimately E0 — that is the honest form — and its alternatives ARE
379
+ * foreclosed, by ownership rather than by evidence. Printing "this does not
380
+ * establish that alternatives remain invalid" over it would be false. The
381
+ * declared field says which kind of authority a record rests on, so it is the
382
+ * field that decides, and the burden table already reads `owner_intent` as
383
+ * binding until the owner changes it.
384
+ *
385
+ * That is also why the earlier provenance gate was subtly wrong rather than
386
+ * simply wrong: it was reaching for this exemption and grabbing a correlate.
387
+ * Most owner-intent records are human-made, so `kind === 'agentic'` approximated
388
+ * it — and silently withheld the notice from every thin HUMAN record that
389
+ * carried no owner claim at all.
390
+ *
391
+ * The name says `lowEvidence` rather than `provisionalAuthority` for a second
392
+ * reason from the same review: ADR-239 and the roadmap both explicitly REJECT
393
+ * the word "provisional" for this output, because it asserts a permission that
394
+ * does not exist. Identifiers carrying the rejected vocabulary would have been
395
+ * the doctrine contradicted in its own implementation.
396
+ *
397
+ * What the caller prints is `authority_effect: disabled-shadow-mode` — a
398
+ * DISABLED effect, not a provisional permission. No grade authorizes anything.
399
+ */
400
+ export function isLowEvidenceAccepted(fm: AdrFrontmatter): boolean {
401
+ if ((fm.scalars.status ?? '') !== 'accepted') return false;
402
+ if (authorityBasisOf(fm) === 'owner_intent') return false;
403
+ const evidence = evidenceOf(fm);
404
+ return evidence?.strength === 'E0' || evidence?.strength === 'E1';
405
+ }
406
+
407
+ // ---------------------------------------------------------------------------
408
+ // Index cells. The two nested axes rendered as two flat table cells, in ONE
409
+ // place, because two generated tables print them (`adr/regenerate_index.ts`
410
+ // and `audit_adr_coverage.render_area_readme`) and both are byte-compared by a
411
+ // `--check` gate — a second copy of the em-dash-on-absent convention would be
412
+ // two gates disagreeing about one record.
413
+ // ---------------------------------------------------------------------------
414
+
415
+ /** The placeholder both generated tables already use for an absent scalar. */
416
+ const CELL_ABSENT = '—';
417
+
418
+ /**
419
+ * Reserved row keys the index generators spread over their frontmatter scalars.
420
+ *
421
+ * Named apart from the frontmatter keys they derive from (`provenance`,
422
+ * `evidence`), which `readAdrFrontmatterScalars` also emits as folded strings —
423
+ * a folded `kind: agentic decision_makers: [...]` is not a table cell, and
424
+ * colliding with those keys would put it in one.
425
+ */
426
+ export interface AdrAxisCells {
427
+ provenance_kind: string;
428
+ evidence_grade: string;
429
+ }
430
+
431
+ /**
432
+ * `provenance.kind` and `evidence.strength` as table cells.
433
+ *
434
+ * `discovery` rides in the evidence cell rather than in a third column: a bare
435
+ * `E0` collapses the five states `adr-layout § evidence.discovery` separates,
436
+ * so an index that printed the grade alone would re-create exactly the
437
+ * conflation the axis exists to undo.
438
+ *
439
+ * Most of the corpus carries neither axis and is not expected to — the backfill
440
+ * is deliberately out of this roadmap — so `—` is the common answer and means
441
+ * "not assessed", never "assessed as weak".
442
+ */
443
+ export function readAdrAxisCells(text: string): AdrAxisCells {
444
+ const fm = readAdrFrontmatter(text);
445
+ if (fm === null) return { provenance_kind: CELL_ABSENT, evidence_grade: CELL_ABSENT };
446
+ const provenance = provenanceOf(fm);
447
+ const evidence = evidenceOf(fm);
448
+ const strength = evidence?.strength ?? null;
449
+ const discovery = evidence?.discovery ?? null;
450
+ return {
451
+ provenance_kind: provenance?.kind ?? CELL_ABSENT,
452
+ evidence_grade:
453
+ strength === null
454
+ ? CELL_ABSENT
455
+ : discovery === null
456
+ ? strength
457
+ : `${strength} (${discovery})`,
458
+ };
459
+ }
@@ -160,7 +160,17 @@ export function readLastVerify(root: string, session_id: string | null): string
160
160
  const command = typeof rec['command'] === 'string' ? rec['command'] : null;
161
161
  if (!command) return null;
162
162
  const at = typeof rec['at'] === 'string' ? rec['at'] : 'unknown time';
163
- return `${command} @ ${at} (no exit status is recorded anywhere)`.slice(0, MAX_FIELD_CHARS);
163
+ // Collapse whitespace BEFORE slicing. A recorded command is whatever
164
+ // the operator ran, and a heredoc (`git commit -F - <<'MSG'`) records a
165
+ // multi-line string — so slicing to 200 chars kept the newlines, and
166
+ // `validateRecycleEnvelope`'s `isShortLine` rejects any value containing
167
+ // one. The result was that `session:recycle` refused its OWN
168
+ // machine-collected field: composing a valid envelope was impossible
169
+ // whenever the previous verification command spanned more than one line,
170
+ // and the error named `last_verify` without hinting that the composer
171
+ // had not supplied it. Reproduced 2026-08-21 on a 512-char heredoc.
172
+ const flat = `${command} @ ${at} (no exit status is recorded anywhere)`.replace(/\s+/g, ' ').trim();
173
+ return flat.slice(0, MAX_FIELD_CHARS);
164
174
  } catch {
165
175
  return null;
166
176
  }
@@ -28,12 +28,21 @@ import * as path from 'node:path';
28
28
  /**
29
29
  * Artefact types both host layers can carry.
30
30
  *
31
- * All four, matching `check_single_delivery` (R2 finding): this notice read three
31
+ * All five, matching `check_single_delivery` (R2 finding): this notice read three
32
32
  * where the gate reads four, so a subagent duplicated across layers was invisible
33
33
  * on one surface and reported on the other — two surfaces disagreeing about the
34
34
  * same question is worse than one surface being silent.
35
+ *
36
+ * `personas` was added 2026-08-21. Both lists had read four types while the
37
+ * installer's `_CLAUDE_SKILL_BUNDLE` (`install.ts:1916-1921`) ships
38
+ * rules · skills · commands · **personas** — so the one family this repository
39
+ * actually delivered from both layers was the one neither surface looked at
40
+ * (29 shared names on a freshly regenerated tree). The two lists stay
41
+ * hand-written and independent of the installer's on purpose: deriving the
42
+ * verifier's scope from the producer would make the check inherit the producer's
43
+ * omissions, which is the defect being closed here.
35
44
  */
36
- const TYPES = ['rules', 'skills', 'commands', 'agents'] as const;
45
+ const TYPES = ['rules', 'skills', 'commands', 'personas', 'user-types', 'agents'] as const;
37
46
 
38
47
  /**
39
48
  * Count shared entry names per artefact type between the two layers.
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Remove empty directories under a root, deepest first.
3
+ *
4
+ * ## Why this exists as a shared helper
5
+ *
6
+ * A generator that stops writing into a tree leaves the tree's DIRECTORIES
7
+ * behind. Measured 2026-08-21 while extending the ADR-236 partition to the
8
+ * colon-form commands: after the generator was gated, `.claude/commands/`
9
+ * held **0 `.md` files and 40 empty cluster directories** — and
10
+ * `check_single_delivery` counts directory NAMES, so it still reported 40
11
+ * overlapping commands against a layer that delivered none. A partition that
12
+ * stops the writes and leaves the shape is invisible to the gate that is
13
+ * supposed to confirm it.
14
+ *
15
+ * `condense.ts` already carried this loop inline for the skills tree. It is
16
+ * lifted here rather than copied because the second caller is what turns an
17
+ * inline block into a shape two producers must agree on — and because
18
+ * `condense.ts` sits above `check_source_size_budget`'s ceiling, where a
19
+ * shrink-only ratchet refuses even a small addition.
20
+ *
21
+ * Best-effort by construction: a race or a permission error on one directory is
22
+ * swallowed, matching the inline original. Pruning is cleanup, never a gate.
23
+ *
24
+ * ## Symlinks: MEASURED, because a neutral review split on it
25
+ *
26
+ * One seat called an unguarded recursion a merge blocker (a directory symlink
27
+ * pointing at an ancestor would loop forever); the other called the finding
28
+ * false, on the grounds that `Dirent.isDirectory()` does not follow the target.
29
+ * A vote cannot settle that, so it was probed — `readdirSync(dir, {withFileTypes:
30
+ * true})` over a real symlink-to-directory on darwin, Node 26:
31
+ *
32
+ * link isDirectory=false isSymbolicLink=true
33
+ * real isDirectory=true isSymbolicLink=false
34
+ *
35
+ * `Dirent` carries `lstat` semantics, so the walk below cannot descend into a
36
+ * symlink at all and no loop is reachable — and no inode set is needed. The
37
+ * second seat was right. Recorded rather than fixed, so the next reader does not
38
+ * re-raise it: a defensive `!e.isSymbolicLink()` here would be dead code
39
+ * asserting something the type already guarantees.
40
+ *
41
+ * The same property is why this is safe on a tree of symlinks: `.claude/commands`
42
+ * holds symlinks into `src/domains/**`, and pruning must not follow one out of
43
+ * the tree it was pointed at.
44
+ */
45
+ import * as fs from 'node:fs';
46
+ import * as path from 'node:path';
47
+
48
+ /** @returns the number of directories removed. */
49
+ export function pruneEmptyDirs(root: string): number {
50
+ let removed = 0;
51
+ const walk = (dir: string): void => {
52
+ let entries: fs.Dirent[];
53
+ try {
54
+ entries = fs.readdirSync(dir, { withFileTypes: true });
55
+ } catch {
56
+ return;
57
+ }
58
+ for (const e of entries) {
59
+ if (e.isDirectory()) walk(path.join(dir, e.name));
60
+ }
61
+ try {
62
+ if (fs.readdirSync(dir).length === 0) {
63
+ fs.rmdirSync(dir);
64
+ removed += 1;
65
+ }
66
+ } catch {
67
+ /* race or permission — cleanup is best-effort, mirrors the inline original */
68
+ }
69
+ };
70
+ let top: fs.Dirent[];
71
+ try {
72
+ top = fs.readdirSync(root, { withFileTypes: true });
73
+ } catch {
74
+ return 0;
75
+ }
76
+ for (const e of top) {
77
+ if (e.isDirectory()) walk(path.join(root, e.name));
78
+ }
79
+ return removed;
80
+ }