spexcode 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/spec-cli/bin/spex.mjs +25 -1
  4. package/spec-cli/hooks/dispatch.sh +24 -1
  5. package/spec-cli/src/anchors.ts +192 -58
  6. package/spec-cli/src/attach.ts +11 -8
  7. package/spec-cli/src/claude-headless.ts +13 -52
  8. package/spec-cli/src/cli.ts +209 -71
  9. package/spec-cli/src/client.ts +143 -5
  10. package/spec-cli/src/gateway-hub.ts +2 -6
  11. package/spec-cli/src/gateway.ts +96 -11
  12. package/spec-cli/src/git.ts +1395 -96
  13. package/spec-cli/src/graph.ts +21 -17
  14. package/spec-cli/src/graphCache.ts +405 -61
  15. package/spec-cli/src/graphStream.ts +581 -182
  16. package/spec-cli/src/guide.ts +13 -3
  17. package/spec-cli/src/harness.ts +946 -71
  18. package/spec-cli/src/headless-controller.ts +63 -0
  19. package/spec-cli/src/help.ts +120 -64
  20. package/spec-cli/src/host-resources.ts +706 -0
  21. package/spec-cli/src/index.ts +95 -21
  22. package/spec-cli/src/init.ts +47 -8
  23. package/spec-cli/src/issues.ts +36 -3
  24. package/spec-cli/src/layout.ts +193 -37
  25. package/spec-cli/src/lint.ts +152 -27
  26. package/spec-cli/src/localIssues.ts +5 -1
  27. package/spec-cli/src/maintenance-wrapper.ts +276 -0
  28. package/spec-cli/src/materialize.ts +6 -7
  29. package/spec-cli/src/opencode-headless.ts +4 -8
  30. package/spec-cli/src/pi-headless.ts +17 -60
  31. package/spec-cli/src/process-identity.ts +49 -0
  32. package/spec-cli/src/project-store.ts +17 -0
  33. package/spec-cli/src/pty-bridge.ts +6 -3
  34. package/spec-cli/src/reviews.ts +17 -13
  35. package/spec-cli/src/runtime-ownership.ts +99 -0
  36. package/spec-cli/src/session-maintenance.ts +750 -0
  37. package/spec-cli/src/session-timeline.ts +6 -6
  38. package/spec-cli/src/sessions.ts +1249 -208
  39. package/spec-cli/src/sh.ts +3 -0
  40. package/spec-cli/src/source-files.ts +27 -2
  41. package/spec-cli/src/specs.ts +58 -25
  42. package/spec-cli/src/supervise.ts +31 -11
  43. package/spec-cli/src/uninstall.ts +7 -6
  44. package/spec-cli/templates/hooks/commit-msg +14 -0
  45. package/spec-cli/templates/hooks/pre-commit +26 -6
  46. package/spec-cli/templates/hooks/prepare-commit-msg +20 -48
  47. package/spec-cli/templates/hooks/reference-transaction +105 -0
  48. package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +1 -1
  49. package/spec-cli/templates/spec/project/.plugins/core/mark-active/mark-active.sh +23 -21
  50. package/spec-cli/templates/spec/project/.plugins/core/spec.md +12 -10
  51. package/spec-cli/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +15 -6
  52. package/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +32 -0
  53. package/spec-dashboard/dist/assets/App-C5vbTw8Q.js +2 -0
  54. package/spec-dashboard/dist/assets/Dashboard-u8RIS3NY.js +27 -0
  55. package/spec-dashboard/dist/assets/EvalsPage-BS7ITcNo.js +2 -0
  56. package/spec-dashboard/dist/assets/IssuesPage-DXbqQFW_.js +1 -0
  57. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  58. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  59. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  60. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  61. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  62. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  63. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  64. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  65. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  66. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  67. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  68. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  69. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  70. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  71. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  72. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  73. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  74. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  75. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  76. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  77. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  78. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  79. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  80. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  81. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  82. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  83. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  84. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  85. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  86. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  87. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  88. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  89. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  90. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  91. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  92. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  93. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  94. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  95. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  96. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  97. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  98. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  99. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  100. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  101. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  102. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  103. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  104. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  105. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  106. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  107. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  108. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  109. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  110. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  111. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  112. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  113. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  114. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  115. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  116. package/spec-dashboard/dist/assets/MobileApp-DVLnk9hz.js +2 -0
  117. package/spec-dashboard/dist/assets/Modal-6mHq6fbZ.js +1 -0
  118. package/spec-dashboard/dist/assets/{PageScroll-CadAKuSy.js → PageScroll-CAY4S4g4.js} +1 -1
  119. package/spec-dashboard/dist/assets/ProjectsPage-UQyzsTWN.js +1 -0
  120. package/spec-dashboard/dist/assets/SessionInterface-DKU4c1Z-.js +39 -0
  121. package/spec-dashboard/dist/assets/SessionWindow-zGwJaGbR.js +1 -0
  122. package/spec-dashboard/dist/assets/Settings-igR17pns.js +1 -0
  123. package/spec-dashboard/dist/assets/Thread-B-ZUarN1.js +13 -0
  124. package/spec-dashboard/dist/assets/TimelineChat-Cp579UoJ.css +1 -0
  125. package/spec-dashboard/dist/assets/TimelineChat-sc49Qj5d.js +276 -0
  126. package/spec-dashboard/dist/assets/data-B1ot4PF0.js +1 -0
  127. package/spec-dashboard/dist/assets/index-BqBNCa1V.js +41 -0
  128. package/spec-dashboard/dist/assets/index-CzutlTDf.css +1 -0
  129. package/spec-dashboard/dist/assets/{terminalFont-DmTPNdbU.js → terminalFont-C6oGNKrf.js} +1 -1
  130. package/spec-dashboard/dist/index.html +5 -4
  131. package/spec-dashboard/src/reviewFilters.js +46 -8
  132. package/spec-dashboard/src/session.js +64 -9
  133. package/spec-eval/src/cli.ts +144 -32
  134. package/spec-eval/src/evaltab.ts +16 -9
  135. package/spec-eval/src/freshness.ts +336 -54
  136. package/spec-eval/src/scenariofresh.ts +51 -1
  137. package/spec-eval/src/scenarios.ts +324 -25
  138. package/spec-eval/src/sessioneval.ts +969 -173
  139. package/spec-cli/src/message-stream.ts +0 -147
  140. package/spec-dashboard/dist/assets/Dashboard-CTAuTyZ3.js +0 -27
  141. package/spec-dashboard/dist/assets/EvalsPage-KbMMownG.js +0 -2
  142. package/spec-dashboard/dist/assets/IssuesPage-DmyLb9Rj.js +0 -1
  143. package/spec-dashboard/dist/assets/MobileApp-D2RZGt4Z.js +0 -2
  144. package/spec-dashboard/dist/assets/Modal-3brXUhM0.js +0 -1
  145. package/spec-dashboard/dist/assets/ProjectsPage-DU3x4Y8l.js +0 -1
  146. package/spec-dashboard/dist/assets/SessionInterface-BtrzlOPs.js +0 -39
  147. package/spec-dashboard/dist/assets/SessionWindow-BWH5O0jh.js +0 -13
  148. package/spec-dashboard/dist/assets/Settings-COgdKTJB.js +0 -1
  149. package/spec-dashboard/dist/assets/TimelineChat-DQ21GSJK.js +0 -1
  150. package/spec-dashboard/dist/assets/index-D6HBvKkJ.js +0 -41
  151. package/spec-dashboard/dist/assets/index-DFdlYy4H.css +0 -1
