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.
Files changed (106) 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/cli.ts +88 -37
  8. package/spec-cli/src/client.ts +34 -11
  9. package/spec-cli/src/doctor.ts +174 -36
  10. package/spec-cli/src/gateway-auth.ts +194 -0
  11. package/spec-cli/src/gateway-hub.ts +360 -0
  12. package/spec-cli/src/gateway.ts +27 -19
  13. package/spec-cli/src/git.ts +12 -5
  14. package/spec-cli/src/graph.ts +54 -43
  15. package/spec-cli/src/graphStream.ts +265 -34
  16. package/spec-cli/src/guide.ts +105 -55
  17. package/spec-cli/src/harness-select.ts +20 -6
  18. package/spec-cli/src/harness.ts +181 -33
  19. package/spec-cli/src/help.ts +64 -27
  20. package/spec-cli/src/host.ts +610 -0
  21. package/spec-cli/src/identity-presets.d.ts +13 -0
  22. package/spec-cli/src/identity-presets.js +128 -0
  23. package/spec-cli/src/index.ts +145 -86
  24. package/spec-cli/src/init.ts +53 -25
  25. package/spec-cli/src/layout.ts +5 -5
  26. package/spec-cli/src/lint.ts +25 -121
  27. package/spec-cli/src/login-page.ts +12 -5
  28. package/spec-cli/src/materialize.ts +39 -13
  29. package/spec-cli/src/mentions.ts +16 -11
  30. package/spec-cli/src/opencode.ts +145 -0
  31. package/spec-cli/src/pi-harness.ts +124 -0
  32. package/spec-cli/src/project-identity.ts +73 -0
  33. package/spec-cli/src/pty-bridge.ts +321 -521
  34. package/spec-cli/src/pty-helper.mjs +86 -0
  35. package/spec-cli/src/reaper.ts +16 -8
  36. package/spec-cli/src/reviewSnapshot.ts +23 -0
  37. package/spec-cli/src/reviews.ts +347 -0
  38. package/spec-cli/src/session-timeline.ts +14 -0
  39. package/spec-cli/src/sessions.ts +188 -66
  40. package/spec-cli/src/shim-runtime.ts +169 -0
  41. package/spec-cli/src/slash-commands.ts +76 -0
  42. package/spec-cli/src/source-files.ts +79 -0
  43. package/spec-cli/src/specs.ts +12 -7
  44. package/spec-cli/src/supervise.ts +37 -15
  45. package/spec-cli/src/tree.ts +8 -8
  46. package/spec-cli/src/uninstall.ts +56 -23
  47. package/spec-cli/templates/hooks/prepare-commit-msg +32 -18
  48. package/spec-cli/templates/spec/project/.plugins/commands/extract/spec.md +24 -18
  49. package/spec-cli/templates/spec/project/.plugins/commands/regroup/spec.md +3 -3
  50. package/spec-cli/templates/spec/project/.plugins/commands/rename/spec.md +11 -0
  51. package/spec-cli/templates/spec/project/.plugins/commands/spec.md +22 -8
  52. package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +1 -1
  53. package/spec-cli/templates/spec/project/.plugins/commands/tidy/spec.md +29 -11
  54. package/spec-cli/templates/spec/project/.plugins/core/idle/spec.md +1 -1
  55. package/spec-cli/templates/spec/project/.plugins/core/mark-active/spec.md +2 -0
  56. package/spec-cli/templates/spec/project/.plugins/core/session-fail/spec.md +1 -1
  57. package/spec-cli/templates/spec/project/.plugins/core/spec-first/spec-first.sh +28 -39
  58. package/spec-cli/templates/spec/project/.plugins/core/spec-first/spec.md +5 -3
  59. package/spec-cli/templates/spec/project/.plugins/core/spec-of-file/spec.md +1 -1
  60. package/spec-cli/templates/spec/project/.plugins/core/spec.md +2 -2
  61. package/spec-cli/templates/spec/project/.plugins/core/stop-gate/spec.md +7 -1
  62. package/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +2 -2
  63. package/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +5 -5
  64. package/spec-cli/templates/spec/project/.plugins/prompts/spec.md +10 -6
  65. package/spec-cli/templates/spec/project/.plugins/skills/distill/spec.md +38 -50
  66. package/spec-cli/templates/spec/project/.plugins/skills/spec.md +9 -7
  67. package/spec-cli/templates/spec/project/.plugins/spec.md +17 -12
  68. package/spec-cli/templates/spec/project/spec.md +6 -5
  69. package/spec-cli/templates/spexcode.json +4 -2
  70. package/spec-dashboard/dist/assets/Dashboard-CiHh-gLD.js +27 -0
  71. package/spec-dashboard/dist/assets/EvalsPage-DmiX3rdU.js +2 -0
  72. package/spec-dashboard/dist/assets/IssuesPage-CIbVGRUJ.js +1 -0
  73. package/spec-dashboard/dist/assets/MobileApp-D-N9_eh0.js +2 -0
  74. package/spec-dashboard/dist/assets/Modal-DHMzSFJ4.js +1 -0
  75. package/spec-dashboard/dist/assets/PageScroll-C15adEYI.js +1 -0
  76. package/spec-dashboard/dist/assets/ProjectsPage-sQpzglp5.js +1 -0
  77. package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +32 -0
  78. package/spec-dashboard/dist/assets/SessionInterface-B8pGU7Rg.js +39 -0
  79. package/spec-dashboard/dist/assets/SessionWindow-CuDO_67z.js +13 -0
  80. package/spec-dashboard/dist/assets/Settings-C_N1wX1f.js +1 -0
  81. package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +1 -0
  82. package/spec-dashboard/dist/assets/index-DmWbmvCq.js +41 -0
  83. package/spec-dashboard/dist/assets/index-GGIVdKwH.css +1 -0
  84. package/spec-dashboard/dist/assets/terminalFont-DmTPNdbU.js +1 -0
  85. package/spec-dashboard/dist/index.html +10 -10
  86. package/spec-dashboard/src/reviewFilters.js +279 -0
  87. package/spec-dashboard/src/reviewQuery.js +174 -0
  88. package/spec-dashboard/src/session.js +167 -0
  89. package/spec-eval/src/cli.ts +49 -28
  90. package/spec-eval/src/humanok.ts +1 -1
  91. package/spec-eval/src/matrix.ts +693 -0
  92. package/spec-eval/src/sessioneval.ts +726 -66
  93. package/spec-cli/src/migrate-table.ts +0 -408
  94. package/spec-cli/src/migrate.ts +0 -386
  95. package/spec-dashboard/dist/assets/Dashboard-C_fGmOKK.js +0 -27
  96. package/spec-dashboard/dist/assets/EvalsPage-Cnr1s3bq.js +0 -2
  97. package/spec-dashboard/dist/assets/FoldToggle-x9gtO1OQ.js +0 -1
  98. package/spec-dashboard/dist/assets/IssuesPage-5f_vL-JV.js +0 -1
  99. package/spec-dashboard/dist/assets/MobileApp-DEO1jgGM.js +0 -1
  100. package/spec-dashboard/dist/assets/SessionInterface-CAlbMOFR.js +0 -66
  101. package/spec-dashboard/dist/assets/SessionInterface-DYP7pi_n.css +0 -32
  102. package/spec-dashboard/dist/assets/SessionWindow-JYbpPwNB.js +0 -13
  103. package/spec-dashboard/dist/assets/Settings-DKb5Ji_X.js +0 -1
  104. package/spec-dashboard/dist/assets/index-BQu-oJ8J.js +0 -41
  105. package/spec-dashboard/dist/assets/index-BbMkwuix.css +0 -1
  106. package/spec-dashboard/dist/assets/launch-BM9GgvkX.js +0 -6
