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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// GSD-2 + metrics.ts: token & cost tracking for auto-mode units
|
|
1
2
|
/**
|
|
2
3
|
* GSD Metrics — Token & Cost Tracking
|
|
3
4
|
*
|
|
@@ -13,12 +14,14 @@
|
|
|
13
14
|
* 4. On crash recovery or fresh start, the ledger is loaded from disk
|
|
14
15
|
*/
|
|
15
16
|
import { join } from "node:path";
|
|
17
|
+
import { openSync, closeSync, unlinkSync, statSync, writeFileSync } from "node:fs";
|
|
16
18
|
import { gsdRoot } from "./paths.js";
|
|
17
19
|
import { getAndClearSkills } from "./skill-telemetry.js";
|
|
18
20
|
import { loadJsonFile, loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
|
|
19
21
|
import { parseUnitId } from "./unit-id.js";
|
|
20
22
|
import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js";
|
|
21
23
|
import { isUnifiedAuditEnabled } from "./uok/audit-toggle.js";
|
|
24
|
+
import { logWarning } from "./workflow-logger.js";
|
|
22
25
|
// Re-export from shared — import directly from format-utils to avoid pulling
|
|
23
26
|
// in the full barrel (mod.js → ui.js → @gsd/pi-tui) which breaks when loaded
|
|
24
27
|
// outside jiti's alias resolution (e.g. dynamic import in auto-loop reports).
|
|
@@ -48,10 +51,15 @@ export function classifyUnitPhase(unitType) {
|
|
|
48
51
|
// ─── In-memory state ──────────────────────────────────────────────────────────
|
|
49
52
|
let ledger = null;
|
|
50
53
|
let basePath = "";
|
|
54
|
+
// Per-workspace ledger map, keyed by workspace.identityKey.
|
|
55
|
+
// Populated by initMetricsByScope; independent of the module singleton.
|
|
56
|
+
const scopedLedgers = new Map();
|
|
51
57
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
52
58
|
/**
|
|
53
59
|
* Initialize the metrics system for a given project.
|
|
54
60
|
* Loads existing ledger from disk if present.
|
|
61
|
+
*
|
|
62
|
+
* @deprecated TODO(C-future): remove module singleton. Use initMetricsByScope instead.
|
|
55
63
|
*/
|
|
56
64
|
export function initMetrics(base) {
|
|
57
65
|
basePath = base;
|
|
@@ -59,6 +67,8 @@ export function initMetrics(base) {
|
|
|
59
67
|
}
|
|
60
68
|
/**
|
|
61
69
|
* Reset in-memory state. Called when auto-mode stops.
|
|
70
|
+
*
|
|
71
|
+
* @deprecated TODO(C-future): remove module singleton. Use resetMetricsByScope instead.
|
|
62
72
|
*/
|
|
63
73
|
export function resetMetrics() {
|
|
64
74
|
ledger = null;
|
|
@@ -67,6 +77,8 @@ export function resetMetrics() {
|
|
|
67
77
|
/**
|
|
68
78
|
* Snapshot usage metrics from the current session before it's wiped.
|
|
69
79
|
* Scans session entries for AssistantMessage usage data.
|
|
80
|
+
*
|
|
81
|
+
* @deprecated TODO(C-future): remove module singleton. Use snapshotUnitMetricsByScope instead.
|
|
70
82
|
*/
|
|
71
83
|
export function snapshotUnitMetrics(ctx, unitType, unitId, startedAt, model, opts) {
|
|
72
84
|
if (!ledger)
|
|
@@ -180,6 +192,147 @@ export function snapshotUnitMetrics(ctx, unitType, unitId, startedAt, model, opt
|
|
|
180
192
|
export function getLedger() {
|
|
181
193
|
return ledger;
|
|
182
194
|
}
|
|
195
|
+
// ─── Scope-aware API (canonical) ─────────────────────────────────────────────
|
|
196
|
+
/**
|
|
197
|
+
* Initialize the metrics system for a given workspace scope.
|
|
198
|
+
* Loads existing ledger from disk into the per-scope ledger map.
|
|
199
|
+
* Does NOT touch the module-level singleton.
|
|
200
|
+
*/
|
|
201
|
+
export function initMetricsByScope(scope) {
|
|
202
|
+
const base = scope.workspace.projectRoot;
|
|
203
|
+
const loaded = loadLedger(base);
|
|
204
|
+
scopedLedgers.set(scope.workspace.identityKey, loaded);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Get the in-memory ledger for the given scope, or null if not initialized.
|
|
208
|
+
*/
|
|
209
|
+
export function getLedgerByScope(scope) {
|
|
210
|
+
return scopedLedgers.get(scope.workspace.identityKey) ?? null;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Reset scoped in-memory state for a workspace. Called when auto-mode stops.
|
|
214
|
+
*/
|
|
215
|
+
export function resetMetricsByScope(scope) {
|
|
216
|
+
scopedLedgers.delete(scope.workspace.identityKey);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Snapshot usage metrics using an explicit workspace scope.
|
|
220
|
+
*
|
|
221
|
+
* This is the canonical variant. It derives the metrics path from
|
|
222
|
+
* scope.workspace.projectRoot rather than the module singleton, so it
|
|
223
|
+
* remains correct across session resume and in multi-workspace processes.
|
|
224
|
+
*
|
|
225
|
+
* Preserves the atomic write-merge logic from saveLedger so concurrent
|
|
226
|
+
* workers cannot silently discard each other's entries.
|
|
227
|
+
*
|
|
228
|
+
* If initMetricsByScope has not been called, the ledger is loaded from
|
|
229
|
+
* disk on first call (lazy init).
|
|
230
|
+
*/
|
|
231
|
+
export function snapshotUnitMetricsByScope(scope, ctx, unitType, unitId, startedAt, model, opts) {
|
|
232
|
+
const base = scope.workspace.projectRoot;
|
|
233
|
+
const key = scope.workspace.identityKey;
|
|
234
|
+
// Lazy init: load from disk if not yet in scoped map.
|
|
235
|
+
if (!scopedLedgers.has(key)) {
|
|
236
|
+
scopedLedgers.set(key, loadLedger(base));
|
|
237
|
+
}
|
|
238
|
+
const scopedLedger = scopedLedgers.get(key);
|
|
239
|
+
const entries = ctx.sessionManager.getEntries();
|
|
240
|
+
if (!entries || entries.length === 0)
|
|
241
|
+
return null;
|
|
242
|
+
const tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
243
|
+
let cost = 0;
|
|
244
|
+
let toolCalls = 0;
|
|
245
|
+
let assistantMessages = 0;
|
|
246
|
+
let userMessages = 0;
|
|
247
|
+
for (const entry of entries) {
|
|
248
|
+
if (entry.type !== "message")
|
|
249
|
+
continue;
|
|
250
|
+
const msg = entry.message;
|
|
251
|
+
if (!msg)
|
|
252
|
+
continue;
|
|
253
|
+
if (msg.role === "assistant") {
|
|
254
|
+
assistantMessages++;
|
|
255
|
+
if (msg.usage) {
|
|
256
|
+
tokens.input += msg.usage.input ?? 0;
|
|
257
|
+
tokens.output += msg.usage.output ?? 0;
|
|
258
|
+
tokens.cacheRead += msg.usage.cacheRead ?? 0;
|
|
259
|
+
tokens.cacheWrite += msg.usage.cacheWrite ?? 0;
|
|
260
|
+
tokens.total += msg.usage.totalTokens ?? 0;
|
|
261
|
+
if (msg.usage.cost != null) {
|
|
262
|
+
const c = msg.usage.cost;
|
|
263
|
+
cost += typeof c === "number" ? c : (c.total ?? 0);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (msg.content && Array.isArray(msg.content)) {
|
|
267
|
+
for (const block of msg.content) {
|
|
268
|
+
if (block.type === "toolCall")
|
|
269
|
+
toolCalls++;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else if (msg.role === "user") {
|
|
274
|
+
userMessages++;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const unit = {
|
|
278
|
+
type: unitType,
|
|
279
|
+
id: unitId,
|
|
280
|
+
model,
|
|
281
|
+
startedAt,
|
|
282
|
+
finishedAt: Date.now(),
|
|
283
|
+
...(opts?.autoSessionKey ? { autoSessionKey: opts.autoSessionKey } : {}),
|
|
284
|
+
tokens,
|
|
285
|
+
cost,
|
|
286
|
+
toolCalls,
|
|
287
|
+
assistantMessages,
|
|
288
|
+
userMessages,
|
|
289
|
+
apiRequests: assistantMessages,
|
|
290
|
+
...(opts?.tier ? { tier: opts.tier } : {}),
|
|
291
|
+
...(opts?.modelDowngraded !== undefined ? { modelDowngraded: opts.modelDowngraded } : {}),
|
|
292
|
+
...(opts?.contextWindowTokens !== undefined ? { contextWindowTokens: opts.contextWindowTokens } : {}),
|
|
293
|
+
...(opts?.truncationSections !== undefined ? { truncationSections: opts.truncationSections } : {}),
|
|
294
|
+
...(opts?.continueHereFired !== undefined ? { continueHereFired: opts.continueHereFired } : {}),
|
|
295
|
+
...(opts?.promptCharCount != null ? { promptCharCount: opts.promptCharCount } : {}),
|
|
296
|
+
...(opts?.baselineCharCount != null ? { baselineCharCount: opts.baselineCharCount } : {}),
|
|
297
|
+
};
|
|
298
|
+
// Auto-capture skill telemetry (#599)
|
|
299
|
+
const skills = getAndClearSkills();
|
|
300
|
+
if (skills.length > 0) {
|
|
301
|
+
unit.skills = skills;
|
|
302
|
+
}
|
|
303
|
+
// Compute cache hit rate
|
|
304
|
+
if (tokens.cacheRead > 0 || tokens.input > 0) {
|
|
305
|
+
const totalInput = tokens.cacheRead + tokens.input;
|
|
306
|
+
unit.cacheHitRate = totalInput > 0 ? Math.round((tokens.cacheRead / totalInput) * 100) : 0;
|
|
307
|
+
}
|
|
308
|
+
// Idempotency guard: update in-place on duplicate, append otherwise.
|
|
309
|
+
const dupeIdx = scopedLedger.units.findIndex((u) => u.type === unit.type && u.id === unit.id && u.startedAt === unit.startedAt);
|
|
310
|
+
if (dupeIdx >= 0) {
|
|
311
|
+
scopedLedger.units[dupeIdx] = unit;
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
scopedLedger.units.push(unit);
|
|
315
|
+
}
|
|
316
|
+
saveLedger(base, scopedLedger);
|
|
317
|
+
if (isUnifiedAuditEnabled()) {
|
|
318
|
+
emitUokAuditEvent(base, buildAuditEnvelope({
|
|
319
|
+
traceId: opts?.traceId ?? `metrics:${unitType}:${unitId}`,
|
|
320
|
+
turnId: opts?.turnId,
|
|
321
|
+
causedBy: opts?.causedBy,
|
|
322
|
+
category: "metrics",
|
|
323
|
+
type: "unit-metrics-snapshot",
|
|
324
|
+
payload: {
|
|
325
|
+
unitType,
|
|
326
|
+
unitId,
|
|
327
|
+
model,
|
|
328
|
+
tokens: unit.tokens,
|
|
329
|
+
cost: unit.cost,
|
|
330
|
+
toolCalls: unit.toolCalls,
|
|
331
|
+
},
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
return unit;
|
|
335
|
+
}
|
|
183
336
|
function emptyTokens() {
|
|
184
337
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
185
338
|
}
|
|
@@ -423,6 +576,12 @@ export function pruneMetricsLedger(base, keepCount) {
|
|
|
423
576
|
if (ledger) {
|
|
424
577
|
ledger.units = ledger.units.slice(-keepCount);
|
|
425
578
|
}
|
|
579
|
+
// Invalidate all scoped ledger cache entries. Prune is rare; clearing the
|
|
580
|
+
// entire map is simpler than tracking which entry belongs to `base`. Without
|
|
581
|
+
// this, scopedLedgers entries for the pruned workspace hold a pre-prune
|
|
582
|
+
// MetricsLedger that snapshotUnitMetricsByScope would merge back in, causing
|
|
583
|
+
// pruned units to reappear in subsequent snapshots.
|
|
584
|
+
scopedLedgers.clear();
|
|
426
585
|
return removed;
|
|
427
586
|
}
|
|
428
587
|
/**
|
|
@@ -461,6 +620,133 @@ function deduplicateUnits(units) {
|
|
|
461
620
|
}
|
|
462
621
|
return Array.from(map.values());
|
|
463
622
|
}
|
|
623
|
+
// How long a lock file must be untouched (in ms) before it is considered
|
|
624
|
+
// orphaned from a crashed process. Set to 2× the acquire timeout.
|
|
625
|
+
export const STALE_LOCK_THRESHOLD_MS = 4000;
|
|
626
|
+
// Retry interval between lock acquire attempts (ms). Caps syscall rate at
|
|
627
|
+
// ~200 attempts over a 2s timeout instead of ~20,000 without any sleep.
|
|
628
|
+
// Exposed for tests.
|
|
629
|
+
export const LOCK_RETRY_INTERVAL_MS = 5;
|
|
630
|
+
// Sync sleep via Atomics.wait — true OS-level sleep, no CPU spin.
|
|
631
|
+
// Int32Array must reference a SharedArrayBuffer; we wait on index 0 which
|
|
632
|
+
// will never be woken by a Atomics.notify, so the wait always times out.
|
|
633
|
+
const _lockSleepBuf = new Int32Array(new SharedArrayBuffer(4));
|
|
634
|
+
function syncSleep(ms) {
|
|
635
|
+
Atomics.wait(_lockSleepBuf, 0, 0, ms);
|
|
636
|
+
}
|
|
637
|
+
// Counts the number of sleepy retries (non-stale-evicting) made by acquireLock
|
|
638
|
+
// across all calls since the last reset. Exported for test instrumentation only.
|
|
639
|
+
let _lockSleepyRetries = 0;
|
|
640
|
+
export function getLockSleepyRetries() { return _lockSleepyRetries; }
|
|
641
|
+
export function resetLockSleepyRetries() { _lockSleepyRetries = 0; }
|
|
642
|
+
/**
|
|
643
|
+
* Acquire an exclusive .lock sentinel file via O_EXCL.
|
|
644
|
+
*
|
|
645
|
+
* Improvements over the original:
|
|
646
|
+
* - No busy spin: the inner `while (Date.now() < waitUntil) {}` spin that
|
|
647
|
+
* burned CPU doing nothing useful is removed. Each retry attempt now makes
|
|
648
|
+
* one `openSync` syscall and immediately re-checks the deadline, which is
|
|
649
|
+
* orders of magnitude cheaper than a tight spin loop.
|
|
650
|
+
* - Stale-lock detection: if the existing lock file's mtime is older than
|
|
651
|
+
* STALE_LOCK_THRESHOLD_MS, the lock is considered orphaned (e.g. the
|
|
652
|
+
* writing process crashed) and is forcibly removed before retrying.
|
|
653
|
+
* A warning is logged so operators can detect crash patterns.
|
|
654
|
+
* - PID stamp: on success, writes the acquiring process's PID and a
|
|
655
|
+
* timestamp into the lock file so external monitors can identify orphans.
|
|
656
|
+
* - Retry sleep: after each non-stale-evicting retry, sleeps
|
|
657
|
+
* LOCK_RETRY_INTERVAL_MS (5ms) via Atomics.wait so the process yields to
|
|
658
|
+
* the OS. This caps syscall rate at ~200–400/s under contention instead of
|
|
659
|
+
* the ~20,000/s that would result from a tight openSync loop.
|
|
660
|
+
* After a stale-lock eviction (lock already removed), no sleep is injected
|
|
661
|
+
* — we retry immediately to close the short race window.
|
|
662
|
+
*
|
|
663
|
+
* Returns true on success, false on timeout.
|
|
664
|
+
*/
|
|
665
|
+
function acquireLock(lockPath, timeoutMs = 2000) {
|
|
666
|
+
const deadline = Date.now() + timeoutMs;
|
|
667
|
+
while (Date.now() < deadline) {
|
|
668
|
+
try {
|
|
669
|
+
const fd = openSync(lockPath, "wx"); // O_WRONLY | O_CREAT | O_EXCL
|
|
670
|
+
closeSync(fd);
|
|
671
|
+
// Write PID stamp so external monitors can identify the lock owner.
|
|
672
|
+
try {
|
|
673
|
+
writeFileSync(lockPath, `${process.pid}\n${new Date().toISOString()}\n`, "utf-8");
|
|
674
|
+
}
|
|
675
|
+
catch { /* non-fatal — stamp is diagnostic only */ }
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
// Lock held by another process — check for staleness before retrying.
|
|
680
|
+
try {
|
|
681
|
+
const st = statSync(lockPath);
|
|
682
|
+
if (Date.now() - st.mtimeMs > STALE_LOCK_THRESHOLD_MS) {
|
|
683
|
+
logWarning("fs", `stale metrics lock at ${lockPath} (age ${Date.now() - st.mtimeMs}ms); forcibly removing and retrying`);
|
|
684
|
+
try {
|
|
685
|
+
unlinkSync(lockPath);
|
|
686
|
+
}
|
|
687
|
+
catch { /* already gone */ }
|
|
688
|
+
// Do NOT sleep after stale-lock eviction — retry the open
|
|
689
|
+
// immediately. The lock file was just removed; a short race window
|
|
690
|
+
// exists and sleeping here would unnecessarily delay recovery.
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
catch { /* lock file disappeared between the failed open and stat — retry */ }
|
|
695
|
+
// Sleep between retries to yield to the OS and cap syscall rate.
|
|
696
|
+
// Uses Atomics.wait for a true blocking sleep (no CPU spin).
|
|
697
|
+
_lockSleepyRetries++;
|
|
698
|
+
syncSleep(LOCK_RETRY_INTERVAL_MS);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
function releaseLock(lockPath) {
|
|
704
|
+
try {
|
|
705
|
+
unlinkSync(lockPath);
|
|
706
|
+
}
|
|
707
|
+
catch { /* ignore */ }
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Save the ledger with cross-process merge semantics.
|
|
711
|
+
*
|
|
712
|
+
* Acquires a .lock sentinel file, reads the current on-disk ledger,
|
|
713
|
+
* merges worker units with existing peer units (worker's entry wins on
|
|
714
|
+
* type+id+startedAt conflict since it has the latest finishedAt),
|
|
715
|
+
* then writes atomically. This prevents parallel auto-mode workers from
|
|
716
|
+
* silently discarding each other's metrics entries.
|
|
717
|
+
*
|
|
718
|
+
* Falls back to a direct write (no merge) if the lock cannot be acquired
|
|
719
|
+
* within the timeout — better to potentially overwrite than to lose data
|
|
720
|
+
* entirely.
|
|
721
|
+
*/
|
|
464
722
|
function saveLedger(base, data) {
|
|
465
|
-
|
|
723
|
+
const path = metricsPath(base);
|
|
724
|
+
const lockPath = `${path}.lock`;
|
|
725
|
+
const acquired = acquireLock(lockPath);
|
|
726
|
+
if (acquired) {
|
|
727
|
+
try {
|
|
728
|
+
// Read current on-disk state and merge with worker's in-memory units.
|
|
729
|
+
// Worker units take precedence on conflict (by finishedAt in deduplicateUnits).
|
|
730
|
+
const onDisk = loadJsonFileOrNull(path, isMetricsLedger);
|
|
731
|
+
if (onDisk && onDisk.units.length > 0) {
|
|
732
|
+
const merged = deduplicateUnits([...onDisk.units, ...data.units]);
|
|
733
|
+
saveJsonFile(path, { ...data, units: merged });
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
saveJsonFile(path, data);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
finally {
|
|
740
|
+
releaseLock(lockPath);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
// Lock could not be acquired within the timeout. Fall back to a direct
|
|
745
|
+
// write (no cross-process merge) to avoid losing this worker's data
|
|
746
|
+
// entirely. A concurrent writer may overwrite us, but that is preferable
|
|
747
|
+
// to a torn write caused by two writers simultaneously executing the
|
|
748
|
+
// read-merge-write sequence without mutual exclusion.
|
|
749
|
+
logWarning("fs", "saveLedger: lock not acquired — falling back to direct write (no merge)");
|
|
750
|
+
saveJsonFile(path, data);
|
|
751
|
+
}
|
|
466
752
|
}
|
|
@@ -8,7 +8,7 @@ import { existsSync, readdirSync } from "node:fs";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { spawnSync } from "node:child_process";
|
|
10
10
|
import { loadFile } from "./files.js";
|
|
11
|
-
import { resolveMilestoneFile } from "./paths.js";
|
|
11
|
+
import { resolveGsdPathContract, resolveMilestoneFile } from "./paths.js";
|
|
12
12
|
import { mergeMilestoneToMain } from "./auto-worktree.js";
|
|
13
13
|
import { MergeConflictError } from "./git-service.js";
|
|
14
14
|
import { removeSessionStatus } from "./session-status-io.js";
|
|
@@ -16,12 +16,13 @@ import { getErrorMessage } from "./error-utils.js";
|
|
|
16
16
|
import { logWarning } from "./workflow-logger.js";
|
|
17
17
|
// ─── Merge Queue ───────────────────────────────────────────────────────────
|
|
18
18
|
/**
|
|
19
|
-
* Check whether a milestone is complete by querying
|
|
19
|
+
* Check whether a milestone is complete by querying the canonical project DB.
|
|
20
20
|
* Uses a subprocess to avoid disrupting the global DB singleton.
|
|
21
|
-
* Returns true when milestones.status = 'complete' in
|
|
21
|
+
* Returns true when milestones.status = 'complete' in project gsd.db.
|
|
22
22
|
*/
|
|
23
|
-
export function
|
|
24
|
-
const
|
|
23
|
+
export function isMilestoneCompleteInProjectDb(basePath, mid) {
|
|
24
|
+
const workRoot = join(basePath, ".gsd", "worktrees", mid);
|
|
25
|
+
const dbPath = resolveGsdPathContract(workRoot, basePath).projectDb;
|
|
25
26
|
if (!existsSync(dbPath))
|
|
26
27
|
return false;
|
|
27
28
|
try {
|
|
@@ -34,15 +35,15 @@ export function isMilestoneCompleteInWorktreeDb(basePath, mid) {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
/**
|
|
37
|
-
* Discover milestone IDs with status='complete' in
|
|
38
|
-
*
|
|
38
|
+
* Discover milestone IDs with status='complete' in the canonical DB,
|
|
39
|
+
* using worktree directories only to enumerate active parallel workers.
|
|
39
40
|
*/
|
|
40
41
|
function discoverDbCompletedMilestones(basePath) {
|
|
41
42
|
const completed = new Set();
|
|
42
43
|
const worktreeDir = join(basePath, ".gsd", "worktrees");
|
|
43
44
|
try {
|
|
44
45
|
for (const entry of readdirSync(worktreeDir)) {
|
|
45
|
-
if (entry.startsWith("M") &&
|
|
46
|
+
if (entry.startsWith("M") && isMilestoneCompleteInProjectDb(basePath, entry)) {
|
|
46
47
|
completed.add(entry);
|
|
47
48
|
}
|
|
48
49
|
}
|
|
@@ -57,15 +58,15 @@ function discoverDbCompletedMilestones(basePath) {
|
|
|
57
58
|
* Sequential: merge in milestone ID order (M001 before M002).
|
|
58
59
|
* By-completion: merge in the order milestones finished.
|
|
59
60
|
*
|
|
60
|
-
* When basePath is provided, also checks
|
|
61
|
-
* source of truth
|
|
62
|
-
* are included if their
|
|
61
|
+
* When basePath is provided, also checks the canonical project DB as the
|
|
62
|
+
* source of truth. Workers with stale orchestrator state (e.g. "error")
|
|
63
|
+
* are included if their project DB row shows status='complete'.
|
|
63
64
|
* See: https://github.com/gsd-build/gsd-2/issues/2812
|
|
64
65
|
*/
|
|
65
66
|
export function determineMergeOrder(workers, order = "sequential", basePath) {
|
|
66
67
|
// Start with workers the orchestrator already knows are stopped
|
|
67
68
|
const stoppedIds = new Set(workers.filter(w => w.state === "stopped").map(w => w.milestoneId));
|
|
68
|
-
// When basePath is available, also check
|
|
69
|
+
// When basePath is available, also check the project DB for milestones
|
|
69
70
|
// whose orchestrator state is stale but are actually complete (#2812)
|
|
70
71
|
const dbCompleted = basePath ? discoverDbCompletedMilestones(basePath) : new Set();
|
|
71
72
|
// Union: milestone is mergeable if stopped OR DB-complete
|
|
@@ -80,7 +81,7 @@ export function determineMergeOrder(workers, order = "sequential", basePath) {
|
|
|
80
81
|
allMergeable.push(w);
|
|
81
82
|
}
|
|
82
83
|
else {
|
|
83
|
-
// Milestone discovered from
|
|
84
|
+
// Milestone discovered from project DB but not in workers list
|
|
84
85
|
allMergeable.push({
|
|
85
86
|
milestoneId: mid,
|
|
86
87
|
title: mid,
|
|
@@ -13,6 +13,7 @@ import { spawnSync } from "node:child_process";
|
|
|
13
13
|
import { matchesKey, Key } from "@gsd/pi-tui";
|
|
14
14
|
import { formatDuration } from "../shared/mod.js";
|
|
15
15
|
import { formattedShortcutPair } from "./shortcut-defs.js";
|
|
16
|
+
import { resolveGsdPathContract } from "./paths.js";
|
|
16
17
|
// ─── Data Helpers ─────────────────────────────────────────────────────────
|
|
17
18
|
function readJsonSafe(filePath) {
|
|
18
19
|
try {
|
|
@@ -74,7 +75,8 @@ function discoverWorkers(basePath) {
|
|
|
74
75
|
return [...mids].sort();
|
|
75
76
|
}
|
|
76
77
|
function querySliceProgress(basePath, mid) {
|
|
77
|
-
const
|
|
78
|
+
const workRoot = join(basePath, ".gsd", "worktrees", mid);
|
|
79
|
+
const dbPath = resolveGsdPathContract(workRoot, basePath).projectDb;
|
|
78
80
|
if (!existsSync(dbPath))
|
|
79
81
|
return [];
|
|
80
82
|
try {
|
|
@@ -119,7 +121,8 @@ function extractCostFromNdjson(basePath, mid) {
|
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
function queryRecentCompletions(basePath, mid) {
|
|
122
|
-
const
|
|
124
|
+
const workRoot = join(basePath, ".gsd", "worktrees", mid);
|
|
125
|
+
const dbPath = resolveGsdPathContract(workRoot, basePath).projectDb;
|
|
123
126
|
if (!existsSync(dbPath))
|
|
124
127
|
return [];
|
|
125
128
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// GSD-2 — ID-based path resolution for GSD project files and directories
|
|
1
2
|
/**
|
|
2
3
|
* GSD Paths — ID-based path resolution
|
|
3
4
|
*
|
|
@@ -9,12 +10,13 @@
|
|
|
9
10
|
* via prefix matching, so existing projects work without migration.
|
|
10
11
|
*/
|
|
11
12
|
import { readdirSync, existsSync, realpathSync, Dirent } from "node:fs";
|
|
12
|
-
import { join, dirname, normalize } from "node:path";
|
|
13
|
+
import { join, dirname, normalize, resolve } from "node:path";
|
|
13
14
|
import { homedir } from "node:os";
|
|
14
15
|
import { spawnSync } from "node:child_process";
|
|
15
16
|
import { nativeScanGsdTree } from "./native-parser-bridge.js";
|
|
16
17
|
import { DIR_CACHE_MAX } from "./constants.js";
|
|
17
18
|
import { gsdHome } from "./gsd-home.js";
|
|
19
|
+
import { isGsdWorktreePath, resolveWorktreeProjectRoot } from "./worktree-root.js";
|
|
18
20
|
// ─── Directory Listing Cache ──────────────────────────────────────────────────
|
|
19
21
|
const dirEntryCache = new Map();
|
|
20
22
|
const dirListCache = new Map();
|
|
@@ -120,9 +122,15 @@ function cachedReaddir(dirPath) {
|
|
|
120
122
|
return entries;
|
|
121
123
|
}
|
|
122
124
|
/**
|
|
123
|
-
* Clear the directory listing
|
|
125
|
+
* Clear the volatile directory listing caches.
|
|
124
126
|
* Call after milestone transitions, file creation in planning directories,
|
|
125
127
|
* or at the start/end of a dispatch cycle.
|
|
128
|
+
*
|
|
129
|
+
* NOTE: This does NOT clear gsdRootCache. The project root is stable for
|
|
130
|
+
* the lifetime of a process; clearing it on every agent turn-end caused a
|
|
131
|
+
* 250–2500 ms regression per session (git rev-parse + dir walk per turn).
|
|
132
|
+
* Use _clearGsdRootCache() at session-reset boundaries (workspace switch,
|
|
133
|
+
* process exit) when the project root may genuinely change.
|
|
126
134
|
*/
|
|
127
135
|
export function clearPathCache() {
|
|
128
136
|
dirEntryCache.clear();
|
|
@@ -269,11 +277,78 @@ const LEGACY_GSD_ROOT_FILES = {
|
|
|
269
277
|
CODEBASE: "codebase.md",
|
|
270
278
|
};
|
|
271
279
|
// ─── GSD Root Discovery ───────────────────────────────────────────────────────
|
|
280
|
+
// Process-lifetime cache for gsdRoot() results.
|
|
281
|
+
// Keys are realpath-normalized (via normCacheKey) so /foo and /foo/ share the
|
|
282
|
+
// same entry and so do case-variant paths on case-insensitive volumes. This
|
|
283
|
+
// normalization is the safety net that prevents cache poisoning from the
|
|
284
|
+
// ~/.gsd walk-up bug (fixed in c46cf4786 + b35e070eb), making it safe to
|
|
285
|
+
// hold this cache for the entire process lifetime.
|
|
286
|
+
// Use _clearGsdRootCache() only at session-reset boundaries (workspace switch,
|
|
287
|
+
// process exit) — NOT inside clearPathCache(), which runs on every agent turn.
|
|
272
288
|
const gsdRootCache = new Map();
|
|
273
|
-
|
|
289
|
+
export function resolveGsdPathContract(workRoot, originalProjectRoot) {
|
|
290
|
+
const resolvedWorkRoot = resolve(workRoot || process.cwd());
|
|
291
|
+
const isWorktree = isGsdWorktreePath(resolvedWorkRoot);
|
|
292
|
+
if (isWorktree && !originalProjectRoot?.trim()) {
|
|
293
|
+
const externalMatch = /[/\\]\.gsd[/\\]projects[/\\][^/\\]+[/\\]worktrees(?:[/\\]|$)/.exec(resolvedWorkRoot);
|
|
294
|
+
if (externalMatch) {
|
|
295
|
+
const worktreesIdx = externalMatch[0].search(/[/\\]worktrees(?:[/\\]|$)/);
|
|
296
|
+
const projectGsd = resolvedWorkRoot.slice(0, externalMatch.index + worktreesIdx);
|
|
297
|
+
return {
|
|
298
|
+
projectRoot: dirname(dirname(projectGsd)),
|
|
299
|
+
workRoot: resolvedWorkRoot,
|
|
300
|
+
projectGsd,
|
|
301
|
+
worktreeGsd: join(resolvedWorkRoot, ".gsd"),
|
|
302
|
+
projectDb: join(projectGsd, "gsd.db"),
|
|
303
|
+
isWorktree,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const projectRoot = resolve(resolveWorktreeProjectRoot(resolvedWorkRoot, originalProjectRoot));
|
|
308
|
+
const projectGsd = join(projectRoot, ".gsd");
|
|
309
|
+
const worktreeGsd = isWorktree ? join(resolvedWorkRoot, ".gsd") : null;
|
|
310
|
+
return {
|
|
311
|
+
projectRoot,
|
|
312
|
+
workRoot: resolvedWorkRoot,
|
|
313
|
+
projectGsd,
|
|
314
|
+
worktreeGsd,
|
|
315
|
+
projectDb: join(projectGsd, "gsd.db"),
|
|
316
|
+
isWorktree,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Invalidate the gsdRoot cache.
|
|
321
|
+
* Use ONLY at session-reset boundaries: workspace switch, process exit, or
|
|
322
|
+
* any context where the project root itself may genuinely change.
|
|
323
|
+
* Do NOT call this on every agent turn — use clearPathCache() for volatile
|
|
324
|
+
* directory listing invalidation instead.
|
|
325
|
+
*/
|
|
274
326
|
export function _clearGsdRootCache() {
|
|
275
327
|
gsdRootCache.clear();
|
|
276
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Resolve a path to its canonical real path using the native resolver.
|
|
331
|
+
* On macOS case-insensitive (HFS+/APFS) volumes, realpathSync.native normalizes
|
|
332
|
+
* case — ensuring that /foo/Bar and /foo/bar resolve to the same string.
|
|
333
|
+
* Falls back to resolve(p) for non-existent paths.
|
|
334
|
+
*
|
|
335
|
+
* Use this helper everywhere a path is used as an identity/cache key so that
|
|
336
|
+
* all callers agree on the canonical form.
|
|
337
|
+
*/
|
|
338
|
+
export function normalizeRealPath(p) {
|
|
339
|
+
try {
|
|
340
|
+
return realpathSync.native(p);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return resolve(p);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/** Normalize a path for use as a gsdRootCache key (realpath + trailing-slash strip). */
|
|
347
|
+
function normCacheKey(p) {
|
|
348
|
+
const r = normalizeRealPath(p);
|
|
349
|
+
const s = r.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
350
|
+
return process.platform === "win32" ? s.toLowerCase() : s;
|
|
351
|
+
}
|
|
277
352
|
/**
|
|
278
353
|
* Resolve the `.gsd` directory for a given project base path.
|
|
279
354
|
*
|
|
@@ -283,19 +358,25 @@ export function _clearGsdRootCache() {
|
|
|
283
358
|
* 3. Walk up from basePath — handles moved .gsd in an ancestor (bounded by git root)
|
|
284
359
|
* 4. basePath/.gsd — creation fallback (init scenario)
|
|
285
360
|
*
|
|
286
|
-
* Result is cached per basePath for the process lifetime.
|
|
361
|
+
* Result is cached per normalized basePath for the process lifetime.
|
|
362
|
+
* Keys are realpath-normalized so /foo and /foo/ share the same cache entry.
|
|
287
363
|
*/
|
|
288
364
|
export function gsdRoot(basePath) {
|
|
289
|
-
const
|
|
365
|
+
const cacheKey = normCacheKey(basePath);
|
|
366
|
+
const cached = gsdRootCache.get(cacheKey);
|
|
290
367
|
if (cached)
|
|
291
368
|
return cached;
|
|
292
|
-
|
|
369
|
+
// Canonicalize result via realpath before asserting and caching so that
|
|
370
|
+
// callers always receive a canonical path regardless of whether probeGsdRoot
|
|
371
|
+
// returned a path through a symlink. Without this, the cached value can
|
|
372
|
+
// diverge from other realpath-normalized paths (e.g. workspace.identityKey).
|
|
373
|
+
const result = normalizeRealPath(probeGsdRoot(basePath));
|
|
293
374
|
// Defense-in-depth: if basePath resolves to the user's home directory and
|
|
294
375
|
// the result equals gsdHome(), refuse — project-scoped writes must never
|
|
295
376
|
// land in the global ~/.gsd. Paths under ~/.gsd/projects/<hash>/ are still
|
|
296
377
|
// valid (their basePath does not equal homedir).
|
|
297
378
|
assertNotGlobalGsdHome(basePath, result);
|
|
298
|
-
gsdRootCache.set(
|
|
379
|
+
gsdRootCache.set(cacheKey, result);
|
|
299
380
|
return result;
|
|
300
381
|
}
|
|
301
382
|
function assertNotGlobalGsdHome(basePath, result) {
|
|
@@ -362,6 +443,9 @@ function isInsideGsdWorktree(p) {
|
|
|
362
443
|
return false;
|
|
363
444
|
}
|
|
364
445
|
function probeGsdRoot(rawBasePath) {
|
|
446
|
+
const contract = resolveGsdPathContract(rawBasePath);
|
|
447
|
+
if (contract.isWorktree)
|
|
448
|
+
return contract.projectGsd;
|
|
365
449
|
// 1. Fast path — check the input path directly
|
|
366
450
|
const local = join(rawBasePath, ".gsd");
|
|
367
451
|
if (existsSync(local))
|
|
@@ -401,9 +485,30 @@ function probeGsdRoot(rawBasePath) {
|
|
|
401
485
|
}
|
|
402
486
|
}
|
|
403
487
|
catch { /* git not available */ }
|
|
488
|
+
// Compute gsdHome once for the skip-check used in steps 2 and 3.
|
|
489
|
+
const normPath = (p) => {
|
|
490
|
+
let r;
|
|
491
|
+
try {
|
|
492
|
+
r = realpathSync.native(p);
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
r = p;
|
|
496
|
+
}
|
|
497
|
+
const s = r.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
498
|
+
return process.platform === "win32" ? s.toLowerCase() : s;
|
|
499
|
+
};
|
|
500
|
+
let gsdHomeNorm;
|
|
501
|
+
try {
|
|
502
|
+
gsdHomeNorm = normPath(gsdHome());
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
gsdHomeNorm = "";
|
|
506
|
+
}
|
|
404
507
|
if (gitRoot) {
|
|
405
508
|
const candidate = join(gitRoot, ".gsd");
|
|
406
|
-
if
|
|
509
|
+
// Skip if the candidate resolves to the global GSD home — a subdir basePath
|
|
510
|
+
// must not be anchored to ~/.gsd just because $HOME is a git repo.
|
|
511
|
+
if (existsSync(candidate) && normPath(candidate) !== gsdHomeNorm)
|
|
407
512
|
return candidate;
|
|
408
513
|
}
|
|
409
514
|
// 3. Walk up from basePath to the git root (only if we are in a subdirectory)
|
|
@@ -411,7 +516,7 @@ function probeGsdRoot(rawBasePath) {
|
|
|
411
516
|
let cur = dirname(basePath);
|
|
412
517
|
while (cur !== basePath) {
|
|
413
518
|
const candidate = join(cur, ".gsd");
|
|
414
|
-
if (existsSync(candidate))
|
|
519
|
+
if (existsSync(candidate) && normPath(candidate) !== gsdHomeNorm)
|
|
415
520
|
return candidate;
|
|
416
521
|
if (cur === gitRoot)
|
|
417
522
|
break;
|