@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +245 -388
- package/README.md +1 -1
- package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
- package/dist/agent-src/commands/evals/roadmap.json +17 -1
- package/dist/agent-src/commands/pr/create.md +14 -2
- package/dist/agent-src/commands/pr/merge.md +379 -0
- package/dist/agent-src/commands/roadmap/process-full.md +157 -3
- package/dist/agent-src/commands/roadmap.md +1 -1
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +64 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +154 -2
- package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
- package/dist/agent-src/rules/decision-revisit-gate.md +65 -33
- package/dist/agent-src/rules/recurring-criticism.md +1 -1
- package/dist/agent-src/rules/roadmap-progress-sync.md +19 -0
- package/dist/agent-src/rules/source-confidentiality.md +1 -1
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
- package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
- package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
- package/dist/agent-src/skills/security/SKILL.md +8 -0
- package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +20 -2
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-JZ4NR2TW.js → chunk-7PRE6ZV2.js} +1 -1
- package/dist/cli-delegate/{chunk-7F3VYSM2.js → chunk-EEJUMCDZ.js} +14 -14
- package/dist/cli-delegate/{chunk-74E3TKWV.js → chunk-H4VIVD4C.js} +5 -5
- package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
- package/dist/cli-delegate/{chunk-4VR4ORQ6.js → chunk-LPPWO5CE.js} +4 -3
- package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-N7CDDGI3.js} +4 -23
- package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
- package/dist/cli-delegate/cmd_conformance.js +14 -14
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +7 -7
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_handoff.js +7 -6
- package/dist/cli-delegate/cmd_memory_get.js +2 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +5 -5
- package/dist/cli-delegate/cmd_prune.js +6 -6
- package/dist/cli-delegate/cmd_route_audit.js +6 -5
- package/dist/cli-delegate/cmd_route_explain.js +3 -3
- package/dist/cli-delegate/cmd_session_recycle.js +3 -3
- package/dist/cli-delegate/cmd_settings_get.js +3 -3
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- package/dist/cli-delegate/cmd_update.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +53 -22
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -6
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -5
- package/dist/hooks/dispatch.js +806 -681
- package/dist/install/globalRuleLayers.js +169 -0
- package/dist/install/globalRuleLayers.js.map +1 -0
- package/dist/install/partitionEligibility.js +156 -0
- package/dist/install/partitionEligibility.js.map +1 -1
- package/dist/install/ruleLayerPartition.js +157 -0
- package/dist/install/ruleLayerPartition.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +654 -578
- package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
- package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
- package/dist/scripts/condense.js +52 -89
- package/dist/scripts/condense.js.map +1 -1
- package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
- package/docs/guidelines/agent-infra/false-green.md +47 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/src/agent-src/scripts/dashboard_mode.ts +212 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/src/config/ci-local-parity.yml +21 -0
- package/src/config/estate-count-budget.json +275 -2
- package/src/config/evaluator-budgets.json +3 -3
- package/src/config/gate-coverage.yml +220 -12
- package/src/config/gate-violation-baselines.json +73 -63
- package/src/config/gitignore-block.txt +15 -2
- package/src/config/hook-latency-budget.json +5 -0
- package/src/config/rule-stub-ceilings.json +17 -3
- package/src/install/globalRuleLayers.ts +197 -0
- package/src/install/partitionEligibility.ts +177 -0
- package/src/install/ruleLayerPartition.ts +212 -0
- package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
- package/src/scripts/_dispatch.bash +11 -0
- package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
- package/src/scripts/_lib/adr_frontmatter.ts +459 -0
- package/src/scripts/_lib/envelope_grounding.ts +11 -1
- package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
- package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
- package/src/scripts/adr/evidence_census.ts +899 -0
- package/src/scripts/adr/regenerate_index.ts +12 -32
- package/src/scripts/adr_cite_check.ts +540 -39
- package/src/scripts/audit_adr_coverage.ts +103 -17
- package/src/scripts/bench_hook_latency.ts +204 -0
- package/src/scripts/build_archive_index.ts +34 -0
- package/src/scripts/check_adr_frontmatter.ts +574 -39
- package/src/scripts/check_hook_bundle_content.ts +219 -0
- package/src/scripts/check_new_adr_evidence.ts +529 -0
- package/src/scripts/check_references.ts +18 -0
- package/src/scripts/check_rule_layer_partition.ts +531 -0
- package/src/scripts/check_single_delivery.ts +137 -4
- package/src/scripts/check_standing_rule_delivery.ts +8 -2
- package/src/scripts/condense.ts +61 -91
- package/src/scripts/external_sources_denylist.json +4 -1
- package/src/scripts/hooks/block_unauthorized_git.ts +22 -2
- package/src/scripts/hooks/dispatch_hook.ts +154 -187
- package/src/scripts/hooks/dispatch_issues.ts +38 -19
- package/src/scripts/hooks/fallback_yaml.ts +66 -0
- package/src/scripts/hooks/host_semantics.ts +21 -0
- package/src/scripts/hooks/injection_budget.ts +13 -3
- package/src/scripts/hooks/py_json_dumps.ts +103 -0
- package/src/scripts/hooks/state_io.ts +88 -1
- package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
- package/src/scripts/injection_scan_hook.ts +137 -5
- package/src/scripts/lint_hook_manifest.ts +49 -0
- package/src/scripts/lint_provenance_vocabulary.ts +472 -4
- package/src/scripts/routing_doctor.ts +5 -1
- package/src/scripts/sync_pr_branch.ts +163 -20
- package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
- package/dist/cli-delegate/{chunk-6EOCNQAM.js → chunk-E4XIZDIB.js} +0 -0
- package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-MMSGVZKV.js} +3 -3
- 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
|
-
|
|
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
|
|
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
|
+
}
|