@@ -0,0 +1,63 @@
1
+ import { createConnection, type Socket } from 'node:net'
2
+ import type { DispatchResult } from './harness.js'
3
+
4
+ export type ControlRequestLabel = {
5
+ name: string
6
+ session: string
7
+ timeoutMs: number
8
+ rejected: string
9
+ }
10
+
11
+ export function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
12
+ return new Promise((resolve, reject) => {
13
+ const timer = setTimeout(() => reject(new Error(message)), ms)
14
+ promise.then(
15
+ (value) => { clearTimeout(timer); resolve(value) },
16
+ (error) => { clearTimeout(timer); reject(error) },
17
+ )
18
+ })
19
+ }
20
+
21
+ export function controlRequest(
22
+ sockPath: string,
23
+ request: object,
24
+ label: ControlRequestLabel,
25
+ ): Promise<DispatchResult> {
26
+ return new Promise((resolve) => {
27
+ let socket: Socket | undefined
28
+ let buffer = ''
29
+ let settled = false
30
+ const finish = (result: DispatchResult) => {
31
+ if (settled) return
32
+ settled = true
33
+ clearTimeout(timer)
34
+ socket?.destroy()
35
+ resolve(result)
36
+ }
37
+ const timer = setTimeout(
38
+ () => finish({ ok: false, error: `${label.name} control timed out for session ${label.session}` }),
39
+ label.timeoutMs,
40
+ )
41
+ try {
42
+ socket = createConnection(sockPath)
43
+ } catch (error) {
44
+ finish({ ok: false, error: `${label.name} controller connect failed for session ${label.session}: ${(error as Error).message}` })
45
+ return
46
+ }
47
+ socket.setEncoding('utf8')
48
+ socket.on('connect', () => socket!.write(`${JSON.stringify(request)}\n`))
49
+ socket.on('data', (chunk) => {
50
+ buffer += chunk
51
+ const nl = buffer.indexOf('\n')
52
+ if (nl < 0) return
53
+ try {
54
+ const response = JSON.parse(buffer.slice(0, nl)) as DispatchResult
55
+ finish(response.ok ? { ok: true } : { ok: false, error: response.error || label.rejected })
56
+ } catch (error) {
57
+ finish({ ok: false, error: `${label.name} returned an invalid control response: ${(error as Error).message}` })
58
+ }
59
+ })
60
+ socket.on('error', (error) => finish({ ok: false, error: `${label.name} controller unreachable for session ${label.session}: ${error.message}` }))
61
+ socket.on('close', () => finish({ ok: false, error: `${label.name} controller closed before confirming session ${label.session}` }))
62
+ })
63
+ }
@@ -29,6 +29,97 @@ const MENTION_NOTE = `Mentions: @session · [[node]] · @new / @new:<launcher> w
29
29
  text passed as a CLI arg included. [[node]] names the topic node; @session hands the text to that live agent;
30
30
  @new spawns a fresh worker on the thread's node (bare = configured default; :<launcher> = that named profile).`
