gsd-pi 2.38.0-dev.eeb3520 → 2.39.0-dev.20aba06
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -11
- package/dist/app-paths.js +1 -1
- package/dist/cli.js +9 -0
- package/dist/extension-discovery.d.ts +5 -3
- package/dist/extension-discovery.js +14 -9
- package/dist/extension-registry.js +2 -2
- package/dist/remote-questions-config.js +2 -2
- package/dist/resource-loader.js +100 -3
- package/dist/resources/extensions/async-jobs/index.js +10 -0
- package/dist/resources/extensions/browser-tools/index.js +3 -1
- package/dist/resources/extensions/browser-tools/package.json +3 -1
- package/dist/resources/extensions/browser-tools/tools/verify.js +97 -0
- package/dist/resources/extensions/cmux/index.js +55 -1
- package/dist/resources/extensions/context7/package.json +1 -1
- package/dist/resources/extensions/get-secrets-from-user.js +5 -24
- package/dist/resources/extensions/github-sync/cli.js +284 -0
- package/dist/resources/extensions/github-sync/index.js +73 -0
- package/dist/resources/extensions/github-sync/mapping.js +67 -0
- package/dist/resources/extensions/github-sync/sync.js +424 -0
- package/dist/resources/extensions/github-sync/templates.js +118 -0
- package/dist/resources/extensions/github-sync/types.js +7 -0
- package/dist/resources/extensions/google-search/package.json +3 -1
- package/dist/resources/extensions/gsd/auto/session.js +6 -23
- package/dist/resources/extensions/gsd/auto-dashboard.js +7 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +8 -9
- package/dist/resources/extensions/gsd/auto-loop.js +923 -787
- package/dist/resources/extensions/gsd/auto-post-unit.js +107 -70
- package/dist/resources/extensions/gsd/auto-prompts.js +205 -51
- package/dist/resources/extensions/gsd/auto-start.js +19 -3
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
- package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
- package/dist/resources/extensions/gsd/auto.js +149 -100
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +126 -0
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +233 -0
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +59 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +38 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +156 -0
- package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +46 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +300 -0
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +38 -0
- package/dist/resources/extensions/gsd/captures.js +9 -1
- package/dist/resources/extensions/gsd/commands/catalog.js +278 -0
- package/dist/resources/extensions/gsd/commands/context.js +84 -0
- package/dist/resources/extensions/gsd/commands/dispatcher.js +21 -0
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +72 -0
- package/dist/resources/extensions/gsd/commands/handlers/core.js +246 -0
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +166 -0
- package/dist/resources/extensions/gsd/commands/handlers/parallel.js +94 -0
- package/dist/resources/extensions/gsd/commands/handlers/workflow.js +102 -0
- package/dist/resources/extensions/gsd/commands/index.js +11 -0
- package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
- package/dist/resources/extensions/gsd/commands-handlers.js +17 -4
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +8 -1169
- package/dist/resources/extensions/gsd/context-budget.js +2 -10
- package/dist/resources/extensions/gsd/dashboard-overlay.js +9 -0
- package/dist/resources/extensions/gsd/detection.js +1 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
- package/dist/resources/extensions/gsd/doctor-format.js +15 -0
- package/dist/resources/extensions/gsd/doctor-proactive.js +80 -10
- package/dist/resources/extensions/gsd/doctor-providers.js +30 -11
- package/dist/resources/extensions/gsd/doctor.js +234 -12
- package/dist/resources/extensions/gsd/env-utils.js +29 -0
- package/dist/resources/extensions/gsd/exit-command.js +2 -1
- package/dist/resources/extensions/gsd/export-html.js +46 -0
- package/dist/resources/extensions/gsd/export.js +1 -1
- package/dist/resources/extensions/gsd/files.js +48 -9
- package/dist/resources/extensions/gsd/forensics.js +1 -1
- package/dist/resources/extensions/gsd/git-service.js +30 -12
- package/dist/resources/extensions/gsd/gitignore.js +16 -3
- package/dist/resources/extensions/gsd/guided-flow.js +149 -38
- package/dist/resources/extensions/gsd/health-widget-core.js +32 -70
- package/dist/resources/extensions/gsd/health-widget.js +4 -87
- package/dist/resources/extensions/gsd/index.js +4 -1111
- package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
- package/dist/resources/extensions/gsd/migrate-external.js +18 -1
- package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
- package/dist/resources/extensions/gsd/package.json +1 -1
- package/dist/resources/extensions/gsd/paths.js +3 -0
- package/dist/resources/extensions/gsd/preferences-models.js +0 -12
- package/dist/resources/extensions/gsd/preferences-types.js +1 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +59 -11
- package/dist/resources/extensions/gsd/preferences.js +22 -11
- package/dist/resources/extensions/gsd/progress-score.js +20 -1
- package/dist/resources/extensions/gsd/prompt-loader.js +6 -2
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -3
- package/dist/resources/extensions/gsd/prompts/forensics.md +121 -46
- package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/run-uat.md +28 -11
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/dist/resources/extensions/gsd/repo-identity.js +21 -4
- package/dist/resources/extensions/gsd/resource-version.js +2 -1
- package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
- package/dist/resources/extensions/gsd/state.js +42 -23
- package/dist/resources/extensions/gsd/templates/runtime.md +21 -0
- package/dist/resources/extensions/gsd/templates/task-plan.md +3 -0
- package/dist/resources/extensions/gsd/visualizer-data.js +27 -2
- package/dist/resources/extensions/gsd/visualizer-views.js +52 -0
- package/dist/resources/extensions/gsd/worktree.js +35 -16
- package/dist/resources/extensions/mcp-client/index.js +14 -1
- package/dist/resources/extensions/remote-questions/status.js +4 -1
- package/dist/resources/extensions/remote-questions/store.js +4 -1
- package/dist/resources/extensions/search-the-web/provider.js +2 -1
- package/dist/resources/extensions/shared/frontmatter.js +1 -1
- package/dist/resources/extensions/subagent/index.js +12 -3
- package/dist/resources/extensions/subagent/isolation.js +2 -1
- package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
- package/dist/resources/extensions/universal-config/package.json +1 -1
- package/dist/welcome-screen.d.ts +13 -0
- package/dist/welcome-screen.js +97 -0
- package/package.json +1 -1
- package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
- package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
- package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +12 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +107 -24
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/skill-tool.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/skill-tool.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/skill-tool.test.js +70 -0
- package/packages/pi-coding-agent/dist/core/skill-tool.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/skills.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.js +8 -2
- package/packages/pi-coding-agent/dist/core/skills.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 +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts +17 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +244 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +58 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts +12 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +54 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.d.ts +6 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js +63 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +38 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +15 -457
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +122 -23
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
- package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
- package/packages/pi-coding-agent/src/core/skill-tool.test.ts +89 -0
- package/packages/pi-coding-agent/src/core/skills.ts +11 -2
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +302 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +59 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +68 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts +71 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +37 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +18 -510
- package/pkg/package.json +1 -1
- package/src/resources/extensions/async-jobs/index.ts +11 -0
- package/src/resources/extensions/browser-tools/index.ts +3 -0
- package/src/resources/extensions/browser-tools/tools/verify.ts +117 -0
- package/src/resources/extensions/cmux/index.ts +57 -1
- package/src/resources/extensions/get-secrets-from-user.ts +5 -24
- package/src/resources/extensions/github-sync/cli.ts +364 -0
- package/src/resources/extensions/github-sync/index.ts +93 -0
- package/src/resources/extensions/github-sync/mapping.ts +81 -0
- package/src/resources/extensions/github-sync/sync.ts +556 -0
- package/src/resources/extensions/github-sync/templates.ts +183 -0
- package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
- package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
- package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
- package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
- package/src/resources/extensions/github-sync/types.ts +47 -0
- package/src/resources/extensions/gsd/auto/session.ts +7 -25
- package/src/resources/extensions/gsd/auto-dashboard.ts +10 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +7 -9
- package/src/resources/extensions/gsd/auto-loop.ts +1285 -1138
- package/src/resources/extensions/gsd/auto-post-unit.ts +90 -46
- package/src/resources/extensions/gsd/auto-prompts.ts +250 -53
- package/src/resources/extensions/gsd/auto-start.ts +24 -3
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
- package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
- package/src/resources/extensions/gsd/auto.ts +152 -111
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +142 -0
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +238 -0
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +90 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +46 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +167 -0
- package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +55 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +340 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +51 -0
- package/src/resources/extensions/gsd/captures.ts +10 -1
- package/src/resources/extensions/gsd/commands/catalog.ts +301 -0
- package/src/resources/extensions/gsd/commands/context.ts +101 -0
- package/src/resources/extensions/gsd/commands/dispatcher.ts +32 -0
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +74 -0
- package/src/resources/extensions/gsd/commands/handlers/core.ts +274 -0
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +169 -0
- package/src/resources/extensions/gsd/commands/handlers/parallel.ts +118 -0
- package/src/resources/extensions/gsd/commands/handlers/workflow.ts +109 -0
- package/src/resources/extensions/gsd/commands/index.ts +14 -0
- package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
- package/src/resources/extensions/gsd/commands-handlers.ts +18 -3
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +10 -1307
- package/src/resources/extensions/gsd/context-budget.ts +2 -12
- package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
- package/src/resources/extensions/gsd/detection.ts +2 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
- package/src/resources/extensions/gsd/doctor-format.ts +20 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +106 -10
- package/src/resources/extensions/gsd/doctor-providers.ts +30 -9
- package/src/resources/extensions/gsd/doctor-types.ts +16 -1
- package/src/resources/extensions/gsd/doctor.ts +243 -14
- package/src/resources/extensions/gsd/env-utils.ts +31 -0
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/export-html.ts +51 -0
- package/src/resources/extensions/gsd/export.ts +1 -1
- package/src/resources/extensions/gsd/files.ts +51 -11
- package/src/resources/extensions/gsd/forensics.ts +1 -1
- package/src/resources/extensions/gsd/git-service.ts +44 -10
- package/src/resources/extensions/gsd/gitignore.ts +17 -3
- package/src/resources/extensions/gsd/guided-flow.ts +177 -44
- package/src/resources/extensions/gsd/health-widget-core.ts +28 -80
- package/src/resources/extensions/gsd/health-widget.ts +4 -89
- package/src/resources/extensions/gsd/index.ts +12 -1307
- package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
- package/src/resources/extensions/gsd/migrate-external.ts +18 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
- package/src/resources/extensions/gsd/paths.ts +4 -0
- package/src/resources/extensions/gsd/preferences-models.ts +0 -12
- package/src/resources/extensions/gsd/preferences-types.ts +4 -4
- package/src/resources/extensions/gsd/preferences-validation.ts +51 -11
- package/src/resources/extensions/gsd/preferences.ts +25 -11
- package/src/resources/extensions/gsd/progress-score.ts +23 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +7 -2
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/src/resources/extensions/gsd/prompts/execute-task.md +5 -3
- package/src/resources/extensions/gsd/prompts/forensics.md +121 -46
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +4 -8
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/run-uat.md +28 -11
- package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/src/resources/extensions/gsd/repo-identity.ts +23 -4
- package/src/resources/extensions/gsd/resource-version.ts +3 -1
- package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
- package/src/resources/extensions/gsd/state.ts +39 -21
- package/src/resources/extensions/gsd/templates/runtime.md +21 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +3 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +135 -77
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
- package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +16 -54
- package/src/resources/extensions/gsd/tests/parsers.test.ts +131 -14
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +16 -16
- package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +16 -4
- package/src/resources/extensions/gsd/tests/skill-activation.test.ts +140 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +10 -10
- package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
- package/src/resources/extensions/gsd/types.ts +18 -1
- package/src/resources/extensions/gsd/verification-evidence.ts +16 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +52 -2
- package/src/resources/extensions/gsd/visualizer-views.ts +58 -0
- package/src/resources/extensions/gsd/worktree.ts +35 -15
- package/src/resources/extensions/mcp-client/index.ts +17 -1
- package/src/resources/extensions/remote-questions/status.ts +5 -1
- package/src/resources/extensions/remote-questions/store.ts +5 -1
- package/src/resources/extensions/search-the-web/provider.ts +2 -1
- package/src/resources/extensions/shared/frontmatter.ts +1 -1
- package/src/resources/extensions/subagent/index.ts +12 -3
- package/src/resources/extensions/subagent/isolation.ts +3 -1
- package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
- package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
- package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
- package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
- package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
- package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
- package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
- package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
- package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
- package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
- package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
- package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
|
@@ -5,20 +5,20 @@
|
|
|
5
5
|
* pattern with a while loop. The agent_end event resolves a promise instead
|
|
6
6
|
* of recursing.
|
|
7
7
|
*
|
|
8
|
-
* MAINTENANCE RULE:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* MAINTENANCE RULE: Module-level mutable state is limited to `_currentResolve`
|
|
9
|
+
* (per-unit one-shot resolver) and `_sessionSwitchInFlight` (guard for
|
|
10
|
+
* session rotation). No queue — stale agent_end events are dropped.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type
|
|
13
|
+
import { importExtensionModule, type ExtensionAPI, type ExtensionContext } from "@gsd/pi-coding-agent";
|
|
14
14
|
|
|
15
|
-
import type { AutoSession } from "./auto/session.js";
|
|
15
|
+
import type { AutoSession, SidecarItem } from "./auto/session.js";
|
|
16
16
|
import { NEW_SESSION_TIMEOUT_MS } from "./auto/session.js";
|
|
17
17
|
import type { GSDPreferences } from "./preferences.js";
|
|
18
18
|
import type { SessionLockStatus } from "./session-lock.js";
|
|
19
19
|
import type { GSDState } from "./types.js";
|
|
20
20
|
import type { CloseoutOptions } from "./auto-unit-closeout.js";
|
|
21
|
-
import type { PostUnitContext } from "./auto-post-unit.js";
|
|
21
|
+
import type { PostUnitContext, PreVerificationOpts } from "./auto-post-unit.js";
|
|
22
22
|
import type {
|
|
23
23
|
VerificationContext,
|
|
24
24
|
VerificationResult,
|
|
@@ -26,6 +26,9 @@ import type {
|
|
|
26
26
|
import type { DispatchAction } from "./auto-dispatch.js";
|
|
27
27
|
import type { WorktreeResolver } from "./worktree-resolver.js";
|
|
28
28
|
import { debugLog } from "./debug-logger.js";
|
|
29
|
+
import { gsdRoot } from "./paths.js";
|
|
30
|
+
import { atomicWriteSync } from "./atomic-write.js";
|
|
31
|
+
import { join } from "node:path";
|
|
29
32
|
import type { CmuxLogLevel } from "../cmux/index.js";
|
|
30
33
|
|
|
31
34
|
/**
|
|
@@ -35,6 +38,23 @@ import type { CmuxLogLevel } from "../cmux/index.js";
|
|
|
35
38
|
* generous headroom including retries and sidecar work.
|
|
36
39
|
*/
|
|
37
40
|
const MAX_LOOP_ITERATIONS = 500;
|
|
41
|
+
/** Maximum characters of failure/crash context included in recovery prompts. */
|
|
42
|
+
const MAX_RECOVERY_CHARS = 50_000;
|
|
43
|
+
|
|
44
|
+
/** Data-driven budget threshold notifications (descending). The 100% entry
|
|
45
|
+
* triggers special enforcement logic (halt/pause/warn); sub-100 entries fire
|
|
46
|
+
* a simple notification. */
|
|
47
|
+
const BUDGET_THRESHOLDS: Array<{
|
|
48
|
+
pct: number;
|
|
49
|
+
label: string;
|
|
50
|
+
notifyLevel: "info" | "warning" | "error";
|
|
51
|
+
cmuxLevel: "progress" | "warning" | "error";
|
|
52
|
+
}> = [
|
|
53
|
+
{ pct: 100, label: "Budget ceiling reached", notifyLevel: "error", cmuxLevel: "error" },
|
|
54
|
+
{ pct: 90, label: "Budget 90%", notifyLevel: "warning", cmuxLevel: "warning" },
|
|
55
|
+
{ pct: 80, label: "Approaching budget ceiling — 80%", notifyLevel: "warning", cmuxLevel: "warning" },
|
|
56
|
+
{ pct: 75, label: "Budget 75%", notifyLevel: "info", cmuxLevel: "progress" },
|
|
57
|
+
];
|
|
38
58
|
|
|
39
59
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
40
60
|
|
|
@@ -54,17 +74,56 @@ export interface UnitResult {
|
|
|
54
74
|
event?: AgentEndEvent;
|
|
55
75
|
}
|
|
56
76
|
|
|
57
|
-
// ───
|
|
77
|
+
// ─── Phase pipeline types ────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
type PhaseResult<T = void> =
|
|
80
|
+
| { action: "continue" }
|
|
81
|
+
| { action: "break"; reason: string }
|
|
82
|
+
| { action: "next"; data: T }
|
|
83
|
+
|
|
84
|
+
interface IterationContext {
|
|
85
|
+
ctx: ExtensionContext;
|
|
86
|
+
pi: ExtensionAPI;
|
|
87
|
+
s: AutoSession;
|
|
88
|
+
deps: LoopDeps;
|
|
89
|
+
prefs: GSDPreferences | undefined;
|
|
90
|
+
iteration: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface LoopState {
|
|
94
|
+
recentUnits: Array<{ key: string; error?: string }>;
|
|
95
|
+
stuckRecoveryAttempts: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface PreDispatchData {
|
|
99
|
+
state: GSDState;
|
|
100
|
+
mid: string;
|
|
101
|
+
midTitle: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface IterationData {
|
|
105
|
+
unitType: string;
|
|
106
|
+
unitId: string;
|
|
107
|
+
prompt: string;
|
|
108
|
+
finalPrompt: string;
|
|
109
|
+
pauseAfterUatDispatch: boolean;
|
|
110
|
+
observabilityIssues: unknown[];
|
|
111
|
+
state: GSDState;
|
|
112
|
+
mid: string | undefined;
|
|
113
|
+
midTitle: string | undefined;
|
|
114
|
+
isRetry: boolean;
|
|
115
|
+
previousTier: string | undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Per-unit one-shot promise state ────────────────────────────────────────
|
|
58
119
|
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
120
|
+
// A single module-level resolve function scoped to the current unit execution.
|
|
121
|
+
// No queue — if an agent_end arrives with no pending resolver, it is dropped
|
|
122
|
+
// (logged as warning). This is simpler and safer than the previous session-
|
|
123
|
+
// scoped pendingResolve + pendingAgentEndQueue pattern.
|
|
61
124
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
* on entry so that the agent_end handler in index.ts can resolve the correct
|
|
65
|
-
* session's promise without needing a direct reference to `s`.
|
|
66
|
-
*/
|
|
67
|
-
let _activeSession: AutoSession | null = null;
|
|
125
|
+
let _currentResolve: ((result: UnitResult) => void) | null = null;
|
|
126
|
+
let _sessionSwitchInFlight = false;
|
|
68
127
|
|
|
69
128
|
// ─── resolveAgentEnd ─────────────────────────────────────────────────────────
|
|
70
129
|
|
|
@@ -73,60 +132,105 @@ let _activeSession: AutoSession | null = null;
|
|
|
73
132
|
* in-flight unit promise. One-shot: the resolver is nulled before calling
|
|
74
133
|
* to prevent double-resolution from model fallback retries.
|
|
75
134
|
*
|
|
76
|
-
* If no
|
|
77
|
-
* the event is
|
|
135
|
+
* If no resolver exists (event arrived between loop iterations or during
|
|
136
|
+
* session switch), the event is dropped with a debug warning.
|
|
78
137
|
*/
|
|
79
138
|
export function resolveAgentEnd(event: AgentEndEvent): void {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
debugLog("resolveAgentEnd", {
|
|
83
|
-
status: "no-active-session",
|
|
84
|
-
warning: "agent_end with no active loop session",
|
|
85
|
-
});
|
|
139
|
+
if (_sessionSwitchInFlight) {
|
|
140
|
+
debugLog("resolveAgentEnd", { status: "ignored-during-switch" });
|
|
86
141
|
return;
|
|
87
142
|
}
|
|
88
|
-
|
|
89
|
-
if (s.pendingResolve) {
|
|
143
|
+
if (_currentResolve) {
|
|
90
144
|
debugLog("resolveAgentEnd", { status: "resolving", hasEvent: true });
|
|
91
|
-
const r =
|
|
92
|
-
|
|
145
|
+
const r = _currentResolve;
|
|
146
|
+
_currentResolve = null;
|
|
93
147
|
r({ status: "completed", event });
|
|
94
148
|
} else {
|
|
95
|
-
// Queue the event so the next runUnit picks it up immediately
|
|
96
149
|
debugLog("resolveAgentEnd", {
|
|
97
|
-
status: "
|
|
98
|
-
|
|
99
|
-
warning:
|
|
100
|
-
"agent_end arrived between loop iterations — queued for next runUnit",
|
|
150
|
+
status: "no-pending-resolve",
|
|
151
|
+
warning: "agent_end with no pending unit",
|
|
101
152
|
});
|
|
102
|
-
s.pendingAgentEndQueue.push(event);
|
|
103
153
|
}
|
|
104
154
|
}
|
|
105
155
|
|
|
106
156
|
export function isSessionSwitchInFlight(): boolean {
|
|
107
|
-
return
|
|
157
|
+
return _sessionSwitchInFlight;
|
|
108
158
|
}
|
|
109
159
|
|
|
110
160
|
// ─── resetPendingResolve (test helper) ───────────────────────────────────────
|
|
111
161
|
|
|
112
162
|
/**
|
|
113
|
-
* Reset
|
|
114
|
-
* should never call this.
|
|
163
|
+
* Reset module-level promise state. Only exported for test cleanup —
|
|
164
|
+
* production code should never call this.
|
|
115
165
|
*/
|
|
116
166
|
export function _resetPendingResolve(): void {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
_activeSession.pendingAgentEndQueue = [];
|
|
120
|
-
}
|
|
121
|
-
_activeSession = null;
|
|
167
|
+
_currentResolve = null;
|
|
168
|
+
_sessionSwitchInFlight = false;
|
|
122
169
|
}
|
|
123
170
|
|
|
124
171
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
172
|
+
* No-op for backward compatibility with tests that previously set the
|
|
173
|
+
* active session. The module no longer holds a session reference.
|
|
127
174
|
*/
|
|
128
|
-
export function _setActiveSession(
|
|
129
|
-
|
|
175
|
+
export function _setActiveSession(_session: AutoSession | null): void {
|
|
176
|
+
// No-op — kept for test backward compatibility
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── detectStuck ─────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
type WindowEntry = { key: string; error?: string };
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Analyze a sliding window of recent unit dispatches for stuck patterns.
|
|
185
|
+
* Returns a signal with reason if stuck, null otherwise.
|
|
186
|
+
*
|
|
187
|
+
* Rule 1: Same error string twice in a row → stuck immediately.
|
|
188
|
+
* Rule 2: Same unit key 3+ consecutive times → stuck (preserves prior behavior).
|
|
189
|
+
* Rule 3: Oscillation A→B→A→B in last 4 entries → stuck.
|
|
190
|
+
*/
|
|
191
|
+
export function detectStuck(
|
|
192
|
+
window: readonly WindowEntry[],
|
|
193
|
+
): { stuck: true; reason: string } | null {
|
|
194
|
+
if (window.length < 2) return null;
|
|
195
|
+
|
|
196
|
+
const last = window[window.length - 1];
|
|
197
|
+
const prev = window[window.length - 2];
|
|
198
|
+
|
|
199
|
+
// Rule 1: Same error repeated consecutively
|
|
200
|
+
if (last.error && prev.error && last.error === prev.error) {
|
|
201
|
+
return {
|
|
202
|
+
stuck: true,
|
|
203
|
+
reason: `Same error repeated: ${last.error.slice(0, 200)}`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Rule 2: Same unit 3+ consecutive times
|
|
208
|
+
if (window.length >= 3) {
|
|
209
|
+
const lastThree = window.slice(-3);
|
|
210
|
+
if (lastThree.every((u) => u.key === last.key)) {
|
|
211
|
+
return {
|
|
212
|
+
stuck: true,
|
|
213
|
+
reason: `${last.key} derived 3 consecutive times without progress`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Rule 3: Oscillation (A→B→A→B in last 4)
|
|
219
|
+
if (window.length >= 4) {
|
|
220
|
+
const w = window.slice(-4);
|
|
221
|
+
if (
|
|
222
|
+
w[0].key === w[2].key &&
|
|
223
|
+
w[1].key === w[3].key &&
|
|
224
|
+
w[0].key !== w[1].key
|
|
225
|
+
) {
|
|
226
|
+
return {
|
|
227
|
+
stuck: true,
|
|
228
|
+
reason: `Oscillation detected: ${w[0].key} ↔ ${w[1].key}`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
130
234
|
}
|
|
131
235
|
|
|
132
236
|
// ─── runUnit ─────────────────────────────────────────────────────────────────
|
|
@@ -146,45 +250,18 @@ export async function runUnit(
|
|
|
146
250
|
unitType: string,
|
|
147
251
|
unitId: string,
|
|
148
252
|
prompt: string,
|
|
149
|
-
_prefs: GSDPreferences | undefined,
|
|
150
253
|
): Promise<UnitResult> {
|
|
151
254
|
debugLog("runUnit", { phase: "start", unitType, unitId });
|
|
152
255
|
|
|
153
|
-
// ── Drain queued events from error-recovery retries ──
|
|
154
|
-
// If an agent_end arrived between iterations (e.g. from a model fallback
|
|
155
|
-
// sendMessage retry), consume it immediately instead of creating a new promise.
|
|
156
|
-
// Cap queue to 3 entries to prevent unbounded growth from stale events.
|
|
157
|
-
if (s.pendingAgentEndQueue.length > 3) {
|
|
158
|
-
debugLog("runUnit", {
|
|
159
|
-
phase: "queue-overflow",
|
|
160
|
-
dropped: s.pendingAgentEndQueue.length - 1,
|
|
161
|
-
unitType,
|
|
162
|
-
unitId,
|
|
163
|
-
});
|
|
164
|
-
s.pendingAgentEndQueue = [
|
|
165
|
-
s.pendingAgentEndQueue[s.pendingAgentEndQueue.length - 1]!,
|
|
166
|
-
];
|
|
167
|
-
}
|
|
168
|
-
if (s.pendingAgentEndQueue.length > 0) {
|
|
169
|
-
const queued = s.pendingAgentEndQueue.shift()!;
|
|
170
|
-
debugLog("runUnit", {
|
|
171
|
-
phase: "drained-queued-event",
|
|
172
|
-
unitType,
|
|
173
|
-
unitId,
|
|
174
|
-
queueRemaining: s.pendingAgentEndQueue.length,
|
|
175
|
-
});
|
|
176
|
-
return { status: "completed", event: queued };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
256
|
// ── Session creation with timeout ──
|
|
180
257
|
debugLog("runUnit", { phase: "session-create", unitType, unitId });
|
|
181
258
|
|
|
182
259
|
let sessionResult: { cancelled: boolean };
|
|
183
260
|
let sessionTimeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
184
|
-
|
|
261
|
+
_sessionSwitchInFlight = true;
|
|
185
262
|
try {
|
|
186
263
|
const sessionPromise = s.cmdCtx!.newSession().finally(() => {
|
|
187
|
-
|
|
264
|
+
_sessionSwitchInFlight = false;
|
|
188
265
|
});
|
|
189
266
|
const timeoutPromise = new Promise<{ cancelled: true }>((resolve) => {
|
|
190
267
|
sessionTimeoutHandle = setTimeout(
|
|
@@ -216,11 +293,12 @@ export async function runUnit(
|
|
|
216
293
|
return { status: "cancelled" };
|
|
217
294
|
}
|
|
218
295
|
|
|
219
|
-
// ── Create the agent_end promise (
|
|
296
|
+
// ── Create the agent_end promise (per-unit one-shot) ──
|
|
220
297
|
// This happens after newSession completes so session-switch agent_end events
|
|
221
298
|
// from the previous session cannot resolve the new unit.
|
|
299
|
+
_sessionSwitchInFlight = false;
|
|
222
300
|
const unitPromise = new Promise<UnitResult>((resolve) => {
|
|
223
|
-
|
|
301
|
+
_currentResolve = resolve;
|
|
224
302
|
});
|
|
225
303
|
|
|
226
304
|
// Ensure cwd matches basePath before dispatch (#1389).
|
|
@@ -250,6 +328,20 @@ export async function runUnit(
|
|
|
250
328
|
status: result.status,
|
|
251
329
|
});
|
|
252
330
|
|
|
331
|
+
// Discard trailing follow-up messages (e.g. async_job_result notifications)
|
|
332
|
+
// from the completed unit. Without this, queued follow-ups trigger wasteful
|
|
333
|
+
// LLM turns before the next session can start (#1642).
|
|
334
|
+
// clearQueue() lives on AgentSession but isn't part of the typed
|
|
335
|
+
// ExtensionCommandContext interface — call it via runtime check.
|
|
336
|
+
try {
|
|
337
|
+
const cmdCtxAny = s.cmdCtx as Record<string, unknown> | null;
|
|
338
|
+
if (typeof cmdCtxAny?.clearQueue === "function") {
|
|
339
|
+
(cmdCtxAny.clearQueue as () => unknown)();
|
|
340
|
+
}
|
|
341
|
+
} catch {
|
|
342
|
+
// Non-fatal — clearQueue may not be available in all contexts
|
|
343
|
+
}
|
|
344
|
+
|
|
253
345
|
return result;
|
|
254
346
|
}
|
|
255
347
|
|
|
@@ -383,6 +475,7 @@ export interface LoopDeps {
|
|
|
383
475
|
midTitle: string;
|
|
384
476
|
state: GSDState;
|
|
385
477
|
prefs: GSDPreferences | undefined;
|
|
478
|
+
session?: AutoSession;
|
|
386
479
|
}) => Promise<DispatchAction>;
|
|
387
480
|
runPreDispatchHooks: (
|
|
388
481
|
unitType: string,
|
|
@@ -500,6 +593,7 @@ export interface LoopDeps {
|
|
|
500
593
|
// Post-unit processing
|
|
501
594
|
postUnitPreVerification: (
|
|
502
595
|
pctx: PostUnitContext,
|
|
596
|
+
opts?: PreVerificationOpts,
|
|
503
597
|
) => Promise<"dispatched" | "continue">;
|
|
504
598
|
runPostUnitVerification: (
|
|
505
599
|
vctx: VerificationContext,
|
|
@@ -513,1193 +607,1246 @@ export interface LoopDeps {
|
|
|
513
607
|
getSessionFile: (ctx: ExtensionContext) => string;
|
|
514
608
|
}
|
|
515
609
|
|
|
516
|
-
// ───
|
|
610
|
+
// ─── generateMilestoneReport ──────────────────────────────────────────────────
|
|
517
611
|
|
|
518
612
|
/**
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
* terminal condition is reached.
|
|
522
|
-
*
|
|
523
|
-
* This is the linear replacement for the recursive
|
|
524
|
-
* dispatchNextUnit → handleAgentEnd → dispatchNextUnit chain.
|
|
613
|
+
* Generate and write an HTML milestone report snapshot.
|
|
614
|
+
* Extracted from the milestone-transition block in autoLoop.
|
|
525
615
|
*/
|
|
526
|
-
|
|
616
|
+
async function generateMilestoneReport(
|
|
617
|
+
s: AutoSession,
|
|
618
|
+
ctx: ExtensionContext,
|
|
619
|
+
milestoneId: string,
|
|
620
|
+
): Promise<void> {
|
|
621
|
+
const { loadVisualizerData } = await importExtensionModule<typeof import("./visualizer-data.js")>(import.meta.url, "./visualizer-data.js");
|
|
622
|
+
const { generateHtmlReport } = await importExtensionModule<typeof import("./export-html.js")>(import.meta.url, "./export-html.js");
|
|
623
|
+
const { writeReportSnapshot } = await importExtensionModule<typeof import("./reports.js")>(import.meta.url, "./reports.js");
|
|
624
|
+
const { basename } = await import("node:path");
|
|
625
|
+
|
|
626
|
+
const snapData = await loadVisualizerData(s.basePath);
|
|
627
|
+
const completedMs = snapData.milestones.find(
|
|
628
|
+
(m: { id: string }) => m.id === milestoneId,
|
|
629
|
+
);
|
|
630
|
+
const msTitle = completedMs?.title ?? milestoneId;
|
|
631
|
+
const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
|
|
632
|
+
const projName = basename(s.basePath);
|
|
633
|
+
const doneSlices = snapData.milestones.reduce(
|
|
634
|
+
(acc: number, m: { slices: { done: boolean }[] }) =>
|
|
635
|
+
acc + m.slices.filter((sl: { done: boolean }) => sl.done).length,
|
|
636
|
+
0,
|
|
637
|
+
);
|
|
638
|
+
const totalSlices = snapData.milestones.reduce(
|
|
639
|
+
(acc: number, m: { slices: unknown[] }) => acc + m.slices.length,
|
|
640
|
+
0,
|
|
641
|
+
);
|
|
642
|
+
const outPath = writeReportSnapshot({
|
|
643
|
+
basePath: s.basePath,
|
|
644
|
+
html: generateHtmlReport(snapData, {
|
|
645
|
+
projectName: projName,
|
|
646
|
+
projectPath: s.basePath,
|
|
647
|
+
gsdVersion,
|
|
648
|
+
milestoneId,
|
|
649
|
+
indexRelPath: "index.html",
|
|
650
|
+
}),
|
|
651
|
+
milestoneId,
|
|
652
|
+
milestoneTitle: msTitle,
|
|
653
|
+
kind: "milestone",
|
|
654
|
+
projectName: projName,
|
|
655
|
+
projectPath: s.basePath,
|
|
656
|
+
gsdVersion,
|
|
657
|
+
totalCost: snapData.totals?.cost ?? 0,
|
|
658
|
+
totalTokens: snapData.totals?.tokens.total ?? 0,
|
|
659
|
+
totalDuration: snapData.totals?.duration ?? 0,
|
|
660
|
+
doneSlices,
|
|
661
|
+
totalSlices,
|
|
662
|
+
doneMilestones: snapData.milestones.filter(
|
|
663
|
+
(m: { status: string }) => m.status === "complete",
|
|
664
|
+
).length,
|
|
665
|
+
totalMilestones: snapData.milestones.length,
|
|
666
|
+
phase: snapData.phase,
|
|
667
|
+
});
|
|
668
|
+
ctx.ui.notify(
|
|
669
|
+
`Report saved: .gsd/reports/${basename(outPath)} — open index.html to browse progression.`,
|
|
670
|
+
"info",
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ─── closeoutAndStop ──────────────────────────────────────────────────────────
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* If a unit is in-flight, close it out, then stop auto-mode.
|
|
678
|
+
* Extracted from ~4 identical if-closeout-then-stop sequences in autoLoop.
|
|
679
|
+
*/
|
|
680
|
+
async function closeoutAndStop(
|
|
527
681
|
ctx: ExtensionContext,
|
|
528
682
|
pi: ExtensionAPI,
|
|
529
683
|
s: AutoSession,
|
|
530
684
|
deps: LoopDeps,
|
|
685
|
+
reason: string,
|
|
531
686
|
): Promise<void> {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
687
|
+
if (s.currentUnit) {
|
|
688
|
+
await deps.closeoutUnit(
|
|
689
|
+
ctx,
|
|
690
|
+
s.basePath,
|
|
691
|
+
s.currentUnit.type,
|
|
692
|
+
s.currentUnit.id,
|
|
693
|
+
s.currentUnit.startedAt,
|
|
694
|
+
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
await deps.stopAuto(ctx, pi, reason);
|
|
698
|
+
}
|
|
537
699
|
|
|
538
|
-
|
|
700
|
+
// ─── runPreDispatch ───────────────────────────────────────────────────────────
|
|
539
701
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
702
|
+
/**
|
|
703
|
+
* Phase 1: Pre-dispatch — resource guard, health gate, state derivation,
|
|
704
|
+
* milestone transition, terminal conditions.
|
|
705
|
+
* Returns break to exit the loop, or next with PreDispatchData on success.
|
|
706
|
+
*/
|
|
707
|
+
async function runPreDispatch(
|
|
708
|
+
ic: IterationContext,
|
|
709
|
+
loopState: LoopState,
|
|
710
|
+
): Promise<PhaseResult<PreDispatchData>> {
|
|
711
|
+
const { ctx, pi, s, deps, prefs } = ic;
|
|
543
712
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
713
|
+
// Resource version guard
|
|
714
|
+
const staleMsg = deps.checkResourcesStale(s.resourceVersionOnStart);
|
|
715
|
+
if (staleMsg) {
|
|
716
|
+
await deps.stopAuto(ctx, pi, staleMsg);
|
|
717
|
+
debugLog("autoLoop", { phase: "exit", reason: "resources-stale" });
|
|
718
|
+
return { action: "break", reason: "resources-stale" };
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
deps.invalidateAllCaches();
|
|
722
|
+
s.lastPromptCharCount = undefined;
|
|
723
|
+
s.lastBaselineCharCount = undefined;
|
|
724
|
+
|
|
725
|
+
// Pre-dispatch health gate
|
|
726
|
+
try {
|
|
727
|
+
const healthGate = await deps.preDispatchHealthGate(s.basePath);
|
|
728
|
+
if (healthGate.fixesApplied.length > 0) {
|
|
729
|
+
ctx.ui.notify(
|
|
730
|
+
`Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`,
|
|
731
|
+
"info",
|
|
554
732
|
);
|
|
555
|
-
break;
|
|
556
733
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
734
|
+
if (!healthGate.proceed) {
|
|
735
|
+
ctx.ui.notify(
|
|
736
|
+
healthGate.reason ?? "Pre-dispatch health check failed.",
|
|
737
|
+
"error",
|
|
738
|
+
);
|
|
739
|
+
await deps.pauseAuto(ctx, pi);
|
|
740
|
+
debugLog("autoLoop", { phase: "exit", reason: "health-gate-failed" });
|
|
741
|
+
return { action: "break", reason: "health-gate-failed" };
|
|
561
742
|
}
|
|
743
|
+
} catch {
|
|
744
|
+
// Non-fatal
|
|
745
|
+
}
|
|
562
746
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
});
|
|
576
|
-
deps.handleLostSessionLock(ctx, lockStatus);
|
|
577
|
-
debugLog("autoLoop", {
|
|
578
|
-
phase: "exit",
|
|
579
|
-
reason: "session-lock-lost",
|
|
580
|
-
detail: lockStatus.failureReason ?? "unknown",
|
|
581
|
-
});
|
|
582
|
-
break;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
747
|
+
// Sync project root artifacts into worktree
|
|
748
|
+
if (
|
|
749
|
+
s.originalBasePath &&
|
|
750
|
+
s.basePath !== s.originalBasePath &&
|
|
751
|
+
s.currentMilestoneId
|
|
752
|
+
) {
|
|
753
|
+
deps.syncProjectRootToWorktree(
|
|
754
|
+
s.originalBasePath,
|
|
755
|
+
s.basePath,
|
|
756
|
+
s.currentMilestoneId,
|
|
757
|
+
);
|
|
758
|
+
}
|
|
585
759
|
|
|
586
|
-
|
|
760
|
+
// Derive state
|
|
761
|
+
let state = await deps.deriveState(s.basePath);
|
|
762
|
+
deps.syncCmuxSidebar(prefs, state);
|
|
763
|
+
let mid = state.activeMilestone?.id;
|
|
764
|
+
let midTitle = state.activeMilestone?.title;
|
|
765
|
+
debugLog("autoLoop", {
|
|
766
|
+
phase: "state-derived",
|
|
767
|
+
iteration: ic.iteration,
|
|
768
|
+
mid,
|
|
769
|
+
statePhase: state.phase,
|
|
770
|
+
});
|
|
587
771
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
772
|
+
// ── Milestone transition ────────────────────────────────────────────
|
|
773
|
+
if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
|
|
774
|
+
ctx.ui.notify(
|
|
775
|
+
`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`,
|
|
776
|
+
"info",
|
|
777
|
+
);
|
|
778
|
+
deps.sendDesktopNotification(
|
|
779
|
+
"GSD",
|
|
780
|
+
`Milestone ${s.currentMilestoneId} complete!`,
|
|
781
|
+
"success",
|
|
782
|
+
"milestone",
|
|
783
|
+
);
|
|
784
|
+
deps.logCmuxEvent(
|
|
785
|
+
prefs,
|
|
786
|
+
`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`,
|
|
787
|
+
"success",
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
const vizPrefs = prefs;
|
|
791
|
+
if (vizPrefs?.auto_visualize) {
|
|
792
|
+
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
793
|
+
}
|
|
794
|
+
if (vizPrefs?.auto_report !== false) {
|
|
795
|
+
try {
|
|
796
|
+
await generateMilestoneReport(s, ctx, s.currentMilestoneId!);
|
|
797
|
+
} catch (err) {
|
|
798
|
+
ctx.ui.notify(
|
|
799
|
+
`Report generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
800
|
+
"warning",
|
|
801
|
+
);
|
|
594
802
|
}
|
|
803
|
+
}
|
|
595
804
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
805
|
+
// Reset dispatch counters for new milestone
|
|
806
|
+
s.unitDispatchCount.clear();
|
|
807
|
+
s.unitRecoveryCount.clear();
|
|
808
|
+
s.unitLifetimeDispatches.clear();
|
|
809
|
+
loopState.recentUnits.length = 0;
|
|
810
|
+
loopState.stuckRecoveryAttempts = 0;
|
|
599
811
|
|
|
600
|
-
|
|
812
|
+
// Worktree lifecycle on milestone transition — merge current, enter next
|
|
813
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId!, ctx.ui);
|
|
814
|
+
|
|
815
|
+
// Opt-in: create draft PR on milestone completion
|
|
816
|
+
if (prefs?.git?.auto_pr) {
|
|
601
817
|
try {
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
if (
|
|
610
|
-
ctx.ui.notify(
|
|
611
|
-
healthGate.reason ?? "Pre-dispatch health check failed.",
|
|
612
|
-
"error",
|
|
613
|
-
);
|
|
614
|
-
await deps.pauseAuto(ctx, pi);
|
|
615
|
-
debugLog("autoLoop", { phase: "exit", reason: "health-gate-failed" });
|
|
616
|
-
break;
|
|
818
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
819
|
+
const prUrl = createDraftPR(
|
|
820
|
+
s.basePath,
|
|
821
|
+
s.currentMilestoneId!,
|
|
822
|
+
`[GSD] ${s.currentMilestoneId} complete`,
|
|
823
|
+
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
824
|
+
);
|
|
825
|
+
if (prUrl) {
|
|
826
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
617
827
|
}
|
|
618
828
|
} catch {
|
|
619
|
-
// Non-fatal
|
|
829
|
+
// Non-fatal — PR creation is best-effort
|
|
620
830
|
}
|
|
831
|
+
}
|
|
621
832
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
);
|
|
833
|
+
deps.invalidateAllCaches();
|
|
834
|
+
|
|
835
|
+
state = await deps.deriveState(s.basePath);
|
|
836
|
+
mid = state.activeMilestone?.id;
|
|
837
|
+
midTitle = state.activeMilestone?.title;
|
|
838
|
+
|
|
839
|
+
if (mid) {
|
|
840
|
+
if (deps.getIsolationMode() !== "none") {
|
|
841
|
+
deps.captureIntegrationBranch(s.basePath, mid, {
|
|
842
|
+
commitDocs: prefs?.git?.commit_docs,
|
|
843
|
+
});
|
|
633
844
|
}
|
|
845
|
+
deps.resolver.enterMilestone(mid, ctx.ui);
|
|
846
|
+
} else {
|
|
847
|
+
// mid is undefined — no milestone to capture integration branch for
|
|
848
|
+
}
|
|
634
849
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
mid,
|
|
644
|
-
statePhase: state.phase,
|
|
645
|
-
});
|
|
850
|
+
const pendingIds = state.registry
|
|
851
|
+
.filter(
|
|
852
|
+
(m: { status: string }) =>
|
|
853
|
+
m.status !== "complete" && m.status !== "parked",
|
|
854
|
+
)
|
|
855
|
+
.map((m: { id: string }) => m.id);
|
|
856
|
+
deps.pruneQueueOrder(s.basePath, pendingIds);
|
|
857
|
+
}
|
|
646
858
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
"info",
|
|
652
|
-
);
|
|
653
|
-
deps.sendDesktopNotification(
|
|
654
|
-
"GSD",
|
|
655
|
-
`Milestone ${s.currentMilestoneId} complete!`,
|
|
656
|
-
"success",
|
|
657
|
-
"milestone",
|
|
658
|
-
);
|
|
659
|
-
deps.logCmuxEvent(
|
|
660
|
-
deps.loadEffectiveGSDPreferences()?.preferences,
|
|
661
|
-
`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`,
|
|
662
|
-
"success",
|
|
663
|
-
);
|
|
859
|
+
if (mid) {
|
|
860
|
+
s.currentMilestoneId = mid;
|
|
861
|
+
deps.setActiveMilestoneId(s.basePath, mid);
|
|
862
|
+
}
|
|
664
863
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
864
|
+
// ── Terminal conditions ──────────────────────────────────────────────
|
|
865
|
+
|
|
866
|
+
if (!mid) {
|
|
867
|
+
if (s.currentUnit) {
|
|
868
|
+
await deps.closeoutUnit(
|
|
869
|
+
ctx,
|
|
870
|
+
s.basePath,
|
|
871
|
+
s.currentUnit.type,
|
|
872
|
+
s.currentUnit.id,
|
|
873
|
+
s.currentUnit.startedAt,
|
|
874
|
+
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const incomplete = state.registry.filter(
|
|
879
|
+
(m: { status: string }) =>
|
|
880
|
+
m.status !== "complete" && m.status !== "parked",
|
|
881
|
+
);
|
|
882
|
+
if (incomplete.length === 0 && state.registry.length > 0) {
|
|
883
|
+
// All milestones complete — merge milestone branch before stopping
|
|
884
|
+
if (s.currentMilestoneId) {
|
|
885
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
886
|
+
|
|
887
|
+
// Opt-in: create draft PR on milestone completion
|
|
888
|
+
if (prefs?.git?.auto_pr) {
|
|
670
889
|
try {
|
|
671
|
-
const {
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
(m: { id: string }) => m.id === s.currentMilestoneId,
|
|
678
|
-
);
|
|
679
|
-
const msTitle = completedMs?.title ?? s.currentMilestoneId;
|
|
680
|
-
const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
|
|
681
|
-
const projName = basename(s.basePath);
|
|
682
|
-
const doneSlices = snapData.milestones.reduce(
|
|
683
|
-
(acc: number, m: { slices: { done: boolean }[] }) =>
|
|
684
|
-
acc +
|
|
685
|
-
m.slices.filter((sl: { done: boolean }) => sl.done).length,
|
|
686
|
-
0,
|
|
687
|
-
);
|
|
688
|
-
const totalSlices = snapData.milestones.reduce(
|
|
689
|
-
(acc: number, m: { slices: unknown[] }) => acc + m.slices.length,
|
|
690
|
-
0,
|
|
691
|
-
);
|
|
692
|
-
const outPath = writeReportSnapshot({
|
|
693
|
-
basePath: s.basePath,
|
|
694
|
-
html: generateHtmlReport(snapData, {
|
|
695
|
-
projectName: projName,
|
|
696
|
-
projectPath: s.basePath,
|
|
697
|
-
gsdVersion,
|
|
698
|
-
milestoneId: s.currentMilestoneId,
|
|
699
|
-
indexRelPath: "index.html",
|
|
700
|
-
}),
|
|
701
|
-
milestoneId: s.currentMilestoneId!,
|
|
702
|
-
milestoneTitle: msTitle,
|
|
703
|
-
kind: "milestone",
|
|
704
|
-
projectName: projName,
|
|
705
|
-
projectPath: s.basePath,
|
|
706
|
-
gsdVersion,
|
|
707
|
-
totalCost: snapData.totals?.cost ?? 0,
|
|
708
|
-
totalTokens: snapData.totals?.tokens.total ?? 0,
|
|
709
|
-
totalDuration: snapData.totals?.duration ?? 0,
|
|
710
|
-
doneSlices,
|
|
711
|
-
totalSlices,
|
|
712
|
-
doneMilestones: snapData.milestones.filter(
|
|
713
|
-
(m: { status: string }) => m.status === "complete",
|
|
714
|
-
).length,
|
|
715
|
-
totalMilestones: snapData.milestones.length,
|
|
716
|
-
phase: snapData.phase,
|
|
717
|
-
});
|
|
718
|
-
ctx.ui.notify(
|
|
719
|
-
`Report saved: .gsd/reports/${(await import("node:path")).basename(outPath)} — open index.html to browse progression.`,
|
|
720
|
-
"info",
|
|
721
|
-
);
|
|
722
|
-
} catch (err) {
|
|
723
|
-
ctx.ui.notify(
|
|
724
|
-
`Report generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
725
|
-
"warning",
|
|
890
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
891
|
+
const prUrl = createDraftPR(
|
|
892
|
+
s.basePath,
|
|
893
|
+
s.currentMilestoneId,
|
|
894
|
+
`[GSD] ${s.currentMilestoneId} complete`,
|
|
895
|
+
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
726
896
|
);
|
|
897
|
+
if (prUrl) {
|
|
898
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
899
|
+
}
|
|
900
|
+
} catch {
|
|
901
|
+
// Non-fatal — PR creation is best-effort
|
|
727
902
|
}
|
|
728
903
|
}
|
|
904
|
+
}
|
|
905
|
+
deps.sendDesktopNotification(
|
|
906
|
+
"GSD",
|
|
907
|
+
"All milestones complete!",
|
|
908
|
+
"success",
|
|
909
|
+
"milestone",
|
|
910
|
+
);
|
|
911
|
+
deps.logCmuxEvent(
|
|
912
|
+
prefs,
|
|
913
|
+
"All milestones complete.",
|
|
914
|
+
"success",
|
|
915
|
+
);
|
|
916
|
+
await deps.stopAuto(ctx, pi, "All milestones complete");
|
|
917
|
+
} else if (incomplete.length === 0 && state.registry.length === 0) {
|
|
918
|
+
// Empty registry — no milestones visible, likely a path resolution bug
|
|
919
|
+
const diag = `basePath=${s.basePath}, phase=${state.phase}`;
|
|
920
|
+
ctx.ui.notify(
|
|
921
|
+
`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`,
|
|
922
|
+
"error",
|
|
923
|
+
);
|
|
924
|
+
await deps.stopAuto(
|
|
925
|
+
ctx,
|
|
926
|
+
pi,
|
|
927
|
+
`No milestones found — check basePath resolution`,
|
|
928
|
+
);
|
|
929
|
+
} else if (state.phase === "blocked") {
|
|
930
|
+
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
931
|
+
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
932
|
+
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
933
|
+
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
934
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
935
|
+
} else {
|
|
936
|
+
const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
|
|
937
|
+
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
938
|
+
ctx.ui.notify(
|
|
939
|
+
`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`,
|
|
940
|
+
"error",
|
|
941
|
+
);
|
|
942
|
+
await deps.stopAuto(
|
|
943
|
+
ctx,
|
|
944
|
+
pi,
|
|
945
|
+
`No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`,
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
debugLog("autoLoop", { phase: "exit", reason: "no-active-milestone" });
|
|
949
|
+
return { action: "break", reason: "no-active-milestone" };
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (!midTitle) {
|
|
953
|
+
midTitle = mid;
|
|
954
|
+
ctx.ui.notify(
|
|
955
|
+
`Milestone ${mid} has no title in roadmap — using ID as fallback.`,
|
|
956
|
+
"warning",
|
|
957
|
+
);
|
|
958
|
+
}
|
|
729
959
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
960
|
+
// Mid-merge safety check
|
|
961
|
+
if (deps.reconcileMergeState(s.basePath, ctx)) {
|
|
962
|
+
deps.invalidateAllCaches();
|
|
963
|
+
state = await deps.deriveState(s.basePath);
|
|
964
|
+
mid = state.activeMilestone?.id;
|
|
965
|
+
midTitle = state.activeMilestone?.title;
|
|
966
|
+
}
|
|
736
967
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
968
|
+
if (!mid || !midTitle) {
|
|
969
|
+
const noMilestoneReason = !mid
|
|
970
|
+
? "No active milestone after merge reconciliation"
|
|
971
|
+
: `Milestone ${mid} has no title after reconciliation`;
|
|
972
|
+
await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
|
|
973
|
+
debugLog("autoLoop", {
|
|
974
|
+
phase: "exit",
|
|
975
|
+
reason: "no-milestone-after-reconciliation",
|
|
976
|
+
});
|
|
977
|
+
return { action: "break", reason: "no-milestone-after-reconciliation" };
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Terminal: complete
|
|
981
|
+
if (state.phase === "complete") {
|
|
982
|
+
// Milestone merge on complete (before closeout so branch state is clean)
|
|
983
|
+
if (s.currentMilestoneId) {
|
|
984
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
740
985
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
986
|
+
// Opt-in: create draft PR on milestone completion
|
|
987
|
+
if (prefs?.git?.auto_pr) {
|
|
988
|
+
try {
|
|
989
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
990
|
+
const prUrl = createDraftPR(
|
|
991
|
+
s.basePath,
|
|
992
|
+
s.currentMilestoneId,
|
|
993
|
+
`[GSD] ${s.currentMilestoneId} complete`,
|
|
994
|
+
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
995
|
+
);
|
|
996
|
+
if (prUrl) {
|
|
997
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
752
998
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
// mid is undefined — no milestone to capture integration branch for
|
|
999
|
+
} catch {
|
|
1000
|
+
// Non-fatal — PR creation is best-effort
|
|
756
1001
|
}
|
|
757
|
-
|
|
758
|
-
const pendingIds = state.registry
|
|
759
|
-
.filter(
|
|
760
|
-
(m: { status: string }) =>
|
|
761
|
-
m.status !== "complete" && m.status !== "parked",
|
|
762
|
-
)
|
|
763
|
-
.map((m: { id: string }) => m.id);
|
|
764
|
-
deps.pruneQueueOrder(s.basePath, pendingIds);
|
|
765
1002
|
}
|
|
1003
|
+
}
|
|
1004
|
+
deps.sendDesktopNotification(
|
|
1005
|
+
"GSD",
|
|
1006
|
+
`Milestone ${mid} complete!`,
|
|
1007
|
+
"success",
|
|
1008
|
+
"milestone",
|
|
1009
|
+
);
|
|
1010
|
+
deps.logCmuxEvent(
|
|
1011
|
+
prefs,
|
|
1012
|
+
`Milestone ${mid} complete.`,
|
|
1013
|
+
"success",
|
|
1014
|
+
);
|
|
1015
|
+
await closeoutAndStop(ctx, pi, s, deps, `Milestone ${mid} complete`);
|
|
1016
|
+
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
1017
|
+
return { action: "break", reason: "milestone-complete" };
|
|
1018
|
+
}
|
|
766
1019
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1020
|
+
// Terminal: blocked
|
|
1021
|
+
if (state.phase === "blocked") {
|
|
1022
|
+
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
1023
|
+
await closeoutAndStop(ctx, pi, s, deps, blockerMsg);
|
|
1024
|
+
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
1025
|
+
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
1026
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
1027
|
+
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
1028
|
+
return { action: "break", reason: "blocked" };
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
return { action: "next", data: { state, mid, midTitle } };
|
|
1032
|
+
}
|
|
771
1033
|
|
|
772
|
-
|
|
1034
|
+
// ─── runDispatch ──────────────────────────────────────────────────────────────
|
|
773
1035
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1036
|
+
/**
|
|
1037
|
+
* Phase 3: Dispatch resolution — resolve next unit, stuck detection, pre-dispatch hooks.
|
|
1038
|
+
* Returns break/continue to control the loop, or next with IterationData on success.
|
|
1039
|
+
*/
|
|
1040
|
+
async function runDispatch(
|
|
1041
|
+
ic: IterationContext,
|
|
1042
|
+
preData: PreDispatchData,
|
|
1043
|
+
loopState: LoopState,
|
|
1044
|
+
): Promise<PhaseResult<IterationData>> {
|
|
1045
|
+
const { ctx, pi, s, deps, prefs } = ic;
|
|
1046
|
+
const { state, mid, midTitle } = preData;
|
|
1047
|
+
const STUCK_WINDOW_SIZE = 6;
|
|
1048
|
+
|
|
1049
|
+
debugLog("autoLoop", { phase: "dispatch-resolve", iteration: ic.iteration });
|
|
1050
|
+
const dispatchResult = await deps.resolveDispatch({
|
|
1051
|
+
basePath: s.basePath,
|
|
1052
|
+
mid,
|
|
1053
|
+
midTitle,
|
|
1054
|
+
state,
|
|
1055
|
+
prefs,
|
|
1056
|
+
session: s,
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
if (dispatchResult.action === "stop") {
|
|
1060
|
+
await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
|
|
1061
|
+
debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
|
|
1062
|
+
return { action: "break", reason: "dispatch-stop" };
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (dispatchResult.action !== "dispatch") {
|
|
1066
|
+
// Non-dispatch action (e.g. "skip") — re-derive state
|
|
1067
|
+
await new Promise((r) => setImmediate(r));
|
|
1068
|
+
return { action: "continue" };
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
let unitType = dispatchResult.unitType;
|
|
1072
|
+
let unitId = dispatchResult.unitId;
|
|
1073
|
+
let prompt = dispatchResult.prompt;
|
|
1074
|
+
const pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
785
1075
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1076
|
+
// ── Sliding-window stuck detection with graduated recovery ──
|
|
1077
|
+
const derivedKey = `${unitType}/${unitId}`;
|
|
1078
|
+
|
|
1079
|
+
if (!s.pendingVerificationRetry) {
|
|
1080
|
+
loopState.recentUnits.push({ key: derivedKey });
|
|
1081
|
+
if (loopState.recentUnits.length > STUCK_WINDOW_SIZE) loopState.recentUnits.shift();
|
|
1082
|
+
|
|
1083
|
+
const stuckSignal = detectStuck(loopState.recentUnits);
|
|
1084
|
+
if (stuckSignal) {
|
|
1085
|
+
debugLog("autoLoop", {
|
|
1086
|
+
phase: "stuck-check",
|
|
1087
|
+
unitType,
|
|
1088
|
+
unitId,
|
|
1089
|
+
reason: stuckSignal.reason,
|
|
1090
|
+
recoveryAttempts: loopState.stuckRecoveryAttempts,
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
if (loopState.stuckRecoveryAttempts === 0) {
|
|
1094
|
+
// Level 1: try verifying the artifact, then cache invalidation + retry
|
|
1095
|
+
loopState.stuckRecoveryAttempts++;
|
|
1096
|
+
const artifactExists = deps.verifyExpectedArtifact(
|
|
1097
|
+
unitType,
|
|
1098
|
+
unitId,
|
|
1099
|
+
s.basePath,
|
|
789
1100
|
);
|
|
790
|
-
if (
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
"GSD",
|
|
797
|
-
"All milestones complete!",
|
|
798
|
-
"success",
|
|
799
|
-
"milestone",
|
|
800
|
-
);
|
|
801
|
-
deps.logCmuxEvent(
|
|
802
|
-
deps.loadEffectiveGSDPreferences()?.preferences,
|
|
803
|
-
"All milestones complete.",
|
|
804
|
-
"success",
|
|
805
|
-
);
|
|
806
|
-
await deps.stopAuto(ctx, pi, "All milestones complete");
|
|
807
|
-
} else if (state.phase === "blocked") {
|
|
808
|
-
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
809
|
-
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
810
|
-
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
811
|
-
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
812
|
-
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
|
|
813
|
-
} else {
|
|
814
|
-
const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
|
|
815
|
-
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
1101
|
+
if (artifactExists) {
|
|
1102
|
+
debugLog("autoLoop", {
|
|
1103
|
+
phase: "stuck-recovery",
|
|
1104
|
+
level: 1,
|
|
1105
|
+
action: "artifact-found",
|
|
1106
|
+
});
|
|
816
1107
|
ctx.ui.notify(
|
|
817
|
-
`
|
|
818
|
-
"
|
|
819
|
-
);
|
|
820
|
-
await deps.stopAuto(
|
|
821
|
-
ctx,
|
|
822
|
-
pi,
|
|
823
|
-
`No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`,
|
|
1108
|
+
`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`,
|
|
1109
|
+
"info",
|
|
824
1110
|
);
|
|
1111
|
+
deps.invalidateAllCaches();
|
|
1112
|
+
return { action: "continue" };
|
|
825
1113
|
}
|
|
826
|
-
debugLog("autoLoop", { phase: "exit", reason: "no-active-milestone" });
|
|
827
|
-
break;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
if (!midTitle) {
|
|
831
|
-
midTitle = mid;
|
|
832
1114
|
ctx.ui.notify(
|
|
833
|
-
`
|
|
1115
|
+
`Stuck on ${unitType} ${unitId} (${stuckSignal.reason}). Invalidating caches and retrying.`,
|
|
834
1116
|
"warning",
|
|
835
1117
|
);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// Mid-merge safety check
|
|
839
|
-
if (deps.reconcileMergeState(s.basePath, ctx)) {
|
|
840
1118
|
deps.invalidateAllCaches();
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
midTitle = state.activeMilestone?.title;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
if (!mid || !midTitle) {
|
|
847
|
-
if (s.currentUnit) {
|
|
848
|
-
await deps.closeoutUnit(
|
|
849
|
-
ctx,
|
|
850
|
-
s.basePath,
|
|
851
|
-
s.currentUnit.type,
|
|
852
|
-
s.currentUnit.id,
|
|
853
|
-
s.currentUnit.startedAt,
|
|
854
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
855
|
-
);
|
|
856
|
-
}
|
|
857
|
-
const noMilestoneReason = !mid
|
|
858
|
-
? "No active milestone after merge reconciliation"
|
|
859
|
-
: `Milestone ${mid} has no title after reconciliation`;
|
|
860
|
-
await deps.stopAuto(ctx, pi, noMilestoneReason);
|
|
1119
|
+
} else {
|
|
1120
|
+
// Level 2: hard stop — genuinely stuck
|
|
861
1121
|
debugLog("autoLoop", {
|
|
862
|
-
phase: "
|
|
863
|
-
|
|
1122
|
+
phase: "stuck-detected",
|
|
1123
|
+
unitType,
|
|
1124
|
+
unitId,
|
|
1125
|
+
reason: stuckSignal.reason,
|
|
864
1126
|
});
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
if (state.phase === "complete") {
|
|
870
|
-
if (s.currentUnit) {
|
|
871
|
-
await deps.closeoutUnit(
|
|
872
|
-
ctx,
|
|
873
|
-
s.basePath,
|
|
874
|
-
s.currentUnit.type,
|
|
875
|
-
s.currentUnit.id,
|
|
876
|
-
s.currentUnit.startedAt,
|
|
877
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
878
|
-
);
|
|
879
|
-
}
|
|
880
|
-
// Milestone merge on complete
|
|
881
|
-
if (s.currentMilestoneId) {
|
|
882
|
-
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
883
|
-
}
|
|
884
|
-
deps.sendDesktopNotification(
|
|
885
|
-
"GSD",
|
|
886
|
-
`Milestone ${mid} complete!`,
|
|
887
|
-
"success",
|
|
888
|
-
"milestone",
|
|
1127
|
+
await deps.stopAuto(
|
|
1128
|
+
ctx,
|
|
1129
|
+
pi,
|
|
1130
|
+
`Stuck: ${stuckSignal.reason}`,
|
|
889
1131
|
);
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
"success",
|
|
1132
|
+
ctx.ui.notify(
|
|
1133
|
+
`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}. The expected artifact was not written.`,
|
|
1134
|
+
"error",
|
|
894
1135
|
);
|
|
895
|
-
|
|
896
|
-
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
897
|
-
break;
|
|
1136
|
+
return { action: "break", reason: "stuck-detected" };
|
|
898
1137
|
}
|
|
899
|
-
|
|
900
|
-
//
|
|
901
|
-
if (
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
s.currentUnit.startedAt,
|
|
909
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
910
|
-
);
|
|
911
|
-
}
|
|
912
|
-
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
913
|
-
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
914
|
-
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
915
|
-
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
916
|
-
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
|
|
917
|
-
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
918
|
-
break;
|
|
1138
|
+
} else {
|
|
1139
|
+
// Progress detected — reset recovery counter
|
|
1140
|
+
if (loopState.stuckRecoveryAttempts > 0) {
|
|
1141
|
+
debugLog("autoLoop", {
|
|
1142
|
+
phase: "stuck-counter-reset",
|
|
1143
|
+
from: loopState.recentUnits[loopState.recentUnits.length - 2]?.key ?? "",
|
|
1144
|
+
to: derivedKey,
|
|
1145
|
+
});
|
|
1146
|
+
loopState.stuckRecoveryAttempts = 0;
|
|
919
1147
|
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
920
1150
|
|
|
921
|
-
|
|
1151
|
+
// Pre-dispatch hooks
|
|
1152
|
+
const preDispatchResult = deps.runPreDispatchHooks(
|
|
1153
|
+
unitType,
|
|
1154
|
+
unitId,
|
|
1155
|
+
prompt,
|
|
1156
|
+
s.basePath,
|
|
1157
|
+
);
|
|
1158
|
+
if (preDispatchResult.firedHooks.length > 0) {
|
|
1159
|
+
ctx.ui.notify(
|
|
1160
|
+
`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`,
|
|
1161
|
+
"info",
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
if (preDispatchResult.action === "skip") {
|
|
1165
|
+
ctx.ui.notify(
|
|
1166
|
+
`Skipping ${unitType} ${unitId} (pre-dispatch hook).`,
|
|
1167
|
+
"info",
|
|
1168
|
+
);
|
|
1169
|
+
await new Promise((r) => setImmediate(r));
|
|
1170
|
+
return { action: "continue" };
|
|
1171
|
+
}
|
|
1172
|
+
if (preDispatchResult.action === "replace") {
|
|
1173
|
+
prompt = preDispatchResult.prompt ?? prompt;
|
|
1174
|
+
if (preDispatchResult.unitType) unitType = preDispatchResult.unitType;
|
|
1175
|
+
} else if (preDispatchResult.prompt) {
|
|
1176
|
+
prompt = preDispatchResult.prompt;
|
|
1177
|
+
}
|
|
922
1178
|
|
|
923
|
-
|
|
1179
|
+
const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(
|
|
1180
|
+
s.basePath,
|
|
1181
|
+
deps.getMainBranch(s.basePath),
|
|
1182
|
+
unitType,
|
|
1183
|
+
unitId,
|
|
1184
|
+
);
|
|
1185
|
+
if (priorSliceBlocker) {
|
|
1186
|
+
await deps.stopAuto(ctx, pi, priorSliceBlocker);
|
|
1187
|
+
debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
|
|
1188
|
+
return { action: "break", reason: "prior-slice-blocker" };
|
|
1189
|
+
}
|
|
924
1190
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
: 0;
|
|
932
|
-
const budgetPct = totalCost / budgetCeiling;
|
|
933
|
-
const budgetAlertLevel = deps.getBudgetAlertLevel(budgetPct);
|
|
934
|
-
const newBudgetAlertLevel = deps.getNewBudgetAlertLevel(
|
|
935
|
-
s.lastBudgetAlertLevel,
|
|
936
|
-
budgetPct,
|
|
937
|
-
);
|
|
938
|
-
const enforcement = prefs?.budget_enforcement ?? "pause";
|
|
939
|
-
const budgetEnforcementAction = deps.getBudgetEnforcementAction(
|
|
940
|
-
enforcement,
|
|
941
|
-
budgetPct,
|
|
942
|
-
);
|
|
1191
|
+
const observabilityIssues = await deps.collectObservabilityWarnings(
|
|
1192
|
+
ctx,
|
|
1193
|
+
s.basePath,
|
|
1194
|
+
unitType,
|
|
1195
|
+
unitId,
|
|
1196
|
+
);
|
|
943
1197
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
if (budgetEnforcementAction === "pause") {
|
|
955
|
-
ctx.ui.notify(
|
|
956
|
-
`${msg} Pausing auto-mode — /gsd auto to override and continue.`,
|
|
957
|
-
"warning",
|
|
958
|
-
);
|
|
959
|
-
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
960
|
-
deps.logCmuxEvent(prefs, msg, "warning");
|
|
961
|
-
await deps.pauseAuto(ctx, pi);
|
|
962
|
-
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
963
|
-
break;
|
|
964
|
-
}
|
|
965
|
-
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
966
|
-
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
967
|
-
deps.logCmuxEvent(prefs, msg, "warning");
|
|
968
|
-
} else if (newBudgetAlertLevel === 90) {
|
|
969
|
-
s.lastBudgetAlertLevel =
|
|
970
|
-
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
971
|
-
ctx.ui.notify(
|
|
972
|
-
`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
973
|
-
"warning",
|
|
974
|
-
);
|
|
975
|
-
deps.sendDesktopNotification(
|
|
976
|
-
"GSD",
|
|
977
|
-
`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
978
|
-
"warning",
|
|
979
|
-
"budget",
|
|
980
|
-
);
|
|
981
|
-
deps.logCmuxEvent(
|
|
982
|
-
prefs,
|
|
983
|
-
`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
984
|
-
"warning",
|
|
985
|
-
);
|
|
986
|
-
} else if (newBudgetAlertLevel === 80) {
|
|
987
|
-
s.lastBudgetAlertLevel =
|
|
988
|
-
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
989
|
-
ctx.ui.notify(
|
|
990
|
-
`Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
991
|
-
"warning",
|
|
992
|
-
);
|
|
993
|
-
deps.sendDesktopNotification(
|
|
994
|
-
"GSD",
|
|
995
|
-
`Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
996
|
-
"warning",
|
|
997
|
-
"budget",
|
|
998
|
-
);
|
|
999
|
-
deps.logCmuxEvent(
|
|
1000
|
-
prefs,
|
|
1001
|
-
`Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1002
|
-
"warning",
|
|
1003
|
-
);
|
|
1004
|
-
} else if (newBudgetAlertLevel === 75) {
|
|
1005
|
-
s.lastBudgetAlertLevel =
|
|
1006
|
-
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
1007
|
-
ctx.ui.notify(
|
|
1008
|
-
`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1009
|
-
"info",
|
|
1010
|
-
);
|
|
1011
|
-
deps.sendDesktopNotification(
|
|
1012
|
-
"GSD",
|
|
1013
|
-
`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1014
|
-
"info",
|
|
1015
|
-
"budget",
|
|
1016
|
-
);
|
|
1017
|
-
deps.logCmuxEvent(
|
|
1018
|
-
prefs,
|
|
1019
|
-
`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1020
|
-
"progress",
|
|
1021
|
-
);
|
|
1022
|
-
} else if (budgetAlertLevel === 0) {
|
|
1023
|
-
s.lastBudgetAlertLevel = 0;
|
|
1024
|
-
}
|
|
1025
|
-
} else {
|
|
1026
|
-
s.lastBudgetAlertLevel = 0;
|
|
1027
|
-
}
|
|
1198
|
+
return {
|
|
1199
|
+
action: "next",
|
|
1200
|
+
data: {
|
|
1201
|
+
unitType, unitId, prompt, finalPrompt: prompt,
|
|
1202
|
+
pauseAfterUatDispatch, observabilityIssues,
|
|
1203
|
+
state, mid, midTitle,
|
|
1204
|
+
isRetry: false, previousTier: undefined,
|
|
1205
|
+
},
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1028
1208
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1209
|
+
// ─── runGuards ────────────────────────────────────────────────────────────────
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Phase 2: Guards — budget ceiling, context window, secrets re-check.
|
|
1213
|
+
* Returns break to exit the loop, or next to proceed to dispatch.
|
|
1214
|
+
*/
|
|
1215
|
+
async function runGuards(
|
|
1216
|
+
ic: IterationContext,
|
|
1217
|
+
mid: string,
|
|
1218
|
+
): Promise<PhaseResult> {
|
|
1219
|
+
const { ctx, pi, s, deps, prefs } = ic;
|
|
1220
|
+
|
|
1221
|
+
// Budget ceiling guard
|
|
1222
|
+
const budgetCeiling = prefs?.budget_ceiling;
|
|
1223
|
+
if (budgetCeiling !== undefined && budgetCeiling > 0) {
|
|
1224
|
+
const currentLedger = deps.getLedger() as { units: unknown } | null;
|
|
1225
|
+
const totalCost = currentLedger
|
|
1226
|
+
? deps.getProjectTotals(currentLedger.units).cost
|
|
1227
|
+
: 0;
|
|
1228
|
+
const budgetPct = totalCost / budgetCeiling;
|
|
1229
|
+
const budgetAlertLevel = deps.getBudgetAlertLevel(budgetPct);
|
|
1230
|
+
const newBudgetAlertLevel = deps.getNewBudgetAlertLevel(
|
|
1231
|
+
s.lastBudgetAlertLevel,
|
|
1232
|
+
budgetPct,
|
|
1233
|
+
);
|
|
1234
|
+
const enforcement = prefs?.budget_enforcement ?? "pause";
|
|
1235
|
+
const budgetEnforcementAction = deps.getBudgetEnforcementAction(
|
|
1236
|
+
enforcement,
|
|
1237
|
+
budgetPct,
|
|
1238
|
+
);
|
|
1239
|
+
|
|
1240
|
+
// Data-driven threshold check — loop descending, fire first match
|
|
1241
|
+
const threshold = BUDGET_THRESHOLDS.find(
|
|
1242
|
+
(t) => newBudgetAlertLevel >= t.pct,
|
|
1243
|
+
);
|
|
1244
|
+
if (threshold) {
|
|
1245
|
+
s.lastBudgetAlertLevel =
|
|
1246
|
+
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
1247
|
+
|
|
1248
|
+
if (threshold.pct === 100 && budgetEnforcementAction !== "none") {
|
|
1249
|
+
// 100% — special enforcement logic (halt/pause/warn)
|
|
1250
|
+
const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
|
|
1251
|
+
if (budgetEnforcementAction === "halt") {
|
|
1252
|
+
deps.sendDesktopNotification("GSD", msg, "error", "budget");
|
|
1253
|
+
await deps.stopAuto(ctx, pi, "Budget ceiling reached");
|
|
1254
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
|
|
1255
|
+
return { action: "break", reason: "budget-halt" };
|
|
1256
|
+
}
|
|
1257
|
+
if (budgetEnforcementAction === "pause") {
|
|
1039
1258
|
ctx.ui.notify(
|
|
1040
|
-
`${msg}
|
|
1259
|
+
`${msg} Pausing auto-mode — /gsd auto to override and continue.`,
|
|
1041
1260
|
"warning",
|
|
1042
1261
|
);
|
|
1043
|
-
deps.sendDesktopNotification(
|
|
1044
|
-
|
|
1045
|
-
`Context ${contextUsage.percent}% — paused`,
|
|
1046
|
-
"warning",
|
|
1047
|
-
"attention",
|
|
1048
|
-
);
|
|
1262
|
+
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
1263
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
1049
1264
|
await deps.pauseAuto(ctx, pi);
|
|
1050
|
-
debugLog("autoLoop", { phase: "exit", reason: "
|
|
1051
|
-
break;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
// Secrets re-check gate
|
|
1056
|
-
try {
|
|
1057
|
-
const manifestStatus = await deps.getManifestStatus(s.basePath, mid, s.originalBasePath);
|
|
1058
|
-
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
1059
|
-
const result = await deps.collectSecretsFromManifest(
|
|
1060
|
-
s.basePath,
|
|
1061
|
-
mid,
|
|
1062
|
-
ctx,
|
|
1063
|
-
);
|
|
1064
|
-
if (
|
|
1065
|
-
result &&
|
|
1066
|
-
result.applied &&
|
|
1067
|
-
result.skipped &&
|
|
1068
|
-
result.existingSkipped
|
|
1069
|
-
) {
|
|
1070
|
-
ctx.ui.notify(
|
|
1071
|
-
`Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`,
|
|
1072
|
-
"info",
|
|
1073
|
-
);
|
|
1074
|
-
} else {
|
|
1075
|
-
ctx.ui.notify("Secrets collection skipped.", "info");
|
|
1076
|
-
}
|
|
1265
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
1266
|
+
return { action: "break", reason: "budget-pause" };
|
|
1077
1267
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1268
|
+
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
1269
|
+
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
1270
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
1271
|
+
} else if (threshold.pct < 100) {
|
|
1272
|
+
// Sub-100% — simple notification
|
|
1273
|
+
const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
|
|
1274
|
+
ctx.ui.notify(msg, threshold.notifyLevel);
|
|
1275
|
+
deps.sendDesktopNotification(
|
|
1276
|
+
"GSD",
|
|
1277
|
+
msg,
|
|
1278
|
+
threshold.notifyLevel,
|
|
1279
|
+
"budget",
|
|
1082
1280
|
);
|
|
1281
|
+
deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
|
|
1083
1282
|
}
|
|
1283
|
+
} else if (budgetAlertLevel === 0) {
|
|
1284
|
+
s.lastBudgetAlertLevel = 0;
|
|
1285
|
+
}
|
|
1286
|
+
} else {
|
|
1287
|
+
s.lastBudgetAlertLevel = 0;
|
|
1288
|
+
}
|
|
1084
1289
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
})
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
if (dispatchResult.action !== "dispatch") {
|
|
1113
|
-
// Non-dispatch action (e.g. "skip") — re-derive state
|
|
1114
|
-
await new Promise((r) => setImmediate(r));
|
|
1115
|
-
continue;
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
let unitType = dispatchResult.unitType;
|
|
1119
|
-
let unitId = dispatchResult.unitId;
|
|
1120
|
-
let prompt = dispatchResult.prompt;
|
|
1121
|
-
const pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
1122
|
-
|
|
1123
|
-
// ── Same-unit stuck counter with graduated recovery ──
|
|
1124
|
-
const derivedKey = `${unitType}/${unitId}`;
|
|
1125
|
-
if (derivedKey === lastDerivedUnit && !s.pendingVerificationRetry) {
|
|
1126
|
-
sameUnitCount++;
|
|
1127
|
-
debugLog("autoLoop", {
|
|
1128
|
-
phase: "stuck-check",
|
|
1129
|
-
unitType,
|
|
1130
|
-
unitId,
|
|
1131
|
-
sameUnitCount,
|
|
1132
|
-
});
|
|
1133
|
-
|
|
1134
|
-
if (sameUnitCount === 3) {
|
|
1135
|
-
// Level 1: try verifying the artifact — maybe it was written but not detected
|
|
1136
|
-
const artifactExists = deps.verifyExpectedArtifact(
|
|
1137
|
-
unitType,
|
|
1138
|
-
unitId,
|
|
1139
|
-
s.basePath,
|
|
1140
|
-
);
|
|
1141
|
-
if (artifactExists) {
|
|
1142
|
-
debugLog("autoLoop", {
|
|
1143
|
-
phase: "stuck-recovery",
|
|
1144
|
-
level: 1,
|
|
1145
|
-
action: "artifact-found",
|
|
1146
|
-
});
|
|
1147
|
-
ctx.ui.notify(
|
|
1148
|
-
`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`,
|
|
1149
|
-
"info",
|
|
1150
|
-
);
|
|
1151
|
-
deps.invalidateAllCaches();
|
|
1152
|
-
continue;
|
|
1153
|
-
}
|
|
1154
|
-
ctx.ui.notify(
|
|
1155
|
-
`Stuck on ${unitType} ${unitId} (attempt ${sameUnitCount}). Invalidating caches and retrying.`,
|
|
1156
|
-
"warning",
|
|
1157
|
-
);
|
|
1158
|
-
deps.invalidateAllCaches();
|
|
1159
|
-
} else if (sameUnitCount === 5) {
|
|
1160
|
-
// Level 2: hard stop — genuinely stuck
|
|
1161
|
-
debugLog("autoLoop", {
|
|
1162
|
-
phase: "stuck-detected",
|
|
1163
|
-
unitType,
|
|
1164
|
-
unitId,
|
|
1165
|
-
sameUnitCount,
|
|
1166
|
-
});
|
|
1167
|
-
await deps.stopAuto(
|
|
1168
|
-
ctx,
|
|
1169
|
-
pi,
|
|
1170
|
-
`Stuck: ${unitType} ${unitId} derived ${sameUnitCount} consecutive times without progress`,
|
|
1171
|
-
);
|
|
1172
|
-
ctx.ui.notify(
|
|
1173
|
-
`Stuck on ${unitType} ${unitId} — deriveState returns the same unit after ${sameUnitCount} attempts. The expected artifact was not written.`,
|
|
1174
|
-
"error",
|
|
1175
|
-
);
|
|
1176
|
-
break;
|
|
1177
|
-
}
|
|
1178
|
-
} else {
|
|
1179
|
-
if (derivedKey !== lastDerivedUnit) {
|
|
1180
|
-
debugLog("autoLoop", {
|
|
1181
|
-
phase: "stuck-counter-reset",
|
|
1182
|
-
from: lastDerivedUnit,
|
|
1183
|
-
to: derivedKey,
|
|
1184
|
-
});
|
|
1185
|
-
}
|
|
1186
|
-
lastDerivedUnit = derivedKey;
|
|
1187
|
-
sameUnitCount = 0;
|
|
1188
|
-
}
|
|
1290
|
+
// Context window guard
|
|
1291
|
+
const contextThreshold = prefs?.context_pause_threshold ?? 0;
|
|
1292
|
+
if (contextThreshold > 0 && s.cmdCtx) {
|
|
1293
|
+
const contextUsage = s.cmdCtx.getContextUsage();
|
|
1294
|
+
if (
|
|
1295
|
+
contextUsage &&
|
|
1296
|
+
contextUsage.percent !== null &&
|
|
1297
|
+
contextUsage.percent >= contextThreshold
|
|
1298
|
+
) {
|
|
1299
|
+
const msg = `Context window at ${contextUsage.percent}% (threshold: ${contextThreshold}%). Pausing to prevent truncated output.`;
|
|
1300
|
+
ctx.ui.notify(
|
|
1301
|
+
`${msg} Run /gsd auto to continue (will start fresh session).`,
|
|
1302
|
+
"warning",
|
|
1303
|
+
);
|
|
1304
|
+
deps.sendDesktopNotification(
|
|
1305
|
+
"GSD",
|
|
1306
|
+
`Context ${contextUsage.percent}% — paused`,
|
|
1307
|
+
"warning",
|
|
1308
|
+
"attention",
|
|
1309
|
+
);
|
|
1310
|
+
await deps.pauseAuto(ctx, pi);
|
|
1311
|
+
debugLog("autoLoop", { phase: "exit", reason: "context-window" });
|
|
1312
|
+
return { action: "break", reason: "context-window" };
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1189
1315
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1316
|
+
// Secrets re-check gate
|
|
1317
|
+
try {
|
|
1318
|
+
const manifestStatus = await deps.getManifestStatus(s.basePath, mid, s.originalBasePath);
|
|
1319
|
+
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
1320
|
+
const result = await deps.collectSecretsFromManifest(
|
|
1195
1321
|
s.basePath,
|
|
1322
|
+
mid,
|
|
1323
|
+
ctx,
|
|
1196
1324
|
);
|
|
1197
|
-
if (
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
if (preDispatchResult.action === "skip") {
|
|
1325
|
+
if (
|
|
1326
|
+
result &&
|
|
1327
|
+
result.applied &&
|
|
1328
|
+
result.skipped &&
|
|
1329
|
+
result.existingSkipped
|
|
1330
|
+
) {
|
|
1204
1331
|
ctx.ui.notify(
|
|
1205
|
-
`
|
|
1332
|
+
`Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`,
|
|
1206
1333
|
"info",
|
|
1207
1334
|
);
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
}
|
|
1211
|
-
if (preDispatchResult.action === "replace") {
|
|
1212
|
-
prompt = preDispatchResult.prompt ?? prompt;
|
|
1213
|
-
if (preDispatchResult.unitType) unitType = preDispatchResult.unitType;
|
|
1214
|
-
} else if (preDispatchResult.prompt) {
|
|
1215
|
-
prompt = preDispatchResult.prompt;
|
|
1335
|
+
} else {
|
|
1336
|
+
ctx.ui.notify("Secrets collection skipped.", "info");
|
|
1216
1337
|
}
|
|
1338
|
+
}
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
ctx.ui.notify(
|
|
1341
|
+
`Secrets collection error: ${err instanceof Error ? err.message : String(err)}. Continuing with next task.`,
|
|
1342
|
+
"warning",
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1217
1345
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
deps.getMainBranch(s.basePath),
|
|
1221
|
-
unitType,
|
|
1222
|
-
unitId,
|
|
1223
|
-
);
|
|
1224
|
-
if (priorSliceBlocker) {
|
|
1225
|
-
await deps.stopAuto(ctx, pi, priorSliceBlocker);
|
|
1226
|
-
debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
|
|
1227
|
-
break;
|
|
1228
|
-
}
|
|
1346
|
+
return { action: "next", data: undefined as void };
|
|
1347
|
+
}
|
|
1229
1348
|
|
|
1230
|
-
|
|
1231
|
-
ctx,
|
|
1232
|
-
s.basePath,
|
|
1233
|
-
unitType,
|
|
1234
|
-
unitId,
|
|
1235
|
-
);
|
|
1349
|
+
// ─── runUnitPhase ─────────────────────────────────────────────────────────────
|
|
1236
1350
|
|
|
1237
|
-
|
|
1351
|
+
/**
|
|
1352
|
+
* Phase 4: Unit execution — dispatch prompt, await agent_end, closeout, artifact verify.
|
|
1353
|
+
* Returns break or next with unitStartedAt for downstream phases.
|
|
1354
|
+
*/
|
|
1355
|
+
async function runUnitPhase(
|
|
1356
|
+
ic: IterationContext,
|
|
1357
|
+
iterData: IterationData,
|
|
1358
|
+
loopState: LoopState,
|
|
1359
|
+
sidecarItem?: SidecarItem,
|
|
1360
|
+
): Promise<PhaseResult<{ unitStartedAt: number }>> {
|
|
1361
|
+
const { ctx, pi, s, deps, prefs } = ic;
|
|
1362
|
+
const { unitType, unitId, prompt, observabilityIssues, state, mid } = iterData;
|
|
1363
|
+
|
|
1364
|
+
debugLog("autoLoop", {
|
|
1365
|
+
phase: "unit-execution",
|
|
1366
|
+
iteration: ic.iteration,
|
|
1367
|
+
unitType,
|
|
1368
|
+
unitId,
|
|
1369
|
+
});
|
|
1238
1370
|
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1371
|
+
// Detect retry and capture previous tier for escalation
|
|
1372
|
+
const isRetry = !!(
|
|
1373
|
+
s.currentUnit &&
|
|
1374
|
+
s.currentUnit.type === unitType &&
|
|
1375
|
+
s.currentUnit.id === unitId
|
|
1376
|
+
);
|
|
1377
|
+
const previousTier = s.currentUnitRouting?.tier;
|
|
1245
1378
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1379
|
+
s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
1380
|
+
deps.captureAvailableSkills();
|
|
1381
|
+
deps.writeUnitRuntimeRecord(
|
|
1382
|
+
s.basePath,
|
|
1383
|
+
unitType,
|
|
1384
|
+
unitId,
|
|
1385
|
+
s.currentUnit.startedAt,
|
|
1386
|
+
{
|
|
1387
|
+
phase: "dispatched",
|
|
1388
|
+
wrapupWarningSent: false,
|
|
1389
|
+
timeoutAt: null,
|
|
1390
|
+
lastProgressAt: s.currentUnit.startedAt,
|
|
1391
|
+
progressCount: 0,
|
|
1392
|
+
lastProgressKind: "dispatch",
|
|
1393
|
+
},
|
|
1394
|
+
);
|
|
1253
1395
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1396
|
+
// Status bar + progress widget
|
|
1397
|
+
ctx.ui.setStatus("gsd-auto", "auto");
|
|
1398
|
+
if (mid)
|
|
1399
|
+
deps.updateSliceProgressCache(s.basePath, mid, state.activeSlice?.id);
|
|
1400
|
+
deps.updateProgressWidget(ctx, unitType, unitId, state);
|
|
1401
|
+
|
|
1402
|
+
deps.ensurePreconditions(unitType, unitId, s.basePath, state);
|
|
1403
|
+
|
|
1404
|
+
// Prompt injection
|
|
1405
|
+
let finalPrompt = prompt;
|
|
1406
|
+
|
|
1407
|
+
if (s.pendingVerificationRetry) {
|
|
1408
|
+
const retryCtx = s.pendingVerificationRetry;
|
|
1409
|
+
s.pendingVerificationRetry = null;
|
|
1410
|
+
const capped =
|
|
1411
|
+
retryCtx.failureContext.length > MAX_RECOVERY_CHARS
|
|
1412
|
+
? retryCtx.failureContext.slice(0, MAX_RECOVERY_CHARS) +
|
|
1413
|
+
"\n\n[...failure context truncated]"
|
|
1414
|
+
: retryCtx.failureContext;
|
|
1415
|
+
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}`;
|
|
1416
|
+
}
|
|
1264
1417
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1418
|
+
if (s.pendingCrashRecovery) {
|
|
1419
|
+
const capped =
|
|
1420
|
+
s.pendingCrashRecovery.length > MAX_RECOVERY_CHARS
|
|
1421
|
+
? s.pendingCrashRecovery.slice(0, MAX_RECOVERY_CHARS) +
|
|
1422
|
+
"\n\n[...recovery briefing truncated to prevent memory exhaustion]"
|
|
1423
|
+
: s.pendingCrashRecovery;
|
|
1424
|
+
finalPrompt = `${capped}\n\n---\n\n${finalPrompt}`;
|
|
1425
|
+
s.pendingCrashRecovery = null;
|
|
1426
|
+
} else if ((s.unitDispatchCount.get(`${unitType}/${unitId}`) ?? 0) > 1) {
|
|
1427
|
+
const diagnostic = deps.getDeepDiagnostic(s.basePath);
|
|
1428
|
+
if (diagnostic) {
|
|
1429
|
+
const cappedDiag =
|
|
1430
|
+
diagnostic.length > MAX_RECOVERY_CHARS
|
|
1431
|
+
? diagnostic.slice(0, MAX_RECOVERY_CHARS) +
|
|
1432
|
+
"\n\n[...diagnostic truncated to prevent memory exhaustion]"
|
|
1433
|
+
: diagnostic;
|
|
1434
|
+
finalPrompt = `**RETRY — your previous attempt did not produce the required artifact.**\n\nDiagnostic from previous attempt:\n${cappedDiag}\n\nFix whatever went wrong and make sure you write the required file this time.\n\n---\n\n${finalPrompt}`;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1274
1437
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
deps.verifyExpectedArtifact(
|
|
1281
|
-
s.currentUnit.type,
|
|
1282
|
-
s.currentUnit.id,
|
|
1283
|
-
s.basePath,
|
|
1284
|
-
);
|
|
1285
|
-
if (closeoutKey !== incomingKey && artifactVerified) {
|
|
1286
|
-
s.completedUnits.push({
|
|
1287
|
-
type: s.currentUnit.type,
|
|
1288
|
-
id: s.currentUnit.id,
|
|
1289
|
-
startedAt: s.currentUnit.startedAt,
|
|
1290
|
-
finishedAt: Date.now(),
|
|
1291
|
-
});
|
|
1292
|
-
if (s.completedUnits.length > 200) {
|
|
1293
|
-
s.completedUnits = s.completedUnits.slice(-200);
|
|
1294
|
-
}
|
|
1295
|
-
deps.clearUnitRuntimeRecord(
|
|
1296
|
-
s.basePath,
|
|
1297
|
-
s.currentUnit.type,
|
|
1298
|
-
s.currentUnit.id,
|
|
1299
|
-
);
|
|
1300
|
-
s.unitDispatchCount.delete(
|
|
1301
|
-
`${s.currentUnit.type}/${s.currentUnit.id}`,
|
|
1302
|
-
);
|
|
1303
|
-
s.unitRecoveryCount.delete(
|
|
1304
|
-
`${s.currentUnit.type}/${s.currentUnit.id}`,
|
|
1305
|
-
);
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1438
|
+
const repairBlock =
|
|
1439
|
+
deps.buildObservabilityRepairBlock(observabilityIssues);
|
|
1440
|
+
if (repairBlock) {
|
|
1441
|
+
finalPrompt = `${finalPrompt}${repairBlock}`;
|
|
1442
|
+
}
|
|
1308
1443
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1444
|
+
// Prompt char measurement
|
|
1445
|
+
s.lastPromptCharCount = finalPrompt.length;
|
|
1446
|
+
s.lastBaselineCharCount = undefined;
|
|
1447
|
+
if (deps.isDbAvailable()) {
|
|
1448
|
+
try {
|
|
1449
|
+
const { inlineGsdRootFile } = await importExtensionModule<typeof import("./auto-prompts.js")>(import.meta.url, "./auto-prompts.js");
|
|
1450
|
+
const [decisionsContent, requirementsContent, projectContent] =
|
|
1451
|
+
await Promise.all([
|
|
1452
|
+
inlineGsdRootFile(s.basePath, "decisions.md", "Decisions"),
|
|
1453
|
+
inlineGsdRootFile(s.basePath, "requirements.md", "Requirements"),
|
|
1454
|
+
inlineGsdRootFile(s.basePath, "project.md", "Project"),
|
|
1455
|
+
]);
|
|
1456
|
+
s.lastBaselineCharCount =
|
|
1457
|
+
(decisionsContent?.length ?? 0) +
|
|
1458
|
+
(requirementsContent?.length ?? 0) +
|
|
1459
|
+
(projectContent?.length ?? 0);
|
|
1460
|
+
} catch {
|
|
1461
|
+
// Non-fatal
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1325
1464
|
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
let finalPrompt = prompt;
|
|
1337
|
-
|
|
1338
|
-
if (s.pendingVerificationRetry) {
|
|
1339
|
-
const retryCtx = s.pendingVerificationRetry;
|
|
1340
|
-
s.pendingVerificationRetry = null;
|
|
1341
|
-
const capped =
|
|
1342
|
-
retryCtx.failureContext.length > MAX_RECOVERY_CHARS
|
|
1343
|
-
? retryCtx.failureContext.slice(0, MAX_RECOVERY_CHARS) +
|
|
1344
|
-
"\n\n[...failure context truncated]"
|
|
1345
|
-
: retryCtx.failureContext;
|
|
1346
|
-
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}`;
|
|
1347
|
-
}
|
|
1465
|
+
// Cache-optimize prompt section ordering
|
|
1466
|
+
try {
|
|
1467
|
+
finalPrompt = deps.reorderForCaching(finalPrompt);
|
|
1468
|
+
} catch (reorderErr) {
|
|
1469
|
+
const msg =
|
|
1470
|
+
reorderErr instanceof Error ? reorderErr.message : String(reorderErr);
|
|
1471
|
+
process.stderr.write(
|
|
1472
|
+
`[gsd] prompt reorder failed (non-fatal): ${msg}\n`,
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1348
1475
|
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1476
|
+
// Select and apply model (with tier escalation on retry — normal units only)
|
|
1477
|
+
const modelResult = await deps.selectAndApplyModel(
|
|
1478
|
+
ctx,
|
|
1479
|
+
pi,
|
|
1480
|
+
unitType,
|
|
1481
|
+
unitId,
|
|
1482
|
+
s.basePath,
|
|
1483
|
+
prefs,
|
|
1484
|
+
s.verbose,
|
|
1485
|
+
s.autoModeStartModel,
|
|
1486
|
+
sidecarItem ? undefined : { isRetry, previousTier },
|
|
1487
|
+
);
|
|
1488
|
+
s.currentUnitRouting =
|
|
1489
|
+
modelResult.routing as AutoSession["currentUnitRouting"];
|
|
1490
|
+
|
|
1491
|
+
// Start unit supervision
|
|
1492
|
+
deps.clearUnitTimeout();
|
|
1493
|
+
deps.startUnitSupervision({
|
|
1494
|
+
s,
|
|
1495
|
+
ctx,
|
|
1496
|
+
pi,
|
|
1497
|
+
unitType,
|
|
1498
|
+
unitId,
|
|
1499
|
+
prefs,
|
|
1500
|
+
buildSnapshotOpts: () => deps.buildSnapshotOpts(unitType, unitId),
|
|
1501
|
+
buildRecoveryContext: () => ({}),
|
|
1502
|
+
pauseAuto: deps.pauseAuto,
|
|
1503
|
+
});
|
|
1368
1504
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1505
|
+
// Session + send + await
|
|
1506
|
+
const sessionFile = deps.getSessionFile(ctx);
|
|
1507
|
+
deps.updateSessionLock(
|
|
1508
|
+
deps.lockBase(),
|
|
1509
|
+
unitType,
|
|
1510
|
+
unitId,
|
|
1511
|
+
s.completedUnits.length,
|
|
1512
|
+
sessionFile,
|
|
1513
|
+
);
|
|
1514
|
+
deps.writeLock(
|
|
1515
|
+
deps.lockBase(),
|
|
1516
|
+
unitType,
|
|
1517
|
+
unitId,
|
|
1518
|
+
s.completedUnits.length,
|
|
1519
|
+
sessionFile,
|
|
1520
|
+
);
|
|
1374
1521
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1522
|
+
debugLog("autoLoop", {
|
|
1523
|
+
phase: "runUnit-start",
|
|
1524
|
+
iteration: ic.iteration,
|
|
1525
|
+
unitType,
|
|
1526
|
+
unitId,
|
|
1527
|
+
});
|
|
1528
|
+
const unitResult = await runUnit(
|
|
1529
|
+
ctx,
|
|
1530
|
+
pi,
|
|
1531
|
+
s,
|
|
1532
|
+
unitType,
|
|
1533
|
+
unitId,
|
|
1534
|
+
finalPrompt,
|
|
1535
|
+
);
|
|
1536
|
+
debugLog("autoLoop", {
|
|
1537
|
+
phase: "runUnit-end",
|
|
1538
|
+
iteration: ic.iteration,
|
|
1539
|
+
unitType,
|
|
1540
|
+
unitId,
|
|
1541
|
+
status: unitResult.status,
|
|
1542
|
+
});
|
|
1395
1543
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1544
|
+
// Tag the most recent window entry with error info for stuck detection
|
|
1545
|
+
if (unitResult.status === "error" || unitResult.status === "cancelled") {
|
|
1546
|
+
const lastEntry = loopState.recentUnits[loopState.recentUnits.length - 1];
|
|
1547
|
+
if (lastEntry) {
|
|
1548
|
+
lastEntry.error = `${unitResult.status}:${unitType}/${unitId}`;
|
|
1549
|
+
}
|
|
1550
|
+
} else if (unitResult.event?.messages?.length) {
|
|
1551
|
+
const lastMsg = unitResult.event.messages[unitResult.event.messages.length - 1];
|
|
1552
|
+
const msgStr = typeof lastMsg === "string" ? lastMsg : JSON.stringify(lastMsg);
|
|
1553
|
+
if (/error|fail|exception/i.test(msgStr)) {
|
|
1554
|
+
const lastEntry = loopState.recentUnits[loopState.recentUnits.length - 1];
|
|
1555
|
+
if (lastEntry) {
|
|
1556
|
+
lastEntry.error = msgStr.slice(0, 200);
|
|
1405
1557
|
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1406
1560
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
s.autoModeStartModel,
|
|
1417
|
-
{ isRetry, previousTier },
|
|
1418
|
-
);
|
|
1419
|
-
s.currentUnitRouting =
|
|
1420
|
-
modelResult.routing as AutoSession["currentUnitRouting"];
|
|
1421
|
-
|
|
1422
|
-
// Start unit supervision
|
|
1423
|
-
deps.clearUnitTimeout();
|
|
1424
|
-
deps.startUnitSupervision({
|
|
1425
|
-
s,
|
|
1426
|
-
ctx,
|
|
1427
|
-
pi,
|
|
1428
|
-
unitType,
|
|
1429
|
-
unitId,
|
|
1430
|
-
prefs,
|
|
1431
|
-
buildSnapshotOpts: () => deps.buildSnapshotOpts(unitType, unitId),
|
|
1432
|
-
buildRecoveryContext: () => ({}),
|
|
1433
|
-
pauseAuto: deps.pauseAuto,
|
|
1434
|
-
});
|
|
1435
|
-
|
|
1436
|
-
// Session + send + await
|
|
1437
|
-
const sessionFile = deps.getSessionFile(ctx);
|
|
1438
|
-
deps.updateSessionLock(
|
|
1439
|
-
deps.lockBase(),
|
|
1440
|
-
unitType,
|
|
1441
|
-
unitId,
|
|
1442
|
-
s.completedUnits.length,
|
|
1443
|
-
sessionFile,
|
|
1444
|
-
);
|
|
1445
|
-
deps.writeLock(
|
|
1446
|
-
deps.lockBase(),
|
|
1447
|
-
unitType,
|
|
1448
|
-
unitId,
|
|
1449
|
-
s.completedUnits.length,
|
|
1450
|
-
sessionFile,
|
|
1451
|
-
);
|
|
1452
|
-
|
|
1453
|
-
debugLog("autoLoop", {
|
|
1454
|
-
phase: "runUnit-start",
|
|
1455
|
-
iteration,
|
|
1456
|
-
unitType,
|
|
1457
|
-
unitId,
|
|
1458
|
-
});
|
|
1459
|
-
const unitResult = await runUnit(
|
|
1460
|
-
ctx,
|
|
1461
|
-
pi,
|
|
1462
|
-
s,
|
|
1463
|
-
unitType,
|
|
1464
|
-
unitId,
|
|
1465
|
-
finalPrompt,
|
|
1466
|
-
prefs,
|
|
1467
|
-
);
|
|
1468
|
-
debugLog("autoLoop", {
|
|
1469
|
-
phase: "runUnit-end",
|
|
1470
|
-
iteration,
|
|
1471
|
-
unitType,
|
|
1472
|
-
unitId,
|
|
1473
|
-
status: unitResult.status,
|
|
1474
|
-
});
|
|
1561
|
+
if (unitResult.status === "cancelled") {
|
|
1562
|
+
ctx.ui.notify(
|
|
1563
|
+
`Session creation timed out or was cancelled for ${unitType} ${unitId}. Will retry.`,
|
|
1564
|
+
"warning",
|
|
1565
|
+
);
|
|
1566
|
+
await deps.stopAuto(ctx, pi, "Session creation failed");
|
|
1567
|
+
debugLog("autoLoop", { phase: "exit", reason: "session-failed" });
|
|
1568
|
+
return { action: "break", reason: "session-failed" };
|
|
1569
|
+
}
|
|
1475
1570
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1571
|
+
// ── Immediate unit closeout (metrics, activity log, memory) ────────
|
|
1572
|
+
// Run right after runUnit() returns so telemetry is never lost to a
|
|
1573
|
+
// crash between iterations.
|
|
1574
|
+
await deps.closeoutUnit(
|
|
1575
|
+
ctx,
|
|
1576
|
+
s.basePath,
|
|
1577
|
+
unitType,
|
|
1578
|
+
unitId,
|
|
1579
|
+
s.currentUnit.startedAt,
|
|
1580
|
+
deps.buildSnapshotOpts(unitType, unitId),
|
|
1581
|
+
);
|
|
1485
1582
|
|
|
1486
|
-
|
|
1583
|
+
if (s.currentUnitRouting) {
|
|
1584
|
+
deps.recordOutcome(
|
|
1585
|
+
unitType,
|
|
1586
|
+
s.currentUnitRouting.tier as "light" | "standard" | "heavy",
|
|
1587
|
+
true, // success assumed; dispatch will re-dispatch if artifact missing
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1487
1590
|
|
|
1488
|
-
|
|
1591
|
+
const isHookUnit = unitType.startsWith("hook/");
|
|
1592
|
+
const artifactVerified =
|
|
1593
|
+
isHookUnit ||
|
|
1594
|
+
deps.verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
1595
|
+
if (artifactVerified) {
|
|
1596
|
+
s.completedUnits.push({
|
|
1597
|
+
type: unitType,
|
|
1598
|
+
id: unitId,
|
|
1599
|
+
startedAt: s.currentUnit.startedAt,
|
|
1600
|
+
finishedAt: Date.now(),
|
|
1601
|
+
});
|
|
1602
|
+
if (s.completedUnits.length > 200) {
|
|
1603
|
+
s.completedUnits = s.completedUnits.slice(-200);
|
|
1604
|
+
}
|
|
1605
|
+
// Flush completed-units to disk so the record survives crashes
|
|
1606
|
+
try {
|
|
1607
|
+
const completedKeysPath = join(gsdRoot(s.basePath), "completed-units.json");
|
|
1608
|
+
const keys = s.completedUnits.map((u) => `${u.type}/${u.id}`);
|
|
1609
|
+
atomicWriteSync(completedKeysPath, JSON.stringify(keys, null, 2));
|
|
1610
|
+
} catch { /* non-fatal: disk flush failure */ }
|
|
1611
|
+
|
|
1612
|
+
deps.clearUnitRuntimeRecord(s.basePath, unitType, unitId);
|
|
1613
|
+
s.unitDispatchCount.delete(`${unitType}/${unitId}`);
|
|
1614
|
+
s.unitRecoveryCount.delete(`${unitType}/${unitId}`);
|
|
1615
|
+
}
|
|
1489
1616
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1617
|
+
return { action: "next", data: { unitStartedAt: s.currentUnit.startedAt } };
|
|
1618
|
+
}
|
|
1492
1619
|
|
|
1493
|
-
|
|
1494
|
-
const postUnitCtx: PostUnitContext = {
|
|
1495
|
-
s,
|
|
1496
|
-
ctx,
|
|
1497
|
-
pi,
|
|
1498
|
-
buildSnapshotOpts: deps.buildSnapshotOpts,
|
|
1499
|
-
lockBase: deps.lockBase,
|
|
1500
|
-
stopAuto: deps.stopAuto,
|
|
1501
|
-
pauseAuto: deps.pauseAuto,
|
|
1502
|
-
updateProgressWidget: deps.updateProgressWidget,
|
|
1503
|
-
};
|
|
1620
|
+
// ─── runFinalize ──────────────────────────────────────────────────────────────
|
|
1504
1621
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1622
|
+
/**
|
|
1623
|
+
* Phase 5: Post-unit finalize — pre/post verification, UAT pause, step-wizard.
|
|
1624
|
+
* Returns break/continue/next to control the outer loop.
|
|
1625
|
+
*/
|
|
1626
|
+
async function runFinalize(
|
|
1627
|
+
ic: IterationContext,
|
|
1628
|
+
iterData: IterationData,
|
|
1629
|
+
sidecarItem?: SidecarItem,
|
|
1630
|
+
): Promise<PhaseResult> {
|
|
1631
|
+
const { ctx, pi, s, deps } = ic;
|
|
1632
|
+
const { pauseAfterUatDispatch } = iterData;
|
|
1633
|
+
|
|
1634
|
+
debugLog("autoLoop", { phase: "finalize", iteration: ic.iteration });
|
|
1635
|
+
|
|
1636
|
+
// Clear unit timeout (unit completed)
|
|
1637
|
+
deps.clearUnitTimeout();
|
|
1638
|
+
|
|
1639
|
+
// Post-unit context for pre/post verification
|
|
1640
|
+
const postUnitCtx: PostUnitContext = {
|
|
1641
|
+
s,
|
|
1642
|
+
ctx,
|
|
1643
|
+
pi,
|
|
1644
|
+
buildSnapshotOpts: deps.buildSnapshotOpts,
|
|
1645
|
+
lockBase: deps.lockBase,
|
|
1646
|
+
stopAuto: deps.stopAuto,
|
|
1647
|
+
pauseAuto: deps.pauseAuto,
|
|
1648
|
+
updateProgressWidget: deps.updateProgressWidget,
|
|
1649
|
+
};
|
|
1514
1650
|
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1651
|
+
// Pre-verification processing (commit, doctor, state rebuild, etc.)
|
|
1652
|
+
// Sidecar items use lightweight pre-verification opts
|
|
1653
|
+
const preVerificationOpts: PreVerificationOpts | undefined = sidecarItem
|
|
1654
|
+
? sidecarItem.kind === "hook"
|
|
1655
|
+
? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
|
|
1656
|
+
: { skipSettleDelay: true, skipStateRebuild: true }
|
|
1657
|
+
: undefined;
|
|
1658
|
+
const preResult = await deps.postUnitPreVerification(postUnitCtx, preVerificationOpts);
|
|
1659
|
+
if (preResult === "dispatched") {
|
|
1660
|
+
debugLog("autoLoop", {
|
|
1661
|
+
phase: "exit",
|
|
1662
|
+
reason: "pre-verification-dispatched",
|
|
1663
|
+
});
|
|
1664
|
+
return { action: "break", reason: "pre-verification-dispatched" };
|
|
1665
|
+
}
|
|
1524
1666
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1667
|
+
if (pauseAfterUatDispatch) {
|
|
1668
|
+
ctx.ui.notify(
|
|
1669
|
+
"UAT requires human execution. Auto-mode will pause after this unit writes the result file.",
|
|
1670
|
+
"info",
|
|
1671
|
+
);
|
|
1672
|
+
await deps.pauseAuto(ctx, pi);
|
|
1673
|
+
debugLog("autoLoop", { phase: "exit", reason: "uat-pause" });
|
|
1674
|
+
return { action: "break", reason: "uat-pause" };
|
|
1675
|
+
}
|
|
1530
1676
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1677
|
+
// Verification gate
|
|
1678
|
+
// Hook sidecar items skip verification entirely.
|
|
1679
|
+
// Non-hook sidecar items run verification but skip retries (just continue).
|
|
1680
|
+
const skipVerification = sidecarItem?.kind === "hook";
|
|
1681
|
+
if (!skipVerification) {
|
|
1682
|
+
const verificationResult = await deps.runPostUnitVerification(
|
|
1683
|
+
{ s, ctx, pi },
|
|
1684
|
+
deps.pauseAuto,
|
|
1685
|
+
);
|
|
1686
|
+
|
|
1687
|
+
if (verificationResult === "pause") {
|
|
1688
|
+
debugLog("autoLoop", { phase: "exit", reason: "verification-pause" });
|
|
1689
|
+
return { action: "break", reason: "verification-pause" };
|
|
1690
|
+
}
|
|
1535
1691
|
|
|
1536
|
-
|
|
1692
|
+
if (verificationResult === "retry") {
|
|
1693
|
+
if (sidecarItem) {
|
|
1694
|
+
// Sidecar verification retries are skipped — just continue
|
|
1695
|
+
debugLog("autoLoop", { phase: "sidecar-verification-retry-skipped", iteration: ic.iteration });
|
|
1696
|
+
} else {
|
|
1537
1697
|
// s.pendingVerificationRetry was set by runPostUnitVerification.
|
|
1538
1698
|
// Continue the loop — next iteration will inject the retry context into the prompt.
|
|
1539
|
-
debugLog("autoLoop", { phase: "verification-retry", iteration });
|
|
1540
|
-
continue;
|
|
1699
|
+
debugLog("autoLoop", { phase: "verification-retry", iteration: ic.iteration });
|
|
1700
|
+
return { action: "continue" };
|
|
1541
1701
|
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1542
1704
|
|
|
1543
|
-
|
|
1544
|
-
|
|
1705
|
+
// Post-verification processing (DB dual-write, hooks, triage, quick-tasks)
|
|
1706
|
+
const postResult = await deps.postUnitPostVerification(postUnitCtx);
|
|
1545
1707
|
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1708
|
+
if (postResult === "stopped") {
|
|
1709
|
+
debugLog("autoLoop", {
|
|
1710
|
+
phase: "exit",
|
|
1711
|
+
reason: "post-verification-stopped",
|
|
1712
|
+
});
|
|
1713
|
+
return { action: "break", reason: "post-verification-stopped" };
|
|
1714
|
+
}
|
|
1553
1715
|
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1716
|
+
if (postResult === "step-wizard") {
|
|
1717
|
+
// Step mode — exit the loop (caller handles wizard)
|
|
1718
|
+
debugLog("autoLoop", { phase: "exit", reason: "step-wizard" });
|
|
1719
|
+
return { action: "break", reason: "step-wizard" };
|
|
1720
|
+
}
|
|
1559
1721
|
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
while (s.sidecarQueue.length > 0 && s.active) {
|
|
1563
|
-
const item = s.sidecarQueue.shift()!;
|
|
1564
|
-
debugLog("autoLoop", {
|
|
1565
|
-
phase: "sidecar-dequeue",
|
|
1566
|
-
kind: item.kind,
|
|
1567
|
-
unitType: item.unitType,
|
|
1568
|
-
unitId: item.unitId,
|
|
1569
|
-
});
|
|
1722
|
+
return { action: "next", data: undefined as void };
|
|
1723
|
+
}
|
|
1570
1724
|
|
|
1571
|
-
|
|
1572
|
-
const sidecarStartedAt = Date.now();
|
|
1573
|
-
s.currentUnit = {
|
|
1574
|
-
type: item.unitType,
|
|
1575
|
-
id: item.unitId,
|
|
1576
|
-
startedAt: sidecarStartedAt,
|
|
1577
|
-
};
|
|
1578
|
-
deps.writeUnitRuntimeRecord(
|
|
1579
|
-
s.basePath,
|
|
1580
|
-
item.unitType,
|
|
1581
|
-
item.unitId,
|
|
1582
|
-
sidecarStartedAt,
|
|
1583
|
-
{
|
|
1584
|
-
phase: "dispatched",
|
|
1585
|
-
wrapupWarningSent: false,
|
|
1586
|
-
timeoutAt: null,
|
|
1587
|
-
lastProgressAt: sidecarStartedAt,
|
|
1588
|
-
progressCount: 0,
|
|
1589
|
-
lastProgressKind: "dispatch",
|
|
1590
|
-
},
|
|
1591
|
-
);
|
|
1725
|
+
// ─── autoLoop ────────────────────────────────────────────────────────────────
|
|
1592
1726
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1727
|
+
/**
|
|
1728
|
+
* Main auto-mode execution loop. Iterates: derive → dispatch → guards →
|
|
1729
|
+
* runUnit → finalize → repeat. Exits when s.active becomes false or a
|
|
1730
|
+
* terminal condition is reached.
|
|
1731
|
+
*
|
|
1732
|
+
* This is the linear replacement for the recursive
|
|
1733
|
+
* dispatchNextUnit → handleAgentEnd → dispatchNextUnit chain.
|
|
1734
|
+
*/
|
|
1735
|
+
export async function autoLoop(
|
|
1736
|
+
ctx: ExtensionContext,
|
|
1737
|
+
pi: ExtensionAPI,
|
|
1738
|
+
s: AutoSession,
|
|
1739
|
+
deps: LoopDeps,
|
|
1740
|
+
): Promise<void> {
|
|
1741
|
+
debugLog("autoLoop", { phase: "enter" });
|
|
1742
|
+
let iteration = 0;
|
|
1743
|
+
const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
|
|
1744
|
+
let consecutiveErrors = 0;
|
|
1604
1745
|
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
s,
|
|
1609
|
-
ctx,
|
|
1610
|
-
pi,
|
|
1611
|
-
unitType: item.unitType,
|
|
1612
|
-
unitId: item.unitId,
|
|
1613
|
-
prefs,
|
|
1614
|
-
buildSnapshotOpts: () =>
|
|
1615
|
-
deps.buildSnapshotOpts(item.unitType, item.unitId),
|
|
1616
|
-
buildRecoveryContext: () => ({}),
|
|
1617
|
-
pauseAuto: deps.pauseAuto,
|
|
1618
|
-
});
|
|
1746
|
+
while (s.active) {
|
|
1747
|
+
iteration++;
|
|
1748
|
+
debugLog("autoLoop", { phase: "loop-top", iteration });
|
|
1619
1749
|
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1750
|
+
if (iteration > MAX_LOOP_ITERATIONS) {
|
|
1751
|
+
debugLog("autoLoop", {
|
|
1752
|
+
phase: "exit",
|
|
1753
|
+
reason: "max-iterations",
|
|
1754
|
+
iteration,
|
|
1755
|
+
});
|
|
1756
|
+
await deps.stopAuto(
|
|
1757
|
+
ctx,
|
|
1758
|
+
pi,
|
|
1759
|
+
`Safety: loop exceeded ${MAX_LOOP_ITERATIONS} iterations — possible runaway`,
|
|
1760
|
+
);
|
|
1761
|
+
break;
|
|
1762
|
+
}
|
|
1629
1763
|
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
s,
|
|
1635
|
-
item.unitType,
|
|
1636
|
-
item.unitId,
|
|
1637
|
-
item.prompt,
|
|
1638
|
-
prefs,
|
|
1639
|
-
);
|
|
1640
|
-
deps.clearUnitTimeout();
|
|
1764
|
+
if (!s.cmdCtx) {
|
|
1765
|
+
debugLog("autoLoop", { phase: "exit", reason: "no-cmdCtx" });
|
|
1766
|
+
break;
|
|
1767
|
+
}
|
|
1641
1768
|
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
"warning",
|
|
1646
|
-
);
|
|
1647
|
-
await deps.stopAuto(ctx, pi, "Sidecar session creation failed");
|
|
1648
|
-
sidecarBroke = true;
|
|
1649
|
-
break;
|
|
1650
|
-
}
|
|
1769
|
+
try {
|
|
1770
|
+
// ── Blanket try/catch: one bad iteration must not kill the session
|
|
1771
|
+
const prefs = deps.loadEffectiveGSDPreferences()?.preferences;
|
|
1651
1772
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1773
|
+
// ── Check sidecar queue before deriveState ──
|
|
1774
|
+
let sidecarItem: SidecarItem | undefined;
|
|
1775
|
+
if (s.sidecarQueue.length > 0) {
|
|
1776
|
+
sidecarItem = s.sidecarQueue.shift()!;
|
|
1777
|
+
debugLog("autoLoop", {
|
|
1778
|
+
phase: "sidecar-dequeue",
|
|
1779
|
+
kind: sidecarItem.kind,
|
|
1780
|
+
unitType: sidecarItem.unitType,
|
|
1781
|
+
unitId: sidecarItem.unitId,
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
const sessionLockBase = deps.lockBase();
|
|
1786
|
+
if (sessionLockBase) {
|
|
1787
|
+
const lockStatus = deps.validateSessionLock(sessionLockBase);
|
|
1788
|
+
if (!lockStatus.valid) {
|
|
1657
1789
|
debugLog("autoLoop", {
|
|
1658
|
-
phase: "
|
|
1659
|
-
reason: "
|
|
1790
|
+
phase: "session-lock-invalid",
|
|
1791
|
+
reason: lockStatus.failureReason ?? "unknown",
|
|
1792
|
+
existingPid: lockStatus.existingPid,
|
|
1793
|
+
expectedPid: lockStatus.expectedPid,
|
|
1660
1794
|
});
|
|
1661
|
-
|
|
1662
|
-
break;
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
// Verification gate for non-hook sidecar units (triage, quick-tasks)
|
|
1666
|
-
// Hook units are lightweight and don't need verification.
|
|
1667
|
-
if (item.kind !== "hook") {
|
|
1668
|
-
const sidecarVerification = await deps.runPostUnitVerification(
|
|
1669
|
-
{ s, ctx, pi },
|
|
1670
|
-
deps.pauseAuto,
|
|
1671
|
-
);
|
|
1672
|
-
if (sidecarVerification === "pause") {
|
|
1673
|
-
debugLog("autoLoop", {
|
|
1674
|
-
phase: "exit",
|
|
1675
|
-
reason: "sidecar-verification-pause",
|
|
1676
|
-
});
|
|
1677
|
-
sidecarBroke = true;
|
|
1678
|
-
break;
|
|
1679
|
-
}
|
|
1680
|
-
// "retry" for sidecars — skip retry, just continue (sidecar retries are not worth the complexity)
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
// Post-verification (may enqueue more sidecar items)
|
|
1684
|
-
const sidecarPostResult =
|
|
1685
|
-
await deps.postUnitPostVerification(postUnitCtx);
|
|
1686
|
-
if (sidecarPostResult === "stopped") {
|
|
1687
|
-
debugLog("autoLoop", { phase: "exit", reason: "sidecar-stopped" });
|
|
1688
|
-
sidecarBroke = true;
|
|
1689
|
-
break;
|
|
1690
|
-
}
|
|
1691
|
-
if (sidecarPostResult === "step-wizard") {
|
|
1795
|
+
deps.handleLostSessionLock(ctx, lockStatus);
|
|
1692
1796
|
debugLog("autoLoop", {
|
|
1693
1797
|
phase: "exit",
|
|
1694
|
-
reason: "
|
|
1798
|
+
reason: "session-lock-lost",
|
|
1799
|
+
detail: lockStatus.failureReason ?? "unknown",
|
|
1695
1800
|
});
|
|
1696
|
-
sidecarBroke = true;
|
|
1697
1801
|
break;
|
|
1698
1802
|
}
|
|
1699
|
-
// "continue" — loop checks sidecarQueue again
|
|
1700
1803
|
}
|
|
1701
1804
|
|
|
1702
|
-
|
|
1805
|
+
const ic: IterationContext = { ctx, pi, s, deps, prefs, iteration };
|
|
1806
|
+
let iterData: IterationData;
|
|
1807
|
+
|
|
1808
|
+
if (!sidecarItem) {
|
|
1809
|
+
// ── Phase 1: Pre-dispatch ─────────────────────────────────────────
|
|
1810
|
+
const preDispatchResult = await runPreDispatch(ic, loopState);
|
|
1811
|
+
if (preDispatchResult.action === "break") break;
|
|
1812
|
+
if (preDispatchResult.action === "continue") continue;
|
|
1813
|
+
|
|
1814
|
+
const preData = preDispatchResult.data;
|
|
1815
|
+
|
|
1816
|
+
// ── Phase 2: Guards ───────────────────────────────────────────────
|
|
1817
|
+
const guardsResult = await runGuards(ic, preData.mid);
|
|
1818
|
+
if (guardsResult.action === "break") break;
|
|
1819
|
+
|
|
1820
|
+
// ── Phase 3: Dispatch ─────────────────────────────────────────────
|
|
1821
|
+
const dispatchResult = await runDispatch(ic, preData, loopState);
|
|
1822
|
+
if (dispatchResult.action === "break") break;
|
|
1823
|
+
if (dispatchResult.action === "continue") continue;
|
|
1824
|
+
iterData = dispatchResult.data;
|
|
1825
|
+
} else {
|
|
1826
|
+
// ── Sidecar path: use values from the sidecar item directly ──
|
|
1827
|
+
const sidecarState = await deps.deriveState(s.basePath);
|
|
1828
|
+
iterData = {
|
|
1829
|
+
unitType: sidecarItem.unitType,
|
|
1830
|
+
unitId: sidecarItem.unitId,
|
|
1831
|
+
prompt: sidecarItem.prompt,
|
|
1832
|
+
finalPrompt: sidecarItem.prompt,
|
|
1833
|
+
pauseAfterUatDispatch: false,
|
|
1834
|
+
observabilityIssues: [],
|
|
1835
|
+
state: sidecarState,
|
|
1836
|
+
mid: sidecarState.activeMilestone?.id,
|
|
1837
|
+
midTitle: sidecarState.activeMilestone?.title,
|
|
1838
|
+
isRetry: false, previousTier: undefined,
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
const unitPhaseResult = await runUnitPhase(ic, iterData, loopState, sidecarItem);
|
|
1843
|
+
if (unitPhaseResult.action === "break") break;
|
|
1844
|
+
|
|
1845
|
+
// ── Phase 5: Finalize ───────────────────────────────────────────────
|
|
1846
|
+
|
|
1847
|
+
const finalizeResult = await runFinalize(ic, iterData, sidecarItem);
|
|
1848
|
+
if (finalizeResult.action === "break") break;
|
|
1849
|
+
if (finalizeResult.action === "continue") continue;
|
|
1703
1850
|
|
|
1704
1851
|
consecutiveErrors = 0; // Iteration completed successfully
|
|
1705
1852
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|
|
@@ -1740,6 +1887,6 @@ export async function autoLoop(
|
|
|
1740
1887
|
}
|
|
1741
1888
|
}
|
|
1742
1889
|
|
|
1743
|
-
|
|
1890
|
+
_currentResolve = null;
|
|
1744
1891
|
debugLog("autoLoop", { phase: "exit", totalIterations: iteration });
|
|
1745
1892
|
}
|