spexcode 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +37 -17
  2. package/package.json +9 -5
  3. package/spec-cli/README.md +23 -14
  4. package/spec-cli/hooks/dispatch.sh +7 -5
  5. package/spec-cli/hooks/harness.sh +19 -8
  6. package/spec-cli/src/anchors.ts +110 -18
  7. package/spec-cli/src/claude-headless.ts +271 -0
  8. package/spec-cli/src/cli.ts +112 -38
  9. package/spec-cli/src/client.ts +42 -11
  10. package/spec-cli/src/doctor.ts +174 -36
  11. package/spec-cli/src/gateway-auth.ts +194 -0
  12. package/spec-cli/src/gateway-hub.ts +360 -0
  13. package/spec-cli/src/gateway.ts +27 -19
  14. package/spec-cli/src/git.ts +12 -5
  15. package/spec-cli/src/graph.ts +54 -43
  16. package/spec-cli/src/graphStream.ts +265 -34
  17. package/spec-cli/src/guide.ts +115 -56
  18. package/spec-cli/src/harness-select.ts +20 -6
  19. package/spec-cli/src/harness.ts +295 -39
  20. package/spec-cli/src/help.ts +68 -28
  21. package/spec-cli/src/host.ts +610 -0
  22. package/spec-cli/src/identity-presets.d.ts +13 -0
  23. package/spec-cli/src/identity-presets.js +128 -0
  24. package/spec-cli/src/index.ts +158 -87
  25. package/spec-cli/src/init.ts +53 -25
  26. package/spec-cli/src/layout.ts +6 -5
  27. package/spec-cli/src/lint.ts +25 -121
  28. package/spec-cli/src/login-page.ts +12 -5
  29. package/spec-cli/src/materialize.ts +39 -13
  30. package/spec-cli/src/mentions.ts +16 -11
  31. package/spec-cli/src/message-stream.ts +147 -0
  32. package/spec-cli/src/opencode-headless.ts +95 -0
  33. package/spec-cli/src/opencode.ts +145 -0
  34. package/spec-cli/src/pi-harness.ts +124 -0
  35. package/spec-cli/src/pi-headless.ts +195 -0
  36. package/spec-cli/src/project-identity.ts +73 -0
  37. package/spec-cli/src/pty-bridge.ts +321 -521
  38. package/spec-cli/src/pty-helper.mjs +86 -0
  39. package/spec-cli/src/reaper.ts +16 -8
  40. package/spec-cli/src/reviewSnapshot.ts +23 -0
  41. package/spec-cli/src/reviews.ts +347 -0
  42. package/spec-cli/src/session-timeline.ts +14 -0
  43. package/spec-cli/src/sessions.ts +211 -75
  44. package/spec-cli/src/shim-runtime.ts +169 -0
  45. package/spec-cli/src/slash-commands.ts +76 -0
  46. package/spec-cli/src/source-files.ts +79 -0
  47. package/spec-cli/src/specs.ts +12 -7
  48. package/spec-cli/src/supervise.ts +37 -15
  49. package/spec-cli/src/tree.ts +8 -8
  50. package/spec-cli/src/uninstall.ts +56 -23
  51. package/spec-cli/templates/hooks/prepare-commit-msg +32 -18
  52. package/spec-cli/templates/spec/project/.plugins/commands/extract/spec.md +24 -18
  53. package/spec-cli/templates/spec/project/.plugins/commands/regroup/spec.md +3 -3
  54. package/spec-cli/templates/spec/project/.plugins/commands/rename/spec.md +11 -0
  55. package/spec-cli/templates/spec/project/.plugins/commands/spec.md +22 -8
  56. package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +1 -1
  57. package/spec-cli/templates/spec/project/.plugins/commands/tidy/spec.md +29 -11
  58. package/spec-cli/templates/spec/project/.plugins/core/idle/spec.md +1 -1
  59. package/spec-cli/templates/spec/project/.plugins/core/mark-active/spec.md +2 -0
  60. package/spec-cli/templates/spec/project/.plugins/core/session-fail/spec.md +1 -1
  61. package/spec-cli/templates/spec/project/.plugins/core/spec-first/spec-first.sh +28 -39
  62. package/spec-cli/templates/spec/project/.plugins/core/spec-first/spec.md +5 -3
  63. package/spec-cli/templates/spec/project/.plugins/core/spec-of-file/spec.md +1 -1
  64. package/spec-cli/templates/spec/project/.plugins/core/spec.md +2 -2
  65. package/spec-cli/templates/spec/project/.plugins/core/stop-gate/spec.md +7 -1
  66. package/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +2 -2
  67. package/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +5 -5
  68. package/spec-cli/templates/spec/project/.plugins/prompts/spec.md +10 -6
  69. package/spec-cli/templates/spec/project/.plugins/skills/distill/spec.md +38 -50
  70. package/spec-cli/templates/spec/project/.plugins/skills/spec.md +9 -7
  71. package/spec-cli/templates/spec/project/.plugins/spec.md +17 -12
  72. package/spec-cli/templates/spec/project/spec.md +6 -5
  73. package/spec-cli/templates/spexcode.json +10 -2
  74. package/spec-dashboard/dist/assets/Dashboard-C_w_wdk5.js +27 -0
  75. package/spec-dashboard/dist/assets/EvalsPage-5_nfIYll.js +2 -0
  76. package/spec-dashboard/dist/assets/IssuesPage-By-u--95.js +1 -0
  77. package/spec-dashboard/dist/assets/MobileApp-CVEwjHr9.js +2 -0
  78. package/spec-dashboard/dist/assets/Modal-BqgvzMJD.js +1 -0
  79. package/spec-dashboard/dist/assets/PageScroll-B_dKCuXx.js +1 -0
  80. package/spec-dashboard/dist/assets/ProjectsPage-RVP8AqK4.js +1 -0
  81. package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +32 -0
  82. package/spec-dashboard/dist/assets/SessionInterface-Bh3vq8SU.js +39 -0
  83. package/spec-dashboard/dist/assets/SessionWindow-BuJ5mzjC.js +13 -0
  84. package/spec-dashboard/dist/assets/Settings-B8KFocsz.js +1 -0
  85. package/spec-dashboard/dist/assets/TimelineChat-K0wdlweB.js +1 -0
  86. package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +1 -0
  87. package/spec-dashboard/dist/assets/index-BKaTHjmU.js +41 -0
  88. package/spec-dashboard/dist/assets/index-DcnCaBAC.css +1 -0
  89. package/spec-dashboard/dist/assets/terminalFont-DmTPNdbU.js +1 -0
  90. package/spec-dashboard/dist/index.html +10 -10
  91. package/spec-dashboard/src/reviewFilters.js +279 -0
  92. package/spec-dashboard/src/reviewQuery.js +174 -0
  93. package/spec-dashboard/src/session.js +167 -0
  94. package/spec-eval/src/cli.ts +49 -28
  95. package/spec-eval/src/humanok.ts +1 -1
  96. package/spec-eval/src/matrix.ts +693 -0
  97. package/spec-eval/src/sessioneval.ts +726 -66
  98. package/spec-cli/src/migrate-table.ts +0 -408
  99. package/spec-cli/src/migrate.ts +0 -386
  100. package/spec-dashboard/dist/assets/Dashboard-C_fGmOKK.js +0 -27
  101. package/spec-dashboard/dist/assets/EvalsPage-Cnr1s3bq.js +0 -2
  102. package/spec-dashboard/dist/assets/FoldToggle-x9gtO1OQ.js +0 -1
  103. package/spec-dashboard/dist/assets/IssuesPage-5f_vL-JV.js +0 -1
  104. package/spec-dashboard/dist/assets/MobileApp-DEO1jgGM.js +0 -1
  105. package/spec-dashboard/dist/assets/SessionInterface-CAlbMOFR.js +0 -66
  106. package/spec-dashboard/dist/assets/SessionInterface-DYP7pi_n.css +0 -32
  107. package/spec-dashboard/dist/assets/SessionWindow-JYbpPwNB.js +0 -13
  108. package/spec-dashboard/dist/assets/Settings-DKb5Ji_X.js +0 -1
  109. package/spec-dashboard/dist/assets/index-BQu-oJ8J.js +0 -41
  110. package/spec-dashboard/dist/assets/index-BbMkwuix.css +0 -1
  111. package/spec-dashboard/dist/assets/launch-BM9GgvkX.js +0 -6
