gsd-pi 2.50.0-dev.d210a87 → 2.51.0-dev.7d435fe
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/claude-code-cli/stream-adapter.js +18 -19
- 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 +4 -2
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +95 -69
- 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 +1 -1
- package/dist/resources/extensions/gsd/detection.js +6 -6
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +3 -3
- package/dist/resources/extensions/gsd/error-classifier.js +105 -0
- 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/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.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 +21 -2
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -10
- package/dist/resources/extensions/gsd/tools/complete-slice.js +3 -17
- package/dist/resources/extensions/gsd/tools/complete-task.js +7 -18
- package/dist/resources/extensions/gsd/tools/plan-milestone.js +26 -17
- package/dist/resources/extensions/gsd/tools/plan-slice.js +25 -14
- package/dist/resources/extensions/gsd/tools/plan-task.js +21 -11
- package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +47 -37
- package/dist/resources/extensions/gsd/tools/replan-slice.js +49 -38
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +23 -16
- 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/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +21 -21
- 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 +21 -21
- 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/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/pkg/package.json +1 -1
- package/src/resources/extensions/async-jobs/async-bash-tool.ts +22 -11
- 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-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 +5 -2
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +115 -72
- 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 +1 -1
- package/src/resources/extensions/gsd/detection.ts +6 -6
- package/src/resources/extensions/gsd/docs/preferences-reference.md +3 -3
- package/src/resources/extensions/gsd/error-classifier.ts +139 -0
- 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/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.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 +22 -2
- 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 +1 -1
- 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 +1 -1
- 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/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/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 +3 -14
- package/src/resources/extensions/gsd/tools/complete-slice.ts +3 -21
- package/src/resources/extensions/gsd/tools/complete-task.ts +9 -22
- package/src/resources/extensions/gsd/tools/plan-milestone.ts +28 -18
- package/src/resources/extensions/gsd/tools/plan-slice.ts +28 -16
- package/src/resources/extensions/gsd/tools/plan-task.ts +24 -12
- package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +54 -42
- package/src/resources/extensions/gsd/tools/replan-slice.ts +53 -40
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +26 -20
- 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/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/{yJIyd5cXPNpmXTv18ZlyC → RqOU-jOv9uZ1Q03P6L6nn}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{yJIyd5cXPNpmXTv18ZlyC → RqOU-jOv9uZ1Q03P6L6nn}/_ssgManifest.js +0 -0
- /package/src/resources/extensions/gsd/templates/{preferences.md → PREFERENCES.md} +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree-preferences-sync.test.ts — Regression test for #2684.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that preferences.md is seeded into auto-mode worktrees:
|
|
5
|
+
*
|
|
6
|
+
* 1. copyPlanningArtifacts() copies preferences.md on initial worktree creation
|
|
7
|
+
* 2. syncGsdStateToWorktree() forward-syncs preferences.md (additive only)
|
|
8
|
+
* 3. syncWorktreeStateBack() does NOT overwrite project root preferences.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import {
|
|
14
|
+
existsSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
mkdtempSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
rmSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
} from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
syncGsdStateToWorktree,
|
|
26
|
+
syncWorktreeStateBack,
|
|
27
|
+
} from "../auto-worktree.ts";
|
|
28
|
+
|
|
29
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
function makeTempDir(prefix: string): string {
|
|
32
|
+
return mkdtempSync(join(tmpdir(), `gsd-prefs-test-${prefix}-`));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function cleanup(...dirs: string[]): void {
|
|
36
|
+
for (const dir of dirs) {
|
|
37
|
+
rmSync(dir, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeFile(dir: string, relativePath: string, content: string): void {
|
|
42
|
+
const fullPath = join(dir, relativePath);
|
|
43
|
+
mkdirSync(join(fullPath, ".."), { recursive: true });
|
|
44
|
+
writeFileSync(fullPath, content, "utf-8");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Tests ───────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
const PREFS_CONTENT = [
|
|
50
|
+
"# Preferences",
|
|
51
|
+
"",
|
|
52
|
+
"post_unit_hooks:",
|
|
53
|
+
" - npm run lint",
|
|
54
|
+
"",
|
|
55
|
+
"skill_rules:",
|
|
56
|
+
' - use: "frontend-design"',
|
|
57
|
+
].join("\n");
|
|
58
|
+
|
|
59
|
+
test("#2684: syncGsdStateToWorktree forward-syncs preferences.md when missing from worktree", (t) => {
|
|
60
|
+
const mainBase = makeTempDir("main");
|
|
61
|
+
const wtBase = makeTempDir("wt");
|
|
62
|
+
t.after(() => cleanup(mainBase, wtBase));
|
|
63
|
+
|
|
64
|
+
// Project root has preferences.md
|
|
65
|
+
writeFile(mainBase, ".gsd/preferences.md", PREFS_CONTENT);
|
|
66
|
+
|
|
67
|
+
// Worktree has .gsd/ but no preferences.md
|
|
68
|
+
mkdirSync(join(wtBase, ".gsd"), { recursive: true });
|
|
69
|
+
|
|
70
|
+
const result = syncGsdStateToWorktree(mainBase, wtBase);
|
|
71
|
+
|
|
72
|
+
assert.ok(
|
|
73
|
+
existsSync(join(wtBase, ".gsd", "preferences.md")),
|
|
74
|
+
"preferences.md should be copied to worktree",
|
|
75
|
+
);
|
|
76
|
+
assert.equal(
|
|
77
|
+
readFileSync(join(wtBase, ".gsd", "preferences.md"), "utf-8"),
|
|
78
|
+
PREFS_CONTENT,
|
|
79
|
+
"preferences.md content should match source",
|
|
80
|
+
);
|
|
81
|
+
assert.ok(
|
|
82
|
+
result.synced.includes("preferences.md"),
|
|
83
|
+
"preferences.md should appear in synced list",
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("#2684: syncGsdStateToWorktree does NOT overwrite existing worktree preferences.md", (t) => {
|
|
88
|
+
const mainBase = makeTempDir("main");
|
|
89
|
+
const wtBase = makeTempDir("wt");
|
|
90
|
+
t.after(() => cleanup(mainBase, wtBase));
|
|
91
|
+
|
|
92
|
+
const rootPrefs = "# Root preferences\nold: true";
|
|
93
|
+
const wtPrefs = "# Worktree preferences\nmodified: true";
|
|
94
|
+
|
|
95
|
+
writeFile(mainBase, ".gsd/preferences.md", rootPrefs);
|
|
96
|
+
writeFile(wtBase, ".gsd/preferences.md", wtPrefs);
|
|
97
|
+
|
|
98
|
+
syncGsdStateToWorktree(mainBase, wtBase);
|
|
99
|
+
|
|
100
|
+
assert.equal(
|
|
101
|
+
readFileSync(join(wtBase, ".gsd", "preferences.md"), "utf-8"),
|
|
102
|
+
wtPrefs,
|
|
103
|
+
"existing worktree preferences.md must not be overwritten",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("#2684: syncWorktreeStateBack does NOT overwrite project root preferences.md", (t) => {
|
|
108
|
+
const mainBase = makeTempDir("main");
|
|
109
|
+
const wtBase = makeTempDir("wt");
|
|
110
|
+
const mid = "M001";
|
|
111
|
+
t.after(() => cleanup(mainBase, wtBase));
|
|
112
|
+
|
|
113
|
+
const rootPrefs = "# Root preferences\nauthoritative: true";
|
|
114
|
+
const wtPrefs = "# Worktree preferences\nstale-copy: true";
|
|
115
|
+
|
|
116
|
+
writeFile(mainBase, ".gsd/preferences.md", rootPrefs);
|
|
117
|
+
writeFile(wtBase, ".gsd/preferences.md", wtPrefs);
|
|
118
|
+
|
|
119
|
+
// Worktree needs at least a milestone dir for the function to proceed
|
|
120
|
+
mkdirSync(join(wtBase, ".gsd", "milestones", mid), { recursive: true });
|
|
121
|
+
mkdirSync(join(mainBase, ".gsd", "milestones"), { recursive: true });
|
|
122
|
+
|
|
123
|
+
syncWorktreeStateBack(mainBase, wtBase, mid);
|
|
124
|
+
|
|
125
|
+
assert.equal(
|
|
126
|
+
readFileSync(join(mainBase, ".gsd", "preferences.md"), "utf-8"),
|
|
127
|
+
rootPrefs,
|
|
128
|
+
"project root preferences.md must NOT be overwritten by worktree copy",
|
|
129
|
+
);
|
|
130
|
+
});
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
getMilestone,
|
|
15
15
|
getMilestoneSlices,
|
|
16
16
|
getSliceTasks,
|
|
17
|
-
|
|
17
|
+
updateMilestoneStatus,
|
|
18
18
|
} from "../gsd-db.js";
|
|
19
19
|
import { resolveMilestonePath, clearPathCache } from "../paths.js";
|
|
20
20
|
import { saveFile, clearParseCache } from "../files.js";
|
|
@@ -165,13 +165,7 @@ export async function handleCompleteMilestone(
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
// All guards passed — perform write
|
|
168
|
-
|
|
169
|
-
adapter.prepare(
|
|
170
|
-
`UPDATE milestones SET status = 'complete', completed_at = :completed_at WHERE id = :mid`,
|
|
171
|
-
).run({
|
|
172
|
-
":completed_at": completedAt,
|
|
173
|
-
":mid": params.milestoneId,
|
|
174
|
-
});
|
|
168
|
+
updateMilestoneStatus(params.milestoneId, 'complete', completedAt);
|
|
175
169
|
});
|
|
176
170
|
|
|
177
171
|
if (guardError) {
|
|
@@ -199,12 +193,7 @@ export async function handleCompleteMilestone(
|
|
|
199
193
|
process.stderr.write(
|
|
200
194
|
`gsd-db: complete_milestone — disk render failed, rolling back DB status: ${(renderErr as Error).message}\n`,
|
|
201
195
|
);
|
|
202
|
-
|
|
203
|
-
if (rollbackAdapter) {
|
|
204
|
-
rollbackAdapter.prepare(
|
|
205
|
-
`UPDATE milestones SET status = 'active', completed_at = NULL WHERE id = :mid`,
|
|
206
|
-
).run({ ":mid": params.milestoneId });
|
|
207
|
-
}
|
|
196
|
+
updateMilestoneStatus(params.milestoneId, 'active', null);
|
|
208
197
|
invalidateStateCache();
|
|
209
198
|
return { error: `disk render failed: ${(renderErr as Error).message}` };
|
|
210
199
|
}
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
getSliceTasks,
|
|
20
20
|
getMilestone,
|
|
21
21
|
updateSliceStatus,
|
|
22
|
-
|
|
22
|
+
setSliceSummaryMd,
|
|
23
23
|
} from "../gsd-db.js";
|
|
24
24
|
import { resolveSliceFile, resolveSlicePath, clearPathCache } from "../paths.js";
|
|
25
25
|
import { checkOwnership, sliceUnitKey } from "../unit-ownership.js";
|
|
@@ -299,31 +299,13 @@ export async function handleCompleteSlice(
|
|
|
299
299
|
process.stderr.write(
|
|
300
300
|
`gsd-db: complete_slice — disk render failed, rolling back DB status: ${(renderErr as Error).message}\n`,
|
|
301
301
|
);
|
|
302
|
-
|
|
303
|
-
if (rollbackAdapter) {
|
|
304
|
-
rollbackAdapter.prepare(
|
|
305
|
-
`UPDATE slices SET status = 'pending' WHERE milestone_id = :mid AND id = :sid`,
|
|
306
|
-
).run({
|
|
307
|
-
":mid": params.milestoneId,
|
|
308
|
-
":sid": params.sliceId,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
302
|
+
updateSliceStatus(params.milestoneId, params.sliceId, 'pending');
|
|
311
303
|
invalidateStateCache();
|
|
312
304
|
return { error: `disk render failed: ${(renderErr as Error).message}` };
|
|
313
305
|
}
|
|
314
306
|
|
|
315
307
|
// Store rendered markdown in DB for D004 recovery
|
|
316
|
-
|
|
317
|
-
if (adapter) {
|
|
318
|
-
adapter.prepare(
|
|
319
|
-
`UPDATE slices SET full_summary_md = :summary_md, full_uat_md = :uat_md WHERE milestone_id = :mid AND id = :sid`,
|
|
320
|
-
).run({
|
|
321
|
-
":summary_md": summaryMd,
|
|
322
|
-
":uat_md": uatMd,
|
|
323
|
-
":mid": params.milestoneId,
|
|
324
|
-
":sid": params.sliceId,
|
|
325
|
-
});
|
|
326
|
-
}
|
|
308
|
+
setSliceSummaryMd(params.milestoneId, params.sliceId, summaryMd, uatMd);
|
|
327
309
|
|
|
328
310
|
// Invalidate all caches
|
|
329
311
|
invalidateStateCache();
|
|
@@ -20,7 +20,9 @@ import {
|
|
|
20
20
|
getMilestone,
|
|
21
21
|
getSlice,
|
|
22
22
|
getTask,
|
|
23
|
-
|
|
23
|
+
updateTaskStatus,
|
|
24
|
+
setTaskSummaryMd,
|
|
25
|
+
deleteVerificationEvidence,
|
|
24
26
|
} from "../gsd-db.js";
|
|
25
27
|
import { resolveSliceFile, resolveTasksDir, clearPathCache } from "../paths.js";
|
|
26
28
|
import { checkOwnership, taskUnitKey } from "../unit-ownership.js";
|
|
@@ -248,32 +250,17 @@ export async function handleCompleteTask(
|
|
|
248
250
|
process.stderr.write(
|
|
249
251
|
`gsd-db: complete_task — disk render failed, rolling back DB status: ${(renderErr as Error).message}\n`,
|
|
250
252
|
);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
":mid": params.milestoneId,
|
|
257
|
-
":sid": params.sliceId,
|
|
258
|
-
":tid": params.taskId,
|
|
259
|
-
});
|
|
260
|
-
}
|
|
253
|
+
// Delete orphaned verification_evidence rows first (FK constraint
|
|
254
|
+
// references tasks, so evidence must go before status change).
|
|
255
|
+
// Without this, retries accumulate duplicate evidence rows (#2724).
|
|
256
|
+
deleteVerificationEvidence(params.milestoneId, params.sliceId, params.taskId);
|
|
257
|
+
updateTaskStatus(params.milestoneId, params.sliceId, params.taskId, 'pending');
|
|
261
258
|
invalidateStateCache();
|
|
262
259
|
return { error: `disk render failed: ${(renderErr as Error).message}` };
|
|
263
260
|
}
|
|
264
261
|
|
|
265
262
|
// Store rendered markdown in DB for D004 recovery
|
|
266
|
-
|
|
267
|
-
if (adapter) {
|
|
268
|
-
adapter.prepare(
|
|
269
|
-
`UPDATE tasks SET full_summary_md = :md WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`,
|
|
270
|
-
).run({
|
|
271
|
-
":md": summaryMd,
|
|
272
|
-
":mid": params.milestoneId,
|
|
273
|
-
":sid": params.sliceId,
|
|
274
|
-
":tid": params.taskId,
|
|
275
|
-
});
|
|
276
|
-
}
|
|
263
|
+
setTaskSummaryMd(params.milestoneId, params.sliceId, params.taskId, summaryMd);
|
|
277
264
|
|
|
278
265
|
// Invalidate all caches
|
|
279
266
|
invalidateStateCache();
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
insertSlice,
|
|
7
7
|
upsertMilestonePlanning,
|
|
8
8
|
upsertSlicePlanning,
|
|
9
|
-
_getAdapter,
|
|
10
9
|
} from "../gsd-db.js";
|
|
11
10
|
import { invalidateStateCache } from "../state.js";
|
|
12
11
|
import { renderRoadmapFromDb } from "../markdown-renderer.js";
|
|
@@ -189,27 +188,34 @@ export async function handlePlanMilestone(
|
|
|
189
188
|
return { error: `validation failed: ${(err as Error).message}` };
|
|
190
189
|
}
|
|
191
190
|
|
|
192
|
-
// ──
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
191
|
+
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
|
|
192
|
+
// Guards must be inside the transaction so the state they check cannot
|
|
193
|
+
// change between the read and the write (#2723).
|
|
194
|
+
let guardError: string | null = null;
|
|
197
195
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return
|
|
196
|
+
try {
|
|
197
|
+
transaction(() => {
|
|
198
|
+
const existingMilestone = getMilestone(params.milestoneId);
|
|
199
|
+
if (existingMilestone && (existingMilestone.status === "complete" || existingMilestone.status === "done")) {
|
|
200
|
+
guardError = `cannot re-plan milestone ${params.milestoneId}: it is already complete`;
|
|
201
|
+
return;
|
|
204
202
|
}
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
|
|
204
|
+
// Validate depends_on: all dependencies must exist and be complete
|
|
205
|
+
if (params.dependsOn && params.dependsOn.length > 0) {
|
|
206
|
+
for (const depId of params.dependsOn) {
|
|
207
|
+
const dep = getMilestone(depId);
|
|
208
|
+
if (!dep) {
|
|
209
|
+
guardError = `depends_on references unknown milestone: ${depId}`;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (dep.status !== "complete" && dep.status !== "done") {
|
|
213
|
+
guardError = `depends_on milestone ${depId} is not yet complete (status: ${dep.status})`;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
207
217
|
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
218
|
|
|
211
|
-
try {
|
|
212
|
-
transaction(() => {
|
|
213
219
|
insertMilestone({
|
|
214
220
|
id: params.milestoneId,
|
|
215
221
|
title: params.title,
|
|
@@ -254,6 +260,10 @@ export async function handlePlanMilestone(
|
|
|
254
260
|
return { error: `db write failed: ${(err as Error).message}` };
|
|
255
261
|
}
|
|
256
262
|
|
|
263
|
+
if (guardError) {
|
|
264
|
+
return { error: guardError };
|
|
265
|
+
}
|
|
266
|
+
|
|
257
267
|
let roadmapPath: string;
|
|
258
268
|
try {
|
|
259
269
|
const renderResult = await renderRoadmapFromDb(basePath, params.milestoneId);
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
upsertSlicePlanning,
|
|
8
8
|
upsertTaskPlanning,
|
|
9
9
|
insertGateRow,
|
|
10
|
-
_getAdapter,
|
|
11
10
|
} from "../gsd-db.js";
|
|
12
11
|
import type { GateId } from "../types.js";
|
|
13
12
|
import { invalidateStateCache } from "../state.js";
|
|
@@ -146,24 +145,33 @@ export async function handlePlanSlice(
|
|
|
146
145
|
return { error: `validation failed: ${(err as Error).message}` };
|
|
147
146
|
}
|
|
148
147
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (parentMilestone.status === "complete" || parentMilestone.status === "done") {
|
|
154
|
-
return { error: `cannot plan slice in a closed milestone: ${params.milestoneId} (status: ${parentMilestone.status})` };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const parentSlice = getSlice(params.milestoneId, params.sliceId);
|
|
158
|
-
if (!parentSlice) {
|
|
159
|
-
return { error: `missing parent slice: ${params.milestoneId}/${params.sliceId}` };
|
|
160
|
-
}
|
|
161
|
-
if (parentSlice.status === "complete" || parentSlice.status === "done") {
|
|
162
|
-
return { error: `cannot re-plan slice ${params.sliceId}: it is already complete — use gsd_slice_reopen first` };
|
|
163
|
-
}
|
|
148
|
+
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
|
|
149
|
+
// Guards must be inside the transaction so the state they check cannot
|
|
150
|
+
// change between the read and the write (#2723).
|
|
151
|
+
let guardError: string | null = null;
|
|
164
152
|
|
|
165
153
|
try {
|
|
166
154
|
transaction(() => {
|
|
155
|
+
const parentMilestone = getMilestone(params.milestoneId);
|
|
156
|
+
if (!parentMilestone) {
|
|
157
|
+
guardError = `milestone not found: ${params.milestoneId}`;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (parentMilestone.status === "complete" || parentMilestone.status === "done") {
|
|
161
|
+
guardError = `cannot plan slice in a closed milestone: ${params.milestoneId} (status: ${parentMilestone.status})`;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const parentSlice = getSlice(params.milestoneId, params.sliceId);
|
|
166
|
+
if (!parentSlice) {
|
|
167
|
+
guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (parentSlice.status === "complete" || parentSlice.status === "done") {
|
|
171
|
+
guardError = `cannot re-plan slice ${params.sliceId}: it is already complete — use gsd_slice_reopen first`;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
167
175
|
upsertSlicePlanning(params.milestoneId, params.sliceId, {
|
|
168
176
|
goal: params.goal,
|
|
169
177
|
successCriteria: params.successCriteria,
|
|
@@ -211,6 +219,10 @@ export async function handlePlanSlice(
|
|
|
211
219
|
return { error: `db write failed: ${(err as Error).message}` };
|
|
212
220
|
}
|
|
213
221
|
|
|
222
|
+
if (guardError) {
|
|
223
|
+
return { error: guardError };
|
|
224
|
+
}
|
|
225
|
+
|
|
214
226
|
try {
|
|
215
227
|
const renderResult = await renderPlanFromDb(basePath, params.milestoneId, params.sliceId);
|
|
216
228
|
invalidateStateCache();
|
|
@@ -77,21 +77,29 @@ export async function handlePlanTask(
|
|
|
77
77
|
return { error: `validation failed: ${(err as Error).message}` };
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (parentSlice.status === "complete" || parentSlice.status === "done") {
|
|
85
|
-
return { error: `cannot plan task in a closed slice: ${params.sliceId} (status: ${parentSlice.status})` };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
|
|
89
|
-
if (existingTask && (existingTask.status === "complete" || existingTask.status === "done")) {
|
|
90
|
-
return { error: `cannot re-plan task ${params.taskId}: it is already complete — use gsd_task_reopen first` };
|
|
91
|
-
}
|
|
80
|
+
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
|
|
81
|
+
// Guards must be inside the transaction so the state they check cannot
|
|
82
|
+
// change between the read and the write (#2723).
|
|
83
|
+
let guardError: string | null = null;
|
|
92
84
|
|
|
93
85
|
try {
|
|
94
86
|
transaction(() => {
|
|
87
|
+
const parentSlice = getSlice(params.milestoneId, params.sliceId);
|
|
88
|
+
if (!parentSlice) {
|
|
89
|
+
guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (parentSlice.status === "complete" || parentSlice.status === "done") {
|
|
93
|
+
guardError = `cannot plan task in a closed slice: ${params.sliceId} (status: ${parentSlice.status})`;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
|
|
98
|
+
if (existingTask && (existingTask.status === "complete" || existingTask.status === "done")) {
|
|
99
|
+
guardError = `cannot re-plan task ${params.taskId}: it is already complete — use gsd_task_reopen first`;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
95
103
|
if (!existingTask) {
|
|
96
104
|
insertTask({
|
|
97
105
|
id: params.taskId,
|
|
@@ -117,6 +125,10 @@ export async function handlePlanTask(
|
|
|
117
125
|
return { error: `db write failed: ${(err as Error).message}` };
|
|
118
126
|
}
|
|
119
127
|
|
|
128
|
+
if (guardError) {
|
|
129
|
+
return { error: guardError };
|
|
130
|
+
}
|
|
131
|
+
|
|
120
132
|
try {
|
|
121
133
|
const renderResult = await renderTaskPlanFromDb(basePath, params.milestoneId, params.sliceId, params.taskId);
|
|
122
134
|
invalidateStateCache();
|
|
@@ -104,47 +104,6 @@ export async function handleReassessRoadmap(
|
|
|
104
104
|
return { error: `validation failed: ${(err as Error).message}` };
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
// ── Verify milestone exists and is active ────────────────────────
|
|
108
|
-
const milestone = getMilestone(params.milestoneId);
|
|
109
|
-
if (!milestone) {
|
|
110
|
-
return { error: `milestone not found: ${params.milestoneId}` };
|
|
111
|
-
}
|
|
112
|
-
if (milestone.status === "complete" || milestone.status === "done") {
|
|
113
|
-
return { error: `cannot reassess a closed milestone: ${params.milestoneId} (status: ${milestone.status})` };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ── Verify completedSliceId is actually complete ──────────────────
|
|
117
|
-
const completedSlice = getSlice(params.milestoneId, params.completedSliceId);
|
|
118
|
-
if (!completedSlice) {
|
|
119
|
-
return { error: `completedSliceId not found: ${params.milestoneId}/${params.completedSliceId}` };
|
|
120
|
-
}
|
|
121
|
-
if (completedSlice.status !== "complete" && completedSlice.status !== "done") {
|
|
122
|
-
return { error: `completedSliceId ${params.completedSliceId} is not complete (status: ${completedSlice.status}) — reassess can only be called after a slice finishes` };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ── Structural enforcement ────────────────────────────────────────
|
|
126
|
-
const existingSlices = getMilestoneSlices(params.milestoneId);
|
|
127
|
-
const completedSliceIds = new Set<string>();
|
|
128
|
-
for (const slice of existingSlices) {
|
|
129
|
-
if (slice.status === "complete" || slice.status === "done") {
|
|
130
|
-
completedSliceIds.add(slice.id);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Reject modifications to completed slices
|
|
135
|
-
for (const modifiedSlice of params.sliceChanges.modified) {
|
|
136
|
-
if (completedSliceIds.has(modifiedSlice.sliceId)) {
|
|
137
|
-
return { error: `cannot modify completed slice ${modifiedSlice.sliceId}` };
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Reject removal of completed slices
|
|
142
|
-
for (const removedId of params.sliceChanges.removed) {
|
|
143
|
-
if (completedSliceIds.has(removedId)) {
|
|
144
|
-
return { error: `cannot remove completed slice ${removedId}` };
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
107
|
// ── Compute assessment artifact path ──────────────────────────────
|
|
149
108
|
// Assessment lives in the completed slice's directory
|
|
150
109
|
const assessmentRelPath = join(
|
|
@@ -153,9 +112,58 @@ export async function handleReassessRoadmap(
|
|
|
153
112
|
`${params.completedSliceId}-ASSESSMENT.md`,
|
|
154
113
|
);
|
|
155
114
|
|
|
156
|
-
// ──
|
|
115
|
+
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
|
|
116
|
+
// Guards must be inside the transaction so the state they check cannot
|
|
117
|
+
// change between the read and the write (#2723).
|
|
118
|
+
let guardError: string | null = null;
|
|
119
|
+
|
|
157
120
|
try {
|
|
158
121
|
transaction(() => {
|
|
122
|
+
// Verify milestone exists and is active
|
|
123
|
+
const milestone = getMilestone(params.milestoneId);
|
|
124
|
+
if (!milestone) {
|
|
125
|
+
guardError = `milestone not found: ${params.milestoneId}`;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (milestone.status === "complete" || milestone.status === "done") {
|
|
129
|
+
guardError = `cannot reassess a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Verify completedSliceId is actually complete
|
|
134
|
+
const completedSlice = getSlice(params.milestoneId, params.completedSliceId);
|
|
135
|
+
if (!completedSlice) {
|
|
136
|
+
guardError = `completedSliceId not found: ${params.milestoneId}/${params.completedSliceId}`;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (completedSlice.status !== "complete" && completedSlice.status !== "done") {
|
|
140
|
+
guardError = `completedSliceId ${params.completedSliceId} is not complete (status: ${completedSlice.status}) — reassess can only be called after a slice finishes`;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Structural enforcement — reject modifications/removal of completed slices
|
|
145
|
+
const existingSlices = getMilestoneSlices(params.milestoneId);
|
|
146
|
+
const completedSliceIds = new Set<string>();
|
|
147
|
+
for (const slice of existingSlices) {
|
|
148
|
+
if (slice.status === "complete" || slice.status === "done") {
|
|
149
|
+
completedSliceIds.add(slice.id);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const modifiedSlice of params.sliceChanges.modified) {
|
|
154
|
+
if (completedSliceIds.has(modifiedSlice.sliceId)) {
|
|
155
|
+
guardError = `cannot modify completed slice ${modifiedSlice.sliceId}`;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const removedId of params.sliceChanges.removed) {
|
|
161
|
+
if (completedSliceIds.has(removedId)) {
|
|
162
|
+
guardError = `cannot remove completed slice ${removedId}`;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
159
167
|
// Record assessment
|
|
160
168
|
insertAssessment({
|
|
161
169
|
path: assessmentRelPath,
|
|
@@ -198,6 +206,10 @@ export async function handleReassessRoadmap(
|
|
|
198
206
|
return { error: `db write failed: ${(err as Error).message}` };
|
|
199
207
|
}
|
|
200
208
|
|
|
209
|
+
if (guardError) {
|
|
210
|
+
return { error: guardError };
|
|
211
|
+
}
|
|
212
|
+
|
|
201
213
|
// ── Render artifacts ──────────────────────────────────────────────
|
|
202
214
|
try {
|
|
203
215
|
const roadmapResult = await renderRoadmapFromDb(basePath, params.milestoneId);
|