spexcode 0.2.0 → 0.2.2

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 (48) hide show
  1. package/README.md +149 -102
  2. package/README.zh-CN.md +170 -0
  3. package/package.json +1 -1
  4. package/spec-cli/bin/spex.mjs +24 -1
  5. package/spec-cli/src/attach.ts +50 -0
  6. package/spec-cli/src/cli.ts +227 -66
  7. package/spec-cli/src/client.ts +47 -9
  8. package/spec-cli/src/{self.ts → doctor.ts} +26 -25
  9. package/spec-cli/src/gateway.ts +15 -11
  10. package/spec-cli/src/guide.ts +73 -17
  11. package/spec-cli/src/harness.ts +48 -19
  12. package/spec-cli/src/help.ts +141 -51
  13. package/spec-cli/src/index.ts +41 -14
  14. package/spec-cli/src/issues.ts +109 -31
  15. package/spec-cli/src/layout.ts +4 -4
  16. package/spec-cli/src/localIssues.ts +59 -58
  17. package/spec-cli/src/materialize.ts +4 -2
  18. package/spec-cli/src/mentions.ts +22 -1
  19. package/spec-cli/src/pty-bridge.ts +39 -4
  20. package/spec-cli/src/ranker.ts +31 -12
  21. package/spec-cli/src/search.bench.mjs +30 -7
  22. package/spec-cli/src/search.ts +39 -0
  23. package/spec-cli/src/sessions.ts +149 -62
  24. package/spec-cli/src/specs.ts +16 -4
  25. package/spec-cli/src/supervise.ts +30 -6
  26. package/spec-cli/src/tree.ts +118 -0
  27. package/spec-cli/templates/hooks/post-merge +2 -2
  28. package/spec-cli/templates/hooks/pre-commit +34 -15
  29. package/spec-cli/templates/hooks/prepare-commit-msg +8 -1
  30. package/spec-dashboard/dist/assets/Dashboard-BwZ2KzxB.js +27 -0
  31. package/spec-dashboard/dist/assets/Dashboard-C5ap-Sga.css +1 -0
  32. package/spec-dashboard/dist/assets/EvalsPage-DV75EdP4.js +3 -0
  33. package/spec-dashboard/dist/assets/FoldToggle-GwE0-k1d.js +1 -0
  34. package/spec-dashboard/dist/assets/IssuesPage-B17pnl9I.js +1 -0
  35. package/spec-dashboard/dist/assets/MobileApp-WEZbR8M1.js +1 -0
  36. package/spec-dashboard/dist/assets/SessionInterface-DYP7pi_n.css +32 -0
  37. package/spec-dashboard/dist/assets/SessionInterface-Sh8kHpnj.js +71 -0
  38. package/spec-dashboard/dist/assets/SessionWindow-EzFq-hLG.js +9 -0
  39. package/spec-dashboard/dist/assets/Settings-Dgtg-Xb9.js +1 -0
  40. package/spec-dashboard/dist/assets/index-CCmnCbKS.css +1 -0
  41. package/spec-dashboard/dist/assets/index-Dd0_U5rk.js +41 -0
  42. package/spec-dashboard/dist/index.html +2 -2
  43. package/spec-forge/src/cli.ts +4 -10
  44. package/spec-forge/src/drivers.ts +13 -0
  45. package/spec-yatsu/src/cli.ts +89 -15
  46. package/spec-yatsu/src/scenariofresh.ts +100 -30
  47. package/spec-dashboard/dist/assets/index-B0tgHeEQ.js +0 -145
  48. package/spec-dashboard/dist/assets/index-BTU-44Os.css +0 -32
@@ -12,8 +12,19 @@ type Entry = { line: string; body: string; see?: string }
12
12
  const SEL_NOTE = `SEL = session id (or unique id-prefix) | node id | branch — every session read/control verb
13
13
  accepts any of the three; none (or @all) means every session.`
14
14
 
15
+ const ROUTING_NOTE = `Backend routing: every session verb accepts --api <url> (--port <n> = localhost sugar) to name its
16
+ backend explicitly — the flag always wins. Bare, it resolves: worker env / the cwd project's live
17
+ recorded backend / fallback / :8787 (spex guide config → BACKEND ROUTING).`
18
+
15
19
  // aliases resolve to a canonical entry so `spex help session` and `spex session new --help` meet the same text.
16
- const ALIAS: Record<string, string> = { 'review-proof': 'review', help: 'help' }
20
+ // The session-sub tokens mirror the CLI's verb-mirror rule: each typeable sub also answers bare at the top
21
+ // level, so its help probe (`spex send --help`, `spex help send`) must land on the session entry, not dead-end.
22
+ const SESSION_SUB_TOKENS = ['reopen', 'done', 'park', 'ask', 'exit', 'close', 'send', 'capture', 'attach', 'rename', 'rawkey', 'prompt']
23
+ const ALIAS: Record<string, string> = {
24
+ 'review-proof': 'eval',
25
+ help: 'help',
26
+ ...Object.fromEntries(SESSION_SUB_TOKENS.map((t) => [t, 'session'])),
27
+ }
17
28
 
