gsd-pi 2.78.1-dev.b0759e59b → 2.78.1-dev.d8826a445
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 +8 -5
- package/dist/headless-recover.d.ts +23 -0
- package/dist/headless-recover.js +93 -0
- package/dist/headless.js +9 -0
- package/dist/help-text.js +1 -0
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/browser-tools/tools/intent.js +8 -1
- package/dist/resources/extensions/gsd/auto/phases.js +7 -2
- package/dist/resources/extensions/gsd/auto/session.js +3 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +7 -58
- package/dist/resources/extensions/gsd/auto-post-unit.js +14 -28
- package/dist/resources/extensions/gsd/auto-start.js +1 -8
- package/dist/resources/extensions/gsd/auto-worktree.js +244 -216
- package/dist/resources/extensions/gsd/auto.js +86 -7
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +9 -77
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -16
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
- package/dist/resources/extensions/gsd/commands-codebase.js +2 -2
- package/dist/resources/extensions/gsd/commands-handlers.js +5 -5
- package/dist/resources/extensions/gsd/commands-logs.js +2 -2
- package/dist/resources/extensions/gsd/commands-scan.js +2 -2
- package/dist/resources/extensions/gsd/commands-ship.js +2 -2
- package/dist/resources/extensions/gsd/commands-workflow-templates.js +5 -5
- package/dist/resources/extensions/gsd/db-writer.js +106 -95
- package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
- package/dist/resources/extensions/gsd/dispatch-guard.js +6 -10
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +2 -2
- package/dist/resources/extensions/gsd/gsd-db.js +268 -8
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +141 -32
- package/dist/resources/extensions/gsd/markdown-renderer.js +14 -51
- package/dist/resources/extensions/gsd/metrics.js +287 -1
- package/dist/resources/extensions/gsd/parallel-merge.js +14 -13
- package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +5 -2
- package/dist/resources/extensions/gsd/paths.js +114 -9
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +6 -0
- package/dist/resources/extensions/gsd/queue-order.js +6 -1
- package/dist/resources/extensions/gsd/rethink.js +2 -2
- package/dist/resources/extensions/gsd/state.js +91 -372
- package/dist/resources/extensions/gsd/templates/project.md +10 -0
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +6 -5
- package/dist/resources/extensions/gsd/tools/complete-slice.js +7 -12
- package/dist/resources/extensions/gsd/tools/complete-task.js +19 -31
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +7 -5
- package/dist/resources/extensions/gsd/workflow-manifest.js +2 -1
- package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +3 -21
- package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
- package/dist/resources/extensions/gsd/workflow-reconcile.js +3 -3
- package/dist/resources/extensions/gsd/workspace.js +59 -0
- package/dist/resources/extensions/gsd/worktree-command.js +4 -3
- package/dist/resources/extensions/gsd/worktree-resolver.js +15 -2
- package/dist/resources/extensions/gsd/write-intercept.js +3 -3
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- 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.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- 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 +1 -1
- 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 +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- 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 +1 -1
- 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 +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
- package/dist/web/standalone/.next/server/chunks/6336.js +1 -0
- package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/README.md +2 -11
- package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
- package/packages/mcp-server/dist/remote-questions.js +28 -0
- package/packages/mcp-server/dist/remote-questions.js.map +1 -1
- package/packages/mcp-server/dist/server.d.ts +28 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +94 -4
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts +6 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +56 -2
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +226 -0
- package/packages/mcp-server/src/parse-workflow-args.test.ts +80 -0
- package/packages/mcp-server/src/remote-questions.test.ts +103 -0
- package/packages/mcp-server/src/remote-questions.ts +35 -0
- package/packages/mcp-server/src/server.ts +129 -6
- package/packages/mcp-server/src/workflow-tools.ts +62 -3
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/browser-tools/tools/intent.ts +13 -2
- package/src/resources/extensions/gsd/auto/phases.ts +8 -2
- package/src/resources/extensions/gsd/auto/session.ts +4 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +14 -62
- package/src/resources/extensions/gsd/auto-post-unit.ts +15 -27
- package/src/resources/extensions/gsd/auto-start.ts +1 -8
- package/src/resources/extensions/gsd/auto-worktree.ts +286 -251
- package/src/resources/extensions/gsd/auto.ts +102 -7
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +9 -84
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +17 -17
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
- package/src/resources/extensions/gsd/commands-codebase.ts +2 -2
- package/src/resources/extensions/gsd/commands-handlers.ts +5 -5
- package/src/resources/extensions/gsd/commands-logs.ts +2 -2
- package/src/resources/extensions/gsd/commands-scan.ts +2 -2
- package/src/resources/extensions/gsd/commands-ship.ts +2 -2
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +5 -5
- package/src/resources/extensions/gsd/db-writer.ts +123 -94
- package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +6 -11
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +2 -2
- package/src/resources/extensions/gsd/gsd-db.ts +269 -8
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +181 -32
- package/src/resources/extensions/gsd/markdown-renderer.ts +13 -64
- package/src/resources/extensions/gsd/metrics.ts +321 -1
- package/src/resources/extensions/gsd/parallel-merge.ts +14 -13
- package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +5 -2
- package/src/resources/extensions/gsd/paths.ts +122 -9
- package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +6 -0
- package/src/resources/extensions/gsd/queue-order.ts +6 -1
- package/src/resources/extensions/gsd/rethink.ts +2 -2
- package/src/resources/extensions/gsd/state.ts +91 -389
- package/src/resources/extensions/gsd/templates/project.md +10 -0
- package/src/resources/extensions/gsd/tests/artifact-corruption-2630.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
- package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/auto-remediate-slice-status.test.ts +21 -34
- package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
- package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +6 -7
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +8 -6
- package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +12 -27
- package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +18 -5
- package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
- package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +14 -16
- package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +6 -5
- package/src/resources/extensions/gsd/tests/derive-state-db-disk-reconcile.test.ts +10 -38
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +136 -56
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +119 -61
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +6 -20
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +4 -5
- package/src/resources/extensions/gsd/tests/dispatcher-stuck-planning.test.ts +14 -15
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
- package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +11 -16
- package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
- package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
- package/src/resources/extensions/gsd/tests/gsdroot-worktree-detection.test.ts +15 -36
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/handler-worktree-write-isolation.test.ts +57 -0
- package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +15 -15
- package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +15 -5
- package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +371 -0
- package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +14 -8
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
- package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
- package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
- package/src/resources/extensions/gsd/tests/park-milestone.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
- package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +25 -16
- package/src/resources/extensions/gsd/tests/projection-regression.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
- package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +184 -0
- package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +28 -16
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/resolve-ts.mjs +4 -0
- package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/rogue-file-detection.test.ts +3 -4
- package/src/resources/extensions/gsd/tests/slice-disk-reconcile.test.ts +10 -56
- package/src/resources/extensions/gsd/tests/stale-slice-rows.test.ts +15 -16
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +23 -27
- package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +13 -14
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +453 -0
- package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +10 -33
- package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
- package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +102 -0
- package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +7 -8
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
- package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +12 -7
- package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +26 -3
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/workspace.test.ts +190 -0
- package/src/resources/extensions/gsd/tests/worktree-db-same-file.test.ts +13 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +65 -71
- package/src/resources/extensions/gsd/tests/worktree-sync-tasks.test.ts +26 -151
- package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +67 -52
- package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +7 -5
- package/src/resources/extensions/gsd/tools/complete-slice.ts +7 -14
- package/src/resources/extensions/gsd/tools/complete-task.ts +19 -34
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +7 -5
- package/src/resources/extensions/gsd/workflow-manifest.ts +4 -1
- package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +2 -18
- package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
- package/src/resources/extensions/gsd/workflow-reconcile.ts +3 -3
- package/src/resources/extensions/gsd/workspace.ts +95 -0
- package/src/resources/extensions/gsd/worktree-command.ts +4 -3
- package/src/resources/extensions/gsd/worktree-resolver.ts +16 -2
- package/src/resources/extensions/gsd/write-intercept.ts +3 -3
- package/dist/web/standalone/.next/server/chunks/8527.js +0 -1
- /package/dist/web/standalone/.next/static/{rk1EN3FQTE6Z1yalkW_GE → AT5qi39nKXkdmQIOIoh0f}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{rk1EN3FQTE6Z1yalkW_GE → AT5qi39nKXkdmQIOIoh0f}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// GSD-2 + Unit tests for the workspace registry that replaced the originalBase singleton
|
|
2
|
+
|
|
3
|
+
import { describe, test, beforeEach } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
getAutoWorktreeOriginalBase,
|
|
12
|
+
getActiveAutoWorktreeContext,
|
|
13
|
+
_resetAutoWorktreeOriginalBaseForTests,
|
|
14
|
+
createAutoWorktree,
|
|
15
|
+
enterAutoWorktree,
|
|
16
|
+
teardownAutoWorktree,
|
|
17
|
+
} from "../auto-worktree.ts";
|
|
18
|
+
|
|
19
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
// Safe: all inputs below are hardcoded test strings, not user input.
|
|
22
|
+
function git(subArgs: string[], cwd: string): void {
|
|
23
|
+
execFileSync("git", subArgs, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createTempRepo(t: { after: (fn: () => void) => void }): string {
|
|
27
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "awreg-test-")));
|
|
28
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
29
|
+
git(["init"], dir);
|
|
30
|
+
git(["config", "user.email", "test@test.com"], dir);
|
|
31
|
+
git(["config", "user.name", "Test"], dir);
|
|
32
|
+
writeFileSync(join(dir, "README.md"), "# test\n");
|
|
33
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
34
|
+
git(["add", "."], dir);
|
|
35
|
+
git(["commit", "-m", "init"], dir);
|
|
36
|
+
git(["branch", "-M", "main"], dir);
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
describe("auto-worktree workspace registry", () => {
|
|
43
|
+
const savedCwd = process.cwd();
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
47
|
+
process.chdir(savedCwd);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("getAutoWorktreeOriginalBase() is null at baseline", () => {
|
|
51
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("getActiveAutoWorktreeContext() is null at baseline", () => {
|
|
55
|
+
assert.strictEqual(getActiveAutoWorktreeContext(), null);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("_resetAutoWorktreeOriginalBaseForTests() clears the registry — idempotent", () => {
|
|
59
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
60
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null);
|
|
61
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
62
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("behavioral equivalence: createAutoWorktree populates registry; teardown clears it", (t) => {
|
|
66
|
+
const tempDir = createTempRepo(t);
|
|
67
|
+
const msDir = join(tempDir, ".gsd", "milestones", "M001");
|
|
68
|
+
mkdirSync(msDir, { recursive: true });
|
|
69
|
+
writeFileSync(join(msDir, "CONTEXT.md"), "# M001 Context\n");
|
|
70
|
+
git(["add", "."], tempDir);
|
|
71
|
+
git(["commit", "-m", "add milestone"], tempDir);
|
|
72
|
+
|
|
73
|
+
// Before entering: registry must be empty
|
|
74
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null,
|
|
75
|
+
"originalBase is null before entering worktree");
|
|
76
|
+
|
|
77
|
+
createAutoWorktree(tempDir, "M001");
|
|
78
|
+
|
|
79
|
+
// After enter: getAutoWorktreeOriginalBase must equal tempDir
|
|
80
|
+
assert.strictEqual(
|
|
81
|
+
getAutoWorktreeOriginalBase(),
|
|
82
|
+
tempDir,
|
|
83
|
+
"getAutoWorktreeOriginalBase() returns projectRoot after createAutoWorktree",
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// getActiveAutoWorktreeContext must return the correct shape
|
|
87
|
+
const ctx = getActiveAutoWorktreeContext();
|
|
88
|
+
assert.ok(ctx !== null, "context is non-null inside worktree");
|
|
89
|
+
assert.strictEqual(ctx.originalBase, tempDir, "context.originalBase matches tempDir");
|
|
90
|
+
assert.strictEqual(ctx.worktreeName, "M001", "context.worktreeName is M001");
|
|
91
|
+
assert.strictEqual(ctx.branch, "milestone/M001", "context.branch is milestone/M001");
|
|
92
|
+
|
|
93
|
+
// Teardown: registry must be cleared
|
|
94
|
+
teardownAutoWorktree(tempDir, "M001");
|
|
95
|
+
|
|
96
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null,
|
|
97
|
+
"getAutoWorktreeOriginalBase() is null after teardown");
|
|
98
|
+
assert.strictEqual(getActiveAutoWorktreeContext(), null,
|
|
99
|
+
"getActiveAutoWorktreeContext() is null after teardown");
|
|
100
|
+
|
|
101
|
+
try { process.chdir(savedCwd); } catch { /* ignore */ }
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("behavioral equivalence: enterAutoWorktree also populates registry", (t) => {
|
|
105
|
+
const tempDir = createTempRepo(t);
|
|
106
|
+
const msDir = join(tempDir, ".gsd", "milestones", "M002");
|
|
107
|
+
mkdirSync(msDir, { recursive: true });
|
|
108
|
+
writeFileSync(join(msDir, "CONTEXT.md"), "# M002 Context\n");
|
|
109
|
+
git(["add", "."], tempDir);
|
|
110
|
+
git(["commit", "-m", "add milestone"], tempDir);
|
|
111
|
+
|
|
112
|
+
createAutoWorktree(tempDir, "M002");
|
|
113
|
+
|
|
114
|
+
// Simulate leaving the worktree (crash/pause)
|
|
115
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
116
|
+
process.chdir(tempDir);
|
|
117
|
+
|
|
118
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null,
|
|
119
|
+
"registry is empty after manual reset");
|
|
120
|
+
|
|
121
|
+
// Re-enter via enterAutoWorktree
|
|
122
|
+
enterAutoWorktree(tempDir, "M002");
|
|
123
|
+
|
|
124
|
+
assert.strictEqual(
|
|
125
|
+
getAutoWorktreeOriginalBase(),
|
|
126
|
+
tempDir,
|
|
127
|
+
"getAutoWorktreeOriginalBase() returns projectRoot after enterAutoWorktree",
|
|
128
|
+
);
|
|
129
|
+
const ctx = getActiveAutoWorktreeContext();
|
|
130
|
+
assert.ok(ctx !== null, "context is non-null after re-entry");
|
|
131
|
+
assert.strictEqual(ctx.originalBase, tempDir);
|
|
132
|
+
assert.strictEqual(ctx.worktreeName, "M002");
|
|
133
|
+
assert.strictEqual(ctx.branch, "milestone/M002");
|
|
134
|
+
|
|
135
|
+
teardownAutoWorktree(tempDir, "M002");
|
|
136
|
+
try { process.chdir(savedCwd); } catch { /* ignore */ }
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("single-occupancy: entering a new workspace replaces the previous one", (t) => {
|
|
140
|
+
const dir1 = createTempRepo(t);
|
|
141
|
+
const dir2 = createTempRepo(t);
|
|
142
|
+
|
|
143
|
+
// Set up milestone in dir1
|
|
144
|
+
const ms1Dir = join(dir1, ".gsd", "milestones", "M010");
|
|
145
|
+
mkdirSync(ms1Dir, { recursive: true });
|
|
146
|
+
writeFileSync(join(ms1Dir, "CONTEXT.md"), "# M010\n");
|
|
147
|
+
git(["add", "."], dir1);
|
|
148
|
+
git(["commit", "-m", "add milestone"], dir1);
|
|
149
|
+
|
|
150
|
+
// Set up milestone in dir2
|
|
151
|
+
const ms2Dir = join(dir2, ".gsd", "milestones", "M020");
|
|
152
|
+
mkdirSync(ms2Dir, { recursive: true });
|
|
153
|
+
writeFileSync(join(ms2Dir, "CONTEXT.md"), "# M020\n");
|
|
154
|
+
git(["add", "."], dir2);
|
|
155
|
+
git(["commit", "-m", "add milestone"], dir2);
|
|
156
|
+
|
|
157
|
+
// Enter dir1/M010
|
|
158
|
+
createAutoWorktree(dir1, "M010");
|
|
159
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), dir1,
|
|
160
|
+
"registry holds dir1 after entering M010");
|
|
161
|
+
|
|
162
|
+
// Tear down dir1 cleanly
|
|
163
|
+
teardownAutoWorktree(dir1, "M010");
|
|
164
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null, "registry cleared after M010 teardown");
|
|
165
|
+
|
|
166
|
+
// Enter dir2/M020 — registry should now hold dir2 only
|
|
167
|
+
createAutoWorktree(dir2, "M020");
|
|
168
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), dir2,
|
|
169
|
+
"registry holds dir2 after entering M020 (single-occupancy preserved)");
|
|
170
|
+
assert.notStrictEqual(getAutoWorktreeOriginalBase(), dir1,
|
|
171
|
+
"dir1 is no longer in the registry");
|
|
172
|
+
|
|
173
|
+
teardownAutoWorktree(dir2, "M020");
|
|
174
|
+
try { process.chdir(savedCwd); } catch { /* ignore */ }
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -41,7 +41,7 @@ const VALID_PARAMS = {
|
|
|
41
41
|
],
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
describe("complete-task
|
|
44
|
+
describe("complete-task projection failures keep DB completion committed", () => {
|
|
45
45
|
let base: string;
|
|
46
46
|
|
|
47
47
|
afterEach(() => {
|
|
@@ -75,7 +75,7 @@ describe("complete-task rollback cleans up verification_evidence (#2724)", () =>
|
|
|
75
75
|
assert.equal(rows.length, 2, "should have 2 evidence rows after success");
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
it("
|
|
78
|
+
it("keeps task completion and verification_evidence when disk projection write fails", async () => {
|
|
79
79
|
base = makeTmpBase();
|
|
80
80
|
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
81
81
|
insertMilestone({ id: "M001" });
|
|
@@ -87,20 +87,19 @@ describe("complete-task rollback cleans up verification_evidence (#2724)", () =>
|
|
|
87
87
|
writeFileSync(tasksDir, "not-a-directory");
|
|
88
88
|
|
|
89
89
|
const result = await handleCompleteTask(VALID_PARAMS, base);
|
|
90
|
-
assert.ok("error" in result,
|
|
90
|
+
assert.ok(!("error" in result), `unexpected error: ${"error" in result ? result.error : ""}`);
|
|
91
|
+
assert.equal(result.stale, true, "result should report stale projection");
|
|
91
92
|
|
|
92
|
-
// Task should be rolled back to pending
|
|
93
93
|
const adapter = _getAdapter()!;
|
|
94
94
|
const task = adapter.prepare(
|
|
95
95
|
`SELECT status FROM tasks WHERE milestone_id = 'M001' AND slice_id = 'S01' AND id = 'T01'`,
|
|
96
96
|
).get() as { status: string } | undefined;
|
|
97
97
|
assert.ok(task, "task row should still exist");
|
|
98
|
-
assert.equal(task!.status, "
|
|
98
|
+
assert.equal(task!.status, "complete", "task status should remain complete");
|
|
99
99
|
|
|
100
|
-
// Verification evidence should be cleaned up — no orphaned rows
|
|
101
100
|
const evidenceRows = adapter.prepare(
|
|
102
101
|
`SELECT * FROM verification_evidence WHERE task_id = 'T01' AND slice_id = 'S01' AND milestone_id = 'M001'`,
|
|
103
102
|
).all();
|
|
104
|
-
assert.equal(evidenceRows.length,
|
|
103
|
+
assert.equal(evidenceRows.length, 2, "verification_evidence should remain committed");
|
|
105
104
|
});
|
|
106
105
|
});
|
|
@@ -403,12 +403,11 @@ console.log('\n=== complete-task: handler idempotency ===');
|
|
|
403
403
|
const r1 = await handleCompleteTask(params, basePath);
|
|
404
404
|
assertTrue(!('error' in r1), 'first call should succeed');
|
|
405
405
|
|
|
406
|
-
// Verify complete-task did not duplicate T01.
|
|
407
|
-
// the remaining plan task
|
|
406
|
+
// Verify complete-task did not duplicate T01. S01-PLAN.md is a projection,
|
|
407
|
+
// so the remaining plan task is not imported implicitly.
|
|
408
408
|
const tasks = getSliceTasks('M001', 'S01');
|
|
409
|
-
assertEq(tasks.length,
|
|
409
|
+
assertEq(tasks.length, 1, 'should only have the completed DB task after first call');
|
|
410
410
|
assertEq(tasks.filter(t => t.id === 'T01').length, 1, 'should have exactly one T01 row after first call');
|
|
411
|
-
assertEq(tasks.find(t => t.id === 'T02')?.status, 'pending', 'T02 should be reconciled as pending');
|
|
412
411
|
|
|
413
412
|
// Second call with same params — state machine guard rejects (task is already complete)
|
|
414
413
|
const r2 = await handleCompleteTask(params, basePath);
|
|
@@ -419,7 +418,7 @@ console.log('\n=== complete-task: handler idempotency ===');
|
|
|
419
418
|
|
|
420
419
|
// Still no duplicate rows from the rejected second call.
|
|
421
420
|
const tasksAfter = getSliceTasks('M001', 'S01');
|
|
422
|
-
assertEq(tasksAfter.length,
|
|
421
|
+
assertEq(tasksAfter.length, 1, 'should still only have T01 after rejected second call');
|
|
423
422
|
assertEq(tasksAfter.filter(t => t.id === 'T01').length, 1, 'should still have exactly one T01 row');
|
|
424
423
|
|
|
425
424
|
cleanupDir(basePath);
|
|
@@ -447,10 +446,13 @@ console.log('\n=== complete-task: handler with missing plan file ===');
|
|
|
447
446
|
const params = makeValidParams();
|
|
448
447
|
const result = await handleCompleteTask(params, basePath);
|
|
449
448
|
|
|
450
|
-
// Should succeed
|
|
449
|
+
// Should succeed and regenerate the missing plan projection from DB.
|
|
451
450
|
assertTrue(!('error' in result), 'handler should succeed without plan file');
|
|
452
451
|
if (!('error' in result)) {
|
|
453
452
|
assertTrue(fs.existsSync(result.summaryPath), 'summary should be written even without plan file');
|
|
453
|
+
const planPath = path.join(basePath, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'S01-PLAN.md');
|
|
454
|
+
assertTrue(fs.existsSync(planPath), 'missing plan projection should be regenerated from DB');
|
|
455
|
+
assertTrue(fs.readFileSync(planPath, 'utf-8').includes('[x] **T01:'), 'regenerated plan should reflect DB task completion');
|
|
454
456
|
}
|
|
455
457
|
|
|
456
458
|
cleanupDir(basePath);
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Behavioural regression test for
|
|
2
|
+
* Behavioural regression test for DB-authoritative task completion.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* timestamp, leaving completed_at NULL forever.
|
|
8
|
-
*
|
|
9
|
-
* The fix passes new Date().toISOString() as the 5th argument; this test
|
|
10
|
-
* exercises that path end-to-end and asserts the column is populated.
|
|
11
|
-
*
|
|
12
|
-
* Refs #4829 (rewrite from positional source-grep).
|
|
4
|
+
* A task SUMMARY.md on disk is a projection, not a completion command.
|
|
5
|
+
* deriveStateFromDb must not flip a pending DB task to complete or invent a
|
|
6
|
+
* completed_at timestamp from disk evidence.
|
|
13
7
|
*/
|
|
14
8
|
|
|
15
9
|
import { describe, test, beforeEach, afterEach } from 'node:test';
|
|
@@ -46,27 +40,26 @@ function setupProject(): void {
|
|
|
46
40
|
`# M001\n\n## Slices\n\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n - After this: works\n`,
|
|
47
41
|
);
|
|
48
42
|
|
|
49
|
-
// Plan file for the slice
|
|
43
|
+
// Plan file for the slice. It is a projection and must not drive DB state.
|
|
50
44
|
writeFileSync(
|
|
51
45
|
join(basePath, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'S01-PLAN.md'),
|
|
52
46
|
`# S01: Slice\n\n## Tasks\n\n- [ ] **T01: Test task** \`est:30m\`\n - Do: x\n - Verify: y\n`,
|
|
53
47
|
);
|
|
54
48
|
|
|
55
|
-
// The summary file
|
|
56
|
-
// status to "complete" inside reconcileSliceTasks.
|
|
49
|
+
// The summary file is a projection and must not complete the task.
|
|
57
50
|
writeFileSync(
|
|
58
51
|
join(basePath, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks', 'T01-SUMMARY.md'),
|
|
59
52
|
'---\nid: T01\nparent: S01\nmilestone: M001\nblocker_discovered: false\n---\n# T01\n',
|
|
60
53
|
);
|
|
61
54
|
}
|
|
62
55
|
|
|
63
|
-
describe('completed_at
|
|
56
|
+
describe('completed_at DB-authoritative derivation', () => {
|
|
64
57
|
beforeEach(() => {
|
|
65
58
|
setupProject();
|
|
66
59
|
openDatabase(join(basePath, '.gsd', 'gsd.db'));
|
|
67
60
|
insertMilestone({ id: 'M001', title: 'M001', status: 'active' });
|
|
68
61
|
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Slice', status: 'active' });
|
|
69
|
-
// Task is "pending" in DB,
|
|
62
|
+
// Task is "pending" in DB, even though SUMMARY.md exists on disk.
|
|
70
63
|
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Test task', status: 'pending' });
|
|
71
64
|
invalidateStateCache();
|
|
72
65
|
});
|
|
@@ -76,24 +69,16 @@ describe('completed_at reconcile (#4129)', () => {
|
|
|
76
69
|
try { rmSync(basePath, { recursive: true, force: true }); } catch { /* */ }
|
|
77
70
|
});
|
|
78
71
|
|
|
79
|
-
test('
|
|
72
|
+
test('deriveStateFromDb does not set completed_at from a disk SUMMARY projection', async () => {
|
|
80
73
|
const before = getTask('M001', 'S01', 'T01');
|
|
81
74
|
assert.strictEqual(before?.status, 'pending', 'task starts pending');
|
|
82
75
|
assert.strictEqual(before?.completed_at, null, 'task starts with completed_at NULL');
|
|
83
76
|
|
|
84
|
-
//
|
|
77
|
+
// Derive runtime state. Disk SUMMARY.md must not mutate the DB row.
|
|
85
78
|
await deriveStateFromDb(basePath);
|
|
86
79
|
|
|
87
80
|
const after = getTask('M001', 'S01', 'T01');
|
|
88
|
-
assert.strictEqual(after?.status, '
|
|
89
|
-
assert.
|
|
90
|
-
typeof after?.completed_at === 'string' && after.completed_at.length > 0,
|
|
91
|
-
`completed_at must be populated by reconcileSliceTasks (#4129); got ${JSON.stringify(after?.completed_at)}`,
|
|
92
|
-
);
|
|
93
|
-
// Sanity: timestamp parses as a valid ISO date.
|
|
94
|
-
assert.ok(
|
|
95
|
-
!Number.isNaN(Date.parse(after!.completed_at!)),
|
|
96
|
-
`completed_at should be a valid ISO timestamp, got ${after!.completed_at}`,
|
|
97
|
-
);
|
|
81
|
+
assert.strictEqual(after?.status, 'pending', 'task remains pending');
|
|
82
|
+
assert.strictEqual(after?.completed_at, null, 'completed_at remains NULL');
|
|
98
83
|
});
|
|
99
84
|
});
|
|
@@ -58,13 +58,26 @@ test("#2313: syncStateToProjectRoot should sync metrics.json", () => {
|
|
|
58
58
|
);
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
test("
|
|
61
|
+
test("syncStateToProjectRoot should back-sync completed-units.json", () => {
|
|
62
|
+
const syncSrcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
|
|
63
|
+
const syncSrc = readFileSync(syncSrcPath, "utf-8");
|
|
64
|
+
const fnIdx = syncSrc.indexOf("export function syncStateToProjectRoot(");
|
|
65
|
+
assert.ok(fnIdx !== -1, "syncStateToProjectRoot exists");
|
|
66
|
+
const fnBlock = syncSrc.slice(fnIdx, syncSrc.indexOf("// ─── Resource Staleness", fnIdx));
|
|
67
|
+
|
|
68
|
+
assert.ok(
|
|
69
|
+
fnBlock.includes('"completed-units.json"'),
|
|
70
|
+
"syncStateToProjectRoot should copy completed-units.json back to the project root",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("#2313: syncWorktreeStateBack should include metrics.json in ROOT_DIAGNOSTIC_FILES", () => {
|
|
62
75
|
const autoWorktreeSrcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
|
|
63
76
|
const autoWorktreeSrc = readFileSync(autoWorktreeSrcPath, "utf-8");
|
|
64
77
|
|
|
65
|
-
// Find the
|
|
66
|
-
const constIdx = autoWorktreeSrc.indexOf("
|
|
67
|
-
assert.ok(constIdx !== -1, "
|
|
78
|
+
// Find the ROOT_DIAGNOSTIC_FILES constant used for worktree copy-back.
|
|
79
|
+
const constIdx = autoWorktreeSrc.indexOf("ROOT_DIAGNOSTIC_FILES");
|
|
80
|
+
assert.ok(constIdx !== -1, "ROOT_DIAGNOSTIC_FILES constant exists");
|
|
68
81
|
|
|
69
82
|
// Get the array content
|
|
70
83
|
const arrayStart = autoWorktreeSrc.indexOf("[", constIdx);
|
|
@@ -73,7 +86,7 @@ test("#2313: syncWorktreeStateBack should include metrics.json in ROOT_STATE_FIL
|
|
|
73
86
|
|
|
74
87
|
assert.ok(
|
|
75
88
|
rootFilesBlock.includes("metrics.json"),
|
|
76
|
-
"metrics.json should be in
|
|
89
|
+
"metrics.json should be in ROOT_DIAGNOSTIC_FILES list",
|
|
77
90
|
);
|
|
78
91
|
});
|
|
79
92
|
|
|
@@ -47,7 +47,7 @@ const symlinkResult = resolveProjectRootDbPath(symlinkPath);
|
|
|
47
47
|
assertEq(
|
|
48
48
|
symlinkResult,
|
|
49
49
|
join("/home/user/myproject/.gsd/projects/abc123def", "gsd.db"),
|
|
50
|
-
"/.gsd/projects/<hash>/worktrees/ resolves to
|
|
50
|
+
"/.gsd/projects/<hash>/worktrees/ resolves to external project state DB",
|
|
51
51
|
);
|
|
52
52
|
|
|
53
53
|
// Windows-style separators for symlink layout
|
|
@@ -57,7 +57,7 @@ if (sep === "\\") {
|
|
|
57
57
|
assertEq(
|
|
58
58
|
winResult,
|
|
59
59
|
join("C:\\Users\\dev\\project\\.gsd\\projects\\abc123def", "gsd.db"),
|
|
60
|
-
"Windows /.gsd/projects/<hash>/worktrees/ resolves to
|
|
60
|
+
"Windows /.gsd/projects/<hash>/worktrees/ resolves to external project state DB",
|
|
61
61
|
);
|
|
62
62
|
} else {
|
|
63
63
|
// On non-Windows, test forward-slash variant explicitly
|
|
@@ -66,7 +66,7 @@ if (sep === "\\") {
|
|
|
66
66
|
assertEq(
|
|
67
67
|
fwdResult,
|
|
68
68
|
join("/home/user/myproject/.gsd/projects/abc123def", "gsd.db"),
|
|
69
|
-
"Forward-slash /.gsd/projects/<hash>/worktrees/ resolves to
|
|
69
|
+
"Forward-slash /.gsd/projects/<hash>/worktrees/ resolves to external project state DB on POSIX",
|
|
70
70
|
);
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -76,7 +76,7 @@ const deepResult = resolveProjectRootDbPath(deepSymlinkPath);
|
|
|
76
76
|
assertEq(
|
|
77
77
|
deepResult,
|
|
78
78
|
join("/home/user/myproject/.gsd/projects/deadbeef42", "gsd.db"),
|
|
79
|
-
"Deep /.gsd/projects/<hash>/worktrees/ path resolves to
|
|
79
|
+
"Deep /.gsd/projects/<hash>/worktrees/ path resolves to external project state DB",
|
|
80
80
|
);
|
|
81
81
|
|
|
82
82
|
// Non-worktree path should be unchanged
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// GSD-2 + db-writer path containment: regression tests for path.relative-based traversal guard
|
|
2
|
+
|
|
3
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
import { openDatabase, closeDatabase } from "../gsd-db.ts";
|
|
10
|
+
import { createWorkspace, scopeMilestone } from "../workspace.ts";
|
|
11
|
+
import {
|
|
12
|
+
saveArtifactToDbForWorkspace,
|
|
13
|
+
saveArtifactToDbByScope,
|
|
14
|
+
} from "../db-writer.ts";
|
|
15
|
+
|
|
16
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function makeProjectDir(base: string): string {
|
|
19
|
+
mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
|
|
20
|
+
return base;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
describe("saveArtifactToDbForWorkspace: path.relative containment guard", () => {
|
|
26
|
+
let tmp: string;
|
|
27
|
+
let projectDir: string;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
tmp = mkdtempSync(join(tmpdir(), "gsd-path-contain-fw-"));
|
|
31
|
+
projectDir = makeProjectDir(tmp);
|
|
32
|
+
openDatabase(join(projectDir, ".gsd", "gsd.db"));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
closeDatabase();
|
|
37
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Attack: /foo/.gsd-other/file resolves to a path that startsWith("/foo/.gsd")
|
|
41
|
+
// but is NOT inside /foo/.gsd/. The path.relative fix correctly detects this.
|
|
42
|
+
test("rejects sibling directory that startsWith would have accepted", async () => {
|
|
43
|
+
// Create a sibling directory next to .gsd that shares the prefix
|
|
44
|
+
const sibling = join(projectDir, ".gsd-other");
|
|
45
|
+
mkdirSync(sibling, { recursive: true });
|
|
46
|
+
|
|
47
|
+
const ws = createWorkspace(projectDir);
|
|
48
|
+
// Craft an opts.path that traverses out of .gsd into .gsd-other
|
|
49
|
+
// resolve(gsdDir, "../.gsd-other/evil.md") === projectDir + "/.gsd-other/evil.md"
|
|
50
|
+
// which startsWith(projectDir + "/.gsd") because ".gsd-other" starts with ".gsd"
|
|
51
|
+
const traversalPath = "../.gsd-other/evil.md";
|
|
52
|
+
|
|
53
|
+
await assert.rejects(
|
|
54
|
+
() =>
|
|
55
|
+
saveArtifactToDbForWorkspace(ws, {
|
|
56
|
+
path: traversalPath,
|
|
57
|
+
artifact_type: "CONTEXT",
|
|
58
|
+
content: "attack",
|
|
59
|
+
}),
|
|
60
|
+
/path escapes \.gsd\/ directory/,
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("rejects absolute path input", async () => {
|
|
65
|
+
const ws = createWorkspace(projectDir);
|
|
66
|
+
await assert.rejects(
|
|
67
|
+
() =>
|
|
68
|
+
saveArtifactToDbForWorkspace(ws, {
|
|
69
|
+
path: "/etc/passwd",
|
|
70
|
+
artifact_type: "CONTEXT",
|
|
71
|
+
content: "attack",
|
|
72
|
+
}),
|
|
73
|
+
/path escapes \.gsd\/ directory/,
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("accepts a legitimate path inside .gsd/", async () => {
|
|
78
|
+
const ws = createWorkspace(projectDir);
|
|
79
|
+
// Should not throw — CONTEXT.md inside .gsd is valid
|
|
80
|
+
await assert.doesNotReject(() =>
|
|
81
|
+
saveArtifactToDbForWorkspace(ws, {
|
|
82
|
+
path: "CONTEXT.md",
|
|
83
|
+
artifact_type: "CONTEXT",
|
|
84
|
+
content: "# Context\n",
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("saveArtifactToDbByScope: path.relative containment guard", () => {
|
|
91
|
+
let tmp: string;
|
|
92
|
+
let projectDir: string;
|
|
93
|
+
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
tmp = mkdtempSync(join(tmpdir(), "gsd-path-contain-bs-"));
|
|
96
|
+
projectDir = makeProjectDir(tmp);
|
|
97
|
+
openDatabase(join(projectDir, ".gsd", "gsd.db"));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
afterEach(() => {
|
|
101
|
+
closeDatabase();
|
|
102
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("rejects sibling directory that startsWith would have accepted", async () => {
|
|
106
|
+
const sibling = join(projectDir, ".gsd-other");
|
|
107
|
+
mkdirSync(sibling, { recursive: true });
|
|
108
|
+
|
|
109
|
+
const ws = createWorkspace(projectDir);
|
|
110
|
+
const scope = scopeMilestone(ws, "M001");
|
|
111
|
+
const traversalPath = "../.gsd-other/evil.md";
|
|
112
|
+
|
|
113
|
+
await assert.rejects(
|
|
114
|
+
() =>
|
|
115
|
+
saveArtifactToDbByScope(scope, {
|
|
116
|
+
path: traversalPath,
|
|
117
|
+
artifact_type: "CONTEXT",
|
|
118
|
+
content: "attack",
|
|
119
|
+
}),
|
|
120
|
+
/path escapes \.gsd\/ directory/,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("rejects absolute path input", async () => {
|
|
125
|
+
const ws = createWorkspace(projectDir);
|
|
126
|
+
const scope = scopeMilestone(ws, "M001");
|
|
127
|
+
await assert.rejects(
|
|
128
|
+
() =>
|
|
129
|
+
saveArtifactToDbByScope(scope, {
|
|
130
|
+
path: "/etc/passwd",
|
|
131
|
+
artifact_type: "CONTEXT",
|
|
132
|
+
content: "attack",
|
|
133
|
+
}),
|
|
134
|
+
/path escapes \.gsd\/ directory/,
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("accepts a legitimate milestone-relative path inside .gsd/", async () => {
|
|
139
|
+
mkdirSync(join(projectDir, ".gsd", "milestones", "M001"), {
|
|
140
|
+
recursive: true,
|
|
141
|
+
});
|
|
142
|
+
const ws = createWorkspace(projectDir);
|
|
143
|
+
const scope = scopeMilestone(ws, "M001");
|
|
144
|
+
await assert.doesNotReject(() =>
|
|
145
|
+
saveArtifactToDbByScope(scope, {
|
|
146
|
+
path: "milestones/M001/M001-CONTEXT.md",
|
|
147
|
+
artifact_type: "CONTEXT",
|
|
148
|
+
content: "# Context\n",
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
});
|