spexcode 0.6.2 → 0.6.6
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 +3 -1
- package/bin/spex.mjs +4 -0
- package/node_modules/@spexcode/session-core/dist/delivery-queue.d.ts +23 -0
- package/node_modules/@spexcode/session-core/dist/delivery-queue.js +195 -0
- package/node_modules/@spexcode/session-core/dist/index.d.ts +5 -0
- package/node_modules/@spexcode/session-core/dist/index.js +5 -0
- package/node_modules/@spexcode/session-core/dist/internal.d.ts +3 -0
- package/node_modules/@spexcode/session-core/dist/internal.js +3 -0
- package/node_modules/@spexcode/session-core/dist/message.d.ts +22 -0
- package/node_modules/@spexcode/session-core/dist/message.js +52 -0
- package/node_modules/@spexcode/session-core/dist/record-lock.d.ts +7 -0
- package/node_modules/@spexcode/session-core/dist/record-lock.js +152 -0
- package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +50 -0
- package/node_modules/@spexcode/session-core/dist/runtime-session.js +286 -0
- package/node_modules/@spexcode/session-core/dist/session-cursors.d.ts +14 -0
- package/node_modules/@spexcode/session-core/dist/session-cursors.js +82 -0
- package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +46 -0
- package/node_modules/@spexcode/session-core/dist/session-timeline.js +216 -0
- package/node_modules/@spexcode/session-core/package.json +33 -0
- package/node_modules/@spexcode/spec-cli/bin/spex.mjs +77 -0
- package/node_modules/@spexcode/spec-cli/dist/attach.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/attach.js +48 -0
- package/node_modules/@spexcode/spec-cli/dist/claude-headless.d.ts +36 -0
- package/node_modules/@spexcode/spec-cli/dist/claude-headless.js +316 -0
- package/node_modules/@spexcode/spec-cli/dist/cli.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/cli.js +1908 -0
- package/node_modules/@spexcode/spec-cli/dist/client.d.ts +112 -0
- package/node_modules/@spexcode/spec-cli/dist/client.js +481 -0
- package/node_modules/@spexcode/spec-cli/dist/cockpit.d.ts +7 -0
- package/node_modules/@spexcode/spec-cli/dist/cockpit.js +26 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-headless.d.ts +1 -0
- package/{spec-cli/src/codex-headless.ts → node_modules/@spexcode/spec-cli/dist/codex-headless.js} +3 -9
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +58 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +798 -0
- package/node_modules/@spexcode/spec-cli/dist/commit-surgery.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/commit-surgery.js +90 -0
- package/node_modules/@spexcode/spec-cli/dist/contract-filter.d.ts +14 -0
- package/node_modules/@spexcode/spec-cli/dist/contract-filter.js +219 -0
- package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.js +50 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.js +553 -0
- package/node_modules/@spexcode/spec-cli/dist/eval-host.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/eval-host.js +19 -0
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +26 -0
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +475 -0
- package/node_modules/@spexcode/spec-cli/dist/file-write.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/file-write.js +26 -0
- package/node_modules/@spexcode/spec-cli/dist/flat.d.ts +89 -0
- package/node_modules/@spexcode/spec-cli/dist/flat.js +1076 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-auth.d.ts +56 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-auth.js +168 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-hub.d.ts +28 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +363 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway.d.ts +38 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway.js +527 -0
- package/node_modules/@spexcode/spec-cli/dist/graphCache.d.ts +27 -0
- package/node_modules/@spexcode/spec-cli/dist/graphCache.js +717 -0
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.d.ts +14 -0
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +36 -0
- package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +85 -0
- package/node_modules/@spexcode/spec-cli/dist/graphStream.js +1195 -0
- package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.d.ts +64 -0
- package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.js +166 -0
- package/node_modules/@spexcode/spec-cli/dist/guide.d.ts +8 -0
- package/{spec-cli/src/guide.ts → node_modules/@spexcode/spec-cli/dist/guide.js} +36 -32
- package/node_modules/@spexcode/spec-cli/dist/harness-select.d.ts +18 -0
- package/node_modules/@spexcode/spec-cli/dist/harness-select.js +62 -0
- package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +338 -0
- package/node_modules/@spexcode/spec-cli/dist/harness.js +3146 -0
- package/node_modules/@spexcode/spec-cli/dist/headless-controller.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/headless-controller.js +47 -0
- package/node_modules/@spexcode/spec-cli/dist/help.d.ts +15 -0
- package/node_modules/@spexcode/spec-cli/dist/help.js +594 -0
- package/node_modules/@spexcode/spec-cli/dist/hook-prompts.d.ts +18 -0
- package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +122 -0
- package/node_modules/@spexcode/spec-cli/dist/hooks.d.ts +1 -0
- package/{spec-cli/src/hooks.ts → node_modules/@spexcode/spec-cli/dist/hooks.js} +15 -15
- package/node_modules/@spexcode/spec-cli/dist/host-resources.d.ts +97 -0
- package/node_modules/@spexcode/spec-cli/dist/host-resources.js +795 -0
- package/node_modules/@spexcode/spec-cli/dist/host.d.ts +88 -0
- package/node_modules/@spexcode/spec-cli/dist/host.js +663 -0
- package/node_modules/@spexcode/spec-cli/dist/index.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/index.js +899 -0
- package/node_modules/@spexcode/spec-cli/dist/init.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/init.js +284 -0
- package/node_modules/@spexcode/spec-cli/dist/issues-cli.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/issues-cli.js +334 -0
- package/node_modules/@spexcode/spec-cli/dist/issues.d.ts +69 -0
- package/node_modules/@spexcode/spec-cli/dist/issues.js +189 -0
- package/node_modules/@spexcode/spec-cli/dist/lint.d.ts +34 -0
- package/node_modules/@spexcode/spec-cli/dist/lint.js +476 -0
- package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +8 -0
- package/node_modules/@spexcode/spec-cli/dist/listen.js +24 -0
- package/node_modules/@spexcode/spec-cli/dist/localIssues.d.ts +65 -0
- package/node_modules/@spexcode/spec-cli/dist/localIssues.js +582 -0
- package/node_modules/@spexcode/spec-cli/dist/login-page.d.ts +5 -0
- package/{spec-cli/src/login-page.ts → node_modules/@spexcode/spec-cli/dist/login-page.js} +7 -7
- package/node_modules/@spexcode/spec-cli/dist/loop-in.d.ts +23 -0
- package/{spec-cli/src/loop-in.ts → node_modules/@spexcode/spec-cli/dist/loop-in.js} +35 -45
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +69 -0
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +603 -0
- package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +18 -0
- package/node_modules/@spexcode/spec-cli/dist/materialize.js +468 -0
- package/node_modules/@spexcode/spec-cli/dist/mentions.d.ts +50 -0
- package/node_modules/@spexcode/spec-cli/dist/mentions.js +117 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode-headless.d.ts +5 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode-headless.js +215 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode.d.ts +3 -0
- package/{spec-cli/src/opencode.ts → node_modules/@spexcode/spec-cli/dist/opencode.js} +9 -13
- package/node_modules/@spexcode/spec-cli/dist/pi-harness.d.ts +3 -0
- package/{spec-cli/src/pi-harness.ts → node_modules/@spexcode/spec-cli/dist/pi-harness.js} +45 -40
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +25 -0
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +196 -0
- package/node_modules/@spexcode/spec-cli/dist/plugin-harness.d.ts +21 -0
- package/node_modules/@spexcode/spec-cli/dist/plugin-harness.js +146 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-bridge.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +404 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-helper.d.mts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-helper.mjs +101 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.d.mts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.mjs +23 -0
- package/node_modules/@spexcode/spec-cli/dist/public-graph.d.ts +46 -0
- package/node_modules/@spexcode/spec-cli/dist/public-graph.js +56 -0
- package/node_modules/@spexcode/spec-cli/dist/ranker.d.ts +15 -0
- package/node_modules/@spexcode/spec-cli/dist/ranker.js +188 -0
- package/node_modules/@spexcode/spec-cli/dist/reaper.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/reaper.js +81 -0
- package/node_modules/@spexcode/spec-cli/dist/reviews.d.ts +189 -0
- package/node_modules/@spexcode/spec-cli/dist/reviews.js +350 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-guard.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-guard.js +37 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +21 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +84 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-rotate.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-rotate.js +58 -0
- package/node_modules/@spexcode/spec-cli/dist/search.bench.d.mts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/search.bench.mjs +75 -0
- package/node_modules/@spexcode/spec-cli/dist/search.d.ts +20 -0
- package/node_modules/@spexcode/spec-cli/dist/search.js +61 -0
- package/node_modules/@spexcode/spec-cli/dist/session-declarations.d.ts +13 -0
- package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +126 -0
- package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/session-execution.js +70 -0
- package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +28 -0
- package/node_modules/@spexcode/spec-cli/dist/session-files.js +130 -0
- package/node_modules/@spexcode/spec-cli/dist/session-follow.d.ts +31 -0
- package/node_modules/@spexcode/spec-cli/dist/session-follow.js +179 -0
- package/node_modules/@spexcode/spec-cli/dist/session-reparent.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/session-reparent.js +33 -0
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +11 -0
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +20 -0
- package/node_modules/@spexcode/spec-cli/dist/session-web.d.ts +24 -0
- package/node_modules/@spexcode/spec-cli/dist/session-web.js +141 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +433 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +4667 -0
- package/node_modules/@spexcode/spec-cli/dist/sh.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/sh.js +3 -0
- package/node_modules/@spexcode/spec-cli/dist/shim-runtime.d.ts +1 -0
- package/{spec-cli/src/shim-runtime.ts → node_modules/@spexcode/spec-cli/dist/shim-runtime.js} +3 -4
- package/node_modules/@spexcode/spec-cli/dist/slash-commands.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/slash-commands.js +307 -0
- package/node_modules/@spexcode/spec-cli/dist/source-files.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/source-files.js +113 -0
- package/node_modules/@spexcode/spec-cli/dist/supervise.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/supervise.js +308 -0
- package/node_modules/@spexcode/spec-cli/dist/tree.d.ts +25 -0
- package/node_modules/@spexcode/spec-cli/dist/tree.js +98 -0
- package/node_modules/@spexcode/spec-cli/dist/tsx-bin.d.ts +3 -0
- package/node_modules/@spexcode/spec-cli/dist/tsx-bin.js +41 -0
- package/node_modules/@spexcode/spec-cli/dist/uninstall.d.ts +3 -0
- package/node_modules/@spexcode/spec-cli/dist/uninstall.js +177 -0
- package/node_modules/@spexcode/spec-cli/dist/uploads.d.ts +36 -0
- package/node_modules/@spexcode/spec-cli/dist/uploads.js +243 -0
- package/node_modules/@spexcode/spec-cli/dist/worktree-sources.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/worktree-sources.js +47 -0
- package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/dispatch.sh +5 -5
- package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/harness.sh +1 -1
- package/node_modules/@spexcode/spec-cli/package.json +49 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
- package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec-first.sh +1 -1
- package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec-of-file.sh +1 -0
- package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/spec.md +2 -2
- package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +31 -29
- package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +94 -0
- package/node_modules/@spexcode/spec-core/dist/anchors.js +730 -0
- package/node_modules/@spexcode/spec-core/dist/git.d.ts +170 -0
- package/node_modules/@spexcode/spec-core/dist/git.js +2768 -0
- package/node_modules/@spexcode/spec-core/dist/graph-delta.d.ts +44 -0
- package/node_modules/@spexcode/spec-core/dist/graph-delta.js +72 -0
- package/node_modules/@spexcode/spec-core/dist/graph.d.ts +34 -0
- package/node_modules/@spexcode/spec-core/dist/graph.js +237 -0
- package/node_modules/@spexcode/spec-core/dist/graphDelta.d.ts +3 -0
- package/node_modules/@spexcode/spec-core/dist/graphDelta.js +13 -0
- package/node_modules/@spexcode/spec-core/dist/harness-identity.d.ts +31 -0
- package/node_modules/@spexcode/spec-core/dist/harness-identity.js +20 -0
- package/node_modules/@spexcode/spec-core/dist/identity-presets.d.ts +152 -0
- package/node_modules/@spexcode/spec-core/dist/identity-presets.js +132 -0
- package/node_modules/@spexcode/spec-core/dist/index.d.ts +45 -0
- package/node_modules/@spexcode/spec-core/dist/index.js +19 -0
- package/node_modules/@spexcode/spec-core/dist/layout.d.ts +187 -0
- package/node_modules/@spexcode/spec-core/dist/layout.js +548 -0
- package/node_modules/@spexcode/spec-core/dist/process-identity.d.ts +37 -0
- package/node_modules/@spexcode/spec-core/dist/process-identity.js +214 -0
- package/node_modules/@spexcode/spec-core/dist/project-identity.d.ts +12 -0
- package/node_modules/@spexcode/spec-core/dist/project-identity.js +71 -0
- package/node_modules/@spexcode/spec-core/dist/project-store.d.ts +3 -0
- package/node_modules/@spexcode/spec-core/dist/project-store.js +14 -0
- package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +2 -0
- package/node_modules/@spexcode/spec-core/dist/resilience.js +40 -0
- package/node_modules/@spexcode/spec-core/dist/review/index.d.ts +3 -0
- package/node_modules/@spexcode/spec-core/dist/review/index.js +5 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +77 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +308 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +66 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +180 -0
- package/node_modules/@spexcode/spec-core/dist/review/session.d.ts +4 -0
- package/node_modules/@spexcode/spec-core/dist/review/session.js +8 -0
- package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.d.ts +15 -0
- package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.js +12 -0
- package/node_modules/@spexcode/spec-core/dist/root-lru.d.ts +4 -0
- package/{spec-cli/src/root-lru.ts → node_modules/@spexcode/spec-core/dist/root-lru.js} +26 -30
- package/node_modules/@spexcode/spec-core/dist/specs.d.ts +117 -0
- package/node_modules/@spexcode/spec-core/dist/specs.js +489 -0
- package/node_modules/@spexcode/spec-core/package.json +28 -0
- package/node_modules/@spexcode/spec-eval/dist/cache.d.ts +10 -0
- package/node_modules/@spexcode/spec-eval/dist/cache.js +50 -0
- package/node_modules/@spexcode/spec-eval/dist/cli.d.ts +22 -0
- package/node_modules/@spexcode/spec-eval/dist/cli.js +981 -0
- package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +98 -0
- package/node_modules/@spexcode/spec-eval/dist/evaltab.js +176 -0
- package/node_modules/@spexcode/spec-eval/dist/filing.d.ts +16 -0
- package/node_modules/@spexcode/spec-eval/dist/filing.js +36 -0
- package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +48 -0
- package/node_modules/@spexcode/spec-eval/dist/freshness.js +799 -0
- package/node_modules/@spexcode/spec-eval/dist/host.d.ts +46 -0
- package/node_modules/@spexcode/spec-eval/dist/host.js +53 -0
- package/node_modules/@spexcode/spec-eval/dist/humanok.d.ts +11 -0
- package/node_modules/@spexcode/spec-eval/dist/humanok.js +27 -0
- package/node_modules/@spexcode/spec-eval/dist/index.d.ts +4 -0
- package/node_modules/@spexcode/spec-eval/dist/index.js +4 -0
- package/node_modules/@spexcode/spec-eval/dist/remarks.d.ts +31 -0
- package/node_modules/@spexcode/spec-eval/dist/remarks.js +1 -0
- package/node_modules/@spexcode/spec-eval/dist/scenariofresh.d.ts +13 -0
- package/node_modules/@spexcode/spec-eval/dist/scenariofresh.js +303 -0
- package/node_modules/@spexcode/spec-eval/dist/scenarios.d.ts +99 -0
- package/node_modules/@spexcode/spec-eval/dist/scenarios.js +650 -0
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +307 -0
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +1883 -0
- package/node_modules/@spexcode/spec-eval/dist/sidecar.d.ts +55 -0
- package/node_modules/@spexcode/spec-eval/dist/sidecar.js +82 -0
- package/node_modules/@spexcode/spec-eval/dist/timeline.d.ts +25 -0
- package/node_modules/@spexcode/spec-eval/dist/timeline.js +65 -0
- package/node_modules/@spexcode/spec-eval/dist/ui-path.d.ts +1 -0
- package/node_modules/@spexcode/spec-eval/dist/ui-path.js +2 -0
- package/node_modules/@spexcode/spec-eval/package.json +36 -0
- package/node_modules/@spexcode/spec-forge/dist/cache.d.ts +29 -0
- package/node_modules/@spexcode/spec-forge/dist/cache.js +59 -0
- package/node_modules/@spexcode/spec-forge/dist/cli.d.ts +1 -0
- package/node_modules/@spexcode/spec-forge/dist/cli.js +100 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/github.d.ts +2 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/github.js +116 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.d.ts +7 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.js +144 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers.d.ts +10 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers.js +83 -0
- package/node_modules/@spexcode/spec-forge/dist/index.d.ts +4 -0
- package/node_modules/@spexcode/spec-forge/dist/index.js +4 -0
- package/node_modules/@spexcode/spec-forge/dist/links.d.ts +15 -0
- package/node_modules/@spexcode/spec-forge/dist/links.js +65 -0
- package/node_modules/@spexcode/spec-forge/dist/needs-eval.d.ts +10 -0
- package/node_modules/@spexcode/spec-forge/dist/needs-eval.js +20 -0
- package/node_modules/@spexcode/spec-forge/dist/port.d.ts +53 -0
- package/node_modules/@spexcode/spec-forge/dist/port.js +1 -0
- package/node_modules/@spexcode/spec-forge/dist/resident.d.ts +7 -0
- package/node_modules/@spexcode/spec-forge/dist/resident.js +43 -0
- package/node_modules/@spexcode/spec-forge/package.json +32 -0
- package/package.json +33 -19
- package/spec-cli/bin/spex.mjs +0 -69
- package/spec-cli/src/anchors.ts +0 -728
- package/spec-cli/src/attach.ts +0 -43
- package/spec-cli/src/claude-headless.ts +0 -321
- package/spec-cli/src/cli.ts +0 -1258
- package/spec-cli/src/client.ts +0 -414
- package/spec-cli/src/cockpit.ts +0 -43
- package/spec-cli/src/codex-runtime-generations.ts +0 -646
- package/spec-cli/src/commit-surgery.ts +0 -68
- package/spec-cli/src/contract-filter.ts +0 -202
- package/spec-cli/src/delivery-queue.ts +0 -165
- package/spec-cli/src/doctor.ts +0 -516
- package/spec-cli/src/execution-trace.ts +0 -444
- package/spec-cli/src/file-write.ts +0 -22
- package/spec-cli/src/gateway-auth.ts +0 -181
- package/spec-cli/src/gateway-hub.ts +0 -358
- package/spec-cli/src/gateway.ts +0 -461
- package/spec-cli/src/git.ts +0 -2556
- package/spec-cli/src/graph.ts +0 -271
- package/spec-cli/src/graphCache.ts +0 -710
- package/spec-cli/src/graphDelta.ts +0 -77
- package/spec-cli/src/graphStream.ts +0 -1019
- package/spec-cli/src/harness-select.ts +0 -77
- package/spec-cli/src/harness.ts +0 -3025
- package/spec-cli/src/headless-controller.ts +0 -63
- package/spec-cli/src/help.ts +0 -492
- package/spec-cli/src/host-resources.ts +0 -762
- package/spec-cli/src/host.ts +0 -611
- package/spec-cli/src/identity-presets.d.ts +0 -13
- package/spec-cli/src/identity-presets.js +0 -138
- package/spec-cli/src/index.ts +0 -818
- package/spec-cli/src/init.ts +0 -258
- package/spec-cli/src/issues-cli.ts +0 -276
- package/spec-cli/src/issues.ts +0 -237
- package/spec-cli/src/layout.ts +0 -640
- package/spec-cli/src/lint.ts +0 -488
- package/spec-cli/src/listen.ts +0 -28
- package/spec-cli/src/localIssues.ts +0 -538
- package/spec-cli/src/materialize.ts +0 -417
- package/spec-cli/src/mentions.ts +0 -81
- package/spec-cli/src/opencode-headless.ts +0 -205
- package/spec-cli/src/pi-headless.ts +0 -153
- package/spec-cli/src/plugin-harness.ts +0 -157
- package/spec-cli/src/process-identity.ts +0 -207
- package/spec-cli/src/project-identity.ts +0 -73
- package/spec-cli/src/project-store.ts +0 -17
- package/spec-cli/src/pty-bridge.ts +0 -396
- package/spec-cli/src/pty-helper.mjs +0 -96
- package/spec-cli/src/pty-native-helper.mjs +0 -22
- package/spec-cli/src/ranker.ts +0 -191
- package/spec-cli/src/reaper.ts +0 -125
- package/spec-cli/src/resilience.ts +0 -41
- package/spec-cli/src/reviewSnapshot.ts +0 -28
- package/spec-cli/src/reviews.ts +0 -434
- package/spec-cli/src/runtime-guard.ts +0 -35
- package/spec-cli/src/runtime-ownership.ts +0 -88
- package/spec-cli/src/search.bench.mjs +0 -80
- package/spec-cli/src/search.ts +0 -63
- package/spec-cli/src/session-cursors.ts +0 -91
- package/spec-cli/src/session-execution.ts +0 -68
- package/spec-cli/src/session-files.ts +0 -121
- package/spec-cli/src/session-follow.ts +0 -182
- package/spec-cli/src/session-reparent.ts +0 -33
- package/spec-cli/src/session-timeline.ts +0 -247
- package/spec-cli/src/session-web.ts +0 -135
- package/spec-cli/src/sessions.ts +0 -3883
- package/spec-cli/src/sh.ts +0 -3
- package/spec-cli/src/slash-commands.ts +0 -318
- package/spec-cli/src/source-files.ts +0 -104
- package/spec-cli/src/specs.ts +0 -498
- package/spec-cli/src/supervise.ts +0 -245
- package/spec-cli/src/tree.ts +0 -118
- package/spec-cli/src/tsx-bin.ts +0 -18
- package/spec-cli/src/uninstall.ts +0 -171
- package/spec-cli/src/uploads.ts +0 -253
- package/spec-cli/src/worktree-sources.ts +0 -42
- package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +0 -8
- package/spec-dashboard/dist/assets/App-B6IbFbGu.js +0 -2
- package/spec-dashboard/dist/assets/Dashboard-C5ap-Sga.css +0 -1
- package/spec-dashboard/dist/assets/Dashboard-_sDk7Rcg.js +0 -27
- package/spec-dashboard/dist/assets/EvalsPage-BT357Lyt.js +0 -2
- package/spec-dashboard/dist/assets/IssuesPage-BG9rNOgV.js +0 -1
- 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-D9yPMR3A.js +0 -2
- package/spec-dashboard/dist/assets/Modal-B3EcgeA3.js +0 -1
- package/spec-dashboard/dist/assets/PageScroll-CNZOugWV.js +0 -1
- package/spec-dashboard/dist/assets/ProjectsPage-C2CRHmvs.js +0 -1
- package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +0 -32
- package/spec-dashboard/dist/assets/SessionInterface-DOEjuT3l.js +0 -40
- package/spec-dashboard/dist/assets/SessionWindow-1nYgZ4CL.js +0 -1
- package/spec-dashboard/dist/assets/Settings-7WeTJn0V.js +0 -1
- package/spec-dashboard/dist/assets/Thread-CEbzAFQn.js +0 -13
- package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +0 -1
- package/spec-dashboard/dist/assets/data-SlvxNA2r.js +0 -1
- package/spec-dashboard/dist/assets/index-DLVeOHL-.js +0 -41
- package/spec-dashboard/dist/assets/index-DYLGfPjx.css +0 -1
- package/spec-dashboard/dist/assets/launch-Cp579UoJ.css +0 -1
- package/spec-dashboard/dist/assets/launch-yxCYr64x.js +0 -276
- package/spec-dashboard/dist/assets/sessionSurface--7l69h4F.js +0 -1
- package/spec-dashboard/dist/index.html +0 -29
- package/spec-dashboard/src/reviewFilters.js +0 -322
- package/spec-dashboard/src/reviewQuery.js +0 -174
- package/spec-dashboard/src/session.js +0 -199
- package/spec-eval/src/cache.ts +0 -57
- package/spec-eval/src/cli.ts +0 -894
- package/spec-eval/src/evaltab.ts +0 -264
- package/spec-eval/src/filing.ts +0 -39
- package/spec-eval/src/freshness.ts +0 -836
- package/spec-eval/src/humanok.ts +0 -28
- package/spec-eval/src/scenariofresh.ts +0 -244
- package/spec-eval/src/scenarios.ts +0 -672
- package/spec-eval/src/sessioneval.ts +0 -2220
- package/spec-eval/src/sidecar.ts +0 -93
- package/spec-eval/src/timeline.ts +0 -54
- package/spec-forge/src/__fixtures__/github-forge.json +0 -63
- package/spec-forge/src/cache.ts +0 -66
- package/spec-forge/src/cli.ts +0 -99
- package/spec-forge/src/drivers/github.ts +0 -132
- package/spec-forge/src/drivers/gitlab.ts +0 -153
- package/spec-forge/src/drivers.ts +0 -73
- package/spec-forge/src/links.ts +0 -72
- package/spec-forge/src/needs-eval.ts +0 -29
- package/spec-forge/src/port.ts +0 -36
- package/spec-forge/src/resident.ts +0 -46
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/README.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-sed-v0.fixture +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/commit-msg +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-checkout +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-merge +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/pre-commit +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/prepare-commit-msg +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/reference-transaction +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/extract/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/regroup/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/rename/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/tidy/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/comment-altitude/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/idle.sh +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/mark-active.sh +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/fail.sh +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/forge-link/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/digest.mjs +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-core}/templates/spexcode.json +0 -0
package/spec-cli/src/index.ts
DELETED
|
@@ -1,818 +0,0 @@
|
|
|
1
|
-
import { serve } from '@hono/node-server'
|
|
2
|
-
import type { Server as HttpServer, ServerResponse as HttpServerResponse } from 'node:http'
|
|
3
|
-
import { randomUUID } from 'node:crypto'
|
|
4
|
-
import { createReadStream } from 'node:fs'
|
|
5
|
-
import { Readable } from 'node:stream'
|
|
6
|
-
import { installConnectionReaper } from './reaper.js'
|
|
7
|
-
import { Hono } from 'hono'
|
|
8
|
-
import { cors } from 'hono/cors'
|
|
9
|
-
import { etag } from 'hono/etag'
|
|
10
|
-
import { createNodeWebSocket } from '@hono/node-ws'
|
|
11
|
-
import { loadSpecs, loadSpecsLite, specContent, specHistory, specDiffAt, loadConfig, loadReviewConfig } from './specs.js'
|
|
12
|
-
import { issuesEnabled, resolveRemark, retractRemark } from './localIssues.js'
|
|
13
|
-
import { closeIssue, createIssue, findIssue, issueStores, mergedIssues, promote } from './issues.js'
|
|
14
|
-
import { remarkWithLoopIn, replyIssueWithLoopIn } from './loop-in.js'
|
|
15
|
-
import { residentForgeState, refreshForgeNow } from '../../spec-forge/src/resident.js'
|
|
16
|
-
import { resolveForgeHost } from '../../spec-forge/src/drivers.js'
|
|
17
|
-
import { summarizeLoopIn } from './mentions.js'
|
|
18
|
-
import { resolveLayout, mainBranch } from './layout.js'
|
|
19
|
-
import { getBoardJson } from './graphCache.js'
|
|
20
|
-
import { boardStream, closeBoardFileWatchers, ensureBoardFileWatchers, notifyBoardChanged, flushDeferredWorktreeRegistryChange } from './graphStream.js'
|
|
21
|
-
import { gitA, gitTry, repoRoot } from './git.js'
|
|
22
|
-
import { cockpitReview } from './cockpit.js'
|
|
23
|
-
import { listSessions, sendText, interruptSession, rawKey, stopSession, closeSession, quarantineCorruptRecord, restoreQuarantinedRecord, archiveSession, resumeSession, mergeSession, captureSessionResult, sessionPrompt, renameSession, setSessionSort, sessionCreateRequest, superviseQueue, superviseTurnFailures, superviseDelivery, SessionRecordUnusable, TMUX_SOCK } from './sessions.js'
|
|
24
|
-
import { readTimeline } from './session-timeline.js'
|
|
25
|
-
import { readSessionExecution, sessionExecutionStream } from './session-execution.js'
|
|
26
|
-
import { defaultHarness, HARNESSES, dashboardLauncherList, launcherDefault } from './harness.js'
|
|
27
|
-
import { readBlobByHash } from '../../spec-eval/src/evaltab.js'
|
|
28
|
-
import { putBlob } from '../../spec-eval/src/cache.js'
|
|
29
|
-
import { fileHumanReading } from '../../spec-eval/src/filing.js'
|
|
30
|
-
import { fileHumanOk } from '../../spec-eval/src/humanok.js'
|
|
31
|
-
import { buildExportModel, renderExportHtml, buildSessionEvals, SessionEvalUnavailableError } from '../../spec-eval/src/sessioneval.js'
|
|
32
|
-
import { appendUpload, cancelUpload, completeUpload, createUpload, evidenceMaxBytes, startUploadReaper, UploadError, uploadStatus } from './uploads.js'
|
|
33
|
-
import { listSessionFiles, openSessionFile, SESSION_FILE_PREVIEW_MAX_BYTES, sessionFilePreviewKind, SessionFileError } from './session-files.js'
|
|
34
|
-
import { attachViewer, detachViewer, resizeBridge, hideViewer, forwardInput, superviseBridges, type Viewer } from './pty-bridge.js'
|
|
35
|
-
import { installProcessGuards } from './resilience.js'
|
|
36
|
-
import { resolveProjectIdentity } from './project-identity.js'
|
|
37
|
-
import { evalDetailReview, evalsReview, issuesReview } from './reviews.js'
|
|
38
|
-
import { collectResourceReport, ResourceConflict } from './host-resources.js'
|
|
39
|
-
import { reparentRequest, SessionReparentRequestError } from './session-reparent.js'
|
|
40
|
-
|
|
41
|
-
// last-resort net: an unforeseen async throw (e.g. a worktree vanishing mid-read during a worker
|
|
42
|
-
// self-merge) is logged and the server KEEPS SERVING instead of exiting and dropping the public port.
|
|
43
|
-
installProcessGuards()
|
|
44
|
-
|
|
45
|
-
const app = new Hono()
|
|
46
|
-
startUploadReaper()
|
|
47
|
-
app.use('/api/*', cors())
|
|
48
|
-
app.onError((error, c) => {
|
|
49
|
-
if (error instanceof SessionEvalUnavailableError) return c.json({ error: error.message }, 503)
|
|
50
|
-
// a record that cannot carry state is a CONFLICT with the caller's request, not a server fault: the refusal
|
|
51
|
-
// is deliberate and already carries its own diagnosis + repair ([[sessions-core]]). Answering 500 with a
|
|
52
|
-
// stack would hide exactly the sentence the human needs.
|
|
53
|
-
if (error instanceof SessionRecordUnusable) return c.json({ error: error.message, code: error.code }, 409)
|
|
54
|
-
if (error instanceof ResourceConflict) return c.json({ error: error.message, code: error.code }, 409)
|
|
55
|
-
if (error instanceof SessionReparentRequestError) return c.json({ error: error.message }, 400)
|
|
56
|
-
console.error(error)
|
|
57
|
-
return c.text('Internal Server Error', 500)
|
|
58
|
-
})
|
|
59
|
-
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app })
|
|
60
|
-
|
|
61
|
-
app.get('/', (c) => c.text('spec-cli — GET /api/graph · /api/specs · /api/specs/:id/history · /api/settings · /api/sessions · /api/resources · /api/slash-commands'))
|
|
62
|
-
// the supervisor's readiness gate (supervise.ts): a bare git-free 200 so a booting child reports ready the
|
|
63
|
-
// instant Hono is listening. Not under /api/* — loopback-only (supervisor→child), no CORS needed.
|
|
64
|
-
app.get('/health', (c) => c.text('ok'))
|
|
65
|
-
// @@@ instance identity - who THIS backend is: the serve generation's instanceId (minted by the supervisor,
|
|
66
|
-
// constant across zero-downtime reloads, handed down via env) and the project root it serves. This is the
|
|
67
|
-
// answer the host gateway ([[host-gateway]]) compares an endpoint record against before proxying to it — a
|
|
68
|
-
// recycled port serving another project or a stale record fails the match instead of being routed to. Git-free
|
|
69
|
-
// after the first memoized resolution; a self-run child (no supervisor) answers instanceId:null, which no
|
|
70
|
-
// record claims, so it is simply not hosted.
|
|
71
|
-
const instanceStartedAt = new Date().toISOString()
|
|
72
|
-
app.get('/api/instance', (c) => {
|
|
73
|
-
const root = repoRoot()
|
|
74
|
-
return c.json({
|
|
75
|
-
instanceId: process.env.SPEXCODE_INSTANCE_ID ?? null,
|
|
76
|
-
root,
|
|
77
|
-
identity: resolveProjectIdentity(root, root),
|
|
78
|
-
pid: process.pid,
|
|
79
|
-
startedAt: instanceStartedAt,
|
|
80
|
-
})
|
|
81
|
-
})
|
|
82
|
-
// the assembled graph (merged tree + overlay + sessions) — the dashboard's single source. Same data
|
|
83
|
-
// as `spex graph --json`; the frontend only adds x/y pixels on top. Freshness is PUSH-first ([[graph-stream]]): the
|
|
84
|
-
// dashboard reloads on a `/api/graph/stream` event, not a tight poll, so the route is a conditional-request
|
|
85
|
-
// endpoint: `etag()` hashes the serialized body, and a reload whose `If-None-Match` matches gets a bodyless 304
|
|
86
|
-
// instead of the full transfer (~1 MB on the dogfood board — it scales with the node count). The 304 saves the
|
|
87
|
-
// WIRE only; the COMPUTE is saved by [[graph-cache]]: getBoard() is single-flight + cached, so a poll storm
|
|
88
|
-
// shares ONE build instead of each running its own — the poll-frequency cut (push channel) and the
|
|
89
|
-
// build-coalescing cut compound. A hard timeout bounds a wedged build to a loud 503 rather than an
|
|
90
|
-
// unboundedly-held connection (the wall sits well above the legitimately-several-seconds cold first build);
|
|
91
|
-
// a merely-slow single-flight build keeps running and caches for the next poll, while a NEVER-settling one
|
|
92
|
-
// is bounded by [[graph-cache]]'s own build watchdog, so the next poll retries a fresh build.
|
|
93
|
-
const BOARD_TIMEOUT_MS = Number(process.env.SPEXCODE_BOARD_TIMEOUT_MS || 20000)
|
|
94
|
-
app.get('/api/graph', etag(), async (c) => {
|
|
95
|
-
await ensureBoardFileWatchers()
|
|
96
|
-
const timeout = Symbol('timeout')
|
|
97
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
98
|
-
const result = await Promise.race([
|
|
99
|
-
getBoardJson('stale-ok'),
|
|
100
|
-
new Promise<typeof timeout>((resolve) => {
|
|
101
|
-
timer = setTimeout(() => resolve(timeout), BOARD_TIMEOUT_MS)
|
|
102
|
-
timer.unref?.()
|
|
103
|
-
}),
|
|
104
|
-
])
|
|
105
|
-
clearTimeout(timer)
|
|
106
|
-
if (result === timeout) return c.json({ error: 'graph build timed out' }, 503)
|
|
107
|
-
const freshness = result.refreshing ? `${result.freshness}, refreshing` : result.freshness
|
|
108
|
-
c.header('x-spexcode-graph', freshness)
|
|
109
|
-
return c.body(result.json, 200, { 'content-type': 'application/json; charset=UTF-8' })
|
|
110
|
-
})
|
|
111
|
-
// the graph's push channel: an SSE that fires `board-changed` on any session-store write, so the dashboard
|
|
112
|
-
// reloads the instant status moves instead of waiting for its slow fallback poll ([[graph-stream]]).
|
|
113
|
-
app.get('/api/graph/stream', (c) => boardStream(c))
|
|
114
|
-
app.get('/api/specs', async (c) => c.json(await loadSpecs()))
|
|
115
|
-
// the search corpus ([[graph-lean]]): a filesystem-only {id,title,path,desc,body} for every node, NO git. The
|
|
116
|
-
// board omits `body` to stay lean, so the search palette fetches this ONCE when it opens (cached client-side)
|
|
117
|
-
// to rank nodes over their prose — off the board's hot poll. Review rows, including scenarios, come only
|
|
118
|
-
// from their paged endpoints and cannot be reconstructed from this corpus.
|
|
119
|
-
app.get('/api/specs/lite', (c) => c.json(loadSpecsLite()))
|
|
120
|
-
// one node's body + parsed parts ([[graph-lean]]): the board no longer ships either, so the detail view
|
|
121
|
-
// fetches this when a node opens. 404 for an unknown id.
|
|
122
|
-
app.get('/api/specs/:id/content', (c) => {
|
|
123
|
-
const x = specContent(c.req.param('id'))
|
|
124
|
-
return x ? c.json(x) : c.json({ body: '', parts: null }, 404)
|
|
125
|
-
})
|
|
126
|
-
app.get('/api/specs/:id/history', async (c) => c.json(await specHistory(c.req.param('id'))))
|
|
127
|
-
// the spec.md line diff one version introduced — the history tab's per-version proof-of-change, fetched
|
|
128
|
-
// lazily when an older version's item expands (the latest version's diff ships with the board as node.lastDiff).
|
|
129
|
-
app.get('/api/specs/:id/diff/:hash', async (c) => c.json(await specDiffAt(c.req.param('id'), c.req.param('hash'))))
|
|
130
|
-
// a unified diff of a node's spec.md from its fork point (the worktree's merge-base with main) to that
|
|
131
|
-
// worktree's working tree. An untracked brand-new node is invisible to `git diff <base>`, so when the base
|
|
132
|
-
// diff is empty AND status is `??` synthesize an all-additions view via `diff --no-index` (gitTry — --no-index
|
|
133
|
-
// exits 1, which gitA would swallow). Gated on `??` so a tracked file with no pending change stays empty.
|
|
134
|
-
app.get('/api/edit', async (c) => {
|
|
135
|
-
const source = c.req.query('source') || '', path = c.req.query('path') || ''
|
|
136
|
-
if (!source || !path) return c.json({ patch: '' })
|
|
137
|
-
const mb = mainBranch()
|
|
138
|
-
const base = (await gitA(['-C', source, 'merge-base', mb, 'HEAD'])).trim() || mb
|
|
139
|
-
let patch = await gitA(['-C', source, 'diff', base, '--', path])
|
|
140
|
-
if (!patch) {
|
|
141
|
-
const status = await gitA(['-C', source, 'status', '--porcelain', '--untracked-files=all', '--', path])
|
|
142
|
-
if (status.startsWith('??')) patch = (await gitTry(['-C', source, 'diff', '--no-index', '--', '/dev/null', path])).stdout
|
|
143
|
-
}
|
|
144
|
-
return c.json({ patch })
|
|
145
|
-
})
|
|
146
|
-
// the eval seam's WRITE half over HTTP ([[spec-eval]] filing.ts): a
|
|
147
|
-
// programmatic caller files a reading (verdict + optional transcript) through the SAME append the CLI
|
|
148
|
-
// uses. The dashboard does not call this — [[event-detail]] reads readings and hosts remarks, never files.
|
|
149
|
-
app.post('/api/specs/:id/evals', async (c) => {
|
|
150
|
-
const b = await c.req.json().catch(() => null)
|
|
151
|
-
if (!b || typeof b.scenario !== 'string') return c.json({ error: 'body needs { scenario, status, note?, transcript? }' }, 400)
|
|
152
|
-
const r = fileHumanReading(c.req.param('id'), b)
|
|
153
|
-
return r.ok ? c.json({ ok: true, reading: r.reading }) : c.json({ error: r.error }, 400)
|
|
154
|
-
})
|
|
155
|
-
// the HUMAN SIGN-OFF write ([[human-ok]]) — the dashboard's ok affordance and `spex eval ok` share this ONE
|
|
156
|
-
// write (LAW L: no dashboard-only path). Identity is SERVER-DERIVED 'human', never the request body (the
|
|
157
|
-
// same rule as /api/remarks). The write appends a monotonic human-ok event bound to the scenario's latest
|
|
158
|
-
// reading and — on the trunk checkout — commits it straight to trunk; the board cache is invalidated
|
|
159
|
-
// atomically with persistence so the writer's own refetch never races a stale cache.
|
|
160
|
-
app.post('/api/specs/:id/evals/ok', async (c) => {
|
|
161
|
-
const b = await c.req.json().catch(() => null)
|
|
162
|
-
if (!b || typeof b.scenario !== 'string') return c.json({ error: 'body needs { scenario }' }, 400)
|
|
163
|
-
const r = fileHumanOk(c.req.param('id'), b.scenario, 'human')
|
|
164
|
-
if (!r.ok) return c.json({ error: r.error }, 400)
|
|
165
|
-
notifyBoardChanged('full')
|
|
166
|
-
return c.json({ ok: true, already: r.already, humanOk: r.humanOk })
|
|
167
|
-
})
|
|
168
|
-
// serve a reading's evidence blob by content hash (bytes never enter git): bad hash → 400, missing → 404,
|
|
169
|
-
// else the bytes with a sniffed MIME and an immutable cache header (the name IS the content hash).
|
|
170
|
-
// HTTP Range is honored — a <video> can only SEEK when the server answers byte ranges (a browser clamps
|
|
171
|
-
// currentTime to the seekable window, which stays [0,0] without them); one general mechanism at the
|
|
172
|
-
// transport, so every evidence kind streams the same way. A trailing `.<ext>` on the hash is IGNORED
|
|
173
|
-
// decoration for third-party markdown renderers (GitLab/GitHub only emit a <video> player when the URL
|
|
174
|
-
// ends in a video extension); the served bytes and MIME stay the stored ones — a wrong suffix never lies.
|
|
175
|
-
app.get('/api/evidence/:hash', (c) => {
|
|
176
|
-
const r = readBlobByHash(c.req.param('hash').replace(/\.[a-z0-9]+$/i, ''))
|
|
177
|
-
if (!r.ok) return c.text(r.message, r.reason === 'invalid' ? 400 : 404)
|
|
178
|
-
const total = r.bytes.length
|
|
179
|
-
const base = { 'Content-Type': r.mime, 'Cache-Control': 'public, max-age=31536000, immutable', 'Accept-Ranges': 'bytes' }
|
|
180
|
-
const m = /^bytes=(\d*)-(\d*)$/.exec(c.req.header('range') ?? '')
|
|
181
|
-
if (m && (m[1] || m[2])) {
|
|
182
|
-
const start = m[1] ? parseInt(m[1], 10) : total - parseInt(m[2], 10)
|
|
183
|
-
const end = m[1] && m[2] ? Math.min(parseInt(m[2], 10), total - 1) : total - 1
|
|
184
|
-
if (!(start >= 0 && start <= end && end < total)) return c.body(null, 416, { 'Content-Range': `bytes */${total}` })
|
|
185
|
-
return c.body(new Uint8Array(r.bytes.subarray(start, end + 1)), 206, { ...base, 'Content-Range': `bytes ${start}-${end}/${total}` })
|
|
186
|
-
}
|
|
187
|
-
return c.body(new Uint8Array(r.bytes), 200, base)
|
|
188
|
-
})
|
|
189
|
-
// the WRITE half of the blob store ([[annotator]]): the annotator captures a circled video frame to a PNG
|
|
190
|
-
// and stashes the bytes here, content-addressed (same putBlob the eval cache uses). The returned hash is
|
|
191
|
-
// what an anchored comment references (image link in the body, and the typed evidence[] on its thread) —
|
|
192
|
-
// bytes never enter git. Raw body, sniffed by the same content-addressed name. Empty → 400, over cap → 413.
|
|
193
|
-
app.post('/api/evidence', async (c) => {
|
|
194
|
-
const buf = Buffer.from(await c.req.arrayBuffer())
|
|
195
|
-
if (buf.length === 0) return c.json({ error: 'empty evidence' }, 400)
|
|
196
|
-
if (buf.length > evidenceMaxBytes()) return c.json({ error: 'evidence too large' }, 413)
|
|
197
|
-
return c.json({ hash: putBlob(buf) }, 201)
|
|
198
|
-
})
|
|
199
|
-
// the SETTINGS read surface — one route for everything spexcode.json / spexcode.local.json resolves to:
|
|
200
|
-
// `layout` (resolveLayout()'s main/worktrees/branch shape — the write-guard's project-identity probe reads
|
|
201
|
-
// `.layout.main`) and the dashboard-visible launcher profiles ([[launcher-visibility]]) the New-Session picker
|
|
202
|
-
// offers — `{ name, harness, cmd, headless }`: the cmd is read-only display data for the picker (the dashboard sits
|
|
203
|
-
// behind the gateway auth; the browser can read but never edit config) — plus the configured `default` NAME
|
|
204
|
-
// so the picker pre-selects the SAME launcher a bare `spex session new` uses when that row is visible, else
|
|
205
|
-
// its first visible row rather than a hidden headless default,
|
|
206
|
-
// Missing defaultLauncher is returned as an actionable config error, not hidden by falling through to the
|
|
207
|
-
// built-in `claude` launcher.
|
|
208
|
-
// `tmuxSocket` is the `-L <name>` label our private tmux server runs under (a backend fact, env-overridable),
|
|
209
|
-
// so the row's attach modal ([[attach-menu]]) can offer the RAW `tmux -L <socket> attach -t <id>` fallback
|
|
210
|
-
// beside the blessed `spex session attach` command — the frontend never hardcodes the socket.
|
|
211
|
-
app.get('/api/settings', async (c) => c.json({
|
|
212
|
-
layout: await resolveLayout(),
|
|
213
|
-
launchers: dashboardLauncherList(),
|
|
214
|
-
tmuxSocket: TMUX_SOCK,
|
|
215
|
-
...launcherDefault(),
|
|
216
|
-
}))
|
|
217
|
-
// the `surface: command` plugin-root nodes (built/active only) for new-session and live-inbox `/` dropdowns — each with
|
|
218
|
-
// its prompt `body` ({{targets}} placeholder), `kind`, and folder `dir` + co-located `files`. surface is a
|
|
219
|
-
// frontmatter field, not a dir (specs.ts loadSurface); `surface: system` siblings are gathered elsewhere.
|
|
220
|
-
// `?surface=review` lists the review-track presets instead ([[review-commands]] — the eval detail's
|
|
221
|
-
// remark-composer `/` dropdown); the exposed surfaces stay this explicit whitelist, never a passthrough.
|
|
222
|
-
app.get('/api/plugins', (c) => c.json(c.req.query('surface') === 'review' ? loadReviewConfig() : loadConfig()))
|
|
223
|
-
// the ISSUES read surface ([[issues]]) for the dashboard's issues page — the merged list over every store
|
|
224
|
-
// (local threads + the resident forge slice), the SAME mergedIssues() the CLI drain reads, verbatim
|
|
225
|
-
// (the dashboard computes nothing over it: no re-sort, no salience ranking). The `enabled` flag mirrors
|
|
226
|
-
// the issues-workflow on/off switch so the frontend hides the view when the feature is OFF.
|
|
227
|
-
app.get('/api/issues', etag(), async (c) => c.json(await issuesReview(c.req.query('q'), c.req.query('page'))))
|
|
228
|
-
// Evals uses the identical paged-review response. `scope:` inside q selects the worktree source; without
|
|
229
|
-
// it the source is the current cached board. Filtering/counts always precede the one 25-row slice.
|
|
230
|
-
app.get('/api/evals', etag(), async (c) => {
|
|
231
|
-
const scope = c.req.query('q')?.match(/(?:^|\s)scope:([^\s]+)/)?.[1]
|
|
232
|
-
await ensureBoardFileWatchers(scope)
|
|
233
|
-
const page = await evalsReview(c.req.query('q'), c.req.query('page'), { view: c.req.query('view') })
|
|
234
|
-
return page ? c.json(page) : c.json({ error: 'no such review source' }, 404)
|
|
235
|
-
})
|
|
236
|
-
// The exact impact graph proves a scope's membership and selector decisions. It is deliberately a named
|
|
237
|
-
// read: paged Evals rows retain their own reasons but never transport this scope-sized projection.
|
|
238
|
-
app.get('/api/evals/impact', etag(), async (c) => {
|
|
239
|
-
const scope = c.req.query('scope')?.trim()
|
|
240
|
-
if (!scope) return c.json({ error: 'scope is required' }, 400)
|
|
241
|
-
await ensureBoardFileWatchers(scope)
|
|
242
|
-
const model = await buildSessionEvals(scope)
|
|
243
|
-
return model
|
|
244
|
-
? c.json({ scope, impact: model.impact, evalRevision: model.evalRevision })
|
|
245
|
-
: c.json({ error: 'no such review source' }, 404)
|
|
246
|
-
})
|
|
247
|
-
// ONE bounded detail response for both source roots: the selected scenario's complete A/B history and at
|
|
248
|
-
// most five lightweight neighbors. A missing worktree scope resolves explicitly to trunk; it never
|
|
249
|
-
// serializes another scenario's history or the scoped model.
|
|
250
|
-
app.get('/api/evals/detail', etag(), async (c) => {
|
|
251
|
-
await ensureBoardFileWatchers(c.req.query('scope')?.trim() || undefined)
|
|
252
|
-
const node = c.req.query('node')?.trim()
|
|
253
|
-
const scenario = c.req.query('scenario')?.trim()
|
|
254
|
-
if (!node || !scenario) return c.json({ error: 'node and scenario are required' }, 400)
|
|
255
|
-
const detail = await evalDetailReview(node, scenario, c.req.query('scope')?.trim() || null)
|
|
256
|
-
return c.json(detail)
|
|
257
|
-
})
|
|
258
|
-
// the single-thread read ([[issues]]) behind `spex issue show <id>` — the SAME findIssue lookup, from the
|
|
259
|
-
// resident forge slice (instant view, background reconcile — the list route's freshness contract). A local
|
|
260
|
-
// id, or a forge id (`<host>#<n>`); unknown → 404 (eval-remark threads are not issues, so they 404 here too).
|
|
261
|
-
app.get('/api/issues/:id', (c) => {
|
|
262
|
-
const t = findIssue(c.req.param('id'), { host: resolveForgeHost(), state: residentForgeState() }, loadSpecsLite().map((s) => s.id))
|
|
263
|
-
return t ? c.json(t) : c.json({ error: `no issue '${c.req.param('id')}'` }, 404)
|
|
264
|
-
})
|
|
265
|
-
// the WRITE surface ([[local-issues]] / [[issues-view]]) — the human reply path, STORE-ROUTED through the one
|
|
266
|
-
// reply verb ([[issues]] replyIssue): a local id git-commits to the trunk store, a forge id ('github#N')
|
|
267
|
-
// posts a REAL comment through the driver. @session remains prose in either store; it never sends or spawns.
|
|
268
|
-
// The server owns its freshness: a forge write forces the resident slice's read-back before answering, so the
|
|
269
|
-
// reload that follows shows the comment. Honor the on/off switch: 403 when the feature is OFF; an unknown
|
|
270
|
-
// local thread → 404; a failed forge write → 502 with the driver's own message (fail loud, never queued).
|
|
271
|
-
app.post('/api/issues/:id/reply', async (c) => {
|
|
272
|
-
if (!issuesEnabled()) return c.json({ error: 'issues workflow is off' }, 403)
|
|
273
|
-
const body = await c.req.json().catch(() => ({}))
|
|
274
|
-
const text = typeof body?.body === 'string' ? body.body : ''
|
|
275
|
-
if (!text.trim()) return c.json({ error: 'empty reply' }, 400)
|
|
276
|
-
// typed evidence[] — an anchored annotation's frame-blob hashes accrue onto the local thread (same shape
|
|
277
|
-
// as the create route); a forge reply ignores them (its frame rides the comment body's image link).
|
|
278
|
-
const evidence = Array.isArray(body?.evidence) ? (body.evidence as unknown[]).filter((h): h is string => typeof h === 'string' && /^[0-9a-f]{64}$/.test(h)) : []
|
|
279
|
-
const id = c.req.param('id')
|
|
280
|
-
try {
|
|
281
|
-
// the mention prompt's node context, from the same resident merge the GET serves
|
|
282
|
-
const node = id.includes('#')
|
|
283
|
-
? mergedIssues({ host: resolveForgeHost(), state: residentForgeState() }, loadSpecsLite().map((s) => s.id)).find((i) => i.id === id)?.nodes[0] ?? null
|
|
284
|
-
: null
|
|
285
|
-
const r = await replyIssueWithLoopIn(id, text, { author: 'human', node, evidence })
|
|
286
|
-
if (r.store !== 'local') await refreshForgeNow()
|
|
287
|
-
notifyBoardChanged('full') // atomic with persistence — see the /api/remarks block below
|
|
288
|
-
return c.json({ ok: true, replies: r.replies, url: r.url, outcomes: summarizeLoopIn(r.loopIn) })
|
|
289
|
-
} catch (e) {
|
|
290
|
-
const msg = String((e as Error).message || e)
|
|
291
|
-
return c.json({ error: msg }, id.includes('#') ? 502 : 404)
|
|
292
|
-
}
|
|
293
|
-
})
|
|
294
|
-
// store-routed lifecycle close ([[issues]]): local resolves the local thread, forge closes the remote
|
|
295
|
-
// issue through the driver. A forge close forces read-back before the dashboard reloads the resident list.
|
|
296
|
-
app.post('/api/issues/:id/close', async (c) => {
|
|
297
|
-
if (!issuesEnabled()) return c.json({ error: 'issues workflow is off' }, 403)
|
|
298
|
-
const id = c.req.param('id')
|
|
299
|
-
try {
|
|
300
|
-
const r = await closeIssue(id)
|
|
301
|
-
if (r.store !== 'local') await refreshForgeNow()
|
|
302
|
-
notifyBoardChanged('full') // atomic with persistence — see the /api/remarks block below
|
|
303
|
-
return c.json({ ok: true, ...r })
|
|
304
|
-
} catch (e) {
|
|
305
|
-
const msg = String((e as Error).message || e)
|
|
306
|
-
return c.json({ error: msg }, id.includes('#') ? 502 : 404)
|
|
307
|
-
}
|
|
308
|
-
})
|
|
309
|
-
app.post('/api/issues', async (c) => {
|
|
310
|
-
if (!issuesEnabled()) return c.json({ error: 'issues workflow is off' }, 403)
|
|
311
|
-
const body = await c.req.json().catch(() => ({}))
|
|
312
|
-
const concern = typeof body?.concern === 'string' ? body.concern.trim() : ''
|
|
313
|
-
if (!concern) return c.json({ error: 'empty concern' }, 400)
|
|
314
|
-
const nodes = Array.isArray(body?.nodes) ? (body.nodes as unknown[]).filter((n): n is string => typeof n === 'string') : []
|
|
315
|
-
const postBody = typeof body?.body === 'string' ? body.body : undefined
|
|
316
|
-
const store = typeof body?.store === 'string' && body.store.trim() ? body.store.trim() : 'local'
|
|
317
|
-
// typed evidence[] — content-addressed evidence hashes (the annotator's clip reference rides here, not prose)
|
|
318
|
-
const evidence = Array.isArray(body?.evidence) ? (body.evidence as unknown[]).filter((h): h is string => typeof h === 'string' && /^[0-9a-f]{64}$/.test(h)) : []
|
|
319
|
-
try {
|
|
320
|
-
const r = await createIssue(concern, { store, nodes, body: postBody, evidence, author: 'human' })
|
|
321
|
-
if (r.store !== 'local') await refreshForgeNow()
|
|
322
|
-
notifyBoardChanged('full') // atomic with persistence — see the /api/remarks block below
|
|
323
|
-
return c.json({ ok: true, id: r.id, store: r.store, url: r.url }, 201)
|
|
324
|
-
} catch (e) {
|
|
325
|
-
return c.json({ error: String((e as Error).message || e) }, store === 'local' ? 500 : 502)
|
|
326
|
-
}
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
// promotion moves an open local thread to the forge as one recorded action ([[issues]]'s promote verb,
|
|
330
|
-
// verbatim: forge issue first, then the permalink reply + local close. The forced forge read-back means
|
|
331
|
-
// the reload that follows shows the promoted issue in the merged list. Fail loud: an unreachable forge is a
|
|
332
|
-
// 502 with the local thread untouched.
|
|
333
|
-
app.post('/api/issues/:id/promote', async (c) => {
|
|
334
|
-
if (!issuesEnabled()) return c.json({ error: 'issues workflow is off' }, 403)
|
|
335
|
-
const id = c.req.param('id')
|
|
336
|
-
if (id.includes('#')) return c.json({ error: 'only a local issue promotes' }, 400)
|
|
337
|
-
try {
|
|
338
|
-
const r = await promote(id, { author: 'human' })
|
|
339
|
-
await refreshForgeNow()
|
|
340
|
-
notifyBoardChanged('full') // atomic with persistence — see the /api/remarks block below
|
|
341
|
-
return c.json({ ok: true, ...r })
|
|
342
|
-
} catch (e) {
|
|
343
|
-
const msg = String((e as Error).message || e)
|
|
344
|
-
return c.json({ error: msg }, /^no local issue/.test(msg) ? 404 : 502)
|
|
345
|
-
}
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
// the REMARK write surface ([[remark-substrate]]) — server PARITY with the CLI: the dashboard can author /
|
|
349
|
-
// resolve / retract a remark through the SAME functions `spex remark|resolve|retract` call, adding no
|
|
350
|
-
// capability. A ref (`<thread-id>#<rid>`) rides the request BODY, not the path (a '#' in a URL is a
|
|
351
|
-
// fragment). Identity is derived SERVER-SIDE — this is the dashboard's human surface, so the actor is
|
|
352
|
-
// `'human'`, the SAME sentinel /api/issues stamps; it is NEVER read from the request body. That keeps R3's
|
|
353
|
-
// teeth structural (identity is not spoofable over the wire) and identical on both surfaces: resolve is any
|
|
354
|
-
// SECOND party's deliberate judgment — the human resolves an agent's remark here exactly as an agent
|
|
355
|
-
// resolves through the CLI, and self-resolve stays rejected by the same identity comparison ('human' can
|
|
356
|
-
// never resolve a human-authored remark) — and retract binds to the author (only the human's own remarks).
|
|
357
|
-
// Who-may-resolve/retract cannot depend on transport.
|
|
358
|
-
//
|
|
359
|
-
// Every issue/remark write route below ends its success path with notifyBoardChanged('full') — the board
|
|
360
|
-
// cache is invalidated ATOMICALLY with persistence ([[remark-substrate]] write-visibility), before the
|
|
361
|
-
// response, so the writer's own post-write refetch can never race an async fs event into the stale cache.
|
|
362
|
-
// This explicit nudge is the ONE in-process mechanism (the store dir is deliberately NOT in the watch set);
|
|
363
|
-
// a cross-process write (a CLI `spex remark add`) reaches the board through its trunk commit via the
|
|
364
|
-
// existing refs watcher instead.
|
|
365
|
-
app.post('/api/remarks', async (c) => {
|
|
366
|
-
if (!issuesEnabled()) return c.json({ error: 'issues workflow is off' }, 403)
|
|
367
|
-
const body = await c.req.json().catch(() => ({}))
|
|
368
|
-
const text = typeof body?.body === 'string' ? body.body : ''
|
|
369
|
-
if (!text.trim()) return c.json({ error: 'empty remark' }, 400)
|
|
370
|
-
const evidence = Array.isArray(body?.evidence) ? (body.evidence as unknown[]).filter((h): h is string => typeof h === 'string' && /^[0-9a-f]{64}$/.test(h)) : []
|
|
371
|
-
const host = typeof body?.scenario === 'string' && body.scenario
|
|
372
|
-
? { node: typeof body?.node === 'string' ? body.node : undefined, scenario: body.scenario as string }
|
|
373
|
-
: { issue: typeof body?.issue === 'string' ? body.issue : undefined }
|
|
374
|
-
const codeSha = typeof body?.codeSha === 'string' ? body.codeSha : undefined
|
|
375
|
-
try {
|
|
376
|
-
const r = await remarkWithLoopIn(host, text, { codeSha, author: 'human', evidence })
|
|
377
|
-
notifyBoardChanged('full')
|
|
378
|
-
return c.json({ ok: true, ref: r.ref, rid: r.rid, codeSha: r.codeSha, outcomes: summarizeLoopIn(r.loopIn) }, 201)
|
|
379
|
-
} catch (e) {
|
|
380
|
-
return c.json({ error: String((e as Error).message || e) }, 400)
|
|
381
|
-
}
|
|
382
|
-
})
|
|
383
|
-
app.post('/api/remarks/:action{resolve|retract}', async (c) => {
|
|
384
|
-
if (!issuesEnabled()) return c.json({ error: 'issues workflow is off' }, 403)
|
|
385
|
-
const body = await c.req.json().catch(() => ({}))
|
|
386
|
-
const ref = typeof body?.ref === 'string' ? body.ref : ''
|
|
387
|
-
if (!ref) return c.json({ error: 'missing remark ref' }, 400)
|
|
388
|
-
const by = 'human' // server-derived identity — never the request body (see /api/remarks above)
|
|
389
|
-
try {
|
|
390
|
-
if (c.req.param('action') === 'resolve') resolveRemark(ref, by)
|
|
391
|
-
else retractRemark(ref, by)
|
|
392
|
-
notifyBoardChanged('full')
|
|
393
|
-
return c.json({ ok: true, ref })
|
|
394
|
-
} catch (e) {
|
|
395
|
-
return c.json({ error: String((e as Error).message || e) }, 400)
|
|
396
|
-
}
|
|
397
|
-
})
|
|
398
|
-
// the harness slice of the dashboard input's `/` dropdown — computed by the launcher's HARNESS adapter the same way that harness
|
|
399
|
-
// computes its own `/` menu ([[harness-adapter]]). The client passes `?harness=<id>` for the ACTIVE session,
|
|
400
|
-
// so a codex tab gets CODEX's menu, not the default's; unknown/absent → default. Insert-only on the client.
|
|
401
|
-
app.get('/api/slash-commands', (c) => {
|
|
402
|
-
const h = HARNESSES.find((x) => x.id === c.req.query('harness')) || defaultHarness
|
|
403
|
-
return c.json(h.slashCommands())
|
|
404
|
-
})
|
|
405
|
-
|
|
406
|
-
function uploadFailure(error: unknown): Response {
|
|
407
|
-
if (!(error instanceof UploadError)) throw error
|
|
408
|
-
const body: { error: string; offset?: number } = { error: error.message }
|
|
409
|
-
if (error.offset != null) body.offset = error.offset
|
|
410
|
-
return new Response(JSON.stringify(body), { status: error.status, headers: { 'content-type': 'application/json' } })
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// One offset protocol for every attachment. Chunks stream through the existing /api proxy and stage only on
|
|
414
|
-
// the worker machine; completion is the one boundary that makes a prompt-visible absolute path exist.
|
|
415
|
-
app.post('/api/uploads', async (c) => {
|
|
416
|
-
const body = await c.req.json().catch(() => null) as { name?: unknown; size?: unknown } | null
|
|
417
|
-
try {
|
|
418
|
-
return c.json(createUpload(body?.name, body?.size), 201)
|
|
419
|
-
} catch (error) {
|
|
420
|
-
return uploadFailure(error)
|
|
421
|
-
}
|
|
422
|
-
})
|
|
423
|
-
app.get('/api/uploads/:id', (c) => {
|
|
424
|
-
try {
|
|
425
|
-
return c.json(uploadStatus(c.req.param('id')))
|
|
426
|
-
} catch (error) {
|
|
427
|
-
return uploadFailure(error)
|
|
428
|
-
}
|
|
429
|
-
})
|
|
430
|
-
app.patch('/api/uploads/:id', async (c) => {
|
|
431
|
-
try {
|
|
432
|
-
return c.json(await appendUpload(
|
|
433
|
-
c.req.param('id'), Number(c.req.header('upload-offset')), c.req.raw.body, c.req.header('content-length'),
|
|
434
|
-
))
|
|
435
|
-
} catch (error) {
|
|
436
|
-
return uploadFailure(error)
|
|
437
|
-
}
|
|
438
|
-
})
|
|
439
|
-
app.post('/api/uploads/:id/complete', (c) => {
|
|
440
|
-
try {
|
|
441
|
-
return c.json({ path: completeUpload(c.req.param('id')) }, 201)
|
|
442
|
-
} catch (error) {
|
|
443
|
-
return uploadFailure(error)
|
|
444
|
-
}
|
|
445
|
-
})
|
|
446
|
-
app.delete('/api/uploads/:id', (c) => {
|
|
447
|
-
try {
|
|
448
|
-
cancelUpload(c.req.param('id'))
|
|
449
|
-
return c.body(null, 204)
|
|
450
|
-
} catch (error) {
|
|
451
|
-
return uploadFailure(error)
|
|
452
|
-
}
|
|
453
|
-
})
|
|
454
|
-
|
|
455
|
-
// sessions: real tmux-backed Claude Code sessions. List + spawn, stream the live pane (WebSocket),
|
|
456
|
-
// forward keystrokes, and close.
|
|
457
|
-
app.get('/api/sessions', async (c) => c.json(await listSessions(c.req.query('all') === '1' || c.req.query('all') === 'true')))
|
|
458
|
-
app.get('/api/resources', async (c) => c.json(await collectResourceReport()))
|
|
459
|
-
app.post('/api/sessions', async (c) => {
|
|
460
|
-
const requestKey = c.req.header('idempotency-key') || randomUUID()
|
|
461
|
-
const controller = new AbortController()
|
|
462
|
-
const rawSignal = c.req.raw.signal
|
|
463
|
-
const outgoing = (c.env as { outgoing?: HttpServerResponse }).outgoing
|
|
464
|
-
const cancel = () => {
|
|
465
|
-
if (!outgoing?.writableEnded) controller.abort(new Error('session-create caller disconnected'))
|
|
466
|
-
}
|
|
467
|
-
const cancelFromRequest = () => controller.abort(rawSignal.reason)
|
|
468
|
-
rawSignal.addEventListener('abort', cancelFromRequest, { once: true })
|
|
469
|
-
outgoing?.once('close', cancel)
|
|
470
|
-
try {
|
|
471
|
-
const body = await c.req.json().catch(() => null)
|
|
472
|
-
const result = await sessionCreateRequest(body, { requestKey, signal: controller.signal })
|
|
473
|
-
// The durable row is now public. Nudge the cheap session projection explicitly so a dashboard does not
|
|
474
|
-
// wait for the best-effort store watcher; any held candidate worktree event remains a separate full claim.
|
|
475
|
-
if (result.status === 201) notifyBoardChanged('sessions')
|
|
476
|
-
// A candidate registry event is intentionally held while Git creates the private worktree. Once the
|
|
477
|
-
// transaction has published or cleaned up its record, release the one deferred full refresh.
|
|
478
|
-
flushDeferredWorktreeRegistryChange()
|
|
479
|
-
if (result.status === 201) {
|
|
480
|
-
c.header('Idempotency-Key', requestKey)
|
|
481
|
-
return c.json(result.session, 201)
|
|
482
|
-
}
|
|
483
|
-
return c.json({ error: result.error, ...(result.code ? { code: result.code } : {}), ...(result.phase ? { phase: result.phase } : {}) }, result.status as any)
|
|
484
|
-
} finally {
|
|
485
|
-
flushDeferredWorktreeRegistryChange()
|
|
486
|
-
rawSignal.removeEventListener('abort', cancelFromRequest)
|
|
487
|
-
outgoing?.off('close', cancel)
|
|
488
|
-
}
|
|
489
|
-
})
|
|
490
|
-
// one server-side merge bundle (ahead/dirty/diff(merge-base)/gates/proposal) for the manager cockpit;
|
|
491
|
-
// dashboard and `spex session review` are thin callers. 404 for an unknown id. See [[manager-cockpit]].
|
|
492
|
-
app.get('/api/sessions/:id/review', async (c) => {
|
|
493
|
-
const r = await cockpitReview(c.req.param('id'))
|
|
494
|
-
return r ? c.json(r) : c.json({ error: 'no such session' }, 404)
|
|
495
|
-
})
|
|
496
|
-
// The self-contained HTML is the sole full-model transport exception. Interactive rows, including the CLI,
|
|
497
|
-
// use /api/evals pages; a bare request fails loudly rather than reopening a hidden full JSON path.
|
|
498
|
-
app.get('/api/sessions/:id/evals', async (c) => {
|
|
499
|
-
if (c.req.query('format') === 'html') {
|
|
500
|
-
const m = await buildExportModel(c.req.param('id'))
|
|
501
|
-
return m ? c.html(renderExportHtml(m)) : c.text('no such session', 404)
|
|
502
|
-
}
|
|
503
|
-
return c.json({ error: 'interactive eval rows use /api/evals pagination; use ?format=html only for export' }, 400)
|
|
504
|
-
})
|
|
505
|
-
// the session's live pane as text (one-shot snapshot) for a backend client (`spex session show --capture`). Empty and fail
|
|
506
|
-
// stay distinct: an empty pane is 200 with empty body; unknown id → 404, offline (no live pane) → 409, error → 502.
|
|
507
|
-
app.get('/api/sessions/:id/capture', async (c) => {
|
|
508
|
-
const r = await captureSessionResult(c.req.param('id'))
|
|
509
|
-
if (r.ok) return c.text(r.pane)
|
|
510
|
-
if (r.reason === 'unknown') return c.text('no such session', 404)
|
|
511
|
-
if (r.reason === 'offline') return c.text('session offline (no live pane)', 409)
|
|
512
|
-
return c.text('capture failed', 502)
|
|
513
|
-
})
|
|
514
|
-
// A live adapter-owned execution observation, intentionally distinct from the durable conversation timeline.
|
|
515
|
-
// The response carries only the backend-normalized latest working note and typed tool rows; no transcript bytes
|
|
516
|
-
// or parser schema cross this API boundary.
|
|
517
|
-
app.get('/api/sessions/:id/execution', (c) => {
|
|
518
|
-
const execution = readSessionExecution(c.req.param('id') || '')
|
|
519
|
-
return execution ? c.json(execution) : c.json({ error: 'no such session' }, 404)
|
|
520
|
-
})
|
|
521
|
-
app.get('/api/sessions/:id/execution/stream', (c) => sessionExecutionStream(c))
|
|
522
|
-
// the session's persisted interaction history ([[session-timeline]]): authored status transitions (with the
|
|
523
|
-
// FULL note text) + delivered prompts, timestamped, oldest first — what a terminal-free surface renders as
|
|
524
|
-
// the conversation. `?limit=<n>` caps the tail (default 500). 404 for an unknown/non-governed id.
|
|
525
|
-
app.get('/api/sessions/:id/timeline', (c) => {
|
|
526
|
-
const limit = Number(c.req.query('limit'))
|
|
527
|
-
const r = readTimeline(c.req.param('id'), Number.isFinite(limit) && limit > 0 ? limit : undefined)
|
|
528
|
-
return r ? c.json(r) : c.json({ error: 'no such session' }, 404)
|
|
529
|
-
})
|
|
530
|
-
// the session RECORD detail (`spex session show`): the board row (status · node · branch · launcher · …)
|
|
531
|
-
// plus the full originating prompt (the row itself carries only the preview). One id-addressed read backs
|
|
532
|
-
// the CLI's show; 404 for an unknown id.
|
|
533
|
-
app.get('/api/sessions/:id', async (c) => {
|
|
534
|
-
const id = c.req.param('id')
|
|
535
|
-
const row = (await listSessions(true)).find((s) => s.id === id)
|
|
536
|
-
if (!row) return c.json({ error: 'no such session' }, 404)
|
|
537
|
-
return c.json({ ...row, prompt: await sessionPrompt(id) })
|
|
538
|
-
})
|
|
539
|
-
app.get('/api/sessions/:id/files', (c) => {
|
|
540
|
-
try { return c.json({ files: listSessionFiles(c.req.param('id')) }) }
|
|
541
|
-
catch (error) {
|
|
542
|
-
if (error instanceof SessionFileError) return c.json({ error: error.message }, error.status)
|
|
543
|
-
throw error
|
|
544
|
-
}
|
|
545
|
-
})
|
|
546
|
-
const sessionFileDownload = (c: any) => {
|
|
547
|
-
try {
|
|
548
|
-
const requested = c.req.query('path')
|
|
549
|
-
if (!requested) return c.json({ error: 'download needs a posted path' }, 400)
|
|
550
|
-
const file = openSessionFile(c.req.param('id'), requested)
|
|
551
|
-
const preview = c.req.query('preview') === '1'
|
|
552
|
-
const previewType = preview ? sessionFilePreviewKind(file.path) : null
|
|
553
|
-
if (preview && !previewType) return c.json({ error: 'no preview for this file type; download it instead' }, 415)
|
|
554
|
-
if (preview && file.size > SESSION_FILE_PREVIEW_MAX_BYTES)
|
|
555
|
-
return c.json({ error: `preview is limited to ${SESSION_FILE_PREVIEW_MAX_BYTES / (1024 * 1024)} MiB; download this ${file.size}-byte file instead` }, 413)
|
|
556
|
-
const headers: Record<string, string> = {
|
|
557
|
-
'Cache-Control': 'no-store',
|
|
558
|
-
'Content-Disposition': `${preview ? 'inline' : 'attachment'}; filename*=UTF-8''${encodeURIComponent(file.name)}`,
|
|
559
|
-
'Content-Length': String(file.size),
|
|
560
|
-
'Content-Type': previewType?.contentType ?? 'application/octet-stream',
|
|
561
|
-
}
|
|
562
|
-
if (previewType) headers['X-Spexcode-Preview-Kind'] = previewType.kind
|
|
563
|
-
if (c.req.method === 'HEAD') return c.body(null, 200, headers)
|
|
564
|
-
return c.body(Readable.toWeb(createReadStream(file.path)) as ReadableStream, 200, headers)
|
|
565
|
-
} catch (error) {
|
|
566
|
-
if (error instanceof SessionFileError) return c.json({ error: error.message }, error.status)
|
|
567
|
-
throw error
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
app.get('/api/sessions/:id/files/download', sessionFileDownload)
|
|
571
|
-
app.on('HEAD', '/api/sessions/:id/files/download', sessionFileDownload)
|
|
572
|
-
// lifecycle transitions (thin callers of the session state machine)
|
|
573
|
-
// relaunch ONLY if confirmed offline; demotes working→idle, keeps any declaration. The RESUME GUARD refuses
|
|
574
|
-
// (409) when the agent is alive or its liveness is unproven — restore-on-alive was the incident's kill-shot.
|
|
575
|
-
// `force` (query ?force=1 or JSON {force:true}) overrides for a wedged-but-alive process.
|
|
576
|
-
app.post('/api/sessions/:id/resume', async (c) => {
|
|
577
|
-
const body = await c.req.json().catch(() => ({} as { force?: boolean }))
|
|
578
|
-
const force = body?.force === true || c.req.query('force') === '1'
|
|
579
|
-
const r = await resumeSession(c.req.param('id'), { force })
|
|
580
|
-
return c.json(r, r.ok ? 200 : (r.refused ? 409 : 404))
|
|
581
|
-
})
|
|
582
|
-
// A reviewed dispatch to the session's own agent (it runs the merge), never a server merge. The caller returns
|
|
583
|
-
// the exact branch/base OIDs with a durable key; sessions.ts validates and accepts them once before ensure-live.
|
|
584
|
-
app.post('/api/sessions/:id/merge', async (c) => {
|
|
585
|
-
const requestKey = c.req.header('idempotency-key')
|
|
586
|
-
if (requestKey === undefined) {
|
|
587
|
-
return c.json({ dispatched: false, reason: 'Idempotency-Key is required with reviewed merge authority', code: 'session_merge_invalid_request' }, 400)
|
|
588
|
-
}
|
|
589
|
-
const rawBody = await c.req.text()
|
|
590
|
-
let body: unknown = {}
|
|
591
|
-
if (rawBody.trim()) {
|
|
592
|
-
try { body = JSON.parse(rawBody) }
|
|
593
|
-
catch { return c.json({ dispatched: false, reason: 'body must be valid JSON', code: 'session_merge_invalid_request' }, 400) }
|
|
594
|
-
}
|
|
595
|
-
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
596
|
-
return c.json({ dispatched: false, reason: 'body must be a JSON object', code: 'session_merge_invalid_request' }, 400)
|
|
597
|
-
}
|
|
598
|
-
const input = body as Record<string, unknown>
|
|
599
|
-
const keys = Object.keys(input)
|
|
600
|
-
const allowed = new Set(['expectedBranchHead', 'expectedBaseHead'])
|
|
601
|
-
if (keys.some((key) => !allowed.has(key))) {
|
|
602
|
-
return c.json({ dispatched: false, reason: `unknown session-merge field(s): ${keys.filter((key) => !allowed.has(key)).join(', ')}`, code: 'session_merge_invalid_request' }, 400)
|
|
603
|
-
}
|
|
604
|
-
if (typeof input.expectedBranchHead !== 'string' || typeof input.expectedBaseHead !== 'string') {
|
|
605
|
-
return c.json({ dispatched: false, reason: 'expectedBranchHead and expectedBaseHead must be strings', code: 'session_merge_invalid_request' }, 400)
|
|
606
|
-
}
|
|
607
|
-
const r = await mergeSession(c.req.param('id'), {
|
|
608
|
-
requestKey,
|
|
609
|
-
expectedBranchHead: input.expectedBranchHead,
|
|
610
|
-
expectedBaseHead: input.expectedBaseHead,
|
|
611
|
-
})
|
|
612
|
-
return c.json(r, r.dispatched ? 200 : (r.status ?? 409))
|
|
613
|
-
})
|
|
614
|
-
|
|
615
|
-
// one WS owns one native tmux client (pty-bridge): server→client = that client's rendered PTY bytes (binary);
|
|
616
|
-
// client→server text controls resize, visibility, and xterm-native input (which carries the mouse/wheel
|
|
617
|
-
// SGR reports xterm produces natively in mouse-report mode). Server→client text commits a completed resize immediately before its binary tmux transaction;
|
|
618
|
-
// hiding starts that viewer's bounded helper release without closing the warm socket. tmux itself resolves wheel input between
|
|
619
|
-
// copy-mode and a mouse-owning TUI. The bridge never splices capture-pane state into this stream.
|
|
620
|
-
// keep-alive ping cadence for the terminal socket — the server half of [[reconnect]]'s heartbeat contract,
|
|
621
|
-
// and the contract's ONE primitive number: the client mirrors it (SERVER_PING_MS in the dashboard's
|
|
622
|
-
// resilientSocket.js, pinned by its test) and DERIVES its silence deadline (2.5×) from it.
|
|
623
|
-
// A healthy link is guaranteed inbound traffic every PING window, so the client may presume an OPEN socket
|
|
624
|
-
// silent past its derived window dead. The same tick also sends a WebSocket protocol ping; browsers answer its
|
|
625
|
-
// pong below JavaScript, so a backend reload stays compatible with a tab running the previous frontend bundle.
|
|
626
|
-
// The server owns that mirror deadline and detaches the viewer itself when a half-open link never reports close.
|
|
627
|
-
// Terminal pixels remain binary frames, so heartbeat controls never enter xterm.
|
|
628
|
-
const TERM_PING_MS = 10000
|
|
629
|
-
const TERM_DEAD_MS = 2.5 * TERM_PING_MS
|
|
630
|
-
app.get('/api/sessions/:id/socket', upgradeWebSocket((c) => {
|
|
631
|
-
const id = c.req.param('id') as string
|
|
632
|
-
let viewer: Viewer | null = null
|
|
633
|
-
let ping: ReturnType<typeof setInterval> | undefined
|
|
634
|
-
let pongDeadline: ReturnType<typeof setTimeout> | undefined
|
|
635
|
-
let cleaned = false
|
|
636
|
-
const disarmPongDeadline = () => { if (pongDeadline) clearTimeout(pongDeadline); pongDeadline = undefined }
|
|
637
|
-
let armPongDeadline = () => {}
|
|
638
|
-
const cleanup = () => {
|
|
639
|
-
if (cleaned) return
|
|
640
|
-
cleaned = true
|
|
641
|
-
if (ping) clearInterval(ping)
|
|
642
|
-
disarmPongDeadline()
|
|
643
|
-
if (viewer) detachViewer(id, viewer)
|
|
644
|
-
viewer = null
|
|
645
|
-
}
|
|
646
|
-
return {
|
|
647
|
-
onOpen(_evt, ws) {
|
|
648
|
-
viewer = {
|
|
649
|
-
send: (buf) => { try { ws.send(Uint8Array.from(buf)) } catch { /* viewer gone */ } },
|
|
650
|
-
commitSize: (cols, rows) => { try { ws.send(JSON.stringify({ t: 'resize-commit', cols, rows })) } catch { /* viewer gone */ } },
|
|
651
|
-
}
|
|
652
|
-
attachViewer(id, viewer)
|
|
653
|
-
armPongDeadline = () => {
|
|
654
|
-
if (cleaned) return
|
|
655
|
-
disarmPongDeadline()
|
|
656
|
-
pongDeadline = setTimeout(() => {
|
|
657
|
-
cleanup()
|
|
658
|
-
try { ws.close() } catch { /* cleanup already detached the dead viewer */ }
|
|
659
|
-
}, TERM_DEAD_MS)
|
|
660
|
-
pongDeadline.unref()
|
|
661
|
-
}
|
|
662
|
-
// `raw` is @hono/node-ws's real ws.WebSocket. Protocol pong is intentionally the server-side liveness
|
|
663
|
-
// signal: unlike an application text reply, every browser generation answers it automatically.
|
|
664
|
-
ws.raw.on('pong', armPongDeadline)
|
|
665
|
-
armPongDeadline()
|
|
666
|
-
ping = setInterval(() => {
|
|
667
|
-
try { ws.raw.ping() } catch { /* viewer gone; onClose reaps */ }
|
|
668
|
-
try { ws.send('ping') } catch { /* client dead-man still needs observable inbound traffic */ }
|
|
669
|
-
}, TERM_PING_MS)
|
|
670
|
-
},
|
|
671
|
-
onMessage(evt) {
|
|
672
|
-
if (!viewer) return
|
|
673
|
-
const data = evt.data
|
|
674
|
-
// Binary input is ignored; JSON keeps terminal input distinct from binary pane output while preserving
|
|
675
|
-
// xterm's ordered string exactly. The bridge accepts input only from this viewer's visible claim.
|
|
676
|
-
if (typeof data === 'string') {
|
|
677
|
-
if (data === 'pong') {
|
|
678
|
-
armPongDeadline()
|
|
679
|
-
return
|
|
680
|
-
}
|
|
681
|
-
try {
|
|
682
|
-
const m = JSON.parse(data)
|
|
683
|
-
if (m?.t === 'resize') resizeBridge(id, viewer, Number(m.cols), Number(m.rows))
|
|
684
|
-
else if (m?.t === 'visible' && m.visible === false) hideViewer(id, viewer)
|
|
685
|
-
else if (m?.t === 'input' && typeof m.data === 'string') forwardInput(id, viewer, m.data)
|
|
686
|
-
} catch { /* ignore */ }
|
|
687
|
-
}
|
|
688
|
-
},
|
|
689
|
-
onClose() { cleanup() },
|
|
690
|
-
}
|
|
691
|
-
}))
|
|
692
|
-
// ONE input route, `kind` the discriminator — the transport split is an implementation fact, not API surface.
|
|
693
|
-
// kind:"text" (`spex session send`, the server-side merge dispatch) appends the prompt to the
|
|
694
|
-
// target timeline, then best-effort pokes its adapter. A dead channel delays context injection but does not
|
|
695
|
-
// change the successful append response; 502 means the record rejected the write.
|
|
696
|
-
// kind:"keys" is the LAST-RESORT raw face (`spex session send --keys`): an ORDERED BATCH of
|
|
697
|
-
// nav-mode key tokens over tmux send-keys, delivered in array order so tap order survives
|
|
698
|
-
// ([[nav-mode-key-ordering]]); unstable by nature — callers try a plain text send first. An unknown kind is a
|
|
699
|
-
// loud 400, never a guessed channel.
|
|
700
|
-
app.post('/api/sessions/:id/input', async (c) => {
|
|
701
|
-
const body = await c.req.json().catch(() => ({}))
|
|
702
|
-
if (body?.kind === 'text' || body?.kind === 'command') {
|
|
703
|
-
// `from` (the sender's session id) rides only an agent-to-agent send → the backend records the comms
|
|
704
|
-
// edge ([[session-timeline]]); a raw human dispatch omits it and is not logged. `replyVia:"note"` marks a
|
|
705
|
-
// terminal-free sender ([[session-timeline]]): the server appends the note-reply insert to the delivery.
|
|
706
|
-
const r = await sendText(c.req.param('id'), typeof body?.text === 'string' ? body.text : '', typeof body?.from === 'string' ? body.from : undefined, {
|
|
707
|
-
...(body?.replyVia === 'note' ? { replyVia: 'note' as const } : {}),
|
|
708
|
-
})
|
|
709
|
-
return c.json(r, r.ok ? 200 : 502)
|
|
710
|
-
}
|
|
711
|
-
if (body?.kind === 'keys') {
|
|
712
|
-
const keys = Array.isArray(body?.keys) ? body.keys.filter((k: unknown) => typeof k === 'string') : []
|
|
713
|
-
const ok = await rawKey(c.req.param('id'), keys)
|
|
714
|
-
return c.json({ ok }, ok ? 200 : 404)
|
|
715
|
-
}
|
|
716
|
-
return c.json({ error: 'input needs kind: "text" | "command" | "keys"' }, 400)
|
|
717
|
-
})
|
|
718
|
-
app.post('/api/sessions/reparent', async (c) => {
|
|
719
|
-
const result = await reparentRequest(await c.req.json().catch(() => null))
|
|
720
|
-
notifyBoardChanged('sessions')
|
|
721
|
-
return c.json(result)
|
|
722
|
-
})
|
|
723
|
-
// soft stop: kill the agent's tmux + socket but KEEP the worktree (resumable). Distinct from close, which
|
|
724
|
-
// removes the worktree. {ok:false} = no such session.
|
|
725
|
-
app.post('/api/sessions/:id/stop', async (c) => {
|
|
726
|
-
const sessionId = c.req.param('id')
|
|
727
|
-
const ok = await stopSession(sessionId)
|
|
728
|
-
return c.json(ok ? { ok: true } : { ok: false, error: `no stop transition was committed for session ${sessionId}` }, ok ? 200 : 404)
|
|
729
|
-
})
|
|
730
|
-
app.post('/api/sessions/:id/interrupt', async (c) => {
|
|
731
|
-
const result = await interruptSession(c.req.param('id'))
|
|
732
|
-
return c.json(result, result.ok ? 200 : 502)
|
|
733
|
-
})
|
|
734
|
-
app.post('/api/sessions/:id/close', async (c) => {
|
|
735
|
-
const sessionId = c.req.param('id')
|
|
736
|
-
const body = await c.req.json().catch(() => ({}))
|
|
737
|
-
const ok = await closeSession(sessionId, body?.source)
|
|
738
|
-
// The close route owns its write's visible boundary: filesystem watchers can be unavailable, so cache
|
|
739
|
-
// invalidation must happen before the success response rather than leaving the confirming board to patrol.
|
|
740
|
-
if (ok) notifyBoardChanged('sessions')
|
|
741
|
-
return c.json(ok ? { ok: true } : { ok: false, error: `no close transition was committed for session ${sessionId}` }, ok ? 200 : 404)
|
|
742
|
-
})
|
|
743
|
-
app.post('/api/sessions/:id/quarantine', async (c) => {
|
|
744
|
-
const body = await c.req.json().catch(() => null)
|
|
745
|
-
const result = await quarantineCorruptRecord(c.req.param('id'), body)
|
|
746
|
-
return c.json({ ok: true, ...result })
|
|
747
|
-
})
|
|
748
|
-
app.post('/api/sessions/:id/quarantine/restore', async (c) => {
|
|
749
|
-
const result = await restoreQuarantinedRecord(c.req.param('id'))
|
|
750
|
-
return c.json({ ok: true, ...result })
|
|
751
|
-
})
|
|
752
|
-
// archive / legacy unarchive signpost ([[archive]]) — archive proves exact cold/offline ownership before filing;
|
|
753
|
-
// `{on:false}` enters the same resume transition and recreates the preserved conversation. {ok:false}=no such session.
|
|
754
|
-
app.post('/api/sessions/:id/archive', async (c) => {
|
|
755
|
-
const body = await c.req.json().catch(() => ({}))
|
|
756
|
-
return c.json({ ok: await archiveSession(c.req.param('id'), body?.on !== false) })
|
|
757
|
-
})
|
|
758
|
-
// set (or clear, with a blank) a session's display-name override; persists to the session's global record
|
|
759
|
-
// (`session.json`) so it survives a restart. Unknown id → 404. That record sits INSIDE the watched store, but
|
|
760
|
-
// the store watch is best-effort (it can fail to attach), so the route still nudges the stream explicitly
|
|
761
|
-
// ([[graph-stream]]) — the rename shows in ~150ms deterministically, never waiting out a cold tick.
|
|
762
|
-
app.post('/api/sessions/:id/rename', async (c) => {
|
|
763
|
-
const body = await c.req.json().catch(() => ({}))
|
|
764
|
-
const ok = await renameSession(c.req.param('id'), typeof body?.name === 'string' ? body.name : '')
|
|
765
|
-
if (ok) notifyBoardChanged('sessions')
|
|
766
|
-
return c.json({ ok }, ok ? 200 : 404)
|
|
767
|
-
})
|
|
768
|
-
|
|
769
|
-
// set/clear a session's sort-key ([[session-reorder]]): a finite number pins the row's slot, null (or
|
|
770
|
-
// non-numeric) restores birth order. Mirrors /rename.
|
|
771
|
-
app.post('/api/sessions/:id/sort', async (c) => {
|
|
772
|
-
const body = await c.req.json().catch(() => ({}))
|
|
773
|
-
const key = typeof body?.key === 'number' && Number.isFinite(body.key) ? body.key : null
|
|
774
|
-
const ok = await setSessionSort(c.req.param('id'), key)
|
|
775
|
-
return c.json({ ok }, ok ? 200 : 404)
|
|
776
|
-
})
|
|
777
|
-
|
|
778
|
-
const port = Number(process.env.PORT || 8787)
|
|
779
|
-
// @@@ server-side connection reaping ([[spec-cli]]) - abandoned connections must die SERVER-SIDE, or they
|
|
780
|
-
// pile up and wedge the backend (135 leaked conns once starved :8787 into looking dead — the cascade that
|
|
781
|
-
// triggered the mass-restore incident, since every client-side timeout-kill leaks one). The ONE mechanism is
|
|
782
|
-
// the socket-level `installConnectionReaper` below (reaper.ts): a per-socket deadline that reaps a
|
|
783
|
-
// slow-loris / idle keep-alive but exempts an ACTIVE WS/SSE stream (board-stream, terminal socket) for as
|
|
784
|
-
// long as it streams. Deliberately NO `serverOptions` timeouts here: they were measured to be not harmless
|
|
785
|
-
// but a second mechanism racing the reaper (issue #65 — a 20s headersTimeout won at default config and
|
|
786
|
-
// silently capped SPEXCODE_REAP_HEADER_MS); the install disables Node's overlapping timeouts so the
|
|
787
|
-
// deadlines have a single owner.
|
|
788
|
-
// @@@ loopback bind ([[public-mode]]) - this child is NEVER the internet face: the supervisor (and in public
|
|
789
|
-
// mode the gateway) fronts it, and dials it only via 127.0.0.1. Binding loopback is what makes "loopback is
|
|
790
|
-
// the trust boundary" true — without a hostname Node binds all interfaces and the child is reachable from
|
|
791
|
-
// the LAN with no password, bypassing the gate entirely (measured: eval auth-boundary).
|
|
792
|
-
const server = serve({ fetch: app.fetch, port, hostname: '127.0.0.1' })
|
|
793
|
-
installConnectionReaper(server as unknown as HttpServer)
|
|
794
|
-
injectWebSocket(server)
|
|
795
|
-
superviseBridges() // restore visible helpers after failure; their viewer subscriptions survive replacement
|
|
796
|
-
superviseQueue() // launch queued sessions as slots free (catches agent-authored proposals/crashes the server never sees directly)
|
|
797
|
-
superviseTurnFailures() // reconcile adapter-owned native failure subscriptions across backend replacement
|
|
798
|
-
superviseDelivery() // hand over messages an earlier pass could not ([[delivery-queue]]): the retry half of dispatch
|
|
799
|
-
console.log(`spec-cli serving .spec (from git) on http://localhost:${port}`)
|
|
800
|
-
|
|
801
|
-
let graphWatchersClosed = false
|
|
802
|
-
const closeGraphWatchers = (): void => {
|
|
803
|
-
if (graphWatchersClosed) return
|
|
804
|
-
graphWatchersClosed = true
|
|
805
|
-
closeBoardFileWatchers()
|
|
806
|
-
}
|
|
807
|
-
process.once('exit', closeGraphWatchers)
|
|
808
|
-
|
|
809
|
-
// graceful drain (the other half of zero-downtime reload, supervise.ts): on SIGTERM stop accepting new
|
|
810
|
-
// connections, let in-flight requests finish, and sweep now-idle keep-alive sockets so close() fires the
|
|
811
|
-
// instant the last request drains. A hard cap still forces exit if a connection won't close.
|
|
812
|
-
process.on('SIGTERM', () => {
|
|
813
|
-
closeGraphWatchers()
|
|
814
|
-
const srv = server as unknown as { close(cb?: () => void): void; closeIdleConnections?(): void }
|
|
815
|
-
const sweep = setInterval(() => srv.closeIdleConnections?.(), 200)
|
|
816
|
-
srv.close(() => { clearInterval(sweep); process.exit(0) })
|
|
817
|
-
setTimeout(() => process.exit(0), 10000).unref()
|
|
818
|
-
})
|