spexcode 0.4.1 → 0.4.3
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/README.md +37 -17
- package/package.json +9 -5
- package/spec-cli/README.md +23 -14
- package/spec-cli/hooks/dispatch.sh +7 -5
- package/spec-cli/hooks/harness.sh +19 -8
- package/spec-cli/src/anchors.ts +110 -18
- package/spec-cli/src/cli.ts +88 -37
- package/spec-cli/src/client.ts +34 -11
- package/spec-cli/src/doctor.ts +174 -36
- package/spec-cli/src/gateway-auth.ts +194 -0
- package/spec-cli/src/gateway-hub.ts +360 -0
- package/spec-cli/src/gateway.ts +27 -19
- package/spec-cli/src/git.ts +12 -5
- package/spec-cli/src/graph.ts +54 -43
- package/spec-cli/src/graphStream.ts +265 -34
- package/spec-cli/src/guide.ts +105 -55
- package/spec-cli/src/harness-select.ts +20 -6
- package/spec-cli/src/harness.ts +181 -33
- package/spec-cli/src/help.ts +64 -27
- package/spec-cli/src/host.ts +610 -0
- package/spec-cli/src/identity-presets.d.ts +13 -0
- package/spec-cli/src/identity-presets.js +128 -0
- package/spec-cli/src/index.ts +145 -86
- package/spec-cli/src/init.ts +53 -25
- package/spec-cli/src/layout.ts +5 -5
- package/spec-cli/src/lint.ts +25 -121
- package/spec-cli/src/login-page.ts +12 -5
- package/spec-cli/src/materialize.ts +39 -13
- package/spec-cli/src/mentions.ts +16 -11
- package/spec-cli/src/opencode.ts +145 -0
- package/spec-cli/src/pi-harness.ts +124 -0
- package/spec-cli/src/project-identity.ts +73 -0
- package/spec-cli/src/pty-bridge.ts +321 -521
- package/spec-cli/src/pty-helper.mjs +86 -0
- package/spec-cli/src/reaper.ts +16 -8
- package/spec-cli/src/reviewSnapshot.ts +23 -0
- package/spec-cli/src/reviews.ts +347 -0
- package/spec-cli/src/session-timeline.ts +14 -0
- package/spec-cli/src/sessions.ts +188 -66
- package/spec-cli/src/shim-runtime.ts +169 -0
- package/spec-cli/src/slash-commands.ts +76 -0
- package/spec-cli/src/source-files.ts +79 -0
- package/spec-cli/src/specs.ts +12 -7
- package/spec-cli/src/supervise.ts +37 -15
- package/spec-cli/src/tree.ts +8 -8
- package/spec-cli/src/uninstall.ts +56 -23
- package/spec-cli/templates/hooks/prepare-commit-msg +32 -18
- package/spec-cli/templates/spec/project/.plugins/commands/extract/spec.md +24 -18
- package/spec-cli/templates/spec/project/.plugins/commands/regroup/spec.md +3 -3
- package/spec-cli/templates/spec/project/.plugins/commands/rename/spec.md +11 -0
- package/spec-cli/templates/spec/project/.plugins/commands/spec.md +22 -8
- package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/commands/tidy/spec.md +29 -11
- package/spec-cli/templates/spec/project/.plugins/core/idle/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/core/mark-active/spec.md +2 -0
- package/spec-cli/templates/spec/project/.plugins/core/session-fail/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/core/spec-first/spec-first.sh +28 -39
- package/spec-cli/templates/spec/project/.plugins/core/spec-first/spec.md +5 -3
- package/spec-cli/templates/spec/project/.plugins/core/spec-of-file/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/core/spec.md +2 -2
- package/spec-cli/templates/spec/project/.plugins/core/stop-gate/spec.md +7 -1
- package/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +2 -2
- package/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +5 -5
- package/spec-cli/templates/spec/project/.plugins/prompts/spec.md +10 -6
- package/spec-cli/templates/spec/project/.plugins/skills/distill/spec.md +38 -50
- package/spec-cli/templates/spec/project/.plugins/skills/spec.md +9 -7
- package/spec-cli/templates/spec/project/.plugins/spec.md +17 -12
- package/spec-cli/templates/spec/project/spec.md +6 -5
- package/spec-cli/templates/spexcode.json +4 -2
- package/spec-dashboard/dist/assets/Dashboard-CiHh-gLD.js +27 -0
- package/spec-dashboard/dist/assets/EvalsPage-DmiX3rdU.js +2 -0
- package/spec-dashboard/dist/assets/IssuesPage-CIbVGRUJ.js +1 -0
- package/spec-dashboard/dist/assets/MobileApp-D-N9_eh0.js +2 -0
- package/spec-dashboard/dist/assets/Modal-DHMzSFJ4.js +1 -0
- package/spec-dashboard/dist/assets/PageScroll-C15adEYI.js +1 -0
- package/spec-dashboard/dist/assets/ProjectsPage-sQpzglp5.js +1 -0
- package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +32 -0
- package/spec-dashboard/dist/assets/SessionInterface-B8pGU7Rg.js +39 -0
- package/spec-dashboard/dist/assets/SessionWindow-CuDO_67z.js +13 -0
- package/spec-dashboard/dist/assets/Settings-C_N1wX1f.js +1 -0
- package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +1 -0
- package/spec-dashboard/dist/assets/index-DmWbmvCq.js +41 -0
- package/spec-dashboard/dist/assets/index-GGIVdKwH.css +1 -0
- package/spec-dashboard/dist/assets/terminalFont-DmTPNdbU.js +1 -0
- package/spec-dashboard/dist/index.html +10 -10
- package/spec-dashboard/src/reviewFilters.js +279 -0
- package/spec-dashboard/src/reviewQuery.js +174 -0
- package/spec-dashboard/src/session.js +167 -0
- package/spec-eval/src/cli.ts +49 -28
- package/spec-eval/src/humanok.ts +1 -1
- package/spec-eval/src/matrix.ts +693 -0
- package/spec-eval/src/sessioneval.ts +726 -66
- package/spec-cli/src/migrate-table.ts +0 -408
- package/spec-cli/src/migrate.ts +0 -386
- package/spec-dashboard/dist/assets/Dashboard-C_fGmOKK.js +0 -27
- package/spec-dashboard/dist/assets/EvalsPage-Cnr1s3bq.js +0 -2
- package/spec-dashboard/dist/assets/FoldToggle-x9gtO1OQ.js +0 -1
- package/spec-dashboard/dist/assets/IssuesPage-5f_vL-JV.js +0 -1
- package/spec-dashboard/dist/assets/MobileApp-DEO1jgGM.js +0 -1
- package/spec-dashboard/dist/assets/SessionInterface-CAlbMOFR.js +0 -66
- package/spec-dashboard/dist/assets/SessionInterface-DYP7pi_n.css +0 -32
- package/spec-dashboard/dist/assets/SessionWindow-JYbpPwNB.js +0 -13
- package/spec-dashboard/dist/assets/Settings-DKb5Ji_X.js +0 -1
- package/spec-dashboard/dist/assets/index-BQu-oJ8J.js +0 -41
- package/spec-dashboard/dist/assets/index-BbMkwuix.css +0 -1
- package/spec-dashboard/dist/assets/launch-BM9GgvkX.js +0 -6
package/spec-cli/src/cli.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export {} // make this a module so top-level await is allowed
|
|
2
2
|
// static import is fine here: mentions.ts is dependency-free at module level, and stripRefSigil is needed
|
|
3
|
-
// by several verbs (spec owner,
|
|
3
|
+
// by several verbs (spec owner, graph, issue/eval node args) — a CLI reference arg tolerates an optional @/[[ ]]
|
|
4
4
|
// sigil ([[mentions]]).
|
|
5
5
|
import { stripRefSigil } from './mentions.js'
|
|
6
6
|
|
|
@@ -60,6 +60,19 @@ function positionals(from: number): string[] {
|
|
|
60
60
|
return out
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function rejectUnknownFlags(command: string, from: number, allowed: readonly string[]): void {
|
|
64
|
+
const known = new Set(allowed.map((name) => `--${name}`))
|
|
65
|
+
for (let i = from; i < process.argv.length; i++) {
|
|
66
|
+
const token = process.argv[i]
|
|
67
|
+
if (!token.startsWith('--')) continue
|
|
68
|
+
if (!known.has(token)) {
|
|
69
|
+
console.error(`${command}: unknown flag ${token}`)
|
|
70
|
+
process.exit(2)
|
|
71
|
+
}
|
|
72
|
+
if (VALUE_FLAGS.has(token)) i++
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
// @@@ signposts (one version only — delete in 0.4.0) - every spelling v0.3.0 removed maps to its new home.
|
|
64
77
|
// A signpost REPORTS and exits 2; it never executes (not an alias): a stale hook or a human's muscle memory
|
|
65
78
|
// gets a readable failure that names the migration, and nothing old keeps silently working.
|
|
@@ -73,7 +86,6 @@ const SIGNPOSTS: Record<string, string> = {
|
|
|
73
86
|
blob: 'spex evidence put|get',
|
|
74
87
|
issues: 'spex issue — ls (was: bare issues) · show · open · reply · close · promote; on|off|status → the `issues.enabled` key in spexcode.json; `issues nudge` → spex internal nudge',
|
|
75
88
|
forge: 'spex issue links [--pending] [--store <host>] (--host is now --store)',
|
|
76
|
-
dashboard: 'spex serve ui',
|
|
77
89
|
new: 'spex session new',
|
|
78
90
|
ls: 'spex session ls',
|
|
79
91
|
watch: 'spex session watch',
|
|
@@ -173,7 +185,7 @@ async function withWatchEdge<T>(selectors: string[], intervalMs: number, body: (
|
|
|
173
185
|
}
|
|
174
186
|
|
|
175
187
|
async function resolveSelectorOrExit(selector: string): Promise<string> {
|
|
176
|
-
if (!selector) { console.error('spex: missing session selector (id | id-prefix | node | branch)'); process.exit(2) }
|
|
188
|
+
if (!selector) { console.error('spex: missing session selector (id | id-prefix | node | branch | . for self)'); process.exit(2) }
|
|
177
189
|
const { resolveClientSession } = await import('./client.js')
|
|
178
190
|
const { sessionLabel } = await import('./sessions.js')
|
|
179
191
|
const r = await resolveClientSession(selector)
|
|
@@ -309,6 +321,19 @@ if (cmd === 'serve') {
|
|
|
309
321
|
console.error(`spex serve: unknown target '${target}' — spex serve [api] (the backend) | spex serve ui (the dashboard)`)
|
|
310
322
|
process.exit(2)
|
|
311
323
|
}
|
|
324
|
+
} else if (cmd === 'dashboard') {
|
|
325
|
+
// the HOST-level dashboard ([[host-gateway]]): ONE gateway for every project this user serves. The
|
|
326
|
+
// engine is [[gateway-hub]] (routing + [[gateway-auth]] authorization: admin scope implicit from
|
|
327
|
+
// loopback until an admin password is set; per-project gates as configured); the host layer mounts the
|
|
328
|
+
// instance-validated project registry, its SSE stream, the durable catalog, and the /projects
|
|
329
|
+
// operations (register · init · doctor · start a backend) as the hub's admin extension. No --api-port
|
|
330
|
+
// pairing: which backend a request reaches is named in its /p/:projectId path, resolved per request.
|
|
331
|
+
// `spex serve ui` remains the explicit one-backend pairing; this verb is the zero-config many-project face.
|
|
332
|
+
const { startHostDashboard } = await import('./host.js')
|
|
333
|
+
const port = Number(flag('port') ?? process.env.SPEXCODE_DASHBOARD_PORT ?? 5173)
|
|
334
|
+
const host = flag('host') ?? '127.0.0.1'
|
|
335
|
+
if (!Number.isInteger(port)) { console.error('spex dashboard: --port must be an integer'); process.exit(2) }
|
|
336
|
+
startHostDashboard({ port, host })
|
|
312
337
|
} else if (cmd === undefined || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
313
338
|
// `spex help <cmd>` drills into one command; bare help is the map. Both name the next layer down.
|
|
314
339
|
const { commandHelp, overviewHelp } = await import('./help.js')
|
|
@@ -467,9 +492,9 @@ if (cmd === 'serve') {
|
|
|
467
492
|
}
|
|
468
493
|
} else if (cmd === 'init') {
|
|
469
494
|
// scaffold a repo to adopt SpexCode: copy the shipped DATA templates (seed spec tree + git hooks)
|
|
470
|
-
// into <targetDir> (default cwd). spex init [targetDir] [--preset <tier>]
|
|
495
|
+
// into <targetDir> (default cwd). spex init [targetDir] --harness <ids> [--preset <tier>]
|
|
471
496
|
const { specInit } = await import('./init.js')
|
|
472
|
-
await specInit(positionals(3)[0], flag('preset'))
|
|
497
|
+
await specInit(positionals(3)[0], flag('preset'), flag('harness'))
|
|
473
498
|
} else if (cmd === 'uninstall') {
|
|
474
499
|
// the surgical inverse of init: remove every SpexCode-generated artifact (harness shims/contract/trust, the
|
|
475
500
|
// .gitignore block, the global store, any plugin bundle) — NEVER the user's .spec/.plugins data or their own
|
|
@@ -497,42 +522,35 @@ if (cmd === 'serve') {
|
|
|
497
522
|
if (!r.ok) { console.error(`no evals for ${id} (status ${r.status})`); process.exit(1) }
|
|
498
523
|
if (has('json')) { console.log(JSON.stringify(r.model, null, 2)); await flushExit(0) }
|
|
499
524
|
const m = r.model
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
return { n, blind, rows }
|
|
510
|
-
})
|
|
511
|
-
const own = groups.reduce((a, g) => a + g.rows.filter((e) => e.inSession).length, 0)
|
|
512
|
-
console.log(`eval ${m.title} [${m.id}]`)
|
|
513
|
-
console.log(` branch : ${m.branch ?? '—'} · ${m.ahead} commit(s) ahead · ${m.dirtyNonRuntime} uncommitted`)
|
|
525
|
+
const byNode = new Map<string, any[]>()
|
|
526
|
+
for (const item of m.items) {
|
|
527
|
+
const rows = byNode.get(item.node) ?? []
|
|
528
|
+
rows.push(item)
|
|
529
|
+
byNode.set(item.node, rows)
|
|
530
|
+
}
|
|
531
|
+
const groups = [...byNode].map(([node, rows]) => ({ node, rows }))
|
|
532
|
+
const own = m.items.filter((item) => item.inSession).length
|
|
533
|
+
console.log(`eval session [${m.id}]`)
|
|
514
534
|
console.log(` gates : ${m.gates.map((g) => `${g.ok ? '✓' : '✗'} ${g.label} — ${g.detail}`).join(' · ')}`)
|
|
515
535
|
if (own) console.log(` ✦ : ${own} scenario(s) measured by THIS session (unmarked rows = inherited baseline)`)
|
|
516
|
-
if (!m.
|
|
517
|
-
for (const {
|
|
518
|
-
console.log(`\n${
|
|
519
|
-
for (const
|
|
536
|
+
if (!m.items.length) console.log('\n no affected scenarios to evaluate yet')
|
|
537
|
+
for (const { node, rows } of groups) {
|
|
538
|
+
console.log(`\n${node}`)
|
|
539
|
+
for (const item of rows.filter((row) => row.filterKind === 'blind')) console.log(` ∅ unmeasured ${item.scenario} — declared, never measured (blind spot)`)
|
|
520
540
|
let divided = false
|
|
521
|
-
for (const e of rows) {
|
|
522
|
-
if (!e.inSession && !divided && rows.some((x) => x.inSession)) { console.log(` ── inherited baseline (other sessions' latest evals) ──`); divided = true }
|
|
541
|
+
for (const e of rows.filter((row) => row.filterKind === 'result')) {
|
|
542
|
+
if (!e.inSession && !divided && rows.some((x) => x.filterKind === 'result' && x.inSession)) { console.log(` ── inherited baseline (other sessions' latest evals) ──`); divided = true }
|
|
523
543
|
const verdict = e.verdict?.status === 'pass' ? '✓ pass' : e.verdict?.status === 'fail' ? '✗ fail' : '· unscored'
|
|
524
|
-
const stale = e.fresh ? '' : ` (stale: ${e.staleAxes.join(',')})`
|
|
544
|
+
const stale = e.fresh ? '' : ` (stale: ${(e.staleAxes || []).join(',')})`
|
|
525
545
|
console.log(` ${e.inSession ? '✦' : ' '} ${verdict}${stale} ${e.scenario} — ${e.ts}${e.evaluator ? ` · ${e.evaluator}` : ''}`)
|
|
526
546
|
}
|
|
527
|
-
if (!n.hasEvalFile && !n.uncoveredFrontend) console.log(' (no eval.md — nothing declared to measure)')
|
|
528
|
-
else if (n.hasEvalFile && !n.scenarios.length) console.log(' (eval.md declares no scenarios)')
|
|
529
547
|
}
|
|
530
|
-
} else if (['add', 'ls', 'scenario', 'lint', 'ok', 'retract', 'clean'].includes(sub)) {
|
|
548
|
+
} else if (['add', 'ls', 'scenario', 'matrix', 'lint', 'ok', 'retract', 'clean'].includes(sub)) {
|
|
531
549
|
// node-scoped verbs — thin route; the logic lives in spec-eval.
|
|
532
550
|
const { runEval } = await import('../../spec-eval/src/cli.js')
|
|
533
551
|
await flushExit(await runEval(process.argv.slice(3)))
|
|
534
552
|
} else {
|
|
535
|
-
console.error(`spex eval: unknown verb '${sub}' — add | ls | scenario ls | lint | ok | retract | clean (spex help eval)`)
|
|
553
|
+
console.error(`spex eval: unknown verb '${sub}' — add | ls | scenario ls | matrix | lint | ok | retract | clean (spex help eval)`)
|
|
536
554
|
if (!sub.startsWith('--')) console.error(` (the old \`spex eval <SEL>\` session read is now \`spex eval ls --session <SEL>\` [--export])`) // dead-words-ok: signpost — one-version tombstone teaching the renamed spelling (0.4.0 removes it)
|
|
537
555
|
process.exit(2)
|
|
538
556
|
}
|
|
@@ -583,7 +601,14 @@ if (cmd === 'serve') {
|
|
|
583
601
|
// trust, for cwd's project. Anchored on git-native events only ([[commit-surgery]]): this verb, init,
|
|
584
602
|
// session-worktree creation, and the planted pre-commit/post-checkout/post-merge hooks.
|
|
585
603
|
const { materialize } = await import('./materialize.js')
|
|
586
|
-
|
|
604
|
+
try {
|
|
605
|
+
console.log(`materialized — content-hash ${materialize().contentHash}`)
|
|
606
|
+
} catch (e) {
|
|
607
|
+
// a policy error (e.g. a missing/illegal `harnesses` set) is a user-facing verdict, not a crash — one
|
|
608
|
+
// line + the repair it already carries, never a stack trace.
|
|
609
|
+
console.error(`spex materialize: ${(e as Error).message}`)
|
|
610
|
+
process.exit(1)
|
|
611
|
+
}
|
|
587
612
|
} else if (cmd === 'doctor') {
|
|
588
613
|
// @@@ doctor - the diagnosis surface ([[doctor]], né `self` — renamed: "self" read as the tool itself /
|
|
589
614
|
// the global install, while the report is about THIS agent's wiring): does the materialized workflow
|
|
@@ -599,12 +624,16 @@ if (cmd === 'serve') {
|
|
|
599
624
|
} else if (SESSION_SIGNPOSTS[sub]) {
|
|
600
625
|
signpost(`spex session ${sub}`, SESSION_SIGNPOSTS[sub])
|
|
601
626
|
} else if (sub === 'new') {
|
|
602
|
-
// spex session new "<prompt>"
|
|
627
|
+
// spex session new "<prompt>" (prompt = first positional or --prompt, or --prompt-file
|
|
603
628
|
// <path>|- so a long multi-paragraph prompt never fights shell quoting — [[prompt-file]]).
|
|
604
629
|
// createSession POSTs to the running backend so the launch runs in the backend's process (auth env + cap);
|
|
605
630
|
// it falls back to an in-process launch only when no backend answers.
|
|
631
|
+
if (has('node')) {
|
|
632
|
+
console.error('spex session new: --node was removed — put a [[<id>]] mention in the prompt — the first mention binds')
|
|
633
|
+
process.exit(2)
|
|
634
|
+
}
|
|
635
|
+
rejectUnknownFlags('spex session new', 4, ['prompt', 'prompt-file', 'launcher', 'api', 'port'])
|
|
606
636
|
const { createSession } = await import('./sessions.js')
|
|
607
|
-
if (has('harness')) { console.error('spex session new: --harness was removed; use --launcher <name> (for example --launcher codex)'); process.exit(2) }
|
|
608
637
|
const promptFile = flag('prompt-file')
|
|
609
638
|
const inline = flag('prompt') ?? positionals(4)[0]
|
|
610
639
|
let prompt = inline ?? ''
|
|
@@ -616,8 +645,7 @@ if (cmd === 'serve') {
|
|
|
616
645
|
catch (e) { console.error(`spex session new: --prompt-file ${promptFile}: ${e instanceof Error ? e.message : e}`); process.exit(2) }
|
|
617
646
|
if (!prompt.trim()) { console.error(`spex session new: --prompt-file ${promptFile === '-' ? 'stdin' : promptFile} is empty — refusing a promptless launch`); process.exit(2) }
|
|
618
647
|
}
|
|
619
|
-
const
|
|
620
|
-
const created = await createSession(nodeArg ? stripRefSigil(nodeArg) : null, prompt, flag('launcher') ?? undefined)
|
|
648
|
+
const created = await createSession(prompt, flag('launcher') ?? undefined)
|
|
621
649
|
console.log(JSON.stringify(created, null, 2))
|
|
622
650
|
await launchMonitorReminder(created.id)
|
|
623
651
|
} else if (sub === 'ls') {
|
|
@@ -728,7 +756,7 @@ if (cmd === 'serve') {
|
|
|
728
756
|
// overrides for a genuinely wedged process. A following prompt is what actually re-drives it.
|
|
729
757
|
const full = await resolveSelectorOrExit(id)
|
|
730
758
|
const r = await c.clientResume(full, process.argv.includes('--force'))
|
|
731
|
-
if (r.ok) console.log(`${full} -> resumed`)
|
|
759
|
+
if (r.ok) console.log(r.info ? `${full} -> ${r.info}` : `${full} -> resumed`)
|
|
732
760
|
else { console.error(`spex session resume: ${r.error || `no such session ${full}`}`); process.exit(2) }
|
|
733
761
|
} else if (sub === 'done') {
|
|
734
762
|
// sugar for awaiting; --propose merge|nothing|close, optional --note
|
|
@@ -855,6 +883,18 @@ if (cmd === 'serve') {
|
|
|
855
883
|
// line; GET /api/settings exposes the same resolution (`.layout`).
|
|
856
884
|
const { mainBranch } = await import('./layout.js')
|
|
857
885
|
console.log(mainBranch())
|
|
886
|
+
} else if (sub === 'spec-governors') {
|
|
887
|
+
// Stable machine projection for spec-aware hooks: one real code: governor per row, with the live spec
|
|
888
|
+
// path the block reason can point at. Empty stdout means ungoverned (including related-only).
|
|
889
|
+
const file = process.argv[4]
|
|
890
|
+
if (!file) { console.error('usage: spex internal spec-governors <path>'); process.exit(2) }
|
|
891
|
+
const { specOwners, loadSpecsLite } = await import('./specs.js')
|
|
892
|
+
const paths = new Map(loadSpecsLite().map((node) => [node.id, node.path]))
|
|
893
|
+
for (const owner of specOwners(file)) {
|
|
894
|
+
const path = paths.get(owner.id)
|
|
895
|
+
if (!path) throw new Error(`governor '${owner.id}' has no live spec path`)
|
|
896
|
+
console.log(`${owner.id}\t${path}`)
|
|
897
|
+
}
|
|
858
898
|
} else if (sub === 'codex-launch') {
|
|
859
899
|
// BACKEND-owned codex thread. On the shared per-project app-server: thread/start { cwd = this worktree }
|
|
860
900
|
// (codex loads that worktree's config/hooks/AGENTS.md), store the new id on the governed record (keyed by
|
|
@@ -868,7 +908,7 @@ if (cmd === 'serve') {
|
|
|
868
908
|
// On the bypass-trust path (the codex install supports the flag → materialize skipped writeCodexTrust's hash),
|
|
869
909
|
// the thread the BACKEND owns must carry `bypass_hook_trust` in thread/start's config so the app-server fires
|
|
870
910
|
// the worktree's local hooks — mirror materialize's capability decision so the two stay in lockstep.
|
|
871
|
-
const bypassHookTrust = codexSupportsBypassHookTrust(codexBinary(process.env.SPEXCODE_CODEX_CMD || 'codex
|
|
911
|
+
const bypassHookTrust = codexSupportsBypassHookTrust(codexBinary(process.env.SPEXCODE_CODEX_CMD || 'codex'))
|
|
872
912
|
const r = await codexStartThread(sock, cwd, bypassHookTrust)
|
|
873
913
|
if (!r.ok) { console.error(r.error); process.exit(1) }
|
|
874
914
|
if (prompt) {
|
|
@@ -887,6 +927,17 @@ if (cmd === 'serve') {
|
|
|
887
927
|
const sid = process.env.SPEXCODE_SESSION_ID
|
|
888
928
|
if (sid) markHarnessSessionId(sid, r.threadId)
|
|
889
929
|
console.log(r.threadId)
|
|
930
|
+
} else if (sub === 'opencode-capture') {
|
|
931
|
+
// opencode MINTS its own session id (no launch flag pins it), so the generated plugin's FIRST event calls
|
|
932
|
+
// this to store that id as harness_session_id on the governed record (SPEXCODE_SESSION_ID from the launch
|
|
933
|
+
// env, inherited by the opencode process → plugin). That is what lets reopen() resume the SAME
|
|
934
|
+
// conversation (`--session <id>`). A missing record/env is a clean no-op — a plugin loaded outside a
|
|
935
|
+
// governed launch has nothing to mark.
|
|
936
|
+
const { markHarnessSessionId } = await import('./sessions.js')
|
|
937
|
+
const ocid = process.argv[4]
|
|
938
|
+
if (!ocid) { console.error('usage: spex internal opencode-capture <opencode-session-id>'); process.exit(2) }
|
|
939
|
+
const sid = process.env.SPEXCODE_SESSION_ID
|
|
940
|
+
console.log(sid && markHarnessSessionId(sid, ocid) ? `captured ${ocid}` : 'noop (no governed session record)')
|
|
890
941
|
} else if (sub === 'commit-surgery') {
|
|
891
942
|
// the pre-commit footprint anchor ([[commit-surgery]]): unconditional materialize + staged-index repair
|
|
892
943
|
// (strip our sentinel block from staged blobs, unstage HEAD-untracked generated artifacts). Called only
|
package/spec-cli/src/client.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { apiBase, assertProjectMatch, resolveSession, type Session, type Resolved, type DispatchResult, type ReviewPayload } from './sessions.js'
|
|
2
|
-
import type { SessionEvals } from '../../spec-eval/src/sessioneval.js'
|
|
3
2
|
|
|
4
3
|
export class BackendError extends Error {
|
|
5
4
|
constructor(message: string, readonly status?: number) {
|
|
@@ -74,15 +73,38 @@ export async function clientEvalExport(id: string): Promise<ExportResult> {
|
|
|
74
73
|
return { ok: false, status: r.status }
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
// The CLI's explicit aggregate walks the same 25-row pages as the dashboard. No server response contains
|
|
77
|
+
// the full session model; aggregation exists only for this one-shot terminal rendering.
|
|
78
|
+
type SessionEvalPage = {
|
|
79
|
+
items: any[]
|
|
80
|
+
page: number
|
|
81
|
+
pageCount: number
|
|
82
|
+
total: number
|
|
83
|
+
gates: any[]
|
|
84
|
+
unknown: number
|
|
85
|
+
revision: string
|
|
86
|
+
summary?: any
|
|
87
|
+
evalRevision?: any
|
|
88
|
+
}
|
|
89
|
+
export type EvalsResult = { ok: true; model: SessionEvalPage & { id: string } } | { ok: false; status: number }
|
|
82
90
|
export async function clientEvals(id: string): Promise<EvalsResult> {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
const q = encodeURIComponent(`is:eval scope:${id}`)
|
|
92
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
93
|
+
const items: any[] = []
|
|
94
|
+
let first: SessionEvalPage | null = null
|
|
95
|
+
let changed = false
|
|
96
|
+
for (let page = 1;; page++) {
|
|
97
|
+
const r = await apiFetch(`/api/evals?q=${q}&page=${page}`)
|
|
98
|
+
if (!r.ok) return { ok: false, status: r.status }
|
|
99
|
+
const current = await r.json() as SessionEvalPage
|
|
100
|
+
first ??= current
|
|
101
|
+
if (current.revision !== first.revision) { changed = true; break }
|
|
102
|
+
items.push(...current.items)
|
|
103
|
+
if (page >= current.pageCount) break
|
|
104
|
+
}
|
|
105
|
+
if (!changed) return { ok: true, model: { ...first!, id, items } }
|
|
106
|
+
}
|
|
107
|
+
throw new BackendError(`session eval pages changed while reading ${id}; retry the command`)
|
|
86
108
|
}
|
|
87
109
|
|
|
88
110
|
// POST /api/sessions/:id/merge — the cockpit's merge DISPATCH (200 {dispatched:true} / 409 {reason}).
|
|
@@ -94,8 +116,9 @@ export async function clientMerge(id: string): Promise<{ dispatched: boolean; re
|
|
|
94
116
|
|
|
95
117
|
// POST /api/sessions/:id/resume — bring the agent back (relaunch ONLY if confirmed offline); demotes
|
|
96
118
|
// working→idle, keeps any declaration. The RESUME GUARD REFUSES (409 {refused:true}) on a live/unproven agent;
|
|
97
|
-
// `force` overrides for a wedged-but-alive process. {ok:false} otherwise = no such session (404).
|
|
98
|
-
|
|
119
|
+
// `force` overrides for a wedged-but-alive process. {ok:false} otherwise = no such session (404). `info`
|
|
120
|
+
// carries a non-error advisory.
|
|
121
|
+
export async function clientResume(id: string, force = false): Promise<{ ok: boolean; error?: string; refused?: boolean; info?: string }> {
|
|
99
122
|
await guarded('session resume')
|
|
100
123
|
const r = await apiFetch(`/api/sessions/${seg(id)}/resume`, post({ force }))
|
|
101
124
|
return await r.json().catch(() => ({ ok: false, error: `bad backend response (${r.status})` }))
|
package/spec-cli/src/doctor.ts
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
// @@@ spex doctor - the DIAGNOSIS surface ([[doctor]]
|
|
2
|
-
//
|
|
3
|
-
// in the launch, the workflow reaches
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// LAYER, looping the same HARNESSES adapter materialize delivers through (so claude AND codex are covered
|
|
7
|
-
// with no hardcoded paths). It catches the SILENT failure: a shim whose handler is missing, a PATH that
|
|
8
|
-
// can't resolve `spex`, a contract that never landed. Read-only today: the bare report, `contract` (print
|
|
9
|
-
// the surface:system text any agent reads), `conflicts`. install/uninstall are STAGED (noteStaged).
|
|
1
|
+
// @@@ spex doctor - the opt-in, read-only project DIAGNOSIS surface ([[doctor]]). Bare doctor composes
|
|
2
|
+
// spec-health findings with the delivery audit: when a user launches their OWN agent with no SpexCode
|
|
3
|
+
// process in the launch, the workflow reaches it only through the files materialize() writes. The audit
|
|
4
|
+
// loops the same HARNESSES adapter materialize uses and catches missing delivery or duplicate discovery.
|
|
5
|
+
// `--contract` and `--conflicts` remain focused representations of that same diagnosis.
|
|
10
6
|
import { existsSync, readFileSync, readdirSync, accessSync, constants } from 'node:fs'
|
|
11
7
|
import { join, dirname, basename } from 'node:path'
|
|
12
8
|
import { fileURLToPath } from 'node:url'
|
|
13
9
|
import { execFileSync } from 'node:child_process'
|
|
14
10
|
import { homedir } from 'node:os'
|
|
15
|
-
import { loadSystemConfig, loadSkillConfig } from './specs.js'
|
|
16
|
-
import { runtimeRoot, treeSlotDir, envSessionId, readAliasedRawRecord, mainCheckout } from './layout.js'
|
|
11
|
+
import { loadSystemConfig, loadSkillConfig, loadSpecs } from './specs.js'
|
|
12
|
+
import { runtimeRoot, treeSlotDir, envSessionId, readAliasedRawRecord, mainCheckout, readJsonConfig } from './layout.js'
|
|
13
|
+
import { loadConfig } from './lint.js'
|
|
14
|
+
import { trackedSourceFiles } from './source-files.js'
|
|
17
15
|
|
|
18
16
|
// this file lives at <pkgRoot>/src/self.ts, so `..` is the package root — the same derivation init.ts/
|
|
19
17
|
// materialize.ts use (never a hardcoded repo path), so the git-hook template lookup survives a relocated install.
|
|
@@ -31,6 +29,156 @@ function commonHooksDir(dir: string): string | null {
|
|
|
31
29
|
}
|
|
32
30
|
const read = (f: string): string => { try { return readFileSync(f, 'utf8') } catch { return '' } }
|
|
33
31
|
|
|
32
|
+
type AltitudeConfig = {
|
|
33
|
+
lineBudget: number
|
|
34
|
+
charBudget: number
|
|
35
|
+
sizeable: number
|
|
36
|
+
dense: number
|
|
37
|
+
steps: number
|
|
38
|
+
identifierExtensions: string[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type BreadthConfig = {
|
|
42
|
+
maxChildren: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type HealthFinding = {
|
|
46
|
+
check: string
|
|
47
|
+
spec: string
|
|
48
|
+
summary: string
|
|
49
|
+
evidence: string[]
|
|
50
|
+
repair: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const DEFAULT_ALTITUDE: AltitudeConfig = {
|
|
54
|
+
lineBudget: 50,
|
|
55
|
+
charBudget: 4200,
|
|
56
|
+
sizeable: 35,
|
|
57
|
+
dense: 1.3,
|
|
58
|
+
steps: 3,
|
|
59
|
+
identifierExtensions: [],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const DEFAULT_BREADTH: BreadthConfig = {
|
|
63
|
+
maxChildren: 8,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function loadAltitudeConfig(root: string): AltitudeConfig {
|
|
67
|
+
const configured = readJsonConfig(join(root, 'spexcode.json'))?.doctor?.altitude ?? {}
|
|
68
|
+
const merged = { ...DEFAULT_ALTITUDE, ...configured }
|
|
69
|
+
return {
|
|
70
|
+
...merged,
|
|
71
|
+
identifierExtensions: (merged.identifierExtensions ?? []).map((ext: string) => ext.replace(/^\.+/, '')),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function loadBreadthConfig(root: string): BreadthConfig {
|
|
76
|
+
const configured = readJsonConfig(join(root, 'spexcode.json'))?.doctor?.breadth ?? {}
|
|
77
|
+
return { ...DEFAULT_BREADTH, ...configured }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Filename rows are lint coverage's exact tracked candidates. Compatibility extensions lower to wildcard
|
|
81
|
+
// rows before the one identifier matcher is compiled.
|
|
82
|
+
function identifierFilenameCandidates(sourceFiles: string[], compatibilityExtensions: string[]): string[] {
|
|
83
|
+
return [...new Set([
|
|
84
|
+
...sourceFiles.map((path) => basename(path)),
|
|
85
|
+
...compatibilityExtensions.map((ext) => `*.${ext}`),
|
|
86
|
+
])]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function identRe(filenameCandidates: string[]): RegExp {
|
|
90
|
+
const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
91
|
+
const filenames = filenameCandidates
|
|
92
|
+
.map((candidate) => candidate.startsWith('*.')
|
|
93
|
+
? `[\\w-]+\\.${escape(candidate.slice(2))}`
|
|
94
|
+
: escape(candidate))
|
|
95
|
+
.sort((a, b) => b.length - a.length)
|
|
96
|
+
const signals = [
|
|
97
|
+
'[a-z][A-Za-z0-9]*[A-Z][A-Za-z0-9]*',
|
|
98
|
+
'\\b[a-z]+_[a-z0-9_]+\\b',
|
|
99
|
+
'\\b\\w+\\(',
|
|
100
|
+
'`[^`]+`',
|
|
101
|
+
'\\/[\\w./-]+\\.\\w+',
|
|
102
|
+
]
|
|
103
|
+
if (filenames.length) signals.push(`(?<![\\w./-])(?:${filenames.join('|')})(?![\\w.-])`)
|
|
104
|
+
return new RegExp(signals.join('|'), 'g')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const STEP_LINE = /^\s*(\d+[.)]\s|[-*]\s*(first|then|next|finally)\b)|(^|[,;]\s*)(first|then|next|finally),/i
|
|
108
|
+
|
|
109
|
+
function altitudeEvidence(body: string, cfg: AltitudeConfig, ident: RegExp): string[] {
|
|
110
|
+
const lines = body.split('\n')
|
|
111
|
+
const nonBlank = lines.filter((line) => line.trim()).length
|
|
112
|
+
let inFence = false
|
|
113
|
+
let signals = 0
|
|
114
|
+
let steps = 0
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
if (/^\s*```/.test(line)) { inFence = !inFence; continue }
|
|
117
|
+
if (inFence || !line.trim()) continue
|
|
118
|
+
signals += line.match(ident)?.length ?? 0
|
|
119
|
+
if (STEP_LINE.test(line)) steps++
|
|
120
|
+
}
|
|
121
|
+
const density = signals / Math.max(1, nonBlank)
|
|
122
|
+
const evidence: string[] = []
|
|
123
|
+
if (nonBlank > cfg.lineBudget || body.length > cfg.charBudget)
|
|
124
|
+
evidence.push(`${nonBlank} non-blank lines / ${body.length} chars over budget (${cfg.lineBudget}/${cfg.charBudget})`)
|
|
125
|
+
if (nonBlank > cfg.sizeable && density > cfg.dense)
|
|
126
|
+
evidence.push(`code-identifier density ${density.toFixed(2)}/line over ${cfg.dense}`)
|
|
127
|
+
if (nonBlank > cfg.sizeable && steps >= cfg.steps)
|
|
128
|
+
evidence.push(`${steps} step-by-step how-to lines`)
|
|
129
|
+
return evidence
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function specHealthDiagnosis(root: string): Promise<HealthFinding[]> {
|
|
133
|
+
const lint = loadConfig(root)
|
|
134
|
+
const altitude = loadAltitudeConfig(root)
|
|
135
|
+
const breadth = loadBreadthConfig(root)
|
|
136
|
+
const governed = trackedSourceFiles(root, lint.governedRoots, lint)
|
|
137
|
+
const ident = identRe(identifierFilenameCandidates(governed, altitude.identifierExtensions))
|
|
138
|
+
const specs = await loadSpecs(root)
|
|
139
|
+
const childCount = new Map<string, number>()
|
|
140
|
+
for (const spec of specs) if (spec.parent) childCount.set(spec.parent, (childCount.get(spec.parent) ?? 0) + 1)
|
|
141
|
+
const findings: HealthFinding[] = []
|
|
142
|
+
for (const spec of specs) {
|
|
143
|
+
const evidence = altitudeEvidence(spec.body, altitude, ident)
|
|
144
|
+
if (evidence.length) findings.push({
|
|
145
|
+
check: 'altitude',
|
|
146
|
+
spec: spec.id,
|
|
147
|
+
summary: 'body reads like mechanics rather than a contract',
|
|
148
|
+
evidence,
|
|
149
|
+
repair: `rewrite '${spec.id}' around observable intent and invariants; the tidy workflow can perform the semantic review`,
|
|
150
|
+
})
|
|
151
|
+
const children = childCount.get(spec.id) ?? 0
|
|
152
|
+
if (children >= breadth.maxChildren) findings.push({
|
|
153
|
+
check: 'breadth',
|
|
154
|
+
spec: spec.id,
|
|
155
|
+
summary: 'tree fan-out may be missing a natural grouping layer',
|
|
156
|
+
evidence: [`${children} direct child nodes (>= ${breadth.maxChildren})`],
|
|
157
|
+
repair: `review '${spec.id}' with the regroup workflow; group only along a real seam and leave genuine peers flat`,
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
return findings
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function healthReport(findings: HealthFinding[], adopted: boolean): string[] {
|
|
164
|
+
const lines = ['Spec health diagnosis (opt-in advisory; never part of spex spec lint)']
|
|
165
|
+
if (!adopted) {
|
|
166
|
+
lines.push(' status : unavailable — adopt the repository with `spex init`')
|
|
167
|
+
return lines
|
|
168
|
+
}
|
|
169
|
+
const checks = ['altitude', 'breadth', ...new Set(findings.map((finding) => finding.check).filter((check) => check !== 'altitude' && check !== 'breadth'))]
|
|
170
|
+
for (const check of checks) {
|
|
171
|
+
const rows = findings.filter((finding) => finding.check === check)
|
|
172
|
+
lines.push(` ${check.padEnd(16)}: ${rows.length ? `${rows.length} finding(s)` : 'healthy'}`)
|
|
173
|
+
for (const finding of rows) {
|
|
174
|
+
lines.push(` ${finding.spec.padEnd(16)}: ${finding.summary}`)
|
|
175
|
+
for (const evidence of finding.evidence) lines.push(` evidence : ${evidence}`)
|
|
176
|
+
lines.push(` repair : ${finding.repair}`)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return lines
|
|
180
|
+
}
|
|
181
|
+
|
|
34
182
|
// @@@ contractText - the layer-2 payload: the composed `surface:system` bodies, the SAME join materialize()
|
|
35
183
|
// folds into each harness's contract file — so a BYOA agent reads byte-identical guidance.
|
|
36
184
|
function contractText(): { names: string[]; body: string } {
|
|
@@ -170,9 +318,9 @@ async function doubleDeliveryReport(base: string): Promise<{ lines: string[]; co
|
|
|
170
318
|
if (conflict) {
|
|
171
319
|
L.push('Repair — SpexCode is reaching this agent through MORE THAN ONE discovery channel. Keep exactly one:')
|
|
172
320
|
L.push(' • remove the independently-installed plugin bundle (delete its dir, or `claude plugin uninstall spexcode`); or')
|
|
173
|
-
L.push(' • if you WANT the plugin, stop the native delivery: set spexcode.json
|
|
174
|
-
L.push('
|
|
175
|
-
L.push(' • remove
|
|
321
|
+
L.push(' • if you WANT the plugin, stop the native delivery: set spexcode.json to')
|
|
322
|
+
L.push(' `"harnesses": [{"plugin":".claude"}]`, then run `spex materialize` to prune the loose shim/contract/skills; or')
|
|
323
|
+
L.push(' • remove SpexCode\'s generated delivery with `spex uninstall`.')
|
|
176
324
|
} else {
|
|
177
325
|
L.push('No double-delivery: each harness is reached by at most one spexcode-stamped channel.')
|
|
178
326
|
}
|
|
@@ -233,8 +381,14 @@ async function doctor(): Promise<number> {
|
|
|
233
381
|
})
|
|
234
382
|
line('issues workflow', `${issuesEnabled() ? 'ON' : 'OFF'} (spexcode.json issues.enabled)`)
|
|
235
383
|
if (legacy.length) line(' LEGACY key', `\`proposals\` found in ${legacy.map((f) => join(cfgHome, f)).join(', ')} — no longer read; rename it to "issues": { "enabled": … }`)
|
|
384
|
+
const projectConfig = readJsonConfig(join(base, 'spexcode.json'))
|
|
385
|
+
if (Object.prototype.hasOwnProperty.call(projectConfig.lint ?? {}, 'maxChildren'))
|
|
386
|
+
line('RETIRED key', '`lint.maxChildren` is no longer read — move the value to `doctor.breadth.maxChildren`')
|
|
236
387
|
}
|
|
237
388
|
|
|
389
|
+
const health = adopted ? await specHealthDiagnosis(base) : []
|
|
390
|
+
L.push('\n' + healthReport(health, adopted).join('\n'))
|
|
391
|
+
|
|
238
392
|
// --- preconditions: nothing downstream fires without these ---
|
|
239
393
|
L.push('\nPreconditions (without these nothing downstream fires)')
|
|
240
394
|
for (const bin of ['spex', 'claude', 'codex']) {
|
|
@@ -347,37 +501,23 @@ async function conflicts(): Promise<number> {
|
|
|
347
501
|
return conflict ? 1 : 0
|
|
348
502
|
}
|
|
349
503
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
// detect a missing managed session and degrade. So the diagnosis ships first; the installer lands behind it.
|
|
353
|
-
function noteStaged(verb: string): number {
|
|
354
|
-
console.error(`spex doctor ${verb} is not available yet — it is staged behind the hook-degradation prerequisite
|
|
355
|
-
(the live hooks must detect a missing managed session and degrade before they can be safely wired into your
|
|
356
|
-
own agent's config). Meanwhile: \`spex doctor\` reports your coverage, and \`spex doctor --contract\` prints
|
|
357
|
-
the workflow text you can hand any agent.`)
|
|
504
|
+
function migrationRemoved(): number {
|
|
505
|
+
console.error('spex: `spex doctor --migrate` was removed in v0.4.0 — migrate this tree with a 0.3.x SpexCode release, then reinstall the current release. Nothing was changed.')
|
|
358
506
|
return 2
|
|
359
507
|
}
|
|
360
508
|
|
|
361
509
|
function usage(): number {
|
|
362
|
-
console.error(`spex doctor — diagnose how the SpexCode workflow reaches your agent
|
|
363
|
-
(bare)
|
|
510
|
+
console.error(`spex doctor — diagnose spec health and how the SpexCode workflow reaches your agent
|
|
511
|
+
(bare) spec-health findings + delivery report: preconditions · git-hook floor · contract · hooks(+handlers) · backend · footprint
|
|
364
512
|
--contract print the surface:system contract text (hand it to any agent)
|
|
365
|
-
--conflicts detect double-delivery — the same agent reached via loose native delivery AND a plugin bundle (exits non-zero on conflict)
|
|
366
|
-
--migrate one-shot 0.2.x → 0.3.0 vocabulary migration for an adopter repo (renames + hook-asset
|
|
367
|
-
upgrade + body rewrites; refuses on a dirty tree / undrained sessions / an already-migrated
|
|
368
|
-
tree; stages everything, commits nothing; removed in 0.4.0)
|
|
369
|
-
install [staged] wire the materialized contract + hooks into your agent (--agent claude, --minimal)
|
|
370
|
-
uninstall [staged] reverse exactly what install wrote`)
|
|
513
|
+
--conflicts detect double-delivery — the same agent reached via loose native delivery AND a plugin bundle (exits non-zero on conflict)`)
|
|
371
514
|
return 0
|
|
372
515
|
}
|
|
373
516
|
|
|
374
517
|
export async function runDoctor(args: string[]): Promise<number> {
|
|
375
518
|
// contract/conflicts are FLAGS, not subcommands ([[cli-surface]] §4: another representation of the same
|
|
376
519
|
// diagnosis read, not a distinct action). The old positional spellings signpost — report, never run.
|
|
377
|
-
|
|
378
|
-
// ships with 0.3.0, deleted in 0.4.0) — dispatched first so its refusal/summary is never diluted
|
|
379
|
-
// by the diagnosis report.
|
|
380
|
-
if (args.includes('--migrate')) return await (await import('./migrate.js')).runMigrate()
|
|
520
|
+
if (args.includes('--migrate')) return migrationRemoved()
|
|
381
521
|
if (args.includes('--contract')) return contract()
|
|
382
522
|
if (args.includes('--conflicts')) return await conflicts()
|
|
383
523
|
switch (args[0]) {
|
|
@@ -385,8 +525,6 @@ export async function runDoctor(args: string[]): Promise<number> {
|
|
|
385
525
|
case 'contract': case 'conflicts':
|
|
386
526
|
console.error(`spex: \`spex doctor ${args[0]}\` was removed in v0.3.0 — use: spex doctor --${args[0]}`)
|
|
387
527
|
return 2
|
|
388
|
-
case 'install': return noteStaged('install')
|
|
389
|
-
case 'uninstall': return noteStaged('uninstall')
|
|
390
528
|
case 'help': case '--help': case '-h': return usage()
|
|
391
529
|
default: console.error(`spex doctor: unknown subcommand "${args[0]}"`); usage(); return 2
|
|
392
530
|
}
|