31
31
 
32
+ type SessionVerbNote = 'selector' | 'project-bound'
33
+ type SessionVerbHelp = readonly [usage: string | readonly string[], detail: string, notes?: readonly SessionVerbNote[]]
34
+
35
+ // One definition feeds both the complete session drawer and exact `session <verb> --help` projections.
36
+ // Keeping the behavioral prose here prevents a compact probe from becoming a second, drifting manual.
37
+ function sessionHelpDefinitions(): Record<string, SessionVerbHelp> {
38
+ return {
39
+ new: ['spex session new "<prompt>" [--prompt-file <path>|-] [--launcher <name>]',
40
+ `Launch a worker in its own node worktree. The materialized system contract reaches it
41
+ automatically; the prompt supplies the task context. Its first [[id]] mention binds the
42
+ session to that node. --prompt-file <path>|- carries a long prompt without shell quoting
43
+ (exclusive with the inline prompt). The successful receipt names what to read, monitor, and reply on.`],
44
+ ls: ['spex session ls [SEL…] [--status a,b] [--all] [--json]',
45
+ 'One-shot table of living sessions. Shelved sessions ([[archive]]) are hidden; --all includes them, and naming one explicitly always shows it.', ['selector']],
46
+ resources: ['spex session resources [--json]', 'Read-only host/process ownership, budgets, shared refs, and findings.'],
47
+ watch: ['spex session watch [SEL…] [--as NAME] [--idle] [--interval N=5]',
48
+ 'Streams lifecycle transitions and blocks until killed; `session wait` is the one-shot alternative.', ['selector']],
49
+ wait: ['spex session wait <SEL> [--timeout S=1200] [--interval S=2] [--idle]',
50
+ `EDGE-TRIGGERED wait on one session. Prints the current status immediately (stderr), then
51
+ exits 0 only when it OBSERVES
52
+ the session TRANSITION from a non-actionable status into an actionable one, printing the
53
+ observed path on stdout (e.g. working→review — read the LAST token as the status reached).
54
+ It NEVER returns just because the session is actionable ALREADY — for "what is it right NOW"
55
+ use \`session ls\` / \`session review\` instead. --timeout is the guaranteed exit (code 1,
56
+ observed path on stderr).`, ['selector']],
57
+ review: ['spex session review <SEL> [--json]', 'Reports ahead · uncommitted · proposal · gates · merge-base diff.', ['selector']],
58
+ merge: ['spex session merge <SEL>', 'Dispatches a gated merge to the session\'s own agent; it does not close the session.', ['selector', 'project-bound']],
59
+ send: [['spex session send <SEL> "<msg>"', 'spex session send <SEL> --keys "<keys>"'],
60
+ `Plain send delivers a message and fails loud when dispatch is dead. --keys is the LAST RESORT:
61
+ raw nav-mode keystrokes to a TUI dialog ("Up Up Enter", C-/M-/S- combos). The raw key surface
62
+ is UNSTABLE and can confirm dangerous dialogs — try a plain send first; use keys only when text
63
+ provably cannot land.`, ['selector', 'project-bound']],
64
+ interrupt: ['spex session interrupt <SEL>', 'Hard-interrupt the current turn through native harness control.', ['selector', 'project-bound']],
65
+ rename: ['spex session rename <SEL> "<name>"', 'Set the display name; an empty name clears it.', ['selector', 'project-bound']],
66
+ show: ['spex session show <SEL> [--capture] [--json]', `The session record: status · node · branch · launcher · the full originating prompt.
67
+ --capture prints the LIVE PANE as text instead (empty pane = exit 0; unknown session = exit 2).`, ['selector']],
68
+ resume: ['spex session resume <SEL> [--force]', 'Relaunch ONLY if confirmed offline; --force is for a wedged session.', ['selector', 'project-bound']],
69
+ stop: ['spex session stop <SEL>', 'Soft stop: kill the exact agent and KEEP the worktree resumable.', ['selector', 'project-bound']],
70
+ archive: ['spex session archive <SEL>', 'Cold-archive it: exact leaf/runtime stopped, worktree and conversation kept.', ['selector']],
71
+ unarchive: ['spex session unarchive <SEL>', 'Deprecated compatibility spelling: same behavior as resume, relaunching the same conversation.', ['selector']],
72
+ close: ['spex session close <SEL>', 'Retire the session and its worktree.', ['selector', 'project-bound']],
73
+ maintain: [[
74
+ 'spex session maintain --allow-stop <SEL> [--allow-resume <SEL>[:force]] … -- <command> [args…]',
75
+ 'spex session maintain --status',
76
+ ], `Hold one project maintenance lease around a bounded operator command. Only the exact one-shot
77
+ stop/resume plan is brokered; ordinary session writes stay closed. The bearer never prints or enters
78
+ argv/environment. --status is the sanitized read-only lease view.`, ['selector', 'project-bound']],
79
+ done: ['spex session done --propose merge|nothing|close [--note T]', 'Declare your own work committed and stop.'],
80
+ park: ['spex session park --note <what-you-await>', 'Declare that a real background task will wake your own session.'],
81
+ ask: ['spex session ask --note <your-question>', 'Declare that your own session is stopped on the human and resumes on reply.'],
82
+ attach: ['spex session attach <SEL>', `Attaches the current terminal to the worker's tmux (detach: C-b d) and blocks until detached.
83
+ LOCAL-only (fails loud on a remote backend); show --capture and send are non-interactive.`, ['selector']],
84
+ }
85
+ }
86
+
87
+ const SESSION_HELP_GROUPS = [
88
+ { title: 'Manager verbs (dispatch, monitor, land)', verbs: ['new', 'ls', 'resources', 'watch', 'wait', 'review', 'merge'] },
89
+ { title: 'Control another session', verbs: ['send', 'interrupt', 'rename', 'show', 'resume', 'stop', 'archive', 'unarchive', 'close', 'maintain'] },
90
+ { title: 'Worker verbs (declare YOUR OWN state — a claim the graph and your supervisor act on)', verbs: ['done', 'park', 'ask'] },
91
+ { title: 'Human escape hatch', verbs: ['attach'] },
92
+ ] as const
93
+
94
+ const SESSION_WRITE_NOTE = `Manager verbs that WRITE (send/interrupt/rename/resume/stop/close/maintain/merge) are PROJECT-BOUND: a backend serving
95
+ another project's repo refuses loudly — name the target with --api <url> to drive it on purpose.`
96
+
97
+ function indent(text: string, spaces: number): string {
98
+ const prefix = ' '.repeat(spaces)
99
+ return text.split('\n').map((line) => `${prefix}${line}`).join('\n')
100
+ }
101
+
102
+ function sessionDrawerHelp(): string {
103
+ const definitions = sessionHelpDefinitions()
104
+ const groups = SESSION_HELP_GROUPS.map(({ title, verbs }) => `${title}:\n${verbs.map((verb) => {
105
+ const [rawUsage, detail] = definitions[verb]
106
+ const usages = typeof rawUsage === 'string' ? [rawUsage] : rawUsage
107
+ return `${usages.map((usage) => ` ${usage}`).join('\n')}\n${indent(detail, 6)}`
108
+ }).join('\n')}`).join('\n\n')
109
+ return `${groups}\n\n${SEL_NOTE}\n${SESSION_WRITE_NOTE}\n${MENTION_NOTE}`
110
+ }
111
+
112
+ function sessionVerbHelp(verb: string): string | null {
113
+ const entry = sessionHelpDefinitions()[verb]
114
+ if (!entry) return null
115
+ const [rawUsage, detail, sharedNotes = []] = entry
116
+ const usages = typeof rawUsage === 'string' ? [rawUsage] : rawUsage
117
+ const notes = [detail]
118
+ if (sharedNotes.includes('selector')) notes.push(SEL_NOTE)
119
+ if (sharedNotes.includes('project-bound')) notes.push(SESSION_WRITE_NOTE)
120
+ return `${usages.map((line, index) => `${index === 0 ? 'Usage: ' : ' '}${line}`).join('\n')}\n\n${notes.join('\n\n')}`
121
+ }
122
+
32
123
  const ENTRIES: Record<string, Entry> = {
33
124
  // ── project verbs (implicit object = this project) ────────────────────────
34
125
  graph: {
@@ -164,7 +255,7 @@ leaf id reused) · mention (a [[id]] naming no node). Warns: coverage · drift
164
255
  (UNANCHORED drift — always advisory, never blocks; on a scoped file's MISS, \`lint.scopedCodeMiss:
165
256
  "ignore"\` may silence it) · anchor (anchoring a type) · related-drift (a scoped related row warns
166
257
  per selector HIT, misses silent) · owners (whole-file governors only; scoped don't count) ·
167
- confusable-id (two leaf ids one edit apart). spec lint's errors BLOCK commits (the pre-commit shim; bypass SPEXCODE_SKIP_LINT=1);
258
+ confusable-id (two leaf ids one edit apart). spec lint's errors BLOCK commits (the prepared reference hook; bypass SPEXCODE_SKIP_LINT=1);
168
259
  contrast \`spex eval lint\`, which is pure advisory and never blocks anyone.
169
260
 
170
261
  ack — stamp Spec-OK on HEAD (an empty stamp commit): the drift remedy when only MECHANICS changed
@@ -175,70 +266,18 @@ edit the spec instead — same commit as the code.`,
175
266
  },
176
267
  session: {
177
268
  line: 'session <verb> the worktree state machine: new · ls · watch · wait · review · merge · send · …',
178
- body: `Manager verbs (dispatch, monitor, land):
179
- spex session new "<prompt>" [--prompt-file <path>|-] [--launcher <name>]
180
- Launch a worker in its own node worktree. Give it ONLY its task — the dev-flow contract
181
- reaches it through the materialized system prompt. The prompt's first [[id]] mention binds
182
- the session to that node. --prompt-file <path>|- carries a long prompt
183
- without shell quoting (exclusive with the inline prompt). Then MONITOR it (wait/watch below).
184
- spex session ls [SEL…] [--status a,b] [--json] one-shot table of living sessions
185
- spex session watch [SEL…] [--as NAME] [--idle] [--interval N=5]
186
- Streams lifecycle transitions until killed — it NEVER EXITS; the human's forever stream. An
187
- agent must background it or use wait; blocking a turn on watch freezes you.
188
- spex session wait <SEL> [--timeout S=1200] [--interval S=2] [--idle]
189
- EDGE-TRIGGERED sleep on one session — ALWAYS run it in the BACKGROUND; its exit is your wake-up.
190
- Prints the session's current status immediately (stderr), then exits 0 only when it OBSERVES
191
- the session TRANSITION from a non-actionable status into an actionable one, printing the
192
- observed path on stdout (e.g. working→review — read the LAST token as the status reached).
193
- USE IT to sleep until a dispatched worker next needs you — including a dispatched MERGE
194
- actually landing (review→working while the merge runs, then the edge back is your wake-up).
195
- It NEVER returns just because the session is actionable ALREADY — for "what is it right NOW"
196
- use \`session ls\` / \`session review\` instead. --timeout is the guaranteed exit (code 1,
197
- observed path on stderr). Background one wait per worker.
198
- spex session review <SEL> [--json] the merge cockpit: ahead · uncommitted · proposal · gates ·
199
- merge-base diff — decide from this, don't hand-run git
200
- spex session merge <SEL> gated merge, dispatched to the session's OWN agent; confirm
201
- HEAD advanced before closing — closing unmerged discards work
202
-
203
- Control another session (all take SEL):
204
- spex session send <SEL> "<msg>" deliver a message (fail-loud: a dead dispatch exits non-zero)
205
- spex session interrupt <SEL> hard-interrupt the current turn through native harness control
206
- spex session send <SEL> --keys "<keys>"
207
- LAST RESORT: raw nav-mode keystrokes to a TUI dialog ("Up Up Enter", C-/M-/S- combos). The raw
208
- key surface is UNSTABLE and can confirm dangerous dialogs — don't reach for it unless a plain
209
- \`session send\` text provably cannot land.
210
- spex session rename <SEL> "<name>" set the display name ("" clears)
211
- spex session show <SEL> [--capture] [--json]
212
- The session record: status · node · branch · launcher · the full originating prompt.
213
- --capture prints the LIVE PANE as text instead (empty pane = exit 0; unknown session = exit 2).
214
- spex session resume <SEL> [--force] relaunch ONLY if confirmed offline (--force for a wedged one)
215
- spex session stop <SEL> soft stop: kill the agent, KEEP the worktree (resumable)
216
- spex session close <SEL> retire the session and its worktree
217
-
218
- Worker verbs (declare YOUR OWN state — a claim the graph and your supervisor act on):
219
- spex session done --propose merge|nothing|close [--note T] committed and stopping
220
- spex session park --note <what-you-await> a real background task will wake you
221
- spex session ask --note <your-question> stopped on the human; resumes on reply
222
-
223
- Human escape hatch:
224
- spex session attach <SEL> sit in the worker's REAL tmux (detach: C-b d). INTERACTIVE
225
- AND BLOCKING — an agent must NEVER run it in a turn: use
226
- show --capture / send. LOCAL-only (fails loud on a remote backend).
227
-
228
- ${SEL_NOTE}
229
- Manager verbs that WRITE (send/interrupt/rename/resume/stop/close/merge) are PROJECT-BOUND: a backend serving
230
- another project's repo refuses loudly — name the target with --api <url> to drive it on purpose.
231
- ${MENTION_NOTE}`,
269
+ body: sessionDrawerHelp(),
232
270
  see: 'spex eval ls --session <SEL> (the session’s measured loss) · spex help eval',
233
271
  },
234
272
  eval: {
235
- line: 'eval <verb> the measurement system: add · ls · scenario ls · matrix · lint · ok · retract · clean',
273
+ line: 'eval <verb> the measurement system: add · ls · scenario ls/write · matrix · lint · ok · retract · clean',
236
274
  body: `Usage: spex eval add [<node>|.] [--scenario <name>] (--pass|--fail) [--note <text>]
237
275
  [--image <png> …repeatable] [--result <path|->] [--video <webm|mp4>] [--timeline <json>]
238
276
  spex eval ls [<node>|.] [--json] a node's eval timeline, newest first
239
277
  spex eval ls --session <SEL> [--json] a session's aggregate: its changed nodes' scores
240
278
  spex eval ls --session <SEL> --export [--open | --out <path>]
241
- spex eval scenario ls [<node>|.] [--unmeasured] [--json] declared scenarios; bare = every node
279
+ spex eval scenario ls [<node>|.] [--unmeasured] [--json] declared scenarios; JSON = canonical index
280
+ spex eval scenario write --mutation <json> < eval.md propose one canonical metadata mutation
242
281
  spex eval matrix <launcher> [--node <id>] [--rows k1,k2] the harness live-behavior matrix
243
282
  spex eval lint [--changed] measurement-layer findings (advisory, always exit 0)
244
283
  spex eval ok <node> [--scenario <name>] the HUMAN sign-off on the scenario's latest measurement
@@ -251,12 +290,17 @@ the behaviour: MOVING/timed behaviour records a --video; a STATIC end state scre
251
290
  backend/CLI files a --result transcript. A fix's evidence is a fail→pass pair on the SAME scenario.
252
291
 
253
292
  ls — node-scoped bare (its per-scenario eval history); session-scoped with an EXPLICIT --session
254
- (never type-sniffed): every node the session's diff touches, blind spots first, its OWN measurements
255
- ✦-marked ahead of the inherited baseline. --export writes that evaluation as ONE self-contained
293
+ (never type-sniffed): filed evals are newest-first across nodes and source ownership; own measurements
294
+ are ✦-marked, evals filed by other sessions are unmarked, and blind spots follow measured rows. --export writes ONE self-contained
256
295
  HTML artifact (diff · evidence inlined · gates) for CI/sharing.
257
296
 
258
- scenario ls — the DECLARED contracts (name · tags · normalized test reference · latest verdict), no evals: bare lists every
259
- measurable node's scenarios; --unmeasured keeps only the never-measured the blind-spot worklist.
297
+ scenario ls — the DECLARED contracts, no evals: text may show the latest verdict and --unmeasured is its blind-spot
298
+ worklist; --json emits the complete canonical semantic/measurement index with stable hashes and fixed-tree provenance
299
+ (--unmeasured is text-only because the JSON projection never reads the eval sidecar).
300
+
301
+ scenario write — the fixed-tree declaration writer for an external measurement guard: stdin is the authoritative
302
+ eval.md, --mutation is one closed JSON insert/delete request for one scenario's test metadata, and stdout is only
303
+ the proposed eval.md bytes. It reads no worktree or runner and fails without stdout on malformed or ambiguous input.
260
304
 
261
305
  matrix — run the eight-row harness live-behavior matrix against a REAL dispatched session of the named
262
306
  launcher (the harness-adapter acceptance rule, defined once in spec-eval/src/matrix.ts): it syncs the
@@ -359,6 +403,7 @@ Machine plumbing — called by generated hooks and launch scripts, never typed b
359
403
  session-idle --session <id> the idle-prompt hook marks an active session idle
360
404
  commit-gate the Stop gate's deterministic commit check (exit 0 = ready to declare done)
361
405
  nudge <node> the post-merge hook prints the issue nudge for a merged node
406
+ shared-runtime-spawn <cwd> <log> <pid> <scope> <cmd> [args…] detached adapter control plane (launch script)
362
407
  codex-launch <sock> <cwd> [prompt…] backend-owned codex thread/start + first turn (launch script)
363
408
  codex-turn <sock> <threadId> <text…> fire a follow-up turn on an owned thread (tests/scripts)
364
409
  claude-headless-run <id> <runtime> <cmd> -- <tail…> resident stream-json controller (launch script)
@@ -377,9 +422,13 @@ your own state is declared with spex session done|park|ask.`,
377
422
  },
378
423
  }
