@spexcode/spec-cli 0.6.5

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 (201) hide show
  1. package/README.md +95 -0
  2. package/bin/spex.mjs +76 -0
  3. package/dist/attach.d.ts +2 -0
  4. package/dist/attach.js +48 -0
  5. package/dist/claude-headless.d.ts +33 -0
  6. package/dist/claude-headless.js +307 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +1859 -0
  9. package/dist/client.d.ts +111 -0
  10. package/dist/client.js +476 -0
  11. package/dist/cockpit.d.ts +7 -0
  12. package/dist/cockpit.js +26 -0
  13. package/dist/codex-headless.d.ts +1 -0
  14. package/dist/codex-headless.js +7 -0
  15. package/dist/codex-runtime-generations.d.ts +53 -0
  16. package/dist/codex-runtime-generations.js +686 -0
  17. package/dist/commit-surgery.d.ts +1 -0
  18. package/dist/commit-surgery.js +90 -0
  19. package/dist/contract-filter.d.ts +14 -0
  20. package/dist/contract-filter.js +219 -0
  21. package/dist/dashboard-assets.d.ts +6 -0
  22. package/dist/dashboard-assets.js +50 -0
  23. package/dist/delivery-queue.d.ts +23 -0
  24. package/dist/delivery-queue.js +179 -0
  25. package/dist/doctor.d.ts +9 -0
  26. package/dist/doctor.js +547 -0
  27. package/dist/eval-host.d.ts +1 -0
  28. package/dist/eval-host.js +19 -0
  29. package/dist/execution-trace.d.ts +26 -0
  30. package/dist/execution-trace.js +475 -0
  31. package/dist/file-write.d.ts +2 -0
  32. package/dist/file-write.js +26 -0
  33. package/dist/flat.d.ts +89 -0
  34. package/dist/flat.js +1076 -0
  35. package/dist/gateway-auth.d.ts +56 -0
  36. package/dist/gateway-auth.js +168 -0
  37. package/dist/gateway-hub.d.ts +28 -0
  38. package/dist/gateway-hub.js +361 -0
  39. package/dist/gateway.d.ts +37 -0
  40. package/dist/gateway.js +503 -0
  41. package/dist/graphCache.d.ts +27 -0
  42. package/dist/graphCache.js +716 -0
  43. package/dist/graphSnapshot.d.ts +14 -0
  44. package/dist/graphSnapshot.js +35 -0
  45. package/dist/graphStream.d.ts +85 -0
  46. package/dist/graphStream.js +1195 -0
  47. package/dist/guidance-catalog.d.ts +64 -0
  48. package/dist/guidance-catalog.js +166 -0
  49. package/dist/guide.d.ts +8 -0
  50. package/dist/guide.js +684 -0
  51. package/dist/harness-select.d.ts +18 -0
  52. package/dist/harness-select.js +62 -0
  53. package/dist/harness.d.ts +325 -0
  54. package/dist/harness.js +3026 -0
  55. package/dist/headless-controller.d.ts +9 -0
  56. package/dist/headless-controller.js +47 -0
  57. package/dist/help.d.ts +10 -0
  58. package/dist/help.js +574 -0
  59. package/dist/hook-prompts.d.ts +18 -0
  60. package/dist/hook-prompts.js +122 -0
  61. package/dist/hooks.d.ts +1 -0
  62. package/dist/hooks.js +27 -0
  63. package/dist/host-resources.d.ts +97 -0
  64. package/dist/host-resources.js +795 -0
  65. package/dist/host.d.ts +88 -0
  66. package/dist/host.js +663 -0
  67. package/dist/index.d.ts +1 -0
  68. package/dist/index.js +899 -0
  69. package/dist/init.d.ts +1 -0
  70. package/dist/init.js +284 -0
  71. package/dist/issues-cli.d.ts +6 -0
  72. package/dist/issues-cli.js +334 -0
  73. package/dist/issues.d.ts +69 -0
  74. package/dist/issues.js +189 -0
  75. package/dist/lint.d.ts +34 -0
  76. package/dist/lint.js +476 -0
  77. package/dist/listen.d.ts +7 -0
  78. package/dist/listen.js +24 -0
  79. package/dist/localIssues.d.ts +65 -0
  80. package/dist/localIssues.js +582 -0
  81. package/dist/login-page.d.ts +5 -0
  82. package/dist/login-page.js +86 -0
  83. package/dist/loop-in.d.ts +23 -0
  84. package/dist/loop-in.js +68 -0
  85. package/dist/machine-peer.d.ts +69 -0
  86. package/dist/machine-peer.js +603 -0
  87. package/dist/materialize.d.ts +18 -0
  88. package/dist/materialize.js +468 -0
  89. package/dist/mentions.d.ts +50 -0
  90. package/dist/mentions.js +117 -0
  91. package/dist/opencode-headless.d.ts +4 -0
  92. package/dist/opencode-headless.js +208 -0
  93. package/dist/opencode.d.ts +3 -0
  94. package/dist/opencode.js +142 -0
  95. package/dist/pi-harness.d.ts +3 -0
  96. package/dist/pi-harness.js +129 -0
  97. package/dist/pi-headless.d.ts +25 -0
  98. package/dist/pi-headless.js +196 -0
  99. package/dist/plugin-harness.d.ts +21 -0
  100. package/dist/plugin-harness.js +146 -0
  101. package/dist/pty-bridge.d.ts +10 -0
  102. package/dist/pty-bridge.js +404 -0
  103. package/dist/pty-helper.d.mts +1 -0
  104. package/dist/pty-helper.mjs +101 -0
  105. package/dist/pty-native-helper.d.mts +2 -0
  106. package/dist/pty-native-helper.mjs +23 -0
  107. package/dist/public-graph.d.ts +46 -0
  108. package/dist/public-graph.js +56 -0
  109. package/dist/ranker.d.ts +15 -0
  110. package/dist/ranker.js +188 -0
  111. package/dist/reaper.d.ts +6 -0
  112. package/dist/reaper.js +81 -0
  113. package/dist/reviews.d.ts +189 -0
  114. package/dist/reviews.js +350 -0
  115. package/dist/runtime-guard.d.ts +6 -0
  116. package/dist/runtime-guard.js +37 -0
  117. package/dist/runtime-ownership.d.ts +21 -0
  118. package/dist/runtime-ownership.js +84 -0
  119. package/dist/search.bench.d.mts +1 -0
  120. package/dist/search.bench.mjs +75 -0
  121. package/dist/search.d.ts +20 -0
  122. package/dist/search.js +61 -0
  123. package/dist/session-cursors.d.ts +14 -0
  124. package/dist/session-cursors.js +82 -0
  125. package/dist/session-declarations.d.ts +13 -0
  126. package/dist/session-declarations.js +126 -0
  127. package/dist/session-execution.d.ts +10 -0
  128. package/dist/session-execution.js +70 -0
  129. package/dist/session-files.d.ts +28 -0
  130. package/dist/session-files.js +130 -0
  131. package/dist/session-follow.d.ts +31 -0
  132. package/dist/session-follow.js +179 -0
  133. package/dist/session-reparent.d.ts +10 -0
  134. package/dist/session-reparent.js +33 -0
  135. package/dist/session-timeline.d.ts +51 -0
  136. package/dist/session-timeline.js +233 -0
  137. package/dist/session-web.d.ts +24 -0
  138. package/dist/session-web.js +141 -0
  139. package/dist/sessions.d.ts +412 -0
  140. package/dist/sessions.js +4239 -0
  141. package/dist/sh.d.ts +1 -0
  142. package/dist/sh.js +3 -0
  143. package/dist/shim-runtime.d.ts +1 -0
  144. package/dist/shim-runtime.js +161 -0
  145. package/dist/slash-commands.d.ts +9 -0
  146. package/dist/slash-commands.js +307 -0
  147. package/dist/source-files.d.ts +9 -0
  148. package/dist/source-files.js +113 -0
  149. package/dist/supervise.d.ts +1 -0
  150. package/dist/supervise.js +308 -0
  151. package/dist/tree.d.ts +25 -0
  152. package/dist/tree.js +98 -0
  153. package/dist/tsx-bin.d.ts +3 -0
  154. package/dist/tsx-bin.js +41 -0
  155. package/dist/uninstall.d.ts +3 -0
  156. package/dist/uninstall.js +177 -0
  157. package/dist/uploads.d.ts +36 -0
  158. package/dist/uploads.js +243 -0
  159. package/dist/worktree-sources.d.ts +1 -0
  160. package/dist/worktree-sources.js +47 -0
  161. package/hooks/compat/mark-active-0.5.2-eef1.fixture +53 -0
  162. package/hooks/compat/mark-active-sed-v0.fixture +46 -0
  163. package/hooks/dispatch.sh +105 -0
  164. package/hooks/harness.sh +272 -0
  165. package/package.json +47 -0
  166. package/templates/hooks/commit-msg +14 -0
  167. package/templates/hooks/post-checkout +25 -0
  168. package/templates/hooks/post-merge +34 -0
  169. package/templates/hooks/pre-commit +120 -0
  170. package/templates/hooks/prepare-commit-msg +35 -0
  171. package/templates/hooks/reference-transaction +105 -0
  172. package/templates/spec/project/.plugins/commands/extract/spec.md +66 -0
  173. package/templates/spec/project/.plugins/commands/regroup/spec.md +25 -0
  174. package/templates/spec/project/.plugins/commands/rename/spec.md +11 -0
  175. package/templates/spec/project/.plugins/commands/spec.md +30 -0
  176. package/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
  177. package/templates/spec/project/.plugins/commands/tidy/spec.md +43 -0
  178. package/templates/spec/project/.plugins/core/comment-altitude/spec.md +9 -0
  179. package/templates/spec/project/.plugins/core/idle/idle.sh +15 -0
  180. package/templates/spec/project/.plugins/core/idle/spec.md +13 -0
  181. package/templates/spec/project/.plugins/core/mark-active/mark-active.sh +59 -0
  182. package/templates/spec/project/.plugins/core/mark-active/spec.md +22 -0
  183. package/templates/spec/project/.plugins/core/session-fail/fail.sh +12 -0
  184. package/templates/spec/project/.plugins/core/session-fail/spec.md +15 -0
  185. package/templates/spec/project/.plugins/core/spec-first/spec-first.sh +43 -0
  186. package/templates/spec/project/.plugins/core/spec-first/spec.md +17 -0
  187. package/templates/spec/project/.plugins/core/spec-of-file/spec-of-file.sh +66 -0
  188. package/templates/spec/project/.plugins/core/spec-of-file/spec.md +15 -0
  189. package/templates/spec/project/.plugins/core/spec.md +22 -0
  190. package/templates/spec/project/.plugins/core/stop-gate/spec.md +23 -0
  191. package/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +149 -0
  192. package/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +15 -0
  193. package/templates/spec/project/.plugins/prompts/forge-link/spec.md +10 -0
  194. package/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +13 -0
  195. package/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +12 -0
  196. package/templates/spec/project/.plugins/prompts/spec.md +24 -0
  197. package/templates/spec/project/.plugins/skills/distill/digest.mjs +136 -0
  198. package/templates/spec/project/.plugins/skills/distill/spec.md +67 -0
  199. package/templates/spec/project/.plugins/skills/spec.md +19 -0
  200. package/templates/spec/project/.plugins/spec.md +23 -0
  201. package/templates/spec/project/spec.md +20 -0