18
29
  const ENTRIES: Record<string, Entry> = {
19
30
  // ── find & read the graph ─────────────────────────────────────────────────
@@ -23,25 +34,42 @@ const ENTRIES: Record<string, Entry> = {
23
34
 
24
35
  Finds the spec node(s) whose INTENT matches your topic — ranked by user-story relevance, which
25
36
  surfaces user-facing behaviour a code-grep misses. Run it BEFORE touching code: the returned node's
26
- spec.md body is the current contract for that area. Prints title, id, path, snippet per hit.`,
37
+ spec.md body is the current contract for that area. Prints title, id, path, snippet per hit.
38
+ The corpus is English — query in English (translate first if your question isn't).`,
27
39
  see: 'spex owner (file → node, the reverse edge) · spex guide spec (what a node is)',
28
40
  },
29
41
  owner: {
30
- line: 'owner <path> the reverse edge: which spec node(s) claim a file',
42
+ line: 'owner <path> the reverse edge: which spec node(s) govern or reference a file',
31
43
  body: `Usage: spex owner <path> [--actionable]
32
44
 
33
- Maps a source file to the spec node(s) governing it, with the verdict spelled out: uncovered
34
- ("give it a home"), sanely governed (read/honor that spec), or over-owned (> maxOwners — split the
35
- file). --actionable prints NOTHING for the sane case (hook use: only fires when action is needed).`,
45
+ Maps a source file to BOTH spec relations: its GOVERNORS (code: the source of truth; drives
46
+ drift/yatsu) and its REFERENCERS (related: coverage only), with the verdict spelled out:
47
+ uncovered ("give it a home"), related-only (covered, but nothing tracks its drift), sanely
48
+ governed (read/honor that spec), or over-owned (> maxOwners — split the file). --actionable
49
+ prints NOTHING unless action is needed (hook use): only uncovered / over-owned fire.`,
36
50
  see: 'spex search (topic → node) · spex lint (coverage over the whole tree)',
37
51
  },
52
+ tree: {
53
+ line: 'tree the graph as a human-readable tree (status-coloured, badges)',
54
+ body: `Usage: spex tree [--node <id>] [--depth N] [--json]
55
+
56
+ Prints the assembled spec graph as an indented tree — the CLI twin of the dashboard's tidy-tree,
57
+ built from the same board (merged tree + worktree overlay). One line per node: id, derived status
58
+ (coloured when stdout is a tty; NO_COLOR respected — the status word always prints), title, and
59
+ attention badges: drift:N (drifted files), stale:N (yatsu scenarios whose latest reading aged),
60
+ issues:N (open issues), ghost (being added by a worktree).
61
+ --node <id> render just that subtree (unknown id fails loud)
62
+ --depth N limit levels below the shown root; prunes are counted, never silent
63
+ --json the same filtered subtree as nested objects, badge counts precomputed`,
64
+ see: 'spex board (the full flat JSON payload) · spex search (find one node by intent)',
65
+ },
38
66
  board: {
39
67
  line: 'board dump the assembled board as JSON (tree · overlay · sessions)',
40
68
  body: `Usage: spex board
41
69
 
42
70
  Prints the full dashboard board state as JSON — the merged spec tree, per-worktree overlay, and the
43
71
  session list. Identical to GET /api/board; needs the backend (spex serve) reachable.`,
44
- see: 'spex ls (just the sessions, as a table) · spex search (find one node instead of dumping all)',
72
+ see: 'spex tree (the same graph, human-readable) · spex ls (just the sessions, as a table) · spex search (find one node instead of dumping all)',
45
73
  },
46
74
  guide: {
47
75
  line: 'guide [topic] the manuals: setup workflow · spec/yatsu file formats · spexcode.json',
@@ -82,40 +110,52 @@ only the trailer. If the intent DID change, edit the spec instead — same commi
82
110
  yatsu: {
83
111
  line: 'yatsu <sub> measure a node’s scenarios & file the loss signal: scan | eval | retract | show | clean',
84
112
  body: `Usage: spex yatsu scan [--changed] list nodes/scenarios missing readings
85
- spex yatsu eval [.|<node>] [--scenario <name>] (--pass|--fail)
86
- [--note <text>] [--image <png> | --result <path|->]
113
+ spex yatsu eval [.|<node>] [--scenario <name>] (--pass|--fail) [--note <text>]
114
+ [--image <png>] [--result <path|->] [--video <webm|mp4> [--timeline <json>]]
87
115
  spex yatsu retract [.|<node>] [--scenario <name>] [--last | --ts <iso>] [--note <why>]
88
116
  spex yatsu show [.|<node>] [--json] readings history for a node
89
117
  spex yatsu clean [--keep-latest | --all] prune stored readings
90
118
 
91
119
  Files a reading of a scenario against its expected — the loss signal the optimizer reads. Measure
92
- through the REAL product surface (run it, drive a browser, screenshot), never by reasoning about
93
- the code. A fix's proof is a fail→pass pair on the SAME scenario. \`retract\` is the sanctioned
94
- undo for a botched filing: it APPENDS a retraction event (traceable, never deletes a line).`,
120
+ through the REAL product surface (run it, drive a browser, capture), never by reasoning about
121
+ the code. Evidence kind follows the behaviour: MOVING/timed behaviour (scroll, animation,
122
+ playback, a multi-step flow) records a \`--video\`; a STATIC end state screenshots \`--image\`;
123
+ backend/CLI files a \`--result\` transcript. A fix's proof is a fail→pass pair on the SAME
124
+ scenario. \`retract\` is the sanctioned undo for a botched filing: it APPENDS a retraction
125
+ event (traceable, never deletes a line).`,
95
126
  see: 'spex guide yatsu (yatsu.md format + evidence rules) · spex blob (stash evidence bytes)',
96
127
  },
97
128
  blob: {
98
- line: 'blob put <file|-> stash evidence bytes in the content-addressed cache, print the hash',
129
+ line: 'blob put|get evidence bytes content hash: put stashes & prints the hash, get reads back',
99
130
  body: `Usage: spex blob put <file|->
131
+ spex blob get <hash> [-o <file>]
100
132
 
101
- Writes bytes into the shared content-addressed evidence cache and prints the hash — transport only,
102
- no reading filed. Use the hash with --evidence on issues/remarks; re-putting the same content
103
- restores a pruned or cloned-away blob.`,
133
+ put writes bytes into the shared content-addressed evidence cache and prints the hash — transport
134
+ only, no reading filed. Use the hash with --evidence on issues/remarks; re-putting the same content
135
+ restores a pruned or cloned-away blob.
136
+
137
+ get is the symmetric read: hash in, bytes out. Local cache first (no backend needed — the evidence
138
+ is usually on this disk), then the backend's /api/yatsu/blob/:hash on a local miss; both missing
139
+ fails loud naming each path. Bytes go to stdout by default (pipe-friendly); -o writes a file.`,
104
140
  see: 'spex yatsu eval (file a reading with evidence) · spex issues open --evidence <hash>',
105
141
  },
106
142
  issues: {
107
143
  line: 'issues … THE issue surface: one merged local+forge list, plus all write verbs',
108
144
  body: `Usage: spex issues the merged read (local + forge, store-tagged)
109
145
  [--node <id>] [--store local|github] [--all] [--json]
110
- spex issues open "<concern>" [--node <id>…] [--evidence <hash>…] [--body -|<text>]
146
+ spex issues open "<concern>" [--store local|<host>] [--node <id>…] [--evidence <hash>…] [--body -|<text>]
111
147
  spex issues reply <id> --body -|<text> [--evidence <hash>…] (routes by the issue's store)
112
- spex issues sign <id> co-sign a local issue
113
- spex issues resolve <id> --as accepted|rejected|landed
148
+ spex issues close <id> close by the issue's store: local lands, forge closes remote
114
149
  spex issues promote <id> move an OPEN local issue to the forge (one recorded action)
115
150
  spex issues on|off|status toggle/inspect the local-issue workflow
116
151
 
117
152
  Bare \`spex issues\` is the drain view a supervisor reads. \`open\` welcomes taste, annotations, and
118
- off-mainline smells — not only bugs. (\`nudge\` exists but is fired by the post-merge hook, not typed.)`,
153
+ off-mainline smells — not only bugs; \`--store <host>\` opens straight on the forge (the same port the
154
+ dashboard's New form uses). \`close\` and \`reply\` route by the issue's store — one verb, local or forge,
155
+ the same routing as the dashboard. (\`nudge\` exists but is fired by the post-merge hook, not typed.)
156
+ Mentions: @session · [[node]] work in any concern/body — CLI args included. [[node]] links the topic
157
+ node (it also tags the issue, like --node); @session hands the text to that live agent; @new spawns
158
+ a fresh worker on the thread's node.`,
119
159
  see: 'spex remark (pin a resolvable concern to an issue or scenario) · spex forge (trace forge → nodes)',
120
160
  },
121
161
  remark: {
@@ -138,35 +178,27 @@ Resolves a forge's open issues/PRs to the spec nodes they serve (the Spec: marke
138
178
  a node/<id> branch links its PR for free). Read-only — git/.spec stays the single source of truth.`,
139
179
  see: 'spex issues (the merged read that includes forge threads)',
140
180
  },
141
- self: {
142
- line: 'self <sub> diagnose whether the workflow actually reaches THIS agent: doctor | contract | conflicts',
143
- body: `Usage: spex self [doctor] per-layer coverage report: preconditions · git-hook floor · contract ·
144
- hooks + handler existence · backend — for every harness materialize renders
145
- spex self contract print the composed surface:system text any agent here reads
146
- spex self conflicts detect double-delivery (loose artifacts beside the managed ones)
147
-
148
- Run it when a worker seems to be missing its contract or hooks — it names the broken layer and the
149
- repair, instead of you diffing materialized files by hand.`,
150
- see: 'spex materialize (re-render the artifacts doctor checks)',
151
- },
152
-
153
181
  // ── dispatch & manage sessions (manager loop) ─────────────────────────────
154
182
  new: {
155
- line: 'new "<prompt>" launch a worker session in its own node worktree [--node <id>]',
156
- body: `Usage: spex new "<task prompt>" [--node <id>] [--harness <name>] [--launcher <cmd>]
183
+ line: 'new "<prompt>" launch a worker session in its own node worktree [--node <id>] [--launcher <name>]',
184
+ body: `Usage: spex new "<task prompt>" [--node <id>] [--launcher <name>]
157
185
 
158
186
  Creates a session: node branch + worktree + a launched agent carrying your prompt (= session new).
159
187
  Give it ONLY its task — the dev-flow contract reaches it through the materialized system prompt.
188
+ The launcher name selects both the agent harness and the command/auth profile (built-ins: claude, codex);
189
+ omitting it requires sessions.defaultLauncher in spexcode.json or spexcode.local.json.
160
190
  Routes through the running backend (auth env + concurrency cap); prints the created session JSON.
161
191
  Then MONITOR it: background \`spex wait <id>\`, or \`spex watch\` for the whole stream.`,
162
192
  see: 'spex wait / spex watch (monitor) · spex review (when it proposes) · ' + SEL_NOTE.split('\n')[0],
163
193
  },
164
194
  ls: {
165
- line: 'ls [SEL…] living-sessions table [--status a,b] [--json]',
166
- body: `Usage: spex ls [SEL…] [--status a,b] [--json]
195
+ line: 'ls [SEL…] living-sessions table [--status a,b] [--json] [--api URL]',
196
+ body: `Usage: spex ls [SEL…] [--status a,b] [--json] [--api <url> | --port <n>]
167
197
 
168
- One-shot table of living sessions and their states, from whatever backend SPEXCODE_API_URL points
169
- at (including a remote machine's). ${SEL_NOTE}`,
198
+ One-shot table of living sessions and their states, from the resolved backend bare \`spex ls\` in a
199
+ project's tree hits THAT project's live backend; --api <url> (or --port <n>) points it anywhere,
200
+ including a remote machine's. ${SEL_NOTE}
201
+ ${ROUTING_NOTE}`,
170
202
  see: 'spex watch (the live stream) · spex wait (block on one session)',
171
203
  },
172
204
  watch: {
@@ -194,14 +226,29 @@ ${SEL_NOTE}`,
194
226
  review: {
195
227
  line: 'review <SEL> manager cockpit: ahead · merge-base diff · gates · proposal [--json]',
196
228
  body: `Usage: spex review <SEL> [--json]
197
- spex review proof <SEL> [--open | --out <path> | --json]
198
229
 
199
230
  The ONE review payload for a session: commits ahead of the trunk, uncommitted files, its proposal,
200
231
  the gates (conflicts with the trunk, lint), and the merge-base diff — decide from this, don't
201
- hand-run git. \`review proof\` renders the session's proof-of-work as self-contained HTML (diff ·
202
- measured yatsu loss · gates), fully derived from git + readings.
232
+ hand-run git. The MEASURED side of the decision is \`spex eval <SEL>\`: the changed nodes' eval
233
+ readings, and (--export) the self-contained HTML export.
203
234
  ${SEL_NOTE}`,
204
- see: 'spex merge (act on an approved review)',
235
+ see: 'spex eval (the session’s measured loss) · spex merge (act on an approved review)',
236
+ },
237
+ eval: {
238
+ line: 'eval <SEL> the session’s eval readings: its changed nodes’ measured loss [--export]',
239
+ body: `Usage: spex eval <SEL> [--json]
240
+ spex eval <SEL> --export [--open | --out <path> | --json]
241
+
242
+ The session's evaluation, read from the backend (the dashboard Eval tab's CLI twin): every spec node
243
+ the session's diff touches, each DECLARED scenario at its CURRENT score (latest reading, rooted at
244
+ the session's worktree). Blind spots lead (declared, never measured — the outstanding loss), then
245
+ the session's OWN measurements ✦-marked, then the inherited baseline (other sessions' latest
246
+ readings) under an explicit divider. A frontend change with no yatsu.md is flagged, never hidden.
247
+ --export writes the evaluation as ONE self-contained HTML artifact instead (diff · evidence
248
+ inlined · gates; --json = the model) for CI/sharing. (\`spex review proof\` is its deprecated alias.)
249
+ This is the READ; filing a reading stays \`spex yatsu eval\`.
250
+ ${SEL_NOTE}`,
251
+ see: 'spex review (gates + diff — the merge decision) · spex yatsu eval (FILE a reading, the write verb)',
205
252
  },
206
253
  merge: {
207
254
  line: 'merge <SEL> gated merge into the trunk — dispatched to the session’s own agent',
@@ -210,11 +257,13 @@ ${SEL_NOTE}`,
210
257
  Dispatches the merge to the session's OWN agent (it knows the work's intent and resolves conflicts);
211
258
  the server never touches the trunk's tree. Gates re-check first. After it lands, confirm HEAD
212
259
  advanced before closing the session — closing an unmerged branch discards the work.
260
+ Mutating verbs are PROJECT-BOUND: a backend serving another project's repo refuses the write loudly
261
+ (name the target with --api <url> to write cross-project on purpose).
213
262
  ${SEL_NOTE}`,
214
263
  see: 'spex review (before) · spex session close (after the merge is confirmed)',
215
264
  },
216
265
  session: {
217
- line: 'session <sub> the state machine: new·reopen·done·park·ask·exit·close·send·capture·prompt',
266
+ line: 'session <sub> every session verb answers here: new·ls·watch·wait·review·merge·reopen·done·park·ask·exit·close·send·capture·attach·rename·rawkey·prompt',
218
267
  body: `Worker verbs (declare YOUR OWN state — a claim the board and your supervisor act on):
219
268
  spex session done --propose merge|nothing|close [--note T] committed and stopping; merge = ready for review
220
269
  spex session park --note <what-you-await> a real background task will wake you
@@ -223,14 +272,36 @@ ${SEL_NOTE}`,
223
272
  Manager verbs (control another session; all take SEL):
224
273
  spex session send <SEL> "<msg>" deliver a message (fail-loud: a dead dispatch exits non-zero)
225
274
  spex session capture <SEL> the live pane as text
275
+ spex session rename <SEL> "<name>" set the display name ("" clears; the right-click rename, as a verb)
276
+ spex session rawkey <SEL> "<keys>" raw nav keys to a TUI dialog, in strike order (e.g. "Up Up Enter";
277
+ named keys · single chars · C-/M-/S- combos; fail-loud)
226
278
  spex session prompt <SEL> the session's originating prompt
227
279
  spex session reopen <SEL> [--force] relaunch ONLY if confirmed offline (--force for a wedged live one)
228
280
  spex session exit <SEL> soft stop: kill the agent, KEEP the worktree (resumable)
229
281
  spex session close <SEL> retire the session and its worktree
230
- spex session new "<prompt>" = spex new
282
+
283
+ Promoted verbs — they answer in this drawer too (same verb, either drawer):
284
+ spex session new|ls|watch|wait|review|merge … ≡ spex new|ls|watch|wait|review|merge …
285
+ And the reverse holds for every sub above: it also answers bare at the top level
286
+ (spex send <SEL> "…" ≡ spex session send <SEL> "…"). One implementation, two spellings —
287
+ you never have to guess which drawer a session verb lives in.
288
+
289
+ Human escape hatch:
290
+ spex session attach <SEL> sit in the worker's REAL tmux (detach: C-b d; the session keeps
291
+ running). INTERACTIVE AND BLOCKING — like watch, an agent must
292
+ NEVER run it in a turn (it freezes you): use capture/send/rawkey.
293
+ LOCAL-only — the tmux server is the backend machine's, so it fails
294
+ loud when the resolved backend is remote. Offline session → loud.
295
+
296
+ Mentions: @session · [[node]] work in ANY prompt, issue, or remark body — text passed as a CLI arg
297
+ included. [[node]] names the topic (a new session derives its node from the prompt's first one);
298
+ @session hands the surrounding text to that live agent; @new dispatches a fresh worker.
231
299
 
232
300
  (state · fail · idle · commit-gate also exist but are hook-driven — the lifecycle hooks call them;
233
- never type them.) ${SEL_NOTE}`,
301
+ never type them.) ${SEL_NOTE}
302
+ Manager verbs that WRITE (send/rename/rawkey/reopen/exit/close) are PROJECT-BOUND: a backend serving
303
+ another project's repo refuses loudly — name the target with --api <url> to drive it on purpose.
304
+ ${ROUTING_NOTE}`,
234
305
  see: 'spex new (launch) · spex wait/watch (monitor) · spex review/merge (land)',
235
306
  },
236
307
 
@@ -261,7 +332,18 @@ Renders the surface:system config nodes into the managed <!-- spexcode --> block
261
332
  CLAUDE.md/AGENTS.md plus the .claude/.codex shims, and prints the content hash. Run it after a
262
333
  toolchain update or any .config edit that the automatic dispatch gate hasn't picked up — these
263
334
  artifacts are generated and gitignored, so they never arrive via git.`,
264
- see: 'spex self doctor (verify the render actually reaches an agent)',
335
+ see: 'spex doctor (verify the render actually reaches an agent)',
336
+ },
337
+ doctor: {
338
+ line: 'doctor diagnose whether the workflow actually reaches this agent — per-layer, per-harness',
339
+ body: `Usage: spex doctor per-layer coverage report: preconditions · git-hook floor · contract ·
340
+ hooks + handler existence · backend — for every harness materialize renders
341
+ spex doctor contract print the composed surface:system text any agent here reads
342
+ spex doctor conflicts detect double-delivery (loose artifacts beside the managed ones)
343
+
344
+ Run it when a worker seems to be missing its contract or hooks — it names the broken layer and the
345
+ repair, instead of you diffing materialized files by hand.`,
346
+ see: 'spex materialize (re-render the artifacts doctor checks)',
265
347
  },
266
348
  serve: {
267
349
  line: 'serve run the API backend (default :8787) [--port N] [--public --password pw]',
@@ -270,16 +352,20 @@ artifacts are generated and gitignored, so they never arrive via git.`,
270
352
 
271
353
  Runs the backend for the repo at cwd behind a zero-downtime supervisor (hot-reloads on source
272
354
  change; the public port never gaps). --port pairs with \`spex dashboard --api-port\`, so many
273
- projects coexist on one host. --public exposes it on a public IP behind a password + self-signed
274
- TLS (own cert via --tls-cert/--tls-key; --http drops TLS).`,
355
+ projects coexist on one host. On a successful bind it RECORDS its endpoint in the per-project
356
+ runtime tier that's how a bare \`spex\` run from this project's tree finds this backend (see
357
+ spex guide config → BACKEND ROUTING). --public exposes it on a public IP behind a password +
358
+ self-signed TLS (own cert via --tls-cert/--tls-key; --http drops TLS).`,
275
359
  see: 'spex dashboard (the UI on top) · GET /health (liveness probe)',
276
360
  },
277
361
  dashboard: {
278
362
  line: 'dashboard serve the dashboard UI (default :5173), proxying /api to a running serve',
279
- body: `Usage: spex dashboard [--port N=5173] [--api-port N=8787]
363
+ body: `Usage: spex dashboard [--port N=5173] [--api-port N=8787] [--host H=127.0.0.1]
280
364
 
281
365
  Serves the bundled dashboard on its own port and proxies /api + the terminal socket to a running
282
- \`spex serve\`. The installed replacement for the dev-only \`npm run web\`.`,
366
+ \`spex serve\`. The installed replacement for the dev-only \`npm run web\`. Loopback-only by default;
367
+ --host 0.0.0.0 (or a specific interface) opens it to a LAN/tailnet — still plain HTTP with no gate,
368
+ so bind wide only on a network you trust (for the internet, use \`spex serve --public\`).`,
283
369
  see: 'spex serve (must be running first)',
284
370
  },
285
371
 
@@ -324,6 +410,7 @@ Usage: spex <command> [args] one command's usage: spex help <command> (or
324
410
  Find & read the graph
325
411
  ${ENTRIES.search.line}
326
412
  ${ENTRIES.owner.line}
413
+ ${ENTRIES.tree.line}
327
414
  ${ENTRIES.board.line}
328
415
  ${ENTRIES.guide.line}
329
416
 
@@ -335,7 +422,6 @@ Author & verify (the worker loop)
335
422
  ${ENTRIES.issues.line}
336
423
  ${ENTRIES.remark.line}
337
424
  ${ENTRIES.forge.line}
338
- ${ENTRIES.self.line}
339
425
 
340
426
  Dispatch & manage sessions (the manager loop)
341
427
  ${ENTRIES.new.line}
@@ -343,6 +429,7 @@ Dispatch & manage sessions (the manager loop)
343
429
  ${ENTRIES.watch.line}
344
430
  ${ENTRIES.wait.line}
345
431
  ${ENTRIES.review.line}
432
+ ${ENTRIES.eval.line}
346
433
  ${ENTRIES.merge.line}
347
434
  ${ENTRIES.session.line}
348
435
 
@@ -350,11 +437,14 @@ Install & serve (the operator loop)
350
437
  ${ENTRIES.init.line}
351
438
  ${ENTRIES.uninstall.line}
352
439
  ${ENTRIES.materialize.line}
440
+ ${ENTRIES.doctor.line}
353
441
  ${ENTRIES.serve.line}
354
442
  ${ENTRIES.dashboard.line}
355
443
 
356
444
  ${SEL_NOTE}
357
445
 
446
+ ${ROUTING_NOTE}
447
+
358
448
  Concepts & best practice live in the guide: spex guide (setup) · guide spec · guide yatsu · guide config.
359
449
  Machine plumbing (hook/launch-script callees) lives under \`spex internal\` — not part of your vocabulary.`
360
450
  }
