spexcode 0.6.7 → 0.6.8
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 +12 -7
- package/node_modules/@spexcode/session-application/dist/index.d.ts +17 -0
- package/node_modules/@spexcode/session-application/dist/index.js +35 -0
- package/node_modules/@spexcode/session-application/dist/migration.d.ts +51 -0
- package/node_modules/@spexcode/session-application/dist/migration.js +694 -0
- package/node_modules/@spexcode/session-application/dist/production.d.ts +109 -0
- package/node_modules/@spexcode/session-application/dist/production.js +438 -0
- package/node_modules/@spexcode/session-application/dist/schema.d.ts +2 -0
- package/node_modules/@spexcode/session-application/dist/schema.js +39 -0
- package/node_modules/@spexcode/session-application/package.json +29 -0
- package/node_modules/@spexcode/session-events/dist/errors.d.ts +6 -0
- package/node_modules/@spexcode/session-events/dist/errors.js +11 -0
- package/node_modules/@spexcode/session-events/dist/index.d.ts +46 -0
- package/node_modules/@spexcode/session-events/dist/index.js +245 -0
- package/node_modules/@spexcode/session-events/dist/schema.d.ts +3 -0
- package/node_modules/@spexcode/session-events/dist/schema.js +44 -0
- package/node_modules/@spexcode/session-events/package.json +24 -0
- package/node_modules/@spexcode/session-protocol/dist/canonical.d.ts +36 -0
- package/node_modules/@spexcode/session-protocol/dist/canonical.js +139 -0
- package/node_modules/@spexcode/session-protocol/dist/engine.d.ts +22 -0
- package/node_modules/@spexcode/session-protocol/dist/engine.js +478 -0
- package/node_modules/@spexcode/session-protocol/dist/errors.d.ts +8 -0
- package/node_modules/@spexcode/session-protocol/dist/errors.js +39 -0
- package/node_modules/@spexcode/session-protocol/dist/index.d.ts +60 -0
- package/node_modules/@spexcode/session-protocol/dist/index.js +4 -0
- package/node_modules/@spexcode/session-protocol/dist/schema.d.ts +36 -0
- package/node_modules/@spexcode/session-protocol/dist/schema.js +192 -0
- package/node_modules/@spexcode/{session-core → session-protocol}/package.json +4 -8
- package/node_modules/@spexcode/session-runtime/dist/errors.d.ts +6 -0
- package/node_modules/@spexcode/session-runtime/dist/errors.js +11 -0
- package/node_modules/@spexcode/session-runtime/dist/index.d.ts +33 -0
- package/node_modules/@spexcode/session-runtime/dist/index.js +193 -0
- package/node_modules/@spexcode/session-runtime/dist/schema.d.ts +2 -0
- package/node_modules/@spexcode/session-runtime/dist/schema.js +37 -0
- package/node_modules/@spexcode/session-runtime/package.json +24 -0
- package/node_modules/@spexcode/session-selflaunch/bin/spex-session.mjs +4 -0
- package/node_modules/@spexcode/session-selflaunch/dist/cli.d.ts +25 -0
- package/node_modules/@spexcode/session-selflaunch/dist/cli.js +156 -0
- package/node_modules/@spexcode/session-selflaunch/dist/index.d.ts +28 -0
- package/node_modules/@spexcode/session-selflaunch/dist/index.js +25 -0
- package/node_modules/@spexcode/session-selflaunch/dist/locality.d.ts +20 -0
- package/node_modules/@spexcode/session-selflaunch/dist/locality.js +83 -0
- package/node_modules/@spexcode/session-selflaunch/dist/path.d.ts +13 -0
- package/node_modules/@spexcode/session-selflaunch/dist/path.js +45 -0
- package/node_modules/@spexcode/session-selflaunch/package.json +35 -0
- package/node_modules/@spexcode/session-topology/dist/errors.d.ts +6 -0
- package/node_modules/@spexcode/session-topology/dist/errors.js +11 -0
- package/node_modules/@spexcode/session-topology/dist/index.d.ts +24 -0
- package/node_modules/@spexcode/session-topology/dist/index.js +173 -0
- package/node_modules/@spexcode/session-topology/dist/schema.d.ts +3 -0
- package/node_modules/@spexcode/session-topology/dist/schema.js +27 -0
- package/node_modules/@spexcode/session-topology/package.json +24 -0
- package/node_modules/@spexcode/spec-cli/bin/spex.mjs +56 -19
- package/node_modules/@spexcode/spec-cli/dist/cli.js +102 -59
- package/node_modules/@spexcode/spec-cli/dist/client.d.ts +1 -3
- package/node_modules/@spexcode/spec-cli/dist/client.js +49 -30
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +11 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +46 -9
- package/node_modules/@spexcode/spec-cli/dist/delivery-lock.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/delivery-lock.js +58 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.js +53 -11
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +2 -2
- package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +2 -1
- package/node_modules/@spexcode/spec-cli/dist/gateway.js +6 -3
- package/node_modules/@spexcode/spec-cli/dist/graphCache.js +32 -2
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +57 -2
- package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/graphStream.js +83 -3
- package/node_modules/@spexcode/spec-cli/dist/guide.js +20 -7
- package/node_modules/@spexcode/spec-cli/dist/harness-select.js +16 -3
- package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +15 -3
- package/node_modules/@spexcode/spec-cli/dist/harness.js +331 -50
- package/node_modules/@spexcode/spec-cli/dist/help.js +11 -8
- package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +8 -0
- package/node_modules/@spexcode/spec-cli/dist/host-resources.js +29 -8
- package/node_modules/@spexcode/spec-cli/dist/host.d.ts +7 -0
- package/node_modules/@spexcode/spec-cli/dist/host.js +93 -0
- package/node_modules/@spexcode/spec-cli/dist/index.js +324 -22
- package/node_modules/@spexcode/spec-cli/dist/init.js +1 -1
- package/node_modules/@spexcode/spec-cli/dist/lint.js +70 -35
- package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +3 -2
- package/node_modules/@spexcode/spec-cli/dist/listen.js +14 -2
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +1 -1
- package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +2 -2
- package/node_modules/@spexcode/spec-cli/dist/materialize.js +176 -35
- package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +14 -14
- package/node_modules/@spexcode/spec-cli/dist/reviews.js +12 -7
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +11 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +79 -1
- package/node_modules/@spexcode/spec-cli/dist/session-application.d.ts +23 -0
- package/node_modules/@spexcode/spec-cli/dist/session-application.js +189 -0
- package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +13 -1
- package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/session-files.js +13 -1
- package/node_modules/@spexcode/spec-cli/dist/session-follow.js +39 -22
- package/node_modules/@spexcode/{session-core/dist/record-lock.d.ts → spec-cli/dist/session-record-lock.d.ts} +0 -4
- package/node_modules/@spexcode/spec-cli/dist/session-record-lock.js +94 -0
- package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.d.ts +44 -0
- package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.js +37 -0
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +25 -2
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +68 -11
- package/node_modules/@spexcode/spec-cli/dist/session-web.js +4 -4
- package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +108 -15
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +1465 -744
- package/node_modules/@spexcode/spec-cli/dist/source-list.d.ts +13 -0
- package/node_modules/@spexcode/spec-cli/dist/source-list.js +99 -0
- package/node_modules/@spexcode/spec-cli/dist/source-read.d.ts +16 -0
- package/node_modules/@spexcode/spec-cli/dist/source-read.js +84 -0
- package/node_modules/@spexcode/spec-cli/dist/spec-attachments.d.ts +7 -0
- package/node_modules/@spexcode/spec-cli/dist/spec-attachments.js +89 -0
- package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.d.ts +23 -0
- package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.js +138 -0
- package/node_modules/@spexcode/spec-cli/dist/supervise.js +15 -6
- package/node_modules/@spexcode/spec-cli/dist/transcript-reader.d.ts +36 -0
- package/node_modules/@spexcode/spec-cli/dist/transcript-reader.js +251 -0
- package/node_modules/@spexcode/spec-cli/hooks/dispatch.sh +19 -31
- package/node_modules/@spexcode/spec-cli/hooks/harness.sh +6 -6
- package/node_modules/@spexcode/spec-cli/package.json +6 -6
- package/node_modules/@spexcode/spec-cli/templates/hooks/post-checkout +4 -2
- package/node_modules/@spexcode/spec-cli/templates/hooks/post-merge +2 -1
- package/node_modules/@spexcode/spec-cli/templates/hooks/pre-commit +5 -3
- package/node_modules/@spexcode/spec-cli/templates/hooks/reference-transaction +5 -3
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/spec.md +2 -7
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/idle.sh +4 -10
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/spec.md +1 -1
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/mark-active.sh +22 -24
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/spec.md +10 -2
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/fail.sh +8 -7
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/spec.md +3 -1
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/session-listen.sh +133 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/spec.md +36 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/spec.md +2 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/spec.md +1 -1
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +17 -20
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/merge/spec.md +33 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/spec.md +2 -6
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/spec.md +7 -0
- package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +13 -2
- package/node_modules/@spexcode/spec-core/dist/anchors.js +311 -15
- package/node_modules/@spexcode/spec-core/dist/git.d.ts +4 -0
- package/node_modules/@spexcode/spec-core/dist/git.js +9 -9
- package/node_modules/@spexcode/spec-core/dist/layout.d.ts +12 -0
- package/node_modules/@spexcode/spec-core/dist/layout.js +4 -2
- package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +1 -0
- package/node_modules/@spexcode/spec-core/dist/resilience.js +8 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +1 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +7 -3
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +1 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +4 -0
- package/node_modules/@spexcode/spec-core/dist/specs.d.ts +2 -0
- package/node_modules/@spexcode/spec-core/dist/specs.js +30 -0
- package/node_modules/@spexcode/spec-core/package.json +4 -1
- package/node_modules/@spexcode/spec-eval/dist/cli.js +1 -1
- package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +1 -0
- package/node_modules/@spexcode/spec-eval/dist/evaltab.js +6 -1
- package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +1 -1
- package/node_modules/@spexcode/spec-eval/dist/freshness.js +12 -12
- package/node_modules/@spexcode/spec-eval/dist/host.d.ts +2 -1
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +2 -1
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +20 -3
- package/node_modules/@spexcode/spec-eval/package.json +2 -2
- package/node_modules/@spexcode/spec-forge/package.json +2 -2
- package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
- package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
- package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
- package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
- package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
- package/package.json +9 -3
- package/node_modules/@spexcode/session-core/dist/delivery-queue.d.ts +0 -24
- package/node_modules/@spexcode/session-core/dist/delivery-queue.js +0 -206
- package/node_modules/@spexcode/session-core/dist/index.d.ts +0 -5
- package/node_modules/@spexcode/session-core/dist/index.js +0 -5
- package/node_modules/@spexcode/session-core/dist/internal.d.ts +0 -3
- package/node_modules/@spexcode/session-core/dist/internal.js +0 -3
- package/node_modules/@spexcode/session-core/dist/message.d.ts +0 -22
- package/node_modules/@spexcode/session-core/dist/message.js +0 -53
- package/node_modules/@spexcode/session-core/dist/record-lock.js +0 -152
- package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +0 -62
- package/node_modules/@spexcode/session-core/dist/runtime-session.js +0 -326
- package/node_modules/@spexcode/session-core/dist/session-cursors.d.ts +0 -14
- package/node_modules/@spexcode/session-core/dist/session-cursors.js +0 -82
- package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +0 -47
- package/node_modules/@spexcode/session-core/dist/session-timeline.js +0 -216
- package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -53
- package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-sed-v0.fixture +0 -46
|
@@ -7,7 +7,7 @@ import { Hono } from 'hono';
|
|
|
7
7
|
import { cors } from 'hono/cors';
|
|
8
8
|
import { etag } from 'hono/etag';
|
|
9
9
|
import { createNodeWebSocket } from '@hono/node-ws';
|
|
10
|
-
import { loadSpecs, loadSpecsLite, specContent, specHistory, specDiffAt, loadConfig, loadReviewConfig } from '@spexcode/spec-core';
|
|
10
|
+
import { loadSpecs, loadSpecsLite, specContent, specHistory, specDiffAt, loadConfig, loadReviewConfig, readAliasedRawRecord, runtimeRoot } from '@spexcode/spec-core';
|
|
11
11
|
import { issuesEnabled, resolveRemark, retractRemark } from './localIssues.js';
|
|
12
12
|
import { closeIssue, createIssue, findIssue, mergedIssues, promote } from './issues.js';
|
|
13
13
|
import { remarkWithLoopIn, replyIssueWithLoopIn } from './loop-in.js';
|
|
@@ -19,10 +19,12 @@ import { getBoardJson } from './graphCache.js';
|
|
|
19
19
|
import { boardStream, closeBoardFileWatchers, ensureBoardFileWatchers, notifyBoardChanged, flushDeferredWorktreeRegistryChange } from './graphStream.js';
|
|
20
20
|
import { gitA, gitTry, repoRoot } from '@spexcode/spec-core';
|
|
21
21
|
import { cockpitReview } from './cockpit.js';
|
|
22
|
-
import { listSessions, sendText, interruptSession, rawKey, stopSession, closeSession, quarantineCorruptRecord, restoreQuarantinedRecord,
|
|
22
|
+
import { EMPTY_PROMPT_ERROR, retractDiffComment, listSessions, listArchivedSessionIndex, sendText, drainSession, markHumanPromptActive, interruptSession, rawKey, stopSession, closeSession, quarantineCorruptRecord, restoreQuarantinedRecord, resumeSession, mergeSession, captureSessionResult, sessionPrompt, renameSession, setSessionSort, linkZCodeChildSession, projectCreatedSession, sessionCreateRequest, superviseQueue, superviseTurnFailures, superviseDelivery, startWorktreeTrashReaper, SessionRecordUnusable, TMUX_SOCK, sessionDiff, saveDiffComment, sendDiffComments, canonicalWatchRecipients } from './sessions.js';
|
|
23
23
|
import { readTimeline } from './session-timeline.js';
|
|
24
24
|
import { readSessionExecution, sessionExecutionStream } from './session-execution.js';
|
|
25
|
-
import { defaultHarness, HARNESSES, dashboardLauncherList, launcherDefault } from './harness.js';
|
|
25
|
+
import { defaultHarness, HARNESSES, codexHarness, dashboardLauncherList, launcherDefault, harnessById } from './harness.js';
|
|
26
|
+
import { ensureCodexGenerationLedger, reclaimDrainingCodexGenerations } from './codex-runtime-generations.js';
|
|
27
|
+
import { TranscriptReadError } from './transcript-reader.js';
|
|
26
28
|
import { readBlobByHash } from '@spexcode/spec-eval/evaltab';
|
|
27
29
|
import { putBlob } from '@spexcode/spec-eval/cache';
|
|
28
30
|
import { fileHumanReading } from '@spexcode/spec-eval/filing';
|
|
@@ -30,6 +32,10 @@ import { fileHumanOk } from '@spexcode/spec-eval/humanok';
|
|
|
30
32
|
import { buildExportModel, renderExportHtml, buildSessionEvals, SessionEvalUnavailableError } from '@spexcode/spec-eval/sessioneval';
|
|
31
33
|
import { appendUpload, cancelUpload, completeUpload, createUpload, evidenceMaxBytes, startUploadReaper, UploadError, uploadStatus } from './uploads.js';
|
|
32
34
|
import { listSessionFiles, openSessionFile, SESSION_FILE_PREVIEW_MAX_BYTES, sessionFilePreviewKind, SessionFileError } from './session-files.js';
|
|
35
|
+
import { readSourceSlice, SourceReadError, SOURCE_SLICE_MAX_BYTES } from './source-read.js';
|
|
36
|
+
import { listSourceDir } from './source-list.js';
|
|
37
|
+
import { loadConfig as loadLintConfig } from './lint.js';
|
|
38
|
+
import { listNodeAttachments, readNodeAttachment } from './spec-attachments.js';
|
|
33
39
|
import { attachViewer, detachViewer, resizeBridge, hideViewer, forwardInput, superviseBridges } from './pty-bridge.js';
|
|
34
40
|
import { installProcessGuards } from '@spexcode/spec-core';
|
|
35
41
|
import { resolveProjectIdentity } from '@spexcode/spec-core';
|
|
@@ -38,11 +44,17 @@ import { collectResourceReport, ResourceConflict } from './host-resources.js';
|
|
|
38
44
|
import { reparentRequest, SessionReparentRequestError } from './session-reparent.js';
|
|
39
45
|
import { buildGuidanceCatalog } from './guidance-catalog.js';
|
|
40
46
|
import { installEvalHost } from './eval-host.js';
|
|
47
|
+
import { configuredSessionApplicationIfCutover, setSessionApplicationCommitObserver } from './session-application.js';
|
|
48
|
+
import { editSpecBody, readSpecBodyEdit, SpecBodyEditError } from './spec-body-edit.js';
|
|
41
49
|
installEvalHost();
|
|
42
50
|
// last-resort net: an unforeseen async throw (e.g. a worktree vanishing mid-read during a worker
|
|
43
51
|
// self-merge) is logged and the server KEEPS SERVING instead of exiting and dropping the public port.
|
|
44
52
|
installProcessGuards();
|
|
53
|
+
startWorktreeTrashReaper();
|
|
45
54
|
const app = new Hono();
|
|
55
|
+
// Canonical lifecycle commits do not touch a watched JSON file. Bridge those commits into the existing board
|
|
56
|
+
// stream so status/proposal/parent changes arrive without waiting for a later human send or delivery tick.
|
|
57
|
+
setSessionApplicationCommitObserver(() => notifyBoardChanged('sessions'));
|
|
46
58
|
startUploadReaper();
|
|
47
59
|
app.use('/api/*', cors());
|
|
48
60
|
app.onError((error, c) => {
|
|
@@ -127,6 +139,24 @@ app.get('/api/specs/:id/content', (c) => {
|
|
|
127
139
|
const x = specContent(c.req.param('id'));
|
|
128
140
|
return x ? c.json(x) : c.json({ body: '', parts: null }, 404);
|
|
129
141
|
});
|
|
142
|
+
// [[spec-body-edit]]: the WRITE half of the spec document — a human at the board replaces a line range of
|
|
143
|
+
// a node's body and it lands as a real commit. The endpoint takes no path (it derives one from the node id)
|
|
144
|
+
// and rewrites nothing but the body, so a request cannot reach code or frontmatter; a region that moved
|
|
145
|
+
// since it was read is a 409 with the current text, never a merge. The version bump and drift are
|
|
146
|
+
// recomputed from the commit by [[source-of-truth]] — nothing else is written.
|
|
147
|
+
app.post('/api/specs/:id/body', async (c) => {
|
|
148
|
+
try {
|
|
149
|
+
const result = await editSpecBody(c.req.param('id'), readSpecBodyEdit(await c.req.json().catch(() => null)));
|
|
150
|
+
if (result.changed)
|
|
151
|
+
notifyBoardChanged('full');
|
|
152
|
+
return c.json(result);
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
if (e instanceof SpecBodyEditError)
|
|
156
|
+
return c.json({ error: e.message, code: e.code, ...(e.detail ?? {}) }, e.status);
|
|
157
|
+
throw e;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
130
160
|
app.get('/api/specs/:id/history', async (c) => c.json(await specHistory(c.req.param('id'))));
|
|
131
161
|
// the spec.md line diff one version introduced — the history tab's per-version proof-of-change, fetched
|
|
132
162
|
// lazily when an older version's item expands (the latest version's diff ships with the board as node.lastDiff).
|
|
@@ -135,6 +165,64 @@ app.get('/api/specs/:id/diff/:hash', async (c) => c.json(await specDiffAt(c.req.
|
|
|
135
165
|
// worktree's working tree. An untracked brand-new node is invisible to `git diff <base>`, so when the base
|
|
136
166
|
// diff is empty AND status is `??` synthesize an all-additions view via `diff --no-index` (gitTry — --no-index
|
|
137
167
|
// exits 1, which gitA would swallow). Gated on `??` so a tracked file with no pending change stays empty.
|
|
168
|
+
// [[source-read]]: a governed source file, read as a byte WINDOW. The spec tree names the files it governs
|
|
169
|
+
// but the board could never open one — this is the read half of "spec and code on one screen". The policy
|
|
170
|
+
// gate is `isSourceFile`, the SAME predicate the coverage walk uses, so the set of files the board can show
|
|
171
|
+
// is by construction the set the project governs. Query: path (repo-relative), offset, limit; the response
|
|
172
|
+
// carries the file's total size so the client can page without a second HEAD.
|
|
173
|
+
app.get('/api/source', (c) => {
|
|
174
|
+
try {
|
|
175
|
+
const root = repoRoot();
|
|
176
|
+
const slice = readSourceSlice(root, c.req.query('path') || '', loadLintConfig(root), Number(c.req.query('offset') ?? 0), Number(c.req.query('limit') ?? SOURCE_SLICE_MAX_BYTES));
|
|
177
|
+
return c.json(slice);
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
if (e instanceof SourceReadError)
|
|
181
|
+
return c.json({ error: e.message }, e.status);
|
|
182
|
+
throw e;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
// [[source-list]]: the LISTING half of the same surface. /api/source opens a file the caller can already
|
|
186
|
+
// name; this names what is there to open, one directory at a time, so the explorer can browse ordinary code
|
|
187
|
+
// the way any editor does. Same `isSourceFile` gate and the same refusals — a row the reader clicks and gets
|
|
188
|
+
// a 404 from is worse than a row that was never drawn. `dir` empty lists the governed roots themselves.
|
|
189
|
+
app.get('/api/files', (c) => {
|
|
190
|
+
try {
|
|
191
|
+
const root = repoRoot();
|
|
192
|
+
const cfg = loadLintConfig(root);
|
|
193
|
+
return c.json(listSourceDir(root, c.req.query('dir') || '', cfg, cfg.governedRoots));
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
if (e instanceof SourceReadError)
|
|
197
|
+
return c.json({ error: e.message }, e.status);
|
|
198
|
+
throw e;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
// [[node-attachments]]: what a node carries in its own folder besides its body and its readings. The board
|
|
202
|
+
// showed exactly one file per node folder; these are the rest — eval contracts, evidence dirs, raw captures.
|
|
203
|
+
// A different gate from /api/source on purpose (the spec tree is the product's own data, deliberately outside
|
|
204
|
+
// the coverage policy), the same windowed read underneath.
|
|
205
|
+
app.get('/api/specs/:id/files', (c) => {
|
|
206
|
+
try {
|
|
207
|
+
return c.json({ files: listNodeAttachments(repoRoot(), c.req.param('id')) });
|
|
208
|
+
}
|
|
209
|
+
catch (e) {
|
|
210
|
+
if (e instanceof SourceReadError)
|
|
211
|
+
return c.json({ error: e.message }, e.status);
|
|
212
|
+
throw e;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
app.get('/api/specs/:id/files/content', (c) => {
|
|
216
|
+
try {
|
|
217
|
+
const slice = readNodeAttachment(repoRoot(), c.req.param('id'), c.req.query('name') || '', Number(c.req.query('offset') ?? 0), Number(c.req.query('limit') ?? SOURCE_SLICE_MAX_BYTES));
|
|
218
|
+
return c.json(slice);
|
|
219
|
+
}
|
|
220
|
+
catch (e) {
|
|
221
|
+
if (e instanceof SourceReadError)
|
|
222
|
+
return c.json({ error: e.message }, e.status);
|
|
223
|
+
throw e;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
138
226
|
app.get('/api/edit', async (c) => {
|
|
139
227
|
const source = c.req.query('source') || '', path = c.req.query('path') || '';
|
|
140
228
|
if (!source || !path)
|
|
@@ -265,7 +353,8 @@ app.get('/api/evals/impact', etag(), async (c) => {
|
|
|
265
353
|
// most five lightweight neighbors. A missing worktree scope resolves explicitly to trunk; it never
|
|
266
354
|
// serializes another scenario's history or the scoped model.
|
|
267
355
|
app.get('/api/evals/detail', etag(), async (c) => {
|
|
268
|
-
|
|
356
|
+
// Detail is a bounded, direct eval read. It builds only the addressed scope and must not
|
|
357
|
+
// synchronously reconcile the global worktree watcher registry before answering.
|
|
269
358
|
const node = c.req.query('node')?.trim();
|
|
270
359
|
const scenario = c.req.query('scenario')?.trim();
|
|
271
360
|
if (!node || !scenario)
|
|
@@ -495,6 +584,7 @@ app.delete('/api/uploads/:id', (c) => {
|
|
|
495
584
|
// sessions: real tmux-backed Claude Code sessions. List + spawn, stream the live pane (WebSocket),
|
|
496
585
|
// forward keystrokes, and close.
|
|
497
586
|
app.get('/api/sessions', async (c) => c.json(await listSessions(c.req.query('all') === '1' || c.req.query('all') === 'true')));
|
|
587
|
+
app.get('/api/sessions/archive-index', async (c) => c.json(await listArchivedSessionIndex()));
|
|
498
588
|
app.get('/api/resources', async (c) => c.json(await collectResourceReport()));
|
|
499
589
|
app.post('/api/sessions', async (c) => {
|
|
500
590
|
const requestKey = c.req.header('idempotency-key') || randomUUID();
|
|
@@ -510,7 +600,7 @@ app.post('/api/sessions', async (c) => {
|
|
|
510
600
|
outgoing?.once('close', cancel);
|
|
511
601
|
try {
|
|
512
602
|
const body = await c.req.json().catch(() => null);
|
|
513
|
-
const result = await sessionCreateRequest(body, { requestKey, signal: controller.signal });
|
|
603
|
+
const result = await sessionCreateRequest(body, { requestKey, signal: controller.signal, onPublished: projectCreatedSession });
|
|
514
604
|
// The durable row is now public. Nudge the cheap session projection explicitly so a dashboard does not
|
|
515
605
|
// wait for the best-effort store watcher; any held candidate worktree event remains a separate full claim.
|
|
516
606
|
if (result.status === 201)
|
|
@@ -530,12 +620,149 @@ app.post('/api/sessions', async (c) => {
|
|
|
530
620
|
outgoing?.off('close', cancel);
|
|
531
621
|
}
|
|
532
622
|
});
|
|
623
|
+
const runtimeApplicationOr503 = (_c) => {
|
|
624
|
+
const application = configuredSessionApplicationIfCutover();
|
|
625
|
+
if (!application)
|
|
626
|
+
throw new ResourceConflict('session runtime is unavailable until the legacy JSON store is migrated');
|
|
627
|
+
return application;
|
|
628
|
+
};
|
|
629
|
+
app.get('/api/session-runtime/:id/events', (c) => {
|
|
630
|
+
const application = runtimeApplicationOr503(c);
|
|
631
|
+
return c.json(application.events.read(c.req.param('id')));
|
|
632
|
+
});
|
|
633
|
+
app.get('/api/session-runtime/:id/replay', (c) => {
|
|
634
|
+
const application = runtimeApplicationOr503(c);
|
|
635
|
+
return c.json(application.replayState(c.req.param('id')));
|
|
636
|
+
});
|
|
637
|
+
app.post('/api/session-runtime/:id/state', async (c) => {
|
|
638
|
+
const application = runtimeApplicationOr503(c);
|
|
639
|
+
const body = await c.req.json().catch(() => null);
|
|
640
|
+
if (body?.status !== undefined && typeof body.status !== 'string')
|
|
641
|
+
return c.json({ error: 'status must be a string' }, 400);
|
|
642
|
+
if (body?.proposal !== undefined && body.proposal !== null && typeof body.proposal !== 'string')
|
|
643
|
+
return c.json({ error: 'proposal must be a string or null' }, 400);
|
|
644
|
+
if (body?.note !== undefined && body.note !== null && typeof body.note !== 'string')
|
|
645
|
+
return c.json({ error: 'note must be a string or null' }, 400);
|
|
646
|
+
if (body?.parentSessionId !== undefined && body.parentSessionId !== null && typeof body.parentSessionId !== 'string')
|
|
647
|
+
return c.json({ error: 'parentSessionId must be a string or null' }, 400);
|
|
648
|
+
try {
|
|
649
|
+
const sessionId = c.req.param('id');
|
|
650
|
+
const nextStatus = body?.status ?? application.readState(sessionId)?.status;
|
|
651
|
+
return c.json(application.transitionSession(c.req.param('id'), {
|
|
652
|
+
status: body?.status,
|
|
653
|
+
proposal: body?.proposal,
|
|
654
|
+
note: body?.note,
|
|
655
|
+
parentSessionId: body?.parentSessionId,
|
|
656
|
+
reason: typeof body?.reason === 'string' ? body.reason : null,
|
|
657
|
+
recipientSessionIds: nextStatus === undefined ? undefined : canonicalWatchRecipients(application, sessionId, nextStatus),
|
|
658
|
+
}));
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
return c.json({ error: error instanceof Error ? error.message : String(error), code: error?.code }, 409);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
app.post('/api/session-runtime/:id/watch', async (c) => {
|
|
665
|
+
const application = runtimeApplicationOr503(c);
|
|
666
|
+
const body = await c.req.json().catch(() => null);
|
|
667
|
+
if (typeof body?.watcherSessionId !== 'string' || !body.watcherSessionId.trim())
|
|
668
|
+
return c.json({ error: 'watcherSessionId is required; identity is never inferred' }, 400);
|
|
669
|
+
const edge = application.attachWatcher(body.watcherSessionId, c.req.param('id'), typeof body.channel === 'string' ? body.channel : undefined);
|
|
670
|
+
return c.json(edge, 201);
|
|
671
|
+
});
|
|
672
|
+
app.post('/api/session-runtime/:id/bind', async (c) => {
|
|
673
|
+
const application = runtimeApplicationOr503(c);
|
|
674
|
+
const body = await c.req.json().catch(() => null);
|
|
675
|
+
if (!body || typeof body.namespace !== 'string' || typeof body.runtimeKind !== 'string' || typeof body.nativeSessionId !== 'string' || typeof body.nativeStartToken !== 'string') {
|
|
676
|
+
return c.json({ error: 'namespace, runtimeKind, nativeSessionId, and nativeStartToken are required; identity is never inferred' }, 400);
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
const binding = application.bindRuntime(c.req.param('id'), {
|
|
680
|
+
namespace: body.namespace,
|
|
681
|
+
runtimeKind: body.runtimeKind,
|
|
682
|
+
nativeSessionId: body.nativeSessionId,
|
|
683
|
+
nativeStartToken: body.nativeStartToken,
|
|
684
|
+
metadata: body.metadata && typeof body.metadata === 'object' && !Array.isArray(body.metadata) ? body.metadata : undefined,
|
|
685
|
+
}, typeof body.expectedGeneration === 'number' ? body.expectedGeneration : undefined);
|
|
686
|
+
return c.json(binding, 200);
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
return c.json({ error: error instanceof Error ? error.message : String(error), code: error?.code }, 409);
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
app.post('/api/session-runtime/:id/publish', async (c) => {
|
|
693
|
+
const application = runtimeApplicationOr503(c);
|
|
694
|
+
const body = await c.req.json().catch(() => null);
|
|
695
|
+
if (typeof body?.kind !== 'string' || typeof body.body !== 'string')
|
|
696
|
+
return c.json({ error: 'kind and UTF-8 body are required' }, 400);
|
|
697
|
+
const result = application.notifyRecipients(c.req.param('id'), {
|
|
698
|
+
kind: body.kind,
|
|
699
|
+
body: Buffer.from(body.body, 'utf8'),
|
|
700
|
+
senderSessionId: typeof body.senderSessionId === 'string' ? body.senderSessionId : undefined,
|
|
701
|
+
});
|
|
702
|
+
return c.json(result, 201);
|
|
703
|
+
});
|
|
704
|
+
app.post('/api/session-runtime/:id/dequeue', async (c) => {
|
|
705
|
+
const application = runtimeApplicationOr503(c);
|
|
706
|
+
const body = await c.req.json().catch(() => null);
|
|
707
|
+
if (typeof body?.namespace !== 'string')
|
|
708
|
+
return c.json({ error: 'namespace is required' }, 400);
|
|
709
|
+
try {
|
|
710
|
+
const message = application.dequeueForRuntime(c.req.param('id'), body.namespace, typeof body.expectedGeneration === 'number' ? body.expectedGeneration : undefined);
|
|
711
|
+
return c.json(message, 200);
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
return c.json({ error: error instanceof Error ? error.message : String(error), code: error?.code }, 409);
|
|
715
|
+
}
|
|
716
|
+
});
|
|
533
717
|
// one server-side merge bundle (ahead/dirty/diff(merge-base)/gates/proposal) for the manager cockpit;
|
|
534
718
|
// dashboard and `spex session review` are thin callers. 404 for an unknown id. See [[manager-cockpit]].
|
|
535
719
|
app.get('/api/sessions/:id/review', async (c) => {
|
|
536
720
|
const r = await cockpitReview(c.req.param('id'));
|
|
537
721
|
return r ? c.json(r) : c.json({ error: 'no such session' }, 404);
|
|
538
722
|
});
|
|
723
|
+
// Per-worktree diff: the branch's commits plus, while the session's worktree is on disk, its uncommitted
|
|
724
|
+
// changes. Metadata is cheap and patches are fetched per file — `path` with `scope` names which one — with an
|
|
725
|
+
// explicit byte window so a large review never materializes the whole tree in one response.
|
|
726
|
+
app.get('/api/sessions/:id/diff', async (c) => {
|
|
727
|
+
const offset = Math.max(0, Number(c.req.query('offset')) || 0);
|
|
728
|
+
const limit = Math.min(240_000, Math.max(1, Number(c.req.query('limit')) || 120_000));
|
|
729
|
+
const scope = c.req.query('scope') === 'working' ? 'working' : 'branch';
|
|
730
|
+
const result = await sessionDiff(c.req.param('id'), c.req.query('path') || undefined, offset, limit, scope);
|
|
731
|
+
return result ? c.json(result) : c.json({ error: 'no such session' }, 404);
|
|
732
|
+
});
|
|
733
|
+
app.post('/api/sessions/:id/diff-comments', async (c) => {
|
|
734
|
+
const body = await c.req.json().catch(() => ({}));
|
|
735
|
+
try {
|
|
736
|
+
const comment = await saveDiffComment(c.req.param('id'), {
|
|
737
|
+
id: typeof body?.id === 'string' ? body.id : undefined,
|
|
738
|
+
filePath: typeof body?.filePath === 'string' ? body.filePath : '',
|
|
739
|
+
lineStart: Number(body?.lineStart), lineEnd: Number(body?.lineEnd),
|
|
740
|
+
body: typeof body?.body === 'string' ? body.body : '',
|
|
741
|
+
diffIdentity: typeof body?.diffIdentity === 'string' ? body.diffIdentity : '',
|
|
742
|
+
});
|
|
743
|
+
return comment ? c.json(comment, 201) : c.json({ error: 'no such session' }, 404);
|
|
744
|
+
}
|
|
745
|
+
catch (error) {
|
|
746
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
// Retract removes one row from the record's review conversation. 404 for a row (or session) that is not
|
|
750
|
+
// there, so a double-retract is honestly "already gone" rather than a silent success.
|
|
751
|
+
app.delete('/api/sessions/:id/diff-comments/:commentId', async (c) => {
|
|
752
|
+
try {
|
|
753
|
+
const removed = await retractDiffComment(c.req.param('id'), c.req.param('commentId'));
|
|
754
|
+
return removed ? c.json(removed) : c.json({ error: 'no such diff comment' }, 404);
|
|
755
|
+
}
|
|
756
|
+
catch (error) {
|
|
757
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
app.post('/api/sessions/:id/diff-comments/send', async (c) => {
|
|
761
|
+
const body = await c.req.json().catch(() => ({}));
|
|
762
|
+
const ids = Array.isArray(body?.ids) ? body.ids.filter((id) => typeof id === 'string') : undefined;
|
|
763
|
+
const result = await sendDiffComments(c.req.param('id'), ids);
|
|
764
|
+
return c.json(result, result.ok ? 200 : 409);
|
|
765
|
+
});
|
|
539
766
|
// The self-contained HTML is the sole full-model transport exception. Interactive rows, including the CLI,
|
|
540
767
|
// use /api/evals pages; a bare request fails loudly rather than reopening a hidden full JSON path.
|
|
541
768
|
app.get('/api/sessions/:id/evals', async (c) => {
|
|
@@ -565,6 +792,52 @@ app.get('/api/sessions/:id/execution', (c) => {
|
|
|
565
792
|
return execution ? c.json(execution) : c.json({ error: 'no such session' }, 404);
|
|
566
793
|
});
|
|
567
794
|
app.get('/api/sessions/:id/execution/stream', (c) => sessionExecutionStream(c));
|
|
795
|
+
// The native transcript is a bounded payload behind the durable timeline index. Bounds are explicit epoch
|
|
796
|
+
// milliseconds so the route never guesses which status interval the caller intended.
|
|
797
|
+
app.get('/api/sessions/:id/transcript', async (c) => {
|
|
798
|
+
const id = c.req.param('id') || '';
|
|
799
|
+
const fromRaw = c.req.query('from');
|
|
800
|
+
const toRaw = c.req.query('to');
|
|
801
|
+
if (fromRaw == null || toRaw == null || fromRaw === '' || toRaw === '')
|
|
802
|
+
return c.json({ error: 'transcript needs both from and to epoch milliseconds' }, 400);
|
|
803
|
+
const from = Number(fromRaw);
|
|
804
|
+
const to = Number(toRaw);
|
|
805
|
+
if (!Number.isFinite(from) || !Number.isFinite(to) || !Number.isInteger(from) || !Number.isInteger(to) || from >= to)
|
|
806
|
+
return c.json({ error: 'transcript interval is invalid: from and to must be integer epoch milliseconds with from < to' }, 400);
|
|
807
|
+
let raw;
|
|
808
|
+
try {
|
|
809
|
+
raw = readAliasedRawRecord(id);
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
return c.json({ error: `session ${id} record is unreadable: ${error instanceof Error ? error.message : String(error)}` }, 500);
|
|
813
|
+
}
|
|
814
|
+
if (!raw || !raw.governed)
|
|
815
|
+
return c.json({ error: `session ${id} does not exist` }, 404);
|
|
816
|
+
let harness;
|
|
817
|
+
try {
|
|
818
|
+
harness = harnessById(typeof raw.harness === 'string' && raw.harness ? raw.harness : defaultHarness.id);
|
|
819
|
+
}
|
|
820
|
+
catch (error) {
|
|
821
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
|
|
822
|
+
}
|
|
823
|
+
const threadId = harness.exactNativeTargetId({
|
|
824
|
+
session: raw.session_id,
|
|
825
|
+
harnessSessionId: typeof raw.harness_session_id === 'string' ? raw.harness_session_id : null,
|
|
826
|
+
stopped: !!raw.stopped,
|
|
827
|
+
archived: !!raw.archived,
|
|
828
|
+
});
|
|
829
|
+
if (!threadId)
|
|
830
|
+
return c.json({ error: `session ${id} transcript is unavailable: native harness identity is missing` }, 409);
|
|
831
|
+
try {
|
|
832
|
+
return c.json(await harness.readTranscript(threadId, { from, to }));
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
if (!(error instanceof TranscriptReadError))
|
|
836
|
+
throw error;
|
|
837
|
+
const status = error.reason === 'unsupported' ? 501 : error.reason === 'invalid' ? 422 : 409;
|
|
838
|
+
return c.json({ error: error.message, reason: error.reason }, status);
|
|
839
|
+
}
|
|
840
|
+
});
|
|
568
841
|
// the session's persisted interaction history ([[session-timeline]]): authored status transitions (with the
|
|
569
842
|
// FULL note text) + delivered prompts, timestamped, oldest first — what a terminal-free surface renders as
|
|
570
843
|
// the conversation. `?limit=<n>` caps the tail (default 500). 404 for an unknown/non-governed id.
|
|
@@ -576,13 +849,6 @@ app.get('/api/sessions/:id/timeline', (c) => {
|
|
|
576
849
|
// the session RECORD detail (`spex session show`): the board row (status · node · branch · launcher · …)
|
|
577
850
|
// plus the full originating prompt (the row itself carries only the preview). One id-addressed read backs
|
|
578
851
|
// the CLI's show; 404 for an unknown id.
|
|
579
|
-
app.get('/api/sessions/:id/closure', (c) => {
|
|
580
|
-
// A missing close fact and a backend that predates this route are different answers. The client requires
|
|
581
|
-
// this capability marker even on 404, so an old backend can never turn a closed id into "never existed".
|
|
582
|
-
c.header('x-spexcode-close-history', 'v1');
|
|
583
|
-
const closure = findSessionClosure(c.req.param('id'));
|
|
584
|
-
return closure ? c.json(closure) : c.json({ error: 'no terminal close history for this session' }, 404);
|
|
585
|
-
});
|
|
586
852
|
app.get('/api/sessions/:id', async (c) => {
|
|
587
853
|
const id = c.req.param('id');
|
|
588
854
|
const row = (await listSessions(true)).find((s) => s.id === id);
|
|
@@ -645,7 +911,7 @@ app.post('/api/sessions/:id/resume', async (c) => {
|
|
|
645
911
|
// A merge intent to the session's own agent (it runs the merge), never a server merge.
|
|
646
912
|
app.post('/api/sessions/:id/merge', async (c) => {
|
|
647
913
|
const r = await mergeSession(c.req.param('id'));
|
|
648
|
-
return c.json(r, r.dispatched ? 200 :
|
|
914
|
+
return c.json(r, r.dispatched ? 200 : 409);
|
|
649
915
|
});
|
|
650
916
|
// one WS owns one native tmux client (pty-bridge): server→client = that client's rendered PTY bytes (binary);
|
|
651
917
|
// client→server text controls resize, visibility, and xterm-native input (which carries the mouse/wheel
|
|
@@ -764,7 +1030,10 @@ app.post('/api/sessions/:id/input', async (c) => {
|
|
|
764
1030
|
// `from` (the sender's session id) rides only an agent-to-agent send → the backend records the comms
|
|
765
1031
|
// edge ([[session-timeline]]); a raw human dispatch omits it and is not logged. `replyVia:"note"` marks a
|
|
766
1032
|
// terminal-free sender ([[session-timeline]]): the server appends the note-reply insert to the delivery.
|
|
767
|
-
const
|
|
1033
|
+
const text = typeof body?.text === 'string' ? body.text : '';
|
|
1034
|
+
if (!text.trim())
|
|
1035
|
+
return c.json({ ok: false, error: EMPTY_PROMPT_ERROR }, 400);
|
|
1036
|
+
const r = await sendText(c.req.param('id'), text, typeof body?.from === 'string' ? body.from : undefined, {
|
|
768
1037
|
...(body?.replyVia === 'note' ? { replyVia: 'note' } : {}),
|
|
769
1038
|
});
|
|
770
1039
|
return c.json(r, r.ok ? 200 : 502);
|
|
@@ -772,9 +1041,27 @@ app.post('/api/sessions/:id/input', async (c) => {
|
|
|
772
1041
|
if (body?.kind === 'command') {
|
|
773
1042
|
const id = c.req.param('id');
|
|
774
1043
|
const text = typeof body?.text === 'string' ? body.text : '';
|
|
775
|
-
|
|
1044
|
+
if (!text.trim())
|
|
1045
|
+
return c.json({ ok: false, error: EMPTY_PROMPT_ERROR }, 400);
|
|
1046
|
+
const deliveryKey = typeof body?.deliveryId === 'string' && body.deliveryId.trim() ? body.deliveryId.trim() : undefined;
|
|
1047
|
+
// Command Box acceptance is the durable append. Do not hold the HTTP request on a
|
|
1048
|
+
// slow native handoff: the delivery supervisor already owns the queued retry path.
|
|
1049
|
+
const r = await sendText(id, text, undefined, deliveryKey ? { deliveryKey, deferDrain: true } : { deferDrain: true });
|
|
776
1050
|
if (!r.ok)
|
|
777
1051
|
return c.json(r, 502);
|
|
1052
|
+
// Start the first handoff without holding the HTTP response on native readiness. The queue remains the
|
|
1053
|
+
// acceptance boundary; only a successful dequeue is allowed to publish human activity.
|
|
1054
|
+
// A deferred drain is an asynchronous handoff, not proof that a prompt was delivered. Only the
|
|
1055
|
+
// accepted queue state for this request may reopen a waiting lifecycle; an empty/raced drain must
|
|
1056
|
+
// never turn a focus-only or retry-only path into `working`.
|
|
1057
|
+
const handoffDeferred = r.delivery === 'deferred';
|
|
1058
|
+
void drainSession(id).then(() => {
|
|
1059
|
+
if (!handoffDeferred)
|
|
1060
|
+
return;
|
|
1061
|
+
const application = configuredSessionApplicationIfCutover();
|
|
1062
|
+
if (application ? !application.readPendingMessages(id).length : true)
|
|
1063
|
+
markHumanPromptActive(id);
|
|
1064
|
+
}).catch((error) => console.error(`spex: command handoff deferred for ${id}: ${error instanceof Error ? error.message : String(error)}`));
|
|
778
1065
|
const outcomes = await dispatchNewMentions(text, { sessionId: id });
|
|
779
1066
|
return c.json({ ...r, outcomes, mentionSummary: summarizeDispatch(outcomes) });
|
|
780
1067
|
}
|
|
@@ -820,14 +1107,8 @@ app.post('/api/sessions/:id/quarantine/restore', async (c) => {
|
|
|
820
1107
|
const result = await restoreQuarantinedRecord(c.req.param('id'));
|
|
821
1108
|
return c.json({ ok: true, ...result });
|
|
822
1109
|
});
|
|
823
|
-
// archive / legacy unarchive signpost ([[archive]]) — archive proves exact cold/offline ownership before filing;
|
|
824
|
-
// `{on:false}` enters the same resume transition and recreates the preserved conversation. {ok:false}=no such session.
|
|
825
|
-
app.post('/api/sessions/:id/archive', async (c) => {
|
|
826
|
-
const body = await c.req.json().catch(() => ({}));
|
|
827
|
-
return c.json({ ok: await archiveSession(c.req.param('id'), body?.on !== false) });
|
|
828
|
-
});
|
|
829
1110
|
// set (or clear, with a blank) a session's display-name override; persists to the session's global record
|
|
830
|
-
// (`
|
|
1111
|
+
// (`runtime.json`) so it survives a restart. Unknown id → 404. That envelope sits INSIDE the watched store, but
|
|
831
1112
|
// the store watch is best-effort (it can fail to attach), so the route still nudges the stream explicitly
|
|
832
1113
|
// ([[graph-stream]]) — the rename shows in ~150ms deterministically, never waiting out a cold tick.
|
|
833
1114
|
app.post('/api/sessions/:id/rename', async (c) => {
|
|
@@ -875,6 +1156,27 @@ const port = Number(process.env.PORT || 8787);
|
|
|
875
1156
|
const server = serve({ fetch: app.fetch, port, hostname: '127.0.0.1' });
|
|
876
1157
|
installConnectionReaper(server);
|
|
877
1158
|
injectWebSocket(server);
|
|
1159
|
+
// Reclaim only stale, draining Codex generations whose exact detached identity and native reference census
|
|
1160
|
+
// prove they have no loaded threads. The current generation and any uncertain/shared process remain intact.
|
|
1161
|
+
try {
|
|
1162
|
+
const root = runtimeRoot();
|
|
1163
|
+
await ensureCodexGenerationLedger(root);
|
|
1164
|
+
const descriptors = new Map((codexHarness.sharedRuntimes?.(root) ?? []).map((descriptor) => [descriptor.key, descriptor]));
|
|
1165
|
+
const results = await reclaimDrainingCodexGenerations(root, async (endpoint) => {
|
|
1166
|
+
const key = endpoint.id === 'legacy' ? 'codex-app-server' : `codex-app-server:${endpoint.id}`;
|
|
1167
|
+
const residency = descriptors.get(key)?.residency;
|
|
1168
|
+
if (!residency)
|
|
1169
|
+
return { healthy: false, referenceIds: [], peerCount: 0 };
|
|
1170
|
+
const result = await residency();
|
|
1171
|
+
return { healthy: result.healthy, referenceIds: result.referenceIds, peerCount: 0 };
|
|
1172
|
+
});
|
|
1173
|
+
for (const result of results)
|
|
1174
|
+
if (result.reclaimed)
|
|
1175
|
+
console.log(`[codex] reclaimed stale generation ${result.generationId}`);
|
|
1176
|
+
}
|
|
1177
|
+
catch (error) {
|
|
1178
|
+
console.error(`[codex] stale generation sweep retained resources: ${error.message}`);
|
|
1179
|
+
}
|
|
878
1180
|
superviseBridges(); // restore visible helpers after failure; their viewer subscriptions survive replacement
|
|
879
1181
|
superviseQueue(); // launch queued sessions as slots free (catches agent-authored proposals/crashes the server never sees directly)
|
|
880
1182
|
superviseTurnFailures(); // reconcile adapter-owned native failure subscriptions across backend replacement
|
|
@@ -127,7 +127,7 @@ export async function specInit(targetArg, presetArg, harnessArg) {
|
|
|
127
127
|
const flagRaw = (harnessArg ?? '').trim() ? parseHarnessFlag(harnessArg.trim()) : null;
|
|
128
128
|
const chosenHarnesses = flagRaw ?? readConfig(targetDir).harnesses ?? null;
|
|
129
129
|
if (chosenHarnesses === null) {
|
|
130
|
-
console.error(`spex init: --harness is required — name the harness(es) this repo delivers into, e.g. \`spex init --harness claude\`. Known native ids: ${NATIVE_HARNESS_IDS.join(', ')} (comma-separate several; a plugin bundle: --harness plugin:<folder>). A pre-existing spexcode.json "harnesses" field also satisfies this.`);
|
|
130
|
+
console.error(`spex init: --harness is required — name the harness(es) this repo delivers into, e.g. \`spex init --harness claude\`. Known native ids: ${NATIVE_HARNESS_IDS.join(', ')} (comma-separate several; a plugin bundle: --harness plugin:<folder>). Use --harness none to adopt the spec tree and its lint WITHOUT writing into any agent's config. A pre-existing spexcode.json "harnesses" field also satisfies this.`);
|
|
131
131
|
process.exit(1);
|
|
132
132
|
}
|
|
133
133
|
let selectedNativeEvents = null;
|