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
@@ -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,claude-headless,opencode-headless,pi-headless # 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 and launcher visibility, 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 or launcher-visibility policy, 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,12 +289,27 @@ 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.
299
+ dashboard.showHeadlessLaunchers
300
+ include launchers whose harness declares itself headless in the dashboard New Session
301
+ picker. Default: false. This changes dashboard visibility only; explicit CLI
302
+ --launcher selection can still use every configured launcher.
303
+ Example:
304
+ { "dashboard": { "title": "MyApp specs", "icon": "mdi:rocket-launch",
305
+ "showHeadlessLaunchers": false } }
306
+
307
+ ── HOST GATEWAY ($SPEXCODE_HOME/config.json — per-user host identity, never a project file) ──
308
+ gateway.icon the global /projects icon, using the same preset ids above. Default: "gateway".
309
+ The admin-only Projects picker writes this field atomically. It is the gateway's ONE
310
+ authored icon setting and is never copied into spexcode.json or spexcode.local.json.
284
311
  Example:
285
- { "dashboard": { "title": "MyApp specs", "icon": "mdi:rocket-launch" } }
312
+ { "gateway": { "icon": "database" } }
286
313
 
287
314
  ── SESSIONS / WORKERS ──
288
315
  sessions.maxActive concurrency cap — max agents AUTONOMOUSLY PROGRESSING at once (default 8;
@@ -291,8 +318,8 @@ Example:
291
318
  Counts compute slots, not total sessions: idle/asking/review/done do not
292
319
  occupy one. A policy number → committed spexcode.json; omit it to use the
293
320
  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.
321
+ sessions.launchers the NAMED launcher profiles (see LAUNCHERS). \`spex init\` seeds one entry per
322
+ SELECTED harness (--harness); edit/add more like any other.
296
323
  sessions.defaultLauncher the launcher name a create with no explicit --launcher/dropdown pick uses
297
324
  (required for no-choice creates). A portable NAME → committed.
298
325
  A launcher \`cmd\` that is a HOST-SPECIFIC ABSOLUTE PATH belongs in spexcode.local.json — the committed file
@@ -301,18 +328,28 @@ must stay free of machine paths.
301
328
  ── LAUNCHERS (the profile block, split across the two files) ──
302
329
  A named launcher profile fixes BOTH a session's harness AND its exact launch command; a create picks one
303
330
  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
331
+ reuses the same auth. There are NO magic built-ins: \`spex init\` SEEDS an ordinary named launcher for each
332
+ harness the adopter SELECTED (--harness), from the template pool
333
+ "claude" → { "harness": "claude", "cmd": "claude" }
334
+ "claude-headless" → { "harness": "claude-headless", "cmd": "claude" }
335
+ "codex" → { "harness": "codex", "cmd": "codex" }
336
+ "opencode" → { "harness": "opencode", "cmd": "opencode" }
337
+ "opencode-headless" → { "harness": "opencode-headless", "cmd": "opencode --auto" }
338
+ "pi" → { "harness": "pi", "cmd": "pi" }
339
+ "pi-headless" → { "harness": "pi-headless", "cmd": "pi" }
340
+ The interactive profiles preserve each harness's normal permission model. \`opencode-headless\` is the one
341
+ deliberate seed exception: its terminal-free run requires \`opencode --auto\`; interactive \`opencode\` stays
342
+ plain. Other automatic-permission commands are NEVER clean-init defaults: define and select one explicitly
343
+ only when that access is intended. To run workers under an auth
309
344
  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:
345
+ override that rewrites a launcher's command. Add more profiles when a project needs named auth/config-dir or
346
+ permission variants. Shape:
347
+ "launchers": { "<name>": { "harness": "claude" | "codex" | "opencode" | "pi" | "claude-headless" | "opencode-headless" | "pi-headless",
348
+ "cmd": "<launch command>" } }
349
+ \`harness\` defaults to "claude"; \`cmd\` is required and embedded whole. A portable plain command may live
350
+ in committed spexcode.json (as the init seeds do). A host-specific command — an absolute wrapper path,
351
+ credential route, or personal permission choice — belongs in gitignored spexcode.local.json, while its
352
+ portable defaultLauncher NAME may stay in committed spexcode.json; the merge keeps both:
316
353
 
317
354
  spexcode.json (committed — the portable name reference)
318
355
  { "sessions": { "defaultLauncher": "gpt5" } }
@@ -344,9 +381,11 @@ resolves its backend per this ladder, flag first:
344
381
  sugar for --api http://127.0.0.1:<n>).
345
382
  2a. worker (SPEXCODE_SESSION_ID set): env SPEXCODE_API_URL — the backend-injected lifeline; cwd
346
383
  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).
384
+ 2b. human (no session id): the cwd project's RECORDED live backend — \`spex serve\` publishes
385
+ {url,pid,instanceId,root} atomically to ~/.spexcode/projects/<enc>/backend.json
386
+ at bind time; the reader health-probes before trusting (a dead record is
387
+ ignored). The same record is what \`spex dashboard\` reconciles its
388
+ host-wide project list from.
350
389
  3. the other side as fallback (human with no live record → env; worker with no env → record).
351
390
  4. default http://127.0.0.1:$PORT||8787.
352
391
  WRITES are project-bound: every mutating verb (new/merge/send/close/rename/resume/stop) refuses
@@ -371,12 +410,14 @@ the guard (the flag is the declaration of intent). Reads point anywhere.
371
410
  lint.governedRoots dirs whose source files must each be governed by a spec (coverage).
372
411
  '.' = the whole project (only git-TRACKED files). Default
373
412
  ["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).
413
+ lint.sourceIncludeGlobs optional repo-relative includes. Omit = ALL tracked current regular text under
414
+ governedRoots; [] = intentionally none. Slash-less globs match at any depth.
415
+ lint.sourceExcludeGlobs repo-relative paths subtracted from the includes (default []). Nothing about
416
+ docs/vendor/build/config is guessed: exclude it here when that is project policy.
417
+ lint.sourceExtensions compatibility shorthand compiled into include globs (".py" "**/*.py") and
418
+ unioned with sourceIncludeGlobs; it has no separate matching path.
419
+ lint.testGlobs globs EXCLUDED from coverage. Defaults cover .test/.spec names, test/tests/
420
+ directories, and test_* / *_test conventions; [] governs tests too.
380
421
  lint.maxOwners warn when a file is governed WHOLE-FILE by > this many nodes (default 3).
381
422
  Selector-scoped governors (code: path#symbol) don't count toward the bound.
382
423
  (lint.driftErrorThreshold is RETIRED: the count-based commit gate is replaced
@@ -389,14 +430,32 @@ the guard (the flag is the declaration of intent). Reads point anywhere.
389
430
  freshness. A project policy → committed spexcode.json.
390
431
  lint.scenarioTags the closed vocabulary an eval scenario's tags: must draw from (default
391
432
  ["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 } } }
433
+ Example — govern your own source dir:
434
+ { "lint": { "governedRoots": ["src"] } }
435
+ Example — declare project-specific exclusions (nothing is guessed from these names):
436
+ { "lint": { "governedRoots": ["."], "sourceExcludeGlobs": ["vendor/**", "dist/**", "docs/**"] } }
437
+ Migration: \`lint.maxChildren\` is RETIRED and no longer read; move its value to
438
+ \`doctor.breadth.maxChildren\`. Bare \`spex doctor\` flags a stale key.
439
+
440
+ ── DOCTOR HEALTH (spexcode.json — portable advisory-diagnosis budgets) ──
441
+ doctor.altitude the one altitude proxy config consumed by bare \`spex doctor\`:
442
+ { lineBudget, charBudget, sizeable, dense, steps, identifierExtensions }
443
+ Defaults: 50 / 4200 / 35 / 1.3 / 3 / []. Exact filename signals derive from
444
+ lint's tracked source candidates; identifierExtensions adds compatibility wildcard
445
+ rows (".legacy" → "*.legacy") to that same matcher.
446
+ doctor.breadth the one tree-breadth hypothesis consumed by bare \`spex doctor\`:
447
+ { maxChildren }. Default: 8. Reports each affected node's direct child count and
448
+ points to the regroup workflow; genuine flat peers may remain flat.
449
+ Example — tune opt-in health diagnosis without changing the lint gate:
450
+ { "doctor": { "altitude": { "lineBudget": 70 }, "breadth": { "maxChildren": 10 } } }
394
451
 
395
452
  ── OTHER (spexcode.json unless noted) ──
396
453
  preset the SELECTED init preset — which cumulative .plugins tier \`spex init\` seeds (default
397
454
  '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
455
+ harnesses which harness targets \`spex materialize\` delivers into — native ids
456
+ ("claude"|"codex"|"opencode"|"pi"|"claude-headless"|"opencode-headless"|"pi-headless") or a
457
+ { "plugin": "<folder>" } bundle. REQUIRED — there is no default set: \`spex init --harness\`
458
+ stamps the explicit choice, and a missing field fails materialize loud. PERSISTENT and
400
459
  git-transactional: the edit takes effect at the next git-native materialize anchor (the commit
401
460
  that carries it, a checkout/merge that receives it, or a manual \`spex materialize\`) — a
402
461
  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') {