gsd-pi 2.51.0-dev.ae8f7cb → 2.52.0-dev.585e355
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 +4 -4
- package/dist/headless-events.d.ts +18 -0
- package/dist/headless-events.js +36 -0
- package/dist/headless-types.d.ts +28 -0
- package/dist/headless-types.js +7 -0
- package/dist/headless.d.ts +8 -3
- package/dist/headless.js +47 -16
- package/dist/help-text.js +16 -5
- package/dist/onboarding.js +5 -4
- package/dist/remote-questions-config.js +1 -1
- package/dist/resources/extensions/async-jobs/async-bash-tool.js +29 -17
- package/dist/resources/extensions/async-jobs/job-manager.js +4 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +18 -19
- package/dist/resources/extensions/gsd/auto/phases.js +6 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +18 -0
- package/dist/resources/extensions/gsd/auto-start.js +2 -0
- package/dist/resources/extensions/gsd/auto-timers.js +24 -2
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +25 -7
- package/dist/resources/extensions/gsd/auto-worktree.js +21 -0
- package/dist/resources/extensions/gsd/auto.js +8 -4
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +105 -70
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +12 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +1 -1
- package/dist/resources/extensions/gsd/claude-import.js +60 -9
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +69 -6
- package/dist/resources/extensions/gsd/commands-config.js +10 -5
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +4 -4
- package/dist/resources/extensions/gsd/detection.js +6 -6
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +5 -5
- package/dist/resources/extensions/gsd/error-classifier.js +105 -0
- package/dist/resources/extensions/gsd/git-service.js +4 -3
- package/dist/resources/extensions/gsd/gitignore.js +7 -7
- package/dist/resources/extensions/gsd/gsd-db.js +298 -45
- package/dist/resources/extensions/gsd/init-wizard.js +2 -2
- package/dist/resources/extensions/gsd/key-manager.js +7 -16
- package/dist/resources/extensions/gsd/markdown-renderer.js +5 -4
- package/dist/resources/extensions/gsd/memory-store.js +28 -13
- package/dist/resources/extensions/gsd/milestone-actions.js +19 -0
- package/dist/resources/extensions/gsd/preferences-models.js +1 -13
- package/dist/resources/extensions/gsd/preferences-types.js +1 -1
- package/dist/resources/extensions/gsd/preferences.js +13 -13
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/provider-error-pause.js +0 -44
- package/dist/resources/extensions/gsd/rule-registry.js +1 -1
- package/dist/resources/extensions/gsd/service-tier.js +13 -2
- package/dist/resources/extensions/gsd/state.js +33 -19
- package/dist/resources/extensions/gsd/status-guards.js +12 -0
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +7 -13
- package/dist/resources/extensions/gsd/tools/complete-slice.js +7 -20
- package/dist/resources/extensions/gsd/tools/complete-task.js +11 -21
- package/dist/resources/extensions/gsd/tools/plan-milestone.js +28 -29
- package/dist/resources/extensions/gsd/tools/plan-slice.js +27 -26
- package/dist/resources/extensions/gsd/tools/plan-task.js +23 -23
- package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +50 -41
- package/dist/resources/extensions/gsd/tools/reopen-slice.js +4 -3
- package/dist/resources/extensions/gsd/tools/reopen-task.js +5 -4
- package/dist/resources/extensions/gsd/tools/replan-slice.js +51 -41
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +23 -16
- package/dist/resources/extensions/gsd/validation.js +21 -0
- package/dist/resources/extensions/gsd/workflow-logger.js +0 -1
- package/dist/resources/extensions/remote-questions/config.js +1 -1
- package/dist/resources/extensions/remote-questions/remote-command.js +1 -1
- package/dist/resources/extensions/search-the-web/native-search.js +1 -1
- package/dist/resources/extensions/search-the-web/provider.js +1 -1
- package/dist/resources/extensions/shared/rtk.js +5 -3
- package/dist/rtk.js +3 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +2 -2
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
- package/dist/web/standalone/.next/server/chunks/2229.js +2 -2
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/4024.21054f459af5cc78.js +9 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-cfc9a116e6450a6b.js → webpack-024d82be84800e52.js} +1 -1
- package/dist/web/standalone/.next/static/css/a58ef8a151aa0493.css +1 -0
- package/dist/wizard.js +4 -1
- package/package.json +2 -2
- package/packages/mcp-server/README.md +202 -0
- package/packages/mcp-server/package.json +36 -0
- package/packages/mcp-server/src/cli.ts +68 -0
- package/packages/mcp-server/src/index.ts +14 -0
- package/packages/mcp-server/src/mcp-server.test.ts +628 -0
- package/packages/mcp-server/src/server.ts +278 -0
- package/packages/mcp-server/src/session-manager.ts +328 -0
- package/packages/mcp-server/src/types.ts +107 -0
- package/packages/mcp-server/tsconfig.json +24 -0
- package/packages/pi-ai/dist/models.d.ts +14 -3
- package/packages/pi-ai/dist/models.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.js +53 -10
- package/packages/pi-ai/dist/models.js.map +1 -1
- package/packages/pi-ai/dist/models.test.js +102 -1
- package/packages/pi-ai/dist/models.test.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +30 -0
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/src/models.test.ts +114 -1
- package/packages/pi-ai/src/models.ts +70 -13
- package/packages/pi-ai/src/types.ts +31 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts +2 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +3 -0
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/bash-executor.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/bash-executor.js +5 -1
- package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +9 -4
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.d.ts +19 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.js +83 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +5 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +5 -3
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +0 -2
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts +28 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js +49 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +114 -6
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.d.ts +9 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.js +831 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +66 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
- package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/utils/shell.js +0 -1
- package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +4 -0
- package/packages/pi-coding-agent/src/core/bash-executor.ts +5 -1
- package/packages/pi-coding-agent/src/core/model-registry.ts +10 -3
- package/packages/pi-coding-agent/src/core/tools/bash-spawn-windows.test.ts +101 -0
- package/packages/pi-coding-agent/src/core/tools/bash.ts +5 -1
- package/packages/pi-coding-agent/src/index.ts +3 -0
- package/packages/pi-coding-agent/src/main.ts +5 -3
- package/packages/pi-coding-agent/src/modes/index.ts +8 -1
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +0 -2
- package/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts +54 -1
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +124 -6
- package/packages/pi-coding-agent/src/modes/rpc/rpc-protocol-v2.test.ts +971 -0
- package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +61 -4
- package/packages/pi-coding-agent/src/utils/shell.ts +0 -1
- package/packages/rpc-client/package.json +20 -0
- package/pkg/package.json +1 -1
- package/scripts/ensure-workspace-builds.cjs +36 -8
- package/src/resources/extensions/async-jobs/async-bash-tool.ts +22 -11
- package/src/resources/extensions/async-jobs/job-manager.ts +4 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +19 -20
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +21 -0
- package/src/resources/extensions/gsd/auto/phases.ts +6 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +19 -0
- package/src/resources/extensions/gsd/auto-start.ts +2 -0
- package/src/resources/extensions/gsd/auto-timers.ts +25 -1
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +30 -6
- package/src/resources/extensions/gsd/auto-worktree.ts +21 -0
- package/src/resources/extensions/gsd/auto.ts +10 -4
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +125 -73
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +11 -2
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +1 -1
- package/src/resources/extensions/gsd/claude-import.ts +58 -9
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +73 -6
- package/src/resources/extensions/gsd/commands-config.ts +11 -5
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +4 -4
- package/src/resources/extensions/gsd/detection.ts +6 -6
- package/src/resources/extensions/gsd/docs/preferences-reference.md +5 -5
- package/src/resources/extensions/gsd/error-classifier.ts +139 -0
- package/src/resources/extensions/gsd/git-service.ts +4 -3
- package/src/resources/extensions/gsd/gitignore.ts +7 -7
- package/src/resources/extensions/gsd/gsd-db.ts +355 -63
- package/src/resources/extensions/gsd/init-wizard.ts +2 -2
- package/src/resources/extensions/gsd/key-manager.ts +7 -16
- package/src/resources/extensions/gsd/markdown-renderer.ts +5 -4
- package/src/resources/extensions/gsd/memory-store.ts +29 -18
- package/src/resources/extensions/gsd/milestone-actions.ts +17 -0
- package/src/resources/extensions/gsd/preferences-models.ts +1 -13
- package/src/resources/extensions/gsd/preferences-types.ts +1 -1
- package/src/resources/extensions/gsd/preferences.ts +12 -13
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/provider-error-pause.ts +0 -57
- package/src/resources/extensions/gsd/rule-registry.ts +1 -1
- package/src/resources/extensions/gsd/service-tier.ts +14 -2
- package/src/resources/extensions/gsd/state.ts +34 -20
- package/src/resources/extensions/gsd/status-guards.ts +13 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-milestone-target.test.ts +61 -0
- package/src/resources/extensions/gsd/tests/claude-import-marketplace-discovery.test.ts +191 -0
- package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/commands-config.test.ts +24 -0
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +35 -7
- package/src/resources/extensions/gsd/tests/detection.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/doctor-git.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/empty-db-reconciliation.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +37 -4
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/idle-watchdog-stall-override.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/init-wizard.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/interactive-tool-idle-exemption.test.ts +119 -0
- package/src/resources/extensions/gsd/tests/key-manager.test.ts +16 -1
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +7 -7
- package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +91 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +77 -70
- package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +29 -0
- package/src/resources/extensions/gsd/tests/status-guards.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +42 -31
- package/src/resources/extensions/gsd/tests/token-cost-display.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/vacuous-truth-slices.test.ts +115 -0
- package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +90 -0
- package/src/resources/extensions/gsd/tests/validation.test.ts +72 -0
- package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +81 -1
- package/src/resources/extensions/gsd/tests/worktree-preferences-sync.test.ts +130 -0
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +7 -17
- package/src/resources/extensions/gsd/tools/complete-slice.ts +7 -24
- package/src/resources/extensions/gsd/tools/complete-task.ts +13 -25
- package/src/resources/extensions/gsd/tools/plan-milestone.ts +30 -32
- package/src/resources/extensions/gsd/tools/plan-slice.ts +30 -30
- package/src/resources/extensions/gsd/tools/plan-task.ts +26 -26
- package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +57 -46
- package/src/resources/extensions/gsd/tools/reopen-slice.ts +4 -3
- package/src/resources/extensions/gsd/tools/reopen-task.ts +5 -4
- package/src/resources/extensions/gsd/tools/replan-slice.ts +55 -44
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +26 -20
- package/src/resources/extensions/gsd/validation.ts +23 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +0 -1
- package/src/resources/extensions/remote-questions/config.ts +1 -1
- package/src/resources/extensions/remote-questions/remote-command.ts +1 -1
- package/src/resources/extensions/search-the-web/native-search.ts +1 -1
- package/src/resources/extensions/search-the-web/provider.ts +1 -1
- package/src/resources/extensions/shared/rtk.ts +12 -3
- package/dist/web/standalone/.next/static/chunks/4024.9ad5def014d90ce4.js +0 -9
- package/dist/web/standalone/.next/static/css/de141508b083f922.css +0 -1
- /package/dist/resources/extensions/gsd/templates/{preferences.md → PREFERENCES.md} +0 -0
- /package/dist/web/standalone/.next/static/{5_KeZz1X0tXJK-d_4OhjB → KTe1kB5nPLQFIIFz2OcmI}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{5_KeZz1X0tXJK-d_4OhjB → KTe1kB5nPLQFIIFz2OcmI}/_ssgManifest.js +0 -0
- /package/src/resources/extensions/gsd/templates/{preferences.md → PREFERENCES.md} +0 -0
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { mkdirSync } from "node:fs";
|
|
11
|
-
import {
|
|
11
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
12
|
+
import { transaction, insertMilestone, insertSlice, insertTask, insertVerificationEvidence, getMilestone, getSlice, getTask, updateTaskStatus, setTaskSummaryMd, deleteVerificationEvidence, } from "../gsd-db.js";
|
|
12
13
|
import { resolveSliceFile, resolveTasksDir, clearPathCache } from "../paths.js";
|
|
13
14
|
import { checkOwnership, taskUnitKey } from "../unit-ownership.js";
|
|
14
15
|
import { saveFile, clearParseCache } from "../files.js";
|
|
@@ -122,17 +123,17 @@ export async function handleCompleteTask(params, basePath) {
|
|
|
122
123
|
// Milestone/slice not existing is OK — insertMilestone/insertSlice below will auto-create.
|
|
123
124
|
// Only block if they exist and are closed.
|
|
124
125
|
const milestone = getMilestone(params.milestoneId);
|
|
125
|
-
if (milestone && (milestone.status
|
|
126
|
+
if (milestone && isClosedStatus(milestone.status)) {
|
|
126
127
|
guardError = `cannot complete task in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
127
128
|
return;
|
|
128
129
|
}
|
|
129
130
|
const slice = getSlice(params.milestoneId, params.sliceId);
|
|
130
|
-
if (slice && (slice.status
|
|
131
|
+
if (slice && isClosedStatus(slice.status)) {
|
|
131
132
|
guardError = `cannot complete task in a closed slice: ${params.sliceId} (status: ${slice.status})`;
|
|
132
133
|
return;
|
|
133
134
|
}
|
|
134
135
|
const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
|
|
135
|
-
if (existingTask && (existingTask.status
|
|
136
|
+
if (existingTask && isClosedStatus(existingTask.status)) {
|
|
136
137
|
guardError = `task ${params.taskId} is already complete — use gsd_task_reopen first if you need to redo it`;
|
|
137
138
|
return;
|
|
138
139
|
}
|
|
@@ -202,27 +203,16 @@ export async function handleCompleteTask(params, basePath) {
|
|
|
202
203
|
catch (renderErr) {
|
|
203
204
|
// Disk render failed — roll back DB status so state stays consistent
|
|
204
205
|
process.stderr.write(`gsd-db: complete_task — disk render failed, rolling back DB status: ${renderErr.message}\n`);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
":tid": params.taskId,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
206
|
+
// Delete orphaned verification_evidence rows first (FK constraint
|
|
207
|
+
// references tasks, so evidence must go before status change).
|
|
208
|
+
// Without this, retries accumulate duplicate evidence rows (#2724).
|
|
209
|
+
deleteVerificationEvidence(params.milestoneId, params.sliceId, params.taskId);
|
|
210
|
+
updateTaskStatus(params.milestoneId, params.sliceId, params.taskId, 'pending');
|
|
213
211
|
invalidateStateCache();
|
|
214
212
|
return { error: `disk render failed: ${renderErr.message}` };
|
|
215
213
|
}
|
|
216
214
|
// Store rendered markdown in DB for D004 recovery
|
|
217
|
-
|
|
218
|
-
if (adapter) {
|
|
219
|
-
adapter.prepare(`UPDATE tasks SET full_summary_md = :md WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`).run({
|
|
220
|
-
":md": summaryMd,
|
|
221
|
-
":mid": params.milestoneId,
|
|
222
|
-
":sid": params.sliceId,
|
|
223
|
-
":tid": params.taskId,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
215
|
+
setTaskSummaryMd(params.milestoneId, params.sliceId, params.taskId, summaryMd);
|
|
226
216
|
// Invalidate all caches
|
|
227
217
|
invalidateStateCache();
|
|
228
218
|
clearPathCache();
|
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import { clearParseCache } from "../files.js";
|
|
2
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
3
|
+
import { isNonEmptyString, validateStringArray } from "../validation.js";
|
|
2
4
|
import { transaction, getMilestone, insertMilestone, insertSlice, upsertMilestonePlanning, upsertSlicePlanning, } from "../gsd-db.js";
|
|
3
5
|
import { invalidateStateCache } from "../state.js";
|
|
4
6
|
import { renderRoadmapFromDb } from "../markdown-renderer.js";
|
|
5
7
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
6
8
|
import { writeManifest } from "../workflow-manifest.js";
|
|
7
9
|
import { appendEvent } from "../workflow-events.js";
|
|
8
|
-
function isNonEmptyString(value) {
|
|
9
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
10
|
-
}
|
|
11
|
-
function validateStringArray(value, field) {
|
|
12
|
-
if (!Array.isArray(value)) {
|
|
13
|
-
throw new Error(`${field} must be an array`);
|
|
14
|
-
}
|
|
15
|
-
if (value.some((item) => !isNonEmptyString(item))) {
|
|
16
|
-
throw new Error(`${field} must contain only non-empty strings`);
|
|
17
|
-
}
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
10
|
function validateRiskEntries(value) {
|
|
21
11
|
if (!Array.isArray(value)) {
|
|
22
12
|
throw new Error("keyRisks must be an array");
|
|
@@ -145,25 +135,31 @@ export async function handlePlanMilestone(rawParams, basePath) {
|
|
|
145
135
|
catch (err) {
|
|
146
136
|
return { error: `validation failed: ${err.message}` };
|
|
147
137
|
}
|
|
148
|
-
// ──
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
// Validate depends_on: all dependencies must exist and be complete
|
|
154
|
-
if (params.dependsOn && params.dependsOn.length > 0) {
|
|
155
|
-
for (const depId of params.dependsOn) {
|
|
156
|
-
const dep = getMilestone(depId);
|
|
157
|
-
if (!dep) {
|
|
158
|
-
return { error: `depends_on references unknown milestone: ${depId}` };
|
|
159
|
-
}
|
|
160
|
-
if (dep.status !== "complete" && dep.status !== "done") {
|
|
161
|
-
return { error: `depends_on milestone ${depId} is not yet complete (status: ${dep.status})` };
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
138
|
+
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
|
|
139
|
+
// Guards must be inside the transaction so the state they check cannot
|
|
140
|
+
// change between the read and the write (#2723).
|
|
141
|
+
let guardError = null;
|
|
165
142
|
try {
|
|
166
143
|
transaction(() => {
|
|
144
|
+
const existingMilestone = getMilestone(params.milestoneId);
|
|
145
|
+
if (existingMilestone && isClosedStatus(existingMilestone.status)) {
|
|
146
|
+
guardError = `cannot re-plan milestone ${params.milestoneId}: it is already complete`;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Validate depends_on: all dependencies must exist and be complete
|
|
150
|
+
if (params.dependsOn && params.dependsOn.length > 0) {
|
|
151
|
+
for (const depId of params.dependsOn) {
|
|
152
|
+
const dep = getMilestone(depId);
|
|
153
|
+
if (!dep) {
|
|
154
|
+
guardError = `depends_on references unknown milestone: ${depId}`;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!isClosedStatus(dep.status)) {
|
|
158
|
+
guardError = `depends_on milestone ${depId} is not yet complete (status: ${dep.status})`;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
167
163
|
insertMilestone({
|
|
168
164
|
id: params.milestoneId,
|
|
169
165
|
title: params.title,
|
|
@@ -206,6 +202,9 @@ export async function handlePlanMilestone(rawParams, basePath) {
|
|
|
206
202
|
catch (err) {
|
|
207
203
|
return { error: `db write failed: ${err.message}` };
|
|
208
204
|
}
|
|
205
|
+
if (guardError) {
|
|
206
|
+
return { error: guardError };
|
|
207
|
+
}
|
|
209
208
|
let roadmapPath;
|
|
210
209
|
try {
|
|
211
210
|
const renderResult = await renderRoadmapFromDb(basePath, params.milestoneId);
|
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import { clearParseCache } from "../files.js";
|
|
2
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
3
|
+
import { isNonEmptyString } from "../validation.js";
|
|
2
4
|
import { transaction, getMilestone, getSlice, insertTask, upsertSlicePlanning, upsertTaskPlanning, insertGateRow, } from "../gsd-db.js";
|
|
3
5
|
import { invalidateStateCache } from "../state.js";
|
|
4
6
|
import { renderPlanFromDb } from "../markdown-renderer.js";
|
|
5
7
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
6
8
|
import { writeManifest } from "../workflow-manifest.js";
|
|
7
9
|
import { appendEvent } from "../workflow-events.js";
|
|
8
|
-
function isNonEmptyString(value) {
|
|
9
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
10
|
-
}
|
|
11
|
-
function validateStringArray(value, field) {
|
|
12
|
-
if (!Array.isArray(value)) {
|
|
13
|
-
throw new Error(`${field} must be an array`);
|
|
14
|
-
}
|
|
15
|
-
if (value.some((item) => !isNonEmptyString(item))) {
|
|
16
|
-
throw new Error(`${field} must contain only non-empty strings`);
|
|
17
|
-
}
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
10
|
function validateTasks(value) {
|
|
21
11
|
if (!Array.isArray(value) || value.length === 0) {
|
|
22
12
|
throw new Error("tasks must be a non-empty array");
|
|
@@ -102,22 +92,30 @@ export async function handlePlanSlice(rawParams, basePath) {
|
|
|
102
92
|
catch (err) {
|
|
103
93
|
return { error: `validation failed: ${err.message}` };
|
|
104
94
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (parentMilestone.status === "complete" || parentMilestone.status === "done") {
|
|
110
|
-
return { error: `cannot plan slice in a closed milestone: ${params.milestoneId} (status: ${parentMilestone.status})` };
|
|
111
|
-
}
|
|
112
|
-
const parentSlice = getSlice(params.milestoneId, params.sliceId);
|
|
113
|
-
if (!parentSlice) {
|
|
114
|
-
return { error: `missing parent slice: ${params.milestoneId}/${params.sliceId}` };
|
|
115
|
-
}
|
|
116
|
-
if (parentSlice.status === "complete" || parentSlice.status === "done") {
|
|
117
|
-
return { error: `cannot re-plan slice ${params.sliceId}: it is already complete — use gsd_slice_reopen first` };
|
|
118
|
-
}
|
|
95
|
+
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
|
|
96
|
+
// Guards must be inside the transaction so the state they check cannot
|
|
97
|
+
// change between the read and the write (#2723).
|
|
98
|
+
let guardError = null;
|
|
119
99
|
try {
|
|
120
100
|
transaction(() => {
|
|
101
|
+
const parentMilestone = getMilestone(params.milestoneId);
|
|
102
|
+
if (!parentMilestone) {
|
|
103
|
+
guardError = `milestone not found: ${params.milestoneId}`;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (isClosedStatus(parentMilestone.status)) {
|
|
107
|
+
guardError = `cannot plan slice in a closed milestone: ${params.milestoneId} (status: ${parentMilestone.status})`;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const parentSlice = getSlice(params.milestoneId, params.sliceId);
|
|
111
|
+
if (!parentSlice) {
|
|
112
|
+
guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (isClosedStatus(parentSlice.status)) {
|
|
116
|
+
guardError = `cannot re-plan slice ${params.sliceId}: it is already complete — use gsd_slice_reopen first`;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
121
119
|
upsertSlicePlanning(params.milestoneId, params.sliceId, {
|
|
122
120
|
goal: params.goal,
|
|
123
121
|
successCriteria: params.successCriteria,
|
|
@@ -163,6 +161,9 @@ export async function handlePlanSlice(rawParams, basePath) {
|
|
|
163
161
|
catch (err) {
|
|
164
162
|
return { error: `db write failed: ${err.message}` };
|
|
165
163
|
}
|
|
164
|
+
if (guardError) {
|
|
165
|
+
return { error: guardError };
|
|
166
|
+
}
|
|
166
167
|
try {
|
|
167
168
|
const renderResult = await renderPlanFromDb(basePath, params.milestoneId, params.sliceId);
|
|
168
169
|
invalidateStateCache();
|
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import { clearParseCache } from "../files.js";
|
|
2
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
3
|
+
import { isNonEmptyString, validateStringArray } from "../validation.js";
|
|
2
4
|
import { transaction, getSlice, getTask, insertTask, upsertTaskPlanning } from "../gsd-db.js";
|
|
3
5
|
import { invalidateStateCache } from "../state.js";
|
|
4
6
|
import { renderTaskPlanFromDb } from "../markdown-renderer.js";
|
|
5
7
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
6
8
|
import { writeManifest } from "../workflow-manifest.js";
|
|
7
9
|
import { appendEvent } from "../workflow-events.js";
|
|
8
|
-
function isNonEmptyString(value) {
|
|
9
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
10
|
-
}
|
|
11
|
-
function validateStringArray(value, field) {
|
|
12
|
-
if (!Array.isArray(value)) {
|
|
13
|
-
throw new Error(`${field} must be an array`);
|
|
14
|
-
}
|
|
15
|
-
if (value.some((item) => !isNonEmptyString(item))) {
|
|
16
|
-
throw new Error(`${field} must contain only non-empty strings`);
|
|
17
|
-
}
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
10
|
function validateParams(params) {
|
|
21
11
|
if (!isNonEmptyString(params?.milestoneId))
|
|
22
12
|
throw new Error("milestoneId is required");
|
|
@@ -50,19 +40,26 @@ export async function handlePlanTask(rawParams, basePath) {
|
|
|
50
40
|
catch (err) {
|
|
51
41
|
return { error: `validation failed: ${err.message}` };
|
|
52
42
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (parentSlice.status === "complete" || parentSlice.status === "done") {
|
|
58
|
-
return { error: `cannot plan task in a closed slice: ${params.sliceId} (status: ${parentSlice.status})` };
|
|
59
|
-
}
|
|
60
|
-
const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
|
|
61
|
-
if (existingTask && (existingTask.status === "complete" || existingTask.status === "done")) {
|
|
62
|
-
return { error: `cannot re-plan task ${params.taskId}: it is already complete — use gsd_task_reopen first` };
|
|
63
|
-
}
|
|
43
|
+
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
|
|
44
|
+
// Guards must be inside the transaction so the state they check cannot
|
|
45
|
+
// change between the read and the write (#2723).
|
|
46
|
+
let guardError = null;
|
|
64
47
|
try {
|
|
65
48
|
transaction(() => {
|
|
49
|
+
const parentSlice = getSlice(params.milestoneId, params.sliceId);
|
|
50
|
+
if (!parentSlice) {
|
|
51
|
+
guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (isClosedStatus(parentSlice.status)) {
|
|
55
|
+
guardError = `cannot plan task in a closed slice: ${params.sliceId} (status: ${parentSlice.status})`;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
|
|
59
|
+
if (existingTask && isClosedStatus(existingTask.status)) {
|
|
60
|
+
guardError = `cannot re-plan task ${params.taskId}: it is already complete — use gsd_task_reopen first`;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
66
63
|
if (!existingTask) {
|
|
67
64
|
insertTask({
|
|
68
65
|
id: params.taskId,
|
|
@@ -88,6 +85,9 @@ export async function handlePlanTask(rawParams, basePath) {
|
|
|
88
85
|
catch (err) {
|
|
89
86
|
return { error: `db write failed: ${err.message}` };
|
|
90
87
|
}
|
|
88
|
+
if (guardError) {
|
|
89
|
+
return { error: guardError };
|
|
90
|
+
}
|
|
91
91
|
try {
|
|
92
92
|
const renderResult = await renderTaskPlanFromDb(basePath, params.milestoneId, params.sliceId, params.taskId);
|
|
93
93
|
invalidateStateCache();
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
1
2
|
import { clearParseCache } from "../files.js";
|
|
3
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
4
|
+
import { isNonEmptyString } from "../validation.js";
|
|
2
5
|
import { transaction, getMilestone, getMilestoneSlices, getSlice, insertSlice, updateSliceFields, insertAssessment, deleteSlice, } from "../gsd-db.js";
|
|
3
6
|
import { invalidateStateCache } from "../state.js";
|
|
4
7
|
import { renderRoadmapFromDb, renderAssessmentFromDb } from "../markdown-renderer.js";
|
|
5
8
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
6
9
|
import { writeManifest } from "../workflow-manifest.js";
|
|
7
10
|
import { appendEvent } from "../workflow-events.js";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
function isNonEmptyString(value) {
|
|
10
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
11
|
-
}
|
|
12
11
|
function validateParams(params) {
|
|
13
12
|
if (!isNonEmptyString(params?.milestoneId))
|
|
14
13
|
throw new Error("milestoneId is required");
|
|
@@ -61,48 +60,55 @@ export async function handleReassessRoadmap(rawParams, basePath) {
|
|
|
61
60
|
catch (err) {
|
|
62
61
|
return { error: `validation failed: ${err.message}` };
|
|
63
62
|
}
|
|
64
|
-
// ── Verify milestone exists and is active ────────────────────────
|
|
65
|
-
const milestone = getMilestone(params.milestoneId);
|
|
66
|
-
if (!milestone) {
|
|
67
|
-
return { error: `milestone not found: ${params.milestoneId}` };
|
|
68
|
-
}
|
|
69
|
-
if (milestone.status === "complete" || milestone.status === "done") {
|
|
70
|
-
return { error: `cannot reassess a closed milestone: ${params.milestoneId} (status: ${milestone.status})` };
|
|
71
|
-
}
|
|
72
|
-
// ── Verify completedSliceId is actually complete ──────────────────
|
|
73
|
-
const completedSlice = getSlice(params.milestoneId, params.completedSliceId);
|
|
74
|
-
if (!completedSlice) {
|
|
75
|
-
return { error: `completedSliceId not found: ${params.milestoneId}/${params.completedSliceId}` };
|
|
76
|
-
}
|
|
77
|
-
if (completedSlice.status !== "complete" && completedSlice.status !== "done") {
|
|
78
|
-
return { error: `completedSliceId ${params.completedSliceId} is not complete (status: ${completedSlice.status}) — reassess can only be called after a slice finishes` };
|
|
79
|
-
}
|
|
80
|
-
// ── Structural enforcement ────────────────────────────────────────
|
|
81
|
-
const existingSlices = getMilestoneSlices(params.milestoneId);
|
|
82
|
-
const completedSliceIds = new Set();
|
|
83
|
-
for (const slice of existingSlices) {
|
|
84
|
-
if (slice.status === "complete" || slice.status === "done") {
|
|
85
|
-
completedSliceIds.add(slice.id);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
// Reject modifications to completed slices
|
|
89
|
-
for (const modifiedSlice of params.sliceChanges.modified) {
|
|
90
|
-
if (completedSliceIds.has(modifiedSlice.sliceId)) {
|
|
91
|
-
return { error: `cannot modify completed slice ${modifiedSlice.sliceId}` };
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
// Reject removal of completed slices
|
|
95
|
-
for (const removedId of params.sliceChanges.removed) {
|
|
96
|
-
if (completedSliceIds.has(removedId)) {
|
|
97
|
-
return { error: `cannot remove completed slice ${removedId}` };
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
63
|
// ── Compute assessment artifact path ──────────────────────────────
|
|
101
64
|
// Assessment lives in the completed slice's directory
|
|
102
65
|
const assessmentRelPath = join(".gsd", "milestones", params.milestoneId, "slices", params.completedSliceId, `${params.completedSliceId}-ASSESSMENT.md`);
|
|
103
|
-
// ──
|
|
66
|
+
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
|
|
67
|
+
// Guards must be inside the transaction so the state they check cannot
|
|
68
|
+
// change between the read and the write (#2723).
|
|
69
|
+
let guardError = null;
|
|
104
70
|
try {
|
|
105
71
|
transaction(() => {
|
|
72
|
+
// Verify milestone exists and is active
|
|
73
|
+
const milestone = getMilestone(params.milestoneId);
|
|
74
|
+
if (!milestone) {
|
|
75
|
+
guardError = `milestone not found: ${params.milestoneId}`;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (isClosedStatus(milestone.status)) {
|
|
79
|
+
guardError = `cannot reassess a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Verify completedSliceId is actually complete
|
|
83
|
+
const completedSlice = getSlice(params.milestoneId, params.completedSliceId);
|
|
84
|
+
if (!completedSlice) {
|
|
85
|
+
guardError = `completedSliceId not found: ${params.milestoneId}/${params.completedSliceId}`;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!isClosedStatus(completedSlice.status)) {
|
|
89
|
+
guardError = `completedSliceId ${params.completedSliceId} is not complete (status: ${completedSlice.status}) — reassess can only be called after a slice finishes`;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Structural enforcement — reject modifications/removal of completed slices
|
|
93
|
+
const existingSlices = getMilestoneSlices(params.milestoneId);
|
|
94
|
+
const completedSliceIds = new Set();
|
|
95
|
+
for (const slice of existingSlices) {
|
|
96
|
+
if (isClosedStatus(slice.status)) {
|
|
97
|
+
completedSliceIds.add(slice.id);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const modifiedSlice of params.sliceChanges.modified) {
|
|
101
|
+
if (completedSliceIds.has(modifiedSlice.sliceId)) {
|
|
102
|
+
guardError = `cannot modify completed slice ${modifiedSlice.sliceId}`;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const removedId of params.sliceChanges.removed) {
|
|
107
|
+
if (completedSliceIds.has(removedId)) {
|
|
108
|
+
guardError = `cannot remove completed slice ${removedId}`;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
106
112
|
// Record assessment
|
|
107
113
|
insertAssessment({
|
|
108
114
|
path: assessmentRelPath,
|
|
@@ -142,6 +148,9 @@ export async function handleReassessRoadmap(rawParams, basePath) {
|
|
|
142
148
|
catch (err) {
|
|
143
149
|
return { error: `db write failed: ${err.message}` };
|
|
144
150
|
}
|
|
151
|
+
if (guardError) {
|
|
152
|
+
return { error: guardError };
|
|
153
|
+
}
|
|
145
154
|
// ── Render artifacts ──────────────────────────────────────────────
|
|
146
155
|
try {
|
|
147
156
|
const roadmapResult = await renderRoadmapFromDb(basePath, params.milestoneId);
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
12
12
|
import { getMilestone, getSlice, getSliceTasks, updateSliceStatus, updateTaskStatus, transaction, } from "../gsd-db.js";
|
|
13
13
|
import { invalidateStateCache } from "../state.js";
|
|
14
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
14
15
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
15
16
|
import { writeManifest } from "../workflow-manifest.js";
|
|
16
17
|
import { appendEvent } from "../workflow-events.js";
|
|
@@ -31,8 +32,8 @@ export async function handleReopenSlice(params, basePath) {
|
|
|
31
32
|
guardError = `milestone not found: ${params.milestoneId}`;
|
|
32
33
|
return;
|
|
33
34
|
}
|
|
34
|
-
if (milestone.status
|
|
35
|
-
guardError = `cannot reopen slice
|
|
35
|
+
if (isClosedStatus(milestone.status)) {
|
|
36
|
+
guardError = `cannot reopen slice in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
38
39
|
const slice = getSlice(params.milestoneId, params.sliceId);
|
|
@@ -40,7 +41,7 @@ export async function handleReopenSlice(params, basePath) {
|
|
|
40
41
|
guardError = `slice not found: ${params.milestoneId}/${params.sliceId}`;
|
|
41
42
|
return;
|
|
42
43
|
}
|
|
43
|
-
if (slice.status
|
|
44
|
+
if (!isClosedStatus(slice.status)) {
|
|
44
45
|
guardError = `slice ${params.sliceId} is not complete (status: ${slice.status}) — nothing to reopen`;
|
|
45
46
|
return;
|
|
46
47
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
11
11
|
import { getMilestone, getSlice, getTask, updateTaskStatus, transaction, } from "../gsd-db.js";
|
|
12
12
|
import { invalidateStateCache } from "../state.js";
|
|
13
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
13
14
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
14
15
|
import { writeManifest } from "../workflow-manifest.js";
|
|
15
16
|
import { appendEvent } from "../workflow-events.js";
|
|
@@ -32,7 +33,7 @@ export async function handleReopenTask(params, basePath) {
|
|
|
32
33
|
guardError = `milestone not found: ${params.milestoneId}`;
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
|
-
if (milestone.status
|
|
36
|
+
if (isClosedStatus(milestone.status)) {
|
|
36
37
|
guardError = `cannot reopen task in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
@@ -41,8 +42,8 @@ export async function handleReopenTask(params, basePath) {
|
|
|
41
42
|
guardError = `slice not found: ${params.milestoneId}/${params.sliceId}`;
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
|
-
if (slice.status
|
|
45
|
-
guardError = `cannot reopen task
|
|
45
|
+
if (isClosedStatus(slice.status)) {
|
|
46
|
+
guardError = `cannot reopen task in a closed slice: ${params.sliceId} (status: ${slice.status}) — use gsd_slice_reopen first`;
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
48
49
|
const task = getTask(params.milestoneId, params.sliceId, params.taskId);
|
|
@@ -50,7 +51,7 @@ export async function handleReopenTask(params, basePath) {
|
|
|
50
51
|
guardError = `task not found: ${params.milestoneId}/${params.sliceId}/${params.taskId}`;
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
|
-
if (task.status
|
|
54
|
+
if (!isClosedStatus(task.status)) {
|
|
54
55
|
guardError = `task ${params.taskId} is not complete (status: ${task.status}) — nothing to reopen`;
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { clearParseCache } from "../files.js";
|
|
2
2
|
import { transaction, getSlice, getSliceTasks, getTask, insertTask, upsertTaskPlanning, insertReplanHistory, deleteTask, } from "../gsd-db.js";
|
|
3
3
|
import { invalidateStateCache } from "../state.js";
|
|
4
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
5
|
+
import { isNonEmptyString } from "../validation.js";
|
|
4
6
|
import { renderPlanFromDb, renderReplanFromDb } from "../markdown-renderer.js";
|
|
5
7
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
6
8
|
import { writeManifest } from "../workflow-manifest.js";
|
|
7
9
|
import { appendEvent } from "../workflow-events.js";
|
|
8
|
-
function isNonEmptyString(value) {
|
|
9
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
10
|
-
}
|
|
11
10
|
function validateParams(params) {
|
|
12
11
|
if (!isNonEmptyString(params?.milestoneId))
|
|
13
12
|
throw new Error("milestoneId is required");
|
|
@@ -46,46 +45,54 @@ export async function handleReplanSlice(rawParams, basePath) {
|
|
|
46
45
|
catch (err) {
|
|
47
46
|
return { error: `validation failed: ${err.message}` };
|
|
48
47
|
}
|
|
49
|
-
// ──
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (parentSlice.status === "complete" || parentSlice.status === "done") {
|
|
55
|
-
return { error: `cannot replan a closed slice: ${params.sliceId} (status: ${parentSlice.status})` };
|
|
56
|
-
}
|
|
57
|
-
// ── Verify blocker task exists and is complete ────────────────────
|
|
58
|
-
const blockerTask = getTask(params.milestoneId, params.sliceId, params.blockerTaskId);
|
|
59
|
-
if (!blockerTask) {
|
|
60
|
-
return { error: `blockerTaskId not found: ${params.milestoneId}/${params.sliceId}/${params.blockerTaskId}` };
|
|
61
|
-
}
|
|
62
|
-
if (blockerTask.status !== "complete" && blockerTask.status !== "done") {
|
|
63
|
-
return { error: `blockerTaskId ${params.blockerTaskId} is not complete (status: ${blockerTask.status}) — the blocker task must be finished before a replan is triggered` };
|
|
64
|
-
}
|
|
65
|
-
// ── Structural enforcement ────────────────────────────────────────
|
|
66
|
-
const existingTasks = getSliceTasks(params.milestoneId, params.sliceId);
|
|
67
|
-
const completedTaskIds = new Set();
|
|
68
|
-
for (const task of existingTasks) {
|
|
69
|
-
if (task.status === "complete" || task.status === "done") {
|
|
70
|
-
completedTaskIds.add(task.id);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
// Reject updates to completed tasks
|
|
74
|
-
for (const updatedTask of params.updatedTasks) {
|
|
75
|
-
if (completedTaskIds.has(updatedTask.taskId)) {
|
|
76
|
-
return { error: `cannot modify completed task ${updatedTask.taskId}` };
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
// Reject removal of completed tasks
|
|
80
|
-
for (const removedId of params.removedTaskIds) {
|
|
81
|
-
if (completedTaskIds.has(removedId)) {
|
|
82
|
-
return { error: `cannot remove completed task ${removedId}` };
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
// ── Transaction: DB mutations ─────────────────────────────────────
|
|
86
|
-
const existingTaskIds = new Set(existingTasks.map((t) => t.id));
|
|
48
|
+
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
|
|
49
|
+
// Guards must be inside the transaction so the state they check cannot
|
|
50
|
+
// change between the read and the write (#2723).
|
|
51
|
+
let guardError = null;
|
|
52
|
+
let existingTaskIds = new Set();
|
|
87
53
|
try {
|
|
88
54
|
transaction(() => {
|
|
55
|
+
// Verify parent slice exists and is not closed
|
|
56
|
+
const parentSlice = getSlice(params.milestoneId, params.sliceId);
|
|
57
|
+
if (!parentSlice) {
|
|
58
|
+
guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (isClosedStatus(parentSlice.status)) {
|
|
62
|
+
guardError = `cannot replan a closed slice: ${params.sliceId} (status: ${parentSlice.status})`;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Verify blocker task exists and is complete
|
|
66
|
+
const blockerTask = getTask(params.milestoneId, params.sliceId, params.blockerTaskId);
|
|
67
|
+
if (!blockerTask) {
|
|
68
|
+
guardError = `blockerTaskId not found: ${params.milestoneId}/${params.sliceId}/${params.blockerTaskId}`;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!isClosedStatus(blockerTask.status)) {
|
|
72
|
+
guardError = `blockerTaskId ${params.blockerTaskId} is not complete (status: ${blockerTask.status}) — the blocker task must be finished before a replan is triggered`;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Structural enforcement — reject modifications/removal of completed tasks
|
|
76
|
+
const existingTasks = getSliceTasks(params.milestoneId, params.sliceId);
|
|
77
|
+
const completedTaskIds = new Set();
|
|
78
|
+
for (const task of existingTasks) {
|
|
79
|
+
if (isClosedStatus(task.status)) {
|
|
80
|
+
completedTaskIds.add(task.id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const updatedTask of params.updatedTasks) {
|
|
84
|
+
if (completedTaskIds.has(updatedTask.taskId)) {
|
|
85
|
+
guardError = `cannot modify completed task ${updatedTask.taskId}`;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const removedId of params.removedTaskIds) {
|
|
90
|
+
if (completedTaskIds.has(removedId)) {
|
|
91
|
+
guardError = `cannot remove completed task ${removedId}`;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
existingTaskIds = new Set(existingTasks.map((t) => t.id));
|
|
89
96
|
// Record replan history
|
|
90
97
|
insertReplanHistory({
|
|
91
98
|
milestoneId: params.milestoneId,
|
|
@@ -138,6 +145,9 @@ export async function handleReplanSlice(rawParams, basePath) {
|
|
|
138
145
|
catch (err) {
|
|
139
146
|
return { error: `db write failed: ${err.message}` };
|
|
140
147
|
}
|
|
148
|
+
if (guardError) {
|
|
149
|
+
return { error: guardError };
|
|
150
|
+
}
|
|
141
151
|
// ── Render artifacts ──────────────────────────────────────────────
|
|
142
152
|
try {
|
|
143
153
|
const renderResult = await renderPlanFromDb(basePath, params.milestoneId, params.sliceId);
|