@opengsd/gsd-pi 1.4.0 → 1.5.0
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 +11 -11
- package/dist/onboarding.js +11 -0
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/GSD-WORKFLOW.md +20 -25
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +118 -53
- package/dist/resources/extensions/cursor-cli/index.js +16 -0
- package/dist/resources/extensions/cursor-cli/models.js +63 -0
- package/dist/resources/extensions/cursor-cli/package.json +11 -0
- package/dist/resources/extensions/cursor-cli/readiness.js +128 -0
- package/dist/resources/extensions/cursor-cli/stream-adapter.js +353 -0
- package/dist/resources/extensions/gsd/activity-log.js +4 -1
- package/dist/resources/extensions/gsd/artifact-verification.js +25 -9
- package/dist/resources/extensions/gsd/auto/closeout.js +45 -42
- package/dist/resources/extensions/gsd/auto/finalize.js +9 -0
- package/dist/resources/extensions/gsd/auto/orchestrator.js +1 -1
- package/dist/resources/extensions/gsd/auto/run-unit.js +5 -2
- package/dist/resources/extensions/gsd/auto/schedule-wakeup.js +3 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +15 -5
- package/dist/resources/extensions/gsd/auto-model-selection.js +36 -18
- package/dist/resources/extensions/gsd/auto-prompts.js +76 -2
- package/dist/resources/extensions/gsd/auto-start.js +4 -3
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +4 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +38 -11
- package/dist/resources/extensions/gsd/auto.js +23 -3
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +3 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +114 -5
- package/dist/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.js +58 -9
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +10 -2
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +14 -1
- package/dist/resources/extensions/gsd/codebase-generator.js +157 -19
- package/dist/resources/extensions/gsd/commands/handlers/core.js +40 -4
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +153 -1
- package/dist/resources/extensions/gsd/commands-worktree.js +11 -2
- package/dist/resources/extensions/gsd/db/queries.js +39 -0
- package/dist/resources/extensions/gsd/discussion-handoff.js +8 -3
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +8 -2
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +206 -14
- package/dist/resources/extensions/gsd/doctor-providers.js +38 -10
- package/dist/resources/extensions/gsd/doctor.js +93 -3
- package/dist/resources/extensions/gsd/exec-sandbox.js +6 -2
- package/dist/resources/extensions/gsd/flat-phase-migration.js +74 -23
- package/dist/resources/extensions/gsd/git-service.js +14 -18
- package/dist/resources/extensions/gsd/gsd-db.js +52 -0
- package/dist/resources/extensions/gsd/guidance.js +1 -0
- package/dist/resources/extensions/gsd/guided-flow.js +40 -2
- package/dist/resources/extensions/gsd/key-manager.js +1 -0
- package/dist/resources/extensions/gsd/markdown-renderer.js +25 -8
- package/dist/resources/extensions/gsd/migrate-external.js +24 -7
- package/dist/resources/extensions/gsd/milestone-merge-transaction.js +3 -3
- package/dist/resources/extensions/gsd/preferences-diagnostics.js +24 -1
- package/dist/resources/extensions/gsd/preferences-models.js +6 -4
- package/dist/resources/extensions/gsd/preferences-validation.js +25 -2
- package/dist/resources/extensions/gsd/preferences.js +29 -2
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +4 -2
- package/dist/resources/extensions/gsd/redact-secrets.js +37 -0
- package/dist/resources/extensions/gsd/repo-identity.js +16 -0
- package/dist/resources/extensions/gsd/repository-registry.js +6 -1
- package/dist/resources/extensions/gsd/root-write-leak-guard.js +4 -2
- package/dist/resources/extensions/gsd/safe-fs.js +23 -2
- package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +7 -9
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +1 -0
- package/dist/resources/extensions/gsd/tools/plan-task.js +76 -8
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +40 -0
- package/dist/resources/extensions/gsd/unit-context-manifest.js +5 -4
- package/dist/resources/extensions/gsd/unit-registry.js +27 -2
- package/dist/resources/extensions/gsd/unit-runtime.js +21 -0
- package/dist/resources/extensions/gsd/workspace.js +26 -8
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +67 -5
- package/dist/resources/extensions/gsd/worktree-manager.js +97 -4
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +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 +7 -7
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/wizard.d.ts +8 -0
- package/dist/wizard.js +8 -1
- package/integrations/hermes/CONTEXT.md +47 -0
- package/integrations/hermes/README.md +21 -0
- package/integrations/hermes/__init__.py +31 -0
- package/integrations/hermes/docs/issue-1162-grilling.md +197 -0
- package/integrations/hermes/docs/setup.md +4 -0
- package/integrations/hermes/open_gsd_hermes/commands.py +141 -0
- package/integrations/hermes/open_gsd_hermes/config.py +2 -0
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +594 -31
- package/integrations/hermes/open_gsd_hermes/notifications.py +10 -1
- package/integrations/hermes/plugin.yaml +4 -0
- package/integrations/hermes/tests/test_config.py +6 -0
- package/integrations/hermes/tests/test_directory_loader.py +37 -0
- package/integrations/hermes/tests/test_gsd_client_cache.py +3 -0
- package/integrations/hermes/tests/test_gsd_client_execute.py +35 -0
- package/integrations/hermes/tests/test_gsd_client_process_cwd.py +22 -0
- package/integrations/hermes/tests/test_gsd_client_timeout.py +98 -0
- package/integrations/hermes/tests/test_milestone_client.py +447 -0
- package/integrations/hermes/tests/test_new_milestone_command.py +313 -0
- package/integrations/hermes/tests/test_supervisor_fsm.py +40 -0
- package/package.json +8 -8
- package/packages/cloud-mcp-gateway/dist/auth-store.d.ts +1 -0
- package/packages/cloud-mcp-gateway/dist/auth-store.d.ts.map +1 -1
- package/packages/cloud-mcp-gateway/dist/auth-store.js +21 -1
- package/packages/cloud-mcp-gateway/dist/auth-store.js.map +1 -1
- package/packages/cloud-mcp-gateway/dist/auth-store.test.js +34 -0
- package/packages/cloud-mcp-gateway/dist/auth-store.test.js.map +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/dist/cloud-runtime.d.ts +2 -0
- package/packages/daemon/dist/cloud-runtime.d.ts.map +1 -1
- package/packages/daemon/dist/cloud-runtime.js +25 -1
- package/packages/daemon/dist/cloud-runtime.js.map +1 -1
- package/packages/daemon/dist/cloud-runtime.test.js +51 -0
- package/packages/daemon/dist/cloud-runtime.test.js.map +1 -1
- package/packages/daemon/dist/message-batcher.d.ts +7 -0
- package/packages/daemon/dist/message-batcher.d.ts.map +1 -1
- package/packages/daemon/dist/message-batcher.js +13 -0
- package/packages/daemon/dist/message-batcher.js.map +1 -1
- package/packages/daemon/dist/message-batcher.test.js +48 -0
- package/packages/daemon/dist/message-batcher.test.js.map +1 -1
- package/packages/daemon/dist/session-manager.js +1 -1
- package/packages/daemon/dist/session-manager.js.map +1 -1
- package/packages/daemon/package.json +5 -5
- package/packages/gsd-agent-core/dist/compaction/compaction.d.ts +13 -1
- package/packages/gsd-agent-core/dist/compaction/compaction.d.ts.map +1 -1
- package/packages/gsd-agent-core/dist/compaction/compaction.js +15 -1
- package/packages/gsd-agent-core/dist/compaction/compaction.js.map +1 -1
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.d.ts.map +1 -1
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.js +4 -1
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.js.map +1 -1
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js +1 -1
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/server.d.ts +18 -5
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +46 -5
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/session-manager.js +1 -1
- package/packages/mcp-server/dist/session-manager.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/env-api-keys.d.ts.map +1 -1
- package/packages/pi-ai/dist/env-api-keys.js +3 -0
- package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +2 -2
- 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/package.json +1 -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/session-manager-types.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager-types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +3 -0
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/dist/rpc-client.js +1 -1
- package/packages/rpc-client/dist/rpc-client.js.map +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/scripts/link-workspace-packages.cjs +14 -4
- package/src/resources/GSD-WORKFLOW.md +20 -25
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +130 -51
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +371 -39
- package/src/resources/extensions/cursor-cli/index.ts +18 -0
- package/src/resources/extensions/cursor-cli/models.ts +65 -0
- package/src/resources/extensions/cursor-cli/package.json +11 -0
- package/src/resources/extensions/cursor-cli/readiness.ts +143 -0
- package/src/resources/extensions/cursor-cli/stream-adapter.ts +440 -0
- package/src/resources/extensions/cursor-cli/tests/index.test.ts +54 -0
- package/src/resources/extensions/cursor-cli/tests/readiness.test.ts +29 -0
- package/src/resources/extensions/cursor-cli/tests/stream-adapter.test.ts +166 -0
- package/src/resources/extensions/gsd/activity-log.ts +4 -1
- package/src/resources/extensions/gsd/artifact-verification.ts +25 -9
- package/src/resources/extensions/gsd/auto/closeout.ts +54 -56
- package/src/resources/extensions/gsd/auto/finalize.ts +8 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/orchestrator.ts +1 -1
- package/src/resources/extensions/gsd/auto/run-unit.ts +5 -2
- package/src/resources/extensions/gsd/auto/schedule-wakeup.ts +8 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +17 -5
- package/src/resources/extensions/gsd/auto-model-selection.ts +49 -18
- package/src/resources/extensions/gsd/auto-prompts.ts +73 -2
- package/src/resources/extensions/gsd/auto-start.ts +4 -3
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +4 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +50 -11
- package/src/resources/extensions/gsd/auto.ts +22 -3
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +3 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +128 -5
- package/src/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.ts +80 -12
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +12 -2
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +23 -1
- package/src/resources/extensions/gsd/codebase-generator.ts +179 -18
- package/src/resources/extensions/gsd/commands/handlers/core.ts +39 -3
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +135 -1
- package/src/resources/extensions/gsd/commands-worktree.ts +13 -2
- package/src/resources/extensions/gsd/db/queries.ts +40 -0
- package/src/resources/extensions/gsd/discussion-handoff.ts +10 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +8 -2
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +269 -18
- package/src/resources/extensions/gsd/doctor-providers.ts +39 -10
- package/src/resources/extensions/gsd/doctor-types.ts +5 -1
- package/src/resources/extensions/gsd/doctor.ts +95 -3
- package/src/resources/extensions/gsd/exec-sandbox.ts +6 -2
- package/src/resources/extensions/gsd/flat-phase-migration.ts +98 -22
- package/src/resources/extensions/gsd/git-service.ts +14 -17
- package/src/resources/extensions/gsd/gsd-db.ts +72 -0
- package/src/resources/extensions/gsd/guidance.ts +2 -0
- package/src/resources/extensions/gsd/guided-flow.ts +45 -2
- package/src/resources/extensions/gsd/key-manager.ts +1 -0
- package/src/resources/extensions/gsd/markdown-renderer.ts +25 -7
- package/src/resources/extensions/gsd/migrate-external.ts +23 -6
- package/src/resources/extensions/gsd/milestone-merge-transaction.ts +4 -4
- package/src/resources/extensions/gsd/model-router.ts +4 -5
- package/src/resources/extensions/gsd/preferences-diagnostics.ts +24 -0
- package/src/resources/extensions/gsd/preferences-models.ts +6 -3
- package/src/resources/extensions/gsd/preferences-validation.ts +28 -2
- package/src/resources/extensions/gsd/preferences.ts +38 -4
- package/src/resources/extensions/gsd/prompts/plan-slice.md +4 -2
- package/src/resources/extensions/gsd/redact-secrets.ts +40 -0
- package/src/resources/extensions/gsd/repo-identity.ts +17 -0
- package/src/resources/extensions/gsd/repository-registry.ts +7 -1
- package/src/resources/extensions/gsd/root-write-leak-guard.ts +3 -2
- package/src/resources/extensions/gsd/safe-fs.ts +22 -2
- package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +7 -9
- package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +1 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +131 -5
- package/src/resources/extensions/gsd/tests/auto-migrating-recovery.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +178 -0
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/auto-remote-session-lock-cleanup.test.ts +41 -0
- package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +41 -0
- package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/auto-timeout-recovery-harness-abort.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/batch-task-query.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +29 -0
- package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +365 -0
- package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +62 -1
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +138 -1
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +397 -5
- package/src/resources/extensions/gsd/tests/doctor-workspace.test.ts +207 -0
- package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +38 -1
- package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +87 -19
- package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +89 -0
- package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +122 -0
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +152 -22
- package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +50 -6
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/plan-task.test.ts +264 -1
- package/src/resources/extensions/gsd/tests/preferences.test.ts +150 -0
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +142 -0
- package/src/resources/extensions/gsd/tests/recovery-verify-logs.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/redact-secrets.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/register-hooks-flat-phase-migration.test.ts +73 -0
- package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +275 -0
- package/src/resources/extensions/gsd/tests/repository-registry.test.ts +41 -2
- package/src/resources/extensions/gsd/tests/require-slice-discussion-dispatch.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +31 -9
- package/src/resources/extensions/gsd/tests/schedule-wakeup-tool.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/status-per-repo.test.ts +94 -0
- package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +63 -0
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +66 -18
- package/src/resources/extensions/gsd/tests/unit-registry.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/uok-gitops-turn-action.test.ts +52 -0
- package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +53 -1
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +208 -1
- package/src/resources/extensions/gsd/tests/workspace.test.ts +18 -4
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +31 -1
- package/src/resources/extensions/gsd/tests/worktree-teardown-safety.test.ts +32 -0
- package/src/resources/extensions/gsd/tools/plan-task.ts +107 -9
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +58 -0
- package/src/resources/extensions/gsd/unit-context-manifest.ts +11 -5
- package/src/resources/extensions/gsd/unit-registry.ts +34 -2
- package/src/resources/extensions/gsd/unit-runtime.ts +42 -0
- package/src/resources/extensions/gsd/workspace.ts +42 -8
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +135 -11
- package/src/resources/extensions/gsd/worktree-manager.ts +143 -5
- /package/dist/web/standalone/.next/static/{zQLcXm0Za8lexoXtgDpMG → bCg3aYrFN1DKZr2dqgE7L}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{zQLcXm0Za8lexoXtgDpMG → bCg3aYrFN1DKZr2dqgE7L}/_ssgManifest.js +0 -0
|
@@ -13,6 +13,7 @@ import { resolveManifest } from "../unit-context-manifest.js";
|
|
|
13
13
|
import { isBlockedStateFile, isBashWriteToStateFile, BLOCKED_WRITE_ERROR } from "../write-intercept.js";
|
|
14
14
|
import { loadFile, saveFile, formatContinue } from "../files.js";
|
|
15
15
|
import { clearAutoCompletionStopInProgress, clearToolInvocationError, getAutoRuntimeSnapshot, getSourceObservationStore, isAutoActive, isAutoCompletionStopInProgress, isAutoPaused, isInteractiveElicitationInFlight, markToolEnd, markToolStart, recordAutoToolSurfaceSnapshot, recordToolInvocationError, } from "../auto-runtime-state.js";
|
|
16
|
+
import { isDeterministicPolicyError, isQueuedUserMessageSkip, isToolInvocationError, isToolUnavailableError, } from "../auto-tool-tracking.js";
|
|
16
17
|
import { applyProviderPayloadPolicy } from "../provider-payload-policy.js";
|
|
17
18
|
import { checkToolCallLoop, recordToolCallLoopMutation, resetToolCallLoopGuard } from "./tool-call-loop-guard.js";
|
|
18
19
|
import { MINIMAL_AUTO_BASE_TOOL_NAMES } from "./core-session-tools.js";
|
|
@@ -44,7 +45,14 @@ import { clearPendingAutoStart } from "../pending-auto-start.js";
|
|
|
44
45
|
import { resolveWorkflowToolBasePath } from "./dynamic-tools.js";
|
|
45
46
|
import { getRequiredWorkflowToolsForUnit } from "../unit-tool-contracts.js";
|
|
46
47
|
import { flushAllManifests } from "../workflow-manifest.js";
|
|
48
|
+
import { recordUnitHarnessAbort } from "../unit-runtime.js";
|
|
47
49
|
let approvalQuestionAbortInFlight = false;
|
|
50
|
+
function recordCurrentUnitHarnessAbort(abort) {
|
|
51
|
+
const dash = getAutoRuntimeSnapshot();
|
|
52
|
+
if (!dash.active || !dash.basePath || !dash.currentUnit)
|
|
53
|
+
return;
|
|
54
|
+
recordUnitHarnessAbort(dash.basePath, dash.currentUnit.type, dash.currentUnit.id, dash.currentUnit.startedAt, abort);
|
|
55
|
+
}
|
|
48
56
|
async function loadWelcomeScreenModule() {
|
|
49
57
|
const candidates = [];
|
|
50
58
|
const gsdBinPath = process.env.GSD_BIN_PATH;
|
|
@@ -160,9 +168,23 @@ const WORKFLOW_GSD_TOOL_NAMES = [
|
|
|
160
168
|
...MINIMAL_GSD_TOOL_NAMES,
|
|
161
169
|
...Object.values(AUTO_UNIT_SCOPED_TOOLS).flat(),
|
|
162
170
|
].filter(isGsdManagedTool);
|
|
171
|
+
const WORKFLOW_ONLY_AUTO_BASE_TOOL_NAMES = [
|
|
172
|
+
"find",
|
|
173
|
+
"glob",
|
|
174
|
+
"grep",
|
|
175
|
+
"ls",
|
|
176
|
+
"read",
|
|
177
|
+
"subagent",
|
|
178
|
+
];
|
|
163
179
|
function isGsdManagedTool(name) {
|
|
164
180
|
return name.startsWith("gsd_") || name === "memory_query" || name === "capture_thought" || name === "gsd_graph";
|
|
165
181
|
}
|
|
182
|
+
function autoBaseToolNamesForUnit(unitType) {
|
|
183
|
+
const manifest = unitType ? resolveManifest(unitType) : null;
|
|
184
|
+
return manifest?.tools.mode === "workflow-only"
|
|
185
|
+
? WORKFLOW_ONLY_AUTO_BASE_TOOL_NAMES
|
|
186
|
+
: MINIMAL_AUTO_BASE_TOOL_NAMES;
|
|
187
|
+
}
|
|
166
188
|
/**
|
|
167
189
|
* Resolves requested tool names against active tools using exact and MCP-scoped matches.
|
|
168
190
|
*
|
|
@@ -213,7 +235,7 @@ export function buildMinimalAutoGsdToolSet(activeToolNames, unitType, registered
|
|
|
213
235
|
return buildRunUatGsdToolSet(activeToolNames, registeredToolNames);
|
|
214
236
|
}
|
|
215
237
|
const unitTools = unitType ? AUTO_UNIT_SCOPED_TOOLS[unitType] ?? [] : [];
|
|
216
|
-
const autoBaseTools = new Set(
|
|
238
|
+
const autoBaseTools = new Set(autoBaseToolNamesForUnit(unitType));
|
|
217
239
|
const availableBaseTools = registeredToolNames.filter((name) => autoBaseTools.has(name));
|
|
218
240
|
const preserved = [...new Set([
|
|
219
241
|
...activeToolNames.filter((name) => autoBaseTools.has(name)),
|
|
@@ -446,6 +468,75 @@ function formatLoopGuardBlockReason(reason) {
|
|
|
446
468
|
return reason;
|
|
447
469
|
return LOOP_GUARD_INTERACTIVE_INSTRUCTIONS.reduce((formatted, instruction) => formatted.replace(instruction, LOOP_GUARD_AUTO_INSTRUCTION), reason);
|
|
448
470
|
}
|
|
471
|
+
function isGateResultPersistenceTool(toolName) {
|
|
472
|
+
return toolName === "gsd_save_gate_result" || toolName === "gsd_uat_result_save";
|
|
473
|
+
}
|
|
474
|
+
function resultDetails(result) {
|
|
475
|
+
if (!result || typeof result !== "object")
|
|
476
|
+
return {};
|
|
477
|
+
const details = result.details;
|
|
478
|
+
return details && typeof details === "object" ? details : {};
|
|
479
|
+
}
|
|
480
|
+
function isAbortedExecutionToolResult(toolName, result) {
|
|
481
|
+
if (toolName !== "gsd_exec" && toolName !== "gsd_uat_exec")
|
|
482
|
+
return false;
|
|
483
|
+
const details = resultDetails(result);
|
|
484
|
+
return details.aborted === true || details.force_resolved === true || details.timed_out === true;
|
|
485
|
+
}
|
|
486
|
+
function isRetryableHarnessToolError(toolName, result, errorText) {
|
|
487
|
+
if (isGateResultPersistenceTool(toolName))
|
|
488
|
+
return false;
|
|
489
|
+
if (isDeterministicPolicyError(errorText))
|
|
490
|
+
return false;
|
|
491
|
+
if (toolName === "gsd_exec" || toolName === "gsd_uat_exec") {
|
|
492
|
+
if (isAbortedExecutionToolResult(toolName, result))
|
|
493
|
+
return true;
|
|
494
|
+
if (isToolUnavailableError(errorText))
|
|
495
|
+
return true;
|
|
496
|
+
if (isQueuedUserMessageSkip(errorText))
|
|
497
|
+
return true;
|
|
498
|
+
return isToolInvocationError(errorText);
|
|
499
|
+
}
|
|
500
|
+
if (isToolUnavailableError(errorText))
|
|
501
|
+
return true;
|
|
502
|
+
if (isQueuedUserMessageSkip(errorText))
|
|
503
|
+
return true;
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
function recordRetryableHarnessToolError(toolName, result, errorText) {
|
|
507
|
+
if (!isRetryableHarnessToolError(toolName, result, errorText))
|
|
508
|
+
return;
|
|
509
|
+
recordCurrentUnitHarnessAbort({
|
|
510
|
+
kind: "tool-error",
|
|
511
|
+
reason: errorText || "Tool execution failed before the unit could complete its gate evaluation.",
|
|
512
|
+
toolName,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
function recordRetryableTurnAbort(event) {
|
|
516
|
+
const origin = typeof event.abortOrigin === "string" ? event.abortOrigin : undefined;
|
|
517
|
+
if (origin === "session-transition")
|
|
518
|
+
return;
|
|
519
|
+
const messages = Array.isArray(event.messages) ? event.messages : [];
|
|
520
|
+
const lastMsg = messages[messages.length - 1];
|
|
521
|
+
const stopReason = lastMsg && typeof lastMsg === "object"
|
|
522
|
+
? lastMsg.stopReason
|
|
523
|
+
: undefined;
|
|
524
|
+
if (stopReason !== "aborted" && stopReason !== "error")
|
|
525
|
+
return;
|
|
526
|
+
const errorMessage = lastMsg && typeof lastMsg === "object"
|
|
527
|
+
? lastMsg.errorMessage
|
|
528
|
+
: undefined;
|
|
529
|
+
const reason = [
|
|
530
|
+
"Agent turn aborted before the unit could complete its gate evaluation.",
|
|
531
|
+
origin ? `origin=${origin}` : undefined,
|
|
532
|
+
typeof stopReason === "string" ? `stopReason=${stopReason}` : undefined,
|
|
533
|
+
typeof errorMessage === "string" && errorMessage.trim() ? `error=${errorMessage.trim()}` : undefined,
|
|
534
|
+
].filter(Boolean).join(" ");
|
|
535
|
+
recordCurrentUnitHarnessAbort({
|
|
536
|
+
kind: "turn-abort",
|
|
537
|
+
reason,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
449
540
|
function beginSourceObservationStoreForCurrentUnit(ctx) {
|
|
450
541
|
if (!isAutoActive())
|
|
451
542
|
return null;
|
|
@@ -696,8 +787,8 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
696
787
|
await applyCompactionThresholdOverride(ctx);
|
|
697
788
|
await prepareWorkflowMcpForHookContext(ctx, basePath);
|
|
698
789
|
// Migrate legacy .gsd/milestones/ to flat-phase .gsd/phases/ when detected.
|
|
699
|
-
//
|
|
700
|
-
//
|
|
790
|
+
// Fail closed on migration errors: resolvers assume flat-phase paths after
|
|
791
|
+
// startup, so continuing with nested disk state corrupts later checks.
|
|
701
792
|
try {
|
|
702
793
|
const { isInAutoWorktree } = await import("../auto-worktree.js");
|
|
703
794
|
if (!isInAutoWorktree(basePath)) {
|
|
@@ -710,10 +801,18 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
710
801
|
await migrateToFlatPhase(basePath);
|
|
711
802
|
}
|
|
712
803
|
else {
|
|
713
|
-
safetyLogWarning("bootstrap", "flat-phase migration
|
|
804
|
+
safetyLogWarning("bootstrap", "flat-phase migration required: legacy .gsd/milestones/ layout detected but the workflow database could not be opened — fix database access before starting GSD");
|
|
805
|
+
throw new Error("flat-phase migration required but the workflow database could not be opened; fix database access before starting GSD");
|
|
714
806
|
}
|
|
715
807
|
}
|
|
716
808
|
}
|
|
809
|
+
}
|
|
810
|
+
catch (err) {
|
|
811
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
812
|
+
safetyLogWarning("bootstrap", `flat-phase migration failed: ${message}`);
|
|
813
|
+
throw new Error(`flat-phase migration failed: ${message}`);
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
717
816
|
const projectRoot = resolveWorktreeProjectRoot(basePath);
|
|
718
817
|
const { pruneStaleFlatPhaseBackups } = await import("../flat-phase-migration.js");
|
|
719
818
|
const pruned = pruneStaleFlatPhaseBackups(projectRoot);
|
|
@@ -722,7 +821,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
722
821
|
}
|
|
723
822
|
}
|
|
724
823
|
catch (err) {
|
|
725
|
-
safetyLogWarning("bootstrap", `flat-phase
|
|
824
|
+
safetyLogWarning("bootstrap", `flat-phase backup pruning: ${err instanceof Error ? err.message : String(err)}`);
|
|
726
825
|
}
|
|
727
826
|
// Apply show_token_cost preference (#1515)
|
|
728
827
|
try {
|
|
@@ -871,6 +970,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
871
970
|
});
|
|
872
971
|
pi.on("agent_end", async (event, ctx) => {
|
|
873
972
|
approvalQuestionAbortInFlight = false;
|
|
973
|
+
recordRetryableTurnAbort(event);
|
|
874
974
|
resetToolCallLoopGuard();
|
|
875
975
|
resetPendingGatePauseGuard();
|
|
876
976
|
await resetAskUserQuestionsTurnCache();
|
|
@@ -1077,6 +1177,12 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
1077
1177
|
// ── Loop guard: block repeated identical tool calls ──
|
|
1078
1178
|
const loopCheck = checkToolCallLoop(toolName, event.input);
|
|
1079
1179
|
if (loopCheck.block) {
|
|
1180
|
+
recordCurrentUnitHarnessAbort({
|
|
1181
|
+
kind: "tool-loop-guard",
|
|
1182
|
+
reason: loopCheck.reason ?? "Tool-call loop guard blocked a repeated tool call.",
|
|
1183
|
+
toolName,
|
|
1184
|
+
count: loopCheck.count,
|
|
1185
|
+
});
|
|
1080
1186
|
return { block: true, reason: formatLoopGuardBlockReason(loopCheck.reason) };
|
|
1081
1187
|
}
|
|
1082
1188
|
const deferredGateGuard = shouldBlockDeferredApprovalTool(toolName, event.input, discussionBasePath);
|
|
@@ -1299,6 +1405,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
1299
1405
|
// Let recordToolInvocationError classify the failure so non-gsd_ harness
|
|
1300
1406
|
// errors and deterministic policy rejections are handled consistently.
|
|
1301
1407
|
recordToolInvocationError(event.toolName, errorText);
|
|
1408
|
+
recordRetryableHarnessToolError(toolName, resultPayload, errorText);
|
|
1302
1409
|
}
|
|
1303
1410
|
else if (isAutoActive()) {
|
|
1304
1411
|
clearToolInvocationError();
|
|
@@ -1466,6 +1573,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
1466
1573
|
// for every finalized tool call on every engine, so error classification
|
|
1467
1574
|
// and evidence persistence here cover external engines that skip tool_result.
|
|
1468
1575
|
pi.on("tool_execution_end", async (event) => {
|
|
1576
|
+
const toolName = canonicalToolName(event.toolName);
|
|
1469
1577
|
markToolEnd(event.toolCallId);
|
|
1470
1578
|
// #2883/#4974: Capture deterministic invocation/policy errors
|
|
1471
1579
|
// so postUnitPreVerification can break the retry loop instead of re-dispatching.
|
|
@@ -1476,6 +1584,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
1476
1584
|
// Let recordToolInvocationError classify the failure so non-gsd_ harness
|
|
1477
1585
|
// errors and deterministic policy rejections are handled consistently.
|
|
1478
1586
|
recordToolInvocationError(event.toolName, errorText);
|
|
1587
|
+
recordRetryableHarnessToolError(toolName, event.result, errorText);
|
|
1479
1588
|
}
|
|
1480
1589
|
else if (isAutoActive()) {
|
|
1481
1590
|
clearToolInvocationError();
|
|
@@ -3,19 +3,63 @@
|
|
|
3
3
|
import { Type } from "@sinclair/typebox";
|
|
4
4
|
import { getAutoRuntimeSnapshot } from "../auto-runtime-state.js";
|
|
5
5
|
import { scheduleAutoWakeup } from "../auto/schedule-wakeup.js";
|
|
6
|
+
import { logWarning } from "../workflow-logger.js";
|
|
6
7
|
import { resolveCtxCwd } from "./dynamic-tools.js";
|
|
7
8
|
const MAX_WAKEUP_DELAY_SECONDS = 24 * 60 * 60;
|
|
9
|
+
const INTERACTIVE_WAKEUP_CUSTOM_TYPE = "gsd-schedule-wakeup";
|
|
10
|
+
// One pending interactive wakeup per session, keyed by base path — the same
|
|
11
|
+
// scoping auto-mode uses for its wakeup map (see `wakeupKey`). Re-arming cancels
|
|
12
|
+
// only that session's prior timer, so repeated polling never stacks overlapping
|
|
13
|
+
// wakeups, and concurrent projects in one host process don't cancel each other.
|
|
14
|
+
const pendingInteractiveWakeups = new Map();
|
|
15
|
+
function scheduleInteractiveWakeup(pi, key, delaySeconds, prompt, reason) {
|
|
16
|
+
const existing = pendingInteractiveWakeups.get(key);
|
|
17
|
+
if (existing)
|
|
18
|
+
clearTimeout(existing);
|
|
19
|
+
const handle = setTimeout(() => {
|
|
20
|
+
if (pendingInteractiveWakeups.get(key) === handle) {
|
|
21
|
+
pendingInteractiveWakeups.delete(key);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
void Promise.resolve(pi.sendMessage({
|
|
25
|
+
customType: INTERACTIVE_WAKEUP_CUSTOM_TYPE,
|
|
26
|
+
content: prompt,
|
|
27
|
+
display: true,
|
|
28
|
+
details: { delaySeconds, reason },
|
|
29
|
+
}, { triggerTurn: true })).catch((error) => {
|
|
30
|
+
logWarning("bootstrap", `ScheduleWakeup interactive dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
logWarning("bootstrap", `ScheduleWakeup interactive dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
35
|
+
}
|
|
36
|
+
}, delaySeconds * 1000);
|
|
37
|
+
pendingInteractiveWakeups.set(key, handle);
|
|
38
|
+
if (typeof handle === "object" &&
|
|
39
|
+
handle !== null &&
|
|
40
|
+
"unref" in handle &&
|
|
41
|
+
typeof handle.unref === "function") {
|
|
42
|
+
handle.unref();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function _resetInteractiveWakeupsForTest() {
|
|
46
|
+
for (const handle of pendingInteractiveWakeups.values()) {
|
|
47
|
+
clearTimeout(handle);
|
|
48
|
+
}
|
|
49
|
+
pendingInteractiveWakeups.clear();
|
|
50
|
+
}
|
|
8
51
|
export function registerScheduleWakeupTool(pi) {
|
|
9
52
|
pi.registerTool({
|
|
10
53
|
name: "ScheduleWakeup",
|
|
11
54
|
label: "Schedule Wakeup",
|
|
12
|
-
description: "In GSD auto-mode,
|
|
13
|
-
"
|
|
14
|
-
promptSnippet: "Schedule a
|
|
55
|
+
description: "Schedule a delayed continuation turn. In GSD auto-mode, continue the current unit in the same session; " +
|
|
56
|
+
"outside auto-mode, start a new triggered turn with the supplied wakeup prompt.",
|
|
57
|
+
promptSnippet: "Schedule a wakeup prompt after a delay.",
|
|
15
58
|
promptGuidelines: [
|
|
16
59
|
"Use ScheduleWakeup at the end of an execute-task turn when waiting for a long external process.",
|
|
17
60
|
"Include a prompt that says exactly what external state to check next and what artifact to write when done.",
|
|
18
61
|
"Re-arm ScheduleWakeup on each polling turn if the external process is still running.",
|
|
62
|
+
"Outside auto-mode, use ScheduleWakeup when the user asks you to check back or poll later.",
|
|
19
63
|
],
|
|
20
64
|
parameters: Type.Object({
|
|
21
65
|
delaySeconds: Type.Number({
|
|
@@ -34,22 +78,27 @@ export function registerScheduleWakeupTool(pi) {
|
|
|
34
78
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
35
79
|
const dash = getAutoRuntimeSnapshot();
|
|
36
80
|
const currentUnit = dash.currentUnit;
|
|
37
|
-
const
|
|
81
|
+
const delaySeconds = Math.max(1, Math.min(MAX_WAKEUP_DELAY_SECONDS, Math.floor(params.delaySeconds)));
|
|
82
|
+
const reason = params.reason ?? "";
|
|
38
83
|
if (!dash.active || !currentUnit) {
|
|
84
|
+
const interactiveKey = dash.basePath || resolveCtxCwd(ctx);
|
|
85
|
+
scheduleInteractiveWakeup(pi, interactiveKey, delaySeconds, params.prompt, reason);
|
|
39
86
|
return {
|
|
40
|
-
content: [{
|
|
41
|
-
|
|
42
|
-
|
|
87
|
+
content: [{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: `Wakeup scheduled for ${delaySeconds}s. GSD will start a new turn with the wakeup prompt.`,
|
|
90
|
+
}],
|
|
91
|
+
details: { operation: "schedule_wakeup", delaySeconds, mode: "interactive" },
|
|
43
92
|
};
|
|
44
93
|
}
|
|
45
|
-
const
|
|
94
|
+
const basePath = dash.basePath || resolveCtxCwd(ctx);
|
|
46
95
|
scheduleAutoWakeup({
|
|
47
96
|
basePath,
|
|
48
97
|
unitType: currentUnit.type,
|
|
49
98
|
unitId: currentUnit.id,
|
|
50
99
|
delayMs: delaySeconds * 1000,
|
|
51
100
|
prompt: params.prompt,
|
|
52
|
-
reason
|
|
101
|
+
reason,
|
|
53
102
|
createdAt: Date.now(),
|
|
54
103
|
});
|
|
55
104
|
return {
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { createHash } from "node:crypto";
|
|
26
26
|
import { INHERENTLY_REPEATABLE_TOOL_SET } from "./core-session-tools.js";
|
|
27
|
+
import { hasBrowserContractPrefix } from "../../shared/browser-contract.js";
|
|
28
|
+
import { canonicalToolName } from "../engine-hook-contract.js";
|
|
27
29
|
const MAX_CONSECUTIVE_IDENTICAL_CALLS = 4;
|
|
28
30
|
/** Interactive/user-facing tools where even 1 duplicate is confusing. */
|
|
29
31
|
const STRICT_LOOP_TOOLS = new Set(["ask_user_questions"]);
|
|
@@ -111,8 +113,14 @@ export function checkToolCallLoop(toolName, args) {
|
|
|
111
113
|
perToolCounts.set(toolName, perToolCount);
|
|
112
114
|
perToolLastMutationEpoch.set(toolName, mutationEpoch);
|
|
113
115
|
// Read-only navigation tools are normal context gathering; Guard 1 still
|
|
114
|
-
// catches true reread loops with identical arguments.
|
|
115
|
-
|
|
116
|
+
// catches true reread loops with identical arguments. Browser Automation
|
|
117
|
+
// Contract tools (browser_*) are the same shape: a browser-backed UAT makes
|
|
118
|
+
// many distinct-arg calls (read a message, count edits, inspect a card, …),
|
|
119
|
+
// so the arg-independent per-tool cap misfires on legitimate verification;
|
|
120
|
+
// Guard 1's identical-signature streak still catches a genuinely stuck
|
|
121
|
+
// browser loop.
|
|
122
|
+
if (PER_TOOL_CAP_EXEMPT_TOOLS.has(toolName) ||
|
|
123
|
+
hasBrowserContractPrefix(canonicalToolName(toolName))) {
|
|
116
124
|
return { block: false, count: consecutiveCount };
|
|
117
125
|
}
|
|
118
126
|
const perToolCap = INHERENTLY_REPEATABLE_TOOL_SET.has(toolName)
|
|
@@ -947,7 +947,7 @@ function allowedPlanningDispatchAgentsList() {
|
|
|
947
947
|
return [...ALLOWED_PLANNING_DISPATCH_AGENTS].join(", ");
|
|
948
948
|
}
|
|
949
949
|
function allowsControlledSubagentDispatch(policy) {
|
|
950
|
-
return ((policy.mode === "planning-dispatch" || policy.mode === "verification") &&
|
|
950
|
+
return ((policy.mode === "planning-dispatch" || policy.mode === "verification" || policy.mode === "workflow-only") &&
|
|
951
951
|
Array.isArray(policy.allowedSubagents));
|
|
952
952
|
}
|
|
953
953
|
function warnMissingControlledDispatchAgentClasses(unitType, mode, toolName) {
|
|
@@ -975,6 +975,7 @@ function warnMissingControlledDispatchAgentClasses(unitType, mode, toolName) {
|
|
|
975
975
|
const PLANNING_SAFE_TOOLS = new Set([
|
|
976
976
|
"read", "grep", "find", "ls", "glob",
|
|
977
977
|
"ask_user_questions",
|
|
978
|
+
"memory_query",
|
|
978
979
|
"search-the-web", "resolve_library", "get_library_docs", "fetch_page",
|
|
979
980
|
"search_and_read",
|
|
980
981
|
]);
|
|
@@ -1031,6 +1032,9 @@ function planningBlock(unitType, mode, what) {
|
|
|
1031
1032
|
* → allows Bash for project verification commands, keeps
|
|
1032
1033
|
* writes restricted to .gsd/, and permits subagent dispatch
|
|
1033
1034
|
* only when the manifest declares allowedSubagents.
|
|
1035
|
+
* - "workflow-only"
|
|
1036
|
+
* → allows reads, GSD workflow tools, and declared read-only
|
|
1037
|
+
* subagents; blocks generic Bash and direct file writes.
|
|
1034
1038
|
*
|
|
1035
1039
|
* `pathOrCommand` is the file path for write/edit-shaped tools and the
|
|
1036
1040
|
* shell command for bash. Other tools ignore this argument.
|
|
@@ -1101,6 +1105,15 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
|
|
|
1101
1105
|
}
|
|
1102
1106
|
return planningBlock(unitType, policy.mode, "subagent dispatch is not permitted in planning units");
|
|
1103
1107
|
}
|
|
1108
|
+
if (policy.mode === "workflow-only") {
|
|
1109
|
+
if (tool === "bash") {
|
|
1110
|
+
return planningBlock(unitType, policy.mode, `bash is not permitted; use the unit's dedicated GSD workflow tools for durable validation output`);
|
|
1111
|
+
}
|
|
1112
|
+
if (PLANNING_WRITE_TOOLS.has(tool)) {
|
|
1113
|
+
return planningBlock(unitType, policy.mode, `cannot ${tool} "${pathOrCommand}" — direct artifact writes are disabled; call the required GSD workflow tool instead`);
|
|
1114
|
+
}
|
|
1115
|
+
return planningBlock(unitType, policy.mode, `tool "${tool}" is not on the workflow-only allowlist`);
|
|
1116
|
+
}
|
|
1104
1117
|
if (tool === "bash") {
|
|
1105
1118
|
if (policy.mode === "verification") {
|
|
1106
1119
|
if (BASH_VERIFICATION_RE.test(pathOrCommand) || BASH_READ_ONLY_RE.test(pathOrCommand))
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
12
|
-
import { join, dirname, extname } from "node:path";
|
|
12
|
+
import { join, dirname, extname, relative, sep } from "node:path";
|
|
13
13
|
import { execSync } from "node:child_process";
|
|
14
14
|
import { gsdRoot } from "./paths.js";
|
|
15
|
+
import { createRepositoryRegistryFromPreferences, } from "./repository-registry.js";
|
|
16
|
+
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
15
17
|
// ─── Defaults ────────────────────────────────────────────────────────────────
|
|
16
18
|
const DEFAULT_EXCLUDES = [
|
|
17
19
|
// ── AI / tooling meta ──
|
|
@@ -144,6 +146,84 @@ function enumerateFiles(basePath, excludes, maxFiles) {
|
|
|
144
146
|
const truncated = filtered.length > maxFiles;
|
|
145
147
|
return { files: truncated ? filtered.slice(0, maxFiles) : filtered, truncated };
|
|
146
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Build a workspace-aware registry for parent mode, or null when the project is
|
|
151
|
+
* single-repo (no declared child repositories). Returns null so the caller falls
|
|
152
|
+
* back to the legacy single-root enumeration and produces byte-identical output.
|
|
153
|
+
*/
|
|
154
|
+
function loadWorkspaceRegistry(basePath) {
|
|
155
|
+
const preferences = loadEffectiveGSDPreferences(basePath)?.preferences;
|
|
156
|
+
const workspace = preferences?.workspace;
|
|
157
|
+
const mode = workspace?.mode ?? "project";
|
|
158
|
+
// Parent mode is only meaningful with at least one declared child repo; the
|
|
159
|
+
// implicit "project" repo is always present, so require a declared non-project id.
|
|
160
|
+
const hasDeclaredChildRepo = workspace?.repositories
|
|
161
|
+
? Object.keys(workspace.repositories).some((id) => id !== "project")
|
|
162
|
+
: false;
|
|
163
|
+
if (mode !== "parent" || !hasDeclaredChildRepo) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
return createRepositoryRegistryFromPreferences(basePath, preferences);
|
|
167
|
+
}
|
|
168
|
+
/** Stable key fragment for workspace-aware enumeration inputs (mode + declared repos). */
|
|
169
|
+
function workspaceEnumerationSignature(basePath) {
|
|
170
|
+
const registry = loadWorkspaceRegistry(basePath);
|
|
171
|
+
if (!registry)
|
|
172
|
+
return "single-root";
|
|
173
|
+
return JSON.stringify({
|
|
174
|
+
mode: registry.mode,
|
|
175
|
+
repositories: registry.repositories
|
|
176
|
+
.map((repo) => ({ id: repo.id, root: repo.root }))
|
|
177
|
+
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Enumerate tracked files across every declared repository in a parent workspace.
|
|
182
|
+
* Child-repo paths are rewritten to be workspace-relative (e.g. `frontend/src/x`)
|
|
183
|
+
* and tagged with their declaring repo id so the map can render repo-labelled
|
|
184
|
+
* sections. The maxFiles cap is applied across the union of all repositories.
|
|
185
|
+
*/
|
|
186
|
+
function enumerateWorkspaceFiles(registry, excludes, maxFiles) {
|
|
187
|
+
const perRepo = [];
|
|
188
|
+
const seen = new Set();
|
|
189
|
+
let totalEligible = 0;
|
|
190
|
+
for (const repo of registry.repositories) {
|
|
191
|
+
const rawFiles = lsFiles(repo.root);
|
|
192
|
+
const repoPrefix = relative(registry.projectRoot, repo.root).split(sep).join("/");
|
|
193
|
+
const repoEntries = [];
|
|
194
|
+
for (const file of rawFiles) {
|
|
195
|
+
// Rewrite before exclusion so workspace-relative patterns (e.g. frontend/…) apply.
|
|
196
|
+
const workspacePath = repoPrefix ? `${repoPrefix}/${file}` : file;
|
|
197
|
+
if (shouldExclude(workspacePath, excludes))
|
|
198
|
+
continue;
|
|
199
|
+
if (seen.has(workspacePath))
|
|
200
|
+
continue;
|
|
201
|
+
seen.add(workspacePath);
|
|
202
|
+
totalEligible++;
|
|
203
|
+
repoEntries.push({ workspacePath, repoId: repo.id });
|
|
204
|
+
}
|
|
205
|
+
perRepo.push(repoEntries);
|
|
206
|
+
}
|
|
207
|
+
const files = [];
|
|
208
|
+
const repos = [];
|
|
209
|
+
const indices = new Array(perRepo.length).fill(0);
|
|
210
|
+
while (files.length < maxFiles) {
|
|
211
|
+
let progressed = false;
|
|
212
|
+
for (let i = 0; i < perRepo.length; i++) {
|
|
213
|
+
if (indices[i] < perRepo[i].length) {
|
|
214
|
+
const entry = perRepo[i][indices[i]++];
|
|
215
|
+
files.push(entry.workspacePath);
|
|
216
|
+
repos.push(entry.repoId);
|
|
217
|
+
progressed = true;
|
|
218
|
+
if (files.length >= maxFiles)
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!progressed)
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
return { files, repos, truncated: totalEligible > files.length };
|
|
226
|
+
}
|
|
147
227
|
function resolveGeneratorOptions(options) {
|
|
148
228
|
const excludes = [...DEFAULT_EXCLUDES, ...(options?.excludePatterns ?? [])];
|
|
149
229
|
const maxFiles = options?.maxFiles ?? DEFAULT_MAX_FILES;
|
|
@@ -159,38 +239,69 @@ function resolveGeneratorOptions(options) {
|
|
|
159
239
|
}),
|
|
160
240
|
};
|
|
161
241
|
}
|
|
162
|
-
function computeCodebaseFingerprint(files, resolved, truncated) {
|
|
242
|
+
function computeCodebaseFingerprint(files, resolved, truncated, repos) {
|
|
163
243
|
return createHash("sha1")
|
|
164
244
|
.update(JSON.stringify({
|
|
165
245
|
files,
|
|
246
|
+
...(repos ? { repos } : {}),
|
|
166
247
|
truncated,
|
|
167
248
|
optionSignature: resolved.optionSignature,
|
|
168
249
|
}))
|
|
169
250
|
.digest("hex");
|
|
170
251
|
}
|
|
171
252
|
// ─── Grouping ────────────────────────────────────────────────────────────────
|
|
172
|
-
function groupByDirectory(files, descriptions, collapseThreshold) {
|
|
253
|
+
function groupByDirectory(files, descriptions, collapseThreshold, repos) {
|
|
173
254
|
const dirMap = new Map();
|
|
174
|
-
for (
|
|
255
|
+
for (let i = 0; i < files.length; i++) {
|
|
256
|
+
const file = files[i];
|
|
175
257
|
const dir = dirname(file);
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
258
|
+
const dirPath = dir === "." ? "" : dir;
|
|
259
|
+
const repoId = repos?.[i];
|
|
260
|
+
const groupKey = repoId !== undefined ? `${repoId}\0${dirPath}` : dirPath;
|
|
261
|
+
if (!dirMap.has(groupKey)) {
|
|
262
|
+
dirMap.set(groupKey, []);
|
|
179
263
|
}
|
|
180
|
-
dirMap.get(
|
|
264
|
+
dirMap.get(groupKey).push({
|
|
181
265
|
path: file,
|
|
182
266
|
description: descriptions.get(file) ?? "",
|
|
267
|
+
repo: repoId,
|
|
183
268
|
});
|
|
184
269
|
}
|
|
185
270
|
const groups = [];
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
271
|
+
const repoOrder = new Map();
|
|
272
|
+
if (repos) {
|
|
273
|
+
for (let i = 0; i < repos.length; i++) {
|
|
274
|
+
if (!repoOrder.has(repos[i])) {
|
|
275
|
+
repoOrder.set(repos[i], repoOrder.size);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const sortedDirs = [...dirMap.keys()].sort((a, b) => {
|
|
280
|
+
// Single-repo (project) mode must stay byte-identical to the pre-workspace
|
|
281
|
+
// output, which ordered directories with a bare `.sort()` (UTF-16 code-unit
|
|
282
|
+
// order, so uppercase sorts before lowercase). localeCompare would diverge
|
|
283
|
+
// for mixed-case names (e.g. "Zoo" before "apple" flips), so preserve
|
|
284
|
+
// code-unit ordering here; only the repo-partitioning key is added on top.
|
|
285
|
+
if (!repos)
|
|
286
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
287
|
+
const repoA = dirMap.get(a)[0]?.repo ?? "";
|
|
288
|
+
const repoB = dirMap.get(b)[0]?.repo ?? "";
|
|
289
|
+
const orderA = repoOrder.get(repoA) ?? 0;
|
|
290
|
+
const orderB = repoOrder.get(repoB) ?? 0;
|
|
291
|
+
if (orderA !== orderB)
|
|
292
|
+
return orderA - orderB;
|
|
293
|
+
return a.localeCompare(b);
|
|
294
|
+
});
|
|
295
|
+
for (const groupKey of sortedDirs) {
|
|
296
|
+
const dirFiles = dirMap.get(groupKey);
|
|
189
297
|
dirFiles.sort((a, b) => a.path.localeCompare(b.path));
|
|
298
|
+
const dirPath = repos ? groupKey.slice(groupKey.indexOf("\0") + 1) : groupKey;
|
|
190
299
|
groups.push({
|
|
191
|
-
path:
|
|
300
|
+
path: dirPath,
|
|
192
301
|
files: dirFiles,
|
|
193
302
|
collapsed: dirFiles.length > collapseThreshold,
|
|
303
|
+
// Each group is scoped to one repo in workspace-aware mode.
|
|
304
|
+
repo: dirFiles[0]?.repo,
|
|
194
305
|
});
|
|
195
306
|
}
|
|
196
307
|
return groups;
|
|
@@ -207,7 +318,18 @@ function renderCodebaseMap(groups, totalFiles, truncated, metadata) {
|
|
|
207
318
|
lines.push(`Note: Truncated to first ${totalFiles} files. Run with higher --max-files to include all.`);
|
|
208
319
|
}
|
|
209
320
|
lines.push("");
|
|
321
|
+
let emittedRepo;
|
|
210
322
|
for (const group of groups) {
|
|
323
|
+
// In a workspace-aware map, partition directories under their declaring
|
|
324
|
+
// repository. `## [repo-id]` headings are used (not `###`) so they never
|
|
325
|
+
// collide with the per-directory `### <dir>/` headings or the
|
|
326
|
+
// `parseCodebaseMap` list-item regexes. Groups are sorted by declaring
|
|
327
|
+
// repo (enumeration order) then directory path so each repo is contiguous.
|
|
328
|
+
if (group.repo && group.repo !== emittedRepo) {
|
|
329
|
+
emittedRepo = group.repo;
|
|
330
|
+
lines.push(`## [${group.repo}]`);
|
|
331
|
+
lines.push("");
|
|
332
|
+
}
|
|
211
333
|
const heading = group.path || "(root)";
|
|
212
334
|
lines.push(`### ${heading}/`);
|
|
213
335
|
if (group.collapsed) {
|
|
@@ -250,13 +372,15 @@ function renderCodebaseMap(groups, totalFiles, truncated, metadata) {
|
|
|
250
372
|
function buildCodebaseMap(basePath, resolved, existingDescriptions, enumerated) {
|
|
251
373
|
const listed = enumerated ?? enumerateFiles(basePath, resolved.excludes, resolved.maxFiles);
|
|
252
374
|
const descriptions = existingDescriptions ?? new Map();
|
|
253
|
-
const groups = groupByDirectory(listed.files, descriptions, resolved.collapseThreshold);
|
|
375
|
+
const groups = groupByDirectory(listed.files, descriptions, resolved.collapseThreshold, listed.repos);
|
|
254
376
|
const generatedAt = new Date().toISOString().split(".")[0] + "Z";
|
|
377
|
+
const repoIds = listed.repos ? Array.from(new Set(listed.repos)) : undefined;
|
|
255
378
|
const metadata = {
|
|
256
379
|
generatedAt,
|
|
257
|
-
fingerprint: computeCodebaseFingerprint(listed.files, resolved, listed.truncated),
|
|
380
|
+
fingerprint: computeCodebaseFingerprint(listed.files, resolved, listed.truncated, listed.repos),
|
|
258
381
|
fileCount: listed.files.length,
|
|
259
382
|
truncated: listed.truncated,
|
|
383
|
+
...(repoIds ? { repositories: repoIds } : {}),
|
|
260
384
|
};
|
|
261
385
|
const content = renderCodebaseMap(groups, listed.files.length, listed.truncated, metadata);
|
|
262
386
|
return {
|
|
@@ -273,9 +397,21 @@ function buildCodebaseMap(basePath, resolved, existingDescriptions, enumerated)
|
|
|
273
397
|
* Generate a fresh CODEBASE.md from scratch.
|
|
274
398
|
* Preserves existing descriptions if `existingDescriptions` is provided.
|
|
275
399
|
*/
|
|
400
|
+
/**
|
|
401
|
+
* Resolve workspace-aware enumeration for parent mode, or undefined to fall
|
|
402
|
+
* back to the legacy single-root enumeration. Centralized so all three public
|
|
403
|
+
* entry points (generate/update/ensure) share one discovery path.
|
|
404
|
+
*/
|
|
405
|
+
function resolveWorkspaceEnumeration(basePath, resolved) {
|
|
406
|
+
const registry = loadWorkspaceRegistry(basePath);
|
|
407
|
+
if (!registry)
|
|
408
|
+
return undefined;
|
|
409
|
+
return enumerateWorkspaceFiles(registry, resolved.excludes, resolved.maxFiles);
|
|
410
|
+
}
|
|
276
411
|
export function generateCodebaseMap(basePath, options, existingDescriptions) {
|
|
277
412
|
const resolved = resolveGeneratorOptions(options);
|
|
278
|
-
|
|
413
|
+
const enumerated = resolveWorkspaceEnumeration(basePath, resolved);
|
|
414
|
+
return buildCodebaseMap(basePath, resolved, existingDescriptions, enumerated);
|
|
279
415
|
}
|
|
280
416
|
/**
|
|
281
417
|
* Incremental update: re-scan files, preserve existing descriptions,
|
|
@@ -293,7 +429,8 @@ export function updateCodebaseMap(basePath, options) {
|
|
|
293
429
|
const existingFiles = new Set(existingDescriptions.keys());
|
|
294
430
|
// Generate new map preserving descriptions — reuse the returned file list
|
|
295
431
|
// to avoid a second enumeration (prevents race between content and stats).
|
|
296
|
-
const
|
|
432
|
+
const enumerated = resolveWorkspaceEnumeration(basePath, resolved);
|
|
433
|
+
const result = buildCodebaseMap(basePath, resolved, existingDescriptions, enumerated);
|
|
297
434
|
const currentSet = new Set(result.files);
|
|
298
435
|
// Count changes
|
|
299
436
|
let added = 0;
|
|
@@ -326,7 +463,7 @@ function clearFreshnessCache(basePath) {
|
|
|
326
463
|
}
|
|
327
464
|
export function ensureCodebaseMapFresh(basePath, options, ensureOptions) {
|
|
328
465
|
const resolved = resolveGeneratorOptions(options);
|
|
329
|
-
const cacheKey = `${basePath}::${resolved.optionSignature}`;
|
|
466
|
+
const cacheKey = `${basePath}::${resolved.optionSignature}::${workspaceEnumerationSignature(basePath)}`;
|
|
330
467
|
const ttlMs = ensureOptions?.ttlMs ?? DEFAULT_REFRESH_TTL_MS;
|
|
331
468
|
const force = ensureOptions?.force === true;
|
|
332
469
|
const now = Date.now();
|
|
@@ -338,8 +475,9 @@ export function ensureCodebaseMapFresh(basePath, options, ensureOptions) {
|
|
|
338
475
|
}
|
|
339
476
|
}
|
|
340
477
|
const existing = readCodebaseMap(basePath);
|
|
341
|
-
const listed =
|
|
342
|
-
|
|
478
|
+
const listed = resolveWorkspaceEnumeration(basePath, resolved)
|
|
479
|
+
?? enumerateFiles(basePath, resolved.excludes, resolved.maxFiles);
|
|
480
|
+
const fingerprint = computeCodebaseFingerprint(listed.files, resolved, listed.truncated, listed.repos);
|
|
343
481
|
const cacheAndReturn = (result) => {
|
|
344
482
|
freshnessCache.set(cacheKey, { checkedAt: now, result });
|
|
345
483
|
return result;
|