@@ -4,8 +4,8 @@ import { cors } from 'hono/cors'
4
4
  import { etag } from 'hono/etag'
5
5
  import { createNodeWebSocket } from '@hono/node-ws'
6
6
  import { loadSpecs, loadSpecsLite, specContent, specHistory, specDiffAt, loadConfig } from './specs.js'
7
- import { issuesEnabled, postLocalIssue, remarkOnHost, resolveRemark, retractRemark } from './localIssues.js'
8
- import { closeIssue, mergedIssues, replyIssue } from './issues.js'
7
+ import { issuesEnabled, remarkOnHost, resolveRemark, retractRemark } from './localIssues.js'
8
+ import { closeIssue, createIssue, issueStores, mergedIssues, promote, replyIssue } from './issues.js'
9
9
  import { residentForgeState, refreshForgeNow } from '../../spec-forge/src/resident.js'
10
10
  import { summarize } from './mentions.js'
11
11
  import { resolveLayout, mainBranch } from './layout.js'
@@ -13,7 +13,7 @@ import { getBoardJson } from './boardCache.js'
13
13
  import { boardStream, notifyBoardChanged } from './boardStream.js'
14
14
  import { gitA, gitTry, repoRoot } from './git.js'
15
15
  import { newSession, listSessions, sendKeys, rawKey, exitSession, closeSession, reopen, mergeSession, reviewPayload, captureSessionResult, sessionPrompt, sessionGraph, registerWatch, deregisterWatch, renameSession, setSessionSort, superviseQueue } from './sessions.js'