@@ -1,386 +0,0 @@
1
- // @@@ spex doctor --migrate - the ONE-SHOT 0.2.x → 0.3.0 migrator for an ADOPTER repo ([[migrate]]).
2
- // Ships with 0.3.0, deleted in 0.4.0 (a released, named, term-limited migrator — not a runtime residue).
3
- // It performs the v0.3.0 vocabulary migration in one run: tree renames (.config → .plugins, config →
4
- // plugin-system, yatsu.md → eval.md, yatsu.evals.ndjson → evals.ndjson), template hook-asset upgrades,
5
- // body vocabulary rewrites (only what is EXECUTED or PARSED — command spellings, lint labels, mentions,
6
- // routes, .spec paths, issue-frontmatter bindings — never conceptual prose), and the legacy settings key.
7
- //
8
- // Iron rules, enforced structurally:
9
- // • NO GUESSING: a hook asset is replaced only when its git blob sha matches a KNOWN historical stock
10
- // template version (migrate-table.ts, generated from this repo's own git history). Anything else is
11
- // flagged for human review and left byte-identical.
12
- // • FAIL LOUD, NEVER HALF-MIGRATE: every precondition (clean tree, drained sessions, tracked .spec,
13
- // main checkout, no rename collisions) is checked BEFORE the first write; any failure prints ALL
14
- // refusals and exits 2 with the tree untouched.
15
- // • EVERYTHING STAGED, NOTHING COMMITTED: the operator reviews the staged diff + flagged items and
16
- // commits through the ritual; `git reset --hard` undoes the whole run.
17
- import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, renameSync } from 'node:fs'
18
- import { join, relative, dirname, basename } from 'node:path'
19
- import { fileURLToPath } from 'node:url'
20
- import { execFileSync } from 'node:child_process'
21
- import { TEMPLATE_ASSETS, RETIRED_ASSETS, CONFIG_NODE_SHAS } from './migrate-table.js'
22
-
23
- const PKG_ROOT = fileURLToPath(new URL('..', import.meta.url))
24
-
25
- // loud git: any failure throws with git's own stderr. GIT_DIR/GIT_INDEX_FILE are stripped (same reason as
26
- // git.ts's git(): an inherited hook env would silently point repo discovery at the wrong place).
27
- function git(cwd: string, args: string[]): string {
28
- const env = { ...process.env }
29
- delete env.GIT_DIR; delete env.GIT_INDEX_FILE; delete env.GIT_WORK_TREE
30
- return execFileSync('git', ['-C', cwd, ...args], { encoding: 'utf8', env, maxBuffer: 64 * 1024 * 1024 }).trim()
31
- }
32
- function tryGit(cwd: string, args: string[]): string | null {
33
- try { return git(cwd, args) } catch { return null }
34
- }
35
-
36
- function walk(dir: string): string[] {
37
- let out: string[] = []
38
- for (const e of readdirSync(dir, { withFileTypes: true })) {
39
- const p = join(dir, e.name)
40
- if (e.isDirectory()) out = out.concat(walk(p))
41
- else if (e.isFile()) out.push(p)
42
- }
43
- return out
44
- }
45
-
46
- // ---------- the vocabulary tables (§3.1/§3.2 of the v0.3.0 plan — old spelling → its ONE new home) ----------
47
- // Only surfaces that are executed or parsed. Ordered: longer/more-specific rules run before their prefixes.
48
- const COMMAND_REWRITES: [RegExp, string][] = [
49
- // eval domain (né yatsu)
50
- [/\bspex yatsu check-staged\b/g, 'spex internal check-staged'],
51
- [/\bspex yatsu eval\b/g, 'spex eval add'],
52
- [/\bspex yatsu show\b/g, 'spex eval ls'],
53
- [/\bspex yatsu scan\b/g, 'spex eval lint'],
54
- [/\bspex yatsu retract\b/g, 'spex eval retract'],
55
- [/\bspex yatsu clean\b/g, 'spex eval clean'],
56
- [/\bspex guide yatsu\b/g, 'spex guide eval'],
57
- [/\bspex guide config\b/g, 'spex guide settings'],
58
- [/\bspex blob put\b/g, 'spex evidence put'],
59
- [/\bspex blob get\b/g, 'spex evidence get'],
60
- // issue domain (né issues/forge drawers)
61
- [/\bspex issues nudge\b/g, 'spex internal nudge'],
62
- [/\bspex issues open\b/g, 'spex issue open'],
63
- [/\bspex issues reply\b/g, 'spex issue reply'],
64
- [/\bspex issues close\b/g, 'spex issue close'],
65
- [/\bspex issues promote\b/g, 'spex issue promote'],
66
- [/\bspex issues ls\b/g, 'spex issue ls'],
67
- [/\bspex issues\b(?!\s+(?:on|off|status)\b)/g, 'spex issue ls'], // bare `spex issues` listed; on|off|status are DELETED → flagged below
68
- [/\bspex forge links\b/g, 'spex issue links'],
69
- [/\bspex forge eval-pending\b/g, 'spex issue links --pending'],
70
- // remark domain (né bare remark/resolve/retract)
71
- [/\bspex resolve\b/g, 'spex remark resolve'],
72
- [/\bspex retract\b/g, 'spex remark retract'],
73
- [/\bspex remark\b(?!\s+(?:add|resolve|retract)\b)/g, 'spex remark add'],
74
- // session drawer: renamed subs first, then the retired promoted spellings
75
- [/\bspex session reopen\b/g, 'spex session resume'],
76
- [/\bspex session exit\b/g, 'spex session stop'],
77
- [/\bspex session rawkey\b/g, 'spex session send --keys'],
78
- [/\bspex session capture\b/g, 'spex session show --capture'],
79
- [/\bspex session prompt\b/g, 'spex session show'],
80
- [/\bspex session state\b/g, 'spex internal session-state'],
81
- [/\bspex session fail\b/g, 'spex internal session-fail'],
82
- [/\bspex session idle\b/g, 'spex internal session-idle'],
83
- [/\bspex session commit-gate\b/g, 'spex internal commit-gate'],
84
- [/\bspex reopen\b/g, 'spex session resume'],
85
- [/\bspex exit\b/g, 'spex session stop'],
86
- [/\bspex rawkey\b/g, 'spex session send --keys'],
87
- [/\bspex capture\b/g, 'spex session show --capture'],
88
- [/\bspex prompt\b/g, 'spex session show'],
89
- [/\bspex new\b/g, 'spex session new'],
90
- [/\bspex ls\b/g, 'spex session ls'],
91
- [/\bspex watch\b/g, 'spex session watch'],
92
- [/\bspex wait\b/g, 'spex session wait'],
93
- [/\bspex review\b(?!\s+proof\b)/g, 'spex session review'], // `spex review proof` is DELETED → flagged below
94
- [/\bspex merge\b/g, 'spex session merge'],
95
- [/\bspex done\b/g, 'spex session done'],
96
- [/\bspex park\b/g, 'spex session park'],
97
- [/\bspex ask\b/g, 'spex session ask'],
98
- [/\bspex send\b/g, 'spex session send'],
99
- [/\bspex close\b/g, 'spex session close'],
100
- [/\bspex attach\b/g, 'spex session attach'],
101
- [/\bspex rename\b/g, 'spex session rename'],
102
- // spec drawer (né promoted verbs)
103
- [/\bspex search\b/g, 'spex spec search'],
104
- [/\bspex owner\b/g, 'spex spec owner'],
105
- [/\bspex lint\b/g, 'spex spec lint'],
106
- [/\bspex ack\b/g, 'spex spec ack'],
107
- // project verbs
108
- [/\bspex board\b/g, 'spex graph --json'],
109
- [/\bspex tree\b/g, 'spex graph'],
110
- [/\bspex dashboard\b/g, 'spex serve ui'],
111
- [/\bspex self\b/g, 'spex doctor'],
112
- [/\bspex doctor contract\b/g, 'spex doctor --contract'],
113
- [/\bspex doctor conflicts\b/g, 'spex doctor --conflicts'],
114
- ]
115
- const ROUTE_REWRITES: [RegExp, string][] = [
116
- [/\/api\/board\/stream\b/g, '/api/graph/stream'],
117
- [/\/api\/board\b/g, '/api/graph'],
118
- [/\/api\/sessions\/graph\b/g, '/api/sessions/edges'],
119
- [/\/api\/yatsu\/blob\b/g, '/api/evidence'],
120
- [/\/yatsu\/eval\b/g, '/evals'],
121
- [/\/api\/config\b/g, '/api/plugins'],
122
- [/\/api\/layout\b/g, '/api/settings'],
123
- [/\/api\/launchers\b/g, '/api/settings'],
124
- ]
125
- const LABEL_REWRITES: [RegExp, string][] = [
126
- [/\byatsu-uncovered:/g, 'eval-coverage:'],
127
- [/\byatsu-(schema|missing|drift|dangling|owners):/g, 'eval-$1:'],
128
- ]
129
- const FILE_REWRITES: [RegExp, string][] = [
130
- [/\byatsu\.evals\.ndjson\b/g, 'evals.ndjson'], // also rewrites the `.yatsu.evals.ndjson` suffix form
131
- [/\byatsu\.md\b/g, 'eval.md'],
132
- ]
133
- // dead spellings that survive every rewrite rule (no deterministic new home) → review items, never guessed at.
134
- const MD_FLAG_PATTERNS: [RegExp, string][] = [
135
- [/\bspex yatsu\b/, 'unmapped `spex yatsu …` spelling'], // dead-words-ok: the migrator names the old vocabulary it migrates from
136
- [/\byatsu (scan|eval|show|retract|clean)\b/, 'un-prefixed yatsu verb (script-style invocation)'], // dead-words-ok: the migrator names the old vocabulary it migrates from
137
- [/\bspex forge\b/, 'unmapped `spex forge …` spelling (drawer dissolved into `spex issue links`)'],
138
- [/\bspex issues (on|off|status)\b/, '`spex issues on|off|status` was deleted — the switch is the spexcode.json `issues.enabled` key'],
139
- [/\bspex review proof\b/, '`spex review proof` was deleted — use `spex eval ls --session <SEL> --export`'], // dead-words-ok: the migrator names the old vocabulary it migrates from
140
- [/\bspex eval\b(?!\s+(?:add|ls|scenario|lint|retract|clean)\b|\s+--|\s*[.,;:)`'"]|\s*$)/m, 'old top-level `spex eval <SEL>` session read — now `spex eval ls --session <SEL>`'],
141
- [/\byatsu-[a-z]+:/, 'unmapped yatsu-* lint label'], // dead-words-ok: the migrator names the old vocabulary it migrates from
142
- [/\byatsu\.md\b/, 'a yatsu.md reference survived rewriting'], // dead-words-ok: the migrator names the old vocabulary it migrates from
143
- ]
144
- // executable patterns that make an UNKNOWN (non-template) script a review item — scripts are never rewritten.
145
- const SCRIPT_FLAG_PATTERNS: [RegExp, string][] = [
146
- [/\byatsu\b/, 'invokes/greps the yatsu vocabulary'], // dead-words-ok: the migrator names the old vocabulary it migrates from
147
- [/\bsession (state|fail|idle|commit-gate)\b/, 'calls a hook verb that moved to `spex internal …`'],
148
- [/\bblob (put|get)\b/, 'calls the blob verbs (now `spex evidence put|get`)'], // dead-words-ok: the migrator names the old vocabulary it migrates from
149
- [/\bspex (board|tree|dashboard|search|owner|lint|ack|new|ls|watch|wait|review|merge)\b/, 'calls a retired top-level spelling'],
150
- [/\/api\/(board|config|layout|launchers|yatsu)\b/, 'hits a renamed API route'],
151
- ]
152
-
153
- type Flag = { file: string; line: number | null; reason: string }
154
-
155
- // ---------- preconditions: check EVERYTHING before the first write ----------
156
- type World = {
157
- root: string
158
- specRoots: string[] // .spec/<root> dirs that are spec roots (have spec.md)
159
- configRoots: string[] // spec roots carrying an old-world .config
160
- yatsuFiles: string[] // absolute paths of yatsu.md / *yatsu.evals.ndjson under .spec
161
- configNode: string | null // .spec/<root>/config dir to rename, iff hash-gated as spexcode's plugin-system spec
162
- configNodeFlag: Flag | null // a node named `config` that is NOT stock → flagged, not renamed
163
- }
164
-
165
- // inspectAsync (bottom of file) gathers the world and appends every refusal — ALL preconditions are
166
- // evaluated before runMigrate performs its first write.
167
-
168
- export async function runMigrate(): Promise<number> {
169
- const refusals: string[] = []
170
- let world: World | null = null
171
- try { world = await inspectAsync(process.cwd(), refusals) } catch (e) { refusals.push((e as Error).message) }
172
- if (refusals.length || !world) {
173
- console.error('spex doctor --migrate: REFUSED — nothing was changed. Fix these and re-run:')
174
- for (const r of refusals) console.error(` ✗ ${r}`)
175
- return 2
176
- }
177
- const { root } = world
178
- const rel = (f: string) => relative(root, f)
179
- const flags: Flag[] = world.configNodeFlag ? [world.configNodeFlag] : []
180
- const summary: string[] = []
181
- const renamedNodes: [string, string][] = [] // node-id renames actually performed → drives mention re-pointing
182
-
183
- console.log('spex doctor --migrate — one-shot 0.2.x → 0.3.0 migration (everything staged, nothing committed)\n')
184
-
185
- // ---------- 1. tree renames (git mv — history-preserving) ----------
186
- for (const dir of world.configRoots) {
187
- git(root, ['mv', join(dir, '.config'), join(dir, '.plugins')])
188
- summary.push(`renamed ${rel(dir)}/.config → .plugins (git mv)`)
189
- }
190
- renamedNodes.push(['.config', '.plugins'])
191
- if (world.configNode) {
192
- git(root, ['mv', world.configNode, join(dirname(world.configNode), 'plugin-system')])
193
- renamedNodes.push(['config', 'plugin-system'])
194
- summary.push(`renamed node ${rel(world.configNode)} → plugin-system (git mv; stock plugin-system spec, hash-verified)`)
195
- }
196
- // yatsu file renames — re-walk (paths moved under .plugins)
197
- const yatsuNow = walk(join(root, '.spec')).filter((f) => basename(f) === 'yatsu.md' || basename(f).endsWith('yatsu.evals.ndjson')) // dead-words-ok: the migrator names the old vocabulary it migrates from
198
- for (const f of yatsuNow) {
199
- const target = basename(f) === 'yatsu.md' ? join(dirname(f), 'eval.md') : join(dirname(f), basename(f).replace(/yatsu\.evals\.ndjson$/, 'evals.ndjson')) // dead-words-ok: the migrator names the old vocabulary it migrates from
200
- git(root, ['mv', f, target])
201
- }
202
- if (yatsuNow.length) summary.push(`renamed ${yatsuNow.length} measurement file(s): yatsu.md → eval.md, *yatsu.evals.ndjson → *evals.ndjson (git mv)`) // dead-words-ok: the migrator names the old vocabulary it migrates from
203
-
204
- // ---------- 2. template hook-asset upgrade (exact-match replace or flag — NEVER a silent rewrite) ----------
205
- let replaced = 0, current = 0, retiredStock = 0
206
- const touched = new Set<string>() // files replaced (skip vocab) …
207
- const frozen = new Set<string>() // … and files flagged (must stay byte-identical)
208
- for (const dir of world.configRoots) {
209
- const plugRoot = join(dir, '.plugins')
210
- for (const f of walk(plugRoot)) {
211
- const r = relative(plugRoot, f)
212
- const asset = TEMPLATE_ASSETS.find((a) => a.rel === r)
213
- if (asset) {
214
- const newContent = readFileSync(join(PKG_ROOT, asset.template), 'utf8')
215
- if (readFileSync(f, 'utf8') === newContent) { current++; continue }
216
- const sha = git(root, ['hash-object', f])
217
- if (asset.oldShas.includes(sha)) {
218
- writeFileSync(f, newContent); git(root, ['add', f]); replaced++; touched.add(f)
219
- } else {
220
- flags.push({ file: rel(f), line: null, reason: `differs from EVERY known stock template version (hand-customized?) — left untouched; port your customization onto the new template by hand (shipped at ${join('spec-cli', asset.template)})` })
221
- frozen.add(f)
222
- }
223
- } else {
224
- // not shipped any more: a stock copy of a retired template is reported; an unknown script with
225
- // executable old vocabulary is flagged. Unknown .md bodies fall through to the vocabulary pass.
226
- const oldNames = [r, r.replace(/eval\.md$/, 'yatsu.md'), r.replace(/evals\.ndjson$/, 'yatsu.evals.ndjson')] // dead-words-ok: the migrator names the old vocabulary it migrates from
227
- const retiredKey = oldNames.find((n) => RETIRED_ASSETS[n])
228
- if (retiredKey && RETIRED_ASSETS[retiredKey].includes(git(root, ['hash-object', f]))) {
229
- retiredStock++
230
- flags.push({ file: rel(f), line: null, reason: `stock copy of a template the current release no longer ships (${retiredKey}) — kept; review whether to keep or delete it` })
231
- // hash-verified STOCK content: a retired .md still gets the vocabulary pass (we know every byte
232
- // is ours, so rewriting its executable surfaces is not a guess); a retired SCRIPT has no shipped
233
- // successor and no rewrite table, so it stays frozen byte-identical.
234
- if (!f.endsWith('.md')) frozen.add(f)
235
- } else if (!f.endsWith('.md')) {
236
- const text = readFileSync(f, 'utf8')
237
- for (const [re, why] of SCRIPT_FLAG_PATTERNS) {
238
- const i = text.split('\n').findIndex((l) => re.test(l))
239
- if (i >= 0) { flags.push({ file: rel(f), line: i + 1, reason: `unknown script ${why} — scripts are never auto-rewritten; update it by hand` }); frozen.add(f); break }
240
- }
241
- }
242
- }
243
- }
244
- }
245
- summary.push(`template assets: ${replaced} upgraded (stock, hash-verified), ${current} already current, ${retiredStock} retired-stock, ${flags.length} flagged for review`)
246
-
247
- // ---------- 3. body vocabulary pass (every .spec .md not replaced/frozen above) ----------
248
- let rewrote = 0
249
- const mentionRules: [RegExp, string][] = renamedNodes.map(([o, n]) => [new RegExp(`\\[\\[${o.replace('.', '\\.')}\\]\\]`, 'g'), `[[${n}]]`])
250
- for (const f of walk(join(root, '.spec'))) {
251
- if (!f.endsWith('.md') || touched.has(f) || frozen.has(f)) continue
252
- const before = readFileSync(f, 'utf8')
253
- let text = before
254
- for (const [re, to] of [...FILE_REWRITES, ...LABEL_REWRITES, ...COMMAND_REWRITES, ...ROUTE_REWRITES, ...mentionRules]) text = text.replace(re, to)
255
- // .spec-internal path strings (code: frontmatter, prose paths): /.config/ → /.plugins/ on lines that name .spec
256
- text = text.split('\n').map((l) => l.includes('.spec/') ? l.replaceAll('/.config/', '/.plugins/') : l).join('\n')
257
- // issue-thread frontmatter `nodes:` bindings follow the performed node renames
258
- if (renamedNodes.some(([o]) => o === 'config'))
259
- text = text.replace(/^(nodes:.*)$/m, (line) => line.replace(/\bconfig\b/g, 'plugin-system'))
260
- if (text !== before) { writeFileSync(f, text); git(root, ['add', f]); rewrote++ }
261
- // whatever old vocabulary SURVIVES the rewrite has no deterministic home → review, never guess
262
- const lines = text.split('\n')
263
- for (const [re, why] of MD_FLAG_PATTERNS) {
264
- const i = lines.findIndex((l) => re.test(l))
265
- if (i >= 0) flags.push({ file: rel(f), line: i + 1, reason: why })
266
- }
267
- }
268
- summary.push(`bodies: ${rewrote} .md file(s) rewritten (command spellings · lint labels · file/route names · [[mention]] re-pointing · .spec paths)`)
269
-
270
- // ---------- 4. legacy settings key: proposals.enabled → issues.enabled ----------
271
- for (const name of ['spexcode.json', 'spexcode.local.json']) {
272
- const p = join(root, name)
273
- if (!existsSync(p)) continue
274
- let cfg: Record<string, unknown>
275
- try { cfg = JSON.parse(readFileSync(p, 'utf8')) } catch { flags.push({ file: name, line: null, reason: 'unparseable JSON — legacy `proposals` key (if any) not migrated' }); continue }
276
- if (!('proposals' in cfg)) continue
277
- const prop = cfg.proposals as { enabled?: unknown }
278
- const issues = (cfg.issues ?? {}) as Record<string, unknown>
279
- if (typeof prop?.enabled === 'boolean' && !('enabled' in issues)) cfg.issues = { ...issues, enabled: prop.enabled }
280
- delete cfg.proposals
281
- writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n')
282
- if (tryGit(root, ['ls-files', '--error-unmatch', name])) git(root, ['add', p])
283
- summary.push(`${name}: legacy \`proposals\` key rewritten to \`issues.enabled\` (v0.3.0 reads only the new key)`)
284
- }
285
-
286
- // ---------- 5. per-clone evidence cache: yatsu-blobs → evidence (lossless dir rename) ----------
287
- const commonDir = git(root, ['rev-parse', '--path-format=absolute', '--git-common-dir'])
288
- const oldCache = join(commonDir, 'spexcode', 'yatsu-blobs'), newCache = join(commonDir, 'spexcode', 'evidence') // dead-words-ok: the migrator names the old vocabulary it migrates from
289
- if (existsSync(oldCache) && !existsSync(newCache)) { renameSync(oldCache, newCache); summary.push('evidence cache: .git/spexcode/yatsu-blobs → evidence (bytes preserved)') } // dead-words-ok: the migrator names the old vocabulary it migrates from
290
-
291
- // ---------- 6. post-checks: the new CLI must actually work on the migrated tree ----------
292
- console.log('— migration applied; running post-checks —\n')
293
- const post: string[] = []
294
- try {
295
- const { specLint } = await import('./lint.js')
296
- const findings = await specLint()
297
- const errs = findings.filter((x) => x.level === 'error')
298
- for (const x of findings) console.error(` ${x.level === 'error' ? '✗' : '•'} ${x.rule}: ${x.msg}`)
299
- post.push(`spec lint: ${errs.length} error(s), ${findings.length - errs.length} warning(s)${errs.length ? ' — FIX BEFORE COMMITTING' : ''}`)
300
- } catch (e) { post.push(`spec lint FAILED to run: ${(e as Error).message}`) }
301
- try {
302
- const { runEval } = await import('../../spec-eval/src/cli.js')
303
- await runEval(['lint'])
304
- post.push('eval lint: ran (advisory — findings above, if any)')
305
- } catch (e) { post.push(`eval lint FAILED to run: ${(e as Error).message}`) }
306
- try {
307
- const { materialize } = await import('./materialize.js')
308
- post.push(`materialize: ok (content-hash ${materialize(root)}) — hook manifest + contract now speak .plugins`)
309
- } catch (e) { post.push(`materialize FAILED: ${(e as Error).message}`) }
310
-
311
- // ---------- summary ----------
312
- console.log('\n===== migration summary =====')
313
- for (const s of summary) console.log(` ✓ ${s}`)
314
- for (const s of post) console.log(` · ${s}`)
315
- if (flags.length) {
316
- console.log(`\n NEEDS REVIEW (${flags.length}) — reported, deliberately NOT rewritten:`)
317
- for (const fl of flags) console.log(` ! ${fl.file}${fl.line ? `:${fl.line}` : ''} — ${fl.reason}`)
318
- }
319
- console.log(`\n Everything is STAGED, nothing committed. Undo entirely with: git reset --hard
320
- Next steps:
321
- 1. review the staged diff (git diff --cached) and every NEEDS REVIEW item above
322
- 2. commit through the ritual (on a trunk this is topology surgery: SPEXCODE_ALLOW_MAIN=1 git commit)
323
- 3. npm run hooks — in EVERY clone of this repo (git hooks are per-clone copies)
324
- 4. full backend restart (the supervisor env must be respawned), then rebuild the dashboard dist
325
- 5. if a node was renamed above, retarget OPEN forge issues' \`Spec:\` lines via your forge (closed ones are archive — leave them)`)
326
- return 0
327
- }
328
-
329
- // the precondition gatherer — every check appends a refusal; the caller only proceeds on an empty list.
330
- async function inspectAsync(cwd: string, refusals: string[]): Promise<World | null> {
331
- const root = tryGit(cwd, ['rev-parse', '--show-toplevel'])
332
- if (!root) { refusals.push('not inside a git repository.'); return null }
333
- const gitDir = git(root, ['rev-parse', '--path-format=absolute', '--git-dir'])
334
- const commonDir = git(root, ['rev-parse', '--path-format=absolute', '--git-common-dir'])
335
- if (gitDir !== commonDir) refusals.push(`this is a linked worktree (${root}) — run the migration from the main checkout.`)
336
- const specDir = join(root, '.spec')
337
- if (!existsSync(specDir)) { refusals.push(`no .spec/ at ${root} — this repo has not adopted SpexCode; nothing to migrate.`); return null }
338
-
339
- const specRoots: string[] = [], configRoots: string[] = [], pluginRoots: string[] = []
340
- for (const e of readdirSync(specDir, { withFileTypes: true })) {
341
- if (!e.isDirectory() || e.name === '.issues') continue
342
- const dir = join(specDir, e.name)
343
- if (!existsSync(join(dir, 'spec.md'))) continue
344
- specRoots.push(dir)
345
- if (existsSync(join(dir, '.config'))) configRoots.push(dir)
346
- if (existsSync(join(dir, '.plugins'))) pluginRoots.push(dir)
347
- }
348
- for (const p of pluginRoots) refusals.push(`${relative(root, p)}/.plugins already exists — this tree is already migrated${configRoots.length ? ' (and a .config root ALSO exists — half-migrated state, resolve by hand)' : ''}.`)
349
-
350
- const yatsuFiles = walk(specDir).filter((f) => basename(f) === 'yatsu.md' || basename(f).endsWith('yatsu.evals.ndjson')) // dead-words-ok: the migrator names the old vocabulary it migrates from
351
- if (!configRoots.length && !yatsuFiles.length && !pluginRoots.length)
352
- refusals.push('nothing to migrate — no .config plugin root and no yatsu.md/yatsu.evals.ndjson files under .spec/ (this tree already speaks 0.3.0, or never had the 0.2.x vocabulary).') // dead-words-ok: the migrator names the old vocabulary it migrates from
353
-
354
- if (!git(root, ['ls-files', '.spec']).length)
355
- refusals.push('.spec is not tracked by git (an old `/.spec` exclude line?) — remove the exclude, `git add .spec` and commit it first, then re-run.')
356
-
357
- const dirty = git(root, ['status', '--porcelain']).split('\n').filter((l) => l && !l.startsWith('??'))
358
- if (dirty.length) refusals.push(`working tree not clean — commit or stash first:\n${dirty.slice(0, 10).map((l) => ' ' + l).join('\n')}${dirty.length > 10 ? `\n … +${dirty.length - 10} more` : ''}`)
359
-
360
- try {
361
- const { listSessionIds, readRawRecord } = await import('./layout.js')
362
- const ids = listSessionIds()
363
- if (ids.length) {
364
- const rows = ids.map((id) => { const r = readRawRecord(id); return ` ${id} (${r?.status ?? 'unreadable'})` })
365
- refusals.push(`${ids.length} session(s) still exist for this project — drain them first (merge, then \`spex session close <id>\`; a worktree branched pre-migration would conflict):\n${rows.join('\n')}`)
366
- }
367
- } catch (e) { refusals.push(`could not enumerate this project's sessions (${(e as Error).message}) — refusing to migrate blind.`) }
368
-
369
- for (const dir of configRoots) if (existsSync(join(dir, '.plugins'))) refusals.push(`${relative(root, dir)}/.plugins already exists beside .config — resolve by hand.`)
370
- for (const f of yatsuFiles) {
371
- const target = basename(f) === 'yatsu.md' ? join(dirname(f), 'eval.md') : join(dirname(f), basename(f).replace(/yatsu\.evals\.ndjson$/, 'evals.ndjson')) // dead-words-ok: the migrator names the old vocabulary it migrates from
372
- if (existsSync(target)) refusals.push(`rename collision: ${relative(root, target)} already exists beside ${relative(root, f)}.`)
373
- }
374
-
375
- let configNode: string | null = null, configNodeFlag: Flag | null = null
376
- for (const dir of specRoots) {
377
- const cand = join(dir, 'config')
378
- if (!existsSync(join(cand, 'spec.md'))) continue
379
- if (existsSync(join(dir, 'plugin-system'))) { refusals.push(`${relative(root, dir)}/plugin-system already exists beside config/ — resolve by hand.`); continue }
380
- const sha = git(root, ['hash-object', join(cand, 'spec.md')])
381
- if (CONFIG_NODE_SHAS.includes(sha)) configNode = cand
382
- else configNodeFlag = { file: relative(root, join(cand, 'spec.md')), line: null, reason: 'a node named `config` exists but its spec.md matches no stock version of spexcode\'s plugin-system spec — left untouched (rename it by hand ONLY if it really is the plugin-system spec)' }
383
- }
384
-
385
- return { root, specRoots, configRoots, yatsuFiles, configNode, configNodeFlag }
386
- }