@opengsd/gsd-pi 1.4.0-dev.c18009cd → 1.5.0-dev.4fb2d460
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 +10 -10
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/ask-user-questions.js +1 -1
- package/dist/resources/extensions/gsd/auto/closeout.js +97 -5
- package/dist/resources/extensions/gsd/auto/orchestrator.js +4 -3
- package/dist/resources/extensions/gsd/auto/pre-dispatch.js +6 -3
- package/dist/resources/extensions/gsd/auto/unit-phase.js +5 -9
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +12 -3
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +11 -7
- package/dist/resources/extensions/gsd/auto-dispatch.js +16 -2
- package/dist/resources/extensions/gsd/auto-recovery.js +50 -6
- package/dist/resources/extensions/gsd/auto-start.js +4 -3
- package/dist/resources/extensions/gsd/auto-worktree-branch-lifecycle.js +189 -0
- package/dist/resources/extensions/gsd/auto-worktree-cleanup.js +133 -0
- package/dist/resources/extensions/gsd/auto-worktree-conflict-auto-resolve.js +35 -0
- package/dist/resources/extensions/gsd/auto-worktree-creation.js +130 -0
- package/dist/resources/extensions/gsd/auto-worktree-entry.js +118 -0
- package/dist/resources/extensions/gsd/auto-worktree-merge-already-merged.js +98 -0
- package/dist/resources/extensions/gsd/auto-worktree-merge-branch-head.js +52 -0
- package/dist/resources/extensions/gsd/auto-worktree-merge-cleanup.js +87 -0
- package/dist/resources/extensions/gsd/auto-worktree-merge-code-changes.js +46 -0
- package/dist/resources/extensions/gsd/auto-worktree-merge-db-ready.js +68 -0
- package/dist/resources/extensions/gsd/auto-worktree-merge-integration-branch.js +51 -0
- package/dist/resources/extensions/gsd/auto-worktree-merge-message.js +106 -0
- package/dist/resources/extensions/gsd/auto-worktree-merge-pre-teardown.js +64 -0
- package/dist/resources/extensions/gsd/auto-worktree-merge-stash.js +168 -0
- package/dist/resources/extensions/gsd/auto-worktree-merge.js +302 -0
- package/dist/resources/extensions/gsd/auto-worktree-milestone-shelter.js +119 -0
- package/dist/resources/extensions/gsd/auto-worktree-path-resolution.js +5 -0
- package/dist/resources/extensions/gsd/auto-worktree-path-utils.js +5 -0
- package/dist/resources/extensions/gsd/auto-worktree-resource-version.js +41 -0
- package/dist/resources/extensions/gsd/auto-worktree-runtime-cleanup.js +99 -0
- package/dist/resources/extensions/gsd/auto-worktree-session-registry.js +57 -0
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +249 -0
- package/dist/resources/extensions/gsd/auto-worktree-teardown.js +120 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +20 -2073
- package/dist/resources/extensions/gsd/auto.js +9 -3
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +20 -2
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +185 -27
- package/dist/resources/extensions/gsd/codebase-generator.js +7 -1
- package/dist/resources/extensions/gsd/commands-handlers.js +1 -1
- package/dist/resources/extensions/gsd/commands-workflow-templates.js +3 -3
- package/dist/resources/extensions/gsd/db/queries.js +9 -9
- package/dist/resources/extensions/gsd/db/writers/cascades.js +20 -2
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +93 -11
- package/dist/resources/extensions/gsd/flat-phase-migration.js +32 -3
- package/dist/resources/extensions/gsd/forensics.js +1 -1
- package/dist/resources/extensions/gsd/git-conflict-resolve.js +2 -2
- package/dist/resources/extensions/gsd/git-service.js +45 -0
- package/dist/resources/extensions/gsd/gsd-db.js +23 -11
- package/dist/resources/extensions/gsd/guided-flow.js +22 -17
- package/dist/resources/extensions/gsd/markdown-renderer.js +15 -3
- package/dist/resources/extensions/gsd/md-importer.js +89 -23
- package/dist/resources/extensions/gsd/migration-auto-check.js +30 -3
- package/dist/resources/extensions/gsd/milestone-merge-transaction.js +11 -0
- package/dist/resources/extensions/gsd/milestone-settlement.js +1 -1
- package/dist/resources/extensions/gsd/parallel-merge.js +1 -1
- package/dist/resources/extensions/gsd/parallel-orchestrator.js +3 -1
- package/dist/resources/extensions/gsd/paths.js +17 -3
- package/dist/resources/extensions/gsd/preferences-skills.js +10 -16
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +82 -0
- package/dist/resources/extensions/gsd/preferences.js +20 -0
- package/dist/resources/extensions/gsd/prompts/system.md +6 -6
- package/dist/resources/extensions/gsd/quick.js +12 -3
- package/dist/resources/extensions/gsd/skill-discovery.js +36 -23
- package/dist/resources/extensions/gsd/slice-parallel-orchestrator.js +2 -1
- package/dist/resources/extensions/gsd/state/derive/from-db.js +88 -153
- package/dist/resources/extensions/gsd/tool-contract.js +14 -0
- package/dist/resources/extensions/gsd/tools/reopen-slice.js +5 -0
- package/dist/resources/extensions/gsd/tools/reopen-task.js +13 -5
- package/dist/resources/extensions/gsd/unit-registry.js +12 -2
- package/dist/resources/extensions/gsd/unmerged-milestone-guard.js +1 -1
- package/dist/resources/extensions/gsd/validation-block-guard.js +2 -1
- package/dist/resources/extensions/gsd/workspace-git-preflight.js +2 -2
- package/dist/resources/extensions/gsd/worktree-command.js +1 -1
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +14 -9
- package/dist/resources/extensions/gsd/worktree-reentry.js +2 -1
- package/dist/resources/extensions/shared/interview-ui.js +62 -9
- 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 +8 -8
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +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/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 +8 -8
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +24 -35
- package/integrations/hermes/tests/test_gsd_client_timeout.py +4 -4
- package/package.json +6 -6
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/dist/export-html/template.js +53 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +3 -2
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +14 -9
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/index.d.ts +1 -0
- package/packages/pi-ai/dist/index.d.ts.map +1 -1
- package/packages/pi-ai/dist/index.js +1 -0
- package/packages/pi-ai/dist/index.js.map +1 -1
- package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/transform-messages.js +7 -3
- package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
- package/packages/pi-ai/dist/tool-result-content.d.ts +3 -0
- package/packages/pi-ai/dist/tool-result-content.d.ts.map +1 -0
- package/packages/pi-ai/dist/tool-result-content.js +62 -0
- package/packages/pi-ai/dist/tool-result-content.js.map +1 -0
- package/packages/pi-ai/dist/types.d.ts +8 -20
- 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/package-manager.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +74 -53
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.js +30 -2
- package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/skill-directories.d.ts +66 -0
- package/packages/pi-coding-agent/dist/core/skill-directories.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/skill-directories.js +138 -0
- package/packages/pi-coding-agent/dist/core/skill-directories.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/skills.d.ts +4 -1
- package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.js +4 -1
- package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -0
- package/packages/pi-coding-agent/dist/index.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/package.json +2 -2
- package/pkg/dist/core/export-html/template.js +53 -4
- package/pkg/package.json +1 -1
- package/src/resources/extensions/ask-user-questions.ts +1 -1
- package/src/resources/extensions/gsd/auto/closeout.ts +114 -7
- package/src/resources/extensions/gsd/auto/orchestrator.ts +4 -3
- package/src/resources/extensions/gsd/auto/pre-dispatch.ts +6 -3
- package/src/resources/extensions/gsd/auto/unit-phase.ts +5 -9
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +12 -3
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +13 -7
- package/src/resources/extensions/gsd/auto-dispatch.ts +18 -2
- package/src/resources/extensions/gsd/auto-recovery.ts +50 -6
- package/src/resources/extensions/gsd/auto-start.ts +5 -3
- package/src/resources/extensions/gsd/auto-worktree-branch-lifecycle.ts +237 -0
- package/src/resources/extensions/gsd/auto-worktree-cleanup.ts +166 -0
- package/src/resources/extensions/gsd/auto-worktree-conflict-auto-resolve.ts +37 -0
- package/src/resources/extensions/gsd/auto-worktree-creation.ts +171 -0
- package/src/resources/extensions/gsd/auto-worktree-entry.ts +154 -0
- package/src/resources/extensions/gsd/auto-worktree-merge-already-merged.ts +161 -0
- package/src/resources/extensions/gsd/auto-worktree-merge-branch-head.ts +83 -0
- package/src/resources/extensions/gsd/auto-worktree-merge-cleanup.ts +132 -0
- package/src/resources/extensions/gsd/auto-worktree-merge-code-changes.ts +71 -0
- package/src/resources/extensions/gsd/auto-worktree-merge-db-ready.ts +112 -0
- package/src/resources/extensions/gsd/auto-worktree-merge-integration-branch.ts +99 -0
- package/src/resources/extensions/gsd/auto-worktree-merge-message.ts +166 -0
- package/src/resources/extensions/gsd/auto-worktree-merge-pre-teardown.ts +93 -0
- package/src/resources/extensions/gsd/auto-worktree-merge-stash.ts +213 -0
- package/src/resources/extensions/gsd/auto-worktree-merge.ts +391 -0
- package/src/resources/extensions/gsd/auto-worktree-milestone-shelter.ts +159 -0
- package/src/resources/extensions/gsd/auto-worktree-path-resolution.ts +6 -0
- package/src/resources/extensions/gsd/auto-worktree-path-utils.ts +10 -0
- package/src/resources/extensions/gsd/auto-worktree-resource-version.ts +44 -0
- package/src/resources/extensions/gsd/auto-worktree-runtime-cleanup.ts +119 -0
- package/src/resources/extensions/gsd/auto-worktree-session-registry.ts +71 -0
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +370 -0
- package/src/resources/extensions/gsd/auto-worktree-teardown.ts +161 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +69 -2429
- package/src/resources/extensions/gsd/auto.ts +11 -17
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +20 -2
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +218 -27
- package/src/resources/extensions/gsd/codebase-generator.ts +7 -1
- package/src/resources/extensions/gsd/commands-handlers.ts +1 -1
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +3 -3
- package/src/resources/extensions/gsd/db/queries.ts +9 -9
- package/src/resources/extensions/gsd/db/writers/cascades.ts +23 -1
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +109 -10
- package/src/resources/extensions/gsd/flat-phase-migration.ts +34 -3
- package/src/resources/extensions/gsd/forensics.ts +1 -1
- package/src/resources/extensions/gsd/git-conflict-resolve.ts +2 -2
- package/src/resources/extensions/gsd/git-service.ts +53 -0
- package/src/resources/extensions/gsd/gsd-db.ts +33 -11
- package/src/resources/extensions/gsd/guided-flow.ts +26 -18
- package/src/resources/extensions/gsd/markdown-renderer.ts +15 -3
- package/src/resources/extensions/gsd/md-importer.ts +93 -22
- package/src/resources/extensions/gsd/migration-auto-check.ts +32 -3
- package/src/resources/extensions/gsd/milestone-merge-transaction.ts +15 -0
- package/src/resources/extensions/gsd/milestone-settlement.ts +1 -1
- package/src/resources/extensions/gsd/parallel-merge.ts +1 -1
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +3 -1
- package/src/resources/extensions/gsd/paths.ts +21 -3
- package/src/resources/extensions/gsd/preferences-skills.ts +10 -16
- package/src/resources/extensions/gsd/preferences-types.ts +30 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +69 -0
- package/src/resources/extensions/gsd/preferences.ts +20 -0
- package/src/resources/extensions/gsd/prompts/system.md +6 -6
- package/src/resources/extensions/gsd/quick.ts +11 -3
- package/src/resources/extensions/gsd/skill-discovery.ts +47 -23
- package/src/resources/extensions/gsd/slice-parallel-orchestrator.ts +3 -1
- package/src/resources/extensions/gsd/state/derive/from-db.ts +201 -157
- package/src/resources/extensions/gsd/tests/auto-artifact-paths.test.ts +169 -1
- package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +57 -0
- package/src/resources/extensions/gsd/tests/auto-start-clean-runtime-db-gated.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/auto-worktree-auto-resolve.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-worktree-merge-already-merged.test.ts +177 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-merge-branch-head.test.ts +118 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-merge-cleanup.test.ts +115 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-merge-code-changes.test.ts +76 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-merge-db-ready.test.ts +140 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-merge-integration-branch.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-merge-message.test.ts +114 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-merge-pre-teardown.test.ts +84 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-merge-stash.test.ts +70 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-shelter.test.ts +52 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/auto-worktree-resource-version.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-untracked-content.test.ts +3 -1
- package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +174 -0
- package/src/resources/extensions/gsd/tests/fast-forward-reused-milestone-branch.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +71 -1
- package/src/resources/extensions/gsd/tests/flat-phase-renderer.test.ts +25 -1
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/import-completion-lost-update.test.ts +196 -0
- package/src/resources/extensions/gsd/tests/integration/all-milestones-complete-merge.test.ts +4 -6
- package/src/resources/extensions/gsd/tests/integration/auto-stash-merge.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +7 -8
- package/src/resources/extensions/gsd/tests/integration/feature-branch-lifecycle-integration.test.ts +2 -5
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +76 -0
- package/src/resources/extensions/gsd/tests/integration/merge-cwd-restore.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/integration/merge-preserve-worktree-on-preteardown-error.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/integration/merge-strategy-regular.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/integration/milestone-transition-worktree.test.ts +5 -7
- package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +2 -4
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +16 -15
- package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/merge-self-branch-guard.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +91 -0
- package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +439 -0
- package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +24 -0
- package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/parallel-worker-lock-contention.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +1 -4
- package/src/resources/extensions/gsd/tests/preferences.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +5 -0
- package/src/resources/extensions/gsd/tests/reopen-slice.test.ts +52 -0
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/skill-discovery.test.ts +46 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +3 -5
- package/src/resources/extensions/gsd/tests/stash-pop-gsd-conflict.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/system-flat-phase-layout.test.ts +33 -0
- package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +196 -0
- package/src/resources/extensions/gsd/tests/uat-stuck-loop-orphaned-worktree.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/unit-registry.test.ts +26 -1
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-db-respawn-truncation.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-db-same-file.test.ts +1 -3
- package/src/resources/extensions/gsd/tests/worktree-expected-warnings.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-git-pathspec.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-main-branch.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-preferences-sync.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +5 -2
- package/src/resources/extensions/gsd/tests/worktree-sync-overwrite-loop.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-sync-tasks.test.ts +1 -1
- package/src/resources/extensions/gsd/tool-contract.ts +29 -0
- package/src/resources/extensions/gsd/tools/reopen-slice.ts +5 -0
- package/src/resources/extensions/gsd/tools/reopen-task.ts +13 -5
- package/src/resources/extensions/gsd/unit-registry.ts +16 -2
- package/src/resources/extensions/gsd/unmerged-milestone-guard.ts +1 -1
- package/src/resources/extensions/gsd/validation-block-guard.ts +2 -1
- package/src/resources/extensions/gsd/workspace-git-preflight.ts +2 -2
- package/src/resources/extensions/gsd/worktree-command.ts +1 -1
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +19 -15
- package/src/resources/extensions/gsd/worktree-reentry.ts +2 -1
- package/src/resources/extensions/shared/interview-ui.ts +58 -11
- package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +32 -0
- /package/dist/web/standalone/.next/static/{L2UMqXELiDH3oz-7yguXp → gXoWec4UNqcR0tkuHwYCr}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{L2UMqXELiDH3oz-7yguXp → gXoWec4UNqcR0tkuHwYCr}/_ssgManifest.js +0 -0
|
@@ -177,24 +177,18 @@ import {
|
|
|
177
177
|
import { GitServiceImpl } from "./git-service.js";
|
|
178
178
|
import { nativeCheckoutBranch } from "./native-git-bridge.js";
|
|
179
179
|
import { getPriorSliceCompletionBlocker } from "./dispatch-guard.js";
|
|
180
|
-
import {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
autoWorktreeBranch,
|
|
190
|
-
syncWorktreeStateBack,
|
|
191
|
-
readResourceVersion,
|
|
192
|
-
checkResourcesStale,
|
|
193
|
-
escapeStaleWorktree,
|
|
194
|
-
} from "./auto-worktree.js";
|
|
180
|
+
import { autoWorktreeBranch, enterBranchModeForMilestone } from "./auto-worktree-branch-lifecycle.js";
|
|
181
|
+
import { createAutoWorktree } from "./auto-worktree-creation.js";
|
|
182
|
+
import { enterAutoWorktree, isInAutoWorktree } from "./auto-worktree-entry.js";
|
|
183
|
+
import { getAutoWorktreePath } from "./auto-worktree-path-resolution.js";
|
|
184
|
+
import { checkResourcesStale, readResourceVersion } from "./auto-worktree-resource-version.js";
|
|
185
|
+
import { escapeStaleWorktree } from "./auto-worktree-runtime-cleanup.js";
|
|
186
|
+
import { getAutoWorktreeOriginalBase } from "./auto-worktree-session-registry.js";
|
|
187
|
+
import { syncWorktreeStateBack } from "./auto-worktree-sync.js";
|
|
188
|
+
import { teardownAutoWorktree } from "./auto-worktree-teardown.js";
|
|
195
189
|
import { pruneQueueOrder } from "./queue-order.js";
|
|
196
190
|
import { startCommandPolling as _startCommandPolling, isRemoteConfigured } from "../remote-questions/manager.js";
|
|
197
|
-
import {
|
|
191
|
+
import { createDefaultMilestoneMergeTransaction } from "./milestone-merge-transaction.js";
|
|
198
192
|
|
|
199
193
|
import { debugLog, isDebugEnabled, writeDebugSummary } from "./debug-logger.js";
|
|
200
194
|
import {
|
|
@@ -2326,7 +2320,7 @@ export function buildWorktreeLifecycleDeps(): WorktreeLifecycleDeps {
|
|
|
2326
2320
|
return new GitServiceImpl(basePath, gitConfig);
|
|
2327
2321
|
},
|
|
2328
2322
|
worktreeProjection: new WorktreeStateProjection(),
|
|
2329
|
-
mergeMilestone:
|
|
2323
|
+
mergeMilestone: createDefaultMilestoneMergeTransaction(),
|
|
2330
2324
|
};
|
|
2331
2325
|
}
|
|
2332
2326
|
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
type WorkflowDatabaseOpenResult,
|
|
15
15
|
type WorkflowDatabaseStatus,
|
|
16
16
|
} from "../db-workspace.js";
|
|
17
|
-
import { getAutoWorktreePath } from "../auto-worktree.js";
|
|
17
|
+
import { getAutoWorktreePath } from "../auto-worktree-path-resolution.js";
|
|
18
18
|
import { resolveWorktreeProjectRoot } from "../worktree-root.js";
|
|
19
19
|
import { worktreesDirs } from "../worktree-placement.js";
|
|
20
20
|
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
} from "../auto-tool-tracking.js";
|
|
41
41
|
import { applyProviderPayloadPolicy } from "../provider-payload-policy.js";
|
|
42
42
|
|
|
43
|
-
import { checkToolCallLoop, recordToolCallLoopMutation, resetToolCallLoopGuard } from "./tool-call-loop-guard.js";
|
|
43
|
+
import { checkToolCallLoop, configureToolCallLoopGuard, recordToolCallLoopMutation, resetToolCallLoopGuard } from "./tool-call-loop-guard.js";
|
|
44
44
|
import { MINIMAL_AUTO_BASE_TOOL_NAMES } from "./core-session-tools.js";
|
|
45
45
|
import { maybePauseAutoForApprovalGate, resetPendingGatePauseGuard } from "./pending-gate-pause.js";
|
|
46
46
|
import { saveActivityLog } from "../activity-log.js";
|
|
@@ -594,6 +594,22 @@ async function applyCompactionThresholdOverride(ctx: ExtensionContext): Promise<
|
|
|
594
594
|
}
|
|
595
595
|
}
|
|
596
596
|
|
|
597
|
+
/**
|
|
598
|
+
* Apply user-tunable tool-call loop guard thresholds (#1198) from GSD
|
|
599
|
+
* preferences plus GSD_TOOL_LOOP_* env overrides. Runs at session boundaries
|
|
600
|
+
* so both interactive sessions and `/gsd auto` pick up the configuration.
|
|
601
|
+
* Non-fatal: falls back to built-in defaults when preferences cannot be loaded.
|
|
602
|
+
*/
|
|
603
|
+
async function applyToolCallLoopGuardConfig(basePath: string): Promise<void> {
|
|
604
|
+
try {
|
|
605
|
+
const { loadEffectiveGSDPreferences } = await import("../preferences.js");
|
|
606
|
+
const prefs = loadEffectiveGSDPreferences(basePath);
|
|
607
|
+
configureToolCallLoopGuard(prefs?.preferences.tool_call_loop_guard);
|
|
608
|
+
} catch {
|
|
609
|
+
configureToolCallLoopGuard(null);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
597
613
|
function clearDeferredApprovalGate(basePath?: string): void {
|
|
598
614
|
if (!basePath) {
|
|
599
615
|
deferredApprovalGates.clear();
|
|
@@ -996,6 +1012,7 @@ export function registerHooks(
|
|
|
996
1012
|
}
|
|
997
1013
|
resetWriteGateState(basePath);
|
|
998
1014
|
resetToolCallLoopGuard();
|
|
1015
|
+
await applyToolCallLoopGuardConfig(basePath);
|
|
999
1016
|
approvalQuestionAbortInFlight = false;
|
|
1000
1017
|
clearDeferredApprovalGate();
|
|
1001
1018
|
await resetAskUserQuestionsTurnCache();
|
|
@@ -1087,6 +1104,7 @@ export function registerHooks(
|
|
|
1087
1104
|
initSessionNotifications(ctx);
|
|
1088
1105
|
resetWriteGateState(basePath);
|
|
1089
1106
|
resetToolCallLoopGuard();
|
|
1107
|
+
await applyToolCallLoopGuardConfig(basePath);
|
|
1090
1108
|
clearDeferredApprovalGate();
|
|
1091
1109
|
await resetAskUserQuestionsTurnCache();
|
|
1092
1110
|
clearDiscussionFlowState(basePath);
|
|
@@ -1707,7 +1725,7 @@ export function registerHooks(
|
|
|
1707
1725
|
}
|
|
1708
1726
|
}
|
|
1709
1727
|
if (!event.isError) {
|
|
1710
|
-
recordToolCallLoopMutation(toolName);
|
|
1728
|
+
recordToolCallLoopMutation(toolName, event.details);
|
|
1711
1729
|
refreshSourceObservationAfterMutation(toolName, event.input, ctx);
|
|
1712
1730
|
clearSourceObservationsAfterShell(toolName);
|
|
1713
1731
|
}
|
|
@@ -14,7 +14,7 @@ import { resolveModelWithFallbacksForUnit } from "../preferences-models.js";
|
|
|
14
14
|
import { resolveGsdRootFile, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTaskFiles, resolveTasksDir, relSliceFile, relSlicePath, relTaskFile } from "../paths.js";
|
|
15
15
|
import { extractIntroAndRules } from "../knowledge-parser.js";
|
|
16
16
|
import { ensureCodebaseMapFresh, readCodebaseMap } from "../codebase-generator.js";
|
|
17
|
-
import { getActiveAutoWorktreeContext } from "../auto-worktree.js";
|
|
17
|
+
import { getActiveAutoWorktreeContext } from "../auto-worktree-session-registry.js";
|
|
18
18
|
import { getActiveWorktreeName, getWorktreeOriginalCwd } from "../worktree-session-state.js";
|
|
19
19
|
import { deriveState } from "../state.js";
|
|
20
20
|
import { formatOverridesSection, formatShortcut, loadActiveOverrides, loadFile, parseContinue, parseSummary } from "../files.js";
|
|
@@ -14,13 +14,21 @@
|
|
|
14
14
|
* to stop tooling for the rest of that turn and answer in text.
|
|
15
15
|
*
|
|
16
16
|
* The per-tool-name check (#783 Brief C) tracks call counts within a
|
|
17
|
-
* turn regardless of args, decaying after successful state-
|
|
17
|
+
* turn regardless of args, decaying after successful state-progressing tools.
|
|
18
18
|
* This catches improvisation
|
|
19
19
|
* loops where the model attempts the same missing workflow tool through
|
|
20
20
|
* varied surfaces (bash → `node -e` → CLI), each with a different
|
|
21
21
|
* signature, so the identical-args streak never trips, while allowing
|
|
22
|
-
* tool-heavy turns that are making
|
|
23
|
-
*
|
|
22
|
+
* tool-heavy turns that are making progress. Progress means a successful file
|
|
23
|
+
* mutation (edit/write) or a successful shell/exec call (bg_shell,
|
|
24
|
+
* gsd_uat_exec, …) — so a local model running a legitimate iterative
|
|
25
|
+
* debugging loop (restart server, npm install, curl an endpoint) is not
|
|
26
|
+
* blocked as a false loop (#1206). Whichever guard trips first blocks.
|
|
27
|
+
*
|
|
28
|
+
* Thresholds, exempt tools, and enable flags are user-tunable (#1198) via the
|
|
29
|
+
* `tool_call_loop_guard` key in `.gsd/PREFERENCES.md` and `GSD_TOOL_LOOP_*`
|
|
30
|
+
* environment variables, applied through {@link configureToolCallLoopGuard}.
|
|
31
|
+
* Defaults preserve the original hardcoded behavior.
|
|
24
32
|
*/
|
|
25
33
|
|
|
26
34
|
import { createHash } from "node:crypto";
|
|
@@ -28,26 +36,189 @@ import { INHERENTLY_REPEATABLE_TOOL_SET } from "./core-session-tools.js";
|
|
|
28
36
|
import { hasBrowserContractPrefix } from "../../shared/browser-contract.js";
|
|
29
37
|
import { canonicalToolName } from "../engine-hook-contract.js";
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
/** Built-in defaults. Preserved when preferences/env do not override them. */
|
|
40
|
+
const DEFAULT_MAX_CONSECUTIVE_IDENTICAL_CALLS = 4;
|
|
41
|
+
const DEFAULT_PER_TOOL_DEFAULT_CAP = 6;
|
|
42
|
+
const DEFAULT_PER_TOOL_REPEATABLE_CAP = 15;
|
|
43
|
+
const DEFAULT_PER_TOOL_CAP_EXEMPT_TOOLS = ["find", "glob", "grep", "ls", "read", "search_and_read"] as const;
|
|
32
44
|
|
|
33
45
|
/** Interactive/user-facing tools where even 1 duplicate is confusing. */
|
|
34
46
|
const STRICT_LOOP_TOOLS = new Set(["ask_user_questions"]);
|
|
35
47
|
const MAX_CONSECUTIVE_STRICT = 1;
|
|
36
48
|
|
|
49
|
+
const STATE_MUTATING_TOOL_SET = new Set(["edit", "write", "multi_edit", "notebook_edit"]);
|
|
50
|
+
|
|
37
51
|
/**
|
|
38
|
-
*
|
|
52
|
+
* Successful shell/exec calls are state progression too (#1206), not just file
|
|
53
|
+
* mutations. A local model debugging a UAT scenario legitimately restarts
|
|
54
|
+
* servers, runs `npm install`, and curls endpoints via bg_shell / gsd_uat_exec
|
|
55
|
+
* with varied args — productive work that never touches a file, so Guard 2's
|
|
56
|
+
* per-tool cap would otherwise misfire as a false loop. Decaying on these
|
|
57
|
+
* successful calls keeps the guard from aborting a progressing turn.
|
|
39
58
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
59
|
+
* Only successful calls reach {@link recordToolCallLoopMutation}: the
|
|
60
|
+
* tool_result hook skips `isError` results, and the bash tool throws on
|
|
61
|
+
* non-zero exit, so a genuinely stuck improvisation loop of failing commands
|
|
62
|
+
* (#783) never decays and still trips the cap. `async_bash` is excluded
|
|
63
|
+
* because it only registers a background job; command success is reported
|
|
64
|
+
* later via `await_job`. `bg_shell` may return `isError: false` for failed
|
|
65
|
+
* `run`/`start` outcomes, so the hook passes `details` for validation.
|
|
46
66
|
*/
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
67
|
+
const STATE_PROGRESSING_EXEC_TOOL_SET = new Set([
|
|
68
|
+
"bash",
|
|
69
|
+
"bg_shell",
|
|
70
|
+
"shell",
|
|
71
|
+
"powershell",
|
|
72
|
+
"gsd_exec",
|
|
73
|
+
"gsd_uat_exec",
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
function isSuccessfulBgShellLoopProgress(details: unknown): boolean {
|
|
77
|
+
if (!details || typeof details !== "object") return false;
|
|
78
|
+
const record = details as Record<string, unknown>;
|
|
79
|
+
switch (record.action) {
|
|
80
|
+
case "run":
|
|
81
|
+
return record.exitCode === 0 && record.timedOut !== true;
|
|
82
|
+
case "start":
|
|
83
|
+
case "restart": {
|
|
84
|
+
const process = record.process;
|
|
85
|
+
if (!process || typeof process !== "object") return false;
|
|
86
|
+
return (process as Record<string, unknown>).alive === true;
|
|
87
|
+
}
|
|
88
|
+
case "wait_for_ready":
|
|
89
|
+
return record.ready === true;
|
|
90
|
+
case "send_and_wait":
|
|
91
|
+
return record.matched === true;
|
|
92
|
+
default:
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* User-tunable configuration shape for the loop guard (#1198).
|
|
99
|
+
*
|
|
100
|
+
* Mirrors the `tool_call_loop_guard` key in `.gsd/PREFERENCES.md`. All fields
|
|
101
|
+
* are optional; anything omitted falls back to the built-in defaults so that
|
|
102
|
+
* existing installs keep their current behavior.
|
|
103
|
+
*/
|
|
104
|
+
export interface ToolCallLoopGuardConfig {
|
|
105
|
+
enabled?: boolean;
|
|
106
|
+
identical_args?: {
|
|
107
|
+
enabled?: boolean;
|
|
108
|
+
max_consecutive_calls?: number;
|
|
109
|
+
};
|
|
110
|
+
repeated_tool?: {
|
|
111
|
+
enabled?: boolean;
|
|
112
|
+
default_cap?: number;
|
|
113
|
+
repeatable_cap?: number;
|
|
114
|
+
exempt_tools?: string[];
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Active, resolved configuration. Set by {@link configureToolCallLoopGuard}
|
|
120
|
+
* from preferences + environment overrides, and deliberately NOT touched by
|
|
121
|
+
* {@link resetToolCallLoopGuard} so a session's tuning survives turn
|
|
122
|
+
* boundaries.
|
|
123
|
+
*/
|
|
124
|
+
interface ResolvedGuardConfig {
|
|
125
|
+
/** Master switch for both guards. */
|
|
126
|
+
guardEnabled: boolean;
|
|
127
|
+
identicalEnabled: boolean;
|
|
128
|
+
maxConsecutiveIdentical: number;
|
|
129
|
+
repeatedEnabled: boolean;
|
|
130
|
+
perToolDefaultCap: number;
|
|
131
|
+
perToolRepeatableCap: number;
|
|
132
|
+
/** Tool names exempt from Guard 2 (per-tool-name cap). */
|
|
133
|
+
perToolExempt: Set<string>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function defaultGuardConfig(): ResolvedGuardConfig {
|
|
137
|
+
return {
|
|
138
|
+
guardEnabled: true,
|
|
139
|
+
identicalEnabled: true,
|
|
140
|
+
maxConsecutiveIdentical: DEFAULT_MAX_CONSECUTIVE_IDENTICAL_CALLS,
|
|
141
|
+
repeatedEnabled: true,
|
|
142
|
+
perToolDefaultCap: DEFAULT_PER_TOOL_DEFAULT_CAP,
|
|
143
|
+
perToolRepeatableCap: DEFAULT_PER_TOOL_REPEATABLE_CAP,
|
|
144
|
+
perToolExempt: new Set<string>(DEFAULT_PER_TOOL_CAP_EXEMPT_TOOLS),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let config: ResolvedGuardConfig = defaultGuardConfig();
|
|
149
|
+
|
|
150
|
+
/** Parse a positive-integer env var, returning undefined when unset/invalid. */
|
|
151
|
+
function envPositiveInt(name: string): number | undefined {
|
|
152
|
+
const raw = process.env[name];
|
|
153
|
+
if (raw === undefined || raw.trim() === "") return undefined;
|
|
154
|
+
const n = Number(raw);
|
|
155
|
+
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Parse a boolean env var (`true`/`1` vs `false`/`0`), undefined when unset. */
|
|
159
|
+
function envBool(name: string): boolean | undefined {
|
|
160
|
+
const raw = process.env[name];
|
|
161
|
+
if (raw === undefined || raw.trim() === "") return undefined;
|
|
162
|
+
const v = raw.trim().toLowerCase();
|
|
163
|
+
if (v === "true" || v === "1" || v === "yes" || v === "on") return true;
|
|
164
|
+
if (v === "false" || v === "0" || v === "no" || v === "off") return false;
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Parse a comma-separated tool-name list env var. */
|
|
169
|
+
function envToolList(name: string): string[] {
|
|
170
|
+
const raw = process.env[name];
|
|
171
|
+
if (!raw) return [];
|
|
172
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Resolve preferences + environment overrides into the active configuration.
|
|
177
|
+
* Environment variables win over preferences; preferences win over built-in
|
|
178
|
+
* defaults. User-supplied exempt tools are additive to the built-in exempt
|
|
179
|
+
* set so defaults are always preserved.
|
|
180
|
+
*
|
|
181
|
+
* Applied to both interactive sessions and `/gsd auto` (called from the
|
|
182
|
+
* session_start / session_switch hooks).
|
|
183
|
+
*/
|
|
184
|
+
export function configureToolCallLoopGuard(prefs?: ToolCallLoopGuardConfig | null): void {
|
|
185
|
+
const next = defaultGuardConfig();
|
|
186
|
+
|
|
187
|
+
if (prefs) {
|
|
188
|
+
if (typeof prefs.enabled === "boolean") next.guardEnabled = prefs.enabled;
|
|
189
|
+
if (prefs.identical_args) {
|
|
190
|
+
if (typeof prefs.identical_args.enabled === "boolean") next.identicalEnabled = prefs.identical_args.enabled;
|
|
191
|
+
if (typeof prefs.identical_args.max_consecutive_calls === "number" && prefs.identical_args.max_consecutive_calls >= 1) {
|
|
192
|
+
next.maxConsecutiveIdentical = Math.floor(prefs.identical_args.max_consecutive_calls);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (prefs.repeated_tool) {
|
|
196
|
+
if (typeof prefs.repeated_tool.enabled === "boolean") next.repeatedEnabled = prefs.repeated_tool.enabled;
|
|
197
|
+
if (typeof prefs.repeated_tool.default_cap === "number" && prefs.repeated_tool.default_cap >= 1) {
|
|
198
|
+
next.perToolDefaultCap = Math.floor(prefs.repeated_tool.default_cap);
|
|
199
|
+
}
|
|
200
|
+
if (typeof prefs.repeated_tool.repeatable_cap === "number" && prefs.repeated_tool.repeatable_cap >= 1) {
|
|
201
|
+
next.perToolRepeatableCap = Math.floor(prefs.repeated_tool.repeatable_cap);
|
|
202
|
+
}
|
|
203
|
+
for (const tool of prefs.repeated_tool.exempt_tools ?? []) {
|
|
204
|
+
if (typeof tool === "string" && tool.trim()) next.perToolExempt.add(tool.trim());
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Environment overrides (win over preferences).
|
|
210
|
+
const envEnabled = envBool("GSD_TOOL_LOOP_GUARD_ENABLED");
|
|
211
|
+
if (envEnabled !== undefined) next.guardEnabled = envEnabled;
|
|
212
|
+
const envIdenticalMax = envPositiveInt("GSD_TOOL_LOOP_IDENTICAL_MAX");
|
|
213
|
+
if (envIdenticalMax !== undefined) next.maxConsecutiveIdentical = envIdenticalMax;
|
|
214
|
+
const envDefaultCap = envPositiveInt("GSD_TOOL_LOOP_REPEATED_DEFAULT_CAP");
|
|
215
|
+
if (envDefaultCap !== undefined) next.perToolDefaultCap = envDefaultCap;
|
|
216
|
+
const envRepeatableCap = envPositiveInt("GSD_TOOL_LOOP_REPEATED_REPEATABLE_CAP");
|
|
217
|
+
if (envRepeatableCap !== undefined) next.perToolRepeatableCap = envRepeatableCap;
|
|
218
|
+
for (const tool of envToolList("GSD_TOOL_LOOP_EXEMPT_TOOLS")) next.perToolExempt.add(tool);
|
|
219
|
+
|
|
220
|
+
config = next;
|
|
221
|
+
}
|
|
51
222
|
|
|
52
223
|
let consecutiveCount = 0;
|
|
53
224
|
let lastSignature = "";
|
|
@@ -82,17 +253,20 @@ function hashToolCall(toolName: string, args: Record<string, unknown>): string {
|
|
|
82
253
|
* Returns `{ block: true, reason }` when the loop threshold is exceeded.
|
|
83
254
|
*
|
|
84
255
|
* Two independent guards run; whichever trips first blocks:
|
|
85
|
-
* 1. Identical-signature streak (
|
|
256
|
+
* 1. Identical-signature streak (config.maxConsecutiveIdentical, strict for
|
|
86
257
|
* ask_user_questions).
|
|
87
|
-
* 2. Per-tool-name cap (
|
|
258
|
+
* 2. Per-tool-name cap (config.perToolDefaultCap / config.perToolRepeatableCap),
|
|
88
259
|
* independent of args, reset after file-mutation progress — catches
|
|
89
260
|
* improvisation loops (#783).
|
|
261
|
+
*
|
|
262
|
+
* Both guards, their thresholds, and the per-tool exempt set are user-tunable
|
|
263
|
+
* via {@link configureToolCallLoopGuard} (#1198).
|
|
90
264
|
*/
|
|
91
265
|
export function checkToolCallLoop(
|
|
92
266
|
toolName: string,
|
|
93
267
|
args: Record<string, unknown>,
|
|
94
268
|
): { block: boolean; reason?: string; count?: number } {
|
|
95
|
-
if (!enabled) return { block: false, count: 0 };
|
|
269
|
+
if (!enabled || !config.guardEnabled) return { block: false, count: 0 };
|
|
96
270
|
|
|
97
271
|
const sig = hashToolCall(toolName, args);
|
|
98
272
|
|
|
@@ -107,14 +281,16 @@ export function checkToolCallLoop(
|
|
|
107
281
|
// ── Guard 1: identical-signature streak ──
|
|
108
282
|
const threshold = STRICT_LOOP_TOOLS.has(toolName)
|
|
109
283
|
? MAX_CONSECUTIVE_STRICT
|
|
110
|
-
:
|
|
284
|
+
: config.maxConsecutiveIdentical;
|
|
111
285
|
|
|
112
|
-
if (consecutiveCount > threshold) {
|
|
286
|
+
if (config.identicalEnabled && consecutiveCount > threshold) {
|
|
113
287
|
return {
|
|
114
288
|
block: true,
|
|
115
289
|
reason:
|
|
116
290
|
`Tool loop detected (identical args): ${toolName} called ${consecutiveCount} times ` +
|
|
117
|
-
`with identical arguments. Blocking to prevent infinite loop. ` +
|
|
291
|
+
`with identical arguments (max ${threshold}). Blocking to prevent infinite loop. ` +
|
|
292
|
+
`Raise tool_call_loop_guard.identical_args.max_consecutive_calls in .gsd/PREFERENCES.md ` +
|
|
293
|
+
`(or GSD_TOOL_LOOP_IDENTICAL_MAX) if this is expected. ` +
|
|
118
294
|
`Do not retry this tool or call other tools this turn — stop and respond to the user in text.`,
|
|
119
295
|
count: consecutiveCount,
|
|
120
296
|
};
|
|
@@ -139,15 +315,16 @@ export function checkToolCallLoop(
|
|
|
139
315
|
// Guard 1's identical-signature streak still catches a genuinely stuck
|
|
140
316
|
// browser loop.
|
|
141
317
|
if (
|
|
142
|
-
|
|
318
|
+
!config.repeatedEnabled ||
|
|
319
|
+
config.perToolExempt.has(toolName) ||
|
|
143
320
|
hasBrowserContractPrefix(canonicalToolName(toolName))
|
|
144
321
|
) {
|
|
145
322
|
return { block: false, count: consecutiveCount };
|
|
146
323
|
}
|
|
147
324
|
|
|
148
325
|
const perToolCap = INHERENTLY_REPEATABLE_TOOL_SET.has(toolName)
|
|
149
|
-
?
|
|
150
|
-
:
|
|
326
|
+
? config.perToolRepeatableCap
|
|
327
|
+
: config.perToolDefaultCap;
|
|
151
328
|
|
|
152
329
|
if (perToolCount > perToolCap) {
|
|
153
330
|
return {
|
|
@@ -156,6 +333,9 @@ export function checkToolCallLoop(
|
|
|
156
333
|
`Tool loop detected (repeated tool): ${toolName} called ${perToolCount} times ` +
|
|
157
334
|
`this turn (cap ${perToolCap}). Blocking to prevent infinite loop. ` +
|
|
158
335
|
`The tool may be unavailable or failing repeatedly. ` +
|
|
336
|
+
`Raise tool_call_loop_guard.repeated_tool.default_cap/repeatable_cap or add "${toolName}" ` +
|
|
337
|
+
`to tool_call_loop_guard.repeated_tool.exempt_tools in .gsd/PREFERENCES.md ` +
|
|
338
|
+
`(or GSD_TOOL_LOOP_REPEATED_* / GSD_TOOL_LOOP_EXEMPT_TOOLS) if this is expected. ` +
|
|
159
339
|
`Do not retry this tool or pivot to other tools this turn — stop and respond to the user in text.`,
|
|
160
340
|
count: perToolCount,
|
|
161
341
|
};
|
|
@@ -164,10 +344,21 @@ export function checkToolCallLoop(
|
|
|
164
344
|
return { block: false, count: consecutiveCount };
|
|
165
345
|
}
|
|
166
346
|
|
|
167
|
-
/**
|
|
168
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Record successful state progress so Guard 2 can decay on the next count.
|
|
349
|
+
* File mutations (edit/write/…) and successful shell/exec calls (bash,
|
|
350
|
+
* bg_shell, gsd_uat_exec, …) both count as progress (#1092, #1206). The caller
|
|
351
|
+
* only invokes this for non-error results, so failing improvisation loops
|
|
352
|
+
* (#783) never decay and still trip the per-tool cap.
|
|
353
|
+
*/
|
|
354
|
+
export function recordToolCallLoopMutation(toolName: string, details?: unknown): void {
|
|
169
355
|
if (!enabled) return;
|
|
170
|
-
if (
|
|
356
|
+
if (STATE_MUTATING_TOOL_SET.has(toolName)) {
|
|
357
|
+
mutationEpoch++;
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (!STATE_PROGRESSING_EXEC_TOOL_SET.has(toolName)) return;
|
|
361
|
+
if (toolName === "bg_shell" && details !== undefined && !isSuccessfulBgShellLoopProgress(details)) return;
|
|
171
362
|
mutationEpoch++;
|
|
172
363
|
}
|
|
173
364
|
|
|
@@ -675,10 +675,16 @@ export function ensureCodebaseMapFresh(
|
|
|
675
675
|
|
|
676
676
|
const metadata = parseCodebaseMapMetadata(existing);
|
|
677
677
|
const existingDescriptions = parseCodebaseMap(existing);
|
|
678
|
+
// Compare against the truncated count: when the repository has more tracked
|
|
679
|
+
// files than maxFiles, the map only lists the first maxFiles entries, so a
|
|
680
|
+
// stored fileCount above the cap (e.g. from an older map that recorded the
|
|
681
|
+
// full count) still matches the current truncated listing. Comparing raw
|
|
682
|
+
// values would flag "file-count-changed" on every turn, rewriting the
|
|
683
|
+
// timestamp and invalidating the provider KV cache prefix.
|
|
678
684
|
const staleReason =
|
|
679
685
|
!metadata ? "missing-metadata"
|
|
680
686
|
: metadata.fingerprint !== fingerprint ? "files-changed"
|
|
681
|
-
: metadata.fileCount !== listed.files.length ? "file-count-changed"
|
|
687
|
+
: Math.min(metadata.fileCount, resolved.maxFiles) !== listed.files.length ? "file-count-changed"
|
|
682
688
|
: metadata.truncated !== listed.truncated ? "truncation-changed"
|
|
683
689
|
: undefined;
|
|
684
690
|
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
filterDoctorIssues,
|
|
26
26
|
} from "./doctor.js";
|
|
27
27
|
import { isAutoActive, checkRemoteAutoSession } from "./auto.js";
|
|
28
|
-
import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
28
|
+
import { getAutoWorktreePath } from "./auto-worktree-path-resolution.js";
|
|
29
29
|
import { currentDirectoryRoot, projectRoot } from "./commands/context.js";
|
|
30
30
|
import { loadPrompt } from "./prompt-loader.js";
|
|
31
31
|
import { buildClaudeRuntimeFloorAdvisory } from "../../shared/claude-runtime-floor.js";
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from "./workflow-templates.js";
|
|
24
24
|
import { loadPrompt } from "./prompt-loader.js";
|
|
25
25
|
import { gsdRoot } from "./paths.js";
|
|
26
|
-
import { createGitService, runGit } from "./git-service.js";
|
|
26
|
+
import { createGitService, runGit, taskBranchArgs } from "./git-service.js";
|
|
27
27
|
import { isAutoActive, isAutoPaused } from "./auto.js";
|
|
28
28
|
import { getErrorMessage } from "./error-utils.js";
|
|
29
29
|
import { resolvePlugin, type WorkflowPlugin } from "./workflow-plugins.js";
|
|
@@ -464,7 +464,7 @@ export async function handleStart(
|
|
|
464
464
|
try {
|
|
465
465
|
git.autoCommit("workflow-template", templateId, []);
|
|
466
466
|
} catch { /* nothing to commit */ }
|
|
467
|
-
runGit(basePath,
|
|
467
|
+
runGit(basePath, taskBranchArgs(basePath, branchName, current, git.getMainBranch()));
|
|
468
468
|
branchCreated = true;
|
|
469
469
|
}
|
|
470
470
|
} catch (err) {
|
|
@@ -636,7 +636,7 @@ export function dispatchMarkdownPhasePlugin(
|
|
|
636
636
|
const current = git.getCurrentBranch();
|
|
637
637
|
if (current !== branchName) {
|
|
638
638
|
try { git.autoCommit("workflow-template", templateId, []); } catch { /* nothing to commit */ }
|
|
639
|
-
runGit(basePath,
|
|
639
|
+
runGit(basePath, taskBranchArgs(basePath, branchName, current, git.getMainBranch()));
|
|
640
640
|
branchCreated = true;
|
|
641
641
|
}
|
|
642
642
|
} catch (err) {
|
|
@@ -281,7 +281,7 @@ export function getMilestone(id: string): MilestoneRow | null {
|
|
|
281
281
|
export function getActiveMilestoneFromDb(): MilestoneRow | null {
|
|
282
282
|
if (!getDbOrNull()!) return null;
|
|
283
283
|
const row = getDbOrNull()!.prepare(
|
|
284
|
-
|
|
284
|
+
`SELECT * FROM milestones WHERE status NOT IN (${TERMINAL_STATUS_SQL}, 'parked') ORDER BY id LIMIT 1`,
|
|
285
285
|
).get();
|
|
286
286
|
if (!row) return null;
|
|
287
287
|
return rowToMilestone(row);
|
|
@@ -295,11 +295,11 @@ export function getActiveSliceFromDb(milestoneId: string): SliceRow | null {
|
|
|
295
295
|
const row = getDbOrNull()!.prepare(
|
|
296
296
|
`SELECT s.* FROM slices s
|
|
297
297
|
WHERE s.milestone_id = :mid
|
|
298
|
-
AND s.status NOT IN (
|
|
298
|
+
AND s.status NOT IN (${TERMINAL_STATUS_SQL})
|
|
299
299
|
AND NOT EXISTS (
|
|
300
300
|
SELECT 1 FROM json_each(s.depends) AS dep
|
|
301
301
|
WHERE dep.value NOT IN (
|
|
302
|
-
SELECT id FROM slices WHERE milestone_id = :mid AND status IN (
|
|
302
|
+
SELECT id FROM slices WHERE milestone_id = :mid AND status IN (${TERMINAL_STATUS_SQL})
|
|
303
303
|
)
|
|
304
304
|
)
|
|
305
305
|
ORDER BY s.sequence, s.id
|
|
@@ -312,7 +312,7 @@ export function getActiveSliceFromDb(milestoneId: string): SliceRow | null {
|
|
|
312
312
|
export function getActiveTaskFromDb(milestoneId: string, sliceId: string): TaskRow | null {
|
|
313
313
|
if (!getDbOrNull()!) return null;
|
|
314
314
|
const row = getDbOrNull()!.prepare(
|
|
315
|
-
|
|
315
|
+
`SELECT * FROM tasks WHERE milestone_id = :mid AND slice_id = :sid AND status NOT IN (${TERMINAL_STATUS_SQL}) ORDER BY sequence, id LIMIT 1`,
|
|
316
316
|
).get({ ":mid": milestoneId, ":sid": sliceId });
|
|
317
317
|
if (!row) return null;
|
|
318
318
|
return rowToTask(row);
|
|
@@ -339,7 +339,7 @@ export function getParallelMonitorSliceProgress(milestoneId: string): ParallelMo
|
|
|
339
339
|
s.id AS id,
|
|
340
340
|
s.status AS status,
|
|
341
341
|
COUNT(t.id) AS total,
|
|
342
|
-
COALESCE(SUM(CASE WHEN t.status
|
|
342
|
+
COALESCE(SUM(CASE WHEN t.status IN (${TERMINAL_STATUS_SQL}) THEN 1 ELSE 0 END), 0) AS done
|
|
343
343
|
FROM slices s
|
|
344
344
|
LEFT JOIN tasks t ON s.milestone_id=t.milestone_id AND s.id=t.slice_id
|
|
345
345
|
WHERE s.milestone_id=:mid
|
|
@@ -518,7 +518,7 @@ export function getSliceScopedArtifacts(milestoneId: string, sliceId: string): A
|
|
|
518
518
|
export function getActiveMilestoneIdFromDb(): IdStatusSummary | null {
|
|
519
519
|
if (!getDbOrNull()!) return null;
|
|
520
520
|
const row = getDbOrNull()!.prepare(
|
|
521
|
-
|
|
521
|
+
`SELECT id, status FROM milestones WHERE status NOT IN (${TERMINAL_STATUS_SQL}, 'parked') ORDER BY id LIMIT 1`,
|
|
522
522
|
).get();
|
|
523
523
|
if (!row) return null;
|
|
524
524
|
return rowToIdStatusSummary(row);
|
|
@@ -536,7 +536,7 @@ export function getSliceStatusSummary(milestoneId: string): IdStatusSummary[] {
|
|
|
536
536
|
export function getActiveTaskIdFromDb(milestoneId: string, sliceId: string): ActiveTaskSummary | null {
|
|
537
537
|
if (!getDbOrNull()!) return null;
|
|
538
538
|
const row = getDbOrNull()!.prepare(
|
|
539
|
-
|
|
539
|
+
`SELECT id, status, title FROM tasks WHERE milestone_id = :mid AND slice_id = :sid AND status NOT IN (${TERMINAL_STATUS_SQL}) ORDER BY sequence, id LIMIT 1`,
|
|
540
540
|
).get({ ":mid": milestoneId, ":sid": sliceId });
|
|
541
541
|
if (!row) return null;
|
|
542
542
|
return rowToActiveTaskSummary(row);
|
|
@@ -548,8 +548,8 @@ export function getSliceTaskCounts(milestoneId: string, sliceId: string): TaskSt
|
|
|
548
548
|
const row = getDbOrNull()!.prepare(
|
|
549
549
|
`SELECT
|
|
550
550
|
COUNT(*) as total,
|
|
551
|
-
SUM(CASE WHEN status IN (
|
|
552
|
-
SUM(CASE WHEN status NOT IN (
|
|
551
|
+
SUM(CASE WHEN status IN (${TERMINAL_STATUS_SQL}) THEN 1 ELSE 0 END) as done,
|
|
552
|
+
SUM(CASE WHEN status NOT IN (${TERMINAL_STATUS_SQL}) THEN 1 ELSE 0 END) as pending
|
|
553
553
|
FROM tasks WHERE milestone_id = :mid AND slice_id = :sid`,
|
|
554
554
|
).get({ ":mid": milestoneId, ":sid": sliceId });
|
|
555
555
|
return rowToTaskStatusCounts(row);
|
|
@@ -38,6 +38,17 @@ export type ReopenSliceOutcome =
|
|
|
38
38
|
* completion timestamps cleared, in one commit. Folds the hand-rolled
|
|
39
39
|
* transaction-plus-cascade previously in tools/reopen-slice.ts. Guards run
|
|
40
40
|
* inside the transaction (TOCTOU-safe).
|
|
41
|
+
*
|
|
42
|
+
* A closed slice is always reopenable. An open slice is only reopenable when it
|
|
43
|
+
* carries a completed-task state desync (#1205): a UAT→planning fallback can
|
|
44
|
+
* leave the slice "pending" while *all* its tasks stay "complete", which
|
|
45
|
+
* deadlocks every recovery path (plan-slice/replan-slice/complete-task all
|
|
46
|
+
* reject the still-closed tasks, and slice_reopen historically rejected the
|
|
47
|
+
* still-open slice). Resetting those tasks to pending gives the planner the
|
|
48
|
+
* clean slate it needs. An open slice that still has any unfinished
|
|
49
|
+
* (pending/in-progress) task is a normal in-flight slice, not the desync, and
|
|
50
|
+
* stays rejected as "slice-not-complete" so a full reset can't wipe its
|
|
51
|
+
* legitimate progress.
|
|
41
52
|
*/
|
|
42
53
|
export function reopenSliceCascade(milestoneId: string, sliceId: string): ReopenSliceOutcome {
|
|
43
54
|
requireDb();
|
|
@@ -48,9 +59,20 @@ export function reopenSliceCascade(milestoneId: string, sliceId: string): Reopen
|
|
|
48
59
|
if (isClosedStatus(milestone.status)) { outcome = { ok: false, reason: "milestone-closed", status: milestone.status }; return; }
|
|
49
60
|
const slice = getSlice(milestoneId, sliceId);
|
|
50
61
|
if (!slice) { outcome = { ok: false, reason: "slice-not-found" }; return; }
|
|
51
|
-
if (!isClosedStatus(slice.status)) { outcome = { ok: false, reason: "slice-not-complete", status: slice.status }; return; }
|
|
52
62
|
|
|
53
63
|
const tasks = getSliceTasks(milestoneId, sliceId);
|
|
64
|
+
// A closed slice is always reopenable. An open slice is only the #1205
|
|
65
|
+
// desync when EVERY task is already closed (a UAT→planning fallback left the
|
|
66
|
+
// slice open with all tasks still "complete"). Requiring *all* tasks closed
|
|
67
|
+
// (not merely one) means a normal in-flight slice — which keeps at least one
|
|
68
|
+
// pending/in-progress task — is rejected, so a full reset can't wipe its
|
|
69
|
+
// legitimate progress.
|
|
70
|
+
const allTasksClosed = tasks.length > 0 && tasks.every((t) => isClosedStatus(t.status));
|
|
71
|
+
if (!isClosedStatus(slice.status) && !allTasksClosed) {
|
|
72
|
+
outcome = { ok: false, reason: "slice-not-complete", status: slice.status };
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
getDbOrNull()!.prepare(
|
|
55
77
|
`UPDATE slices SET status = 'in_progress', completed_at = NULL WHERE milestone_id = :mid AND id = :sid`,
|
|
56
78
|
).run({ ":mid": milestoneId, ":sid": sliceId });
|