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,371 @@
|
|
|
1
|
+
// GSD-2 + Integration regression suite for workspace collapse (feat/workspace-collapse)
|
|
2
|
+
|
|
3
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import {
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
realpathSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { execFileSync } from "node:child_process";
|
|
16
|
+
|
|
17
|
+
import { createWorkspace, scopeMilestone } from "../../workspace.ts";
|
|
18
|
+
import {
|
|
19
|
+
gsdRoot,
|
|
20
|
+
clearPathCache,
|
|
21
|
+
_clearGsdRootCache,
|
|
22
|
+
} from "../../paths.ts";
|
|
23
|
+
import {
|
|
24
|
+
loadWriteGateSnapshot,
|
|
25
|
+
markDepthVerified,
|
|
26
|
+
clearDiscussionFlowState,
|
|
27
|
+
} from "../../bootstrap/write-gate.ts";
|
|
28
|
+
import {
|
|
29
|
+
teardownAutoWorktree,
|
|
30
|
+
_resetAutoWorktreeOriginalBaseForTests,
|
|
31
|
+
} from "../../auto-worktree.ts";
|
|
32
|
+
import {
|
|
33
|
+
openDatabaseByWorkspace,
|
|
34
|
+
closeAllDatabases,
|
|
35
|
+
_getDbCache,
|
|
36
|
+
} from "../../gsd-db.ts";
|
|
37
|
+
|
|
38
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function makeProjectDir(): string {
|
|
41
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-collapse-int-")));
|
|
42
|
+
mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
|
|
43
|
+
return dir;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function git(args: string[], cwd: string): void {
|
|
47
|
+
execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeGitRepo(): string {
|
|
51
|
+
const dir = makeProjectDir();
|
|
52
|
+
git(["init"], dir);
|
|
53
|
+
git(["config", "user.email", "test@gsd.test"], dir);
|
|
54
|
+
git(["config", "user.name", "GSD Test"], dir);
|
|
55
|
+
writeFileSync(join(dir, "README.md"), "# test\n");
|
|
56
|
+
git(["add", "README.md"], dir);
|
|
57
|
+
git(["commit", "-m", "init"], dir);
|
|
58
|
+
git(["branch", "-M", "main"], dir);
|
|
59
|
+
return dir;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Test 1: Writer/validator path agreement under cwd-drift ─────────────────
|
|
63
|
+
|
|
64
|
+
describe("workspace-collapse integration: Test 1 — cwd-drift path agreement", () => {
|
|
65
|
+
let projectDir: string;
|
|
66
|
+
let otherDir: string;
|
|
67
|
+
const savedCwd = process.cwd();
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
projectDir = makeProjectDir();
|
|
71
|
+
otherDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cwd-drift-other-")));
|
|
72
|
+
_clearGsdRootCache();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
process.chdir(savedCwd);
|
|
77
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
78
|
+
rmSync(otherDir, { recursive: true, force: true });
|
|
79
|
+
_clearGsdRootCache();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("contextFile() returns same absolute path before and after cwd change", () => {
|
|
83
|
+
const worktreeDir = join(projectDir, ".gsd", "worktrees", "M001");
|
|
84
|
+
mkdirSync(worktreeDir, { recursive: true });
|
|
85
|
+
|
|
86
|
+
const ws = createWorkspace(projectDir);
|
|
87
|
+
const scope = scopeMilestone(ws, "M001");
|
|
88
|
+
|
|
89
|
+
// Record the path the "writer" would use
|
|
90
|
+
const writerPath = scope.contextFile();
|
|
91
|
+
assert.ok(writerPath.startsWith(projectDir), "writer path is under projectDir");
|
|
92
|
+
|
|
93
|
+
// Simulate cwd drift
|
|
94
|
+
process.chdir(otherDir);
|
|
95
|
+
assert.notEqual(process.cwd(), projectDir, "cwd has drifted away from projectDir");
|
|
96
|
+
|
|
97
|
+
// The "validator" recomputes via the same scope
|
|
98
|
+
const validatorPath = scope.contextFile();
|
|
99
|
+
|
|
100
|
+
assert.equal(
|
|
101
|
+
validatorPath,
|
|
102
|
+
writerPath,
|
|
103
|
+
"contextFile() must return the same absolute path regardless of cwd drift",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("scopeMilestone paths are stable across cwd changes (roadmap, state, db)", () => {
|
|
108
|
+
const ws = createWorkspace(projectDir);
|
|
109
|
+
const scope = scopeMilestone(ws, "M001");
|
|
110
|
+
|
|
111
|
+
const before = {
|
|
112
|
+
roadmap: scope.roadmapFile(),
|
|
113
|
+
state: scope.stateFile(),
|
|
114
|
+
db: scope.dbPath(),
|
|
115
|
+
milestoneDir: scope.milestoneDir(),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
process.chdir(otherDir);
|
|
119
|
+
|
|
120
|
+
assert.equal(scope.roadmapFile(), before.roadmap, "roadmapFile() stable after cwd drift");
|
|
121
|
+
assert.equal(scope.stateFile(), before.state, "stateFile() stable after cwd drift");
|
|
122
|
+
assert.equal(scope.dbPath(), before.db, "dbPath() stable after cwd drift");
|
|
123
|
+
assert.equal(scope.milestoneDir(), before.milestoneDir, "milestoneDir() stable after cwd drift");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ─── Test 2: Abort path leaves no stale state ────────────────────────────────
|
|
128
|
+
|
|
129
|
+
describe("workspace-collapse integration: Test 2 — abort teardown clears stale state", () => {
|
|
130
|
+
let repoDir: string;
|
|
131
|
+
const savedCwd = process.cwd();
|
|
132
|
+
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
repoDir = makeGitRepo();
|
|
135
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
afterEach(() => {
|
|
139
|
+
process.chdir(savedCwd);
|
|
140
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
141
|
+
rmSync(repoDir, { recursive: true, force: true });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("STATE.md, auto.lock, and M001-META.json are removed by teardownAutoWorktree", () => {
|
|
145
|
+
const gsdDir = join(repoDir, ".gsd");
|
|
146
|
+
const milestonesDir = join(gsdDir, "milestones", "M001");
|
|
147
|
+
mkdirSync(milestonesDir, { recursive: true });
|
|
148
|
+
|
|
149
|
+
const stateMd = join(gsdDir, "STATE.md");
|
|
150
|
+
const autoLock = join(gsdDir, "auto.lock");
|
|
151
|
+
const metaJson = join(milestonesDir, "M001-META.json");
|
|
152
|
+
|
|
153
|
+
writeFileSync(stateMd, "# State\nactive\n");
|
|
154
|
+
writeFileSync(autoLock, JSON.stringify({ pid: process.pid, unitType: "plan-milestone", unitId: "M001" }));
|
|
155
|
+
writeFileSync(metaJson, JSON.stringify({ milestoneId: "M001" }));
|
|
156
|
+
|
|
157
|
+
assert.ok(existsSync(stateMd), "STATE.md exists before teardown");
|
|
158
|
+
assert.ok(existsSync(autoLock), "auto.lock exists before teardown");
|
|
159
|
+
assert.ok(existsSync(metaJson), "M001-META.json exists before teardown");
|
|
160
|
+
|
|
161
|
+
// teardownAutoWorktree clears state files before the git step; git removal
|
|
162
|
+
// may fail in a minimal test repo — that is acceptable.
|
|
163
|
+
try {
|
|
164
|
+
teardownAutoWorktree(repoDir, "M001");
|
|
165
|
+
} catch {
|
|
166
|
+
// git worktree removal may fail when no worktree was created — non-fatal for this assertion
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
assert.ok(!existsSync(stateMd), "STATE.md removed by teardownAutoWorktree (regression: A5)");
|
|
170
|
+
assert.ok(!existsSync(autoLock), "auto.lock removed by teardownAutoWorktree (regression: A5)");
|
|
171
|
+
assert.ok(!existsSync(metaJson), "M001-META.json removed by teardownAutoWorktree (regression: A5)");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ─── Test 3: Cwd drift between persist and load of write-gate state ──────────
|
|
176
|
+
|
|
177
|
+
describe("workspace-collapse integration: Test 3 — write-gate snapshot survives cwd drift", () => {
|
|
178
|
+
let projectDir: string;
|
|
179
|
+
let otherDir: string;
|
|
180
|
+
const savedCwd = process.cwd();
|
|
181
|
+
|
|
182
|
+
beforeEach(() => {
|
|
183
|
+
projectDir = makeProjectDir();
|
|
184
|
+
otherDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-wg-other-")));
|
|
185
|
+
// Start with a clean write-gate state for projectDir
|
|
186
|
+
clearDiscussionFlowState(projectDir);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
afterEach(() => {
|
|
190
|
+
process.chdir(savedCwd);
|
|
191
|
+
clearDiscussionFlowState(projectDir);
|
|
192
|
+
try { clearDiscussionFlowState(otherDir); } catch { /* best-effort */ }
|
|
193
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
194
|
+
rmSync(otherDir, { recursive: true, force: true });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("loadWriteGateSnapshot returns persisted state after cwd drift", () => {
|
|
198
|
+
// Persist a snapshot: mark M001 depth-verified for projectDir
|
|
199
|
+
markDepthVerified("M001", projectDir);
|
|
200
|
+
|
|
201
|
+
// Drift cwd away from projectDir
|
|
202
|
+
process.chdir(otherDir);
|
|
203
|
+
assert.notEqual(process.cwd(), projectDir, "cwd has drifted");
|
|
204
|
+
|
|
205
|
+
// Load the snapshot using the explicit basePath — must not be affected by cwd
|
|
206
|
+
const snapshot = loadWriteGateSnapshot(projectDir);
|
|
207
|
+
|
|
208
|
+
assert.ok(
|
|
209
|
+
snapshot.verifiedDepthMilestones.includes("M001"),
|
|
210
|
+
"snapshot loaded from projectDir includes M001 despite cwd drift",
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("loadWriteGateSnapshot from different basePath does not bleed state", () => {
|
|
215
|
+
markDepthVerified("M001", projectDir);
|
|
216
|
+
|
|
217
|
+
process.chdir(otherDir);
|
|
218
|
+
|
|
219
|
+
// otherDir has no persisted state — should return empty snapshot
|
|
220
|
+
const snapshot = loadWriteGateSnapshot(otherDir);
|
|
221
|
+
|
|
222
|
+
assert.ok(
|
|
223
|
+
!snapshot.verifiedDepthMilestones.includes("M001"),
|
|
224
|
+
"otherDir snapshot must not bleed M001 state from projectDir",
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ─── Test 4: Sibling worktrees share DB connection ───────────────────────────
|
|
230
|
+
|
|
231
|
+
describe("workspace-collapse integration: Test 4 — sibling worktrees share DB connection", () => {
|
|
232
|
+
let projectDir: string;
|
|
233
|
+
|
|
234
|
+
beforeEach(() => {
|
|
235
|
+
projectDir = makeProjectDir();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
afterEach(() => {
|
|
239
|
+
closeAllDatabases();
|
|
240
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("ws1 and ws2 (sibling worktrees) have same identityKey", () => {
|
|
244
|
+
const wt1 = join(projectDir, ".gsd", "worktrees", "M001");
|
|
245
|
+
const wt2 = join(projectDir, ".gsd", "worktrees", "M002");
|
|
246
|
+
mkdirSync(wt1, { recursive: true });
|
|
247
|
+
mkdirSync(wt2, { recursive: true });
|
|
248
|
+
|
|
249
|
+
const ws1 = createWorkspace(wt1);
|
|
250
|
+
const ws2 = createWorkspace(wt2);
|
|
251
|
+
|
|
252
|
+
assert.equal(
|
|
253
|
+
ws1.identityKey,
|
|
254
|
+
ws2.identityKey,
|
|
255
|
+
"sibling worktrees M001 and M002 must share the same identityKey",
|
|
256
|
+
);
|
|
257
|
+
assert.equal(
|
|
258
|
+
ws1.identityKey,
|
|
259
|
+
realpathSync(projectDir),
|
|
260
|
+
"identityKey is the realpath of the project root",
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("openDatabaseByWorkspace for sibling worktrees resolves to the same DB path", () => {
|
|
265
|
+
const wt1 = join(projectDir, ".gsd", "worktrees", "M001");
|
|
266
|
+
const wt2 = join(projectDir, ".gsd", "worktrees", "M002");
|
|
267
|
+
mkdirSync(wt1, { recursive: true });
|
|
268
|
+
mkdirSync(wt2, { recursive: true });
|
|
269
|
+
|
|
270
|
+
const ws1 = createWorkspace(wt1);
|
|
271
|
+
const ws2 = createWorkspace(wt2);
|
|
272
|
+
|
|
273
|
+
const ok1 = openDatabaseByWorkspace(ws1);
|
|
274
|
+
assert.ok(ok1, "openDatabaseByWorkspace(ws1) must succeed");
|
|
275
|
+
const cacheAfterWs1 = _getDbCache();
|
|
276
|
+
const entry1 = cacheAfterWs1.get(ws1.identityKey);
|
|
277
|
+
assert.ok(entry1, "cache entry for ws1.identityKey must exist");
|
|
278
|
+
const dbPath1 = entry1.dbPath;
|
|
279
|
+
|
|
280
|
+
const ok2 = openDatabaseByWorkspace(ws2);
|
|
281
|
+
assert.ok(ok2, "openDatabaseByWorkspace(ws2) must succeed");
|
|
282
|
+
const cacheAfterWs2 = _getDbCache();
|
|
283
|
+
const entry2 = cacheAfterWs2.get(ws2.identityKey);
|
|
284
|
+
assert.ok(entry2, "cache entry for ws2.identityKey must exist");
|
|
285
|
+
const dbPath2 = entry2.dbPath;
|
|
286
|
+
|
|
287
|
+
assert.equal(
|
|
288
|
+
dbPath1,
|
|
289
|
+
dbPath2,
|
|
290
|
+
"sibling worktrees must resolve to the same DB path (shared WAL)",
|
|
291
|
+
);
|
|
292
|
+
assert.equal(
|
|
293
|
+
cacheAfterWs2.size,
|
|
294
|
+
1,
|
|
295
|
+
"only one cache entry for project + two sibling worktrees",
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// ─── Test 5: gsdRootCache normalization survives trailing-slash inputs ────────
|
|
301
|
+
|
|
302
|
+
describe("workspace-collapse integration: Test 5 — gsdRootCache normalization deduplicates trailing-slash inputs", () => {
|
|
303
|
+
let projectDir: string;
|
|
304
|
+
let fakeHome: string;
|
|
305
|
+
let savedHome: string | undefined;
|
|
306
|
+
let savedUserProfile: string | undefined;
|
|
307
|
+
let savedGsdHome: string | undefined;
|
|
308
|
+
|
|
309
|
+
beforeEach(() => {
|
|
310
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cache-int-")));
|
|
311
|
+
mkdirSync(join(projectDir, ".gsd"), { recursive: true });
|
|
312
|
+
|
|
313
|
+
fakeHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cache-int-home-")));
|
|
314
|
+
|
|
315
|
+
savedHome = process.env.HOME;
|
|
316
|
+
savedUserProfile = process.env.USERPROFILE;
|
|
317
|
+
savedGsdHome = process.env.GSD_HOME;
|
|
318
|
+
|
|
319
|
+
// Prevent ~/.gsd interference
|
|
320
|
+
process.env.HOME = fakeHome;
|
|
321
|
+
process.env.USERPROFILE = fakeHome;
|
|
322
|
+
process.env.GSD_HOME = join(fakeHome, ".gsd");
|
|
323
|
+
|
|
324
|
+
clearPathCache();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
afterEach(() => {
|
|
328
|
+
if (savedHome === undefined) delete process.env.HOME;
|
|
329
|
+
else process.env.HOME = savedHome;
|
|
330
|
+
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
|
|
331
|
+
else process.env.USERPROFILE = savedUserProfile;
|
|
332
|
+
if (savedGsdHome === undefined) delete process.env.GSD_HOME;
|
|
333
|
+
else process.env.GSD_HOME = savedGsdHome;
|
|
334
|
+
|
|
335
|
+
clearPathCache();
|
|
336
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
337
|
+
rmSync(fakeHome, { recursive: true, force: true });
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("gsdRoot('/path/to/project') and gsdRoot('/path/to/project/') return identical paths", () => {
|
|
341
|
+
const withoutSlash = gsdRoot(projectDir);
|
|
342
|
+
const withSlash = gsdRoot(projectDir + "/");
|
|
343
|
+
|
|
344
|
+
assert.equal(
|
|
345
|
+
withoutSlash,
|
|
346
|
+
withSlash,
|
|
347
|
+
"gsdRoot must return identical paths for inputs with and without trailing slash",
|
|
348
|
+
);
|
|
349
|
+
assert.equal(
|
|
350
|
+
withoutSlash,
|
|
351
|
+
join(projectDir, ".gsd"),
|
|
352
|
+
"both calls must resolve to projectDir/.gsd",
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("both calls after clearPathCache() return identical paths (no duplicate cache entries)", () => {
|
|
357
|
+
// Start clean
|
|
358
|
+
clearPathCache();
|
|
359
|
+
|
|
360
|
+
const r1 = gsdRoot(projectDir);
|
|
361
|
+
const r2 = gsdRoot(projectDir + "/");
|
|
362
|
+
|
|
363
|
+
assert.equal(r1, r2, "r1 and r2 must be the same string after normalization");
|
|
364
|
+
// The cache normalizes both inputs to the same key — no duplicate entries.
|
|
365
|
+
// We can't inspect the cache size directly, but the behavioral proof is
|
|
366
|
+
// that a second call after clearPathCache re-probes and still matches.
|
|
367
|
+
clearPathCache();
|
|
368
|
+
const r3 = gsdRoot(projectDir + "/");
|
|
369
|
+
assert.equal(r3, r1, "re-probe after clearPathCache must produce the same result");
|
|
370
|
+
});
|
|
371
|
+
});
|
|
@@ -856,10 +856,10 @@ test('── markdown-renderer: renderAllFromDb produces all files ──', asyn
|
|
|
856
856
|
});
|
|
857
857
|
|
|
858
858
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
859
|
-
//
|
|
859
|
+
// DB-authoritative regeneration
|
|
860
860
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
861
861
|
|
|
862
|
-
test('── markdown-renderer:
|
|
862
|
+
test('── markdown-renderer: missing artifact regenerates from DB without importing disk projection ──', async () => {
|
|
863
863
|
const tmpDir = makeTmpDir();
|
|
864
864
|
const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
|
|
865
865
|
openDatabase(dbPath);
|
|
@@ -874,7 +874,7 @@ test('── markdown-renderer: graceful fallback reads from disk when artifact
|
|
|
874
874
|
// Write roadmap to disk but NOT in artifacts DB
|
|
875
875
|
const roadmapContent = makeRoadmapContent([
|
|
876
876
|
{ id: 'S01', title: 'Core', done: false },
|
|
877
|
-
]);
|
|
877
|
+
]) + '\n\nDISK_ONLY_SENTINEL';
|
|
878
878
|
const roadmapPath = path.join(tmpDir, '.gsd', 'milestones', 'M001', 'M001-ROADMAP.md');
|
|
879
879
|
fs.writeFileSync(roadmapPath, roadmapContent);
|
|
880
880
|
clearAllCaches();
|
|
@@ -883,14 +883,20 @@ test('── markdown-renderer: graceful fallback reads from disk when artifact
|
|
|
883
883
|
const before = getArtifact('milestones/M001/M001-ROADMAP.md');
|
|
884
884
|
assert.deepStrictEqual(before, null, 'artifact not in DB before render');
|
|
885
885
|
|
|
886
|
-
// Render — should
|
|
886
|
+
// Render — should regenerate from DB rows, not import/patch disk content.
|
|
887
887
|
const ok = await renderRoadmapCheckboxes(tmpDir, 'M001');
|
|
888
|
-
assert.ok(ok, 'render succeeds
|
|
888
|
+
assert.ok(ok, 'render succeeds by regenerating from DB');
|
|
889
889
|
|
|
890
|
-
// Verify artifact now in DB
|
|
890
|
+
// Verify artifact now exists in DB but does not contain disk-only content.
|
|
891
891
|
const after = getArtifact('milestones/M001/M001-ROADMAP.md');
|
|
892
|
-
assert.ok(after !== null, 'artifact
|
|
893
|
-
assert.ok(after!.full_content.includes('
|
|
892
|
+
assert.ok(after !== null, 'artifact regenerated in DB');
|
|
893
|
+
assert.ok(!after!.full_content.includes('DISK_ONLY_SENTINEL'), 'disk projection content was not imported');
|
|
894
|
+
assert.ok(after!.full_content.includes('S01'), 'DB artifact reflects DB slice state');
|
|
895
|
+
|
|
896
|
+
assert.ok(fs.existsSync(roadmapPath), 'roadmap projection regenerated on disk');
|
|
897
|
+
const diskAfter = fs.readFileSync(roadmapPath, 'utf-8');
|
|
898
|
+
assert.ok(!diskAfter.includes('DISK_ONLY_SENTINEL'), 'disk projection was rewritten from DB');
|
|
899
|
+
assert.ok(diskAfter.includes('S01'), 'disk projection reflects DB slice state');
|
|
894
900
|
} finally {
|
|
895
901
|
closeDatabase();
|
|
896
902
|
cleanupDir(tmpDir);
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getRequirementById,
|
|
10
10
|
getActiveRequirements,
|
|
11
11
|
insertArtifact,
|
|
12
|
+
SCHEMA_VERSION,
|
|
12
13
|
_getAdapter,
|
|
13
14
|
} from '../gsd-db.ts';
|
|
14
15
|
import {
|
|
@@ -363,7 +364,7 @@ test('md-importer: schema v1→v2 migration', () => {
|
|
|
363
364
|
openDatabase(':memory:');
|
|
364
365
|
const adapter = _getAdapter();
|
|
365
366
|
const version = adapter?.prepare('SELECT MAX(version) as v FROM schema_version').get();
|
|
366
|
-
assert.deepStrictEqual(version?.v,
|
|
367
|
+
assert.deepStrictEqual(version?.v, SCHEMA_VERSION, `new DB should be at schema version ${SCHEMA_VERSION}`);
|
|
367
368
|
|
|
368
369
|
// Artifacts table should exist
|
|
369
370
|
const tableCheck = adapter?.prepare("SELECT count(*) as c FROM sqlite_master WHERE type='table' AND name='artifacts'").get();
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
openDatabase,
|
|
3
3
|
closeDatabase,
|
|
4
4
|
isDbAvailable,
|
|
5
|
+
SCHEMA_VERSION,
|
|
5
6
|
_getAdapter,
|
|
6
7
|
} from '../gsd-db.ts';
|
|
7
8
|
import {
|
|
@@ -328,9 +329,9 @@ test('memory-store: schema includes memories table', () => {
|
|
|
328
329
|
const viewCount = adapter.prepare('SELECT count(*) as cnt FROM active_memories').get();
|
|
329
330
|
assert.deepStrictEqual(viewCount?.['cnt'], 0, 'active_memories view should exist');
|
|
330
331
|
|
|
331
|
-
// Verify schema version is
|
|
332
|
+
// Verify schema version is current (includes quality_gates DDL fix and later migrations)
|
|
332
333
|
const version = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
|
|
333
|
-
assert.deepStrictEqual(version?.["v"],
|
|
334
|
+
assert.deepStrictEqual(version?.["v"], SCHEMA_VERSION, `schema version should be ${SCHEMA_VERSION}`);
|
|
334
335
|
|
|
335
336
|
closeDatabase();
|
|
336
337
|
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD2 Metrics — regression test for parallel-mode atomic merge
|
|
3
|
+
*
|
|
4
|
+
* Verifies that concurrent metrics.json writers do not silently discard
|
|
5
|
+
* each other's entries (last-writer-wins). Two child processes each write
|
|
6
|
+
* a distinct milestone unit; after both complete, the merged file must
|
|
7
|
+
* contain both units.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import {
|
|
13
|
+
mkdtempSync,
|
|
14
|
+
mkdirSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { tmpdir } from "node:os";
|
|
21
|
+
import { spawnSync } from "node:child_process";
|
|
22
|
+
|
|
23
|
+
// ─── Worker script source ────────────────────────────────────────────────────
|
|
24
|
+
//
|
|
25
|
+
// Each child process runs this script with two env vars:
|
|
26
|
+
// GSD_TEST_METRICS_PATH — absolute path to metrics.json
|
|
27
|
+
// GSD_TEST_MILESTONE_ID — milestone ID to record (e.g. "M001" or "M002")
|
|
28
|
+
//
|
|
29
|
+
// The script uses the same lock-acquire → read → merge → atomic-write
|
|
30
|
+
// pattern implemented in metrics.ts saveLedger(), but using only built-in
|
|
31
|
+
// Node.js modules so it runs without the full extension dependency tree.
|
|
32
|
+
//
|
|
33
|
+
const WORKER_SCRIPT = `
|
|
34
|
+
const { openSync, closeSync, unlinkSync, existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } = require('node:fs');
|
|
35
|
+
const { dirname } = require('node:path');
|
|
36
|
+
const { randomBytes } = require('node:crypto');
|
|
37
|
+
|
|
38
|
+
const metricsPath = process.env.GSD_TEST_METRICS_PATH;
|
|
39
|
+
const milestoneId = process.env.GSD_TEST_MILESTONE_ID;
|
|
40
|
+
const lockPath = metricsPath + '.lock';
|
|
41
|
+
|
|
42
|
+
// ── Lock helpers ──────────────────────────────────────────────────────────
|
|
43
|
+
function acquireLock(lockPath, timeoutMs) {
|
|
44
|
+
const deadline = Date.now() + timeoutMs;
|
|
45
|
+
while (Date.now() < deadline) {
|
|
46
|
+
try {
|
|
47
|
+
const fd = openSync(lockPath, 'wx');
|
|
48
|
+
closeSync(fd);
|
|
49
|
+
return true;
|
|
50
|
+
} catch {
|
|
51
|
+
const waitUntil = Date.now() + Math.min(50, deadline - Date.now());
|
|
52
|
+
while (Date.now() < waitUntil) { /* spin */ }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function releaseLock(lockPath) {
|
|
59
|
+
try { unlinkSync(lockPath); } catch {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Atomic write helper ───────────────────────────────────────────────────
|
|
63
|
+
function saveJsonAtomic(filePath, data) {
|
|
64
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
65
|
+
const tmp = filePath + '.tmp.' + randomBytes(4).toString('hex');
|
|
66
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\\n', 'utf-8');
|
|
67
|
+
renameSync(tmp, filePath);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Dedup helper (same logic as metrics.ts deduplicateUnits) ─────────────
|
|
71
|
+
function deduplicateUnits(units) {
|
|
72
|
+
const map = new Map();
|
|
73
|
+
for (const u of units) {
|
|
74
|
+
const key = u.type + '\\0' + u.id + '\\0' + u.startedAt;
|
|
75
|
+
const existing = map.get(key);
|
|
76
|
+
if (!existing || u.finishedAt > existing.finishedAt) {
|
|
77
|
+
map.set(key, u);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return Array.from(map.values());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Worker unit ───────────────────────────────────────────────────────────
|
|
84
|
+
const workerUnit = {
|
|
85
|
+
type: 'execute-task',
|
|
86
|
+
id: milestoneId + '/S01/T01',
|
|
87
|
+
model: 'test-model',
|
|
88
|
+
startedAt: 1000,
|
|
89
|
+
finishedAt: Date.now(),
|
|
90
|
+
tokens: { input: 100, output: 50, cacheRead: 0, cacheWrite: 0, total: 150 },
|
|
91
|
+
cost: 0.01,
|
|
92
|
+
toolCalls: 1,
|
|
93
|
+
assistantMessages: 1,
|
|
94
|
+
userMessages: 1,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const workerLedger = {
|
|
98
|
+
version: 1,
|
|
99
|
+
projectStartedAt: 1000,
|
|
100
|
+
units: [workerUnit],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ── Merge write ───────────────────────────────────────────────────────────
|
|
104
|
+
const acquired = acquireLock(lockPath, 5000);
|
|
105
|
+
try {
|
|
106
|
+
let onDiskUnits = [];
|
|
107
|
+
if (existsSync(metricsPath)) {
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(readFileSync(metricsPath, 'utf-8'));
|
|
110
|
+
if (parsed && Array.isArray(parsed.units)) onDiskUnits = parsed.units;
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
113
|
+
const merged = deduplicateUnits([...onDiskUnits, ...workerLedger.units]);
|
|
114
|
+
saveJsonAtomic(metricsPath, { ...workerLedger, units: merged });
|
|
115
|
+
} finally {
|
|
116
|
+
if (acquired) releaseLock(lockPath);
|
|
117
|
+
}
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
function spawnWorker(metricsPath: string, milestoneId: string): void {
|
|
123
|
+
const result = spawnSync(process.execPath, ["-e", WORKER_SCRIPT], {
|
|
124
|
+
env: {
|
|
125
|
+
...process.env,
|
|
126
|
+
GSD_TEST_METRICS_PATH: metricsPath,
|
|
127
|
+
GSD_TEST_MILESTONE_ID: milestoneId,
|
|
128
|
+
},
|
|
129
|
+
encoding: "utf-8",
|
|
130
|
+
timeout: 10_000,
|
|
131
|
+
});
|
|
132
|
+
if (result.error) throw result.error;
|
|
133
|
+
if (result.status !== 0) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Worker for ${milestoneId} exited with status ${result.status}:\n${result.stderr}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
describe("metrics atomic merge — parallel workers", () => {
|
|
143
|
+
let tmpDir: string;
|
|
144
|
+
let gsdDir: string;
|
|
145
|
+
let metricsPath: string;
|
|
146
|
+
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
tmpDir = mkdtempSync(join(tmpdir(), "gsd-metrics-atomic-"));
|
|
149
|
+
gsdDir = join(tmpDir, ".gsd");
|
|
150
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
151
|
+
metricsPath = join(gsdDir, "metrics.json");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
afterEach(() => {
|
|
155
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("sequential writes from two workers both land in metrics.json", () => {
|
|
159
|
+
// Sequential baseline: M001 then M002. Both must survive.
|
|
160
|
+
spawnWorker(metricsPath, "M001");
|
|
161
|
+
spawnWorker(metricsPath, "M002");
|
|
162
|
+
|
|
163
|
+
const raw = readFileSync(metricsPath, "utf-8");
|
|
164
|
+
const ledger = JSON.parse(raw);
|
|
165
|
+
|
|
166
|
+
assert.ok(Array.isArray(ledger.units), "units must be an array");
|
|
167
|
+
|
|
168
|
+
const ids = ledger.units.map((u: { id: string }) => u.id) as string[];
|
|
169
|
+
assert.ok(ids.some(id => id.startsWith("M001")), "M001 unit must be present");
|
|
170
|
+
assert.ok(ids.some(id => id.startsWith("M002")), "M002 unit must be present");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("concurrent writes from two workers both land in metrics.json (no last-writer-wins)", () => {
|
|
174
|
+
// Write an existing M001 entry to disk first, then run M002 worker.
|
|
175
|
+
// This simulates the race: M001 finishes and saves, then M002 reads-merges-writes.
|
|
176
|
+
const initialLedger = {
|
|
177
|
+
version: 1,
|
|
178
|
+
projectStartedAt: 1000,
|
|
179
|
+
units: [
|
|
180
|
+
{
|
|
181
|
+
type: "execute-task",
|
|
182
|
+
id: "M001/S01/T01",
|
|
183
|
+
model: "test-model",
|
|
184
|
+
startedAt: 1000,
|
|
185
|
+
finishedAt: 2000,
|
|
186
|
+
tokens: { input: 100, output: 50, cacheRead: 0, cacheWrite: 0, total: 150 },
|
|
187
|
+
cost: 0.01,
|
|
188
|
+
toolCalls: 1,
|
|
189
|
+
assistantMessages: 1,
|
|
190
|
+
userMessages: 1,
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
writeFileSync(metricsPath, JSON.stringify(initialLedger, null, 2) + "\n", "utf-8");
|
|
195
|
+
|
|
196
|
+
// M002 worker runs — without merge semantics it would overwrite M001's data.
|
|
197
|
+
spawnWorker(metricsPath, "M002");
|
|
198
|
+
|
|
199
|
+
const raw = readFileSync(metricsPath, "utf-8");
|
|
200
|
+
const ledger = JSON.parse(raw);
|
|
201
|
+
|
|
202
|
+
assert.ok(Array.isArray(ledger.units), "units must be an array");
|
|
203
|
+
assert.equal(ledger.units.length, 2, "must contain exactly 2 units (M001 + M002)");
|
|
204
|
+
|
|
205
|
+
const ids = ledger.units.map((u: { id: string }) => u.id) as string[];
|
|
206
|
+
assert.ok(ids.some(id => id.startsWith("M001")), "M001 unit must be preserved after M002 write");
|
|
207
|
+
assert.ok(ids.some(id => id.startsWith("M002")), "M002 unit must be present");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("idempotent write does not duplicate units", () => {
|
|
211
|
+
// Writing the same milestone unit twice must not create duplicates.
|
|
212
|
+
spawnWorker(metricsPath, "M001");
|
|
213
|
+
spawnWorker(metricsPath, "M001");
|
|
214
|
+
|
|
215
|
+
const raw = readFileSync(metricsPath, "utf-8");
|
|
216
|
+
const ledger = JSON.parse(raw);
|
|
217
|
+
|
|
218
|
+
assert.ok(Array.isArray(ledger.units), "units must be an array");
|
|
219
|
+
const m001Units = ledger.units.filter((u: { id: string }) => u.id.startsWith("M001"));
|
|
220
|
+
assert.equal(m001Units.length, 1, "duplicate units must be collapsed to one");
|
|
221
|
+
});
|
|
222
|
+
});
|