379
424
 
380
- // `spex <cmd> --help` must meet the user wherever they typed it: cli.ts intercepts the probe pre-verb and
381
- // asks for the FIRST token's entry, so `spex session send --help` answers with the session drawer's entry.
382
- export function commandHelp(name: string): string | null {
425
+ // `spex <cmd> --help` must meet the user wherever they typed it: cli.ts intercepts the probe pre-verb.
426
+ // Session's noun-verb shape gets one extra projection; a bare noun still returns the complete drawer.
427
+ export function commandHelp(name: string, verb?: string): string | null {
428
+ if (name === 'session' && verb) {
429
+ const exact = sessionVerbHelp(verb)
430
+ if (exact) return `${exact}\n\nsee also: spex session (the complete drawer)\n\nmap: spex help · skills: spex guide`
431
+ }
383
432
  const e = ENTRIES[name]
384
433
  if (!e) return null
385
434
  const oneLiner = e.line.replace(/^\S+(\s+\S+)*?\s{2,}/, '') // the map line minus its "cmd args" column
@@ -387,6 +436,13 @@ export function commandHelp(name: string): string | null {
387
436
  return `${header}${e.body}${e.see ? `\n\nsee also: ${e.see}` : ''}\n\nmap: spex help · skills: spex guide`
388
437
  }
389
438
 
439
+ export function sessionLaunchReceipt(id: string): string {
440
+ return `spex: launched session ${id}
441
+ current result: the session JSON is on stdout now; \`spex session ls ${id}\` is the later one-shot snapshot
442
+ next lifecycle change: background \`spex session wait ${id}\` (edge-triggered; exits on the next non-actionable→actionable transition); \`spex session watch ${id}\` streams and NEVER EXITS
443
+ response channel: \`spex session send ${id} "<msg>"\`; \`send --keys\` is an UNSTABLE LAST RESORT after a plain send cannot land`
444
+ }
445
+
390
446
  export function overviewHelp(): string {
391
447
  return `spex — SpexCode CLI (spec↔code graph + worktree session state machine)
392
448