@event4u/agent-config 12.1.0 → 13.0.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 +151 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/cost/report.md +23 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
- package/dist/agent-src/contexts/subagent-configuration.md +69 -7
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/design-fidelity.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +10 -5
- package/dist/agent-src/rules/evaluator-independence.md +27 -5
- package/dist/agent-src/rules/git-history-discipline.md +5 -1
- package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
- package/dist/agent-src/rules/ui-audit-gate.md +9 -4
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +17 -2
- package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
- package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +4 -4
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
- package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/{chunk-O52B4F5S.js → chunk-3B72LSZL.js} +1 -1
- package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
- package/dist/cli-delegate/{chunk-K34QTSD5.js → chunk-N7FEST6D.js} +17 -17
- package/dist/cli-delegate/{chunk-LKZ5DOYW.js → chunk-XURJ4AQC.js} +11 -3
- package/dist/cli-delegate/cmd_conformance.js +2 -2
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +43 -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 +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +936 -346
- package/dist/install/install.mjs +12 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1192 -160
- package/dist/router.json +1 -1
- package/dist/server/schemas/settings.js +2 -2
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/guidelines/design-fidelity-mechanics.md +21 -0
- package/docs/guidelines/design-handover-extraction.md +87 -0
- package/docs/guidelines/prompt-templates.md +24 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +17 -2
- package/src/config/evaluator-budgets.json +5 -4
- package/src/config/gate-coverage.yml +33 -11
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/hook-token-budget.json +8 -1
- package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
- package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
- package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
- package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
- package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
- package/src/scripts/_lib/skill_catalogue.ts +258 -5
- package/src/scripts/_lib/tier_budget_routing.ts +31 -331
- package/src/scripts/ai_council/clients.ts +21 -21
- package/src/scripts/bench_ab_v2_complexity.ts +252 -0
- package/src/scripts/bench_ab_v2_run.ts +3 -42
- package/src/scripts/bench_ab_v2_safety.ts +296 -0
- package/src/scripts/bench_ab_v2_search.ts +343 -0
- package/src/scripts/bench_ab_v2_stats.ts +191 -0
- package/src/scripts/build_archive_index.ts +383 -0
- package/src/scripts/capture_skill_catalogue.ts +107 -2
- package/src/scripts/check_source_size_budget.ts +428 -0
- package/src/scripts/check_suppression_hygiene.ts +5 -1
- package/src/scripts/cost/backfill_rates.mjs +298 -0
- package/src/scripts/cost/budget.mjs +12 -32
- package/src/scripts/cost_summary.ts +25 -0
- package/src/scripts/external_sources_denylist.json +1 -0
- package/src/scripts/hook_manifest.yaml +28 -2
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/skill_route_hook.ts +226 -0
- package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
- package/src/scripts/lint_design_antipattern_parity.ts +40 -6
- package/src/scripts/lint_harvest_provenance.ts +35 -1
- package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
- package/src/scripts/lint_skill_router_head.ts +21 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
- package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
- package/src/scripts/mcp_server/tools.ts +87 -0
- package/src/scripts/release.ts +5 -5
- package/src/scripts/routing_doctor.ts +50 -13
- package/src/scripts/schemas/command.schema.json +1 -1
- package/src/scripts/schemas/skill.schema.json +18 -1
- package/src/scripts/trigger_coverage.ts +224 -3
- package/src/server/schemas/settings.ts +2 -2
- package/src/config/budget-routing.json +0 -7
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Build a machine-readable index over the roadmap archive.
|
|
4
|
+
*
|
|
5
|
+
* "Has this already been tried, closed, or refuted?" is answered today by
|
|
6
|
+
* walking `agents/roadmaps/archive/` — 500 top-level files. This emits
|
|
7
|
+
* `INDEX.md` (human) and `index.json` (machine) beside them so the question
|
|
8
|
+
* costs one read instead of a walk.
|
|
9
|
+
*
|
|
10
|
+
* ## What is and is not extracted
|
|
11
|
+
*
|
|
12
|
+
* Every field is derived by a deterministic rule from the file's own bytes.
|
|
13
|
+
* There are NO model-written summaries: a verdict the frontmatter does not
|
|
14
|
+
* carry is emitted as `null` in JSON and `not extractable` in the table, so a
|
|
15
|
+
* reader can tell a missing verdict from an invented one. That is the
|
|
16
|
+
* roadmap's non-goal, enforced here rather than promised.
|
|
17
|
+
*
|
|
18
|
+
* `disposition` is the one derived field, and it is derived from the checkbox
|
|
19
|
+
* tally alone — including acceptance-criteria boxes, which are checkboxes in
|
|
20
|
+
* the same file and are not distinguished. It records how the file LOOKS at
|
|
21
|
+
* archival, never why it was archived:
|
|
22
|
+
*
|
|
23
|
+
* | tally | disposition |
|
|
24
|
+
* |---|---|
|
|
25
|
+
* | no checkbox anywhere | `not-extractable` |
|
|
26
|
+
* | any `[ ]` | `archived-with-open-steps` |
|
|
27
|
+
* | any `[-]`, none open | `closed-with-cancellations` |
|
|
28
|
+
* | any `[~]`, none open/cancelled | `completed-with-deferrals` |
|
|
29
|
+
* | otherwise | `completed` |
|
|
30
|
+
*
|
|
31
|
+
* ## Drift
|
|
32
|
+
*
|
|
33
|
+
* `--check` regenerates in memory and compares byte-for-byte against the two
|
|
34
|
+
* committed artefacts, following the shape `compile_router --check` already
|
|
35
|
+
* uses. A generated artefact nobody re-derives goes stale in one merge.
|
|
36
|
+
*
|
|
37
|
+
* The output carries no timestamp on purpose — a clock in a generated file
|
|
38
|
+
* makes every `--check` run a false red. The generation time lives in git
|
|
39
|
+
* history, the same convention `agents/roadmaps-progress.md` states.
|
|
40
|
+
*
|
|
41
|
+
* Reuses `validate_frontmatter`'s reader; this file adds no second parser.
|
|
42
|
+
*/
|
|
43
|
+
import * as fs from 'node:fs';
|
|
44
|
+
import * as path from 'node:path';
|
|
45
|
+
import process from 'node:process';
|
|
46
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
47
|
+
|
|
48
|
+
import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
|
|
49
|
+
import { parse_frontmatter } from './validate_frontmatter.js';
|
|
50
|
+
|
|
51
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
52
|
+
export const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
53
|
+
|
|
54
|
+
const ARCHIVE_REL = 'agents/roadmaps/archive';
|
|
55
|
+
const INDEX_MD = 'INDEX.md';
|
|
56
|
+
const INDEX_JSON = 'index.json';
|
|
57
|
+
|
|
58
|
+
/** The generated artefacts themselves are never indexed. */
|
|
59
|
+
const SELF: ReadonlySet<string> = new Set([INDEX_MD, INDEX_JSON]);
|
|
60
|
+
|
|
61
|
+
/** `- [ ]` / `* [x]` / `- [~]` / `- [-]`, the four glyphs the roadmaps use. */
|
|
62
|
+
const CHECKBOX_RE = /^[ \t]*[-*][ \t]+\[([ xX~-])\][ \t]/;
|
|
63
|
+
|
|
64
|
+
/** `## Phase 3 — …` or `### Phase 0: …`. Two heading levels are in use. */
|
|
65
|
+
const PHASE_RE = /^#{2,3}[ \t]+Phase\b/;
|
|
66
|
+
|
|
67
|
+
/** The first level-1 heading, used as the title when frontmatter has none. */
|
|
68
|
+
const H1_RE = /^#[ \t]+(.+?)[ \t]*$/;
|
|
69
|
+
|
|
70
|
+
export type Disposition =
|
|
71
|
+
| 'completed'
|
|
72
|
+
| 'completed-with-deferrals'
|
|
73
|
+
| 'closed-with-cancellations'
|
|
74
|
+
| 'archived-with-open-steps'
|
|
75
|
+
| 'not-extractable';
|
|
76
|
+
|
|
77
|
+
export interface StepTally {
|
|
78
|
+
open: number;
|
|
79
|
+
done: number;
|
|
80
|
+
deferred: number;
|
|
81
|
+
cancelled: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ArchiveEntry {
|
|
85
|
+
slug: string;
|
|
86
|
+
title: string | null;
|
|
87
|
+
disposition: Disposition;
|
|
88
|
+
phases: number;
|
|
89
|
+
steps: StepTally;
|
|
90
|
+
/** Frontmatter `verdict:`, verbatim. `null` = the file carries none. */
|
|
91
|
+
verdict: string | null;
|
|
92
|
+
/** Frontmatter `status:`, verbatim. Authoring status, not a closure. */
|
|
93
|
+
status: string | null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Frontmatter value → string, or `null` for absent / non-scalar. */
|
|
97
|
+
function _scalar(value: unknown): string | null {
|
|
98
|
+
if (typeof value === 'string') {
|
|
99
|
+
const t = value.trim();
|
|
100
|
+
return t === '' ? null : t;
|
|
101
|
+
}
|
|
102
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
103
|
+
return String(value);
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Frontmatter, or `{}` when the file has none or carries a malformed block.
|
|
110
|
+
*
|
|
111
|
+
* A 500-file corpus assembled over months contains frontmatter the strict
|
|
112
|
+
* subset parser rejects; that is a property of the archive, not an error in
|
|
113
|
+
* this pass. Such a file still gets an entry — with its frontmatter-derived
|
|
114
|
+
* fields absent, which is exactly what "not extractable" means.
|
|
115
|
+
*/
|
|
116
|
+
function _frontmatter(text: string): Record<string, unknown> {
|
|
117
|
+
try {
|
|
118
|
+
const [data] = parse_frontmatter(text);
|
|
119
|
+
return data ?? {};
|
|
120
|
+
} catch {
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Body lines, i.e. everything after a frontmatter block if one is present. */
|
|
126
|
+
function _bodyLines(text: string): string[] {
|
|
127
|
+
const lines = text.split('\n');
|
|
128
|
+
if (lines[0]?.trimEnd() !== '---') {
|
|
129
|
+
return lines;
|
|
130
|
+
}
|
|
131
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
132
|
+
if (lines[i]?.trimEnd() === '---') {
|
|
133
|
+
return lines.slice(i + 1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return lines;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function tally(bodyLines: readonly string[]): StepTally {
|
|
140
|
+
const t: StepTally = { open: 0, done: 0, deferred: 0, cancelled: 0 };
|
|
141
|
+
for (const line of bodyLines) {
|
|
142
|
+
const m = CHECKBOX_RE.exec(line);
|
|
143
|
+
if (m === null) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
switch (m[1]) {
|
|
147
|
+
case ' ':
|
|
148
|
+
t.open += 1;
|
|
149
|
+
break;
|
|
150
|
+
case '~':
|
|
151
|
+
t.deferred += 1;
|
|
152
|
+
break;
|
|
153
|
+
case '-':
|
|
154
|
+
t.cancelled += 1;
|
|
155
|
+
break;
|
|
156
|
+
default:
|
|
157
|
+
t.done += 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return t;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function disposition(t: StepTally): Disposition {
|
|
164
|
+
if (t.open + t.done + t.deferred + t.cancelled === 0) {
|
|
165
|
+
return 'not-extractable';
|
|
166
|
+
}
|
|
167
|
+
if (t.open > 0) {
|
|
168
|
+
return 'archived-with-open-steps';
|
|
169
|
+
}
|
|
170
|
+
if (t.cancelled > 0) {
|
|
171
|
+
return 'closed-with-cancellations';
|
|
172
|
+
}
|
|
173
|
+
if (t.deferred > 0) {
|
|
174
|
+
return 'completed-with-deferrals';
|
|
175
|
+
}
|
|
176
|
+
return 'completed';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function buildEntry(slug: string, text: string): ArchiveEntry {
|
|
180
|
+
const fm = _frontmatter(text);
|
|
181
|
+
const body = _bodyLines(text);
|
|
182
|
+
|
|
183
|
+
let title = _scalar(fm['title']);
|
|
184
|
+
if (title === null) {
|
|
185
|
+
for (const line of body) {
|
|
186
|
+
const m = H1_RE.exec(line);
|
|
187
|
+
if (m !== null) {
|
|
188
|
+
title = (m[1] ?? '').trim() || null;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const steps = tally(body);
|
|
195
|
+
return {
|
|
196
|
+
slug,
|
|
197
|
+
title,
|
|
198
|
+
disposition: disposition(steps),
|
|
199
|
+
phases: body.filter((l) => PHASE_RE.test(l)).length,
|
|
200
|
+
steps,
|
|
201
|
+
verdict: _scalar(fm['verdict']),
|
|
202
|
+
status: _scalar(fm['status']),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Top-level `*.md` under the archive, sorted, excluding the generated pair. */
|
|
207
|
+
export function archiveFiles(dir: string): string[] {
|
|
208
|
+
let entries: fs.Dirent[];
|
|
209
|
+
try {
|
|
210
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
211
|
+
} catch {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
return entries
|
|
215
|
+
.filter((e) => !e.isDirectory() && e.name.endsWith('.md') && !SELF.has(e.name))
|
|
216
|
+
.map((e) => e.name)
|
|
217
|
+
.sort();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function buildIndex(dir: string): ArchiveEntry[] {
|
|
221
|
+
return archiveFiles(dir).map((name) =>
|
|
222
|
+
buildEntry(name.replace(/\.md$/, ''), fs.readFileSync(path.join(dir, name), 'utf8')),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Table-cell text: pipes escaped, newlines flattened, never empty. */
|
|
227
|
+
function _cell(value: string | null): string {
|
|
228
|
+
if (value === null) {
|
|
229
|
+
return '_not extractable_';
|
|
230
|
+
}
|
|
231
|
+
return value.replace(/\|/g, '\\|').replace(/\s+/g, ' ').trim();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function renderJson(entries: readonly ArchiveEntry[]): string {
|
|
235
|
+
return (
|
|
236
|
+
JSON.stringify(
|
|
237
|
+
{
|
|
238
|
+
generated_by: 'src/scripts/build_archive_index.ts',
|
|
239
|
+
source: ARCHIVE_REL,
|
|
240
|
+
count: entries.length,
|
|
241
|
+
entries,
|
|
242
|
+
},
|
|
243
|
+
null,
|
|
244
|
+
2,
|
|
245
|
+
) + '\n'
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function renderMarkdown(entries: readonly ArchiveEntry[]): string {
|
|
250
|
+
const byDisposition = new Map<Disposition, number>();
|
|
251
|
+
for (const e of entries) {
|
|
252
|
+
byDisposition.set(e.disposition, (byDisposition.get(e.disposition) ?? 0) + 1);
|
|
253
|
+
}
|
|
254
|
+
const summary = [...byDisposition.entries()]
|
|
255
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : 1))
|
|
256
|
+
.map(([d, n]) => `${d} ${String(n)}`)
|
|
257
|
+
.join(' · ');
|
|
258
|
+
|
|
259
|
+
const out: string[] = [
|
|
260
|
+
'# Archive index',
|
|
261
|
+
'',
|
|
262
|
+
'> Auto-generated — do not edit. Regenerate with `task build-archive-index`;',
|
|
263
|
+
'> `task check-archive-index` fails on drift. Generation time lives in git',
|
|
264
|
+
'> history, not in this file (a clock here would make every drift check a',
|
|
265
|
+
'> false red).',
|
|
266
|
+
'',
|
|
267
|
+
`**${String(entries.length)} archived roadmaps** · ${summary}`,
|
|
268
|
+
'',
|
|
269
|
+
'Every column is extracted deterministically from the file itself. No',
|
|
270
|
+
'summary here is model-written: a verdict the frontmatter does not carry',
|
|
271
|
+
'reads _not extractable_ rather than being inferred. `Disposition` is',
|
|
272
|
+
'derived from the checkbox tally alone (acceptance-criteria boxes',
|
|
273
|
+
'included) and records how the file looks, never why it was archived —',
|
|
274
|
+
'so for anything the index marks _not extractable_, open the file.',
|
|
275
|
+
'',
|
|
276
|
+
'**Limits worth knowing before you rely on this.** Only a handful of',
|
|
277
|
+
'archived roadmaps carry a frontmatter `verdict:`, so the index answers',
|
|
278
|
+
'_tried?_ and _closed?_ and sends you to the file for _why_. The measured',
|
|
279
|
+
'saving is 82 % fewer files opened on a pre-registered question set — and',
|
|
280
|
+
'below roughly five candidate files it is a wash, so a single narrow',
|
|
281
|
+
'question is cheaper answered directly. Reading, method and full',
|
|
282
|
+
'distribution: [`archive-index-saving`](../../evidence/analysis/archive-index-saving.md).',
|
|
283
|
+
'',
|
|
284
|
+
'| Roadmap | Title | Disposition | Phases | Steps (done/total) | Verdict |',
|
|
285
|
+
'|---|---|---|---:|---|---|',
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
for (const e of entries) {
|
|
289
|
+
const total = e.steps.open + e.steps.done + e.steps.deferred + e.steps.cancelled;
|
|
290
|
+
const steps = total === 0 ? '—' : `${String(e.steps.done)}/${String(total)}`;
|
|
291
|
+
out.push(
|
|
292
|
+
`| [\`${e.slug}\`](${e.slug}.md) | ${_cell(e.title)} | ${e.disposition} | ` +
|
|
293
|
+
`${String(e.phases)} | ${steps} | ${_cell(e.verdict)} |`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
out.push('');
|
|
297
|
+
return out.join('\n');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function _read(p: string): string | null {
|
|
301
|
+
try {
|
|
302
|
+
return fs.readFileSync(p, 'utf8');
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function main(argv: readonly string[]): number {
|
|
309
|
+
const quiet = argv.includes('--quiet');
|
|
310
|
+
const dir = path.join(ROOT, ARCHIVE_REL);
|
|
311
|
+
|
|
312
|
+
const entries = buildIndex(dir);
|
|
313
|
+
try {
|
|
314
|
+
reportScanned({
|
|
315
|
+
gate: 'build_archive_index',
|
|
316
|
+
scanned: entries.length,
|
|
317
|
+
units: 'archived roadmap(s)',
|
|
318
|
+
roots: [ARCHIVE_REL],
|
|
319
|
+
});
|
|
320
|
+
} catch (err) {
|
|
321
|
+
if (err instanceof DeadScopeError) {
|
|
322
|
+
process.stderr.write(`${err.message}\n`);
|
|
323
|
+
return 1;
|
|
324
|
+
}
|
|
325
|
+
throw err;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const mdPath = path.join(dir, INDEX_MD);
|
|
329
|
+
const jsonPath = path.join(dir, INDEX_JSON);
|
|
330
|
+
const md = renderMarkdown(entries);
|
|
331
|
+
const json = renderJson(entries);
|
|
332
|
+
|
|
333
|
+
if (argv.includes('--check')) {
|
|
334
|
+
const stale = [
|
|
335
|
+
_read(mdPath) === md ? null : `${ARCHIVE_REL}/${INDEX_MD}`,
|
|
336
|
+
_read(jsonPath) === json ? null : `${ARCHIVE_REL}/${INDEX_JSON}`,
|
|
337
|
+
].filter((v): v is string => v !== null);
|
|
338
|
+
if (stale.length > 0) {
|
|
339
|
+
process.stderr.write(
|
|
340
|
+
`❌ archive index out of date (${stale.join(', ')}) — run \`task build-archive-index\`\n`,
|
|
341
|
+
);
|
|
342
|
+
return 1;
|
|
343
|
+
}
|
|
344
|
+
if (!quiet) {
|
|
345
|
+
process.stdout.write('✅ archive index is up to date\n');
|
|
346
|
+
}
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
fs.writeFileSync(mdPath, md, 'utf8');
|
|
351
|
+
fs.writeFileSync(jsonPath, json, 'utf8');
|
|
352
|
+
if (!quiet) {
|
|
353
|
+
process.stdout.write(
|
|
354
|
+
`✅ archive index — ${String(entries.length)} roadmap(s) → ` +
|
|
355
|
+
`${ARCHIVE_REL}/{${INDEX_MD},${INDEX_JSON}}\n`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function _isCliEntry(): boolean {
|
|
362
|
+
if (process.argv[1] === undefined) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
366
|
+
if (import.meta.url === argvUrl) {
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
// A symlinked invocation (an installed projection, or macOS /var →
|
|
370
|
+
// /private/var temp dirs) makes the raw URLs differ: import.meta.url is
|
|
371
|
+
// the resolved real path while argv[1] keeps the symlink path.
|
|
372
|
+
try {
|
|
373
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
374
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
375
|
+
return here === argv;
|
|
376
|
+
} catch {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
382
|
+
process.exit(main(process.argv.slice(2)));
|
|
383
|
+
}
|
|
@@ -75,16 +75,24 @@ import {
|
|
|
75
75
|
OBSERVATION_LOG,
|
|
76
76
|
analyzeSelector,
|
|
77
77
|
buildHostEventRecord,
|
|
78
|
+
buildNoTruncationRecord,
|
|
78
79
|
buildObservationRecord,
|
|
80
|
+
classifyHostProjection,
|
|
79
81
|
formatPerHostVerdicts,
|
|
82
|
+
formatProjectionModes,
|
|
80
83
|
formatReport,
|
|
81
84
|
knownHostLimits,
|
|
82
85
|
measureCatalogueVolume,
|
|
83
86
|
parseHostBudgetEvent,
|
|
84
87
|
readObservationLog,
|
|
85
88
|
readProjectedCatalogue,
|
|
89
|
+
type HostProjectionRow,
|
|
90
|
+
type ProjectionMode,
|
|
91
|
+
type ProjectionModeCounts,
|
|
86
92
|
type SelectorReport,
|
|
87
93
|
} from './_lib/skill_catalogue.js';
|
|
94
|
+
import { scoped_projection_stats } from './_lib/scoped_projection.js';
|
|
95
|
+
import { iter_skills } from './update_counts.js';
|
|
88
96
|
|
|
89
97
|
// Re-exported so the CLI module stays the one public name for this tool.
|
|
90
98
|
export * from './_lib/skill_catalogue.js';
|
|
@@ -114,6 +122,30 @@ function argValue(flag: string): string | null {
|
|
|
114
122
|
return process.argv[index + 1]!;
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
/** Every occurrence of a repeatable flag, in argv order. */
|
|
126
|
+
function argValues(flag: string): string[] {
|
|
127
|
+
const out: string[] = [];
|
|
128
|
+
for (let i = 0; i < process.argv.length - 1; i += 1) {
|
|
129
|
+
if (process.argv[i] === flag) out.push(process.argv[i + 1]!);
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `--projection-mode <scoped|legacy-all>`, validated against the closed enum.
|
|
136
|
+
*
|
|
137
|
+
* Returns `undefined` when the flag is absent — which the record schema reads
|
|
138
|
+
* as "not captured", never as `legacy-all`. An unrecognised value throws
|
|
139
|
+
* rather than falling back: a typo silently recorded as the wrong mode is the
|
|
140
|
+
* failure this whole module refuses elsewhere.
|
|
141
|
+
*/
|
|
142
|
+
function argProjectionMode(): ProjectionMode | undefined {
|
|
143
|
+
const raw = argValue('--projection-mode');
|
|
144
|
+
if (raw === null) return undefined;
|
|
145
|
+
if (raw === 'scoped' || raw === 'legacy-all') return raw;
|
|
146
|
+
throw new Error(`--projection-mode must be scoped or legacy-all, got: ${raw}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
117
149
|
/** `~` expanded against the current user's home. */
|
|
118
150
|
function expandHome(p: string): string {
|
|
119
151
|
return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p;
|
|
@@ -151,6 +183,47 @@ function runLimitsMode(): number {
|
|
|
151
183
|
return 0;
|
|
152
184
|
}
|
|
153
185
|
|
|
186
|
+
/**
|
|
187
|
+
* `--projection-modes` — the two modes side by side, as a MEASUREMENT.
|
|
188
|
+
*
|
|
189
|
+
* It reads no setting, writes no setting, and recommends no default. The
|
|
190
|
+
* scoped-projection default and its migration notice are owned by a different
|
|
191
|
+
* roadmap and were decided on 2026-08-15; this mode exists so the size of the
|
|
192
|
+
* difference is a published number instead of an intuition, and it deliberately
|
|
193
|
+
* offers no flag that would change anything.
|
|
194
|
+
*
|
|
195
|
+
* `--host-root` is repeatable, so several installed hosts are read against the
|
|
196
|
+
* same pair of numbers in one run. Each host is reported on its own row: no
|
|
197
|
+
* host's count is ever used to say anything about another's.
|
|
198
|
+
*/
|
|
199
|
+
function runProjectionModesMode(): number {
|
|
200
|
+
const stats = scoped_projection_stats(REPO, iter_skills());
|
|
201
|
+
const counts: ProjectionModeCounts = {
|
|
202
|
+
scoped: stats.projected,
|
|
203
|
+
legacyAll: stats.total,
|
|
204
|
+
prunedUnderScoped: stats.pruned,
|
|
205
|
+
activePacks: stats.active_packs,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const rows: HostProjectionRow[] = [];
|
|
209
|
+
for (const rootArg of argValues('--host-root')) {
|
|
210
|
+
const root = expandHome(rootArg);
|
|
211
|
+
if (!fs.existsSync(root)) {
|
|
212
|
+
process.stderr.write(`❌ host root does not exist: ${root}\n`);
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
const host = path.basename(root).replace(/^\./, '');
|
|
216
|
+
rows.push(classifyHostProjection(measureCatalogueVolume(host, root), counts));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (process.argv.includes('--json')) {
|
|
220
|
+
process.stdout.write(`${JSON.stringify({ counts, hosts: rows }, null, 2)}\n`);
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
process.stdout.write(`${formatProjectionModes(counts, rows)}\n`);
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
154
227
|
/** `--volume <host-root>` — the projection half for one host, byte-accurate. */
|
|
155
228
|
function runVolumeMode(rootArg: string): number {
|
|
156
229
|
const root = expandHome(rootArg);
|
|
@@ -213,8 +286,32 @@ function runHostEventMode(source: string): number {
|
|
|
213
286
|
return 1;
|
|
214
287
|
}
|
|
215
288
|
process.stdout.write(
|
|
216
|
-
`host: ${host}\noffered: ${volume.artefacts}
|
|
289
|
+
`host: ${host}\noffered: ${volume.artefacts} (${volume.skillEntries} skills + ${volume.commandEntries} commands)\n` +
|
|
290
|
+
`dropped: 0 (asserted — no budget event in the stream)\n` +
|
|
291
|
+
'descriptions stripped: no — the host published nothing to strip.\n',
|
|
217
292
|
);
|
|
293
|
+
if (process.argv.includes('--record')) {
|
|
294
|
+
const stampedAt = argValue('--observed-at');
|
|
295
|
+
if (!stampedAt) {
|
|
296
|
+
process.stderr.write('❌ --record requires --observed-at <ISO date>\n');
|
|
297
|
+
return 1;
|
|
298
|
+
}
|
|
299
|
+
const logPath = path.join(REPO, OBSERVATION_LOG);
|
|
300
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
301
|
+
fs.appendFileSync(
|
|
302
|
+
logPath,
|
|
303
|
+
`${JSON.stringify(
|
|
304
|
+
buildNoTruncationRecord(
|
|
305
|
+
host,
|
|
306
|
+
stampedAt,
|
|
307
|
+
volume.artefacts,
|
|
308
|
+
volume.skillEntries,
|
|
309
|
+
argProjectionMode(),
|
|
310
|
+
),
|
|
311
|
+
)}\n`,
|
|
312
|
+
);
|
|
313
|
+
process.stdout.write(`recorded → ${OBSERVATION_LOG}\n`);
|
|
314
|
+
}
|
|
218
315
|
return 0;
|
|
219
316
|
}
|
|
220
317
|
|
|
@@ -238,7 +335,14 @@ function runHostEventMode(source: string): number {
|
|
|
238
335
|
}
|
|
239
336
|
const logPath = path.join(REPO, OBSERVATION_LOG);
|
|
240
337
|
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
241
|
-
const record = buildHostEventRecord(
|
|
338
|
+
const record = buildHostEventRecord(
|
|
339
|
+
host,
|
|
340
|
+
stampedAt,
|
|
341
|
+
volume.artefacts,
|
|
342
|
+
event,
|
|
343
|
+
volume.skillEntries,
|
|
344
|
+
argProjectionMode(),
|
|
345
|
+
);
|
|
242
346
|
fs.appendFileSync(logPath, `${JSON.stringify(record)}\n`);
|
|
243
347
|
process.stdout.write(`recorded → ${OBSERVATION_LOG}\n`);
|
|
244
348
|
}
|
|
@@ -247,6 +351,7 @@ function runHostEventMode(source: string): number {
|
|
|
247
351
|
|
|
248
352
|
function main(): number {
|
|
249
353
|
if (process.argv.includes('--limits')) return runLimitsMode();
|
|
354
|
+
if (process.argv.includes('--projection-modes')) return runProjectionModesMode();
|
|
250
355
|
|
|
251
356
|
const volumeRoot = argValue('--volume');
|
|
252
357
|
if (volumeRoot) return runVolumeMode(volumeRoot);
|