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,12 +5,16 @@
|
|
|
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
|
+
import { importExtensionModule } from "@gsd/pi-coding-agent";
|
|
12
13
|
import { NEW_SESSION_TIMEOUT_MS } from "./auto/session.js";
|
|
13
14
|
import { debugLog } from "./debug-logger.js";
|
|
15
|
+
import { gsdRoot } from "./paths.js";
|
|
16
|
+
import { atomicWriteSync } from "./atomic-write.js";
|
|
17
|
+
import { join } from "node:path";
|
|
14
18
|
/**
|
|
15
19
|
* Maximum total loop iterations before forced stop. Prevents runaway loops
|
|
16
20
|
* when units alternate IDs (bypassing the same-unit stuck detector).
|
|
@@ -18,71 +22,114 @@ import { debugLog } from "./debug-logger.js";
|
|
|
18
22
|
* generous headroom including retries and sidecar work.
|
|
19
23
|
*/
|
|
20
24
|
const MAX_LOOP_ITERATIONS = 500;
|
|
21
|
-
|
|
25
|
+
/** Maximum characters of failure/crash context included in recovery prompts. */
|
|
26
|
+
const MAX_RECOVERY_CHARS = 50_000;
|
|
27
|
+
/** Data-driven budget threshold notifications (descending). The 100% entry
|
|
28
|
+
* triggers special enforcement logic (halt/pause/warn); sub-100 entries fire
|
|
29
|
+
* a simple notification. */
|
|
30
|
+
const BUDGET_THRESHOLDS = [
|
|
31
|
+
{ pct: 100, label: "Budget ceiling reached", notifyLevel: "error", cmuxLevel: "error" },
|
|
32
|
+
{ pct: 90, label: "Budget 90%", notifyLevel: "warning", cmuxLevel: "warning" },
|
|
33
|
+
{ pct: 80, label: "Approaching budget ceiling — 80%", notifyLevel: "warning", cmuxLevel: "warning" },
|
|
34
|
+
{ pct: 75, label: "Budget 75%", notifyLevel: "info", cmuxLevel: "progress" },
|
|
35
|
+
];
|
|
36
|
+
// ─── Per-unit one-shot promise state ────────────────────────────────────────
|
|
22
37
|
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
*/
|
|
30
|
-
let _activeSession = null;
|
|
38
|
+
// A single module-level resolve function scoped to the current unit execution.
|
|
39
|
+
// No queue — if an agent_end arrives with no pending resolver, it is dropped
|
|
40
|
+
// (logged as warning). This is simpler and safer than the previous session-
|
|
41
|
+
// scoped pendingResolve + pendingAgentEndQueue pattern.
|
|
42
|
+
let _currentResolve = null;
|
|
43
|
+
let _sessionSwitchInFlight = false;
|
|
31
44
|
// ─── resolveAgentEnd ─────────────────────────────────────────────────────────
|
|
32
45
|
/**
|
|
33
46
|
* Called from the agent_end event handler in index.ts to resolve the
|
|
34
47
|
* in-flight unit promise. One-shot: the resolver is nulled before calling
|
|
35
48
|
* to prevent double-resolution from model fallback retries.
|
|
36
49
|
*
|
|
37
|
-
* If no
|
|
38
|
-
* the event is
|
|
50
|
+
* If no resolver exists (event arrived between loop iterations or during
|
|
51
|
+
* session switch), the event is dropped with a debug warning.
|
|
39
52
|
*/
|
|
40
53
|
export function resolveAgentEnd(event) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
debugLog("resolveAgentEnd", {
|
|
44
|
-
status: "no-active-session",
|
|
45
|
-
warning: "agent_end with no active loop session",
|
|
46
|
-
});
|
|
54
|
+
if (_sessionSwitchInFlight) {
|
|
55
|
+
debugLog("resolveAgentEnd", { status: "ignored-during-switch" });
|
|
47
56
|
return;
|
|
48
57
|
}
|
|
49
|
-
if (
|
|
58
|
+
if (_currentResolve) {
|
|
50
59
|
debugLog("resolveAgentEnd", { status: "resolving", hasEvent: true });
|
|
51
|
-
const r =
|
|
52
|
-
|
|
60
|
+
const r = _currentResolve;
|
|
61
|
+
_currentResolve = null;
|
|
53
62
|
r({ status: "completed", event });
|
|
54
63
|
}
|
|
55
64
|
else {
|
|
56
|
-
// Queue the event so the next runUnit picks it up immediately
|
|
57
65
|
debugLog("resolveAgentEnd", {
|
|
58
|
-
status: "
|
|
59
|
-
|
|
60
|
-
warning: "agent_end arrived between loop iterations — queued for next runUnit",
|
|
66
|
+
status: "no-pending-resolve",
|
|
67
|
+
warning: "agent_end with no pending unit",
|
|
61
68
|
});
|
|
62
|
-
s.pendingAgentEndQueue.push(event);
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
export function isSessionSwitchInFlight() {
|
|
66
|
-
return
|
|
72
|
+
return _sessionSwitchInFlight;
|
|
67
73
|
}
|
|
68
74
|
// ─── resetPendingResolve (test helper) ───────────────────────────────────────
|
|
69
75
|
/**
|
|
70
|
-
* Reset
|
|
71
|
-
* should never call this.
|
|
76
|
+
* Reset module-level promise state. Only exported for test cleanup —
|
|
77
|
+
* production code should never call this.
|
|
72
78
|
*/
|
|
73
79
|
export function _resetPendingResolve() {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
_activeSession.pendingAgentEndQueue = [];
|
|
77
|
-
}
|
|
78
|
-
_activeSession = null;
|
|
80
|
+
_currentResolve = null;
|
|
81
|
+
_sessionSwitchInFlight = false;
|
|
79
82
|
}
|
|
80
83
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
84
|
+
* No-op for backward compatibility with tests that previously set the
|
|
85
|
+
* active session. The module no longer holds a session reference.
|
|
83
86
|
*/
|
|
84
|
-
export function _setActiveSession(
|
|
85
|
-
|
|
87
|
+
export function _setActiveSession(_session) {
|
|
88
|
+
// No-op — kept for test backward compatibility
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Analyze a sliding window of recent unit dispatches for stuck patterns.
|
|
92
|
+
* Returns a signal with reason if stuck, null otherwise.
|
|
93
|
+
*
|
|
94
|
+
* Rule 1: Same error string twice in a row → stuck immediately.
|
|
95
|
+
* Rule 2: Same unit key 3+ consecutive times → stuck (preserves prior behavior).
|
|
96
|
+
* Rule 3: Oscillation A→B→A→B in last 4 entries → stuck.
|
|
97
|
+
*/
|
|
98
|
+
export function detectStuck(window) {
|
|
99
|
+
if (window.length < 2)
|
|
100
|
+
return null;
|
|
101
|
+
const last = window[window.length - 1];
|
|
102
|
+
const prev = window[window.length - 2];
|
|
103
|
+
// Rule 1: Same error repeated consecutively
|
|
104
|
+
if (last.error && prev.error && last.error === prev.error) {
|
|
105
|
+
return {
|
|
106
|
+
stuck: true,
|
|
107
|
+
reason: `Same error repeated: ${last.error.slice(0, 200)}`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// Rule 2: Same unit 3+ consecutive times
|
|
111
|
+
if (window.length >= 3) {
|
|
112
|
+
const lastThree = window.slice(-3);
|
|
113
|
+
if (lastThree.every((u) => u.key === last.key)) {
|
|
114
|
+
return {
|
|
115
|
+
stuck: true,
|
|
116
|
+
reason: `${last.key} derived 3 consecutive times without progress`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Rule 3: Oscillation (A→B→A→B in last 4)
|
|
121
|
+
if (window.length >= 4) {
|
|
122
|
+
const w = window.slice(-4);
|
|
123
|
+
if (w[0].key === w[2].key &&
|
|
124
|
+
w[1].key === w[3].key &&
|
|
125
|
+
w[0].key !== w[1].key) {
|
|
126
|
+
return {
|
|
127
|
+
stuck: true,
|
|
128
|
+
reason: `Oscillation detected: ${w[0].key} ↔ ${w[1].key}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
86
133
|
}
|
|
87
134
|
// ─── runUnit ─────────────────────────────────────────────────────────────────
|
|
88
135
|
/**
|
|
@@ -93,41 +140,16 @@ export function _setActiveSession(session) {
|
|
|
93
140
|
* On session creation failure or timeout, returns { status: 'cancelled' }
|
|
94
141
|
* without awaiting the promise.
|
|
95
142
|
*/
|
|
96
|
-
export async function runUnit(ctx, pi, s, unitType, unitId, prompt
|
|
143
|
+
export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
|
|
97
144
|
debugLog("runUnit", { phase: "start", unitType, unitId });
|
|
98
|
-
// ── Drain queued events from error-recovery retries ──
|
|
99
|
-
// If an agent_end arrived between iterations (e.g. from a model fallback
|
|
100
|
-
// sendMessage retry), consume it immediately instead of creating a new promise.
|
|
101
|
-
// Cap queue to 3 entries to prevent unbounded growth from stale events.
|
|
102
|
-
if (s.pendingAgentEndQueue.length > 3) {
|
|
103
|
-
debugLog("runUnit", {
|
|
104
|
-
phase: "queue-overflow",
|
|
105
|
-
dropped: s.pendingAgentEndQueue.length - 1,
|
|
106
|
-
unitType,
|
|
107
|
-
unitId,
|
|
108
|
-
});
|
|
109
|
-
s.pendingAgentEndQueue = [
|
|
110
|
-
s.pendingAgentEndQueue[s.pendingAgentEndQueue.length - 1],
|
|
111
|
-
];
|
|
112
|
-
}
|
|
113
|
-
if (s.pendingAgentEndQueue.length > 0) {
|
|
114
|
-
const queued = s.pendingAgentEndQueue.shift();
|
|
115
|
-
debugLog("runUnit", {
|
|
116
|
-
phase: "drained-queued-event",
|
|
117
|
-
unitType,
|
|
118
|
-
unitId,
|
|
119
|
-
queueRemaining: s.pendingAgentEndQueue.length,
|
|
120
|
-
});
|
|
121
|
-
return { status: "completed", event: queued };
|
|
122
|
-
}
|
|
123
145
|
// ── Session creation with timeout ──
|
|
124
146
|
debugLog("runUnit", { phase: "session-create", unitType, unitId });
|
|
125
147
|
let sessionResult;
|
|
126
148
|
let sessionTimeoutHandle;
|
|
127
|
-
|
|
149
|
+
_sessionSwitchInFlight = true;
|
|
128
150
|
try {
|
|
129
151
|
const sessionPromise = s.cmdCtx.newSession().finally(() => {
|
|
130
|
-
|
|
152
|
+
_sessionSwitchInFlight = false;
|
|
131
153
|
});
|
|
132
154
|
const timeoutPromise = new Promise((resolve) => {
|
|
133
155
|
sessionTimeoutHandle = setTimeout(() => resolve({ cancelled: true }), NEW_SESSION_TIMEOUT_MS);
|
|
@@ -155,11 +177,12 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt, _prefs) {
|
|
|
155
177
|
if (!s.active) {
|
|
156
178
|
return { status: "cancelled" };
|
|
157
179
|
}
|
|
158
|
-
// ── Create the agent_end promise (
|
|
180
|
+
// ── Create the agent_end promise (per-unit one-shot) ──
|
|
159
181
|
// This happens after newSession completes so session-switch agent_end events
|
|
160
182
|
// from the previous session cannot resolve the new unit.
|
|
183
|
+
_sessionSwitchInFlight = false;
|
|
161
184
|
const unitPromise = new Promise((resolve) => {
|
|
162
|
-
|
|
185
|
+
_currentResolve = resolve;
|
|
163
186
|
});
|
|
164
187
|
// Ensure cwd matches basePath before dispatch (#1389).
|
|
165
188
|
// async_bash and background jobs can drift cwd away from the worktree.
|
|
@@ -182,8 +205,787 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt, _prefs) {
|
|
|
182
205
|
unitId,
|
|
183
206
|
status: result.status,
|
|
184
207
|
});
|
|
208
|
+
// Discard trailing follow-up messages (e.g. async_job_result notifications)
|
|
209
|
+
// from the completed unit. Without this, queued follow-ups trigger wasteful
|
|
210
|
+
// LLM turns before the next session can start (#1642).
|
|
211
|
+
// clearQueue() lives on AgentSession but isn't part of the typed
|
|
212
|
+
// ExtensionCommandContext interface — call it via runtime check.
|
|
213
|
+
try {
|
|
214
|
+
const cmdCtxAny = s.cmdCtx;
|
|
215
|
+
if (typeof cmdCtxAny?.clearQueue === "function") {
|
|
216
|
+
cmdCtxAny.clearQueue();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// Non-fatal — clearQueue may not be available in all contexts
|
|
221
|
+
}
|
|
185
222
|
return result;
|
|
186
223
|
}
|
|
224
|
+
// ─── generateMilestoneReport ──────────────────────────────────────────────────
|
|
225
|
+
/**
|
|
226
|
+
* Generate and write an HTML milestone report snapshot.
|
|
227
|
+
* Extracted from the milestone-transition block in autoLoop.
|
|
228
|
+
*/
|
|
229
|
+
async function generateMilestoneReport(s, ctx, milestoneId) {
|
|
230
|
+
const { loadVisualizerData } = await importExtensionModule(import.meta.url, "./visualizer-data.js");
|
|
231
|
+
const { generateHtmlReport } = await importExtensionModule(import.meta.url, "./export-html.js");
|
|
232
|
+
const { writeReportSnapshot } = await importExtensionModule(import.meta.url, "./reports.js");
|
|
233
|
+
const { basename } = await import("node:path");
|
|
234
|
+
const snapData = await loadVisualizerData(s.basePath);
|
|
235
|
+
const completedMs = snapData.milestones.find((m) => m.id === milestoneId);
|
|
236
|
+
const msTitle = completedMs?.title ?? milestoneId;
|
|
237
|
+
const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
|
|
238
|
+
const projName = basename(s.basePath);
|
|
239
|
+
const doneSlices = snapData.milestones.reduce((acc, m) => acc + m.slices.filter((sl) => sl.done).length, 0);
|
|
240
|
+
const totalSlices = snapData.milestones.reduce((acc, m) => acc + m.slices.length, 0);
|
|
241
|
+
const outPath = writeReportSnapshot({
|
|
242
|
+
basePath: s.basePath,
|
|
243
|
+
html: generateHtmlReport(snapData, {
|
|
244
|
+
projectName: projName,
|
|
245
|
+
projectPath: s.basePath,
|
|
246
|
+
gsdVersion,
|
|
247
|
+
milestoneId,
|
|
248
|
+
indexRelPath: "index.html",
|
|
249
|
+
}),
|
|
250
|
+
milestoneId,
|
|
251
|
+
milestoneTitle: msTitle,
|
|
252
|
+
kind: "milestone",
|
|
253
|
+
projectName: projName,
|
|
254
|
+
projectPath: s.basePath,
|
|
255
|
+
gsdVersion,
|
|
256
|
+
totalCost: snapData.totals?.cost ?? 0,
|
|
257
|
+
totalTokens: snapData.totals?.tokens.total ?? 0,
|
|
258
|
+
totalDuration: snapData.totals?.duration ?? 0,
|
|
259
|
+
doneSlices,
|
|
260
|
+
totalSlices,
|
|
261
|
+
doneMilestones: snapData.milestones.filter((m) => m.status === "complete").length,
|
|
262
|
+
totalMilestones: snapData.milestones.length,
|
|
263
|
+
phase: snapData.phase,
|
|
264
|
+
});
|
|
265
|
+
ctx.ui.notify(`Report saved: .gsd/reports/${basename(outPath)} — open index.html to browse progression.`, "info");
|
|
266
|
+
}
|
|
267
|
+
// ─── closeoutAndStop ──────────────────────────────────────────────────────────
|
|
268
|
+
/**
|
|
269
|
+
* If a unit is in-flight, close it out, then stop auto-mode.
|
|
270
|
+
* Extracted from ~4 identical if-closeout-then-stop sequences in autoLoop.
|
|
271
|
+
*/
|
|
272
|
+
async function closeoutAndStop(ctx, pi, s, deps, reason) {
|
|
273
|
+
if (s.currentUnit) {
|
|
274
|
+
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
275
|
+
}
|
|
276
|
+
await deps.stopAuto(ctx, pi, reason);
|
|
277
|
+
}
|
|
278
|
+
// ─── runPreDispatch ───────────────────────────────────────────────────────────
|
|
279
|
+
/**
|
|
280
|
+
* Phase 1: Pre-dispatch — resource guard, health gate, state derivation,
|
|
281
|
+
* milestone transition, terminal conditions.
|
|
282
|
+
* Returns break to exit the loop, or next with PreDispatchData on success.
|
|
283
|
+
*/
|
|
284
|
+
async function runPreDispatch(ic, loopState) {
|
|
285
|
+
const { ctx, pi, s, deps, prefs } = ic;
|
|
286
|
+
// Resource version guard
|
|
287
|
+
const staleMsg = deps.checkResourcesStale(s.resourceVersionOnStart);
|
|
288
|
+
if (staleMsg) {
|
|
289
|
+
await deps.stopAuto(ctx, pi, staleMsg);
|
|
290
|
+
debugLog("autoLoop", { phase: "exit", reason: "resources-stale" });
|
|
291
|
+
return { action: "break", reason: "resources-stale" };
|
|
292
|
+
}
|
|
293
|
+
deps.invalidateAllCaches();
|
|
294
|
+
s.lastPromptCharCount = undefined;
|
|
295
|
+
s.lastBaselineCharCount = undefined;
|
|
296
|
+
// Pre-dispatch health gate
|
|
297
|
+
try {
|
|
298
|
+
const healthGate = await deps.preDispatchHealthGate(s.basePath);
|
|
299
|
+
if (healthGate.fixesApplied.length > 0) {
|
|
300
|
+
ctx.ui.notify(`Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`, "info");
|
|
301
|
+
}
|
|
302
|
+
if (!healthGate.proceed) {
|
|
303
|
+
ctx.ui.notify(healthGate.reason ?? "Pre-dispatch health check failed.", "error");
|
|
304
|
+
await deps.pauseAuto(ctx, pi);
|
|
305
|
+
debugLog("autoLoop", { phase: "exit", reason: "health-gate-failed" });
|
|
306
|
+
return { action: "break", reason: "health-gate-failed" };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// Non-fatal
|
|
311
|
+
}
|
|
312
|
+
// Sync project root artifacts into worktree
|
|
313
|
+
if (s.originalBasePath &&
|
|
314
|
+
s.basePath !== s.originalBasePath &&
|
|
315
|
+
s.currentMilestoneId) {
|
|
316
|
+
deps.syncProjectRootToWorktree(s.originalBasePath, s.basePath, s.currentMilestoneId);
|
|
317
|
+
}
|
|
318
|
+
// Derive state
|
|
319
|
+
let state = await deps.deriveState(s.basePath);
|
|
320
|
+
deps.syncCmuxSidebar(prefs, state);
|
|
321
|
+
let mid = state.activeMilestone?.id;
|
|
322
|
+
let midTitle = state.activeMilestone?.title;
|
|
323
|
+
debugLog("autoLoop", {
|
|
324
|
+
phase: "state-derived",
|
|
325
|
+
iteration: ic.iteration,
|
|
326
|
+
mid,
|
|
327
|
+
statePhase: state.phase,
|
|
328
|
+
});
|
|
329
|
+
// ── Milestone transition ────────────────────────────────────────────
|
|
330
|
+
if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
|
|
331
|
+
ctx.ui.notify(`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`, "info");
|
|
332
|
+
deps.sendDesktopNotification("GSD", `Milestone ${s.currentMilestoneId} complete!`, "success", "milestone");
|
|
333
|
+
deps.logCmuxEvent(prefs, `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`, "success");
|
|
334
|
+
const vizPrefs = prefs;
|
|
335
|
+
if (vizPrefs?.auto_visualize) {
|
|
336
|
+
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
337
|
+
}
|
|
338
|
+
if (vizPrefs?.auto_report !== false) {
|
|
339
|
+
try {
|
|
340
|
+
await generateMilestoneReport(s, ctx, s.currentMilestoneId);
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
ctx.ui.notify(`Report generation failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Reset dispatch counters for new milestone
|
|
347
|
+
s.unitDispatchCount.clear();
|
|
348
|
+
s.unitRecoveryCount.clear();
|
|
349
|
+
s.unitLifetimeDispatches.clear();
|
|
350
|
+
loopState.recentUnits.length = 0;
|
|
351
|
+
loopState.stuckRecoveryAttempts = 0;
|
|
352
|
+
// Worktree lifecycle on milestone transition — merge current, enter next
|
|
353
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
354
|
+
// Opt-in: create draft PR on milestone completion
|
|
355
|
+
if (prefs?.git?.auto_pr) {
|
|
356
|
+
try {
|
|
357
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
358
|
+
const prUrl = createDraftPR(s.basePath, s.currentMilestoneId, `[GSD] ${s.currentMilestoneId} complete`, `Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`);
|
|
359
|
+
if (prUrl) {
|
|
360
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// Non-fatal — PR creation is best-effort
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
deps.invalidateAllCaches();
|
|
368
|
+
state = await deps.deriveState(s.basePath);
|
|
369
|
+
mid = state.activeMilestone?.id;
|
|
370
|
+
midTitle = state.activeMilestone?.title;
|
|
371
|
+
if (mid) {
|
|
372
|
+
if (deps.getIsolationMode() !== "none") {
|
|
373
|
+
deps.captureIntegrationBranch(s.basePath, mid, {
|
|
374
|
+
commitDocs: prefs?.git?.commit_docs,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
deps.resolver.enterMilestone(mid, ctx.ui);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
// mid is undefined — no milestone to capture integration branch for
|
|
381
|
+
}
|
|
382
|
+
const pendingIds = state.registry
|
|
383
|
+
.filter((m) => m.status !== "complete" && m.status !== "parked")
|
|
384
|
+
.map((m) => m.id);
|
|
385
|
+
deps.pruneQueueOrder(s.basePath, pendingIds);
|
|
386
|
+
}
|
|
387
|
+
if (mid) {
|
|
388
|
+
s.currentMilestoneId = mid;
|
|
389
|
+
deps.setActiveMilestoneId(s.basePath, mid);
|
|
390
|
+
}
|
|
391
|
+
// ── Terminal conditions ──────────────────────────────────────────────
|
|
392
|
+
if (!mid) {
|
|
393
|
+
if (s.currentUnit) {
|
|
394
|
+
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
395
|
+
}
|
|
396
|
+
const incomplete = state.registry.filter((m) => m.status !== "complete" && m.status !== "parked");
|
|
397
|
+
if (incomplete.length === 0 && state.registry.length > 0) {
|
|
398
|
+
// All milestones complete — merge milestone branch before stopping
|
|
399
|
+
if (s.currentMilestoneId) {
|
|
400
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
401
|
+
// Opt-in: create draft PR on milestone completion
|
|
402
|
+
if (prefs?.git?.auto_pr) {
|
|
403
|
+
try {
|
|
404
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
405
|
+
const prUrl = createDraftPR(s.basePath, s.currentMilestoneId, `[GSD] ${s.currentMilestoneId} complete`, `Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`);
|
|
406
|
+
if (prUrl) {
|
|
407
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
// Non-fatal — PR creation is best-effort
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
deps.sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
|
|
416
|
+
deps.logCmuxEvent(prefs, "All milestones complete.", "success");
|
|
417
|
+
await deps.stopAuto(ctx, pi, "All milestones complete");
|
|
418
|
+
}
|
|
419
|
+
else if (incomplete.length === 0 && state.registry.length === 0) {
|
|
420
|
+
// Empty registry — no milestones visible, likely a path resolution bug
|
|
421
|
+
const diag = `basePath=${s.basePath}, phase=${state.phase}`;
|
|
422
|
+
ctx.ui.notify(`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`, "error");
|
|
423
|
+
await deps.stopAuto(ctx, pi, `No milestones found — check basePath resolution`);
|
|
424
|
+
}
|
|
425
|
+
else if (state.phase === "blocked") {
|
|
426
|
+
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
427
|
+
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
428
|
+
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
429
|
+
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
430
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
const ids = incomplete.map((m) => m.id).join(", ");
|
|
434
|
+
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
435
|
+
ctx.ui.notify(`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`, "error");
|
|
436
|
+
await deps.stopAuto(ctx, pi, `No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`);
|
|
437
|
+
}
|
|
438
|
+
debugLog("autoLoop", { phase: "exit", reason: "no-active-milestone" });
|
|
439
|
+
return { action: "break", reason: "no-active-milestone" };
|
|
440
|
+
}
|
|
441
|
+
if (!midTitle) {
|
|
442
|
+
midTitle = mid;
|
|
443
|
+
ctx.ui.notify(`Milestone ${mid} has no title in roadmap — using ID as fallback.`, "warning");
|
|
444
|
+
}
|
|
445
|
+
// Mid-merge safety check
|
|
446
|
+
if (deps.reconcileMergeState(s.basePath, ctx)) {
|
|
447
|
+
deps.invalidateAllCaches();
|
|
448
|
+
state = await deps.deriveState(s.basePath);
|
|
449
|
+
mid = state.activeMilestone?.id;
|
|
450
|
+
midTitle = state.activeMilestone?.title;
|
|
451
|
+
}
|
|
452
|
+
if (!mid || !midTitle) {
|
|
453
|
+
const noMilestoneReason = !mid
|
|
454
|
+
? "No active milestone after merge reconciliation"
|
|
455
|
+
: `Milestone ${mid} has no title after reconciliation`;
|
|
456
|
+
await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
|
|
457
|
+
debugLog("autoLoop", {
|
|
458
|
+
phase: "exit",
|
|
459
|
+
reason: "no-milestone-after-reconciliation",
|
|
460
|
+
});
|
|
461
|
+
return { action: "break", reason: "no-milestone-after-reconciliation" };
|
|
462
|
+
}
|
|
463
|
+
// Terminal: complete
|
|
464
|
+
if (state.phase === "complete") {
|
|
465
|
+
// Milestone merge on complete (before closeout so branch state is clean)
|
|
466
|
+
if (s.currentMilestoneId) {
|
|
467
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
468
|
+
// Opt-in: create draft PR on milestone completion
|
|
469
|
+
if (prefs?.git?.auto_pr) {
|
|
470
|
+
try {
|
|
471
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
472
|
+
const prUrl = createDraftPR(s.basePath, s.currentMilestoneId, `[GSD] ${s.currentMilestoneId} complete`, `Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`);
|
|
473
|
+
if (prUrl) {
|
|
474
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
// Non-fatal — PR creation is best-effort
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
deps.sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
|
|
483
|
+
deps.logCmuxEvent(prefs, `Milestone ${mid} complete.`, "success");
|
|
484
|
+
await closeoutAndStop(ctx, pi, s, deps, `Milestone ${mid} complete`);
|
|
485
|
+
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
486
|
+
return { action: "break", reason: "milestone-complete" };
|
|
487
|
+
}
|
|
488
|
+
// Terminal: blocked
|
|
489
|
+
if (state.phase === "blocked") {
|
|
490
|
+
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
491
|
+
await closeoutAndStop(ctx, pi, s, deps, blockerMsg);
|
|
492
|
+
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
493
|
+
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
494
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
495
|
+
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
496
|
+
return { action: "break", reason: "blocked" };
|
|
497
|
+
}
|
|
498
|
+
return { action: "next", data: { state, mid, midTitle } };
|
|
499
|
+
}
|
|
500
|
+
// ─── runDispatch ──────────────────────────────────────────────────────────────
|
|
501
|
+
/**
|
|
502
|
+
* Phase 3: Dispatch resolution — resolve next unit, stuck detection, pre-dispatch hooks.
|
|
503
|
+
* Returns break/continue to control the loop, or next with IterationData on success.
|
|
504
|
+
*/
|
|
505
|
+
async function runDispatch(ic, preData, loopState) {
|
|
506
|
+
const { ctx, pi, s, deps, prefs } = ic;
|
|
507
|
+
const { state, mid, midTitle } = preData;
|
|
508
|
+
const STUCK_WINDOW_SIZE = 6;
|
|
509
|
+
debugLog("autoLoop", { phase: "dispatch-resolve", iteration: ic.iteration });
|
|
510
|
+
const dispatchResult = await deps.resolveDispatch({
|
|
511
|
+
basePath: s.basePath,
|
|
512
|
+
mid,
|
|
513
|
+
midTitle,
|
|
514
|
+
state,
|
|
515
|
+
prefs,
|
|
516
|
+
session: s,
|
|
517
|
+
});
|
|
518
|
+
if (dispatchResult.action === "stop") {
|
|
519
|
+
await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
|
|
520
|
+
debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
|
|
521
|
+
return { action: "break", reason: "dispatch-stop" };
|
|
522
|
+
}
|
|
523
|
+
if (dispatchResult.action !== "dispatch") {
|
|
524
|
+
// Non-dispatch action (e.g. "skip") — re-derive state
|
|
525
|
+
await new Promise((r) => setImmediate(r));
|
|
526
|
+
return { action: "continue" };
|
|
527
|
+
}
|
|
528
|
+
let unitType = dispatchResult.unitType;
|
|
529
|
+
let unitId = dispatchResult.unitId;
|
|
530
|
+
let prompt = dispatchResult.prompt;
|
|
531
|
+
const pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
532
|
+
// ── Sliding-window stuck detection with graduated recovery ──
|
|
533
|
+
const derivedKey = `${unitType}/${unitId}`;
|
|
534
|
+
if (!s.pendingVerificationRetry) {
|
|
535
|
+
loopState.recentUnits.push({ key: derivedKey });
|
|
536
|
+
if (loopState.recentUnits.length > STUCK_WINDOW_SIZE)
|
|
537
|
+
loopState.recentUnits.shift();
|
|
538
|
+
const stuckSignal = detectStuck(loopState.recentUnits);
|
|
539
|
+
if (stuckSignal) {
|
|
540
|
+
debugLog("autoLoop", {
|
|
541
|
+
phase: "stuck-check",
|
|
542
|
+
unitType,
|
|
543
|
+
unitId,
|
|
544
|
+
reason: stuckSignal.reason,
|
|
545
|
+
recoveryAttempts: loopState.stuckRecoveryAttempts,
|
|
546
|
+
});
|
|
547
|
+
if (loopState.stuckRecoveryAttempts === 0) {
|
|
548
|
+
// Level 1: try verifying the artifact, then cache invalidation + retry
|
|
549
|
+
loopState.stuckRecoveryAttempts++;
|
|
550
|
+
const artifactExists = deps.verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
551
|
+
if (artifactExists) {
|
|
552
|
+
debugLog("autoLoop", {
|
|
553
|
+
phase: "stuck-recovery",
|
|
554
|
+
level: 1,
|
|
555
|
+
action: "artifact-found",
|
|
556
|
+
});
|
|
557
|
+
ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`, "info");
|
|
558
|
+
deps.invalidateAllCaches();
|
|
559
|
+
return { action: "continue" };
|
|
560
|
+
}
|
|
561
|
+
ctx.ui.notify(`Stuck on ${unitType} ${unitId} (${stuckSignal.reason}). Invalidating caches and retrying.`, "warning");
|
|
562
|
+
deps.invalidateAllCaches();
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
// Level 2: hard stop — genuinely stuck
|
|
566
|
+
debugLog("autoLoop", {
|
|
567
|
+
phase: "stuck-detected",
|
|
568
|
+
unitType,
|
|
569
|
+
unitId,
|
|
570
|
+
reason: stuckSignal.reason,
|
|
571
|
+
});
|
|
572
|
+
await deps.stopAuto(ctx, pi, `Stuck: ${stuckSignal.reason}`);
|
|
573
|
+
ctx.ui.notify(`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}. The expected artifact was not written.`, "error");
|
|
574
|
+
return { action: "break", reason: "stuck-detected" };
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
// Progress detected — reset recovery counter
|
|
579
|
+
if (loopState.stuckRecoveryAttempts > 0) {
|
|
580
|
+
debugLog("autoLoop", {
|
|
581
|
+
phase: "stuck-counter-reset",
|
|
582
|
+
from: loopState.recentUnits[loopState.recentUnits.length - 2]?.key ?? "",
|
|
583
|
+
to: derivedKey,
|
|
584
|
+
});
|
|
585
|
+
loopState.stuckRecoveryAttempts = 0;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// Pre-dispatch hooks
|
|
590
|
+
const preDispatchResult = deps.runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
|
|
591
|
+
if (preDispatchResult.firedHooks.length > 0) {
|
|
592
|
+
ctx.ui.notify(`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`, "info");
|
|
593
|
+
}
|
|
594
|
+
if (preDispatchResult.action === "skip") {
|
|
595
|
+
ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
|
|
596
|
+
await new Promise((r) => setImmediate(r));
|
|
597
|
+
return { action: "continue" };
|
|
598
|
+
}
|
|
599
|
+
if (preDispatchResult.action === "replace") {
|
|
600
|
+
prompt = preDispatchResult.prompt ?? prompt;
|
|
601
|
+
if (preDispatchResult.unitType)
|
|
602
|
+
unitType = preDispatchResult.unitType;
|
|
603
|
+
}
|
|
604
|
+
else if (preDispatchResult.prompt) {
|
|
605
|
+
prompt = preDispatchResult.prompt;
|
|
606
|
+
}
|
|
607
|
+
const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(s.basePath, deps.getMainBranch(s.basePath), unitType, unitId);
|
|
608
|
+
if (priorSliceBlocker) {
|
|
609
|
+
await deps.stopAuto(ctx, pi, priorSliceBlocker);
|
|
610
|
+
debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
|
|
611
|
+
return { action: "break", reason: "prior-slice-blocker" };
|
|
612
|
+
}
|
|
613
|
+
const observabilityIssues = await deps.collectObservabilityWarnings(ctx, s.basePath, unitType, unitId);
|
|
614
|
+
return {
|
|
615
|
+
action: "next",
|
|
616
|
+
data: {
|
|
617
|
+
unitType, unitId, prompt, finalPrompt: prompt,
|
|
618
|
+
pauseAfterUatDispatch, observabilityIssues,
|
|
619
|
+
state, mid, midTitle,
|
|
620
|
+
isRetry: false, previousTier: undefined,
|
|
621
|
+
},
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
// ─── runGuards ────────────────────────────────────────────────────────────────
|
|
625
|
+
/**
|
|
626
|
+
* Phase 2: Guards — budget ceiling, context window, secrets re-check.
|
|
627
|
+
* Returns break to exit the loop, or next to proceed to dispatch.
|
|
628
|
+
*/
|
|
629
|
+
async function runGuards(ic, mid) {
|
|
630
|
+
const { ctx, pi, s, deps, prefs } = ic;
|
|
631
|
+
// Budget ceiling guard
|
|
632
|
+
const budgetCeiling = prefs?.budget_ceiling;
|
|
633
|
+
if (budgetCeiling !== undefined && budgetCeiling > 0) {
|
|
634
|
+
const currentLedger = deps.getLedger();
|
|
635
|
+
const totalCost = currentLedger
|
|
636
|
+
? deps.getProjectTotals(currentLedger.units).cost
|
|
637
|
+
: 0;
|
|
638
|
+
const budgetPct = totalCost / budgetCeiling;
|
|
639
|
+
const budgetAlertLevel = deps.getBudgetAlertLevel(budgetPct);
|
|
640
|
+
const newBudgetAlertLevel = deps.getNewBudgetAlertLevel(s.lastBudgetAlertLevel, budgetPct);
|
|
641
|
+
const enforcement = prefs?.budget_enforcement ?? "pause";
|
|
642
|
+
const budgetEnforcementAction = deps.getBudgetEnforcementAction(enforcement, budgetPct);
|
|
643
|
+
// Data-driven threshold check — loop descending, fire first match
|
|
644
|
+
const threshold = BUDGET_THRESHOLDS.find((t) => newBudgetAlertLevel >= t.pct);
|
|
645
|
+
if (threshold) {
|
|
646
|
+
s.lastBudgetAlertLevel =
|
|
647
|
+
newBudgetAlertLevel;
|
|
648
|
+
if (threshold.pct === 100 && budgetEnforcementAction !== "none") {
|
|
649
|
+
// 100% — special enforcement logic (halt/pause/warn)
|
|
650
|
+
const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
|
|
651
|
+
if (budgetEnforcementAction === "halt") {
|
|
652
|
+
deps.sendDesktopNotification("GSD", msg, "error", "budget");
|
|
653
|
+
await deps.stopAuto(ctx, pi, "Budget ceiling reached");
|
|
654
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
|
|
655
|
+
return { action: "break", reason: "budget-halt" };
|
|
656
|
+
}
|
|
657
|
+
if (budgetEnforcementAction === "pause") {
|
|
658
|
+
ctx.ui.notify(`${msg} Pausing auto-mode — /gsd auto to override and continue.`, "warning");
|
|
659
|
+
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
660
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
661
|
+
await deps.pauseAuto(ctx, pi);
|
|
662
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
663
|
+
return { action: "break", reason: "budget-pause" };
|
|
664
|
+
}
|
|
665
|
+
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
666
|
+
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
667
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
668
|
+
}
|
|
669
|
+
else if (threshold.pct < 100) {
|
|
670
|
+
// Sub-100% — simple notification
|
|
671
|
+
const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
|
|
672
|
+
ctx.ui.notify(msg, threshold.notifyLevel);
|
|
673
|
+
deps.sendDesktopNotification("GSD", msg, threshold.notifyLevel, "budget");
|
|
674
|
+
deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
else if (budgetAlertLevel === 0) {
|
|
678
|
+
s.lastBudgetAlertLevel = 0;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
s.lastBudgetAlertLevel = 0;
|
|
683
|
+
}
|
|
684
|
+
// Context window guard
|
|
685
|
+
const contextThreshold = prefs?.context_pause_threshold ?? 0;
|
|
686
|
+
if (contextThreshold > 0 && s.cmdCtx) {
|
|
687
|
+
const contextUsage = s.cmdCtx.getContextUsage();
|
|
688
|
+
if (contextUsage &&
|
|
689
|
+
contextUsage.percent !== null &&
|
|
690
|
+
contextUsage.percent >= contextThreshold) {
|
|
691
|
+
const msg = `Context window at ${contextUsage.percent}% (threshold: ${contextThreshold}%). Pausing to prevent truncated output.`;
|
|
692
|
+
ctx.ui.notify(`${msg} Run /gsd auto to continue (will start fresh session).`, "warning");
|
|
693
|
+
deps.sendDesktopNotification("GSD", `Context ${contextUsage.percent}% — paused`, "warning", "attention");
|
|
694
|
+
await deps.pauseAuto(ctx, pi);
|
|
695
|
+
debugLog("autoLoop", { phase: "exit", reason: "context-window" });
|
|
696
|
+
return { action: "break", reason: "context-window" };
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// Secrets re-check gate
|
|
700
|
+
try {
|
|
701
|
+
const manifestStatus = await deps.getManifestStatus(s.basePath, mid, s.originalBasePath);
|
|
702
|
+
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
703
|
+
const result = await deps.collectSecretsFromManifest(s.basePath, mid, ctx);
|
|
704
|
+
if (result &&
|
|
705
|
+
result.applied &&
|
|
706
|
+
result.skipped &&
|
|
707
|
+
result.existingSkipped) {
|
|
708
|
+
ctx.ui.notify(`Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`, "info");
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
ctx.ui.notify("Secrets collection skipped.", "info");
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
catch (err) {
|
|
716
|
+
ctx.ui.notify(`Secrets collection error: ${err instanceof Error ? err.message : String(err)}. Continuing with next task.`, "warning");
|
|
717
|
+
}
|
|
718
|
+
return { action: "next", data: undefined };
|
|
719
|
+
}
|
|
720
|
+
// ─── runUnitPhase ─────────────────────────────────────────────────────────────
|
|
721
|
+
/**
|
|
722
|
+
* Phase 4: Unit execution — dispatch prompt, await agent_end, closeout, artifact verify.
|
|
723
|
+
* Returns break or next with unitStartedAt for downstream phases.
|
|
724
|
+
*/
|
|
725
|
+
async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
726
|
+
const { ctx, pi, s, deps, prefs } = ic;
|
|
727
|
+
const { unitType, unitId, prompt, observabilityIssues, state, mid } = iterData;
|
|
728
|
+
debugLog("autoLoop", {
|
|
729
|
+
phase: "unit-execution",
|
|
730
|
+
iteration: ic.iteration,
|
|
731
|
+
unitType,
|
|
732
|
+
unitId,
|
|
733
|
+
});
|
|
734
|
+
// Detect retry and capture previous tier for escalation
|
|
735
|
+
const isRetry = !!(s.currentUnit &&
|
|
736
|
+
s.currentUnit.type === unitType &&
|
|
737
|
+
s.currentUnit.id === unitId);
|
|
738
|
+
const previousTier = s.currentUnitRouting?.tier;
|
|
739
|
+
s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
740
|
+
deps.captureAvailableSkills();
|
|
741
|
+
deps.writeUnitRuntimeRecord(s.basePath, unitType, unitId, s.currentUnit.startedAt, {
|
|
742
|
+
phase: "dispatched",
|
|
743
|
+
wrapupWarningSent: false,
|
|
744
|
+
timeoutAt: null,
|
|
745
|
+
lastProgressAt: s.currentUnit.startedAt,
|
|
746
|
+
progressCount: 0,
|
|
747
|
+
lastProgressKind: "dispatch",
|
|
748
|
+
});
|
|
749
|
+
// Status bar + progress widget
|
|
750
|
+
ctx.ui.setStatus("gsd-auto", "auto");
|
|
751
|
+
if (mid)
|
|
752
|
+
deps.updateSliceProgressCache(s.basePath, mid, state.activeSlice?.id);
|
|
753
|
+
deps.updateProgressWidget(ctx, unitType, unitId, state);
|
|
754
|
+
deps.ensurePreconditions(unitType, unitId, s.basePath, state);
|
|
755
|
+
// Prompt injection
|
|
756
|
+
let finalPrompt = prompt;
|
|
757
|
+
if (s.pendingVerificationRetry) {
|
|
758
|
+
const retryCtx = s.pendingVerificationRetry;
|
|
759
|
+
s.pendingVerificationRetry = null;
|
|
760
|
+
const capped = retryCtx.failureContext.length > MAX_RECOVERY_CHARS
|
|
761
|
+
? retryCtx.failureContext.slice(0, MAX_RECOVERY_CHARS) +
|
|
762
|
+
"\n\n[...failure context truncated]"
|
|
763
|
+
: retryCtx.failureContext;
|
|
764
|
+
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}`;
|
|
765
|
+
}
|
|
766
|
+
if (s.pendingCrashRecovery) {
|
|
767
|
+
const capped = s.pendingCrashRecovery.length > MAX_RECOVERY_CHARS
|
|
768
|
+
? s.pendingCrashRecovery.slice(0, MAX_RECOVERY_CHARS) +
|
|
769
|
+
"\n\n[...recovery briefing truncated to prevent memory exhaustion]"
|
|
770
|
+
: s.pendingCrashRecovery;
|
|
771
|
+
finalPrompt = `${capped}\n\n---\n\n${finalPrompt}`;
|
|
772
|
+
s.pendingCrashRecovery = null;
|
|
773
|
+
}
|
|
774
|
+
else if ((s.unitDispatchCount.get(`${unitType}/${unitId}`) ?? 0) > 1) {
|
|
775
|
+
const diagnostic = deps.getDeepDiagnostic(s.basePath);
|
|
776
|
+
if (diagnostic) {
|
|
777
|
+
const cappedDiag = diagnostic.length > MAX_RECOVERY_CHARS
|
|
778
|
+
? diagnostic.slice(0, MAX_RECOVERY_CHARS) +
|
|
779
|
+
"\n\n[...diagnostic truncated to prevent memory exhaustion]"
|
|
780
|
+
: diagnostic;
|
|
781
|
+
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}`;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const repairBlock = deps.buildObservabilityRepairBlock(observabilityIssues);
|
|
785
|
+
if (repairBlock) {
|
|
786
|
+
finalPrompt = `${finalPrompt}${repairBlock}`;
|
|
787
|
+
}
|
|
788
|
+
// Prompt char measurement
|
|
789
|
+
s.lastPromptCharCount = finalPrompt.length;
|
|
790
|
+
s.lastBaselineCharCount = undefined;
|
|
791
|
+
if (deps.isDbAvailable()) {
|
|
792
|
+
try {
|
|
793
|
+
const { inlineGsdRootFile } = await importExtensionModule(import.meta.url, "./auto-prompts.js");
|
|
794
|
+
const [decisionsContent, requirementsContent, projectContent] = await Promise.all([
|
|
795
|
+
inlineGsdRootFile(s.basePath, "decisions.md", "Decisions"),
|
|
796
|
+
inlineGsdRootFile(s.basePath, "requirements.md", "Requirements"),
|
|
797
|
+
inlineGsdRootFile(s.basePath, "project.md", "Project"),
|
|
798
|
+
]);
|
|
799
|
+
s.lastBaselineCharCount =
|
|
800
|
+
(decisionsContent?.length ?? 0) +
|
|
801
|
+
(requirementsContent?.length ?? 0) +
|
|
802
|
+
(projectContent?.length ?? 0);
|
|
803
|
+
}
|
|
804
|
+
catch {
|
|
805
|
+
// Non-fatal
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
// Cache-optimize prompt section ordering
|
|
809
|
+
try {
|
|
810
|
+
finalPrompt = deps.reorderForCaching(finalPrompt);
|
|
811
|
+
}
|
|
812
|
+
catch (reorderErr) {
|
|
813
|
+
const msg = reorderErr instanceof Error ? reorderErr.message : String(reorderErr);
|
|
814
|
+
process.stderr.write(`[gsd] prompt reorder failed (non-fatal): ${msg}\n`);
|
|
815
|
+
}
|
|
816
|
+
// Select and apply model (with tier escalation on retry — normal units only)
|
|
817
|
+
const modelResult = await deps.selectAndApplyModel(ctx, pi, unitType, unitId, s.basePath, prefs, s.verbose, s.autoModeStartModel, sidecarItem ? undefined : { isRetry, previousTier });
|
|
818
|
+
s.currentUnitRouting =
|
|
819
|
+
modelResult.routing;
|
|
820
|
+
// Start unit supervision
|
|
821
|
+
deps.clearUnitTimeout();
|
|
822
|
+
deps.startUnitSupervision({
|
|
823
|
+
s,
|
|
824
|
+
ctx,
|
|
825
|
+
pi,
|
|
826
|
+
unitType,
|
|
827
|
+
unitId,
|
|
828
|
+
prefs,
|
|
829
|
+
buildSnapshotOpts: () => deps.buildSnapshotOpts(unitType, unitId),
|
|
830
|
+
buildRecoveryContext: () => ({}),
|
|
831
|
+
pauseAuto: deps.pauseAuto,
|
|
832
|
+
});
|
|
833
|
+
// Session + send + await
|
|
834
|
+
const sessionFile = deps.getSessionFile(ctx);
|
|
835
|
+
deps.updateSessionLock(deps.lockBase(), unitType, unitId, s.completedUnits.length, sessionFile);
|
|
836
|
+
deps.writeLock(deps.lockBase(), unitType, unitId, s.completedUnits.length, sessionFile);
|
|
837
|
+
debugLog("autoLoop", {
|
|
838
|
+
phase: "runUnit-start",
|
|
839
|
+
iteration: ic.iteration,
|
|
840
|
+
unitType,
|
|
841
|
+
unitId,
|
|
842
|
+
});
|
|
843
|
+
const unitResult = await runUnit(ctx, pi, s, unitType, unitId, finalPrompt);
|
|
844
|
+
debugLog("autoLoop", {
|
|
845
|
+
phase: "runUnit-end",
|
|
846
|
+
iteration: ic.iteration,
|
|
847
|
+
unitType,
|
|
848
|
+
unitId,
|
|
849
|
+
status: unitResult.status,
|
|
850
|
+
});
|
|
851
|
+
// Tag the most recent window entry with error info for stuck detection
|
|
852
|
+
if (unitResult.status === "error" || unitResult.status === "cancelled") {
|
|
853
|
+
const lastEntry = loopState.recentUnits[loopState.recentUnits.length - 1];
|
|
854
|
+
if (lastEntry) {
|
|
855
|
+
lastEntry.error = `${unitResult.status}:${unitType}/${unitId}`;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
else if (unitResult.event?.messages?.length) {
|
|
859
|
+
const lastMsg = unitResult.event.messages[unitResult.event.messages.length - 1];
|
|
860
|
+
const msgStr = typeof lastMsg === "string" ? lastMsg : JSON.stringify(lastMsg);
|
|
861
|
+
if (/error|fail|exception/i.test(msgStr)) {
|
|
862
|
+
const lastEntry = loopState.recentUnits[loopState.recentUnits.length - 1];
|
|
863
|
+
if (lastEntry) {
|
|
864
|
+
lastEntry.error = msgStr.slice(0, 200);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (unitResult.status === "cancelled") {
|
|
869
|
+
ctx.ui.notify(`Session creation timed out or was cancelled for ${unitType} ${unitId}. Will retry.`, "warning");
|
|
870
|
+
await deps.stopAuto(ctx, pi, "Session creation failed");
|
|
871
|
+
debugLog("autoLoop", { phase: "exit", reason: "session-failed" });
|
|
872
|
+
return { action: "break", reason: "session-failed" };
|
|
873
|
+
}
|
|
874
|
+
// ── Immediate unit closeout (metrics, activity log, memory) ────────
|
|
875
|
+
// Run right after runUnit() returns so telemetry is never lost to a
|
|
876
|
+
// crash between iterations.
|
|
877
|
+
await deps.closeoutUnit(ctx, s.basePath, unitType, unitId, s.currentUnit.startedAt, deps.buildSnapshotOpts(unitType, unitId));
|
|
878
|
+
if (s.currentUnitRouting) {
|
|
879
|
+
deps.recordOutcome(unitType, s.currentUnitRouting.tier, true);
|
|
880
|
+
}
|
|
881
|
+
const isHookUnit = unitType.startsWith("hook/");
|
|
882
|
+
const artifactVerified = isHookUnit ||
|
|
883
|
+
deps.verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
884
|
+
if (artifactVerified) {
|
|
885
|
+
s.completedUnits.push({
|
|
886
|
+
type: unitType,
|
|
887
|
+
id: unitId,
|
|
888
|
+
startedAt: s.currentUnit.startedAt,
|
|
889
|
+
finishedAt: Date.now(),
|
|
890
|
+
});
|
|
891
|
+
if (s.completedUnits.length > 200) {
|
|
892
|
+
s.completedUnits = s.completedUnits.slice(-200);
|
|
893
|
+
}
|
|
894
|
+
// Flush completed-units to disk so the record survives crashes
|
|
895
|
+
try {
|
|
896
|
+
const completedKeysPath = join(gsdRoot(s.basePath), "completed-units.json");
|
|
897
|
+
const keys = s.completedUnits.map((u) => `${u.type}/${u.id}`);
|
|
898
|
+
atomicWriteSync(completedKeysPath, JSON.stringify(keys, null, 2));
|
|
899
|
+
}
|
|
900
|
+
catch { /* non-fatal: disk flush failure */ }
|
|
901
|
+
deps.clearUnitRuntimeRecord(s.basePath, unitType, unitId);
|
|
902
|
+
s.unitDispatchCount.delete(`${unitType}/${unitId}`);
|
|
903
|
+
s.unitRecoveryCount.delete(`${unitType}/${unitId}`);
|
|
904
|
+
}
|
|
905
|
+
return { action: "next", data: { unitStartedAt: s.currentUnit.startedAt } };
|
|
906
|
+
}
|
|
907
|
+
// ─── runFinalize ──────────────────────────────────────────────────────────────
|
|
908
|
+
/**
|
|
909
|
+
* Phase 5: Post-unit finalize — pre/post verification, UAT pause, step-wizard.
|
|
910
|
+
* Returns break/continue/next to control the outer loop.
|
|
911
|
+
*/
|
|
912
|
+
async function runFinalize(ic, iterData, sidecarItem) {
|
|
913
|
+
const { ctx, pi, s, deps } = ic;
|
|
914
|
+
const { pauseAfterUatDispatch } = iterData;
|
|
915
|
+
debugLog("autoLoop", { phase: "finalize", iteration: ic.iteration });
|
|
916
|
+
// Clear unit timeout (unit completed)
|
|
917
|
+
deps.clearUnitTimeout();
|
|
918
|
+
// Post-unit context for pre/post verification
|
|
919
|
+
const postUnitCtx = {
|
|
920
|
+
s,
|
|
921
|
+
ctx,
|
|
922
|
+
pi,
|
|
923
|
+
buildSnapshotOpts: deps.buildSnapshotOpts,
|
|
924
|
+
lockBase: deps.lockBase,
|
|
925
|
+
stopAuto: deps.stopAuto,
|
|
926
|
+
pauseAuto: deps.pauseAuto,
|
|
927
|
+
updateProgressWidget: deps.updateProgressWidget,
|
|
928
|
+
};
|
|
929
|
+
// Pre-verification processing (commit, doctor, state rebuild, etc.)
|
|
930
|
+
// Sidecar items use lightweight pre-verification opts
|
|
931
|
+
const preVerificationOpts = sidecarItem
|
|
932
|
+
? sidecarItem.kind === "hook"
|
|
933
|
+
? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
|
|
934
|
+
: { skipSettleDelay: true, skipStateRebuild: true }
|
|
935
|
+
: undefined;
|
|
936
|
+
const preResult = await deps.postUnitPreVerification(postUnitCtx, preVerificationOpts);
|
|
937
|
+
if (preResult === "dispatched") {
|
|
938
|
+
debugLog("autoLoop", {
|
|
939
|
+
phase: "exit",
|
|
940
|
+
reason: "pre-verification-dispatched",
|
|
941
|
+
});
|
|
942
|
+
return { action: "break", reason: "pre-verification-dispatched" };
|
|
943
|
+
}
|
|
944
|
+
if (pauseAfterUatDispatch) {
|
|
945
|
+
ctx.ui.notify("UAT requires human execution. Auto-mode will pause after this unit writes the result file.", "info");
|
|
946
|
+
await deps.pauseAuto(ctx, pi);
|
|
947
|
+
debugLog("autoLoop", { phase: "exit", reason: "uat-pause" });
|
|
948
|
+
return { action: "break", reason: "uat-pause" };
|
|
949
|
+
}
|
|
950
|
+
// Verification gate
|
|
951
|
+
// Hook sidecar items skip verification entirely.
|
|
952
|
+
// Non-hook sidecar items run verification but skip retries (just continue).
|
|
953
|
+
const skipVerification = sidecarItem?.kind === "hook";
|
|
954
|
+
if (!skipVerification) {
|
|
955
|
+
const verificationResult = await deps.runPostUnitVerification({ s, ctx, pi }, deps.pauseAuto);
|
|
956
|
+
if (verificationResult === "pause") {
|
|
957
|
+
debugLog("autoLoop", { phase: "exit", reason: "verification-pause" });
|
|
958
|
+
return { action: "break", reason: "verification-pause" };
|
|
959
|
+
}
|
|
960
|
+
if (verificationResult === "retry") {
|
|
961
|
+
if (sidecarItem) {
|
|
962
|
+
// Sidecar verification retries are skipped — just continue
|
|
963
|
+
debugLog("autoLoop", { phase: "sidecar-verification-retry-skipped", iteration: ic.iteration });
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
// s.pendingVerificationRetry was set by runPostUnitVerification.
|
|
967
|
+
// Continue the loop — next iteration will inject the retry context into the prompt.
|
|
968
|
+
debugLog("autoLoop", { phase: "verification-retry", iteration: ic.iteration });
|
|
969
|
+
return { action: "continue" };
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
// Post-verification processing (DB dual-write, hooks, triage, quick-tasks)
|
|
974
|
+
const postResult = await deps.postUnitPostVerification(postUnitCtx);
|
|
975
|
+
if (postResult === "stopped") {
|
|
976
|
+
debugLog("autoLoop", {
|
|
977
|
+
phase: "exit",
|
|
978
|
+
reason: "post-verification-stopped",
|
|
979
|
+
});
|
|
980
|
+
return { action: "break", reason: "post-verification-stopped" };
|
|
981
|
+
}
|
|
982
|
+
if (postResult === "step-wizard") {
|
|
983
|
+
// Step mode — exit the loop (caller handles wizard)
|
|
984
|
+
debugLog("autoLoop", { phase: "exit", reason: "step-wizard" });
|
|
985
|
+
return { action: "break", reason: "step-wizard" };
|
|
986
|
+
}
|
|
987
|
+
return { action: "next", data: undefined };
|
|
988
|
+
}
|
|
187
989
|
// ─── autoLoop ────────────────────────────────────────────────────────────────
|
|
188
990
|
/**
|
|
189
991
|
* Main auto-mode execution loop. Iterates: derive → dispatch → guards →
|
|
@@ -195,10 +997,8 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt, _prefs) {
|
|
|
195
997
|
*/
|
|
196
998
|
export async function autoLoop(ctx, pi, s, deps) {
|
|
197
999
|
debugLog("autoLoop", { phase: "enter" });
|
|
198
|
-
_activeSession = s;
|
|
199
1000
|
let iteration = 0;
|
|
200
|
-
|
|
201
|
-
let sameUnitCount = 0;
|
|
1001
|
+
const loopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
|
|
202
1002
|
let consecutiveErrors = 0;
|
|
203
1003
|
while (s.active) {
|
|
204
1004
|
iteration++;
|
|
@@ -218,6 +1018,18 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
218
1018
|
}
|
|
219
1019
|
try {
|
|
220
1020
|
// ── Blanket try/catch: one bad iteration must not kill the session
|
|
1021
|
+
const prefs = deps.loadEffectiveGSDPreferences()?.preferences;
|
|
1022
|
+
// ── Check sidecar queue before deriveState ──
|
|
1023
|
+
let sidecarItem;
|
|
1024
|
+
if (s.sidecarQueue.length > 0) {
|
|
1025
|
+
sidecarItem = s.sidecarQueue.shift();
|
|
1026
|
+
debugLog("autoLoop", {
|
|
1027
|
+
phase: "sidecar-dequeue",
|
|
1028
|
+
kind: sidecarItem.kind,
|
|
1029
|
+
unitType: sidecarItem.unitType,
|
|
1030
|
+
unitId: sidecarItem.unitId,
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
221
1033
|
const sessionLockBase = deps.lockBase();
|
|
222
1034
|
if (sessionLockBase) {
|
|
223
1035
|
const lockStatus = deps.validateSessionLock(sessionLockBase);
|
|
@@ -237,729 +1049,53 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
237
1049
|
break;
|
|
238
1050
|
}
|
|
239
1051
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
await
|
|
245
|
-
|
|
246
|
-
break;
|
|
247
|
-
}
|
|
248
|
-
deps.invalidateAllCaches();
|
|
249
|
-
s.lastPromptCharCount = undefined;
|
|
250
|
-
s.lastBaselineCharCount = undefined;
|
|
251
|
-
// Pre-dispatch health gate
|
|
252
|
-
try {
|
|
253
|
-
const healthGate = await deps.preDispatchHealthGate(s.basePath);
|
|
254
|
-
if (healthGate.fixesApplied.length > 0) {
|
|
255
|
-
ctx.ui.notify(`Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`, "info");
|
|
256
|
-
}
|
|
257
|
-
if (!healthGate.proceed) {
|
|
258
|
-
ctx.ui.notify(healthGate.reason ?? "Pre-dispatch health check failed.", "error");
|
|
259
|
-
await deps.pauseAuto(ctx, pi);
|
|
260
|
-
debugLog("autoLoop", { phase: "exit", reason: "health-gate-failed" });
|
|
1052
|
+
const ic = { ctx, pi, s, deps, prefs, iteration };
|
|
1053
|
+
let iterData;
|
|
1054
|
+
if (!sidecarItem) {
|
|
1055
|
+
// ── Phase 1: Pre-dispatch ─────────────────────────────────────────
|
|
1056
|
+
const preDispatchResult = await runPreDispatch(ic, loopState);
|
|
1057
|
+
if (preDispatchResult.action === "break")
|
|
261
1058
|
break;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (s.originalBasePath &&
|
|
269
|
-
s.basePath !== s.originalBasePath &&
|
|
270
|
-
s.currentMilestoneId) {
|
|
271
|
-
deps.syncProjectRootToWorktree(s.originalBasePath, s.basePath, s.currentMilestoneId);
|
|
272
|
-
}
|
|
273
|
-
// Derive state
|
|
274
|
-
let state = await deps.deriveState(s.basePath);
|
|
275
|
-
deps.syncCmuxSidebar(deps.loadEffectiveGSDPreferences()?.preferences, state);
|
|
276
|
-
let mid = state.activeMilestone?.id;
|
|
277
|
-
let midTitle = state.activeMilestone?.title;
|
|
278
|
-
debugLog("autoLoop", {
|
|
279
|
-
phase: "state-derived",
|
|
280
|
-
iteration,
|
|
281
|
-
mid,
|
|
282
|
-
statePhase: state.phase,
|
|
283
|
-
});
|
|
284
|
-
// ── Milestone transition ────────────────────────────────────────────
|
|
285
|
-
if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
|
|
286
|
-
ctx.ui.notify(`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`, "info");
|
|
287
|
-
deps.sendDesktopNotification("GSD", `Milestone ${s.currentMilestoneId} complete!`, "success", "milestone");
|
|
288
|
-
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`, "success");
|
|
289
|
-
const vizPrefs = deps.loadEffectiveGSDPreferences()?.preferences;
|
|
290
|
-
if (vizPrefs?.auto_visualize) {
|
|
291
|
-
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
292
|
-
}
|
|
293
|
-
if (vizPrefs?.auto_report !== false) {
|
|
294
|
-
try {
|
|
295
|
-
const { loadVisualizerData } = await import("./visualizer-data.js");
|
|
296
|
-
const { generateHtmlReport } = await import("./export-html.js");
|
|
297
|
-
const { writeReportSnapshot } = await import("./reports.js");
|
|
298
|
-
const { basename } = await import("node:path");
|
|
299
|
-
const snapData = await loadVisualizerData(s.basePath);
|
|
300
|
-
const completedMs = snapData.milestones.find((m) => m.id === s.currentMilestoneId);
|
|
301
|
-
const msTitle = completedMs?.title ?? s.currentMilestoneId;
|
|
302
|
-
const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
|
|
303
|
-
const projName = basename(s.basePath);
|
|
304
|
-
const doneSlices = snapData.milestones.reduce((acc, m) => acc +
|
|
305
|
-
m.slices.filter((sl) => sl.done).length, 0);
|
|
306
|
-
const totalSlices = snapData.milestones.reduce((acc, m) => acc + m.slices.length, 0);
|
|
307
|
-
const outPath = writeReportSnapshot({
|
|
308
|
-
basePath: s.basePath,
|
|
309
|
-
html: generateHtmlReport(snapData, {
|
|
310
|
-
projectName: projName,
|
|
311
|
-
projectPath: s.basePath,
|
|
312
|
-
gsdVersion,
|
|
313
|
-
milestoneId: s.currentMilestoneId,
|
|
314
|
-
indexRelPath: "index.html",
|
|
315
|
-
}),
|
|
316
|
-
milestoneId: s.currentMilestoneId,
|
|
317
|
-
milestoneTitle: msTitle,
|
|
318
|
-
kind: "milestone",
|
|
319
|
-
projectName: projName,
|
|
320
|
-
projectPath: s.basePath,
|
|
321
|
-
gsdVersion,
|
|
322
|
-
totalCost: snapData.totals?.cost ?? 0,
|
|
323
|
-
totalTokens: snapData.totals?.tokens.total ?? 0,
|
|
324
|
-
totalDuration: snapData.totals?.duration ?? 0,
|
|
325
|
-
doneSlices,
|
|
326
|
-
totalSlices,
|
|
327
|
-
doneMilestones: snapData.milestones.filter((m) => m.status === "complete").length,
|
|
328
|
-
totalMilestones: snapData.milestones.length,
|
|
329
|
-
phase: snapData.phase,
|
|
330
|
-
});
|
|
331
|
-
ctx.ui.notify(`Report saved: .gsd/reports/${(await import("node:path")).basename(outPath)} — open index.html to browse progression.`, "info");
|
|
332
|
-
}
|
|
333
|
-
catch (err) {
|
|
334
|
-
ctx.ui.notify(`Report generation failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
// Reset dispatch counters for new milestone
|
|
338
|
-
s.unitDispatchCount.clear();
|
|
339
|
-
s.unitRecoveryCount.clear();
|
|
340
|
-
s.unitLifetimeDispatches.clear();
|
|
341
|
-
lastDerivedUnit = "";
|
|
342
|
-
sameUnitCount = 0;
|
|
343
|
-
// Worktree lifecycle on milestone transition — merge current, enter next
|
|
344
|
-
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
345
|
-
deps.invalidateAllCaches();
|
|
346
|
-
state = await deps.deriveState(s.basePath);
|
|
347
|
-
mid = state.activeMilestone?.id;
|
|
348
|
-
midTitle = state.activeMilestone?.title;
|
|
349
|
-
if (mid) {
|
|
350
|
-
if (deps.getIsolationMode() !== "none") {
|
|
351
|
-
deps.captureIntegrationBranch(s.basePath, mid, {
|
|
352
|
-
commitDocs: deps.loadEffectiveGSDPreferences()?.preferences?.git
|
|
353
|
-
?.commit_docs,
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
deps.resolver.enterMilestone(mid, ctx.ui);
|
|
357
|
-
}
|
|
358
|
-
else {
|
|
359
|
-
// mid is undefined — no milestone to capture integration branch for
|
|
360
|
-
}
|
|
361
|
-
const pendingIds = state.registry
|
|
362
|
-
.filter((m) => m.status !== "complete" && m.status !== "parked")
|
|
363
|
-
.map((m) => m.id);
|
|
364
|
-
deps.pruneQueueOrder(s.basePath, pendingIds);
|
|
365
|
-
}
|
|
366
|
-
if (mid) {
|
|
367
|
-
s.currentMilestoneId = mid;
|
|
368
|
-
deps.setActiveMilestoneId(s.basePath, mid);
|
|
369
|
-
}
|
|
370
|
-
// ── Terminal conditions ──────────────────────────────────────────────
|
|
371
|
-
if (!mid) {
|
|
372
|
-
if (s.currentUnit) {
|
|
373
|
-
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
374
|
-
}
|
|
375
|
-
const incomplete = state.registry.filter((m) => m.status !== "complete" && m.status !== "parked");
|
|
376
|
-
if (incomplete.length === 0) {
|
|
377
|
-
// All milestones complete — merge milestone branch before stopping
|
|
378
|
-
if (s.currentMilestoneId) {
|
|
379
|
-
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
380
|
-
}
|
|
381
|
-
deps.sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
|
|
382
|
-
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, "All milestones complete.", "success");
|
|
383
|
-
await deps.stopAuto(ctx, pi, "All milestones complete");
|
|
384
|
-
}
|
|
385
|
-
else if (state.phase === "blocked") {
|
|
386
|
-
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
387
|
-
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
388
|
-
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
389
|
-
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
390
|
-
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
|
|
391
|
-
}
|
|
392
|
-
else {
|
|
393
|
-
const ids = incomplete.map((m) => m.id).join(", ");
|
|
394
|
-
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
395
|
-
ctx.ui.notify(`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`, "error");
|
|
396
|
-
await deps.stopAuto(ctx, pi, `No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`);
|
|
397
|
-
}
|
|
398
|
-
debugLog("autoLoop", { phase: "exit", reason: "no-active-milestone" });
|
|
399
|
-
break;
|
|
400
|
-
}
|
|
401
|
-
if (!midTitle) {
|
|
402
|
-
midTitle = mid;
|
|
403
|
-
ctx.ui.notify(`Milestone ${mid} has no title in roadmap — using ID as fallback.`, "warning");
|
|
404
|
-
}
|
|
405
|
-
// Mid-merge safety check
|
|
406
|
-
if (deps.reconcileMergeState(s.basePath, ctx)) {
|
|
407
|
-
deps.invalidateAllCaches();
|
|
408
|
-
state = await deps.deriveState(s.basePath);
|
|
409
|
-
mid = state.activeMilestone?.id;
|
|
410
|
-
midTitle = state.activeMilestone?.title;
|
|
411
|
-
}
|
|
412
|
-
if (!mid || !midTitle) {
|
|
413
|
-
if (s.currentUnit) {
|
|
414
|
-
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
415
|
-
}
|
|
416
|
-
const noMilestoneReason = !mid
|
|
417
|
-
? "No active milestone after merge reconciliation"
|
|
418
|
-
: `Milestone ${mid} has no title after reconciliation`;
|
|
419
|
-
await deps.stopAuto(ctx, pi, noMilestoneReason);
|
|
420
|
-
debugLog("autoLoop", {
|
|
421
|
-
phase: "exit",
|
|
422
|
-
reason: "no-milestone-after-reconciliation",
|
|
423
|
-
});
|
|
424
|
-
break;
|
|
425
|
-
}
|
|
426
|
-
// Terminal: complete
|
|
427
|
-
if (state.phase === "complete") {
|
|
428
|
-
if (s.currentUnit) {
|
|
429
|
-
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
430
|
-
}
|
|
431
|
-
// Milestone merge on complete
|
|
432
|
-
if (s.currentMilestoneId) {
|
|
433
|
-
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
434
|
-
}
|
|
435
|
-
deps.sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
|
|
436
|
-
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, `Milestone ${mid} complete.`, "success");
|
|
437
|
-
await deps.stopAuto(ctx, pi, `Milestone ${mid} complete`);
|
|
438
|
-
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
439
|
-
break;
|
|
440
|
-
}
|
|
441
|
-
// Terminal: blocked
|
|
442
|
-
if (state.phase === "blocked") {
|
|
443
|
-
if (s.currentUnit) {
|
|
444
|
-
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
445
|
-
}
|
|
446
|
-
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
447
|
-
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
448
|
-
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
449
|
-
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
450
|
-
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
|
|
451
|
-
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
452
|
-
break;
|
|
453
|
-
}
|
|
454
|
-
// ── Phase 2: Guards ─────────────────────────────────────────────────
|
|
455
|
-
const prefs = deps.loadEffectiveGSDPreferences()?.preferences;
|
|
456
|
-
// Budget ceiling guard
|
|
457
|
-
const budgetCeiling = prefs?.budget_ceiling;
|
|
458
|
-
if (budgetCeiling !== undefined && budgetCeiling > 0) {
|
|
459
|
-
const currentLedger = deps.getLedger();
|
|
460
|
-
const totalCost = currentLedger
|
|
461
|
-
? deps.getProjectTotals(currentLedger.units).cost
|
|
462
|
-
: 0;
|
|
463
|
-
const budgetPct = totalCost / budgetCeiling;
|
|
464
|
-
const budgetAlertLevel = deps.getBudgetAlertLevel(budgetPct);
|
|
465
|
-
const newBudgetAlertLevel = deps.getNewBudgetAlertLevel(s.lastBudgetAlertLevel, budgetPct);
|
|
466
|
-
const enforcement = prefs?.budget_enforcement ?? "pause";
|
|
467
|
-
const budgetEnforcementAction = deps.getBudgetEnforcementAction(enforcement, budgetPct);
|
|
468
|
-
if (newBudgetAlertLevel === 100 && budgetEnforcementAction !== "none") {
|
|
469
|
-
const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
|
|
470
|
-
s.lastBudgetAlertLevel =
|
|
471
|
-
newBudgetAlertLevel;
|
|
472
|
-
if (budgetEnforcementAction === "halt") {
|
|
473
|
-
deps.sendDesktopNotification("GSD", msg, "error", "budget");
|
|
474
|
-
await deps.stopAuto(ctx, pi, "Budget ceiling reached");
|
|
475
|
-
debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
|
|
476
|
-
break;
|
|
477
|
-
}
|
|
478
|
-
if (budgetEnforcementAction === "pause") {
|
|
479
|
-
ctx.ui.notify(`${msg} Pausing auto-mode — /gsd auto to override and continue.`, "warning");
|
|
480
|
-
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
481
|
-
deps.logCmuxEvent(prefs, msg, "warning");
|
|
482
|
-
await deps.pauseAuto(ctx, pi);
|
|
483
|
-
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
484
|
-
break;
|
|
485
|
-
}
|
|
486
|
-
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
487
|
-
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
488
|
-
deps.logCmuxEvent(prefs, msg, "warning");
|
|
489
|
-
}
|
|
490
|
-
else if (newBudgetAlertLevel === 90) {
|
|
491
|
-
s.lastBudgetAlertLevel =
|
|
492
|
-
newBudgetAlertLevel;
|
|
493
|
-
ctx.ui.notify(`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
|
|
494
|
-
deps.sendDesktopNotification("GSD", `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning", "budget");
|
|
495
|
-
deps.logCmuxEvent(prefs, `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
|
|
496
|
-
}
|
|
497
|
-
else if (newBudgetAlertLevel === 80) {
|
|
498
|
-
s.lastBudgetAlertLevel =
|
|
499
|
-
newBudgetAlertLevel;
|
|
500
|
-
ctx.ui.notify(`Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
|
|
501
|
-
deps.sendDesktopNotification("GSD", `Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning", "budget");
|
|
502
|
-
deps.logCmuxEvent(prefs, `Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
|
|
503
|
-
}
|
|
504
|
-
else if (newBudgetAlertLevel === 75) {
|
|
505
|
-
s.lastBudgetAlertLevel =
|
|
506
|
-
newBudgetAlertLevel;
|
|
507
|
-
ctx.ui.notify(`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "info");
|
|
508
|
-
deps.sendDesktopNotification("GSD", `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "info", "budget");
|
|
509
|
-
deps.logCmuxEvent(prefs, `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "progress");
|
|
510
|
-
}
|
|
511
|
-
else if (budgetAlertLevel === 0) {
|
|
512
|
-
s.lastBudgetAlertLevel = 0;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
else {
|
|
516
|
-
s.lastBudgetAlertLevel = 0;
|
|
517
|
-
}
|
|
518
|
-
// Context window guard
|
|
519
|
-
const contextThreshold = prefs?.context_pause_threshold ?? 0;
|
|
520
|
-
if (contextThreshold > 0 && s.cmdCtx) {
|
|
521
|
-
const contextUsage = s.cmdCtx.getContextUsage();
|
|
522
|
-
if (contextUsage &&
|
|
523
|
-
contextUsage.percent !== null &&
|
|
524
|
-
contextUsage.percent >= contextThreshold) {
|
|
525
|
-
const msg = `Context window at ${contextUsage.percent}% (threshold: ${contextThreshold}%). Pausing to prevent truncated output.`;
|
|
526
|
-
ctx.ui.notify(`${msg} Run /gsd auto to continue (will start fresh session).`, "warning");
|
|
527
|
-
deps.sendDesktopNotification("GSD", `Context ${contextUsage.percent}% — paused`, "warning", "attention");
|
|
528
|
-
await deps.pauseAuto(ctx, pi);
|
|
529
|
-
debugLog("autoLoop", { phase: "exit", reason: "context-window" });
|
|
1059
|
+
if (preDispatchResult.action === "continue")
|
|
1060
|
+
continue;
|
|
1061
|
+
const preData = preDispatchResult.data;
|
|
1062
|
+
// ── Phase 2: Guards ───────────────────────────────────────────────
|
|
1063
|
+
const guardsResult = await runGuards(ic, preData.mid);
|
|
1064
|
+
if (guardsResult.action === "break")
|
|
530
1065
|
break;
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
try {
|
|
535
|
-
const manifestStatus = await deps.getManifestStatus(s.basePath, mid, s.originalBasePath);
|
|
536
|
-
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
537
|
-
const result = await deps.collectSecretsFromManifest(s.basePath, mid, ctx);
|
|
538
|
-
if (result &&
|
|
539
|
-
result.applied &&
|
|
540
|
-
result.skipped &&
|
|
541
|
-
result.existingSkipped) {
|
|
542
|
-
ctx.ui.notify(`Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`, "info");
|
|
543
|
-
}
|
|
544
|
-
else {
|
|
545
|
-
ctx.ui.notify("Secrets collection skipped.", "info");
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
catch (err) {
|
|
550
|
-
ctx.ui.notify(`Secrets collection error: ${err instanceof Error ? err.message : String(err)}. Continuing with next task.`, "warning");
|
|
551
|
-
}
|
|
552
|
-
// ── Phase 3: Dispatch resolution ────────────────────────────────────
|
|
553
|
-
debugLog("autoLoop", { phase: "dispatch-resolve", iteration });
|
|
554
|
-
const dispatchResult = await deps.resolveDispatch({
|
|
555
|
-
basePath: s.basePath,
|
|
556
|
-
mid,
|
|
557
|
-
midTitle: midTitle,
|
|
558
|
-
state,
|
|
559
|
-
prefs,
|
|
560
|
-
});
|
|
561
|
-
if (dispatchResult.action === "stop") {
|
|
562
|
-
if (s.currentUnit) {
|
|
563
|
-
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
564
|
-
}
|
|
565
|
-
await deps.stopAuto(ctx, pi, dispatchResult.reason);
|
|
566
|
-
debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
|
|
567
|
-
break;
|
|
568
|
-
}
|
|
569
|
-
if (dispatchResult.action !== "dispatch") {
|
|
570
|
-
// Non-dispatch action (e.g. "skip") — re-derive state
|
|
571
|
-
await new Promise((r) => setImmediate(r));
|
|
572
|
-
continue;
|
|
573
|
-
}
|
|
574
|
-
let unitType = dispatchResult.unitType;
|
|
575
|
-
let unitId = dispatchResult.unitId;
|
|
576
|
-
let prompt = dispatchResult.prompt;
|
|
577
|
-
const pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
578
|
-
// ── Same-unit stuck counter with graduated recovery ──
|
|
579
|
-
const derivedKey = `${unitType}/${unitId}`;
|
|
580
|
-
if (derivedKey === lastDerivedUnit && !s.pendingVerificationRetry) {
|
|
581
|
-
sameUnitCount++;
|
|
582
|
-
debugLog("autoLoop", {
|
|
583
|
-
phase: "stuck-check",
|
|
584
|
-
unitType,
|
|
585
|
-
unitId,
|
|
586
|
-
sameUnitCount,
|
|
587
|
-
});
|
|
588
|
-
if (sameUnitCount === 3) {
|
|
589
|
-
// Level 1: try verifying the artifact — maybe it was written but not detected
|
|
590
|
-
const artifactExists = deps.verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
591
|
-
if (artifactExists) {
|
|
592
|
-
debugLog("autoLoop", {
|
|
593
|
-
phase: "stuck-recovery",
|
|
594
|
-
level: 1,
|
|
595
|
-
action: "artifact-found",
|
|
596
|
-
});
|
|
597
|
-
ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`, "info");
|
|
598
|
-
deps.invalidateAllCaches();
|
|
599
|
-
continue;
|
|
600
|
-
}
|
|
601
|
-
ctx.ui.notify(`Stuck on ${unitType} ${unitId} (attempt ${sameUnitCount}). Invalidating caches and retrying.`, "warning");
|
|
602
|
-
deps.invalidateAllCaches();
|
|
603
|
-
}
|
|
604
|
-
else if (sameUnitCount === 5) {
|
|
605
|
-
// Level 2: hard stop — genuinely stuck
|
|
606
|
-
debugLog("autoLoop", {
|
|
607
|
-
phase: "stuck-detected",
|
|
608
|
-
unitType,
|
|
609
|
-
unitId,
|
|
610
|
-
sameUnitCount,
|
|
611
|
-
});
|
|
612
|
-
await deps.stopAuto(ctx, pi, `Stuck: ${unitType} ${unitId} derived ${sameUnitCount} consecutive times without progress`);
|
|
613
|
-
ctx.ui.notify(`Stuck on ${unitType} ${unitId} — deriveState returns the same unit after ${sameUnitCount} attempts. The expected artifact was not written.`, "error");
|
|
1066
|
+
// ── Phase 3: Dispatch ─────────────────────────────────────────────
|
|
1067
|
+
const dispatchResult = await runDispatch(ic, preData, loopState);
|
|
1068
|
+
if (dispatchResult.action === "break")
|
|
614
1069
|
break;
|
|
615
|
-
|
|
1070
|
+
if (dispatchResult.action === "continue")
|
|
1071
|
+
continue;
|
|
1072
|
+
iterData = dispatchResult.data;
|
|
616
1073
|
}
|
|
617
1074
|
else {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
}
|
|
633
|
-
if (preDispatchResult.action === "skip") {
|
|
634
|
-
ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
|
|
635
|
-
await new Promise((r) => setImmediate(r));
|
|
636
|
-
continue;
|
|
637
|
-
}
|
|
638
|
-
if (preDispatchResult.action === "replace") {
|
|
639
|
-
prompt = preDispatchResult.prompt ?? prompt;
|
|
640
|
-
if (preDispatchResult.unitType)
|
|
641
|
-
unitType = preDispatchResult.unitType;
|
|
642
|
-
}
|
|
643
|
-
else if (preDispatchResult.prompt) {
|
|
644
|
-
prompt = preDispatchResult.prompt;
|
|
645
|
-
}
|
|
646
|
-
const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(s.basePath, deps.getMainBranch(s.basePath), unitType, unitId);
|
|
647
|
-
if (priorSliceBlocker) {
|
|
648
|
-
await deps.stopAuto(ctx, pi, priorSliceBlocker);
|
|
649
|
-
debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
|
|
650
|
-
break;
|
|
651
|
-
}
|
|
652
|
-
const observabilityIssues = await deps.collectObservabilityWarnings(ctx, s.basePath, unitType, unitId);
|
|
653
|
-
// ── Phase 4: Unit execution ─────────────────────────────────────────
|
|
654
|
-
debugLog("autoLoop", {
|
|
655
|
-
phase: "unit-execution",
|
|
656
|
-
iteration,
|
|
657
|
-
unitType,
|
|
658
|
-
unitId,
|
|
659
|
-
});
|
|
660
|
-
// Detect retry and capture previous tier for escalation
|
|
661
|
-
const isRetry = !!(s.currentUnit &&
|
|
662
|
-
s.currentUnit.type === unitType &&
|
|
663
|
-
s.currentUnit.id === unitId);
|
|
664
|
-
const previousTier = s.currentUnitRouting?.tier;
|
|
665
|
-
// Closeout previous unit
|
|
666
|
-
if (s.currentUnit) {
|
|
667
|
-
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
668
|
-
if (s.currentUnitRouting) {
|
|
669
|
-
const isRetry = s.currentUnit.type === unitType && s.currentUnit.id === unitId;
|
|
670
|
-
deps.recordOutcome(s.currentUnit.type, s.currentUnitRouting.tier, !isRetry);
|
|
671
|
-
}
|
|
672
|
-
const closeoutKey = `${s.currentUnit.type}/${s.currentUnit.id}`;
|
|
673
|
-
const incomingKey = `${unitType}/${unitId}`;
|
|
674
|
-
const isHookUnit = s.currentUnit.type.startsWith("hook/");
|
|
675
|
-
const artifactVerified = isHookUnit ||
|
|
676
|
-
deps.verifyExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.basePath);
|
|
677
|
-
if (closeoutKey !== incomingKey && artifactVerified) {
|
|
678
|
-
s.completedUnits.push({
|
|
679
|
-
type: s.currentUnit.type,
|
|
680
|
-
id: s.currentUnit.id,
|
|
681
|
-
startedAt: s.currentUnit.startedAt,
|
|
682
|
-
finishedAt: Date.now(),
|
|
683
|
-
});
|
|
684
|
-
if (s.completedUnits.length > 200) {
|
|
685
|
-
s.completedUnits = s.completedUnits.slice(-200);
|
|
686
|
-
}
|
|
687
|
-
deps.clearUnitRuntimeRecord(s.basePath, s.currentUnit.type, s.currentUnit.id);
|
|
688
|
-
s.unitDispatchCount.delete(`${s.currentUnit.type}/${s.currentUnit.id}`);
|
|
689
|
-
s.unitRecoveryCount.delete(`${s.currentUnit.type}/${s.currentUnit.id}`);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
693
|
-
deps.captureAvailableSkills();
|
|
694
|
-
deps.writeUnitRuntimeRecord(s.basePath, unitType, unitId, s.currentUnit.startedAt, {
|
|
695
|
-
phase: "dispatched",
|
|
696
|
-
wrapupWarningSent: false,
|
|
697
|
-
timeoutAt: null,
|
|
698
|
-
lastProgressAt: s.currentUnit.startedAt,
|
|
699
|
-
progressCount: 0,
|
|
700
|
-
lastProgressKind: "dispatch",
|
|
701
|
-
});
|
|
702
|
-
// Status bar + progress widget
|
|
703
|
-
ctx.ui.setStatus("gsd-auto", "auto");
|
|
704
|
-
if (mid)
|
|
705
|
-
deps.updateSliceProgressCache(s.basePath, mid, state.activeSlice?.id);
|
|
706
|
-
deps.updateProgressWidget(ctx, unitType, unitId, state);
|
|
707
|
-
deps.ensurePreconditions(unitType, unitId, s.basePath, state);
|
|
708
|
-
// Prompt injection
|
|
709
|
-
const MAX_RECOVERY_CHARS = 50_000;
|
|
710
|
-
let finalPrompt = prompt;
|
|
711
|
-
if (s.pendingVerificationRetry) {
|
|
712
|
-
const retryCtx = s.pendingVerificationRetry;
|
|
713
|
-
s.pendingVerificationRetry = null;
|
|
714
|
-
const capped = retryCtx.failureContext.length > MAX_RECOVERY_CHARS
|
|
715
|
-
? retryCtx.failureContext.slice(0, MAX_RECOVERY_CHARS) +
|
|
716
|
-
"\n\n[...failure context truncated]"
|
|
717
|
-
: retryCtx.failureContext;
|
|
718
|
-
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}`;
|
|
719
|
-
}
|
|
720
|
-
if (s.pendingCrashRecovery) {
|
|
721
|
-
const capped = s.pendingCrashRecovery.length > MAX_RECOVERY_CHARS
|
|
722
|
-
? s.pendingCrashRecovery.slice(0, MAX_RECOVERY_CHARS) +
|
|
723
|
-
"\n\n[...recovery briefing truncated to prevent memory exhaustion]"
|
|
724
|
-
: s.pendingCrashRecovery;
|
|
725
|
-
finalPrompt = `${capped}\n\n---\n\n${finalPrompt}`;
|
|
726
|
-
s.pendingCrashRecovery = null;
|
|
727
|
-
}
|
|
728
|
-
else if ((s.unitDispatchCount.get(`${unitType}/${unitId}`) ?? 0) > 1) {
|
|
729
|
-
const diagnostic = deps.getDeepDiagnostic(s.basePath);
|
|
730
|
-
if (diagnostic) {
|
|
731
|
-
const cappedDiag = diagnostic.length > MAX_RECOVERY_CHARS
|
|
732
|
-
? diagnostic.slice(0, MAX_RECOVERY_CHARS) +
|
|
733
|
-
"\n\n[...diagnostic truncated to prevent memory exhaustion]"
|
|
734
|
-
: diagnostic;
|
|
735
|
-
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}`;
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
const repairBlock = deps.buildObservabilityRepairBlock(observabilityIssues);
|
|
739
|
-
if (repairBlock) {
|
|
740
|
-
finalPrompt = `${finalPrompt}${repairBlock}`;
|
|
741
|
-
}
|
|
742
|
-
// Prompt char measurement
|
|
743
|
-
s.lastPromptCharCount = finalPrompt.length;
|
|
744
|
-
s.lastBaselineCharCount = undefined;
|
|
745
|
-
if (deps.isDbAvailable()) {
|
|
746
|
-
try {
|
|
747
|
-
const { inlineGsdRootFile } = await import("./auto-prompts.js");
|
|
748
|
-
const [decisionsContent, requirementsContent, projectContent] = await Promise.all([
|
|
749
|
-
inlineGsdRootFile(s.basePath, "decisions.md", "Decisions"),
|
|
750
|
-
inlineGsdRootFile(s.basePath, "requirements.md", "Requirements"),
|
|
751
|
-
inlineGsdRootFile(s.basePath, "project.md", "Project"),
|
|
752
|
-
]);
|
|
753
|
-
s.lastBaselineCharCount =
|
|
754
|
-
(decisionsContent?.length ?? 0) +
|
|
755
|
-
(requirementsContent?.length ?? 0) +
|
|
756
|
-
(projectContent?.length ?? 0);
|
|
757
|
-
}
|
|
758
|
-
catch {
|
|
759
|
-
// Non-fatal
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
// Cache-optimize prompt section ordering
|
|
763
|
-
try {
|
|
764
|
-
finalPrompt = deps.reorderForCaching(finalPrompt);
|
|
765
|
-
}
|
|
766
|
-
catch (reorderErr) {
|
|
767
|
-
const msg = reorderErr instanceof Error ? reorderErr.message : String(reorderErr);
|
|
768
|
-
process.stderr.write(`[gsd] prompt reorder failed (non-fatal): ${msg}\n`);
|
|
1075
|
+
// ── Sidecar path: use values from the sidecar item directly ──
|
|
1076
|
+
const sidecarState = await deps.deriveState(s.basePath);
|
|
1077
|
+
iterData = {
|
|
1078
|
+
unitType: sidecarItem.unitType,
|
|
1079
|
+
unitId: sidecarItem.unitId,
|
|
1080
|
+
prompt: sidecarItem.prompt,
|
|
1081
|
+
finalPrompt: sidecarItem.prompt,
|
|
1082
|
+
pauseAfterUatDispatch: false,
|
|
1083
|
+
observabilityIssues: [],
|
|
1084
|
+
state: sidecarState,
|
|
1085
|
+
mid: sidecarState.activeMilestone?.id,
|
|
1086
|
+
midTitle: sidecarState.activeMilestone?.title,
|
|
1087
|
+
isRetry: false, previousTier: undefined,
|
|
1088
|
+
};
|
|
769
1089
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
s.currentUnitRouting =
|
|
773
|
-
modelResult.routing;
|
|
774
|
-
// Start unit supervision
|
|
775
|
-
deps.clearUnitTimeout();
|
|
776
|
-
deps.startUnitSupervision({
|
|
777
|
-
s,
|
|
778
|
-
ctx,
|
|
779
|
-
pi,
|
|
780
|
-
unitType,
|
|
781
|
-
unitId,
|
|
782
|
-
prefs,
|
|
783
|
-
buildSnapshotOpts: () => deps.buildSnapshotOpts(unitType, unitId),
|
|
784
|
-
buildRecoveryContext: () => ({}),
|
|
785
|
-
pauseAuto: deps.pauseAuto,
|
|
786
|
-
});
|
|
787
|
-
// Session + send + await
|
|
788
|
-
const sessionFile = deps.getSessionFile(ctx);
|
|
789
|
-
deps.updateSessionLock(deps.lockBase(), unitType, unitId, s.completedUnits.length, sessionFile);
|
|
790
|
-
deps.writeLock(deps.lockBase(), unitType, unitId, s.completedUnits.length, sessionFile);
|
|
791
|
-
debugLog("autoLoop", {
|
|
792
|
-
phase: "runUnit-start",
|
|
793
|
-
iteration,
|
|
794
|
-
unitType,
|
|
795
|
-
unitId,
|
|
796
|
-
});
|
|
797
|
-
const unitResult = await runUnit(ctx, pi, s, unitType, unitId, finalPrompt, prefs);
|
|
798
|
-
debugLog("autoLoop", {
|
|
799
|
-
phase: "runUnit-end",
|
|
800
|
-
iteration,
|
|
801
|
-
unitType,
|
|
802
|
-
unitId,
|
|
803
|
-
status: unitResult.status,
|
|
804
|
-
});
|
|
805
|
-
if (unitResult.status === "cancelled") {
|
|
806
|
-
ctx.ui.notify(`Session creation timed out or was cancelled for ${unitType} ${unitId}. Will retry.`, "warning");
|
|
807
|
-
await deps.stopAuto(ctx, pi, "Session creation failed");
|
|
808
|
-
debugLog("autoLoop", { phase: "exit", reason: "session-failed" });
|
|
1090
|
+
const unitPhaseResult = await runUnitPhase(ic, iterData, loopState, sidecarItem);
|
|
1091
|
+
if (unitPhaseResult.action === "break")
|
|
809
1092
|
break;
|
|
810
|
-
}
|
|
811
1093
|
// ── Phase 5: Finalize ───────────────────────────────────────────────
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
deps.clearUnitTimeout();
|
|
815
|
-
// Post-unit context for pre/post verification
|
|
816
|
-
const postUnitCtx = {
|
|
817
|
-
s,
|
|
818
|
-
ctx,
|
|
819
|
-
pi,
|
|
820
|
-
buildSnapshotOpts: deps.buildSnapshotOpts,
|
|
821
|
-
lockBase: deps.lockBase,
|
|
822
|
-
stopAuto: deps.stopAuto,
|
|
823
|
-
pauseAuto: deps.pauseAuto,
|
|
824
|
-
updateProgressWidget: deps.updateProgressWidget,
|
|
825
|
-
};
|
|
826
|
-
// Pre-verification processing (commit, doctor, state rebuild, etc.)
|
|
827
|
-
const preResult = await deps.postUnitPreVerification(postUnitCtx);
|
|
828
|
-
if (preResult === "dispatched") {
|
|
829
|
-
debugLog("autoLoop", {
|
|
830
|
-
phase: "exit",
|
|
831
|
-
reason: "pre-verification-dispatched",
|
|
832
|
-
});
|
|
833
|
-
break;
|
|
834
|
-
}
|
|
835
|
-
if (pauseAfterUatDispatch) {
|
|
836
|
-
ctx.ui.notify("UAT requires human execution. Auto-mode will pause after this unit writes the result file.", "info");
|
|
837
|
-
await deps.pauseAuto(ctx, pi);
|
|
838
|
-
debugLog("autoLoop", { phase: "exit", reason: "uat-pause" });
|
|
1094
|
+
const finalizeResult = await runFinalize(ic, iterData, sidecarItem);
|
|
1095
|
+
if (finalizeResult.action === "break")
|
|
839
1096
|
break;
|
|
840
|
-
|
|
841
|
-
// Verification gate — the loop handles retries via s.pendingVerificationRetry
|
|
842
|
-
const verificationResult = await deps.runPostUnitVerification({ s, ctx, pi }, deps.pauseAuto);
|
|
843
|
-
if (verificationResult === "pause") {
|
|
844
|
-
debugLog("autoLoop", { phase: "exit", reason: "verification-pause" });
|
|
845
|
-
break;
|
|
846
|
-
}
|
|
847
|
-
if (verificationResult === "retry") {
|
|
848
|
-
// s.pendingVerificationRetry was set by runPostUnitVerification.
|
|
849
|
-
// Continue the loop — next iteration will inject the retry context into the prompt.
|
|
850
|
-
debugLog("autoLoop", { phase: "verification-retry", iteration });
|
|
1097
|
+
if (finalizeResult.action === "continue")
|
|
851
1098
|
continue;
|
|
852
|
-
}
|
|
853
|
-
// Post-verification processing (DB dual-write, hooks, triage, quick-tasks)
|
|
854
|
-
const postResult = await deps.postUnitPostVerification(postUnitCtx);
|
|
855
|
-
if (postResult === "stopped") {
|
|
856
|
-
debugLog("autoLoop", {
|
|
857
|
-
phase: "exit",
|
|
858
|
-
reason: "post-verification-stopped",
|
|
859
|
-
});
|
|
860
|
-
break;
|
|
861
|
-
}
|
|
862
|
-
if (postResult === "step-wizard") {
|
|
863
|
-
// Step mode — exit the loop (caller handles wizard)
|
|
864
|
-
debugLog("autoLoop", { phase: "exit", reason: "step-wizard" });
|
|
865
|
-
break;
|
|
866
|
-
}
|
|
867
|
-
// ── Sidecar drain: dispatch enqueued hooks/triage/quick-tasks ──
|
|
868
|
-
let sidecarBroke = false;
|
|
869
|
-
while (s.sidecarQueue.length > 0 && s.active) {
|
|
870
|
-
const item = s.sidecarQueue.shift();
|
|
871
|
-
debugLog("autoLoop", {
|
|
872
|
-
phase: "sidecar-dequeue",
|
|
873
|
-
kind: item.kind,
|
|
874
|
-
unitType: item.unitType,
|
|
875
|
-
unitId: item.unitId,
|
|
876
|
-
});
|
|
877
|
-
// Set up as current unit
|
|
878
|
-
const sidecarStartedAt = Date.now();
|
|
879
|
-
s.currentUnit = {
|
|
880
|
-
type: item.unitType,
|
|
881
|
-
id: item.unitId,
|
|
882
|
-
startedAt: sidecarStartedAt,
|
|
883
|
-
};
|
|
884
|
-
deps.writeUnitRuntimeRecord(s.basePath, item.unitType, item.unitId, sidecarStartedAt, {
|
|
885
|
-
phase: "dispatched",
|
|
886
|
-
wrapupWarningSent: false,
|
|
887
|
-
timeoutAt: null,
|
|
888
|
-
lastProgressAt: sidecarStartedAt,
|
|
889
|
-
progressCount: 0,
|
|
890
|
-
lastProgressKind: "dispatch",
|
|
891
|
-
});
|
|
892
|
-
// Model selection (handles hook model override)
|
|
893
|
-
await deps.selectAndApplyModel(ctx, pi, item.unitType, item.unitId, s.basePath, prefs, s.verbose, s.autoModeStartModel);
|
|
894
|
-
// Supervision
|
|
895
|
-
deps.clearUnitTimeout();
|
|
896
|
-
deps.startUnitSupervision({
|
|
897
|
-
s,
|
|
898
|
-
ctx,
|
|
899
|
-
pi,
|
|
900
|
-
unitType: item.unitType,
|
|
901
|
-
unitId: item.unitId,
|
|
902
|
-
prefs,
|
|
903
|
-
buildSnapshotOpts: () => deps.buildSnapshotOpts(item.unitType, item.unitId),
|
|
904
|
-
buildRecoveryContext: () => ({}),
|
|
905
|
-
pauseAuto: deps.pauseAuto,
|
|
906
|
-
});
|
|
907
|
-
// Write lock
|
|
908
|
-
const sidecarSessionFile = deps.getSessionFile(ctx);
|
|
909
|
-
deps.writeLock(deps.lockBase(), item.unitType, item.unitId, s.completedUnits.length, sidecarSessionFile);
|
|
910
|
-
// Execute via standard runUnit
|
|
911
|
-
const sidecarResult = await runUnit(ctx, pi, s, item.unitType, item.unitId, item.prompt, prefs);
|
|
912
|
-
deps.clearUnitTimeout();
|
|
913
|
-
if (sidecarResult.status === "cancelled") {
|
|
914
|
-
ctx.ui.notify(`Sidecar unit ${item.unitType} ${item.unitId} session cancelled. Stopping.`, "warning");
|
|
915
|
-
await deps.stopAuto(ctx, pi, "Sidecar session creation failed");
|
|
916
|
-
sidecarBroke = true;
|
|
917
|
-
break;
|
|
918
|
-
}
|
|
919
|
-
// Run pre-verification for the sidecar unit
|
|
920
|
-
const sidecarPreResult = await deps.postUnitPreVerification(postUnitCtx);
|
|
921
|
-
if (sidecarPreResult === "dispatched") {
|
|
922
|
-
// Pre-verification caused stop/pause
|
|
923
|
-
debugLog("autoLoop", {
|
|
924
|
-
phase: "exit",
|
|
925
|
-
reason: "sidecar-pre-verification-stop",
|
|
926
|
-
});
|
|
927
|
-
sidecarBroke = true;
|
|
928
|
-
break;
|
|
929
|
-
}
|
|
930
|
-
// Verification gate for non-hook sidecar units (triage, quick-tasks)
|
|
931
|
-
// Hook units are lightweight and don't need verification.
|
|
932
|
-
if (item.kind !== "hook") {
|
|
933
|
-
const sidecarVerification = await deps.runPostUnitVerification({ s, ctx, pi }, deps.pauseAuto);
|
|
934
|
-
if (sidecarVerification === "pause") {
|
|
935
|
-
debugLog("autoLoop", {
|
|
936
|
-
phase: "exit",
|
|
937
|
-
reason: "sidecar-verification-pause",
|
|
938
|
-
});
|
|
939
|
-
sidecarBroke = true;
|
|
940
|
-
break;
|
|
941
|
-
}
|
|
942
|
-
// "retry" for sidecars — skip retry, just continue (sidecar retries are not worth the complexity)
|
|
943
|
-
}
|
|
944
|
-
// Post-verification (may enqueue more sidecar items)
|
|
945
|
-
const sidecarPostResult = await deps.postUnitPostVerification(postUnitCtx);
|
|
946
|
-
if (sidecarPostResult === "stopped") {
|
|
947
|
-
debugLog("autoLoop", { phase: "exit", reason: "sidecar-stopped" });
|
|
948
|
-
sidecarBroke = true;
|
|
949
|
-
break;
|
|
950
|
-
}
|
|
951
|
-
if (sidecarPostResult === "step-wizard") {
|
|
952
|
-
debugLog("autoLoop", {
|
|
953
|
-
phase: "exit",
|
|
954
|
-
reason: "sidecar-step-wizard",
|
|
955
|
-
});
|
|
956
|
-
sidecarBroke = true;
|
|
957
|
-
break;
|
|
958
|
-
}
|
|
959
|
-
// "continue" — loop checks sidecarQueue again
|
|
960
|
-
}
|
|
961
|
-
if (sidecarBroke)
|
|
962
|
-
break;
|
|
963
1099
|
consecutiveErrors = 0; // Iteration completed successfully
|
|
964
1100
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|
|
965
1101
|
}
|
|
@@ -990,6 +1126,6 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
990
1126
|
}
|
|
991
1127
|
}
|
|
992
1128
|
}
|
|
993
|
-
|
|
1129
|
+
_currentResolve = null;
|
|
994
1130
|
debugLog("autoLoop", { phase: "exit", totalIterations: iteration });
|
|
995
1131
|
}
|