package/dist/guide.js ADDED
@@ -0,0 +1,684 @@
1
+ const SETUP = `spex guide — run SpexCode on your own repo
2
+
3
+ The product model: install SpexCode ONCE, then use it across all your projects — an agent drives
4
+ the rest, you don't hand-author the spec tree or wire the dashboard yourself.
5
+
6
+ 1. Install the CLI (one-time, global — ONE install serves every project)
7
+ npm i -g spexcode # ONE command lands on PATH: \`spex\` (Node ≥ 22)
8
+ Project-scoped commands operate on the repo of your current directory; the dashboard is host-scoped.
9
+ (Dogfooding an unpublished HEAD from a source checkout? \`npm link\` at the repo ROOT — that links
10
+ the \`spexcode\` package itself, never the internal @spexcode/spec-cli. Both paths own the same
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\`). 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
+
16
+ 2. Adopt a repo
17
+ cd <your-repo> && spex init --harness claude,codex,opencode,pi,zcode,claude-headless,opencode-headless,pi-headless,codex-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).
21
+ Works on any git repo. Edit .spec/project/spec.md to describe it, then grow child nodes
22
+ (each a dir with a spec.md + a \`code:\` list of the files it governs).
23
+
24
+ 3. Run this repo's backend — it reads .spec + git from cwd and registers in this user's host registry
25
+ spex serve # http://localhost:8787 (--port <n> for another endpoint)
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
+
35
+ 5. Govern your layout (optional)
36
+ spexcode.json sets lint's governedRoots/include/exclude/test source policy and any non-default layout.
37
+ \`spex spec lint\` must report 0 errors; coverage warnings are your adoption TODO (files no node claims yet).
38
+
39
+ Look these up on demand — the formats an agent authors, and the settings it configures:
40
+ spex guide spec the spec.md format (frontmatter + body + the rules lint enforces)
41
+ spex guide eval the eval.md format (scenario schema + how loss is measured and filed)
42
+ spex guide settings the spexcode.json / spexcode.local.json settings (launchers, dashboard icon, upload,
43
+ lint budgets, layout) — every field, and which of the two files it belongs in
44
+ spex guide footprint the footprint model — what SpexCode plants in a repo, and who sees it
45
+ (committed | ignored | hidden), and every migration recipe`;
46
+ const SPEC = `spex guide spec — the spec.md file format
47
+
48
+ A spec node is a DIRECTORY under .spec/<project>/…/<id>/ holding a spec.md. The node's id is its leaf dir
49
+ name when that is globally unique, else the shortest parent-qualified path-suffix that disambiguates (so ids
50
+ are unique by construction) — the same id \`spex graph\`, \`spec ack\`, and a node/<id> branch use. A spec states a node's PRESENT
51
+ intent at CONTRACT altitude — what it guarantees and why — and is rewritten in place as intent changes;
52
+ version history is git's job, never a changelog in the body.
53
+
54
+ FRONTMATTER (YAML between the opening and closing --- lines; every field optional, sensible defaults):
55
+ title display name. Defaults to the dir id.
56
+ desc one-line summary shown on the graph.
57
+ hue node colour on the graph, 0–360. Default 210.
58
+ status pending | active | merged | drift. Usually DERIVED from git state — rarely hand-set.
59
+ code: the file this node GOVERNS (is source of truth for) — a YAML list, but AT MOST ONE entry
60
+ (the \`one-govern\` lint error otherwise: keep the true subject, move the rest to related:).
61
+ Drives drift + eval freshness. Many nodes MAY govern the same file (ordinary
62
+ composition); a file governed by > maxOwners nodes warns (the \`owners\` rule — split it). Omit
63
+ for a pure-prose node: a cross-cutting contract no file owns.
64
+ The entry may pin named units — ANCHORS: one or more \`path#symbol\` rows, ALL on the same
65
+ base file (\`#Class.method\` for a class method; top-level functions, arrow/const declarations,
66
+ classes, enums anchor cleanly; a type/interface anchor warns). One-govern counts DISTINCT
67
+ base paths, so selectors never widen govern past one file. Several selectors are OR: a
68
+ commit hitting ANY upgrades drift to the blocking \`anchor-drift\` error (one error per
69
+ entry, naming the hit selectors — a commit counts once). Without an anchor, drift stays
70
+ advisory forever. A base path is either whole-file (bare) or selector-scoped, never both;
71
+ duplicates, globs/directories with a selector, and dead/ambiguous units all error loud. A
72
+ selector-scoped governor claims units, not the file, so it stays out of the \`owners\` bound
73
+ (spex spec owner still displays it, marked "(scoped)"). Anchors are optional.
74
+ Python \`.py\`/\`.pyi\` anchors recognize structural \`def\`, \`async def\`, and \`class\`
75
+ declarations. Methods and nested declarations use lexical qualified names such as
76
+ \`Class.method\`, \`outer.inner\`, or \`Outer.Inner.method\`; attached decorators belong to
77
+ the declaration's range. Runtime-created/assigned callables, imported aliases, and generated
78
+ names are outside this declaration extractor and therefore resolve as dead anchors.
79
+ related: files this node REFERENCES but does not own — a YAML list, same path forms. Carries coverage
80
+ (never drift, never eval freshness, nothing to ack); it is the many-to-many net that claims the files
81
+ govern doesn't. Every listed path must exist (lint integrity error otherwise). A related row
82
+ may also pin \`path#symbol\`: the node then hears about a commit ONLY when it moves that
83
+ unit — a hit is a soft \`related-drift\` warn naming the selector, a miss is SILENT (a scoped
84
+ related file's ordinary file-level nudge is off). Still never blocks, no ack, no eval freshness.
85
+ surface plugin-system/.plugins nodes only: one or MORE of system (folded into every agent's prompt) |
86
+ command (a /command) | skill (an on-demand SKILL.md the harness loads when a task matches the
87
+ node's desc) | agent (a spawnable sub-agent definition; its \`tools:\` list is the spawned
88
+ agent's tool allowlist) | hook (a lifecycle hook handler — a co-located script the dispatcher
89
+ runs on the harness events in events:, ordered by order:, blocking when block: true).
90
+ Comma-list several to plug into each. hook nodes may nest under a grouping
91
+ plugin (e.g. .plugins/core/<id>); surface is a field, discovered recursively.
92
+ events hook surface only: harness lifecycle events this node binds (YAML list — PreToolUse, Stop, …).
93
+ order hook surface only: integer; the dispatcher runs same-event hooks low to high.
94
+ block hook surface only: true if the hook may block its event (honored only on block-capable events).
95
+
96
+ BODY (Markdown after the frontmatter): the contract — intent, invariants, outward behaviour; NOT how the
97
+ code does it. Two optional level-2 headings split ground truth from detail:
98
+ ## raw source human-authored, rarely-changed intent — the loss function's target.
99
+ ## expanded spec agent-authored detail that must keep serving the raw source.
100
+ Bodies without those headings are read whole. Link sibling nodes with [[node-id]] — every link must name
101
+ a REAL node (lint's mention rule; backtick a placeholder like \`[[node]]\` so it reads as sample text).
102
+
103
+ COMMENT ALTITUDE: specs own intent, invariants, policy, and observable contracts; comments only navigate
104
+ non-obvious local decisions. Put contract or intent in the owning spec body, not a code comment. Keep a short
105
+ nearby comment only for ordering, platform behavior, measured pitfalls, or why a plausible alternative is
106
+ unsafe; delete commentary that merely translates code or repeats a name/type. Preserve measured values,
107
+ version-specific behavior, and rejected alternatives not in the spec. \`@@@title - explanation\` is reserved for
108
+ genuinely tricky surviving local reasoning.
109
+
110
+ WHAT lint CHECKS (spex spec lint; the pre-commit hook gates on errors):
111
+ integrity (error) every code:/related: path exists — and every anchor RESOLVES: a dead anchor (unit
112
+ deleted/renamed), an ambiguous one (two same-named units in one file), a file that
113
+ no longer parses, a language with no designated extractor, or an extractor that
114
+ can't run here (e.g. no host typescript — 'npm i -D typescript' or drop the anchor)
115
+ all error, never silently pass. So do a relation's STRUCTURAL defects: a duplicate
116
+ entry, a base path both bare and selector-scoped, or a selector on a
117
+ glob/directory.
118
+ anchor-drift (error) a commit since the node's version intersected an ANCHORED unit's lines (judged
119
+ from the file as it existed AT each commit) and no Spec-OK ack covers it — the
120
+ blocking tier of drift. Selectors on one file are OR'd: one error per entry, the
121
+ hit selectors named, each commit counted once. Remedy: update the spec, or
122
+ \`spex spec ack\` with a real reason (recorded in the ack commit body).
123
+ one-govern (error) a node governs (code:) at most ONE file — keep the true subject, move the rest
124
+ to related:.
125
+ living (error) no "## vN" changelog headings — the body is current-state.
126
+ id-format (error) each id char is ascii [a-z0-9-] or a non-ascii unicode letter/number (CJK ok; one
127
+ optional leading dot; no space / '/' / '_' / uppercase Latin), and its leaf dir name
128
+ is unique tree-wide.
129
+ mention (error) every [[node-id]] in prose names a real node (fenced/backticked samples exempt).
130
+ coverage (warn) every source file is claimed by ≥1 node — via code: OR related: (related is the net).
131
+ drift (warn) a governed file has commits newer than the node's spec version — it may be stale.
132
+ ALWAYS advisory: unanchored drift never blocks a commit (the blocking tier is
133
+ anchor-drift above). On a selector-SCOPED code file whose window has NO hit (a
134
+ miss), this advisory stays by default; the committed \`lint.scopedCodeMiss:
135
+ "ignore"\` silences ONLY it (hit blocks, bare drift, integrity, acks, related,
136
+ eval freshness all untouched). Remedy: edit the spec to the new intent
137
+ (re-versions the node), OR \`spex spec ack <node> --reason "…"\` when only
138
+ mechanics changed and the contract still holds.
139
+ anchor (warn) an anchor pins a type/interface — types reshape with every refactor; anchor the
140
+ behaviour-bearing unit instead.
141
+ related-drift (warn) a related: file moved ahead of the node — a soft nudge, one summary line, never
142
+ blocks. A selector-scoped related row instead warns per HIT (selector named);
143
+ its file-level misses are silent.
144
+ owners (warn) a file governed WHOLE-FILE by > maxOwners nodes (default 3) does too much — SPLIT
145
+ it so each governor owns its own module (or merge the nodes, or give it one
146
+ foundation owner). Selector-scoped governors don't count toward the bound.
147
+ confusable-id (warn) two leaf ids one edit apart read as the same word — rename one to read apart.
148
+
149
+ Heuristic spec health is deliberately outside this production gate. Bare \`spex doctor\` reports opt-in
150
+ altitude and breadth findings; the tidy workflow consumes that report and adds semantic judgment.
151
+
152
+ LIFECYCLE: author each node on a node/<id> branch, one node per commit; \`spex spec lint\` must reach 0 errors
153
+ before merge. \`spex init\` seeds the first tree; \`spex guide eval\` covers the sibling eval.md, the measurement file.
154
+
155
+ SHARED LANDING: if the shared checkout is mid-merge, wait. Never abort or resolve someone else's merge; if your
156
+ own landing stops half-merged, abort it and report.`;
157
+ const EVAL = `spex guide eval — the eval.md file format
158
+
159
+ An eval.md sits BESIDE a node's spec.md and says how to MEASURE the node's loss — the gap between live
160
+ behaviour and the spec. It is optional, but a node that governs a file admitted by lint's shared tracked-text
161
+ include-minus-exclude/test policy with no eval.md is
162
+ a blind spot: \`spex eval lint\` flags it \`eval-coverage\`. The eval system defines no DSL and RUNS
163
+ NOTHING — the agent measures; eval keeps score.
164
+
165
+ FRONTMATTER: a \`scenarios:\` list (a YAML block sequence of mappings). Each scenario:
166
+ name REQUIRED. Unique within the file — it keys the sidecar and \`--scenario <name>\`.
167
+ description REQUIRED. What to check / how to measure it through the running product.
168
+ expected REQUIRED. What ZERO loss looks like — the target the measurement is compared against.
169
+ tags REQUIRED. ≥1 classification tag (a comma list / flow list \`[a, b]\`), each drawn from the
170
+ configured library (\`lint.scenarioTags\` in spexcode.json; ships
171
+ \`frontend-e2e, backend-api, cli, desktop, mobile\`). A tag outside the library is rejected —
172
+ use an existing one, or add it to the library to mint it. Tags classify a scenario (surface,
173
+ device) so it can be filtered and, later, routed to the right driver.
174
+ test optional. Either a repo-path scalar (the backward-compatible shorthand) or a strict object:
175
+ test:
176
+ path: tests/auth.spec.ts
177
+ name: rejects an expired session
178
+ Both forms normalize in JSON to \`{ "path": "..." }\` with optional \`"name"\`. The object
179
+ requires exactly \`path\` + \`name\`; its case name is opaque text preserved for the measuring
180
+ hand. The path must exist. SpexCode does not parse WDIO/Playwright or execute anything.
181
+ code optional. The file THIS scenario GOVERNS, ideally one (a comma list / flow list \`[a, b]\` is
182
+ allowed) — its own slice of the code freshness axis, so scenarios on one node go stale
183
+ independently. Absent → it inherits the node's \`code:\` list. A file governed by > maxOwners
184
+ scenarios warns \`eval-owners\` (split it). Each path must exist (a ghost → \`eval-schema\`).
185
+ related optional. Files this scenario REFERENCES but does not govern — same path forms. They do NOT
186
+ stale it (the freshness mirror of a spec node's govern/related). Each path must exist.
187
+ Multi-line prose uses YAML block scalars: \`|\` keeps newlines, \`>\` folds wrapped lines to spaces.
188
+ An eval.md OWNS nothing — only its scenarios govern and relate (see governed-related).
189
+
190
+ THE SCHEMA IS ENFORCED (closed field set, four required fields, unique names, tags within the library). A
191
+ missing required field, an unknown key (a typo like \`descripton:\`), a duplicate name, an out-of-library
192
+ tag, or no scenarios at all is rejected LOUD: \`spex eval lint\` reports it as \`eval-schema\`, and the
193
+ pre-commit \`internal check-staged\` BLOCKS the commit.
194
+
195
+ BODY (after the frontmatter): prose naming the measurement method — YATU ("You As The User"): the agent
196
+ looks at / calls the real product surface, not an internal helper chosen to make the evidence easy.
197
+
198
+ REPAIR EVIDENCE: for a bug fix, use one scenario's fail→pass pair. A, BEFORE EDITING: find the violated scenario
199
+ or add one to \`eval.md\`, run it against the old committed behavior, and file
200
+ \`spex eval add <node> --scenario <s> --fail\` with evidence of the failure. B, AFTER EDITING: run that same
201
+ scenario against the working tree until it passes; commit the verified tree; then file
202
+ \`spex eval add <node> --scenario <s> --pass\`. The measurement's \`codeSha\` must be that commit. New intent has no
203
+ prior failure to reproduce.
204
+
205
+ MEASURING AND FILING: the agent runs the scenario however it likes (a browser run, an API
206
+ transcript, a by-hand pass), compares the result to \`expected\`, and files it:
207
+ spex eval add <node> [--scenario <name>] (--pass | --fail) [--note <text>]
208
+ [--image <png> …repeatable] [--result <txt>|-] [--video <webm|mp4>] [--timeline <json>]
209
+ (--scenario may be omitted only when the node declares exactly one scenario.)
210
+ The verdict is \`--pass\` or \`--fail\` (a measurement must commit to one — an unmeasured scenario is \`missing\`,
211
+ not a hedged fail). \`--note <text>\` is an OPTIONAL one-line annotation on either (why it failed, how far a
212
+ pass sits from ideal); it does NOT replace evidence — the image/video/transcript is the captured actual behaviour.
213
+ PICK THE EVIDENCE KIND BY WHAT THE BEHAVIOUR DOES OVER TIME:
214
+ MOVES / is timed → \`--video <webm|mp4>\`. Terminal scroll or redraw, an animation or transition, media
215
+ playback, a multi-step interaction flow, keyboard timing — a still of a moving thing
216
+ proves the wrong thing; RECORD the run (e.g. playwright \`recordVideo\` on the context).
217
+ STEP EVIDENCE gets a STEP-MAP: when the evidence unfolds in steps, carry named steps
218
+ anchored to a POSITION on the evidence's OWN axis, EXPORTED BY THE RUN that produced it
219
+ — never a value the agent eyeballs off the finished artefact afterwards (that's
220
+ misaligned and dishonest, worse than none). \`--timeline <json>\` carries one; its \`axis\`
221
+ is the evidence's: a video is \`time\` (ms), a transcript \`line\`, a still SEQUENCE \`frame\`,
222
+ a structured data export \`index\` (record ordinals; the format's axis set is open — an
223
+ unknown axis just renders as a bare number).
224
+ \`at\` = the position on that axis, \`step\` = a short name for that moment; copy this shape:
225
+ { "v": 2, "axis": "time",
226
+ "events": [ { "at": 0, "step": "open graph" },
227
+ { "at": 1200, "step": "type query" } ] }
228
+ The run exports it: in whatever drives the evidence — Playwright, a computer-use hand, a
229
+ CLI harness stamping line numbers — take a baseline and at EACH real step push
230
+ \`{ at: <position>, step: "…" }\`; dump that array as \`--timeline\`. Its \`axis\` MUST match the
231
+ evidence it rides (a \`line\` map needs a \`--result\` transcript, a \`time\` map a \`--video\`);
232
+ skip it for a short single-step artefact. A step name is a SHORT human label for its
233
+ moment ("open graph") — never a metadata channel: the run's identity, its verdict, and
234
+ the evidence's extent already have canonical homes (the scenario's \`test:\` field, the
235
+ measurement's --pass/--fail, the artefact itself), so a step like
236
+ "runner start: path/to/spec.ts :: <full case title>" smuggles data the measurement already
237
+ carries and turns the step ruler into noise. (Legacy \`{ "v": 1, "events": [{ "tMs" }] }\` — the
238
+ time axis with \`tMs\` — is still accepted, read as \`axis: "time"\`.)
239
+ STATIC end state → \`--image <png>\` (repeatable — N stills). Layout, an icon, copy, one rendered frame.
240
+ backend / CLI → \`--result <txt>\` (a transcript; \`-\` reads stdin). A STRUCTURED export (a tool's
241
+ \`--export-json\` dump, an API payload, a metrics dump) is recognized BY CONTENT and kept as
242
+ \`data\` — rendered as a validatable data block, not flattened into scrolling transcript
243
+ text; free-form output stays a transcript. You pick the flag; the KIND follows the bytes.
244
+ The flags combine in ONE filing — several stills can ride beside the clip of the same run.
245
+ POPULATION DISCIPLINE: when \`expected\` quantifies over a set ("every X is Y", "nothing under Z"), the
246
+ measurement reports the set's SIZE, not only the verdict. An empty set satisfies a universal claim
247
+ VACUOUSLY, so a pass over nothing and a real pass are indistinguishable — and the universal form reads
248
+ STRICTER than an enumeration, which is what makes it the hardest wrong-population case to catch. File
249
+ \`N of N\`, never a bare boolean: \`0 of 0\` shows itself, \`true\` does not. The denominator must come from a
250
+ source that can DISAGREE with the numerator — count the population off the surface that does NOT decide the
251
+ outcome (a backend export, a ledger, the enumeration upstream of the thing under test) and the passing members
252
+ off the surface under test. A ratio whose halves share one source says only "what I selected, I selected": a
253
+ selector that silently drops half the population reports \`3 of 3\` when the truth is 6. Two weaker defences
254
+ that do not substitute — a precondition sentence depends on the next author remembering it, a printed
255
+ denominator depends on a reader noticing it. Nothing here enforces either (the schema has no population
256
+ field), so treat a zero-population run as a NON-measurement and do not file it. Cheaper than any of that, when
257
+ you can get it: restate the claim over something the product CANNOT make empty. "every active node's name is
258
+ readable" needs someone to arrange activity and goes vacuous when nobody does; "the rendered size never falls
259
+ below the authored size" is a property of the viewport itself, true of a one-node graph, and has no
260
+ population to get wrong. A claim with no population beats a well-reported one.
261
+ RENDERED GEOMETRY (browser): measure the rendered BOX, never the authored STYLE. An ancestor CSS \`transform\`
262
+ — the ordinary zoom/pan wrapper on a canvas or graph view — does NOT change computed style, so
263
+ \`getComputedStyle(el).fontSize\` answers the AUTHORED size while the screen shows that size times the
264
+ ancestor scale, and the measurement is a SILENT FALSE PASS. (No numbers here on purpose: the authored size is
265
+ whatever this project's stylesheet says today, and a manual that hard-codes one teaches a constant that
266
+ drifts — read it off the tree you are measuring.) Go through \`getBoundingClientRect()\` (it carries the
267
+ ancestor scale) and derive the effective size from the box, or from a known string's width. This is also why
268
+ a geometric claim ships WITH its \`--image\`: a rect can be computed wrong, while "is that text legible" is
269
+ human-judgeable, so the two evidences cover each other.
270
+ ANCHOR DISCIPLINE: an eval's \`codeSha\` is HEAD at filing time, and a git sha names only a COMMIT — an
271
+ uncommitted change has none. So measure the tree you are about to commit, COMMIT it, then file; confidence
272
+ is earned on the working tree, but the anchor can only land after the commit. Filing from a dirty tree
273
+ mis-anchors the eval (its sha lacks the change it measured) and it goes stale the moment you commit.
274
+
275
+ A botched filing (a junk e2e/smoke run, a wrong verdict) is undone through the SAME surface:
276
+ spex eval retract <node> [--scenario <name>] [--last | --ts <iso>] [--note <why>]
277
+ retract APPENDS a retraction event to the sidecar (never deletes a line — the trace stays, git records
278
+ who/when/why); the scoreboard then drops the retracted eval everywhere: the previous eval becomes
279
+ the latest again, or the scenario honestly returns to \`missing\`. Default target is the scenario's latest
280
+ eval (\`--last\` makes that explicit; repeat to peel junk back one filing at a time); \`--ts\` pins one.
281
+
282
+ THE SCOREBOARD: evals live in evals.ndjson beside the eval.md — one JSON line per measurement
283
+ (a second git-as-database axis). Freshness is derived live from git: an eval goes STALE when a governed
284
+ code file or the scenario (the eval.md) moves since it was filed.
285
+ spex eval lint [--changed] the measurement layer's findings — PURE ADVISORY, always exit 0 (spec
286
+ lint's errors block commits; a measurement gap never blocks anyone):
287
+ eval-schema (malformed) · eval-drift (stale) · eval-missing (never
288
+ measured) · eval-dangling (orphaned remark track) · eval-coverage
289
+ (governed source, no eval.md — spec lint's coverage, one rule per layer) ·
290
+ eval-owners (a file governed by > maxOwners scenarios — split it)
291
+ spex eval ls <node> the eval timeline (verdict · freshness · evidence), newest first
292
+ spex eval scenario ls [<node>] the declared contracts; text --unmeasured = blind-spot worklist; --json = canonical declaration index
293
+ spex eval clean GC the content-addressed evidence cache`;
294
+ const UPLOAD_DEFAULTS = uploadPolicyDefaults();
295
+ const SETTINGS = `spex guide settings — SpexCode's runtime settings (spexcode.json / spexcode.local.json)
296
+
297
+ SpexCode reads PROJECT runtime settings from TWO optional JSON files at the repo root. There is no imperative
298
+ settings verb — an agent CONFIGURES SpexCode by EDITING these files directly. The two split by
299
+ PORTABILITY, and picking the right one is the whole discipline:
300
+
301
+ spexcode.json COMMITTED — portable, shared by everyone on the repo. Layout, upload policy,
302
+ dashboard identity and launcher visibility, lint policy, resource and doctor health budgets, launcher NAMES. "Git is the database": tracked so the
303
+ team shares ONE configuration.
304
+ spexcode.local.json GITIGNORED — host-specific, never committed. Absolute launcher paths, cert/secret
305
+ paths. Layered OVER spexcode.json (see MERGE
306
+ below); a targeted env override (SPEXCODE_CODEX_SERVER_CMD, …) still wins at its read site.
307
+
308
+ Rule of thumb — is the value TRUE FOR THE PROJECT or TRUE FOR THIS MACHINE? A branch name, a dashboard
309
+ icon or launcher-visibility policy, upload policy, lint policy, resource and doctor health budgets, and a launcher's name+harness are project facts → committed spexcode.json. The ABSOLUTE
310
+ PATH of a launcher wrapper or a TLS cert path are machine facts → gitignored spexcode.local.json.
311
+ Both files are optional; omit any field to take its default, except \`sessions.defaultLauncher\` when using
312
+ \`spex session new\` or the dashboard without an explicit launcher choice.
313
+
314
+ The host-wide gateway has one separate per-user setting, \`gateway.icon\` in
315
+ \`$SPEXCODE_HOME/config.json\`. It is documented below and never belongs to either project file.
316
+
317
+ MERGE: spexcode.local.json is layered over spexcode.json ONE LEVEL DEEP — per top-level section (dashboard,
318
+ uploads, sessions, …), the two objects are shallow-merged with LOCAL WINNING per key; sections only one file names
319
+ pass through untouched. This is exactly what lets a launcher's portable NAME reference (defaultLauncher)
320
+ sit in the committed file while its host-specific DEFINITION (with the abs cmd) sits in the local file —
321
+ see LAUNCHERS.
322
+
323
+ ── LAYOUT (spexcode.json — portable; set only for a NON-DEFAULT repo layout) ──
324
+ main path to the source-of-truth checkout. Default: the \`main\` worktree.
325
+ mainBranch the stable source-of-truth BRANCH worktrees fork from. spex init stamps the root checkout's
326
+ branch at adoption and the stamped value stays put for every later checkout, so a clone or a
327
+ fresh worktree does not re-guess it; an older omitted value uses the conventional main.
328
+ branchPrefix how a node branch is named. Default "node/".
329
+ Example — a repo whose trunk is \`staging\`, not \`main\`:
330
+ { "mainBranch": "staging" }
331
+
332
+ ── DASHBOARD (spexcode.json — portable project identity) ──
333
+ dashboard.title browser-tab name. Default: the repo-root basename.
334
+ dashboard.icon the project's icon identity. The Projects UI writes one shared preset id:
335
+ spexcode | gateway | mdi:rocket-launch | compass | terminal | package | database | spark.
336
+ Existing emoji ("🔭"), arbitrary Iconify names ("lucide:radar"), and full URLs remain
337
+ supported; picking a preset migrates that one existing field, never creates another.
338
+ dashboard.apiUrl the per-project backend the dashboard proxies to (read frontend-side). For a SHARED
339
+ install prefer the API_URL env var; apiUrl here is the default only when the dashboard
340
+ lives inside the project.
341
+ dashboard.showHeadlessLaunchers
342
+ include launchers whose harness declares itself headless in the dashboard New Session
343
+ picker. Default: false. This changes dashboard visibility only; explicit CLI
344
+ --launcher selection can still use every configured launcher.
345
+ Example:
346
+ { "dashboard": { "title": "MyApp specs", "icon": "mdi:rocket-launch",
347
+ "showHeadlessLaunchers": false } }
348
+
349
+ ── HOST GATEWAY ($SPEXCODE_HOME/config.json — per-user host identity, never a project file) ──
350
+ gateway.icon the global /projects icon, using the same preset ids above. Default: "gateway".
351
+ The admin-only Projects picker writes this field atomically. It is the gateway's ONE
352
+ authored icon setting and is never copied into spexcode.json or spexcode.local.json.
353
+ Example:
354
+ { "gateway": { "icon": "database" } }
355
+
356
+ ── SESSIONS / WORKERS ──
357
+ sessions.maxActive concurrency cap — max agents AUTONOMOUSLY PROGRESSING at once (default 8;
358
+ precedence: spexcode.json → SPEXCODE_MAX_ACTIVE env → default; read live, so
359
+ an edit applies without a restart).
360
+ Counts compute slots, not total sessions: idle/asking/review/done do not
361
+ occupy one. A policy number → committed spexcode.json; omit it to use the
362
+ default, or tune higher/lower for the project's usual host.
363
+ sessions.launchers the NAMED launcher profiles (see LAUNCHERS). \`spex init\` seeds one entry per
364
+ SELECTED harness (--harness); edit/add more like any other.
365
+ sessions.defaultLauncher the launcher name a create with no explicit --launcher/dropdown pick uses
366
+ (required for no-choice creates). A portable NAME → committed.
367
+ A launcher \`cmd\` that is a HOST-SPECIFIC ABSOLUTE PATH belongs in spexcode.local.json — the committed file
368
+ must stay free of machine paths.
369
+
370
+ ── RESOURCE GOVERNANCE (spexcode.json — portable project budgets) ──
371
+ resources.sessionRssMiB resident-memory budget per governed session. Default 1024.
372
+ resources.backendRssMiB resident-memory budget for a backend/shared runtime owner. Default 2048.
373
+ resources.idleCpuPercent CPU budget for a non-progressing owner. Default 2.
374
+ resources.sampleMs on-demand CPU measurement window. Default 1000 (minimum 50).
375
+ resources.reportIntervalMs supervisor-owned snapshot cadence. Default 60000 (minimum 5000).
376
+ The report is read-only: \`spex session resources [--json]\`. Reclaim eligibility is advisory and the
377
+ projection never issues mutation authority or signals a process. Budgets are project policy and belong in committed spexcode.json;
378
+ host-specific tuning may override this top-level section in spexcode.local.json.
379
+
380
+ ── UPLOADS (spexcode.json — portable transfer policy; local overrides are useful for one machine's disk/network) ──
381
+ uploads.maxBytes maximum bytes in one attached file. Default ${UPLOAD_DEFAULTS.maxBytes}.
382
+ uploads.chunkBytes maximum raw PATCH body and client slice size. Default ${UPLOAD_DEFAULTS.chunkBytes}.
383
+ uploads.concurrency simultaneous attachment streams from one dashboard batch. Default ${UPLOAD_DEFAULTS.concurrency}.
384
+ uploads.requestTimeoutMs browser timeout for one chunk or completion request. Default ${UPLOAD_DEFAULTS.requestTimeoutMs}.
385
+ uploads.retryLimit automatic retries after the initial transient chunk failure. Default ${UPLOAD_DEFAULTS.retryLimit}.
386
+ uploads.retryDelayMs wait between those retries. Default ${UPLOAD_DEFAULTS.retryDelayMs}.
387
+ uploads.incompleteTtlMs idle staging lifetime before an unfinished transfer expires. Default ${UPLOAD_DEFAULTS.incompleteTtlMs}.
388
+ uploads.cleanupIntervalMs stale-staging reaper cadence. Default ${UPLOAD_DEFAULTS.cleanupIntervalMs}.
389
+ uploads.minFreeBytes bytes retained on the backend filesystem while reserving a new attachment.
390
+ Default ${UPLOAD_DEFAULTS.minFreeBytes}.
391
+ uploads.evidenceMaxBytes retained POST-body ceiling for eval evidence. Default ${UPLOAD_DEFAULTS.evidenceMaxBytes}.
392
+ All fields are positive integers except retryLimit, retryDelayMs, and minFreeBytes, which may be zero.
393
+ The seed template is the one default source; omit a field to use it. The backend reads the merged files for
394
+ each transfer and cleanup pass. The dashboard receives chunk, concurrency, timeout, and retry policy from
395
+ the create/status response, so a portable project policy takes effect without another browser setting.
396
+ Put team-wide transfer policy in committed spexcode.json; use the same keys in gitignored spexcode.local.json
397
+ only when the backend machine needs a local override. The normal top-level shallow merge lets a local
398
+ uploads.chunkBytes replace only that key.
399
+
400
+ ── LAUNCHERS (the profile block, split across the two files) ──
401
+ A named launcher profile fixes BOTH a session's harness AND its exact launch command; a create picks one
402
+ by name with --launcher/the dashboard dropdown, and the chosen name is persisted on the record so a resume
403
+ reuses the same auth. There are NO magic built-ins: \`spex init\` SEEDS an ordinary named launcher for each
404
+ harness the adopter SELECTED (--harness), from the template pool
405
+ "claude" → { "harness": "claude", "cmd": "claude" }
406
+ "claude-headless" → { "harness": "claude-headless", "cmd": "claude" }
407
+ "codex" → { "harness": "codex", "cmd": "codex" }
408
+ "codex-headless" → { "harness": "codex-headless", "cmd": "codex --yolo" }
409
+ "opencode" → { "harness": "opencode", "cmd": "opencode" }
410
+ "opencode-headless" → { "harness": "opencode-headless", "cmd": "opencode --auto" }
411
+ "pi" → { "harness": "pi", "cmd": "pi" }
412
+ "pi-headless" → { "harness": "pi-headless", "cmd": "pi" }
413
+ The interactive profiles preserve each harness's normal permission model. \`opencode-headless\` and
414
+ \`codex-headless\` are deliberate seed exceptions: their terminal-free runs require \`opencode --auto\` and
415
+ \`codex --yolo\`; interactive profiles stay plain. Other automatic-permission commands are NEVER clean-init
416
+ defaults: define and select one explicitly only when that access is intended. To run workers under an auth
417
+ wrapper (e.g. reclaude), point a launcher's \`cmd\` at it in spexcode.local.json — there is no environment
418
+ override that rewrites a launcher's command. Add more profiles when a project needs named auth/config-dir or
419
+ permission variants. Shape:
420
+ "launchers": { "<name>": { "harness": "claude" | "codex" | "opencode" | "pi" | "claude-headless" | "codex-headless" | "opencode-headless" | "pi-headless",
421
+ "cmd": "<launch command>" } }
422
+ \`harness\` defaults to "claude"; \`cmd\` is required and embedded whole. A portable plain command may live
423
+ in committed spexcode.json (as the init seeds do). A host-specific command — an absolute wrapper path,
424
+ credential route, or personal permission choice — belongs in gitignored spexcode.local.json, while its
425
+ portable defaultLauncher NAME may stay in committed spexcode.json; the merge keeps both:
426
+
427
+ spexcode.json (committed — the portable name reference)
428
+ { "sessions": { "defaultLauncher": "gpt5" } }
429
+
430
+ spexcode.local.json (gitignored — the host-specific definitions)
431
+ {
432
+ "sessions": {
433
+ "launchers": {
434
+ "gpt5": { "harness": "codex", "cmd": "/Users/me/bin/reclaude-codex --yolo" },
435
+ "claude-prod": { "harness": "claude", "cmd": "/Users/me/bin/reclaude --dangerously-skip-permissions" }
436
+ }
437
+ }
438
+ }
439
+
440
+ ── SERVE (spexcode.json ONLY — public-exposure for \`spex serve --public\`; this section is read straight
441
+ from spexcode.json, the local overlay is NOT consulted here) ──
442
+ serve.public.enabled turn public mode on without the --public flag.
443
+ serve.public.http drop TLS (the --http escape hatch) — the password then travels in cleartext.
444
+ serve.public.tls { "cert": "<path>", "key": "<path>" } — PATHS to your own cert/key; omit for a
445
+ cached self-signed default. Host-specific paths can instead ride the
446
+ --tls-cert/--tls-key flags or SPEXCODE_TLS_CERT/SPEXCODE_TLS_KEY env (which win).
447
+ The gateway password is NEVER read from these files (flag/env only), so serve.public stays committable.
448
+
449
+ ── BACKEND ROUTING (not a config field — how a \`spex\` command picks its backend) ──
450
+ One host runs many projects' backends, and a shell inherits the launching backend's SPEXCODE_API_URL —
451
+ an env var cannot prove intent (exported-on-this-command vs inherited look identical), so the client
452
+ resolves its backend per this ladder, flag first:
453
+ 1. --api <url> explicit flag on any session verb — ALWAYS wins (--port <n> is localhost
454
+ sugar for --api http://127.0.0.1:<n>).
455
+ 2a. worker (SPEXCODE_SESSION_ID set): env SPEXCODE_API_URL — the backend-injected lifeline; cwd
456
+ discovery never steals it.
457
+ 2b. human (no session id): the cwd project's RECORDED live backend — \`spex serve\` publishes
458
+ {url,pid,instanceId,root} atomically to ~/.spexcode/projects/<enc>/backend.json
459
+ at bind time; the reader health-probes before trusting (a dead record is
460
+ ignored). The same record is what \`spex dashboard\` reconciles its
461
+ host-wide project list from.
462
+ 3. the other side as fallback (human with no live record → env; worker with no env → record).
463
+ 4. default http://127.0.0.1:$PORT||8787.
464
+ WRITES are project-bound: every mutating verb (new/merge/send/close/rename/resume/stop) refuses
465
+ loudly when the resolved backend serves a DIFFERENT same-host project — an explicit --api/--port skips
466
+ the guard (the flag is the declaration of intent). Reads point anywhere.
467
+
468
+ ── ISSUES (spexcode.json — portable policy) ──
469
+ issues.enabled the issues-workflow on/off switch (default ON). OFF silences the post-merge nudge and
470
+ hides the dashboard view. Flip it by editing the JSON — there is no CLI toggle verb;
471
+ \`spex doctor\` reports the current state (and flags a legacy \`proposals.enabled\` key,
472
+ which is no longer read).
473
+
474
+ ── FORGE (spexcode.json — which forge this repo's remote is; a project fact, so committed) ──
475
+ forge.host explicit forge host id ('github' | 'gitlab' | …) overriding the automatic derivation.
476
+ Normally OMIT it: spec-forge resolves the host from the origin remote's hostname —
477
+ github.com → github, bitbucket → bitbucket, any other remote → gitlab (the common
478
+ self-hosted shape) — and only a domain the heuristic misreads needs the override.
479
+ A resolved host with no registered driver degrades to an EMPTY forge slice (local
480
+ issues still work, no error).
481
+
482
+ ── LINT (spexcode.json — a top-level "lint" key; budgets are portable, so committed only) ──
483
+ lint.governedRoots dirs whose source files must each be governed by a spec (coverage).
484
+ '.' = the whole project (only git-TRACKED files). Default
485
+ ["spec-dashboard/src", "spec-cli/src"].
486
+ lint.sourceIncludeGlobs optional repo-relative includes. Omit = ALL tracked current regular text under
487
+ governedRoots; [] = intentionally none. Slash-less globs match at any depth.
488
+ lint.sourceExcludeGlobs repo-relative paths subtracted from the includes (default []). Nothing about
489
+ docs/vendor/build/config is guessed: exclude it here when that is project policy.
490
+ lint.sourceExtensions compatibility shorthand compiled into include globs (".py" → "**/*.py") and
491
+ unioned with sourceIncludeGlobs; it has no separate matching path.
492
+ lint.testGlobs globs EXCLUDED from coverage. Defaults cover .test/.spec names, test/tests/
493
+ directories, and test_* / *_test conventions; [] governs tests too.
494
+ lint.maxOwners warn when a file is governed WHOLE-FILE by > this many nodes (default 3).
495
+ Selector-scoped governors (code: path#symbol) don't count toward the bound.
496
+ (lint.driftErrorThreshold is RETIRED: the count-based commit gate is replaced
497
+ by code anchors — \`code: path#symbol\` — whose hits error unconditionally; a
498
+ leftover key is ignored.)
499
+ lint.scopedCodeMiss "warn" (default) | "ignore" — the file-level drift ADVISORY on a selector-
500
+ scoped code: file whose window commits hit no selector (a miss). "ignore"
501
+ silences ONLY that advisory; it never touches hit blocks (anchor-drift),
502
+ bare-path drift, integrity, Spec-OK acks, related semantics, or eval
503
+ freshness. A project policy → committed spexcode.json.
504
+ lint.scenarioTags the closed vocabulary an eval scenario's tags: must draw from (default
505
+ ["frontend-e2e","backend-api","cli","desktop","mobile"]); extend to mint a tag.
506
+ Example — govern your own source dir:
507
+ { "lint": { "governedRoots": ["src"] } }
508
+ Example — declare project-specific exclusions (nothing is guessed from these names):
509
+ { "lint": { "governedRoots": ["."], "sourceExcludeGlobs": ["vendor/**", "dist/**", "docs/**"] } }
510
+ Migration: \`lint.maxChildren\` is RETIRED and no longer read; move its value to
511
+ \`doctor.breadth.maxChildren\`. Bare \`spex doctor\` flags a stale key.
512
+
513
+ ── DOCTOR HEALTH (spexcode.json — portable advisory-diagnosis budgets) ──
514
+ doctor.altitude the one altitude proxy config consumed by bare \`spex doctor\`:
515
+ { lineBudget, charBudget, sizeable, dense, steps, identifierExtensions }
516
+ Defaults: 50 / 4200 / 35 / 1.3 / 3 / []. Exact filename signals derive from
517
+ lint's tracked source candidates; identifierExtensions adds compatibility wildcard
518
+ rows (".legacy" → "*.legacy") to that same matcher.
519
+ doctor.breadth the one tree-breadth hypothesis consumed by bare \`spex doctor\`:
520
+ { maxChildren }. Default: 8. Reports each affected node's direct child count and
521
+ points to the regroup workflow; genuine flat peers may remain flat.
522
+ Example — tune opt-in health diagnosis without changing the lint gate:
523
+ { "doctor": { "altitude": { "lineBudget": 70 }, "breadth": { "maxChildren": 10 } } }
524
+
525
+ ── OTHER (spexcode.json unless noted) ──
526
+ preset the SELECTED init preset — which cumulative .plugins tier \`spex init\` seeds (default
527
+ 'default'; seed-time only, read by init.ts).
528
+ harnesses which harness targets \`spex materialize\` delivers into — native ids
529
+ ("claude"|"codex"|"opencode"|"pi"|"claude-headless"|"codex-headless"|"opencode-headless"|"pi-headless") or a
530
+ { "plugin": "<folder>" } bundle. REQUIRED — there is no default set: \`spex init --harness\`
531
+ stamps the explicit choice, and a missing field fails materialize loud. PERSISTENT and
532
+ git-transactional: the edit takes effect at the next git-native materialize anchor (the commit
533
+ that carries it, a checkout/merge that receives it, or a manual \`spex materialize\`) — a
534
+ deselected harness's artifacts are pruned by that pass.`;
535
+ const FOOTPRINT = `spex guide footprint — what SpexCode plants in a repo, and who sees it (one fixed behavior per kind)
536
+
537
+ SpexCode claims software engineering's HEAD (the recording of intent) and TAIL (the storage of
538
+ measurement) and leaves the MIDDLE — construction — to the harness/agent/test framework; freshness
539
+ stitches the two ends into a closed loop. Materialize is the base operation of harness ADAPTATION:
540
+ one pass renders the spec tree into whatever artifacts the selected harness auto-discovers, so that
541
+ is how SpexCode reaches an agent — never a launch-time flag. The footprint follows: the head+tail
542
+ (.spec, spexcode.json, evals) is the ASSET and lives in git like source; everything else is derived
543
+ wiring or a machine fact. Materialized artifacts carry no facts, so they are NEVER tracked — there is exactly one residence
544
+ behavior, decided per KIND (and, for a contract file, by its live CONTENT).
545
+
546
+ ── THE FOUR KINDS (all fixed) ──
547
+ spec data .spec/ (incl .plugins/) + spexcode.json — ALWAYS tracked. Git is the database; there is
548
+ deliberately NO way to say "untrack the spec" in this schema.
549
+ machine facts spexcode.local.json, the hook shims (.claude/settings.json, .codex/hooks.json), plugin
550
+ bundles — NEVER tracked; always in the per-clone exclude.
551
+ artifacts the CLAUDE.md/AGENTS.md contract blocks + materialized skills/agents — derived, NEVER
552
+ tracked; hidden via .git/info/exclude. The host's tracked .gitignore is never touched.
553
+ run residue .worktrees/, the global store (~/.spexcode), .git/spexcode evidence — never tracked;
554
+ out-of-tree, or exclude-ruled where in-tree.
555
+
556
+ ── A CONTRACT FILE'S RESIDENCE IS A LIVE CONTENT FACT (re-judged at every materialize) ──
557
+ host-tracked → the clean/smudge content filter: the repo keeps the pristine host prose, your
558
+ working tree carries prose + block, status stays clean.
559
+ untracked, wholly ours→ one exclude entry. The exclude is the ignored-bit DECLARATION the rest of git
560
+ consults (checkout may overwrite, clean -fd spares, status/add -A/stash silent).
561
+ untracked, YOUR prose → the exclude entry is withdrawn (hiding user content would be data-loss shaped)
562
+ entered the file and the clean filter is pre-armed: the file shows honestly as untracked, and
563
+ IF you choose to \`git add\` it, the block is stripped automatically — tracking
564
+ is always your act; SpexCode never stages or commits anything for you.
565
+
566
+ ── THE GIT-NATIVE ANCHORS (no harness event ever triggers a materialize) ──
567
+ spex init / spex materialize / session-worktree creation — the explicit passes;
568
+ pre-commit the correctness anchor: an UNCONDITIONAL materialize (masks provably fresh at the only
569
+ moment history is written) + staged-index surgery — a staged file carrying the sentinel
570
+ block is cleaned IN PLACE (partial staging survives; source is the staged content), a
571
+ HEAD-untracked generated artifact is unstaged. Repairs and proceeds, never rejects.
572
+ post-checkout/post-merge freshness anchors: .spec/.plugins edits are git-transactional — they take
573
+ effect at the commit/checkout/merge that carries them, like any other source change.
574
+ An environment with no spex-planted hooks (CI, a cloud agent's fresh clone, a teammate who hasn't
575
+ installed) simply runs \`spex materialize\` in its setup step — there is no committed-artifact mode.
576
+ TRACK ≠ PUSH: none of this ever touches remotes; where commits GO is branch/remote policy.
577
+
578
+ ── GUARANTEES (the forgetting law) ──
579
+ materialize(P₂) ∘ materialize(P₁) = materialize(P₂): every materialize first ERASES all landing points by
580
+ SpexCode's own identity stamps, then re-asserts — legacy states (a .gitignore managed block, a committed
581
+ artifact) are forgotten by the same pass. \`spex uninstall\` is the empty
582
+ materialize plus the global store: a total backout that never touches your .spec/.plugins or prose. Fresh
583
+ clones and session worktrees are self-sufficient: data by checkout, materialized artifacts by
584
+ re-materialize, the machine snapshot (spexcode.local.json) by copy.
585
+
586
+ ── THE CONTENT FILTER (mixed-content contract files) ──
587
+ Per-clone only — git config filter.spexcode.* + .git/info/attributes + a shim under .git/spexcode/ —
588
+ planted where mixed content exists or is imminent (tracked, or untracked with your prose). clean strips
589
+ the sentinel block (history never sees it); smudge re-injects it on checkout. A missing shim degrades to
590
+ identity (never a git fatal). Your own edits to the prose still show as real modifications; only the
591
+ block is invisible to git.
592
+
593
+ ── PRIVATE LOCAL NODES (manual posture — works today) ──
594
+ Spec data is always tracked, but tracked WHERE is yours to choose: to keep a node off the shared
595
+ remote, give it a different git HOME instead of untracking it. The manual recipe:
596
+ 1. create the node dir under .spec/ as usual (spec.md and friends);
597
+ 2. add the dir's path to .git/info/exclude — per-clone, so the shared repo never sees it;
598
+ 3. inside the dir, \`git init --separate-git-dir\` pointing somewhere under the main repo's .git/
599
+ (e.g. .git/spexcode/<name>.git) — the dir then holds only a one-line .git pointer file, so the
600
+ spec loader never walks an object store;
601
+ 4. commit the node's changes through that inner repo.
602
+ The effect, honestly: filesystem-derived surfaces see the node (graph, search, lint); git-derived
603
+ views are blind to it (version count, the history tab, drift), and a dispatched worker's worktree
604
+ checkout does not contain it. Those gaps are what the pending spec-local design (a first-class
605
+ private overlay root) closes — not built yet. Cautions: \`git clean -fdx\` in the outer repo deletes
606
+ the inner repo along with the dir, so off-machine backup means giving the INNER repo its own private
607
+ remote; and taking the node public later is a migration (move it into the shared tree and commit),
608
+ not a flag flip.
609
+
610
+ ── MIGRATIONS ──
611
+ legacy untracked spec track the sources once: git add .spec spexcode.json (commit on your branch)
612
+ WARNING: tracking is not retroactive secrecy — history already pushed
613
+ elsewhere cannot be recalled.
614
+ back out entirely \`spex uninstall\` (add --hooks to also remove the spexcode git hooks).`;
615
+ const FILES = `spex guide files — hand an agent artifact back to the human
616
+
617
+ Use the session's file list when an artifact belongs in the human's hands:
618
+
619
+ spex session files add <path> publish one current file path
620
+ spex session files ls read your published paths
621
+ spex session files retract <path> withdraw one path
622
+
623
+ Posting resolves a relative path from your current directory and records its absolute path beside the global
624
+ session record. It copies, moves, stages, and uploads NOTHING. The path is live: editing the file after
625
+ posting changes what the human downloads. The reference is host-local; opening the session elsewhere cannot
626
+ make its path point at another machine's file.
627
+
628
+ The session page's top-right files icon is grey while the list is empty. Once live, it opens the posted list;
629
+ choosing a path previews its current text or raster-image bytes in a pop-out, while the adjacent download tool
630
+ downloads it through the backend at that moment. Previews are limited to 2 MiB, text and PNG/JPEG/GIF/WebP;
631
+ other types and larger files say to download instead. A missing, moved, or unreadable target stays listed but
632
+ reports that it no longer exists. The backend refuses a preview or download for any path not on that session's
633
+ list.
634
+
635
+ This is the reverse of a dashboard attachment: [[file-attach]] sends human bytes to an agent. Files publishes
636
+ an agent-owned path for a human to download.`;
637
+ const WEB = `spex guide web — hand a running local web service to the human
638
+
639
+ Use the session web list after starting a local page the human should inspect:
640
+
641
+ spex session web add http://127.0.0.1:5173/
642
+ spex session web ls
643
+ spex session web retract http://127.0.0.1:5173/
644
+
645
+ Posting records the canonical loopback HTTP URL beside the global session record. It does not fetch the page,
646
+ start a process, or move bytes. The dashboard uses its own same-origin gateway only when the human opens the
647
+ preview, including WebSocket traffic, so changes to the running service are visible at that moment.
648
+
649
+ Only loopback HTTP URLs with an explicit port are accepted: 127.0.0.1, localhost, or ::1. The service remains
650
+ host-local and must stay running. A stopped service remains listed and reports that its upstream is unavailable.
651
+ The proxy has a path prefix, so use relative asset/navigation URLs or configure your dev server's base path;
652
+ hard-coded root-absolute Vite/Next assets cannot be made portable by a proxy guessing at JavaScript.`;
653
+ const TOPICS = { spec: SPEC, eval: EVAL, settings: SETTINGS, footprint: FOOTPRINT, files: FILES, web: WEB };
654
+ // every guide page ends by naming the OTHER help layer, so a reader never dead-ends here: guide is
655
+ // the skill layer (workflows · formats · settings); command usage lives in help.ts's two layers.
656
+ const FOOTER = `\n\n(This is the skill layer. Command usage: \`spex help\` for the map, \`spex help <command>\` for one command.)`;
657
+ // null = unknown topic: the caller fails loud (exit non-zero) while still naming the layers to go
658
+ // back to — an unknown topic must never read as a successful page ([[cli-surface]]'s dead-end rule).
659
+ export function guideText(topic) {
660
+ if (!topic)
661
+ return SETUP + FOOTER;
662
+ const t = TOPICS[topic];
663
+ return t ? t + FOOTER : null;
664
+ }
665
+ // @@@ the unknown-topic list is DERIVED, never re-typed - a hand-kept enumeration goes blind the moment a
666
+ // topic is added beside it, and it did: `files` and `web` shipped while the error still named four.
667
+ export function guideTopics() {
668
+ return Object.keys(TOPICS);
669
+ }
670
+ // The catalog indexes the same rendered pages the CLI serves. Keeping this projection beside TOPICS makes a
671
+ // newly registered page automatically appear in the export and in the unknown-topic diagnostic.
672
+ export function guideCatalogEntries() {
673
+ const entries = [];
674
+ const setup = guideText();
675
+ if (setup)
676
+ entries.push({ id: 'setup', title: 'spex guide', text: setup });
677
+ for (const topic of guideTopics()) {
678
+ const text = guideText(topic);
679
+ if (text)
680
+ entries.push({ id: topic, title: `spex guide ${topic}`, text });
681
+ }
682
+ return entries;
683
+ }
684
+ import { uploadPolicyDefaults } from '@spexcode/spec-core';