spexcode 0.4.0 → 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 (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 +163 -30
  7. package/spec-cli/src/cli.ts +97 -42
  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 +31 -16
  13. package/spec-cli/src/git.ts +12 -5
  14. package/spec-cli/src/graph.ts +53 -36
  15. package/spec-cli/src/graphStream.ts +265 -34
  16. package/spec-cli/src/guide.ts +157 -76
  17. package/spec-cli/src/harness-select.ts +20 -6
  18. package/spec-cli/src/harness.ts +266 -67
  19. package/spec-cli/src/help.ts +84 -41
  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 +163 -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 +115 -169
  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 +162 -0
  39. package/spec-cli/src/sessions.ts +210 -63
  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 +47 -24
  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 -13
  48. package/spec-cli/templates/spec/project/.plugins/{extract → commands/extract}/spec.md +24 -18
  49. package/spec-cli/templates/spec/project/.plugins/{regroup → 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 +30 -0
  52. package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
  53. package/spec-cli/templates/spec/project/.plugins/commands/tidy/spec.md +43 -0
  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 +62 -0
  66. package/spec-cli/templates/spec/project/.plugins/skills/spec.md +19 -0
  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 +80 -45
  90. package/spec-eval/src/evaltab.ts +4 -3
  91. package/spec-eval/src/humanok.ts +1 -1
  92. package/spec-eval/src/matrix.ts +693 -0
  93. package/spec-eval/src/scenarios.ts +116 -4
  94. package/spec-eval/src/sessioneval.ts +726 -66
  95. package/spec-cli/src/migrate-table.ts +0 -398
  96. package/spec-cli/src/migrate.ts +0 -386
  97. package/spec-cli/templates/spec/project/.plugins/distill/spec.md +0 -74
  98. package/spec-cli/templates/spec/project/.plugins/supervisor/spec.md +0 -8
  99. package/spec-cli/templates/spec/project/.plugins/tidy/spec.md +0 -25
  100. package/spec-dashboard/dist/assets/Dashboard-CTcH2eW9.js +0 -27
  101. package/spec-dashboard/dist/assets/EvalsPage-CJNKwHLN.js +0 -2
  102. package/spec-dashboard/dist/assets/FoldToggle-CVFbBpyW.js +0 -1
  103. package/spec-dashboard/dist/assets/IssuesPage-kULjonqj.js +0 -1
  104. package/spec-dashboard/dist/assets/MobileApp-B0ZJju8K.js +0 -1
  105. package/spec-dashboard/dist/assets/SessionInterface-BRKJqU2U.js +0 -71
  106. package/spec-dashboard/dist/assets/SessionInterface-DYP7pi_n.css +0 -32
  107. package/spec-dashboard/dist/assets/SessionWindow-CDhEL7wO.js +0 -9
  108. package/spec-dashboard/dist/assets/Settings-BL6FV_8S.js +0 -1
  109. package/spec-dashboard/dist/assets/index-DmQsNYKK.css +0 -1
  110. package/spec-dashboard/dist/assets/index-DulGPk6A.js +0 -41
  111. /package/spec-cli/templates/spec/project/.plugins/{distill → skills/distill}/digest.mjs +0 -0
@@ -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:
@@ -62,13 +62,27 @@ FRONTMATTER (YAML between the opening and closing --- lines; every field optiona
62
62
  Drives drift + eval freshness. Many nodes MAY govern the same file (ordinary
63
63
  composition); a file governed by > maxOwners nodes warns (the \`owners\` rule — split it). Omit
64
64
  for a pure-prose node: a cross-cutting contract no file owns.
65
- The entry may pin ONE named unitan ANCHOR: \`path#symbol\` (\`#Class.method\` for a class
66
- method; top-level functions, arrow/const declarations, classes, enums anchor cleanly; a
67
- type/interface anchor warns). An anchor upgrades drift on THAT unit to a blocking error
68
- (\`anchor-drift\`); without one, drift stays advisory forever. Anchors are optional.
65
+ The entry may pin named unitsANCHORS: one or more \`path#symbol\` rows, ALL on the same
66
+ base file (\`#Class.method\` for a class method; top-level functions, arrow/const declarations,
67
+ classes, enums anchor cleanly; a type/interface anchor warns). One-govern counts DISTINCT
68
+ base paths, so selectors never widen govern past one file. Several selectors are OR: a
69
+ commit hitting ANY upgrades drift to the blocking \`anchor-drift\` error (one error per
70
+ entry, naming the hit selectors — a commit counts once). Without an anchor, drift stays
71
+ advisory forever. A base path is either whole-file (bare) or selector-scoped, never both;
72
+ duplicates, globs/directories with a selector, and dead/ambiguous units all error loud. A
73
+ selector-scoped governor claims units, not the file, so it stays out of the \`owners\` bound
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.
69
80
  related: files this node REFERENCES but does not own — a YAML list, same path forms. Carries coverage
70
81
  (never drift, never eval freshness, nothing to ack); it is the many-to-many net that claims the files
71
- govern doesn't. Every listed path must exist (lint integrity error otherwise).
82
+ govern doesn't. Every listed path must exist (lint integrity error otherwise). A related row
83
+ may also pin \`path#symbol\`: the node then hears about a commit ONLY when it moves that
84
+ unit — a hit is a soft \`related-drift\` warn naming the selector, a miss is SILENT (a scoped
85
+ related file's ordinary file-level nudge is off). Still never blocks, no ack, no eval freshness.
72
86
  surface plugin-system/.plugins nodes only: one or MORE of system (folded into every agent's prompt) |
73
87
  command (a /command) | skill (an on-demand SKILL.md the harness loads when a task matches the
74
88
  node's desc) | agent (a spawnable sub-agent definition; its \`tools:\` list is the spawned
@@ -92,11 +106,14 @@ WHAT lint CHECKS (spex spec lint; the pre-commit hook gates on errors):
92
106
  deleted/renamed), an ambiguous one (two same-named units in one file), a file that
93
107
  no longer parses, a language with no designated extractor, or an extractor that
94
108
  can't run here (e.g. no host typescript — 'npm i -D typescript' or drop the anchor)
95
- all error, never silently pass.
96
- anchor-drift (error) a commit since the node's version intersected the ANCHORED unit's lines (judged
109
+ all error, never silently pass. So do a relation's STRUCTURAL defects: a duplicate
110
+ entry, a base path both bare and selector-scoped, or a selector on a
111
+ glob/directory.
112
+ anchor-drift (error) a commit since the node's version intersected an ANCHORED unit's lines (judged
97
113
  from the file as it existed AT each commit) and no Spec-OK ack covers it — the
98
- blocking tier of drift. Remedy: update the spec, or \`spex spec ack\` with a real
99
- reason (recorded in the ack commit body).
114
+ blocking tier of drift. Selectors on one file are OR'd: one error per entry, the
115
+ hit selectors named, each commit counted once. Remedy: update the spec, or
116
+ \`spex spec ack\` with a real reason (recorded in the ack commit body).
100
117
  one-govern (error) a node governs (code:) at most ONE file — keep the true subject, move the rest
101
118
  to related:.
102
119
  living (error) no "## vN" changelog headings — the body is current-state.
@@ -104,31 +121,36 @@ WHAT lint CHECKS (spex spec lint; the pre-commit hook gates on errors):
104
121
  optional leading dot; no space / '/' / '_' / uppercase Latin), and its leaf dir name
105
122
  is unique tree-wide.
106
123
  mention (error) every [[node-id]] in prose names a real node (fenced/backticked samples exempt).
107
- altitude (warn) the body stays high-altitude: line/char budgets (~50 lines / 4200 chars), low
108
- code-identifier density, no step-by-step phrasing. Over budget = rewrite higher.
109
- breadth (warn) a node with >= maxChildren direct children (default 8) — altitude's structural
110
- twin; is an intermediate grouping layer missing?
111
124
  coverage (warn) every source file is claimed by ≥1 node — via code: OR related: (related is the net).
112
125
  drift (warn) a governed file has commits newer than the node's spec version — it may be stale.
113
126
  ALWAYS advisory: unanchored drift never blocks a commit (the blocking tier is
114
- anchor-drift above). Remedy: edit the spec to the new intent (re-versions the
115
- node), OR \`spex spec ack <node> --reason "…"\` when only mechanics changed and the
116
- contract still holds.
127
+ anchor-drift above). On a selector-SCOPED code file whose window has NO hit (a
128
+ miss), this advisory stays by default; the committed \`lint.scopedCodeMiss:
129
+ "ignore"\` silences ONLY it (hit blocks, bare drift, integrity, acks, related,
130
+ eval freshness all untouched). Remedy: edit the spec to the new intent
131
+ (re-versions the node), OR \`spex spec ack <node> --reason "…"\` when only
132
+ mechanics changed and the contract still holds.
117
133
  anchor (warn) an anchor pins a type/interface — types reshape with every refactor; anchor the
118
134
  behaviour-bearing unit instead.
119
- related-drift (warn) a related: file moved ahead of the node — a soft nudge, one summary line, never blocks.
120
- owners (warn) a file governed by > maxOwners nodes (default 3) does too much — SPLIT it so each
121
- governor owns its own module (or merge the nodes, or give it one foundation owner).
135
+ related-drift (warn) a related: file moved ahead of the node — a soft nudge, one summary line, never
136
+ blocks. A selector-scoped related row instead warns per HIT (selector named);
137
+ its file-level misses are silent.
138
+ owners (warn) a file governed WHOLE-FILE by > maxOwners nodes (default 3) does too much — SPLIT
139
+ it so each governor owns its own module (or merge the nodes, or give it one
140
+ foundation owner). Selector-scoped governors don't count toward the bound.
122
141
  confusable-id (warn) two leaf ids one edit apart read as the same word — rename one to read apart.
123
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
+
124
146
  LIFECYCLE: author each node on a node/<id> branch, one node per commit; \`spex spec lint\` must reach 0 errors
125
147
  before merge. \`spex init\` seeds the first tree; \`spex guide eval\` covers the sibling eval.md, the measurement file.`
126
148
 
127
149
  const EVAL = `spex guide eval — the eval.md file format
128
150
 
129
151
  An eval.md sits BESIDE a node's spec.md and says how to MEASURE the node's loss — the gap between live
130
- behaviour and the spec. It is optional, but a node that governs SOURCE code (its code: includes a file whose extension is in
131
- \`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
132
154
  a blind spot: \`spex eval lint\` flags it \`eval-coverage\`. The eval system defines no DSL and RUNS
133
155
  NOTHING — the agent measures; eval keeps score.
134
156
 
@@ -141,8 +163,13 @@ FRONTMATTER: a \`scenarios:\` list (a YAML block sequence of mappings). Each sce
141
163
  \`frontend-e2e, backend-api, cli, desktop, mobile\`). A tag outside the library is rejected —
142
164
  use an existing one, or add it to the library to mint it. Tags classify a scenario (surface,
143
165
  device) so it can be filtered and, later, routed to the right driver.
144
- test optional. A repo path to a co-located runnable file (a playwright.spec.ts, a script)
145
- the agent MAY run by hand. Not a driver — eval never executes it.
166
+ test optional. Either a repo-path scalar (the backward-compatible shorthand) or a strict object:
167
+ test:
168
+ path: tests/auth.spec.ts
169
+ name: rejects an expired session
170
+ Both forms normalize in JSON to \`{ "path": "..." }\` with optional \`"name"\`. The object
171
+ requires exactly \`path\` + \`name\`; its case name is opaque text preserved for the measuring
172
+ hand. The path must exist. SpexCode does not parse WDIO/Playwright or execute anything.
146
173
  code optional. The file THIS scenario GOVERNS, ideally one (a comma list / flow list \`[a, b]\` is
147
174
  allowed) — its own slice of the code freshness axis, so scenarios on one node go stale
148
175
  independently. Absent → it inherits the node's \`code:\` list. A file governed by > maxOwners
@@ -187,7 +214,12 @@ PICK THE EVIDENCE KIND BY WHAT THE BEHAVIOUR DOES OVER TIME:
187
214
  CLI harness stamping line numbers — take a baseline and at EACH real step push
188
215
  \`{ at: <position>, step: "…" }\`; dump that array as \`--timeline\`. Its \`axis\` MUST match the
189
216
  evidence it rides (a \`line\` map needs a \`--result\` transcript, a \`time\` map a \`--video\`);
190
- 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
191
223
  time axis with \`tMs\` — is still accepted, read as \`axis: "time"\`.)
192
224
  STATIC end state → \`--image <png>\` (repeatable — N stills). Layout, an icon, copy, one rendered frame.
193
225
  backend / CLI → \`--result <txt>\` (a transcript; \`-\` reads stdin). A STRUCTURED export (a tool's
@@ -222,23 +254,26 @@ code file or the scenario (the eval.md) moves since it was filed.
222
254
 
223
255
  const SETTINGS = `spex guide settings — SpexCode's runtime settings (spexcode.json / spexcode.local.json)
224
256
 
225
- 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
226
258
  settings verb — an agent CONFIGURES SpexCode by EDITING these files directly. The two split by
227
259
  PORTABILITY, and picking the right one is the whole discipline:
228
260
 
229
261
  spexcode.json COMMITTED — portable, shared by everyone on the repo. Layout, policy, dashboard
230
- 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
231
263
  team shares ONE configuration.
232
264
  spexcode.local.json GITIGNORED — host-specific, never committed. Absolute launcher paths, cert/secret
233
265
  paths. Layered OVER spexcode.json (see MERGE
234
266
  below); a targeted env override (SPEXCODE_CODEX_SERVER_CMD, …) still wins at its read site.
235
267
 
236
268
  Rule of thumb — is the value TRUE FOR THE PROJECT or TRUE FOR THIS MACHINE? A branch name, a dashboard
237
- 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
238
270
  PATH of a launcher wrapper or a TLS cert path are machine facts → gitignored spexcode.local.json.
239
271
  Both files are optional; omit any field to take its default, except \`sessions.defaultLauncher\` when using
240
272
  \`spex session new\` or the dashboard without an explicit launcher choice.
241
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
+
242
277
  MERGE: spexcode.local.json is layered over spexcode.json ONE LEVEL DEEP — per top-level section (dashboard,
243
278
  sessions, …), the two objects are shallow-merged with LOCAL WINNING per key; sections only one file names
244
279
  pass through untouched. This is exactly what lets a launcher's portable NAME reference (defaultLauncher)
@@ -254,13 +289,23 @@ Example — a repo whose trunk is \`staging\`, not \`main\`:
254
289
 
255
290
  ── DASHBOARD (spexcode.json — portable project identity) ──
256
291
  dashboard.title browser-tab name. Default: the repo-root basename.
257
- 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.
258
296
  dashboard.apiUrl the per-project backend the dashboard proxies to (read frontend-side). For a SHARED
259
297
  install prefer the API_URL env var; apiUrl here is the default only when the dashboard
260
298
  lives inside the project.
261
299
  Example:
262
300
  { "dashboard": { "title": "MyApp specs", "icon": "mdi:rocket-launch" } }
263
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
+
264
309
  ── SESSIONS / WORKERS ──
265
310
  sessions.maxActive concurrency cap — max agents AUTONOMOUSLY PROGRESSING at once (default 8;
266
311
  precedence: spexcode.json → SPEXCODE_MAX_ACTIVE env → default; read live, so
@@ -268,8 +313,8 @@ Example:
268
313
  Counts compute slots, not total sessions: idle/asking/review/done do not
269
314
  occupy one. A policy number → committed spexcode.json; omit it to use the
270
315
  default, or tune higher/lower for the project's usual host.
271
- sessions.launchers the NAMED launcher profiles (see LAUNCHERS). \`spex init\` seeds "claude" and
272
- "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.
273
318
  sessions.defaultLauncher the launcher name a create with no explicit --launcher/dropdown pick uses
274
319
  (required for no-choice creates). A portable NAME → committed.
275
320
  A launcher \`cmd\` that is a HOST-SPECIFIC ABSOLUTE PATH belongs in spexcode.local.json — the committed file
@@ -278,18 +323,24 @@ must stay free of machine paths.
278
323
  ── LAUNCHERS (the profile block, split across the two files) ──
279
324
  A named launcher profile fixes BOTH a session's harness AND its exact launch command; a create picks one
280
325
  by name with --launcher/the dashboard dropdown, and the chosen name is persisted on the record so a resume
281
- reuses the same auth. There are NO magic built-ins: \`spex init\` SEEDS "claude" and "codex" as ordinary
282
- named launchers,
283
- "claude" → { "harness": "claude", "cmd": "claude --dangerously-skip-permissions" }
284
- "codex" → { "harness": "codex", "cmd": "codex --yolo" }
285
- 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
286
335
  wrapper (e.g. reclaude), point a launcher's \`cmd\` at it in spexcode.local.json — there is no environment
287
- override that rewrites a launcher's command. Add more profiles when a project needs named auth/config-dir
288
- variants. Shape:
289
- "launchers": { "<name>": { "harness": "claude" | "codex", "cmd": "<launch command>" } }
290
- \`harness\` defaults to "claude"; \`cmd\` is required. Because \`cmd\` is a machine fact (an abs wrapper path),
291
- the DEFINITION lives in the gitignored spexcode.local.json, while the portable defaultLauncher NAME sits
292
- 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:
293
344
 
294
345
  spexcode.json (committed — the portable name reference)
295
346
  { "sessions": { "defaultLauncher": "gpt5" } }
@@ -321,9 +372,11 @@ resolves its backend per this ladder, flag first:
321
372
  sugar for --api http://127.0.0.1:<n>).
322
373
  2a. worker (SPEXCODE_SESSION_ID set): env SPEXCODE_API_URL — the backend-injected lifeline; cwd
323
374
  discovery never steals it.
324
- 2b. human (no session id): the cwd project's RECORDED live backend — \`spex serve\` records {url,pid}
325
- in ~/.spexcode/projects/<enc>/backend.json at bind time; the reader
326
- 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.
327
380
  3. the other side as fallback (human with no live record → env; worker with no env → record).
328
381
  4. default http://127.0.0.1:$PORT||8787.
329
382
  WRITES are project-bound: every mutating verb (new/merge/send/close/rename/resume/stop) refuses
@@ -348,26 +401,52 @@ the guard (the flag is the declaration of intent). Reads point anywhere.
348
401
  lint.governedRoots dirs whose source files must each be governed by a spec (coverage).
349
402
  '.' = the whole project (only git-TRACKED files). Default
350
403
  ["spec-dashboard/src", "spec-cli/src"].
351
- lint.sourceExtensions extensions coverage treats as source. Default ["ts","tsx","js","jsx"].
352
- lint.testGlobs globs EXCLUDED from coverage (default ["**/*.test.*"]; [] to govern tests too).
353
- lint.identifierExtensions extensions the altitude bare-filename signal recognises.
354
- lint.altitude body budgets: { lineBudget, charBudget, sizeable, dense, steps }
355
- (defaults 50 / 4200 / 35 / 1.3 / 3).
356
- lint.maxChildren breadth budget: warn at >= this many direct children (default 8).
357
- lint.maxOwners warn when a file is governed by > this many nodes (default 3).
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.
412
+ lint.maxOwners warn when a file is governed WHOLE-FILE by > this many nodes (default 3).
413
+ Selector-scoped governors (code: path#symbol) don't count toward the bound.
358
414
  (lint.driftErrorThreshold is RETIRED: the count-based commit gate is replaced
359
415
  by code anchors — \`code: path#symbol\` — whose hits error unconditionally; a
360
416
  leftover key is ignored.)
417
+ lint.scopedCodeMiss "warn" (default) | "ignore" — the file-level drift ADVISORY on a selector-
418
+ scoped code: file whose window commits hit no selector (a miss). "ignore"
419
+ silences ONLY that advisory; it never touches hit blocks (anchor-drift),
420
+ bare-path drift, integrity, Spec-OK acks, related semantics, or eval
421
+ freshness. A project policy → committed spexcode.json.
361
422
  lint.scenarioTags the closed vocabulary an eval scenario's tags: must draw from (default
362
423
  ["frontend-e2e","backend-api","cli","desktop","mobile"]); extend to mint a tag.
363
- Example — govern your own source dir and loosen the altitude budget:
364
- { "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 } } }
365
442
 
366
443
  ── OTHER (spexcode.json unless noted) ──
367
444
  preset the SELECTED init preset — which cumulative .plugins tier \`spex init\` seeds (default
368
445
  'default'; seed-time only, read by init.ts).
369
- harnesses which harness targets \`spex materialize\` delivers into — native ids ("claude"|"codex") or a
370
- { "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
371
450
  git-transactional: the edit takes effect at the next git-native materialize anchor (the commit
372
451
  that carries it, a checkout/merge that receives it, or a manual \`spex materialize\`) — a
373
452
  deselected harness's artifacts are pruned by that pass.`
@@ -376,9 +455,11 @@ const FOOTPRINT = `spex guide footprint — what SpexCode plants in a repo, and
376
455
 
377
456
  SpexCode claims software engineering's HEAD (the recording of intent) and TAIL (the storage of
378
457
  measurement) and leaves the MIDDLE — construction — to the harness/agent/test framework; freshness
379
- stitches the two ends into a closed loop. The footprint follows: the head+tail (.spec, spexcode.json,
380
- evals) is the ASSET and lives in git like source; everything else is derived wiring or a machine fact.
381
- Materialized artifacts carry no facts, so they are NEVER tracked there is exactly one residence
458
+ stitches the two ends into a closed loop. Materialize is the base operation of harness ADAPTATION:
459
+ one pass renders the spec tree into whatever artifacts the selected harness auto-discovers, so that
460
+ is how SpexCode reaches an agent never a launch-time flag. The footprint follows: the head+tail
461
+ (.spec, spexcode.json, evals) is the ASSET and lives in git like source; everything else is derived
462
+ wiring or a machine fact. Materialized artifacts carry no facts, so they are NEVER tracked — there is exactly one residence
382
463
  behavior, decided per KIND (and, for a contract file, by its live CONTENT).
383
464
 
384
465
  ── THE FOUR KINDS (all fixed) ──
@@ -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') {