@gobing-ai/knowledge-kit 0.0.13 → 0.0.14

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 (109) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +20 -0
  3. package/package.json +1 -1
  4. package/plugins/generations/content-gen/dist/index.js +20 -0
  5. package/plugins/generations/core-facts-gen/dist/index.js +20 -0
  6. package/plugins/generations/daily-article-gen/dist/index.js +29 -1
  7. package/plugins/generations/daily-article-gen/src/index.ts +13 -1
  8. package/plugins/generations/dailynews-gen/dist/index.js +20 -0
  9. package/plugins/generations/episode-plan-gen/dist/index.js +24 -0
  10. package/plugins/generations/episode-plan-gen/src/index.ts +11 -0
  11. package/plugins/generations/image-gen/dist/index.js +2296 -53
  12. package/plugins/generations/image-gen/src/providers/agnes.ts +110 -0
  13. package/plugins/generations/image-gen/src/providers/azure.ts +153 -0
  14. package/plugins/generations/image-gen/src/providers/codex-cli.ts +170 -0
  15. package/plugins/generations/image-gen/src/providers/dashscope.ts +485 -0
  16. package/plugins/generations/image-gen/src/providers/google.ts +268 -0
  17. package/plugins/generations/image-gen/src/providers/huggingface.ts +59 -0
  18. package/plugins/generations/image-gen/src/providers/jimeng.ts +259 -0
  19. package/plugins/generations/image-gen/src/providers/minimax.ts +171 -0
  20. package/plugins/generations/image-gen/src/providers/openai.ts +319 -0
  21. package/plugins/generations/image-gen/src/providers/openrouter.ts +257 -0
  22. package/plugins/generations/image-gen/src/providers/refs.ts +24 -0
  23. package/plugins/generations/image-gen/src/providers/replicate.ts +279 -0
  24. package/plugins/generations/image-gen/src/providers/seedream.ts +1 -1
  25. package/plugins/generations/image-gen/src/providers/types.ts +52 -51
  26. package/plugins/generations/image-gen/src/providers/zai.ts +237 -0
  27. package/plugins/generations/news-report-gen/dist/index.js +22193 -0
  28. package/plugins/generations/news-report-gen/package.json +17 -0
  29. package/plugins/generations/news-report-gen/plugin.json +7 -0
  30. package/plugins/generations/news-report-gen/src/index.ts +308 -0
  31. package/plugins/generations/news-report-gen/tsconfig.json +4 -0
  32. package/plugins/generations/omni-voice-gen/Makefile +14 -0
  33. package/plugins/generations/omni-voice-gen/README.md +112 -0
  34. package/plugins/generations/omni-voice-gen/bin/omni-voice-gen +2 -0
  35. package/plugins/generations/omni-voice-gen/dist/omni-voice-gen-prr8skpb. +2 -0
  36. package/plugins/generations/omni-voice-gen/dist/omni-voice-gen.js +6 -0
  37. package/plugins/generations/omni-voice-gen/plugin.json +6 -0
  38. package/plugins/generations/omni-voice-gen/profiles.json +12 -0
  39. package/plugins/generations/omni-voice-gen/pyproject.toml +25 -0
  40. package/plugins/generations/omni-voice-gen/scripts/coverage_gate.py +74 -0
  41. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__init__.py +1 -0
  42. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__main__.py +39 -0
  43. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/audio.py +190 -0
  44. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/backend.py +150 -0
  45. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/contract.py +76 -0
  46. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/mp3.py +60 -0
  47. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +289 -0
  48. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/profiles.py +100 -0
  49. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +234 -0
  50. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +352 -0
  51. package/plugins/generations/omni-voice-gen/uv.lock +3510 -0
  52. package/plugins/generations/voice-gen/dist/index.js +30 -2
  53. package/plugins/generations/voice-gen/src/index.ts +16 -1
  54. package/plugins/generations/voice-gen/src/voicebox-client.ts +3 -1
  55. package/plugins/ingestions/aihot-ingest/dist/index.js +20 -0
  56. package/plugins/ingestions/horizon-ingest/dist/index.js +20 -0
  57. package/plugins/ingestions/last30days-ingest/dist/index.js +20 -0
  58. package/plugins/ingestions/web-search/dist/index.js +20 -0
  59. package/plugins/kk/commands/image-extract.md +40 -0
  60. package/plugins/kk/commands/image-generate.md +18 -1
  61. package/plugins/kk/plugin.json +1 -1
  62. package/plugins/kk/skills/image-authoring/SKILL.md +10 -3
  63. package/plugins/kk/skills/image-authoring/references/format-drafting.md +57 -0
  64. package/plugins/kk/skills/image-authoring/references/style-extraction.md +14 -9
  65. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +130 -30
  66. package/plugins/publishings/emdash-pub/dist/index.js +20 -0
  67. package/plugins/publishings/podcast-pub/dist/index.js +57 -3
  68. package/plugins/publishings/podcast-pub/src/index.ts +18 -2
  69. package/plugins/publishings/podcast-pub/src/show-notes.ts +56 -9
  70. package/plugins/publishings/qiita-pub/dist/index.js +20 -0
  71. package/plugins/publishings/surfdash-pub/dist/index.js +96 -6
  72. package/plugins/publishings/surfdash-pub/src/index.ts +109 -9
  73. package/plugins/publishings/zenn-pub/dist/index.js +20 -0
  74. package/plugins/sp/scripts/batch-preflight.mjs +346 -0
  75. package/plugins/sp/scripts/batch-preflight.ts +459 -0
  76. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +615 -0
  77. package/plugins/sp/scripts/daily-summary/daily-summary.ts +846 -0
  78. package/plugins/sp/scripts/daily-summary/logger.ts +28 -0
  79. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +223 -0
  80. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +367 -0
  81. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +132 -0
  82. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +169 -0
  83. package/plugins/sp/scripts/feature-dev-precheck.mjs +171 -0
  84. package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
  85. package/plugins/sp/scripts/feature-sync-bounded.mjs +285 -0
  86. package/plugins/sp/scripts/feature-sync-bounded.ts +478 -0
  87. package/plugins/sp/scripts/history-anatomy-cache.mjs +902 -0
  88. package/plugins/sp/scripts/history-anatomy-cache.ts +1028 -0
  89. package/plugins/sp/scripts/idea-handoff.mjs +22 -0
  90. package/plugins/sp/scripts/idea-handoff.ts +44 -0
  91. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
  92. package/plugins/sp/scripts/inline-run-setup.ts +198 -0
  93. package/plugins/sp/scripts/pr-reviewing.mjs +769 -0
  94. package/plugins/sp/scripts/pr-reviewing.ts +925 -0
  95. package/plugins/sp/scripts/quality-gate.mjs +179 -0
  96. package/plugins/sp/scripts/quality-gate.ts +217 -0
  97. package/plugins/sp/scripts/script-contract-check.ts +319 -0
  98. package/plugins/sp/scripts/stage-registry-adapter.ts +1533 -0
  99. package/plugins/sp/scripts/surface-drift-inventory.ts +929 -0
  100. package/plugins/sp/scripts/task-evidence-precheck.ts +181 -0
  101. package/plugins/sp/scripts/task-size-precheck.ts +175 -0
  102. package/plugins/sp/scripts/transition-shim-check.ts +238 -0
  103. package/plugins/sp/scripts/validate-commands.ts +689 -0
  104. package/plugins/sp/scripts/validate-flag-contracts.ts +878 -0
  105. package/plugins/sp/scripts/verify-answer-lint.ts +530 -0
  106. package/plugins/sp/scripts/workflow-step-profile.mjs +316 -0
  107. package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
  108. package/plugins/sp/scripts/wrapup-steps.mjs +373 -0
  109. package/plugins/sp/scripts/wrapup-steps.ts +466 -0
