@opengsd/gsd-pi 1.2.0-dev.0b870afa → 1.2.0-dev.23d85b63
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/dist/cli-model-override.d.ts +15 -0
- package/dist/cli-model-override.js +21 -0
- package/dist/cli.js +14 -19
- package/dist/headless-events.d.ts +16 -1
- package/dist/headless-events.js +19 -2
- package/dist/headless.js +8 -1
- package/dist/loader.js +6 -4
- package/dist/onboarding.js +9 -4
- package/dist/provider-migrations.d.ts +23 -0
- package/dist/provider-migrations.js +41 -0
- package/dist/register-agent-bundles.d.ts +11 -2
- package/dist/register-agent-bundles.js +18 -4
- package/dist/resource-loader.d.ts +10 -5
- package/dist/resource-loader.js +121 -6
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/ask-user-questions.js +3 -2
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +447 -215
- package/dist/resources/extensions/claude-code-cli/turn-assembler.js +33 -1
- package/dist/resources/extensions/google-cli/stream-adapter.js +16 -1
- package/dist/resources/extensions/gsd/auto/closeout.js +215 -0
- package/dist/resources/extensions/gsd/auto/dispatch-history.js +21 -6
- package/dist/resources/extensions/gsd/auto/dispatch.js +365 -0
- package/dist/resources/extensions/gsd/auto/finalize.js +347 -0
- package/dist/resources/extensions/gsd/auto/loop.js +4 -1
- package/dist/resources/extensions/gsd/auto/milestone-lease-reclaim.js +56 -0
- package/dist/resources/extensions/gsd/auto/orchestrator.js +119 -18
- package/dist/resources/extensions/gsd/auto/phase-helpers.js +146 -0
- package/dist/resources/extensions/gsd/auto/phases.js +17 -2372
- package/dist/resources/extensions/gsd/auto/pre-dispatch.js +542 -0
- package/dist/resources/extensions/gsd/auto/unit-phase.js +694 -0
- package/dist/resources/extensions/gsd/auto/workflow-unit-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto/worktree-safety-phase.js +125 -0
- package/dist/resources/extensions/gsd/auto-closeout-messaging.js +90 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +255 -431
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +15 -3
- package/dist/resources/extensions/gsd/auto-dispatch.js +19 -1
- package/dist/resources/extensions/gsd/auto-model-selection.js +9 -6
- package/dist/resources/extensions/gsd/auto-post-unit.js +12 -8
- package/dist/resources/extensions/gsd/auto-prompts.js +5 -1
- package/dist/resources/extensions/gsd/auto-recovery.js +52 -6
- package/dist/resources/extensions/gsd/auto-start.js +28 -7
- package/dist/resources/extensions/gsd/auto-worktree.js +48 -3
- package/dist/resources/extensions/gsd/auto.js +83 -19
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +4 -2
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +37 -7
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +3 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +32 -3
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +56 -16
- package/dist/resources/extensions/gsd/closeout-wizard.js +8 -3
- package/dist/resources/extensions/gsd/commands/handlers/core.js +22 -8
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +2 -2
- package/dist/resources/extensions/gsd/commands-mcp-status.js +2 -2
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +8 -0
- package/dist/resources/extensions/gsd/commands-workflow-templates.js +9 -2
- package/dist/resources/extensions/gsd/config-overlay.js +11 -8
- package/dist/resources/extensions/gsd/db/engine.js +24 -6
- package/dist/resources/extensions/gsd/db/queries.js +30 -0
- package/dist/resources/extensions/gsd/db/writers/reconcile.js +19 -1
- package/dist/resources/extensions/gsd/db-migration-backup.js +51 -8
- package/dist/resources/extensions/gsd/db-transaction.js +27 -23
- package/dist/resources/extensions/gsd/db-writer.js +8 -17
- package/dist/resources/extensions/gsd/doctor-environment.js +256 -125
- package/dist/resources/extensions/gsd/doctor-git-checks.js +5 -1
- package/dist/resources/extensions/gsd/doctor-providers.js +1 -1
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +11 -9
- package/dist/resources/extensions/gsd/gsd-db.js +15 -20
- package/dist/resources/extensions/gsd/guided-flow.js +88 -2
- package/dist/resources/extensions/gsd/health-widget.js +87 -28
- package/dist/resources/extensions/gsd/mcp-bridge.js +10 -0
- package/dist/resources/extensions/gsd/memory-relations.js +1 -1
- package/dist/resources/extensions/gsd/milestone-settlement.js +2 -2
- package/dist/resources/extensions/gsd/notifications.js +12 -7
- package/dist/resources/extensions/gsd/preferences-models.js +17 -9
- package/dist/resources/extensions/gsd/preferences.js +91 -5
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +8 -2
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +6 -2
- package/dist/resources/extensions/gsd/prompts/execute-task.md +7 -2
- package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +2 -2
- package/dist/resources/extensions/gsd/prompts/run-uat.md +7 -1
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +1 -1
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -1
- package/dist/resources/extensions/gsd/provider-error-guidance.js +24 -0
- package/dist/resources/extensions/gsd/session-lock.js +4 -3
- package/dist/resources/extensions/gsd/skill-activation.js +3 -6
- package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +13 -6
- package/dist/resources/extensions/gsd/state.js +6 -2
- package/dist/resources/extensions/gsd/tool-surface-readiness.js +83 -31
- package/dist/resources/extensions/gsd/tools/complete-task.js +62 -0
- package/dist/resources/extensions/gsd/tools/exec-tool.js +2 -109
- package/dist/resources/extensions/gsd/tui/render-kit.js +38 -13
- package/dist/resources/extensions/gsd/unit-context-composer.js +1 -1
- package/dist/resources/extensions/gsd/unit-registry.js +34 -4
- package/dist/resources/extensions/gsd/workflow-logger.js +4 -0
- package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +2 -0
- package/dist/resources/extensions/gsd/workflow-mcp-readiness-cache.js +105 -0
- package/dist/resources/extensions/gsd/worktree-manager.js +101 -2
- package/dist/resources/extensions/gsd/worktree-safety.js +28 -26
- package/dist/resources/extensions/gsd/worktree-shell-guard.js +113 -0
- package/dist/resources/extensions/gsd/worktree.js +8 -1
- package/dist/resources/extensions/mcp-client/manager.js +6 -1
- package/dist/resources/extensions/search-the-web/index.js +41 -9
- package/dist/resources/extensions/search-the-web/native-search.js +18 -4
- package/dist/resources/extensions/shared/gsd-browser-cli.js +40 -2
- package/dist/resources/extensions/subagent/index.js +20 -15
- package/dist/resources/extensions/subagent/worktree-cwd.js +31 -0
- package/dist/resources/skills/create-skill/SKILL.md +3 -0
- package/dist/resources/skills/create-skill/references/skill-structure.md +1 -0
- package/dist/runtime-checks.d.ts +10 -0
- package/dist/runtime-checks.js +27 -0
- 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 +10 -10
- 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/api/boot/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
- package/dist/web/standalone/.next/server/chunks/{5942.js → 1128.js} +1 -1
- 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/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/package.json +4 -4
- 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/sdk.d.ts.map +1 -1
- package/packages/gsd-agent-core/dist/sdk.js +12 -6
- package/packages/gsd-agent-core/dist/sdk.js.map +1 -1
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.d.ts +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.js +12 -24
- package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/bash-execution.js +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/branch-summary-message.d.ts +3 -3
- package/packages/gsd-agent-modes/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/branch-summary-message.js +20 -11
- package/packages/gsd-agent-modes/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/chat-turn-connect.d.ts +4 -3
- package/packages/gsd-agent-modes/dist/modes/interactive/components/chat-turn-connect.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/chat-turn-connect.js +5 -54
- package/packages/gsd-agent-modes/dist/modes/interactive/components/chat-turn-connect.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/compaction-summary-message.d.ts +2 -4
- package/packages/gsd-agent-modes/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/compaction-summary-message.js +2 -4
- package/packages/gsd-agent-modes/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/custom-editor.d.ts +2 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/custom-editor.js +4 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/footer.d.ts +9 -12
- package/packages/gsd-agent-modes/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/footer.js +100 -166
- package/packages/gsd-agent-modes/dist/modes/interactive/components/footer.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/gsd-progress-state.d.ts +2 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/gsd-progress-state.d.ts.map +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/gsd-progress-state.js +4 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/gsd-progress-state.js.map +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/gsd-status-widget.d.ts +23 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/gsd-status-widget.d.ts.map +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/gsd-status-widget.js +178 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/gsd-status-widget.js.map +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.d.ts +8 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.js +21 -9
- package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js +10 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/skill-invocation-message.d.ts +2 -3
- package/packages/gsd-agent-modes/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/skill-invocation-message.js +2 -3
- package/packages/gsd-agent-modes/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts +11 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +85 -19
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts +71 -3
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +257 -37
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/user-message.d.ts +3 -3
- package/packages/gsd-agent-modes/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/user-message.js +19 -19
- package/packages/gsd-agent-modes/dist/modes/interactive/components/user-message.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts +3 -0
- 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 +26 -10
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/extension-ui-controller.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/extension-ui-controller.js +12 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.js +5 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js +3 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js +3 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-dialogs.js +2 -2
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-dialogs.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-widgets.d.ts +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-widgets.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-widgets.js +25 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-widgets.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-key-handlers.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-key-handlers.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-key-handlers.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-init.d.ts +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-init.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-init.js +20 -49
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-init.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-state.d.ts +4 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-state.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts +10 -2
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +48 -6
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.js +4 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-ui-messaging.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-ui-messaging.js +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-ui-messaging.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-mode.js +3 -0
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/README.md +12 -3
- package/packages/mcp-server/dist/cli-runner.d.ts +40 -0
- package/packages/mcp-server/dist/cli-runner.d.ts.map +1 -0
- package/packages/mcp-server/dist/cli-runner.js +137 -0
- package/packages/mcp-server/dist/cli-runner.js.map +1 -0
- package/packages/mcp-server/dist/cli.js +2 -58
- package/packages/mcp-server/dist/cli.js.map +1 -1
- package/packages/mcp-server/dist/pid-registry.d.ts +46 -0
- package/packages/mcp-server/dist/pid-registry.d.ts.map +1 -0
- package/packages/mcp-server/dist/pid-registry.js +459 -0
- package/packages/mcp-server/dist/pid-registry.js.map +1 -0
- package/packages/mcp-server/dist/probe-mode.d.ts +4 -0
- package/packages/mcp-server/dist/probe-mode.d.ts.map +1 -0
- package/packages/mcp-server/dist/probe-mode.js +10 -0
- package/packages/mcp-server/dist/probe-mode.js.map +1 -0
- package/packages/mcp-server/dist/stdio-watchdog.d.ts +8 -0
- package/packages/mcp-server/dist/stdio-watchdog.d.ts.map +1 -0
- package/packages/mcp-server/dist/stdio-watchdog.js +40 -0
- package/packages/mcp-server/dist/stdio-watchdog.js.map +1 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +62 -43
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +5 -5
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +52 -2
- 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/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts +28 -2
- package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +5 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/theme/theme.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/theme/theme.js +45 -17
- package/packages/pi-coding-agent/dist/theme/theme.js.map +1 -1
- package/packages/pi-coding-agent/package.json +8 -8
- package/packages/pi-tui/README.md +15 -0
- package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/packages/pi-tui/dist/autocomplete.js +6 -1
- package/packages/pi-tui/dist/autocomplete.js.map +1 -1
- package/packages/pi-tui/dist/components/input.js +1 -1
- package/packages/pi-tui/dist/components/input.js.map +1 -1
- package/packages/pi-tui/dist/components/loader.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/loader.js +1 -0
- package/packages/pi-tui/dist/components/loader.js.map +1 -1
- package/packages/pi-tui/dist/components/select-list.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/select-list.js +8 -2
- package/packages/pi-tui/dist/components/select-list.js.map +1 -1
- package/packages/pi-tui/dist/index.d.ts +2 -2
- package/packages/pi-tui/dist/index.d.ts.map +1 -1
- package/packages/pi-tui/dist/index.js +2 -2
- package/packages/pi-tui/dist/index.js.map +1 -1
- package/packages/pi-tui/dist/terminal-image.d.ts +33 -0
- package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
- package/packages/pi-tui/dist/terminal-image.js +54 -2
- package/packages/pi-tui/dist/terminal-image.js.map +1 -1
- package/packages/pi-tui/dist/terminal.d.ts +12 -0
- package/packages/pi-tui/dist/terminal.d.ts.map +1 -1
- package/packages/pi-tui/dist/terminal.js +70 -25
- package/packages/pi-tui/dist/terminal.js.map +1 -1
- package/packages/pi-tui/dist/tui.d.ts +15 -0
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +106 -21
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/dist/utils.d.ts.map +1 -1
- package/packages/pi-tui/dist/utils.js +110 -36
- package/packages/pi-tui/dist/utils.js.map +1 -1
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/dist/theme/theme.d.ts.map +1 -1
- package/pkg/dist/theme/theme.js +45 -17
- package/pkg/dist/theme/theme.js.map +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/ask-user-questions.ts +7 -2
- package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +80 -0
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +531 -226
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +672 -7
- package/src/resources/extensions/claude-code-cli/turn-assembler.ts +38 -1
- package/src/resources/extensions/google-cli/stream-adapter.ts +22 -1
- package/src/resources/extensions/gsd/auto/closeout.ts +309 -0
- package/src/resources/extensions/gsd/auto/dispatch-history.ts +22 -6
- package/src/resources/extensions/gsd/auto/dispatch.ts +449 -0
- package/src/resources/extensions/gsd/auto/finalize.ts +445 -0
- package/src/resources/extensions/gsd/auto/loop.ts +4 -1
- package/src/resources/extensions/gsd/auto/milestone-lease-reclaim.ts +74 -0
- package/src/resources/extensions/gsd/auto/orchestrator.ts +140 -18
- package/src/resources/extensions/gsd/auto/phase-helpers.ts +199 -0
- package/src/resources/extensions/gsd/auto/phases.ts +58 -3061
- package/src/resources/extensions/gsd/auto/pre-dispatch.ts +716 -0
- package/src/resources/extensions/gsd/auto/unit-phase.ts +910 -0
- package/src/resources/extensions/gsd/auto/workflow-unit-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto/worktree-safety-phase.ts +149 -0
- package/src/resources/extensions/gsd/auto-closeout-messaging.ts +90 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +310 -454
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +15 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +24 -2
- package/src/resources/extensions/gsd/auto-model-selection.ts +20 -5
- package/src/resources/extensions/gsd/auto-post-unit.ts +16 -9
- package/src/resources/extensions/gsd/auto-prompts.ts +5 -1
- package/src/resources/extensions/gsd/auto-recovery.ts +62 -8
- package/src/resources/extensions/gsd/auto-start.ts +44 -7
- package/src/resources/extensions/gsd/auto-worktree.ts +51 -3
- package/src/resources/extensions/gsd/auto.ts +118 -18
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +6 -2
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +56 -6
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +36 -3
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +69 -16
- package/src/resources/extensions/gsd/closeout-wizard.ts +11 -2
- package/src/resources/extensions/gsd/commands/handlers/core.ts +27 -8
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -2
- package/src/resources/extensions/gsd/commands-mcp-status.ts +2 -2
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +7 -0
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +11 -4
- package/src/resources/extensions/gsd/config-overlay.ts +22 -9
- package/src/resources/extensions/gsd/db/engine.ts +26 -6
- package/src/resources/extensions/gsd/db/queries.ts +29 -0
- package/src/resources/extensions/gsd/db/writers/reconcile.ts +24 -1
- package/src/resources/extensions/gsd/db-migration-backup.ts +56 -7
- package/src/resources/extensions/gsd/db-transaction.ts +37 -20
- package/src/resources/extensions/gsd/db-writer.ts +11 -19
- package/src/resources/extensions/gsd/doctor-environment.ts +267 -142
- package/src/resources/extensions/gsd/doctor-git-checks.ts +4 -1
- package/src/resources/extensions/gsd/doctor-providers.ts +1 -1
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +10 -10
- package/src/resources/extensions/gsd/gsd-db.ts +15 -19
- package/src/resources/extensions/gsd/guided-flow.ts +128 -2
- package/src/resources/extensions/gsd/health-widget.ts +91 -27
- package/src/resources/extensions/gsd/mcp-bridge.ts +39 -0
- package/src/resources/extensions/gsd/memory-relations.ts +1 -1
- package/src/resources/extensions/gsd/milestone-settlement.ts +2 -2
- package/src/resources/extensions/gsd/notifications.ts +13 -6
- package/src/resources/extensions/gsd/preferences-models.ts +26 -8
- package/src/resources/extensions/gsd/preferences.ts +111 -5
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +8 -2
- package/src/resources/extensions/gsd/prompts/complete-slice.md +6 -2
- package/src/resources/extensions/gsd/prompts/execute-task.md +7 -2
- package/src/resources/extensions/gsd/prompts/quick-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +2 -2
- package/src/resources/extensions/gsd/prompts/run-uat.md +7 -1
- package/src/resources/extensions/gsd/prompts/triage-captures.md +1 -1
- package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -1
- package/src/resources/extensions/gsd/provider-error-guidance.ts +32 -0
- package/src/resources/extensions/gsd/session-lock.ts +8 -7
- package/src/resources/extensions/gsd/skill-activation.ts +3 -6
- package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +23 -10
- package/src/resources/extensions/gsd/state.ts +7 -1
- package/src/resources/extensions/gsd/tests/auto-abort-pause-regression.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-blocked-remediation-message.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-closeout-messaging.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +68 -116
- package/src/resources/extensions/gsd/tests/auto-direct-dispatch-parse.test.ts +33 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +206 -22
- package/src/resources/extensions/gsd/tests/auto-model-selection-tool-poisoning.test.ts +7 -2
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +16 -1
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +89 -22
- package/src/resources/extensions/gsd/tests/auto-pause-double-entry-guard.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +97 -4
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +236 -0
- package/src/resources/extensions/gsd/tests/auto-unit-closeout.test.ts +169 -1
- package/src/resources/extensions/gsd/tests/auto-verification.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/blocker-placeholder-logs.test.ts +218 -0
- package/src/resources/extensions/gsd/tests/complete-milestone-prompt-rendering.test.ts +6 -2
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +141 -5
- package/src/resources/extensions/gsd/tests/core-overlay-fallback.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/db-engine-logs.test.ts +207 -0
- package/src/resources/extensions/gsd/tests/db-migration-backup.test.ts +68 -19
- package/src/resources/extensions/gsd/tests/db-transaction.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +15 -4
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +62 -0
- package/src/resources/extensions/gsd/tests/discuss-routing-fixes.test.ts +12 -2
- package/src/resources/extensions/gsd/tests/dispatch-db-degradation-logs.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/dispatch-history.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/dispatch-logs.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/dispatch-reactive-logs.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/dist-redirect.mjs +8 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +6 -4
- package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +117 -91
- package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +113 -0
- package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/gsd-command-home.test.ts +40 -7
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/guided-dispatch-root.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +75 -0
- package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/integration/doctor-environment-async.test.ts +104 -0
- package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/interactive-routing-bypass.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +47 -16
- package/src/resources/extensions/gsd/tests/loop.test.ts +60 -0
- package/src/resources/extensions/gsd/tests/mcp-readiness-preflight.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/mcp-status.test.ts +6 -5
- package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/milestone-report-path.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/milestone-settlement.test.ts +92 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-state-rebuild.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/model-router.test.ts +139 -0
- package/src/resources/extensions/gsd/tests/notifications.test.ts +64 -9
- package/src/resources/extensions/gsd/tests/oauth-api-model-routing.test.ts +13 -1
- package/src/resources/extensions/gsd/tests/orchestrator-legacy-parity.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/orchestrator-logs.test.ts +335 -0
- package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +5 -0
- package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/phases-terminal-complete-idempotent.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/plan-gate-failed-doctor-heal-hint.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/prefs-missing-models-crash.test.ts +35 -4
- package/src/resources/extensions/gsd/tests/progress-strip-test-helpers.ts +79 -0
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +11 -2
- package/src/resources/extensions/gsd/tests/provider-error-guidance.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +2 -4
- package/src/resources/extensions/gsd/tests/reconcile-logs.test.ts +244 -0
- package/src/resources/extensions/gsd/tests/recovery-finalize-logs.test.ts +119 -0
- package/src/resources/extensions/gsd/tests/recovery-verify-logs.test.ts +428 -0
- package/src/resources/extensions/gsd/tests/register-extension-guard.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/remote-notification-from-desktop.test.ts +31 -81
- package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +7 -1
- package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +68 -0
- package/src/resources/extensions/gsd/tests/show-config-command.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +170 -48
- package/src/resources/extensions/gsd/tests/skill-activation.test.ts +20 -17
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +7 -3
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +17 -1
- package/src/resources/extensions/gsd/tests/stop-auto-race-null-unit.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/thinking-level-resolution.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/tool-surface-readiness.test.ts +184 -10
- package/src/resources/extensions/gsd/tests/tui-header-lifecycle.test.ts +40 -86
- package/src/resources/extensions/gsd/tests/tui-render-kit.test.ts +44 -6
- package/src/resources/extensions/gsd/tests/uok-audit.test.ts +194 -0
- package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/workflow-mcp-readiness-cache.test.ts +119 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +65 -2
- package/src/resources/extensions/gsd/tests/workflow-phase-contract-matrix.test.ts +332 -0
- package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +92 -0
- package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/worktree-project-root-degrade.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-safety-phase.test.ts +100 -0
- package/src/resources/extensions/gsd/tests/worktree-safety.test.ts +72 -0
- package/src/resources/extensions/gsd/tests/worktree-teardown-safety.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/worktree-write-gate.test.ts +75 -3
- package/src/resources/extensions/gsd/tests/worktree.test.ts +18 -0
- package/src/resources/extensions/gsd/tool-surface-readiness.ts +126 -19
- package/src/resources/extensions/gsd/tools/complete-task.ts +87 -0
- package/src/resources/extensions/gsd/tools/exec-tool.ts +2 -118
- package/src/resources/extensions/gsd/tui/render-kit.ts +56 -13
- package/src/resources/extensions/gsd/unit-context-composer.ts +1 -1
- package/src/resources/extensions/gsd/unit-registry.ts +34 -4
- package/src/resources/extensions/gsd/workflow-logger.ts +5 -0
- package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +2 -0
- package/src/resources/extensions/gsd/workflow-mcp-readiness-cache.ts +150 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +97 -2
- package/src/resources/extensions/gsd/worktree-safety.ts +41 -39
- package/src/resources/extensions/gsd/worktree-shell-guard.ts +123 -0
- package/src/resources/extensions/gsd/worktree.ts +7 -1
- package/src/resources/extensions/mcp-client/manager.ts +7 -1
- package/src/resources/extensions/search-the-web/index.ts +45 -9
- package/src/resources/extensions/search-the-web/native-search.ts +16 -4
- package/src/resources/extensions/shared/gsd-browser-cli.ts +41 -2
- package/src/resources/extensions/subagent/index.ts +20 -15
- package/src/resources/extensions/subagent/tests/worktree-cwd.test.ts +57 -0
- package/src/resources/extensions/subagent/worktree-cwd.ts +35 -0
- package/src/resources/skills/create-skill/SKILL.md +3 -0
- package/src/resources/skills/create-skill/references/skill-structure.md +1 -0
- package/dist/resources/skills/gsd-browser/SKILL.md +0 -41
- package/src/resources/skills/gsd-browser/SKILL.md +0 -41
- /package/dist/web/standalone/.next/static/{T-LTxEw5wir5Lm5T3qEVd → Wn9u2NYq0cyUigB_3Z0_N}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{T-LTxEw5wir5Lm5T3qEVd → Wn9u2NYq0cyUigB_3Z0_N}/_ssgManifest.js +0 -0
|
@@ -1,1450 +1,26 @@
|
|
|
1
1
|
// Project/App: gsd-pi
|
|
2
|
-
// File Purpose: Auto-loop pipeline phases
|
|
2
|
+
// File Purpose: Auto-loop pipeline phases — compatibility shim.
|
|
3
3
|
/**
|
|
4
4
|
* auto/phases.ts — Pipeline phases for the auto-loop.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Imports from: auto/types, auto/detect-stuck, auto/run-unit, auto/loop-deps
|
|
6
|
+
* This file is now a thin compatibility shim. The implementation lives in
|
|
7
|
+
* focused modules (pre-dispatch, dispatch, unit-phase, finalize, closeout)
|
|
8
|
+
* and in the shared helpers (phase-helpers, worktree-safety-phase).
|
|
10
9
|
*/
|
|
11
|
-
import {
|
|
12
|
-
import { USER_DRIVEN_DEEP_UNITS, isAwaitingUserInput, } from "../auto-post-unit.js";
|
|
13
|
-
import { lastAssistantText } from "../consent-question.js";
|
|
14
|
-
import { resolveEffectiveUnitIsolationMode, getIsolationMode } from "../preferences.js";
|
|
15
|
-
import { MAX_RECOVERY_CHARS, BUDGET_THRESHOLDS, MAX_FINALIZE_TIMEOUTS, } from "./types.js";
|
|
16
|
-
import { detectStuck } from "./detect-stuck.js";
|
|
17
|
-
import { STUCK_WINDOW_SIZE } from "./dispatch-history.js";
|
|
18
|
-
import { runUnit } from "./run-unit.js";
|
|
10
|
+
import { basename } from "node:path";
|
|
19
11
|
import { debugLog } from "../debug-logger.js";
|
|
20
|
-
import {
|
|
21
|
-
import { buildManualValidationGuidance } from "../worktree-manager.js";
|
|
22
|
-
import { relSliceFile } from "../paths.js";
|
|
23
|
-
import { classifyProject } from "../detection.js";
|
|
24
|
-
import { MergeConflictError } from "../git-service.js";
|
|
25
|
-
import { setCurrentPhase, clearCurrentPhase } from "../../shared/gsd-phase-state.js";
|
|
26
|
-
import { pauseAutoForProviderError } from "../provider-error-pause.js";
|
|
27
|
-
import { resumeAutoAfterProviderDelay } from "../bootstrap/provider-error-resume.js";
|
|
28
|
-
import { join, basename } from "node:path";
|
|
29
|
-
import { existsSync, cpSync } from "node:fs";
|
|
30
|
-
import { logWarning, logError, _resetLogs, drainLogs, drainAndSummarize, formatForNotification, hasAnyIssues, } from "../workflow-logger.js";
|
|
31
|
-
import { gsdRoot, normalizeRealPath } from "../paths.js";
|
|
32
|
-
import { atomicWriteSync } from "../atomic-write.js";
|
|
33
|
-
import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps, refreshRecoveryDbForArtifact } from "../auto-recovery.js";
|
|
34
|
-
import { writeUnitRuntimeRecord } from "../unit-runtime.js";
|
|
35
|
-
import { withTimeout, FINALIZE_PRE_TIMEOUT_MS, FINALIZE_POST_TIMEOUT_MS } from "./finalize-timeout.js";
|
|
36
|
-
import { getEligibleSlices } from "../slice-parallel-eligibility.js";
|
|
37
|
-
import { isSliceParallelActive, startSliceParallel } from "../slice-parallel-orchestrator.js";
|
|
38
|
-
import { isDbAvailable, getMilestone, getMilestoneSlices, getSlice, getTask } from "../gsd-db.js";
|
|
39
|
-
import { refreshWorkflowDatabaseFromDisk } from "../db-workspace.js";
|
|
40
|
-
import { isClosedStatus } from "../status-guards.js";
|
|
41
|
-
import { findUnmergedCompletedMilestones } from "../unmerged-milestone-guard.js";
|
|
42
|
-
import { setRuntimeKv } from "../db/runtime-kv.js";
|
|
43
|
-
import { getLatestForUnit } from "../db/unit-dispatches.js";
|
|
44
|
-
import { reconcileBeforeSpawn } from "../state-reconciliation.js";
|
|
45
|
-
import { countUnmappedActiveRequirements, formatCompletePhaseNextAction, } from "../requirements-backlog.js";
|
|
46
|
-
import { ensurePlanV2Graph, isEmptyPlanV2GraphResult, isMissingFinalizedContextResult } from "../uok/plan-v2.js";
|
|
47
|
-
import { resolveUokFlags } from "../uok/flags.js";
|
|
48
|
-
import { UokGateRunner } from "../uok/gate-runner.js";
|
|
49
|
-
import { resetEvidence, loadEvidenceFromDisk } from "../safety/evidence-collector.js";
|
|
50
|
-
import { parseUnitId } from "../unit-id.js";
|
|
51
|
-
import { createCheckpoint, cleanupCheckpoint, rollbackToCheckpoint } from "../safety/git-checkpoint.js";
|
|
52
|
-
import { resolveSafetyHarnessConfig } from "../safety/safety-harness.js";
|
|
12
|
+
import { logWarning } from "../workflow-logger.js";
|
|
53
13
|
import { getContextPauseAction } from "../auto-budget.js";
|
|
54
|
-
import {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
import { captureRootDirtySnapshot, detectRootWriteLeak, formatRootWriteLeakMessage, } from "../root-write-leak-guard.js";
|
|
65
|
-
import { classifyError, isTransient } from "../error-classifier.js";
|
|
66
|
-
const STUCK_RECOVERY_ATTEMPTS_KEY = "stuck_recovery_attempts";
|
|
67
|
-
const ZERO_TOOL_PROVIDER_ERROR_PREFIX_RE = /^(?:api error(?::|$|\s*\()|provider error(?::|$|\s*\()|request failed\b|(?:http\s*)?(?:429|500|502|503)\b|\b(?:econnreset|etimedout|econnrefused|epipe)\b|socket hang up\b|fetch failed\b|(?:network|connection|server) error(?::|$)|connection (?:reset|refused)(?::|$|\s+by\b)|dns\b.*(?:fail|error|timeout)|unexpected eof\b|stream idle timeout\b|partial response received\b|stream_exhausted\b|terminated(?::|$)|(?:connection|stream|request)\b.{0,40}\bterminated\b|other side closed\b|rate.?limit(?:ed| exceeded| reached| error)|too many requests\b|you(?:'ve| have) (?:hit|reached) your (?:\w+ )?limit\b|.*\b(?:usage|session|weekly|daily|monthly|quota) limit\b|limit\b.{0,40}\bresets?\b|out of extra usage\b|service.?unavailable\b|internal(?: server)? error(?::|$)|internal(?:[_-]server)?[_-]error\b|server[_-]error\b|(?:provider|server|api|model|codex|claude|openai|anthropic|gemini)\b.{0,80}\boverloaded\b|overloaded\b.{0,80}\b(?:provider|server|api|model)\b|context (?:window|length) exceed|context window exceed)/i;
|
|
68
|
-
const ZERO_TOOL_PROVIDER_ERROR_SIGNAL_RE = /(?:\b(?:http|status(?: code)?|code|error:)\s*(?:429|500|502|503)\b|\b(?:api|provider) error\s*[:(]?\s*(?:429|500|502|503)\b|\b(?:typeerror|error):\s*(?:fetch failed\b|socket hang up\b|terminated(?::|$)|connection (?:reset|refused)(?::|$|\s+by\b)|(?:network|connection|server) error(?::|$)|stream idle timeout\b|partial response received\b|unexpected eof\b)|\b(?:server_error|api_error|stream_exhausted(?:_without_result)?)\b|\b(?:econnreset|etimedout|econnrefused|epipe)\b|context (?:window|length) exceed|context window exceed)/i;
|
|
69
|
-
function classifyZeroToolProviderMessage(message) {
|
|
70
|
-
const firstLine = message.trim().split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
71
|
-
if (!firstLine ||
|
|
72
|
-
(!ZERO_TOOL_PROVIDER_ERROR_PREFIX_RE.test(firstLine) &&
|
|
73
|
-
!ZERO_TOOL_PROVIDER_ERROR_SIGNAL_RE.test(firstLine)))
|
|
74
|
-
return null;
|
|
75
|
-
return classifyError(firstLine);
|
|
76
|
-
}
|
|
77
|
-
export const _classifyZeroToolProviderMessageForTest = classifyZeroToolProviderMessage;
|
|
78
|
-
export function resolveDispatchRecoveryAttempts(unitRecoveryCount, unitType, unitId) {
|
|
79
|
-
return (unitRecoveryCount.get(`${unitType}/${unitId}`) ?? 0) > 0
|
|
80
|
-
? 0
|
|
81
|
-
: undefined;
|
|
82
|
-
}
|
|
83
|
-
// ─── Path Comparison Helper ───────────────────────────────────────────────
|
|
84
|
-
/** Compare two paths for physical identity, tolerating trailing slashes and symlinks. */
|
|
85
|
-
function isSamePathLocal(a, b) {
|
|
86
|
-
return normalizeWorktreePathForCompare(a) === normalizeWorktreePathForCompare(b);
|
|
87
|
-
}
|
|
88
|
-
function isIsolatedWorktreeSession(s) {
|
|
89
|
-
return Boolean(s.originalBasePath)
|
|
90
|
-
&& Boolean(s.basePath)
|
|
91
|
-
&& !isSamePathLocal(s.originalBasePath, s.basePath);
|
|
92
|
-
}
|
|
93
|
-
function persistStuckRecoveryAttempts(s, loopState) {
|
|
94
|
-
const scopeId = normalizeRealPath(s.scope?.workspace.projectRoot ?? (s.originalBasePath || s.basePath));
|
|
95
|
-
if (!scopeId)
|
|
96
|
-
return;
|
|
97
|
-
try {
|
|
98
|
-
setRuntimeKv("global", scopeId, STUCK_RECOVERY_ATTEMPTS_KEY, loopState.stuckRecoveryAttempts);
|
|
99
|
-
}
|
|
100
|
-
catch (err) {
|
|
101
|
-
debugLog("autoLoop", {
|
|
102
|
-
phase: "save-stuck-state-failed",
|
|
103
|
-
error: err instanceof Error ? err.message : String(err),
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
async function applyVerificationRetryPolicy(ic, unitType, phase) {
|
|
108
|
-
const { ctx, pi, s, deps } = ic;
|
|
109
|
-
const retryInfo = s.pendingVerificationRetry;
|
|
110
|
-
const key = unitType && retryInfo
|
|
111
|
-
? verificationRetryKey(unitType, retryInfo.unitId)
|
|
112
|
-
: undefined;
|
|
113
|
-
const decision = decideVerificationRetry({
|
|
114
|
-
unitType,
|
|
115
|
-
retryInfo,
|
|
116
|
-
previousFailureHash: key ? s.verificationRetryFailureHashes.get(key) : undefined,
|
|
117
|
-
});
|
|
118
|
-
if (decision.action === "pause") {
|
|
119
|
-
s.pendingVerificationRetry = null;
|
|
120
|
-
debugLog("autoLoop", {
|
|
121
|
-
phase: `${phase}-paused`,
|
|
122
|
-
reason: decision.reason,
|
|
123
|
-
unitType,
|
|
124
|
-
unitId: retryInfo?.unitId,
|
|
125
|
-
failureHash: decision.failureHash,
|
|
126
|
-
});
|
|
127
|
-
ctx.ui.notify(decision.reason === "duplicate-failure-context"
|
|
128
|
-
? `Verification retry for ${unitType ?? "unit"} ${retryInfo?.unitId ?? "unknown"} produced the same failure context. Pausing auto-mode instead of re-dispatching.`
|
|
129
|
-
: "Verification retry requested without retry context. Pausing auto-mode instead of re-dispatching.", "warning");
|
|
130
|
-
await deps.pauseAuto(ctx, pi);
|
|
131
|
-
return { action: "break", reason: decision.reason };
|
|
132
|
-
}
|
|
133
|
-
s.verificationRetryFailureHashes.set(decision.key, decision.failureHash);
|
|
134
|
-
debugLog("autoLoop", {
|
|
135
|
-
phase: `${phase}-backoff`,
|
|
136
|
-
iteration: ic.iteration,
|
|
137
|
-
unitType,
|
|
138
|
-
unitId: retryInfo?.unitId,
|
|
139
|
-
attempt: retryInfo?.attempt,
|
|
140
|
-
delayMs: decision.delayMs,
|
|
141
|
-
baseDelayMs: decision.baseDelayMs,
|
|
142
|
-
failureHash: decision.failureHash,
|
|
143
|
-
});
|
|
144
|
-
await new Promise((resolve) => setTimeout(resolve, decision.delayMs));
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
function rememberRetryDispatch(s, unit, iterData) {
|
|
148
|
-
if (!unit)
|
|
149
|
-
return;
|
|
150
|
-
s.pendingVerificationRetryDispatch = {
|
|
151
|
-
unitType: unit.type,
|
|
152
|
-
unitId: unit.id,
|
|
153
|
-
prompt: iterData.prompt,
|
|
154
|
-
pauseAfterUatDispatch: iterData.pauseAfterUatDispatch,
|
|
155
|
-
state: iterData.state,
|
|
156
|
-
mid: iterData.mid,
|
|
157
|
-
midTitle: iterData.midTitle,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
function getAlreadyClosedDispatchReason(unitType, unitId) {
|
|
161
|
-
if (!isDbAvailable())
|
|
162
|
-
return null;
|
|
163
|
-
refreshWorkflowDatabaseFromDisk();
|
|
164
|
-
const { milestone, slice, task } = parseUnitId(unitId);
|
|
165
|
-
if (unitType === "execute-task" && milestone && slice && task) {
|
|
166
|
-
const row = getTask(milestone, slice, task);
|
|
167
|
-
return row && isClosedStatus(row.status)
|
|
168
|
-
? `execute-task ${unitId} is already ${row.status}`
|
|
169
|
-
: null;
|
|
170
|
-
}
|
|
171
|
-
if (unitType === "complete-slice" && milestone && slice) {
|
|
172
|
-
const row = getSlice(milestone, slice);
|
|
173
|
-
return row && isClosedStatus(row.status)
|
|
174
|
-
? `complete-slice ${unitId} is already ${row.status}`
|
|
175
|
-
: null;
|
|
176
|
-
}
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
export function shouldDegradeEmptyWorktreeToProjectRoot(worktreeClassification, projectRootClassification) {
|
|
180
|
-
return (worktreeClassification.kind === "greenfield" &&
|
|
181
|
-
projectRootClassification.kind !== "greenfield" &&
|
|
182
|
-
projectRootClassification.kind !== "invalid-repo");
|
|
183
|
-
}
|
|
184
|
-
function unitWritesSource(unitType) {
|
|
185
|
-
if (unitType.startsWith("hook/"))
|
|
186
|
-
return false;
|
|
187
|
-
// Backward compatibility: sidecar queues from older builds may persist
|
|
188
|
-
// prefixed unit types (e.g. "sidecar/quick-task").
|
|
189
|
-
const normalizedUnitType = unitType.startsWith("sidecar/")
|
|
190
|
-
? unitType.slice("sidecar/".length)
|
|
191
|
-
: unitType;
|
|
192
|
-
const manifest = resolveManifest(normalizedUnitType);
|
|
193
|
-
if (!manifest)
|
|
194
|
-
return null;
|
|
195
|
-
return manifest.tools.mode === "all" || manifest.tools.mode === "docs";
|
|
196
|
-
}
|
|
197
|
-
function formatWorktreeSafetyFailure(result) {
|
|
198
|
-
return `Worktree Safety failed (${result.kind}): ${result.reason} ${result.remediation}`;
|
|
199
|
-
}
|
|
200
|
-
function formatWorktreeSafetyStopReason(result) {
|
|
201
|
-
if (result.kind === "empty-worktree-with-project-content") {
|
|
202
|
-
return `Worktree Safety failed (${result.kind}). Run /gsd doctor fix, then /gsd auto.`;
|
|
203
|
-
}
|
|
204
|
-
return `Worktree Safety failed (${result.kind}).`;
|
|
205
|
-
}
|
|
206
|
-
function classifyBlocker(blocker) {
|
|
207
|
-
const normalized = blocker.toLowerCase();
|
|
208
|
-
if (normalized.includes("needs-remediation") && normalized.includes("all slices are complete")) {
|
|
209
|
-
return "needs-remediation-dead-end";
|
|
210
|
-
}
|
|
211
|
-
return "other";
|
|
212
|
-
}
|
|
213
|
-
function sanitizeBlockerForUser(blocker) {
|
|
214
|
-
return blocker.replaceAll("gsd_reassess_roadmap", "/gsd dispatch reassess");
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Formats blocked resume guidance for users, ensuring internal tool names are
|
|
218
|
-
* never surfaced in notification text.
|
|
219
|
-
*/
|
|
220
|
-
function formatBlockedResumeMessage(blockers) {
|
|
221
|
-
const classifiedBlockers = blockers.map((blocker) => ({
|
|
222
|
-
blocker: sanitizeBlockerForUser(blocker),
|
|
223
|
-
kind: classifyBlocker(blocker),
|
|
224
|
-
}));
|
|
225
|
-
const hasNeedsRemediationDeadEnd = classifiedBlockers.some((classifiedBlocker) => classifiedBlocker.kind === "needs-remediation-dead-end");
|
|
226
|
-
if (hasNeedsRemediationDeadEnd) {
|
|
227
|
-
return "Blocked: milestone validation requires remediation but all slices are complete. Run /gsd dispatch reassess to add remediation slices, then /gsd auto to continue.";
|
|
228
|
-
}
|
|
229
|
-
return `Blocked: ${classifiedBlockers.map((classifiedBlocker) => classifiedBlocker.blocker).join(", ")}. Fix and run /gsd auto to resume.`;
|
|
230
|
-
}
|
|
231
|
-
function resolveEmptyWorktreeWithProjectContent(unitRoot, projectRoot) {
|
|
232
|
-
if (isSamePathLocal(unitRoot, projectRoot))
|
|
233
|
-
return false;
|
|
234
|
-
const worktreeClassification = classifyProject(unitRoot);
|
|
235
|
-
if (worktreeClassification.kind !== "greenfield")
|
|
236
|
-
return false;
|
|
237
|
-
const projectRootClassification = classifyProject(projectRoot);
|
|
238
|
-
return shouldDegradeEmptyWorktreeToProjectRoot(worktreeClassification, projectRootClassification);
|
|
239
|
-
}
|
|
240
|
-
async function validateSourceWriteWorktreeSafety(ic, unitType, unitId, milestoneId, phase) {
|
|
241
|
-
const { ctx, pi, s, deps } = ic;
|
|
242
|
-
if (!s.basePath)
|
|
243
|
-
return null;
|
|
244
|
-
// Custom engine workflows (graph-driven, registered via run dirs) define
|
|
245
|
-
// their own step ids that are not in the GSD UnitContextManifest. Don't
|
|
246
|
-
// fail closed for those — the custom engine owns its own dispatch
|
|
247
|
-
// contract. The fail-closed safety check applies only to built-in GSD
|
|
248
|
-
// units whose Tool Contract is registered in the manifest. Use a truthy
|
|
249
|
-
// check so undefined (test sessions that never set the field) routes
|
|
250
|
-
// through the safety check, matching the regression test contract.
|
|
251
|
-
if (s.activeEngineId)
|
|
252
|
-
return null;
|
|
253
|
-
const writesSource = unitWritesSource(unitType);
|
|
254
|
-
if (writesSource === null) {
|
|
255
|
-
const msg = `Worktree Safety failed (missing-tool-contract): missing Tool Contract for ${unitType}. Add a UnitContextManifest entry before dispatching this Unit.`;
|
|
256
|
-
debugLog("worktreeSafety", {
|
|
257
|
-
phase,
|
|
258
|
-
unitType,
|
|
259
|
-
unitId,
|
|
260
|
-
milestoneId,
|
|
261
|
-
result: { ok: false, kind: "missing-tool-contract", reason: msg },
|
|
262
|
-
basePath: s.basePath,
|
|
263
|
-
});
|
|
264
|
-
ctx.ui.notify(msg, "error");
|
|
265
|
-
await deps.stopAuto(ctx, pi, msg);
|
|
266
|
-
return { action: "break", reason: "missing-tool-contract" };
|
|
267
|
-
}
|
|
268
|
-
if (!writesSource)
|
|
269
|
-
return null;
|
|
270
|
-
const projectRoot = s.canonicalProjectRoot ?? resolveWorktreeProjectRoot(s.basePath, s.originalBasePath);
|
|
271
|
-
// A degraded session already fell back to the milestone branch in the
|
|
272
|
-
// project root — validating against the canonical worktree root there
|
|
273
|
-
// would fail every dispatch with a false invalid-root. The same applies
|
|
274
|
-
// to a stranded-recovery session that adopted the milestone branch.
|
|
275
|
-
const isolationMode = resolveEffectiveUnitIsolationMode(deps.getIsolationMode(projectRoot), s.isolationDegraded, s.strandedRecoveryIsolationMode);
|
|
276
|
-
if (isolationMode !== "worktree")
|
|
277
|
-
return null;
|
|
278
|
-
const safety = createWorktreeSafetyModule();
|
|
279
|
-
const result = safety.validateUnitRoot({
|
|
280
|
-
unitType,
|
|
281
|
-
unitId,
|
|
282
|
-
writeScope: "source-writing",
|
|
283
|
-
projectRoot,
|
|
284
|
-
unitRoot: s.basePath,
|
|
285
|
-
milestoneId,
|
|
286
|
-
isolationMode,
|
|
287
|
-
expectedBranch: milestoneId ? deps.autoWorktreeBranch(milestoneId) : null,
|
|
288
|
-
emptyWorktreeWithProjectContent: resolveEmptyWorktreeWithProjectContent(s.basePath, projectRoot),
|
|
289
|
-
lease: s.workerId
|
|
290
|
-
? {
|
|
291
|
-
required: true,
|
|
292
|
-
held: s.currentMilestoneId === milestoneId && s.milestoneLeaseToken !== null,
|
|
293
|
-
owner: s.workerId,
|
|
294
|
-
}
|
|
295
|
-
: undefined,
|
|
296
|
-
});
|
|
297
|
-
if (result.ok)
|
|
298
|
-
return null;
|
|
299
|
-
const msg = formatWorktreeSafetyFailure(result);
|
|
300
|
-
debugLog("worktreeSafety", {
|
|
301
|
-
phase,
|
|
302
|
-
unitType,
|
|
303
|
-
unitId,
|
|
304
|
-
milestoneId,
|
|
305
|
-
result,
|
|
306
|
-
basePath: s.basePath,
|
|
307
|
-
projectRoot,
|
|
308
|
-
});
|
|
309
|
-
ctx.ui.notify(msg, "error");
|
|
310
|
-
await deps.stopAuto(ctx, pi, formatWorktreeSafetyStopReason(result));
|
|
311
|
-
return { action: "break", reason: result.kind };
|
|
312
|
-
}
|
|
313
|
-
// ─── Session timeout auto-resume state ────────────────────────────────────────
|
|
314
|
-
let consecutiveSessionTimeouts = 0;
|
|
315
|
-
const MAX_SESSION_TIMEOUT_AUTO_RESUMES = 3;
|
|
316
|
-
/** Maximum zero-tool-call retries before pausing — context exhaustion is deterministic. */
|
|
317
|
-
const MAX_ZERO_TOOL_RETRIES = 1;
|
|
318
|
-
export function resetSessionTimeoutState() {
|
|
319
|
-
consecutiveSessionTimeouts = 0;
|
|
320
|
-
}
|
|
321
|
-
// ─── generateMilestoneReport ──────────────────────────────────────────────────
|
|
322
|
-
/**
|
|
323
|
-
* Resolve the base path for milestone reports.
|
|
324
|
-
* Prefers originalBasePath (project root) over basePath (which may be a worktree).
|
|
325
|
-
* Exported for testing as _resolveReportBasePath.
|
|
326
|
-
*/
|
|
327
|
-
export function _resolveReportBasePath(s) {
|
|
328
|
-
return resolveWorktreeProjectRoot(s.basePath, s.originalBasePath);
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Resolve the authoritative project base for dispatch guards.
|
|
332
|
-
* Prior-milestone completion lives at the project root, even when the active
|
|
333
|
-
* unit is running inside an auto worktree.
|
|
334
|
-
*/
|
|
335
|
-
export function _resolveDispatchGuardBasePath(s) {
|
|
336
|
-
return resolveWorktreeProjectRoot(s.basePath, s.originalBasePath);
|
|
337
|
-
}
|
|
338
|
-
const PLAN_V2_GATE_PHASES = new Set([
|
|
339
|
-
"executing",
|
|
340
|
-
"summarizing",
|
|
341
|
-
"validating-milestone",
|
|
342
|
-
"completing-milestone",
|
|
343
|
-
]);
|
|
344
|
-
export function shouldRunPlanV2Gate(phase) {
|
|
345
|
-
return PLAN_V2_GATE_PHASES.has(phase);
|
|
346
|
-
}
|
|
347
|
-
export function _shouldProceedWithInvalidRepoClassificationForTest(reason, hasGit) {
|
|
348
|
-
return reason === "missing .git" && hasGit;
|
|
349
|
-
}
|
|
350
|
-
export function _resolveCurrentUnitStartedAtForTest(currentUnit) {
|
|
351
|
-
return currentUnit?.startedAt;
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Generate and write an HTML milestone report snapshot.
|
|
355
|
-
* Extracted from the milestone-transition block in autoLoop.
|
|
356
|
-
*/
|
|
357
|
-
async function generateMilestoneReport(s, ctx, milestoneId) {
|
|
358
|
-
const { loadVisualizerData } = await importExtensionModule(import.meta.url, "../visualizer-data.js");
|
|
359
|
-
const { generateHtmlReport } = await importExtensionModule(import.meta.url, "../export-html.js");
|
|
360
|
-
const { writeReportSnapshot } = await importExtensionModule(import.meta.url, "../reports.js");
|
|
361
|
-
const { basename } = await import("node:path");
|
|
362
|
-
const reportBasePath = _resolveReportBasePath(s);
|
|
363
|
-
const snapData = await loadVisualizerData(reportBasePath);
|
|
364
|
-
const completedMs = snapData.milestones.find((m) => m.id === milestoneId);
|
|
365
|
-
const msTitle = completedMs?.title ?? milestoneId;
|
|
366
|
-
const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
|
|
367
|
-
const projName = basename(reportBasePath);
|
|
368
|
-
const doneSlices = snapData.milestones.reduce((acc, m) => acc + m.slices.filter((sl) => sl.done).length, 0);
|
|
369
|
-
const totalSlices = snapData.milestones.reduce((acc, m) => acc + m.slices.length, 0);
|
|
370
|
-
const outPath = writeReportSnapshot({
|
|
371
|
-
basePath: reportBasePath,
|
|
372
|
-
html: generateHtmlReport(snapData, {
|
|
373
|
-
projectName: projName,
|
|
374
|
-
projectPath: reportBasePath,
|
|
375
|
-
gsdVersion,
|
|
376
|
-
milestoneId,
|
|
377
|
-
indexRelPath: "index.html",
|
|
378
|
-
}),
|
|
379
|
-
milestoneId,
|
|
380
|
-
milestoneTitle: msTitle,
|
|
381
|
-
kind: "milestone",
|
|
382
|
-
projectName: projName,
|
|
383
|
-
projectPath: reportBasePath,
|
|
384
|
-
gsdVersion,
|
|
385
|
-
totalCost: snapData.totals?.cost ?? 0,
|
|
386
|
-
totalTokens: snapData.totals?.tokens.total ?? 0,
|
|
387
|
-
totalDuration: snapData.totals?.duration ?? 0,
|
|
388
|
-
doneSlices,
|
|
389
|
-
totalSlices,
|
|
390
|
-
doneMilestones: snapData.milestones.filter((m) => m.status === "complete").length,
|
|
391
|
-
totalMilestones: snapData.milestones.length,
|
|
392
|
-
phase: snapData.phase,
|
|
393
|
-
});
|
|
394
|
-
ctx.ui.notify(`Report saved: .gsd/reports/${basename(outPath)} — open index.html to browse progression.`, "info");
|
|
395
|
-
}
|
|
396
|
-
// ─── closeoutAndStop ──────────────────────────────────────────────────────────
|
|
397
|
-
/**
|
|
398
|
-
* If a unit is in-flight, close it out, then stop auto-mode.
|
|
399
|
-
* Extracted from ~4 identical if-closeout-then-stop sequences in autoLoop.
|
|
400
|
-
*/
|
|
401
|
-
async function closeoutAndStop(ctx, pi, s, deps, reason) {
|
|
402
|
-
if (s.currentUnit) {
|
|
403
|
-
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
404
|
-
s.clearCurrentUnit();
|
|
405
|
-
}
|
|
406
|
-
await deps.stopAuto(ctx, pi, reason);
|
|
407
|
-
}
|
|
408
|
-
async function stopOnPostflightRecoveryNeeded(ic, result, milestoneId) {
|
|
409
|
-
if (!result.needsManualRecovery)
|
|
410
|
-
return null;
|
|
411
|
-
const { ctx, pi, deps } = ic;
|
|
412
|
-
const reason = `Post-merge stash restore failed for milestone ${milestoneId}`;
|
|
413
|
-
ctx.ui.notify(`${reason}. Resolve the working tree before resuming auto-mode. ${result.message}`, "error");
|
|
414
|
-
await deps.stopAuto(ctx, pi, reason);
|
|
415
|
-
return { action: "break", reason: "postflight-stash-restore-failed" };
|
|
416
|
-
}
|
|
417
|
-
async function restorePreflightStashOrStop(ic, preflight, milestoneId) {
|
|
418
|
-
if (!preflight.stashPushed)
|
|
419
|
-
return null;
|
|
420
|
-
const { ctx, s, deps } = ic;
|
|
421
|
-
const result = deps.postflightPopStash(s.originalBasePath || s.basePath, milestoneId, preflight.stashMarker, ctx.ui.notify.bind(ctx.ui));
|
|
422
|
-
return stopOnPostflightRecoveryNeeded(ic, result, milestoneId);
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
* Run a milestone merge surrounded by preflight stash + always-on postflight
|
|
426
|
-
* pop. The previous code popped the stash only after a successful merge, which
|
|
427
|
-
* leaked `gsd-preflight-stash:M00x:*` entries whenever `mergeAndExit` threw —
|
|
428
|
-
* leaving the user's pre-merge working tree silently stashed away after a
|
|
429
|
-
* merge-conflict or other merge error. This helper restores the stash on
|
|
430
|
-
* every exit path, then surfaces the merge or stash failure (in priority
|
|
431
|
-
* order) as the loop's stop reason.
|
|
432
|
-
*
|
|
433
|
-
* Returns a `break` action when auto-mode must stop, or `null` when the merge
|
|
434
|
-
* succeeded and the stash (if any) was restored cleanly.
|
|
435
|
-
*/
|
|
436
|
-
export async function _runMilestoneMergeWithStashRestore(ic, milestoneId, options = {}) {
|
|
437
|
-
const { ctx, pi, s, deps } = ic;
|
|
438
|
-
const preflight = deps.preflightCleanRoot(s.originalBasePath || s.basePath, milestoneId, ctx.ui.notify.bind(ctx.ui));
|
|
439
|
-
if (preflight.blocked) {
|
|
440
|
-
const reason = preflight.blockedReason === "unmerged-conflicts"
|
|
441
|
-
? `Pre-merge unresolved Git conflicts block milestone ${milestoneId}`
|
|
442
|
-
: `Pre-merge dirty working tree overlaps milestone ${milestoneId}`;
|
|
443
|
-
await deps.stopAuto(ctx, pi, reason, {
|
|
444
|
-
preserveCompletedMilestoneBranch: true,
|
|
445
|
-
preserveCloseoutTranscript: options.preserveCloseoutTranscript,
|
|
446
|
-
});
|
|
447
|
-
return {
|
|
448
|
-
action: "break",
|
|
449
|
-
reason: preflight.blockedReason === "unmerged-conflicts"
|
|
450
|
-
? "preflight-unmerged-conflicts"
|
|
451
|
-
: "preflight-dirty-overlap",
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
let mergeError = null;
|
|
455
|
-
const exitResult = deps.lifecycle.exitMilestone(milestoneId, { merge: true }, ctx.ui);
|
|
456
|
-
if (exitResult.ok) {
|
|
457
|
-
s.milestoneMergedInPhases = true;
|
|
458
|
-
try {
|
|
459
|
-
const projectRoot = s.originalBasePath || s.canonicalProjectRoot || s.basePath;
|
|
460
|
-
const { rebuildMarkdownProjectionsFromDb } = await import("../commands-maintenance.js");
|
|
461
|
-
await rebuildMarkdownProjectionsFromDb(projectRoot);
|
|
462
|
-
}
|
|
463
|
-
catch (err) {
|
|
464
|
-
logWarning("engine", `markdown projection rebuild after milestone merge failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
else {
|
|
468
|
-
mergeError = exitResult.cause ?? new Error(`exit ${exitResult.reason}`);
|
|
469
|
-
}
|
|
470
|
-
// Always attempt to restore the stashed working tree, even on merge error.
|
|
471
|
-
// postflightPopStash itself does not throw; failures surface via the
|
|
472
|
-
// PostflightResult.needsManualRecovery flag.
|
|
473
|
-
let stashResult = null;
|
|
474
|
-
if (preflight.stashPushed) {
|
|
475
|
-
stashResult = deps.postflightPopStash(s.originalBasePath || s.basePath, milestoneId, preflight.stashMarker, ctx.ui.notify.bind(ctx.ui));
|
|
476
|
-
}
|
|
477
|
-
// Merge failure takes priority over stash recovery — the merge is the
|
|
478
|
-
// authoritative gate. If the stash also needed manual recovery, the user
|
|
479
|
-
// already saw the postflightPopStash notify above.
|
|
480
|
-
if (mergeError) {
|
|
481
|
-
if (mergeError instanceof MergeConflictError) {
|
|
482
|
-
// A merge conflict is a recoverable human checkpoint, not an
|
|
483
|
-
// infrastructure failure — the user resolves the conflict and runs
|
|
484
|
-
// `/gsd auto` to resume. Pause (don't stop): stopAuto tears down the
|
|
485
|
-
// session and, because `milestoneMergedInPhases` stays false here,
|
|
486
|
-
// re-runs the already-failed worktree merge in its cleanup step
|
|
487
|
-
// (#2645), then drops the user out of the interactive TUI onto a
|
|
488
|
-
// "stopped" surface.
|
|
489
|
-
const conflictReason = `Merge conflict on milestone ${milestoneId}: ${mergeError.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`;
|
|
490
|
-
ctx.ui.notify(conflictReason, "error");
|
|
491
|
-
await deps.pauseAuto(ctx, pi, {
|
|
492
|
-
message: conflictReason,
|
|
493
|
-
category: "unknown",
|
|
494
|
-
});
|
|
495
|
-
return { action: "break", reason: "merge-conflict" };
|
|
496
|
-
}
|
|
497
|
-
logError("engine", "Milestone merge failed with non-conflict error", {
|
|
498
|
-
milestone: milestoneId,
|
|
499
|
-
error: String(mergeError),
|
|
500
|
-
});
|
|
501
|
-
// Like a merge conflict, a non-conflict merge failure (index lock,
|
|
502
|
-
// network, permissions) is recoverable — the user fixes the cause and
|
|
503
|
-
// runs `/gsd auto` to resume. Pause (don't stop) so the session stays
|
|
504
|
-
// resumable and stopAuto's teardown does not re-run the failed merge.
|
|
505
|
-
const mergeFailReason = `Merge error on milestone ${milestoneId}: ${mergeError instanceof Error ? mergeError.message : String(mergeError)}. Resolve and run /gsd auto to resume.`;
|
|
506
|
-
ctx.ui.notify(mergeFailReason, "error");
|
|
507
|
-
await deps.pauseAuto(ctx, pi, {
|
|
508
|
-
message: mergeFailReason,
|
|
509
|
-
category: "unknown",
|
|
510
|
-
});
|
|
511
|
-
return { action: "break", reason: "merge-failed" };
|
|
512
|
-
}
|
|
513
|
-
if (stashResult) {
|
|
514
|
-
return stopOnPostflightRecoveryNeeded(ic, stashResult, milestoneId);
|
|
515
|
-
}
|
|
516
|
-
return null;
|
|
517
|
-
}
|
|
518
|
-
export async function _runMilestoneMergeOnceWithStashRestore(ic, milestoneId, options = {}) {
|
|
519
|
-
if (ic.s.milestoneMergedInPhases) {
|
|
520
|
-
debugLog("autoLoop", {
|
|
521
|
-
phase: "milestone-merge-skip",
|
|
522
|
-
reason: "already-merged-in-phases",
|
|
523
|
-
milestoneId,
|
|
524
|
-
});
|
|
525
|
-
return null;
|
|
526
|
-
}
|
|
527
|
-
return _runMilestoneMergeWithStashRestore(ic, milestoneId, options);
|
|
528
|
-
}
|
|
529
|
-
async function emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, errorContext) {
|
|
530
|
-
ic.deps.emitJournalEvent({
|
|
531
|
-
ts: new Date().toISOString(),
|
|
532
|
-
flowId: ic.flowId,
|
|
533
|
-
seq: ic.nextSeq(),
|
|
534
|
-
eventType: "unit-end",
|
|
535
|
-
data: {
|
|
536
|
-
unitType,
|
|
537
|
-
unitId,
|
|
538
|
-
status: "cancelled",
|
|
539
|
-
artifactVerified: false,
|
|
540
|
-
...(errorContext ? { errorContext } : {}),
|
|
541
|
-
},
|
|
542
|
-
causedBy: { flowId: ic.flowId, seq: unitStartSeq },
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
export function _buildCancelledUnitStopReason(unitType, unitId, errorContext) {
|
|
546
|
-
const cancellationMessage = errorContext?.message ?? "unknown";
|
|
547
|
-
const isSessionCreationFailure = errorContext?.category === "session-failed";
|
|
548
|
-
if (isSessionCreationFailure) {
|
|
549
|
-
return {
|
|
550
|
-
notifyMessage: `Session creation failed for ${unitType} ${unitId}: ${cancellationMessage}. Stopping auto-mode.`,
|
|
551
|
-
stopReason: `Session creation failed: ${cancellationMessage}`,
|
|
552
|
-
loopReason: "session-failed",
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
return {
|
|
556
|
-
notifyMessage: `Unit ${unitType} ${unitId} aborted after dispatch: ${cancellationMessage}. Stopping auto-mode.`,
|
|
557
|
-
stopReason: `Unit aborted: ${cancellationMessage}`,
|
|
558
|
-
loopReason: "unit-aborted",
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
export function _isPauseOriginCancelledResult(isPaused, errorContext) {
|
|
562
|
-
return isPaused && !errorContext;
|
|
563
|
-
}
|
|
564
|
-
async function failClosedOnFinalizeTimeout(ic, iterData, loopState, stage, startedAt) {
|
|
565
|
-
const { ctx, pi, s, deps } = ic;
|
|
566
|
-
const now = Date.now();
|
|
567
|
-
const unitType = iterData.unitType;
|
|
568
|
-
const unitId = iterData.unitId;
|
|
569
|
-
const timeoutMs = stage === "pre" ? FINALIZE_PRE_TIMEOUT_MS : FINALIZE_POST_TIMEOUT_MS;
|
|
570
|
-
const progressKind = stage === "pre" ? "finalize-pre-timeout" : "finalize-post-timeout";
|
|
571
|
-
writeUnitRuntimeRecord(s.basePath, unitType, unitId, startedAt, {
|
|
572
|
-
phase: "finalize-timeout",
|
|
573
|
-
timeoutAt: now,
|
|
574
|
-
lastProgressAt: now,
|
|
575
|
-
lastProgressKind: progressKind,
|
|
576
|
-
});
|
|
577
|
-
deps.emitJournalEvent({
|
|
578
|
-
ts: new Date(now).toISOString(),
|
|
579
|
-
flowId: ic.flowId,
|
|
580
|
-
seq: ic.nextSeq(),
|
|
581
|
-
eventType: "unit-end",
|
|
582
|
-
data: {
|
|
583
|
-
unitType,
|
|
584
|
-
unitId,
|
|
585
|
-
status: "timed-out-finalize",
|
|
586
|
-
artifactVerified: false,
|
|
587
|
-
finalizeStage: stage,
|
|
588
|
-
},
|
|
589
|
-
});
|
|
590
|
-
loopState.consecutiveFinalizeTimeouts++;
|
|
591
|
-
debugLog("autoLoop", {
|
|
592
|
-
phase: progressKind,
|
|
593
|
-
iteration: ic.iteration,
|
|
594
|
-
unitType,
|
|
595
|
-
unitId,
|
|
596
|
-
consecutiveTimeouts: loopState.consecutiveFinalizeTimeouts,
|
|
597
|
-
});
|
|
598
|
-
ctx.ui.notify(`${stage === "pre" ? "postUnitPreVerification" : "postUnitPostVerification"} timed out after ${timeoutMs / 1000}s for ${unitType} ${unitId} (${loopState.consecutiveFinalizeTimeouts}/${MAX_FINALIZE_TIMEOUTS}) — pausing auto-mode for recovery.`, "warning");
|
|
599
|
-
await deps.pauseAuto(ctx, pi);
|
|
600
|
-
s.clearCurrentUnit();
|
|
601
|
-
clearCurrentPhase();
|
|
602
|
-
drainLogs();
|
|
603
|
-
return { action: "break", reason: progressKind };
|
|
604
|
-
}
|
|
605
|
-
export async function shouldSkipTerminalMilestoneCloseout(s, state, mid) {
|
|
606
|
-
const closeoutMilestoneId = mid ?? s.currentMilestoneId ?? state.lastCompletedMilestone?.id;
|
|
607
|
-
if (s.completionStopInProgress) {
|
|
608
|
-
return { skip: true, milestoneId: closeoutMilestoneId };
|
|
609
|
-
}
|
|
610
|
-
if (!closeoutMilestoneId) {
|
|
611
|
-
return { skip: false };
|
|
612
|
-
}
|
|
613
|
-
if (isDbAvailable())
|
|
614
|
-
refreshWorkflowDatabaseFromDisk();
|
|
615
|
-
const closeoutBasePath = s.originalBasePath || s.canonicalProjectRoot || s.basePath;
|
|
616
|
-
let closeoutMergePending = false;
|
|
617
|
-
if (getIsolationMode(closeoutBasePath) !== "none") {
|
|
618
|
-
try {
|
|
619
|
-
const blockers = await findUnmergedCompletedMilestones(closeoutBasePath);
|
|
620
|
-
closeoutMergePending = blockers.some((blocker) => blocker.milestoneId === closeoutMilestoneId);
|
|
621
|
-
}
|
|
622
|
-
catch {
|
|
623
|
-
// Fail open: without git/DB inspection we cannot safely treat closeout as done.
|
|
624
|
-
closeoutMergePending = true;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
const milestoneAlreadyClosedOut = isDbAvailable()
|
|
628
|
-
&& isClosedStatus(getMilestone(closeoutMilestoneId)?.status ?? "")
|
|
629
|
-
&& !closeoutMergePending;
|
|
630
|
-
if (milestoneAlreadyClosedOut) {
|
|
631
|
-
return { skip: true, milestoneId: closeoutMilestoneId };
|
|
632
|
-
}
|
|
633
|
-
return { skip: false, milestoneId: closeoutMilestoneId };
|
|
634
|
-
}
|
|
635
|
-
// ─── runPreDispatch ───────────────────────────────────────────────────────────
|
|
636
|
-
/**
|
|
637
|
-
* Phase 1: Pre-dispatch — resource guard, health gate, state derivation,
|
|
638
|
-
* milestone transition, terminal conditions.
|
|
639
|
-
* Returns break to exit the loop, or next with PreDispatchData on success.
|
|
640
|
-
*/
|
|
641
|
-
export async function runPreDispatch(ic, loopState) {
|
|
642
|
-
const { ctx, pi, s, deps, prefs } = ic;
|
|
643
|
-
const uokFlags = resolveUokFlags(prefs);
|
|
644
|
-
const runPreDispatchGate = async (input) => {
|
|
645
|
-
if (!uokFlags.gates)
|
|
646
|
-
return;
|
|
647
|
-
const gateRunner = new UokGateRunner();
|
|
648
|
-
gateRunner.register({
|
|
649
|
-
id: input.gateId,
|
|
650
|
-
type: input.gateType,
|
|
651
|
-
execute: async () => ({
|
|
652
|
-
outcome: input.outcome,
|
|
653
|
-
failureClass: input.failureClass,
|
|
654
|
-
rationale: input.rationale,
|
|
655
|
-
findings: input.findings ?? "",
|
|
656
|
-
}),
|
|
657
|
-
});
|
|
658
|
-
await gateRunner.run(input.gateId, {
|
|
659
|
-
basePath: s.basePath,
|
|
660
|
-
traceId: `pre-dispatch:${ic.flowId}`,
|
|
661
|
-
turnId: `iter-${ic.iteration}`,
|
|
662
|
-
milestoneId: input.milestoneId ?? s.currentMilestoneId ?? undefined,
|
|
663
|
-
unitType: "pre-dispatch",
|
|
664
|
-
unitId: `iter-${ic.iteration}`,
|
|
665
|
-
});
|
|
666
|
-
};
|
|
667
|
-
// Resource version guard
|
|
668
|
-
const staleMsg = deps.checkResourcesStale(s.resourceVersionOnStart);
|
|
669
|
-
if (staleMsg) {
|
|
670
|
-
await runPreDispatchGate({
|
|
671
|
-
gateId: "resource-version-guard",
|
|
672
|
-
gateType: "policy",
|
|
673
|
-
outcome: "fail",
|
|
674
|
-
failureClass: "policy",
|
|
675
|
-
rationale: "resource version guard blocked dispatch",
|
|
676
|
-
findings: staleMsg,
|
|
677
|
-
});
|
|
678
|
-
await deps.stopAuto(ctx, pi, staleMsg);
|
|
679
|
-
debugLog("autoLoop", { phase: "exit", reason: "resources-stale" });
|
|
680
|
-
return { action: "break", reason: "resources-stale" };
|
|
681
|
-
}
|
|
682
|
-
await runPreDispatchGate({
|
|
683
|
-
gateId: "resource-version-guard",
|
|
684
|
-
gateType: "policy",
|
|
685
|
-
outcome: "pass",
|
|
686
|
-
failureClass: "none",
|
|
687
|
-
rationale: "resource version guard passed",
|
|
688
|
-
});
|
|
689
|
-
deps.invalidateAllCaches();
|
|
690
|
-
s.lastPromptCharCount = undefined;
|
|
691
|
-
s.lastBaselineCharCount = undefined;
|
|
692
|
-
// Pre-dispatch health gate
|
|
693
|
-
try {
|
|
694
|
-
const expectedCurrentUnit = null;
|
|
695
|
-
const healthGate = await deps.preDispatchHealthGate(s.basePath);
|
|
696
|
-
if (healthGate.fixesApplied.length > 0) {
|
|
697
|
-
ctx.ui.notify(`Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`, "info");
|
|
698
|
-
}
|
|
699
|
-
if (!healthGate.proceed) {
|
|
700
|
-
await runPreDispatchGate({
|
|
701
|
-
gateId: "pre-dispatch-health-gate",
|
|
702
|
-
gateType: "execution",
|
|
703
|
-
outcome: "manual-attention",
|
|
704
|
-
failureClass: "manual-attention",
|
|
705
|
-
rationale: "pre-dispatch health gate blocked dispatch",
|
|
706
|
-
findings: healthGate.reason,
|
|
707
|
-
});
|
|
708
|
-
ctx.ui.notify(healthGate.reason || "Pre-dispatch health check failed — run /gsd doctor for details.", "error");
|
|
709
|
-
await deps.pauseAuto(ctx, pi, undefined, { expectedCurrentUnit });
|
|
710
|
-
debugLog("autoLoop", { phase: "exit", reason: "health-gate-failed" });
|
|
711
|
-
return { action: "break", reason: "health-gate-failed" };
|
|
712
|
-
}
|
|
713
|
-
await runPreDispatchGate({
|
|
714
|
-
gateId: "pre-dispatch-health-gate",
|
|
715
|
-
gateType: "execution",
|
|
716
|
-
outcome: "pass",
|
|
717
|
-
failureClass: "none",
|
|
718
|
-
rationale: "pre-dispatch health gate passed",
|
|
719
|
-
findings: healthGate.fixesApplied.length > 0 ? healthGate.fixesApplied.join(", ") : "",
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
catch (e) {
|
|
723
|
-
await runPreDispatchGate({
|
|
724
|
-
gateId: "pre-dispatch-health-gate",
|
|
725
|
-
gateType: "execution",
|
|
726
|
-
outcome: "manual-attention",
|
|
727
|
-
failureClass: "manual-attention",
|
|
728
|
-
rationale: "pre-dispatch health gate threw unexpectedly",
|
|
729
|
-
findings: String(e),
|
|
730
|
-
});
|
|
731
|
-
logWarning("engine", "Pre-dispatch health gate threw unexpectedly", { error: String(e) });
|
|
732
|
-
}
|
|
733
|
-
// Sync project root artifacts into worktree
|
|
734
|
-
if (s.originalBasePath &&
|
|
735
|
-
!isSamePathLocal(s.basePath, s.originalBasePath) &&
|
|
736
|
-
s.currentMilestoneId &&
|
|
737
|
-
s.scope) {
|
|
738
|
-
deps.worktreeProjection.projectRootToWorktree(s.scope);
|
|
739
|
-
}
|
|
740
|
-
// Derive state — use canonical project root so the cache key is stable
|
|
741
|
-
// across worktree↔project-root path-form alternation. See PR #5236
|
|
742
|
-
// (workspace handle infrastructure) and the Phase A pt 2 plan.
|
|
743
|
-
let state = await deps.deriveState(s.canonicalProjectRoot);
|
|
744
|
-
const { getDeepStageGate } = await import("../auto-dispatch.js");
|
|
745
|
-
const deepStageGate = getDeepStageGate(prefs, s.basePath);
|
|
746
|
-
const canRunDeepSetupGate = state.phase === "pre-planning" ||
|
|
747
|
-
state.phase === "needs-discussion" ||
|
|
748
|
-
state.phase === "planning";
|
|
749
|
-
if (canRunDeepSetupGate &&
|
|
750
|
-
(deepStageGate.status === "pending" || deepStageGate.status === "blocked")) {
|
|
751
|
-
debugLog("autoLoop", {
|
|
752
|
-
phase: "deep-project-stage-gate",
|
|
753
|
-
stage: deepStageGate.stage,
|
|
754
|
-
status: deepStageGate.status,
|
|
755
|
-
reason: deepStageGate.reason,
|
|
756
|
-
});
|
|
757
|
-
return {
|
|
758
|
-
action: "next",
|
|
759
|
-
data: {
|
|
760
|
-
state: {
|
|
761
|
-
...state,
|
|
762
|
-
phase: "pre-planning",
|
|
763
|
-
activeMilestone: null,
|
|
764
|
-
activeSlice: null,
|
|
765
|
-
activeTask: null,
|
|
766
|
-
nextAction: deepStageGate.reason,
|
|
767
|
-
},
|
|
768
|
-
mid: "PROJECT",
|
|
769
|
-
midTitle: "Project setup",
|
|
770
|
-
},
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
if (uokFlags.planV2 && shouldRunPlanV2Gate(state.phase)) {
|
|
774
|
-
let compiled = ensurePlanV2Graph(s.basePath, state);
|
|
775
|
-
if (isEmptyPlanV2GraphResult(compiled)) {
|
|
776
|
-
deps.invalidateAllCaches();
|
|
777
|
-
state = await deps.deriveState(s.canonicalProjectRoot);
|
|
778
|
-
compiled = shouldRunPlanV2Gate(state.phase)
|
|
779
|
-
? ensurePlanV2Graph(s.basePath, state)
|
|
780
|
-
: {
|
|
781
|
-
ok: true,
|
|
782
|
-
reason: "empty plan-v2 graph recovered by state rederive",
|
|
783
|
-
nodeCount: 0,
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
if (!compiled.ok) {
|
|
787
|
-
const reason = compiled.reason ?? "Plan v2 compilation failed";
|
|
788
|
-
if (isMissingFinalizedContextResult(compiled)) {
|
|
789
|
-
await runPreDispatchGate({
|
|
790
|
-
gateId: "plan-v2-gate",
|
|
791
|
-
gateType: "policy",
|
|
792
|
-
outcome: "pass",
|
|
793
|
-
failureClass: "none",
|
|
794
|
-
rationale: "plan v2 missing context recovery deferred to dispatch",
|
|
795
|
-
findings: reason,
|
|
796
|
-
milestoneId: state.activeMilestone?.id ?? undefined,
|
|
797
|
-
});
|
|
798
|
-
}
|
|
799
|
-
else {
|
|
800
|
-
await runPreDispatchGate({
|
|
801
|
-
gateId: "plan-v2-gate",
|
|
802
|
-
gateType: "policy",
|
|
803
|
-
outcome: "manual-attention",
|
|
804
|
-
failureClass: "manual-attention",
|
|
805
|
-
rationale: "plan v2 compile gate failed",
|
|
806
|
-
findings: reason,
|
|
807
|
-
milestoneId: state.activeMilestone?.id ?? undefined,
|
|
808
|
-
});
|
|
809
|
-
ctx.ui.notify(`Plan gate failed-closed: ${reason}\n\nIf this keeps happening, try: /gsd doctor heal`, "error");
|
|
810
|
-
await deps.pauseAuto(ctx, pi);
|
|
811
|
-
return { action: "break", reason: "plan-v2-gate-failed" };
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
if (compiled.ok) {
|
|
815
|
-
await runPreDispatchGate({
|
|
816
|
-
gateId: "plan-v2-gate",
|
|
817
|
-
gateType: "policy",
|
|
818
|
-
outcome: "pass",
|
|
819
|
-
failureClass: "none",
|
|
820
|
-
rationale: "plan v2 compile gate passed",
|
|
821
|
-
milestoneId: state.activeMilestone?.id ?? undefined,
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
deps.syncCmuxSidebar(prefs, state);
|
|
826
|
-
let mid = state.activeMilestone?.id;
|
|
827
|
-
let midTitle = state.activeMilestone?.title;
|
|
828
|
-
debugLog("autoLoop", {
|
|
829
|
-
phase: "state-derived",
|
|
830
|
-
iteration: ic.iteration,
|
|
831
|
-
mid,
|
|
832
|
-
statePhase: state.phase,
|
|
833
|
-
});
|
|
834
|
-
// ── Slice-level parallelism gate (#2340) ─────────────────────────────
|
|
835
|
-
// When slice_parallel is enabled, check if multiple slices are eligible
|
|
836
|
-
// for parallel execution. If so, dispatch them in parallel and stop the
|
|
837
|
-
// sequential loop. Workers are spawned via slice-parallel-orchestrator.ts.
|
|
838
|
-
if (prefs?.slice_parallel?.enabled &&
|
|
839
|
-
mid &&
|
|
840
|
-
!process.env.GSD_PARALLEL_WORKER &&
|
|
841
|
-
isDbAvailable()) {
|
|
842
|
-
try {
|
|
843
|
-
const projectRoot = _resolveDispatchGuardBasePath(s);
|
|
844
|
-
if (isSliceParallelActive(projectRoot)) {
|
|
845
|
-
ctx.ui.notify("Slice-parallel: workers are still running; waiting for completion before next dispatch.", "info");
|
|
846
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
847
|
-
return { action: "continue" };
|
|
848
|
-
}
|
|
849
|
-
const dbSlices = getMilestoneSlices(mid);
|
|
850
|
-
if (dbSlices.length > 0) {
|
|
851
|
-
const doneIds = new Set(dbSlices.filter(sl => sl.status === "complete" || sl.status === "done").map(sl => sl.id));
|
|
852
|
-
const sliceInputs = dbSlices.map(sl => ({
|
|
853
|
-
id: sl.id,
|
|
854
|
-
done: doneIds.has(sl.id),
|
|
855
|
-
depends: sl.depends ?? [],
|
|
856
|
-
}));
|
|
857
|
-
const eligible = getEligibleSlices(sliceInputs, doneIds);
|
|
858
|
-
if (eligible.length > 1) {
|
|
859
|
-
debugLog("autoLoop", {
|
|
860
|
-
phase: "slice-parallel-dispatch",
|
|
861
|
-
iteration: ic.iteration,
|
|
862
|
-
mid,
|
|
863
|
-
eligibleSlices: eligible.map(e => e.id),
|
|
864
|
-
});
|
|
865
|
-
ctx.ui.notify(`Slice-parallel: dispatching ${eligible.length} eligible slices for ${mid}.`, "info");
|
|
866
|
-
// ADR-017 #5707: reconcile before spawning so each worker doesn't
|
|
867
|
-
// independently race on the same drift. Failure aborts the spawn.
|
|
868
|
-
const spawnGate = await reconcileBeforeSpawn(projectRoot);
|
|
869
|
-
if (!spawnGate.ok) {
|
|
870
|
-
ctx.ui.notify(`Slice-parallel: aborting spawn — ${spawnGate.reason}`, "error");
|
|
871
|
-
return { action: "break", reason: `slice-parallel-reconciliation-failed: ${spawnGate.reason}` };
|
|
872
|
-
}
|
|
873
|
-
const result = await startSliceParallel(projectRoot, mid, eligible, {
|
|
874
|
-
maxWorkers: prefs.slice_parallel.max_workers ?? 2,
|
|
875
|
-
useExecutionGraph: uokFlags.executionGraph,
|
|
876
|
-
});
|
|
877
|
-
if (result.started.length > 0) {
|
|
878
|
-
ctx.ui.notify(`Slice-parallel: started ${result.started.length} worker(s): ${result.started.join(", ")}.`, "info");
|
|
879
|
-
return { action: "continue" };
|
|
880
|
-
}
|
|
881
|
-
if (result.errors.length > 0) {
|
|
882
|
-
const detail = result.errors
|
|
883
|
-
.map((err) => `${err.sid}: ${err.error}`)
|
|
884
|
-
.join("; ");
|
|
885
|
-
ctx.ui.notify(`Slice-parallel startup failed; falling back to sequential execution. ${detail}`, "warning");
|
|
886
|
-
}
|
|
887
|
-
// Fall through to sequential if no workers started
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
catch (err) {
|
|
892
|
-
debugLog("autoLoop", {
|
|
893
|
-
phase: "slice-parallel-check-error",
|
|
894
|
-
error: err instanceof Error ? err.message : String(err),
|
|
895
|
-
});
|
|
896
|
-
// Non-fatal — fall through to sequential dispatch
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
// ── Milestone transition ────────────────────────────────────────────
|
|
900
|
-
if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
|
|
901
|
-
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "milestone-transition", data: { from: s.currentMilestoneId, to: mid } });
|
|
902
|
-
ctx.ui.notify(`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`, "info");
|
|
903
|
-
deps.sendDesktopNotification("GSD", `Milestone ${s.currentMilestoneId} complete!`, "success", "milestone", basename(s.originalBasePath || s.basePath));
|
|
904
|
-
deps.logCmuxEvent(prefs, `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`, "success");
|
|
905
|
-
const vizPrefs = prefs;
|
|
906
|
-
if (vizPrefs?.auto_visualize) {
|
|
907
|
-
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
908
|
-
}
|
|
909
|
-
if (vizPrefs?.auto_report !== false) {
|
|
910
|
-
try {
|
|
911
|
-
await generateMilestoneReport(s, ctx, s.currentMilestoneId);
|
|
912
|
-
}
|
|
913
|
-
catch (err) {
|
|
914
|
-
ctx.ui.notify(`Report generation failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
// Reset dispatch counters for new milestone
|
|
918
|
-
s.unitDispatchCount.clear();
|
|
919
|
-
s.unitRecoveryCount.clear();
|
|
920
|
-
s.unitLifetimeDispatches.clear();
|
|
921
|
-
loopState.recentUnits.length = 0;
|
|
922
|
-
loopState.stuckRecoveryAttempts = 0;
|
|
923
|
-
persistStuckRecoveryAttempts(s, loopState);
|
|
924
|
-
// Worktree lifecycle on milestone transition — merge current, enter next.
|
|
925
|
-
// #2909 / #5538-followup: preflight stash + always-on postflight pop.
|
|
926
|
-
{
|
|
927
|
-
const stop = await _runMilestoneMergeOnceWithStashRestore(ic, s.currentMilestoneId);
|
|
928
|
-
if (stop)
|
|
929
|
-
return stop;
|
|
930
|
-
}
|
|
931
|
-
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
932
|
-
deps.invalidateAllCaches();
|
|
933
|
-
state = await deps.deriveState(s.canonicalProjectRoot);
|
|
934
|
-
mid = state.activeMilestone?.id;
|
|
935
|
-
midTitle = state.activeMilestone?.title;
|
|
936
|
-
if (mid) {
|
|
937
|
-
if (deps.getIsolationMode(s.basePath) !== "none") {
|
|
938
|
-
deps.captureIntegrationBranch(s.basePath, mid);
|
|
939
|
-
}
|
|
940
|
-
const enterResult = deps.lifecycle.enterMilestone(mid, ctx.ui);
|
|
941
|
-
if (!enterResult.ok) {
|
|
942
|
-
ctx.ui.notify(`Milestone transition stopped: failed to enter ${mid} (${enterResult.reason}).`, "error");
|
|
943
|
-
if (enterResult.reason === "lease-conflict") {
|
|
944
|
-
await deps.pauseAuto(ctx, pi);
|
|
945
|
-
}
|
|
946
|
-
return { action: "break", reason: "milestone-enter-failed" };
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
else {
|
|
950
|
-
// mid is undefined — no milestone to capture integration branch for
|
|
951
|
-
}
|
|
952
|
-
const pendingIds = state.registry
|
|
953
|
-
.filter((m) => m.status !== "complete" && m.status !== "parked")
|
|
954
|
-
.map((m) => m.id);
|
|
955
|
-
deps.pruneQueueOrder(s.basePath, pendingIds);
|
|
956
|
-
// Archive the old completed-units.json instead of wiping it (#2313).
|
|
957
|
-
try {
|
|
958
|
-
const completedKeysPath = join(gsdRoot(s.basePath), "completed-units.json");
|
|
959
|
-
if (existsSync(completedKeysPath) && s.currentMilestoneId) {
|
|
960
|
-
const archivePath = join(gsdRoot(s.basePath), `completed-units-${s.currentMilestoneId}.json`);
|
|
961
|
-
cpSync(completedKeysPath, archivePath);
|
|
962
|
-
}
|
|
963
|
-
atomicWriteSync(completedKeysPath, JSON.stringify([], null, 2));
|
|
964
|
-
}
|
|
965
|
-
catch (e) {
|
|
966
|
-
logWarning("engine", "Failed to archive completed-units on milestone transition", { error: String(e) });
|
|
967
|
-
}
|
|
968
|
-
// Rebuild STATE.md immediately so it reflects the new active milestone.
|
|
969
|
-
// This bypasses the 30-second throttle in the normal rebuild path —
|
|
970
|
-
// milestone transitions are rare and important enough to warrant an
|
|
971
|
-
// immediate write.
|
|
972
|
-
try {
|
|
973
|
-
await deps.rebuildState(s.basePath);
|
|
974
|
-
}
|
|
975
|
-
catch (e) {
|
|
976
|
-
logWarning("engine", "STATE.md rebuild failed after milestone transition", { error: String(e) });
|
|
977
|
-
}
|
|
978
|
-
// Re-project ROADMAP/PLAN markdown from the authoritative DB. Worktree DB
|
|
979
|
-
// reconciliation during merge can leave main-branch markdown stale relative
|
|
980
|
-
// to gsd.db (the 3M/3S/10T vs 3M/5S/16T drift class at /gsd startup).
|
|
981
|
-
try {
|
|
982
|
-
const { rebuildMarkdownProjectionsFromDb } = await import("../commands-maintenance.js");
|
|
983
|
-
await rebuildMarkdownProjectionsFromDb(s.canonicalProjectRoot);
|
|
984
|
-
if (s.basePath !== s.canonicalProjectRoot) {
|
|
985
|
-
await rebuildMarkdownProjectionsFromDb(s.basePath);
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
catch (e) {
|
|
989
|
-
logWarning("engine", "markdown projection rebuild failed after milestone transition", { error: String(e) });
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
if (mid) {
|
|
993
|
-
s.currentMilestoneId = mid;
|
|
994
|
-
deps.setActiveMilestoneId(s.basePath, mid);
|
|
995
|
-
}
|
|
996
|
-
// ── Terminal conditions ──────────────────────────────────────────────
|
|
997
|
-
if (state.phase === "complete") {
|
|
998
|
-
const closeoutSkip = await shouldSkipTerminalMilestoneCloseout(s, state, mid);
|
|
999
|
-
if (closeoutSkip.skip) {
|
|
1000
|
-
debugLog("autoLoop", { phase: "complete", reason: "milestone-already-closed", milestoneId: closeoutSkip.milestoneId });
|
|
1001
|
-
return { action: "break", reason: "milestone-complete" };
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
if (!mid) {
|
|
1005
|
-
if (s.currentUnit) {
|
|
1006
|
-
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
1007
|
-
}
|
|
1008
|
-
const incomplete = state.registry.filter((m) => m.status !== "complete" && m.status !== "parked");
|
|
1009
|
-
if (incomplete.length === 0 && state.registry.length > 0) {
|
|
1010
|
-
// All milestones complete — merge milestone branch before stopping.
|
|
1011
|
-
if (s.currentMilestoneId) {
|
|
1012
|
-
// #2909 / #5538-followup: preflight stash + always-on postflight pop.
|
|
1013
|
-
const stop = await _runMilestoneMergeOnceWithStashRestore(ic, s.currentMilestoneId);
|
|
1014
|
-
if (stop)
|
|
1015
|
-
return stop;
|
|
1016
|
-
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
1017
|
-
}
|
|
1018
|
-
const unmappedActive = countUnmappedActiveRequirements();
|
|
1019
|
-
const completionStopReason = formatCompletePhaseNextAction(unmappedActive);
|
|
1020
|
-
deps.sendDesktopNotification("GSD", unmappedActive > 0 ? "All milestones complete — requirements backlog remains" : "All milestones complete!", "success", "milestone", basename(s.originalBasePath || s.basePath));
|
|
1021
|
-
deps.logCmuxEvent(prefs, completionStopReason, "success");
|
|
1022
|
-
await deps.stopAuto(ctx, pi, completionStopReason, {
|
|
1023
|
-
completionWidget: {
|
|
1024
|
-
milestoneId: s.currentMilestoneId,
|
|
1025
|
-
milestoneTitle: midTitle,
|
|
1026
|
-
allMilestonesComplete: true,
|
|
1027
|
-
},
|
|
1028
|
-
});
|
|
1029
|
-
}
|
|
1030
|
-
else if (incomplete.length === 0 && state.registry.length === 0) {
|
|
1031
|
-
// Empty registry — no milestones visible, likely a path resolution bug
|
|
1032
|
-
const diag = `basePath=${s.basePath}, phase=${state.phase}`;
|
|
1033
|
-
ctx.ui.notify(`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`, "error");
|
|
1034
|
-
await deps.stopAuto(ctx, pi, `No milestones found — check basePath resolution`);
|
|
1035
|
-
}
|
|
1036
|
-
else if (state.phase === "blocked") {
|
|
1037
|
-
const blockedResumeMessage = formatBlockedResumeMessage(state.blockers);
|
|
1038
|
-
// Pause instead of hard-stop so the session is resumable with `/gsd auto`.
|
|
1039
|
-
// Hard-stop here was causing premature termination when slice dependencies
|
|
1040
|
-
// were temporarily unresolvable (e.g. after reassessment added new slices).
|
|
1041
|
-
await deps.pauseAuto(ctx, pi);
|
|
1042
|
-
ctx.ui.notify(blockedResumeMessage, "warning");
|
|
1043
|
-
deps.sendDesktopNotification("GSD", blockedResumeMessage, "warning", "attention", basename(s.originalBasePath || s.basePath));
|
|
1044
|
-
deps.logCmuxEvent(prefs, blockedResumeMessage, "warning");
|
|
1045
|
-
}
|
|
1046
|
-
else {
|
|
1047
|
-
const ids = incomplete.map((m) => m.id).join(", ");
|
|
1048
|
-
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
1049
|
-
ctx.ui.notify(`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`, "error");
|
|
1050
|
-
await deps.stopAuto(ctx, pi, `No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`);
|
|
1051
|
-
}
|
|
1052
|
-
debugLog("autoLoop", { phase: "exit", reason: "no-active-milestone" });
|
|
1053
|
-
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "no-active-milestone" } });
|
|
1054
|
-
return { action: "break", reason: "no-active-milestone" };
|
|
1055
|
-
}
|
|
1056
|
-
if (!midTitle) {
|
|
1057
|
-
midTitle = mid;
|
|
1058
|
-
ctx.ui.notify(`Milestone ${mid} has no title in roadmap — using ID as fallback.`, "warning");
|
|
1059
|
-
}
|
|
1060
|
-
// Mid-merge safety check
|
|
1061
|
-
const mergeReconcileResult = deps.reconcileMergeState(s.basePath, ctx);
|
|
1062
|
-
if (mergeReconcileResult === "blocked") {
|
|
1063
|
-
await deps.pauseAuto(ctx, pi);
|
|
1064
|
-
debugLog("autoLoop", { phase: "exit", reason: "merge-reconciliation-blocked" });
|
|
1065
|
-
return { action: "break", reason: "merge-reconciliation-blocked" };
|
|
1066
|
-
}
|
|
1067
|
-
if (mergeReconcileResult === "reconciled") {
|
|
1068
|
-
deps.invalidateAllCaches();
|
|
1069
|
-
state = await deps.deriveState(s.canonicalProjectRoot);
|
|
1070
|
-
mid = state.activeMilestone?.id;
|
|
1071
|
-
midTitle = state.activeMilestone?.title;
|
|
1072
|
-
}
|
|
1073
|
-
if (!mid || !midTitle) {
|
|
1074
|
-
const noMilestoneReason = !mid
|
|
1075
|
-
? "No active milestone after merge reconciliation"
|
|
1076
|
-
: `Milestone ${mid} has no title after reconciliation`;
|
|
1077
|
-
await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
|
|
1078
|
-
debugLog("autoLoop", {
|
|
1079
|
-
phase: "exit",
|
|
1080
|
-
reason: "no-milestone-after-reconciliation",
|
|
1081
|
-
});
|
|
1082
|
-
return { action: "break", reason: "no-milestone-after-reconciliation" };
|
|
1083
|
-
}
|
|
1084
|
-
// Terminal: complete
|
|
1085
|
-
if (state.phase === "complete") {
|
|
1086
|
-
// Milestone merge on complete (before closeout so branch state is clean).
|
|
1087
|
-
if (s.currentMilestoneId) {
|
|
1088
|
-
// #2909 / #5538-followup: preflight stash + always-on postflight pop.
|
|
1089
|
-
const stop = await _runMilestoneMergeOnceWithStashRestore(ic, s.currentMilestoneId);
|
|
1090
|
-
if (stop)
|
|
1091
|
-
return stop;
|
|
1092
|
-
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
1093
|
-
}
|
|
1094
|
-
deps.sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone", basename(s.originalBasePath || s.basePath));
|
|
1095
|
-
deps.logCmuxEvent(prefs, `Milestone ${mid} complete.`, "success");
|
|
1096
|
-
if (s.currentUnit) {
|
|
1097
|
-
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
1098
|
-
s.clearCurrentUnit();
|
|
1099
|
-
}
|
|
1100
|
-
await deps.stopAuto(ctx, pi, `Milestone ${mid} complete`, {
|
|
1101
|
-
completionWidget: {
|
|
1102
|
-
milestoneId: mid,
|
|
1103
|
-
milestoneTitle: midTitle,
|
|
1104
|
-
},
|
|
1105
|
-
});
|
|
1106
|
-
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
1107
|
-
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "milestone-complete", milestoneId: mid } });
|
|
1108
|
-
return { action: "break", reason: "milestone-complete" };
|
|
1109
|
-
}
|
|
1110
|
-
// Terminal: blocked — pause instead of hard-stop so the session is resumable.
|
|
1111
|
-
if (state.phase === "blocked") {
|
|
1112
|
-
const blockedResumeMessage = formatBlockedResumeMessage(state.blockers);
|
|
1113
|
-
if (s.currentUnit) {
|
|
1114
|
-
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
1115
|
-
}
|
|
1116
|
-
await deps.pauseAuto(ctx, pi);
|
|
1117
|
-
ctx.ui.notify(blockedResumeMessage, "warning");
|
|
1118
|
-
deps.sendDesktopNotification("GSD", blockedResumeMessage, "warning", "attention", basename(s.originalBasePath || s.basePath));
|
|
1119
|
-
deps.logCmuxEvent(prefs, blockedResumeMessage, "warning");
|
|
1120
|
-
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
1121
|
-
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "blocked", blockers: state.blockers } });
|
|
1122
|
-
return { action: "break", reason: "blocked" };
|
|
1123
|
-
}
|
|
1124
|
-
return { action: "next", data: { state, mid, midTitle } };
|
|
1125
|
-
}
|
|
1126
|
-
// ─── runDispatch ──────────────────────────────────────────────────────────────
|
|
1127
|
-
function isUnhandledPhaseWarning(dispatchResult) {
|
|
1128
|
-
return dispatchResult.action === "stop" &&
|
|
1129
|
-
dispatchResult.level === "warning" &&
|
|
1130
|
-
dispatchResult.matchedRule === "<no-match>" &&
|
|
1131
|
-
/^Unhandled phase "/.test(dispatchResult.reason);
|
|
1132
|
-
}
|
|
1133
|
-
/**
|
|
1134
|
-
* Phase 3: Dispatch resolution — resolve next unit, stuck detection, pre-dispatch hooks.
|
|
1135
|
-
* Returns break/continue to control the loop, or next with IterationData on success.
|
|
1136
|
-
*/
|
|
1137
|
-
export async function runDispatch(ic, preData, loopState) {
|
|
1138
|
-
const { ctx, pi, s, deps, prefs } = ic;
|
|
1139
|
-
const { state, mid, midTitle } = preData;
|
|
1140
|
-
const provider = ctx.model?.provider;
|
|
1141
|
-
const authMode = provider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
|
|
1142
|
-
? ctx.modelRegistry.getProviderAuthMode(provider)
|
|
1143
|
-
: undefined;
|
|
1144
|
-
// Use the baseline snapshot rather than the live active-tool set: a prior
|
|
1145
|
-
// unit's per-provider narrowing (hook overrides, Groq 128-tool cap, etc.)
|
|
1146
|
-
// can strip required MCP tools from the live set even though
|
|
1147
|
-
// selectAndApplyModel will restore them before the unit is dispatched.
|
|
1148
|
-
// Checking a stale-narrowed set causes false transport-preflight warnings
|
|
1149
|
-
// that repeat on every /gsd auto resume (#477 follow-up).
|
|
1150
|
-
const activeTools = getToolBaselineSnapshot(pi);
|
|
1151
|
-
const registeredTools = getRegisteredToolSnapshot(pi);
|
|
1152
|
-
// Deep planning intentionally keeps human checkpoints in plain chat. In
|
|
1153
|
-
// Claude Code/local MCP transports, structured question requests can be
|
|
1154
|
-
// cancelled outside the normal chat flow, which made approval gates easy to
|
|
1155
|
-
// skip or bury under tool output.
|
|
1156
|
-
const structuredQuestionsAvailable = prefs?.planning_depth === "deep"
|
|
1157
|
-
? "false"
|
|
1158
|
-
: supportsStructuredQuestions(activeTools, {
|
|
1159
|
-
authMode,
|
|
1160
|
-
baseUrl: ctx.model?.baseUrl,
|
|
1161
|
-
}) ? "true" : "false";
|
|
1162
|
-
debugLog("autoLoop", { phase: "dispatch-resolve", iteration: ic.iteration });
|
|
1163
|
-
let dispatchResult = await deps.resolveDispatch({
|
|
1164
|
-
basePath: s.basePath,
|
|
1165
|
-
mid,
|
|
1166
|
-
midTitle,
|
|
1167
|
-
state,
|
|
1168
|
-
prefs,
|
|
1169
|
-
session: s,
|
|
1170
|
-
structuredQuestionsAvailable,
|
|
1171
|
-
sessionContextWindow: ctx.model?.contextWindow,
|
|
1172
|
-
sessionProvider: ctx.model?.provider,
|
|
1173
|
-
modelRegistry: ctx.modelRegistry,
|
|
1174
|
-
activeTools,
|
|
1175
|
-
registeredTools,
|
|
1176
|
-
sessionBaseUrl: ctx.model?.baseUrl,
|
|
1177
|
-
sessionAuthMode: authMode,
|
|
1178
|
-
});
|
|
1179
|
-
if (isUnhandledPhaseWarning(dispatchResult)) {
|
|
1180
|
-
deps.invalidateAllCaches();
|
|
1181
|
-
const freshState = await deps.deriveState(s.canonicalProjectRoot);
|
|
1182
|
-
const freshMid = freshState.activeMilestone?.id ?? mid;
|
|
1183
|
-
const freshMidTitle = freshState.activeMilestone?.title ?? freshMid ?? midTitle;
|
|
1184
|
-
debugLog("autoLoop", {
|
|
1185
|
-
phase: "dispatch-unhandled-phase-retry",
|
|
1186
|
-
iteration: ic.iteration,
|
|
1187
|
-
stalePhase: state.phase,
|
|
1188
|
-
freshPhase: freshState.phase,
|
|
1189
|
-
});
|
|
1190
|
-
dispatchResult = await deps.resolveDispatch({
|
|
1191
|
-
basePath: s.basePath,
|
|
1192
|
-
mid: freshMid,
|
|
1193
|
-
midTitle: freshMidTitle,
|
|
1194
|
-
state: freshState,
|
|
1195
|
-
prefs,
|
|
1196
|
-
session: s,
|
|
1197
|
-
structuredQuestionsAvailable,
|
|
1198
|
-
sessionContextWindow: ctx.model?.contextWindow,
|
|
1199
|
-
sessionProvider: ctx.model?.provider,
|
|
1200
|
-
modelRegistry: ctx.modelRegistry,
|
|
1201
|
-
activeTools,
|
|
1202
|
-
registeredTools,
|
|
1203
|
-
sessionBaseUrl: ctx.model?.baseUrl,
|
|
1204
|
-
sessionAuthMode: authMode,
|
|
1205
|
-
});
|
|
1206
|
-
}
|
|
1207
|
-
if (dispatchResult.action === "stop") {
|
|
1208
|
-
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "dispatch-stop", rule: dispatchResult.matchedRule, data: { reason: dispatchResult.reason } });
|
|
1209
|
-
// Warning-level stops are recoverable human checkpoints (e.g. UAT verdict
|
|
1210
|
-
// gate) — pause instead of hard-stopping so the session is resumable with
|
|
1211
|
-
// `/gsd auto`. Error/info-level stops remain hard stops for infrastructure
|
|
1212
|
-
// failures and terminal conditions respectively.
|
|
1213
|
-
// See: https://github.com/open-gsd/gsd-pi/issues/2474
|
|
1214
|
-
if (dispatchResult.level === "warning") {
|
|
1215
|
-
ctx.ui.notify(dispatchResult.reason, "warning");
|
|
1216
|
-
await deps.pauseAuto(ctx, pi, {
|
|
1217
|
-
message: dispatchResult.reason,
|
|
1218
|
-
category: "unknown",
|
|
1219
|
-
});
|
|
1220
|
-
}
|
|
1221
|
-
else {
|
|
1222
|
-
await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
|
|
1223
|
-
}
|
|
1224
|
-
debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
|
|
1225
|
-
return { action: "break", reason: "dispatch-stop" };
|
|
1226
|
-
}
|
|
1227
|
-
if (dispatchResult.action !== "dispatch") {
|
|
1228
|
-
// Non-dispatch action (e.g. "skip") — re-derive state
|
|
1229
|
-
await new Promise((r) => setImmediate(r));
|
|
1230
|
-
return { action: "continue" };
|
|
1231
|
-
}
|
|
1232
|
-
let unitType = dispatchResult.unitType;
|
|
1233
|
-
let unitId = dispatchResult.unitId;
|
|
1234
|
-
let prompt = dispatchResult.prompt;
|
|
1235
|
-
let pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
1236
|
-
let dispatchState = state;
|
|
1237
|
-
let dispatchMid = mid;
|
|
1238
|
-
let dispatchMidTitle = midTitle;
|
|
1239
|
-
const pendingRetryDispatch = s.pendingVerificationRetryDispatch;
|
|
1240
|
-
if (pendingRetryDispatch) {
|
|
1241
|
-
unitType = pendingRetryDispatch.unitType;
|
|
1242
|
-
unitId = pendingRetryDispatch.unitId;
|
|
1243
|
-
prompt = pendingRetryDispatch.prompt;
|
|
1244
|
-
pauseAfterUatDispatch = pendingRetryDispatch.pauseAfterUatDispatch;
|
|
1245
|
-
dispatchState = pendingRetryDispatch.state;
|
|
1246
|
-
dispatchMid = pendingRetryDispatch.mid ?? mid;
|
|
1247
|
-
dispatchMidTitle = pendingRetryDispatch.midTitle ?? midTitle;
|
|
1248
|
-
s.pendingVerificationRetryDispatch = null;
|
|
1249
|
-
debugLog("autoLoop", {
|
|
1250
|
-
phase: "dispatch-pending-verification-retry",
|
|
1251
|
-
unitType,
|
|
1252
|
-
unitId,
|
|
1253
|
-
});
|
|
1254
|
-
}
|
|
1255
|
-
const alreadyClosedReason = getAlreadyClosedDispatchReason(unitType, unitId);
|
|
1256
|
-
if (alreadyClosedReason) {
|
|
1257
|
-
s.pendingVerificationRetry = null;
|
|
1258
|
-
loopState.recentUnits = [];
|
|
1259
|
-
loopState.stuckRecoveryAttempts = Math.max(loopState.stuckRecoveryAttempts, 1);
|
|
1260
|
-
deps.invalidateAllCaches();
|
|
1261
|
-
debugLog("autoLoop", {
|
|
1262
|
-
phase: "dispatch-skip-already-closed",
|
|
1263
|
-
unitType,
|
|
1264
|
-
unitId,
|
|
1265
|
-
reason: alreadyClosedReason,
|
|
1266
|
-
});
|
|
1267
|
-
deps.emitJournalEvent({
|
|
1268
|
-
ts: new Date().toISOString(),
|
|
1269
|
-
flowId: ic.flowId,
|
|
1270
|
-
seq: ic.nextSeq(),
|
|
1271
|
-
eventType: "guard-block",
|
|
1272
|
-
data: { unitType, unitId, reason: alreadyClosedReason },
|
|
1273
|
-
});
|
|
1274
|
-
ctx.ui.notify(`Skipping ${unitType} ${unitId}: ${alreadyClosedReason}.`, "info");
|
|
1275
|
-
await new Promise((r) => setImmediate(r));
|
|
1276
|
-
return { action: "continue" };
|
|
1277
|
-
}
|
|
1278
|
-
deps.emitJournalEvent({
|
|
1279
|
-
ts: new Date().toISOString(),
|
|
1280
|
-
flowId: ic.flowId,
|
|
1281
|
-
seq: ic.nextSeq(),
|
|
1282
|
-
eventType: "dispatch-match",
|
|
1283
|
-
rule: pendingRetryDispatch ? "verification-retry" : dispatchResult.matchedRule,
|
|
1284
|
-
data: { unitType, unitId },
|
|
1285
|
-
});
|
|
1286
|
-
// Resolve hooks and prior-slice gating before health/stuck accounting so
|
|
1287
|
-
// those checks run against the final dispatch unit.
|
|
1288
|
-
const preDispatchResult = deps.runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
|
|
1289
|
-
if (preDispatchResult.firedHooks.length > 0) {
|
|
1290
|
-
ctx.ui.notify(`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`, "info");
|
|
1291
|
-
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "pre-dispatch-hook", data: { firedHooks: preDispatchResult.firedHooks, action: preDispatchResult.action } });
|
|
1292
|
-
}
|
|
1293
|
-
if (preDispatchResult.action === "skip") {
|
|
1294
|
-
ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
|
|
1295
|
-
await new Promise((r) => setImmediate(r));
|
|
1296
|
-
return { action: "continue" };
|
|
1297
|
-
}
|
|
1298
|
-
if (preDispatchResult.action === "replace") {
|
|
1299
|
-
prompt = preDispatchResult.prompt ?? prompt;
|
|
1300
|
-
if (preDispatchResult.unitType)
|
|
1301
|
-
unitType = preDispatchResult.unitType;
|
|
1302
|
-
}
|
|
1303
|
-
else if (preDispatchResult.prompt) {
|
|
1304
|
-
prompt = preDispatchResult.prompt;
|
|
1305
|
-
}
|
|
1306
|
-
const guardBasePath = _resolveDispatchGuardBasePath(s);
|
|
1307
|
-
let mainBranch = "main";
|
|
1308
|
-
try {
|
|
1309
|
-
mainBranch = deps.getMainBranch(guardBasePath);
|
|
1310
|
-
}
|
|
1311
|
-
catch (err) {
|
|
1312
|
-
debugLog("autoLoop", { phase: "getMainBranch-failed", error: String(err) });
|
|
1313
|
-
}
|
|
1314
|
-
const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(guardBasePath, mainBranch, unitType, unitId);
|
|
1315
|
-
if (priorSliceBlocker) {
|
|
1316
|
-
await deps.stopAuto(ctx, pi, priorSliceBlocker);
|
|
1317
|
-
debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
|
|
1318
|
-
return { action: "break", reason: "prior-slice-blocker" };
|
|
1319
|
-
}
|
|
1320
|
-
const consecutiveDispatchBlocker = getConsecutiveDispatchBlocker(loopState, state.phase, unitType, unitId);
|
|
1321
|
-
if (consecutiveDispatchBlocker) {
|
|
1322
|
-
await deps.stopAuto(ctx, pi, consecutiveDispatchBlocker);
|
|
1323
|
-
debugLog("autoLoop", { phase: "exit", reason: "consecutive-dispatch-blocker" });
|
|
1324
|
-
return { action: "break", reason: "consecutive-dispatch-blocker" };
|
|
1325
|
-
}
|
|
1326
|
-
const worktreeSafetyBlock = await validateSourceWriteWorktreeSafety(ic, unitType, unitId, mid, "pre-dispatch");
|
|
1327
|
-
if (worktreeSafetyBlock)
|
|
1328
|
-
return worktreeSafetyBlock;
|
|
1329
|
-
// ── Sliding-window stuck detection with graduated recovery ──
|
|
1330
|
-
const derivedKey = `${unitType}/${unitId}`;
|
|
1331
|
-
// Always record this dispatch in the sliding window and run detection so
|
|
1332
|
-
// Rules 1/3/4 can catch retry loops with repeated failure content (#5719).
|
|
1333
|
-
// Rules 2/2b suppress legitimate retry backoff through the dispatch ledger.
|
|
1334
|
-
const latestDispatch = getLatestForUnit(derivedKey);
|
|
1335
|
-
const recentError = latestDispatch?.error_summary ?? undefined;
|
|
1336
|
-
loopState.recentUnits.push({ key: derivedKey, error: recentError });
|
|
1337
|
-
while (loopState.recentUnits.length > STUCK_WINDOW_SIZE) {
|
|
1338
|
-
loopState.recentUnits.shift();
|
|
1339
|
-
}
|
|
1340
|
-
const stuckSignal = detectStuck(loopState.recentUnits, {
|
|
1341
|
-
pendingRetry: !!s.pendingVerificationRetry,
|
|
1342
|
-
retryAttempt: s.pendingVerificationRetry?.attempt,
|
|
1343
|
-
});
|
|
1344
|
-
if (stuckSignal) {
|
|
1345
|
-
debugLog("autoLoop", {
|
|
1346
|
-
phase: "stuck-check",
|
|
1347
|
-
unitType,
|
|
1348
|
-
unitId,
|
|
1349
|
-
reason: stuckSignal.reason,
|
|
1350
|
-
recoveryAttempts: loopState.stuckRecoveryAttempts,
|
|
1351
|
-
});
|
|
1352
|
-
if (loopState.stuckRecoveryAttempts === 0) {
|
|
1353
|
-
// Level 1: try verifying the artifact, then cache invalidation + retry
|
|
1354
|
-
loopState.stuckRecoveryAttempts++;
|
|
1355
|
-
persistStuckRecoveryAttempts(s, loopState);
|
|
1356
|
-
const artifactExists = verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
1357
|
-
if (artifactExists) {
|
|
1358
|
-
debugLog("autoLoop", {
|
|
1359
|
-
phase: "stuck-recovery",
|
|
1360
|
-
level: 1,
|
|
1361
|
-
action: "artifact-found",
|
|
1362
|
-
});
|
|
1363
|
-
const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId, s.basePath);
|
|
1364
|
-
if (!recoveryDb.ok) {
|
|
1365
|
-
ctx.ui.notify(recoveryDb.fatal
|
|
1366
|
-
? `${recoveryDb.message} Pausing auto-mode for manual recovery.`
|
|
1367
|
-
: `${recoveryDb.message} Keeping stuck state for retry.`, "warning");
|
|
1368
|
-
if (recoveryDb.fatal) {
|
|
1369
|
-
await deps.pauseAuto(ctx, pi);
|
|
1370
|
-
return { action: "break", reason: recoveryDb.reason };
|
|
1371
|
-
}
|
|
1372
|
-
return { action: "continue" };
|
|
1373
|
-
}
|
|
1374
|
-
ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`, "info");
|
|
1375
|
-
deps.invalidateAllCaches();
|
|
1376
|
-
loopState.recentUnits.length = 0;
|
|
1377
|
-
return { action: "continue" };
|
|
1378
|
-
}
|
|
1379
|
-
ctx.ui.notify(`Stuck on ${unitType} ${unitId} (${stuckSignal.reason}). Invalidating caches and retrying.`, "warning");
|
|
1380
|
-
deps.invalidateAllCaches();
|
|
1381
|
-
}
|
|
1382
|
-
else {
|
|
1383
|
-
// Level 2: hard stop — genuinely stuck
|
|
1384
|
-
deps.invalidateAllCaches();
|
|
1385
|
-
const artifactExists = verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
1386
|
-
if (artifactExists) {
|
|
1387
|
-
debugLog("autoLoop", {
|
|
1388
|
-
phase: "stuck-recovery",
|
|
1389
|
-
level: 2,
|
|
1390
|
-
action: "artifact-found",
|
|
1391
|
-
});
|
|
1392
|
-
const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId, s.basePath);
|
|
1393
|
-
if (recoveryDb.ok) {
|
|
1394
|
-
ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk after cache invalidation. Continuing.`, "info");
|
|
1395
|
-
loopState.recentUnits.length = 0;
|
|
1396
|
-
return { action: "continue" };
|
|
1397
|
-
}
|
|
1398
|
-
ctx.ui.notify(recoveryDb.fatal
|
|
1399
|
-
? `${recoveryDb.message} Pausing auto-mode for manual recovery.`
|
|
1400
|
-
: `${recoveryDb.message} Stopping for manual recovery.`, "warning");
|
|
1401
|
-
if (recoveryDb.fatal) {
|
|
1402
|
-
await deps.pauseAuto(ctx, pi);
|
|
1403
|
-
return { action: "break", reason: recoveryDb.reason };
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
debugLog("autoLoop", {
|
|
1407
|
-
phase: "stuck-detected",
|
|
1408
|
-
unitType,
|
|
1409
|
-
unitId,
|
|
1410
|
-
reason: stuckSignal.reason,
|
|
1411
|
-
});
|
|
1412
|
-
const stuckDiag = diagnoseExpectedArtifact(unitType, unitId, s.basePath);
|
|
1413
|
-
const stuckRemediation = buildLoopRemediationSteps(unitType, unitId, s.basePath);
|
|
1414
|
-
const stuckParts = [`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}.`];
|
|
1415
|
-
if (stuckDiag)
|
|
1416
|
-
stuckParts.push(`Expected: ${stuckDiag}`);
|
|
1417
|
-
if (stuckRemediation)
|
|
1418
|
-
stuckParts.push(`To recover:\n${stuckRemediation}`);
|
|
1419
|
-
ctx.ui.notify(stuckParts.join(" "), "error");
|
|
1420
|
-
await deps.stopAuto(ctx, pi, `Stuck: ${stuckSignal.reason}`);
|
|
1421
|
-
return { action: "break", reason: "stuck-detected" };
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
else {
|
|
1425
|
-
// Progress detected — reset recovery counter
|
|
1426
|
-
if (loopState.stuckRecoveryAttempts > 0) {
|
|
1427
|
-
debugLog("autoLoop", {
|
|
1428
|
-
phase: "stuck-counter-reset",
|
|
1429
|
-
from: loopState.recentUnits[loopState.recentUnits.length - 2]?.key ?? "",
|
|
1430
|
-
to: derivedKey,
|
|
1431
|
-
});
|
|
1432
|
-
loopState.stuckRecoveryAttempts = 0;
|
|
1433
|
-
persistStuckRecoveryAttempts(s, loopState);
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
return {
|
|
1437
|
-
action: "next",
|
|
1438
|
-
data: {
|
|
1439
|
-
unitType, unitId, prompt, finalPrompt: prompt,
|
|
1440
|
-
pauseAfterUatDispatch,
|
|
1441
|
-
state: dispatchState, mid: dispatchMid, midTitle: dispatchMidTitle,
|
|
1442
|
-
isRetry: Boolean(pendingRetryDispatch), previousTier: undefined,
|
|
1443
|
-
hookModelOverride: preDispatchResult.model,
|
|
1444
|
-
},
|
|
1445
|
-
};
|
|
1446
|
-
}
|
|
1447
|
-
// ─── runGuards ────────────────────────────────────────────────────────────────
|
|
14
|
+
import { BUDGET_THRESHOLDS } from "./types.js";
|
|
15
|
+
// Re-export phase implementations.
|
|
16
|
+
export { runPreDispatch } from "./pre-dispatch.js";
|
|
17
|
+
export { runDispatch, getAlreadyClosedDispatchReason, isUnhandledPhaseWarning } from "./dispatch.js";
|
|
18
|
+
export { runUnitPhase, resetSessionTimeoutState, _classifyZeroToolProviderMessageForTest, resolveDispatchRecoveryAttempts, _shouldProceedWithInvalidRepoClassificationForTest, } from "./unit-phase.js";
|
|
19
|
+
export { runFinalize, failClosedOnFinalizeTimeout } from "./finalize.js";
|
|
20
|
+
export { closeoutAndStop, generateMilestoneReport, _runMilestoneMergeWithStashRestore, _runMilestoneMergeOnceWithStashRestore, stopOnPostflightRecoveryNeeded, restorePreflightStashOrStop, shouldSkipTerminalMilestoneCloseout, } from "./closeout.js";
|
|
21
|
+
// Re-export shared helpers.
|
|
22
|
+
export { persistStuckRecoveryAttempts, isSamePathLocal, isIsolatedWorktreeSession, _resolveReportBasePath, _resolveDispatchGuardBasePath, shouldRunPlanV2Gate, _resolveCurrentUnitStartedAtForTest, applyVerificationRetryPolicy, rememberRetryDispatch, emitCancelledUnitEnd, _buildCancelledUnitStopReason, _isPauseOriginCancelledResult, } from "./phase-helpers.js";
|
|
23
|
+
export { validateSourceWriteWorktreeSafety, formatWorktreeSafetyFailure, formatWorktreeSafetyStopReason, resolveEmptyWorktreeWithProjectContent, shouldDegradeEmptyWorktreeToProjectRoot, unitWritesSource, } from "./worktree-safety-phase.js";
|
|
1448
24
|
/**
|
|
1449
25
|
* Phase 2: Guards — stop directives, budget ceiling, context window, secrets re-check.
|
|
1450
26
|
* Returns break to exit the loop, or next to proceed to dispatch.
|
|
@@ -1467,7 +43,7 @@ export async function runGuards(ic, mid) {
|
|
|
1467
43
|
: `Stop directive: ${first.text}`;
|
|
1468
44
|
ctx.ui.notify(label, "warning");
|
|
1469
45
|
deps.sendDesktopNotification("GSD", label, "warning", "stop-directive", basename(s.originalBasePath || s.basePath));
|
|
1470
|
-
// Pause first —
|
|
46
|
+
// Pause first — Ensures auto-mode stops even if later steps fail
|
|
1471
47
|
await deps.pauseAuto(ctx, pi);
|
|
1472
48
|
// For backtrack captures, write the backtrack trigger after pausing
|
|
1473
49
|
if (isBacktrack) {
|
|
@@ -1616,934 +192,3 @@ export async function runGuards(ic, mid) {
|
|
|
1616
192
|
}
|
|
1617
193
|
return { action: "next", data: undefined };
|
|
1618
194
|
}
|
|
1619
|
-
// ─── runUnitPhase ─────────────────────────────────────────────────────────────
|
|
1620
|
-
/**
|
|
1621
|
-
* Phase 4: Unit execution — dispatch prompt, await agent_end, closeout, artifact verify.
|
|
1622
|
-
* Returns break or next with unitStartedAt for downstream phases.
|
|
1623
|
-
*/
|
|
1624
|
-
export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
1625
|
-
const { ctx, pi, s, deps, prefs } = ic;
|
|
1626
|
-
const { unitType, unitId, prompt, state, mid } = iterData;
|
|
1627
|
-
debugLog("autoLoop", {
|
|
1628
|
-
phase: "unit-execution",
|
|
1629
|
-
iteration: ic.iteration,
|
|
1630
|
-
unitType,
|
|
1631
|
-
unitId,
|
|
1632
|
-
});
|
|
1633
|
-
const worktreeSafetyBlock = await validateSourceWriteWorktreeSafety(ic, unitType, unitId, mid, "unit-execution");
|
|
1634
|
-
if (worktreeSafetyBlock)
|
|
1635
|
-
return worktreeSafetyBlock;
|
|
1636
|
-
// ── Project classification notice (#1833, #1843) ─────────────────────
|
|
1637
|
-
// Worktree Safety owns source-write root validity. Classification now only
|
|
1638
|
-
// shapes user/model guidance for valid roots.
|
|
1639
|
-
let projectClassification = null;
|
|
1640
|
-
if (s.basePath && unitType === "execute-task") {
|
|
1641
|
-
projectClassification = classifyProject(s.basePath);
|
|
1642
|
-
if (projectClassification.kind === "invalid-repo") {
|
|
1643
|
-
const msg = `Worktree health check failed: ${s.basePath} classified as invalid-repo (${projectClassification.reason}) — refusing to dispatch ${unitType} ${unitId}`;
|
|
1644
|
-
debugLog("runUnitPhase", { phase: "worktree-health-invalid-repo", basePath: s.basePath, classification: projectClassification });
|
|
1645
|
-
const hasGit = deps.existsSync(join(s.basePath, ".git"));
|
|
1646
|
-
if (_shouldProceedWithInvalidRepoClassificationForTest(projectClassification.reason, hasGit)) {
|
|
1647
|
-
ctx.ui.notify(`Warning: ${s.basePath} project classification could not confirm .git; assuming it has no project content yet — proceeding as greenfield project because worktree health reported .git present`, "warning");
|
|
1648
|
-
}
|
|
1649
|
-
else {
|
|
1650
|
-
ctx.ui.notify(msg, "error");
|
|
1651
|
-
await deps.stopAuto(ctx, pi, msg);
|
|
1652
|
-
return { action: "break", reason: "worktree-invalid" };
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
if (projectClassification.kind === "greenfield") {
|
|
1656
|
-
debugLog("runUnitPhase", { phase: "worktree-health-greenfield", basePath: s.basePath, classification: projectClassification });
|
|
1657
|
-
ctx.ui.notify(`Warning: ${s.basePath} has no project content yet — proceeding as greenfield project`, "warning");
|
|
1658
|
-
}
|
|
1659
|
-
else if (projectClassification.kind === "untyped-existing") {
|
|
1660
|
-
debugLog("runUnitPhase", { phase: "worktree-health-untyped-existing", basePath: s.basePath, classification: projectClassification });
|
|
1661
|
-
ctx.ui.notify(`Notice: ${s.basePath} has existing project content but no recognized tooling markers — using generic file-level workflow guidance`, "info");
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
// Detect retry and capture previous tier for escalation
|
|
1665
|
-
const isRetry = !!(s.currentUnit &&
|
|
1666
|
-
s.currentUnit.type === unitType &&
|
|
1667
|
-
s.currentUnit.id === unitId);
|
|
1668
|
-
const previousTier = s.currentUnitRouting?.tier;
|
|
1669
|
-
const dispatchKey = `${unitType}/${unitId}`;
|
|
1670
|
-
const nextDispatchCount = (s.unitDispatchCount.get(dispatchKey) ?? 0) + 1;
|
|
1671
|
-
// Status bar (widget + preconditions deferred until after model selection — see #2899)
|
|
1672
|
-
setAutoActiveStatus(ctx, s.stepMode ? "next" : "auto");
|
|
1673
|
-
if (mid)
|
|
1674
|
-
deps.updateSliceProgressCache(s.basePath, mid, state.activeSlice?.id);
|
|
1675
|
-
// ── Safety harness: reset evidence + create checkpoint ──
|
|
1676
|
-
const safetyConfig = resolveSafetyHarnessConfig(prefs?.safety_harness);
|
|
1677
|
-
if (safetyConfig.enabled && safetyConfig.evidence_collection) {
|
|
1678
|
-
resetEvidence();
|
|
1679
|
-
// Restore persisted evidence so session-restart resumes don't produce
|
|
1680
|
-
// false-positive "no bash calls" warnings (Bug #4385).
|
|
1681
|
-
if (s.basePath && unitType === "execute-task") {
|
|
1682
|
-
const { milestone: eMid, slice: eSid, task: eTid } = parseUnitId(unitId);
|
|
1683
|
-
if (eMid && eSid && eTid) {
|
|
1684
|
-
loadEvidenceFromDisk(s.basePath, eMid, eSid, eTid);
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
// Only checkpoint code-executing units (not lifecycle/planning units)
|
|
1689
|
-
if (safetyConfig.enabled && safetyConfig.checkpoints && unitType === "execute-task") {
|
|
1690
|
-
s.checkpointSha = createCheckpoint(s.basePath, unitId);
|
|
1691
|
-
if (s.checkpointSha) {
|
|
1692
|
-
debugLog("runUnitPhase", { phase: "checkpoint-created", unitId, sha: s.checkpointSha.slice(0, 8) });
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
// Prompt injection
|
|
1696
|
-
let finalPrompt = prompt;
|
|
1697
|
-
if (unitType === "execute-task") {
|
|
1698
|
-
projectClassification ??= classifyProject(s.basePath);
|
|
1699
|
-
if (projectClassification.kind === "untyped-existing") {
|
|
1700
|
-
const samples = projectClassification.contentFiles.slice(0, 8).join(", ") || "project files";
|
|
1701
|
-
finalPrompt +=
|
|
1702
|
-
"\n\n**Project classification:** Existing untyped project. No recognized build/tooling markers were detected, " +
|
|
1703
|
-
"so use generic file-level workflow guidance. Task plans and completion summaries must list every concrete " +
|
|
1704
|
-
`project file changed in \`files\` or \`expected_output\`. Detected content sample: ${samples}.`;
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
if (s.pendingVerificationRetry && s.pendingVerificationRetry.unitId === unitId) {
|
|
1708
|
-
const retryCtx = s.pendingVerificationRetry;
|
|
1709
|
-
s.pendingVerificationRetry = null;
|
|
1710
|
-
const capped = retryCtx.failureContext.length > MAX_RECOVERY_CHARS
|
|
1711
|
-
? retryCtx.failureContext.slice(0, MAX_RECOVERY_CHARS) +
|
|
1712
|
-
"\n\n[...failure context truncated]"
|
|
1713
|
-
: retryCtx.failureContext;
|
|
1714
|
-
finalPrompt = `**VERIFICATION FAILED — AUTO-FIX ATTEMPT ${retryCtx.attempt}**\n\nThe verification gate ran after your previous attempt and found failures. Fix these issues before completing the task.\n\n${capped}\n\n---\n\n${finalPrompt}`;
|
|
1715
|
-
}
|
|
1716
|
-
if (s.pendingCrashRecovery) {
|
|
1717
|
-
const capped = s.pendingCrashRecovery.length > MAX_RECOVERY_CHARS
|
|
1718
|
-
? s.pendingCrashRecovery.slice(0, MAX_RECOVERY_CHARS) +
|
|
1719
|
-
"\n\n[...recovery briefing truncated to prevent memory exhaustion]"
|
|
1720
|
-
: s.pendingCrashRecovery;
|
|
1721
|
-
finalPrompt = `${capped}\n\n---\n\n${finalPrompt}`;
|
|
1722
|
-
s.pendingCrashRecovery = null;
|
|
1723
|
-
}
|
|
1724
|
-
else if (nextDispatchCount > 1) {
|
|
1725
|
-
const diagnostic = deps.getDeepDiagnostic(s.basePath);
|
|
1726
|
-
if (diagnostic) {
|
|
1727
|
-
const cappedDiag = diagnostic.length > MAX_RECOVERY_CHARS
|
|
1728
|
-
? diagnostic.slice(0, MAX_RECOVERY_CHARS) +
|
|
1729
|
-
"\n\n[...diagnostic truncated to prevent memory exhaustion]"
|
|
1730
|
-
: diagnostic;
|
|
1731
|
-
const retryInstruction = unitType === "execute-task"
|
|
1732
|
-
? "The required artifact is `T##-SUMMARY.md`. Do NOT manually write this file. Call `gsd_task_complete` with `milestoneId`, `sliceId`, `taskId`, and the required completion fields. Do not re-run implementation work — call the tool."
|
|
1733
|
-
: "Fix whatever went wrong and make sure you write the required file this time.";
|
|
1734
|
-
finalPrompt = `**RETRY — your previous attempt did not produce the required artifact.**\n\nDiagnostic from previous attempt:\n${cappedDiag}\n\n${retryInstruction}\n\n---\n\n${finalPrompt}`;
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
// Prompt char measurement
|
|
1738
|
-
s.lastPromptCharCount = finalPrompt.length;
|
|
1739
|
-
s.lastBaselineCharCount = undefined;
|
|
1740
|
-
if (deps.isDbAvailable()) {
|
|
1741
|
-
try {
|
|
1742
|
-
const { inlineGsdRootFile } = await importExtensionModule(import.meta.url, "../auto-prompts.js");
|
|
1743
|
-
const [decisionsContent, requirementsContent, projectContent] = await Promise.all([
|
|
1744
|
-
inlineGsdRootFile(s.basePath, "decisions.md", "Decisions"),
|
|
1745
|
-
inlineGsdRootFile(s.basePath, "requirements.md", "Requirements"),
|
|
1746
|
-
inlineGsdRootFile(s.basePath, "project.md", "Project"),
|
|
1747
|
-
]);
|
|
1748
|
-
s.lastBaselineCharCount =
|
|
1749
|
-
(decisionsContent?.length ?? 0) +
|
|
1750
|
-
(requirementsContent?.length ?? 0) +
|
|
1751
|
-
(projectContent?.length ?? 0);
|
|
1752
|
-
}
|
|
1753
|
-
catch (e) {
|
|
1754
|
-
logWarning("engine", "Baseline char count measurement failed", { error: String(e) });
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
// Cache-optimize prompt section ordering
|
|
1758
|
-
try {
|
|
1759
|
-
finalPrompt = deps.reorderForCaching(finalPrompt);
|
|
1760
|
-
}
|
|
1761
|
-
catch (reorderErr) {
|
|
1762
|
-
const msg = reorderErr instanceof Error ? reorderErr.message : String(reorderErr);
|
|
1763
|
-
logWarning("engine", "Prompt reorder failed", { error: msg });
|
|
1764
|
-
}
|
|
1765
|
-
// Select and apply model (with tier escalation on retry — normal units only)
|
|
1766
|
-
const prevUnitRouting = s.currentUnitRouting;
|
|
1767
|
-
const prevUnitModel = s.currentUnitModel;
|
|
1768
|
-
const prevDispatchedModelId = s.currentDispatchedModelId;
|
|
1769
|
-
const prevSessionModel = ctx.model;
|
|
1770
|
-
const prevSessionThinkingLevel = pi.getThinkingLevel();
|
|
1771
|
-
const modelResult = await deps.selectAndApplyModel(ctx, pi, unitType, unitId, s.basePath, prefs, s.verbose, s.autoModeStartModel, sidecarItem ? undefined : { isRetry, previousTier }, undefined, s.manualSessionModelOverride, s.autoModeStartThinkingLevel);
|
|
1772
|
-
s.currentUnitRouting =
|
|
1773
|
-
modelResult.routing;
|
|
1774
|
-
s.currentUnitModel =
|
|
1775
|
-
modelResult.appliedModel;
|
|
1776
|
-
// Apply sidecar/pre-dispatch hook model override (takes priority over standard model selection)
|
|
1777
|
-
const hookModelOverride = sidecarItem?.model ?? iterData.hookModelOverride;
|
|
1778
|
-
if (hookModelOverride) {
|
|
1779
|
-
const availableModels = ctx.modelRegistry.getAvailable();
|
|
1780
|
-
const match = deps.resolveModelId(hookModelOverride, availableModels, ctx.model?.provider);
|
|
1781
|
-
if (match) {
|
|
1782
|
-
const ok = await pi.setModel(match, { persist: false });
|
|
1783
|
-
if (ok) {
|
|
1784
|
-
// Apply the per-phase reasoning effort selectAndApplyModel resolved for
|
|
1785
|
-
// this unit — not the auto-start session snapshot — but route it through
|
|
1786
|
-
// the same floor + capability-clamp pipeline against the *hook* model
|
|
1787
|
-
// (ADR-026). The hook override can pick a different model family than the
|
|
1788
|
-
// one selectAndApplyModel clamped against, so re-clamping here prevents
|
|
1789
|
-
// sending an unsupported level; the floor fills in when no phase level
|
|
1790
|
-
// resolved so a hook-overridden execute-task still meets the floor.
|
|
1791
|
-
const hookThinkingBase = modelResult.appliedThinkingLevel
|
|
1792
|
-
?? floorThinkingLevelForUnit(unitType, s.autoModeStartThinkingLevel);
|
|
1793
|
-
applyThinkingLevelForModel(pi, hookThinkingBase, match, ctx);
|
|
1794
|
-
s.currentUnitModel = match;
|
|
1795
|
-
ctx.ui.notify(`Hook model override: ${match.provider}/${match.id}`, "info");
|
|
1796
|
-
}
|
|
1797
|
-
else {
|
|
1798
|
-
ctx.ui.notify(`Hook model "${hookModelOverride}" found but setModel failed. Using default.`, "warning");
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
else {
|
|
1802
|
-
ctx.ui.notify(`Hook model "${hookModelOverride}" not found in available models. Falling back to current session model. ` +
|
|
1803
|
-
`Ensure the model is defined in models.json and has auth configured.`, "warning");
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
// Store the final dispatched model ID so the dashboard can read it (#2899).
|
|
1807
|
-
// This accounts for hook model overrides applied after selectAndApplyModel.
|
|
1808
|
-
s.currentDispatchedModelId = s.currentUnitModel
|
|
1809
|
-
? `${s.currentUnitModel.provider ?? ""}/${s.currentUnitModel.id ?? ""}`
|
|
1810
|
-
: null;
|
|
1811
|
-
const compatibilityError = getUnitWorkflowDispatchReadinessError({
|
|
1812
|
-
provider: s.currentUnitModel?.provider ?? ctx.model?.provider,
|
|
1813
|
-
projectRoot: s.basePath,
|
|
1814
|
-
surface: "auto-mode",
|
|
1815
|
-
unitType,
|
|
1816
|
-
authMode: s.currentUnitModel?.provider
|
|
1817
|
-
? ctx.modelRegistry.getProviderAuthMode(s.currentUnitModel.provider)
|
|
1818
|
-
: ctx.model?.provider
|
|
1819
|
-
? ctx.modelRegistry.getProviderAuthMode(ctx.model.provider)
|
|
1820
|
-
: undefined,
|
|
1821
|
-
baseUrl: s.currentUnitModel?.baseUrl ?? ctx.model?.baseUrl,
|
|
1822
|
-
activeTools: typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [],
|
|
1823
|
-
});
|
|
1824
|
-
const workflowMcpPrepModel = s.currentUnitModel;
|
|
1825
|
-
if (compatibilityError) {
|
|
1826
|
-
s.currentUnitRouting = prevUnitRouting;
|
|
1827
|
-
s.currentUnitModel = prevUnitModel;
|
|
1828
|
-
s.currentDispatchedModelId = prevDispatchedModelId;
|
|
1829
|
-
if (s.checkpointSha) {
|
|
1830
|
-
cleanupCheckpoint(s.basePath, unitId);
|
|
1831
|
-
s.checkpointSha = null;
|
|
1832
|
-
}
|
|
1833
|
-
if (prevSessionModel) {
|
|
1834
|
-
const ok = await pi.setModel(prevSessionModel, { persist: false });
|
|
1835
|
-
if (!ok) {
|
|
1836
|
-
ctx.ui.notify("Failed to restore previous session model after compatibility check failure.", "warning");
|
|
1837
|
-
}
|
|
1838
|
-
if (prevSessionThinkingLevel) {
|
|
1839
|
-
pi.setThinkingLevel(prevSessionThinkingLevel);
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
const workflowMcpPrep = prepareWorkflowMcpForProject(ctx, s.basePath, workflowMcpPrepModel);
|
|
1843
|
-
if (workflowMcpPrep && workflowMcpPrep.status !== "unchanged") {
|
|
1844
|
-
const pauseMsg = "GSD workflow MCP config has been written. Restart Claude Code (or reload MCP servers), then run /gsd auto to continue.";
|
|
1845
|
-
ctx.ui.notify(pauseMsg, "warning");
|
|
1846
|
-
await deps.pauseAuto(ctx, pi, {
|
|
1847
|
-
category: "provider",
|
|
1848
|
-
isTransient: true,
|
|
1849
|
-
message: pauseMsg,
|
|
1850
|
-
});
|
|
1851
|
-
return { action: "break", reason: "workflow-capability" };
|
|
1852
|
-
}
|
|
1853
|
-
ctx.ui.notify(compatibilityError, "error");
|
|
1854
|
-
await deps.stopAuto(ctx, pi, compatibilityError);
|
|
1855
|
-
return { action: "break", reason: "workflow-capability" };
|
|
1856
|
-
}
|
|
1857
|
-
// Scope workflow-logger buffer to this unit so post-finalize drains are
|
|
1858
|
-
// per-unit. Without this, the module-level _buffer accumulates across every
|
|
1859
|
-
// unit in the same Node process (see workflow-logger.ts module header).
|
|
1860
|
-
_resetLogs();
|
|
1861
|
-
const unitStartedAt = Date.now();
|
|
1862
|
-
s.unitDispatchCount.set(dispatchKey, nextDispatchCount);
|
|
1863
|
-
s.setCurrentUnit({ type: unitType, id: unitId, startedAt: unitStartedAt, workspaceRoot: s.basePath });
|
|
1864
|
-
if (unitType === "execute-task") {
|
|
1865
|
-
const { milestone, slice, task } = parseUnitId(unitId);
|
|
1866
|
-
if (milestone && slice && task && isDbAvailable()) {
|
|
1867
|
-
try {
|
|
1868
|
-
const taskRow = getTask(milestone, slice, task);
|
|
1869
|
-
if (taskRow)
|
|
1870
|
-
s.sourceObservations.observePlanTask(taskRow);
|
|
1871
|
-
}
|
|
1872
|
-
catch (err) {
|
|
1873
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1874
|
-
logWarning("prompt", `failed to preload source observations for ${unitId}: ${message}`);
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
s.rootWriteBaseline = isIsolatedWorktreeSession(s)
|
|
1879
|
-
? captureRootDirtySnapshot(s.originalBasePath)
|
|
1880
|
-
: null;
|
|
1881
|
-
s.lastGitActionFailure = null;
|
|
1882
|
-
s.lastGitActionStatus = null;
|
|
1883
|
-
s.lastUnitAgentEndMessages = null;
|
|
1884
|
-
setCurrentPhase(unitType, {
|
|
1885
|
-
basePath: s.basePath,
|
|
1886
|
-
traceId: ic.flowId,
|
|
1887
|
-
turnId: `iter-${ic.iteration}`,
|
|
1888
|
-
causedBy: "unit-start",
|
|
1889
|
-
});
|
|
1890
|
-
s.lastToolInvocationError = null; // #2883: clear stale error from previous unit
|
|
1891
|
-
if (nextDispatchCount <= 1) {
|
|
1892
|
-
s.toolUnavailableRetries = 0;
|
|
1893
|
-
}
|
|
1894
|
-
const unitStartSeq = ic.nextSeq();
|
|
1895
|
-
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } });
|
|
1896
|
-
deps.captureAvailableSkills();
|
|
1897
|
-
writeUnitRuntimeRecord(s.basePath, unitType, unitId, unitStartedAt, {
|
|
1898
|
-
phase: "dispatched",
|
|
1899
|
-
wrapupWarningSent: false,
|
|
1900
|
-
timeoutAt: null,
|
|
1901
|
-
lastProgressAt: unitStartedAt,
|
|
1902
|
-
progressCount: 0,
|
|
1903
|
-
lastProgressKind: "dispatch",
|
|
1904
|
-
recoveryAttempts: resolveDispatchRecoveryAttempts(s.unitRecoveryCount, unitType, unitId),
|
|
1905
|
-
});
|
|
1906
|
-
// Progress widget + preconditions — deferred to after model selection so the
|
|
1907
|
-
// widget's first render tick shows the correct model (#2899).
|
|
1908
|
-
deps.updateProgressWidget(ctx, unitType, unitId, state);
|
|
1909
|
-
deps.ensurePreconditions(unitType, unitId, s.basePath, state);
|
|
1910
|
-
// Start unit supervision
|
|
1911
|
-
deps.clearUnitTimeout();
|
|
1912
|
-
deps.startUnitSupervision({
|
|
1913
|
-
s,
|
|
1914
|
-
ctx,
|
|
1915
|
-
pi,
|
|
1916
|
-
unitType,
|
|
1917
|
-
unitId,
|
|
1918
|
-
prefs,
|
|
1919
|
-
buildSnapshotOpts: () => deps.buildSnapshotOpts(unitType, unitId),
|
|
1920
|
-
buildRecoveryContext: () => ({
|
|
1921
|
-
basePath: s.basePath,
|
|
1922
|
-
verbose: s.verbose,
|
|
1923
|
-
currentUnitStartedAt: s.currentUnit?.startedAt ?? Date.now(),
|
|
1924
|
-
unitRecoveryCount: s.unitRecoveryCount,
|
|
1925
|
-
}),
|
|
1926
|
-
pauseAuto: deps.pauseAuto,
|
|
1927
|
-
});
|
|
1928
|
-
// Write preliminary lock (no session path yet — runUnit creates a new session).
|
|
1929
|
-
// Crash recovery can still identify the in-flight unit from this lock.
|
|
1930
|
-
deps.writeLock(deps.lockBase(), unitType, unitId);
|
|
1931
|
-
debugLog("autoLoop", {
|
|
1932
|
-
phase: "runUnit-start",
|
|
1933
|
-
iteration: ic.iteration,
|
|
1934
|
-
unitType,
|
|
1935
|
-
unitId,
|
|
1936
|
-
});
|
|
1937
|
-
const pausedBeforeRun = s.paused;
|
|
1938
|
-
const unitResult = await runUnit(ctx, pi, s, unitType, unitId, finalPrompt);
|
|
1939
|
-
s.lastUnitAgentEndMessages = unitResult.event?.messages ?? null;
|
|
1940
|
-
debugLog("autoLoop", {
|
|
1941
|
-
phase: "runUnit-end",
|
|
1942
|
-
iteration: ic.iteration,
|
|
1943
|
-
unitType,
|
|
1944
|
-
unitId,
|
|
1945
|
-
status: unitResult.status,
|
|
1946
|
-
});
|
|
1947
|
-
if (unitResult.status === "completed" &&
|
|
1948
|
-
s.currentUnit &&
|
|
1949
|
-
(unitResult.event?.messages?.length ?? 0) === 0 &&
|
|
1950
|
-
isSuspiciousGhostCompletion(ctx, unitResult.requestDispatchedAt ?? s.currentUnit.startedAt)) {
|
|
1951
|
-
const message = `${unitType} ${unitId} completed without assistant output or tool calls; treating as a stale ghost completion.`;
|
|
1952
|
-
debugLog("autoLoop", {
|
|
1953
|
-
phase: "ghost-completion",
|
|
1954
|
-
iteration: ic.iteration,
|
|
1955
|
-
unitType,
|
|
1956
|
-
unitId,
|
|
1957
|
-
elapsedMs: Date.now() - (unitResult.requestDispatchedAt ?? s.currentUnit.startedAt),
|
|
1958
|
-
});
|
|
1959
|
-
logWarning("engine", message);
|
|
1960
|
-
ctx.ui.notify(`${message} Pausing auto-mode before closeout side effects.`, "warning");
|
|
1961
|
-
await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, {
|
|
1962
|
-
message,
|
|
1963
|
-
category: "unknown",
|
|
1964
|
-
isTransient: true,
|
|
1965
|
-
});
|
|
1966
|
-
s.clearCurrentUnit();
|
|
1967
|
-
await deps.pauseAuto(ctx, pi);
|
|
1968
|
-
return { action: "break", reason: "ghost-completion" };
|
|
1969
|
-
}
|
|
1970
|
-
// Now that runUnit has called newSession(), the session file path is correct.
|
|
1971
|
-
const sessionFile = deps.getSessionFile(ctx);
|
|
1972
|
-
deps.updateSessionLock(deps.lockBase(), unitType, unitId, sessionFile);
|
|
1973
|
-
deps.writeLock(deps.lockBase(), unitType, unitId, sessionFile);
|
|
1974
|
-
// Tag the most recent window entry with error info for stuck detection
|
|
1975
|
-
const lastEntry = loopState.recentUnits[loopState.recentUnits.length - 1];
|
|
1976
|
-
if (lastEntry) {
|
|
1977
|
-
if (unitResult.errorContext) {
|
|
1978
|
-
lastEntry.error = `${unitResult.errorContext.category}:${unitResult.errorContext.message}`.slice(0, 200);
|
|
1979
|
-
}
|
|
1980
|
-
else if (unitResult.status === "error" || unitResult.status === "cancelled") {
|
|
1981
|
-
lastEntry.error = `${unitResult.status}:${unitType}/${unitId}`;
|
|
1982
|
-
}
|
|
1983
|
-
else if (unitResult.event?.messages?.length) {
|
|
1984
|
-
const lastMsg = unitResult.event.messages[unitResult.event.messages.length - 1];
|
|
1985
|
-
const msgStr = typeof lastMsg === "string" ? lastMsg : JSON.stringify(lastMsg);
|
|
1986
|
-
if (/error|fail|exception/i.test(msgStr)) {
|
|
1987
|
-
lastEntry.error = msgStr.slice(0, 200);
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
|
-
if (unitResult.status === "cancelled") {
|
|
1992
|
-
if (_isPauseOriginCancelledResult(s.paused, unitResult.errorContext)) {
|
|
1993
|
-
if (!pausedBeforeRun) {
|
|
1994
|
-
const pauseContext = {
|
|
1995
|
-
message: "Auto-mode paused during unit setup",
|
|
1996
|
-
category: "aborted",
|
|
1997
|
-
isTransient: true,
|
|
1998
|
-
};
|
|
1999
|
-
await deps.autoCommitUnit?.(s.basePath, unitType, unitId, ctx);
|
|
2000
|
-
await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, pauseContext);
|
|
2001
|
-
return { action: "break", reason: "pause-during-setup" };
|
|
2002
|
-
}
|
|
2003
|
-
debugLog("autoLoop", { phase: "cancelled-after-pause", unitType, unitId });
|
|
2004
|
-
return { action: "break", reason: "paused" };
|
|
2005
|
-
}
|
|
2006
|
-
const errorCategory = unitResult.errorContext?.category;
|
|
2007
|
-
// Provider-error pause: agent_end recovery normally pauses before this
|
|
2008
|
-
// branch. Provider readiness failures happen before dispatch, so pause here
|
|
2009
|
-
// if nothing upstream already did.
|
|
2010
|
-
if (errorCategory === "provider") {
|
|
2011
|
-
if (!s.paused) {
|
|
2012
|
-
const detail = unitResult.errorContext?.message ?? `Provider unavailable for ${unitType} ${unitId}`;
|
|
2013
|
-
const isTransient = Boolean(unitResult.errorContext?.isTransient);
|
|
2014
|
-
const retryAfterMs = unitResult.errorContext?.retryAfterMs ?? (isTransient ? 30_000 : undefined);
|
|
2015
|
-
await pauseAutoForProviderError(ctx.ui, detail, () => deps.pauseAuto(ctx, pi), {
|
|
2016
|
-
isRateLimit: false,
|
|
2017
|
-
isTransient,
|
|
2018
|
-
retryAfterMs,
|
|
2019
|
-
resume: isTransient
|
|
2020
|
-
? () => {
|
|
2021
|
-
void resumeAutoAfterProviderDelay(pi, ctx).catch((err) => {
|
|
2022
|
-
logWarning("engine", `Provider error auto-resume failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2023
|
-
});
|
|
2024
|
-
}
|
|
2025
|
-
: undefined,
|
|
2026
|
-
});
|
|
2027
|
-
}
|
|
2028
|
-
await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, unitResult.errorContext);
|
|
2029
|
-
debugLog("autoLoop", { phase: "exit", reason: "provider-pause", isTransient: unitResult.errorContext?.isTransient });
|
|
2030
|
-
return { action: "break", reason: "provider-pause" };
|
|
2031
|
-
}
|
|
2032
|
-
// Timeout category covers two distinct scenarios:
|
|
2033
|
-
// 1. Session creation timeout (120s) — transient, auto-resume with backoff
|
|
2034
|
-
// 2. Unit hard timeout (30min+) — stuck agent, pause for manual review
|
|
2035
|
-
// Transient session-failed covers recoverable newSession failures and should
|
|
2036
|
-
// pause instead of hard-stopping.
|
|
2037
|
-
// Structural errors (TypeError, is not a function) are NOT transient
|
|
2038
|
-
// and must hard-stop to avoid infinite retry loops.
|
|
2039
|
-
if (unitResult.errorContext?.isTransient &&
|
|
2040
|
-
errorCategory === "timeout") {
|
|
2041
|
-
const isSessionCreationTimeout = unitResult.errorContext.message?.includes("Session creation timed out");
|
|
2042
|
-
if (isSessionCreationTimeout) {
|
|
2043
|
-
consecutiveSessionTimeouts += 1;
|
|
2044
|
-
const baseRetryAfterMs = 30_000;
|
|
2045
|
-
const retryAfterMs = baseRetryAfterMs * 2 ** Math.max(0, consecutiveSessionTimeouts - 1);
|
|
2046
|
-
const allowAutoResume = consecutiveSessionTimeouts <= MAX_SESSION_TIMEOUT_AUTO_RESUMES;
|
|
2047
|
-
if (!allowAutoResume) {
|
|
2048
|
-
ctx.ui.notify(`Session creation timed out ${consecutiveSessionTimeouts} consecutive times for ${unitType} ${unitId}. Pausing for manual review.`, "warning");
|
|
2049
|
-
}
|
|
2050
|
-
debugLog("autoLoop", {
|
|
2051
|
-
phase: "session-timeout-pause",
|
|
2052
|
-
unitType, unitId,
|
|
2053
|
-
consecutiveSessionTimeouts,
|
|
2054
|
-
retryAfterMs,
|
|
2055
|
-
allowAutoResume,
|
|
2056
|
-
});
|
|
2057
|
-
const errorDetail = ` for ${unitType} ${unitId}`;
|
|
2058
|
-
await pauseAutoForProviderError(ctx.ui, errorDetail, () => deps.pauseAuto(ctx, pi), {
|
|
2059
|
-
isRateLimit: false,
|
|
2060
|
-
isTransient: allowAutoResume,
|
|
2061
|
-
retryAfterMs,
|
|
2062
|
-
resume: allowAutoResume
|
|
2063
|
-
? () => {
|
|
2064
|
-
void resumeAutoAfterProviderDelay(pi, ctx).catch((err) => {
|
|
2065
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2066
|
-
ctx.ui.notify(`Session timeout recovery failed: ${message}`, "error");
|
|
2067
|
-
});
|
|
2068
|
-
}
|
|
2069
|
-
: undefined,
|
|
2070
|
-
});
|
|
2071
|
-
await deps.autoCommitUnit?.(s.basePath, unitType, unitId, ctx);
|
|
2072
|
-
await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, unitResult.errorContext);
|
|
2073
|
-
return { action: "break", reason: "session-timeout" };
|
|
2074
|
-
}
|
|
2075
|
-
// Unit hard timeout (30min+): pause without auto-resume — stuck agent
|
|
2076
|
-
ctx.ui.notify(`Unit timed out for ${unitType} ${unitId} (supervision may have failed). Pausing auto-mode.`, "warning");
|
|
2077
|
-
debugLog("autoLoop", { phase: "unit-hard-timeout-pause", unitType, unitId });
|
|
2078
|
-
await deps.pauseAuto(ctx, pi);
|
|
2079
|
-
await deps.autoCommitUnit?.(s.basePath, unitType, unitId, ctx);
|
|
2080
|
-
await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, unitResult.errorContext);
|
|
2081
|
-
return { action: "break", reason: "unit-hard-timeout" };
|
|
2082
|
-
}
|
|
2083
|
-
if (unitResult.errorContext?.isTransient &&
|
|
2084
|
-
errorCategory === "session-failed") {
|
|
2085
|
-
ctx.ui.notify(`Session creation failed transiently for ${unitType} ${unitId}: ${unitResult.errorContext?.message ?? "unknown"}. Pausing auto-mode (recoverable).`, "warning");
|
|
2086
|
-
debugLog("autoLoop", { phase: "session-start-transient-pause", unitType, unitId, category: errorCategory });
|
|
2087
|
-
await deps.pauseAuto(ctx, pi);
|
|
2088
|
-
await deps.autoCommitUnit?.(s.basePath, unitType, unitId, ctx);
|
|
2089
|
-
await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, unitResult.errorContext);
|
|
2090
|
-
return { action: "break", reason: "session-timeout" };
|
|
2091
|
-
}
|
|
2092
|
-
if (unitResult.errorContext?.isTransient &&
|
|
2093
|
-
errorCategory === "aborted") {
|
|
2094
|
-
rememberRetryDispatch(s, { type: unitType, id: unitId }, iterData);
|
|
2095
|
-
writeUnitRuntimeRecord(s.basePath, unitType, unitId, s.currentUnit?.startedAt ?? Date.now(), {
|
|
2096
|
-
phase: "paused",
|
|
2097
|
-
lastProgressAt: Date.now(),
|
|
2098
|
-
lastProgressKind: "unit-aborted-pause",
|
|
2099
|
-
});
|
|
2100
|
-
ctx.ui.notify(`Unit ${unitType} ${unitId} was aborted (transient). Pausing auto-mode (recoverable).`, "warning");
|
|
2101
|
-
debugLog("autoLoop", { phase: "unit-aborted-transient-pause", unitType, unitId, category: errorCategory });
|
|
2102
|
-
await deps.pauseAuto(ctx, pi, unitResult.errorContext);
|
|
2103
|
-
await deps.autoCommitUnit?.(s.basePath, unitType, unitId, ctx);
|
|
2104
|
-
await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, unitResult.errorContext);
|
|
2105
|
-
return { action: "break", reason: "unit-aborted-pause" };
|
|
2106
|
-
}
|
|
2107
|
-
// All other cancelled states (structural errors, non-transient failures): hard stop
|
|
2108
|
-
if (s.currentUnit) {
|
|
2109
|
-
await deps.closeoutUnit(ctx, s.basePath, unitType, unitId, s.currentUnit.startedAt, deps.buildSnapshotOpts(unitType, unitId));
|
|
2110
|
-
}
|
|
2111
|
-
await deps.autoCommitUnit?.(s.basePath, unitType, unitId, ctx);
|
|
2112
|
-
await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, unitResult.errorContext);
|
|
2113
|
-
const cancelledStop = _buildCancelledUnitStopReason(unitType, unitId, unitResult.errorContext);
|
|
2114
|
-
ctx.ui.notify(cancelledStop.notifyMessage, "warning");
|
|
2115
|
-
await deps.stopAuto(ctx, pi, cancelledStop.stopReason);
|
|
2116
|
-
debugLog("autoLoop", { phase: "exit", reason: cancelledStop.loopReason });
|
|
2117
|
-
return { action: "break", reason: cancelledStop.loopReason };
|
|
2118
|
-
}
|
|
2119
|
-
// ── Immediate unit closeout (metrics, activity log, memory) ────────
|
|
2120
|
-
// Run right after runUnit() returns so telemetry is never lost to a
|
|
2121
|
-
// crash between iterations.
|
|
2122
|
-
// Guard: stopAuto() may have nulled s.currentUnit via s.reset() while
|
|
2123
|
-
// this coroutine was suspended at `await runUnit(...)` (#2939).
|
|
2124
|
-
if (s.currentUnit) {
|
|
2125
|
-
// Reset session timeout counter — any successful unit clears the slate
|
|
2126
|
-
consecutiveSessionTimeouts = 0;
|
|
2127
|
-
await deps.closeoutUnit(ctx, s.basePath, unitType, unitId, s.currentUnit.startedAt, deps.buildSnapshotOpts(unitType, unitId));
|
|
2128
|
-
}
|
|
2129
|
-
// ── Zero tool-call guard (#1833, #2653) ──────────────────────────
|
|
2130
|
-
// Any unit that completes with 0 tool calls made no real progress —
|
|
2131
|
-
// likely context exhaustion where all tool calls errored out. Treat
|
|
2132
|
-
// as failed so the unit is retried in a fresh context instead of
|
|
2133
|
-
// silently passing through to artifact verification (which loops
|
|
2134
|
-
// forever when the unit never produced its artifact).
|
|
2135
|
-
{
|
|
2136
|
-
const currentLedger = deps.getLedger();
|
|
2137
|
-
if (currentLedger?.units) {
|
|
2138
|
-
const lastUnit = [...currentLedger.units].reverse().find((u) => u.type === unitType && u.id === unitId && u.startedAt === _resolveCurrentUnitStartedAtForTest(s.currentUnit));
|
|
2139
|
-
if (lastUnit && lastUnit.toolCalls === 0) {
|
|
2140
|
-
const lastAssistantMessage = lastAssistantText(s.lastUnitAgentEndMessages);
|
|
2141
|
-
const providerMessageClass = classifyZeroToolProviderMessage(lastAssistantMessage);
|
|
2142
|
-
if (providerMessageClass && isTransient(providerMessageClass)) {
|
|
2143
|
-
const retryAfterMs = "retryAfterMs" in providerMessageClass ? providerMessageClass.retryAfterMs : 15_000;
|
|
2144
|
-
await pauseAutoForProviderError(ctx.ui, ` for ${unitType} ${unitId}`, () => deps.pauseAuto(ctx, pi), {
|
|
2145
|
-
isRateLimit: providerMessageClass.kind === "rate-limit",
|
|
2146
|
-
isTransient: true,
|
|
2147
|
-
retryAfterMs,
|
|
2148
|
-
resume: () => {
|
|
2149
|
-
void resumeAutoAfterProviderDelay(pi, ctx).catch((err) => {
|
|
2150
|
-
logWarning("engine", `Provider error auto-resume failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2151
|
-
});
|
|
2152
|
-
},
|
|
2153
|
-
});
|
|
2154
|
-
await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, {
|
|
2155
|
-
message: lastAssistantMessage.slice(0, 200),
|
|
2156
|
-
category: "provider",
|
|
2157
|
-
isTransient: true,
|
|
2158
|
-
retryAfterMs,
|
|
2159
|
-
});
|
|
2160
|
-
return {
|
|
2161
|
-
action: "break",
|
|
2162
|
-
reason: providerMessageClass.kind === "rate-limit" ? "rate-limit" : "api-timeout",
|
|
2163
|
-
};
|
|
2164
|
-
}
|
|
2165
|
-
if (USER_DRIVEN_DEEP_UNITS.has(unitType) && isAwaitingUserInput(s.lastUnitAgentEndMessages ?? undefined)) {
|
|
2166
|
-
debugLog("runUnitPhase", {
|
|
2167
|
-
phase: "zero-tool-calls-awaiting-user-input",
|
|
2168
|
-
unitType,
|
|
2169
|
-
unitId,
|
|
2170
|
-
});
|
|
2171
|
-
}
|
|
2172
|
-
else {
|
|
2173
|
-
const zeroToolKey = `${unitType}/${unitId}`;
|
|
2174
|
-
const attempt = (s.zeroToolRetryCount.get(zeroToolKey) ?? 0) + 1;
|
|
2175
|
-
debugLog("runUnitPhase", {
|
|
2176
|
-
phase: "zero-tool-calls",
|
|
2177
|
-
unitType,
|
|
2178
|
-
unitId,
|
|
2179
|
-
attempt,
|
|
2180
|
-
warning: "Unit completed with 0 tool calls — likely context exhaustion, marking as failed",
|
|
2181
|
-
});
|
|
2182
|
-
if (attempt > MAX_ZERO_TOOL_RETRIES) {
|
|
2183
|
-
s.zeroToolRetryCount.delete(zeroToolKey);
|
|
2184
|
-
ctx.ui.notify(`${unitType} ${unitId} completed with 0 tool calls — context exhaustion, pausing auto-mode after ${MAX_ZERO_TOOL_RETRIES} retry.`, "error");
|
|
2185
|
-
await deps.pauseAuto(ctx, pi);
|
|
2186
|
-
return { action: "break", reason: "zero-tool-calls-exhausted" };
|
|
2187
|
-
}
|
|
2188
|
-
s.zeroToolRetryCount.set(zeroToolKey, attempt);
|
|
2189
|
-
ctx.ui.notify(`${unitType} ${unitId} completed with 0 tool calls — context exhaustion, will retry (attempt ${attempt}/${MAX_ZERO_TOOL_RETRIES})`, "warning");
|
|
2190
|
-
return {
|
|
2191
|
-
action: "retry",
|
|
2192
|
-
reason: "zero-tool-calls",
|
|
2193
|
-
data: {
|
|
2194
|
-
unitStartedAt: _resolveCurrentUnitStartedAtForTest(s.currentUnit),
|
|
2195
|
-
requestDispatchedAt: unitResult.requestDispatchedAt,
|
|
2196
|
-
},
|
|
2197
|
-
};
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
}
|
|
2202
|
-
const skipArtifactVerification = unitType.startsWith("hook/") || unitType === "custom-step";
|
|
2203
|
-
const artifactVerified = skipArtifactVerification ||
|
|
2204
|
-
verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
2205
|
-
if (s.currentUnitRouting) {
|
|
2206
|
-
deps.recordOutcome(unitType, s.currentUnitRouting.tier, artifactVerified);
|
|
2207
|
-
}
|
|
2208
|
-
if (artifactVerified) {
|
|
2209
|
-
s.unitDispatchCount.delete(dispatchKey);
|
|
2210
|
-
s.unitRecoveryCount.delete(`${unitType}/${unitId}`);
|
|
2211
|
-
s.zeroToolRetryCount.delete(dispatchKey);
|
|
2212
|
-
}
|
|
2213
|
-
// Write phase handoff anchor after successful research/planning completion
|
|
2214
|
-
const anchorPhases = new Set(["research-milestone", "research-slice", "plan-milestone", "plan-slice"]);
|
|
2215
|
-
if (artifactVerified && mid && anchorPhases.has(unitType)) {
|
|
2216
|
-
try {
|
|
2217
|
-
const { writePhaseAnchor } = await import("../phase-anchor.js");
|
|
2218
|
-
writePhaseAnchor(s.basePath, mid, {
|
|
2219
|
-
phase: unitType,
|
|
2220
|
-
milestoneId: mid,
|
|
2221
|
-
generatedAt: new Date().toISOString(),
|
|
2222
|
-
intent: `Completed ${unitType} for ${unitId}`,
|
|
2223
|
-
decisions: [],
|
|
2224
|
-
blockers: [],
|
|
2225
|
-
nextSteps: [],
|
|
2226
|
-
});
|
|
2227
|
-
}
|
|
2228
|
-
catch (err) { /* non-fatal — anchor is advisory */
|
|
2229
|
-
logWarning("engine", `phase anchor failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
const unitEndStatus = !artifactVerified && unitResult.status === "completed"
|
|
2233
|
-
? "no-artifact"
|
|
2234
|
-
: unitResult.status;
|
|
2235
|
-
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "unit-end", data: { unitType, unitId, status: unitEndStatus, artifactVerified, ...(unitResult.errorContext ? { errorContext: unitResult.errorContext } : {}) }, causedBy: { flowId: ic.flowId, seq: unitStartSeq } });
|
|
2236
|
-
// ── Safety harness: checkpoint cleanup or rollback ──
|
|
2237
|
-
if (s.checkpointSha) {
|
|
2238
|
-
if (unitResult.status === "error" && safetyConfig.auto_rollback) {
|
|
2239
|
-
const rolled = rollbackToCheckpoint(s.basePath, unitId, s.checkpointSha);
|
|
2240
|
-
if (rolled) {
|
|
2241
|
-
ctx.ui.notify(`Rolled back to pre-unit checkpoint for ${unitId}`, "info");
|
|
2242
|
-
debugLog("runUnitPhase", { phase: "checkpoint-rollback", unitId });
|
|
2243
|
-
}
|
|
2244
|
-
}
|
|
2245
|
-
else if (unitResult.status === "error") {
|
|
2246
|
-
ctx.ui.notify(`Unit ${unitId} failed. Pre-unit checkpoint available at ${s.checkpointSha.slice(0, 8)}`, "warning");
|
|
2247
|
-
}
|
|
2248
|
-
else {
|
|
2249
|
-
// Success — clean up checkpoint ref
|
|
2250
|
-
cleanupCheckpoint(s.basePath, unitId);
|
|
2251
|
-
debugLog("runUnitPhase", { phase: "checkpoint-cleaned", unitId });
|
|
2252
|
-
}
|
|
2253
|
-
s.checkpointSha = null;
|
|
2254
|
-
}
|
|
2255
|
-
return { action: "next", data: { unitStartedAt: _resolveCurrentUnitStartedAtForTest(s.currentUnit), requestDispatchedAt: unitResult.requestDispatchedAt } };
|
|
2256
|
-
}
|
|
2257
|
-
// ─── runFinalize ──────────────────────────────────────────────────────────────
|
|
2258
|
-
/**
|
|
2259
|
-
* Phase 5: Post-unit finalize — pre/post verification, UAT pause, step-wizard.
|
|
2260
|
-
* Returns break/continue/next to control the outer loop.
|
|
2261
|
-
*/
|
|
2262
|
-
export async function runFinalize(ic, iterData, loopState, sidecarItem) {
|
|
2263
|
-
const { ctx, pi, s, deps } = ic;
|
|
2264
|
-
const { pauseAfterUatDispatch } = iterData;
|
|
2265
|
-
debugLog("autoLoop", { phase: "finalize", iteration: ic.iteration });
|
|
2266
|
-
// Clear unit timeout (unit completed)
|
|
2267
|
-
deps.clearUnitTimeout();
|
|
2268
|
-
// Post-unit context for pre/post verification
|
|
2269
|
-
const postUnitCtx = {
|
|
2270
|
-
s,
|
|
2271
|
-
ctx,
|
|
2272
|
-
pi,
|
|
2273
|
-
buildSnapshotOpts: deps.buildSnapshotOpts,
|
|
2274
|
-
lockBase: deps.lockBase,
|
|
2275
|
-
stopAuto: deps.stopAuto,
|
|
2276
|
-
pauseAuto: deps.pauseAuto,
|
|
2277
|
-
updateProgressWidget: deps.updateProgressWidget,
|
|
2278
|
-
};
|
|
2279
|
-
// Pre-verification processing (commit, doctor, state rebuild, etc.)
|
|
2280
|
-
// Timeout guard: if postUnitPreVerification hangs (e.g., safety harness
|
|
2281
|
-
// deadlock, browser teardown hang, worktree sync stall), force-continue
|
|
2282
|
-
// after timeout so the auto-loop is not permanently frozen (#3757).
|
|
2283
|
-
//
|
|
2284
|
-
// On timeout, null out s.currentUnit so the timed-out task's late async
|
|
2285
|
-
// mutations are harmless — postUnitPreVerification guards all side effects
|
|
2286
|
-
// behind `if (s.currentUnit)`. The next iteration sets a fresh currentUnit.
|
|
2287
|
-
// Sidecar items use lightweight pre-verification opts
|
|
2288
|
-
const preVerificationOpts = sidecarItem
|
|
2289
|
-
? sidecarItem.kind === "hook"
|
|
2290
|
-
? { skipSettleDelay: true, skipWorktreeSync: true, agentEndMessages: s.lastUnitAgentEndMessages ?? undefined }
|
|
2291
|
-
: { skipSettleDelay: true, agentEndMessages: s.lastUnitAgentEndMessages ?? undefined }
|
|
2292
|
-
: { agentEndMessages: s.lastUnitAgentEndMessages ?? undefined };
|
|
2293
|
-
const preUnitSnapshot = s.currentUnit
|
|
2294
|
-
? { type: s.currentUnit.type, id: s.currentUnit.id, startedAt: s.currentUnit.startedAt }
|
|
2295
|
-
: null;
|
|
2296
|
-
const clearFinalizingUnit = () => {
|
|
2297
|
-
if (preUnitSnapshot &&
|
|
2298
|
-
s.currentUnit?.type === preUnitSnapshot.type &&
|
|
2299
|
-
s.currentUnit?.id === preUnitSnapshot.id &&
|
|
2300
|
-
s.currentUnit?.startedAt === preUnitSnapshot.startedAt) {
|
|
2301
|
-
s.clearCurrentUnit();
|
|
2302
|
-
}
|
|
2303
|
-
s.rootWriteBaseline = null;
|
|
2304
|
-
};
|
|
2305
|
-
clearCurrentPhase();
|
|
2306
|
-
const preResultGuard = await withTimeout(deps.postUnitPreVerification(postUnitCtx, preVerificationOpts), FINALIZE_PRE_TIMEOUT_MS, "postUnitPreVerification");
|
|
2307
|
-
if (preResultGuard.timedOut) {
|
|
2308
|
-
return failClosedOnFinalizeTimeout(ic, iterData, loopState, "pre", preUnitSnapshot?.startedAt ?? Date.now());
|
|
2309
|
-
}
|
|
2310
|
-
const preResult = preResultGuard.value;
|
|
2311
|
-
if (preResult === "dispatched") {
|
|
2312
|
-
const dispatchedReason = s.lastGitActionFailure
|
|
2313
|
-
? "git-closeout-failure"
|
|
2314
|
-
: "pre-verification-dispatched";
|
|
2315
|
-
debugLog("autoLoop", {
|
|
2316
|
-
phase: "exit",
|
|
2317
|
-
reason: dispatchedReason,
|
|
2318
|
-
gitError: s.lastGitActionFailure ?? undefined,
|
|
2319
|
-
});
|
|
2320
|
-
clearFinalizingUnit();
|
|
2321
|
-
return { action: "break", reason: dispatchedReason };
|
|
2322
|
-
}
|
|
2323
|
-
if (preResult === "retry") {
|
|
2324
|
-
if (sidecarItem) {
|
|
2325
|
-
// Sidecar artifact retries are skipped — just continue
|
|
2326
|
-
debugLog("autoLoop", { phase: "sidecar-artifact-retry-skipped", iteration: ic.iteration });
|
|
2327
|
-
}
|
|
2328
|
-
else {
|
|
2329
|
-
// s.pendingVerificationRetry was set by postUnitPreVerification.
|
|
2330
|
-
// Emit a dedicated journal event so forensics can distinguish bounded
|
|
2331
|
-
// verification retries from genuine stuck-loop dispatch repetitions (#4540).
|
|
2332
|
-
const retryInfo = s.pendingVerificationRetry;
|
|
2333
|
-
deps.emitJournalEvent({
|
|
2334
|
-
ts: new Date().toISOString(),
|
|
2335
|
-
flowId: ic.flowId,
|
|
2336
|
-
seq: ic.nextSeq(),
|
|
2337
|
-
eventType: "artifact-verification-retry",
|
|
2338
|
-
data: {
|
|
2339
|
-
unitType: preUnitSnapshot?.type,
|
|
2340
|
-
unitId: retryInfo?.unitId,
|
|
2341
|
-
attempt: retryInfo?.attempt,
|
|
2342
|
-
},
|
|
2343
|
-
});
|
|
2344
|
-
const retryPolicyResult = await applyVerificationRetryPolicy(ic, preUnitSnapshot?.type, "artifact-verification-retry");
|
|
2345
|
-
if (retryPolicyResult) {
|
|
2346
|
-
clearFinalizingUnit();
|
|
2347
|
-
return retryPolicyResult;
|
|
2348
|
-
}
|
|
2349
|
-
// Continue the loop — next iteration will inject the retry context into the prompt.
|
|
2350
|
-
rememberRetryDispatch(s, preUnitSnapshot, iterData);
|
|
2351
|
-
debugLog("autoLoop", { phase: "artifact-verification-retry", iteration: ic.iteration });
|
|
2352
|
-
clearFinalizingUnit();
|
|
2353
|
-
return { action: "continue" };
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
if (pauseAfterUatDispatch) {
|
|
2357
|
-
const pauseMid = iterData.mid;
|
|
2358
|
-
const pauseSliceId = pauseMid && iterData.unitId.startsWith(`${pauseMid}/`)
|
|
2359
|
-
? iterData.unitId.slice(pauseMid.length + 1)
|
|
2360
|
-
: undefined;
|
|
2361
|
-
const guidance = pauseMid
|
|
2362
|
-
? buildManualValidationGuidance(s.basePath, pauseMid, {
|
|
2363
|
-
uatPath: pauseSliceId
|
|
2364
|
-
? relSliceFile(s.basePath, pauseMid, pauseSliceId, "UAT")
|
|
2365
|
-
: undefined,
|
|
2366
|
-
})
|
|
2367
|
-
: null;
|
|
2368
|
-
const pauseMessage = guidance
|
|
2369
|
-
? `UAT requires human execution. Auto-mode will pause after this unit writes the result file.\n\n${guidance}`
|
|
2370
|
-
: "UAT requires human execution. Auto-mode will pause after this unit writes the result file.";
|
|
2371
|
-
ctx.ui.notify(pauseMessage, "info");
|
|
2372
|
-
await deps.pauseAuto(ctx, pi);
|
|
2373
|
-
debugLog("autoLoop", { phase: "exit", reason: "uat-pause" });
|
|
2374
|
-
clearFinalizingUnit();
|
|
2375
|
-
return { action: "break", reason: "uat-pause" };
|
|
2376
|
-
}
|
|
2377
|
-
// Verification gate
|
|
2378
|
-
// Hook sidecar items skip verification entirely.
|
|
2379
|
-
// Non-hook sidecar items run verification but skip retries (just continue).
|
|
2380
|
-
const skipVerification = sidecarItem?.kind === "hook";
|
|
2381
|
-
if (!skipVerification) {
|
|
2382
|
-
const verificationResult = await deps.runPostUnitVerification({ s, ctx, pi }, deps.pauseAuto);
|
|
2383
|
-
if (verificationResult === "pause") {
|
|
2384
|
-
debugLog("autoLoop", { phase: "exit", reason: "verification-pause" });
|
|
2385
|
-
clearFinalizingUnit();
|
|
2386
|
-
return { action: "break", reason: "verification-pause" };
|
|
2387
|
-
}
|
|
2388
|
-
if (verificationResult === "retry") {
|
|
2389
|
-
if (sidecarItem) {
|
|
2390
|
-
// Sidecar verification retries are skipped — just continue
|
|
2391
|
-
debugLog("autoLoop", { phase: "sidecar-verification-retry-skipped", iteration: ic.iteration });
|
|
2392
|
-
}
|
|
2393
|
-
else {
|
|
2394
|
-
// s.pendingVerificationRetry was set by runPostUnitVerification.
|
|
2395
|
-
const retryPolicyResult = await applyVerificationRetryPolicy(ic, iterData.unitType, "verification-retry");
|
|
2396
|
-
if (retryPolicyResult) {
|
|
2397
|
-
clearFinalizingUnit();
|
|
2398
|
-
return retryPolicyResult;
|
|
2399
|
-
}
|
|
2400
|
-
// Continue the loop — next iteration will inject the retry context into the prompt.
|
|
2401
|
-
rememberRetryDispatch(s, preUnitSnapshot, iterData);
|
|
2402
|
-
debugLog("autoLoop", { phase: "verification-retry", iteration: ic.iteration });
|
|
2403
|
-
clearFinalizingUnit();
|
|
2404
|
-
return { action: "continue" };
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
// Post-verification processing (DB dual-write, hooks, triage, quick-tasks)
|
|
2409
|
-
// Timeout guard: if postUnitPostVerification hangs (e.g., module import
|
|
2410
|
-
// deadlock, SQLite transaction hang), force-continue after timeout so the
|
|
2411
|
-
// auto-loop is not permanently frozen (#2344).
|
|
2412
|
-
const postResultGuard = await withTimeout(deps.postUnitPostVerification(postUnitCtx), FINALIZE_POST_TIMEOUT_MS, "postUnitPostVerification");
|
|
2413
|
-
if (postResultGuard.timedOut) {
|
|
2414
|
-
return failClosedOnFinalizeTimeout(ic, iterData, loopState, "post", preUnitSnapshot?.startedAt ?? Date.now());
|
|
2415
|
-
}
|
|
2416
|
-
const postResult = postResultGuard.value;
|
|
2417
|
-
if (postResult === "retry") {
|
|
2418
|
-
if (sidecarItem) {
|
|
2419
|
-
debugLog("autoLoop", { phase: "sidecar-pre-execution-retry-skipped", iteration: ic.iteration });
|
|
2420
|
-
}
|
|
2421
|
-
else {
|
|
2422
|
-
const retryInfo = s.pendingVerificationRetry;
|
|
2423
|
-
deps.emitJournalEvent({
|
|
2424
|
-
ts: new Date().toISOString(),
|
|
2425
|
-
flowId: ic.flowId,
|
|
2426
|
-
seq: ic.nextSeq(),
|
|
2427
|
-
eventType: "pre-execution-retry",
|
|
2428
|
-
data: {
|
|
2429
|
-
unitType: preUnitSnapshot?.type,
|
|
2430
|
-
unitId: retryInfo?.unitId,
|
|
2431
|
-
attempt: retryInfo?.attempt,
|
|
2432
|
-
},
|
|
2433
|
-
});
|
|
2434
|
-
const retryPolicyResult = await applyVerificationRetryPolicy(ic, preUnitSnapshot?.type, "pre-execution-retry");
|
|
2435
|
-
if (retryPolicyResult) {
|
|
2436
|
-
clearFinalizingUnit();
|
|
2437
|
-
return retryPolicyResult;
|
|
2438
|
-
}
|
|
2439
|
-
rememberRetryDispatch(s, preUnitSnapshot, iterData);
|
|
2440
|
-
debugLog("autoLoop", {
|
|
2441
|
-
phase: "pre-execution-retry",
|
|
2442
|
-
iteration: ic.iteration,
|
|
2443
|
-
unitType: preUnitSnapshot?.type,
|
|
2444
|
-
unitId: retryInfo?.unitId,
|
|
2445
|
-
attempt: retryInfo?.attempt,
|
|
2446
|
-
});
|
|
2447
|
-
clearFinalizingUnit();
|
|
2448
|
-
return { action: "continue" };
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
if (postResult === "stopped") {
|
|
2452
|
-
debugLog("autoLoop", {
|
|
2453
|
-
phase: "exit",
|
|
2454
|
-
reason: "post-verification-stopped",
|
|
2455
|
-
});
|
|
2456
|
-
clearFinalizingUnit();
|
|
2457
|
-
return { action: "break", reason: "post-verification-stopped" };
|
|
2458
|
-
}
|
|
2459
|
-
if (postResult === "step-wizard") {
|
|
2460
|
-
// Step mode — exit the loop (caller handles wizard)
|
|
2461
|
-
debugLog("autoLoop", { phase: "exit", reason: "step-wizard" });
|
|
2462
|
-
clearFinalizingUnit();
|
|
2463
|
-
return { action: "break", reason: "step-wizard" };
|
|
2464
|
-
}
|
|
2465
|
-
if (preUnitSnapshot && isIsolatedWorktreeSession(s)) {
|
|
2466
|
-
const leak = detectRootWriteLeak({
|
|
2467
|
-
rootPath: s.originalBasePath,
|
|
2468
|
-
worktreePath: s.basePath,
|
|
2469
|
-
unitType: preUnitSnapshot.type,
|
|
2470
|
-
unitId: preUnitSnapshot.id,
|
|
2471
|
-
before: s.rootWriteBaseline,
|
|
2472
|
-
});
|
|
2473
|
-
s.rootWriteBaseline = null;
|
|
2474
|
-
if (leak) {
|
|
2475
|
-
const message = formatRootWriteLeakMessage(leak);
|
|
2476
|
-
debugLog("autoLoop", {
|
|
2477
|
-
phase: "root-write-leak",
|
|
2478
|
-
unitType: preUnitSnapshot.type,
|
|
2479
|
-
unitId: preUnitSnapshot.id,
|
|
2480
|
-
rootPath: leak.rootPath,
|
|
2481
|
-
worktreePath: leak.worktreePath,
|
|
2482
|
-
files: leak.files.map((file) => ({ path: file.path, status: file.status })),
|
|
2483
|
-
});
|
|
2484
|
-
ctx.ui.notify(message, "error");
|
|
2485
|
-
await deps.stopAuto(ctx, pi, "Root-write leak during isolated auto-mode", {
|
|
2486
|
-
preserveCompletedMilestoneBranch: true,
|
|
2487
|
-
});
|
|
2488
|
-
clearFinalizingUnit();
|
|
2489
|
-
return { action: "break", reason: "root-write-leak" };
|
|
2490
|
-
}
|
|
2491
|
-
}
|
|
2492
|
-
else {
|
|
2493
|
-
s.rootWriteBaseline = null;
|
|
2494
|
-
}
|
|
2495
|
-
if (preUnitSnapshot?.type === "complete-milestone" && s.currentMilestoneId) {
|
|
2496
|
-
const stop = await _runMilestoneMergeOnceWithStashRestore(ic, s.currentMilestoneId, {
|
|
2497
|
-
preserveCloseoutTranscript: true,
|
|
2498
|
-
});
|
|
2499
|
-
if (stop) {
|
|
2500
|
-
clearFinalizingUnit();
|
|
2501
|
-
return stop;
|
|
2502
|
-
}
|
|
2503
|
-
}
|
|
2504
|
-
// Both pre and post verification completed without timeout — reset counter
|
|
2505
|
-
loopState.consecutiveFinalizeTimeouts = 0;
|
|
2506
|
-
if (preUnitSnapshot) {
|
|
2507
|
-
writeUnitRuntimeRecord(s.basePath, preUnitSnapshot.type, preUnitSnapshot.id, preUnitSnapshot.startedAt, {
|
|
2508
|
-
phase: "finalized",
|
|
2509
|
-
lastProgressAt: Date.now(),
|
|
2510
|
-
lastProgressKind: "finalize-success",
|
|
2511
|
-
});
|
|
2512
|
-
if (!preUnitSnapshot.type.startsWith("hook/") &&
|
|
2513
|
-
preUnitSnapshot.type !== "custom-step" &&
|
|
2514
|
-
preUnitSnapshot.type !== "complete-milestone") {
|
|
2515
|
-
setAutoOutcomeWidget(ctx, {
|
|
2516
|
-
...buildPhaseHandoffOutcome({
|
|
2517
|
-
unitType: preUnitSnapshot.type,
|
|
2518
|
-
unitId: preUnitSnapshot.id,
|
|
2519
|
-
agentEndMessages: s.lastUnitAgentEndMessages,
|
|
2520
|
-
}),
|
|
2521
|
-
startedAt: s.autoStartTime,
|
|
2522
|
-
});
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
clearFinalizingUnit();
|
|
2526
|
-
// Surface accumulated workflow-logger issues for this unit to the user.
|
|
2527
|
-
// Warnings/errors logged during the unit are buffered in the logger and
|
|
2528
|
-
// drained here so the user sees a single consolidated post-unit alert.
|
|
2529
|
-
if (hasAnyIssues()) {
|
|
2530
|
-
const { logs } = drainAndSummarize();
|
|
2531
|
-
if (logs.length > 0) {
|
|
2532
|
-
const severity = logs.some((e) => e.severity === "error") ? "error" : "warning";
|
|
2533
|
-
ctx.ui.notify(formatForNotification(logs), severity);
|
|
2534
|
-
}
|
|
2535
|
-
}
|
|
2536
|
-
if (preUnitSnapshot?.type === "complete-milestone" && s.currentMilestoneId) {
|
|
2537
|
-
// cleanupAfterLoopExit skips gsd-progress when preserveCompletionSurface is true, so clear stale controls here.
|
|
2538
|
-
ctx.ui.setStatus?.("gsd-step", undefined);
|
|
2539
|
-
ctx.ui.setWidget?.("gsd-progress", undefined);
|
|
2540
|
-
await deps.stopAuto(ctx, pi, `Milestone ${s.currentMilestoneId} complete`, {
|
|
2541
|
-
completionWidget: {
|
|
2542
|
-
milestoneId: s.currentMilestoneId,
|
|
2543
|
-
milestoneTitle: iterData.midTitle,
|
|
2544
|
-
},
|
|
2545
|
-
});
|
|
2546
|
-
return { action: "break", reason: "milestone-complete" };
|
|
2547
|
-
}
|
|
2548
|
-
return { action: "next", data: undefined };
|
|
2549
|
-
}
|