spexcode 0.6.2 → 0.6.5
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/spec-cli/bin/spex.mjs +76 -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 +33 -0
- package/node_modules/@spexcode/spec-cli/dist/claude-headless.js +307 -0
- package/node_modules/@spexcode/spec-cli/dist/cli.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/cli.js +1859 -0
- package/node_modules/@spexcode/spec-cli/dist/client.d.ts +111 -0
- package/node_modules/@spexcode/spec-cli/dist/client.js +476 -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 +53 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +686 -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/delivery-queue.d.ts +23 -0
- package/node_modules/@spexcode/spec-cli/dist/delivery-queue.js +179 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.js +547 -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 +361 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway.d.ts +37 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway.js +503 -0
- package/node_modules/@spexcode/spec-cli/dist/graphCache.d.ts +27 -0
- package/node_modules/@spexcode/spec-cli/dist/graphCache.js +716 -0
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.d.ts +14 -0
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +35 -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 +325 -0
- package/node_modules/@spexcode/spec-cli/dist/harness.js +3026 -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 +10 -0
- package/node_modules/@spexcode/spec-cli/dist/help.js +574 -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 +7 -0
- package/{spec-cli/src/listen.ts → node_modules/@spexcode/spec-cli/dist/listen.js} +13 -17
- 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 +4 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode-headless.js +208 -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/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-cursors.d.ts +14 -0
- package/node_modules/@spexcode/spec-cli/dist/session-cursors.js +82 -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 +51 -0
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +233 -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 +412 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +4239 -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 +47 -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 +166 -0
- package/node_modules/@spexcode/spec-core/dist/git.js +2736 -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 +183 -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/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
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
// @@@ the local issue store - the LOCAL store of the one Issue object ([[issues]] / [[local-issues]] /
|
|
2
|
+
// [[mentions]]). A thread IS an Issue whose `store` is 'local' — membership implied by WHERE the file lives,
|
|
3
|
+
// never written into it. One thread = one PLAIN markdown file at <main>/.spec/.issues/<id>.md; there is
|
|
4
|
+
// deliberately no content-kind taxonomy (a change suggestion, an annotation, a Q&A are the same mechanism —
|
|
5
|
+
// the prose says what it is). Others reply/discuss like an async chatroom; a supervisor drains it via
|
|
6
|
+
// `spex issue ls` (reading is the port's job, issues.ts — this module owns only the store + its write verbs).
|
|
7
|
+
// Because a thread file is NOT named spec.md, the spec walk never nodes it and isSpecMd ignores it —
|
|
8
|
+
// invisible to lint / drift / deriveStatus / board with ZERO exemption. The store lives on the TRUNK, not
|
|
9
|
+
// per-branch: reads and writes target the main checkout and commit STRAIGHT to it (--no-verify, provably
|
|
10
|
+
// store-only), so a post-merge thread lands durably even though the author's own branch already merged.
|
|
11
|
+
// A committing write is allowed ONLY from the trunk checkout itself (isPrimaryCheckout) — a linked-worktree
|
|
12
|
+
// backend sharing that main is refused loud, never left to fabricate a stray commit on a main it doesn't own;
|
|
13
|
+
// SPEXCODE_ISSUES_DIR routes an e2e/test rig to a disposable plain-file store (see localStoreDir below).
|
|
14
|
+
// The on-disk dir was historically `.spec/.forum`; a one-shot self-migration ([[issues-store-rename]])
|
|
15
|
+
// renames any legacy `.spec/.forum` to `.spec/.issues` on the first store touch after a toolchain update.
|
|
16
|
+
import { readdirSync, existsSync, mkdirSync, writeFileSync, readFileSync, rmdirSync, statSync } from 'node:fs';
|
|
17
|
+
import { join, dirname, resolve as resolvePath } from 'node:path';
|
|
18
|
+
import { git, headSha, repoRoot } from '@spexcode/spec-core';
|
|
19
|
+
import { mainCheckout, envSessionId, readConfig } from '@spexcode/spec-core';
|
|
20
|
+
import { dispatchNewMentions, parseMentions } from './mentions.js';
|
|
21
|
+
const LOCAL_STORE_REL = '.spec/.issues';
|
|
22
|
+
// the pre-rename dir ([[issues-store-rename]]); ensureStoreMigrated() renames it to LOCAL_STORE_REL once.
|
|
23
|
+
const LEGACY_STORE_REL = '.spec/.forum';
|
|
24
|
+
// @@@ the on/off switch - the issues workflow is an OPT-OUTABLE feature (default ON). The single source of
|
|
25
|
+
// truth is `spexcode.json`'s `issues.enabled` (the same settings file that carries every other toggle),
|
|
26
|
+
// read via readConfig so a machine-local `spexcode.local.json` can override it. OFF silences the post-merge
|
|
27
|
+
// nudge (and, in the dashboard, hides the issues view); the raw write verbs stay usable, since running one
|
|
28
|
+
// is explicit consent. There is deliberately NO CLI toggle verb (v0.3.0 — "no `spex config set`" is this
|
|
29
|
+
// project's standing rule): flip the key by editing the JSON, effective immediately because readConfig
|
|
30
|
+
// reads the working tree. A legacy pre-rename `proposals.enabled` key is NOT read (no fallback — fail
|
|
31
|
+
// toward the default); `spex doctor` reports it so an old settings file gets repaired, not silently obeyed.
|
|
32
|
+
export const issuesEnabled = () => readConfig(mainCheckout()).issues?.enabled ?? true;
|
|
33
|
+
const list = (v) => v ? v.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
34
|
+
// @@@ where a store write lands — ONE deterministic rule ([[local-issues]] "only the trunk checkout may
|
|
35
|
+
// commit"). Every backend resolves the store to `mainCheckout()` (dirname of the shared git-common-dir), so a
|
|
36
|
+
// backend serving a LINKED WORKTREE would git-commit a stray issue onto the REAL, live main — dirtying it and
|
|
37
|
+
// racing its index.lock. The rule:
|
|
38
|
+
// 1. SPEXCODE_ISSUES_DIR override → a DISPOSABLE store of plain files (no git, no shared main); the e2e/test
|
|
39
|
+
// seam. Both reads and writes target it, so a test rig throws its whole store away.
|
|
40
|
+
// 2. else the trunk `.spec/.issues/` — but a *committing* write is allowed ONLY from the trunk checkout
|
|
41
|
+
// itself (repoRoot === mainCheckout: the primary backend, or a throwaway clone owning its own main). A
|
|
42
|
+
// linked-worktree backend is refused loud (requirePrimaryStore) rather than silently writing someone
|
|
43
|
+
// else's main. Reads always resolve to the trunk (visibility never hostages on being primary).
|
|
44
|
+
const overrideStoreDir = () => {
|
|
45
|
+
const p = process.env.SPEXCODE_ISSUES_DIR?.trim();
|
|
46
|
+
return p ? p : null;
|
|
47
|
+
};
|
|
48
|
+
// is THIS process rooted in the trunk checkout itself? True for the primary backend and for a clone (its own
|
|
49
|
+
// .git → its own disposable main); false for a linked-worktree backend, the store-write footgun.
|
|
50
|
+
const isPrimaryCheckout = () => {
|
|
51
|
+
try {
|
|
52
|
+
return resolvePath(repoRoot()) === resolvePath(mainCheckout());
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
// fail LOUD before a committing write from a non-trunk checkout: never fabricate a commit on a shared main
|
|
59
|
+
// this process does not own. (The disposable override bypasses this — it commits to nothing.)
|
|
60
|
+
function requirePrimaryStore(action) {
|
|
61
|
+
if (isPrimaryCheckout())
|
|
62
|
+
return;
|
|
63
|
+
throw new Error(`refusing to ${action}: this backend serves a linked worktree (${resolvePath(repoRoot())}), not the trunk ` +
|
|
64
|
+
`checkout (${resolvePath(mainCheckout())}) — committing here would land a stray issue on the REAL main and ` +
|
|
65
|
+
`race its index. Run the write from the trunk checkout (or its backend), or, for a throwaway/e2e run, set ` +
|
|
66
|
+
`SPEXCODE_ISSUES_DIR=<disposable-dir> to write plain files nowhere near the real store.`);
|
|
67
|
+
}
|
|
68
|
+
// the local issue store dir — a fixed path directly under the trunk's .spec (name-independent, unlike the
|
|
69
|
+
// .plugins system which nests under the named root node), OR the disposable override. Every read and write goes here.
|
|
70
|
+
const localStoreDir = () => overrideStoreDir() ?? join(mainCheckout(), LOCAL_STORE_REL);
|
|
71
|
+
// the author's signature: the effective governed session id (envSessionId handles the claude/codex split).
|
|
72
|
+
export const currentSession = () => envSessionId() || 'unknown';
|
|
73
|
+
// a synchronous sleep for the commit-retry backoff (Date/timers-free, safe in any runtime).
|
|
74
|
+
const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
75
|
+
// ── file format ──────────────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
// frontmatter (concern/by/status/nodes/evidence/created) + a prose body, then any replies — each
|
|
77
|
+
// preceded by a `<!-- reply: <by> @ <iso> -->` sentinel: invisible in rendered markdown, unambiguous to
|
|
78
|
+
// parse. Only the store writes these files, so a fixed shape is safe.
|
|
79
|
+
//
|
|
80
|
+
// A REMARK ([[remark-substrate]]) is a reply carrying extra state, appended to the SAME sentinel as a
|
|
81
|
+
// ` :: <space-joined k=v attrs>` tail (a plain reply has no tail → parses unchanged, backward compatible):
|
|
82
|
+
// rid=<id> stable per-remark id — its presence marks the reply a remark
|
|
83
|
+
// sha=<targetCodeSha> the reading the remark was authored against
|
|
84
|
+
// resolved=<by>@<at> present only once resolved (absent ⟹ resolved:false)
|
|
85
|
+
const REPLY_RE = /^<!-- reply: (.+?) @ (.+?)(?: :: (.+))? -->$/;
|
|
86
|
+
function parse(id, text) {
|
|
87
|
+
const m = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
88
|
+
const fm = {};
|
|
89
|
+
for (const line of (m ? m[1] : '').split('\n')) {
|
|
90
|
+
const mm = line.match(/^([a-zA-Z]+):\s*(.*)$/);
|
|
91
|
+
if (mm)
|
|
92
|
+
fm[mm[1]] = mm[2].trim();
|
|
93
|
+
}
|
|
94
|
+
const body = [];
|
|
95
|
+
const replies = [];
|
|
96
|
+
let cur = null;
|
|
97
|
+
for (const line of (m ? m[2] : text).replace(/^\n+/, '').split('\n')) {
|
|
98
|
+
const rm = line.match(REPLY_RE);
|
|
99
|
+
if (rm) {
|
|
100
|
+
cur = { by: rm[1], at: rm[2], body: '', ...parseRemarkAttrs(rm[3]) };
|
|
101
|
+
replies.push(cur);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (cur)
|
|
105
|
+
cur.body += (cur.body ? '\n' : '') + line;
|
|
106
|
+
else
|
|
107
|
+
body.push(line);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
id,
|
|
111
|
+
store: 'local',
|
|
112
|
+
concern: fm.concern || id,
|
|
113
|
+
by: fm.by || 'unknown',
|
|
114
|
+
status: fm.status === 'landed' ? 'landed' : 'open',
|
|
115
|
+
nodes: list(fm.nodes),
|
|
116
|
+
created: fm.created || '',
|
|
117
|
+
body: body.join('\n').trim(),
|
|
118
|
+
replies: replies.map((r) => ({ ...r, body: r.body.trim() })),
|
|
119
|
+
evidence: list(fm.evidence),
|
|
120
|
+
labels: [],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// user body text must never FORGE a reply sentinel: a line matching `<!-- reply: … -->` in a body would be
|
|
124
|
+
// re-read as a thread boundary, splitting the thread and truncating the body. Neutralize the marker in user
|
|
125
|
+
// content (a zero-width space breaks the `<!-- reply:` prefix — invisible on render, idempotent), so only
|
|
126
|
+
// serialize's OWN sentinels parse as boundaries. Frontmatter scalars are single-line-stripped for the same
|
|
127
|
+
// reason (a newline/`---` in a concern can't break the block).
|
|
128
|
+
const safeBody = (t) => t.trim().replace(/<!-- reply:/g, '<!--reply:');
|
|
129
|
+
const safeScalar = (t) => t.replace(/[\r\n]+/g, ' ').trim();
|
|
130
|
+
// the ` :: k=v k=v` remark tail on a reply sentinel ↔ the reply's remark fields. `undefined` attrs (a plain
|
|
131
|
+
// reply) → no remark fields; `rid` present → the reply IS a remark ([[remark-substrate]]). Values are
|
|
132
|
+
// space-free (ids, a codeSha, a session id, an ISO instant), so a space-split is unambiguous.
|
|
133
|
+
function parseRemarkAttrs(attrs) {
|
|
134
|
+
if (!attrs)
|
|
135
|
+
return {};
|
|
136
|
+
const kv = new Map();
|
|
137
|
+
for (const tok of attrs.trim().split(/\s+/)) {
|
|
138
|
+
const i = tok.indexOf('=');
|
|
139
|
+
if (i > 0)
|
|
140
|
+
kv.set(tok.slice(0, i), tok.slice(i + 1));
|
|
141
|
+
}
|
|
142
|
+
if (!kv.has('rid'))
|
|
143
|
+
return {};
|
|
144
|
+
const out = { rid: kv.get('rid'), targetCodeSha: kv.get('sha') ?? '', resolved: false };
|
|
145
|
+
const r = kv.get('resolved');
|
|
146
|
+
if (r) {
|
|
147
|
+
const at = r.indexOf('@');
|
|
148
|
+
out.resolved = true;
|
|
149
|
+
out.resolvedBy = r.slice(0, at);
|
|
150
|
+
out.resolvedAt = r.slice(at + 1);
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
function serializeRemarkAttrs(r) {
|
|
155
|
+
if (r.rid === undefined)
|
|
156
|
+
return '';
|
|
157
|
+
const parts = [`rid=${r.rid}`, `sha=${r.targetCodeSha ?? ''}`];
|
|
158
|
+
if (r.resolved)
|
|
159
|
+
parts.push(`resolved=${r.resolvedBy ?? ''}@${r.resolvedAt ?? ''}`);
|
|
160
|
+
return ` :: ${parts.join(' ')}`;
|
|
161
|
+
}
|
|
162
|
+
function serialize(p) {
|
|
163
|
+
const fm = [
|
|
164
|
+
`concern: ${safeScalar(p.concern)}`,
|
|
165
|
+
`by: ${safeScalar(p.by)}`,
|
|
166
|
+
`status: ${safeScalar(p.status)}`,
|
|
167
|
+
p.nodes.length ? `nodes: ${p.nodes.join(', ')}` : '',
|
|
168
|
+
p.evidence.length ? `evidence: ${p.evidence.join(', ')}` : '',
|
|
169
|
+
`created: ${p.created}`,
|
|
170
|
+
].filter(Boolean);
|
|
171
|
+
let out = `---\n${fm.join('\n')}\n---\n\n${safeBody(p.body)}\n`;
|
|
172
|
+
for (const r of p.replies)
|
|
173
|
+
out += `\n<!-- reply: ${safeScalar(r.by)} @ ${safeScalar(r.at)}${serializeRemarkAttrs(r)} -->\n${safeBody(r.body)}\n`;
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
export function loadLocalIssues() {
|
|
177
|
+
ensureStoreMigrated(); // read path: a pre-rename deployment migrates on first read, so no thread is lost
|
|
178
|
+
const dir = localStoreDir();
|
|
179
|
+
if (!existsSync(dir))
|
|
180
|
+
return [];
|
|
181
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
182
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
183
|
+
.map((e) => parse(e.name.replace(/\.md$/, ''), readFileSync(join(dir, e.name), 'utf8')))
|
|
184
|
+
.sort((a, b) => a.created.localeCompare(b.created));
|
|
185
|
+
}
|
|
186
|
+
export function loadOne(id) {
|
|
187
|
+
ensureStoreMigrated();
|
|
188
|
+
const f = join(localStoreDir(), `${id}.md`);
|
|
189
|
+
if (!existsSync(f))
|
|
190
|
+
throw new Error(`no local issue '${id}' (see \`spex issue ls --all --store local\`)`);
|
|
191
|
+
return parse(id, readFileSync(f, 'utf8'));
|
|
192
|
+
}
|
|
193
|
+
// a filesystem-safe, readable, collision-free id from the concern (slug + numeric suffix if taken).
|
|
194
|
+
// RESERVED_IDS are the words an issue address gives its own meaning — `new` is the compose PAGE
|
|
195
|
+
// ([[issues-view]]), so an issue owning that id would be unreachable at its own detail address. The slug
|
|
196
|
+
// steps around them with the same numeric suffix a taken id gets: a collision, not a silent shadow.
|
|
197
|
+
const RESERVED_IDS = new Set(['new']);
|
|
198
|
+
function uniqueId(concern) {
|
|
199
|
+
const base = concern.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'issue';
|
|
200
|
+
const dir = localStoreDir();
|
|
201
|
+
let id = base;
|
|
202
|
+
for (let n = 2; RESERVED_IDS.has(id) || existsSync(join(dir, `${id}.md`)); n++)
|
|
203
|
+
id = `${base}-${n}`;
|
|
204
|
+
return id;
|
|
205
|
+
}
|
|
206
|
+
// @@@ store lock - local-issue writes contend two ways: git commits share the repo index.lock, and a
|
|
207
|
+
// reply's read-modify-write of one thread file loses an update if two race (both read the base, last write
|
|
208
|
+
// wins). A single cross-process lock serializes the WHOLE prepare→write→commit, killing both. It is an
|
|
209
|
+
// atomic `mkdir` (in .git, never committed); a lock whose holder crashed is stolen after it goes stale.
|
|
210
|
+
// Because the commit is `--no-verify` fast (below), serialized writes stay quick even under a burst. The
|
|
211
|
+
// lock FILE keeps its historical `spexcode-forum.lock` name ON PURPOSE: during the one-shot store-dir
|
|
212
|
+
// migration ([[issues-store-rename]]) an old and a new toolchain can run against the SAME checkout at once,
|
|
213
|
+
// and they mutually exclude only while contending on the SAME lock name — renaming it would open exactly
|
|
214
|
+
// the migration race the lock exists to close.
|
|
215
|
+
let lockHeld = false; // re-entrancy guard so ensureStoreMigrated() no-ops when reached from inside a hold
|
|
216
|
+
function withStoreLock(fn) {
|
|
217
|
+
// the disposable store locks inside its own dir; the trunk store locks in the shared `.git` (one lock name
|
|
218
|
+
// across every worktree of the clone, so all writers of the SAME real store mutually exclude).
|
|
219
|
+
const override = overrideStoreDir();
|
|
220
|
+
const lock = override ? join(override, '.spexcode-issues.lock') : join(mainCheckout(), '.git', 'spexcode-forum.lock');
|
|
221
|
+
mkdirSync(dirname(lock), { recursive: true }); // parent must exist for the atomic mkdir-acquire (a no-op for .git)
|
|
222
|
+
for (let i = 0;; i++) {
|
|
223
|
+
try {
|
|
224
|
+
mkdirSync(lock);
|
|
225
|
+
break;
|
|
226
|
+
} // atomic acquire
|
|
227
|
+
catch {
|
|
228
|
+
try {
|
|
229
|
+
if (Date.now() - statSync(lock).mtimeMs > 20000)
|
|
230
|
+
rmdirSync(lock);
|
|
231
|
+
}
|
|
232
|
+
catch { /* released meanwhile */ }
|
|
233
|
+
sleep(40 + Math.floor(Math.random() * 80)); // spin with jitter until free / stolen
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
lockHeld = true;
|
|
237
|
+
try {
|
|
238
|
+
return fn();
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
lockHeld = false;
|
|
242
|
+
try {
|
|
243
|
+
rmdirSync(lock);
|
|
244
|
+
}
|
|
245
|
+
catch { /* already gone */ }
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// @@@ one-shot store-dir migration ([[issues-store-rename]]) - the local issue store's on-disk dir was
|
|
249
|
+
// historically `.spec/.forum`; it is now `.spec/.issues`. Any pre-rename deployment migrates ITSELF on the
|
|
250
|
+
// first store touch after its toolchain updates: a single committed `git mv` on the trunk, so a thread's
|
|
251
|
+
// whole reply history reads identically afterwards (`git log --follow` traces through the rename — the data
|
|
252
|
+
// is git, and a rename preserves it). Called at every store entrypoint BEFORE the lock is taken; it takes
|
|
253
|
+
// the lock itself to do the mv, so the whole find→check→mv is atomic against a concurrent first-touch burst:
|
|
254
|
+
// exactly ONE mv commit, because a racer that waited on the lock re-checks under it and finds nothing to do.
|
|
255
|
+
// Fast path: no legacy dir → instant, no lock (the common case after migration / on a fresh repo). Both
|
|
256
|
+
// dirs present (pathological) → fail LOUD with the repair, never a silent merge.
|
|
257
|
+
function ensureStoreMigrated() {
|
|
258
|
+
if (lockHeld)
|
|
259
|
+
return; // inside a hold: an outer entrypoint already ran this
|
|
260
|
+
if (overrideStoreDir())
|
|
261
|
+
return; // disposable store: no legacy trunk dir to migrate
|
|
262
|
+
if (!isPrimaryCheckout())
|
|
263
|
+
return; // a git-committing migration is trunk-only; the primary migrates on its next touch
|
|
264
|
+
const root = mainCheckout();
|
|
265
|
+
if (!existsSync(join(root, LEGACY_STORE_REL)))
|
|
266
|
+
return; // fresh or already-migrated: nothing to do (no lock)
|
|
267
|
+
withStoreLock(() => {
|
|
268
|
+
const r = mainCheckout();
|
|
269
|
+
const legacy = join(r, LEGACY_STORE_REL), current = join(r, LOCAL_STORE_REL);
|
|
270
|
+
if (!existsSync(legacy))
|
|
271
|
+
return; // a racer migrated it while we waited on the lock
|
|
272
|
+
if (existsSync(current))
|
|
273
|
+
throw new Error(`both ${LEGACY_STORE_REL} and ${LOCAL_STORE_REL} exist in ${r} — refusing to auto-merge the local ` +
|
|
274
|
+
`issue store. Reconcile by hand: move any threads from ${LEGACY_STORE_REL} into ${LOCAL_STORE_REL}, ` +
|
|
275
|
+
`then \`git rm -r ${LEGACY_STORE_REL} && git commit\`, and re-run.`);
|
|
276
|
+
git(['-C', r, 'mv', LEGACY_STORE_REL, LOCAL_STORE_REL]);
|
|
277
|
+
git(['-C', r, 'commit', '--no-verify', '-m', `issues: store dir ${LEGACY_STORE_REL} → ${LOCAL_STORE_REL}`,
|
|
278
|
+
'--', LEGACY_STORE_REL, LOCAL_STORE_REL]);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
// write + commit ONE store file STRAIGHT to the trunk. The commit is `--no-verify`: the file is DATA,
|
|
282
|
+
// structurally invisible to spec-lint, and the commit is provably store-only (one .spec/.issues/ path), so the
|
|
283
|
+
// pre-commit gate would only pass anyway — running it just burns seconds (tsx cold-start) holding the lock.
|
|
284
|
+
// A NO-CHANGE write is idempotent SUCCESS, never a failure: when the serialized bytes already equal the
|
|
285
|
+
// stored state (a duplicate close — the store IS the requested state), `git commit` would exit 1
|
|
286
|
+
// 'nothing to commit', so we detect the no-op after `add` (the staged path equals HEAD → status is silent)
|
|
287
|
+
// and skip the commit. Returns whether the store actually changed, so a caller can say 'already <state>'.
|
|
288
|
+
// MUST run while holding withStoreLock — it is the write half of a locked read-modify-write; its callers
|
|
289
|
+
// (commitStore, findOrCreateEvalThread) own the lock, so it never acquires one itself (mkdir is not re-entrant).
|
|
290
|
+
function writeStoreFile(p, message) {
|
|
291
|
+
const override = overrideStoreDir();
|
|
292
|
+
if (override) { // disposable store: a plain file, never a commit, never a shared main
|
|
293
|
+
mkdirSync(override, { recursive: true });
|
|
294
|
+
const f = join(override, `${p.id}.md`);
|
|
295
|
+
const data = serialize(p);
|
|
296
|
+
if (existsSync(f) && readFileSync(f, 'utf8') === data)
|
|
297
|
+
return false;
|
|
298
|
+
writeFileSync(f, data);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
requirePrimaryStore('write a local issue'); // a linked-worktree backend must NOT commit onto the real main
|
|
302
|
+
const root = mainCheckout();
|
|
303
|
+
const rel = `${LOCAL_STORE_REL}/${p.id}.md`;
|
|
304
|
+
mkdirSync(join(root, LOCAL_STORE_REL), { recursive: true });
|
|
305
|
+
writeFileSync(join(root, rel), serialize(p));
|
|
306
|
+
git(['-C', root, 'add', '--', rel]);
|
|
307
|
+
if (!git(['-C', root, 'status', '--porcelain', '--', rel]).trim())
|
|
308
|
+
return false; // staged == HEAD: the no-op
|
|
309
|
+
git(['-C', root, 'commit', '--no-verify', '-m', message, '--', rel]);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
// @@@ the shared trunk-data commit — the committing half of the store discipline, exported for OTHER
|
|
313
|
+
// git-tracked data writes that need a durable landing with no worktree ritual behind them (the eval
|
|
314
|
+
// sidecar's human-ok, [[human-ok]]). Same rules as the issue store's own write: only the trunk checkout
|
|
315
|
+
// itself commits (a linked-worktree caller gets 'not-primary' back and leaves its append for its own
|
|
316
|
+
// session's ritual commit — the sidecar, unlike the issue store, legitimately lives per-branch and
|
|
317
|
+
// merges); the commit is `--no-verify` and provably scoped to the one data path; the shared store lock
|
|
318
|
+
// serializes it against every other trunk data write so racing writers never fight over the git index.
|
|
319
|
+
export function commitTrunkData(relPath, message) {
|
|
320
|
+
if (overrideStoreDir())
|
|
321
|
+
return 'not-primary'; // a disposable-store test rig commits nothing, ever
|
|
322
|
+
if (!isPrimaryCheckout())
|
|
323
|
+
return 'not-primary';
|
|
324
|
+
return withStoreLock(() => {
|
|
325
|
+
const root = mainCheckout();
|
|
326
|
+
git(['-C', root, 'add', '--', relPath]);
|
|
327
|
+
if (!git(['-C', root, 'status', '--porcelain', '--', relPath]).trim())
|
|
328
|
+
return 'no-op';
|
|
329
|
+
git(['-C', root, 'commit', '--no-verify', '-m', message, '--', relPath]);
|
|
330
|
+
return 'committed';
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
// prepare (a FRESH read-modify or a new thread) + write + commit a single store file, all under the store
|
|
334
|
+
// lock so the read-modify-write is atomic. prepare() runs INSIDE the lock, so a reply/close reads the
|
|
335
|
+
// current thread, never a stale copy. A pre-rename store migrates first (before the lock — ensure takes it).
|
|
336
|
+
// `changed: false` = the store already held exactly this state (see writeStoreFile) — success, nothing committed.
|
|
337
|
+
function commitStore(message, prepare) {
|
|
338
|
+
ensureStoreMigrated();
|
|
339
|
+
return withStoreLock(() => {
|
|
340
|
+
const p = prepare();
|
|
341
|
+
const changed = writeStoreFile(p, message);
|
|
342
|
+
return { issue: p, changed };
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
// `author` defaults to the effective session id, but a caller (the dashboard's human write path) may pass
|
|
346
|
+
// `'human'` — the write mechanism is identical either way, only the signature differs.
|
|
347
|
+
// The thread's `nodes:` are INFERRED from the text's `[[node]]` topic links ([[mentions]] — the one in-text
|
|
348
|
+
// reference primitive every input already carries), unioned with any explicitly-passed ids (`--node`): a
|
|
349
|
+
// writer links nodes by writing them, never by re-typing ids into a separate field.
|
|
350
|
+
export function openIssue(concern, opts = {}) {
|
|
351
|
+
const nodes = [...new Set([...(opts.nodes || []), ...parseMentions(`${concern}\n${opts.body || ''}`).nodes])];
|
|
352
|
+
return commitStore(`issue: ${concern}`, () => ({
|
|
353
|
+
id: uniqueId(concern), // minted INSIDE the lock, so two racing posts can't pick the same id
|
|
354
|
+
store: 'local',
|
|
355
|
+
concern,
|
|
356
|
+
by: opts.author || currentSession(),
|
|
357
|
+
status: 'open',
|
|
358
|
+
nodes,
|
|
359
|
+
created: new Date().toISOString(),
|
|
360
|
+
body: (opts.body || `(no detail given — ${concern})`).trim(),
|
|
361
|
+
replies: [],
|
|
362
|
+
evidence: opts.evidence || [],
|
|
363
|
+
labels: [],
|
|
364
|
+
})).issue;
|
|
365
|
+
}
|
|
366
|
+
// mint a per-thread-unique remark id (retry on the rare collision within one thread). Short + readable so a
|
|
367
|
+
// `<thread-id>#<rid>` ref is typeable; stable, so a resolve/retract never lands on the wrong remark.
|
|
368
|
+
function mintRid(existing) {
|
|
369
|
+
let rid;
|
|
370
|
+
do {
|
|
371
|
+
rid = 'r' + Math.random().toString(36).slice(2, 6);
|
|
372
|
+
} while (existing.has(rid));
|
|
373
|
+
return rid;
|
|
374
|
+
}
|
|
375
|
+
// a REMARK ([[remark-substrate]]) rides the SAME committed reply write, just stamping the remark fields: a
|
|
376
|
+
// fresh unresolved bit + a minted stable rid + the codeSha it was authored against. Absent `remark` this is
|
|
377
|
+
// an ordinary reply, unchanged. Returns the thread; the new reply is its last, so a caller reads back its rid.
|
|
378
|
+
export function reply(id, body, author, evidence, remark) {
|
|
379
|
+
const by = author || currentSession();
|
|
380
|
+
return commitStore(remark ? `remark(${id}): by ${by}` : `issue(${id}): reply by ${by}`, () => {
|
|
381
|
+
const p = loadOne(id); // fresh read under the lock → no lost-update when replies race
|
|
382
|
+
const post = { by, at: new Date().toISOString(), body: body.trim() };
|
|
383
|
+
if (remark) {
|
|
384
|
+
post.rid = mintRid(new Set(p.replies.map((r) => r.rid).filter((x) => !!x)));
|
|
385
|
+
post.targetCodeSha = remark.targetCodeSha;
|
|
386
|
+
post.resolved = false;
|
|
387
|
+
}
|
|
388
|
+
p.replies.push(post);
|
|
389
|
+
// an anchored annotation carries its frame blob: the reply's evidence hashes accrue onto the THREAD's
|
|
390
|
+
// typed evidence[] (deduped), so the thread stays the one place a video finding's blobs are indexed.
|
|
391
|
+
if (evidence?.length)
|
|
392
|
+
p.evidence = [...new Set([...p.evidence, ...evidence])];
|
|
393
|
+
return p;
|
|
394
|
+
}).issue;
|
|
395
|
+
}
|
|
396
|
+
// @@@ the PROGRAMMATIC store write surface — the dashboard's human write path calls these (author `'human'`).
|
|
397
|
+
// The store is git-native data, so a human's write goes through the SAME open/reply the CLI uses (committed
|
|
398
|
+
// straight to the trunk). @session remains text in that committed discussion; only @new dispatches after it.
|
|
399
|
+
// Originator courtesy is composed above this store because it needs eval-aware candidate resolution.
|
|
400
|
+
export async function replyLocalIssue(id, body, author, evidence, remark) {
|
|
401
|
+
const thread = reply(id, body, author, evidence, remark);
|
|
402
|
+
return {
|
|
403
|
+
thread,
|
|
404
|
+
outcomes: await dispatchNewMentions(body, { threadId: id, node: thread.nodes[0] || null, author, status: thread.status }),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
export async function postLocalIssue(concern, opts) {
|
|
408
|
+
const thread = openIssue(concern, { nodes: opts.nodes, body: opts.body, evidence: opts.evidence, author: opts.author });
|
|
409
|
+
return {
|
|
410
|
+
thread,
|
|
411
|
+
outcomes: await dispatchNewMentions(opts.body || concern, {
|
|
412
|
+
threadId: thread.id,
|
|
413
|
+
node: thread.nodes[0] || null,
|
|
414
|
+
author: opts.author,
|
|
415
|
+
status: thread.status,
|
|
416
|
+
}),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
export function closeLocalIssue(id) {
|
|
420
|
+
const { changed } = commitStore(`issue(${id}): close`, () => {
|
|
421
|
+
const p = loadOne(id);
|
|
422
|
+
p.status = 'landed';
|
|
423
|
+
return p;
|
|
424
|
+
});
|
|
425
|
+
return { status: 'landed', already: !changed };
|
|
426
|
+
}
|
|
427
|
+
// ── remarks ([[remark-substrate]]) ──────────────────────────────────────────────────────────────────
|
|
428
|
+
// A remark is a reply carrying a resolvable bit, attached to a HOST: a local issue, or a scenario keyed by
|
|
429
|
+
// (node, scenario). The scenario track is NOT a new store — it is the annotator's lazy eval thread, keyed by
|
|
430
|
+
// its `eval: <node> · <scenario>` concern; a remark reuses it, creating it on first remark as a stub
|
|
431
|
+
// container (every remark is a reply, never the thread body, so the resolved bit always lives in one place).
|
|
432
|
+
// @@@ the eval-concern format, once - composer and parser side by side. The parser existed in two copies while
|
|
433
|
+
// a CLI surface down here had to recognise the format without being able to import the module that composes it;
|
|
434
|
+
// [[issues-cli]] removed that constraint, so the format is one pair now. `node` is matched non-greedily because
|
|
435
|
+
// it can never contain ' · ' while a scenario name may.
|
|
436
|
+
const evalConcernKey = (node, scenario) => `eval: ${node} · ${scenario}`;
|
|
437
|
+
const EVAL_CONCERN_RE = /^eval: (.+?) · (.+)$/;
|
|
438
|
+
export const parseEvalConcern = (concern) => {
|
|
439
|
+
const m = EVAL_CONCERN_RE.exec(concern);
|
|
440
|
+
return m ? { node: m[1].trim(), scenario: m[2].trim() } : null;
|
|
441
|
+
};
|
|
442
|
+
// find-or-create the ONE scenario thread for (node, scenario), keyed by its eval concern, ATOMICALLY under
|
|
443
|
+
// the store lock. R4 says a scenario's remark track lives ONCE — but a concurrent first-remark burst is a
|
|
444
|
+
// normal dogfood situation (SpexCode runs parallel workers), and if the not-found read sat OUTSIDE the lock
|
|
445
|
+
// two racers could both read "absent" and both create, minting a second thread whose remarks are invisible
|
|
446
|
+
// to the concern key (a silent teeth blind spot). Holding one lock across both the find AND the create closes
|
|
447
|
+
// that window: a racer either sees the thread the first created, or is the first. The stub is a pure
|
|
448
|
+
// container (its body carries a [[wiki-link]], never an @-reference), so it needs no async notification — a
|
|
449
|
+
// synchronous create suffices, and staying sync is exactly what lets it share the lock hold.
|
|
450
|
+
function findOrCreateEvalThread(node, scenario, author) {
|
|
451
|
+
ensureStoreMigrated(); // migrate before the lock (ensure takes it itself; never nest a store-lock hold)
|
|
452
|
+
const concern = evalConcernKey(node, scenario);
|
|
453
|
+
return withStoreLock(() => {
|
|
454
|
+
const existing = loadLocalIssues().find((t) => t.store === 'local' && t.concern === concern);
|
|
455
|
+
if (existing)
|
|
456
|
+
return existing;
|
|
457
|
+
const p = {
|
|
458
|
+
id: uniqueId(concern), store: 'local', concern, by: author, status: 'open',
|
|
459
|
+
nodes: [node], created: new Date().toISOString(),
|
|
460
|
+
body: `Remarks on the \`${scenario}\` eval of [[${node}]].`, replies: [], evidence: [], labels: [],
|
|
461
|
+
};
|
|
462
|
+
writeStoreFile(p, `issue: ${concern}`);
|
|
463
|
+
return p;
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
function resolveRemarkHost(host, author) {
|
|
467
|
+
if (host.scenario) {
|
|
468
|
+
const node = host.node;
|
|
469
|
+
if (!node)
|
|
470
|
+
throw new Error('a scenario remark needs a node plus --scenario <name>');
|
|
471
|
+
return findOrCreateEvalThread(node, host.scenario, author).id;
|
|
472
|
+
}
|
|
473
|
+
if (!host.issue)
|
|
474
|
+
throw new Error('a remark needs a host: an issue id, or a node with --scenario <name>');
|
|
475
|
+
return loadOne(host.issue).id; // throws loudly if the issue doesn't exist
|
|
476
|
+
}
|
|
477
|
+
// author a remark on a host — the ONE write both the CLI (`spex remark add`) and the server call. Stamps the
|
|
478
|
+
// codeSha it was authored against (the worktree HEAD by default — R2). Returns the `<thread-id>#<rid>` ref.
|
|
479
|
+
export async function remarkOnHost(host, body, opts = {}) {
|
|
480
|
+
const author = opts.author || currentSession();
|
|
481
|
+
const codeSha = opts.codeSha || headSha(repoRoot());
|
|
482
|
+
const id = resolveRemarkHost(host, author);
|
|
483
|
+
const { thread, outcomes } = await replyLocalIssue(id, body, author, opts.evidence, { targetCodeSha: codeSha });
|
|
484
|
+
const rid = thread.replies[thread.replies.length - 1].rid;
|
|
485
|
+
return { ref: `${id}#${rid}`, rid, codeSha, thread, author, outcomes };
|
|
486
|
+
}
|
|
487
|
+
// a remark ref is `<thread-id>#<rid>`; the thread id (a store slug) never contains '#', so split on the last.
|
|
488
|
+
function parseRemarkRef(ref) {
|
|
489
|
+
const i = ref.lastIndexOf('#');
|
|
490
|
+
if (i <= 0 || i === ref.length - 1)
|
|
491
|
+
throw new Error(`bad remark ref '${ref}' — expected <thread-id>#<rid> (the id \`spex remark add\` printed)`);
|
|
492
|
+
return { id: ref.slice(0, i), rid: ref.slice(i + 1) };
|
|
493
|
+
}
|
|
494
|
+
// resolve a remark (R3): a DELIBERATE call by a SECOND PARTY — never the author (no self-resolve: an
|
|
495
|
+
// identity comparison, so the dashboard's `human` cannot resolve a human-authored remark either), and
|
|
496
|
+
// MONOTONIC (no un-resolve — a regression is a NEW remark). The resolver's identity is derived by the
|
|
497
|
+
// SURFACE ([[remark-substrate]] LAW L): a governed session id from the CLI, the `human` sentinel from the
|
|
498
|
+
// dashboard — both are real second parties, so the same rule runs on both. `by` is the resolving party.
|
|
499
|
+
export function resolveRemark(ref, by) {
|
|
500
|
+
const { id, rid } = parseRemarkRef(ref);
|
|
501
|
+
const { issue: thread } = commitStore(`remark(${id}#${rid}): resolved by ${by}`, () => {
|
|
502
|
+
const p = loadOne(id);
|
|
503
|
+
const r = p.replies.find((x) => x.rid === rid);
|
|
504
|
+
if (!r)
|
|
505
|
+
throw new Error(`no remark '${ref}' in that thread`);
|
|
506
|
+
if (!by || by === 'unknown')
|
|
507
|
+
throw new Error(`resolve needs a real identity (got '${by || 'none'}'): run it under a governed session, or from the dashboard (its actor is 'human')`);
|
|
508
|
+
if (r.resolved)
|
|
509
|
+
throw new Error(`remark '${ref}' is already resolved — monotonic: a regression is a NEW remark, never an un-resolve`);
|
|
510
|
+
if (r.by === by)
|
|
511
|
+
throw new Error(`refusing to self-resolve '${ref}': the author (${by}) may not resolve their own remark — resolve is a second party's deliberate judgment`);
|
|
512
|
+
r.resolved = true;
|
|
513
|
+
r.resolvedBy = by;
|
|
514
|
+
r.resolvedAt = new Date().toISOString();
|
|
515
|
+
return p;
|
|
516
|
+
});
|
|
517
|
+
return { thread, rid };
|
|
518
|
+
}
|
|
519
|
+
// retract a remark (R3): the AUTHOR withdraws their OWN remark, removing it — but ONLY while it is
|
|
520
|
+
// unresolved. Only the author may retract; and once a SECOND party has deliberately resolved it, the remark
|
|
521
|
+
// (and that recorded judgment) is part of the record — retract may not erase it. This makes R3's
|
|
522
|
+
// monotonicity two-sided: resolve can't be undone, and retract can't back-door an un-resolve by deleting the
|
|
523
|
+
// resolved remark. A regression after a resolve is a NEW remark, never a retract-and-reraise.
|
|
524
|
+
export function retractRemark(ref, by) {
|
|
525
|
+
const { id, rid } = parseRemarkRef(ref);
|
|
526
|
+
const { issue: thread } = commitStore(`remark(${id}#${rid}): retracted by ${by}`, () => {
|
|
527
|
+
const p = loadOne(id);
|
|
528
|
+
const idx = p.replies.findIndex((x) => x.rid === rid);
|
|
529
|
+
if (idx < 0)
|
|
530
|
+
throw new Error(`no remark '${ref}' in that thread`);
|
|
531
|
+
if (p.replies[idx].by !== by)
|
|
532
|
+
throw new Error(`only the author (${p.replies[idx].by}) may retract '${ref}' — you are '${by}'`);
|
|
533
|
+
if (p.replies[idx].resolved)
|
|
534
|
+
throw new Error(`refusing to retract '${ref}': it was resolved by ${p.replies[idx].resolvedBy} — a resolved remark is part of the record (monotonic), retract only withdraws an UNRESOLVED remark; a regression is a NEW remark`);
|
|
535
|
+
p.replies.splice(idx, 1);
|
|
536
|
+
return p;
|
|
537
|
+
});
|
|
538
|
+
return { thread, rid };
|
|
539
|
+
}
|
|
540
|
+
// the post-merge nudge TEXT ([[local-issues]]) — produced HERE so the toggle and the wording live in one
|
|
541
|
+
// place; the post-merge git hook is a thin caller that just echoes this. Returns '' when the feature is OFF,
|
|
542
|
+
// so the hook prints nothing.
|
|
543
|
+
export function nudge(node) {
|
|
544
|
+
if (!issuesEnabled())
|
|
545
|
+
return '';
|
|
546
|
+
return [
|
|
547
|
+
'── issues ─────────────────────────────────────────────────────────',
|
|
548
|
+
`Your work (${node || 'this node'}) just landed. Two issue checks before you close:`,
|
|
549
|
+
'',
|
|
550
|
+
'1. CLOSE what you finished. An issue whose work just landed is closed, not',
|
|
551
|
+
' left open — the open set is the OUTSTANDING work, so a stale open reads as a',
|
|
552
|
+
' lie: spex issue ls --store local then spex issue close <id>',
|
|
553
|
+
'',
|
|
554
|
+
'2. RECORD only what OUTLIVES this task — a concern you are NOT acting on now:',
|
|
555
|
+
' an off-mainline smell / awkward boundary / wish, or a trivial-but-must-not-',
|
|
556
|
+
" forget to-do that doesn't earn a spec node. NOT a bug tracker (that is the",
|
|
557
|
+
' spec graph + the forge), NOT your assigned task or a fix you are about to',
|
|
558
|
+
' make — those need no issue. Only the taste that would otherwise evaporate:',
|
|
559
|
+
' spex issue ls # read first — reply if already raised',
|
|
560
|
+
' spex issue open "<concern>" [--node <id>] # else open one',
|
|
561
|
+
'A supervisor drains the store later. (Advisory — skip if nothing is owed.)',
|
|
562
|
+
'───────────────────────────────────────────────────────────────────',
|
|
563
|
+
].join('\n');
|
|
564
|
+
}
|
|
565
|
+
// @@@ close-time issue closeout ([[local-issues]] / [[state]]) — the DATA half of the propose-close nudge:
|
|
566
|
+
// appended to the `done --propose close` declaration BESIDE the resource-cleanup reminder (cli.ts, same
|
|
567
|
+
// insertion point, same semantics — a nudge, never a gate). Data-driven so it earns its line: it lists the
|
|
568
|
+
// still-open local threads THIS session touched (authored or replied; eval `eval: <node> · <scenario>`
|
|
569
|
+
// containers excluded — they host remarks and outlive every session by design) and prints NOTHING when the
|
|
570
|
+
// session owes nothing, when the feature is OFF, or when there is no session identity. Some issues rightly
|
|
571
|
+
// outlive their session (a taste concern waiting for the drain), so the ask is close OR say why it stays
|
|
572
|
+
// open — never a forced close.
|
|
573
|
+
export function closeoutNudge(sessionId) {
|
|
574
|
+
if (!sessionId || sessionId === 'unknown' || !issuesEnabled())
|
|
575
|
+
return '';
|
|
576
|
+
const mine = loadLocalIssues().filter((t) => t.status === 'open' && !parseEvalConcern(t.concern) &&
|
|
577
|
+
(t.by === sessionId || t.replies.some((r) => r.by === sessionId)));
|
|
578
|
+
if (!mine.length)
|
|
579
|
+
return '';
|
|
580
|
+
return `\n\nIssue closeout — ${mine.length} still-open local issue(s) you touched (opened or replied): ${mine.map((t) => t.id).join(', ')}. For each, close it now if its work is finished (\`spex issue close <id>\`), or reply why it should stay open past this session (\`spex issue reply <id> --body "<why>"\`). Some issues rightly outlive their session — this is a reminder to sweep, not a gate.`;
|
|
581
|
+
}
|
|
582
|
+
// ───────────────────────── CLI ─────────────────────────
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
// One page serves every gate: the single-project gateway's /login (the defaults) and the multi-project
|
|
6
6
|
// hub's admin + per-project logins ([[gateway-hub]] passes action/heading/sub). Every option is
|
|
7
7
|
// HTML-escaped at this sink — projectIds are path-derived, attacker-influencable strings.
|
|
8
|
-
export function loginPage(error = false, opts
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
export function loginPage(error = false, opts = {}) {
|
|
9
|
+
const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
10
|
+
const action = esc(opts.action ?? '/login');
|
|
11
|
+
const heading = esc(opts.heading ?? 'Restricted access');
|
|
12
|
+
const sub = esc(opts.sub ?? 'This is a private agent workspace. Enter the access password to continue.');
|
|
13
|
+
return `<!doctype html>
|
|
14
14
|
<html lang="en">
|
|
15
15
|
<head>
|
|
16
16
|
<meta charset="utf-8">
|
|
@@ -82,5 +82,5 @@ export function loginPage(error = false, opts: { action?: string; heading?: stri
|
|
|
82
82
|
<div class="foot">Trusted collaborators only.</div>
|
|
83
83
|
</form>
|
|
84
84
|
</body>
|
|
85
|
-
</html
|
|
85
|
+
</html>`;
|
|
86
86
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type LoopIn } from './mentions.js';
|
|
2
|
+
import { replyIssue } from './issues.js';
|
|
3
|
+
import { remarkOnHost } from './localIssues.js';
|
|
4
|
+
/** `issue reply` for every store, with the originator loop-in composed on top. */
|
|
5
|
+
export declare function replyIssueWithLoopIn(id: string, body: string, opts?: {
|
|
6
|
+
author?: string;
|
|
7
|
+
node?: string | null;
|
|
8
|
+
evidence?: string[];
|
|
9
|
+
}): Promise<Awaited<ReturnType<typeof replyIssue>> & {
|
|
10
|
+
loopIn: LoopIn | null;
|
|
11
|
+
}>;
|
|
12
|
+
/** `remark add` on a host, with the same loop-in composed by the same code. */
|
|
13
|
+
export declare function remarkWithLoopIn(host: {
|
|
14
|
+
issue?: string;
|
|
15
|
+
node?: string;
|
|
16
|
+
scenario?: string;
|
|
17
|
+
}, body: string, opts?: {
|
|
18
|
+
codeSha?: string;
|
|
19
|
+
author?: string;
|
|
20
|
+
evidence?: string[];
|
|
21
|
+
}): Promise<Awaited<ReturnType<typeof remarkOnHost>> & {
|
|
22
|
+
loopIn: LoopIn | null;
|
|
23
|
+
}>;
|