16
- import { defaultHarness, HARNESSES, launcherList, defaultLauncher } from './harness.js'
16
+ import { defaultHarness, HARNESSES, launcherList, launcherDefault } from './harness.js'
17
17
  import { evalTimeline, readBlobByHash } from '../../spec-yatsu/src/evaltab.js'
18
18
  import { putBlob } from '../../spec-yatsu/src/cache.js'
19
19
  import { yatsuNodes } from '../../spec-yatsu/src/yatsu.js'
@@ -146,11 +146,11 @@ app.get('/api/config', (c) => c.json(loadConfig()))
146
146
  // the named launcher profiles ([[launcher-select]]) the New-Session form's dropdown offers — `{ name, harness }`
147
147
  // only (the `cmd` is a host secret, never shipped to the browser) — plus the configured `default` NAME so the
148
148
  // dropdown pre-selects the SAME launcher a bare `spex new` uses (the CLI/config default), instead of the
149
- // alphabetically-first one. `launchers` is empty (and `default` '') when a project configured none, so the form
150
- // falls back to the plain harness picker.
149
+ // alphabetically-first one. Missing defaultLauncher is returned as an actionable config error, not hidden by
150
+ // falling through to the built-in `claude` launcher.
151
151
  app.get('/api/launchers', (c) => c.json({
152
152
  launchers: launcherList().map(({ name, harness }) => ({ name, harness })),
153
- default: defaultLauncher(),
153
+ ...launcherDefault(),
154
154
  }))