@@ -5,35 +5,35 @@ the rest, you don't hand-author the spec tree or wire the dashboard yourself.
5
5
 
6
6
  1. Install the CLI (one-time, global — ONE install serves every project)
7
7
  npm i -g spexcode # ONE command lands on PATH: \`spex\` (Node ≥ 22)
8
- It always operates on the repo of your current directory that cwd is the only "which repo" knob.
8
+ Project-scoped commands operate on the repo of your current directory; the dashboard is host-scoped.
9
9
  (Dogfooding an unpublished HEAD from a source checkout? \`npm link\` at the repo ROOT — that links
10
10
  the \`spexcode\` package itself, never the internal @spexcode/spec-cli. Both paths own the same
11
11
  \`spex\` bin, so uninstall one before switching (\`npm rm -g spexcode\`; a legacy link of the
12
- inner package uninstalls as \`@spexcode/spec-cli\`). A source link ships no
13
- prebuilt dashboard dist \`spex serve ui\` builds it once on first run (needs spec-dashboard's
14
- npm deps installed), or use the dev server.)
12
+ inner package uninstalls as \`@spexcode/spec-cli\`). Contributors run \`npm run api\` and
13
+ \`npm run web\` from the source root for backend reloads and Vite/HMR; those are not installed-user
14
+ startup commands.)
15
15
 
16
16
  2. Adopt a repo
17
- cd <your-repo> && spex init # seeds .spec/ + git hooks (additive, never overwrites)
17
+ cd <your-repo> && spex init --harness claude,codex,opencode,pi # seeds .spec/ + git hooks (additive, never overwrites)
18
+ --harness is required and has no default — the explicit choice of which harness(es) materialize
19
+ delivers into. The example lists every built-in; drop the ones you don't use (any one id or
20
+ comma-separated subset is valid).
18
21
  Works on any git repo. Edit .spec/project/spec.md to describe it, then grow child nodes
19
22
  (each a dir with a spec.md + a \`code:\` list of the files it governs).
20
23
 
21
- 3. Run the backend — it reads .spec + git from the cwd repo
24
+ 3. Run this repo's backend — it reads .spec + git from cwd and registers in this user's host registry
22
25
  spex serve # http://localhost:8787 (--port <n> for another endpoint)
23
- Serve a different repo by running it from there; two repos at once = two \`spex serve\` on two ports.
24
-
25
- 4. Open the dashboard — the SAME dashboard for every project, pointed per project
26
- spex serve ui # serves the bundled dashboard on :5173, proxying /api
27
- Point it at another backend with --api-port (pairs with \`spex serve --port\`); one dashboard per
28
- project. The dashboard is a viewer which backend it proxies is the only "which project" knob.
29
- Loopback-only by default: viewing from another machine needs \`--host 0.0.0.0\` (or a specific
30
- interface) still plain HTTP with no gate, so bind wide only on a LAN/tailnet you trust (for
31
- the internet, use \`spex serve --public\` instead).
32
- (Dogfood/source alternative: API_URL=http://localhost:<port> npm run dev in spec-dashboard —
33
- the dev server; "dashboard": { "apiUrl": "..." } in spexcode.json applies only to that layout.)
26
+ Run one \`spex serve\` from every project you want online. Give each additional backend a free
27
+ --port; the port only binds that backend and is never paired with a dashboard process.
28
+
29
+ 4. Open the shared host dashboard run this ONCE for the current user, from any directory
30
+ spex dashboard # host gateway + UI on http://localhost:5173
31
+ It discovers backends already running and any that start later. \`/projects\` is the global project
32
+ switcher and management surface; each project's dashboard lives under \`/p/:id/\`. The gateway is
33
+ loopback-only by default; \`--host\` widens its bind as described by \`spex dashboard --help\`.
34
34
 
35
35
  5. Govern your layout (optional)
36
- spexcode.json sets lint's governedRoots/sourceExtensions and any non-default worktree layout.
36
+ spexcode.json sets lint's governedRoots/include/exclude/test source policy and any non-default layout.
37
37
  \`spex spec lint\` must report 0 errors; coverage warnings are your adoption TODO (files no node claims yet).
38
38
 
39
39
  Look these up on demand — the formats an agent authors, and the settings it configures:
@@ -72,6 +72,11 @@ FRONTMATTER (YAML between the opening and closing --- lines; every field optiona
72
72
  duplicates, globs/directories with a selector, and dead/ambiguous units all error loud. A
73
73
  selector-scoped governor claims units, not the file, so it stays out of the \`owners\` bound
74
74
  (spex spec owner still displays it, marked "(scoped)"). Anchors are optional.
75
+ Python \`.py\`/\`.pyi\` anchors recognize structural \`def\`, \`async def\`, and \`class\`
76
+ declarations. Methods and nested declarations use lexical qualified names such as
77
+ \`Class.method\`, \`outer.inner\`, or \`Outer.Inner.method\`; attached decorators belong to
78
+ the declaration's range. Runtime-created/assigned callables, imported aliases, and generated
79
+ names are outside this declaration extractor and therefore resolve as dead anchors.
75
80
  related: files this node REFERENCES but does not own — a YAML list, same path forms. Carries coverage
76
81
  (never drift, never eval freshness, nothing to ack); it is the many-to-many net that claims the files
77
82
  govern doesn't. Every listed path must exist (lint integrity error otherwise). A related row
@@ -116,10 +121,6 @@ WHAT lint CHECKS (spex spec lint; the pre-commit hook gates on errors):
116
121
  optional leading dot; no space / '/' / '_' / uppercase Latin), and its leaf dir name
117
122
  is unique tree-wide.
118
123
  mention (error) every [[node-id]] in prose names a real node (fenced/backticked samples exempt).
119
- altitude (warn) the body stays high-altitude: line/char budgets (~50 lines / 4200 chars), low
120
- code-identifier density, no step-by-step phrasing. Over budget = rewrite higher.
121
- breadth (warn) a node with >= maxChildren direct children (default 8) — altitude's structural
122
- twin; is an intermediate grouping layer missing?
123
124
  coverage (warn) every source file is claimed by ≥1 node — via code: OR related: (related is the net).
124
125
  drift (warn) a governed file has commits newer than the node's spec version — it may be stale.
125
126
  ALWAYS advisory: unanchored drift never blocks a commit (the blocking tier is
@@ -139,14 +140,17 @@ WHAT lint CHECKS (spex spec lint; the pre-commit hook gates on errors):
139
140
  foundation owner). Selector-scoped governors don't count toward the bound.
140
141
  confusable-id (warn) two leaf ids one edit apart read as the same word — rename one to read apart.
141
142
 
143
+ Heuristic spec health is deliberately outside this production gate. Bare \`spex doctor\` reports opt-in
144
+ altitude and breadth findings; the tidy workflow consumes that report and adds semantic judgment.
145
+
142
146
  LIFECYCLE: author each node on a node/<id> branch, one node per commit; \`spex spec lint\` must reach 0 errors
143
147
  before merge. \`spex init\` seeds the first tree; \`spex guide eval\` covers the sibling eval.md, the measurement file.`
144
148
 
145
149
  const EVAL = `spex guide eval — the eval.md file format
146
150
 
147
151
  An eval.md sits BESIDE a node's spec.md and says how to MEASURE the node's loss — the gap between live
148
- behaviour and the spec. It is optional, but a node that governs SOURCE code (its code: includes a file whose extension is in
149
- \`lint.sourceExtensions\` — default .ts/.tsx/.js/.jsx, set it for a Rust/Go/Python tree) with no eval.md is
152
+ behaviour and the spec. It is optional, but a node that governs a file admitted by lint's shared tracked-text
153
+ include-minus-exclude/test policy with no eval.md is
150
154
  a blind spot: \`spex eval lint\` flags it \`eval-coverage\`. The eval system defines no DSL and RUNS
151
155
  NOTHING — the agent measures; eval keeps score.
152
156
 
@@ -210,7 +214,12 @@ PICK THE EVIDENCE KIND BY WHAT THE BEHAVIOUR DOES OVER TIME:
210
214
  CLI harness stamping line numbers — take a baseline and at EACH real step push
211
215
  \`{ at: <position>, step: "…" }\`; dump that array as \`--timeline\`. Its \`axis\` MUST match the
212
216
  evidence it rides (a \`line\` map needs a \`--result\` transcript, a \`time\` map a \`--video\`);
213
- skip it for a short single-step artefact. (Legacy \`{ "v": 1, "events": [{ "tMs" }] }\` — the
217
+ skip it for a short single-step artefact. A step name is a SHORT human label for its
218
+ moment ("open graph") — never a metadata channel: the run's identity, its verdict, and
219
+ the evidence's extent already have canonical homes (the scenario's \`test:\` field, the
220
+ measurement's --pass/--fail, the artefact itself), so a step like
221
+ "runner start: path/to/spec.ts :: <full case title>" smuggles data the measurement already
222
+ carries and turns the step ruler into noise. (Legacy \`{ "v": 1, "events": [{ "tMs" }] }\` — the
214
223
  time axis with \`tMs\` — is still accepted, read as \`axis: "time"\`.)
215
224
  STATIC end state → \`--image <png>\` (repeatable — N stills). Layout, an icon, copy, one rendered frame.
216
225
  backend / CLI → \`--result <txt>\` (a transcript; \`-\` reads stdin). A STRUCTURED export (a tool's
@@ -245,23 +254,26 @@ code file or the scenario (the eval.md) moves since it was filed.
245
254
 
246
255
  const SETTINGS = `spex guide settings — SpexCode's runtime settings (spexcode.json / spexcode.local.json)
247
256
 
248
- SpexCode reads its runtime settings from TWO optional JSON files at the repo root. There is no imperative
257
+ SpexCode reads PROJECT runtime settings from TWO optional JSON files at the repo root. There is no imperative
249
258
  settings verb — an agent CONFIGURES SpexCode by EDITING these files directly. The two split by
250
259
  PORTABILITY, and picking the right one is the whole discipline:
251
260
 
252
261
  spexcode.json COMMITTED — portable, shared by everyone on the repo. Layout, policy, dashboard
253
- identity, lint budgets, launcher NAMES. "Git is the database": tracked so the
262
+ identity, lint policy, doctor health budgets, launcher NAMES. "Git is the database": tracked so the
254
263
  team shares ONE configuration.
255
264
  spexcode.local.json GITIGNORED — host-specific, never committed. Absolute launcher paths, cert/secret
256
265
  paths. Layered OVER spexcode.json (see MERGE
257
266
  below); a targeted env override (SPEXCODE_CODEX_SERVER_CMD, …) still wins at its read site.
258
267
 
259
268
  Rule of thumb — is the value TRUE FOR THE PROJECT or TRUE FOR THIS MACHINE? A branch name, a dashboard
260
- icon, a lint budget, a launcher's name+harness are project facts → committed spexcode.json. The ABSOLUTE
269
+ icon, lint policy, doctor health budgets, and a launcher's name+harness are project facts → committed spexcode.json. The ABSOLUTE
261
270
  PATH of a launcher wrapper or a TLS cert path are machine facts → gitignored spexcode.local.json.
262
271
  Both files are optional; omit any field to take its default, except \`sessions.defaultLauncher\` when using
263
272
  \`spex session new\` or the dashboard without an explicit launcher choice.
264
273
 
274
+ The host-wide gateway has one separate per-user setting, \`gateway.icon\` in
275
+ \`$SPEXCODE_HOME/config.json\`. It is documented below and never belongs to either project file.
276
+
265
277
  MERGE: spexcode.local.json is layered over spexcode.json ONE LEVEL DEEP — per top-level section (dashboard,
266
278
  sessions, …), the two objects are shallow-merged with LOCAL WINNING per key; sections only one file names
267
279
  pass through untouched. This is exactly what lets a launcher's portable NAME reference (defaultLauncher)
@@ -277,13 +289,23 @@ Example — a repo whose trunk is \`staging\`, not \`main\`:
277
289
 
278
290
  ── DASHBOARD (spexcode.json — portable project identity) ──
279
291
  dashboard.title browser-tab name. Default: the repo-root basename.
280
- dashboard.icon browser-tab favicon: an emoji ("🔭") OR an Iconify name ("mdi:rocket-launch").
292
+ dashboard.icon the project's icon identity. The Projects UI writes one shared preset id:
293
+ spexcode | gateway | mdi:rocket-launch | compass | terminal | package | database | spark.
294
+ Existing emoji ("🔭"), arbitrary Iconify names ("lucide:radar"), and full URLs remain
295
+ supported; picking a preset migrates that one existing field, never creates another.
281
296
  dashboard.apiUrl the per-project backend the dashboard proxies to (read frontend-side). For a SHARED
282
297
  install prefer the API_URL env var; apiUrl here is the default only when the dashboard
283
298
  lives inside the project.
284
299
  Example:
285
300
  { "dashboard": { "title": "MyApp specs", "icon": "mdi:rocket-launch" } }
286
301
 
302
+ ── HOST GATEWAY ($SPEXCODE_HOME/config.json — per-user host identity, never a project file) ──
303
+ gateway.icon the global /projects icon, using the same preset ids above. Default: "gateway".
304
+ The admin-only Projects picker writes this field atomically. It is the gateway's ONE
305
+ authored icon setting and is never copied into spexcode.json or spexcode.local.json.
306
+ Example:
307
+ { "gateway": { "icon": "database" } }
308
+
287
309
  ── SESSIONS / WORKERS ──
288
310
  sessions.maxActive concurrency cap — max agents AUTONOMOUSLY PROGRESSING at once (default 8;
289
311
  precedence: spexcode.json → SPEXCODE_MAX_ACTIVE env → default; read live, so
@@ -291,8 +313,8 @@ Example:
291
313
  Counts compute slots, not total sessions: idle/asking/review/done do not
292
314
  occupy one. A policy number → committed spexcode.json; omit it to use the
293
315
  default, or tune higher/lower for the project's usual host.
294
- sessions.launchers the NAMED launcher profiles (see LAUNCHERS). \`spex init\` seeds "claude" and
295
- "codex" here as ordinary entries; edit/add more like any other.
316
+ sessions.launchers the NAMED launcher profiles (see LAUNCHERS). \`spex init\` seeds one entry per
317
+ SELECTED harness (--harness); edit/add more like any other.
296
318
  sessions.defaultLauncher the launcher name a create with no explicit --launcher/dropdown pick uses
297
319
  (required for no-choice creates). A portable NAME → committed.
298
320
  A launcher \`cmd\` that is a HOST-SPECIFIC ABSOLUTE PATH belongs in spexcode.local.json — the committed file
@@ -301,18 +323,24 @@ must stay free of machine paths.
301
323
  ── LAUNCHERS (the profile block, split across the two files) ──
302
324
  A named launcher profile fixes BOTH a session's harness AND its exact launch command; a create picks one
303
325
  by name with --launcher/the dashboard dropdown, and the chosen name is persisted on the record so a resume
304
- reuses the same auth. There are NO magic built-ins: \`spex init\` SEEDS "claude" and "codex" as ordinary
305
- named launchers,
306
- "claude" → { "harness": "claude", "cmd": "claude --dangerously-skip-permissions" }
307
- "codex" → { "harness": "codex", "cmd": "codex --yolo" }
308
- after which they are edited (or removed) exactly like any launcher you add. To run workers under an auth
326
+ reuses the same auth. There are NO magic built-ins: \`spex init\` SEEDS an ordinary named launcher for each
327
+ harness the adopter SELECTED (--harness), from the template pool
328
+ "claude" → { "harness": "claude", "cmd": "claude" }
329
+ "codex" → { "harness": "codex", "cmd": "codex" }
330
+ "opencode" { "harness": "opencode", "cmd": "opencode" }
331
+ "pi" → { "harness": "pi", "cmd": "pi" }
332
+ These plain commands preserve each harness's normal permission model. Automatic-permission commands such as
333
+ \`claude --dangerously-skip-permissions\`, \`codex --yolo\`, or \`opencode --auto\` are NEVER clean-init
334
+ defaults: define and select one explicitly only when that access is intended. To run workers under an auth
309
335
  wrapper (e.g. reclaude), point a launcher's \`cmd\` at it in spexcode.local.json — there is no environment
310
- override that rewrites a launcher's command. Add more profiles when a project needs named auth/config-dir
311
- variants. Shape:
312
- "launchers": { "<name>": { "harness": "claude" | "codex", "cmd": "<launch command>" } }
313
- \`harness\` defaults to "claude"; \`cmd\` is required. Because \`cmd\` is a machine fact (an abs wrapper path),
314
- the DEFINITION lives in the gitignored spexcode.local.json, while the portable defaultLauncher NAME sits
315
- in the committed spexcode.json the merge keeps both:
336
+ override that rewrites a launcher's command. Add more profiles when a project needs named auth/config-dir or
337
+ permission variants. Shape:
338
+ "launchers": { "<name>": { "harness": "claude" | "codex" | "opencode" | "pi",
339
+ "cmd": "<launch command>" } }
340
+ \`harness\` defaults to "claude"; \`cmd\` is required and embedded whole. A portable plain command may live
341
+ in committed spexcode.json (as the init seeds do). A host-specific command — an absolute wrapper path,
342
+ credential route, or personal permission choice — belongs in gitignored spexcode.local.json, while its
343
+ portable defaultLauncher NAME may stay in committed spexcode.json; the merge keeps both:
316
344
 
317
345
  spexcode.json (committed — the portable name reference)
318
346
  { "sessions": { "defaultLauncher": "gpt5" } }
@@ -344,9 +372,11 @@ resolves its backend per this ladder, flag first:
344
372
  sugar for --api http://127.0.0.1:<n>).
345
373
  2a. worker (SPEXCODE_SESSION_ID set): env SPEXCODE_API_URL — the backend-injected lifeline; cwd
346
374
  discovery never steals it.
347
- 2b. human (no session id): the cwd project's RECORDED live backend — \`spex serve\` records {url,pid}
348
- in ~/.spexcode/projects/<enc>/backend.json at bind time; the reader
349
- health-probes before trusting (a dead record is ignored).
375
+ 2b. human (no session id): the cwd project's RECORDED live backend — \`spex serve\` publishes
376
+ {url,pid,instanceId,root} atomically to ~/.spexcode/projects/<enc>/backend.json
377
+ at bind time; the reader health-probes before trusting (a dead record is
378
+ ignored). The same record is what \`spex dashboard\` reconciles its
379
+ host-wide project list from.
350
380
  3. the other side as fallback (human with no live record → env; worker with no env → record).
351
381
  4. default http://127.0.0.1:$PORT||8787.
352
382
  WRITES are project-bound: every mutating verb (new/merge/send/close/rename/resume/stop) refuses
@@ -371,12 +401,14 @@ the guard (the flag is the declaration of intent). Reads point anywhere.
371
401
  lint.governedRoots dirs whose source files must each be governed by a spec (coverage).
372
402
  '.' = the whole project (only git-TRACKED files). Default
373
403
  ["spec-dashboard/src", "spec-cli/src"].
374
- lint.sourceExtensions extensions coverage treats as source. Default ["ts","tsx","js","jsx"].
375
- lint.testGlobs globs EXCLUDED from coverage (default ["**/*.test.*"]; [] to govern tests too).
376
- lint.identifierExtensions extensions the altitude bare-filename signal recognises.
377
- lint.altitude body budgets: { lineBudget, charBudget, sizeable, dense, steps }
378
- (defaults 50 / 4200 / 35 / 1.3 / 3).
379
- lint.maxChildren breadth budget: warn at >= this many direct children (default 8).
404
+ lint.sourceIncludeGlobs optional repo-relative includes. Omit = ALL tracked current regular text under
405
+ governedRoots; [] = intentionally none. Slash-less globs match at any depth.
406
+ lint.sourceExcludeGlobs repo-relative paths subtracted from the includes (default []). Nothing about
407
+ docs/vendor/build/config is guessed: exclude it here when that is project policy.
408
+ lint.sourceExtensions compatibility shorthand compiled into include globs (".py" "**/*.py") and
409
+ unioned with sourceIncludeGlobs; it has no separate matching path.
410
+ lint.testGlobs globs EXCLUDED from coverage. Defaults cover .test/.spec names, test/tests/
411
+ directories, and test_* / *_test conventions; [] governs tests too.
380
412
  lint.maxOwners warn when a file is governed WHOLE-FILE by > this many nodes (default 3).
381
413
  Selector-scoped governors (code: path#symbol) don't count toward the bound.
382
414
  (lint.driftErrorThreshold is RETIRED: the count-based commit gate is replaced
@@ -389,14 +421,32 @@ the guard (the flag is the declaration of intent). Reads point anywhere.
389
421
  freshness. A project policy → committed spexcode.json.
390
422
  lint.scenarioTags the closed vocabulary an eval scenario's tags: must draw from (default
391
423
  ["frontend-e2e","backend-api","cli","desktop","mobile"]); extend to mint a tag.
392
- Example — govern your own source dir and loosen the altitude budget:
393
- { "lint": { "governedRoots": ["src"], "altitude": { "lineBudget": 70 } } }
424
+ Example — govern your own source dir:
425
+ { "lint": { "governedRoots": ["src"] } }
426
+ Example — declare project-specific exclusions (nothing is guessed from these names):
427
+ { "lint": { "governedRoots": ["."], "sourceExcludeGlobs": ["vendor/**", "dist/**", "docs/**"] } }
428
+ Migration: \`lint.maxChildren\` is RETIRED and no longer read; move its value to
429
+ \`doctor.breadth.maxChildren\`. Bare \`spex doctor\` flags a stale key.
430
+
431
+ ── DOCTOR HEALTH (spexcode.json — portable advisory-diagnosis budgets) ──
432
+ doctor.altitude the one altitude proxy config consumed by bare \`spex doctor\`:
433
+ { lineBudget, charBudget, sizeable, dense, steps, identifierExtensions }
434
+ Defaults: 50 / 4200 / 35 / 1.3 / 3 / []. Exact filename signals derive from
435
+ lint's tracked source candidates; identifierExtensions adds compatibility wildcard
436
+ rows (".legacy" → "*.legacy") to that same matcher.
437
+ doctor.breadth the one tree-breadth hypothesis consumed by bare \`spex doctor\`:
438
+ { maxChildren }. Default: 8. Reports each affected node's direct child count and
439
+ points to the regroup workflow; genuine flat peers may remain flat.
440
+ Example — tune opt-in health diagnosis without changing the lint gate:
441
+ { "doctor": { "altitude": { "lineBudget": 70 }, "breadth": { "maxChildren": 10 } } }
394
442
 
395
443
  ── OTHER (spexcode.json unless noted) ──
396
444
  preset the SELECTED init preset — which cumulative .plugins tier \`spex init\` seeds (default
397
445
  'default'; seed-time only, read by init.ts).
398
- harnesses which harness targets \`spex materialize\` delivers into — native ids ("claude"|"codex") or a
399
- { "plugin": "<folder>" } bundle. Default (omitted): all native harnesses. PERSISTENT and
446
+ harnesses which harness targets \`spex materialize\` delivers into — native ids
447
+ ("claude"|"codex"|"opencode"|"pi") or a
448
+ { "plugin": "<folder>" } bundle. REQUIRED — there is no default set: \`spex init --harness\`
449
+ stamps the explicit choice, and a missing field fails materialize loud. PERSISTENT and
400
450
  git-transactional: the edit takes effect at the next git-native materialize anchor (the commit
401
451
  that carries it, a checkout/merge that receives it, or a manual \`spex materialize\`) — a
402
452
  deselected harness's artifacts are pruned by that pass.`
@@ -13,19 +13,33 @@ export type HarnessTarget =
13
13
  | { kind: 'native'; id: HarnessId }
14
14
  | { kind: 'plugin'; folder: string }
15
15
 
16
- // the zero-config default: deliver to EVERY native harness, no plugin.
17
- export const DEFAULT_HARNESS_IDS: readonly HarnessId[] = HARNESSES.map((h) => h.id)
16
+ // the native vocabulary (for messages and init's flag parsing). There is NO default set: the delivery
17
+ // targets are an explicit adopter choice, stamped by `spex init --harness` and read back from config forever.
18
+ export const NATIVE_HARNESS_IDS: readonly HarnessId[] = HARNESSES.map((h) => h.id)
18
19
  const KNOWN: readonly string[] = HARNESSES.map((h) => h.id)
19
20
 
21
+ // the one-line repair every missing-selection error carries — the field is REQUIRED, never defaulted,
22
+ // because with many harnesses a silent "deliver everywhere" would litter the adopter's tree with artifacts
23
+ // for tools they never installed.
24
+ const MISSING = `spexcode.json has no "harnesses" field — the delivery targets are an EXPLICIT choice, never a default. Declare it, e.g. "harnesses": ["claude"] — members are native ids (${KNOWN.join(', ')}) or {"plugin":"<folder>"}. Fresh adoption: \`spex init --harness <ids>\` stamps it.`
25
+
26
+ // parse `spex init --harness <spec>` into the raw JSON members the `harnesses` field carries: a comma-
27
+ // separated list where `plugin:<folder>` means a plugin target and anything else is a native id. Pure
28
+ // spelling translation — legality (unknown ids, plugin exclusivity) stays with resolveHarnessTargets.
29
+ export function parseHarnessFlag(spec: string): unknown[] {
30
+ return spec.split(',').map((s) => s.trim()).filter(Boolean)
31
+ .map((s) => (s.startsWith('plugin:') ? { plugin: s.slice('plugin:'.length) } : s))
32
+ }
33
+
20
34
  // parse + validate the spexcode.json `harnesses` field into resolved targets. FAIL LOUD on an illegal set —
21
35
  // materialize and init both gate on this so a bad config never silently delivers the wrong thing. `raw` is the
22
- // JSON value as written; undefined/null the default native set.
36
+ // JSON value as written; a MISSING field is itself illegal (the choice must exist, stamped by init).
23
37
  export function resolveHarnessTargets(raw: unknown): HarnessTarget[] {
24
- if (raw === undefined || raw === null) return DEFAULT_HARNESS_IDS.map((id) => ({ kind: 'native', id }))
38
+ if (raw === undefined || raw === null) throw new Error(MISSING)
25
39
  if (!Array.isArray(raw))
26
- throw new Error(`spexcode.json "harnesses" must be an ARRAY of targets (got ${typeof raw}). Members are native ids (${KNOWN.join(', ')}) or {"plugin":"<folder>"}; omit the field to default to [${DEFAULT_HARNESS_IDS.join(', ')}].`)
40
+ throw new Error(`spexcode.json "harnesses" must be an ARRAY of targets (got ${typeof raw}). Members are native ids (${KNOWN.join(', ')}) or {"plugin":"<folder>"}.`)
27
41
  if (raw.length === 0)
28
- throw new Error(`spexcode.json "harnesses" is EMPTY — list at least one target, or remove the field to default to [${DEFAULT_HARNESS_IDS.join(', ')}].`)
42
+ throw new Error(`spexcode.json "harnesses" is EMPTY — list at least one target: native ids (${KNOWN.join(', ')}) or {"plugin":"<folder>"}.`)
29
43
  const targets: HarnessTarget[] = []
30
44
  for (const m of raw) {
31
45
  if (typeof m === 'string') {