@@ -0,0 +1,929 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * surface-drift-inventory — re-runnable drift inventory for `plugins/sp` and
4
+ * the tracked workflow-YAML SSOT tree against the live
5
+ * source-local `spur` CLI (feature I3, task 0539 R1/R2).
6
+ *
7
+ * Method (mechanical first, prose second — task 0539 Design):
8
+ * A. Plugin assertions: extract every `spur`-invocation (backticked spans, fenced
9
+ * code lines, YAML scalars), every `| Verb |` table row, and every flag span
10
+ * under a verb heading in the noun-mapped `spur-cli` references; check
11
+ * nouns/verbs/flags against live `--help` captures (source-local entry via
12
+ * the I2 helper). `--json` shapes are captured by EXECUTING a read-only probe
13
+ * list; mutating commands are recorded unverified, never passing.
14
+ * B. Scripts: extract the argv arrays `runSpur`/`runSpurJson` build and
15
+ * help-check them; execute the two spur-shelling scripts against a fake bin.
16
+ * C. Hooks: `hooks.json` parses, every referenced hook script exists, the `pi`
17
+ * extension from plugin.json exists. Host-side event-name contract is not
18
+ * in this repo — recorded unverified.
19
+ * D. Workflows: `spur workflow validate` (live engine, schema + semantic) and a
20
+ * `--dry-run` transition walk for all ten definitions; `.spur/workflows`
21
+ * symlink is asserted absent (two-tier project→bundled model, task 0650).
22
+ *
23
+ * Output: markdown inventory on stdout-path via `--out`; exit 1 when any
24
+ * CONFIRMED mismatch remains (ok after repair); unverified entries never fail.
25
+ *
26
+ * Usage: bun plugins/sp/scripts/surface-drift-inventory.ts [--out docs/tasks2/0539-inventory.md]
27
+ */
28
+
29
+ import { execFileSync } from 'node:child_process';
30
+ import {
31
+ chmodSync,
32
+ existsSync,
33
+ mkdirSync,
34
+ mkdtempSync,
35
+ readdirSync,
36
+ readFileSync,
37
+ rmSync,
38
+ writeFileSync,
39
+ } from 'node:fs';
40
+ import { tmpdir } from 'node:os';
41
+ import { dirname, join, relative, resolve } from 'node:path';
42
+ import { captureCliSurface } from '../tests/helpers/cli-surface';
43
+
44
+ const REPO_ROOT = resolve(import.meta.dir, '..', '..', '..');
45
+ const PLUGIN_ROOT = join(REPO_ROOT, 'plugins', 'sp');
46
+ const CLI_ENTRY = join(REPO_ROOT, 'apps', 'cli', 'src', 'index.ts');
47
+ const rel = (p: string): string => relative(REPO_ROOT, p);
48
+
49
+ // ─── Rows ───────────────────────────────────────────────────────────────────
50
+
51
+ interface Row {
52
+ asserted: string;
53
+ method: string;
54
+ status: 'ok' | 'mismatch' | 'unverified';
55
+ actual: string;
56
+ occurrences: { file: string; line: number }[];
57
+ }
58
+
59
+ export const rows: Row[] = [];
60
+
61
+ export function record(
62
+ asserted: string,
63
+ method: string,
64
+ status: Row['status'],
65
+ actual: string,
66
+ occ: { file: string; line: number },
67
+ ): Row {
68
+ let row = rows.find((r) => r.asserted === asserted && r.method === method);
69
+ if (!row) {
70
+ row = { asserted, method, status, actual, occurrences: [] };
71
+ rows.push(row);
72
+ }
73
+ if (!row.occurrences.some((o) => o.file === occ.file && o.line === occ.line)) {
74
+ row.occurrences.push(occ);
75
+ }
76
+ if (status === 'mismatch') {
77
+ row.status = 'mismatch';
78
+ row.actual = actual;
79
+ }
80
+ return row;
81
+ }
82
+
83
+ // ─── Live surface captures (cached) ─────────────────────────────────────────
84
+
85
+ /** Exported so unit tests can seed a fake live surface and keep the verdict logic hermetic. */
86
+ export const surfaceCache = new Map<string, { commands: string[]; flags: string[] }>();
87
+
88
+ function surface(path: string[]): { commands: string[]; flags: string[] } {
89
+ const key = path.join(' ');
90
+ let s = surfaceCache.get(key);
91
+ if (!s) {
92
+ const cap = captureCliSurface(path);
93
+ s = { commands: cap.commands, flags: cap.flags };
94
+ surfaceCache.set(key, s);
95
+ }
96
+ return s;
97
+ }
98
+
99
+ /** How the live-CLI sweeps below reach the CLI. Injectable so unit tests can drive them hermetically. */
100
+ export type CliRunner = (args: string[], timeoutMs?: number) => { exit: number; out: string; err: string };
101
+
102
+ export function runCli(args: string[], timeoutMs = 30_000): { exit: number; out: string; err: string } {
103
+ try {
104
+ const out = execFileSync(process.execPath, ['run', CLI_ENTRY, ...args], {
105
+ cwd: REPO_ROOT,
106
+ encoding: 'utf8',
107
+ timeout: timeoutMs,
108
+ stdio: ['ignore', 'pipe', 'pipe'],
109
+ });
110
+ return { exit: 0, out, err: '' };
111
+ } catch (e) {
112
+ const err = e as { status?: number; stdout?: string; stderr?: string; killed?: boolean; code?: string };
113
+ return {
114
+ // A timeout must map to 124 — sweepWorkflows reads that as "did not terminate"
115
+ // (unverified) rather than reporting a hung dry-run as a clean walk. Bun signals it
116
+ // as code ETIMEDOUT with `killed` undefined and a null status; Node sets `killed`.
117
+ exit: err.killed || err.code === 'ETIMEDOUT' ? 124 : (err.status ?? 1),
118
+ out: err.stdout ?? '',
119
+ err: (err.stderr ?? '').slice(0, 400),
120
+ };
121
+ }
122
+ }
123
+
124
+ interface Parsed {
125
+ nouns: string[];
126
+ verbs: string[];
127
+ flags: string[];
128
+ }
129
+
130
+ const PLACEHOLDER = /^([<[{($#"'`]|…|\.\.\.)/;
131
+ const LEGACY_SELF_ALIASES = new Set(['init', 'migrate', 'serve', 'status']);
132
+
133
+ /** Normalize an asserted invocation span into nouns / verbs / flags. Null when it asserts nothing. */
134
+ export function parseInvocation(spanRaw: string): Parsed | null {
135
+ let s = spanRaw.trim().replace(/[.,;:]+$/, '');
136
+ s = s.replace(/^(?:bun|bunx|npx)\s+(?:run\s+)?/, '');
137
+ // The monorepo dev form (`bun run apps/cli/src/index.ts <noun> <verb>`) names no
138
+ // `spur` binary once the entry path is stripped, so the `^spur` gate below must not
139
+ // apply to it — it used to, which dropped every claim written in the form AGENTS.md
140
+ // mandates for local development.
141
+ const viaEntryPath = /^apps\/cli\/src\/index\.ts\s*/.test(s);
142
+ s = s.replace(/^apps\/cli\/src\/index\.ts\s*/, '');
143
+ if (!viaEntryPath) {
144
+ if (!/^spur\b/.test(s) || s === 'spur') return null;
145
+ s = s.replace(/^spur\s+/, '');
146
+ }
147
+ if (!s) return null;
148
+ // Split on shell separators only. `>` is deliberately excluded: it appears inside
149
+ // the `<wbs>` / `<path>` placeholders this corpus documents flags with, and
150
+ // truncating there silently discarded every flag after the first placeholder.
151
+ s = (s.split(/[;&|]/)[0] ?? s).trim();
152
+ if (!s) return null;
153
+ const tokens = s
154
+ .split(/\s+/)
155
+ .filter(Boolean)
156
+ .filter((t) => t === '…' || t === '...' || (!PLACEHOLDER.test(t) && !/^[A-Z]/.test(t) && !/["']/.test(t)));
157
+ const expand = (t: string): string[] => t.split(/[|/]/).filter((p) => /^[a-z][a-z0-9-]*$/.test(p));
158
+ const nouns: string[] = [];
159
+ const verbs: string[] = [];
160
+ const flags: string[] = [];
161
+ let positional = 0;
162
+ for (const t of tokens) {
163
+ if (t === '…' || t === '...') {
164
+ positional = 99;
165
+ continue;
166
+ } // elided prose — later tokens are not nouns/verbs
167
+ const flag = t.match(/^(-{1,2}[A-Za-z][A-Za-z0-9-]*)/);
168
+ if (flag) {
169
+ flags.push(flag[1] ?? '');
170
+ positional = 99;
171
+ continue;
172
+ }
173
+ if (positional === 0) {
174
+ nouns.push(...expand(t));
175
+ positional = 1;
176
+ } else if (positional === 1) {
177
+ verbs.push(...expand(t));
178
+ positional = 2;
179
+ }
180
+ }
181
+ if (nouns.length === 0 && verbs.length === 0 && flags.length === 0) return null;
182
+ if (process.env.DEBUG_SPANS === '1') {
183
+ process.stderr.write(`SPAN ${JSON.stringify({ spanRaw, nouns, verbs, flags })}\n`);
184
+ }
185
+ return { nouns, verbs, flags };
186
+ }
187
+
188
+ /** All backticked spans in one line. */
189
+ export function backticks(line: string): string[] {
190
+ return [...line.matchAll(/`([^`\n]+)`/g)].map((m) => m[1] ?? '');
191
+ }
192
+
193
+ /** Candidate invocation spans from one line: backticked + bare shell-style matches. */
194
+ export function lineInvocationSpans(line: string): string[] {
195
+ const spans = backticks(line).filter((s) => /(?:^|\s)(?:spur|apps\/cli\/src\/index\.ts)\s/.test(s));
196
+ const bare = [
197
+ ...line.matchAll(
198
+ /(?:^|[`"'=;&|;(]\s*)(?:bun(?:\s+run)?\s+)?(?:apps\/cli\/src\/index\.ts|spur)\s+[^\s`"'|;&)]+/g,
199
+ ),
200
+ ].map((m) => (m[0] ?? '').trim());
201
+ return [...spans, ...bare];
202
+ }
203
+
204
+ /** A pure flag list (backticked "--from-answer <path> --folder" spans). */
205
+ export function isFlagSpan(span: string): boolean {
206
+ const toks = span.trim().split(/\s+/);
207
+ return toks.length > 0 && toks.every((t) => /^-{1,2}[A-Za-z][A-Za-z0-9-]*/.test(t) || PLACEHOLDER.test(t));
208
+ }
209
+
210
+ export function flagNames(span: string): string[] {
211
+ return [...span.matchAll(/-{1,2}[A-Za-z][A-Za-z0-9-]*/g)].map((m) => m[0] ?? '');
212
+ }
213
+
214
+ /** Files in the R1 surfaces: commands/, skills/ (incl. references), scripts/, hooks/. */
215
+ export function walk(dir: string, exts: string[], out: string[] = []): string[] {
216
+ if (!existsSync(dir)) return out;
217
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
218
+ const p = join(dir, e.name);
219
+ if (e.isDirectory()) {
220
+ if (e.name === 'tests' || e.name === 'evals') continue; // the harness itself is out of R1 scope
221
+ walk(p, exts, out);
222
+ } else if (exts.some((x) => e.name.endsWith(x))) {
223
+ out.push(p);
224
+ }
225
+ }
226
+ return out;
227
+ }
228
+
229
+ /** Noun mapped from a spur-cli reference path (references/tasks.md, references/tasks/x.md -> task). */
230
+ export function nounOfReference(file: string): string | null {
231
+ const map: Record<string, string> = {
232
+ tasks: 'task',
233
+ features: 'feature',
234
+ rules: 'rule',
235
+ workflows: 'workflow',
236
+ agent: 'agent',
237
+ message: 'message',
238
+ team: 'team',
239
+ init: 'init',
240
+ serve: 'serve',
241
+ };
242
+ const parts = file.split(/[\\/]/);
243
+ const base = (parts.pop() ?? '').replace(/\.md$/, '');
244
+ const parent = parts.at(-1) ?? '';
245
+ const refsIdx = parts.indexOf('references');
246
+ const refsChild = refsIdx >= 0 ? (parts[refsIdx + 1] ?? '') : '';
247
+ return map[base] ?? map[parent] ?? map[refsChild] ?? null;
248
+ }
249
+
250
+ // ─── A. Plugin assertion checks ─────────────────────────────────────────────
251
+
252
+ export function checkNounVerbFlags(
253
+ nouns: string[],
254
+ verbs: string[],
255
+ flags: string[],
256
+ occ: { file: string; line: number },
257
+ ): void {
258
+ const root = surface([]);
259
+ for (const noun of nouns) {
260
+ if (noun === 'help' || /^(foo|bar|baz)$/.test(noun)) continue;
261
+ if (LEGACY_SELF_ALIASES.has(noun)) {
262
+ const self = surface(['self']);
263
+ const command = surface(['self', noun]);
264
+ const present = self.commands.includes(noun);
265
+ record(
266
+ `spur ${noun}`,
267
+ 'help-capture(spur self; hidden root alias)',
268
+ present ? 'ok' : 'mismatch',
269
+ present
270
+ ? `legacy alias; canonical self/${noun} command is present`
271
+ : `canonical self/${noun} command is absent`,
272
+ occ,
273
+ );
274
+ for (const f of flags) {
275
+ record(
276
+ `spur ${noun} ${f}`,
277
+ `help-capture(spur self ${noun}; hidden root alias)`,
278
+ present && command.flags.includes(f) ? 'ok' : 'mismatch',
279
+ command.flags.includes(f)
280
+ ? 'flag present on canonical command'
281
+ : `flag absent (${command.flags.join(', ')})`,
282
+ occ,
283
+ );
284
+ }
285
+ continue;
286
+ }
287
+ if (!root.commands.includes(noun)) {
288
+ record(
289
+ `spur ${noun}`,
290
+ 'help-capture(root)',
291
+ 'mismatch',
292
+ `noun absent from live root commands (${root.commands.join(', ')})`,
293
+ occ,
294
+ );
295
+ continue;
296
+ }
297
+ const ns = surface([noun]);
298
+ for (const verb of verbs) {
299
+ if (verb === 'help') continue;
300
+ if (!ns.commands.includes(verb)) {
301
+ record(
302
+ `spur ${noun} ${verb}`,
303
+ `help-capture(spur ${noun})`,
304
+ 'mismatch',
305
+ `verb absent (live verbs: ${ns.commands.join(', ')})`,
306
+ occ,
307
+ );
308
+ continue;
309
+ }
310
+ const vs = surface([noun, verb]);
311
+ record(`spur ${noun} ${verb}`, `help-capture(spur ${noun})`, 'ok', 'verb present', occ);
312
+ for (const f of flags) {
313
+ record(
314
+ `spur ${noun} ${verb} ${f}`,
315
+ `help-capture(spur ${noun} ${verb})`,
316
+ vs.flags.includes(f) ? 'ok' : 'mismatch',
317
+ vs.flags.includes(f) ? 'flag present' : `flag absent (live flags: ${vs.flags.join(', ')})`,
318
+ occ,
319
+ );
320
+ }
321
+ }
322
+ if (verbs.length === 0) {
323
+ record(`spur ${noun} (noun)`, `help-capture(spur ${noun})`, 'ok', `${ns.commands.length} live verbs`, occ);
324
+ for (const f of flags) {
325
+ if (ns.flags.includes(f)) {
326
+ record(`spur ${noun} ${f}`, `help-capture(spur ${noun})`, 'ok', 'noun-level flag present', occ);
327
+ } else {
328
+ const withFlag = ns.commands.filter((v) => surface([noun, v]).flags.includes(f));
329
+ record(
330
+ `spur ${noun} ${f}`,
331
+ `help-capture(spur ${noun})`,
332
+ withFlag.length > 0 ? 'unverified' : 'mismatch',
333
+ withFlag.length > 0
334
+ ? `noun-level shorthand — flag present on ${withFlag.length}/${ns.commands.length} verbs`
335
+ : `flag absent (noun and every verb; live noun flags: ${ns.flags.join(', ')})`,
336
+ occ,
337
+ );
338
+ }
339
+ }
340
+ }
341
+ }
342
+ for (const verb of verbs) {
343
+ if (nouns.length === 0 && LEGACY_SELF_ALIASES.has(verb)) {
344
+ checkNounVerbFlags([verb], [], flags, occ);
345
+ continue;
346
+ }
347
+ if (nouns.length === 0 && root.commands.includes(verb)) {
348
+ record(`spur ${verb}`, 'help-capture(root)', 'ok', 'root verb present', occ);
349
+ }
350
+ }
351
+ }
352
+
353
+ export function sweepPluginTrees(root: string = PLUGIN_ROOT): void {
354
+ const files = [
355
+ ...walk(join(root, 'commands'), ['.md']),
356
+ ...walk(join(root, 'skills'), ['.md']),
357
+ ...walk(join(root, 'scripts'), ['.ts']),
358
+ ...walk(join(root, 'hooks'), ['.ts', '.json']),
359
+ ];
360
+ for (const file of files) {
361
+ const text = readFileSync(file, 'utf8');
362
+ const lines = text.split(/\r?\n/);
363
+ const isRef = file.includes(join('skills', 'spur-cli', 'references'));
364
+ const refNoun = isRef ? nounOfReference(file) : null;
365
+ let headingVerb: string | null = null;
366
+ let inVerbTable = false;
367
+ let inFence = false;
368
+ lines.forEach((line: string, i: number) => {
369
+ const occ = { file: rel(file), line: i + 1 };
370
+ if (/^\s*```/.test(line)) {
371
+ inFence = !inFence;
372
+ return;
373
+ }
374
+ if (!inFence) {
375
+ const heading = line.match(/^#{1,6}\s+([a-z][a-z0-9-]*)\b/);
376
+ if (heading) headingVerb = heading[1] ?? null;
377
+ if (/^\|\s*Verb\s*\|/.test(line)) {
378
+ inVerbTable = true;
379
+ return;
380
+ }
381
+ if (inVerbTable && !/^\|/.test(line)) inVerbTable = false;
382
+ const negated = /\bno\s+`spur [^`]+`\s+verb exists\b/i.test(line);
383
+ for (const span of lineInvocationSpans(line)) {
384
+ const parsed = parseInvocation(span);
385
+ if (!parsed) continue;
386
+ if (negated) {
387
+ // Inverted assertion: the line claims absence. Confirm against the live surface.
388
+ for (const noun of parsed.nouns) {
389
+ const ns = surface([noun]);
390
+ for (const verb of parsed.verbs) {
391
+ record(
392
+ `spur ${noun} ${verb}`,
393
+ `help-capture(spur ${noun})`,
394
+ ns.commands.includes(verb) ? 'mismatch' : 'ok',
395
+ ns.commands.includes(verb)
396
+ ? 'line asserts absence but the verb EXISTS on the live surface (stale negation)'
397
+ : 'documented absence confirmed against live surface',
398
+ occ,
399
+ );
400
+ }
401
+ }
402
+ continue;
403
+ }
404
+ checkNounVerbFlags(parsed.nouns, parsed.verbs, parsed.flags, occ);
405
+ }
406
+ if (file.endsWith('.ts')) return;
407
+ if (inVerbTable && refNoun && /^\|/.test(line)) {
408
+ const first = (line.match(/^\|\s*`?([a-z][a-z0-9-]*)/)?.[1] ?? '').replace(/`/g, '');
409
+ const rootCommands = surface([]).commands;
410
+ // Flags live in the LAST cell (Key flags) — Purpose prose contains
411
+ // hyphenated words that are not flags.
412
+ const cells = line.split('|').map((c) => c.trim());
413
+ const lastCell = cells[cells.length - 2] ?? line;
414
+ if (first && first !== 'Verb' && first !== 'spur') {
415
+ const nounCommands = surface([refNoun]).commands;
416
+ if (nounCommands.includes(first)) {
417
+ checkNounVerbFlags([refNoun], [first], flagNames(lastCell), occ);
418
+ } else if (rootCommands.includes(first) || LEGACY_SELF_ALIASES.has(first)) {
419
+ checkNounVerbFlags([], [first], flagNames(lastCell), occ);
420
+ } else {
421
+ checkNounVerbFlags([refNoun], [first], flagNames(lastCell), occ);
422
+ }
423
+ }
424
+ return;
425
+ }
426
+ const perVerbPage = file.endsWith(join('verbs.md'));
427
+ if (perVerbPage && refNoun && headingVerb && surface([refNoun]).commands.includes(headingVerb)) {
428
+ for (const span of backticks(line)) {
429
+ if (isFlagSpan(span) && flagNames(span).length > 0) {
430
+ checkNounVerbFlags([refNoun], [headingVerb], flagNames(span), occ);
431
+ }
432
+ }
433
+ }
434
+ } else {
435
+ for (const span of lineInvocationSpans(line)) {
436
+ const parsed = parseInvocation(span);
437
+ if (parsed) checkNounVerbFlags(parsed.nouns, parsed.verbs, parsed.flags, occ);
438
+ }
439
+ }
440
+ });
441
+ }
442
+ }
443
+
444
+ // ─── B. Scripts: argv extraction + execution ────────────────────────────────
445
+
446
+ export function sweepScriptArgv(root: string = PLUGIN_ROOT): void {
447
+ for (const file of walk(join(root, 'scripts'), ['.ts'])) {
448
+ readFileSync(file, 'utf8')
449
+ .split(/\r?\n/)
450
+ .forEach((line: string, i: number) => {
451
+ for (const m of line.matchAll(/\brunSpur(?:Json)?\(\s*\w+\s*,\s*\[([^\]]*)\]/g)) {
452
+ const toks = [...(m[1] ?? '').matchAll(/'([^']*)'|"([^"]*)"/g)].map((t) => t[1] ?? t[2] ?? '');
453
+ const ids = toks.filter((t) => /^[a-z][a-z0-9-]*$/.test(t));
454
+ const nouns = ids.slice(0, 1);
455
+ const verbs = ids.slice(1, 2);
456
+ const flags = toks.filter((t) => /^--/.test(t));
457
+ if (nouns.length || verbs.length || flags.length) {
458
+ checkNounVerbFlags(nouns, verbs, flags, {
459
+ file: rel(file),
460
+ line: i + 1,
461
+ });
462
+ }
463
+ }
464
+ });
465
+ }
466
+ }
467
+
468
+ export function executeScripts(root: string = PLUGIN_ROOT): void {
469
+ const dir = mkdtempSync(join(tmpdir(), 'spur-drift-'));
470
+ try {
471
+ // task-size-precheck against a fake bin — argument construction under execution.
472
+ const fake = join(dir, 'spur');
473
+ writeFileSync(
474
+ fake,
475
+ `#!/bin/sh
476
+ printf '%s' '{"content":"### Requirements\\n- [ ] R1. x\\n### Plan\\n- [ ] p1"}'
477
+ `,
478
+ );
479
+ chmodSync(fake, 0o755);
480
+ const statusPath = join(dir, '.spur', 'run', '0487-precheck-size.status');
481
+ try {
482
+ execFileSync(
483
+ process.execPath,
484
+ [join(root, 'scripts', 'task-size-precheck.ts'), '0487', '--spur-bin', fake],
485
+ { cwd: dir, encoding: 'utf8', timeout: 30_000, stdio: 'pipe' },
486
+ );
487
+ const content = readFileSync(statusPath, 'utf8');
488
+ record(
489
+ 'task-size-precheck --spur-bin <bin> (executed)',
490
+ 'script-exec(fake-bin)',
491
+ content.trim() === 'PASS' ? 'ok' : 'mismatch',
492
+ `status file: ${content.trim()}`,
493
+ { file: 'plugins/sp/scripts/task-size-precheck.ts', line: 1 },
494
+ );
495
+ } catch (e) {
496
+ record(
497
+ 'task-size-precheck --spur-bin <bin> (executed)',
498
+ 'script-exec(fake-bin)',
499
+ 'mismatch',
500
+ `script exited non-zero: ${String(e).slice(0, 200)}`,
501
+ { file: 'plugins/sp/scripts/task-size-precheck.ts', line: 1 },
502
+ );
503
+ }
504
+
505
+ // feature-sync-bounded against a fake bin — feature show / task list / feature sync argv.
506
+ const fake2 = join(dir, 'spur2');
507
+ writeFileSync(
508
+ fake2,
509
+ `#!/bin/sh
510
+ if [ "$1" = feature ] && [ "$2" = show ]; then printf '%s' '{"content":"x"}';
511
+ elif [ "$1" = task ]; then printf '%s' '[{"wbs":"0001","status":"done"}]';
512
+ elif [ "$1" = feature ] && [ "$2" = sync ]; then printf '%s' '{"status":"ok","changed":0}';
513
+ fi
514
+ `,
515
+ );
516
+ chmodSync(fake2, 0o755);
517
+ try {
518
+ execFileSync(
519
+ process.execPath,
520
+ [
521
+ join(root, 'scripts', 'feature-sync-bounded.ts'),
522
+ '--feature',
523
+ 'I3',
524
+ '--spur-bin',
525
+ fake2,
526
+ '--run-dir',
527
+ join(dir, 'run'),
528
+ ],
529
+ { cwd: dir, encoding: 'utf8', timeout: 30_000, stdio: 'pipe' },
530
+ );
531
+ record(
532
+ 'feature-sync-bounded --feature <id> --spur-bin <bin> (executed)',
533
+ 'script-exec(fake-bin)',
534
+ 'ok',
535
+ 'exited 0 against fake bin',
536
+ { file: 'plugins/sp/scripts/feature-sync-bounded.ts', line: 1 },
537
+ );
538
+ } catch (e) {
539
+ record(
540
+ 'feature-sync-bounded --feature <id> --spur-bin <bin> (executed)',
541
+ 'script-exec(fake-bin)',
542
+ 'unverified',
543
+ `fake-bin execution errored: ${String(e).slice(0, 200)} — argv still checked by extraction`,
544
+ { file: 'plugins/sp/scripts/feature-sync-bounded.ts', line: 1 },
545
+ );
546
+ }
547
+ } finally {
548
+ rmSync(dir, { recursive: true, force: true });
549
+ }
550
+ }
551
+
552
+ // ─── C. Hook contract ───────────────────────────────────────────────────────
553
+
554
+ export function sweepHooks(root: string = PLUGIN_ROOT): void {
555
+ const hooksJsonPath = join(root, 'hooks', 'hooks.json');
556
+ let parsed: {
557
+ hooks?: Record<string, Array<{ matcher?: string; hooks?: Array<{ type?: string; command?: string }> }>>;
558
+ };
559
+ try {
560
+ parsed = JSON.parse(readFileSync(hooksJsonPath, 'utf8'));
561
+ record('hooks.json parses', 'json-parse', 'ok', `${Object.keys(parsed.hooks ?? {}).length} events`, {
562
+ file: rel(hooksJsonPath),
563
+ line: 1,
564
+ });
565
+ } catch (e) {
566
+ record('hooks.json parses', 'json-parse', 'mismatch', String(e).slice(0, 200), {
567
+ file: rel(hooksJsonPath),
568
+ line: 1,
569
+ });
570
+ return;
571
+ }
572
+ for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
573
+ record(
574
+ `event ${event}`,
575
+ 'host-contract',
576
+ 'unverified',
577
+ 'superskill host hook-event schema is not in this repo — cannot be checked mechanically here',
578
+ { file: rel(hooksJsonPath), line: 1 },
579
+ );
580
+ for (const entry of entries) {
581
+ if (/^(PreToolUse|PostToolUse)$/.test(event) && !entry.matcher) {
582
+ record(
583
+ `event ${event} matcher`,
584
+ 'hooks.json structure',
585
+ 'mismatch',
586
+ 'tool-scoped event has no matcher',
587
+ { file: rel(hooksJsonPath), line: 1 },
588
+ );
589
+ }
590
+ for (const h of entry.hooks ?? []) {
591
+ const m = (h.command ?? '').match(/^superskill hook run sp ([a-z-]+)$/);
592
+ if (!m) {
593
+ record(
594
+ `hook command ${h.command}`,
595
+ 'hooks.json structure',
596
+ 'unverified',
597
+ "command does not match 'superskill hook run sp <name>' — not mechanically resolvable",
598
+ { file: rel(hooksJsonPath), line: 1 },
599
+ );
600
+ continue;
601
+ }
602
+ const script = join(root, 'hooks', `${m[1]}.ts`);
603
+ record(
604
+ `hook ${m[1]} script exists`,
605
+ 'file-resolution',
606
+ existsSync(script) ? 'ok' : 'mismatch',
607
+ existsSync(script) ? rel(script) : `${rel(script)} missing`,
608
+ { file: rel(hooksJsonPath), line: 1 },
609
+ );
610
+ }
611
+ }
612
+ }
613
+ // pi-lens-ignore: unchecked-throwing-call
614
+ const plugin = JSON.parse(readFileSync(join(root, 'plugin.json'), 'utf8')) as {
615
+ extensions?: { pi?: string[] };
616
+ };
617
+ for (const ext of plugin.extensions?.pi ?? []) {
618
+ const p = join(root, ext.replace(/^\.\//, ''));
619
+ record(
620
+ `pi extension ${ext}`,
621
+ 'file-resolution',
622
+ existsSync(p) ? 'ok' : 'mismatch',
623
+ existsSync(p) ? rel(p) : `${rel(p)} missing`,
624
+ { file: 'plugins/sp/plugin.json', line: 1 },
625
+ );
626
+ }
627
+ }
628
+
629
+ // ─── D. --json envelope probes ──────────────────────────────────────────────
630
+
631
+ /** Read-only commands executed live to capture actual --json envelope shapes. */
632
+ const JSON_PROBES: string[][] = [
633
+ ['task', 'list', '--json'],
634
+ ['task', 'show', '0539', '--json'],
635
+ ['task', 'check', '0539', '--json'],
636
+ ['feature', 'list', '--json'],
637
+ ['feature', 'show', 'I3', '--json'],
638
+ ['rule', 'list', '--json'],
639
+ ['agent', 'list', '--json'],
640
+ ['agent', 'doctor', 'omp', '--json'],
641
+ ['workflow', 'list', '--json'],
642
+ ['workflow', 'validate', 'basic.yaml', '--json'],
643
+ ['status', '--json'],
644
+ ['team', 'status', '--json'],
645
+ ['projects', 'list', '--json'],
646
+ ];
647
+
648
+ export function flattenKeys(v: unknown, prefix = ''): string[] {
649
+ if (Array.isArray(v)) {
650
+ const inner = [...new Set(v.flatMap((x) => flattenKeys(x, '[]')))];
651
+ return inner.map((k) => (prefix ? `${prefix}.${k}` : k));
652
+ }
653
+ if (v && typeof v === 'object') {
654
+ return Object.entries(v).flatMap(([k, val]) => {
655
+ const path = prefix ? `${prefix}.${k}` : k;
656
+ return typeof val === 'object' && val !== null ? flattenKeys(val, path) : [path];
657
+ });
658
+ }
659
+ return [];
660
+ }
661
+
662
+ const jsonEnvelopeShapes: Record<string, { exit: number; keys: string[] }> = {};
663
+
664
+ export function probeJsonShapes(run: CliRunner = runCli): void {
665
+ for (const args of JSON_PROBES) {
666
+ const r = run(args);
667
+ let keys: string[] = [];
668
+ try {
669
+ keys = flattenKeys(JSON.parse(r.out)).sort();
670
+ } catch {
671
+ keys = [];
672
+ }
673
+ const label = `spur ${args.slice(0, -1).join(' ')}`;
674
+ jsonEnvelopeShapes[label] = { exit: r.exit, keys };
675
+ const ok = r.exit === 0 && keys.length > 0;
676
+ record(
677
+ `${args.join(' ')} (envelope)`,
678
+ 'json-exec',
679
+ ok ? 'ok' : 'unverified',
680
+ ok
681
+ ? `keys: ${keys.slice(0, 12).join(', ')}${keys.length > 12 ? ' …' : ''}`
682
+ : `exit ${r.exit}${r.err ? ` — ${r.err.slice(0, 120)}` : ' — no parseable JSON'}`,
683
+ { file: 'plugins/sp/scripts/surface-drift-inventory.ts', line: 1 },
684
+ );
685
+ }
686
+ // Curated prose flag-claims: assertions phrased as prose ("no explicit `--flag`") that the
687
+ // generic backtick-span extractor cannot scope to a command. Extend this list when a prose
688
+ // claim is found; each entry is verified against the live help capture.
689
+ const proseFlagClaims: {
690
+ flag: string;
691
+ path: string[];
692
+ expect: boolean;
693
+ where: { file: string; line: number };
694
+ note: string;
695
+ }[] = [
696
+ {
697
+ flag: '--stage',
698
+ path: ['agent', 'run'],
699
+ expect: false,
700
+ where: { file: 'plugins/sp/skills/parallel-execution/references/dispatch-surface.md', line: 116 },
701
+ note: 'prose claimed an explicit direct-CLI --stage (0539: corrected - stage context is engine-internal)',
702
+ },
703
+ ];
704
+ for (const c of proseFlagClaims) {
705
+ const vs = surface(c.path);
706
+ const has = vs.flags.includes(c.flag);
707
+ const ok = has === c.expect;
708
+ record(
709
+ `spur ${c.path.join(' ')} ${c.flag} (prose claim: ${c.where.file}:${c.where.line})`,
710
+ 'help-capture(prose-claim)',
711
+ ok ? 'ok' : 'mismatch',
712
+ ok
713
+ ? `live ${has ? 'exposes' : 'does not expose'} ${c.flag} as expected - ${c.note}`
714
+ : `live ${has ? 'exposes' : 'does not expose'} ${c.flag} but prose claims otherwise - ${c.note}`,
715
+ c.where,
716
+ );
717
+ }
718
+ record(
719
+ 'agent run --json -> roleOrigin (asserted by dispatch-surface.md:112)',
720
+ 'json-exec',
721
+ 'unverified',
722
+ "executing 'spur agent run' launches an external coding agent — cannot be executed mechanically here",
723
+ { file: 'plugins/sp/skills/parallel-execution/references/dispatch-surface.md', line: 112 },
724
+ );
725
+ record(
726
+ 'task list --json -> bare array of {wbs,status,…} (asserted by feature-sync-bounded.ts:291)',
727
+ 'json-exec(field-presence)',
728
+ jsonEnvelopeShapes['spur task list']?.exit === 0 ? 'ok' : 'mismatch',
729
+ jsonEnvelopeShapes['spur task list']?.exit === 0 ? 'array envelope confirmed' : 'probe failed',
730
+ { file: 'plugins/sp/scripts/feature-sync-bounded.ts', line: 291 },
731
+ );
732
+ }
733
+
734
+ // ─── E. Workflows + symlink ─────────────────────────────────────────────────
735
+
736
+ export function sweepWorkflows(opts: { run?: CliRunner; wfDir?: string; link?: string } = {}): void {
737
+ const run = opts.run ?? runCli;
738
+ const wfDir = opts.wfDir ?? join(REPO_ROOT, 'config', 'workflows');
739
+ const files = readdirSync(wfDir)
740
+ .filter((f) => f.endsWith('.yaml'))
741
+ .sort();
742
+ for (const f of files) {
743
+ const path = rel(join(wfDir, f));
744
+ const v = run(['workflow', 'validate', path]);
745
+ record(
746
+ `workflow validate ${f}`,
747
+ 'workflow-validate(live engine)',
748
+ v.exit === 0 ? 'ok' : 'mismatch',
749
+ `exit ${v.exit}${v.err ? ` — ${v.err.slice(0, 160)}` : ''}`,
750
+ { file: path, line: 1 },
751
+ );
752
+ const dryVars = f === 'task-pipeline.yaml' ? ['--vars', JSON.stringify({ wbs: '0539', profile: 'auto' })] : [];
753
+ const d = run(['workflow', 'run', path, '--dry-run', ...dryVars, '--quiet'], 45_000);
754
+ const tail = (d.out.trim().split('\n').pop() ?? d.err.trim().split('\n').pop() ?? '').slice(0, 160);
755
+ // A dry-run that terminates (any exit) proves the engine walks the definition; schema
756
+ // divergence is validate's job. Only a timeout is unverified.
757
+ record(
758
+ `workflow run ${f} --dry-run`,
759
+ 'workflow-dry-run(live engine)',
760
+ d.exit === 124 ? 'unverified' : 'ok',
761
+ d.exit === 124
762
+ ? 'dry-run did not terminate in 45s (paused on HITL?)'
763
+ : `walked to termination — ${tail || `exit ${d.exit}`}`,
764
+ { file: path, line: 1 },
765
+ );
766
+ // CLI invocations asserted inside the YAML (shell guards/actions).
767
+ readFileSync(join(wfDir, f), 'utf8')
768
+ .split(/\r?\n/)
769
+ .forEach((line: string, i: number) => {
770
+ for (const span of lineInvocationSpans(line)) {
771
+ const parsed = parseInvocation(span);
772
+ if (parsed)
773
+ checkNounVerbFlags(parsed.nouns, parsed.verbs, parsed.flags, {
774
+ file: path,
775
+ line: i + 1,
776
+ });
777
+ }
778
+ });
779
+ }
780
+ // Two-tier model (task 0648/0650): `.spur/workflows` is no longer a symlink and
781
+ // must not exist — the CLI resolves an explicit project path, then bundled fallback. Assert absence.
782
+ const link = opts.link ?? join(REPO_ROOT, '.spur', 'workflows');
783
+ let status: Row['status'] = 'ok';
784
+ let actual = 'absent (two-tier project→bundled model, task 0650)';
785
+ try {
786
+ if (existsSync(link)) {
787
+ status = 'mismatch';
788
+ actual = 'still present — should be removed (task 0650 R3)';
789
+ }
790
+ } catch (e) {
791
+ actual = `exists-check failed: ${String(e).slice(0, 120)}`;
792
+ }
793
+ record('.spur/workflows symlink', 'symlink-absent', status, actual, { file: '.spur/workflows', line: 1 });
794
+ }
795
+
796
+ // ─── Report ─────────────────────────────────────────────────────────────────
797
+
798
+ export function render(): string {
799
+ const prov = surface([]);
800
+ const byMethod = new Map<string, Row[]>();
801
+ for (const r of rows) {
802
+ const key = r.method.split('(')[0] ?? r.method;
803
+ byMethod.set(key, [...(byMethod.get(key) ?? []), r]);
804
+ }
805
+ const counts = {
806
+ ok: rows.filter((r) => r.status === 'ok').length,
807
+ mismatch: rows.filter((r) => r.status === 'mismatch').length,
808
+ unverified: rows.filter((r) => r.status === 'unverified').length,
809
+ };
810
+ const esc = (s: string): string => s.replaceAll('|', '\\|');
811
+ const L: string[] = [];
812
+ L.push('# 0539 — Plugin & Workflow Surface-Drift Inventory (feature I3)');
813
+ L.push('');
814
+ L.push(
815
+ 'Generated by `bun plugins/sp/scripts/surface-drift-inventory.ts` — re-runnable; every entry names its check method.',
816
+ );
817
+ // pi-lens-ignore: unchecked-throwing-call
818
+ const pkg = JSON.parse(readFileSync(join(REPO_ROOT, 'apps', 'cli', 'package.json'), 'utf8')) as { version: string };
819
+ L.push(`CLI provenance: source-local \`apps/cli/src/index.ts\` @ ${pkg.version} (never a PATH \`spur\`).`);
820
+ L.push(`Root nouns: ${prov.commands.join(', ')}.`);
821
+ L.push('');
822
+ L.push(
823
+ `**Totals: ${counts.ok} ok · ${counts.mismatch} mismatch · ${counts.unverified} unverified** (unverified = no mechanical check can reach it — recorded, never passed).`,
824
+ );
825
+ L.push('');
826
+ L.push(
827
+ "R1 scope: `plugins/sp/{commands,skills,scripts,hooks}` (tests/ and evals/ are the harness itself; agents/ prose is outside R1's named surface list).",
828
+ );
829
+ L.push('R2 scope: all workflow YAML via the bundled tree (two-tier project→bundled model since task 0648/0650).');
830
+ L.push('');
831
+ L.push('## Check methods');
832
+ L.push('');
833
+ L.push('| Method | What it does |');
834
+ L.push('| --- | --- |');
835
+ L.push(
836
+ '| `help-capture` | Parses live Commander `Commands:`/`Options:` blocks from `spur <path> --help` (source-local entry) and diffs asserted noun/verb/flag |',
837
+ );
838
+ L.push(
839
+ '| `json-exec` | Executes read-only commands with `--json` and records the actual envelope; mutating commands stay unverified |',
840
+ );
841
+ L.push(
842
+ '| `script-exec` | Executes the spur-shelling plugin scripts against a fake bin (argument construction under execution) |',
843
+ );
844
+ L.push('| `file-resolution` / `json-parse` | Referenced files exist / JSON parses |');
845
+ L.push('| `host-contract` | Host-owned contract — unverified in-repo, reason recorded |');
846
+ L.push(
847
+ '| `workflow-validate` / `workflow-dry-run` | Live engine: schema+semantic validate; `--dry-run` transition walk |',
848
+ );
849
+ L.push('| `symlink-absent` | `.spur/workflows` is asserted absent (two-tier project→bundled model, task 0650) |');
850
+ L.push('');
851
+ for (const [method, list] of byMethod) {
852
+ const mm = list.filter((r) => r.status === 'mismatch').length;
853
+ L.push(`## ${method} — ${mm} mismatch / ${list.length} entries`);
854
+ L.push('');
855
+ L.push('| Asserted | Status | Actual | Occurrences |');
856
+ L.push('| --- | --- | --- | --- |');
857
+ for (const r of [...list].sort(
858
+ (a, b) => a.status.localeCompare(b.status) || a.asserted.localeCompare(b.asserted),
859
+ )) {
860
+ const occ =
861
+ r.occurrences
862
+ .map((o) => `${o.file}:${o.line}`)
863
+ .slice(0, 4)
864
+ .join('<br>') + (r.occurrences.length > 4 ? `<br>…+${r.occurrences.length - 4}` : '');
865
+ L.push(`| \`${esc(r.asserted)}\` | **${r.status}** | ${esc(r.actual)} | ${occ} |`);
866
+ }
867
+ L.push('');
868
+ }
869
+ L.push('## Live --json envelope shapes (captured by execution)');
870
+ L.push('');
871
+ L.push('| Command | Exit | Flattened keys |');
872
+ L.push('| --- | --- | --- |');
873
+ for (const [cmd, s] of Object.entries(jsonEnvelopeShapes)) {
874
+ L.push(
875
+ `| \`${esc(cmd)}\` | ${s.exit} | ${esc(s.keys.slice(0, 14).join(', '))}${s.keys.length > 14 ? ', …' : ''} |`,
876
+ );
877
+ }
878
+ L.push('');
879
+ L.push('## Confirmed mismatches (R3 disposition)');
880
+ L.push('');
881
+ const mism = rows.filter((r) => r.status === 'mismatch');
882
+ if (mism.length === 0) {
883
+ L.push('None — every mechanically-checked assertion matches the live surface.');
884
+ } else {
885
+ L.push('| # | Entry | Disposition |');
886
+ L.push('| --- | --- | --- |');
887
+ mism.forEach((r, n) => {
888
+ L.push(
889
+ `| ${n + 1} | \`${esc(r.asserted)}\` (${r.method}) — ${esc(r.actual)} | see task 0539 Solution (fixed here / owning WBS) |`,
890
+ );
891
+ });
892
+ }
893
+ L.push('');
894
+ return L.join('\n');
895
+ }
896
+
897
+ // ─── Main ───────────────────────────────────────────────────────────────────
898
+
899
+ /**
900
+ * Entry point. Guarded by `import.meta.main` so the pure span/flag parsers above
901
+ * can be imported and unit-tested without running the full multi-minute sweep
902
+ * (which shells the live CLI and writes files).
903
+ */
904
+ function main(): void {
905
+ const outIdx = process.argv.indexOf('--out');
906
+ sweepPluginTrees();
907
+ sweepScriptArgv();
908
+ executeScripts();
909
+ sweepHooks();
910
+ probeJsonShapes();
911
+ sweepWorkflows();
912
+ const report = render();
913
+ if (outIdx !== -1 && process.argv[outIdx + 1]) {
914
+ const outPath = resolve(process.argv[outIdx + 1] ?? '.');
915
+ mkdirSync(dirname(outPath), { recursive: true });
916
+ writeFileSync(outPath, report);
917
+ process.stderr.write(`wrote ${rel(outPath)}\n`);
918
+ }
919
+ const mismatches = rows.filter((r) => r.status === 'mismatch');
920
+ if (mismatches.length > 0) {
921
+ process.stderr.write(`\nCONFIRMED MISMATCHES (${mismatches.length}):\n`);
922
+ for (const m of mismatches) process.stderr.write(` - ${m.asserted} [${m.method}] ${m.actual}\n`);
923
+ process.exitCode = 1;
924
+ } else {
925
+ process.stderr.write('\nNo confirmed mismatches.\n');
926
+ }
927
+ }
928
+
929
+ if (import.meta.main) main();