gsd-pi 2.72.0 → 2.73.0-dev.1cfd50c
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 +12 -2
- package/dist/cli.js +59 -3
- package/dist/onboarding.js +10 -0
- package/dist/resources/extensions/async-jobs/await-tool.js +7 -4
- package/dist/resources/extensions/async-jobs/job-manager.js +28 -3
- package/dist/resources/extensions/claude-code-cli/partial-builder.js +40 -12
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +48 -23
- package/dist/resources/extensions/gsd/auto/loop.js +84 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +6 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +11 -0
- package/dist/resources/extensions/gsd/auto.js +25 -19
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +9 -11
- package/dist/resources/extensions/gsd/commands-handlers.js +4 -1
- package/dist/resources/extensions/gsd/context-injector.js +1 -1
- package/dist/resources/extensions/gsd/custom-workflow-engine.js +3 -7
- package/dist/resources/extensions/gsd/definition-io.js +15 -0
- package/dist/resources/extensions/gsd/dispatch-guard.js +4 -0
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +6 -3
- package/dist/resources/extensions/gsd/git-service.js +11 -8
- package/dist/resources/extensions/gsd/gitignore.js +12 -6
- package/dist/resources/extensions/gsd/gsd-db.js +49 -6
- package/dist/resources/extensions/gsd/key-manager.js +2 -0
- package/dist/resources/extensions/gsd/preferences-skills.js +2 -34
- package/dist/resources/extensions/gsd/preferences-types.js +15 -0
- package/dist/resources/extensions/gsd/preferences.js +16 -3
- package/dist/resources/extensions/gsd/prompt-loader.js +4 -1
- package/dist/resources/extensions/gsd/prompts/discuss.md +122 -13
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/state.js +21 -1
- package/dist/resources/extensions/gsd/workflow-projections.js +7 -0
- package/dist/resources/extensions/gsd/worktree-manager.js +30 -3
- package/dist/resources/extensions/gsd/write-intercept.js +10 -1
- package/dist/resources/extensions/ollama/index.js +4 -5
- package/dist/resources/extensions/ollama/ollama-client.js +35 -6
- package/dist/resources/extensions/ollama/ollama-discovery.js +32 -6
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +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/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- 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 +3 -3
- 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_client-reference-manifest.js +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_client-reference-manifest.js +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_client-reference-manifest.js +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_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +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_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +3 -3
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- 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 +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
- package/dist/web/standalone/.next/server/chunks/2331.js +16 -16
- package/dist/web/standalone/.next/server/chunks/4741.js +12 -12
- package/dist/web/standalone/.next/server/chunks/5822.js +2 -2
- package/dist/web/standalone/.next/server/chunks/63.js +8 -8
- package/dist/web/standalone/.next/server/chunks/6897.js +3 -3
- package/dist/web/standalone/.next/server/edge-runtime-webpack.js +2 -0
- package/dist/web/standalone/.next/server/functions-config-manifest.json +0 -9
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +29 -2
- package/dist/web/standalone/.next/server/middleware.js +4 -12
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +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/.next/server/webpack-runtime.js +1 -1
- package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-f1e30ab6bb269149.js +1 -0
- package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/env-api-keys.js +1 -0
- package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
- package/packages/pi-ai/dist/models.custom.d.ts +105 -0
- package/packages/pi-ai/dist/models.custom.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.custom.js +97 -0
- package/packages/pi-ai/dist/models.custom.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +648 -140
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +867 -370
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.test.d.ts +2 -0
- package/packages/pi-ai/dist/models.generated.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/models.generated.test.js +334 -0
- package/packages/pi-ai/dist/models.generated.test.js.map +1 -0
- package/packages/pi-ai/dist/models.test.js +105 -0
- package/packages/pi-ai/dist/models.test.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +1 -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/dist/utils/oauth/github-copilot.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/github-copilot.js +5 -1
- package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/github-copilot.test.d.ts +2 -0
- package/packages/pi-ai/dist/utils/oauth/github-copilot.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/utils/oauth/github-copilot.test.js +57 -0
- package/packages/pi-ai/dist/utils/oauth/github-copilot.test.js.map +1 -0
- package/packages/pi-ai/src/env-api-keys.ts +1 -0
- package/packages/pi-ai/src/models.custom.ts +98 -0
- package/packages/pi-ai/src/models.generated.test.ts +373 -0
- package/packages/pi-ai/src/models.generated.ts +867 -370
- package/packages/pi-ai/src/models.test.ts +135 -0
- package/packages/pi-ai/src/types.ts +1 -0
- package/packages/pi-ai/src/utils/oauth/github-copilot.test.ts +71 -0
- package/packages/pi-ai/src/utils/oauth/github-copilot.ts +4 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.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 +9 -0
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +36 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +87 -12
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +22 -9
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +63 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -0
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +10 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +72 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +84 -12
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +71 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +23 -9
- package/packages/pi-tui/dist/components/__tests__/editor.test.js +12 -0
- package/packages/pi-tui/dist/components/__tests__/editor.test.js.map +1 -1
- package/packages/pi-tui/dist/components/__tests__/input.test.js +12 -0
- package/packages/pi-tui/dist/components/__tests__/input.test.js.map +1 -1
- package/packages/pi-tui/dist/keys.d.ts.map +1 -1
- package/packages/pi-tui/dist/keys.js +27 -0
- package/packages/pi-tui/dist/keys.js.map +1 -1
- package/packages/pi-tui/src/components/__tests__/editor.test.ts +18 -0
- package/packages/pi-tui/src/components/__tests__/input.test.ts +18 -0
- package/packages/pi-tui/src/keys.ts +32 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/async-jobs/await-tool.test.ts +40 -7
- package/src/resources/extensions/async-jobs/await-tool.ts +7 -4
- package/src/resources/extensions/async-jobs/job-manager.ts +33 -3
- package/src/resources/extensions/claude-code-cli/partial-builder.ts +45 -12
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +49 -24
- package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +91 -2
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +112 -0
- package/src/resources/extensions/gsd/auto/loop.ts +89 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +7 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +10 -0
- package/src/resources/extensions/gsd/auto.ts +25 -20
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +8 -10
- package/src/resources/extensions/gsd/commands-handlers.ts +5 -1
- package/src/resources/extensions/gsd/context-injector.ts +1 -1
- package/src/resources/extensions/gsd/custom-workflow-engine.ts +4 -8
- package/src/resources/extensions/gsd/definition-io.ts +18 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +5 -0
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +6 -3
- package/src/resources/extensions/gsd/git-service.ts +11 -8
- package/src/resources/extensions/gsd/gitignore.ts +12 -6
- package/src/resources/extensions/gsd/gsd-db.ts +54 -6
- package/src/resources/extensions/gsd/key-manager.ts +2 -0
- package/src/resources/extensions/gsd/preferences-skills.ts +2 -36
- package/src/resources/extensions/gsd/preferences-types.ts +16 -0
- package/src/resources/extensions/gsd/preferences.ts +19 -6
- package/src/resources/extensions/gsd/prompt-loader.ts +6 -1
- package/src/resources/extensions/gsd/prompts/discuss.md +122 -13
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/state.ts +20 -0
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/block-db-writes.test.ts +63 -0
- package/src/resources/extensions/gsd/tests/definition-io.test.ts +57 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/doctor-heal-fixable-warnings.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/false-degraded-mode-warning.test.ts +104 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +107 -5
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +8 -6
- package/src/resources/extensions/gsd/tests/key-manager.test.ts +63 -0
- package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/plan-milestone-artifact-verification.test.ts +62 -0
- package/src/resources/extensions/gsd/tests/post-unit-state-rebuild.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/preferences-formatting.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +53 -0
- package/src/resources/extensions/gsd/tests/projection-regression.test.ts +96 -1
- package/src/resources/extensions/gsd/tests/prompt-loader-working-directory.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +97 -0
- package/src/resources/extensions/gsd/tests/stale-slice-rows.test.ts +41 -0
- package/src/resources/extensions/gsd/workflow-projections.ts +8 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +29 -3
- package/src/resources/extensions/gsd/write-intercept.ts +10 -1
- package/src/resources/extensions/ollama/index.ts +4 -5
- package/src/resources/extensions/ollama/ollama-client.ts +35 -6
- package/src/resources/extensions/ollama/ollama-discovery.ts +37 -6
- package/src/resources/extensions/ollama/tests/ollama-discovery.test.ts +54 -0
- package/dist/resources/extensions/gsd/auto-observability.js +0 -54
- package/dist/resources/extensions/gsd/file-watcher.js +0 -80
- package/dist/resources/extensions/gsd/rtk-status.js +0 -43
- package/dist/web/standalone/.next/static/chunks/app/page-7115e62689b5fd84.js +0 -1
- package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
- package/src/resources/extensions/gsd/auto-observability.ts +0 -72
- package/src/resources/extensions/gsd/file-watcher.ts +0 -100
- package/src/resources/extensions/gsd/rtk-status.ts +0 -53
- /package/dist/web/standalone/.next/static/{Y0I7CjXJl-tWoV__KidV4 → uNGVqSkAnszMl0okA4nnp}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{Y0I7CjXJl-tWoV__KidV4 → uNGVqSkAnszMl0okA4nnp}/_ssgManifest.js +0 -0
|
@@ -24,6 +24,12 @@ export interface Job {
|
|
|
24
24
|
errorText?: string;
|
|
25
25
|
/** Set by await_job when results are consumed. Suppresses follow-up delivery. */
|
|
26
26
|
awaited?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Handle for the pending follow-up delivery timer (set by deliverResult).
|
|
29
|
+
* Stored so suppressFollowUp() can cancel it before the notification fires,
|
|
30
|
+
* even when await_job is called after the job has already completed (#3787).
|
|
31
|
+
*/
|
|
32
|
+
deliveryTimer?: ReturnType<typeof setTimeout>;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
export interface JobManagerOptions {
|
|
@@ -170,12 +176,36 @@ export class AsyncJobManager {
|
|
|
170
176
|
|
|
171
177
|
// ── Private ────────────────────────────────────────────────────────────
|
|
172
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Suppress follow-up notification for a job — cancels any pending delivery
|
|
181
|
+
* timer and marks the job as awaited. Safe to call at any time, including
|
|
182
|
+
* before or after the job completes (#3787).
|
|
183
|
+
*/
|
|
184
|
+
suppressFollowUp(id: string): void {
|
|
185
|
+
const job = this.jobs.get(id);
|
|
186
|
+
if (!job) return;
|
|
187
|
+
job.awaited = true;
|
|
188
|
+
if (job.deliveryTimer !== undefined) {
|
|
189
|
+
clearTimeout(job.deliveryTimer);
|
|
190
|
+
job.deliveryTimer = undefined;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
173
194
|
private deliverResult(job: Job): void {
|
|
174
195
|
if (!this.onJobComplete) return;
|
|
175
|
-
//
|
|
176
|
-
//
|
|
196
|
+
// Use setTimeout(0) instead of queueMicrotask so the handle is cancellable.
|
|
197
|
+
// suppressFollowUp() can clear this timer even when await_job is called in
|
|
198
|
+
// a later LLM turn (after the job already completed). queueMicrotask ran
|
|
199
|
+
// immediately and could not be cancelled (#2762, #3787).
|
|
177
200
|
const cb = this.onJobComplete;
|
|
178
|
-
|
|
201
|
+
job.deliveryTimer = setTimeout(() => {
|
|
202
|
+
job.deliveryTimer = undefined;
|
|
203
|
+
if (!job.awaited) cb(job);
|
|
204
|
+
}, 0);
|
|
205
|
+
// Allow process to exit even if timer is pending
|
|
206
|
+
if (typeof job.deliveryTimer === "object" && "unref" in job.deliveryTimer) {
|
|
207
|
+
(job.deliveryTimer as NodeJS.Timeout).unref();
|
|
208
|
+
}
|
|
179
209
|
}
|
|
180
210
|
|
|
181
211
|
private scheduleEviction(id: string): void {
|
|
@@ -19,6 +19,49 @@ import type {
|
|
|
19
19
|
import { hasXmlParameterTags, repairToolJson } from "@gsd/pi-ai";
|
|
20
20
|
import type { BetaContentBlock, BetaRawMessageStreamEvent, NonNullableUsage } from "./sdk-types.js";
|
|
21
21
|
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// MCP tool name parsing
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Split a Claude Code MCP tool name (`mcp__<server>__<tool>`) into its parts.
|
|
28
|
+
* Returns null for non-prefixed names so callers can fall through unchanged.
|
|
29
|
+
*
|
|
30
|
+
* Server names may contain hyphens (`gsd-workflow`); the SDK uses the literal
|
|
31
|
+
* `__` delimiter between the server name and the tool name.
|
|
32
|
+
*/
|
|
33
|
+
export function parseMcpToolName(name: string): { server: string; tool: string } | null {
|
|
34
|
+
if (!name.startsWith("mcp__")) return null;
|
|
35
|
+
const rest = name.slice("mcp__".length);
|
|
36
|
+
const delim = rest.indexOf("__");
|
|
37
|
+
if (delim <= 0 || delim === rest.length - 2) return null;
|
|
38
|
+
return { server: rest.slice(0, delim), tool: rest.slice(delim + 2) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build a GSD ToolCall block from a Claude Code SDK tool_use block, stripping
|
|
43
|
+
* the `mcp__<server>__` prefix from the name so registered extension renderers
|
|
44
|
+
* (which use the unprefixed canonical names) can match. The original server
|
|
45
|
+
* name is preserved on the block for diagnostics and rendering.
|
|
46
|
+
*/
|
|
47
|
+
function toolCallFromBlock(
|
|
48
|
+
id: string,
|
|
49
|
+
rawName: string,
|
|
50
|
+
input: Record<string, unknown>,
|
|
51
|
+
): ToolCall {
|
|
52
|
+
const parsed = parseMcpToolName(rawName);
|
|
53
|
+
const toolCall: ToolCall = {
|
|
54
|
+
type: "toolCall",
|
|
55
|
+
id,
|
|
56
|
+
name: parsed ? parsed.tool : rawName,
|
|
57
|
+
arguments: input,
|
|
58
|
+
};
|
|
59
|
+
if (parsed) {
|
|
60
|
+
(toolCall as ToolCall & { mcpServer?: string }).mcpServer = parsed.server;
|
|
61
|
+
}
|
|
62
|
+
return toolCall;
|
|
63
|
+
}
|
|
64
|
+
|
|
22
65
|
// ---------------------------------------------------------------------------
|
|
23
66
|
// Content-block mapping helpers
|
|
24
67
|
// ---------------------------------------------------------------------------
|
|
@@ -41,12 +84,7 @@ export function mapContentBlock(
|
|
|
41
84
|
} satisfies ThinkingContent;
|
|
42
85
|
|
|
43
86
|
case "tool_use":
|
|
44
|
-
return
|
|
45
|
-
type: "toolCall",
|
|
46
|
-
id: block.id,
|
|
47
|
-
name: block.name,
|
|
48
|
-
arguments: block.input,
|
|
49
|
-
} satisfies ToolCall;
|
|
87
|
+
return toolCallFromBlock(block.id, block.name, block.input);
|
|
50
88
|
|
|
51
89
|
case "server_tool_use":
|
|
52
90
|
return {
|
|
@@ -183,12 +221,7 @@ export class PartialMessageBuilder {
|
|
|
183
221
|
}
|
|
184
222
|
if (block.type === "tool_use") {
|
|
185
223
|
this.toolJsonAccum.set(streamIndex, "");
|
|
186
|
-
this.partial.content.push({
|
|
187
|
-
type: "toolCall",
|
|
188
|
-
id: block.id,
|
|
189
|
-
name: block.name,
|
|
190
|
-
arguments: {},
|
|
191
|
-
});
|
|
224
|
+
this.partial.content.push(toolCallFromBlock(block.id, block.name, {}));
|
|
192
225
|
return { type: "toolcall_start", contentIndex, partial: this.partial };
|
|
193
226
|
}
|
|
194
227
|
if (block.type === "server_tool_use") {
|
|
@@ -187,20 +187,36 @@ function extractMessageText(msg: { role: string; content: unknown }): string {
|
|
|
187
187
|
* call effectively stateless. This version serialises the complete
|
|
188
188
|
* conversation history (system prompt + all user/assistant turns) so
|
|
189
189
|
* Claude Code has full context for multi-turn continuity.
|
|
190
|
+
*
|
|
191
|
+
* History is wrapped in XML-tag structure rather than `[User]`/`[Assistant]`
|
|
192
|
+
* bracket headers. Bracket headers read to the model as an in-context
|
|
193
|
+
* demonstration of how turns are delimited, causing it to fabricate fake
|
|
194
|
+
* user turns in its own output. XML tags read as document structure and
|
|
195
|
+
* don't get mirrored in free text.
|
|
190
196
|
*/
|
|
191
197
|
export function buildPromptFromContext(context: Context): string {
|
|
192
|
-
const
|
|
198
|
+
const hasContent = Boolean(context.systemPrompt) || context.messages.some((m) => extractMessageText(m));
|
|
199
|
+
if (!hasContent) return "";
|
|
200
|
+
|
|
201
|
+
const parts: string[] = [
|
|
202
|
+
"Respond only to the final user message below. " +
|
|
203
|
+
"Do not emit <user_message>, <assistant_message>, or <prior_system_context> tags in your response.",
|
|
204
|
+
];
|
|
193
205
|
|
|
194
206
|
if (context.systemPrompt) {
|
|
195
|
-
parts.push(
|
|
207
|
+
parts.push(`<prior_system_context>\n${context.systemPrompt}\n</prior_system_context>`);
|
|
196
208
|
}
|
|
197
209
|
|
|
210
|
+
const turns: string[] = [];
|
|
198
211
|
for (const msg of context.messages) {
|
|
199
212
|
const text = extractMessageText(msg);
|
|
200
213
|
if (!text) continue;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
214
|
+
const tag =
|
|
215
|
+
msg.role === "user" ? "user_message" : msg.role === "assistant" ? "assistant_message" : "system_message";
|
|
216
|
+
turns.push(`<${tag}>\n${text}\n</${tag}>`);
|
|
217
|
+
}
|
|
218
|
+
if (turns.length > 0) {
|
|
219
|
+
parts.push(`<conversation_history>\n${turns.join("\n")}\n</conversation_history>`);
|
|
204
220
|
}
|
|
205
221
|
|
|
206
222
|
return parts.join("\n\n");
|
|
@@ -546,15 +562,18 @@ export function makeAbortedMessage(model: string, lastTextContent: string): Assi
|
|
|
546
562
|
/**
|
|
547
563
|
* Resolve the Claude Code permission mode for the current run.
|
|
548
564
|
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
565
|
+
* GSD subagents run underneath a host Claude Code session the user has
|
|
566
|
+
* already consented to, and their work (edits, shell inspection, MCP calls)
|
|
567
|
+
* spans the full workflow toolset. Defaulting the inner SDK to
|
|
568
|
+
* `bypassPermissions` avoids per-tool approval prompts that offer no
|
|
569
|
+
* meaningful safety beyond what the host session and the subagent prompts
|
|
570
|
+
* already enforce. `GSD_CLAUDE_CODE_PERMISSION_MODE` lets security-conscious
|
|
571
|
+
* users opt into a stricter mode (`acceptEdits`, `default`, `plan`).
|
|
554
572
|
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
573
|
+
* Tradeoff: bypass means a prompt-injection payload read from an untrusted
|
|
574
|
+
* file could trigger tool calls without a second gate. Accepted for GSD
|
|
575
|
+
* because the workflow is explicit user intent and the alternative
|
|
576
|
+
* (#4099) is continuous approval fatigue that blocks real work.
|
|
558
577
|
*/
|
|
559
578
|
export async function resolveClaudePermissionMode(
|
|
560
579
|
env: NodeJS.ProcessEnv = process.env,
|
|
@@ -563,17 +582,7 @@ export async function resolveClaudePermissionMode(
|
|
|
563
582
|
if (override === "bypassPermissions" || override === "acceptEdits" || override === "default" || override === "plan") {
|
|
564
583
|
return override;
|
|
565
584
|
}
|
|
566
|
-
|
|
567
|
-
try {
|
|
568
|
-
const autoMod = (await import("../gsd/auto.js")) as { isAutoActive?: () => boolean };
|
|
569
|
-
if (typeof autoMod.isAutoActive === "function" && autoMod.isAutoActive()) {
|
|
570
|
-
return "bypassPermissions";
|
|
571
|
-
}
|
|
572
|
-
return "acceptEdits";
|
|
573
|
-
} catch {
|
|
574
|
-
// auto.ts unavailable (tests, non-GSD contexts) — stay permissive.
|
|
575
|
-
return "bypassPermissions";
|
|
576
|
-
}
|
|
585
|
+
return "bypassPermissions";
|
|
577
586
|
}
|
|
578
587
|
|
|
579
588
|
/**
|
|
@@ -596,6 +605,21 @@ export function buildSdkOptions(
|
|
|
596
605
|
const mcpServers = buildWorkflowMcpServers();
|
|
597
606
|
const permissionMode = overrides?.permissionMode ?? "bypassPermissions";
|
|
598
607
|
const disallowedTools = ["AskUserQuestion"];
|
|
608
|
+
// Pre-authorize the safe built-ins and every registered workflow MCP
|
|
609
|
+
// server's tools. `acceptEdits` mode (the interactive default) only
|
|
610
|
+
// auto-approves file edits — Read/Glob/Grep, basic shell inspection, and
|
|
611
|
+
// every `mcp__gsd-workflow__*` call still surface as "This command
|
|
612
|
+
// requires approval" and block GSD actions (#4099).
|
|
613
|
+
const allowedTools = [
|
|
614
|
+
"Read",
|
|
615
|
+
"Write",
|
|
616
|
+
"Edit",
|
|
617
|
+
"Glob",
|
|
618
|
+
"Grep",
|
|
619
|
+
"Bash(ls:*)",
|
|
620
|
+
"Bash(pwd)",
|
|
621
|
+
...(mcpServers ? Object.keys(mcpServers).map((serverName) => `mcp__${serverName}__*`) : []),
|
|
622
|
+
];
|
|
599
623
|
return {
|
|
600
624
|
pathToClaudeCodeExecutable: getClaudePath(),
|
|
601
625
|
model: modelId,
|
|
@@ -607,6 +631,7 @@ export function buildSdkOptions(
|
|
|
607
631
|
settingSources: ["project"],
|
|
608
632
|
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
609
633
|
disallowedTools,
|
|
634
|
+
...(allowedTools.length > 0 ? { allowedTools } : {}),
|
|
610
635
|
...(mcpServers ? { mcpServers } : {}),
|
|
611
636
|
betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [],
|
|
612
637
|
...extraOptions,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { PartialMessageBuilder } from "../partial-builder.ts";
|
|
4
|
-
import type { BetaRawMessageStreamEvent } from "../sdk-types.ts";
|
|
3
|
+
import { mapContentBlock, parseMcpToolName, PartialMessageBuilder } from "../partial-builder.ts";
|
|
4
|
+
import type { BetaContentBlock, BetaRawMessageStreamEvent } from "../sdk-types.ts";
|
|
5
5
|
|
|
6
6
|
describe("PartialMessageBuilder — malformed tool arguments (#2574)", () => {
|
|
7
7
|
/**
|
|
@@ -148,3 +148,92 @@ describe("PartialMessageBuilder — malformed tool arguments (#2574)", () => {
|
|
|
148
148
|
}
|
|
149
149
|
});
|
|
150
150
|
});
|
|
151
|
+
|
|
152
|
+
describe("parseMcpToolName", () => {
|
|
153
|
+
test("splits mcp__<server>__<tool> into parts", () => {
|
|
154
|
+
assert.deepEqual(
|
|
155
|
+
parseMcpToolName("mcp__gsd-workflow__gsd_plan_milestone"),
|
|
156
|
+
{ server: "gsd-workflow", tool: "gsd_plan_milestone" },
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("preserves server names containing hyphens", () => {
|
|
161
|
+
assert.deepEqual(
|
|
162
|
+
parseMcpToolName("mcp__my-cool-server__do_thing"),
|
|
163
|
+
{ server: "my-cool-server", tool: "do_thing" },
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("preserves tool names containing underscores", () => {
|
|
168
|
+
assert.deepEqual(
|
|
169
|
+
parseMcpToolName("mcp__srv__a_b_c_d"),
|
|
170
|
+
{ server: "srv", tool: "a_b_c_d" },
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("returns null for non-prefixed names", () => {
|
|
175
|
+
assert.equal(parseMcpToolName("Bash"), null);
|
|
176
|
+
assert.equal(parseMcpToolName("gsd_plan_milestone"), null);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("returns null for malformed prefixes", () => {
|
|
180
|
+
assert.equal(parseMcpToolName("mcp__"), null);
|
|
181
|
+
assert.equal(parseMcpToolName("mcp__server"), null);
|
|
182
|
+
assert.equal(parseMcpToolName("mcp__server__"), null);
|
|
183
|
+
assert.equal(parseMcpToolName("mcp____tool"), null);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("PartialMessageBuilder — MCP tool name normalization", () => {
|
|
188
|
+
test("strips mcp__<server>__ prefix on content_block_start", () => {
|
|
189
|
+
const builder = new PartialMessageBuilder("claude-sonnet-4-20250514");
|
|
190
|
+
const event = builder.handleEvent({
|
|
191
|
+
type: "content_block_start",
|
|
192
|
+
index: 0,
|
|
193
|
+
content_block: {
|
|
194
|
+
type: "tool_use",
|
|
195
|
+
id: "tool_1",
|
|
196
|
+
name: "mcp__gsd-workflow__gsd_plan_milestone",
|
|
197
|
+
input: {},
|
|
198
|
+
},
|
|
199
|
+
} as BetaRawMessageStreamEvent);
|
|
200
|
+
|
|
201
|
+
assert.ok(event, "event should not be null");
|
|
202
|
+
assert.equal(event!.type, "toolcall_start");
|
|
203
|
+
if (event!.type === "toolcall_start") {
|
|
204
|
+
const toolCall = (event.partial.content[event.contentIndex] as any);
|
|
205
|
+
assert.equal(toolCall.name, "gsd_plan_milestone");
|
|
206
|
+
assert.equal(toolCall.mcpServer, "gsd-workflow");
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("leaves non-MCP tool names untouched", () => {
|
|
211
|
+
const builder = new PartialMessageBuilder("claude-sonnet-4-20250514");
|
|
212
|
+
const event = builder.handleEvent({
|
|
213
|
+
type: "content_block_start",
|
|
214
|
+
index: 0,
|
|
215
|
+
content_block: { type: "tool_use", id: "tool_1", name: "Bash", input: {} },
|
|
216
|
+
} as BetaRawMessageStreamEvent);
|
|
217
|
+
|
|
218
|
+
assert.ok(event);
|
|
219
|
+
if (event!.type === "toolcall_start") {
|
|
220
|
+
const toolCall = (event.partial.content[event.contentIndex] as any);
|
|
221
|
+
assert.equal(toolCall.name, "Bash");
|
|
222
|
+
assert.equal(toolCall.mcpServer, undefined);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("mapContentBlock strips MCP prefix on full tool_use blocks", () => {
|
|
227
|
+
const block: BetaContentBlock = {
|
|
228
|
+
type: "tool_use",
|
|
229
|
+
id: "tool_2",
|
|
230
|
+
name: "mcp__gsd-workflow__gsd_task_complete",
|
|
231
|
+
input: { taskId: "T001" },
|
|
232
|
+
};
|
|
233
|
+
const mapped = mapContentBlock(block) as any;
|
|
234
|
+
assert.equal(mapped.type, "toolCall");
|
|
235
|
+
assert.equal(mapped.name, "gsd_task_complete");
|
|
236
|
+
assert.equal(mapped.mcpServer, "gsd-workflow");
|
|
237
|
+
assert.deepEqual(mapped.arguments, { taskId: "T001" });
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -167,6 +167,98 @@ describe("stream-adapter — full context prompt (#2859)", () => {
|
|
|
167
167
|
});
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Bug #4102 — transcript fabrication regression tests
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
describe("stream-adapter — no transcript fabrication (#4102)", () => {
|
|
175
|
+
test("buildPromptFromContext never emits forbidden [User]/[Assistant] bracket headers", () => {
|
|
176
|
+
const context: Context = {
|
|
177
|
+
systemPrompt: "You are a helpful assistant.",
|
|
178
|
+
messages: [
|
|
179
|
+
{ role: "user", content: "First" } as Message,
|
|
180
|
+
{
|
|
181
|
+
role: "assistant",
|
|
182
|
+
content: [{ type: "text", text: "Second" }],
|
|
183
|
+
api: "anthropic-messages",
|
|
184
|
+
provider: "claude-code",
|
|
185
|
+
model: "claude-sonnet-4-20250514",
|
|
186
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
|
187
|
+
stopReason: "stop",
|
|
188
|
+
timestamp: Date.now(),
|
|
189
|
+
} as Message,
|
|
190
|
+
{ role: "user", content: "Third" } as Message,
|
|
191
|
+
],
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const prompt = buildPromptFromContext(context);
|
|
195
|
+
|
|
196
|
+
assert.ok(!prompt.includes("[User]"), "prompt must not include literal [User] bracket header");
|
|
197
|
+
assert.ok(!prompt.includes("[Assistant]"), "prompt must not include literal [Assistant] bracket header");
|
|
198
|
+
assert.ok(!prompt.includes("[System]"), "prompt must not include literal [System] bracket header");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("buildPromptFromContext wraps history in XML-tag structure", () => {
|
|
202
|
+
const context: Context = {
|
|
203
|
+
systemPrompt: "You are helpful.",
|
|
204
|
+
messages: [
|
|
205
|
+
{ role: "user", content: "Hello" } as Message,
|
|
206
|
+
{
|
|
207
|
+
role: "assistant",
|
|
208
|
+
content: [{ type: "text", text: "Hi there" }],
|
|
209
|
+
api: "anthropic-messages",
|
|
210
|
+
provider: "claude-code",
|
|
211
|
+
model: "claude-sonnet-4-20250514",
|
|
212
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
|
213
|
+
stopReason: "stop",
|
|
214
|
+
timestamp: Date.now(),
|
|
215
|
+
} as Message,
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const prompt = buildPromptFromContext(context);
|
|
220
|
+
|
|
221
|
+
assert.ok(prompt.includes("<conversation_history>"), "prompt must wrap history in <conversation_history>");
|
|
222
|
+
assert.ok(prompt.includes("</conversation_history>"), "prompt must close <conversation_history>");
|
|
223
|
+
assert.ok(prompt.includes("<user_message>\nHello\n</user_message>"), "user turn must use <user_message> tags");
|
|
224
|
+
assert.ok(prompt.includes("<assistant_message>\nHi there\n</assistant_message>"), "assistant turn must use <assistant_message> tags");
|
|
225
|
+
assert.ok(prompt.includes("<prior_system_context>\nYou are helpful.\n</prior_system_context>"), "system prompt must use <prior_system_context> tags");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("buildPromptFromContext includes a do-not-echo-tags directive as primary instruction", () => {
|
|
229
|
+
const context: Context = {
|
|
230
|
+
messages: [{ role: "user", content: "Anything" } as Message],
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const prompt = buildPromptFromContext(context);
|
|
234
|
+
|
|
235
|
+
assert.ok(
|
|
236
|
+
prompt.startsWith("Respond only to the final user message"),
|
|
237
|
+
"primary directive must lead the prompt",
|
|
238
|
+
);
|
|
239
|
+
assert.ok(prompt.includes("Do not emit <user_message>"), "directive must forbid emitting user_message tag");
|
|
240
|
+
assert.ok(prompt.includes("<assistant_message>"), "directive must mention assistant_message tag");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("buildPromptFromContext omits <conversation_history> when there are no messages but a system prompt", () => {
|
|
244
|
+
const context: Context = {
|
|
245
|
+
systemPrompt: "Seed",
|
|
246
|
+
messages: [],
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const prompt = buildPromptFromContext(context);
|
|
250
|
+
|
|
251
|
+
assert.ok(prompt.includes("<prior_system_context>"), "system prompt must still render");
|
|
252
|
+
assert.ok(!prompt.includes("<conversation_history>"), "no history wrapper when messages are empty");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("buildPromptFromContext still returns empty string when context is entirely empty", () => {
|
|
256
|
+
const context: Context = { messages: [] };
|
|
257
|
+
const prompt = buildPromptFromContext(context);
|
|
258
|
+
assert.equal(prompt, "", "empty context must not emit a bare directive");
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
170
262
|
describe("stream-adapter — Claude Code external tool results", () => {
|
|
171
263
|
test("extractToolResultsFromSdkUserMessage maps tool_result content to tool payloads", () => {
|
|
172
264
|
const message: SDKUserMessage = {
|
|
@@ -277,6 +369,16 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
277
369
|
assert.equal(srv.env.GSD_PERSIST_WRITE_GATE_STATE, "1");
|
|
278
370
|
assert.equal(srv.env.GSD_WORKFLOW_PROJECT_ROOT, "/tmp/project");
|
|
279
371
|
assert.deepEqual(options.disallowedTools, ["AskUserQuestion"]);
|
|
372
|
+
assert.deepEqual(options.allowedTools, [
|
|
373
|
+
"Read",
|
|
374
|
+
"Write",
|
|
375
|
+
"Edit",
|
|
376
|
+
"Glob",
|
|
377
|
+
"Grep",
|
|
378
|
+
"Bash(ls:*)",
|
|
379
|
+
"Bash(pwd)",
|
|
380
|
+
"mcp__gsd-workflow__*",
|
|
381
|
+
]);
|
|
280
382
|
} finally {
|
|
281
383
|
process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
|
|
282
384
|
process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
|
|
@@ -305,6 +407,16 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
305
407
|
const mcpServers = options.mcpServers as Record<string, any>;
|
|
306
408
|
assert.ok(mcpServers?.["custom-workflow"], "expected custom workflow server config");
|
|
307
409
|
assert.deepEqual(options.disallowedTools, ["AskUserQuestion"]);
|
|
410
|
+
assert.deepEqual(options.allowedTools, [
|
|
411
|
+
"Read",
|
|
412
|
+
"Write",
|
|
413
|
+
"Edit",
|
|
414
|
+
"Glob",
|
|
415
|
+
"Grep",
|
|
416
|
+
"Bash(ls:*)",
|
|
417
|
+
"Bash(pwd)",
|
|
418
|
+
"mcp__custom-workflow__*",
|
|
419
|
+
]);
|
|
308
420
|
} finally {
|
|
309
421
|
process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
|
|
310
422
|
process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
|
|
@@ -29,6 +29,69 @@ import {
|
|
|
29
29
|
import { debugLog } from "../debug-logger.js";
|
|
30
30
|
import { isInfrastructureError, isTransientCooldownError, getCooldownRetryAfterMs, COOLDOWN_FALLBACK_WAIT_MS, MAX_COOLDOWN_RETRIES } from "./infra-errors.js";
|
|
31
31
|
import { resolveEngine } from "../engine-resolver.js";
|
|
32
|
+
import { logWarning } from "../workflow-logger.js";
|
|
33
|
+
import { gsdRoot } from "../paths.js";
|
|
34
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
|
|
37
|
+
// ── Stuck detection persistence (#3704) ──────────────────────────────────
|
|
38
|
+
// Persist stuck detection state to disk so it survives session restarts.
|
|
39
|
+
// Without this, restarting auto-mode resets all counters, allowing the
|
|
40
|
+
// same blocked unit to burn a full retry budget each session.
|
|
41
|
+
function stuckStatePath(basePath: string): string {
|
|
42
|
+
return join(gsdRoot(basePath), "runtime", "stuck-state.json");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function loadStuckState(basePath: string): { recentUnits: Array<{ key: string }>; stuckRecoveryAttempts: number } {
|
|
46
|
+
try {
|
|
47
|
+
const data = JSON.parse(readFileSync(stuckStatePath(basePath), "utf-8"));
|
|
48
|
+
return {
|
|
49
|
+
recentUnits: Array.isArray(data.recentUnits) ? data.recentUnits : [],
|
|
50
|
+
stuckRecoveryAttempts: typeof data.stuckRecoveryAttempts === "number" ? data.stuckRecoveryAttempts : 0,
|
|
51
|
+
};
|
|
52
|
+
} catch (err) {
|
|
53
|
+
debugLog("autoLoop", { phase: "load-stuck-state-failed", error: err instanceof Error ? err.message : String(err) });
|
|
54
|
+
return { recentUnits: [], stuckRecoveryAttempts: 0 };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function saveStuckState(basePath: string, state: LoopState): void {
|
|
59
|
+
try {
|
|
60
|
+
const filePath = stuckStatePath(basePath);
|
|
61
|
+
mkdirSync(join(gsdRoot(basePath), "runtime"), { recursive: true });
|
|
62
|
+
writeFileSync(filePath, JSON.stringify({
|
|
63
|
+
recentUnits: state.recentUnits.slice(-20), // keep last 20 entries
|
|
64
|
+
stuckRecoveryAttempts: state.stuckRecoveryAttempts,
|
|
65
|
+
updatedAt: new Date().toISOString(),
|
|
66
|
+
}) + "\n");
|
|
67
|
+
} catch (err) {
|
|
68
|
+
debugLog("autoLoop", { phase: "save-stuck-state-failed", error: err instanceof Error ? err.message : String(err) });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Memory pressure monitoring (#3331) ──────────────────────────────────
|
|
73
|
+
// Check heap usage every N iterations and trigger graceful shutdown before
|
|
74
|
+
// the OS OOM killer sends SIGKILL. The threshold is 90% of the V8 heap
|
|
75
|
+
// limit (--max-old-space-size or default ~1.5-4GB depending on platform).
|
|
76
|
+
const MEMORY_CHECK_INTERVAL = 5; // check every 5 iterations
|
|
77
|
+
const MEMORY_PRESSURE_THRESHOLD = 0.85; // 85% of heap limit
|
|
78
|
+
|
|
79
|
+
function checkMemoryPressure(): { pressured: boolean; heapMB: number; limitMB: number; pct: number } {
|
|
80
|
+
const mem = process.memoryUsage();
|
|
81
|
+
// v8.getHeapStatistics() gives heap_size_limit but requires import
|
|
82
|
+
// Use a conservative estimate: RSS > 3GB is danger zone on most systems
|
|
83
|
+
const heapMB = Math.round(mem.heapUsed / 1024 / 1024);
|
|
84
|
+
const rssMB = Math.round(mem.rss / 1024 / 1024);
|
|
85
|
+
// Try to get the actual V8 heap limit
|
|
86
|
+
let limitMB = 4096; // conservative default
|
|
87
|
+
try {
|
|
88
|
+
const v8 = require("node:v8");
|
|
89
|
+
const stats = v8.getHeapStatistics();
|
|
90
|
+
limitMB = Math.round(stats.heap_size_limit / 1024 / 1024);
|
|
91
|
+
} catch { limitMB = 4096; /* v8 stats unavailable — use conservative default */ }
|
|
92
|
+
const pct = heapMB / limitMB;
|
|
93
|
+
return { pressured: pct > MEMORY_PRESSURE_THRESHOLD, heapMB, limitMB, pct };
|
|
94
|
+
}
|
|
32
95
|
|
|
33
96
|
/**
|
|
34
97
|
* Main auto-mode execution loop. Iterates: derive → dispatch → guards →
|
|
@@ -46,7 +109,13 @@ export async function autoLoop(
|
|
|
46
109
|
): Promise<void> {
|
|
47
110
|
debugLog("autoLoop", { phase: "enter" });
|
|
48
111
|
let iteration = 0;
|
|
49
|
-
|
|
112
|
+
// Load persisted stuck state so counters survive session restarts (#3704)
|
|
113
|
+
const persisted = loadStuckState(s.basePath);
|
|
114
|
+
const loopState: LoopState = {
|
|
115
|
+
recentUnits: persisted.recentUnits,
|
|
116
|
+
stuckRecoveryAttempts: persisted.stuckRecoveryAttempts,
|
|
117
|
+
consecutiveFinalizeTimeouts: 0,
|
|
118
|
+
};
|
|
50
119
|
let consecutiveErrors = 0;
|
|
51
120
|
let consecutiveCooldowns = 0;
|
|
52
121
|
const recentErrorMessages: string[] = [];
|
|
@@ -74,6 +143,24 @@ export async function autoLoop(
|
|
|
74
143
|
break;
|
|
75
144
|
}
|
|
76
145
|
|
|
146
|
+
// ── Memory pressure check (#3331) ──
|
|
147
|
+
// Graceful shutdown before OOM killer sends SIGKILL.
|
|
148
|
+
if (iteration % MEMORY_CHECK_INTERVAL === 0) {
|
|
149
|
+
const mem = checkMemoryPressure();
|
|
150
|
+
debugLog("autoLoop", { phase: "memory-check", ...mem });
|
|
151
|
+
if (mem.pressured) {
|
|
152
|
+
logWarning("dispatch", `Memory pressure: ${mem.heapMB}MB / ${mem.limitMB}MB (${Math.round(mem.pct * 100)}%) — stopping auto-mode to prevent OOM kill`);
|
|
153
|
+
await deps.stopAuto(
|
|
154
|
+
ctx,
|
|
155
|
+
pi,
|
|
156
|
+
`Memory pressure: heap at ${mem.heapMB}MB / ${mem.limitMB}MB (${Math.round(mem.pct * 100)}%). ` +
|
|
157
|
+
`Stopping gracefully to prevent OOM kill after ${iteration} iterations. ` +
|
|
158
|
+
`Resume with /gsd auto to continue from where you left off.`,
|
|
159
|
+
);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
77
164
|
if (!s.cmdCtx) {
|
|
78
165
|
debugLog("autoLoop", { phase: "exit", reason: "no-cmdCtx" });
|
|
79
166
|
break;
|
|
@@ -207,6 +294,7 @@ export async function autoLoop(
|
|
|
207
294
|
consecutiveCooldowns = 0;
|
|
208
295
|
recentErrorMessages.length = 0;
|
|
209
296
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-end", data: { iteration } });
|
|
297
|
+
saveStuckState(s.basePath, loopState); // persist across session restarts (#3704)
|
|
210
298
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|
|
211
299
|
|
|
212
300
|
if (reconcileResult.outcome === "milestone-complete") {
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
buildTaskFileName,
|
|
26
26
|
} from "./paths.js";
|
|
27
27
|
import { invalidateAllCaches } from "./cache.js";
|
|
28
|
+
import { rebuildState } from "./doctor.js";
|
|
28
29
|
import { parseUnitId } from "./unit-id.js";
|
|
29
30
|
import { closeoutUnit, type CloseoutOptions } from "./auto-unit-closeout.js";
|
|
30
31
|
import {
|
|
@@ -367,6 +368,12 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
|
|
|
367
368
|
}
|
|
368
369
|
});
|
|
369
370
|
|
|
371
|
+
// Keep the on-disk STATE.md aligned with the live derived state after
|
|
372
|
+
// ordinary unit completion, before any worktree state is synced back.
|
|
373
|
+
await runSafely("postUnit", "state-rebuild", async () => {
|
|
374
|
+
await rebuildState(s.basePath);
|
|
375
|
+
});
|
|
376
|
+
|
|
370
377
|
// Sync worktree state back to project root (skipped for lightweight sidecars)
|
|
371
378
|
if (!opts?.skipWorktreeSync && s.originalBasePath && s.originalBasePath !== s.basePath) {
|
|
372
379
|
await runSafely("postUnit", "worktree-sync", () => {
|
|
@@ -272,6 +272,16 @@ export function verifyExpectedArtifact(
|
|
|
272
272
|
if (!isValidationTerminal(validationContent)) return false;
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
if (unitType === "plan-milestone") {
|
|
276
|
+
try {
|
|
277
|
+
const roadmap = parseLegacyRoadmap(readFileSync(absPath, "utf-8"));
|
|
278
|
+
if (roadmap.slices.length === 0) return false;
|
|
279
|
+
} catch (err) {
|
|
280
|
+
logWarning("recovery", `plan-milestone roadmap verification failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
275
285
|
// plan-slice must produce a plan with actual task entries, not just a scaffold.
|
|
276
286
|
// The plan file may exist from a prior discussion/context step with only headings
|
|
277
287
|
// but no tasks. Without this check the artifact is considered "complete" and the
|