gsd-pi 2.63.0-dev.026d309 → 2.63.0-dev.351157b
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 +46 -134
- package/dist/cli.js +44 -6
- package/dist/help-text.js +4 -1
- package/dist/onboarding.js +15 -8
- package/dist/resource-loader.js +18 -3
- package/dist/resources/extensions/cmux/index.js +21 -12
- package/dist/resources/extensions/gsd/auto/finalize-timeout.js +40 -0
- package/dist/resources/extensions/gsd/auto/loop.js +4 -0
- package/dist/resources/extensions/gsd/auto/phases.js +123 -22
- package/dist/resources/extensions/gsd/auto/session.js +8 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +9 -3
- package/dist/resources/extensions/gsd/auto-post-unit.js +45 -10
- package/dist/resources/extensions/gsd/auto-prompts.js +25 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +15 -7
- package/dist/resources/extensions/gsd/auto-start.js +10 -21
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +17 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +13 -7
- package/dist/resources/extensions/gsd/auto.js +19 -2
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +73 -60
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +13 -0
- package/dist/resources/extensions/gsd/bootstrap/query-tools.js +85 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +3 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +9 -1
- package/dist/resources/extensions/gsd/bootstrap/sanitize-complete-milestone.js +54 -0
- package/dist/resources/extensions/gsd/commands-handlers.js +9 -4
- package/dist/resources/extensions/gsd/constants.js +42 -0
- package/dist/resources/extensions/gsd/db-writer.js +72 -4
- package/dist/resources/extensions/gsd/forensics.js +20 -4
- package/dist/resources/extensions/gsd/gsd-db.js +64 -17
- package/dist/resources/extensions/gsd/guided-flow.js +19 -0
- package/dist/resources/extensions/gsd/metrics.js +27 -1
- package/dist/resources/extensions/gsd/native-git-bridge.js +5 -3
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences.js +7 -2
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +2 -0
- package/dist/resources/extensions/gsd/prompts/doctor-heal.md +1 -0
- package/dist/resources/extensions/gsd/prompts/forensics.md +2 -0
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +2 -0
- package/dist/resources/extensions/gsd/prompts/system.md +1 -0
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
- package/dist/resources/extensions/gsd/roadmap-mutations.js +1 -1
- package/dist/resources/extensions/gsd/roadmap-slices.js +9 -5
- package/dist/resources/extensions/gsd/slice-parallel-conflict.js +67 -0
- package/dist/resources/extensions/gsd/slice-parallel-eligibility.js +51 -0
- package/dist/resources/extensions/gsd/slice-parallel-orchestrator.js +378 -0
- package/dist/resources/extensions/gsd/state.js +74 -14
- package/dist/resources/extensions/gsd/status-guards.js +11 -0
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +17 -12
- package/dist/resources/extensions/gsd/tools/complete-slice.js +40 -26
- package/dist/resources/extensions/gsd/tools/complete-task.js +12 -12
- package/dist/resources/extensions/gsd/tools/plan-milestone.js +33 -25
- package/dist/resources/extensions/gsd/tools/plan-slice.js +5 -8
- package/dist/resources/extensions/gsd/workflow-projections.js +21 -5
- package/dist/resources/extensions/gsd/worktree-manager.js +82 -29
- package/dist/resources/extensions/gsd/worktree-resolver.js +4 -3
- package/dist/resources/extensions/mcp-client/auth.js +101 -0
- package/dist/resources/extensions/mcp-client/index.js +10 -1
- package/dist/resources/extensions/ollama/index.js +6 -12
- package/dist/resources/extensions/ollama/model-capabilities.js +37 -34
- package/dist/resources/extensions/ollama/ndjson-stream.js +54 -0
- package/dist/resources/extensions/ollama/ollama-chat-provider.js +380 -0
- package/dist/resources/extensions/ollama/ollama-client.js +23 -32
- package/dist/resources/extensions/ollama/ollama-discovery.js +2 -7
- package/dist/resources/extensions/ollama/ollama-tool.js +62 -0
- package/dist/resources/extensions/ollama/thinking-parser.js +104 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- 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/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.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 +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 +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 +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 +2 -2
- 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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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/onboarding/route.js +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 +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 +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 +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 +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 +2 -2
- 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 +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 +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 +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 +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 +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 +2 -2
- 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 +2 -2
- 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 +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 +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 +14 -14
- package/dist/web/standalone/.next/server/chunks/6897.js +12 -0
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.d.ts +8 -0
- package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +50 -0
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.test.ts +221 -5
- package/packages/pi-agent-core/src/agent-loop.ts +53 -0
- package/packages/pi-ai/dist/types.d.ts +16 -1
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/src/types.ts +18 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +50 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js +41 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +31 -4
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.test.js +28 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +3 -3
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +23 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.js +80 -56
- package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +10 -0
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/src/core/auth-storage.test.ts +53 -0
- package/packages/pi-coding-agent/src/core/auth-storage.ts +66 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.test.ts +39 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +34 -4
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +2 -0
- package/packages/pi-coding-agent/src/core/model-resolver.ts +3 -3
- package/packages/pi-coding-agent/src/core/resource-loader.ts +89 -56
- package/packages/pi-coding-agent/src/core/sdk.ts +11 -0
- package/src/resources/extensions/cmux/index.ts +18 -12
- package/src/resources/extensions/gsd/auto/finalize-timeout.ts +46 -0
- package/src/resources/extensions/gsd/auto/loop.ts +5 -0
- package/src/resources/extensions/gsd/auto/phases.ts +156 -34
- package/src/resources/extensions/gsd/auto/session.ts +9 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +11 -3
- package/src/resources/extensions/gsd/auto-post-unit.ts +53 -12
- package/src/resources/extensions/gsd/auto-prompts.ts +21 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +9 -8
- package/src/resources/extensions/gsd/auto-start.ts +11 -20
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +19 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +14 -6
- package/src/resources/extensions/gsd/auto.ts +22 -1
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +74 -60
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +15 -0
- package/src/resources/extensions/gsd/bootstrap/query-tools.ts +98 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +4 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -1
- package/src/resources/extensions/gsd/bootstrap/sanitize-complete-milestone.ts +57 -0
- package/src/resources/extensions/gsd/commands-handlers.ts +10 -4
- package/src/resources/extensions/gsd/constants.ts +44 -0
- package/src/resources/extensions/gsd/db-writer.ts +78 -4
- package/src/resources/extensions/gsd/forensics.ts +21 -5
- package/src/resources/extensions/gsd/gsd-db.ts +64 -17
- package/src/resources/extensions/gsd/guided-flow.ts +22 -0
- package/src/resources/extensions/gsd/metrics.ts +28 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +5 -3
- package/src/resources/extensions/gsd/preferences-types.ts +3 -0
- package/src/resources/extensions/gsd/preferences.ts +9 -2
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +2 -0
- package/src/resources/extensions/gsd/prompts/doctor-heal.md +1 -0
- package/src/resources/extensions/gsd/prompts/forensics.md +2 -0
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +2 -0
- package/src/resources/extensions/gsd/prompts/system.md +1 -0
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
- package/src/resources/extensions/gsd/roadmap-mutations.ts +1 -1
- package/src/resources/extensions/gsd/roadmap-slices.ts +10 -5
- package/src/resources/extensions/gsd/slice-parallel-conflict.ts +86 -0
- package/src/resources/extensions/gsd/slice-parallel-eligibility.ts +73 -0
- package/src/resources/extensions/gsd/slice-parallel-orchestrator.ts +477 -0
- package/src/resources/extensions/gsd/state.ts +67 -12
- package/src/resources/extensions/gsd/status-guards.ts +13 -0
- package/src/resources/extensions/gsd/tests/artifact-corruption-2630.test.ts +288 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +34 -13
- package/src/resources/extensions/gsd/tests/cmux.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts +51 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +140 -0
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/dashboard-model-label-ordering.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/db-access-guardrails.test.ts +109 -0
- package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +13 -9
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +134 -0
- package/src/resources/extensions/gsd/tests/deferred-slice-dispatch.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/discuss-tool-scoping.test.ts +130 -0
- package/src/resources/extensions/gsd/tests/doctor-fix-flag.test.ts +92 -0
- package/src/resources/extensions/gsd/tests/finalize-timeout-guard.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/forensics-stuck-loops.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/insert-slice-no-wipe.test.ts +88 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +27 -7
- package/src/resources/extensions/gsd/tests/integration/idle-recovery.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +116 -1
- package/src/resources/extensions/gsd/tests/milestone-status-tool.test.ts +201 -0
- package/src/resources/extensions/gsd/tests/plan-milestone-title.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +82 -18
- package/src/resources/extensions/gsd/tests/preferences.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/shared-wal.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/slice-context-injection.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/slice-parallel-conflict.test.ts +92 -0
- package/src/resources/extensions/gsd/tests/slice-parallel-eligibility.test.ts +95 -0
- package/src/resources/extensions/gsd/tests/slice-parallel-orchestrator.test.ts +83 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +349 -0
- package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +35 -2
- package/src/resources/extensions/gsd/tests/worktree-health-monorepo.test.ts +73 -0
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/worktree-submodule-safety.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-teardown-safety.test.ts +148 -0
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +34 -20
- package/src/resources/extensions/gsd/tools/complete-slice.ts +41 -26
- package/src/resources/extensions/gsd/tools/complete-task.ts +12 -12
- package/src/resources/extensions/gsd/tools/plan-milestone.ts +55 -30
- package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -8
- package/src/resources/extensions/gsd/types.ts +44 -22
- package/src/resources/extensions/gsd/workflow-projections.ts +23 -5
- package/src/resources/extensions/gsd/worktree-manager.ts +76 -28
- package/src/resources/extensions/gsd/worktree-resolver.ts +4 -3
- package/src/resources/extensions/mcp-client/auth.ts +149 -0
- package/src/resources/extensions/mcp-client/index.ts +16 -1
- package/src/resources/extensions/ollama/index.ts +6 -14
- package/src/resources/extensions/ollama/model-capabilities.ts +41 -34
- package/src/resources/extensions/ollama/ndjson-stream.ts +63 -0
- package/src/resources/extensions/ollama/ollama-chat-provider.ts +459 -0
- package/src/resources/extensions/ollama/ollama-client.ts +30 -30
- package/src/resources/extensions/ollama/ollama-discovery.ts +5 -8
- package/src/resources/extensions/ollama/ollama-tool.ts +69 -0
- package/src/resources/extensions/ollama/tests/ollama-discovery.test.ts +0 -27
- package/src/resources/extensions/ollama/thinking-parser.ts +116 -0
- package/src/resources/extensions/ollama/types.ts +23 -0
- package/dist/web/standalone/.next/server/chunks/2229.js +0 -12
- /package/dist/web/standalone/.next/static/{TTlAguZQ5vR9EOv6G8cel → QmuF-eAbuU_2MQ03t38qr}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{TTlAguZQ5vR9EOv6G8cel → QmuF-eAbuU_2MQ03t38qr}/_ssgManifest.js +0 -0
|
@@ -264,6 +264,146 @@ describe("complete-milestone", () => {
|
|
|
264
264
|
);
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
+
test("sanitizeCompleteMilestoneParams normalizes string parameters", async () => {
|
|
268
|
+
const { sanitizeCompleteMilestoneParams } = await import("../bootstrap/sanitize-complete-milestone.ts");
|
|
269
|
+
|
|
270
|
+
// Simulate params as they might arrive from the SDK after partial JSON parse:
|
|
271
|
+
// - numbers instead of strings
|
|
272
|
+
// - null instead of arrays
|
|
273
|
+
// - extra whitespace in strings
|
|
274
|
+
// - undefined optional fields
|
|
275
|
+
const raw: any = {
|
|
276
|
+
milestoneId: " M011 ",
|
|
277
|
+
title: 42, // number instead of string
|
|
278
|
+
oneLiner: " One-liner with spaces ",
|
|
279
|
+
narrative: "# Big markdown\n\nWith newlines and `backticks`\n\n```ts\ncode();\n```\n",
|
|
280
|
+
successCriteriaResults: null, // null instead of string
|
|
281
|
+
definitionOfDoneResults: undefined, // undefined instead of string
|
|
282
|
+
requirementOutcomes: 12345, // number instead of string
|
|
283
|
+
keyDecisions: "not an array", // string instead of array
|
|
284
|
+
keyFiles: null, // null instead of array
|
|
285
|
+
lessonsLearned: [" lesson one ", null, "", " lesson two "],
|
|
286
|
+
followUps: " follow up ",
|
|
287
|
+
deviations: undefined,
|
|
288
|
+
verificationPassed: "true", // string instead of boolean
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const sanitized = sanitizeCompleteMilestoneParams(raw);
|
|
292
|
+
|
|
293
|
+
// String fields are trimmed and coerced
|
|
294
|
+
assert.strictEqual(sanitized.milestoneId, "M011");
|
|
295
|
+
assert.strictEqual(sanitized.title, "42");
|
|
296
|
+
assert.strictEqual(sanitized.oneLiner, "One-liner with spaces");
|
|
297
|
+
assert.ok(sanitized.narrative.includes("# Big markdown"), "narrative preserves markdown");
|
|
298
|
+
assert.strictEqual(sanitized.successCriteriaResults, "");
|
|
299
|
+
assert.strictEqual(sanitized.definitionOfDoneResults, "");
|
|
300
|
+
assert.strictEqual(sanitized.requirementOutcomes, "12345");
|
|
301
|
+
|
|
302
|
+
// Array fields are normalized
|
|
303
|
+
assert.ok(Array.isArray(sanitized.keyDecisions), "keyDecisions is an array");
|
|
304
|
+
assert.deepStrictEqual(sanitized.keyDecisions, []);
|
|
305
|
+
assert.ok(Array.isArray(sanitized.keyFiles), "keyFiles is an array");
|
|
306
|
+
assert.deepStrictEqual(sanitized.keyFiles, []);
|
|
307
|
+
assert.deepStrictEqual(sanitized.lessonsLearned, ["lesson one", "lesson two"]);
|
|
308
|
+
|
|
309
|
+
// Optional fields — toStr() returns "" for undefined/null
|
|
310
|
+
assert.strictEqual(sanitized.followUps, "follow up");
|
|
311
|
+
assert.strictEqual(sanitized.deviations, "");
|
|
312
|
+
|
|
313
|
+
// Boolean coercion
|
|
314
|
+
assert.strictEqual(sanitized.verificationPassed, true);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("sanitizeCompleteMilestoneParams handles large markdown content", async () => {
|
|
318
|
+
const { sanitizeCompleteMilestoneParams } = await import("../bootstrap/sanitize-complete-milestone.ts");
|
|
319
|
+
|
|
320
|
+
// Generate a large markdown string (~25k characters to exceed the 23667 position from the bug)
|
|
321
|
+
const largeMd = "# Milestone Summary\n\n" +
|
|
322
|
+
Array.from({ length: 500 }, (_, i) =>
|
|
323
|
+
`## Section ${i}\n\n` +
|
|
324
|
+
`- [x] Task ${i} completed with \`code\` and **bold** text\n` +
|
|
325
|
+
` - Sub-item with special chars: <, >, &, ", '\n` +
|
|
326
|
+
` - Another sub-item: \`\`\`ts\nconst x = ${i};\n\`\`\`\n`
|
|
327
|
+
).join("\n");
|
|
328
|
+
|
|
329
|
+
assert.ok(largeMd.length > 23667, `generated markdown is ${largeMd.length} chars, must exceed 23667`);
|
|
330
|
+
|
|
331
|
+
const raw: any = {
|
|
332
|
+
milestoneId: "M011",
|
|
333
|
+
title: "Content Depth, Narrative & Onboarding",
|
|
334
|
+
oneLiner: "Large milestone with many slices",
|
|
335
|
+
narrative: largeMd,
|
|
336
|
+
successCriteriaResults: largeMd,
|
|
337
|
+
definitionOfDoneResults: largeMd,
|
|
338
|
+
requirementOutcomes: largeMd,
|
|
339
|
+
keyDecisions: ["decision 1", "decision 2"],
|
|
340
|
+
keyFiles: ["file1.ts", "file2.ts"],
|
|
341
|
+
lessonsLearned: ["lesson 1"],
|
|
342
|
+
followUps: "Some follow-ups",
|
|
343
|
+
deviations: "Some deviations",
|
|
344
|
+
verificationPassed: true,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const sanitized = sanitizeCompleteMilestoneParams(raw);
|
|
348
|
+
|
|
349
|
+
// Large content should pass through without truncation or corruption
|
|
350
|
+
assert.strictEqual(sanitized.narrative, largeMd.trim());
|
|
351
|
+
assert.strictEqual(sanitized.successCriteriaResults, largeMd.trim());
|
|
352
|
+
assert.strictEqual(sanitized.definitionOfDoneResults, largeMd.trim());
|
|
353
|
+
assert.strictEqual(sanitized.requirementOutcomes, largeMd.trim());
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("milestoneCompleteExecute uses sanitized params", async () => {
|
|
357
|
+
// This test verifies that the execute function sanitizes params before passing
|
|
358
|
+
// to handleCompleteMilestone. We test indirectly: if we pass numeric milestoneId,
|
|
359
|
+
// the handler should still receive a string (and return a meaningful error, not a crash).
|
|
360
|
+
const { handleCompleteMilestone } = await import("../tools/complete-milestone.ts");
|
|
361
|
+
const { sanitizeCompleteMilestoneParams } = await import("../bootstrap/sanitize-complete-milestone.ts");
|
|
362
|
+
const base = createFixtureBase();
|
|
363
|
+
try {
|
|
364
|
+
// Simulate what milestoneCompleteExecute should do: sanitize then call handler
|
|
365
|
+
const raw: any = {
|
|
366
|
+
milestoneId: 42, // number — would crash without sanitization
|
|
367
|
+
title: "Test",
|
|
368
|
+
oneLiner: "Test",
|
|
369
|
+
narrative: "Test narrative",
|
|
370
|
+
successCriteriaResults: "Results",
|
|
371
|
+
definitionOfDoneResults: "Done",
|
|
372
|
+
requirementOutcomes: "Outcomes",
|
|
373
|
+
keyDecisions: null, // null — would crash .length without sanitization
|
|
374
|
+
keyFiles: "not-array", // string — would crash .map without sanitization
|
|
375
|
+
lessonsLearned: undefined, // undefined — would crash .map without sanitization
|
|
376
|
+
followUps: "",
|
|
377
|
+
deviations: "",
|
|
378
|
+
verificationPassed: true,
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const sanitized = sanitizeCompleteMilestoneParams(raw);
|
|
382
|
+
|
|
383
|
+
// Verify sanitization didn't crash and produced valid typed params
|
|
384
|
+
assert.strictEqual(typeof sanitized.milestoneId, "string", "milestoneId is a string after sanitization");
|
|
385
|
+
assert.ok(Array.isArray(sanitized.keyDecisions), "keyDecisions is array after sanitization");
|
|
386
|
+
assert.ok(Array.isArray(sanitized.keyFiles), "keyFiles is array after sanitization");
|
|
387
|
+
assert.ok(Array.isArray(sanitized.lessonsLearned), "lessonsLearned is array after sanitization");
|
|
388
|
+
assert.strictEqual(typeof sanitized.verificationPassed, "boolean", "verificationPassed is boolean after sanitization");
|
|
389
|
+
|
|
390
|
+
// Calling handleCompleteMilestone may throw GSD_STALE_STATE (no DB in test env)
|
|
391
|
+
// but it should NOT throw TypeError from type mismatches — that's the bug fix.
|
|
392
|
+
try {
|
|
393
|
+
await handleCompleteMilestone(sanitized, base);
|
|
394
|
+
} catch (err: any) {
|
|
395
|
+
// GSD_STALE_STATE or "No database open" is acceptable — it means we got past
|
|
396
|
+
// the type-sensitive code and failed on DB access, which is expected in tests.
|
|
397
|
+
assert.ok(
|
|
398
|
+
err.code === "GSD_STALE_STATE" || err.message?.includes("database"),
|
|
399
|
+
`expected DB error, got: ${err.message}`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
} finally {
|
|
403
|
+
cleanup(base);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
267
407
|
test("deriveState completing-milestone integration", async () => {
|
|
268
408
|
const { deriveState, isMilestoneComplete } = await import("../state.ts");
|
|
269
409
|
const { invalidateAllCaches: invalidateAllCachesDynamic } = await import("../cache.ts");
|
|
@@ -449,6 +449,45 @@ console.log('\n=== complete-task: handler with missing plan file ===');
|
|
|
449
449
|
cleanup(dbPath);
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
453
|
+
// complete-task: minimal params — no optional fields (#2771 regression)
|
|
454
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
455
|
+
|
|
456
|
+
console.log('\n=== complete-task: minimal params (no keyFiles, keyDecisions, verificationEvidence, blockerDiscovered) ===');
|
|
457
|
+
{
|
|
458
|
+
const dbPath = tempDbPath();
|
|
459
|
+
openDatabase(dbPath);
|
|
460
|
+
|
|
461
|
+
const { basePath, planPath } = createTempProject();
|
|
462
|
+
|
|
463
|
+
insertMilestone({ id: 'M001', title: 'Test Milestone' });
|
|
464
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice' });
|
|
465
|
+
|
|
466
|
+
// Minimal params — only required fields, all optional enrichment fields omitted
|
|
467
|
+
const minimalParams = {
|
|
468
|
+
taskId: 'T01',
|
|
469
|
+
sliceId: 'S01',
|
|
470
|
+
milestoneId: 'M001',
|
|
471
|
+
oneLiner: 'Basic task',
|
|
472
|
+
narrative: 'Did the work.',
|
|
473
|
+
verification: 'Looks good.',
|
|
474
|
+
// keyFiles, keyDecisions, verificationEvidence, blockerDiscovered intentionally omitted
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const result = await handleCompleteTask(minimalParams as any, basePath);
|
|
478
|
+
|
|
479
|
+
assertTrue(!('error' in result), 'handler should not crash with minimal params (no optional fields)');
|
|
480
|
+
if (!('error' in result)) {
|
|
481
|
+
assertTrue(fs.existsSync(result.summaryPath), 'summary file should be written with minimal params');
|
|
482
|
+
const summaryContent = fs.readFileSync(result.summaryPath, 'utf-8');
|
|
483
|
+
assertMatch(summaryContent, /blocker_discovered:\s*false/, 'blocker_discovered should default to false');
|
|
484
|
+
assertMatch(summaryContent, /\(none\)/, 'key_files/key_decisions should show (none) placeholder');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
cleanupDir(basePath);
|
|
488
|
+
cleanup(dbPath);
|
|
489
|
+
}
|
|
490
|
+
|
|
452
491
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
453
492
|
|
|
454
493
|
report();
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-model-label-ordering.test.ts — Regression test for #2899.
|
|
3
|
+
*
|
|
4
|
+
* The dashboard model label was showing the previous unit's model because
|
|
5
|
+
* updateProgressWidget was called before selectAndApplyModel in phases.ts.
|
|
6
|
+
* This test verifies:
|
|
7
|
+
* 1. updateProgressWidget is called AFTER selectAndApplyModel in phases.ts
|
|
8
|
+
* 2. session.ts has a currentDispatchedModelId field
|
|
9
|
+
* 3. auto.ts exposes getCurrentDispatchedModelId in widgetStateAccessors
|
|
10
|
+
* 4. auto-dashboard.ts reads from a dispatched model accessor, not cmdCtx?.model
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { createTestContext } from "./test-helpers.ts";
|
|
16
|
+
|
|
17
|
+
const { assertTrue, assertMatch, report } = createTestContext();
|
|
18
|
+
|
|
19
|
+
const phasesPath = join(import.meta.dirname, "..", "auto", "phases.ts");
|
|
20
|
+
const sessionPath = join(import.meta.dirname, "..", "auto", "session.ts");
|
|
21
|
+
const autoPath = join(import.meta.dirname, "..", "auto.ts");
|
|
22
|
+
const dashboardPath = join(import.meta.dirname, "..", "auto-dashboard.ts");
|
|
23
|
+
|
|
24
|
+
const phasesSrc = readFileSync(phasesPath, "utf-8");
|
|
25
|
+
const sessionSrc = readFileSync(sessionPath, "utf-8");
|
|
26
|
+
const autoSrc = readFileSync(autoPath, "utf-8");
|
|
27
|
+
const dashboardSrc = readFileSync(dashboardPath, "utf-8");
|
|
28
|
+
|
|
29
|
+
console.log("\n=== #2899: Dashboard model label shows correct (dispatched) model ===");
|
|
30
|
+
|
|
31
|
+
// ── Test 1: updateProgressWidget is called AFTER selectAndApplyModel ──────
|
|
32
|
+
|
|
33
|
+
// Find the positions of the calls in the dispatch function body.
|
|
34
|
+
// selectAndApplyModel must appear BEFORE updateProgressWidget.
|
|
35
|
+
const selectModelPos = phasesSrc.indexOf("deps.selectAndApplyModel(");
|
|
36
|
+
const updateWidgetPos = phasesSrc.indexOf("deps.updateProgressWidget(");
|
|
37
|
+
|
|
38
|
+
assertTrue(
|
|
39
|
+
selectModelPos > 0,
|
|
40
|
+
"phases.ts contains deps.selectAndApplyModel call",
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
assertTrue(
|
|
44
|
+
updateWidgetPos > 0,
|
|
45
|
+
"phases.ts contains deps.updateProgressWidget call",
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
assertTrue(
|
|
49
|
+
selectModelPos < updateWidgetPos,
|
|
50
|
+
`selectAndApplyModel (pos ${selectModelPos}) must be called BEFORE updateProgressWidget (pos ${updateWidgetPos}) — widget needs fresh model`,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// ── Test 2: session.ts declares currentDispatchedModelId ──────────────────
|
|
54
|
+
|
|
55
|
+
assertTrue(
|
|
56
|
+
sessionSrc.includes("currentDispatchedModelId"),
|
|
57
|
+
"session.ts has currentDispatchedModelId field",
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// ── Test 3: auto.ts exposes getCurrentDispatchedModelId in widgetStateAccessors ──
|
|
61
|
+
|
|
62
|
+
assertTrue(
|
|
63
|
+
autoSrc.includes("getCurrentDispatchedModelId"),
|
|
64
|
+
"auto.ts exposes getCurrentDispatchedModelId accessor",
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Verify it's in the widgetStateAccessors object
|
|
68
|
+
const accessorsBlock = autoSrc.slice(
|
|
69
|
+
autoSrc.indexOf("const widgetStateAccessors"),
|
|
70
|
+
autoSrc.indexOf("};", autoSrc.indexOf("const widgetStateAccessors")) + 2,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
assertTrue(
|
|
74
|
+
accessorsBlock.includes("getCurrentDispatchedModelId"),
|
|
75
|
+
"getCurrentDispatchedModelId is in the widgetStateAccessors object",
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// ── Test 4: WidgetStateAccessors interface has getCurrentDispatchedModelId ──
|
|
79
|
+
|
|
80
|
+
assertTrue(
|
|
81
|
+
dashboardSrc.includes("getCurrentDispatchedModelId"),
|
|
82
|
+
"auto-dashboard.ts references getCurrentDispatchedModelId",
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// The dashboard render closure should NOT read model from cmdCtx?.model for display.
|
|
86
|
+
// It should use the accessor for the dispatched model ID.
|
|
87
|
+
// Check that the "Model display" section uses the accessor, not cmdCtx?.model directly.
|
|
88
|
+
const modelDisplaySection = dashboardSrc.slice(
|
|
89
|
+
dashboardSrc.indexOf("// Model display"),
|
|
90
|
+
dashboardSrc.indexOf("// Model display") + 500,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
assertTrue(
|
|
94
|
+
modelDisplaySection.includes("getCurrentDispatchedModelId") ||
|
|
95
|
+
modelDisplaySection.includes("getDispatchedModelId"),
|
|
96
|
+
"Model display section reads from dispatched model accessor, not cmdCtx?.model alone",
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// ── Test 5: currentDispatchedModelId is set after selectAndApplyModel in phases.ts ──
|
|
100
|
+
|
|
101
|
+
// After selectAndApplyModel returns, phases.ts should store the dispatched model ID
|
|
102
|
+
assertTrue(
|
|
103
|
+
phasesSrc.includes("currentDispatchedModelId"),
|
|
104
|
+
"phases.ts stores currentDispatchedModelId after model selection",
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
report();
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// GSD2 — Regression tests: DB anti-pattern guardrails in prompt templates
|
|
2
|
+
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
const promptsDir = join(process.cwd(), "src/resources/extensions/gsd/prompts");
|
|
9
|
+
|
|
10
|
+
function readPrompt(name: string): string {
|
|
11
|
+
return readFileSync(join(promptsDir, `${name}.md`), "utf-8");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ─── Layer 1: system.md global guardrail ──────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
test("system.md anti-patterns section prohibits direct .gsd/gsd.db access", () => {
|
|
17
|
+
const prompt = readPrompt("system");
|
|
18
|
+
assert.match(
|
|
19
|
+
prompt,
|
|
20
|
+
/Never query.*\.gsd\/gsd\.db.*directly/i,
|
|
21
|
+
"system.md must prohibit direct .gsd/gsd.db access in the anti-patterns section",
|
|
22
|
+
);
|
|
23
|
+
assert.match(prompt, /sqlite3/, "system.md DB guardrail must name the sqlite3 CLI");
|
|
24
|
+
assert.match(prompt, /better-sqlite3/, "system.md DB guardrail must name better-sqlite3");
|
|
25
|
+
assert.match(prompt, /gsd_\*/, "system.md DB guardrail must redirect to gsd_* tools");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("system.md DB guardrail explains single-writer WAL risk", () => {
|
|
29
|
+
const prompt = readPrompt("system");
|
|
30
|
+
assert.match(prompt, /single-writer WAL/i, "system.md must explain the WAL architecture risk");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ─── Layer 2: high-risk prompt guardrails ─────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
test("validate-milestone.md contains DB access safety guardrail with tool redirect", () => {
|
|
36
|
+
const prompt = readPrompt("validate-milestone");
|
|
37
|
+
assert.match(prompt, /DB access safety/i, "validate-milestone.md must have DB access safety section");
|
|
38
|
+
assert.match(prompt, /gsd_milestone_status/, "validate-milestone.md must name gsd_milestone_status as alternative");
|
|
39
|
+
assert.match(prompt, /Do NOT query.*\.gsd\/gsd\.db/i, "validate-milestone.md must prohibit direct DB queries");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("complete-milestone.md contains DB access safety guardrail with tool redirect", () => {
|
|
43
|
+
const prompt = readPrompt("complete-milestone");
|
|
44
|
+
assert.match(prompt, /DB access safety/i, "complete-milestone.md must have DB access safety section");
|
|
45
|
+
assert.match(prompt, /gsd_milestone_status/, "complete-milestone.md must name gsd_milestone_status as alternative");
|
|
46
|
+
assert.match(prompt, /Do NOT query.*\.gsd\/gsd\.db/i, "complete-milestone.md must prohibit direct DB queries");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("doctor-heal.md contains DB access guardrail naming gsd_milestone_status", () => {
|
|
50
|
+
const prompt = readPrompt("doctor-heal");
|
|
51
|
+
assert.match(prompt, /gsd_milestone_status/, "doctor-heal.md must name gsd_milestone_status as the DB inspection tool");
|
|
52
|
+
assert.match(prompt, /Do NOT query.*\.gsd\/gsd\.db/i, "doctor-heal.md must prohibit direct DB queries");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("forensics.md contains DB inspection guardrail", () => {
|
|
56
|
+
const prompt = readPrompt("forensics");
|
|
57
|
+
assert.match(prompt, /gsd_milestone_status/, "forensics.md must name gsd_milestone_status as the DB inspection tool");
|
|
58
|
+
assert.match(prompt, /sqlite3.*\.gsd\/gsd\.db/i, "forensics.md must prohibit sqlite3 against .gsd/gsd.db");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("reassess-roadmap.md contains DB access safety guardrail", () => {
|
|
62
|
+
const prompt = readPrompt("reassess-roadmap");
|
|
63
|
+
assert.match(prompt, /DB access safety/i, "reassess-roadmap.md must have DB access safety section");
|
|
64
|
+
assert.match(prompt, /gsd_milestone_status/, "reassess-roadmap.md must name gsd_milestone_status as alternative");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ─── Negative assertion: no prompt instructs running sqlite3 as a command ─────
|
|
68
|
+
|
|
69
|
+
test("no prompt file contains an unguarded sqlite3 command invocation", () => {
|
|
70
|
+
const files = readdirSync(promptsDir).filter((f) => f.endsWith(".md"));
|
|
71
|
+
assert.ok(files.length >= 35, `Expected at least 35 prompt files, found ${files.length}`);
|
|
72
|
+
|
|
73
|
+
const violations: string[] = [];
|
|
74
|
+
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
const content = readFileSync(join(promptsDir, file), "utf-8");
|
|
77
|
+
const lines = content.split("\n");
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < lines.length; i++) {
|
|
80
|
+
const line = lines[i];
|
|
81
|
+
const trimmed = line.trim();
|
|
82
|
+
|
|
83
|
+
// Match lines containing sqlite3 targeting gsd.db in any common form:
|
|
84
|
+
// sqlite3 .gsd/gsd.db, sqlite3 ./.gsd/gsd.db, sqlite3 "/path/.gsd/gsd.db",
|
|
85
|
+
// sqlite3 -header .gsd/gsd.db, etc.
|
|
86
|
+
// Guardrail text that says "Never run" or "Do NOT query" is fine — only flag
|
|
87
|
+
// lines where these appear without a surrounding prohibition keyword.
|
|
88
|
+
if (/sqlite3\b.*gsd\.db/.test(trimmed)) {
|
|
89
|
+
const context = lines.slice(Math.max(0, i - 3), i + 1).join(" ");
|
|
90
|
+
if (!/Never|Do NOT|do not|don't|prohibited|forbidden|never run/i.test(context)) {
|
|
91
|
+
violations.push(`${file}:${i + 1} — unguarded sqlite3 command: ${trimmed}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Match node -e with better-sqlite3 require in any quoting style
|
|
95
|
+
if (/node\s+-e\s+.*(?:require|import).*better-sqlite3/.test(trimmed)) {
|
|
96
|
+
const context = lines.slice(Math.max(0, i - 3), i + 1).join(" ");
|
|
97
|
+
if (!/Never|Do NOT|do not|don't|prohibited|forbidden|never run/i.test(context)) {
|
|
98
|
+
violations.push(`${file}:${i + 1} — unguarded node -e require command: ${trimmed}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
assert.deepEqual(
|
|
105
|
+
violations,
|
|
106
|
+
[],
|
|
107
|
+
`Found prompts with unguarded sqlite3/better-sqlite3 invocations:\n${violations.join("\n")}`,
|
|
108
|
+
);
|
|
109
|
+
});
|
|
@@ -38,13 +38,17 @@ assertEq(
|
|
|
38
38
|
"Standard worktree layout resolves to project root DB path",
|
|
39
39
|
);
|
|
40
40
|
|
|
41
|
-
// Symlink-resolved layout
|
|
41
|
+
// Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/...
|
|
42
|
+
// After PR #2952, these paths resolve to the hash-level DB (same as external-state),
|
|
43
|
+
// because on POSIX getcwd() returns the canonical (symlink-resolved) path anyway, so
|
|
44
|
+
// a path like <proj>/.gsd/projects/<hash>/worktrees/ in practice is always
|
|
45
|
+
// ~/.gsd/projects/<hash>/worktrees/ after the OS resolves the .gsd symlink.
|
|
42
46
|
const symlinkPath = `/home/user/myproject/.gsd/projects/abc123def/worktrees/M001/work`;
|
|
43
47
|
const symlinkResult = resolveProjectRootDbPath(symlinkPath);
|
|
44
48
|
assertEq(
|
|
45
49
|
symlinkResult,
|
|
46
|
-
join("/home/user/myproject
|
|
47
|
-
"
|
|
50
|
+
join("/home/user/myproject/.gsd/projects/abc123def", "gsd.db"),
|
|
51
|
+
"/.gsd/projects/<hash>/worktrees/ resolves to hash-level DB (#2517, updated for #2952)",
|
|
48
52
|
);
|
|
49
53
|
|
|
50
54
|
// Windows-style separators for symlink layout
|
|
@@ -53,8 +57,8 @@ if (sep === "\\") {
|
|
|
53
57
|
const winResult = resolveProjectRootDbPath(winSymlinkPath);
|
|
54
58
|
assertEq(
|
|
55
59
|
winResult,
|
|
56
|
-
join("C:\\Users\\dev\\project
|
|
57
|
-
"Windows
|
|
60
|
+
join("C:\\Users\\dev\\project\\.gsd\\projects\\abc123def", "gsd.db"),
|
|
61
|
+
"Windows /.gsd/projects/<hash>/worktrees/ resolves to hash-level DB",
|
|
58
62
|
);
|
|
59
63
|
} else {
|
|
60
64
|
// On non-Windows, test forward-slash variant explicitly
|
|
@@ -62,8 +66,8 @@ if (sep === "\\") {
|
|
|
62
66
|
const fwdResult = resolveProjectRootDbPath(fwdSymlinkPath);
|
|
63
67
|
assertEq(
|
|
64
68
|
fwdResult,
|
|
65
|
-
join("/home/user/myproject
|
|
66
|
-
"Forward-slash
|
|
69
|
+
join("/home/user/myproject/.gsd/projects/abc123def", "gsd.db"),
|
|
70
|
+
"Forward-slash /.gsd/projects/<hash>/worktrees/ resolves to hash-level DB on POSIX",
|
|
67
71
|
);
|
|
68
72
|
}
|
|
69
73
|
|
|
@@ -72,8 +76,8 @@ const deepSymlinkPath = `/home/user/myproject/.gsd/projects/deadbeef42/worktrees
|
|
|
72
76
|
const deepResult = resolveProjectRootDbPath(deepSymlinkPath);
|
|
73
77
|
assertEq(
|
|
74
78
|
deepResult,
|
|
75
|
-
join("/home/user/myproject
|
|
76
|
-
"Deep
|
|
79
|
+
join("/home/user/myproject/.gsd/projects/deadbeef42", "gsd.db"),
|
|
80
|
+
"Deep /.gsd/projects/<hash>/worktrees/ path resolves to hash-level DB (#2952)",
|
|
77
81
|
);
|
|
78
82
|
|
|
79
83
|
// Non-worktree path should be unchanged
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
saveDecisionToDb,
|
|
25
25
|
updateRequirementInDb,
|
|
26
26
|
saveArtifactToDb,
|
|
27
|
+
extractDeferredSliceRef,
|
|
27
28
|
} from '../db-writer.ts';
|
|
28
29
|
import type { Decision, Requirement } from '../types.ts';
|
|
29
30
|
|
|
@@ -475,6 +476,71 @@ describe('db-writer', () => {
|
|
|
475
476
|
}
|
|
476
477
|
});
|
|
477
478
|
|
|
479
|
+
test('updateRequirementInDb — seeds from REQUIREMENTS.md when DB empty (#3346)', async () => {
|
|
480
|
+
const tmpDir = makeTmpDir();
|
|
481
|
+
const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
|
|
482
|
+
openDatabase(dbPath);
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
// Write a REQUIREMENTS.md with real content (simulating discussion phase output)
|
|
486
|
+
const reqContent = [
|
|
487
|
+
'# Requirements',
|
|
488
|
+
'',
|
|
489
|
+
'## Active',
|
|
490
|
+
'',
|
|
491
|
+
'### R005 — User authentication',
|
|
492
|
+
'- Class: functional',
|
|
493
|
+
'- Why: Users need secure access',
|
|
494
|
+
'- Source: user-research',
|
|
495
|
+
'- Primary owner: M001/S02',
|
|
496
|
+
'',
|
|
497
|
+
'### R007 — API rate limiting',
|
|
498
|
+
'- Class: non-functional',
|
|
499
|
+
'- Why: Prevent abuse',
|
|
500
|
+
'- Source: architecture',
|
|
501
|
+
'- Primary owner: M001/S03',
|
|
502
|
+
'',
|
|
503
|
+
'## Validated',
|
|
504
|
+
'',
|
|
505
|
+
'### R001 — Database schema',
|
|
506
|
+
'- Class: functional',
|
|
507
|
+
'- Why: Foundation for storage',
|
|
508
|
+
'- Source: design',
|
|
509
|
+
'- Validation: S01 verified',
|
|
510
|
+
].join('\n');
|
|
511
|
+
fs.writeFileSync(path.join(tmpDir, '.gsd', 'REQUIREMENTS.md'), reqContent);
|
|
512
|
+
|
|
513
|
+
// DB is empty — no requirements seeded. Update R005 to "validated".
|
|
514
|
+
// Before #3346 fix: this would create a skeleton with empty fields.
|
|
515
|
+
// After fix: this seeds all 3 requirements from REQUIREMENTS.md first.
|
|
516
|
+
await updateRequirementInDb('R005', {
|
|
517
|
+
status: 'validated',
|
|
518
|
+
validation: 'S02 — auth flow verified',
|
|
519
|
+
}, tmpDir);
|
|
520
|
+
|
|
521
|
+
// R005 should have the update AND the original content from markdown
|
|
522
|
+
const r005 = getRequirementById('R005');
|
|
523
|
+
assert.ok(r005, 'R005 should exist');
|
|
524
|
+
assert.equal(r005!.status, 'validated', 'status should be updated');
|
|
525
|
+
assert.equal(r005!.validation, 'S02 — auth flow verified', 'validation should be updated');
|
|
526
|
+
assert.equal(r005!.class, 'functional', 'class should be preserved from REQUIREMENTS.md');
|
|
527
|
+
assert.ok(r005!.description?.includes('authentication') || r005!.full_content?.includes('authentication'),
|
|
528
|
+
'original content should be preserved');
|
|
529
|
+
|
|
530
|
+
// R007 and R001 should also be seeded (not just the one being updated)
|
|
531
|
+
const r007 = getRequirementById('R007');
|
|
532
|
+
assert.ok(r007, 'R007 should be seeded from REQUIREMENTS.md');
|
|
533
|
+
assert.equal(r007!.status, 'active', 'R007 status should be active');
|
|
534
|
+
|
|
535
|
+
const r001 = getRequirementById('R001');
|
|
536
|
+
assert.ok(r001, 'R001 should be seeded from REQUIREMENTS.md');
|
|
537
|
+
assert.equal(r001!.status, 'validated', 'R001 status should be validated (from section heading)');
|
|
538
|
+
} finally {
|
|
539
|
+
closeDatabase();
|
|
540
|
+
cleanupDir(tmpDir);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
478
544
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
479
545
|
// saveArtifactToDb Tests
|
|
480
546
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -694,4 +760,72 @@ describe('db-writer', () => {
|
|
|
694
760
|
|
|
695
761
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
696
762
|
|
|
763
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
764
|
+
// extractDeferredSliceRef
|
|
765
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
766
|
+
|
|
767
|
+
describe('extractDeferredSliceRef', () => {
|
|
768
|
+
const fields = (scope: string, choice: string, decision: string) => ({
|
|
769
|
+
scope,
|
|
770
|
+
choice,
|
|
771
|
+
decision,
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
test('detects deferral in scope with M###/S## pattern in choice', () => {
|
|
775
|
+
const result = extractDeferredSliceRef(
|
|
776
|
+
fields('deferral of low-priority work', 'Move M001/S03 to backlog', ''),
|
|
777
|
+
);
|
|
778
|
+
assert.deepStrictEqual(result, { milestoneId: 'M001', sliceId: 'S03' });
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
test('detects deferral in choice field', () => {
|
|
782
|
+
const result = extractDeferredSliceRef(
|
|
783
|
+
fields('slice prioritization', 'defer M002/S01 until next sprint', ''),
|
|
784
|
+
);
|
|
785
|
+
assert.deepStrictEqual(result, { milestoneId: 'M002', sliceId: 'S01' });
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
test('detects deferral in decision field', () => {
|
|
789
|
+
const result = extractDeferredSliceRef(
|
|
790
|
+
fields('resource constraints', '', 'deferred M010/S12 pending review'),
|
|
791
|
+
);
|
|
792
|
+
assert.deepStrictEqual(result, { milestoneId: 'M010', sliceId: 'S12' });
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
test('returns null when no M###/S## pattern is present', () => {
|
|
796
|
+
const result = extractDeferredSliceRef(
|
|
797
|
+
fields('deferral of work', 'will revisit later', 'deferred indefinitely'),
|
|
798
|
+
);
|
|
799
|
+
assert.strictEqual(result, null);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
test('recognises "deferring" variant', () => {
|
|
803
|
+
const result = extractDeferredSliceRef(
|
|
804
|
+
fields('deferring this slice', 'M005/S02 can wait', ''),
|
|
805
|
+
);
|
|
806
|
+
assert.deepStrictEqual(result, { milestoneId: 'M005', sliceId: 'S02' });
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
test('recognises "defers" variant', () => {
|
|
810
|
+
const result = extractDeferredSliceRef(
|
|
811
|
+
fields('team defers slice', 'M100/S10 not urgent', ''),
|
|
812
|
+
);
|
|
813
|
+
assert.deepStrictEqual(result, { milestoneId: 'M100', sliceId: 'S10' });
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
test('returns first M###/S## match when multiple patterns exist', () => {
|
|
817
|
+
const result = extractDeferredSliceRef(
|
|
818
|
+
fields('', 'defer M003/S01 and M003/S02', ''),
|
|
819
|
+
);
|
|
820
|
+
assert.deepStrictEqual(result, { milestoneId: 'M003', sliceId: 'S01' });
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
test('returns null when no deferral keyword is present', () => {
|
|
824
|
+
const result = extractDeferredSliceRef(
|
|
825
|
+
fields('approved work', 'M001/S01 is ready', 'proceed with M001/S01'),
|
|
826
|
+
);
|
|
827
|
+
assert.strictEqual(result, null);
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
|
|
697
831
|
});
|