155
155
  // the ISSUES read surface ([[issues]]) for the dashboard's issues page — the merged list over every store
156
156
  // (local threads + the resident forge slice), the SAME mergedIssues() the CLI drain reads, verbatim
@@ -159,6 +159,7 @@ app.get('/api/launchers', (c) => c.json({
159
159
  app.get('/api/issues', etag(), (c) =>
160
160
  c.json({
161
161
  enabled: issuesEnabled(),
162
+ stores: issueStores(),
162
163
  issues: mergedIssues({ host: 'github', state: residentForgeState() }, loadSpecsLite().map((s) => s.id)),
163
164
  }))
164
165
  // the WRITE surface ([[local-issues]] / [[issues-view]]) — the human reply path, STORE-ROUTED through the one
@@ -211,10 +212,34 @@ app.post('/api/issues', async (c) => {
211
212
  if (!concern) return c.json({ error: 'empty concern' }, 400)
212
213
  const nodes = Array.isArray(body?.nodes) ? (body.nodes as unknown[]).filter((n): n is string => typeof n === 'string') : []
213
214
  const postBody = typeof body?.body === 'string' ? body.body : undefined
215
+ const store = typeof body?.store === 'string' && body.store.trim() ? body.store.trim() : 'local'
214
216
  // typed evidence[] — yatsu content-addressed hashes (the annotator's clip reference rides here, not prose)
215
217
  const evidence = Array.isArray(body?.evidence) ? (body.evidence as unknown[]).filter((h): h is string => typeof h === 'string' && /^[0-9a-f]{64}$/.test(h)) : []
216
- const { thread, outcomes } = await postLocalIssue(concern, { nodes, body: postBody, evidence, author: 'human' })
217
- return c.json({ ok: true, id: thread.id, outcomes: summarize(outcomes) }, 201)
218
+ try {
219
+ const r = await createIssue(concern, { store, nodes, body: postBody, evidence, author: 'human' })
220
+ if (r.store !== 'local') await refreshForgeNow()
221
+ return c.json({ ok: true, id: r.id, store: r.store, url: r.url, outcomes: summarize(r.outcomes) }, 201)
222
+ } catch (e) {
223
+ return c.json({ error: String((e as Error).message || e) }, store === 'local' ? 500 : 502)
224
+ }
225
+ })
226
+
227
+ // promotion moves an open local thread to the forge as one recorded action ([[issues]]'s promote verb,
228
+ // verbatim: forge issue first, then the permalink reply + local close. The forced forge read-back means
229
+ // the reload that follows shows the promoted issue in the merged list. Fail loud: an unreachable forge is a
230
+ // 502 with the local thread untouched.
231
+ app.post('/api/issues/:id/promote', async (c) => {
232
+ if (!issuesEnabled()) return c.json({ error: 'issues workflow is off' }, 403)
233
+ const id = c.req.param('id')
234
+ if (id.includes('#')) return c.json({ error: 'only a local issue promotes' }, 400)
235
+ try {
236
+ const r = await promote(id, { author: 'human' })
237
+ await refreshForgeNow()
238
+ return c.json({ ok: true, ...r })
239
+ } catch (e) {
240
+ const msg = String((e as Error).message || e)
241
+ return c.json({ error: msg }, /^no local issue/.test(msg) ? 404 : 502)
242
+ }
218
243
  })
219
244
 
220
245
  // the REMARK write surface ([[remark-substrate]]) — server PARITY with the CLI: the dashboard can author /
@@ -222,9 +247,11 @@ app.post('/api/issues', async (c) => {
222
247
  // capability. A ref (`<thread-id>#<rid>`) rides the request BODY, not the path (a '#' in a URL is a
223
248
  // fragment). Identity is derived SERVER-SIDE — this is the dashboard's human surface, so the actor is
224
249
  // `'human'`, the SAME sentinel /api/issues stamps; it is NEVER read from the request body. That keeps R3's
225
- // teeth structural (identity is not spoofable over the wire) and honest on both surfaces: resolve rejects
226
- // `'human'` (agent-onlyan agent resolves through the CLI, never the dashboard), and retract binds to the
227
- // human who authored (only their own `'human'` remarks). Who-may-resolve/retract cannot depend on transport.
250
+ // teeth structural (identity is not spoofable over the wire) and identical on both surfaces: resolve is any
251
+ // SECOND party's deliberate judgment the human resolves an agent's remark here exactly as an agent
252
+ // resolves through the CLI, and self-resolve stays rejected by the same identity comparison ('human' can
253
+ // never resolve a human-authored remark) — and retract binds to the author (only the human's own remarks).
254
+ // Who-may-resolve/retract cannot depend on transport.
228
255
  app.post('/api/remarks', async (c) => {
229
256
  if (!issuesEnabled()) return c.json({ error: 'issues workflow is off' }, 403)
230
257
  const body = await c.req.json().catch(() => ({}))
@@ -299,15 +326,15 @@ app.post('/api/sessions', async (c) => {
299
326
  const body = await c.req.json().catch(() => ({}))
300
327
  const prompt = typeof body?.prompt === 'string' ? body.prompt : ''
301
328
  if (!prompt.trim()) return c.json({ error: 'empty prompt' }, 400)
302
- const harness = typeof body?.harness === 'string' ? body.harness : undefined
329
+ if (typeof body?.harness === 'string') return c.json({ error: 'harness is not a create-session input; use launcher' }, 400)
303
330
  // the named launcher ([[launcher-select]]) — fixes the session's harness AND its persisted launch command.
304
331
  const launcher = typeof body?.launcher === 'string' && body.launcher.trim() ? body.launcher.trim() : undefined
305
332
  // parent = the spawning session's id, resolved by the CALLER (createSession) in its own process and passed
306
333
  // through here ([[session-nesting]]); the browser's New Session omits it → a top-level session.
307
334
  const parent = typeof body?.parent === 'string' && body.parent.trim() ? body.parent.trim() : null
308
335
  try {
309
- return c.json(await newSession(typeof body?.node === 'string' ? body.node : null, prompt, harness, parent, launcher), 201)
310
- } catch (e) { return c.json({ error: String((e as Error).message || e) }, 400) } // unknown harness/launcher id → 400, not a 500
336
+ return c.json(await newSession(typeof body?.node === 'string' ? body.node : null, prompt, parent, launcher), 201)
337
+ } catch (e) { return c.json({ error: String((e as Error).message || e) }, 400) } // unknown launcher id → 400, not a 500
311
338
  })
312
339
  // one server-side merge bundle (ahead/dirty/diff(merge-base)/gates/proposal) for the manager cockpit;
313
340
  // dashboard and `spex review` are thin callers. 404 for an unknown id. See [[manager-cockpit]].