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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/spec-cli/bin/spex.mjs +25 -1
- package/spec-cli/hooks/dispatch.sh +24 -1
- package/spec-cli/src/anchors.ts +192 -58
- package/spec-cli/src/attach.ts +11 -8
- package/spec-cli/src/claude-headless.ts +13 -52
- package/spec-cli/src/cli.ts +209 -71
- package/spec-cli/src/client.ts +143 -5
- package/spec-cli/src/gateway-hub.ts +2 -6
- package/spec-cli/src/gateway.ts +96 -11
- package/spec-cli/src/git.ts +1395 -96
- package/spec-cli/src/graph.ts +21 -17
- package/spec-cli/src/graphCache.ts +405 -61
- package/spec-cli/src/graphStream.ts +581 -182
- package/spec-cli/src/guide.ts +13 -3
- package/spec-cli/src/harness.ts +946 -71
- package/spec-cli/src/headless-controller.ts +63 -0
- package/spec-cli/src/help.ts +120 -64
- package/spec-cli/src/host-resources.ts +706 -0
- package/spec-cli/src/index.ts +95 -21
- package/spec-cli/src/init.ts +47 -8
- package/spec-cli/src/issues.ts +36 -3
- package/spec-cli/src/layout.ts +193 -37
- package/spec-cli/src/lint.ts +152 -27
- package/spec-cli/src/localIssues.ts +5 -1
- package/spec-cli/src/maintenance-wrapper.ts +276 -0
- package/spec-cli/src/materialize.ts +6 -7
- package/spec-cli/src/opencode-headless.ts +4 -8
- package/spec-cli/src/pi-headless.ts +17 -60
- package/spec-cli/src/process-identity.ts +49 -0
- package/spec-cli/src/project-store.ts +17 -0
- package/spec-cli/src/pty-bridge.ts +6 -3
- package/spec-cli/src/reviews.ts +17 -13
- package/spec-cli/src/runtime-ownership.ts +99 -0
- package/spec-cli/src/session-maintenance.ts +750 -0
- package/spec-cli/src/session-timeline.ts +6 -6
- package/spec-cli/src/sessions.ts +1249 -208
- package/spec-cli/src/sh.ts +3 -0
- package/spec-cli/src/source-files.ts +27 -2
- package/spec-cli/src/specs.ts +58 -25
- package/spec-cli/src/supervise.ts +31 -11
- package/spec-cli/src/uninstall.ts +7 -6
- package/spec-cli/templates/hooks/commit-msg +14 -0
- package/spec-cli/templates/hooks/pre-commit +26 -6
- package/spec-cli/templates/hooks/prepare-commit-msg +20 -48
- package/spec-cli/templates/hooks/reference-transaction +105 -0
- package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/core/mark-active/mark-active.sh +23 -21
- package/spec-cli/templates/spec/project/.plugins/core/spec.md +12 -10
- package/spec-cli/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +15 -6
- package/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +32 -0
- package/spec-dashboard/dist/assets/App-C5vbTw8Q.js +2 -0
- package/spec-dashboard/dist/assets/Dashboard-u8RIS3NY.js +27 -0
- package/spec-dashboard/dist/assets/EvalsPage-BS7ITcNo.js +2 -0
- package/spec-dashboard/dist/assets/IssuesPage-DXbqQFW_.js +1 -0
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/spec-dashboard/dist/assets/MobileApp-DVLnk9hz.js +2 -0
- package/spec-dashboard/dist/assets/Modal-6mHq6fbZ.js +1 -0
- package/spec-dashboard/dist/assets/{PageScroll-CadAKuSy.js → PageScroll-CAY4S4g4.js} +1 -1
- package/spec-dashboard/dist/assets/ProjectsPage-UQyzsTWN.js +1 -0
- package/spec-dashboard/dist/assets/SessionInterface-DKU4c1Z-.js +39 -0
- package/spec-dashboard/dist/assets/SessionWindow-zGwJaGbR.js +1 -0
- package/spec-dashboard/dist/assets/Settings-igR17pns.js +1 -0
- package/spec-dashboard/dist/assets/Thread-B-ZUarN1.js +13 -0
- package/spec-dashboard/dist/assets/TimelineChat-Cp579UoJ.css +1 -0
- package/spec-dashboard/dist/assets/TimelineChat-sc49Qj5d.js +276 -0
- package/spec-dashboard/dist/assets/data-B1ot4PF0.js +1 -0
- package/spec-dashboard/dist/assets/index-BqBNCa1V.js +41 -0
- package/spec-dashboard/dist/assets/index-CzutlTDf.css +1 -0
- package/spec-dashboard/dist/assets/{terminalFont-DmTPNdbU.js → terminalFont-C6oGNKrf.js} +1 -1
- package/spec-dashboard/dist/index.html +5 -4
- package/spec-dashboard/src/reviewFilters.js +46 -8
- package/spec-dashboard/src/session.js +64 -9
- package/spec-eval/src/cli.ts +144 -32
- package/spec-eval/src/evaltab.ts +16 -9
- package/spec-eval/src/freshness.ts +336 -54
- package/spec-eval/src/scenariofresh.ts +51 -1
- package/spec-eval/src/scenarios.ts +324 -25
- package/spec-eval/src/sessioneval.ts +969 -173
- package/spec-cli/src/message-stream.ts +0 -147
- package/spec-dashboard/dist/assets/Dashboard-CTAuTyZ3.js +0 -27
- package/spec-dashboard/dist/assets/EvalsPage-KbMMownG.js +0 -2
- package/spec-dashboard/dist/assets/IssuesPage-DmyLb9Rj.js +0 -1
- package/spec-dashboard/dist/assets/MobileApp-D2RZGt4Z.js +0 -2
- package/spec-dashboard/dist/assets/Modal-3brXUhM0.js +0 -1
- package/spec-dashboard/dist/assets/ProjectsPage-DU3x4Y8l.js +0 -1
- package/spec-dashboard/dist/assets/SessionInterface-BtrzlOPs.js +0 -39
- package/spec-dashboard/dist/assets/SessionWindow-BWH5O0jh.js +0 -13
- package/spec-dashboard/dist/assets/Settings-COgdKTJB.js +0 -1
- package/spec-dashboard/dist/assets/TimelineChat-DQ21GSJK.js +0 -1
- package/spec-dashboard/dist/assets/index-D6HBvKkJ.js +0 -41
- package/spec-dashboard/dist/assets/index-DFdlYy4H.css +0 -1
package/spec-cli/src/cli.ts
CHANGED
|
@@ -20,7 +20,8 @@ const cmd = process.argv[2]
|
|
|
20
20
|
// (loadConfig on a malformed spexcode.json) surfaces as uncaughtException, not unhandledRejection, so BOTH
|
|
21
21
|
// paths route through the same printer.
|
|
22
22
|
function fatal(e: unknown): never {
|
|
23
|
-
if (e instanceof Error &&
|
|
23
|
+
if (e instanceof Error && e.name === 'SessionMaintenanceError') console.error(`spex: ${(e as Error & { code?: string }).code ?? 'maintenance_error'}: ${e.message}`)
|
|
24
|
+
else if (e instanceof Error && ['BackendError', 'ConfigError', 'UsageError', 'GuardError'].includes(e.name)) console.error(`spex: ${e.message}`)
|
|
24
25
|
else console.error(e)
|
|
25
26
|
process.exit(1)
|
|
26
27
|
}
|
|
@@ -49,7 +50,7 @@ function flushExit(code = 0): Promise<never> {
|
|
|
49
50
|
}
|
|
50
51
|
const has = (name: string) => process.argv.includes(`--${name}`)
|
|
51
52
|
// bare positionals after argv index `from`, skipping flags and their values (selectors for ls/watch).
|
|
52
|
-
const VALUE_FLAGS = new Set(['--status', '--as', '--interval', '--propose', '--note', '--node', '--prompt', '--prompt-file', '--timeout', '--reason', '--out', '--password', '--tls-cert', '--tls-key', '--harness', '--launcher', '--harness-session', '--port', '--api', '--api-port', '--host', '--preset', '--limit', '--session', '--depth', '--focus', '--keys'])
|
|
53
|
+
const VALUE_FLAGS = new Set(['--status', '--as', '--interval', '--propose', '--note', '--node', '--prompt', '--prompt-file', '--timeout', '--reason', '--out', '--password', '--tls-cert', '--tls-key', '--harness', '--launcher', '--harness-session', '--port', '--api', '--api-port', '--host', '--preset', '--limit', '--session', '--depth', '--focus', '--keys', '--allow-stop', '--allow-resume', '--ttl-ms', '--wait-ms'])
|
|
53
54
|
function positionals(from: number): string[] {
|
|
54
55
|
const out: string[] = []
|
|
55
56
|
for (let i = from; i < process.argv.length; i++) {
|
|
@@ -126,25 +127,6 @@ function signpost(oldSpelling: string, newSpelling: string): never {
|
|
|
126
127
|
}
|
|
127
128
|
if (cmd !== undefined && SIGNPOSTS[cmd]) signpost(`spex ${cmd}`, SIGNPOSTS[cmd])
|
|
128
129
|
|
|
129
|
-
// After a successful launch, nudge the caller to actually MONITOR the session — launch-then-forget is a real
|
|
130
|
-
// gap (a supervisor or human launches and then never watches, so a review/failure goes unnoticed). Goes to
|
|
131
|
-
// STDERR so the JSON on stdout (which callers parse) stays clean; keyed to whoever's calling — a supervising
|
|
132
|
-
// agent has an own-session id, a human at a terminal does not. The hint also names the COMM channel
|
|
133
|
-
// (`spex session send`) — field-tested gap: callers who couldn't find it reached for raw tmux keystrokes instead.
|
|
134
|
-
async function launchMonitorReminder(id: string): Promise<void> {
|
|
135
|
-
const { ownSessionId } = await import('./sessions.js')
|
|
136
|
-
const agent = ownSessionId()
|
|
137
|
-
console.error(`\nspex: launched session ${id} — now MONITOR it, or its review/failure goes unnoticed:`)
|
|
138
|
-
if (agent) {
|
|
139
|
-
// a supervising agent: the per-worker monitor is a backgrounded `spex session wait`, which is edge-triggered.
|
|
140
|
-
console.error(` supervising agent → background \`spex session wait ${id}\` (edge-triggered: exits when it OBSERVES the session transition into an actionable status — also how you await a dispatched merge actually landing; its exit is your wake-up. Already actionable and you just want to read it? \`spex session ls\`)`)
|
|
141
|
-
console.error(` or watch the whole stream: \`spex session watch\``)
|
|
142
|
-
} else {
|
|
143
|
-
console.error(` \`spex session watch\` — the live stream of actionable session transitions (or \`spex session wait ${id}\` to sleep until this one's next transition into actionable)`)
|
|
144
|
-
}
|
|
145
|
-
console.error(` talk to it: \`spex session send ${id} "<msg>"\` — plain text; \`send --keys\` is a LAST RESORT (unstable raw TUI keys — only when a text send provably can't land)`)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
130
|
const greeted = new Set<string>()
|
|
149
131
|
async function greetWatchTargets(watcher: string, selectors: string[]): Promise<void> {
|
|
150
132
|
try {
|
|
@@ -218,7 +200,7 @@ async function evalExport(id: string): Promise<never> {
|
|
|
218
200
|
|
|
219
201
|
// appended to a done/ask/block declaration: the note is durable conversation history even though the
|
|
220
202
|
// CURRENT board projection correctly flips back to active on the next tool call.
|
|
221
|
-
const DECLARED = ' — recorded; the human sees it in the dashboard. This declaration remains in the session timeline; your next tool call flips only the current
|
|
203
|
+
const DECLARED = ' — recorded; the human sees it in the dashboard. This declaration remains in the session timeline; your next tool call flips only the current graph state back to active (the mark-active hook, by design).'
|
|
222
204
|
// appended ONLY to a propose-close declaration: a worktree about to be discarded may still own ephemeral things the agent started to test this change; nudge (not gate) it to reclaim them before the worktree goes, keyed on whether the thing should outlive the task — never on who started it (a deliberately long-running service / a production build is started-by-you yet must be left alone). Project-agnostic on purpose.
|
|
223
205
|
const CLOSE_CLEANUP = '\n\nBefore this worktree closes, check whether you left anything running that you started to test this change — a background process, a dev or preview server, a bound port, a scratch session. If nothing depends on it anymore, shut it down, or it keeps running as an orphan. Leave anything meant to keep running: a service you deliberately stood up, a production build, anything other work relies on. What matters is whether it still needs to exist after this task, not whether you started it. If unsure, leave it. This is a reminder to check, not a required step.'
|
|
224
206
|
|
|
@@ -250,9 +232,16 @@ async function stateKit() {
|
|
|
250
232
|
}
|
|
251
233
|
// a state writer from a non-repo cwd throws git's not-a-repo before it can return false — map exactly
|
|
252
234
|
// that throw to the no-record path (noRecord re-probes and names the cwd); anything else stays loud.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
235
|
+
// A record that exists but CANNOT carry state (unreadable bytes, or a worktree that is gone) is its own
|
|
236
|
+
// answer: the writer refused on purpose and already knows why, so that reason is what the caller prints —
|
|
237
|
+
// never the no-record diagnosis, which would send the author chasing a wrong cwd ([[sessions-core]]).
|
|
238
|
+
const mark = (fn: () => boolean): { ok: boolean; reason?: string } => {
|
|
239
|
+
try { return { ok: fn() } }
|
|
240
|
+
catch (e) {
|
|
241
|
+
if (e instanceof s.SessionRecordUnusable) return { ok: false, reason: e.message }
|
|
242
|
+
if (/not a git repository/i.test(String((e as any)?.stderr ?? e))) return { ok: false }
|
|
243
|
+
throw e
|
|
244
|
+
}
|
|
256
245
|
}
|
|
257
246
|
// truncation transparency ([[state]]): the session table shows only the first NOTE_BOARD_LIMIT chars of a
|
|
258
247
|
// note. When a declared note overflows that cap, the confirmation says so — length, what the board shows,
|
|
@@ -278,12 +267,12 @@ async function stateKit() {
|
|
|
278
267
|
|
|
279
268
|
// a trailing --help/-h prints help and exits BEFORE any verb runs, so a help probe never fires a
|
|
280
269
|
// streaming/mutating command. It prints THAT command's usage when an entry exists (the second layer
|
|
281
|
-
// of the help journey — see help.ts)
|
|
282
|
-
// drawer
|
|
283
|
-
// table above already exited.)
|
|
270
|
+
// of the help journey — see help.ts); session noun-verb probes project the exact verb from the shared
|
|
271
|
+
// drawer definition. Unknown tokens preserve the existing drawer/map fallback. (Removed spellings never
|
|
272
|
+
// reach here — the signpost table above already exited.)
|
|
284
273
|
if (cmd && cmd !== 'help' && (has('help') || process.argv.includes('-h'))) {
|
|
285
274
|
const { commandHelp, overviewHelp } = await import('./help.js')
|
|
286
|
-
console.log(commandHelp(cmd) ?? overviewHelp())
|
|
275
|
+
console.log(commandHelp(cmd, cmd === 'session' ? process.argv[3] : undefined) ?? overviewHelp())
|
|
287
276
|
process.exit(0)
|
|
288
277
|
}
|
|
289
278
|
|
|
@@ -455,14 +444,21 @@ if (cmd === 'serve') {
|
|
|
455
444
|
console.log(`${rel} is governed whole-file by ${whole.length} specs (all claims: ${ids}) — more than one file should hold. This file does TOO MUCH: SPLIT it so each governor owns its own module (or merge the nodes if they're one concern, or give it a single foundation owner + relate the rest).${relLine}`)
|
|
456
445
|
}
|
|
457
446
|
} else if (sub === 'lint') {
|
|
458
|
-
const { specLint, DRIFT_GUIDANCE } = await import('./lint.js')
|
|
459
|
-
const
|
|
447
|
+
const { specLint, pendingTouchesGoverned, DRIFT_GUIDANCE } = await import('./lint.js')
|
|
448
|
+
const pending = flag('pending')
|
|
449
|
+
if (pending && process.env.SPEXCODE_GATE_SCOPE_ONLY === '1') {
|
|
450
|
+
const touches = await pendingTouchesGoverned(process.cwd(), pending)
|
|
451
|
+
if (!touches) process.exit(76)
|
|
452
|
+
}
|
|
453
|
+
const findings = await specLint(undefined, undefined, {
|
|
454
|
+
tip: pending || 'HEAD',
|
|
455
|
+
})
|
|
460
456
|
const errors = findings.filter((f) => f.level === 'error')
|
|
461
457
|
for (const f of findings) console.error(` ${f.level === 'error' ? '✗' : '•'} ${f.rule}: ${f.msg}`)
|
|
462
458
|
console.error(`spex spec lint: ${errors.length} error(s), ${findings.length - errors.length} warning(s)`)
|
|
463
459
|
// drift teaches from the ONE `spex spec lint` (no flag). Unanchored drift stays advisory forever; the
|
|
464
|
-
//
|
|
465
|
-
//
|
|
460
|
+
// Blocking tier is anchor-drift ([[code-anchor]]) — CI/default lint judges HEAD; the local armed hook
|
|
461
|
+
// supplies --pending <real commit oid>; canonical pre-commit leaves the whole lint to that one run.
|
|
466
462
|
if (findings.some((f) => f.rule === 'drift' || f.rule === 'anchor-drift')) console.error(`\n${DRIFT_GUIDANCE}`)
|
|
467
463
|
process.exit(errors.length ? 1 : 0)
|
|
468
464
|
} else if (sub === 'ack') {
|
|
@@ -513,9 +509,9 @@ if (cmd === 'serve') {
|
|
|
513
509
|
} else if (sub === 'ls' && flag('session') !== undefined) {
|
|
514
510
|
// the session EVAL read ([[session-eval]]'s interactive face as a CLI verb): the dashboard Eval tab's
|
|
515
511
|
// text twin. Renders the session's changed nodes with each DECLARED scenario at its CURRENT score
|
|
516
|
-
// (latest reading per scenario, worktree-rooted) —
|
|
517
|
-
//
|
|
518
|
-
// artifact instead.
|
|
512
|
+
// (latest reading per scenario, worktree-rooted) — filed results keep the backend's newest-first order
|
|
513
|
+
// across ownership, the session's OWN measurements are ✦-marked, and blind spots follow. --export writes
|
|
514
|
+
// the self-contained HTML artifact instead.
|
|
519
515
|
const id = await resolveSelectorOrExit(flag('session')!)
|
|
520
516
|
if (has('export')) await evalExport(id)
|
|
521
517
|
const { clientEvals } = await import('./client.js')
|
|
@@ -523,35 +519,31 @@ if (cmd === 'serve') {
|
|
|
523
519
|
if (!r.ok) { console.error(`no evals for ${id} (status ${r.status})`); process.exit(1) }
|
|
524
520
|
if (has('json')) { console.log(JSON.stringify(r.model, null, 2)); await flushExit(0) }
|
|
525
521
|
const m = r.model
|
|
526
|
-
const byNode = new Map<string, any[]>()
|
|
527
|
-
for (const item of m.items) {
|
|
528
|
-
const rows = byNode.get(item.node) ?? []
|
|
529
|
-
rows.push(item)
|
|
530
|
-
byNode.set(item.node, rows)
|
|
531
|
-
}
|
|
532
|
-
const groups = [...byNode].map(([node, rows]) => ({ node, rows }))
|
|
533
522
|
const own = m.items.filter((item) => item.inSession).length
|
|
534
523
|
console.log(`eval session [${m.id}]`)
|
|
535
524
|
console.log(` gates : ${m.gates.map((g) => `${g.ok ? '✓' : '✗'} ${g.label} — ${g.detail}`).join(' · ')}`)
|
|
536
|
-
if (own) console.log(` ✦ : ${own} scenario(s) measured by THIS session (unmarked rows =
|
|
525
|
+
if (own) console.log(` ✦ : ${own} scenario(s) measured by THIS session (unmarked rows = evals filed by other sessions)`)
|
|
537
526
|
if (!m.items.length) console.log('\n no affected scenarios to evaluate yet')
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
527
|
+
let lastNode: string | null = null
|
|
528
|
+
for (const e of m.items) {
|
|
529
|
+
if (e.node !== lastNode) {
|
|
530
|
+
console.log(`\n${e.node}`)
|
|
531
|
+
lastNode = e.node
|
|
532
|
+
}
|
|
533
|
+
if (e.filterKind === 'blind') {
|
|
534
|
+
console.log(` ∅ unmeasured ${e.scenario} — declared, never measured (blind spot)`)
|
|
535
|
+
continue
|
|
547
536
|
}
|
|
537
|
+
const verdict = e.verdict?.status === 'pass' ? '✓ pass' : e.verdict?.status === 'fail' ? '✗ fail' : '· unscored'
|
|
538
|
+
const stale = e.fresh ? '' : ` (stale: ${(e.staleAxes || []).join(',')})`
|
|
539
|
+
console.log(` ${e.inSession ? '✦' : ' '} ${verdict}${stale} ${e.scenario} — ${e.ts}${e.evaluator ? ` · ${e.evaluator}` : ''}`)
|
|
548
540
|
}
|
|
549
541
|
} else if (['add', 'ls', 'scenario', 'matrix', 'lint', 'ok', 'retract', 'clean'].includes(sub)) {
|
|
550
542
|
// node-scoped verbs — thin route; the logic lives in spec-eval.
|
|
551
543
|
const { runEval } = await import('../../spec-eval/src/cli.js')
|
|
552
544
|
await flushExit(await runEval(process.argv.slice(3)))
|
|
553
545
|
} else {
|
|
554
|
-
console.error(`spex eval: unknown verb '${sub}' — add | ls | scenario ls | matrix | lint | ok | retract | clean (spex help eval)`)
|
|
546
|
+
console.error(`spex eval: unknown verb '${sub}' — add | ls | scenario ls/write | matrix | lint | ok | retract | clean (spex help eval)`)
|
|
555
547
|
if (!sub.startsWith('--')) console.error(` (the old \`spex eval <SEL>\` session read is now \`spex eval ls --session <SEL>\` [--export])`) // dead-words-ok: signpost — one-version tombstone teaching the renamed spelling (0.4.0 removes it)
|
|
556
548
|
process.exit(2)
|
|
557
549
|
}
|
|
@@ -624,6 +616,51 @@ if (cmd === 'serve') {
|
|
|
624
616
|
console.log((await import('./help.js')).commandHelp('session'))
|
|
625
617
|
} else if (SESSION_SIGNPOSTS[sub]) {
|
|
626
618
|
signpost(`spex session ${sub}`, SESSION_SIGNPOSTS[sub])
|
|
619
|
+
} else if (sub === 'maintain') {
|
|
620
|
+
const args = process.argv.slice(4)
|
|
621
|
+
if (args.length === 1 && args[0] === '--status') {
|
|
622
|
+
const state = await (await import('./client.js')).clientMaintenanceStatus()
|
|
623
|
+
console.log(JSON.stringify(state, null, 2))
|
|
624
|
+
} else {
|
|
625
|
+
const separator = args.indexOf('--')
|
|
626
|
+
if (separator < 0 || separator === args.length - 1) {
|
|
627
|
+
console.error('usage: spex session maintain [--allow-stop SEL] [--allow-resume SEL[:force]] [--ttl-ms MS] [--wait-ms MS] -- <command> [args...]')
|
|
628
|
+
process.exit(2)
|
|
629
|
+
}
|
|
630
|
+
const options = args.slice(0, separator)
|
|
631
|
+
const command = args.slice(separator + 1)
|
|
632
|
+
const capabilities: import('./session-maintenance.js').Capability[] = []
|
|
633
|
+
let ttlMs = 30_000
|
|
634
|
+
let waitMs = 10_000
|
|
635
|
+
for (let i = 0; i < options.length; i++) {
|
|
636
|
+
const option = options[i]
|
|
637
|
+
if (option === '--api' || option === '--port') { i++; continue }
|
|
638
|
+
if (option === '--ttl-ms' || option === '--wait-ms') {
|
|
639
|
+
const value = Number(options[++i])
|
|
640
|
+
if (!Number.isSafeInteger(value)) { console.error(`spex session maintain: ${option} requires an integer millisecond value`); process.exit(2) }
|
|
641
|
+
if (option === '--ttl-ms') ttlMs = value
|
|
642
|
+
else waitMs = value
|
|
643
|
+
continue
|
|
644
|
+
}
|
|
645
|
+
if (option === '--allow-stop' || option === '--allow-resume') {
|
|
646
|
+
const raw = options[++i]
|
|
647
|
+
if (!raw) { console.error(`spex session maintain: ${option} requires a session selector`); process.exit(2) }
|
|
648
|
+
if (option === '--allow-stop') {
|
|
649
|
+
capabilities.push({ op: 'stop', sessionId: await resolveSelectorOrExit(raw) })
|
|
650
|
+
} else {
|
|
651
|
+
const force = raw.endsWith(':force')
|
|
652
|
+
const selector = force ? raw.slice(0, -':force'.length) : raw
|
|
653
|
+
capabilities.push({ op: 'resume', sessionId: await resolveSelectorOrExit(selector), force })
|
|
654
|
+
}
|
|
655
|
+
continue
|
|
656
|
+
}
|
|
657
|
+
console.error(`spex session maintain: unknown option ${option}`)
|
|
658
|
+
process.exit(2)
|
|
659
|
+
}
|
|
660
|
+
if (capabilities.length === 0) { console.error('spex session maintain: at least one exact stop/resume capability is required'); process.exit(2) }
|
|
661
|
+
const { runMaintenanceWrapper } = await import('./maintenance-wrapper.js')
|
|
662
|
+
process.exit(await runMaintenanceWrapper({ capabilities, ttlMs, waitMs, command }))
|
|
663
|
+
}
|
|
627
664
|
} else if (sub === 'new') {
|
|
628
665
|
// spex session new "<prompt>" (prompt = first positional or --prompt, or --prompt-file
|
|
629
666
|
// <path>|- so a long multi-paragraph prompt never fights shell quoting — [[prompt-file]]).
|
|
@@ -648,22 +685,40 @@ if (cmd === 'serve') {
|
|
|
648
685
|
}
|
|
649
686
|
const created = await createSession(prompt, flag('launcher') ?? undefined)
|
|
650
687
|
console.log(JSON.stringify(created, null, 2))
|
|
651
|
-
await
|
|
688
|
+
console.error((await import('./help.js')).sessionLaunchReceipt(created.id))
|
|
652
689
|
} else if (sub === 'ls') {
|
|
653
690
|
// pretty list of living sessions + states. `spex session ls [SEL...] [--status a,b] [--json]`
|
|
654
691
|
// the board comes from the backend (so it shows the sessions of whatever SPEXCODE_API_URL points at,
|
|
655
692
|
// incl. a remote machine); selectSessions/formatTable are pure presentation, applied client-side.
|
|
656
693
|
const { selectSessions, formatTable } = await import('./sessions.js')
|
|
657
694
|
const { clientListSessions } = await import('./client.js')
|
|
658
|
-
|
|
695
|
+
// The backend's default projection excludes cold archives. --all and an explicit selector request the
|
|
696
|
+
// history projection so an operator can still inspect or unarchive one deliberately.
|
|
697
|
+
const selectors = positionals(4)
|
|
698
|
+
const all = await clientListSessions(has('all') || selectors.length > 0)
|
|
699
|
+
const visible = all
|
|
700
|
+
const picked = selectSessions(visible, selectors, flag('status')?.split(','))
|
|
659
701
|
console.log(has('json') ? JSON.stringify(picked, null, 2) : formatTable(picked))
|
|
702
|
+
} else if (sub === 'resources') {
|
|
703
|
+
rejectUnknownFlags('spex session resources', 4, ['json', 'api', 'port'])
|
|
704
|
+
const { clientResources } = await import('./client.js')
|
|
705
|
+
const report = await clientResources()
|
|
706
|
+
if (has('json')) console.log(JSON.stringify(report, null, 2))
|
|
707
|
+
else console.log((await import('./host-resources.js')).formatResourceReport(report))
|
|
660
708
|
} else if (sub === 'watch') {
|
|
661
709
|
const { watchSessions } = await import('./sessions.js')
|
|
662
710
|
const { clientListSessions } = await import('./client.js')
|
|
663
711
|
const selectors = positionals(4)
|
|
664
712
|
const intervalMs = (Number(flag('interval')) || 5) * 1000
|
|
713
|
+
// Broad watch keeps the default active-only event population, so the archive shelf is not replayed into
|
|
714
|
+
// ordinary monitoring. Presence is a separate all-record read: hiding a cold row must not turn it into
|
|
715
|
+
// "gone"/closed. An explicit selector opts into the history population so its archive/offline transition
|
|
716
|
+
// remains observable.
|
|
717
|
+
const history = () => clientListSessions(true)
|
|
718
|
+
const events = selectors.length ? history : () => clientListSessions(false)
|
|
665
719
|
await withWatchEdge(selectors, intervalMs, () => watchSessions((line) => console.log(line), {
|
|
666
|
-
source:
|
|
720
|
+
source: events,
|
|
721
|
+
presenceSource: history,
|
|
667
722
|
selectors,
|
|
668
723
|
statuses: flag('status')?.split(','),
|
|
669
724
|
includeIdle: has('idle'),
|
|
@@ -683,8 +738,12 @@ if (cmd === 'serve') {
|
|
|
683
738
|
if (own) console.error(`spex session wait: heads-up (managed agent ${own.slice(0, 8)}) — this command BLOCKS until it OBSERVES ${id} transition from non-actionable into an actionable status (edge-triggered: an already-actionable current state does NOT return it — to just read the state now, use \`spex session ls\`/\`review\`); run it in the BACKGROUND or it freezes your whole turn (its exit is your wake-up). Proceeding.`)
|
|
684
739
|
const intervalMs = (Number(flag('interval')) || 2) * 1000
|
|
685
740
|
const timeoutSec = Number(flag('timeout')) || 1200
|
|
741
|
+
// `wait` addresses one explicit record. Read its history row for both events and presence so archive is an
|
|
742
|
+
// offline transition, not a vanished session; only a missing all-record row is a genuine gone/closed result.
|
|
743
|
+
const history = () => clientListSessions(true)
|
|
686
744
|
const r = await withWatchEdge([id], intervalMs, () => watchSessions(() => {}, {
|
|
687
|
-
source:
|
|
745
|
+
source: history,
|
|
746
|
+
presenceSource: history,
|
|
688
747
|
selectors: [id],
|
|
689
748
|
includeIdle: has('idle'),
|
|
690
749
|
intervalMs,
|
|
@@ -732,6 +791,11 @@ if (cmd === 'serve') {
|
|
|
732
791
|
console.log(' gates:')
|
|
733
792
|
console.log(` conflicts w/ main : ${g.conflictsWithMain ? 'YES' : 'no'}`)
|
|
734
793
|
console.log(` lint : ${g.lint.errorCount} error(s), ${g.lint.warningCount} warning(s)`)
|
|
794
|
+
// measured loss is REPORTED, never graded — and a projection that isn't ready says so instead of
|
|
795
|
+
// printing four zeros that would read as a clean session.
|
|
796
|
+
console.log(` evals : ${g.evals.phase === 'ready'
|
|
797
|
+
? `${g.evals.freshPass} fresh pass, ${g.evals.freshFail} fresh fail, ${g.evals.needReview} need review, ${g.evals.blind} blind`
|
|
798
|
+
: `not measured yet (${g.evals.phase})`}`)
|
|
735
799
|
console.log(` diff (merge-base, ${r.diff.length} file(s)):`)
|
|
736
800
|
for (const f of r.diff) console.log(` ${f.status.padEnd(12)} +${f.additions} -${f.deletions} ${f.path}`)
|
|
737
801
|
}
|
|
@@ -771,30 +835,44 @@ if (cmd === 'serve') {
|
|
|
771
835
|
try { closeNote += (await import('./localIssues.js')).closeoutNudge(sess ?? s.ownSessionId()) }
|
|
772
836
|
catch (e) { console.error(`issue closeout check failed (declaration unaffected): ${e instanceof Error ? e.message : e}`) }
|
|
773
837
|
}
|
|
774
|
-
|
|
838
|
+
const done = mark(() => s.markDone(p, sess, flag('note')))
|
|
839
|
+
console.log(done.ok ? `done (${p})${DECLARED}${noteEcho(flag('note'))}${closeNote}` : done.reason ?? noRecord())
|
|
775
840
|
} else if (sub === 'park') {
|
|
776
841
|
// sugar: the agent is waiting on a background task; it will self-resume (NOT idle/awaiting)
|
|
777
842
|
const { s, sess, mark, noRecord, noteEcho } = await stateKit()
|
|
778
|
-
|
|
843
|
+
const parked = mark(() => s.markState('parked', { note: flag('note'), sessionId: sess }))
|
|
844
|
+
console.log(parked.ok ? `parked${DECLARED}${noteEcho(flag('note'))}` : parked.reason ?? noRecord())
|
|
779
845
|
} else if (sub === 'ask') {
|
|
780
846
|
// the agent DELIBERATELY declares it is pausing to ask the human a question (like `done`/`park`, an
|
|
781
847
|
// authored state — NOT guarded active-only). The --note carries the question. Distinct from `park`
|
|
782
848
|
// (waiting on a background task, self-resumes): an asking agent resumes only when the human replies.
|
|
783
849
|
const { s, sess, mark, noRecord, noteEcho } = await stateKit()
|
|
784
|
-
|
|
850
|
+
const asked = mark(() => s.markState('asking', { note: flag('note'), sessionId: sess }))
|
|
851
|
+
console.log(asked.ok ? `asking${DECLARED}${noteEcho(flag('note'))}` : asked.reason ?? noRecord())
|
|
785
852
|
} else if (sub === 'stop') {
|
|
786
853
|
// the SOFT stop: kill the agent's tmux + socket but KEEP the worktree, so the session goes offline and
|
|
787
854
|
// can be resumed (`session resume`). Distinct from `close`, which removes the worktree.
|
|
788
855
|
const full = await resolveSelectorOrExit(id)
|
|
789
|
-
|
|
856
|
+
const stopped = await c.clientStop(full)
|
|
857
|
+
if (!stopped) { console.error(`spex session stop: no such session ${full} (no stop transition was committed)`); process.exit(1) }
|
|
858
|
+
console.log(`stopped ${full} (worktree kept — resumable)`)
|
|
790
859
|
} else if (sub === 'interrupt') {
|
|
791
860
|
const full = await resolveSelectorOrExit(id)
|
|
792
861
|
const r = await c.clientInterrupt(full)
|
|
793
862
|
console.log(r.ok ? `interrupted ${full}` : `interrupt failed: ${r.error}`)
|
|
794
863
|
process.exit(r.ok ? 0 : 1)
|
|
864
|
+
} else if (sub === 'archive' || sub === 'unarchive') {
|
|
865
|
+
// ARCHIVING/legacy unarchive ([[archive]]) — archive exact-stops before filing; unarchive signposts to
|
|
866
|
+
// resume and recreates the same conversation.
|
|
867
|
+
const on = sub === 'archive'
|
|
868
|
+
const full = await resolveSelectorOrExit(id)
|
|
869
|
+
const ok = await c.clientArchive(full, on)
|
|
870
|
+
console.log(!ok ? `no such session ${full}` : `${on ? 'archived' : 'resumed'} ${full}`)
|
|
795
871
|
} else if (sub === 'close') {
|
|
796
872
|
const full = await resolveSelectorOrExit(id)
|
|
797
|
-
|
|
873
|
+
const closed = await c.clientClose(full)
|
|
874
|
+
if (!closed) { console.error(`spex session close: no such session ${full} (record remains; no close was committed)`); process.exit(1) }
|
|
875
|
+
console.log(`closed ${full}`)
|
|
798
876
|
} else if (sub === 'send') {
|
|
799
877
|
const full = await resolveSelectorOrExit(id)
|
|
800
878
|
if (has('keys')) {
|
|
@@ -872,7 +950,7 @@ if (cmd === 'serve') {
|
|
|
872
950
|
// machine's) and terminal-only (an agent must never block its turn on it — capture/send).
|
|
873
951
|
const { assertLocalBackend, attachSession } = await import('./attach.js')
|
|
874
952
|
await assertLocalBackend()
|
|
875
|
-
await attachSession(await resolveSelectorOrExit(id))
|
|
953
|
+
process.exit(await attachSession(await resolveSelectorOrExit(id)))
|
|
876
954
|
} else {
|
|
877
955
|
console.error(`spex session: unknown verb '${sub}' — new | ls | show | watch | wait | review | merge | send | interrupt | rename | resume | stop | close | attach | done | park | ask (spex help session)`)
|
|
878
956
|
process.exit(2)
|
|
@@ -901,6 +979,58 @@ if (cmd === 'serve') {
|
|
|
901
979
|
if (!path) throw new Error(`governor '${owner.id}' has no live spec path`)
|
|
902
980
|
console.log(`${owner.id}\t${path}`)
|
|
903
981
|
}
|
|
982
|
+
} else if (sub === 'maintenance-begin') {
|
|
983
|
+
const ownerPid = Number(process.argv[4]), sessionId = process.argv[5]
|
|
984
|
+
if (!Number.isInteger(ownerPid) || ownerPid <= 0 || !sessionId) {
|
|
985
|
+
console.error('usage: spex internal maintenance-begin <owner-pid> <session-id>')
|
|
986
|
+
process.exit(2)
|
|
987
|
+
}
|
|
988
|
+
const { exactProcessIdentity, maintenanceErrorPayload, sessionMaintenance } = await import('./session-maintenance.js')
|
|
989
|
+
const owner = exactProcessIdentity(ownerPid)
|
|
990
|
+
if (!owner) { console.error('maintenance_identity_unknown: hook dispatcher owner identity is not exact'); process.exit(2) }
|
|
991
|
+
try {
|
|
992
|
+
console.log(sessionMaintenance().beginExternalOperation({ op: 'hook-state', sessionId }, owner))
|
|
993
|
+
} catch (error) {
|
|
994
|
+
const payload = maintenanceErrorPayload(error)
|
|
995
|
+
console.error(payload ? `${payload.code}: ${payload.error}` : String(error))
|
|
996
|
+
process.exit(2)
|
|
997
|
+
}
|
|
998
|
+
} else if (sub === 'maintenance-end') {
|
|
999
|
+
const ticket = process.argv[4], ownerPid = Number(process.argv[5])
|
|
1000
|
+
if (!ticket || !Number.isInteger(ownerPid) || ownerPid <= 0) {
|
|
1001
|
+
console.error('usage: spex internal maintenance-end <ticket-id> <owner-pid>')
|
|
1002
|
+
process.exit(2)
|
|
1003
|
+
}
|
|
1004
|
+
const { exactProcessIdentity, sessionMaintenance } = await import('./session-maintenance.js')
|
|
1005
|
+
const owner = exactProcessIdentity(ownerPid)
|
|
1006
|
+
if (!owner) { console.error('maintenance_identity_unknown: hook dispatcher owner identity is not exact'); process.exit(2) }
|
|
1007
|
+
sessionMaintenance().finishExternalOperation(ticket, owner)
|
|
1008
|
+
} else if (sub === 'shared-runtime-spawn') {
|
|
1009
|
+
const [cwd, logFile, pidFile, isolationFile, command] = process.argv.slice(4, 9)
|
|
1010
|
+
const args = process.argv.slice(9)
|
|
1011
|
+
if (!cwd || !logFile || !pidFile || !isolationFile || !command) {
|
|
1012
|
+
console.error('usage: spex internal shared-runtime-spawn <cwd> <log> <pid-file> <isolation-file> <command> [args...]')
|
|
1013
|
+
process.exit(2)
|
|
1014
|
+
}
|
|
1015
|
+
const { readFileSync } = await import('node:fs')
|
|
1016
|
+
const { runSessionOperation } = await import('./session-maintenance.js')
|
|
1017
|
+
const { spawnDetachedRuntime } = await import('./runtime-ownership.js')
|
|
1018
|
+
const delegateFdValue = process.env.SPEXCODE_MAINTENANCE_DELEGATE_FD
|
|
1019
|
+
const delegateChannelPresent = delegateFdValue !== undefined
|
|
1020
|
+
const delegateFd = Number(delegateFdValue)
|
|
1021
|
+
let delegate = ''
|
|
1022
|
+
if (delegateChannelPresent && Number.isInteger(delegateFd) && delegateFd >= 3) {
|
|
1023
|
+
try { delegate = readFileSync(delegateFd, 'utf8').trim() } catch { delegate = '' }
|
|
1024
|
+
}
|
|
1025
|
+
const sessionId = process.env.SPEXCODE_MAINTENANCE_SESSION_ID?.trim()
|
|
1026
|
+
|| process.env.SPEXCODE_SESSION_ID?.trim() || 'shared-runtime'
|
|
1027
|
+
const env = { ...process.env }
|
|
1028
|
+
delete env.SPEXCODE_MAINTENANCE_DELEGATE_FD
|
|
1029
|
+
delete env.SPEXCODE_MAINTENANCE_SESSION_ID
|
|
1030
|
+
delete env.SPEXCODE_SESSION_ID
|
|
1031
|
+
const runtime = await runSessionOperation({ op: 'shared-spawn', sessionId, ...(delegateChannelPresent ? { delegate } : {}) }, () =>
|
|
1032
|
+
spawnDetachedRuntime({ cwd, logFile, pidFile, isolationFile, command, args, env }))
|
|
1033
|
+
console.log(runtime.pid)
|
|
904
1034
|
} else if (sub === 'codex-launch') {
|
|
905
1035
|
// BACKEND-owned codex thread. On the shared per-project app-server: thread/start { cwd = this worktree }
|
|
906
1036
|
// (codex loads that worktree's config/hooks/AGENTS.md), store the new id on the governed record (keyed by
|
|
@@ -915,7 +1045,11 @@ if (cmd === 'serve') {
|
|
|
915
1045
|
// the thread the BACKEND owns must carry `bypass_hook_trust` in thread/start's config so the app-server fires
|
|
916
1046
|
// the worktree's local hooks — mirror materialize's capability decision so the two stay in lockstep.
|
|
917
1047
|
const bypassHookTrust = codexSupportsBypassHookTrust(codexBinary(process.env.SPEXCODE_CODEX_CMD || 'codex'))
|
|
918
|
-
|
|
1048
|
+
// The governed record id rides into the thread's own shell environment (shell_environment_policy.set), so
|
|
1049
|
+
// every command this thread spawns knows which session it is — the codex equivalent of the launch-injected
|
|
1050
|
+
// id claude gets. codex-launch is exactly where both ids are known ([[harness-adapter]]).
|
|
1051
|
+
const ownId = process.env.SPEXCODE_SESSION_ID?.trim()
|
|
1052
|
+
const r = await codexStartThread(sock, cwd, bypassHookTrust, ownId ? { SPEXCODE_SESSION_ID: ownId } : undefined)
|
|
919
1053
|
if (!r.ok) { console.error(r.error); process.exit(1) }
|
|
920
1054
|
if (prompt) {
|
|
921
1055
|
const t = await codexTurn(sock, r.threadId, prompt, cwd)
|
|
@@ -994,11 +1128,12 @@ if (cmd === 'serve') {
|
|
|
994
1128
|
const { s, sess, mark, noRecord, noteEcho } = await stateKit()
|
|
995
1129
|
const st = process.argv[4] as any
|
|
996
1130
|
const ok = mark(() => s.markState(st, { proposal: flag('propose') as any, note: flag('note'), sessionId: sess }))
|
|
997
|
-
console.log(ok ? `state -> ${st}${noteEcho(flag('note'))}` : noRecord())
|
|
1131
|
+
console.log(ok.ok ? `state -> ${st}${noteEcho(flag('note'))}` : ok.reason ?? noRecord())
|
|
998
1132
|
} else if (sub === 'session-fail') {
|
|
999
1133
|
// the StopFailure hook marks its session (--session from the payload) as error (turn died on an API error)
|
|
1000
1134
|
const { s, sess, mark, noRecord } = await stateKit()
|
|
1001
|
-
|
|
1135
|
+
const failed = mark(() => s.markError(sess))
|
|
1136
|
+
console.log(failed.ok ? 'marked error' : failed.reason ?? noRecord())
|
|
1002
1137
|
} else if (sub === 'session-turn-fail') {
|
|
1003
1138
|
// Headless adapters report an ephemeral turn's non-zero exit through this one shared CAS. A declaration
|
|
1004
1139
|
// that landed before teardown wins, so a late child close can never erase an agent-authored state.
|
|
@@ -1014,14 +1149,17 @@ if (cmd === 'serve') {
|
|
|
1014
1149
|
// at its prompt. INFERRED, so guarded active-only: it no-ops unless the current status is exactly `active`,
|
|
1015
1150
|
// never clobbering a deliberate awaiting/asking/parked/error declaration. Distinct from `session ask`
|
|
1016
1151
|
// (the agent deliberately asking the human) — idle is the undeclared stop the Stop gate missed.
|
|
1017
|
-
const { s, sess } = await stateKit()
|
|
1018
|
-
|
|
1152
|
+
const { s, sess, mark } = await stateKit()
|
|
1153
|
+
const idled = mark(() => s.markIdle(sess))
|
|
1154
|
+
console.log(idled.ok ? 'idle' : idled.reason ?? 'noop (no session record, or not active)')
|
|
1019
1155
|
} else if (sub === 'commit-gate') {
|
|
1020
1156
|
// the Stop gate's deterministic commit check (from cwd = the worktree): exit 0 if the node branch is
|
|
1021
|
-
// ready to declare done
|
|
1157
|
+
// ready to declare done, else print the reason and exit 1. Takes the PROPOSAL being judged — `merge`
|
|
1158
|
+
// additionally requires commits ahead of main, `nothing` only a clean tree (see mergeReadiness). Uses
|
|
1022
1159
|
// git() so the hook's exported GIT_DIR/GIT_INDEX_FILE don't misdirect repo discovery (see git.ts).
|
|
1023
1160
|
const { s } = await stateKit()
|
|
1024
|
-
const
|
|
1161
|
+
const kind = positionals(4)[0] === 'nothing' ? 'nothing' : 'merge'
|
|
1162
|
+
const r = s.mergeReadiness(kind)
|
|
1025
1163
|
if (r.ready) { console.log('ready'); process.exit(0) }
|
|
1026
1164
|
console.log(r.reason)
|
|
1027
1165
|
process.exit(1)
|