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.
- package/README.md +149 -102
- package/README.zh-CN.md +170 -0
- package/package.json +1 -1
- package/spec-cli/bin/spex.mjs +24 -1
- package/spec-cli/src/attach.ts +50 -0
- package/spec-cli/src/cli.ts +227 -66
- package/spec-cli/src/client.ts +47 -9
- package/spec-cli/src/{self.ts → doctor.ts} +26 -25
- package/spec-cli/src/gateway.ts +15 -11
- package/spec-cli/src/guide.ts +73 -17
- package/spec-cli/src/harness.ts +48 -19
- package/spec-cli/src/help.ts +141 -51
- package/spec-cli/src/index.ts +41 -14
- package/spec-cli/src/issues.ts +109 -31
- package/spec-cli/src/layout.ts +4 -4
- package/spec-cli/src/localIssues.ts +59 -58
- package/spec-cli/src/materialize.ts +4 -2
- package/spec-cli/src/mentions.ts +22 -1
- package/spec-cli/src/pty-bridge.ts +39 -4
- package/spec-cli/src/ranker.ts +31 -12
- package/spec-cli/src/search.bench.mjs +30 -7
- package/spec-cli/src/search.ts +39 -0
- package/spec-cli/src/sessions.ts +149 -62
- package/spec-cli/src/specs.ts +16 -4
- package/spec-cli/src/supervise.ts +30 -6
- package/spec-cli/src/tree.ts +118 -0
- package/spec-cli/templates/hooks/post-merge +2 -2
- package/spec-cli/templates/hooks/pre-commit +34 -15
- package/spec-cli/templates/hooks/prepare-commit-msg +8 -1
- package/spec-dashboard/dist/assets/Dashboard-BwZ2KzxB.js +27 -0
- package/spec-dashboard/dist/assets/Dashboard-C5ap-Sga.css +1 -0
- package/spec-dashboard/dist/assets/EvalsPage-DV75EdP4.js +3 -0
- package/spec-dashboard/dist/assets/FoldToggle-GwE0-k1d.js +1 -0
- package/spec-dashboard/dist/assets/IssuesPage-B17pnl9I.js +1 -0
- package/spec-dashboard/dist/assets/MobileApp-WEZbR8M1.js +1 -0
- package/spec-dashboard/dist/assets/SessionInterface-DYP7pi_n.css +32 -0
- package/spec-dashboard/dist/assets/SessionInterface-Sh8kHpnj.js +71 -0
- package/spec-dashboard/dist/assets/SessionWindow-EzFq-hLG.js +9 -0
- package/spec-dashboard/dist/assets/Settings-Dgtg-Xb9.js +1 -0
- package/spec-dashboard/dist/assets/index-CCmnCbKS.css +1 -0
- package/spec-dashboard/dist/assets/index-Dd0_U5rk.js +41 -0
- package/spec-dashboard/dist/index.html +2 -2
- package/spec-forge/src/cli.ts +4 -10
- package/spec-forge/src/drivers.ts +13 -0
- package/spec-yatsu/src/cli.ts +89 -15
- package/spec-yatsu/src/scenariofresh.ts +100 -30
- package/spec-dashboard/dist/assets/index-B0tgHeEQ.js +0 -145
- package/spec-dashboard/dist/assets/index-BTU-44Os.css +0 -32
package/spec-cli/src/help.ts
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
[--
|
|
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,
|
|
93
|
-
the code.
|
|
94
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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>] [--
|
|
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
|
|
169
|
-
|
|
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.
|
|
202
|
-
|
|
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>
|
|
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
|
-
|
|
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
|
|
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.
|
|
274
|
-
|
|
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
|
}
|
package/spec-cli/src/index.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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.
|
|
150
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
|
226
|
-
//
|
|
227
|
-
//
|
|
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
|
-
|
|
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,
|
|
310
|
-
} catch (e) { return c.json({ error: String((e as Error).message || e) }, 400) } // unknown
|
|
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]].
|