gsd-pi 2.26.1-next.1 → 2.28.0-dev.346ee62
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 +76 -12
- package/dist/cli.js +24 -23
- package/dist/extension-discovery.d.ts +16 -0
- package/dist/extension-discovery.js +66 -0
- package/dist/headless-answers.d.ts +51 -0
- package/dist/headless-answers.js +224 -0
- package/dist/headless-context.d.ts +18 -0
- package/dist/headless-context.js +44 -0
- package/dist/headless-events.d.ts +28 -0
- package/dist/headless-events.js +59 -0
- package/dist/headless-query.d.ts +40 -0
- package/dist/headless-query.js +77 -0
- package/dist/headless-ui.d.ts +23 -0
- package/dist/headless-ui.js +103 -0
- package/dist/headless.d.ts +4 -0
- package/dist/headless.js +128 -155
- package/dist/help-text.js +9 -0
- package/dist/loader.js +18 -59
- package/dist/onboarding.js +8 -7
- package/dist/remote-questions-config.js +8 -3
- package/dist/resource-loader.d.ts +1 -6
- package/dist/resource-loader.js +52 -95
- package/dist/resources/extensions/ask-user-questions.ts +3 -2
- package/dist/resources/extensions/bg-shell/bg-shell-command.ts +219 -0
- package/dist/resources/extensions/bg-shell/bg-shell-lifecycle.ts +400 -0
- package/dist/resources/extensions/bg-shell/bg-shell-tool.ts +985 -0
- package/dist/resources/extensions/bg-shell/index.ts +18 -1545
- package/dist/resources/extensions/bg-shell/overlay.ts +4 -0
- package/dist/resources/extensions/bg-shell/process-manager.ts +45 -0
- package/dist/resources/extensions/bg-shell/types.ts +21 -1
- package/dist/resources/extensions/bg-shell/utilities.ts +4 -16
- package/dist/resources/extensions/browser-tools/capture.ts +34 -2
- package/dist/resources/extensions/browser-tools/lifecycle.ts +5 -5
- package/dist/resources/extensions/browser-tools/settle.ts +1 -1
- package/dist/resources/extensions/browser-tools/state.ts +5 -5
- package/dist/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +1 -1
- package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +3 -3
- package/dist/resources/extensions/browser-tools/tools/assertions.ts +1 -1
- package/dist/resources/extensions/browser-tools/tools/device.ts +1 -1
- package/dist/resources/extensions/browser-tools/tools/extract.ts +1 -1
- package/dist/resources/extensions/browser-tools/tools/navigation.ts +6 -6
- package/dist/resources/extensions/browser-tools/tools/network-mock.ts +1 -1
- package/dist/resources/extensions/browser-tools/tools/pages.ts +1 -1
- package/dist/resources/extensions/browser-tools/tools/screenshot.ts +28 -10
- package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +1 -1
- package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +1 -1
- package/dist/resources/extensions/browser-tools/utils.ts +5 -5
- package/dist/resources/extensions/get-secrets-from-user.ts +1 -1
- package/dist/resources/extensions/google-search/index.ts +21 -8
- package/dist/resources/extensions/gsd/activity-log.ts +2 -1
- package/dist/resources/extensions/gsd/atomic-write.ts +35 -0
- package/dist/resources/extensions/gsd/auto/session.ts +236 -0
- package/dist/resources/extensions/gsd/auto-budget.ts +32 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +147 -13
- package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +23 -10
- package/dist/resources/extensions/gsd/auto-idempotency.ts +150 -0
- package/dist/resources/extensions/gsd/auto-model-selection.ts +179 -0
- package/dist/resources/extensions/gsd/auto-observability.ts +74 -0
- package/dist/resources/extensions/gsd/auto-post-unit.ts +591 -0
- package/dist/resources/extensions/gsd/auto-prompts.ts +116 -23
- package/dist/resources/extensions/gsd/auto-recovery.ts +20 -9
- package/dist/resources/extensions/gsd/auto-start.ts +500 -0
- package/dist/resources/extensions/gsd/auto-stuck-detection.ts +220 -0
- package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
- package/dist/resources/extensions/gsd/auto-timers.ts +223 -0
- package/dist/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
- package/dist/resources/extensions/gsd/auto-unit-closeout.ts +48 -0
- package/dist/resources/extensions/gsd/auto-verification.ts +195 -0
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +184 -0
- package/dist/resources/extensions/gsd/auto-worktree.ts +83 -67
- package/dist/resources/extensions/gsd/auto.ts +825 -3069
- package/dist/resources/extensions/gsd/commands-config.ts +102 -0
- package/dist/resources/extensions/gsd/commands-handlers.ts +402 -0
- package/dist/resources/extensions/gsd/commands-inspect.ts +90 -0
- package/dist/resources/extensions/gsd/commands-logs.ts +537 -0
- package/dist/resources/extensions/gsd/commands-maintenance.ts +206 -0
- package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +747 -0
- package/dist/resources/extensions/gsd/commands.ts +380 -1433
- package/dist/resources/extensions/gsd/constants.ts +21 -0
- package/dist/resources/extensions/gsd/context-budget.ts +25 -2
- package/dist/resources/extensions/gsd/crash-recovery.ts +4 -2
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +58 -74
- package/dist/resources/extensions/gsd/db-writer.ts +21 -2
- package/dist/resources/extensions/gsd/detection.ts +469 -0
- package/dist/resources/extensions/gsd/diff-context.ts +2 -1
- package/dist/resources/extensions/gsd/dispatch-guard.ts +4 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +83 -0
- package/dist/resources/extensions/gsd/doctor-checks.ts +564 -0
- package/dist/resources/extensions/gsd/doctor-format.ts +78 -0
- package/dist/resources/extensions/gsd/doctor-proactive.ts +9 -4
- package/dist/resources/extensions/gsd/doctor-types.ts +72 -0
- package/dist/resources/extensions/gsd/doctor.ts +64 -701
- package/dist/resources/extensions/gsd/errors.ts +0 -2
- package/dist/resources/extensions/gsd/export-html.ts +1357 -0
- package/dist/resources/extensions/gsd/export.ts +141 -4
- package/dist/resources/extensions/gsd/files.ts +8 -126
- package/dist/resources/extensions/gsd/forensics.ts +2 -12
- package/dist/resources/extensions/gsd/git-constants.ts +11 -0
- package/dist/resources/extensions/gsd/git-service.ts +19 -9
- package/dist/resources/extensions/gsd/gitignore.ts +4 -1
- package/dist/resources/extensions/gsd/gsd-db.ts +26 -6
- package/dist/resources/extensions/gsd/guided-flow-queue.ts +451 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +252 -516
- package/dist/resources/extensions/gsd/history.ts +2 -20
- package/dist/resources/extensions/gsd/index.ts +260 -46
- package/dist/resources/extensions/gsd/init-wizard.ts +615 -0
- package/dist/resources/extensions/gsd/jsonl-utils.ts +21 -0
- package/dist/resources/extensions/gsd/key-manager.ts +995 -0
- package/dist/resources/extensions/gsd/metrics.ts +32 -5
- package/dist/resources/extensions/gsd/migrate/command.ts +1 -1
- package/dist/resources/extensions/gsd/migrate/parsers.ts +10 -95
- package/dist/resources/extensions/gsd/milestone-actions.ts +126 -0
- package/dist/resources/extensions/gsd/milestone-ids.ts +95 -0
- package/dist/resources/extensions/gsd/native-git-bridge.ts +35 -12
- package/dist/resources/extensions/gsd/parallel-eligibility.ts +3 -3
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
- package/dist/resources/extensions/gsd/paths.ts +1 -3
- package/dist/resources/extensions/gsd/plugin-importer.ts +3 -2
- package/dist/resources/extensions/gsd/preferences-hooks.ts +10 -0
- package/dist/resources/extensions/gsd/preferences-models.ts +323 -0
- package/dist/resources/extensions/gsd/preferences-skills.ts +169 -0
- package/dist/resources/extensions/gsd/preferences-types.ts +223 -0
- package/dist/resources/extensions/gsd/preferences-validation.ts +597 -0
- package/dist/resources/extensions/gsd/preferences.ts +219 -1286
- package/dist/resources/extensions/gsd/prompt-cache-optimizer.ts +213 -0
- package/dist/resources/extensions/gsd/prompt-compressor.ts +508 -0
- package/dist/resources/extensions/gsd/prompt-loader.ts +4 -2
- package/dist/resources/extensions/gsd/prompt-ordering.ts +200 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -2
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -4
- package/dist/resources/extensions/gsd/prompts/discuss.md +13 -5
- package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -2
- package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +0 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -2
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -2
- package/dist/resources/extensions/gsd/prompts/queue.md +30 -0
- package/dist/resources/extensions/gsd/prompts/quick-task.md +0 -6
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +0 -1
- package/dist/resources/extensions/gsd/prompts/rewrite-docs.md +0 -1
- package/dist/resources/extensions/gsd/prompts/system.md +6 -3
- package/dist/resources/extensions/gsd/provider-error-pause.ts +59 -10
- package/dist/resources/extensions/gsd/queue-order.ts +1 -1
- package/dist/resources/extensions/gsd/queue-reorder-ui.ts +15 -2
- package/dist/resources/extensions/gsd/quick.ts +18 -15
- package/dist/resources/extensions/gsd/reports.ts +504 -0
- package/dist/resources/extensions/gsd/roadmap-slices.ts +1 -1
- package/dist/resources/extensions/gsd/safe-fs.ts +47 -0
- package/dist/resources/extensions/gsd/semantic-chunker.ts +336 -0
- package/dist/resources/extensions/gsd/session-forensics.ts +8 -23
- package/dist/resources/extensions/gsd/session-lock.ts +284 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +242 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +83 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +64 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +176 -0
- package/dist/resources/extensions/gsd/state.ts +84 -2
- package/dist/resources/extensions/gsd/structured-data-formatter.ts +144 -0
- package/dist/resources/extensions/gsd/summary-distiller.ts +258 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +34 -0
- package/dist/resources/extensions/gsd/tests/activity-log.test.ts +213 -0
- package/dist/resources/extensions/gsd/tests/agent-end-retry.test.ts +107 -0
- package/dist/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +197 -0
- package/dist/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
- package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +33 -39
- package/dist/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +108 -2
- package/dist/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +257 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -0
- package/dist/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
- package/dist/resources/extensions/gsd/tests/context-budget.test.ts +69 -0
- package/dist/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
- package/dist/resources/extensions/gsd/tests/detection.test.ts +398 -0
- package/dist/resources/extensions/gsd/tests/discuss-prompt.test.ts +12 -24
- package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +118 -94
- package/dist/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
- package/dist/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +126 -0
- package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +7 -3
- package/dist/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +75 -0
- package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +17 -55
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
- package/dist/resources/extensions/gsd/tests/export-html-all.test.ts +105 -0
- package/dist/resources/extensions/gsd/tests/export-html-enhancements.test.ts +375 -0
- package/dist/resources/extensions/gsd/tests/extension-selector-separator.test.ts +122 -0
- package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +3 -0
- package/dist/resources/extensions/gsd/tests/headless-answers.test.ts +340 -0
- package/dist/resources/extensions/gsd/tests/headless-query.test.ts +162 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +24 -82
- package/dist/resources/extensions/gsd/tests/init-wizard.test.ts +197 -0
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/key-manager.test.ts +414 -0
- package/dist/resources/extensions/gsd/tests/metrics.test.ts +173 -305
- package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +4 -1
- package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +59 -1
- package/dist/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
- package/dist/resources/extensions/gsd/tests/next-milestone-id.test.ts +18 -61
- package/dist/resources/extensions/gsd/tests/none-mode-gates.test.ts +17 -8
- package/dist/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
- package/dist/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
- package/dist/resources/extensions/gsd/tests/parallel-merge.test.ts +468 -0
- package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
- package/dist/resources/extensions/gsd/tests/park-edge-cases.test.ts +276 -0
- package/dist/resources/extensions/gsd/tests/park-milestone.test.ts +401 -0
- package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +47 -0
- package/dist/resources/extensions/gsd/tests/preferences.test.ts +284 -0
- package/dist/resources/extensions/gsd/tests/prompt-cache-optimizer.test.ts +314 -0
- package/dist/resources/extensions/gsd/tests/prompt-compressor.test.ts +529 -0
- package/dist/resources/extensions/gsd/tests/prompt-ordering.test.ts +296 -0
- package/dist/resources/extensions/gsd/tests/provider-errors.test.ts +338 -0
- package/dist/resources/extensions/gsd/tests/reassess-detection.test.ts +154 -0
- package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/remote-status.test.ts +2 -2
- package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
- package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -60
- package/dist/resources/extensions/gsd/tests/semantic-chunker.test.ts +426 -0
- package/dist/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
- package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +3 -0
- package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +8 -5
- package/dist/resources/extensions/gsd/tests/structured-data-formatter.test.ts +365 -0
- package/dist/resources/extensions/gsd/tests/summary-distiller.test.ts +323 -0
- package/dist/resources/extensions/gsd/tests/token-counter.test.ts +129 -0
- package/dist/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +1272 -0
- package/dist/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +164 -0
- package/dist/resources/extensions/gsd/tests/token-profile.test.ts +8 -1
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +75 -79
- package/dist/resources/extensions/gsd/tests/update-command.test.ts +67 -0
- package/dist/resources/extensions/gsd/tests/validate-directory.test.ts +222 -0
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
- package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +115 -1
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +2 -1
- package/dist/resources/extensions/gsd/tests/workspace-index.test.ts +24 -61
- package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +5 -2
- package/dist/resources/extensions/gsd/tests/worktree.test.ts +3 -1
- package/dist/resources/extensions/gsd/tests/write-gate.test.ts +132 -43
- package/dist/resources/extensions/gsd/token-counter.ts +20 -0
- package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
- package/dist/resources/extensions/gsd/types.ts +4 -1
- package/dist/resources/extensions/gsd/validate-directory.ts +164 -0
- package/dist/resources/extensions/gsd/verification-evidence.ts +7 -4
- package/dist/resources/extensions/gsd/verification-gate.ts +70 -5
- package/dist/resources/extensions/gsd/visualizer-data.ts +26 -4
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +35 -22
- package/dist/resources/extensions/gsd/visualizer-views.ts +14 -66
- package/dist/resources/extensions/gsd/worktree-command.ts +4 -51
- package/dist/resources/extensions/gsd/worktree-manager.ts +7 -9
- package/dist/resources/extensions/gsd/worktree.ts +41 -1
- package/dist/resources/extensions/mcporter/index.ts +27 -14
- package/dist/resources/extensions/remote-questions/manager.ts +6 -24
- package/dist/resources/extensions/remote-questions/mod.ts +16 -0
- package/dist/resources/extensions/remote-questions/notify.ts +91 -0
- package/dist/resources/extensions/remote-questions/remote-command.ts +1 -1
- package/dist/resources/extensions/remote-questions/store.ts +5 -1
- package/dist/resources/extensions/remote-questions/types.ts +26 -3
- package/dist/resources/extensions/search-the-web/native-search.ts +7 -0
- package/dist/resources/extensions/search-the-web/provider.ts +15 -3
- package/dist/resources/extensions/search-the-web/tool-llm-context.ts +1 -13
- package/dist/resources/extensions/search-the-web/tool-search.ts +27 -13
- package/dist/resources/extensions/shared/format-utils.ts +138 -0
- package/dist/resources/extensions/shared/frontmatter.ts +117 -0
- package/dist/resources/extensions/shared/mod.ts +30 -0
- package/dist/resources/extensions/shared/sanitize.ts +19 -0
- package/dist/resources/extensions/shared/tests/format-utils.test.ts +153 -0
- package/dist/resources/extensions/slash-commands/create-extension.ts +1 -1
- package/dist/resources/extensions/slash-commands/create-slash-command.ts +1 -1
- package/dist/resources/extensions/subagent/index.ts +47 -3
- package/dist/resources/extensions/subagent/isolation.ts +9 -6
- package/dist/resources/extensions/ttsr/index.ts +5 -0
- package/dist/resources/extensions/ttsr/rule-loader.ts +4 -51
- package/dist/resources/extensions/universal-config/discovery.ts +37 -15
- package/dist/resources/extensions/voice/index.ts +1 -1
- package/dist/resources/skills/accessibility/SKILL.md +522 -0
- package/dist/resources/skills/accessibility/references/WCAG.md +162 -0
- package/dist/resources/skills/agent-browser/SKILL.md +517 -0
- package/dist/resources/skills/agent-browser/references/authentication.md +202 -0
- package/dist/resources/skills/agent-browser/references/commands.md +263 -0
- package/dist/resources/skills/agent-browser/references/profiling.md +120 -0
- package/dist/resources/skills/agent-browser/references/proxy-support.md +194 -0
- package/dist/resources/skills/agent-browser/references/session-management.md +193 -0
- package/dist/resources/skills/agent-browser/references/snapshot-refs.md +194 -0
- package/dist/resources/skills/agent-browser/references/video-recording.md +173 -0
- package/dist/resources/skills/agent-browser/templates/authenticated-session.sh +105 -0
- package/dist/resources/skills/agent-browser/templates/capture-workflow.sh +69 -0
- package/dist/resources/skills/agent-browser/templates/form-automation.sh +62 -0
- package/dist/resources/skills/best-practices/SKILL.md +583 -0
- package/dist/resources/skills/code-optimizer/SKILL.md +160 -0
- package/dist/resources/skills/code-optimizer/references/algorithmic-complexity.md +66 -0
- package/dist/resources/skills/code-optimizer/references/build-compilation.md +90 -0
- package/dist/resources/skills/code-optimizer/references/bundle-dependencies.md +82 -0
- package/dist/resources/skills/code-optimizer/references/caching-memoization.md +76 -0
- package/dist/resources/skills/code-optimizer/references/concurrency-async.md +80 -0
- package/dist/resources/skills/code-optimizer/references/config-infra.md +71 -0
- package/dist/resources/skills/code-optimizer/references/data-structures.md +80 -0
- package/dist/resources/skills/code-optimizer/references/database-queries.md +76 -0
- package/dist/resources/skills/code-optimizer/references/dead-code-redundancy.md +84 -0
- package/dist/resources/skills/code-optimizer/references/error-resilience.md +80 -0
- package/dist/resources/skills/code-optimizer/references/io-network.md +89 -0
- package/dist/resources/skills/code-optimizer/references/logging-observability.md +64 -0
- package/dist/resources/skills/code-optimizer/references/memory-resources.md +66 -0
- package/dist/resources/skills/code-optimizer/references/rendering-ui.md +90 -0
- package/dist/resources/skills/code-optimizer/references/security-performance.md +68 -0
- package/dist/resources/skills/core-web-vitals/SKILL.md +441 -0
- package/dist/resources/skills/core-web-vitals/references/LCP.md +208 -0
- package/dist/resources/skills/make-interfaces-feel-better/SKILL.md +122 -0
- package/dist/resources/skills/make-interfaces-feel-better/animations.md +379 -0
- package/dist/resources/skills/make-interfaces-feel-better/performance.md +88 -0
- package/dist/resources/skills/make-interfaces-feel-better/surfaces.md +247 -0
- package/dist/resources/skills/make-interfaces-feel-better/typography.md +123 -0
- package/dist/resources/skills/react-best-practices/README.md +123 -0
- package/dist/resources/skills/react-best-practices/SKILL.md +136 -0
- package/dist/resources/skills/react-best-practices/metadata.json +15 -0
- package/dist/resources/skills/react-best-practices/rules/_sections.md +46 -0
- package/dist/resources/skills/react-best-practices/rules/_template.md +28 -0
- package/dist/resources/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/dist/resources/skills/react-best-practices/rules/advanced-init-once.md +42 -0
- package/dist/resources/skills/react-best-practices/rules/advanced-use-latest.md +39 -0
- package/dist/resources/skills/react-best-practices/rules/async-api-routes.md +38 -0
- package/dist/resources/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/dist/resources/skills/react-best-practices/rules/async-dependencies.md +51 -0
- package/dist/resources/skills/react-best-practices/rules/async-parallel.md +28 -0
- package/dist/resources/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/dist/resources/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/dist/resources/skills/react-best-practices/rules/bundle-conditional.md +31 -0
- package/dist/resources/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/dist/resources/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/dist/resources/skills/react-best-practices/rules/bundle-preload.md +50 -0
- package/dist/resources/skills/react-best-practices/rules/client-event-listeners.md +74 -0
- package/dist/resources/skills/react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/dist/resources/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/dist/resources/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/dist/resources/skills/react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/dist/resources/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/dist/resources/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/dist/resources/skills/react-best-practices/rules/js-cache-storage.md +70 -0
- package/dist/resources/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/dist/resources/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/dist/resources/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/dist/resources/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/dist/resources/skills/react-best-practices/rules/js-length-check-first.md +49 -0
- package/dist/resources/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/dist/resources/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/dist/resources/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/dist/resources/skills/react-best-practices/rules/rendering-activity.md +26 -0
- package/dist/resources/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/dist/resources/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/dist/resources/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/dist/resources/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/dist/resources/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/dist/resources/skills/react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/dist/resources/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/dist/resources/skills/react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/dist/resources/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/dist/resources/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/dist/resources/skills/react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/dist/resources/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/dist/resources/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/dist/resources/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/dist/resources/skills/react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/dist/resources/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/dist/resources/skills/react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/dist/resources/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/dist/resources/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/dist/resources/skills/react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/dist/resources/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/dist/resources/skills/react-best-practices/rules/server-auth-actions.md +96 -0
- package/dist/resources/skills/react-best-practices/rules/server-cache-lru.md +41 -0
- package/dist/resources/skills/react-best-practices/rules/server-cache-react.md +76 -0
- package/dist/resources/skills/react-best-practices/rules/server-dedup-props.md +65 -0
- package/dist/resources/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/dist/resources/skills/react-best-practices/rules/server-serialization.md +38 -0
- package/dist/resources/skills/userinterface-wiki/SKILL.md +253 -0
- package/dist/resources/skills/userinterface-wiki/rules/_sections.md +66 -0
- package/dist/resources/skills/userinterface-wiki/rules/_template.md +24 -0
- package/dist/resources/skills/userinterface-wiki/rules/a11y-reduced-motion-check.md +30 -0
- package/dist/resources/skills/userinterface-wiki/rules/a11y-toggle-setting.md +30 -0
- package/dist/resources/skills/userinterface-wiki/rules/a11y-visual-equivalent.md +36 -0
- package/dist/resources/skills/userinterface-wiki/rules/a11y-volume-control.md +28 -0
- package/dist/resources/skills/userinterface-wiki/rules/appropriate-confirmations-only.md +19 -0
- package/dist/resources/skills/userinterface-wiki/rules/appropriate-errors-warnings.md +18 -0
- package/dist/resources/skills/userinterface-wiki/rules/appropriate-no-decorative.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/appropriate-no-high-frequency.md +28 -0
- package/dist/resources/skills/userinterface-wiki/rules/appropriate-no-punishing.md +27 -0
- package/dist/resources/skills/userinterface-wiki/rules/container-callback-ref.md +31 -0
- package/dist/resources/skills/userinterface-wiki/rules/container-guard-initial-zero.md +25 -0
- package/dist/resources/skills/userinterface-wiki/rules/container-no-excessive-use.md +13 -0
- package/dist/resources/skills/userinterface-wiki/rules/container-overflow-hidden.md +25 -0
- package/dist/resources/skills/userinterface-wiki/rules/container-transition-delay.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/container-two-div-pattern.md +35 -0
- package/dist/resources/skills/userinterface-wiki/rules/container-use-resize-observer.md +48 -0
- package/dist/resources/skills/userinterface-wiki/rules/context-cleanup-nodes.md +25 -0
- package/dist/resources/skills/userinterface-wiki/rules/context-resume-suspended.md +28 -0
- package/dist/resources/skills/userinterface-wiki/rules/context-reuse-single.md +30 -0
- package/dist/resources/skills/userinterface-wiki/rules/design-filter-for-character.md +25 -0
- package/dist/resources/skills/userinterface-wiki/rules/design-noise-for-percussion.md +26 -0
- package/dist/resources/skills/userinterface-wiki/rules/design-oscillator-for-tonal.md +22 -0
- package/dist/resources/skills/userinterface-wiki/rules/duration-max-300ms.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/duration-press-hover.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/duration-shorten-before-curve.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/duration-small-state.md +15 -0
- package/dist/resources/skills/userinterface-wiki/rules/easing-entrance-ease-out.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/easing-exit-ease-in.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/easing-for-state-change.md +27 -0
- package/dist/resources/skills/userinterface-wiki/rules/easing-linear-only-progress.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/easing-natural-decay.md +22 -0
- package/dist/resources/skills/userinterface-wiki/rules/easing-no-linear-motion.md +22 -0
- package/dist/resources/skills/userinterface-wiki/rules/easing-transition-ease-in-out.md +15 -0
- package/dist/resources/skills/userinterface-wiki/rules/envelope-exponential-decay.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/envelope-no-zero-target.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/envelope-set-initial-value.md +22 -0
- package/dist/resources/skills/userinterface-wiki/rules/exit-key-required.md +29 -0
- package/dist/resources/skills/userinterface-wiki/rules/exit-matches-initial.md +29 -0
- package/dist/resources/skills/userinterface-wiki/rules/exit-prop-required.md +33 -0
- package/dist/resources/skills/userinterface-wiki/rules/exit-requires-wrapper.md +27 -0
- package/dist/resources/skills/userinterface-wiki/rules/impl-default-subtle.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/impl-preload-audio.md +34 -0
- package/dist/resources/skills/userinterface-wiki/rules/impl-reset-current-time.md +26 -0
- package/dist/resources/skills/userinterface-wiki/rules/mode-pop-layout-for-lists.md +25 -0
- package/dist/resources/skills/userinterface-wiki/rules/mode-sync-layout-conflict.md +29 -0
- package/dist/resources/skills/userinterface-wiki/rules/mode-wait-doubles-duration.md +25 -0
- package/dist/resources/skills/userinterface-wiki/rules/morphing-aria-hidden.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/morphing-consistent-viewbox.md +23 -0
- package/dist/resources/skills/userinterface-wiki/rules/morphing-group-variants.md +33 -0
- package/dist/resources/skills/userinterface-wiki/rules/morphing-jump-non-grouped.md +29 -0
- package/dist/resources/skills/userinterface-wiki/rules/morphing-reduced-motion.md +28 -0
- package/dist/resources/skills/userinterface-wiki/rules/morphing-spring-rotation.md +23 -0
- package/dist/resources/skills/userinterface-wiki/rules/morphing-strokelinecap-round.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/morphing-three-lines.md +32 -0
- package/dist/resources/skills/userinterface-wiki/rules/morphing-use-collapsed.md +33 -0
- package/dist/resources/skills/userinterface-wiki/rules/native-backdrop-styling.md +27 -0
- package/dist/resources/skills/userinterface-wiki/rules/native-placeholder-styling.md +27 -0
- package/dist/resources/skills/userinterface-wiki/rules/native-selection-styling.md +18 -0
- package/dist/resources/skills/userinterface-wiki/rules/nested-consistent-timing.md +25 -0
- package/dist/resources/skills/userinterface-wiki/rules/nested-propagate-required.md +41 -0
- package/dist/resources/skills/userinterface-wiki/rules/none-context-menu-entrance.md +25 -0
- package/dist/resources/skills/userinterface-wiki/rules/none-high-frequency.md +29 -0
- package/dist/resources/skills/userinterface-wiki/rules/none-keyboard-navigation.md +32 -0
- package/dist/resources/skills/userinterface-wiki/rules/param-click-duration.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/param-filter-frequency-range.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/param-q-value-range.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/param-reasonable-gain.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/physics-active-state.md +23 -0
- package/dist/resources/skills/userinterface-wiki/rules/physics-no-excessive-stagger.md +22 -0
- package/dist/resources/skills/userinterface-wiki/rules/physics-spring-for-overshoot.md +23 -0
- package/dist/resources/skills/userinterface-wiki/rules/physics-subtle-deformation.md +22 -0
- package/dist/resources/skills/userinterface-wiki/rules/prefetch-hit-slop.md +27 -0
- package/dist/resources/skills/userinterface-wiki/rules/prefetch-keyboard-tab.md +19 -0
- package/dist/resources/skills/userinterface-wiki/rules/prefetch-not-everything.md +22 -0
- package/dist/resources/skills/userinterface-wiki/rules/prefetch-touch-fallback.md +34 -0
- package/dist/resources/skills/userinterface-wiki/rules/prefetch-trajectory-over-hover.md +32 -0
- package/dist/resources/skills/userinterface-wiki/rules/prefetch-use-selectively.md +13 -0
- package/dist/resources/skills/userinterface-wiki/rules/presence-disable-interactions.md +31 -0
- package/dist/resources/skills/userinterface-wiki/rules/presence-hook-in-child.md +31 -0
- package/dist/resources/skills/userinterface-wiki/rules/presence-safe-to-remove.md +37 -0
- package/dist/resources/skills/userinterface-wiki/rules/pseudo-content-required.md +28 -0
- package/dist/resources/skills/userinterface-wiki/rules/pseudo-first-line-styling.md +27 -0
- package/dist/resources/skills/userinterface-wiki/rules/pseudo-hit-target-expansion.md +31 -0
- package/dist/resources/skills/userinterface-wiki/rules/pseudo-marker-styling.md +28 -0
- package/dist/resources/skills/userinterface-wiki/rules/pseudo-over-dom-node.md +32 -0
- package/dist/resources/skills/userinterface-wiki/rules/pseudo-position-relative-parent.md +33 -0
- package/dist/resources/skills/userinterface-wiki/rules/pseudo-z-index-layering.md +37 -0
- package/dist/resources/skills/userinterface-wiki/rules/spring-for-gestures.md +27 -0
- package/dist/resources/skills/userinterface-wiki/rules/spring-for-interruptible.md +27 -0
- package/dist/resources/skills/userinterface-wiki/rules/spring-params-balanced.md +29 -0
- package/dist/resources/skills/userinterface-wiki/rules/spring-preserves-velocity.md +28 -0
- package/dist/resources/skills/userinterface-wiki/rules/staging-dim-background.md +22 -0
- package/dist/resources/skills/userinterface-wiki/rules/staging-one-focal-point.md +24 -0
- package/dist/resources/skills/userinterface-wiki/rules/staging-z-index-hierarchy.md +22 -0
- package/dist/resources/skills/userinterface-wiki/rules/timing-consistent.md +24 -0
- package/dist/resources/skills/userinterface-wiki/rules/timing-no-entrance-context-menu.md +22 -0
- package/dist/resources/skills/userinterface-wiki/rules/timing-under-300ms.md +22 -0
- package/dist/resources/skills/userinterface-wiki/rules/transition-name-cleanup.md +28 -0
- package/dist/resources/skills/userinterface-wiki/rules/transition-name-required.md +27 -0
- package/dist/resources/skills/userinterface-wiki/rules/transition-name-unique.md +24 -0
- package/dist/resources/skills/userinterface-wiki/rules/transition-over-js-library.md +32 -0
- package/dist/resources/skills/userinterface-wiki/rules/transition-style-pseudo-elements.md +24 -0
- package/dist/resources/skills/userinterface-wiki/rules/type-antialiased-on-retina.md +18 -0
- package/dist/resources/skills/userinterface-wiki/rules/type-disambiguation-stylistic-set.md +15 -0
- package/dist/resources/skills/userinterface-wiki/rules/type-font-display-swap.md +28 -0
- package/dist/resources/skills/userinterface-wiki/rules/type-justify-with-hyphens.md +24 -0
- package/dist/resources/skills/userinterface-wiki/rules/type-letter-spacing-uppercase.md +28 -0
- package/dist/resources/skills/userinterface-wiki/rules/type-no-font-synthesis.md +18 -0
- package/dist/resources/skills/userinterface-wiki/rules/type-oldstyle-nums-for-prose.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/type-opentype-contextual-alternates.md +15 -0
- package/dist/resources/skills/userinterface-wiki/rules/type-optical-sizing-auto.md +25 -0
- package/dist/resources/skills/userinterface-wiki/rules/type-proper-fractions.md +15 -0
- package/dist/resources/skills/userinterface-wiki/rules/type-slashed-zero.md +17 -0
- package/dist/resources/skills/userinterface-wiki/rules/type-tabular-nums-for-data.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/type-text-wrap-balance-headings.md +21 -0
- package/dist/resources/skills/userinterface-wiki/rules/type-text-wrap-pretty.md +16 -0
- package/dist/resources/skills/userinterface-wiki/rules/type-underline-offset.md +25 -0
- package/dist/resources/skills/userinterface-wiki/rules/type-variable-weight-continuous.md +23 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-aesthetic-usability.md +32 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-cognitive-load-reduce.md +49 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-common-region-boundaries.md +50 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-doherty-perceived-speed.md +29 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-doherty-under-400ms.md +30 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-fitts-hit-area.md +32 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-fitts-target-size.md +31 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-goal-gradient-progress.md +33 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-hicks-minimize-choices.md +45 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-jakobs-familiar-patterns.md +37 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-millers-chunking.md +23 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-pareto-prioritize-features.md +36 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-peak-end-finish-strong.md +35 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-postels-accept-messy-input.md +45 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-pragnanz-simplify.md +33 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-progressive-disclosure.md +41 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-proximity-grouping.md +38 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-serial-position.md +31 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-similarity-consistency.md +35 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-teslers-complexity.md +28 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-uniform-connectedness.md +43 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-von-restorff-emphasis.md +29 -0
- package/dist/resources/skills/userinterface-wiki/rules/ux-zeigarnik-show-incomplete.md +36 -0
- package/dist/resources/skills/userinterface-wiki/rules/visual-animate-shadow-pseudo.md +49 -0
- package/dist/resources/skills/userinterface-wiki/rules/visual-border-alpha-colors.md +25 -0
- package/dist/resources/skills/userinterface-wiki/rules/visual-button-shadow-anatomy.md +49 -0
- package/dist/resources/skills/userinterface-wiki/rules/visual-concentric-radius.md +40 -0
- package/dist/resources/skills/userinterface-wiki/rules/visual-consistent-spacing-scale.md +35 -0
- package/dist/resources/skills/userinterface-wiki/rules/visual-layered-shadows.md +30 -0
- package/dist/resources/skills/userinterface-wiki/rules/visual-no-pure-black-shadow.md +25 -0
- package/dist/resources/skills/userinterface-wiki/rules/visual-shadow-direction.md +25 -0
- package/dist/resources/skills/userinterface-wiki/rules/visual-shadow-matches-elevation.md +23 -0
- package/dist/resources/skills/userinterface-wiki/rules/weight-duration-matches-action.md +29 -0
- package/dist/resources/skills/userinterface-wiki/rules/weight-match-action.md +32 -0
- package/dist/resources/skills/web-design-guidelines/SKILL.md +39 -0
- package/dist/resources/skills/web-quality-audit/SKILL.md +170 -0
- package/dist/resources/skills/web-quality-audit/scripts/analyze.sh +91 -0
- package/dist/update-check.js +1 -1
- package/package.json +20 -8
- package/packages/native/package.json +28 -0
- package/packages/pi-agent-core/package.json +6 -0
- package/packages/pi-ai/dist/models.generated.d.ts +43 -11
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +34 -26
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +3 -2
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js +14 -4
- package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +7 -4
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -0
- package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/github-copilot.js +5 -1
- package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/google-antigravity.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/google-antigravity.js +4 -0
- package/packages/pi-ai/dist/utils/oauth/google-antigravity.js.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.js +6 -0
- package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.js.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/index.js +2 -2
- package/packages/pi-ai/dist/utils/oauth/index.js.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/openai-codex.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/openai-codex.js +2 -0
- package/packages/pi-ai/dist/utils/oauth/openai-codex.js.map +1 -1
- package/packages/pi-ai/oauth.d.ts +1 -0
- package/packages/pi-ai/oauth.js +1 -0
- package/packages/pi-ai/package.json +2 -2
- package/packages/pi-ai/src/models.generated.ts +42 -34
- package/packages/pi-ai/src/providers/anthropic.ts +3 -2
- package/packages/pi-ai/src/providers/openai-codex-responses.ts +15 -4
- package/packages/pi-ai/src/providers/openai-completions.ts +7 -4
- package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -0
- package/packages/pi-ai/src/utils/oauth/github-copilot.ts +5 -1
- package/packages/pi-ai/src/utils/oauth/google-antigravity.ts +4 -0
- package/packages/pi-ai/src/utils/oauth/google-gemini-cli.ts +6 -0
- package/packages/pi-ai/src/utils/oauth/index.ts +2 -2
- package/packages/pi-ai/src/utils/oauth/openai-codex.ts +2 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +2 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/bash-executor.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/bash-executor.js +23 -1
- package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/blob-store.d.ts +8 -0
- package/packages/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/blob-store.js +50 -1
- package/packages/pi-coding-agent/dist/core/blob-store.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/branch-summarization.js +2 -1
- package/packages/pi-coding-agent/dist/core/compaction/branch-summarization.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js +3 -2
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/utils.js +2 -2
- package/packages/pi-coding-agent/dist/core/compaction/utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/constants.d.ts +29 -0
- package/packages/pi-coding-agent/dist/core/constants.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/constants.js +44 -0
- package/packages/pi-coding-agent/dist/core/constants.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +32 -10
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +77 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/fs-utils.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/fs-utils.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/fs-utils.js +12 -0
- package/packages/pi-coding-agent/dist/core/fs-utils.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/fs-utils.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/fs-utils.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/fs-utils.test.js +67 -0
- package/packages/pi-coding-agent/dist/core/fs-utils.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +29 -2
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.js +9 -2
- package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.js +30 -4
- package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.js +13 -5
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +14 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.js +12 -4
- package/packages/pi-coding-agent/dist/core/resolve-config-value.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +49 -0
- package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +108 -14
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +14 -5
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +2 -2
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/find.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/find.js +2 -1
- package/packages/pi-coding-agent/dist/core/tools/find.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/hashline-edit.js +4 -4
- package/packages/pi-coding-agent/dist/core/tools/hashline-edit.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/hashline.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/tools/hashline.js +1 -1
- package/packages/pi-coding-agent/dist/core/tools/hashline.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/hashline.test.js +8 -8
- package/packages/pi-coding-agent/dist/core/tools/hashline.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.js +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/truncate.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/truncate.js +2 -1
- package/packages/pi-coding-agent/dist/core/tools/truncate.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +2 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +3 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +6 -0
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-selector.d.ts +9 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-selector.js +47 -5
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +4 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +12 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +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 +30 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js +11 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +12 -2
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/resources/extensions/memory/pipeline.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/resources/extensions/memory/pipeline.js +25 -3
- package/packages/pi-coding-agent/dist/resources/extensions/memory/pipeline.js.map +1 -1
- package/packages/pi-coding-agent/package.json +0 -4
- package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
- package/packages/pi-coding-agent/src/core/auth-storage.ts +2 -1
- package/packages/pi-coding-agent/src/core/bash-executor.ts +23 -1
- package/packages/pi-coding-agent/src/core/blob-store.ts +46 -1
- package/packages/pi-coding-agent/src/core/compaction/branch-summarization.ts +2 -1
- package/packages/pi-coding-agent/src/core/compaction/compaction.ts +3 -2
- package/packages/pi-coding-agent/src/core/compaction/utils.ts +2 -2
- package/packages/pi-coding-agent/src/core/constants.ts +59 -0
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +1 -1
- package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +84 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +31 -10
- package/packages/pi-coding-agent/src/core/fs-utils.test.ts +66 -0
- package/packages/pi-coding-agent/src/core/fs-utils.ts +12 -0
- package/packages/pi-coding-agent/src/core/lsp/client.ts +30 -2
- package/packages/pi-coding-agent/src/core/lsp/config.ts +9 -2
- package/packages/pi-coding-agent/src/core/lsp/index.ts +31 -5
- package/packages/pi-coding-agent/src/core/lsp/lspmux.ts +13 -5
- package/packages/pi-coding-agent/src/core/model-resolver.ts +14 -0
- package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +58 -0
- package/packages/pi-coding-agent/src/core/resolve-config-value.ts +14 -4
- package/packages/pi-coding-agent/src/core/session-manager.ts +113 -18
- package/packages/pi-coding-agent/src/core/settings-manager.ts +22 -5
- package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +2 -2
- package/packages/pi-coding-agent/src/core/tools/find.ts +2 -1
- package/packages/pi-coding-agent/src/core/tools/hashline-edit.ts +4 -4
- package/packages/pi-coding-agent/src/core/tools/hashline.test.ts +8 -8
- package/packages/pi-coding-agent/src/core/tools/hashline.ts +1 -1
- package/packages/pi-coding-agent/src/core/tools/index.ts +1 -1
- package/packages/pi-coding-agent/src/core/tools/path-utils.ts +1 -1
- package/packages/pi-coding-agent/src/core/tools/truncate.ts +3 -1
- package/packages/pi-coding-agent/src/index.ts +9 -0
- package/packages/pi-coding-agent/src/main.ts +7 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-selector.ts +49 -5
- package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +4 -4
- package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +15 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +32 -1
- package/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts +12 -0
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +11 -2
- package/packages/pi-coding-agent/src/resources/extensions/memory/pipeline.ts +23 -3
- package/packages/pi-tui/dist/__tests__/autocomplete.test.d.ts +2 -0
- package/packages/pi-tui/dist/__tests__/autocomplete.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/__tests__/autocomplete.test.js +149 -0
- package/packages/pi-tui/dist/__tests__/autocomplete.test.js.map +1 -0
- package/packages/pi-tui/dist/__tests__/fuzzy.test.d.ts +2 -0
- package/packages/pi-tui/dist/__tests__/fuzzy.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/__tests__/fuzzy.test.js +94 -0
- package/packages/pi-tui/dist/__tests__/fuzzy.test.js.map +1 -0
- package/packages/pi-tui/dist/autocomplete.d.ts +5 -1
- package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/packages/pi-tui/dist/autocomplete.js +6 -2
- package/packages/pi-tui/dist/autocomplete.js.map +1 -1
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +1 -1
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/package.json +8 -2
- package/packages/pi-tui/src/__tests__/autocomplete.test.ts +186 -0
- package/packages/pi-tui/src/__tests__/fuzzy.test.ts +112 -0
- package/packages/pi-tui/src/autocomplete.ts +8 -1
- package/packages/pi-tui/src/components/editor.ts +3 -1
- package/src/resources/extensions/ask-user-questions.ts +3 -2
- package/src/resources/extensions/bg-shell/bg-shell-command.ts +219 -0
- package/src/resources/extensions/bg-shell/bg-shell-lifecycle.ts +400 -0
- package/src/resources/extensions/bg-shell/bg-shell-tool.ts +985 -0
- package/src/resources/extensions/bg-shell/index.ts +18 -1545
- package/src/resources/extensions/bg-shell/overlay.ts +4 -0
- package/src/resources/extensions/bg-shell/process-manager.ts +45 -0
- package/src/resources/extensions/bg-shell/types.ts +21 -1
- package/src/resources/extensions/bg-shell/utilities.ts +4 -16
- package/src/resources/extensions/browser-tools/capture.ts +34 -2
- package/src/resources/extensions/browser-tools/lifecycle.ts +5 -5
- package/src/resources/extensions/browser-tools/settle.ts +1 -1
- package/src/resources/extensions/browser-tools/state.ts +5 -5
- package/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +1 -1
- package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +3 -3
- package/src/resources/extensions/browser-tools/tools/assertions.ts +1 -1
- package/src/resources/extensions/browser-tools/tools/device.ts +1 -1
- package/src/resources/extensions/browser-tools/tools/extract.ts +1 -1
- package/src/resources/extensions/browser-tools/tools/navigation.ts +6 -6
- package/src/resources/extensions/browser-tools/tools/network-mock.ts +1 -1
- package/src/resources/extensions/browser-tools/tools/pages.ts +1 -1
- package/src/resources/extensions/browser-tools/tools/screenshot.ts +28 -10
- package/src/resources/extensions/browser-tools/tools/state-persistence.ts +1 -1
- package/src/resources/extensions/browser-tools/tools/visual-diff.ts +1 -1
- package/src/resources/extensions/browser-tools/utils.ts +5 -5
- package/src/resources/extensions/get-secrets-from-user.ts +1 -1
- package/src/resources/extensions/google-search/index.ts +21 -8
- package/src/resources/extensions/gsd/activity-log.ts +2 -1
- package/src/resources/extensions/gsd/atomic-write.ts +35 -0
- package/src/resources/extensions/gsd/auto/session.ts +236 -0
- package/src/resources/extensions/gsd/auto-budget.ts +32 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +147 -13
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +23 -10
- package/src/resources/extensions/gsd/auto-idempotency.ts +150 -0
- package/src/resources/extensions/gsd/auto-model-selection.ts +179 -0
- package/src/resources/extensions/gsd/auto-observability.ts +74 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +591 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +116 -23
- package/src/resources/extensions/gsd/auto-recovery.ts +20 -9
- package/src/resources/extensions/gsd/auto-start.ts +500 -0
- package/src/resources/extensions/gsd/auto-stuck-detection.ts +220 -0
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
- package/src/resources/extensions/gsd/auto-timers.ts +223 -0
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
- package/src/resources/extensions/gsd/auto-unit-closeout.ts +48 -0
- package/src/resources/extensions/gsd/auto-verification.ts +195 -0
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +184 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +83 -67
- package/src/resources/extensions/gsd/auto.ts +825 -3069
- package/src/resources/extensions/gsd/commands-config.ts +102 -0
- package/src/resources/extensions/gsd/commands-handlers.ts +402 -0
- package/src/resources/extensions/gsd/commands-inspect.ts +90 -0
- package/src/resources/extensions/gsd/commands-logs.ts +537 -0
- package/src/resources/extensions/gsd/commands-maintenance.ts +206 -0
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +747 -0
- package/src/resources/extensions/gsd/commands.ts +380 -1433
- package/src/resources/extensions/gsd/constants.ts +21 -0
- package/src/resources/extensions/gsd/context-budget.ts +25 -2
- package/src/resources/extensions/gsd/crash-recovery.ts +4 -2
- package/src/resources/extensions/gsd/dashboard-overlay.ts +58 -74
- package/src/resources/extensions/gsd/db-writer.ts +21 -2
- package/src/resources/extensions/gsd/detection.ts +469 -0
- package/src/resources/extensions/gsd/diff-context.ts +2 -1
- package/src/resources/extensions/gsd/dispatch-guard.ts +4 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +83 -0
- package/src/resources/extensions/gsd/doctor-checks.ts +564 -0
- package/src/resources/extensions/gsd/doctor-format.ts +78 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +9 -4
- package/src/resources/extensions/gsd/doctor-types.ts +72 -0
- package/src/resources/extensions/gsd/doctor.ts +64 -701
- package/src/resources/extensions/gsd/errors.ts +0 -2
- package/src/resources/extensions/gsd/export-html.ts +1357 -0
- package/src/resources/extensions/gsd/export.ts +141 -4
- package/src/resources/extensions/gsd/files.ts +8 -126
- package/src/resources/extensions/gsd/forensics.ts +2 -12
- package/src/resources/extensions/gsd/git-constants.ts +11 -0
- package/src/resources/extensions/gsd/git-service.ts +19 -9
- package/src/resources/extensions/gsd/gitignore.ts +4 -1
- package/src/resources/extensions/gsd/gsd-db.ts +26 -6
- package/src/resources/extensions/gsd/guided-flow-queue.ts +451 -0
- package/src/resources/extensions/gsd/guided-flow.ts +252 -516
- package/src/resources/extensions/gsd/history.ts +2 -20
- package/src/resources/extensions/gsd/index.ts +260 -46
- package/src/resources/extensions/gsd/init-wizard.ts +615 -0
- package/src/resources/extensions/gsd/jsonl-utils.ts +21 -0
- package/src/resources/extensions/gsd/key-manager.ts +995 -0
- package/src/resources/extensions/gsd/metrics.ts +32 -5
- package/src/resources/extensions/gsd/migrate/command.ts +1 -1
- package/src/resources/extensions/gsd/migrate/parsers.ts +10 -95
- package/src/resources/extensions/gsd/milestone-actions.ts +126 -0
- package/src/resources/extensions/gsd/milestone-ids.ts +95 -0
- package/src/resources/extensions/gsd/native-git-bridge.ts +35 -12
- package/src/resources/extensions/gsd/parallel-eligibility.ts +3 -3
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
- package/src/resources/extensions/gsd/paths.ts +1 -3
- package/src/resources/extensions/gsd/plugin-importer.ts +3 -2
- package/src/resources/extensions/gsd/preferences-hooks.ts +10 -0
- package/src/resources/extensions/gsd/preferences-models.ts +323 -0
- package/src/resources/extensions/gsd/preferences-skills.ts +169 -0
- package/src/resources/extensions/gsd/preferences-types.ts +223 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +597 -0
- package/src/resources/extensions/gsd/preferences.ts +219 -1286
- package/src/resources/extensions/gsd/prompt-cache-optimizer.ts +213 -0
- package/src/resources/extensions/gsd/prompt-compressor.ts +508 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +4 -2
- package/src/resources/extensions/gsd/prompt-ordering.ts +200 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -2
- package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -4
- package/src/resources/extensions/gsd/prompts/discuss.md +13 -5
- package/src/resources/extensions/gsd/prompts/execute-task.md +1 -2
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +0 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -2
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -2
- package/src/resources/extensions/gsd/prompts/queue.md +30 -0
- package/src/resources/extensions/gsd/prompts/quick-task.md +0 -6
- package/src/resources/extensions/gsd/prompts/replan-slice.md +0 -1
- package/src/resources/extensions/gsd/prompts/rewrite-docs.md +0 -1
- package/src/resources/extensions/gsd/prompts/system.md +6 -3
- package/src/resources/extensions/gsd/provider-error-pause.ts +59 -10
- package/src/resources/extensions/gsd/queue-order.ts +1 -1
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +15 -2
- package/src/resources/extensions/gsd/quick.ts +18 -15
- package/src/resources/extensions/gsd/reports.ts +504 -0
- package/src/resources/extensions/gsd/roadmap-slices.ts +1 -1
- package/src/resources/extensions/gsd/safe-fs.ts +47 -0
- package/src/resources/extensions/gsd/semantic-chunker.ts +336 -0
- package/src/resources/extensions/gsd/session-forensics.ts +8 -23
- package/src/resources/extensions/gsd/session-lock.ts +284 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +242 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +83 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +64 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +176 -0
- package/src/resources/extensions/gsd/state.ts +84 -2
- package/src/resources/extensions/gsd/structured-data-formatter.ts +144 -0
- package/src/resources/extensions/gsd/summary-distiller.ts +258 -0
- package/src/resources/extensions/gsd/templates/preferences.md +34 -0
- package/src/resources/extensions/gsd/tests/activity-log.test.ts +213 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +197 -0
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
- package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +33 -39
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +108 -2
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +257 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
- package/src/resources/extensions/gsd/tests/context-budget.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
- package/src/resources/extensions/gsd/tests/detection.test.ts +398 -0
- package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +12 -24
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +118 -94
- package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +126 -0
- package/src/resources/extensions/gsd/tests/dist-redirect.mjs +7 -3
- package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +75 -0
- package/src/resources/extensions/gsd/tests/doctor-git.test.ts +17 -55
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/export-html-all.test.ts +105 -0
- package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +375 -0
- package/src/resources/extensions/gsd/tests/extension-selector-separator.test.ts +122 -0
- package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/headless-answers.test.ts +340 -0
- package/src/resources/extensions/gsd/tests/headless-query.test.ts +162 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +24 -82
- package/src/resources/extensions/gsd/tests/init-wizard.test.ts +197 -0
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/key-manager.test.ts +414 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +173 -305
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +4 -1
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +59 -1
- package/src/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
- package/src/resources/extensions/gsd/tests/next-milestone-id.test.ts +18 -61
- package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +17 -8
- package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
- package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +468 -0
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
- package/src/resources/extensions/gsd/tests/park-edge-cases.test.ts +276 -0
- package/src/resources/extensions/gsd/tests/park-milestone.test.ts +401 -0
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +284 -0
- package/src/resources/extensions/gsd/tests/prompt-cache-optimizer.test.ts +314 -0
- package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +529 -0
- package/src/resources/extensions/gsd/tests/prompt-ordering.test.ts +296 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +338 -0
- package/src/resources/extensions/gsd/tests/reassess-detection.test.ts +154 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/remote-status.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -60
- package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +426 -0
- package/src/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +8 -5
- package/src/resources/extensions/gsd/tests/structured-data-formatter.test.ts +365 -0
- package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +323 -0
- package/src/resources/extensions/gsd/tests/token-counter.test.ts +129 -0
- package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +1272 -0
- package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +164 -0
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +75 -79
- package/src/resources/extensions/gsd/tests/update-command.test.ts +67 -0
- package/src/resources/extensions/gsd/tests/validate-directory.test.ts +222 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +115 -1
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/workspace-index.test.ts +24 -61
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +5 -2
- package/src/resources/extensions/gsd/tests/worktree.test.ts +3 -1
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +132 -43
- package/src/resources/extensions/gsd/token-counter.ts +20 -0
- package/src/resources/extensions/gsd/triage-ui.ts +1 -1
- package/src/resources/extensions/gsd/types.ts +4 -1
- package/src/resources/extensions/gsd/validate-directory.ts +164 -0
- package/src/resources/extensions/gsd/verification-evidence.ts +7 -4
- package/src/resources/extensions/gsd/verification-gate.ts +70 -5
- package/src/resources/extensions/gsd/visualizer-data.ts +26 -4
- package/src/resources/extensions/gsd/visualizer-overlay.ts +35 -22
- package/src/resources/extensions/gsd/visualizer-views.ts +14 -66
- package/src/resources/extensions/gsd/worktree-command.ts +4 -51
- package/src/resources/extensions/gsd/worktree-manager.ts +7 -9
- package/src/resources/extensions/gsd/worktree.ts +41 -1
- package/src/resources/extensions/mcporter/index.ts +27 -14
- package/src/resources/extensions/remote-questions/manager.ts +6 -24
- package/src/resources/extensions/remote-questions/mod.ts +16 -0
- package/src/resources/extensions/remote-questions/notify.ts +91 -0
- package/src/resources/extensions/remote-questions/remote-command.ts +1 -1
- package/src/resources/extensions/remote-questions/store.ts +5 -1
- package/src/resources/extensions/remote-questions/types.ts +26 -3
- package/src/resources/extensions/search-the-web/native-search.ts +7 -0
- package/src/resources/extensions/search-the-web/provider.ts +15 -3
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +1 -13
- package/src/resources/extensions/search-the-web/tool-search.ts +27 -13
- package/src/resources/extensions/shared/format-utils.ts +138 -0
- package/src/resources/extensions/shared/frontmatter.ts +117 -0
- package/src/resources/extensions/shared/mod.ts +30 -0
- package/src/resources/extensions/shared/sanitize.ts +19 -0
- package/src/resources/extensions/shared/tests/format-utils.test.ts +153 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +1 -1
- package/src/resources/extensions/slash-commands/create-slash-command.ts +1 -1
- package/src/resources/extensions/subagent/index.ts +47 -3
- package/src/resources/extensions/subagent/isolation.ts +9 -6
- package/src/resources/extensions/ttsr/index.ts +5 -0
- package/src/resources/extensions/ttsr/rule-loader.ts +4 -51
- package/src/resources/extensions/universal-config/discovery.ts +37 -15
- package/src/resources/extensions/voice/index.ts +1 -1
- package/src/resources/skills/accessibility/SKILL.md +522 -0
- package/src/resources/skills/accessibility/references/WCAG.md +162 -0
- package/src/resources/skills/agent-browser/SKILL.md +517 -0
- package/src/resources/skills/agent-browser/references/authentication.md +202 -0
- package/src/resources/skills/agent-browser/references/commands.md +263 -0
- package/src/resources/skills/agent-browser/references/profiling.md +120 -0
- package/src/resources/skills/agent-browser/references/proxy-support.md +194 -0
- package/src/resources/skills/agent-browser/references/session-management.md +193 -0
- package/src/resources/skills/agent-browser/references/snapshot-refs.md +194 -0
- package/src/resources/skills/agent-browser/references/video-recording.md +173 -0
- package/src/resources/skills/agent-browser/templates/authenticated-session.sh +105 -0
- package/src/resources/skills/agent-browser/templates/capture-workflow.sh +69 -0
- package/src/resources/skills/agent-browser/templates/form-automation.sh +62 -0
- package/src/resources/skills/best-practices/SKILL.md +583 -0
- package/src/resources/skills/code-optimizer/SKILL.md +160 -0
- package/src/resources/skills/code-optimizer/references/algorithmic-complexity.md +66 -0
- package/src/resources/skills/code-optimizer/references/build-compilation.md +90 -0
- package/src/resources/skills/code-optimizer/references/bundle-dependencies.md +82 -0
- package/src/resources/skills/code-optimizer/references/caching-memoization.md +76 -0
- package/src/resources/skills/code-optimizer/references/concurrency-async.md +80 -0
- package/src/resources/skills/code-optimizer/references/config-infra.md +71 -0
- package/src/resources/skills/code-optimizer/references/data-structures.md +80 -0
- package/src/resources/skills/code-optimizer/references/database-queries.md +76 -0
- package/src/resources/skills/code-optimizer/references/dead-code-redundancy.md +84 -0
- package/src/resources/skills/code-optimizer/references/error-resilience.md +80 -0
- package/src/resources/skills/code-optimizer/references/io-network.md +89 -0
- package/src/resources/skills/code-optimizer/references/logging-observability.md +64 -0
- package/src/resources/skills/code-optimizer/references/memory-resources.md +66 -0
- package/src/resources/skills/code-optimizer/references/rendering-ui.md +90 -0
- package/src/resources/skills/code-optimizer/references/security-performance.md +68 -0
- package/src/resources/skills/core-web-vitals/SKILL.md +441 -0
- package/src/resources/skills/core-web-vitals/references/LCP.md +208 -0
- package/src/resources/skills/make-interfaces-feel-better/SKILL.md +122 -0
- package/src/resources/skills/make-interfaces-feel-better/animations.md +379 -0
- package/src/resources/skills/make-interfaces-feel-better/performance.md +88 -0
- package/src/resources/skills/make-interfaces-feel-better/surfaces.md +247 -0
- package/src/resources/skills/make-interfaces-feel-better/typography.md +123 -0
- package/src/resources/skills/react-best-practices/README.md +123 -0
- package/src/resources/skills/react-best-practices/SKILL.md +136 -0
- package/src/resources/skills/react-best-practices/metadata.json +15 -0
- package/src/resources/skills/react-best-practices/rules/_sections.md +46 -0
- package/src/resources/skills/react-best-practices/rules/_template.md +28 -0
- package/src/resources/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/src/resources/skills/react-best-practices/rules/advanced-init-once.md +42 -0
- package/src/resources/skills/react-best-practices/rules/advanced-use-latest.md +39 -0
- package/src/resources/skills/react-best-practices/rules/async-api-routes.md +38 -0
- package/src/resources/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/src/resources/skills/react-best-practices/rules/async-dependencies.md +51 -0
- package/src/resources/skills/react-best-practices/rules/async-parallel.md +28 -0
- package/src/resources/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/src/resources/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/src/resources/skills/react-best-practices/rules/bundle-conditional.md +31 -0
- package/src/resources/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/src/resources/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/src/resources/skills/react-best-practices/rules/bundle-preload.md +50 -0
- package/src/resources/skills/react-best-practices/rules/client-event-listeners.md +74 -0
- package/src/resources/skills/react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/src/resources/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/src/resources/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/src/resources/skills/react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/src/resources/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/src/resources/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/src/resources/skills/react-best-practices/rules/js-cache-storage.md +70 -0
- package/src/resources/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/src/resources/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/src/resources/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/src/resources/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/src/resources/skills/react-best-practices/rules/js-length-check-first.md +49 -0
- package/src/resources/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/src/resources/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/src/resources/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/src/resources/skills/react-best-practices/rules/rendering-activity.md +26 -0
- package/src/resources/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/src/resources/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/src/resources/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/src/resources/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/src/resources/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/src/resources/skills/react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/src/resources/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/src/resources/skills/react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/src/resources/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/src/resources/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/src/resources/skills/react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/src/resources/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/src/resources/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/src/resources/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/src/resources/skills/react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/src/resources/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/src/resources/skills/react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/src/resources/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/src/resources/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/src/resources/skills/react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/src/resources/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/src/resources/skills/react-best-practices/rules/server-auth-actions.md +96 -0
- package/src/resources/skills/react-best-practices/rules/server-cache-lru.md +41 -0
- package/src/resources/skills/react-best-practices/rules/server-cache-react.md +76 -0
- package/src/resources/skills/react-best-practices/rules/server-dedup-props.md +65 -0
- package/src/resources/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/src/resources/skills/react-best-practices/rules/server-serialization.md +38 -0
- package/src/resources/skills/userinterface-wiki/SKILL.md +253 -0
- package/src/resources/skills/userinterface-wiki/rules/_sections.md +66 -0
- package/src/resources/skills/userinterface-wiki/rules/_template.md +24 -0
- package/src/resources/skills/userinterface-wiki/rules/a11y-reduced-motion-check.md +30 -0
- package/src/resources/skills/userinterface-wiki/rules/a11y-toggle-setting.md +30 -0
- package/src/resources/skills/userinterface-wiki/rules/a11y-visual-equivalent.md +36 -0
- package/src/resources/skills/userinterface-wiki/rules/a11y-volume-control.md +28 -0
- package/src/resources/skills/userinterface-wiki/rules/appropriate-confirmations-only.md +19 -0
- package/src/resources/skills/userinterface-wiki/rules/appropriate-errors-warnings.md +18 -0
- package/src/resources/skills/userinterface-wiki/rules/appropriate-no-decorative.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/appropriate-no-high-frequency.md +28 -0
- package/src/resources/skills/userinterface-wiki/rules/appropriate-no-punishing.md +27 -0
- package/src/resources/skills/userinterface-wiki/rules/container-callback-ref.md +31 -0
- package/src/resources/skills/userinterface-wiki/rules/container-guard-initial-zero.md +25 -0
- package/src/resources/skills/userinterface-wiki/rules/container-no-excessive-use.md +13 -0
- package/src/resources/skills/userinterface-wiki/rules/container-overflow-hidden.md +25 -0
- package/src/resources/skills/userinterface-wiki/rules/container-transition-delay.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/container-two-div-pattern.md +35 -0
- package/src/resources/skills/userinterface-wiki/rules/container-use-resize-observer.md +48 -0
- package/src/resources/skills/userinterface-wiki/rules/context-cleanup-nodes.md +25 -0
- package/src/resources/skills/userinterface-wiki/rules/context-resume-suspended.md +28 -0
- package/src/resources/skills/userinterface-wiki/rules/context-reuse-single.md +30 -0
- package/src/resources/skills/userinterface-wiki/rules/design-filter-for-character.md +25 -0
- package/src/resources/skills/userinterface-wiki/rules/design-noise-for-percussion.md +26 -0
- package/src/resources/skills/userinterface-wiki/rules/design-oscillator-for-tonal.md +22 -0
- package/src/resources/skills/userinterface-wiki/rules/duration-max-300ms.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/duration-press-hover.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/duration-shorten-before-curve.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/duration-small-state.md +15 -0
- package/src/resources/skills/userinterface-wiki/rules/easing-entrance-ease-out.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/easing-exit-ease-in.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/easing-for-state-change.md +27 -0
- package/src/resources/skills/userinterface-wiki/rules/easing-linear-only-progress.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/easing-natural-decay.md +22 -0
- package/src/resources/skills/userinterface-wiki/rules/easing-no-linear-motion.md +22 -0
- package/src/resources/skills/userinterface-wiki/rules/easing-transition-ease-in-out.md +15 -0
- package/src/resources/skills/userinterface-wiki/rules/envelope-exponential-decay.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/envelope-no-zero-target.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/envelope-set-initial-value.md +22 -0
- package/src/resources/skills/userinterface-wiki/rules/exit-key-required.md +29 -0
- package/src/resources/skills/userinterface-wiki/rules/exit-matches-initial.md +29 -0
- package/src/resources/skills/userinterface-wiki/rules/exit-prop-required.md +33 -0
- package/src/resources/skills/userinterface-wiki/rules/exit-requires-wrapper.md +27 -0
- package/src/resources/skills/userinterface-wiki/rules/impl-default-subtle.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/impl-preload-audio.md +34 -0
- package/src/resources/skills/userinterface-wiki/rules/impl-reset-current-time.md +26 -0
- package/src/resources/skills/userinterface-wiki/rules/mode-pop-layout-for-lists.md +25 -0
- package/src/resources/skills/userinterface-wiki/rules/mode-sync-layout-conflict.md +29 -0
- package/src/resources/skills/userinterface-wiki/rules/mode-wait-doubles-duration.md +25 -0
- package/src/resources/skills/userinterface-wiki/rules/morphing-aria-hidden.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/morphing-consistent-viewbox.md +23 -0
- package/src/resources/skills/userinterface-wiki/rules/morphing-group-variants.md +33 -0
- package/src/resources/skills/userinterface-wiki/rules/morphing-jump-non-grouped.md +29 -0
- package/src/resources/skills/userinterface-wiki/rules/morphing-reduced-motion.md +28 -0
- package/src/resources/skills/userinterface-wiki/rules/morphing-spring-rotation.md +23 -0
- package/src/resources/skills/userinterface-wiki/rules/morphing-strokelinecap-round.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/morphing-three-lines.md +32 -0
- package/src/resources/skills/userinterface-wiki/rules/morphing-use-collapsed.md +33 -0
- package/src/resources/skills/userinterface-wiki/rules/native-backdrop-styling.md +27 -0
- package/src/resources/skills/userinterface-wiki/rules/native-placeholder-styling.md +27 -0
- package/src/resources/skills/userinterface-wiki/rules/native-selection-styling.md +18 -0
- package/src/resources/skills/userinterface-wiki/rules/nested-consistent-timing.md +25 -0
- package/src/resources/skills/userinterface-wiki/rules/nested-propagate-required.md +41 -0
- package/src/resources/skills/userinterface-wiki/rules/none-context-menu-entrance.md +25 -0
- package/src/resources/skills/userinterface-wiki/rules/none-high-frequency.md +29 -0
- package/src/resources/skills/userinterface-wiki/rules/none-keyboard-navigation.md +32 -0
- package/src/resources/skills/userinterface-wiki/rules/param-click-duration.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/param-filter-frequency-range.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/param-q-value-range.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/param-reasonable-gain.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/physics-active-state.md +23 -0
- package/src/resources/skills/userinterface-wiki/rules/physics-no-excessive-stagger.md +22 -0
- package/src/resources/skills/userinterface-wiki/rules/physics-spring-for-overshoot.md +23 -0
- package/src/resources/skills/userinterface-wiki/rules/physics-subtle-deformation.md +22 -0
- package/src/resources/skills/userinterface-wiki/rules/prefetch-hit-slop.md +27 -0
- package/src/resources/skills/userinterface-wiki/rules/prefetch-keyboard-tab.md +19 -0
- package/src/resources/skills/userinterface-wiki/rules/prefetch-not-everything.md +22 -0
- package/src/resources/skills/userinterface-wiki/rules/prefetch-touch-fallback.md +34 -0
- package/src/resources/skills/userinterface-wiki/rules/prefetch-trajectory-over-hover.md +32 -0
- package/src/resources/skills/userinterface-wiki/rules/prefetch-use-selectively.md +13 -0
- package/src/resources/skills/userinterface-wiki/rules/presence-disable-interactions.md +31 -0
- package/src/resources/skills/userinterface-wiki/rules/presence-hook-in-child.md +31 -0
- package/src/resources/skills/userinterface-wiki/rules/presence-safe-to-remove.md +37 -0
- package/src/resources/skills/userinterface-wiki/rules/pseudo-content-required.md +28 -0
- package/src/resources/skills/userinterface-wiki/rules/pseudo-first-line-styling.md +27 -0
- package/src/resources/skills/userinterface-wiki/rules/pseudo-hit-target-expansion.md +31 -0
- package/src/resources/skills/userinterface-wiki/rules/pseudo-marker-styling.md +28 -0
- package/src/resources/skills/userinterface-wiki/rules/pseudo-over-dom-node.md +32 -0
- package/src/resources/skills/userinterface-wiki/rules/pseudo-position-relative-parent.md +33 -0
- package/src/resources/skills/userinterface-wiki/rules/pseudo-z-index-layering.md +37 -0
- package/src/resources/skills/userinterface-wiki/rules/spring-for-gestures.md +27 -0
- package/src/resources/skills/userinterface-wiki/rules/spring-for-interruptible.md +27 -0
- package/src/resources/skills/userinterface-wiki/rules/spring-params-balanced.md +29 -0
- package/src/resources/skills/userinterface-wiki/rules/spring-preserves-velocity.md +28 -0
- package/src/resources/skills/userinterface-wiki/rules/staging-dim-background.md +22 -0
- package/src/resources/skills/userinterface-wiki/rules/staging-one-focal-point.md +24 -0
- package/src/resources/skills/userinterface-wiki/rules/staging-z-index-hierarchy.md +22 -0
- package/src/resources/skills/userinterface-wiki/rules/timing-consistent.md +24 -0
- package/src/resources/skills/userinterface-wiki/rules/timing-no-entrance-context-menu.md +22 -0
- package/src/resources/skills/userinterface-wiki/rules/timing-under-300ms.md +22 -0
- package/src/resources/skills/userinterface-wiki/rules/transition-name-cleanup.md +28 -0
- package/src/resources/skills/userinterface-wiki/rules/transition-name-required.md +27 -0
- package/src/resources/skills/userinterface-wiki/rules/transition-name-unique.md +24 -0
- package/src/resources/skills/userinterface-wiki/rules/transition-over-js-library.md +32 -0
- package/src/resources/skills/userinterface-wiki/rules/transition-style-pseudo-elements.md +24 -0
- package/src/resources/skills/userinterface-wiki/rules/type-antialiased-on-retina.md +18 -0
- package/src/resources/skills/userinterface-wiki/rules/type-disambiguation-stylistic-set.md +15 -0
- package/src/resources/skills/userinterface-wiki/rules/type-font-display-swap.md +28 -0
- package/src/resources/skills/userinterface-wiki/rules/type-justify-with-hyphens.md +24 -0
- package/src/resources/skills/userinterface-wiki/rules/type-letter-spacing-uppercase.md +28 -0
- package/src/resources/skills/userinterface-wiki/rules/type-no-font-synthesis.md +18 -0
- package/src/resources/skills/userinterface-wiki/rules/type-oldstyle-nums-for-prose.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/type-opentype-contextual-alternates.md +15 -0
- package/src/resources/skills/userinterface-wiki/rules/type-optical-sizing-auto.md +25 -0
- package/src/resources/skills/userinterface-wiki/rules/type-proper-fractions.md +15 -0
- package/src/resources/skills/userinterface-wiki/rules/type-slashed-zero.md +17 -0
- package/src/resources/skills/userinterface-wiki/rules/type-tabular-nums-for-data.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/type-text-wrap-balance-headings.md +21 -0
- package/src/resources/skills/userinterface-wiki/rules/type-text-wrap-pretty.md +16 -0
- package/src/resources/skills/userinterface-wiki/rules/type-underline-offset.md +25 -0
- package/src/resources/skills/userinterface-wiki/rules/type-variable-weight-continuous.md +23 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-aesthetic-usability.md +32 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-cognitive-load-reduce.md +49 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-common-region-boundaries.md +50 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-doherty-perceived-speed.md +29 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-doherty-under-400ms.md +30 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-fitts-hit-area.md +32 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-fitts-target-size.md +31 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-goal-gradient-progress.md +33 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-hicks-minimize-choices.md +45 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-jakobs-familiar-patterns.md +37 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-millers-chunking.md +23 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-pareto-prioritize-features.md +36 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-peak-end-finish-strong.md +35 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-postels-accept-messy-input.md +45 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-pragnanz-simplify.md +33 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-progressive-disclosure.md +41 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-proximity-grouping.md +38 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-serial-position.md +31 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-similarity-consistency.md +35 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-teslers-complexity.md +28 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-uniform-connectedness.md +43 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-von-restorff-emphasis.md +29 -0
- package/src/resources/skills/userinterface-wiki/rules/ux-zeigarnik-show-incomplete.md +36 -0
- package/src/resources/skills/userinterface-wiki/rules/visual-animate-shadow-pseudo.md +49 -0
- package/src/resources/skills/userinterface-wiki/rules/visual-border-alpha-colors.md +25 -0
- package/src/resources/skills/userinterface-wiki/rules/visual-button-shadow-anatomy.md +49 -0
- package/src/resources/skills/userinterface-wiki/rules/visual-concentric-radius.md +40 -0
- package/src/resources/skills/userinterface-wiki/rules/visual-consistent-spacing-scale.md +35 -0
- package/src/resources/skills/userinterface-wiki/rules/visual-layered-shadows.md +30 -0
- package/src/resources/skills/userinterface-wiki/rules/visual-no-pure-black-shadow.md +25 -0
- package/src/resources/skills/userinterface-wiki/rules/visual-shadow-direction.md +25 -0
- package/src/resources/skills/userinterface-wiki/rules/visual-shadow-matches-elevation.md +23 -0
- package/src/resources/skills/userinterface-wiki/rules/weight-duration-matches-action.md +29 -0
- package/src/resources/skills/userinterface-wiki/rules/weight-match-action.md +32 -0
- package/src/resources/skills/web-design-guidelines/SKILL.md +39 -0
- package/src/resources/skills/web-quality-audit/SKILL.md +170 -0
- package/src/resources/skills/web-quality-audit/scripts/analyze.sh +91 -0
- package/dist/resources/extensions/gsd/complexity.ts +0 -237
- package/dist/resources/extensions/gsd/github-client.ts +0 -235
- package/dist/resources/extensions/gsd/mcp-server.ts +0 -108
- package/dist/resources/extensions/gsd/tests/activity-log-prune.test.ts +0 -297
- package/dist/resources/extensions/gsd/tests/activity-log-save.test.ts +0 -127
- package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +0 -110
- package/dist/resources/extensions/gsd/tests/auto-draft-pause.test.ts +0 -115
- package/dist/resources/extensions/gsd/tests/complexity-routing.test.ts +0 -294
- package/dist/resources/extensions/gsd/tests/marketplace-discovery.test.ts +0 -202
- package/dist/resources/extensions/gsd/tests/metrics-io.test.ts +0 -176
- package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +0 -54
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -120
- package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -226
- package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +0 -110
- package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -207
- package/dist/resources/extensions/gsd/tests/preferences-schema-validation.test.ts +0 -183
- package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +0 -168
- package/dist/resources/extensions/shared/bundled-extension-paths.ts +0 -11
- package/src/resources/extensions/gsd/complexity.ts +0 -237
- package/src/resources/extensions/gsd/github-client.ts +0 -235
- package/src/resources/extensions/gsd/mcp-server.ts +0 -108
- package/src/resources/extensions/gsd/tests/activity-log-prune.test.ts +0 -297
- package/src/resources/extensions/gsd/tests/activity-log-save.test.ts +0 -127
- package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +0 -110
- package/src/resources/extensions/gsd/tests/auto-draft-pause.test.ts +0 -115
- package/src/resources/extensions/gsd/tests/complexity-routing.test.ts +0 -294
- package/src/resources/extensions/gsd/tests/marketplace-discovery.test.ts +0 -202
- package/src/resources/extensions/gsd/tests/metrics-io.test.ts +0 -176
- package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +0 -54
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -120
- package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -226
- package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +0 -110
- package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -207
- package/src/resources/extensions/gsd/tests/preferences-schema-validation.test.ts +0 -183
- package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +0 -168
- package/src/resources/extensions/shared/bundled-extension-paths.ts +0 -11
|
@@ -17,8 +17,8 @@ import type {
|
|
|
17
17
|
} from "@gsd/pi-coding-agent";
|
|
18
18
|
|
|
19
19
|
import { deriveState } from "./state.js";
|
|
20
|
-
import type {
|
|
21
|
-
import { loadFile,
|
|
20
|
+
import type { GSDState } from "./types.js";
|
|
21
|
+
import { loadFile, getManifestStatus, resolveAllOverrides, parsePlan, parseSummary } from "./files.js";
|
|
22
22
|
import { loadPrompt } from "./prompt-loader.js";
|
|
23
23
|
import { runVerificationGate, formatFailureContext, captureRuntimeErrors, runDependencyAudit } from "./verification-gate.js";
|
|
24
24
|
import { writeVerificationJSON } from "./verification-evidence.js";
|
|
@@ -27,26 +27,55 @@ import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
|
27
27
|
import {
|
|
28
28
|
gsdRoot, resolveMilestoneFile, resolveSliceFile, resolveSlicePath,
|
|
29
29
|
resolveMilestonePath, resolveDir, resolveTasksDir, resolveTaskFile,
|
|
30
|
-
|
|
31
|
-
milestonesDir,
|
|
32
|
-
buildMilestoneFileName, buildSliceFileName, buildTaskFileName,
|
|
30
|
+
milestonesDir, buildTaskFileName,
|
|
33
31
|
} from "./paths.js";
|
|
34
32
|
import { invalidateAllCaches } from "./cache.js";
|
|
35
33
|
import { saveActivityLog, clearActivityLogState } from "./activity-log.js";
|
|
36
34
|
import { synthesizeCrashRecovery, getDeepDiagnostic } from "./session-forensics.js";
|
|
37
35
|
import { writeLock, clearLock, readCrashLock, formatCrashInfo, isLockProcessAlive } from "./crash-recovery.js";
|
|
36
|
+
import {
|
|
37
|
+
acquireSessionLock,
|
|
38
|
+
validateSessionLock,
|
|
39
|
+
releaseSessionLock,
|
|
40
|
+
updateSessionLock,
|
|
41
|
+
} from "./session-lock.js";
|
|
38
42
|
import {
|
|
39
43
|
clearUnitRuntimeRecord,
|
|
40
|
-
formatExecuteTaskRecoveryStatus,
|
|
41
44
|
inspectExecuteTaskDurability,
|
|
42
45
|
readUnitRuntimeRecord,
|
|
43
46
|
writeUnitRuntimeRecord,
|
|
44
47
|
} from "./unit-runtime.js";
|
|
45
|
-
import { resolveAutoSupervisorConfig,
|
|
48
|
+
import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences, resolveSkillDiscoveryMode, getIsolationMode } from "./preferences.js";
|
|
46
49
|
import { sendDesktopNotification } from "./notifications.js";
|
|
47
50
|
import type { GSDPreferences } from "./preferences.js";
|
|
48
|
-
import {
|
|
49
|
-
|
|
51
|
+
import {
|
|
52
|
+
type BudgetAlertLevel,
|
|
53
|
+
getBudgetAlertLevel,
|
|
54
|
+
getNewBudgetAlertLevel,
|
|
55
|
+
getBudgetEnforcementAction,
|
|
56
|
+
} from "./auto-budget.js";
|
|
57
|
+
import {
|
|
58
|
+
markToolStart as _markToolStart,
|
|
59
|
+
markToolEnd as _markToolEnd,
|
|
60
|
+
getOldestInFlightToolAgeMs as _getOldestInFlightToolAgeMs,
|
|
61
|
+
getInFlightToolCount,
|
|
62
|
+
getOldestInFlightToolStart,
|
|
63
|
+
clearInFlightTools,
|
|
64
|
+
} from "./auto-tool-tracking.js";
|
|
65
|
+
import {
|
|
66
|
+
collectObservabilityWarnings as _collectObservabilityWarnings,
|
|
67
|
+
buildObservabilityRepairBlock,
|
|
68
|
+
} from "./auto-observability.js";
|
|
69
|
+
import { closeoutUnit } from "./auto-unit-closeout.js";
|
|
70
|
+
import { recoverTimedOutUnit } from "./auto-timeout-recovery.js";
|
|
71
|
+
import { selectAndApplyModel } from "./auto-model-selection.js";
|
|
72
|
+
import {
|
|
73
|
+
syncProjectRootToWorktree,
|
|
74
|
+
syncStateToProjectRoot,
|
|
75
|
+
readResourceVersion,
|
|
76
|
+
checkResourcesStale,
|
|
77
|
+
escapeStaleWorktree,
|
|
78
|
+
} from "./auto-worktree-sync.js";
|
|
50
79
|
import { initRoutingHistory, resetRoutingHistory, recordOutcome } from "./routing-history.js";
|
|
51
80
|
import {
|
|
52
81
|
checkPostUnitHooks,
|
|
@@ -59,12 +88,6 @@ import {
|
|
|
59
88
|
restoreHookState,
|
|
60
89
|
clearPersistedHookState,
|
|
61
90
|
} from "./post-unit-hooks.js";
|
|
62
|
-
import {
|
|
63
|
-
validatePlanBoundary,
|
|
64
|
-
validateExecuteBoundary,
|
|
65
|
-
validateCompleteBoundary,
|
|
66
|
-
formatValidationIssues,
|
|
67
|
-
} from "./observability-validator.js";
|
|
68
91
|
import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
|
|
69
92
|
import { runGSDDoctor, rebuildState, summarizeDoctorIssues } from "./doctor.js";
|
|
70
93
|
import {
|
|
@@ -78,14 +101,16 @@ import {
|
|
|
78
101
|
import { snapshotSkills, clearSkillSnapshot } from "./skill-discovery.js";
|
|
79
102
|
import { captureAvailableSkills, getAndClearSkills, resetSkillTelemetry } from "./skill-telemetry.js";
|
|
80
103
|
import {
|
|
81
|
-
initMetrics, resetMetrics,
|
|
104
|
+
initMetrics, resetMetrics, getLedger,
|
|
82
105
|
getProjectTotals, formatCost, formatTokenCount,
|
|
83
106
|
} from "./metrics.js";
|
|
107
|
+
import { computeBudgets, resolveExecutorContextWindow } from "./context-budget.js";
|
|
108
|
+
import { GSDError, GSD_ARTIFACT_MISSING } from "./errors.js";
|
|
84
109
|
import { join } from "node:path";
|
|
85
110
|
import { sep as pathSep } from "node:path";
|
|
86
|
-
import {
|
|
87
|
-
import {
|
|
88
|
-
import { nativeIsRepo, nativeInit,
|
|
111
|
+
import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync, statSync } from "node:fs";
|
|
112
|
+
import { atomicWriteSync } from "./atomic-write.js";
|
|
113
|
+
import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit } from "./native-git-bridge.js";
|
|
89
114
|
import {
|
|
90
115
|
autoCommitCurrentBranch,
|
|
91
116
|
captureIntegrationBranch,
|
|
@@ -111,7 +136,7 @@ import {
|
|
|
111
136
|
} from "./auto-worktree.js";
|
|
112
137
|
import { pruneQueueOrder } from "./queue-order.js";
|
|
113
138
|
import { consumeSignal } from "./session-status-io.js";
|
|
114
|
-
import { showNextAction } from "../shared/
|
|
139
|
+
import { showNextAction } from "../shared/mod.js";
|
|
115
140
|
import { debugLog, debugTime, debugCount, debugPeak, enableDebug, isDebugEnabled, writeDebugSummary, getDebugLogPath } from "./debug-logger.js";
|
|
116
141
|
import {
|
|
117
142
|
resolveExpectedArtifactPath,
|
|
@@ -128,18 +153,6 @@ import {
|
|
|
128
153
|
reconcileMergeState,
|
|
129
154
|
} from "./auto-recovery.js";
|
|
130
155
|
import { resolveDispatch, resetRewriteCircuitBreaker } from "./auto-dispatch.js";
|
|
131
|
-
import {
|
|
132
|
-
buildResearchSlicePrompt,
|
|
133
|
-
buildResearchMilestonePrompt,
|
|
134
|
-
buildPlanSlicePrompt,
|
|
135
|
-
buildPlanMilestonePrompt,
|
|
136
|
-
buildExecuteTaskPrompt,
|
|
137
|
-
buildCompleteSlicePrompt,
|
|
138
|
-
buildCompleteMilestonePrompt,
|
|
139
|
-
buildReassessRoadmapPrompt,
|
|
140
|
-
buildRunUatPrompt,
|
|
141
|
-
buildReplanSlicePrompt,
|
|
142
|
-
} from "./auto-prompts.js";
|
|
143
156
|
import {
|
|
144
157
|
type AutoDashboardData,
|
|
145
158
|
updateProgressWidget as _updateProgressWidget,
|
|
@@ -147,7 +160,6 @@ import {
|
|
|
147
160
|
clearSliceProgressCache,
|
|
148
161
|
describeNextUnit as _describeNextUnit,
|
|
149
162
|
unitVerb,
|
|
150
|
-
unitPhaseLabel,
|
|
151
163
|
formatAutoElapsed as _formatAutoElapsed,
|
|
152
164
|
formatWidgetTokens,
|
|
153
165
|
hideFooter,
|
|
@@ -161,180 +173,49 @@ import {
|
|
|
161
173
|
import { isDbAvailable } from "./gsd-db.js";
|
|
162
174
|
import { hasPendingCaptures, loadPendingCaptures, countPendingCaptures } from "./captures.js";
|
|
163
175
|
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Sync milestone artifacts from project root INTO worktree before deriveState.
|
|
173
|
-
* Covers the case where the LLM wrote artifacts to the main repo filesystem
|
|
174
|
-
* (e.g. via absolute paths) but the worktree has stale data. Also deletes
|
|
175
|
-
* gsd.db in the worktree so it rebuilds from fresh disk state (#853).
|
|
176
|
-
* Non-fatal — sync failure should never block dispatch.
|
|
177
|
-
*/
|
|
178
|
-
function syncProjectRootToWorktree(projectRoot: string, worktreePath: string, milestoneId: string | null): void {
|
|
179
|
-
if (!worktreePath || !projectRoot || worktreePath === projectRoot) return;
|
|
180
|
-
if (!milestoneId) return;
|
|
181
|
-
|
|
182
|
-
const prGsd = join(projectRoot, ".gsd");
|
|
183
|
-
const wtGsd = join(worktreePath, ".gsd");
|
|
184
|
-
|
|
185
|
-
// Copy milestone directory from project root to worktree if the project root
|
|
186
|
-
// has newer artifacts (e.g. slices that don't exist in the worktree yet)
|
|
187
|
-
try {
|
|
188
|
-
const srcMilestone = join(prGsd, "milestones", milestoneId);
|
|
189
|
-
const dstMilestone = join(wtGsd, "milestones", milestoneId);
|
|
190
|
-
if (existsSync(srcMilestone)) {
|
|
191
|
-
mkdirSync(dstMilestone, { recursive: true });
|
|
192
|
-
cpSync(srcMilestone, dstMilestone, { recursive: true, force: false });
|
|
193
|
-
}
|
|
194
|
-
} catch { /* non-fatal */ }
|
|
195
|
-
|
|
196
|
-
// Delete worktree gsd.db so it rebuilds from the freshly synced files.
|
|
197
|
-
// Stale DB rows are the root cause of the infinite skip loop (#853).
|
|
198
|
-
try {
|
|
199
|
-
const wtDb = join(wtGsd, "gsd.db");
|
|
200
|
-
if (existsSync(wtDb)) {
|
|
201
|
-
unlinkSync(wtDb);
|
|
202
|
-
}
|
|
203
|
-
} catch { /* non-fatal */ }
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Sync dispatch-critical .gsd/ state files from worktree to project root.
|
|
208
|
-
* Only runs when inside an auto-worktree (worktreePath differs from projectRoot).
|
|
209
|
-
* Copies: STATE.md + active milestone directory (roadmap, slice plans, task summaries).
|
|
210
|
-
* Non-fatal — sync failure should never block dispatch.
|
|
211
|
-
*/
|
|
212
|
-
function syncStateToProjectRoot(worktreePath: string, projectRoot: string, milestoneId: string | null): void {
|
|
213
|
-
if (!worktreePath || !projectRoot || worktreePath === projectRoot) return;
|
|
214
|
-
if (!milestoneId) return;
|
|
215
|
-
|
|
216
|
-
const wtGsd = join(worktreePath, ".gsd");
|
|
217
|
-
const prGsd = join(projectRoot, ".gsd");
|
|
218
|
-
|
|
219
|
-
// 1. STATE.md — the quick-glance status used by initial deriveState()
|
|
220
|
-
try {
|
|
221
|
-
const src = join(wtGsd, "STATE.md");
|
|
222
|
-
const dst = join(prGsd, "STATE.md");
|
|
223
|
-
if (existsSync(src)) cpSync(src, dst, { force: true });
|
|
224
|
-
} catch { /* non-fatal */ }
|
|
225
|
-
|
|
226
|
-
// 2. Milestone directory — ROADMAP, slice PLANs, task summaries
|
|
227
|
-
// Copy the entire milestone .gsd subtree so deriveState reads current checkboxes
|
|
228
|
-
try {
|
|
229
|
-
const srcMilestone = join(wtGsd, "milestones", milestoneId);
|
|
230
|
-
const dstMilestone = join(prGsd, "milestones", milestoneId);
|
|
231
|
-
if (existsSync(srcMilestone)) {
|
|
232
|
-
mkdirSync(dstMilestone, { recursive: true });
|
|
233
|
-
cpSync(srcMilestone, dstMilestone, { recursive: true, force: true });
|
|
234
|
-
}
|
|
235
|
-
} catch { /* non-fatal */ }
|
|
236
|
-
|
|
237
|
-
// 3. Merge completed-units.json (set-union of both locations)
|
|
238
|
-
// Prevents already-completed units from being re-dispatched after crash/restart.
|
|
239
|
-
const srcKeysFile = join(wtGsd, "completed-units.json");
|
|
240
|
-
const dstKeysFile = join(prGsd, "completed-units.json");
|
|
241
|
-
if (existsSync(srcKeysFile)) {
|
|
242
|
-
try {
|
|
243
|
-
const srcKeys: string[] = JSON.parse(readFileSync(srcKeysFile, "utf8"));
|
|
244
|
-
let dstKeys: string[] = [];
|
|
245
|
-
if (existsSync(dstKeysFile)) {
|
|
246
|
-
try { dstKeys = JSON.parse(readFileSync(dstKeysFile, "utf8")); } catch { /* ignore corrupt dst */ }
|
|
247
|
-
}
|
|
248
|
-
const merged = [...new Set([...dstKeys, ...srcKeys])];
|
|
249
|
-
writeFileSync(dstKeysFile, JSON.stringify(merged, null, 2));
|
|
250
|
-
} catch { /* non-fatal */ }
|
|
251
|
-
}
|
|
176
|
+
// ── Extracted modules ──────────────────────────────────────────────────────
|
|
177
|
+
import { startUnitSupervision, type SupervisionContext } from "./auto-timers.js";
|
|
178
|
+
import { checkIdempotency, type IdempotencyContext } from "./auto-idempotency.js";
|
|
179
|
+
import { checkStuckAndRecover, type StuckContext } from "./auto-stuck-detection.js";
|
|
180
|
+
import { runPostUnitVerification, type VerificationContext } from "./auto-verification.js";
|
|
181
|
+
import { postUnitPreVerification, postUnitPostVerification, type PostUnitContext } from "./auto-post-unit.js";
|
|
182
|
+
import { bootstrapAutoSession, type BootstrapDeps } from "./auto-start.js";
|
|
252
183
|
|
|
253
|
-
|
|
254
|
-
// Without this, a crash during a unit leaves the runtime record only in the
|
|
255
|
-
// worktree. If the next session resolves basePath before worktree re-entry,
|
|
256
|
-
// selfHeal can't find or clear the stale record (#769).
|
|
257
|
-
try {
|
|
258
|
-
const srcRuntime = join(wtGsd, "runtime", "units");
|
|
259
|
-
const dstRuntime = join(prGsd, "runtime", "units");
|
|
260
|
-
if (existsSync(srcRuntime)) {
|
|
261
|
-
mkdirSync(dstRuntime, { recursive: true });
|
|
262
|
-
cpSync(srcRuntime, dstRuntime, { recursive: true, force: true });
|
|
263
|
-
}
|
|
264
|
-
} catch { /* non-fatal */ }
|
|
265
|
-
}
|
|
184
|
+
// Worktree sync, resource staleness, stale worktree escape → auto-worktree-sync.ts
|
|
266
185
|
|
|
267
|
-
// ─── State
|
|
268
|
-
|
|
269
|
-
let active = false;
|
|
270
|
-
let paused = false;
|
|
271
|
-
let stepMode = false;
|
|
272
|
-
let verbose = false;
|
|
273
|
-
let cmdCtx: ExtensionCommandContext | null = null;
|
|
274
|
-
let basePath = "";
|
|
275
|
-
let originalBasePath = "";
|
|
276
|
-
let gitService: GitServiceImpl | null = null;
|
|
277
|
-
|
|
278
|
-
/** Track total dispatches per unit to detect stuck loops (catches A→B→A→B patterns) */
|
|
279
|
-
const unitDispatchCount = new Map<string, number>();
|
|
280
|
-
const MAX_UNIT_DISPATCHES = 3;
|
|
281
|
-
/** Retry index at which a stub summary placeholder is written when the summary is still absent. */
|
|
282
|
-
const STUB_RECOVERY_THRESHOLD = 2;
|
|
283
|
-
/** Hard cap on total dispatches per unit across ALL reconciliation cycles.
|
|
284
|
-
* unitDispatchCount can be reset by loop-recovery/self-repair paths, but this
|
|
285
|
-
* counter is never reset — it catches infinite reconciliation loops where
|
|
286
|
-
* artifacts exist but deriveState keeps returning the same unit. */
|
|
287
|
-
const unitLifetimeDispatches = new Map<string, number>();
|
|
288
|
-
const MAX_LIFETIME_DISPATCHES = 6;
|
|
289
|
-
|
|
290
|
-
/** Tracks recovery attempt count per unit for backoff and diagnostics. */
|
|
291
|
-
const unitRecoveryCount = new Map<string, number>();
|
|
292
|
-
|
|
293
|
-
/** Track consecutive skips per unit — catches infinite skip loops where deriveState
|
|
294
|
-
* keeps returning the same already-completed unit. Reset on any real dispatch. */
|
|
295
|
-
const unitConsecutiveSkips = new Map<string, number>();
|
|
296
|
-
const MAX_CONSECUTIVE_SKIPS = 3;
|
|
297
|
-
|
|
298
|
-
/** Persisted completed-unit keys — survives restarts. Loaded from .gsd/completed-units.json. */
|
|
299
|
-
const completedKeySet = new Set<string>();
|
|
300
|
-
|
|
301
|
-
/** Resource version captured at auto-mode start. If the managed-resources
|
|
302
|
-
* manifest version changes mid-session (e.g. npm update -g gsd-pi),
|
|
303
|
-
* templates on disk may expect variables the in-memory code doesn't provide.
|
|
304
|
-
* Detect this and stop gracefully instead of crashing.
|
|
305
|
-
* Uses gsdVersion (semver) instead of syncedAt (timestamp) so that
|
|
306
|
-
* launching a second session doesn't falsely trigger staleness (#804). */
|
|
307
|
-
let resourceVersionOnStart: string | null = null;
|
|
308
|
-
|
|
309
|
-
function readResourceVersion(): string | null {
|
|
310
|
-
const agentDir = process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
|
|
311
|
-
const manifestPath = join(agentDir, "managed-resources.json");
|
|
312
|
-
try {
|
|
313
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
314
|
-
return typeof manifest?.gsdVersion === "string" ? manifest.gsdVersion : null;
|
|
315
|
-
} catch {
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
186
|
+
// ─── Session State ─────────────────────────────────────────────────────────
|
|
319
187
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
188
|
+
import {
|
|
189
|
+
AutoSession,
|
|
190
|
+
MAX_UNIT_DISPATCHES, STUB_RECOVERY_THRESHOLD, MAX_LIFETIME_DISPATCHES,
|
|
191
|
+
MAX_CONSECUTIVE_SKIPS, DISPATCH_GAP_TIMEOUT_MS, MAX_SKIP_DEPTH,
|
|
192
|
+
NEW_SESSION_TIMEOUT_MS, DISPATCH_HANG_TIMEOUT_MS,
|
|
193
|
+
} from "./auto/session.js";
|
|
194
|
+
import type { CompletedUnit, CurrentUnit, UnitRouting, StartModel, PendingVerificationRetry } from "./auto/session.js";
|
|
195
|
+
export {
|
|
196
|
+
MAX_UNIT_DISPATCHES, STUB_RECOVERY_THRESHOLD, MAX_LIFETIME_DISPATCHES,
|
|
197
|
+
MAX_CONSECUTIVE_SKIPS, DISPATCH_GAP_TIMEOUT_MS, MAX_SKIP_DEPTH,
|
|
198
|
+
NEW_SESSION_TIMEOUT_MS, DISPATCH_HANG_TIMEOUT_MS,
|
|
199
|
+
} from "./auto/session.js";
|
|
200
|
+
export type { CompletedUnit, CurrentUnit, UnitRouting, StartModel } from "./auto/session.js";
|
|
201
|
+
|
|
202
|
+
// ── ENCAPSULATION INVARIANT ─────────────────────────────────────────────────
|
|
203
|
+
// ALL mutable auto-mode state lives in the AutoSession class (auto/session.ts).
|
|
204
|
+
// This file must NOT declare module-level `let` or `var` variables for state.
|
|
205
|
+
// The single `s` instance below is the only mutable module-level binding.
|
|
206
|
+
//
|
|
207
|
+
// When adding features or fixing bugs:
|
|
208
|
+
// - New mutable state → add a property to AutoSession, not a module-level variable
|
|
209
|
+
// - New constants → module-level `const` is fine (immutable)
|
|
210
|
+
// - New state that needs reset on stopAuto → add to AutoSession.reset()
|
|
211
|
+
//
|
|
212
|
+
// Tests in auto-session-encapsulation.test.ts enforce this invariant.
|
|
213
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
214
|
+
const s = new AutoSession();
|
|
215
|
+
|
|
216
|
+
/** Throttle STATE.md rebuilds — at most once per 30 seconds */
|
|
217
|
+
const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
|
|
329
218
|
|
|
330
|
-
/**
|
|
331
|
-
* Resolve whether auto-mode should use worktree isolation.
|
|
332
|
-
* Returns true for worktree mode (default), false for branch and none modes.
|
|
333
|
-
* Branch mode works directly in the project root — useful for repos
|
|
334
|
-
* with git submodules where worktrees don't work well (#531).
|
|
335
|
-
* None mode skips all worktree and milestone-branch logic — commits
|
|
336
|
-
* land on the current branch with no isolation (#M001-S02).
|
|
337
|
-
*/
|
|
338
219
|
export function shouldUseWorktreeIsolation(): boolean {
|
|
339
220
|
const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
|
|
340
221
|
if (prefs?.isolation === "none") return false;
|
|
@@ -342,54 +223,19 @@ export function shouldUseWorktreeIsolation(): boolean {
|
|
|
342
223
|
return true; // default: worktree
|
|
343
224
|
}
|
|
344
225
|
|
|
345
|
-
/**
|
|
346
|
-
* Detect and escape a stale worktree cwd (#608).
|
|
347
|
-
*
|
|
348
|
-
* After milestone completion + merge, the worktree directory is removed but
|
|
349
|
-
* the process cwd may still point inside `.gsd/worktrees/<MID>/`.
|
|
350
|
-
* When a new session starts, `process.cwd()` is passed as `base` to startAuto
|
|
351
|
-
* and all subsequent writes land in the wrong directory. This function detects
|
|
352
|
-
* that scenario and chdir back to the project root.
|
|
353
|
-
*
|
|
354
|
-
* Returns the corrected base path.
|
|
355
|
-
*/
|
|
356
|
-
function escapeStaleWorktree(base: string): string {
|
|
357
|
-
const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
|
|
358
|
-
const idx = base.indexOf(marker);
|
|
359
|
-
if (idx === -1) return base;
|
|
360
|
-
|
|
361
|
-
// base is inside .gsd/worktrees/<something> — extract the project root
|
|
362
|
-
const projectRoot = base.slice(0, idx);
|
|
363
|
-
try {
|
|
364
|
-
process.chdir(projectRoot);
|
|
365
|
-
} catch {
|
|
366
|
-
// If chdir fails, return the original — caller will handle errors downstream
|
|
367
|
-
return base;
|
|
368
|
-
}
|
|
369
|
-
return projectRoot;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
226
|
/** Crash recovery prompt — set by startAuto, consumed by first dispatchNextUnit */
|
|
373
|
-
let pendingCrashRecovery: string | null = null;
|
|
374
227
|
|
|
375
228
|
/** Pending verification retry — set when gate fails with retries remaining, consumed by dispatchNextUnit */
|
|
376
|
-
|
|
377
|
-
/** Verification retry count per unitId — separate from unitDispatchCount which tracks artifact-missing retries */
|
|
378
|
-
const verificationRetryCount = new Map<string, number>();
|
|
229
|
+
|
|
230
|
+
/** Verification retry count per unitId — separate from s.unitDispatchCount which tracks artifact-missing retries */
|
|
379
231
|
|
|
380
232
|
/** Session file path captured at pause — used to synthesize recovery briefing on resume */
|
|
381
|
-
let pausedSessionFile: string | null = null;
|
|
382
233
|
|
|
383
234
|
/** Dashboard tracking */
|
|
384
|
-
let autoStartTime: number = 0;
|
|
385
|
-
let completedUnits: { type: string; id: string; startedAt: number; finishedAt: number }[] = [];
|
|
386
|
-
let currentUnit: { type: string; id: string; startedAt: number } | null = null;
|
|
387
235
|
|
|
388
236
|
/** Track dynamic routing decision for the current unit (for metrics) */
|
|
389
|
-
let currentUnitRouting: { tier: string; modelDowngraded: boolean } | null = null;
|
|
390
237
|
|
|
391
238
|
/** Queue of quick-task captures awaiting dispatch after triage resolution */
|
|
392
|
-
let pendingQuickTasks: import("./captures.js").CaptureEntry[] = [];
|
|
393
239
|
|
|
394
240
|
/**
|
|
395
241
|
* Model captured at auto-mode start. Used to prevent model bleed between
|
|
@@ -398,78 +244,42 @@ let pendingQuickTasks: import("./captures.js").CaptureEntry[] = [];
|
|
|
398
244
|
* the session's original model is re-applied instead of reading from
|
|
399
245
|
* the shared global settings (which another instance may have overwritten).
|
|
400
246
|
*/
|
|
401
|
-
let autoModeStartModel: { provider: string; id: string } | null = null;
|
|
402
247
|
|
|
403
248
|
/** Track current milestone to detect transitions */
|
|
404
|
-
let currentMilestoneId: string | null = null;
|
|
405
|
-
let lastBudgetAlertLevel: BudgetAlertLevel = 0;
|
|
406
249
|
|
|
407
250
|
/** Model the user had selected before auto-mode started */
|
|
408
|
-
let originalModelId: string | null = null;
|
|
409
|
-
let originalModelProvider: string | null = null;
|
|
410
251
|
|
|
411
252
|
/** Progress-aware timeout supervision */
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
let idleWatchdogHandle: ReturnType<typeof setInterval> | null = null;
|
|
253
|
+
|
|
254
|
+
/** Context-pressure continue-here monitor — fires once when context usage >= 70% */
|
|
415
255
|
|
|
416
256
|
/** Dispatch gap watchdog — detects when the state machine stalls between units.
|
|
417
257
|
* After handleAgentEnd completes, if auto-mode is still active but no new unit
|
|
418
258
|
* has been dispatched (sendMessage not called), this timer fires to force a
|
|
419
259
|
* re-evaluation. Covers the case where dispatchNextUnit silently fails or
|
|
420
260
|
* an unhandled error kills the dispatch chain. */
|
|
421
|
-
let dispatchGapHandle: ReturnType<typeof setTimeout> | null = null;
|
|
422
|
-
const DISPATCH_GAP_TIMEOUT_MS = 5_000; // 5 seconds
|
|
423
261
|
|
|
424
262
|
/** Prompt character measurement for token savings analysis (R051). */
|
|
425
|
-
let lastPromptCharCount: number | undefined;
|
|
426
|
-
let lastBaselineCharCount: number | undefined;
|
|
427
263
|
|
|
428
264
|
/** SIGTERM handler registered while auto-mode is active — cleared on stop/pause. */
|
|
429
|
-
let _sigtermHandler: (() => void) | null = null;
|
|
430
265
|
|
|
431
266
|
/**
|
|
432
267
|
* Tool calls currently being executed — prevents false idle detection during long-running tools.
|
|
433
268
|
* Maps toolCallId → start timestamp (ms) so the idle watchdog can detect tools that have been
|
|
434
269
|
* running suspiciously long (e.g., a Bash command hung because `&` kept stdout open).
|
|
435
270
|
*/
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
type BudgetAlertLevel = 0 | 75 | 80 | 90 | 100;
|
|
439
|
-
|
|
440
|
-
export function getBudgetAlertLevel(budgetPct: number): BudgetAlertLevel {
|
|
441
|
-
if (budgetPct >= 1.0) return 100;
|
|
442
|
-
if (budgetPct >= 0.90) return 90;
|
|
443
|
-
if (budgetPct >= 0.80) return 80;
|
|
444
|
-
if (budgetPct >= 0.75) return 75;
|
|
445
|
-
return 0;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
export function getNewBudgetAlertLevel(previousLevel: BudgetAlertLevel, budgetPct: number): BudgetAlertLevel | null {
|
|
449
|
-
const currentLevel = getBudgetAlertLevel(budgetPct);
|
|
450
|
-
if (currentLevel === 0 || currentLevel <= previousLevel) return null;
|
|
451
|
-
return currentLevel;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
export function getBudgetEnforcementAction(
|
|
455
|
-
enforcement: BudgetEnforcementMode,
|
|
456
|
-
budgetPct: number,
|
|
457
|
-
): "none" | "warn" | "pause" | "halt" {
|
|
458
|
-
if (budgetPct < 1.0) return "none";
|
|
459
|
-
if (enforcement === "halt") return "halt";
|
|
460
|
-
if (enforcement === "pause") return "pause";
|
|
461
|
-
return "warn";
|
|
462
|
-
}
|
|
271
|
+
// Re-export budget utilities for external consumers
|
|
272
|
+
export { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction } from "./auto-budget.js";
|
|
463
273
|
|
|
464
274
|
/** Wrapper: register SIGTERM handler and store reference. */
|
|
465
275
|
function registerSigtermHandler(currentBasePath: string): void {
|
|
466
|
-
|
|
276
|
+
s.sigtermHandler = _registerSigtermHandler(currentBasePath, s.sigtermHandler);
|
|
467
277
|
}
|
|
468
278
|
|
|
469
279
|
/** Wrapper: deregister SIGTERM handler and clear reference. */
|
|
470
280
|
function deregisterSigtermHandler(): void {
|
|
471
|
-
_deregisterSigtermHandler(
|
|
472
|
-
|
|
281
|
+
_deregisterSigtermHandler(s.sigtermHandler);
|
|
282
|
+
s.sigtermHandler = null;
|
|
473
283
|
}
|
|
474
284
|
|
|
475
285
|
export { type AutoDashboardData } from "./auto-dashboard.js";
|
|
@@ -480,21 +290,18 @@ export function getAutoDashboardData(): AutoDashboardData {
|
|
|
480
290
|
// Pending capture count — lazy check, non-fatal
|
|
481
291
|
let pendingCaptureCount = 0;
|
|
482
292
|
try {
|
|
483
|
-
if (basePath) {
|
|
484
|
-
pendingCaptureCount = countPendingCaptures(basePath);
|
|
293
|
+
if (s.basePath) {
|
|
294
|
+
pendingCaptureCount = countPendingCaptures(s.basePath);
|
|
485
295
|
}
|
|
486
296
|
} catch {
|
|
487
297
|
// Non-fatal — captures module may not be loaded
|
|
488
298
|
}
|
|
489
|
-
return {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
currentUnit: currentUnit ? { ...currentUnit } : null,
|
|
496
|
-
completedUnits: [...completedUnits],
|
|
497
|
-
basePath,
|
|
299
|
+
return { active: s.active, paused: s.paused,
|
|
300
|
+
stepMode: s.stepMode,
|
|
301
|
+
startTime: s.autoStartTime,
|
|
302
|
+
elapsed: (s.active || s.paused) ? Date.now() - s.autoStartTime : 0,
|
|
303
|
+
currentUnit: s.currentUnit ? { ...s.currentUnit } : null,
|
|
304
|
+
completedUnits: [...s.completedUnits], basePath: s.basePath,
|
|
498
305
|
totalCost: totals?.cost ?? 0,
|
|
499
306
|
totalTokens: totals?.tokens.total ?? 0,
|
|
500
307
|
pendingCaptureCount,
|
|
@@ -504,37 +311,33 @@ export function getAutoDashboardData(): AutoDashboardData {
|
|
|
504
311
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
505
312
|
|
|
506
313
|
export function isAutoActive(): boolean {
|
|
507
|
-
return active;
|
|
314
|
+
return s.active;
|
|
508
315
|
}
|
|
509
316
|
|
|
510
317
|
export function isAutoPaused(): boolean {
|
|
511
|
-
return paused;
|
|
318
|
+
return s.paused;
|
|
512
319
|
}
|
|
513
320
|
|
|
514
321
|
/**
|
|
515
|
-
*
|
|
516
|
-
*
|
|
322
|
+
* Return the model captured at auto-mode start for this session.
|
|
323
|
+
* Used by error-recovery to fall back to the session's own model
|
|
324
|
+
* instead of reading (potentially stale) preferences from disk (#1065).
|
|
517
325
|
*/
|
|
326
|
+
export function getAutoModeStartModel(): { provider: string; id: string } | null {
|
|
327
|
+
return s.autoModeStartModel;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Tool tracking — delegates to auto-tool-tracking.ts
|
|
518
331
|
export function markToolStart(toolCallId: string): void {
|
|
519
|
-
|
|
520
|
-
inFlightTools.set(toolCallId, Date.now());
|
|
332
|
+
_markToolStart(toolCallId, s.active);
|
|
521
333
|
}
|
|
522
334
|
|
|
523
|
-
/**
|
|
524
|
-
* Mark a tool execution as completed. Called from index.ts on tool_execution_end.
|
|
525
|
-
*/
|
|
526
335
|
export function markToolEnd(toolCallId: string): void {
|
|
527
|
-
|
|
336
|
+
_markToolEnd(toolCallId);
|
|
528
337
|
}
|
|
529
338
|
|
|
530
|
-
/**
|
|
531
|
-
* Returns the age (ms) of the oldest currently in-flight tool, or 0 if none.
|
|
532
|
-
* Exported for testing.
|
|
533
|
-
*/
|
|
534
339
|
export function getOldestInFlightToolAgeMs(): number {
|
|
535
|
-
|
|
536
|
-
const oldestStart = Math.min(...inFlightTools.values());
|
|
537
|
-
return Date.now() - oldestStart;
|
|
340
|
+
return _getOldestInFlightToolAgeMs();
|
|
538
341
|
}
|
|
539
342
|
|
|
540
343
|
/**
|
|
@@ -543,7 +346,7 @@ export function getOldestInFlightToolAgeMs(): number {
|
|
|
543
346
|
* a second terminal can discover and stop a running auto-mode session.
|
|
544
347
|
*/
|
|
545
348
|
function lockBase(): string {
|
|
546
|
-
return originalBasePath || basePath;
|
|
349
|
+
return s.originalBasePath || s.basePath;
|
|
547
350
|
}
|
|
548
351
|
|
|
549
352
|
/**
|
|
@@ -573,50 +376,63 @@ export function stopAutoRemote(projectRoot: string): { found: boolean; pid?: num
|
|
|
573
376
|
}
|
|
574
377
|
|
|
575
378
|
export function isStepMode(): boolean {
|
|
576
|
-
return stepMode;
|
|
379
|
+
return s.stepMode;
|
|
577
380
|
}
|
|
578
381
|
|
|
579
382
|
function clearUnitTimeout(): void {
|
|
580
|
-
if (unitTimeoutHandle) {
|
|
581
|
-
clearTimeout(unitTimeoutHandle);
|
|
582
|
-
unitTimeoutHandle = null;
|
|
383
|
+
if (s.unitTimeoutHandle) {
|
|
384
|
+
clearTimeout(s.unitTimeoutHandle);
|
|
385
|
+
s.unitTimeoutHandle = null;
|
|
583
386
|
}
|
|
584
|
-
if (wrapupWarningHandle) {
|
|
585
|
-
clearTimeout(wrapupWarningHandle);
|
|
586
|
-
wrapupWarningHandle = null;
|
|
387
|
+
if (s.wrapupWarningHandle) {
|
|
388
|
+
clearTimeout(s.wrapupWarningHandle);
|
|
389
|
+
s.wrapupWarningHandle = null;
|
|
587
390
|
}
|
|
588
|
-
if (idleWatchdogHandle) {
|
|
589
|
-
clearInterval(idleWatchdogHandle);
|
|
590
|
-
idleWatchdogHandle = null;
|
|
391
|
+
if (s.idleWatchdogHandle) {
|
|
392
|
+
clearInterval(s.idleWatchdogHandle);
|
|
393
|
+
s.idleWatchdogHandle = null;
|
|
591
394
|
}
|
|
592
|
-
|
|
395
|
+
if (s.continueHereHandle) {
|
|
396
|
+
clearInterval(s.continueHereHandle);
|
|
397
|
+
s.continueHereHandle = null;
|
|
398
|
+
}
|
|
399
|
+
clearInFlightTools();
|
|
593
400
|
clearDispatchGapWatchdog();
|
|
594
401
|
}
|
|
595
402
|
|
|
596
403
|
function clearDispatchGapWatchdog(): void {
|
|
597
|
-
if (dispatchGapHandle) {
|
|
598
|
-
clearTimeout(dispatchGapHandle);
|
|
599
|
-
dispatchGapHandle = null;
|
|
404
|
+
if (s.dispatchGapHandle) {
|
|
405
|
+
clearTimeout(s.dispatchGapHandle);
|
|
406
|
+
s.dispatchGapHandle = null;
|
|
600
407
|
}
|
|
601
408
|
}
|
|
602
409
|
|
|
410
|
+
/** Build snapshot metric opts, enriching with continueHereFired from the runtime record. */
|
|
411
|
+
function buildSnapshotOpts(unitType: string, unitId: string): { continueHereFired?: boolean; promptCharCount?: number; baselineCharCount?: number } & Record<string, unknown> {
|
|
412
|
+
const runtime = s.currentUnit ? readUnitRuntimeRecord(s.basePath, unitType, unitId) : null;
|
|
413
|
+
return {
|
|
414
|
+
promptCharCount: s.lastPromptCharCount,
|
|
415
|
+
baselineCharCount: s.lastBaselineCharCount,
|
|
416
|
+
...(s.currentUnitRouting ?? {}),
|
|
417
|
+
...(runtime?.continueHereFired ? { continueHereFired: true } : {}),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
603
421
|
/**
|
|
604
422
|
* Start a watchdog that fires if no new unit is dispatched within DISPATCH_GAP_TIMEOUT_MS
|
|
605
423
|
* after handleAgentEnd completes. This catches the case where the dispatch chain silently
|
|
606
|
-
* breaks (e.g., unhandled exception in dispatchNextUnit) and auto-mode is left active but idle.
|
|
424
|
+
* breaks (e.g., unhandled exception in dispatchNextUnit) and auto-mode is left s.active but idle.
|
|
607
425
|
*
|
|
608
426
|
* The watchdog is cleared on the next successful unit dispatch (clearUnitTimeout is called
|
|
609
427
|
* at the start of handleAgentEnd, which calls clearDispatchGapWatchdog).
|
|
610
428
|
*/
|
|
611
429
|
function startDispatchGapWatchdog(ctx: ExtensionContext, pi: ExtensionAPI): void {
|
|
612
430
|
clearDispatchGapWatchdog();
|
|
613
|
-
dispatchGapHandle = setTimeout(async () => {
|
|
614
|
-
dispatchGapHandle = null;
|
|
615
|
-
if (!active || !cmdCtx) return;
|
|
431
|
+
s.dispatchGapHandle = setTimeout(async () => {
|
|
432
|
+
s.dispatchGapHandle = null;
|
|
433
|
+
if (!s.active || !s.cmdCtx) return;
|
|
616
434
|
|
|
617
|
-
|
|
618
|
-
// Re-derive state and attempt a fresh dispatch.
|
|
619
|
-
if (verbose) {
|
|
435
|
+
if (s.verbose) {
|
|
620
436
|
ctx.ui.notify(
|
|
621
437
|
"Dispatch gap detected — re-evaluating state.",
|
|
622
438
|
"info",
|
|
@@ -631,39 +447,35 @@ function startDispatchGapWatchdog(ctx: ExtensionContext, pi: ExtensionAPI): void
|
|
|
631
447
|
return;
|
|
632
448
|
}
|
|
633
449
|
|
|
634
|
-
|
|
635
|
-
// (no sendMessage called → no timeout set), auto-mode is permanently
|
|
636
|
-
// stalled. Stop cleanly instead of leaving it active but idle (#537).
|
|
637
|
-
if (active && !unitTimeoutHandle && !wrapupWarningHandle) {
|
|
450
|
+
if (s.active && !s.unitTimeoutHandle && !s.wrapupWarningHandle) {
|
|
638
451
|
await stopAuto(ctx, pi, "Stalled — no dispatchable unit after retry");
|
|
639
452
|
}
|
|
640
453
|
}, DISPATCH_GAP_TIMEOUT_MS);
|
|
641
454
|
}
|
|
642
455
|
|
|
643
456
|
export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason?: string): Promise<void> {
|
|
644
|
-
if (!active && !paused) return;
|
|
457
|
+
if (!s.active && !s.paused) return;
|
|
645
458
|
const reasonSuffix = reason ? ` — ${reason}` : "";
|
|
646
459
|
clearUnitTimeout();
|
|
647
|
-
if (lockBase())
|
|
460
|
+
if (lockBase()) {
|
|
461
|
+
releaseSessionLock(lockBase());
|
|
462
|
+
clearLock(lockBase());
|
|
463
|
+
}
|
|
648
464
|
clearSkillSnapshot();
|
|
649
465
|
resetSkillTelemetry();
|
|
650
|
-
|
|
651
|
-
|
|
466
|
+
s.dispatching = false;
|
|
467
|
+
s.skipDepth = 0;
|
|
652
468
|
|
|
653
469
|
// Remove SIGTERM handler registered at auto-mode start
|
|
654
470
|
deregisterSigtermHandler();
|
|
655
471
|
|
|
656
|
-
// ── Auto-worktree: exit worktree and reset basePath on stop ──
|
|
657
|
-
|
|
658
|
-
// where it left off. The branch is only deleted during milestone
|
|
659
|
-
// completion (mergeMilestoneToMain) after the work has been squash-merged.
|
|
660
|
-
if (currentMilestoneId && isInAutoWorktree(basePath)) {
|
|
472
|
+
// ── Auto-worktree: exit worktree and reset s.basePath on stop ──
|
|
473
|
+
if (s.currentMilestoneId && isInAutoWorktree(s.basePath)) {
|
|
661
474
|
try {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
475
|
+
try { autoCommitCurrentBranch(s.basePath, "stop", s.currentMilestoneId); } catch (e) { debugLog("stop-auto-commit-failed", { error: e instanceof Error ? e.message : String(e) }); }
|
|
476
|
+
teardownAutoWorktree(s.originalBasePath, s.currentMilestoneId, { preserveBranch: true });
|
|
477
|
+
s.basePath = s.originalBasePath;
|
|
478
|
+
s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
667
479
|
ctx?.ui.notify("Exited auto-worktree (branch preserved for resume).", "info");
|
|
668
480
|
} catch (err) {
|
|
669
481
|
ctx?.ui.notify(
|
|
@@ -678,16 +490,12 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
|
|
|
678
490
|
try {
|
|
679
491
|
const { closeDatabase } = await import("./gsd-db.js");
|
|
680
492
|
closeDatabase();
|
|
681
|
-
} catch {
|
|
493
|
+
} catch (e) { debugLog("db-close-failed", { error: e instanceof Error ? e.message : String(e) }); }
|
|
682
494
|
}
|
|
683
495
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
// the worktree directory. Force it back to originalBasePath.
|
|
688
|
-
if (originalBasePath) {
|
|
689
|
-
basePath = originalBasePath;
|
|
690
|
-
try { process.chdir(basePath); } catch { /* best-effort */ }
|
|
496
|
+
if (s.originalBasePath) {
|
|
497
|
+
s.basePath = s.originalBasePath;
|
|
498
|
+
try { process.chdir(s.basePath); } catch { /* best-effort */ }
|
|
691
499
|
}
|
|
692
500
|
|
|
693
501
|
const ledger = getLedger();
|
|
@@ -701,12 +509,10 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
|
|
|
701
509
|
ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}.`, "info");
|
|
702
510
|
}
|
|
703
511
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
try { await rebuildState(basePath); } catch { /* non-fatal */ }
|
|
512
|
+
if (s.basePath) {
|
|
513
|
+
try { await rebuildState(s.basePath); } catch (e) { debugLog("stop-rebuild-state-failed", { error: e instanceof Error ? e.message : String(e) }); }
|
|
707
514
|
}
|
|
708
515
|
|
|
709
|
-
// Write debug summary before resetting state
|
|
710
516
|
if (isDebugEnabled()) {
|
|
711
517
|
const logPath = writeDebugSummary();
|
|
712
518
|
if (logPath) {
|
|
@@ -717,43 +523,44 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
|
|
|
717
523
|
resetMetrics();
|
|
718
524
|
resetRoutingHistory();
|
|
719
525
|
resetHookState();
|
|
720
|
-
if (basePath) clearPersistedHookState(basePath);
|
|
721
|
-
active = false;
|
|
722
|
-
paused = false;
|
|
723
|
-
stepMode = false;
|
|
724
|
-
unitDispatchCount.clear();
|
|
725
|
-
unitRecoveryCount.clear();
|
|
726
|
-
unitConsecutiveSkips.clear();
|
|
727
|
-
|
|
728
|
-
lastBudgetAlertLevel = 0;
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
526
|
+
if (s.basePath) clearPersistedHookState(s.basePath);
|
|
527
|
+
s.active = false;
|
|
528
|
+
s.paused = false;
|
|
529
|
+
s.stepMode = false;
|
|
530
|
+
s.unitDispatchCount.clear();
|
|
531
|
+
s.unitRecoveryCount.clear();
|
|
532
|
+
s.unitConsecutiveSkips.clear();
|
|
533
|
+
clearInFlightTools();
|
|
534
|
+
s.lastBudgetAlertLevel = 0;
|
|
535
|
+
s.lastStateRebuildAt = 0;
|
|
536
|
+
s.unitLifetimeDispatches.clear();
|
|
537
|
+
s.currentUnit = null;
|
|
538
|
+
s.autoModeStartModel = null;
|
|
539
|
+
s.currentMilestoneId = null;
|
|
540
|
+
s.originalBasePath = "";
|
|
541
|
+
s.completedUnits = [];
|
|
542
|
+
s.pendingQuickTasks = [];
|
|
736
543
|
clearSliceProgressCache();
|
|
737
544
|
clearActivityLogState();
|
|
738
545
|
resetProactiveHealing();
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
546
|
+
s.recentlyEvictedKeys.clear();
|
|
547
|
+
s.pendingCrashRecovery = null;
|
|
548
|
+
s.pendingVerificationRetry = null;
|
|
549
|
+
s.verificationRetryCount.clear();
|
|
550
|
+
s.pausedSessionFile = null;
|
|
551
|
+
s.handlingAgentEnd = false;
|
|
744
552
|
ctx?.ui.setStatus("gsd-auto", undefined);
|
|
745
553
|
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
746
554
|
ctx?.ui.setFooter(undefined);
|
|
747
555
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const original = ctx.modelRegistry.find(originalModelProvider, originalModelId);
|
|
556
|
+
if (pi && ctx && s.originalModelId && s.originalModelProvider) {
|
|
557
|
+
const original = ctx.modelRegistry.find(s.originalModelProvider, s.originalModelId);
|
|
751
558
|
if (original) await pi.setModel(original);
|
|
752
|
-
originalModelId = null;
|
|
753
|
-
originalModelProvider = null;
|
|
559
|
+
s.originalModelId = null;
|
|
560
|
+
s.originalModelProvider = null;
|
|
754
561
|
}
|
|
755
562
|
|
|
756
|
-
cmdCtx = null;
|
|
563
|
+
s.cmdCtx = null;
|
|
757
564
|
}
|
|
758
565
|
|
|
759
566
|
/**
|
|
@@ -762,31 +569,28 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
|
|
|
762
569
|
* from disk state. Called when the user presses Escape during auto-mode.
|
|
763
570
|
*/
|
|
764
571
|
export async function pauseAuto(ctx?: ExtensionContext, _pi?: ExtensionAPI): Promise<void> {
|
|
765
|
-
if (!active) return;
|
|
572
|
+
if (!s.active) return;
|
|
766
573
|
clearUnitTimeout();
|
|
767
574
|
|
|
768
|
-
|
|
769
|
-
// recovery briefing on resume so the next agent knows what already happened.
|
|
770
|
-
pausedSessionFile = ctx?.sessionManager?.getSessionFile() ?? null;
|
|
575
|
+
s.pausedSessionFile = ctx?.sessionManager?.getSessionFile() ?? null;
|
|
771
576
|
|
|
772
|
-
if (lockBase())
|
|
577
|
+
if (lockBase()) {
|
|
578
|
+
releaseSessionLock(lockBase());
|
|
579
|
+
clearLock(lockBase());
|
|
580
|
+
}
|
|
773
581
|
|
|
774
|
-
// Remove SIGTERM handler registered at auto-mode start
|
|
775
582
|
deregisterSigtermHandler();
|
|
776
583
|
|
|
777
|
-
active = false;
|
|
778
|
-
paused = true;
|
|
779
|
-
pendingVerificationRetry = null;
|
|
780
|
-
verificationRetryCount.clear();
|
|
781
|
-
// Preserve: unitDispatchCount, currentUnit, basePath, verbose, cmdCtx,
|
|
782
|
-
// completedUnits, autoStartTime, currentMilestoneId, originalModelId
|
|
783
|
-
// — all needed for resume and dashboard display
|
|
584
|
+
s.active = false;
|
|
585
|
+
s.paused = true;
|
|
586
|
+
s.pendingVerificationRetry = null;
|
|
587
|
+
s.verificationRetryCount.clear();
|
|
784
588
|
ctx?.ui.setStatus("gsd-auto", "paused");
|
|
785
589
|
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
786
590
|
ctx?.ui.setFooter(undefined);
|
|
787
|
-
const resumeCmd = stepMode ? "/gsd next" : "/gsd auto";
|
|
591
|
+
const resumeCmd = s.stepMode ? "/gsd next" : "/gsd auto";
|
|
788
592
|
ctx?.ui.notify(
|
|
789
|
-
`${stepMode ? "Step" : "Auto"}-mode paused (Escape). Type to interact, or ${resumeCmd} to resume.`,
|
|
593
|
+
`${s.stepMode ? "Step" : "Auto"}-mode paused (Escape). Type to interact, or ${resumeCmd} to resume.`,
|
|
790
594
|
"info",
|
|
791
595
|
);
|
|
792
596
|
}
|
|
@@ -802,44 +606,45 @@ export async function startAuto(
|
|
|
802
606
|
const requestedStepMode = options?.step ?? false;
|
|
803
607
|
|
|
804
608
|
// Escape stale worktree cwd from a previous milestone (#608).
|
|
805
|
-
// After milestone merge + worktree removal, the process cwd may still point
|
|
806
|
-
// inside .gsd/worktrees/<MID>/ — detect and chdir back to project root.
|
|
807
609
|
base = escapeStaleWorktree(base);
|
|
808
610
|
|
|
809
611
|
// If resuming from paused state, just re-activate and dispatch next unit.
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
612
|
+
if (s.paused) {
|
|
613
|
+
// Re-acquire session lock before resuming
|
|
614
|
+
const resumeLock = acquireSessionLock(base);
|
|
615
|
+
if (!resumeLock.acquired) {
|
|
616
|
+
ctx.ui.notify(
|
|
617
|
+
`Cannot resume: ${resumeLock.reason}`,
|
|
618
|
+
"error",
|
|
619
|
+
);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
s.paused = false;
|
|
624
|
+
s.active = true;
|
|
625
|
+
s.verbose = verboseMode;
|
|
626
|
+
s.stepMode = requestedStepMode;
|
|
627
|
+
s.cmdCtx = ctx;
|
|
628
|
+
s.basePath = base;
|
|
629
|
+
s.unitDispatchCount.clear();
|
|
630
|
+
s.unitLifetimeDispatches.clear();
|
|
631
|
+
s.unitConsecutiveSkips.clear();
|
|
823
632
|
if (!getLedger()) initMetrics(base);
|
|
824
|
-
|
|
825
|
-
if (currentMilestoneId) setActiveMilestoneId(base, currentMilestoneId);
|
|
633
|
+
if (s.currentMilestoneId) setActiveMilestoneId(base, s.currentMilestoneId);
|
|
826
634
|
|
|
827
|
-
// ── Auto-worktree: re-enter worktree on resume
|
|
828
|
-
|
|
829
|
-
// Skip entirely in branch or none isolation mode (#531).
|
|
830
|
-
if (currentMilestoneId && shouldUseWorktreeIsolation() && originalBasePath && !isInAutoWorktree(basePath) && !detectWorktreeName(basePath) && !detectWorktreeName(originalBasePath)) {
|
|
635
|
+
// ── Auto-worktree: re-enter worktree on resume ──
|
|
636
|
+
if (s.currentMilestoneId && shouldUseWorktreeIsolation() && s.originalBasePath && !isInAutoWorktree(s.basePath) && !detectWorktreeName(s.basePath) && !detectWorktreeName(s.originalBasePath)) {
|
|
831
637
|
try {
|
|
832
|
-
const existingWtPath = getAutoWorktreePath(originalBasePath, currentMilestoneId);
|
|
638
|
+
const existingWtPath = getAutoWorktreePath(s.originalBasePath, s.currentMilestoneId);
|
|
833
639
|
if (existingWtPath) {
|
|
834
|
-
const wtPath = enterAutoWorktree(originalBasePath, currentMilestoneId);
|
|
835
|
-
basePath = wtPath;
|
|
836
|
-
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
640
|
+
const wtPath = enterAutoWorktree(s.originalBasePath, s.currentMilestoneId);
|
|
641
|
+
s.basePath = wtPath;
|
|
642
|
+
s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
837
643
|
ctx.ui.notify(`Re-entered auto-worktree at ${wtPath}`, "info");
|
|
838
644
|
} else {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
645
|
+
const wtPath = createAutoWorktree(s.originalBasePath, s.currentMilestoneId);
|
|
646
|
+
s.basePath = wtPath;
|
|
647
|
+
s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
843
648
|
ctx.ui.notify(`Recreated auto-worktree at ${wtPath}`, "info");
|
|
844
649
|
}
|
|
845
650
|
} catch (err) {
|
|
@@ -850,1276 +655,243 @@ export async function startAuto(
|
|
|
850
655
|
}
|
|
851
656
|
}
|
|
852
657
|
|
|
853
|
-
// Re-register SIGTERM handler for the resumed session (use original base for lock)
|
|
854
658
|
registerSigtermHandler(lockBase());
|
|
855
659
|
|
|
856
|
-
ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
|
|
660
|
+
ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
|
|
857
661
|
ctx.ui.setFooter(hideFooter);
|
|
858
|
-
ctx.ui.notify(stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
// Rebuild disk state before resuming — user interaction during pause may have changed files
|
|
862
|
-
try { await rebuildState(basePath); } catch { /* non-fatal */ }
|
|
662
|
+
ctx.ui.notify(s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
|
|
663
|
+
restoreHookState(s.basePath);
|
|
664
|
+
try { await rebuildState(s.basePath); } catch (e) { debugLog("resume-rebuild-state-failed", { error: e instanceof Error ? e.message : String(e) }); }
|
|
863
665
|
try {
|
|
864
|
-
const report = await runGSDDoctor(basePath, { fix: true });
|
|
666
|
+
const report = await runGSDDoctor(s.basePath, { fix: true });
|
|
865
667
|
if (report.fixesApplied.length > 0) {
|
|
866
668
|
ctx.ui.notify(`Resume: applied ${report.fixesApplied.length} fix(es) to state.`, "info");
|
|
867
669
|
}
|
|
868
|
-
} catch {
|
|
869
|
-
|
|
870
|
-
await selfHealRuntimeRecords(basePath, ctx, completedKeySet);
|
|
670
|
+
} catch (e) { debugLog("resume-doctor-failed", { error: e instanceof Error ? e.message : String(e) }); }
|
|
671
|
+
await selfHealRuntimeRecords(s.basePath, ctx, s.completedKeySet);
|
|
871
672
|
invalidateAllCaches();
|
|
872
673
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
if (pausedSessionFile) {
|
|
876
|
-
const activityDir = join(gsdRoot(basePath), "activity");
|
|
674
|
+
if (s.pausedSessionFile) {
|
|
675
|
+
const activityDir = join(gsdRoot(s.basePath), "activity");
|
|
877
676
|
const recovery = synthesizeCrashRecovery(
|
|
878
|
-
basePath,
|
|
879
|
-
currentUnit?.type ?? "unknown",
|
|
880
|
-
currentUnit?.id ?? "unknown",
|
|
881
|
-
pausedSessionFile,
|
|
677
|
+
s.basePath,
|
|
678
|
+
s.currentUnit?.type ?? "unknown",
|
|
679
|
+
s.currentUnit?.id ?? "unknown", s.pausedSessionFile ?? undefined,
|
|
882
680
|
activityDir,
|
|
883
681
|
);
|
|
884
682
|
if (recovery && recovery.trace.toolCallCount > 0) {
|
|
885
|
-
pendingCrashRecovery = recovery.prompt;
|
|
683
|
+
s.pendingCrashRecovery = recovery.prompt;
|
|
886
684
|
ctx.ui.notify(
|
|
887
685
|
`Recovered ${recovery.trace.toolCallCount} tool calls from paused session. Resuming with context.`,
|
|
888
686
|
"info",
|
|
889
687
|
);
|
|
890
688
|
}
|
|
891
|
-
pausedSessionFile = null;
|
|
689
|
+
s.pausedSessionFile = null;
|
|
892
690
|
}
|
|
893
691
|
|
|
894
|
-
//
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
await dispatchNextUnit(ctx, pi);
|
|
898
|
-
return;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
// Ensure git repo exists — GSD needs it for commits and state tracking
|
|
902
|
-
if (!nativeIsRepo(base)) {
|
|
903
|
-
const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
|
|
904
|
-
nativeInit(base, mainBranch);
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
// Ensure .gitignore has baseline patterns
|
|
908
|
-
const commitDocs = loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs;
|
|
909
|
-
ensureGitignore(base, { commitDocs });
|
|
910
|
-
untrackRuntimeFiles(base);
|
|
911
|
-
|
|
912
|
-
// Bootstrap .gsd/ if it doesn't exist
|
|
913
|
-
const gsdDir = join(base, ".gsd");
|
|
914
|
-
if (!existsSync(gsdDir)) {
|
|
915
|
-
mkdirSync(join(gsdDir, "milestones"), { recursive: true });
|
|
916
|
-
// Only commit .gsd/ init when commit_docs is not explicitly false
|
|
917
|
-
if (commitDocs !== false) {
|
|
692
|
+
// If resuming from a secrets pause, re-collect before dispatching (#1146)
|
|
693
|
+
if (s.pausedForSecrets && s.currentMilestoneId) {
|
|
918
694
|
try {
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
// Stale lock from a dead process — validate before synthesizing recovery context.
|
|
941
|
-
// If the recovered unit belongs to a fully-completed milestone (SUMMARY exists),
|
|
942
|
-
// discard recovery context to prevent phantom skip loops (#790).
|
|
943
|
-
const recoveredMid = crashLock.unitId.split("/")[0];
|
|
944
|
-
const milestoneAlreadyComplete = recoveredMid
|
|
945
|
-
? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
|
|
946
|
-
: false;
|
|
947
|
-
|
|
948
|
-
if (milestoneAlreadyComplete) {
|
|
949
|
-
ctx.ui.notify(
|
|
950
|
-
`Crash recovery: discarding stale context for ${crashLock.unitId} — milestone ${recoveredMid} is already complete.`,
|
|
951
|
-
"info",
|
|
952
|
-
);
|
|
953
|
-
} else {
|
|
954
|
-
const activityDir = join(gsdRoot(base), "activity");
|
|
955
|
-
const recovery = synthesizeCrashRecovery(
|
|
956
|
-
base, crashLock.unitType, crashLock.unitId,
|
|
957
|
-
crashLock.sessionFile, activityDir,
|
|
958
|
-
);
|
|
959
|
-
if (recovery && recovery.trace.toolCallCount > 0) {
|
|
960
|
-
pendingCrashRecovery = recovery.prompt;
|
|
961
|
-
ctx.ui.notify(
|
|
962
|
-
`${formatCrashInfo(crashLock)}\nRecovered ${recovery.trace.toolCallCount} tool calls from crashed session. Resuming with full context.`,
|
|
963
|
-
"warning",
|
|
964
|
-
);
|
|
965
|
-
} else {
|
|
695
|
+
const manifestStatus = await getManifestStatus(s.basePath, s.currentMilestoneId);
|
|
696
|
+
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
697
|
+
const result = await collectSecretsFromManifest(s.basePath, s.currentMilestoneId, ctx);
|
|
698
|
+
if (result && result.applied.length > 0) {
|
|
699
|
+
ctx.ui.notify(
|
|
700
|
+
`Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`,
|
|
701
|
+
"info",
|
|
702
|
+
);
|
|
703
|
+
} else if (result && result.applied.length === 0 && result.skipped.length > 0) {
|
|
704
|
+
// All keys were skipped — still pending, re-pause
|
|
705
|
+
s.paused = true;
|
|
706
|
+
s.active = false;
|
|
707
|
+
ctx.ui.notify(
|
|
708
|
+
`All env variables were skipped. Auto-mode remains paused.\nCollect them with /gsd secrets, then resume with /gsd auto.`,
|
|
709
|
+
"warning",
|
|
710
|
+
);
|
|
711
|
+
ctx.ui.setStatus("gsd-auto", "paused");
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
} catch (err) {
|
|
966
716
|
ctx.ui.notify(
|
|
967
|
-
|
|
717
|
+
`Secrets check error: ${err instanceof Error ? err.message : String(err)}. Continuing without secrets.`,
|
|
968
718
|
"warning",
|
|
969
719
|
);
|
|
970
720
|
}
|
|
721
|
+
s.pausedForSecrets = false;
|
|
971
722
|
}
|
|
972
|
-
clearLock(base);
|
|
973
|
-
}
|
|
974
723
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
enableDebug(base);
|
|
978
|
-
}
|
|
979
|
-
if (isDebugEnabled()) {
|
|
980
|
-
const { isNativeParserAvailable } = await import("./native-parser-bridge.js");
|
|
981
|
-
debugLog("debug-start", {
|
|
982
|
-
platform: process.platform,
|
|
983
|
-
arch: process.arch,
|
|
984
|
-
node: process.version,
|
|
985
|
-
model: ctx.model?.id ?? "unknown",
|
|
986
|
-
provider: ctx.model?.provider ?? "unknown",
|
|
987
|
-
nativeParser: isNativeParserAvailable(),
|
|
988
|
-
cwd: base,
|
|
989
|
-
});
|
|
990
|
-
ctx.ui.notify(`Debug logging enabled → ${getDebugLogPath()}`, "info");
|
|
991
|
-
}
|
|
724
|
+
updateSessionLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
|
|
725
|
+
writeLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
|
|
992
726
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
// after a discussion that wrote new artifacts) may cause deriveState to
|
|
996
|
-
// return pre-planning when the roadmap already exists (#800).
|
|
997
|
-
invalidateAllCaches();
|
|
998
|
-
let state = await deriveState(base);
|
|
999
|
-
|
|
1000
|
-
// ── Stale worktree state recovery (#654) ─────────────────────────────────
|
|
1001
|
-
// When auto-mode was previously stopped and restarted, the project root's
|
|
1002
|
-
// .gsd/ directory may have stale metadata (completed units showing as
|
|
1003
|
-
// incomplete). If an auto-worktree exists for the active milestone, it has
|
|
1004
|
-
// the current state — re-derive from there to avoid re-dispatching
|
|
1005
|
-
// finished work.
|
|
1006
|
-
if (
|
|
1007
|
-
state.activeMilestone &&
|
|
1008
|
-
shouldUseWorktreeIsolation() &&
|
|
1009
|
-
!detectWorktreeName(base)
|
|
1010
|
-
) {
|
|
1011
|
-
const wtPath = getAutoWorktreePath(base, state.activeMilestone.id);
|
|
1012
|
-
if (wtPath) {
|
|
1013
|
-
state = await deriveState(wtPath);
|
|
1014
|
-
}
|
|
727
|
+
await dispatchNextUnit(ctx, pi);
|
|
728
|
+
return;
|
|
1015
729
|
}
|
|
1016
730
|
|
|
1017
|
-
// ──
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
// handle it correctly from the branch's state.
|
|
1024
|
-
let hasSurvivorBranch = false;
|
|
1025
|
-
if (
|
|
1026
|
-
state.activeMilestone &&
|
|
1027
|
-
(state.phase === "pre-planning" || state.phase === "needs-discussion") &&
|
|
1028
|
-
shouldUseWorktreeIsolation() &&
|
|
1029
|
-
!detectWorktreeName(base) &&
|
|
1030
|
-
!base.includes(`${pathSep}.gsd${pathSep}worktrees${pathSep}`)
|
|
1031
|
-
) {
|
|
1032
|
-
const milestoneBranch = `milestone/${state.activeMilestone.id}`;
|
|
1033
|
-
const { nativeBranchExists } = await import("./native-git-bridge.js");
|
|
1034
|
-
hasSurvivorBranch = nativeBranchExists(base, milestoneBranch);
|
|
1035
|
-
if (hasSurvivorBranch) {
|
|
1036
|
-
ctx.ui.notify(
|
|
1037
|
-
`Found prior session branch ${milestoneBranch}. Resuming.`,
|
|
1038
|
-
"info",
|
|
1039
|
-
);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
731
|
+
// ── Fresh start path — delegated to auto-start.ts ──
|
|
732
|
+
const bootstrapDeps: BootstrapDeps = {
|
|
733
|
+
shouldUseWorktreeIsolation,
|
|
734
|
+
registerSigtermHandler,
|
|
735
|
+
lockBase,
|
|
736
|
+
};
|
|
1042
737
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
// After discussion completes, checkAutoStartAfterDiscuss() (fired from
|
|
1046
|
-
// agent_end) will detect the new CONTEXT.md and restart auto mode.
|
|
1047
|
-
// If the LLM didn't follow the discussion protocol (e.g. started editing
|
|
1048
|
-
// files directly for a simple task), we re-derive state and either proceed
|
|
1049
|
-
// with what was created or notify the user clearly (#609).
|
|
1050
|
-
if (!state.activeMilestone || state.phase === "complete") {
|
|
1051
|
-
const { showSmartEntry } = await import("./guided-flow.js");
|
|
1052
|
-
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
1053
|
-
|
|
1054
|
-
// Re-derive state after discussion — the LLM may have created artifacts
|
|
1055
|
-
// even if it didn't follow the full protocol.
|
|
1056
|
-
invalidateAllCaches();
|
|
1057
|
-
const postState = await deriveState(base);
|
|
1058
|
-
if (postState.activeMilestone && postState.phase !== "complete" && postState.phase !== "pre-planning") {
|
|
1059
|
-
state = postState;
|
|
1060
|
-
} else if (postState.activeMilestone && postState.phase === "pre-planning") {
|
|
1061
|
-
const contextFile = resolveMilestoneFile(base, postState.activeMilestone.id, "CONTEXT");
|
|
1062
|
-
const hasContext = !!(contextFile && await loadFile(contextFile));
|
|
1063
|
-
if (hasContext) {
|
|
1064
|
-
state = postState;
|
|
1065
|
-
} else {
|
|
1066
|
-
ctx.ui.notify(
|
|
1067
|
-
"Discussion completed but no milestone context was written. Run /gsd to try the discussion again, or /gsd auto after creating the milestone manually.",
|
|
1068
|
-
"warning",
|
|
1069
|
-
);
|
|
1070
|
-
return;
|
|
1071
|
-
}
|
|
1072
|
-
} else {
|
|
1073
|
-
return;
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
738
|
+
const ready = await bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, bootstrapDeps);
|
|
739
|
+
if (!ready) return;
|
|
1076
740
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
if (state.phase === "pre-planning") {
|
|
1081
|
-
const mid = state.activeMilestone!.id;
|
|
1082
|
-
const contextFile = resolveMilestoneFile(base, mid, "CONTEXT");
|
|
1083
|
-
const hasContext = !!(contextFile && await loadFile(contextFile));
|
|
1084
|
-
if (!hasContext) {
|
|
1085
|
-
const { showSmartEntry } = await import("./guided-flow.js");
|
|
1086
|
-
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
1087
|
-
|
|
1088
|
-
// Same re-derive pattern as above
|
|
1089
|
-
invalidateAllCaches();
|
|
1090
|
-
const postState = await deriveState(base);
|
|
1091
|
-
if (postState.activeMilestone && postState.phase !== "pre-planning") {
|
|
1092
|
-
state = postState;
|
|
1093
|
-
} else {
|
|
1094
|
-
ctx.ui.notify(
|
|
1095
|
-
"Discussion completed but milestone context is still missing. Run /gsd to try again.",
|
|
1096
|
-
"warning",
|
|
1097
|
-
);
|
|
1098
|
-
return;
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
// Has context, no roadmap — auto-mode will research + plan it
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
741
|
+
// Dispatch the first unit
|
|
742
|
+
await dispatchNextUnit(ctx, pi);
|
|
743
|
+
}
|
|
1104
744
|
|
|
1105
|
-
|
|
1106
|
-
// hasSurvivorBranch is true (which requires activeMilestone) or
|
|
1107
|
-
// the !activeMilestone early-return above would have fired.
|
|
1108
|
-
if (!state.activeMilestone) {
|
|
1109
|
-
// Unreachable — satisfies TypeScript's null check
|
|
1110
|
-
const { showSmartEntry } = await import("./guided-flow.js");
|
|
1111
|
-
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
1112
|
-
return;
|
|
1113
|
-
}
|
|
745
|
+
// ─── Agent End Handler ────────────────────────────────────────────────────────
|
|
1114
746
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
autoStartTime = Date.now();
|
|
1131
|
-
resourceVersionOnStart = readResourceVersion();
|
|
1132
|
-
completedUnits = [];
|
|
1133
|
-
pendingQuickTasks = [];
|
|
1134
|
-
currentUnit = null;
|
|
1135
|
-
currentMilestoneId = state.activeMilestone?.id ?? null;
|
|
1136
|
-
originalModelId = ctx.model?.id ?? null;
|
|
1137
|
-
originalModelProvider = ctx.model?.provider ?? null;
|
|
1138
|
-
|
|
1139
|
-
// Register a SIGTERM handler so `kill <pid>` cleans up the lock and exits.
|
|
1140
|
-
registerSigtermHandler(base);
|
|
1141
|
-
|
|
1142
|
-
// Capture the integration branch — records the branch the user was on when
|
|
1143
|
-
// auto-mode started. Slice branches will merge back to this branch instead
|
|
1144
|
-
// of the repo's default (main/master). Idempotent when the branch is the
|
|
1145
|
-
// same; updates the record when started from a different branch (#300).
|
|
1146
|
-
if (currentMilestoneId) {
|
|
1147
|
-
if (getIsolationMode() !== "none") {
|
|
1148
|
-
captureIntegrationBranch(base, currentMilestoneId, { commitDocs });
|
|
1149
|
-
}
|
|
1150
|
-
setActiveMilestoneId(base, currentMilestoneId);
|
|
747
|
+
/** Guard against concurrent handleAgentEnd execution. */
|
|
748
|
+
|
|
749
|
+
export async function handleAgentEnd(
|
|
750
|
+
ctx: ExtensionContext,
|
|
751
|
+
pi: ExtensionAPI,
|
|
752
|
+
): Promise<void> {
|
|
753
|
+
if (!s.active || !s.cmdCtx) return;
|
|
754
|
+
if (s.handlingAgentEnd) {
|
|
755
|
+
// Another agent_end arrived while we're still processing the previous one.
|
|
756
|
+
// This happens when a unit dispatched inside handleAgentEnd (e.g. via hooks,
|
|
757
|
+
// triage, or quick-task early-dispatch paths) completes before the outer
|
|
758
|
+
// handleAgentEnd returns. Queue a retry so the completed unit's agent_end
|
|
759
|
+
// is not silently dropped (#1072).
|
|
760
|
+
s.pendingAgentEndRetry = true;
|
|
761
|
+
return;
|
|
1151
762
|
}
|
|
763
|
+
s.handlingAgentEnd = true;
|
|
1152
764
|
|
|
1153
|
-
|
|
1154
|
-
// Store the original project root before any chdir so we can restore on stop.
|
|
1155
|
-
// Skip if already inside a worktree (manual /worktree or another auto-worktree)
|
|
1156
|
-
// to prevent nested worktree creation.
|
|
1157
|
-
originalBasePath = base;
|
|
1158
|
-
|
|
1159
|
-
const isUnderGsdWorktrees = (p: string): boolean => {
|
|
1160
|
-
// Prevent creating nested auto-worktrees when running from within any
|
|
1161
|
-
// `.gsd/worktrees/...` directory (including manual worktrees).
|
|
1162
|
-
const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
|
|
1163
|
-
if (p.includes(marker)) {
|
|
1164
|
-
return true;
|
|
1165
|
-
}
|
|
1166
|
-
const worktreesSuffix = `${pathSep}.gsd${pathSep}worktrees`;
|
|
1167
|
-
return p.endsWith(worktreesSuffix);
|
|
1168
|
-
};
|
|
765
|
+
try {
|
|
1169
766
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
const existingWtPath = getAutoWorktreePath(base, currentMilestoneId);
|
|
1173
|
-
if (existingWtPath) {
|
|
1174
|
-
// Worktree already exists (e.g., previous session created it) — enter it.
|
|
1175
|
-
const wtPath = enterAutoWorktree(base, currentMilestoneId);
|
|
1176
|
-
basePath = wtPath;
|
|
1177
|
-
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
1178
|
-
ctx.ui.notify(`Entered auto-worktree at ${wtPath}`, "info");
|
|
1179
|
-
} else {
|
|
1180
|
-
// Fresh start — create worktree and enter it.
|
|
1181
|
-
const wtPath = createAutoWorktree(base, currentMilestoneId);
|
|
1182
|
-
basePath = wtPath;
|
|
1183
|
-
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
1184
|
-
ctx.ui.notify(`Created auto-worktree at ${wtPath}`, "info");
|
|
1185
|
-
}
|
|
1186
|
-
// Re-register SIGTERM handler with the original basePath (lock lives there)
|
|
1187
|
-
registerSigtermHandler(originalBasePath);
|
|
1188
|
-
|
|
1189
|
-
// After worktree entry, load completed keys from BOTH locations (project root
|
|
1190
|
-
// + worktree) so the in-memory set is the union. Prevents re-dispatch of units
|
|
1191
|
-
// completed in either location after crash/restart (#769).
|
|
1192
|
-
if (basePath !== originalBasePath) {
|
|
1193
|
-
loadPersistedKeys(basePath, completedKeySet);
|
|
1194
|
-
}
|
|
1195
|
-
} catch (err) {
|
|
1196
|
-
// Worktree creation is non-fatal — continue in the project root.
|
|
1197
|
-
ctx.ui.notify(
|
|
1198
|
-
`Auto-worktree setup failed: ${err instanceof Error ? err.message : String(err)}. Continuing in project root.`,
|
|
1199
|
-
"warning",
|
|
1200
|
-
);
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
767
|
+
// Unit completed — clear its timeout
|
|
768
|
+
clearUnitTimeout();
|
|
1203
769
|
|
|
1204
|
-
// ──
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
openDb(gsdDbPath);
|
|
1216
|
-
migrateFromMarkdown(basePath);
|
|
1217
|
-
} catch (err) {
|
|
1218
|
-
process.stderr.write(`gsd-migrate: auto-migration failed: ${(err as Error).message}\n`);
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
if (existsSync(gsdDbPath) && !isDbAvailable()) {
|
|
1223
|
-
try {
|
|
1224
|
-
const { openDatabase: openDb } = await import("./gsd-db.js");
|
|
1225
|
-
openDb(gsdDbPath);
|
|
1226
|
-
} catch (err) {
|
|
1227
|
-
process.stderr.write(`gsd-db: failed to open existing database: ${(err as Error).message}\n`);
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
770
|
+
// ── Pre-verification processing (commit, doctor, state rebuild, etc.) ──
|
|
771
|
+
const postUnitCtx: PostUnitContext = {
|
|
772
|
+
s,
|
|
773
|
+
ctx,
|
|
774
|
+
pi,
|
|
775
|
+
buildSnapshotOpts,
|
|
776
|
+
lockBase,
|
|
777
|
+
stopAuto,
|
|
778
|
+
pauseAuto,
|
|
779
|
+
updateProgressWidget,
|
|
780
|
+
};
|
|
1230
781
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
initMetrics(basePath);
|
|
782
|
+
const preResult = await postUnitPreVerification(postUnitCtx);
|
|
783
|
+
if (preResult === "dispatched") return;
|
|
1234
784
|
|
|
1235
|
-
//
|
|
1236
|
-
|
|
785
|
+
// ── Verification gate: run typecheck/lint/test after execute-task ──
|
|
786
|
+
const verificationResult = await runPostUnitVerification(
|
|
787
|
+
{ s, ctx, pi },
|
|
788
|
+
dispatchNextUnit,
|
|
789
|
+
startDispatchGapWatchdog,
|
|
790
|
+
pauseAuto,
|
|
791
|
+
);
|
|
792
|
+
if (verificationResult === "retry" || verificationResult === "pause") return;
|
|
1237
793
|
|
|
1238
|
-
//
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
794
|
+
// ── Post-verification processing (DB dual-write, hooks, triage, quick-tasks) ──
|
|
795
|
+
const postResult = await postUnitPostVerification(postUnitCtx);
|
|
796
|
+
if (postResult === "dispatched" || postResult === "stopped") return;
|
|
797
|
+
if (postResult === "step-wizard") {
|
|
798
|
+
await showStepWizard(ctx, pi);
|
|
799
|
+
return;
|
|
1244
800
|
}
|
|
1245
801
|
|
|
1246
|
-
//
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
802
|
+
// ── Dispatch with hang detection (#1073) ────────────────────────────────
|
|
803
|
+
// Start a safety watchdog BEFORE calling dispatchNextUnit. If dispatch
|
|
804
|
+
// hangs at any await (newSession, model selection, etc.), the gap watchdog
|
|
805
|
+
// inside handleAgentEnd never fires because we never reach the check.
|
|
806
|
+
// This pre-dispatch watchdog ensures recovery even when dispatchNextUnit
|
|
807
|
+
// itself is permanently blocked.
|
|
808
|
+
const dispatchHangGuard = setTimeout(() => {
|
|
809
|
+
if (!s.active) return;
|
|
810
|
+
// dispatchNextUnit has been running for too long — it's likely hung.
|
|
811
|
+
// Start the gap watchdog which will retry dispatch from scratch.
|
|
812
|
+
if (!s.unitTimeoutHandle && !s.wrapupWarningHandle) {
|
|
813
|
+
ctx.ui.notify(
|
|
814
|
+
`Dispatch hang detected (${DISPATCH_HANG_TIMEOUT_MS / 1000}s without completion). Starting recovery watchdog.`,
|
|
815
|
+
"warning",
|
|
816
|
+
);
|
|
817
|
+
startDispatchGapWatchdog(ctx, pi);
|
|
818
|
+
}
|
|
819
|
+
}, DISPATCH_HANG_TIMEOUT_MS);
|
|
1250
820
|
|
|
1251
|
-
ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
|
|
1252
|
-
ctx.ui.setFooter(hideFooter);
|
|
1253
|
-
const modeLabel = stepMode ? "Step-mode" : "Auto-mode";
|
|
1254
|
-
const pendingCount = state.registry.filter(m => m.status !== 'complete').length;
|
|
1255
|
-
const scopeMsg = pendingCount > 1
|
|
1256
|
-
? `Will loop through ${pendingCount} milestones.`
|
|
1257
|
-
: "Will loop until milestone complete.";
|
|
1258
|
-
ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
|
|
1259
|
-
|
|
1260
|
-
// Write initial lock file immediately so cross-process status detection
|
|
1261
|
-
// works even before the first unit is dispatched (#723).
|
|
1262
|
-
// The lock is updated with unit-specific info on each dispatch and cleared on stop.
|
|
1263
|
-
writeLock(lockBase(), "starting", currentMilestoneId ?? "unknown", 0);
|
|
1264
|
-
|
|
1265
|
-
// Secrets collection gate — collect pending secrets before first dispatch
|
|
1266
|
-
const mid = state.activeMilestone!.id;
|
|
1267
821
|
try {
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
if (result && result.applied && result.skipped && result.existingSkipped) {
|
|
1272
|
-
ctx.ui.notify(
|
|
1273
|
-
`Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`,
|
|
1274
|
-
"info",
|
|
1275
|
-
);
|
|
1276
|
-
} else {
|
|
1277
|
-
ctx.ui.notify("Secrets collection skipped.", "info");
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
} catch (err) {
|
|
822
|
+
await dispatchNextUnit(ctx, pi);
|
|
823
|
+
} catch (dispatchErr) {
|
|
824
|
+
const message = dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr);
|
|
1281
825
|
ctx.ui.notify(
|
|
1282
|
-
`
|
|
1283
|
-
"
|
|
826
|
+
`Dispatch error after unit completion: ${message}. Retrying in ${DISPATCH_GAP_TIMEOUT_MS / 1000}s.`,
|
|
827
|
+
"error",
|
|
1284
828
|
);
|
|
829
|
+
startDispatchGapWatchdog(ctx, pi);
|
|
830
|
+
return;
|
|
831
|
+
} finally {
|
|
832
|
+
clearTimeout(dispatchHangGuard);
|
|
1285
833
|
}
|
|
1286
834
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
await selfHealRuntimeRecords(basePath, ctx, completedKeySet);
|
|
1291
|
-
|
|
1292
|
-
// Self-heal: remove stale .git/index.lock from prior crash.
|
|
1293
|
-
// A stale lock file blocks all git operations (commit, merge, checkout).
|
|
1294
|
-
// Only remove if older than 60 seconds (not from a concurrent process).
|
|
1295
|
-
try {
|
|
1296
|
-
const gitLockFile = join(base, ".git", "index.lock");
|
|
1297
|
-
if (existsSync(gitLockFile)) {
|
|
1298
|
-
const lockAge = Date.now() - statSync(gitLockFile).mtimeMs;
|
|
1299
|
-
if (lockAge > 60_000) {
|
|
1300
|
-
unlinkSync(gitLockFile);
|
|
1301
|
-
ctx.ui.notify("Removed stale .git/index.lock from prior crash.", "info");
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
} catch { /* non-fatal */ }
|
|
835
|
+
if (s.active && !s.unitTimeoutHandle && !s.wrapupWarningHandle) {
|
|
836
|
+
startDispatchGapWatchdog(ctx, pi);
|
|
837
|
+
}
|
|
1305
838
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
ctx
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
839
|
+
} finally {
|
|
840
|
+
s.handlingAgentEnd = false;
|
|
841
|
+
|
|
842
|
+
// If an agent_end event was dropped by the reentrancy guard while we were
|
|
843
|
+
// processing, re-enter handleAgentEnd on the next microtask. This prevents
|
|
844
|
+
// the summarizing phase stall (#1072) where a unit dispatched inside
|
|
845
|
+
// handleAgentEnd (hooks, triage, quick-task) completes before we return,
|
|
846
|
+
// and its agent_end is silently dropped — leaving auto-mode active but
|
|
847
|
+
// permanently stalled with no unit running and no watchdog set.
|
|
848
|
+
if (s.pendingAgentEndRetry) {
|
|
849
|
+
s.pendingAgentEndRetry = false;
|
|
850
|
+
setImmediate(() => {
|
|
851
|
+
handleAgentEnd(ctx, pi).catch((err) => {
|
|
852
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
853
|
+
ctx.ui.notify(`Deferred agent_end retry failed: ${msg}`, "error");
|
|
854
|
+
pauseAuto(ctx, pi).catch(() => {});
|
|
855
|
+
});
|
|
856
|
+
});
|
|
1326
857
|
}
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
// Dispatch the first unit
|
|
1330
|
-
await dispatchNextUnit(ctx, pi);
|
|
858
|
+
}
|
|
1331
859
|
}
|
|
1332
860
|
|
|
1333
|
-
// ───
|
|
1334
|
-
|
|
1335
|
-
/** Guard against concurrent handleAgentEnd execution. Background job
|
|
1336
|
-
* notifications and other system messages can trigger multiple agent_end
|
|
1337
|
-
* events before the first handler finishes (the handler yields at every
|
|
1338
|
-
* await). Without this guard, concurrent dispatchNextUnit calls race on
|
|
1339
|
-
* newSession(), causing one to cancel the other and silently stopping
|
|
1340
|
-
* auto-mode. */
|
|
1341
|
-
let _handlingAgentEnd = false;
|
|
861
|
+
// ─── Step Mode Wizard ─────────────────────────────────────────────────────
|
|
1342
862
|
|
|
1343
|
-
|
|
863
|
+
/**
|
|
864
|
+
* Show the step-mode wizard after a unit completes.
|
|
865
|
+
*/
|
|
866
|
+
async function showStepWizard(
|
|
1344
867
|
ctx: ExtensionContext,
|
|
1345
868
|
pi: ExtensionAPI,
|
|
1346
869
|
): Promise<void> {
|
|
1347
|
-
if (!
|
|
1348
|
-
if (_handlingAgentEnd) return;
|
|
1349
|
-
_handlingAgentEnd = true;
|
|
870
|
+
if (!s.cmdCtx) return;
|
|
1350
871
|
|
|
1351
|
-
|
|
872
|
+
const state = await deriveState(s.basePath);
|
|
873
|
+
const mid = state.activeMilestone?.id;
|
|
1352
874
|
|
|
1353
|
-
|
|
1354
|
-
|
|
875
|
+
const justFinished = s.currentUnit
|
|
876
|
+
? `${unitVerb(s.currentUnit.type)} ${s.currentUnit.id}`
|
|
877
|
+
: "previous unit";
|
|
1355
878
|
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
await stopAuto(ctx, pi);
|
|
1366
|
-
return;
|
|
1367
|
-
}
|
|
1368
|
-
if (signal.signal === "pause") {
|
|
1369
|
-
_handlingAgentEnd = false;
|
|
1370
|
-
await pauseAuto(ctx, pi);
|
|
1371
|
-
return;
|
|
1372
|
-
}
|
|
1373
|
-
// "resume" and "rebase" signals are handled elsewhere or no-op here
|
|
1374
|
-
}
|
|
879
|
+
if (!mid || state.phase === "complete") {
|
|
880
|
+
const incomplete = state.registry.filter(m => m.status !== "complete" && m.status !== "parked");
|
|
881
|
+
if (incomplete.length > 0 && state.phase !== "complete" && state.phase !== "blocked" && state.phase !== "pre-planning") {
|
|
882
|
+
const ids = incomplete.map(m => m.id).join(", ");
|
|
883
|
+
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map(m => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
884
|
+
ctx.ui.notify(`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`, "error");
|
|
885
|
+
await stopAuto(ctx, pi, `No active milestone — ${incomplete.length} incomplete (${ids})`);
|
|
886
|
+
} else {
|
|
887
|
+
await stopAuto(ctx, pi, state.phase === "complete" ? "All work complete" : "No active milestone");
|
|
1375
888
|
}
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
1376
891
|
|
|
1377
|
-
// Invalidate all caches — the unit just completed and may have
|
|
1378
|
-
// written planning files (task summaries, roadmap checkboxes, etc.)
|
|
1379
|
-
invalidateAllCaches();
|
|
1380
|
-
|
|
1381
|
-
// Small delay to let files settle (git commits, file writes)
|
|
1382
|
-
await new Promise(r => setTimeout(r, 500));
|
|
1383
|
-
|
|
1384
|
-
// Commit any dirty files the LLM left behind on the current branch.
|
|
1385
|
-
// For execute-task units, build a meaningful commit message from the
|
|
1386
|
-
// task summary (one-liner, key_files, inferred type). For other unit
|
|
1387
|
-
// types, fall back to the generic chore() message.
|
|
1388
|
-
if (currentUnit) {
|
|
1389
|
-
try {
|
|
1390
|
-
let taskContext: TaskCommitContext | undefined;
|
|
1391
|
-
|
|
1392
|
-
if (currentUnit.type === "execute-task") {
|
|
1393
|
-
const parts = currentUnit.id.split("/");
|
|
1394
|
-
const [mid, sid, tid] = parts;
|
|
1395
|
-
if (mid && sid && tid) {
|
|
1396
|
-
const summaryPath = resolveTaskFile(basePath, mid, sid, tid, "SUMMARY");
|
|
1397
|
-
if (summaryPath) {
|
|
1398
|
-
try {
|
|
1399
|
-
const summaryContent = await loadFile(summaryPath);
|
|
1400
|
-
if (summaryContent) {
|
|
1401
|
-
const summary = parseSummary(summaryContent);
|
|
1402
|
-
taskContext = {
|
|
1403
|
-
taskId: `${sid}/${tid}`,
|
|
1404
|
-
taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
|
|
1405
|
-
oneLiner: summary.oneLiner || undefined,
|
|
1406
|
-
keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
|
|
1407
|
-
};
|
|
1408
|
-
}
|
|
1409
|
-
} catch {
|
|
1410
|
-
// Non-fatal — fall back to generic message
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
const commitMsg = autoCommitCurrentBranch(basePath, currentUnit.type, currentUnit.id, taskContext);
|
|
1417
|
-
if (commitMsg) {
|
|
1418
|
-
ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
|
|
1419
|
-
}
|
|
1420
|
-
} catch {
|
|
1421
|
-
// Non-fatal
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
// Post-hook: fix mechanical bookkeeping the LLM may have skipped.
|
|
1425
|
-
// 1. Doctor handles: checkbox marking (task-level bookkeeping).
|
|
1426
|
-
// 2. STATE.md is always rebuilt from disk state (purely derived, no LLM needed).
|
|
1427
|
-
// fixLevel:"task" ensures doctor only fixes task-level issues (e.g. marking
|
|
1428
|
-
// checkboxes). Slice/milestone completion transitions (summary stubs,
|
|
1429
|
-
// roadmap [x] marking) are left for the complete-slice dispatch unit.
|
|
1430
|
-
// Exception: after complete-slice itself, use fixLevel:"all" so roadmap
|
|
1431
|
-
// checkboxes get fixed even if complete-slice crashed (#839).
|
|
1432
|
-
try {
|
|
1433
|
-
const scopeParts = currentUnit.id.split("/").slice(0, 2);
|
|
1434
|
-
const doctorScope = scopeParts.join("/");
|
|
1435
|
-
const effectiveFixLevel = currentUnit.type === "complete-slice" ? "all" as const : "task" as const;
|
|
1436
|
-
const report = await runGSDDoctor(basePath, { fix: true, scope: doctorScope, fixLevel: effectiveFixLevel });
|
|
1437
|
-
if (report.fixesApplied.length > 0) {
|
|
1438
|
-
ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
// ── Proactive health tracking ──────────────────────────────────────
|
|
1442
|
-
// Record health snapshot for trend analysis and escalation logic.
|
|
1443
|
-
const summary = summarizeDoctorIssues(report.issues);
|
|
1444
|
-
recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
|
|
1445
|
-
|
|
1446
|
-
// Check if we should escalate to LLM-assisted heal
|
|
1447
|
-
if (summary.errors > 0) {
|
|
1448
|
-
const unresolvedErrors = report.issues
|
|
1449
|
-
.filter(i => i.severity === "error" && !i.fixable)
|
|
1450
|
-
.map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
|
|
1451
|
-
const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
|
|
1452
|
-
if (escalation.shouldEscalate) {
|
|
1453
|
-
ctx.ui.notify(
|
|
1454
|
-
`Doctor heal escalation: ${escalation.reason}. Dispatching LLM-assisted heal.`,
|
|
1455
|
-
"warning",
|
|
1456
|
-
);
|
|
1457
|
-
try {
|
|
1458
|
-
const { formatDoctorIssuesForPrompt, formatDoctorReport } = await import("./doctor.js");
|
|
1459
|
-
const { dispatchDoctorHeal } = await import("./commands.js");
|
|
1460
|
-
const actionable = report.issues.filter(i => i.severity === "error");
|
|
1461
|
-
const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
|
|
1462
|
-
const structuredIssues = formatDoctorIssuesForPrompt(actionable);
|
|
1463
|
-
dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
|
|
1464
|
-
} catch {
|
|
1465
|
-
// Non-fatal — escalation dispatch failure
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
} catch {
|
|
1470
|
-
// Non-fatal — doctor failure should never block dispatch
|
|
1471
|
-
}
|
|
1472
|
-
try {
|
|
1473
|
-
await rebuildState(basePath);
|
|
1474
|
-
// State rebuild commit is bookkeeping — generic message is appropriate
|
|
1475
|
-
autoCommitCurrentBranch(basePath, "state-rebuild", currentUnit.id);
|
|
1476
|
-
} catch {
|
|
1477
|
-
// Non-fatal
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
// ── Sync worktree state back to project root ──────────────────────────
|
|
1481
|
-
// Ensures that if auto-mode restarts, deriveState(projectRoot) reads
|
|
1482
|
-
// current milestone progress instead of stale pre-worktree state (#654).
|
|
1483
|
-
if (originalBasePath && originalBasePath !== basePath) {
|
|
1484
|
-
try {
|
|
1485
|
-
syncStateToProjectRoot(basePath, originalBasePath, currentMilestoneId);
|
|
1486
|
-
} catch {
|
|
1487
|
-
// Non-fatal — stale state is the existing behavior, sync is an improvement
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
// ── Rewrite-docs completion: resolve overrides and reset circuit breaker ──
|
|
1492
|
-
if (currentUnit.type === "rewrite-docs") {
|
|
1493
|
-
try {
|
|
1494
|
-
await resolveAllOverrides(basePath);
|
|
1495
|
-
resetRewriteCircuitBreaker();
|
|
1496
|
-
ctx.ui.notify("Override(s) resolved — rewrite-docs completed.", "info");
|
|
1497
|
-
} catch {
|
|
1498
|
-
// Non-fatal — verifyExpectedArtifact will catch unresolved overrides
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
// ── Post-triage: execute actionable resolutions (inject, replan, queue quick-tasks) ──
|
|
1503
|
-
// After a triage-captures unit completes, the LLM has classified captures and
|
|
1504
|
-
// updated CAPTURES.md. Now we execute those classifications: inject tasks into
|
|
1505
|
-
// the plan, write replan triggers, and queue quick-tasks for dispatch.
|
|
1506
|
-
if (currentUnit.type === "triage-captures") {
|
|
1507
|
-
try {
|
|
1508
|
-
const { executeTriageResolutions } = await import("./triage-resolution.js");
|
|
1509
|
-
const state = await deriveState(basePath);
|
|
1510
|
-
const mid = state.activeMilestone?.id;
|
|
1511
|
-
const sid = state.activeSlice?.id;
|
|
1512
|
-
|
|
1513
|
-
if (mid && sid) {
|
|
1514
|
-
const triageResult = executeTriageResolutions(basePath, mid, sid);
|
|
1515
|
-
|
|
1516
|
-
if (triageResult.injected > 0) {
|
|
1517
|
-
ctx.ui.notify(
|
|
1518
|
-
`Triage: injected ${triageResult.injected} task${triageResult.injected === 1 ? "" : "s"} into ${sid} plan.`,
|
|
1519
|
-
"info",
|
|
1520
|
-
);
|
|
1521
|
-
}
|
|
1522
|
-
if (triageResult.replanned > 0) {
|
|
1523
|
-
ctx.ui.notify(
|
|
1524
|
-
`Triage: replan trigger written for ${sid} — next dispatch will enter replanning.`,
|
|
1525
|
-
"info",
|
|
1526
|
-
);
|
|
1527
|
-
}
|
|
1528
|
-
if (triageResult.quickTasks.length > 0) {
|
|
1529
|
-
// Queue quick-tasks for dispatch. They'll be picked up by the
|
|
1530
|
-
// quick-task dispatch block below the triage check.
|
|
1531
|
-
for (const qt of triageResult.quickTasks) {
|
|
1532
|
-
pendingQuickTasks.push(qt);
|
|
1533
|
-
}
|
|
1534
|
-
ctx.ui.notify(
|
|
1535
|
-
`Triage: ${triageResult.quickTasks.length} quick-task${triageResult.quickTasks.length === 1 ? "" : "s"} queued for execution.`,
|
|
1536
|
-
"info",
|
|
1537
|
-
);
|
|
1538
|
-
}
|
|
1539
|
-
for (const action of triageResult.actions) {
|
|
1540
|
-
process.stderr.write(`gsd-triage: ${action}\n`);
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
} catch (err) {
|
|
1544
|
-
// Non-fatal — triage resolution failure shouldn't block dispatch
|
|
1545
|
-
process.stderr.write(`gsd-triage: resolution execution failed: ${(err as Error).message}\n`);
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
// ── Path A fix: verify artifact and persist completion before re-entering dispatch ──
|
|
1550
|
-
// After doctor + rebuildState, check whether the just-completed unit actually
|
|
1551
|
-
// produced its expected artifact. If so, persist the completion key now so the
|
|
1552
|
-
// idempotency check at the top of dispatchNextUnit() skips it — even if
|
|
1553
|
-
// deriveState() still returns this unit as active (e.g. branch mismatch).
|
|
1554
|
-
//
|
|
1555
|
-
// IMPORTANT: For non-hook units, defer persistence until after the hook check.
|
|
1556
|
-
// If a post-unit hook requests a retry, we need to remove the completion key
|
|
1557
|
-
// so dispatchNextUnit re-dispatches the trigger unit.
|
|
1558
|
-
let triggerArtifactVerified = false;
|
|
1559
|
-
if (!currentUnit.type.startsWith("hook/")) {
|
|
1560
|
-
try {
|
|
1561
|
-
triggerArtifactVerified = verifyExpectedArtifact(currentUnit.type, currentUnit.id, basePath);
|
|
1562
|
-
if (triggerArtifactVerified) {
|
|
1563
|
-
const completionKey = `${currentUnit.type}/${currentUnit.id}`;
|
|
1564
|
-
if (!completedKeySet.has(completionKey)) {
|
|
1565
|
-
persistCompletedKey(basePath, completionKey);
|
|
1566
|
-
completedKeySet.add(completionKey);
|
|
1567
|
-
}
|
|
1568
|
-
invalidateAllCaches();
|
|
1569
|
-
}
|
|
1570
|
-
} catch {
|
|
1571
|
-
// Non-fatal — worst case we fall through to normal dispatch which has its own checks
|
|
1572
|
-
}
|
|
1573
|
-
} else {
|
|
1574
|
-
// Hook unit completed — finalize its runtime record and clear it
|
|
1575
|
-
try {
|
|
1576
|
-
writeUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id, currentUnit.startedAt, {
|
|
1577
|
-
phase: "finalized",
|
|
1578
|
-
progressCount: 1,
|
|
1579
|
-
lastProgressKind: "hook-completed",
|
|
1580
|
-
});
|
|
1581
|
-
clearUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id);
|
|
1582
|
-
} catch {
|
|
1583
|
-
// Non-fatal
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
// ── Verification gate: run typecheck/lint/test after execute-task ──
|
|
1589
|
-
if (currentUnit && currentUnit.type === "execute-task") {
|
|
1590
|
-
try {
|
|
1591
|
-
const effectivePrefs = loadEffectiveGSDPreferences();
|
|
1592
|
-
const prefs = effectivePrefs?.preferences;
|
|
1593
|
-
|
|
1594
|
-
// Read task plan verify field from the current task's slice plan
|
|
1595
|
-
// unitId format is "M001/S01/T03" — extract mid, sid, tid
|
|
1596
|
-
const parts = currentUnit.id.split("/");
|
|
1597
|
-
let taskPlanVerify: string | undefined;
|
|
1598
|
-
if (parts.length >= 3) {
|
|
1599
|
-
const [mid, sid, tid] = parts;
|
|
1600
|
-
const planFile = resolveSliceFile(basePath, mid, sid, "PLAN");
|
|
1601
|
-
if (planFile) {
|
|
1602
|
-
const planContent = await loadFile(planFile);
|
|
1603
|
-
if (planContent) {
|
|
1604
|
-
const slicePlan = parsePlan(planContent);
|
|
1605
|
-
const taskEntry = slicePlan?.tasks?.find(t => t.id === tid);
|
|
1606
|
-
taskPlanVerify = taskEntry?.verify;
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
const result = runVerificationGate({
|
|
1612
|
-
basePath,
|
|
1613
|
-
unitId: currentUnit.id,
|
|
1614
|
-
cwd: basePath,
|
|
1615
|
-
preferenceCommands: prefs?.verification_commands,
|
|
1616
|
-
taskPlanVerify,
|
|
1617
|
-
});
|
|
1618
|
-
|
|
1619
|
-
// Capture runtime errors from bg-shell and browser console
|
|
1620
|
-
const runtimeErrors = await captureRuntimeErrors();
|
|
1621
|
-
if (runtimeErrors.length > 0) {
|
|
1622
|
-
result.runtimeErrors = runtimeErrors;
|
|
1623
|
-
// Blocking runtime errors override gate pass
|
|
1624
|
-
if (runtimeErrors.some(e => e.blocking)) {
|
|
1625
|
-
result.passed = false;
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
// Conditional dependency audit (R008)
|
|
1630
|
-
const auditWarnings = runDependencyAudit(basePath);
|
|
1631
|
-
if (auditWarnings.length > 0) {
|
|
1632
|
-
result.auditWarnings = auditWarnings;
|
|
1633
|
-
process.stderr.write(`verification-gate: ${auditWarnings.length} audit warning(s)\n`);
|
|
1634
|
-
for (const w of auditWarnings) {
|
|
1635
|
-
process.stderr.write(` [${w.severity}] ${w.name}: ${w.title}\n`);
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
// Auto-fix retry preferences (R005 / D005)
|
|
1640
|
-
const autoFixEnabled = prefs?.verification_auto_fix !== false; // default true
|
|
1641
|
-
const maxRetries = typeof prefs?.verification_max_retries === "number" ? prefs.verification_max_retries : 2;
|
|
1642
|
-
const completionKey = `${currentUnit.type}/${currentUnit.id}`;
|
|
1643
|
-
|
|
1644
|
-
if (result.checks.length > 0) {
|
|
1645
|
-
const passCount = result.checks.filter(c => c.exitCode === 0).length;
|
|
1646
|
-
const total = result.checks.length;
|
|
1647
|
-
if (result.passed) {
|
|
1648
|
-
ctx.ui.notify(`Verification gate: ${passCount}/${total} checks passed`);
|
|
1649
|
-
} else {
|
|
1650
|
-
const failures = result.checks.filter(c => c.exitCode !== 0);
|
|
1651
|
-
const failNames = failures.map(f => f.command).join(", ");
|
|
1652
|
-
ctx.ui.notify(`Verification gate: FAILED — ${failNames}`);
|
|
1653
|
-
process.stderr.write(`verification-gate: ${total - passCount}/${total} checks failed\n`);
|
|
1654
|
-
for (const f of failures) {
|
|
1655
|
-
process.stderr.write(` ${f.command} exited ${f.exitCode}\n`);
|
|
1656
|
-
if (f.stderr) process.stderr.write(` stderr: ${f.stderr.slice(0, 500)}\n`);
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
// Log blocking runtime errors to stderr
|
|
1662
|
-
if (result.runtimeErrors?.some(e => e.blocking)) {
|
|
1663
|
-
const blockingErrors = result.runtimeErrors.filter(e => e.blocking);
|
|
1664
|
-
process.stderr.write(`verification-gate: ${blockingErrors.length} blocking runtime error(s) detected\n`);
|
|
1665
|
-
for (const err of blockingErrors) {
|
|
1666
|
-
process.stderr.write(` [${err.source}] ${err.severity}: ${err.message.slice(0, 200)}\n`);
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
// Write verification evidence JSON artifact
|
|
1671
|
-
const attempt = verificationRetryCount.get(currentUnit.id) ?? 0;
|
|
1672
|
-
if (parts.length >= 3) {
|
|
1673
|
-
try {
|
|
1674
|
-
const [mid, sid, tid] = parts;
|
|
1675
|
-
const sDir = resolveSlicePath(basePath, mid, sid);
|
|
1676
|
-
if (sDir) {
|
|
1677
|
-
const tasksDir = join(sDir, "tasks");
|
|
1678
|
-
if (result.passed) {
|
|
1679
|
-
writeVerificationJSON(result, tasksDir, tid, currentUnit.id);
|
|
1680
|
-
} else {
|
|
1681
|
-
const nextAttempt = attempt + 1;
|
|
1682
|
-
writeVerificationJSON(result, tasksDir, tid, currentUnit.id, nextAttempt, maxRetries);
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
} catch (evidenceErr) {
|
|
1686
|
-
process.stderr.write(`verification-evidence: write error — ${(evidenceErr as Error).message}\n`);
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
// ── Auto-fix retry logic ──
|
|
1691
|
-
if (result.passed) {
|
|
1692
|
-
// Gate passed — clear retry state and continue normal flow
|
|
1693
|
-
verificationRetryCount.delete(currentUnit.id);
|
|
1694
|
-
pendingVerificationRetry = null;
|
|
1695
|
-
} else if (autoFixEnabled && attempt + 1 <= maxRetries) {
|
|
1696
|
-
// Gate failed, retries remaining — set up retry and return early
|
|
1697
|
-
const nextAttempt = attempt + 1;
|
|
1698
|
-
verificationRetryCount.set(currentUnit.id, nextAttempt);
|
|
1699
|
-
pendingVerificationRetry = {
|
|
1700
|
-
unitId: currentUnit.id,
|
|
1701
|
-
failureContext: formatFailureContext(result),
|
|
1702
|
-
attempt: nextAttempt,
|
|
1703
|
-
};
|
|
1704
|
-
ctx.ui.notify(`Verification failed — auto-fix attempt ${nextAttempt}/${maxRetries}`, "warning");
|
|
1705
|
-
// Remove completion key so dispatchNextUnit re-dispatches this unit
|
|
1706
|
-
completedKeySet.delete(completionKey);
|
|
1707
|
-
removePersistedKey(basePath, completionKey);
|
|
1708
|
-
return; // ← Critical: exit before DB dual-write and post-unit hooks
|
|
1709
|
-
} else {
|
|
1710
|
-
// Gate failed, retries exhausted (or auto-fix disabled) — pause for human review
|
|
1711
|
-
const exhaustedAttempt = attempt + 1;
|
|
1712
|
-
verificationRetryCount.delete(currentUnit.id);
|
|
1713
|
-
pendingVerificationRetry = null;
|
|
1714
|
-
ctx.ui.notify(
|
|
1715
|
-
`Verification gate FAILED after ${exhaustedAttempt > maxRetries ? exhaustedAttempt - 1 : exhaustedAttempt} retries — pausing for human review`,
|
|
1716
|
-
"error",
|
|
1717
|
-
);
|
|
1718
|
-
await pauseAuto(ctx, pi);
|
|
1719
|
-
return;
|
|
1720
|
-
}
|
|
1721
|
-
} catch (err) {
|
|
1722
|
-
// Gate errors are non-fatal — log and continue
|
|
1723
|
-
process.stderr.write(`verification-gate: error — ${(err as Error).message}\n`);
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
// ── DB dual-write: re-import changed markdown files so next unit's prompts use fresh data ──
|
|
1728
|
-
if (isDbAvailable()) {
|
|
1729
|
-
try {
|
|
1730
|
-
const { migrateFromMarkdown } = await import("./md-importer.js");
|
|
1731
|
-
migrateFromMarkdown(basePath);
|
|
1732
|
-
} catch (err) {
|
|
1733
|
-
process.stderr.write(`gsd-db: re-import failed: ${(err as Error).message}\n`);
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
// ── Post-unit hooks: check if a configured hook should run before normal dispatch ──
|
|
1738
|
-
if (currentUnit && !stepMode) {
|
|
1739
|
-
const hookUnit = checkPostUnitHooks(currentUnit.type, currentUnit.id, basePath);
|
|
1740
|
-
if (hookUnit) {
|
|
1741
|
-
// Dispatch the hook unit instead of normal flow
|
|
1742
|
-
const hookStartedAt = Date.now();
|
|
1743
|
-
if (currentUnit) {
|
|
1744
|
-
const modelId = ctx.model?.id ?? "unknown";
|
|
1745
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
1746
|
-
const hookActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1747
|
-
if (hookActivityFile) {
|
|
1748
|
-
try {
|
|
1749
|
-
const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
|
|
1750
|
-
const llmCallFn = buildMemoryLLMCall(ctx);
|
|
1751
|
-
if (llmCallFn) {
|
|
1752
|
-
extractMemoriesFromUnit(hookActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
|
|
1753
|
-
}
|
|
1754
|
-
} catch { /* non-fatal */ }
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
currentUnit = { type: hookUnit.unitType, id: hookUnit.unitId, startedAt: hookStartedAt };
|
|
1758
|
-
writeUnitRuntimeRecord(basePath, hookUnit.unitType, hookUnit.unitId, hookStartedAt, {
|
|
1759
|
-
phase: "dispatched",
|
|
1760
|
-
wrapupWarningSent: false,
|
|
1761
|
-
timeoutAt: null,
|
|
1762
|
-
lastProgressAt: hookStartedAt,
|
|
1763
|
-
progressCount: 0,
|
|
1764
|
-
lastProgressKind: "dispatch",
|
|
1765
|
-
});
|
|
1766
|
-
|
|
1767
|
-
const state = await deriveState(basePath);
|
|
1768
|
-
updateProgressWidget(ctx, hookUnit.unitType, hookUnit.unitId, state);
|
|
1769
|
-
const hookState = getActiveHook();
|
|
1770
|
-
ctx.ui.notify(
|
|
1771
|
-
`Running post-unit hook: ${hookUnit.hookName} (cycle ${hookState?.cycle ?? 1})`,
|
|
1772
|
-
"info",
|
|
1773
|
-
);
|
|
1774
|
-
|
|
1775
|
-
// Switch model if the hook specifies one
|
|
1776
|
-
if (hookUnit.model) {
|
|
1777
|
-
const availableModels = ctx.modelRegistry.getAvailable();
|
|
1778
|
-
const match = availableModels.find(m =>
|
|
1779
|
-
m.id === hookUnit.model || `${m.provider}/${m.id}` === hookUnit.model,
|
|
1780
|
-
);
|
|
1781
|
-
if (match) {
|
|
1782
|
-
try {
|
|
1783
|
-
await pi.setModel(match);
|
|
1784
|
-
} catch { /* non-fatal — use current model */ }
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
const result = await cmdCtx!.newSession();
|
|
1789
|
-
if (result.cancelled) {
|
|
1790
|
-
resetHookState();
|
|
1791
|
-
await stopAuto(ctx, pi, "Hook session cancelled");
|
|
1792
|
-
return;
|
|
1793
|
-
}
|
|
1794
|
-
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1795
|
-
writeLock(lockBase(), hookUnit.unitType, hookUnit.unitId, completedUnits.length, sessionFile);
|
|
1796
|
-
// Persist hook state so cycle counts survive crashes
|
|
1797
|
-
persistHookState(basePath);
|
|
1798
|
-
|
|
1799
|
-
// Start supervision timers for hook units — hooks can get stuck just
|
|
1800
|
-
// like normal units, and without a watchdog auto-mode would hang forever.
|
|
1801
|
-
clearUnitTimeout();
|
|
1802
|
-
const supervisor = resolveAutoSupervisorConfig();
|
|
1803
|
-
const hookHardTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
|
|
1804
|
-
unitTimeoutHandle = setTimeout(async () => {
|
|
1805
|
-
unitTimeoutHandle = null;
|
|
1806
|
-
if (!active) return;
|
|
1807
|
-
if (currentUnit) {
|
|
1808
|
-
writeUnitRuntimeRecord(basePath, hookUnit.unitType, hookUnit.unitId, currentUnit.startedAt, {
|
|
1809
|
-
phase: "timeout",
|
|
1810
|
-
timeoutAt: Date.now(),
|
|
1811
|
-
});
|
|
1812
|
-
}
|
|
1813
|
-
ctx.ui.notify(
|
|
1814
|
-
`Hook ${hookUnit.hookName} exceeded ${supervisor.hard_timeout_minutes ?? 30}min timeout. Pausing auto-mode.`,
|
|
1815
|
-
"warning",
|
|
1816
|
-
);
|
|
1817
|
-
resetHookState();
|
|
1818
|
-
await pauseAuto(ctx, pi);
|
|
1819
|
-
}, hookHardTimeoutMs);
|
|
1820
|
-
|
|
1821
|
-
// Guard against race with timeout/pause before sending
|
|
1822
|
-
if (!active) return;
|
|
1823
|
-
pi.sendMessage(
|
|
1824
|
-
{ customType: "gsd-auto", content: hookUnit.prompt, display: verbose },
|
|
1825
|
-
{ triggerTurn: true },
|
|
1826
|
-
);
|
|
1827
|
-
return; // handleAgentEnd will fire again when hook session completes
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
// Check if a hook requested a retry of the trigger unit
|
|
1831
|
-
if (isRetryPending()) {
|
|
1832
|
-
const trigger = consumeRetryTrigger();
|
|
1833
|
-
if (trigger) {
|
|
1834
|
-
// Remove the trigger unit's completion key so dispatchNextUnit
|
|
1835
|
-
// will re-dispatch it instead of skipping it as already-complete.
|
|
1836
|
-
const triggerKey = `${trigger.unitType}/${trigger.unitId}`;
|
|
1837
|
-
completedKeySet.delete(triggerKey);
|
|
1838
|
-
removePersistedKey(basePath, triggerKey);
|
|
1839
|
-
ctx.ui.notify(
|
|
1840
|
-
`Hook requested retry of ${trigger.unitType} ${trigger.unitId}.`,
|
|
1841
|
-
"info",
|
|
1842
|
-
);
|
|
1843
|
-
// Fall through to normal dispatchNextUnit — state derivation will
|
|
1844
|
-
// re-select the same unit since it hasn't been marked complete
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
// ── Triage check: dispatch triage unit if pending captures exist ──────────
|
|
1850
|
-
// Fires after hooks complete, before normal dispatch. Follows the same
|
|
1851
|
-
// early-dispatch-and-return pattern as hooks and fix-merge.
|
|
1852
|
-
// Skip for: step mode (shows wizard instead), triage units (prevent triage-on-triage),
|
|
1853
|
-
// hook units (hooks run before triage conceptually).
|
|
1854
|
-
if (
|
|
1855
|
-
!stepMode &&
|
|
1856
|
-
currentUnit &&
|
|
1857
|
-
!currentUnit.type.startsWith("hook/") &&
|
|
1858
|
-
currentUnit.type !== "triage-captures" &&
|
|
1859
|
-
currentUnit.type !== "quick-task"
|
|
1860
|
-
) {
|
|
1861
|
-
try {
|
|
1862
|
-
if (hasPendingCaptures(basePath)) {
|
|
1863
|
-
const pending = loadPendingCaptures(basePath);
|
|
1864
|
-
if (pending.length > 0) {
|
|
1865
|
-
const state = await deriveState(basePath);
|
|
1866
|
-
const mid = state.activeMilestone?.id;
|
|
1867
|
-
const sid = state.activeSlice?.id;
|
|
1868
|
-
|
|
1869
|
-
if (mid && sid) {
|
|
1870
|
-
// Build triage prompt with current context
|
|
1871
|
-
let currentPlan = "";
|
|
1872
|
-
let roadmapContext = "";
|
|
1873
|
-
const planFile = resolveSliceFile(basePath, mid, sid, "PLAN");
|
|
1874
|
-
if (planFile) currentPlan = (await loadFile(planFile)) ?? "";
|
|
1875
|
-
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
1876
|
-
if (roadmapFile) roadmapContext = (await loadFile(roadmapFile)) ?? "";
|
|
1877
|
-
|
|
1878
|
-
const capturesList = pending.map(c =>
|
|
1879
|
-
`- **${c.id}**: "${c.text}" (captured: ${c.timestamp})`
|
|
1880
|
-
).join("\n");
|
|
1881
|
-
|
|
1882
|
-
const prompt = loadPrompt("triage-captures", {
|
|
1883
|
-
pendingCaptures: capturesList,
|
|
1884
|
-
currentPlan: currentPlan || "(no active slice plan)",
|
|
1885
|
-
roadmapContext: roadmapContext || "(no active roadmap)",
|
|
1886
|
-
});
|
|
1887
|
-
|
|
1888
|
-
ctx.ui.notify(
|
|
1889
|
-
`Triaging ${pending.length} pending capture${pending.length === 1 ? "" : "s"}...`,
|
|
1890
|
-
"info",
|
|
1891
|
-
);
|
|
1892
|
-
|
|
1893
|
-
// Close out previous unit metrics
|
|
1894
|
-
if (currentUnit) {
|
|
1895
|
-
const modelId = ctx.model?.id ?? "unknown";
|
|
1896
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1897
|
-
const triageActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1898
|
-
if (triageActivityFile) {
|
|
1899
|
-
try {
|
|
1900
|
-
const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
|
|
1901
|
-
const llmCallFn = buildMemoryLLMCall(ctx);
|
|
1902
|
-
if (llmCallFn) {
|
|
1903
|
-
extractMemoriesFromUnit(triageActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
|
|
1904
|
-
}
|
|
1905
|
-
} catch { /* non-fatal */ }
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
// Dispatch triage as a new unit (early-dispatch-and-return)
|
|
1910
|
-
const triageUnitType = "triage-captures";
|
|
1911
|
-
const triageUnitId = `${mid}/${sid}/triage`;
|
|
1912
|
-
const triageStartedAt = Date.now();
|
|
1913
|
-
currentUnit = { type: triageUnitType, id: triageUnitId, startedAt: triageStartedAt };
|
|
1914
|
-
writeUnitRuntimeRecord(basePath, triageUnitType, triageUnitId, triageStartedAt, {
|
|
1915
|
-
phase: "dispatched",
|
|
1916
|
-
wrapupWarningSent: false,
|
|
1917
|
-
timeoutAt: null,
|
|
1918
|
-
lastProgressAt: triageStartedAt,
|
|
1919
|
-
progressCount: 0,
|
|
1920
|
-
lastProgressKind: "dispatch",
|
|
1921
|
-
});
|
|
1922
|
-
updateProgressWidget(ctx, triageUnitType, triageUnitId, state);
|
|
1923
|
-
|
|
1924
|
-
const result = await cmdCtx!.newSession();
|
|
1925
|
-
if (result.cancelled) {
|
|
1926
|
-
await stopAuto(ctx, pi);
|
|
1927
|
-
return;
|
|
1928
|
-
}
|
|
1929
|
-
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1930
|
-
writeLock(lockBase(), triageUnitType, triageUnitId, completedUnits.length, sessionFile);
|
|
1931
|
-
|
|
1932
|
-
// Start unit timeout for triage (use same supervisor config as hooks)
|
|
1933
|
-
clearUnitTimeout();
|
|
1934
|
-
const supervisor = resolveAutoSupervisorConfig();
|
|
1935
|
-
const triageTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
|
|
1936
|
-
unitTimeoutHandle = setTimeout(async () => {
|
|
1937
|
-
unitTimeoutHandle = null;
|
|
1938
|
-
if (!active) return;
|
|
1939
|
-
ctx.ui.notify(
|
|
1940
|
-
`Triage unit exceeded timeout. Pausing auto-mode.`,
|
|
1941
|
-
"warning",
|
|
1942
|
-
);
|
|
1943
|
-
await pauseAuto(ctx, pi);
|
|
1944
|
-
}, triageTimeoutMs);
|
|
1945
|
-
|
|
1946
|
-
if (!active) return;
|
|
1947
|
-
pi.sendMessage(
|
|
1948
|
-
{ customType: "gsd-auto", content: prompt, display: verbose },
|
|
1949
|
-
{ triggerTurn: true },
|
|
1950
|
-
);
|
|
1951
|
-
return; // handleAgentEnd will fire again when triage session completes
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
} catch {
|
|
1956
|
-
// Triage check failure is non-fatal — proceed to normal dispatch
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
// ── Quick-task dispatch: execute queued quick-tasks from triage resolution ──
|
|
1961
|
-
// Quick-tasks are self-contained one-off tasks that don't modify the plan.
|
|
1962
|
-
// They're queued during post-triage resolution and dispatched here one at a time.
|
|
1963
|
-
if (
|
|
1964
|
-
!stepMode &&
|
|
1965
|
-
pendingQuickTasks.length > 0 &&
|
|
1966
|
-
currentUnit &&
|
|
1967
|
-
currentUnit.type !== "quick-task"
|
|
1968
|
-
) {
|
|
1969
|
-
try {
|
|
1970
|
-
const capture = pendingQuickTasks.shift()!;
|
|
1971
|
-
const { buildQuickTaskPrompt } = await import("./triage-resolution.js");
|
|
1972
|
-
const { markCaptureExecuted } = await import("./captures.js");
|
|
1973
|
-
const prompt = buildQuickTaskPrompt(capture);
|
|
1974
|
-
|
|
1975
|
-
ctx.ui.notify(
|
|
1976
|
-
`Executing quick-task: ${capture.id} — "${capture.text}"`,
|
|
1977
|
-
"info",
|
|
1978
|
-
);
|
|
1979
|
-
|
|
1980
|
-
// Close out previous unit metrics
|
|
1981
|
-
if (currentUnit) {
|
|
1982
|
-
const modelId = ctx.model?.id ?? "unknown";
|
|
1983
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1984
|
-
const qtActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1985
|
-
if (qtActivityFile) {
|
|
1986
|
-
try {
|
|
1987
|
-
const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
|
|
1988
|
-
const llmCallFn = buildMemoryLLMCall(ctx);
|
|
1989
|
-
if (llmCallFn) {
|
|
1990
|
-
extractMemoriesFromUnit(qtActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
|
|
1991
|
-
}
|
|
1992
|
-
} catch { /* non-fatal */ }
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
// Dispatch quick-task as a new unit
|
|
1997
|
-
const qtUnitType = "quick-task";
|
|
1998
|
-
const qtUnitId = `${currentMilestoneId}/${capture.id}`;
|
|
1999
|
-
const qtStartedAt = Date.now();
|
|
2000
|
-
currentUnit = { type: qtUnitType, id: qtUnitId, startedAt: qtStartedAt };
|
|
2001
|
-
writeUnitRuntimeRecord(basePath, qtUnitType, qtUnitId, qtStartedAt, {
|
|
2002
|
-
phase: "dispatched",
|
|
2003
|
-
wrapupWarningSent: false,
|
|
2004
|
-
timeoutAt: null,
|
|
2005
|
-
lastProgressAt: qtStartedAt,
|
|
2006
|
-
progressCount: 0,
|
|
2007
|
-
lastProgressKind: "dispatch",
|
|
2008
|
-
});
|
|
2009
|
-
const state = await deriveState(basePath);
|
|
2010
|
-
updateProgressWidget(ctx, qtUnitType, qtUnitId, state);
|
|
2011
|
-
|
|
2012
|
-
const result = await cmdCtx!.newSession();
|
|
2013
|
-
if (result.cancelled) {
|
|
2014
|
-
await stopAuto(ctx, pi);
|
|
2015
|
-
return;
|
|
2016
|
-
}
|
|
2017
|
-
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
2018
|
-
writeLock(lockBase(), qtUnitType, qtUnitId, completedUnits.length, sessionFile);
|
|
2019
|
-
|
|
2020
|
-
// Mark capture as executed now that the unit is dispatched
|
|
2021
|
-
markCaptureExecuted(basePath, capture.id);
|
|
2022
|
-
|
|
2023
|
-
// Start unit timeout for quick-task
|
|
2024
|
-
clearUnitTimeout();
|
|
2025
|
-
const supervisor = resolveAutoSupervisorConfig();
|
|
2026
|
-
const qtTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
|
|
2027
|
-
unitTimeoutHandle = setTimeout(async () => {
|
|
2028
|
-
unitTimeoutHandle = null;
|
|
2029
|
-
if (!active) return;
|
|
2030
|
-
ctx.ui.notify(
|
|
2031
|
-
`Quick-task ${capture.id} exceeded timeout. Pausing auto-mode.`,
|
|
2032
|
-
"warning",
|
|
2033
|
-
);
|
|
2034
|
-
await pauseAuto(ctx, pi);
|
|
2035
|
-
}, qtTimeoutMs);
|
|
2036
|
-
|
|
2037
|
-
if (!active) return;
|
|
2038
|
-
pi.sendMessage(
|
|
2039
|
-
{ customType: "gsd-auto", content: prompt, display: verbose },
|
|
2040
|
-
{ triggerTurn: true },
|
|
2041
|
-
);
|
|
2042
|
-
return; // handleAgentEnd will fire again when quick-task session completes
|
|
2043
|
-
} catch {
|
|
2044
|
-
// Non-fatal — proceed to normal dispatch
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
// In step mode, pause and show a wizard instead of immediately dispatching
|
|
2049
|
-
if (stepMode) {
|
|
2050
|
-
await showStepWizard(ctx, pi);
|
|
2051
|
-
return;
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
try {
|
|
2055
|
-
await dispatchNextUnit(ctx, pi);
|
|
2056
|
-
} catch (dispatchErr) {
|
|
2057
|
-
// dispatchNextUnit threw — without this catch the error would propagate
|
|
2058
|
-
// to the pi event emitter which may silently swallow async rejections,
|
|
2059
|
-
// leaving auto-mode active but permanently stalled (see #381).
|
|
2060
|
-
const message = dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr);
|
|
2061
|
-
ctx.ui.notify(
|
|
2062
|
-
`Dispatch error after unit completion: ${message}. Retrying in ${DISPATCH_GAP_TIMEOUT_MS / 1000}s.`,
|
|
2063
|
-
"error",
|
|
2064
|
-
);
|
|
2065
|
-
|
|
2066
|
-
// Start the dispatch gap watchdog to retry after a delay.
|
|
2067
|
-
// This gives transient issues (dirty working tree, branch state) time to settle.
|
|
2068
|
-
startDispatchGapWatchdog(ctx, pi);
|
|
2069
|
-
return;
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
|
-
// If dispatchNextUnit returned normally but auto-mode is still active and
|
|
2073
|
-
// no new unit timeout was set (meaning sendMessage was never called), start
|
|
2074
|
-
// the dispatch gap watchdog as a safety net.
|
|
2075
|
-
if (active && !unitTimeoutHandle && !wrapupWarningHandle) {
|
|
2076
|
-
startDispatchGapWatchdog(ctx, pi);
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
} finally {
|
|
2080
|
-
_handlingAgentEnd = false;
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
// ─── Step Mode Wizard ─────────────────────────────────────────────────────
|
|
2085
|
-
|
|
2086
|
-
/**
|
|
2087
|
-
* Show the step-mode wizard after a unit completes.
|
|
2088
|
-
* Derives the next unit from disk state and presents it to the user.
|
|
2089
|
-
* If the user confirms, dispatches the next unit. If not, pauses.
|
|
2090
|
-
*/
|
|
2091
|
-
async function showStepWizard(
|
|
2092
|
-
ctx: ExtensionContext,
|
|
2093
|
-
pi: ExtensionAPI,
|
|
2094
|
-
): Promise<void> {
|
|
2095
|
-
if (!cmdCtx) return;
|
|
2096
|
-
|
|
2097
|
-
const state = await deriveState(basePath);
|
|
2098
|
-
const mid = state.activeMilestone?.id;
|
|
2099
|
-
|
|
2100
|
-
// Build summary of what just completed
|
|
2101
|
-
const justFinished = currentUnit
|
|
2102
|
-
? `${unitVerb(currentUnit.type)} ${currentUnit.id}`
|
|
2103
|
-
: "previous unit";
|
|
2104
|
-
|
|
2105
|
-
// If no active milestone or everything is complete, stop
|
|
2106
|
-
if (!mid || state.phase === "complete") {
|
|
2107
|
-
const incomplete = state.registry.filter(m => m.status !== "complete");
|
|
2108
|
-
if (incomplete.length > 0 && state.phase !== "complete" && state.phase !== "blocked") {
|
|
2109
|
-
const ids = incomplete.map(m => m.id).join(", ");
|
|
2110
|
-
const diag = `basePath=${basePath}, milestones=[${state.registry.map(m => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
2111
|
-
ctx.ui.notify(`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`, "error");
|
|
2112
|
-
await stopAuto(ctx, pi, `No active milestone — ${incomplete.length} incomplete (${ids})`);
|
|
2113
|
-
} else {
|
|
2114
|
-
await stopAuto(ctx, pi, state.phase === "complete" ? "All work complete" : "No active milestone");
|
|
2115
|
-
}
|
|
2116
|
-
return;
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
// Peek at what's next by examining state
|
|
2120
892
|
const nextDesc = _describeNextUnit(state);
|
|
2121
893
|
|
|
2122
|
-
const choice = await showNextAction(cmdCtx, {
|
|
894
|
+
const choice = await showNextAction(s.cmdCtx, {
|
|
2123
895
|
title: `GSD — ${justFinished} complete`,
|
|
2124
896
|
summary: [
|
|
2125
897
|
`${mid}: ${state.activeMilestone?.title ?? mid}`,
|
|
@@ -2149,17 +921,15 @@ async function showStepWizard(
|
|
|
2149
921
|
if (choice === "continue") {
|
|
2150
922
|
await dispatchNextUnit(ctx, pi);
|
|
2151
923
|
} else if (choice === "auto") {
|
|
2152
|
-
stepMode = false;
|
|
924
|
+
s.stepMode = false;
|
|
2153
925
|
ctx.ui.setStatus("gsd-auto", "auto");
|
|
2154
926
|
ctx.ui.notify("Switched to auto-mode.", "info");
|
|
2155
927
|
await dispatchNextUnit(ctx, pi);
|
|
2156
928
|
} else if (choice === "status") {
|
|
2157
|
-
// Show status then re-show the wizard
|
|
2158
929
|
const { fireStatusViaCommand } = await import("./commands.js");
|
|
2159
930
|
await fireStatusViaCommand(ctx as ExtensionCommandContext);
|
|
2160
931
|
await showStepWizard(ctx, pi);
|
|
2161
932
|
} else {
|
|
2162
|
-
// "not_yet" — pause
|
|
2163
933
|
await pauseAuto(ctx, pi);
|
|
2164
934
|
}
|
|
2165
935
|
}
|
|
@@ -2174,85 +944,81 @@ function updateProgressWidget(
|
|
|
2174
944
|
unitId: string,
|
|
2175
945
|
state: GSDState,
|
|
2176
946
|
): void {
|
|
2177
|
-
const badge = currentUnitRouting?.tier
|
|
2178
|
-
? ({ light: "L", standard: "S", heavy: "H" }[currentUnitRouting.tier] ?? undefined)
|
|
947
|
+
const badge = s.currentUnitRouting?.tier
|
|
948
|
+
? ({ light: "L", standard: "S", heavy: "H" }[s.currentUnitRouting.tier] ?? undefined)
|
|
2179
949
|
: undefined;
|
|
2180
950
|
_updateProgressWidget(ctx, unitType, unitId, state, widgetStateAccessors, badge);
|
|
2181
951
|
}
|
|
2182
952
|
|
|
2183
953
|
/** State accessors for the widget — closures over module globals. */
|
|
2184
954
|
const widgetStateAccessors: WidgetStateAccessors = {
|
|
2185
|
-
getAutoStartTime: () => autoStartTime,
|
|
2186
|
-
isStepMode: () => stepMode,
|
|
2187
|
-
getCmdCtx: () => cmdCtx,
|
|
2188
|
-
getBasePath: () => basePath,
|
|
2189
|
-
isVerbose: () => verbose,
|
|
955
|
+
getAutoStartTime: () => s.autoStartTime,
|
|
956
|
+
isStepMode: () => s.stepMode,
|
|
957
|
+
getCmdCtx: () => s.cmdCtx,
|
|
958
|
+
getBasePath: () => s.basePath,
|
|
959
|
+
isVerbose: () => s.verbose,
|
|
2190
960
|
};
|
|
2191
961
|
|
|
2192
962
|
// ─── Core Loop ────────────────────────────────────────────────────────────────
|
|
2193
963
|
|
|
2194
|
-
/** Tracks recursive skip depth to prevent TUI freeze on cascading completed-unit skips */
|
|
2195
|
-
let _skipDepth = 0;
|
|
2196
|
-
const MAX_SKIP_DEPTH = 20;
|
|
2197
|
-
|
|
2198
|
-
/** Reentrancy guard for dispatchNextUnit itself (not just handleAgentEnd).
|
|
2199
|
-
* Prevents concurrent dispatch from watchdog timers, step wizard, and direct calls
|
|
2200
|
-
* that bypass the _handlingAgentEnd guard. Recursive calls (from skip paths) are
|
|
2201
|
-
* allowed via _skipDepth > 0. */
|
|
2202
|
-
let _dispatching = false;
|
|
2203
|
-
|
|
2204
964
|
async function dispatchNextUnit(
|
|
2205
965
|
ctx: ExtensionContext,
|
|
2206
966
|
pi: ExtensionAPI,
|
|
2207
967
|
): Promise<void> {
|
|
2208
|
-
if (!active || !cmdCtx) {
|
|
2209
|
-
debugLog(`dispatchNextUnit early return — active=${active}, cmdCtx=${!!cmdCtx}`);
|
|
2210
|
-
if (active && !cmdCtx) {
|
|
968
|
+
if (!s.active || !s.cmdCtx) {
|
|
969
|
+
debugLog(`dispatchNextUnit early return — active=${s.active}, cmdCtx=${!!s.cmdCtx}`);
|
|
970
|
+
if (s.active && !s.cmdCtx) {
|
|
2211
971
|
ctx.ui.notify("Auto-mode session expired. Run /gsd auto to restart.", "info");
|
|
2212
972
|
}
|
|
2213
973
|
return;
|
|
2214
974
|
}
|
|
2215
975
|
|
|
2216
|
-
//
|
|
2217
|
-
|
|
2218
|
-
|
|
976
|
+
// ── Session lock validation: detect if another process has taken over ──
|
|
977
|
+
if (lockBase() && !validateSessionLock(lockBase())) {
|
|
978
|
+
debugLog("dispatchNextUnit session-lock-lost — another process may have taken over");
|
|
979
|
+
ctx.ui.notify(
|
|
980
|
+
"Session lock lost — another GSD process appears to have taken over. Stopping gracefully.",
|
|
981
|
+
"error",
|
|
982
|
+
);
|
|
983
|
+
// Don't call stopAuto here to avoid releasing the lock we don't own
|
|
984
|
+
s.active = false;
|
|
985
|
+
s.paused = false;
|
|
986
|
+
clearUnitTimeout();
|
|
987
|
+
deregisterSigtermHandler();
|
|
988
|
+
ctx.ui.setStatus("gsd-auto", undefined);
|
|
989
|
+
ctx.ui.setWidget("gsd-progress", undefined);
|
|
990
|
+
ctx.ui.setFooter(undefined);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Reentrancy guard
|
|
995
|
+
if (s.dispatching && s.skipDepth === 0) {
|
|
2219
996
|
debugLog("dispatchNextUnit reentrancy guard — another dispatch in progress, bailing");
|
|
2220
|
-
return;
|
|
997
|
+
return;
|
|
2221
998
|
}
|
|
2222
|
-
|
|
999
|
+
s.dispatching = true;
|
|
2223
1000
|
try {
|
|
2224
|
-
// Recursion depth guard
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
if (_skipDepth > MAX_SKIP_DEPTH) {
|
|
2228
|
-
_skipDepth = 0;
|
|
1001
|
+
// Recursion depth guard
|
|
1002
|
+
if (s.skipDepth > MAX_SKIP_DEPTH) {
|
|
1003
|
+
s.skipDepth = 0;
|
|
2229
1004
|
ctx.ui.notify(`Skipped ${MAX_SKIP_DEPTH}+ completed units. Yielding to UI before continuing.`, "info");
|
|
2230
1005
|
await new Promise(r => setTimeout(r, 200));
|
|
2231
1006
|
}
|
|
2232
1007
|
|
|
2233
|
-
// Resource version guard
|
|
2234
|
-
|
|
2235
|
-
// once at startup. If resources were re-synced (e.g. /gsd:update, npm update,
|
|
2236
|
-
// or dev copy-resources), templates may expect variables the in-memory code
|
|
2237
|
-
// doesn't provide. Stop gracefully instead of crashing.
|
|
2238
|
-
const staleMsg = checkResourcesStale();
|
|
1008
|
+
// Resource version guard
|
|
1009
|
+
const staleMsg = checkResourcesStale(s.resourceVersionOnStart);
|
|
2239
1010
|
if (staleMsg) {
|
|
2240
1011
|
await stopAuto(ctx, pi, staleMsg);
|
|
2241
1012
|
return;
|
|
2242
1013
|
}
|
|
2243
1014
|
|
|
2244
|
-
// Clear all caches so deriveState sees fresh disk state (#431).
|
|
2245
|
-
// Parse cache is also cleared — doctor may have re-populated it with
|
|
2246
|
-
// stale data between handleAgentEnd and this dispatch call (Path B fix).
|
|
2247
1015
|
invalidateAllCaches();
|
|
2248
|
-
lastPromptCharCount = undefined;
|
|
2249
|
-
lastBaselineCharCount = undefined;
|
|
1016
|
+
s.lastPromptCharCount = undefined;
|
|
1017
|
+
s.lastBaselineCharCount = undefined;
|
|
2250
1018
|
|
|
2251
|
-
// ── Pre-dispatch health gate
|
|
2252
|
-
// Lightweight check for critical issues that would cause the next unit
|
|
2253
|
-
// to fail or corrupt state. Auto-heals what it can, blocks on the rest.
|
|
1019
|
+
// ── Pre-dispatch health gate ──
|
|
2254
1020
|
try {
|
|
2255
|
-
const healthGate = await preDispatchHealthGate(basePath);
|
|
1021
|
+
const healthGate = await preDispatchHealthGate(s.basePath);
|
|
2256
1022
|
if (healthGate.fixesApplied.length > 0) {
|
|
2257
1023
|
ctx.ui.notify(`Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`, "info");
|
|
2258
1024
|
}
|
|
@@ -2262,19 +1028,16 @@ async function dispatchNextUnit(
|
|
|
2262
1028
|
return;
|
|
2263
1029
|
}
|
|
2264
1030
|
} catch {
|
|
2265
|
-
// Non-fatal
|
|
1031
|
+
// Non-fatal
|
|
2266
1032
|
}
|
|
2267
1033
|
|
|
2268
|
-
// ── Sync project root artifacts into worktree
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
// deriveState to ensure the worktree has the latest artifacts.
|
|
2272
|
-
if (originalBasePath && basePath !== originalBasePath && currentMilestoneId) {
|
|
2273
|
-
syncProjectRootToWorktree(originalBasePath, basePath, currentMilestoneId);
|
|
1034
|
+
// ── Sync project root artifacts into worktree ──
|
|
1035
|
+
if (s.originalBasePath && s.basePath !== s.originalBasePath && s.currentMilestoneId) {
|
|
1036
|
+
syncProjectRootToWorktree(s.originalBasePath, s.basePath, s.currentMilestoneId);
|
|
2274
1037
|
}
|
|
2275
1038
|
|
|
2276
1039
|
const stopDeriveTimer = debugTime("derive-state");
|
|
2277
|
-
let state = await deriveState(basePath);
|
|
1040
|
+
let state = await deriveState(s.basePath);
|
|
2278
1041
|
stopDeriveTimer({
|
|
2279
1042
|
phase: state.phase,
|
|
2280
1043
|
milestone: state.activeMilestone?.id,
|
|
@@ -2285,79 +1048,115 @@ async function dispatchNextUnit(
|
|
|
2285
1048
|
let midTitle = state.activeMilestone?.title;
|
|
2286
1049
|
|
|
2287
1050
|
// Detect milestone transition
|
|
2288
|
-
if (mid && currentMilestoneId && mid !== currentMilestoneId) {
|
|
1051
|
+
if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
|
|
2289
1052
|
ctx.ui.notify(
|
|
2290
|
-
`Milestone ${currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`,
|
|
1053
|
+
`Milestone ${ s.currentMilestoneId } complete. Advancing to ${mid}: ${midTitle}.`,
|
|
2291
1054
|
"info",
|
|
2292
1055
|
);
|
|
2293
|
-
sendDesktopNotification("GSD", `Milestone ${currentMilestoneId} complete!`, "success", "milestone");
|
|
2294
|
-
// Hint: visualizer available after milestone transition
|
|
1056
|
+
sendDesktopNotification("GSD", `Milestone ${s.currentMilestoneId} complete!`, "success", "milestone");
|
|
2295
1057
|
const vizPrefs = loadEffectiveGSDPreferences()?.preferences;
|
|
2296
1058
|
if (vizPrefs?.auto_visualize) {
|
|
2297
1059
|
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
2298
1060
|
}
|
|
1061
|
+
if (vizPrefs?.auto_report !== false) {
|
|
1062
|
+
try {
|
|
1063
|
+
const { loadVisualizerData } = await import("./visualizer-data.js");
|
|
1064
|
+
const { generateHtmlReport } = await import("./export-html.js");
|
|
1065
|
+
const { writeReportSnapshot, reportsDir } = await import("./reports.js");
|
|
1066
|
+
const { basename } = await import("node:path");
|
|
1067
|
+
const snapData = await loadVisualizerData(s.basePath);
|
|
1068
|
+
const completedMs = snapData.milestones.find(m => m.id === s.currentMilestoneId);
|
|
1069
|
+
const msTitle = completedMs?.title ?? s.currentMilestoneId;
|
|
1070
|
+
const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
|
|
1071
|
+
const projName = basename(s.basePath);
|
|
1072
|
+
const doneSlices = snapData.milestones.reduce((s, m) => s + m.slices.filter(sl => sl.done).length, 0);
|
|
1073
|
+
const totalSlices = snapData.milestones.reduce((s, m) => s + m.slices.length, 0);
|
|
1074
|
+
const outPath = writeReportSnapshot({ basePath: s.basePath,
|
|
1075
|
+
html: generateHtmlReport(snapData, {
|
|
1076
|
+
projectName: projName,
|
|
1077
|
+
projectPath: s.basePath,
|
|
1078
|
+
gsdVersion,
|
|
1079
|
+
milestoneId: s.currentMilestoneId,
|
|
1080
|
+
indexRelPath: "index.html",
|
|
1081
|
+
}),
|
|
1082
|
+
milestoneId: s.currentMilestoneId,
|
|
1083
|
+
milestoneTitle: msTitle,
|
|
1084
|
+
kind: "milestone",
|
|
1085
|
+
projectName: projName,
|
|
1086
|
+
projectPath: s.basePath,
|
|
1087
|
+
gsdVersion,
|
|
1088
|
+
totalCost: snapData.totals?.cost ?? 0,
|
|
1089
|
+
totalTokens: snapData.totals?.tokens.total ?? 0,
|
|
1090
|
+
totalDuration: snapData.totals?.duration ?? 0,
|
|
1091
|
+
doneSlices,
|
|
1092
|
+
totalSlices,
|
|
1093
|
+
doneMilestones: snapData.milestones.filter(m => m.status === "complete").length,
|
|
1094
|
+
totalMilestones: snapData.milestones.length,
|
|
1095
|
+
phase: snapData.phase,
|
|
1096
|
+
});
|
|
1097
|
+
ctx.ui.notify(
|
|
1098
|
+
`Report saved: .gsd/reports/${basename(outPath)} — open index.html to browse progression.`,
|
|
1099
|
+
"info",
|
|
1100
|
+
);
|
|
1101
|
+
} catch (err) {
|
|
1102
|
+
ctx.ui.notify(
|
|
1103
|
+
`Report generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1104
|
+
"warning",
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
2299
1108
|
// Reset stuck detection for new milestone
|
|
2300
|
-
unitDispatchCount.clear();
|
|
2301
|
-
unitRecoveryCount.clear();
|
|
2302
|
-
unitConsecutiveSkips.clear();
|
|
2303
|
-
unitLifetimeDispatches.clear();
|
|
2304
|
-
// Clear completed-units.json for the finished milestone
|
|
1109
|
+
s.unitDispatchCount.clear();
|
|
1110
|
+
s.unitRecoveryCount.clear();
|
|
1111
|
+
s.unitConsecutiveSkips.clear();
|
|
1112
|
+
s.unitLifetimeDispatches.clear();
|
|
2305
1113
|
try {
|
|
2306
|
-
const file = completedKeysPath(basePath);
|
|
2307
|
-
if (existsSync(file))
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
//
|
|
2314
|
-
|
|
2315
|
-
// 3. Create a new worktree for the incoming milestone
|
|
2316
|
-
// Without this, M_new runs inside M_old's worktree on the wrong branch,
|
|
2317
|
-
// and artifact paths resolve against the wrong .gsd/ directory.
|
|
2318
|
-
if (isInAutoWorktree(basePath) && originalBasePath && shouldUseWorktreeIsolation()) {
|
|
1114
|
+
const file = completedKeysPath(s.basePath);
|
|
1115
|
+
if (existsSync(file)) {
|
|
1116
|
+
atomicWriteSync(file, JSON.stringify([]));
|
|
1117
|
+
}
|
|
1118
|
+
s.completedKeySet.clear();
|
|
1119
|
+
} catch (e) { debugLog("completed-keys-reset-failed", { error: e instanceof Error ? e.message : String(e) }); }
|
|
1120
|
+
|
|
1121
|
+
// ── Worktree lifecycle on milestone transition (#616) ──
|
|
1122
|
+
if (isInAutoWorktree(s.basePath) && s.originalBasePath && shouldUseWorktreeIsolation()) {
|
|
2319
1123
|
try {
|
|
2320
|
-
const roadmapPath = resolveMilestoneFile(originalBasePath, currentMilestoneId, "ROADMAP");
|
|
1124
|
+
const roadmapPath = resolveMilestoneFile(s.originalBasePath, s.currentMilestoneId, "ROADMAP");
|
|
2321
1125
|
if (roadmapPath) {
|
|
2322
1126
|
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
2323
|
-
const mergeResult = mergeMilestoneToMain(originalBasePath, currentMilestoneId, roadmapContent);
|
|
1127
|
+
const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
|
|
2324
1128
|
ctx.ui.notify(
|
|
2325
|
-
`Milestone ${currentMilestoneId} merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1129
|
+
`Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
2326
1130
|
"info",
|
|
2327
1131
|
);
|
|
2328
1132
|
} else {
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
ctx.ui.notify(`Exited worktree for ${currentMilestoneId} (no roadmap for merge).`, "info");
|
|
1133
|
+
teardownAutoWorktree(s.originalBasePath, s.currentMilestoneId);
|
|
1134
|
+
ctx.ui.notify(`Exited worktree for ${ s.currentMilestoneId } (no roadmap for merge).`, "info");
|
|
2332
1135
|
}
|
|
2333
1136
|
} catch (err) {
|
|
2334
1137
|
ctx.ui.notify(
|
|
2335
1138
|
`Milestone merge failed during transition: ${err instanceof Error ? err.message : String(err)}`,
|
|
2336
1139
|
"warning",
|
|
2337
1140
|
);
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
try { process.chdir(originalBasePath); } catch { /* best-effort */ }
|
|
1141
|
+
if (s.originalBasePath) {
|
|
1142
|
+
try { process.chdir(s.originalBasePath); } catch { /* best-effort */ }
|
|
2341
1143
|
}
|
|
2342
1144
|
}
|
|
2343
1145
|
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
1146
|
+
s.basePath = s.originalBasePath;
|
|
1147
|
+
s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
2347
1148
|
invalidateAllCaches();
|
|
2348
1149
|
|
|
2349
|
-
|
|
2350
|
-
state = await deriveState(basePath);
|
|
1150
|
+
state = await deriveState(s.basePath);
|
|
2351
1151
|
mid = state.activeMilestone?.id;
|
|
2352
1152
|
midTitle = state.activeMilestone?.title;
|
|
2353
1153
|
|
|
2354
|
-
// Create new worktree for the incoming milestone
|
|
2355
1154
|
if (mid) {
|
|
2356
|
-
captureIntegrationBranch(basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
|
|
1155
|
+
captureIntegrationBranch(s.basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
|
|
2357
1156
|
try {
|
|
2358
|
-
const wtPath = createAutoWorktree(basePath, mid);
|
|
2359
|
-
basePath = wtPath;
|
|
2360
|
-
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
1157
|
+
const wtPath = createAutoWorktree(s.basePath, mid);
|
|
1158
|
+
s.basePath = wtPath;
|
|
1159
|
+
s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
2361
1160
|
ctx.ui.notify(`Created auto-worktree for ${mid} at ${wtPath}`, "info");
|
|
2362
1161
|
} catch (err) {
|
|
2363
1162
|
ctx.ui.notify(
|
|
@@ -2367,75 +1166,107 @@ async function dispatchNextUnit(
|
|
|
2367
1166
|
}
|
|
2368
1167
|
}
|
|
2369
1168
|
} else {
|
|
2370
|
-
// Not in worktree — capture integration branch for the new milestone (branch mode only).
|
|
2371
|
-
// In none mode there's no milestone branch to merge back to, so skip.
|
|
2372
1169
|
if (getIsolationMode() !== "none") {
|
|
2373
|
-
captureIntegrationBranch(originalBasePath || basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
|
|
1170
|
+
captureIntegrationBranch(s.originalBasePath || s.basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
|
|
2374
1171
|
}
|
|
2375
1172
|
}
|
|
2376
1173
|
|
|
2377
|
-
// Prune completed milestone from queue order file
|
|
2378
1174
|
const pendingIds = state.registry
|
|
2379
1175
|
.filter(m => m.status !== "complete")
|
|
2380
1176
|
.map(m => m.id);
|
|
2381
|
-
pruneQueueOrder(basePath, pendingIds);
|
|
1177
|
+
pruneQueueOrder(s.basePath, pendingIds);
|
|
2382
1178
|
}
|
|
2383
1179
|
if (mid) {
|
|
2384
|
-
currentMilestoneId = mid;
|
|
2385
|
-
setActiveMilestoneId(basePath, mid);
|
|
1180
|
+
s.currentMilestoneId = mid;
|
|
1181
|
+
setActiveMilestoneId(s.basePath, mid);
|
|
2386
1182
|
}
|
|
2387
1183
|
|
|
2388
1184
|
if (!mid) {
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
const modelId = ctx.model?.id ?? "unknown";
|
|
2392
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2393
|
-
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1185
|
+
if (s.currentUnit) {
|
|
1186
|
+
await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
2394
1187
|
}
|
|
2395
1188
|
|
|
2396
|
-
const incomplete = state.registry.filter(m => m.status !== "complete");
|
|
1189
|
+
const incomplete = state.registry.filter(m => m.status !== "complete" && m.status !== "parked");
|
|
2397
1190
|
if (incomplete.length === 0) {
|
|
2398
|
-
// Genuinely all complete
|
|
1191
|
+
// Genuinely all complete (parked milestones excluded) — merge milestone branch to main before stopping (#962)
|
|
1192
|
+
if (s.currentMilestoneId && isInAutoWorktree(s.basePath) && s.originalBasePath) {
|
|
1193
|
+
try {
|
|
1194
|
+
const roadmapPath = resolveMilestoneFile(s.originalBasePath, s.currentMilestoneId, "ROADMAP");
|
|
1195
|
+
if (roadmapPath) {
|
|
1196
|
+
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1197
|
+
const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
|
|
1198
|
+
s.basePath = s.originalBasePath;
|
|
1199
|
+
s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
1200
|
+
ctx.ui.notify(
|
|
1201
|
+
`Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1202
|
+
"info",
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
} catch (err) {
|
|
1206
|
+
ctx.ui.notify(
|
|
1207
|
+
`Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1208
|
+
"warning",
|
|
1209
|
+
);
|
|
1210
|
+
if (s.originalBasePath) {
|
|
1211
|
+
s.basePath = s.originalBasePath;
|
|
1212
|
+
try { process.chdir(s.basePath); } catch { /* best-effort */ }
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
} else if (s.currentMilestoneId && !isInAutoWorktree(s.basePath) && getIsolationMode() !== "none") {
|
|
1216
|
+
try {
|
|
1217
|
+
const currentBranch = getCurrentBranch(s.basePath);
|
|
1218
|
+
const milestoneBranch = autoWorktreeBranch(s.currentMilestoneId);
|
|
1219
|
+
if (currentBranch === milestoneBranch) {
|
|
1220
|
+
const roadmapPath = resolveMilestoneFile(s.basePath, s.currentMilestoneId, "ROADMAP");
|
|
1221
|
+
if (roadmapPath) {
|
|
1222
|
+
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1223
|
+
const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
|
|
1224
|
+
s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
1225
|
+
ctx.ui.notify(
|
|
1226
|
+
`Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1227
|
+
"info",
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
} catch (err) {
|
|
1232
|
+
ctx.ui.notify(
|
|
1233
|
+
`Milestone merge failed (branch mode): ${err instanceof Error ? err.message : String(err)}`,
|
|
1234
|
+
"warning",
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
2399
1238
|
sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
|
|
2400
1239
|
await stopAuto(ctx, pi, "All milestones complete");
|
|
2401
1240
|
} else if (state.phase === "blocked") {
|
|
2402
|
-
// Milestones exist but are dependency-blocked
|
|
2403
1241
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
2404
1242
|
await stopAuto(ctx, pi, blockerMsg);
|
|
2405
1243
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
2406
1244
|
sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
2407
1245
|
} else {
|
|
2408
|
-
// Milestones with remaining work exist but none became active — unexpected
|
|
2409
1246
|
const ids = incomplete.map(m => m.id).join(", ");
|
|
2410
|
-
const diag = `basePath=${basePath}, milestones=[${state.registry.map(m => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
1247
|
+
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map(m => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
2411
1248
|
ctx.ui.notify(`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`, "error");
|
|
2412
1249
|
await stopAuto(ctx, pi, `No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`);
|
|
2413
1250
|
}
|
|
2414
1251
|
return;
|
|
2415
1252
|
}
|
|
2416
1253
|
|
|
2417
|
-
// Guard: mid/midTitle must be defined strings from this point onward.
|
|
2418
|
-
// The !mid check above returns early if mid is falsy; midTitle comes from
|
|
2419
|
-
// the same object so it should always be present when mid is.
|
|
2420
1254
|
if (!midTitle) {
|
|
2421
|
-
midTitle = mid;
|
|
1255
|
+
midTitle = mid;
|
|
2422
1256
|
ctx.ui.notify(`Milestone ${mid} has no title in roadmap — using ID as fallback.`, "warning");
|
|
2423
1257
|
}
|
|
2424
1258
|
|
|
2425
|
-
// ── Mid-merge safety check
|
|
2426
|
-
if (reconcileMergeState(basePath, ctx)) {
|
|
1259
|
+
// ── Mid-merge safety check ──
|
|
1260
|
+
if (reconcileMergeState(s.basePath, ctx)) {
|
|
2427
1261
|
invalidateAllCaches();
|
|
2428
|
-
state = await deriveState(basePath);
|
|
1262
|
+
state = await deriveState(s.basePath);
|
|
2429
1263
|
mid = state.activeMilestone?.id;
|
|
2430
1264
|
midTitle = state.activeMilestone?.title;
|
|
2431
1265
|
}
|
|
2432
1266
|
|
|
2433
|
-
// After merge guard removal (branchless architecture), mid/midTitle could be undefined
|
|
2434
1267
|
if (!mid || !midTitle) {
|
|
2435
|
-
if (currentUnit) {
|
|
2436
|
-
|
|
2437
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2438
|
-
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1268
|
+
if (s.currentUnit) {
|
|
1269
|
+
await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
2439
1270
|
}
|
|
2440
1271
|
const noMilestoneReason = !mid
|
|
2441
1272
|
? "No active milestone after merge reconciliation"
|
|
@@ -2450,28 +1281,27 @@ async function dispatchNextUnit(
|
|
|
2450
1281
|
let prompt: string;
|
|
2451
1282
|
|
|
2452
1283
|
if (state.phase === "complete") {
|
|
2453
|
-
if (currentUnit) {
|
|
2454
|
-
|
|
2455
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2456
|
-
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1284
|
+
if (s.currentUnit) {
|
|
1285
|
+
await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
2457
1286
|
}
|
|
2458
|
-
// Clear completed-units.json for the finished milestone so it doesn't grow unbounded.
|
|
2459
1287
|
try {
|
|
2460
|
-
const file = completedKeysPath(basePath);
|
|
2461
|
-
if (existsSync(file))
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
1288
|
+
const file = completedKeysPath(s.basePath);
|
|
1289
|
+
if (existsSync(file)) {
|
|
1290
|
+
atomicWriteSync(file, JSON.stringify([]));
|
|
1291
|
+
}
|
|
1292
|
+
s.completedKeySet.clear();
|
|
1293
|
+
} catch (e) { debugLog("completed-keys-reset-failed", { error: e instanceof Error ? e.message : String(e) }); }
|
|
1294
|
+
// ── Milestone merge ──
|
|
1295
|
+
if (s.currentMilestoneId && isInAutoWorktree(s.basePath) && s.originalBasePath) {
|
|
2466
1296
|
try {
|
|
2467
|
-
const roadmapPath = resolveMilestoneFile(originalBasePath, currentMilestoneId, "ROADMAP");
|
|
2468
|
-
if (!roadmapPath) throw new
|
|
1297
|
+
const roadmapPath = resolveMilestoneFile(s.originalBasePath, s.currentMilestoneId, "ROADMAP");
|
|
1298
|
+
if (!roadmapPath) throw new GSDError(GSD_ARTIFACT_MISSING, `Cannot resolve ROADMAP file for milestone ${ s.currentMilestoneId }`);
|
|
2469
1299
|
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
2470
|
-
const mergeResult = mergeMilestoneToMain(originalBasePath, currentMilestoneId, roadmapContent);
|
|
2471
|
-
basePath = originalBasePath;
|
|
2472
|
-
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
1300
|
+
const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
|
|
1301
|
+
s.basePath = s.originalBasePath;
|
|
1302
|
+
s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
2473
1303
|
ctx.ui.notify(
|
|
2474
|
-
`Milestone ${currentMilestoneId} merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1304
|
+
`Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
2475
1305
|
"info",
|
|
2476
1306
|
);
|
|
2477
1307
|
} catch (err) {
|
|
@@ -2479,30 +1309,23 @@ async function dispatchNextUnit(
|
|
|
2479
1309
|
`Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2480
1310
|
"warning",
|
|
2481
1311
|
);
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
if (originalBasePath) {
|
|
2486
|
-
basePath = originalBasePath;
|
|
2487
|
-
try { process.chdir(basePath); } catch { /* best-effort */ }
|
|
1312
|
+
if (s.originalBasePath) {
|
|
1313
|
+
s.basePath = s.originalBasePath;
|
|
1314
|
+
try { process.chdir(s.basePath); } catch { /* best-effort */ }
|
|
2488
1315
|
}
|
|
2489
1316
|
}
|
|
2490
|
-
} else if (currentMilestoneId && !isInAutoWorktree(basePath) && getIsolationMode() !== "none") {
|
|
2491
|
-
// Branch isolation mode (#603): no worktree, but we may be on a milestone/* branch.
|
|
2492
|
-
// Squash-merge back to the integration branch (or main) before stopping.
|
|
1317
|
+
} else if (s.currentMilestoneId && !isInAutoWorktree(s.basePath) && getIsolationMode() !== "none") {
|
|
2493
1318
|
try {
|
|
2494
|
-
const currentBranch = getCurrentBranch(basePath);
|
|
2495
|
-
const milestoneBranch = autoWorktreeBranch(currentMilestoneId);
|
|
1319
|
+
const currentBranch = getCurrentBranch(s.basePath);
|
|
1320
|
+
const milestoneBranch = autoWorktreeBranch(s.currentMilestoneId);
|
|
2496
1321
|
if (currentBranch === milestoneBranch) {
|
|
2497
|
-
const roadmapPath = resolveMilestoneFile(basePath, currentMilestoneId, "ROADMAP");
|
|
1322
|
+
const roadmapPath = resolveMilestoneFile(s.basePath, s.currentMilestoneId, "ROADMAP");
|
|
2498
1323
|
if (roadmapPath) {
|
|
2499
1324
|
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
const mergeResult = mergeMilestoneToMain(basePath, currentMilestoneId, roadmapContent);
|
|
2503
|
-
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
1325
|
+
const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
|
|
1326
|
+
s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
2504
1327
|
ctx.ui.notify(
|
|
2505
|
-
`Milestone ${currentMilestoneId} merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1328
|
+
`Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
2506
1329
|
"info",
|
|
2507
1330
|
);
|
|
2508
1331
|
}
|
|
@@ -2520,10 +1343,8 @@ async function dispatchNextUnit(
|
|
|
2520
1343
|
}
|
|
2521
1344
|
|
|
2522
1345
|
if (state.phase === "blocked") {
|
|
2523
|
-
if (currentUnit) {
|
|
2524
|
-
|
|
2525
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2526
|
-
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1346
|
+
if (s.currentUnit) {
|
|
1347
|
+
await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
2527
1348
|
}
|
|
2528
1349
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
2529
1350
|
await stopAuto(ctx, pi, blockerMsg);
|
|
@@ -2532,25 +1353,23 @@ async function dispatchNextUnit(
|
|
|
2532
1353
|
return;
|
|
2533
1354
|
}
|
|
2534
1355
|
|
|
2535
|
-
//
|
|
2536
|
-
// Ensures the UAT file and slice summary are both on main when UAT runs.
|
|
1356
|
+
// Budget ceiling guard, context window guard, secrets gate, dispatch table
|
|
2537
1357
|
const prefs = loadEffectiveGSDPreferences()?.preferences;
|
|
2538
1358
|
|
|
2539
|
-
// Budget ceiling guard — enforce budget with configurable action
|
|
2540
1359
|
const budgetCeiling = prefs?.budget_ceiling;
|
|
2541
1360
|
if (budgetCeiling !== undefined && budgetCeiling > 0) {
|
|
2542
1361
|
const currentLedger = getLedger();
|
|
2543
1362
|
const totalCost = currentLedger ? getProjectTotals(currentLedger.units).cost : 0;
|
|
2544
1363
|
const budgetPct = totalCost / budgetCeiling;
|
|
2545
1364
|
const budgetAlertLevel = getBudgetAlertLevel(budgetPct);
|
|
2546
|
-
const newBudgetAlertLevel = getNewBudgetAlertLevel(lastBudgetAlertLevel, budgetPct);
|
|
1365
|
+
const newBudgetAlertLevel = getNewBudgetAlertLevel(s.lastBudgetAlertLevel, budgetPct);
|
|
2547
1366
|
const enforcement = prefs?.budget_enforcement ?? "pause";
|
|
2548
1367
|
|
|
2549
1368
|
const budgetEnforcementAction = getBudgetEnforcementAction(enforcement, budgetPct);
|
|
2550
1369
|
|
|
2551
1370
|
if (newBudgetAlertLevel === 100 && budgetEnforcementAction !== "none") {
|
|
2552
1371
|
const msg = `Budget ceiling ${formatCost(budgetCeiling)} reached (spent ${formatCost(totalCost)}).`;
|
|
2553
|
-
lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
1372
|
+
s.lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
2554
1373
|
if (budgetEnforcementAction === "halt") {
|
|
2555
1374
|
sendDesktopNotification("GSD", msg, "error", "budget");
|
|
2556
1375
|
await stopAuto(ctx, pi, "Budget ceiling reached");
|
|
@@ -2565,28 +1384,27 @@ async function dispatchNextUnit(
|
|
|
2565
1384
|
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
2566
1385
|
sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
2567
1386
|
} else if (newBudgetAlertLevel === 90) {
|
|
2568
|
-
lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
1387
|
+
s.lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
2569
1388
|
ctx.ui.notify(`Budget 90%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "warning");
|
|
2570
1389
|
sendDesktopNotification("GSD", `Budget 90%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "warning", "budget");
|
|
2571
1390
|
} else if (newBudgetAlertLevel === 80) {
|
|
2572
|
-
lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
1391
|
+
s.lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
2573
1392
|
ctx.ui.notify(`Approaching budget ceiling — 80%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "warning");
|
|
2574
1393
|
sendDesktopNotification("GSD", `Approaching budget ceiling — 80%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "warning", "budget");
|
|
2575
1394
|
} else if (newBudgetAlertLevel === 75) {
|
|
2576
|
-
lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
1395
|
+
s.lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
2577
1396
|
ctx.ui.notify(`Budget 75%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "info");
|
|
2578
1397
|
sendDesktopNotification("GSD", `Budget 75%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "info", "budget");
|
|
2579
1398
|
} else if (budgetAlertLevel === 0) {
|
|
2580
|
-
lastBudgetAlertLevel = 0;
|
|
1399
|
+
s.lastBudgetAlertLevel = 0;
|
|
2581
1400
|
}
|
|
2582
1401
|
} else {
|
|
2583
|
-
lastBudgetAlertLevel = 0;
|
|
1402
|
+
s.lastBudgetAlertLevel = 0;
|
|
2584
1403
|
}
|
|
2585
1404
|
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
const contextUsage = cmdCtx.getContextUsage();
|
|
1405
|
+
const contextThreshold = prefs?.context_pause_threshold ?? 0;
|
|
1406
|
+
if (contextThreshold > 0 && s.cmdCtx) {
|
|
1407
|
+
const contextUsage = s.cmdCtx.getContextUsage();
|
|
2590
1408
|
if (contextUsage && contextUsage.percent !== null && contextUsage.percent >= contextThreshold) {
|
|
2591
1409
|
const msg = `Context window at ${contextUsage.percent}% (threshold: ${contextThreshold}%). Pausing to prevent truncated output.`;
|
|
2592
1410
|
ctx.ui.notify(`${msg} Run /gsd auto to continue (will start fresh session).`, "warning");
|
|
@@ -2596,16 +1414,12 @@ async function dispatchNextUnit(
|
|
|
2596
1414
|
}
|
|
2597
1415
|
}
|
|
2598
1416
|
|
|
2599
|
-
//
|
|
2600
|
-
// plan-milestone writes the milestone SECRETS file (e.g., M001-SECRETS.md) during its unit. By the time we
|
|
2601
|
-
// reach the next dispatchNextUnit call the manifest exists but hasn't been
|
|
2602
|
-
// presented to the user yet. Without this re-check the model would proceed
|
|
2603
|
-
// into plan-slice / execute-task with no real credentials and mock everything.
|
|
1417
|
+
// Secrets re-check gate
|
|
2604
1418
|
const runSecretsGate = async () => {
|
|
2605
1419
|
try {
|
|
2606
|
-
const manifestStatus = await getManifestStatus(basePath, mid);
|
|
1420
|
+
const manifestStatus = await getManifestStatus(s.basePath, mid);
|
|
2607
1421
|
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
2608
|
-
const result = await collectSecretsFromManifest(basePath, mid, ctx);
|
|
1422
|
+
const result = await collectSecretsFromManifest(s.basePath, mid, ctx);
|
|
2609
1423
|
if (result && result.applied && result.skipped && result.existingSkipped) {
|
|
2610
1424
|
ctx.ui.notify(
|
|
2611
1425
|
`Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`,
|
|
@@ -2625,23 +1439,19 @@ async function dispatchNextUnit(
|
|
|
2625
1439
|
|
|
2626
1440
|
await runSecretsGate();
|
|
2627
1441
|
|
|
2628
|
-
// ── Dispatch table
|
|
2629
|
-
const dispatchResult = await resolveDispatch({
|
|
2630
|
-
basePath, mid, midTitle: midTitle!, state, prefs,
|
|
1442
|
+
// ── Dispatch table ──
|
|
1443
|
+
const dispatchResult = await resolveDispatch({ basePath: s.basePath, mid, midTitle: midTitle!, state, prefs,
|
|
2631
1444
|
});
|
|
2632
1445
|
|
|
2633
1446
|
if (dispatchResult.action === "stop") {
|
|
2634
|
-
if (currentUnit) {
|
|
2635
|
-
|
|
2636
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2637
|
-
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1447
|
+
if (s.currentUnit) {
|
|
1448
|
+
await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
2638
1449
|
}
|
|
2639
1450
|
await stopAuto(ctx, pi, dispatchResult.reason);
|
|
2640
1451
|
return;
|
|
2641
1452
|
}
|
|
2642
1453
|
|
|
2643
1454
|
if (dispatchResult.action !== "dispatch") {
|
|
2644
|
-
// skip action — yield and re-dispatch
|
|
2645
1455
|
await new Promise(r => setImmediate(r));
|
|
2646
1456
|
await dispatchNextUnit(ctx, pi);
|
|
2647
1457
|
return;
|
|
@@ -2652,8 +1462,8 @@ async function dispatchNextUnit(
|
|
|
2652
1462
|
prompt = dispatchResult.prompt;
|
|
2653
1463
|
let pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
2654
1464
|
|
|
2655
|
-
// ── Pre-dispatch hooks
|
|
2656
|
-
const preDispatchResult = runPreDispatchHooks(unitType, unitId, prompt, basePath);
|
|
1465
|
+
// ── Pre-dispatch hooks ──
|
|
1466
|
+
const preDispatchResult = runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
|
|
2657
1467
|
if (preDispatchResult.firedHooks.length > 0) {
|
|
2658
1468
|
ctx.ui.notify(
|
|
2659
1469
|
`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`,
|
|
@@ -2662,7 +1472,6 @@ async function dispatchNextUnit(
|
|
|
2662
1472
|
}
|
|
2663
1473
|
if (preDispatchResult.action === "skip") {
|
|
2664
1474
|
ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
|
|
2665
|
-
// Yield then re-dispatch to advance to next unit
|
|
2666
1475
|
await new Promise(r => setImmediate(r));
|
|
2667
1476
|
await dispatchNextUnit(ctx, pi);
|
|
2668
1477
|
return;
|
|
@@ -2674,476 +1483,171 @@ async function dispatchNextUnit(
|
|
|
2674
1483
|
prompt = preDispatchResult.prompt;
|
|
2675
1484
|
}
|
|
2676
1485
|
|
|
2677
|
-
const priorSliceBlocker = getPriorSliceCompletionBlocker(basePath, getMainBranch(basePath), unitType, unitId);
|
|
1486
|
+
const priorSliceBlocker = getPriorSliceCompletionBlocker(s.basePath, getMainBranch(s.basePath), unitType, unitId);
|
|
2678
1487
|
if (priorSliceBlocker) {
|
|
2679
1488
|
await stopAuto(ctx, pi, priorSliceBlocker);
|
|
2680
1489
|
return;
|
|
2681
1490
|
}
|
|
2682
1491
|
|
|
2683
|
-
const observabilityIssues = await
|
|
2684
|
-
|
|
2685
|
-
// Idempotency: skip units already completed in a prior session.
|
|
2686
|
-
const idempotencyKey = `${unitType}/${unitId}`;
|
|
2687
|
-
if (completedKeySet.has(idempotencyKey)) {
|
|
2688
|
-
// Cross-validate: does the expected artifact actually exist?
|
|
2689
|
-
const artifactExists = verifyExpectedArtifact(unitType, unitId, basePath);
|
|
2690
|
-
if (artifactExists) {
|
|
2691
|
-
// Guard against infinite skip loops: if deriveState keeps returning the
|
|
2692
|
-
// same completed unit, consecutive skips will trip this breaker. Evict the
|
|
2693
|
-
// key so the next dispatch forces full reconciliation instead of looping.
|
|
2694
|
-
const skipCount = (unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
|
|
2695
|
-
unitConsecutiveSkips.set(idempotencyKey, skipCount);
|
|
2696
|
-
if (skipCount > MAX_CONSECUTIVE_SKIPS) {
|
|
2697
|
-
// Cross-check: verify deriveState actually returns this unit (#790).
|
|
2698
|
-
// If the unit's milestone is already complete, this is a phantom skip
|
|
2699
|
-
// loop from stale crash recovery context — don't evict.
|
|
2700
|
-
const skippedMid = unitId.split("/")[0];
|
|
2701
|
-
const skippedMilestoneComplete = skippedMid
|
|
2702
|
-
? !!resolveMilestoneFile(basePath, skippedMid, "SUMMARY")
|
|
2703
|
-
: false;
|
|
2704
|
-
if (skippedMilestoneComplete) {
|
|
2705
|
-
// Milestone is complete — evicting this key would fight self-heal.
|
|
2706
|
-
// Clear skip counter and re-dispatch from fresh state.
|
|
2707
|
-
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2708
|
-
invalidateAllCaches();
|
|
2709
|
-
ctx.ui.notify(
|
|
2710
|
-
`Phantom skip loop cleared: ${unitType} ${unitId} belongs to completed milestone ${skippedMid}. Re-dispatching from fresh state.`,
|
|
2711
|
-
"info",
|
|
2712
|
-
);
|
|
2713
|
-
_skipDepth++;
|
|
2714
|
-
await new Promise(r => setTimeout(r, 50));
|
|
2715
|
-
await dispatchNextUnit(ctx, pi);
|
|
2716
|
-
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2717
|
-
return;
|
|
2718
|
-
}
|
|
2719
|
-
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2720
|
-
completedKeySet.delete(idempotencyKey);
|
|
2721
|
-
removePersistedKey(basePath, idempotencyKey);
|
|
2722
|
-
invalidateAllCaches();
|
|
2723
|
-
ctx.ui.notify(
|
|
2724
|
-
`Skip loop detected: ${unitType} ${unitId} skipped ${skipCount} times without advancing. Evicting completion record and forcing reconciliation.`,
|
|
2725
|
-
"warning",
|
|
2726
|
-
);
|
|
2727
|
-
if (!active) return;
|
|
2728
|
-
_skipDepth++;
|
|
2729
|
-
await new Promise(r => setTimeout(r, 150));
|
|
2730
|
-
await dispatchNextUnit(ctx, pi);
|
|
2731
|
-
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2732
|
-
return;
|
|
2733
|
-
}
|
|
2734
|
-
// Count toward lifetime cap so hard-stop fires during skip loops (#792)
|
|
2735
|
-
const lifeSkip = (unitLifetimeDispatches.get(idempotencyKey) ?? 0) + 1;
|
|
2736
|
-
unitLifetimeDispatches.set(idempotencyKey, lifeSkip);
|
|
2737
|
-
if (lifeSkip > MAX_LIFETIME_DISPATCHES) {
|
|
2738
|
-
await stopAuto(ctx, pi, `Hard loop: ${unitType} ${unitId} (skip cycle)`);
|
|
2739
|
-
ctx.ui.notify(
|
|
2740
|
-
`Hard loop detected: ${unitType} ${unitId} hit lifetime cap during skip cycle (${lifeSkip} iterations).`,
|
|
2741
|
-
"error",
|
|
2742
|
-
);
|
|
2743
|
-
return;
|
|
2744
|
-
}
|
|
2745
|
-
ctx.ui.notify(
|
|
2746
|
-
`Skipping ${unitType} ${unitId} — already completed in a prior session. Advancing.`,
|
|
2747
|
-
"info",
|
|
2748
|
-
);
|
|
2749
|
-
if (!active) return;
|
|
2750
|
-
_skipDepth++;
|
|
2751
|
-
await new Promise(r => setTimeout(r, 150));
|
|
2752
|
-
await dispatchNextUnit(ctx, pi);
|
|
2753
|
-
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2754
|
-
return;
|
|
2755
|
-
} else {
|
|
2756
|
-
// Stale completion record — artifact missing. Remove and re-run.
|
|
2757
|
-
completedKeySet.delete(idempotencyKey);
|
|
2758
|
-
removePersistedKey(basePath, idempotencyKey);
|
|
2759
|
-
ctx.ui.notify(
|
|
2760
|
-
`Re-running ${unitType} ${unitId} — marked complete but expected artifact missing.`,
|
|
2761
|
-
"warning",
|
|
2762
|
-
);
|
|
2763
|
-
}
|
|
2764
|
-
}
|
|
1492
|
+
const observabilityIssues = await _collectObservabilityWarnings(ctx, s.basePath, unitType, unitId);
|
|
2765
1493
|
|
|
2766
|
-
//
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
completedKeySet.add(idempotencyKey);
|
|
2774
|
-
invalidateAllCaches();
|
|
2775
|
-
// Same consecutive-skip guard as the idempotency path above.
|
|
2776
|
-
const skipCount2 = (unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
|
|
2777
|
-
unitConsecutiveSkips.set(idempotencyKey, skipCount2);
|
|
2778
|
-
if (skipCount2 > MAX_CONSECUTIVE_SKIPS) {
|
|
2779
|
-
// Cross-check: verify the unit's milestone is still active (#790).
|
|
2780
|
-
const skippedMid2 = unitId.split("/")[0];
|
|
2781
|
-
const skippedMilestoneComplete2 = skippedMid2
|
|
2782
|
-
? !!resolveMilestoneFile(basePath, skippedMid2, "SUMMARY")
|
|
2783
|
-
: false;
|
|
2784
|
-
if (skippedMilestoneComplete2) {
|
|
2785
|
-
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2786
|
-
invalidateAllCaches();
|
|
2787
|
-
ctx.ui.notify(
|
|
2788
|
-
`Phantom skip loop cleared: ${unitType} ${unitId} belongs to completed milestone ${skippedMid2}. Re-dispatching from fresh state.`,
|
|
2789
|
-
"info",
|
|
2790
|
-
);
|
|
2791
|
-
_skipDepth++;
|
|
2792
|
-
await new Promise(r => setTimeout(r, 50));
|
|
2793
|
-
await dispatchNextUnit(ctx, pi);
|
|
2794
|
-
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2795
|
-
return;
|
|
2796
|
-
}
|
|
2797
|
-
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2798
|
-
completedKeySet.delete(idempotencyKey);
|
|
2799
|
-
removePersistedKey(basePath, idempotencyKey);
|
|
2800
|
-
invalidateAllCaches();
|
|
2801
|
-
ctx.ui.notify(
|
|
2802
|
-
`Skip loop detected: ${unitType} ${unitId} skipped ${skipCount2} times without advancing. Evicting completion record and forcing reconciliation.`,
|
|
2803
|
-
"warning",
|
|
2804
|
-
);
|
|
2805
|
-
if (!active) return;
|
|
2806
|
-
_skipDepth++;
|
|
2807
|
-
await new Promise(r => setTimeout(r, 150));
|
|
2808
|
-
await dispatchNextUnit(ctx, pi);
|
|
2809
|
-
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2810
|
-
return;
|
|
2811
|
-
}
|
|
2812
|
-
// Count toward lifetime cap so hard-stop fires during skip loops (#792)
|
|
2813
|
-
const lifeSkip2 = (unitLifetimeDispatches.get(idempotencyKey) ?? 0) + 1;
|
|
2814
|
-
unitLifetimeDispatches.set(idempotencyKey, lifeSkip2);
|
|
2815
|
-
if (lifeSkip2 > MAX_LIFETIME_DISPATCHES) {
|
|
2816
|
-
await stopAuto(ctx, pi, `Hard loop: ${unitType} ${unitId} (skip cycle)`);
|
|
2817
|
-
ctx.ui.notify(
|
|
2818
|
-
`Hard loop detected: ${unitType} ${unitId} hit lifetime cap during skip cycle (${lifeSkip2} iterations).`,
|
|
2819
|
-
"error",
|
|
2820
|
-
);
|
|
2821
|
-
return;
|
|
2822
|
-
}
|
|
2823
|
-
ctx.ui.notify(
|
|
2824
|
-
`Skipping ${unitType} ${unitId} — artifact exists but completion key was missing. Repaired and advancing.`,
|
|
2825
|
-
"info",
|
|
2826
|
-
);
|
|
2827
|
-
if (!active) return;
|
|
2828
|
-
_skipDepth++;
|
|
2829
|
-
await new Promise(r => setTimeout(r, 150));
|
|
2830
|
-
await dispatchNextUnit(ctx, pi);
|
|
2831
|
-
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2832
|
-
return;
|
|
2833
|
-
}
|
|
2834
|
-
|
|
2835
|
-
// Stuck detection — tracks total dispatches per unit (not just consecutive repeats).
|
|
2836
|
-
// Pattern A→B→A→B would reset retryCount every time; this map catches it.
|
|
2837
|
-
const dispatchKey = `${unitType}/${unitId}`;
|
|
2838
|
-
const prevCount = unitDispatchCount.get(dispatchKey) ?? 0;
|
|
2839
|
-
// Real dispatch reached — clear the consecutive-skip counter for this unit.
|
|
2840
|
-
unitConsecutiveSkips.delete(dispatchKey);
|
|
2841
|
-
|
|
2842
|
-
debugLog("dispatch-unit", {
|
|
2843
|
-
type: unitType,
|
|
2844
|
-
id: unitId,
|
|
2845
|
-
cycle: prevCount + 1,
|
|
2846
|
-
lifetime: (unitLifetimeDispatches.get(dispatchKey) ?? 0) + 1,
|
|
1494
|
+
// ── Idempotency check (delegated to auto-idempotency.ts) ──
|
|
1495
|
+
const idempotencyResult = checkIdempotency({
|
|
1496
|
+
s,
|
|
1497
|
+
unitType,
|
|
1498
|
+
unitId,
|
|
1499
|
+
basePath: s.basePath,
|
|
1500
|
+
notify: (msg, level) => ctx.ui.notify(msg, level),
|
|
2847
1501
|
});
|
|
2848
|
-
debugCount("dispatches");
|
|
2849
|
-
|
|
2850
|
-
// Hard lifetime cap — survives counter resets from loop-recovery/self-repair.
|
|
2851
|
-
// Catches the case where reconciliation "succeeds" (artifacts exist) but
|
|
2852
|
-
// deriveState keeps returning the same unit, creating an infinite cycle.
|
|
2853
|
-
const lifetimeCount = (unitLifetimeDispatches.get(dispatchKey) ?? 0) + 1;
|
|
2854
|
-
unitLifetimeDispatches.set(dispatchKey, lifetimeCount);
|
|
2855
|
-
if (lifetimeCount > MAX_LIFETIME_DISPATCHES) {
|
|
2856
|
-
if (currentUnit) {
|
|
2857
|
-
const modelId = ctx.model?.id ?? "unknown";
|
|
2858
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2859
|
-
}
|
|
2860
|
-
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
2861
|
-
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
2862
|
-
await stopAuto(ctx, pi, `Hard loop: ${unitType} ${unitId}`);
|
|
2863
|
-
ctx.ui.notify(
|
|
2864
|
-
`Hard loop detected: ${unitType} ${unitId} dispatched ${lifetimeCount} times total (across reconciliation cycles).${expected ? `\n Expected artifact: ${expected}` : ""}\n This may indicate deriveState() keeps returning the same unit despite artifacts existing.\n Check .gsd/completed-units.json and the slice plan checkbox state.`,
|
|
2865
|
-
"error",
|
|
2866
|
-
);
|
|
2867
|
-
return;
|
|
2868
|
-
}
|
|
2869
|
-
if (prevCount >= MAX_UNIT_DISPATCHES) {
|
|
2870
|
-
if (currentUnit) {
|
|
2871
|
-
const modelId = ctx.model?.id ?? "unknown";
|
|
2872
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2873
|
-
}
|
|
2874
|
-
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
2875
|
-
|
|
2876
|
-
// Final reconciliation pass for execute-task: write any missing durable
|
|
2877
|
-
// artifacts (summary placeholder + [x] checkbox) so the pipeline can
|
|
2878
|
-
// advance instead of stopping. This is the last resort before halting.
|
|
2879
|
-
if (unitType === "execute-task") {
|
|
2880
|
-
const [mid, sid, tid] = unitId.split("/");
|
|
2881
|
-
if (mid && sid && tid) {
|
|
2882
|
-
const status = await inspectExecuteTaskDurability(basePath, unitId);
|
|
2883
|
-
if (status) {
|
|
2884
|
-
const reconciled = skipExecuteTask(basePath, mid, sid, tid, status, "loop-recovery", prevCount);
|
|
2885
|
-
// reconciled: skipExecuteTask attempted to write missing artifacts.
|
|
2886
|
-
// verifyExpectedArtifact: confirms physical artifacts (summary + [x]) now exist on disk.
|
|
2887
|
-
// Both must pass before we clear the dispatch counter and advance.
|
|
2888
|
-
if (reconciled && verifyExpectedArtifact(unitType, unitId, basePath)) {
|
|
2889
|
-
ctx.ui.notify(
|
|
2890
|
-
`Loop recovery: ${unitId} reconciled after ${prevCount + 1} dispatches — blocker artifacts written, pipeline advancing.\n Review ${status.summaryPath} and replace the placeholder with real work.`,
|
|
2891
|
-
"warning",
|
|
2892
|
-
);
|
|
2893
|
-
// Persist completion so idempotency check prevents re-dispatch
|
|
2894
|
-
// if deriveState keeps returning this unit (#462).
|
|
2895
|
-
const reconciledKey = `${unitType}/${unitId}`;
|
|
2896
|
-
persistCompletedKey(basePath, reconciledKey);
|
|
2897
|
-
completedKeySet.add(reconciledKey);
|
|
2898
|
-
unitDispatchCount.delete(dispatchKey);
|
|
2899
|
-
invalidateAllCaches();
|
|
2900
|
-
await new Promise(r => setImmediate(r));
|
|
2901
|
-
await dispatchNextUnit(ctx, pi);
|
|
2902
|
-
return;
|
|
2903
|
-
}
|
|
2904
|
-
}
|
|
2905
|
-
}
|
|
2906
|
-
}
|
|
2907
1502
|
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
// dispatch limit succeeded but the counter check fires before anyone
|
|
2914
|
-
// verifies disk state. Without this, a successful final attempt is
|
|
2915
|
-
// indistinguishable from a failed one.
|
|
2916
|
-
if (verifyExpectedArtifact(unitType, unitId, basePath)) {
|
|
2917
|
-
ctx.ui.notify(
|
|
2918
|
-
`Loop recovery: ${unitType} ${unitId} — artifact verified after ${prevCount + 1} dispatches. Advancing.`,
|
|
2919
|
-
"info",
|
|
2920
|
-
);
|
|
2921
|
-
// Persist completion so the idempotency check prevents re-dispatch
|
|
2922
|
-
// if deriveState keeps returning this unit (see #462).
|
|
2923
|
-
persistCompletedKey(basePath, dispatchKey);
|
|
2924
|
-
completedKeySet.add(dispatchKey);
|
|
2925
|
-
unitDispatchCount.delete(dispatchKey);
|
|
2926
|
-
invalidateAllCaches();
|
|
2927
|
-
await new Promise(r => setImmediate(r));
|
|
1503
|
+
if (idempotencyResult.action === "skip") {
|
|
1504
|
+
if (idempotencyResult.reason === "completed" || idempotencyResult.reason === "fallback-persisted" || idempotencyResult.reason === "phantom-loop-cleared" || idempotencyResult.reason === "evicted") {
|
|
1505
|
+
if (!s.active) return;
|
|
1506
|
+
s.skipDepth++;
|
|
1507
|
+
await new Promise(r => setTimeout(r, idempotencyResult.reason === "phantom-loop-cleared" ? 50 : 150));
|
|
2928
1508
|
await dispatchNextUnit(ctx, pi);
|
|
1509
|
+
s.skipDepth = Math.max(0, s.skipDepth - 1);
|
|
2929
1510
|
return;
|
|
2930
1511
|
}
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
// All slices are done (otherwise we wouldn't be in completing-milestone phase),
|
|
2934
|
-
// but the LLM failed to write the summary N times. A stub lets the pipeline advance.
|
|
2935
|
-
if (unitType === "complete-milestone") {
|
|
2936
|
-
try {
|
|
2937
|
-
const mPath = resolveMilestonePath(basePath, unitId);
|
|
2938
|
-
if (mPath) {
|
|
2939
|
-
const stubPath = join(mPath, `${unitId}-SUMMARY.md`);
|
|
2940
|
-
if (!existsSync(stubPath)) {
|
|
2941
|
-
writeFileSync(stubPath, `# ${unitId} Summary\n\nAuto-generated stub — milestone tasks completed but summary generation failed after ${prevCount + 1} attempts.\nReview and replace this stub with a proper summary.\n`);
|
|
2942
|
-
ctx.ui.notify(`Generated stub summary for ${unitId} to unblock pipeline. Review later.`, "warning");
|
|
2943
|
-
persistCompletedKey(basePath, dispatchKey);
|
|
2944
|
-
completedKeySet.add(dispatchKey);
|
|
2945
|
-
unitDispatchCount.delete(dispatchKey);
|
|
2946
|
-
invalidateAllCaches();
|
|
2947
|
-
await new Promise(r => setImmediate(r));
|
|
2948
|
-
await dispatchNextUnit(ctx, pi);
|
|
2949
|
-
return;
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
} catch { /* non-fatal — fall through to normal stop */ }
|
|
2953
|
-
}
|
|
2954
|
-
|
|
2955
|
-
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
2956
|
-
const remediation = buildLoopRemediationSteps(unitType, unitId, basePath);
|
|
2957
|
-
await stopAuto(ctx, pi, `Loop: ${unitType} ${unitId}`);
|
|
2958
|
-
sendDesktopNotification("GSD", `Loop detected: ${unitType} ${unitId}`, "error", "error");
|
|
1512
|
+
} else if (idempotencyResult.action === "stop") {
|
|
1513
|
+
await stopAuto(ctx, pi, idempotencyResult.reason);
|
|
2959
1514
|
ctx.ui.notify(
|
|
2960
|
-
`
|
|
1515
|
+
`Hard loop detected: ${unitType} ${unitId} hit lifetime cap during skip cycle.`,
|
|
2961
1516
|
"error",
|
|
2962
1517
|
);
|
|
2963
1518
|
return;
|
|
2964
1519
|
}
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
);
|
|
2982
|
-
// Persist completion so idempotency check prevents re-dispatch (#462).
|
|
2983
|
-
const repairedKey = `${unitType}/${unitId}`;
|
|
2984
|
-
persistCompletedKey(basePath, repairedKey);
|
|
2985
|
-
completedKeySet.add(repairedKey);
|
|
2986
|
-
unitDispatchCount.delete(dispatchKey);
|
|
2987
|
-
invalidateAllCaches();
|
|
2988
|
-
await new Promise(r => setImmediate(r));
|
|
2989
|
-
await dispatchNextUnit(ctx, pi);
|
|
2990
|
-
return;
|
|
2991
|
-
}
|
|
2992
|
-
} else if (prevCount >= STUB_RECOVERY_THRESHOLD && !status.summaryExists) {
|
|
2993
|
-
// Retry STUB_RECOVERY_THRESHOLD+: summary still missing after multiple attempts.
|
|
2994
|
-
// Write a minimal stub summary so the next agent session has a recovery artifact
|
|
2995
|
-
// to overwrite, rather than starting from scratch again.
|
|
2996
|
-
const tasksDir = resolveTasksDir(basePath, mid, sid);
|
|
2997
|
-
const sDir = resolveSlicePath(basePath, mid, sid);
|
|
2998
|
-
const targetDir = tasksDir ?? (sDir ? join(sDir, "tasks") : null);
|
|
2999
|
-
if (targetDir) {
|
|
3000
|
-
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
|
|
3001
|
-
const summaryPath = join(targetDir, buildTaskFileName(tid, "SUMMARY"));
|
|
3002
|
-
if (!existsSync(summaryPath)) {
|
|
3003
|
-
const stubContent = [
|
|
3004
|
-
`# PARTIAL RECOVERY — attempt ${prevCount + 1} of ${MAX_UNIT_DISPATCHES}`,
|
|
3005
|
-
``,
|
|
3006
|
-
`Task \`${tid}\` in slice \`${sid}\` (milestone \`${mid}\`) has not yet produced a real summary.`,
|
|
3007
|
-
`This placeholder was written by auto-mode after ${prevCount} dispatch attempts.`,
|
|
3008
|
-
``,
|
|
3009
|
-
`The next agent session will retry this task. Replace this file with real work when done.`,
|
|
3010
|
-
].join("\n");
|
|
3011
|
-
writeFileSync(summaryPath, stubContent, "utf-8");
|
|
3012
|
-
ctx.ui.notify(
|
|
3013
|
-
`Stub recovery (attempt ${prevCount + 1}/${MAX_UNIT_DISPATCHES}): ${unitId} stub summary placeholder written. Retrying with recovery context.`,
|
|
3014
|
-
"warning",
|
|
3015
|
-
);
|
|
3016
|
-
}
|
|
3017
|
-
}
|
|
3018
|
-
}
|
|
3019
|
-
}
|
|
1520
|
+
// "rerun" and "proceed" fall through to stuck detection
|
|
1521
|
+
|
|
1522
|
+
// ── Stuck detection (delegated to auto-stuck-detection.ts) ──
|
|
1523
|
+
const stuckResult = await checkStuckAndRecover({
|
|
1524
|
+
s,
|
|
1525
|
+
ctx,
|
|
1526
|
+
unitType,
|
|
1527
|
+
unitId,
|
|
1528
|
+
basePath: s.basePath,
|
|
1529
|
+
buildSnapshotOpts: () => buildSnapshotOpts(unitType, unitId),
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
if (stuckResult.action === "stop") {
|
|
1533
|
+
await stopAuto(ctx, pi, stuckResult.reason);
|
|
1534
|
+
if (stuckResult.notifyMessage) {
|
|
1535
|
+
ctx.ui.notify(stuckResult.notifyMessage, "error");
|
|
3020
1536
|
}
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
if (stuckResult.action === "recovered" && stuckResult.dispatchAgain) {
|
|
1540
|
+
await new Promise(r => setImmediate(r));
|
|
1541
|
+
await dispatchNextUnit(ctx, pi);
|
|
1542
|
+
return;
|
|
3025
1543
|
}
|
|
1544
|
+
|
|
3026
1545
|
// Snapshot metrics + activity log for the PREVIOUS unit before we reassign.
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
const modelId = ctx.model?.id ?? "unknown";
|
|
3030
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
3031
|
-
const activityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
3032
|
-
|
|
3033
|
-
// Fire-and-forget memory extraction from completed unit
|
|
3034
|
-
if (activityFile) {
|
|
3035
|
-
try {
|
|
3036
|
-
const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
|
|
3037
|
-
const llmCallFn = buildMemoryLLMCall(ctx);
|
|
3038
|
-
if (llmCallFn) {
|
|
3039
|
-
extractMemoriesFromUnit(activityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
|
|
3040
|
-
}
|
|
3041
|
-
} catch { /* non-fatal */ }
|
|
3042
|
-
}
|
|
1546
|
+
if (s.currentUnit) {
|
|
1547
|
+
await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
3043
1548
|
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
const isRetry = currentUnit.type === unitType && currentUnit.id === unitId;
|
|
1549
|
+
if (s.currentUnitRouting) {
|
|
1550
|
+
const isRetry = s.currentUnit.type === unitType && s.currentUnit.id === unitId;
|
|
3047
1551
|
recordOutcome(
|
|
3048
|
-
currentUnit.type,
|
|
3049
|
-
currentUnitRouting.tier as "light" | "standard" | "heavy",
|
|
3050
|
-
!isRetry,
|
|
1552
|
+
s.currentUnit.type,
|
|
1553
|
+
s.currentUnitRouting.tier as "light" | "standard" | "heavy",
|
|
1554
|
+
!isRetry,
|
|
3051
1555
|
);
|
|
3052
1556
|
}
|
|
3053
1557
|
|
|
3054
|
-
|
|
3055
|
-
// 1. We're not about to re-dispatch the same unit (retry scenario)
|
|
3056
|
-
// 2. The expected artifact actually exists on disk
|
|
3057
|
-
// For hook units, skip artifact verification — hooks don't produce standard
|
|
3058
|
-
// artifacts and their runtime records were already finalized in handleAgentEnd.
|
|
3059
|
-
const closeoutKey = `${currentUnit.type}/${currentUnit.id}`;
|
|
1558
|
+
const closeoutKey = `${s.currentUnit.type}/${s.currentUnit.id}`;
|
|
3060
1559
|
const incomingKey = `${unitType}/${unitId}`;
|
|
3061
|
-
const isHookUnit = currentUnit.type.startsWith("hook/");
|
|
3062
|
-
const artifactVerified = isHookUnit || verifyExpectedArtifact(currentUnit.type, currentUnit.id, basePath);
|
|
1560
|
+
const isHookUnit = s.currentUnit.type.startsWith("hook/");
|
|
1561
|
+
const artifactVerified = isHookUnit || verifyExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.basePath);
|
|
3063
1562
|
if (closeoutKey !== incomingKey && artifactVerified) {
|
|
3064
1563
|
if (!isHookUnit) {
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
persistCompletedKey(basePath, closeoutKey);
|
|
3068
|
-
completedKeySet.add(closeoutKey);
|
|
1564
|
+
persistCompletedKey(s.basePath, closeoutKey);
|
|
1565
|
+
s.completedKeySet.add(closeoutKey);
|
|
3069
1566
|
}
|
|
3070
1567
|
|
|
3071
|
-
completedUnits.push({
|
|
3072
|
-
type: currentUnit.type,
|
|
3073
|
-
id: currentUnit.id,
|
|
3074
|
-
startedAt: currentUnit.startedAt,
|
|
1568
|
+
s.completedUnits.push({
|
|
1569
|
+
type: s.currentUnit.type,
|
|
1570
|
+
id: s.currentUnit.id,
|
|
1571
|
+
startedAt: s.currentUnit.startedAt,
|
|
3075
1572
|
finishedAt: Date.now(),
|
|
3076
1573
|
});
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
completedUnits = completedUnits.slice(-200);
|
|
1574
|
+
if (s.completedUnits.length > 200) {
|
|
1575
|
+
s.completedUnits = s.completedUnits.slice(-200);
|
|
3080
1576
|
}
|
|
3081
|
-
clearUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id);
|
|
3082
|
-
unitDispatchCount.delete(`${currentUnit.type}/${currentUnit.id}`);
|
|
3083
|
-
unitRecoveryCount.delete(`${currentUnit.type}/${currentUnit.id}`);
|
|
1577
|
+
clearUnitRuntimeRecord(s.basePath, s.currentUnit.type, s.currentUnit.id);
|
|
1578
|
+
s.unitDispatchCount.delete(`${s.currentUnit.type}/${s.currentUnit.id}`);
|
|
1579
|
+
s.unitRecoveryCount.delete(`${s.currentUnit.type}/${s.currentUnit.id}`);
|
|
3084
1580
|
}
|
|
3085
1581
|
}
|
|
3086
|
-
currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
3087
|
-
captureAvailableSkills();
|
|
3088
|
-
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
1582
|
+
s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
1583
|
+
captureAvailableSkills();
|
|
1584
|
+
writeUnitRuntimeRecord(s.basePath, unitType, unitId, s.currentUnit.startedAt, {
|
|
3089
1585
|
phase: "dispatched",
|
|
3090
1586
|
wrapupWarningSent: false,
|
|
3091
1587
|
timeoutAt: null,
|
|
3092
|
-
lastProgressAt: currentUnit.startedAt,
|
|
1588
|
+
lastProgressAt: s.currentUnit.startedAt,
|
|
3093
1589
|
progressCount: 0,
|
|
3094
1590
|
lastProgressKind: "dispatch",
|
|
3095
1591
|
});
|
|
3096
1592
|
|
|
3097
1593
|
// Status bar + progress widget
|
|
3098
1594
|
ctx.ui.setStatus("gsd-auto", "auto");
|
|
3099
|
-
if (mid) updateSliceProgressCache(basePath, mid, state.activeSlice?.id);
|
|
1595
|
+
if (mid) updateSliceProgressCache(s.basePath, mid, state.activeSlice?.id);
|
|
3100
1596
|
updateProgressWidget(ctx, unitType, unitId, state);
|
|
3101
1597
|
|
|
3102
|
-
|
|
3103
|
-
// so the LLM doesn't have to get these right
|
|
3104
|
-
ensurePreconditions(unitType, unitId, basePath, state);
|
|
1598
|
+
ensurePreconditions(unitType, unitId, s.basePath, state);
|
|
3105
1599
|
|
|
3106
|
-
// Fresh session
|
|
3107
|
-
|
|
1600
|
+
// Fresh session — with timeout to prevent permanent hangs (#1073).
|
|
1601
|
+
// If newSession() hangs (e.g., session manager deadlock, network issue),
|
|
1602
|
+
// without this timeout the entire dispatch chain stalls permanently: no
|
|
1603
|
+
// timeouts are set, no gap watchdog fires, and auto-mode is left active
|
|
1604
|
+
// but idle until the user Ctrl+C's.
|
|
1605
|
+
let result: { cancelled: boolean };
|
|
1606
|
+
try {
|
|
1607
|
+
const sessionPromise = s.cmdCtx!.newSession();
|
|
1608
|
+
const timeoutPromise = new Promise<{ cancelled: true }>((resolve) =>
|
|
1609
|
+
setTimeout(() => resolve({ cancelled: true }), NEW_SESSION_TIMEOUT_MS),
|
|
1610
|
+
);
|
|
1611
|
+
result = await Promise.race([sessionPromise, timeoutPromise]);
|
|
1612
|
+
} catch (sessionErr) {
|
|
1613
|
+
const msg = sessionErr instanceof Error ? sessionErr.message : String(sessionErr);
|
|
1614
|
+
ctx.ui.notify(`Session creation failed: ${msg}. Retrying via watchdog.`, "error");
|
|
1615
|
+
throw new Error(`newSession() failed: ${msg}`);
|
|
1616
|
+
}
|
|
3108
1617
|
if (result.cancelled) {
|
|
3109
|
-
|
|
1618
|
+
ctx.ui.notify(
|
|
1619
|
+
`Session creation timed out or was cancelled for ${unitType} ${unitId}. Will retry.`,
|
|
1620
|
+
"warning",
|
|
1621
|
+
);
|
|
1622
|
+
await stopAuto(ctx, pi, "Session creation failed");
|
|
3110
1623
|
return;
|
|
3111
1624
|
}
|
|
3112
1625
|
|
|
3113
|
-
// Branchless architecture: all work commits sequentially on the milestone
|
|
3114
|
-
// branch — no per-slice branches or slice-level merges. Milestone merge
|
|
3115
|
-
// happens when phase === "complete" (see mergeMilestoneToMain above).
|
|
3116
|
-
|
|
3117
|
-
// Write lock AFTER newSession so we capture the session file path.
|
|
3118
|
-
// Pi appends entries incrementally via appendFileSync, so on crash the
|
|
3119
|
-
// session file survives with every tool call up to the crash point.
|
|
3120
1626
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
3121
|
-
|
|
1627
|
+
updateSessionLock(lockBase(), unitType, unitId, s.completedUnits.length, sessionFile);
|
|
1628
|
+
writeLock(lockBase(), unitType, unitId, s.completedUnits.length, sessionFile);
|
|
3122
1629
|
|
|
3123
|
-
//
|
|
3124
|
-
// On retry (stuck detection), prepend deep diagnostic from last attempt
|
|
3125
|
-
// Cap injected content to prevent unbounded prompt growth → OOM
|
|
1630
|
+
// Prompt injection
|
|
3126
1631
|
const MAX_RECOVERY_CHARS = 50_000;
|
|
3127
1632
|
let finalPrompt = prompt;
|
|
3128
1633
|
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
pendingVerificationRetry = null;
|
|
1634
|
+
if (s.pendingVerificationRetry) {
|
|
1635
|
+
const retryCtx = s.pendingVerificationRetry;
|
|
1636
|
+
s.pendingVerificationRetry = null;
|
|
3133
1637
|
const capped = retryCtx.failureContext.length > MAX_RECOVERY_CHARS
|
|
3134
1638
|
? retryCtx.failureContext.slice(0, MAX_RECOVERY_CHARS) + "\n\n[...failure context truncated]"
|
|
3135
1639
|
: retryCtx.failureContext;
|
|
3136
1640
|
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}`;
|
|
3137
1641
|
}
|
|
3138
1642
|
|
|
3139
|
-
if (pendingCrashRecovery) {
|
|
3140
|
-
const capped = pendingCrashRecovery.length > MAX_RECOVERY_CHARS
|
|
3141
|
-
? pendingCrashRecovery.slice(0, MAX_RECOVERY_CHARS) + "\n\n[...recovery briefing truncated to prevent memory exhaustion]"
|
|
3142
|
-
: pendingCrashRecovery;
|
|
1643
|
+
if (s.pendingCrashRecovery) {
|
|
1644
|
+
const capped = s.pendingCrashRecovery.length > MAX_RECOVERY_CHARS
|
|
1645
|
+
? s.pendingCrashRecovery.slice(0, MAX_RECOVERY_CHARS) + "\n\n[...recovery briefing truncated to prevent memory exhaustion]"
|
|
1646
|
+
: s.pendingCrashRecovery;
|
|
3143
1647
|
finalPrompt = `${capped}\n\n---\n\n${finalPrompt}`;
|
|
3144
|
-
pendingCrashRecovery = null;
|
|
3145
|
-
} else if ((unitDispatchCount.get(`${unitType}/${unitId}`) ?? 0) > 1) {
|
|
3146
|
-
const diagnostic = getDeepDiagnostic(basePath);
|
|
1648
|
+
s.pendingCrashRecovery = null;
|
|
1649
|
+
} else if ((s.unitDispatchCount.get(`${unitType}/${unitId}`) ?? 0) > 1) {
|
|
1650
|
+
const diagnostic = getDeepDiagnostic(s.basePath);
|
|
3147
1651
|
if (diagnostic) {
|
|
3148
1652
|
const cappedDiag = diagnostic.length > MAX_RECOVERY_CHARS
|
|
3149
1653
|
? diagnostic.slice(0, MAX_RECOVERY_CHARS) + "\n\n[...diagnostic truncated to prevent memory exhaustion]"
|
|
@@ -3152,313 +1656,65 @@ async function dispatchNextUnit(
|
|
|
3152
1656
|
}
|
|
3153
1657
|
}
|
|
3154
1658
|
|
|
3155
|
-
// Inject observability repair instructions so the agent fixes gaps before
|
|
3156
|
-
// proceeding with the unit (see #174).
|
|
3157
1659
|
const repairBlock = buildObservabilityRepairBlock(observabilityIssues);
|
|
3158
1660
|
if (repairBlock) {
|
|
3159
1661
|
finalPrompt = `${finalPrompt}${repairBlock}`;
|
|
3160
1662
|
}
|
|
3161
1663
|
|
|
3162
|
-
// ── Prompt char measurement
|
|
3163
|
-
lastPromptCharCount = finalPrompt.length;
|
|
3164
|
-
lastBaselineCharCount = undefined;
|
|
1664
|
+
// ── Prompt char measurement ──
|
|
1665
|
+
s.lastPromptCharCount = finalPrompt.length;
|
|
1666
|
+
s.lastBaselineCharCount = undefined;
|
|
3165
1667
|
if (isDbAvailable()) {
|
|
3166
1668
|
try {
|
|
3167
1669
|
const { inlineGsdRootFile } = await import("./auto-prompts.js");
|
|
3168
1670
|
const [decisionsContent, requirementsContent, projectContent] = await Promise.all([
|
|
3169
|
-
inlineGsdRootFile(basePath, "decisions.md", "Decisions"),
|
|
3170
|
-
inlineGsdRootFile(basePath, "requirements.md", "Requirements"),
|
|
3171
|
-
inlineGsdRootFile(basePath, "project.md", "Project"),
|
|
1671
|
+
inlineGsdRootFile(s.basePath, "decisions.md", "Decisions"),
|
|
1672
|
+
inlineGsdRootFile(s.basePath, "requirements.md", "Requirements"),
|
|
1673
|
+
inlineGsdRootFile(s.basePath, "project.md", "Project"),
|
|
3172
1674
|
]);
|
|
3173
|
-
lastBaselineCharCount =
|
|
1675
|
+
s.lastBaselineCharCount =
|
|
3174
1676
|
(decisionsContent?.length ?? 0) +
|
|
3175
1677
|
(requirementsContent?.length ?? 0) +
|
|
3176
1678
|
(projectContent?.length ?? 0);
|
|
3177
1679
|
} catch {
|
|
3178
|
-
// Non-fatal
|
|
1680
|
+
// Non-fatal
|
|
3179
1681
|
}
|
|
3180
1682
|
}
|
|
3181
1683
|
|
|
3182
|
-
//
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
// If enabled, classify unit complexity and potentially downgrade to a
|
|
3190
|
-
// cheaper model. The user's configured model is the ceiling.
|
|
3191
|
-
const routingConfig = resolveDynamicRoutingConfig();
|
|
3192
|
-
let effectiveModelConfig = modelConfig;
|
|
3193
|
-
let routingTierLabel = "";
|
|
3194
|
-
currentUnitRouting = null;
|
|
3195
|
-
|
|
3196
|
-
if (routingConfig.enabled) {
|
|
3197
|
-
// Compute budget pressure if budget ceiling is set
|
|
3198
|
-
let budgetPct: number | undefined;
|
|
3199
|
-
if (routingConfig.budget_pressure !== false) {
|
|
3200
|
-
const budgetCeiling = prefs?.budget_ceiling;
|
|
3201
|
-
if (budgetCeiling !== undefined && budgetCeiling > 0) {
|
|
3202
|
-
const currentLedger = getLedger();
|
|
3203
|
-
const totalCost = currentLedger ? getProjectTotals(currentLedger.units).cost : 0;
|
|
3204
|
-
budgetPct = totalCost / budgetCeiling;
|
|
3205
|
-
}
|
|
3206
|
-
}
|
|
3207
|
-
|
|
3208
|
-
// Classify complexity (hook routing controlled by config.hooks)
|
|
3209
|
-
const isHook = unitType.startsWith("hook/");
|
|
3210
|
-
const shouldClassify = !isHook || routingConfig.hooks !== false;
|
|
3211
|
-
|
|
3212
|
-
if (shouldClassify) {
|
|
3213
|
-
const classification = classifyUnitComplexity(unitType, unitId, basePath, budgetPct);
|
|
3214
|
-
const availableModelIds = availableModels.map(m => m.id);
|
|
3215
|
-
const routing = resolveModelForComplexity(classification, modelConfig, routingConfig, availableModelIds);
|
|
3216
|
-
|
|
3217
|
-
if (routing.wasDowngraded) {
|
|
3218
|
-
effectiveModelConfig = {
|
|
3219
|
-
primary: routing.modelId,
|
|
3220
|
-
fallbacks: routing.fallbacks,
|
|
3221
|
-
};
|
|
3222
|
-
if (verbose) {
|
|
3223
|
-
ctx.ui.notify(
|
|
3224
|
-
`Dynamic routing [${tierLabel(classification.tier)}]: ${routing.modelId} (${classification.reason})`,
|
|
3225
|
-
"info",
|
|
3226
|
-
);
|
|
3227
|
-
}
|
|
3228
|
-
}
|
|
3229
|
-
routingTierLabel = ` [${tierLabel(classification.tier)}]`;
|
|
3230
|
-
currentUnitRouting = { tier: classification.tier, modelDowngraded: routing.wasDowngraded };
|
|
3231
|
-
}
|
|
3232
|
-
}
|
|
3233
|
-
|
|
3234
|
-
const modelsToTry = [effectiveModelConfig.primary, ...effectiveModelConfig.fallbacks];
|
|
3235
|
-
let modelSet = false;
|
|
3236
|
-
|
|
3237
|
-
for (const modelId of modelsToTry) {
|
|
3238
|
-
// Resolve model from available models.
|
|
3239
|
-
// Handles multiple formats:
|
|
3240
|
-
// "provider/model" → explicit provider targeting (e.g. "anthropic/claude-opus-4-6")
|
|
3241
|
-
// "bare-id" → match by ID across providers
|
|
3242
|
-
// "org/model-name" → OpenRouter-style IDs where the full string is the model ID
|
|
3243
|
-
// "openrouter/org/model" → explicit provider + OpenRouter model ID
|
|
3244
|
-
const slashIdx = modelId.indexOf("/");
|
|
3245
|
-
let model;
|
|
3246
|
-
if (slashIdx !== -1) {
|
|
3247
|
-
const maybeProvider = modelId.substring(0, slashIdx);
|
|
3248
|
-
const id = modelId.substring(slashIdx + 1);
|
|
3249
|
-
|
|
3250
|
-
// Check if the prefix before the first slash is a known provider
|
|
3251
|
-
const knownProviders = new Set(availableModels.map(m => m.provider.toLowerCase()));
|
|
3252
|
-
if (knownProviders.has(maybeProvider.toLowerCase())) {
|
|
3253
|
-
// Explicit "provider/model" format (handles "openrouter/org/model" too)
|
|
3254
|
-
model = availableModels.find(
|
|
3255
|
-
m => m.provider.toLowerCase() === maybeProvider.toLowerCase()
|
|
3256
|
-
&& m.id.toLowerCase() === id.toLowerCase(),
|
|
3257
|
-
);
|
|
3258
|
-
}
|
|
3259
|
-
|
|
3260
|
-
// If the prefix wasn't a known provider, or no match was found within that provider,
|
|
3261
|
-
// try matching the full string as a model ID (OpenRouter-style IDs like "org/model-name")
|
|
3262
|
-
if (!model) {
|
|
3263
|
-
const lower = modelId.toLowerCase();
|
|
3264
|
-
model = availableModels.find(
|
|
3265
|
-
m => m.id.toLowerCase() === lower
|
|
3266
|
-
|| `${m.provider}/${m.id}`.toLowerCase() === lower,
|
|
3267
|
-
);
|
|
3268
|
-
}
|
|
3269
|
-
} else {
|
|
3270
|
-
// For bare IDs, prefer the current session's provider, then first available match
|
|
3271
|
-
const currentProvider = ctx.model?.provider;
|
|
3272
|
-
const exactProviderMatch = availableModels.find(
|
|
3273
|
-
m => m.id === modelId && m.provider === currentProvider,
|
|
3274
|
-
);
|
|
3275
|
-
const anyMatch = availableModels.find(m => m.id === modelId);
|
|
3276
|
-
model = exactProviderMatch ?? anyMatch;
|
|
3277
|
-
|
|
3278
|
-
// Warn if the ID is ambiguous across providers
|
|
3279
|
-
if (anyMatch && !exactProviderMatch) {
|
|
3280
|
-
const providers = availableModels
|
|
3281
|
-
.filter(m => m.id === modelId)
|
|
3282
|
-
.map(m => m.provider);
|
|
3283
|
-
if (providers.length > 1) {
|
|
3284
|
-
ctx.ui.notify(
|
|
3285
|
-
`Model ID "${modelId}" exists in multiple providers (${providers.join(", ")}). ` +
|
|
3286
|
-
`Resolved to ${anyMatch.provider}. Use "provider/model" format for explicit targeting.`,
|
|
3287
|
-
"warning",
|
|
3288
|
-
);
|
|
3289
|
-
}
|
|
3290
|
-
}
|
|
3291
|
-
}
|
|
3292
|
-
if (!model) {
|
|
3293
|
-
if (verbose) ctx.ui.notify(`Model ${modelId} not found, trying fallback.`, "info");
|
|
3294
|
-
continue;
|
|
3295
|
-
}
|
|
3296
|
-
|
|
3297
|
-
const ok = await pi.setModel(model, { persist: false });
|
|
3298
|
-
if (ok) {
|
|
3299
|
-
const fallbackNote = modelId === effectiveModelConfig.primary
|
|
3300
|
-
? ""
|
|
3301
|
-
: ` (fallback from ${effectiveModelConfig.primary})`;
|
|
3302
|
-
const phase = unitPhaseLabel(unitType);
|
|
3303
|
-
ctx.ui.notify(`Model [${phase}]${routingTierLabel}: ${model.provider}/${model.id}${fallbackNote}`, "info");
|
|
3304
|
-
modelSet = true;
|
|
3305
|
-
break;
|
|
3306
|
-
} else {
|
|
3307
|
-
const nextModel = modelsToTry[modelsToTry.indexOf(modelId) + 1];
|
|
3308
|
-
if (nextModel) {
|
|
3309
|
-
if (verbose) ctx.ui.notify(`Failed to set model ${modelId}, trying ${nextModel}...`, "info");
|
|
3310
|
-
} else {
|
|
3311
|
-
ctx.ui.notify(`All preferred models unavailable for ${unitType}. Using default.`, "warning");
|
|
3312
|
-
}
|
|
3313
|
-
}
|
|
3314
|
-
}
|
|
3315
|
-
|
|
3316
|
-
// modelSet=false is already handled by the "all fallbacks exhausted" warning above
|
|
3317
|
-
} else if (autoModeStartModel) {
|
|
3318
|
-
// No model preference for this unit type — re-apply the model captured
|
|
3319
|
-
// at auto-mode start to prevent bleed from the shared global settings.json
|
|
3320
|
-
// when multiple GSD instances run concurrently (#650).
|
|
3321
|
-
const availableModels = ctx.modelRegistry.getAvailable();
|
|
3322
|
-
const startModel = availableModels.find(
|
|
3323
|
-
m => m.provider === autoModeStartModel!.provider && m.id === autoModeStartModel!.id,
|
|
3324
|
-
);
|
|
3325
|
-
if (startModel) {
|
|
3326
|
-
const ok = await pi.setModel(startModel, { persist: false });
|
|
3327
|
-
if (!ok) {
|
|
3328
|
-
// Fallback: try matching just by ID across providers
|
|
3329
|
-
const byId = availableModels.find(m => m.id === autoModeStartModel!.id);
|
|
3330
|
-
if (byId) await pi.setModel(byId, { persist: false });
|
|
3331
|
-
}
|
|
3332
|
-
}
|
|
1684
|
+
// Cache-optimize prompt section ordering
|
|
1685
|
+
try {
|
|
1686
|
+
const { reorderForCaching } = await import("./prompt-ordering.js");
|
|
1687
|
+
finalPrompt = reorderForCaching(finalPrompt);
|
|
1688
|
+
} catch (reorderErr) {
|
|
1689
|
+
const msg = reorderErr instanceof Error ? reorderErr.message : String(reorderErr);
|
|
1690
|
+
process.stderr.write(`[gsd] prompt reorder failed (non-fatal): ${msg}\n`);
|
|
3333
1691
|
}
|
|
3334
1692
|
|
|
3335
|
-
//
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
clearUnitTimeout();
|
|
3339
|
-
const supervisor = resolveAutoSupervisorConfig();
|
|
3340
|
-
const softTimeoutMs = (supervisor.soft_timeout_minutes ?? 0) * 60 * 1000;
|
|
3341
|
-
const idleTimeoutMs = (supervisor.idle_timeout_minutes ?? 0) * 60 * 1000;
|
|
3342
|
-
const hardTimeoutMs = (supervisor.hard_timeout_minutes ?? 0) * 60 * 1000;
|
|
3343
|
-
|
|
3344
|
-
wrapupWarningHandle = setTimeout(() => {
|
|
3345
|
-
wrapupWarningHandle = null;
|
|
3346
|
-
if (!active || !currentUnit) return;
|
|
3347
|
-
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
3348
|
-
phase: "wrapup-warning-sent",
|
|
3349
|
-
wrapupWarningSent: true,
|
|
3350
|
-
});
|
|
3351
|
-
pi.sendMessage(
|
|
3352
|
-
{
|
|
3353
|
-
customType: "gsd-auto-wrapup",
|
|
3354
|
-
display: verbose,
|
|
3355
|
-
content: [
|
|
3356
|
-
"**TIME BUDGET WARNING — keep going only if progress is real.**",
|
|
3357
|
-
"This unit crossed the soft time budget.",
|
|
3358
|
-
"If you are making progress, continue. If not, switch to wrap-up mode now:",
|
|
3359
|
-
"1. rerun the minimal required verification",
|
|
3360
|
-
"2. write or update the required durable artifacts",
|
|
3361
|
-
"3. mark task or slice state on disk correctly",
|
|
3362
|
-
"4. leave precise resume notes if anything remains unfinished",
|
|
3363
|
-
].join("\n"),
|
|
3364
|
-
},
|
|
3365
|
-
{ triggerTurn: true },
|
|
3366
|
-
);
|
|
3367
|
-
}, softTimeoutMs);
|
|
3368
|
-
|
|
3369
|
-
idleWatchdogHandle = setInterval(async () => {
|
|
3370
|
-
if (!active || !currentUnit) return;
|
|
3371
|
-
const runtime = readUnitRuntimeRecord(basePath, unitType, unitId);
|
|
3372
|
-
if (!runtime) return;
|
|
3373
|
-
if (Date.now() - runtime.lastProgressAt < idleTimeoutMs) return;
|
|
3374
|
-
|
|
3375
|
-
// Agent has tool calls currently executing (await_job, long bash, etc.) —
|
|
3376
|
-
// not idle, just waiting for tool completion. But only suppress recovery
|
|
3377
|
-
// if the tool started recently. A tool in-flight for longer than the idle
|
|
3378
|
-
// timeout is likely stuck — e.g., `python -m http.server 8080 &` keeps the
|
|
3379
|
-
// shell's stdout/stderr open, causing the Bash tool to hang indefinitely.
|
|
3380
|
-
if (inFlightTools.size > 0) {
|
|
3381
|
-
const oldestStart = Math.min(...inFlightTools.values());
|
|
3382
|
-
const toolAgeMs = Date.now() - oldestStart;
|
|
3383
|
-
if (toolAgeMs < idleTimeoutMs) {
|
|
3384
|
-
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
3385
|
-
lastProgressAt: Date.now(),
|
|
3386
|
-
lastProgressKind: "tool-in-flight",
|
|
3387
|
-
});
|
|
3388
|
-
return;
|
|
3389
|
-
}
|
|
3390
|
-
// Oldest tool has been running >= idleTimeoutMs — treat as a stuck/hung
|
|
3391
|
-
// tool (e.g., background process holding stdout open). Fall through to
|
|
3392
|
-
// idle recovery without resetting the progress clock.
|
|
3393
|
-
ctx.ui.notify(
|
|
3394
|
-
`Stalled tool detected: a tool has been in-flight for ${Math.round(toolAgeMs / 60000)}min. Treating as hung — attempting idle recovery.`,
|
|
3395
|
-
"warning",
|
|
3396
|
-
);
|
|
3397
|
-
}
|
|
3398
|
-
|
|
3399
|
-
// Before triggering recovery, check if the agent is actually producing
|
|
3400
|
-
// work on disk. `git status --porcelain` is cheap and catches any
|
|
3401
|
-
// staged/unstaged/untracked changes the agent made since lastProgressAt.
|
|
3402
|
-
if (detectWorkingTreeActivity(basePath)) {
|
|
3403
|
-
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
3404
|
-
lastProgressAt: Date.now(),
|
|
3405
|
-
lastProgressKind: "filesystem-activity",
|
|
3406
|
-
});
|
|
3407
|
-
return;
|
|
3408
|
-
}
|
|
3409
|
-
|
|
3410
|
-
if (currentUnit) {
|
|
3411
|
-
const modelId = ctx.model?.id ?? "unknown";
|
|
3412
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
3413
|
-
}
|
|
3414
|
-
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
3415
|
-
|
|
3416
|
-
const recovery = await recoverTimedOutUnit(ctx, pi, unitType, unitId, "idle");
|
|
3417
|
-
if (recovery === "recovered") return;
|
|
3418
|
-
|
|
3419
|
-
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
3420
|
-
phase: "paused",
|
|
3421
|
-
});
|
|
3422
|
-
ctx.ui.notify(
|
|
3423
|
-
`Unit ${unitType} ${unitId} made no meaningful progress for ${supervisor.idle_timeout_minutes}min. Pausing auto-mode.`,
|
|
3424
|
-
"warning",
|
|
3425
|
-
);
|
|
3426
|
-
await pauseAuto(ctx, pi);
|
|
3427
|
-
}, 15000);
|
|
3428
|
-
|
|
3429
|
-
unitTimeoutHandle = setTimeout(async () => {
|
|
3430
|
-
unitTimeoutHandle = null;
|
|
3431
|
-
if (!active) return;
|
|
3432
|
-
if (currentUnit) {
|
|
3433
|
-
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
3434
|
-
phase: "timeout",
|
|
3435
|
-
timeoutAt: Date.now(),
|
|
3436
|
-
});
|
|
3437
|
-
const modelId = ctx.model?.id ?? "unknown";
|
|
3438
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
3439
|
-
}
|
|
3440
|
-
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
3441
|
-
|
|
3442
|
-
const recovery = await recoverTimedOutUnit(ctx, pi, unitType, unitId, "hard");
|
|
3443
|
-
if (recovery === "recovered") return;
|
|
1693
|
+
// Select and apply model
|
|
1694
|
+
const modelResult = await selectAndApplyModel(ctx, pi, unitType, unitId, s.basePath, prefs, s.verbose, s.autoModeStartModel);
|
|
1695
|
+
s.currentUnitRouting = modelResult.routing;
|
|
3444
1696
|
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
1697
|
+
// ── Start unit supervision (delegated to auto-timers.ts) ──
|
|
1698
|
+
clearUnitTimeout();
|
|
1699
|
+
startUnitSupervision({
|
|
1700
|
+
s,
|
|
1701
|
+
ctx,
|
|
1702
|
+
pi,
|
|
1703
|
+
unitType,
|
|
1704
|
+
unitId,
|
|
1705
|
+
prefs,
|
|
1706
|
+
buildSnapshotOpts: () => buildSnapshotOpts(unitType, unitId),
|
|
1707
|
+
buildRecoveryContext: () => buildRecoveryContext(),
|
|
1708
|
+
pauseAuto,
|
|
1709
|
+
});
|
|
3451
1710
|
|
|
3452
|
-
// Inject prompt
|
|
3453
|
-
if (!active) return;
|
|
1711
|
+
// Inject prompt
|
|
1712
|
+
if (!s.active) return;
|
|
3454
1713
|
pi.sendMessage(
|
|
3455
|
-
{ customType: "gsd-auto", content: finalPrompt, display: verbose },
|
|
1714
|
+
{ customType: "gsd-auto", content: finalPrompt, display: s.verbose },
|
|
3456
1715
|
{ triggerTurn: true },
|
|
3457
1716
|
);
|
|
3458
1717
|
|
|
3459
|
-
// For non-artifact-driven UAT types, pause auto-mode after sending the prompt.
|
|
3460
|
-
// The agent will write the UAT result file surfacing it for human review,
|
|
3461
|
-
// then on resume the result file exists and run-uat is skipped automatically.
|
|
3462
1718
|
if (pauseAfterUatDispatch) {
|
|
3463
1719
|
ctx.ui.notify(
|
|
3464
1720
|
"UAT requires human execution. Auto-mode will pause after this unit writes the result file.",
|
|
@@ -3467,7 +1723,7 @@ async function dispatchNextUnit(
|
|
|
3467
1723
|
await pauseAuto(ctx, pi);
|
|
3468
1724
|
}
|
|
3469
1725
|
} finally {
|
|
3470
|
-
|
|
1726
|
+
s.dispatching = false;
|
|
3471
1727
|
}
|
|
3472
1728
|
}
|
|
3473
1729
|
|
|
@@ -3483,32 +1739,26 @@ function ensurePreconditions(
|
|
|
3483
1739
|
const parts = unitId.split("/");
|
|
3484
1740
|
const mid = parts[0]!;
|
|
3485
1741
|
|
|
3486
|
-
// Always ensure milestone dir exists
|
|
3487
1742
|
const mDir = resolveMilestonePath(base, mid);
|
|
3488
1743
|
if (!mDir) {
|
|
3489
1744
|
const newDir = join(milestonesDir(base), mid);
|
|
3490
1745
|
mkdirSync(join(newDir, "slices"), { recursive: true });
|
|
3491
1746
|
}
|
|
3492
1747
|
|
|
3493
|
-
// For slice-level units, ensure slice dir exists
|
|
3494
1748
|
if (parts.length >= 2) {
|
|
3495
1749
|
const sid = parts[1]!;
|
|
3496
1750
|
|
|
3497
|
-
// Re-resolve milestone path after potential creation
|
|
3498
1751
|
const mDirResolved = resolveMilestonePath(base, mid);
|
|
3499
1752
|
if (mDirResolved) {
|
|
3500
1753
|
const slicesDir = join(mDirResolved, "slices");
|
|
3501
1754
|
const sDir = resolveDir(slicesDir, sid);
|
|
3502
1755
|
if (!sDir) {
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
if (!existsSync(tasksDir)) {
|
|
3510
|
-
mkdirSync(tasksDir, { recursive: true });
|
|
3511
|
-
}
|
|
1756
|
+
mkdirSync(join(slicesDir, sid, "tasks"), { recursive: true });
|
|
1757
|
+
}
|
|
1758
|
+
const resolvedSliceDir = resolveDir(slicesDir, sid) ?? sid;
|
|
1759
|
+
const tasksDir = join(slicesDir, resolvedSliceDir, "tasks");
|
|
1760
|
+
if (!existsSync(tasksDir)) {
|
|
1761
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
3512
1762
|
}
|
|
3513
1763
|
}
|
|
3514
1764
|
}
|
|
@@ -3517,296 +1767,12 @@ function ensurePreconditions(
|
|
|
3517
1767
|
|
|
3518
1768
|
// ─── Diagnostics ──────────────────────────────────────────────────────────────
|
|
3519
1769
|
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
if (unitType.startsWith("hook/")) return [];
|
|
3527
|
-
|
|
3528
|
-
const parts = unitId.split("/");
|
|
3529
|
-
const mid = parts[0];
|
|
3530
|
-
const sid = parts[1];
|
|
3531
|
-
const tid = parts[2];
|
|
3532
|
-
|
|
3533
|
-
if (!mid || !sid) return [];
|
|
3534
|
-
|
|
3535
|
-
let issues = [] as Awaited<ReturnType<typeof validatePlanBoundary>>;
|
|
3536
|
-
|
|
3537
|
-
if (unitType === "plan-slice") {
|
|
3538
|
-
issues = await validatePlanBoundary(basePath, mid, sid);
|
|
3539
|
-
} else if (unitType === "execute-task" && tid) {
|
|
3540
|
-
issues = await validateExecuteBoundary(basePath, mid, sid, tid);
|
|
3541
|
-
} else if (unitType === "complete-slice") {
|
|
3542
|
-
issues = await validateCompleteBoundary(basePath, mid, sid);
|
|
3543
|
-
}
|
|
3544
|
-
|
|
3545
|
-
if (issues.length > 0) {
|
|
3546
|
-
ctx.ui.notify(
|
|
3547
|
-
`Observability check (${unitType}) found ${issues.length} warning${issues.length === 1 ? "" : "s"}:\n${formatValidationIssues(issues)}`,
|
|
3548
|
-
"warning",
|
|
3549
|
-
);
|
|
3550
|
-
}
|
|
3551
|
-
|
|
3552
|
-
return issues;
|
|
3553
|
-
}
|
|
3554
|
-
|
|
3555
|
-
function buildObservabilityRepairBlock(issues: import("./observability-validator.ts").ValidationIssue[]): string {
|
|
3556
|
-
if (issues.length === 0) return "";
|
|
3557
|
-
const items = issues.map(issue => {
|
|
3558
|
-
const fileName = issue.file.split("/").pop() || issue.file;
|
|
3559
|
-
let line = `- **${fileName}**: ${issue.message}`;
|
|
3560
|
-
if (issue.suggestion) line += ` → ${issue.suggestion}`;
|
|
3561
|
-
return line;
|
|
3562
|
-
});
|
|
3563
|
-
return [
|
|
3564
|
-
"",
|
|
3565
|
-
"---",
|
|
3566
|
-
"",
|
|
3567
|
-
"## Pre-flight: Observability gaps to fix FIRST",
|
|
3568
|
-
"",
|
|
3569
|
-
"The following issues were detected in plan/summary files for this unit.",
|
|
3570
|
-
"**Read each flagged file, apply the fix described, then proceed with the unit.**",
|
|
3571
|
-
"",
|
|
3572
|
-
...items,
|
|
3573
|
-
"",
|
|
3574
|
-
"---",
|
|
3575
|
-
"",
|
|
3576
|
-
].join("\n");
|
|
3577
|
-
}
|
|
3578
|
-
|
|
3579
|
-
async function recoverTimedOutUnit(
|
|
3580
|
-
ctx: ExtensionContext,
|
|
3581
|
-
pi: ExtensionAPI,
|
|
3582
|
-
unitType: string,
|
|
3583
|
-
unitId: string,
|
|
3584
|
-
reason: "idle" | "hard",
|
|
3585
|
-
): Promise<"recovered" | "paused"> {
|
|
3586
|
-
if (!currentUnit) return "paused";
|
|
3587
|
-
|
|
3588
|
-
const runtime = readUnitRuntimeRecord(basePath, unitType, unitId);
|
|
3589
|
-
const recoveryAttempts = runtime?.recoveryAttempts ?? 0;
|
|
3590
|
-
const maxRecoveryAttempts = reason === "idle" ? 2 : 1;
|
|
3591
|
-
|
|
3592
|
-
const recoveryKey = `${unitType}/${unitId}`;
|
|
3593
|
-
const attemptNumber = (unitRecoveryCount.get(recoveryKey) ?? 0) + 1;
|
|
3594
|
-
unitRecoveryCount.set(recoveryKey, attemptNumber);
|
|
3595
|
-
|
|
3596
|
-
if (attemptNumber > 1) {
|
|
3597
|
-
// Exponential backoff: 2^(n-1) seconds, capped at 30s
|
|
3598
|
-
const backoffMs = Math.min(1000 * Math.pow(2, attemptNumber - 2), 30000);
|
|
3599
|
-
ctx.ui.notify(
|
|
3600
|
-
`Recovery attempt ${attemptNumber} for ${unitType} ${unitId}. Waiting ${backoffMs / 1000}s before retry.`,
|
|
3601
|
-
"info",
|
|
3602
|
-
);
|
|
3603
|
-
await new Promise(r => setTimeout(r, backoffMs));
|
|
3604
|
-
}
|
|
3605
|
-
|
|
3606
|
-
if (unitType === "execute-task") {
|
|
3607
|
-
const status = await inspectExecuteTaskDurability(basePath, unitId);
|
|
3608
|
-
if (!status) return "paused";
|
|
3609
|
-
|
|
3610
|
-
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
3611
|
-
recovery: status,
|
|
3612
|
-
});
|
|
3613
|
-
|
|
3614
|
-
const durableComplete = status.summaryExists && status.taskChecked && status.nextActionAdvanced;
|
|
3615
|
-
if (durableComplete) {
|
|
3616
|
-
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
3617
|
-
phase: "finalized",
|
|
3618
|
-
recovery: status,
|
|
3619
|
-
});
|
|
3620
|
-
ctx.ui.notify(
|
|
3621
|
-
`${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} already completed on disk. Continuing auto-mode. (attempt ${attemptNumber})`,
|
|
3622
|
-
"info",
|
|
3623
|
-
);
|
|
3624
|
-
unitRecoveryCount.delete(recoveryKey);
|
|
3625
|
-
await dispatchNextUnit(ctx, pi);
|
|
3626
|
-
return "recovered";
|
|
3627
|
-
}
|
|
3628
|
-
|
|
3629
|
-
if (recoveryAttempts < maxRecoveryAttempts) {
|
|
3630
|
-
const isEscalation = recoveryAttempts > 0;
|
|
3631
|
-
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
3632
|
-
phase: "recovered",
|
|
3633
|
-
recovery: status,
|
|
3634
|
-
recoveryAttempts: recoveryAttempts + 1,
|
|
3635
|
-
lastRecoveryReason: reason,
|
|
3636
|
-
lastProgressAt: Date.now(),
|
|
3637
|
-
progressCount: (runtime?.progressCount ?? 0) + 1,
|
|
3638
|
-
lastProgressKind: reason === "idle" ? "idle-recovery-retry" : "hard-recovery-retry",
|
|
3639
|
-
});
|
|
3640
|
-
|
|
3641
|
-
const steeringLines = isEscalation
|
|
3642
|
-
? [
|
|
3643
|
-
`**FINAL ${reason === "idle" ? "IDLE" : "HARD TIMEOUT"} RECOVERY — last chance before this task is skipped.**`,
|
|
3644
|
-
`You are still executing ${unitType} ${unitId}.`,
|
|
3645
|
-
`Recovery attempt ${recoveryAttempts + 1} of ${maxRecoveryAttempts}.`,
|
|
3646
|
-
`Current durability status: ${formatExecuteTaskRecoveryStatus(status)}.`,
|
|
3647
|
-
"You MUST finish the durable output NOW, even if incomplete.",
|
|
3648
|
-
"Write the task summary with whatever you have accomplished so far.",
|
|
3649
|
-
"Mark the task [x] in the plan. Commit your work.",
|
|
3650
|
-
"A partial summary is infinitely better than no summary.",
|
|
3651
|
-
]
|
|
3652
|
-
: [
|
|
3653
|
-
`**${reason === "idle" ? "IDLE" : "HARD TIMEOUT"} RECOVERY — do not stop.**`,
|
|
3654
|
-
`You are still executing ${unitType} ${unitId}.`,
|
|
3655
|
-
`Recovery attempt ${recoveryAttempts + 1} of ${maxRecoveryAttempts}.`,
|
|
3656
|
-
`Current durability status: ${formatExecuteTaskRecoveryStatus(status)}.`,
|
|
3657
|
-
"Do not keep exploring.",
|
|
3658
|
-
"Immediately finish the required durable output for this unit.",
|
|
3659
|
-
"If full completion is impossible, write the partial artifact/state needed for recovery and make the blocker explicit.",
|
|
3660
|
-
];
|
|
3661
|
-
|
|
3662
|
-
pi.sendMessage(
|
|
3663
|
-
{
|
|
3664
|
-
customType: "gsd-auto-timeout-recovery",
|
|
3665
|
-
display: verbose,
|
|
3666
|
-
content: steeringLines.join("\n"),
|
|
3667
|
-
},
|
|
3668
|
-
{ triggerTurn: true, deliverAs: "steer" },
|
|
3669
|
-
);
|
|
3670
|
-
ctx.ui.notify(
|
|
3671
|
-
`${reason === "idle" ? "Idle" : "Timeout"} recovery: steering ${unitType} ${unitId} to finish durable output (attempt ${attemptNumber}, session ${recoveryAttempts + 1}/${maxRecoveryAttempts}).`,
|
|
3672
|
-
"warning",
|
|
3673
|
-
);
|
|
3674
|
-
return "recovered";
|
|
3675
|
-
}
|
|
3676
|
-
|
|
3677
|
-
// Retries exhausted — write missing durable artifacts and advance.
|
|
3678
|
-
const diagnostic = formatExecuteTaskRecoveryStatus(status);
|
|
3679
|
-
const [mid, sid, tid] = unitId.split("/");
|
|
3680
|
-
const skipped = mid && sid && tid
|
|
3681
|
-
? skipExecuteTask(basePath, mid, sid, tid, status, reason, maxRecoveryAttempts)
|
|
3682
|
-
: false;
|
|
3683
|
-
|
|
3684
|
-
if (skipped) {
|
|
3685
|
-
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
3686
|
-
phase: "skipped",
|
|
3687
|
-
recovery: status,
|
|
3688
|
-
recoveryAttempts: recoveryAttempts + 1,
|
|
3689
|
-
lastRecoveryReason: reason,
|
|
3690
|
-
});
|
|
3691
|
-
ctx.ui.notify(
|
|
3692
|
-
`${unitType} ${unitId} skipped after ${maxRecoveryAttempts} recovery attempts (${diagnostic}). Blocker artifacts written. Advancing pipeline. (attempt ${attemptNumber})`,
|
|
3693
|
-
"warning",
|
|
3694
|
-
);
|
|
3695
|
-
unitRecoveryCount.delete(recoveryKey);
|
|
3696
|
-
await dispatchNextUnit(ctx, pi);
|
|
3697
|
-
return "recovered";
|
|
3698
|
-
}
|
|
3699
|
-
|
|
3700
|
-
// Fallback: couldn't write skip artifacts — pause as before.
|
|
3701
|
-
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
3702
|
-
phase: "paused",
|
|
3703
|
-
recovery: status,
|
|
3704
|
-
recoveryAttempts: recoveryAttempts + 1,
|
|
3705
|
-
lastRecoveryReason: reason,
|
|
3706
|
-
});
|
|
3707
|
-
ctx.ui.notify(
|
|
3708
|
-
`${reason === "idle" ? "Idle" : "Timeout"} recovery check for ${unitType} ${unitId}: ${diagnostic}`,
|
|
3709
|
-
"warning",
|
|
3710
|
-
);
|
|
3711
|
-
return "paused";
|
|
3712
|
-
}
|
|
3713
|
-
|
|
3714
|
-
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath) ?? "required durable artifact";
|
|
3715
|
-
|
|
3716
|
-
// Check if the artifact already exists on disk — agent may have written it
|
|
3717
|
-
// without signaling completion.
|
|
3718
|
-
const artifactPath = resolveExpectedArtifactPath(unitType, unitId, basePath);
|
|
3719
|
-
if (artifactPath && existsSync(artifactPath)) {
|
|
3720
|
-
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
3721
|
-
phase: "finalized",
|
|
3722
|
-
recoveryAttempts: recoveryAttempts + 1,
|
|
3723
|
-
lastRecoveryReason: reason,
|
|
3724
|
-
});
|
|
3725
|
-
ctx.ui.notify(
|
|
3726
|
-
`${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} artifact already exists on disk. Advancing. (attempt ${attemptNumber})`,
|
|
3727
|
-
"info",
|
|
3728
|
-
);
|
|
3729
|
-
unitRecoveryCount.delete(recoveryKey);
|
|
3730
|
-
await dispatchNextUnit(ctx, pi);
|
|
3731
|
-
return "recovered";
|
|
3732
|
-
}
|
|
3733
|
-
|
|
3734
|
-
if (recoveryAttempts < maxRecoveryAttempts) {
|
|
3735
|
-
const isEscalation = recoveryAttempts > 0;
|
|
3736
|
-
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
3737
|
-
phase: "recovered",
|
|
3738
|
-
recoveryAttempts: recoveryAttempts + 1,
|
|
3739
|
-
lastRecoveryReason: reason,
|
|
3740
|
-
lastProgressAt: Date.now(),
|
|
3741
|
-
progressCount: (runtime?.progressCount ?? 0) + 1,
|
|
3742
|
-
lastProgressKind: reason === "idle" ? "idle-recovery-retry" : "hard-recovery-retry",
|
|
3743
|
-
});
|
|
3744
|
-
|
|
3745
|
-
const steeringLines = isEscalation
|
|
3746
|
-
? [
|
|
3747
|
-
`**FINAL ${reason === "idle" ? "IDLE" : "HARD TIMEOUT"} RECOVERY — last chance before skip.**`,
|
|
3748
|
-
`You are still executing ${unitType} ${unitId}.`,
|
|
3749
|
-
`Recovery attempt ${recoveryAttempts + 1} of ${maxRecoveryAttempts} — next failure skips this unit.`,
|
|
3750
|
-
`Expected durable output: ${expected}.`,
|
|
3751
|
-
"You MUST write the artifact file NOW, even if incomplete.",
|
|
3752
|
-
"Write whatever you have — partial research, preliminary findings, best-effort analysis.",
|
|
3753
|
-
"A partial artifact is infinitely better than no artifact.",
|
|
3754
|
-
"If you are truly blocked, write the file with a BLOCKER section explaining why.",
|
|
3755
|
-
]
|
|
3756
|
-
: [
|
|
3757
|
-
`**${reason === "idle" ? "IDLE" : "HARD TIMEOUT"} RECOVERY — stay in auto-mode.**`,
|
|
3758
|
-
`You are still executing ${unitType} ${unitId}.`,
|
|
3759
|
-
`Recovery attempt ${recoveryAttempts + 1} of ${maxRecoveryAttempts}.`,
|
|
3760
|
-
`Expected durable output: ${expected}.`,
|
|
3761
|
-
"Stop broad exploration.",
|
|
3762
|
-
"Write the required artifact now.",
|
|
3763
|
-
"If blocked, write the partial artifact and explicitly record the blocker instead of going silent.",
|
|
3764
|
-
];
|
|
3765
|
-
|
|
3766
|
-
pi.sendMessage(
|
|
3767
|
-
{
|
|
3768
|
-
customType: "gsd-auto-timeout-recovery",
|
|
3769
|
-
display: verbose,
|
|
3770
|
-
content: steeringLines.join("\n"),
|
|
3771
|
-
},
|
|
3772
|
-
{ triggerTurn: true, deliverAs: "steer" },
|
|
3773
|
-
);
|
|
3774
|
-
ctx.ui.notify(
|
|
3775
|
-
`${reason === "idle" ? "Idle" : "Timeout"} recovery: steering ${unitType} ${unitId} to produce ${expected} (attempt ${attemptNumber}, session ${recoveryAttempts + 1}/${maxRecoveryAttempts}).`,
|
|
3776
|
-
"warning",
|
|
3777
|
-
);
|
|
3778
|
-
return "recovered";
|
|
3779
|
-
}
|
|
3780
|
-
|
|
3781
|
-
// Retries exhausted — write a blocker placeholder and advance the pipeline
|
|
3782
|
-
// instead of silently stalling.
|
|
3783
|
-
const placeholder = writeBlockerPlaceholder(
|
|
3784
|
-
unitType, unitId, basePath,
|
|
3785
|
-
`${reason} recovery exhausted ${maxRecoveryAttempts} attempts without producing the artifact.`,
|
|
3786
|
-
);
|
|
3787
|
-
|
|
3788
|
-
if (placeholder) {
|
|
3789
|
-
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
3790
|
-
phase: "skipped",
|
|
3791
|
-
recoveryAttempts: recoveryAttempts + 1,
|
|
3792
|
-
lastRecoveryReason: reason,
|
|
3793
|
-
});
|
|
3794
|
-
ctx.ui.notify(
|
|
3795
|
-
`${unitType} ${unitId} skipped after ${maxRecoveryAttempts} recovery attempts. Blocker placeholder written to ${placeholder}. Advancing pipeline. (attempt ${attemptNumber})`,
|
|
3796
|
-
"warning",
|
|
3797
|
-
);
|
|
3798
|
-
unitRecoveryCount.delete(recoveryKey);
|
|
3799
|
-
await dispatchNextUnit(ctx, pi);
|
|
3800
|
-
return "recovered";
|
|
3801
|
-
}
|
|
3802
|
-
|
|
3803
|
-
// Fallback: couldn't resolve artifact path — pause as before.
|
|
3804
|
-
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
3805
|
-
phase: "paused",
|
|
3806
|
-
recoveryAttempts: recoveryAttempts + 1,
|
|
3807
|
-
lastRecoveryReason: reason,
|
|
3808
|
-
});
|
|
3809
|
-
return "paused";
|
|
1770
|
+
/** Build recovery context from module state for recoverTimedOutUnit */
|
|
1771
|
+
function buildRecoveryContext(): import("./auto-timeout-recovery.js").RecoveryContext {
|
|
1772
|
+
return { basePath: s.basePath, verbose: s.verbose,
|
|
1773
|
+
currentUnitStartedAt: s.currentUnit?.startedAt ?? Date.now(), unitRecoveryCount: s.unitRecoveryCount,
|
|
1774
|
+
dispatchNextUnit,
|
|
1775
|
+
};
|
|
3810
1776
|
}
|
|
3811
1777
|
|
|
3812
1778
|
// Re-export recovery functions for external consumers
|
|
@@ -3822,9 +1788,8 @@ export {
|
|
|
3822
1788
|
* Test-only: expose skip-loop state for unit tests.
|
|
3823
1789
|
* Not part of the public API.
|
|
3824
1790
|
*/
|
|
3825
|
-
export function _getUnitConsecutiveSkips(): Map<string, number> { return unitConsecutiveSkips; }
|
|
3826
|
-
export function _resetUnitConsecutiveSkips(): void { unitConsecutiveSkips.clear(); }
|
|
3827
|
-
export { MAX_CONSECUTIVE_SKIPS };
|
|
1791
|
+
export function _getUnitConsecutiveSkips(): Map<string, number> { return s.unitConsecutiveSkips; }
|
|
1792
|
+
export function _resetUnitConsecutiveSkips(): void { s.unitConsecutiveSkips.clear(); }
|
|
3828
1793
|
|
|
3829
1794
|
/**
|
|
3830
1795
|
* Dispatch a hook unit directly, bypassing normal pre-dispatch hooks.
|
|
@@ -3840,37 +1805,31 @@ export async function dispatchHookUnit(
|
|
|
3840
1805
|
hookModel: string | undefined,
|
|
3841
1806
|
targetBasePath: string,
|
|
3842
1807
|
): Promise<boolean> {
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
completedUnits = [];
|
|
3853
|
-
pendingQuickTasks = [];
|
|
1808
|
+
if (!s.active) {
|
|
1809
|
+
s.active = true;
|
|
1810
|
+
s.stepMode = true;
|
|
1811
|
+
s.cmdCtx = ctx as ExtensionCommandContext;
|
|
1812
|
+
s.basePath = targetBasePath;
|
|
1813
|
+
s.autoStartTime = Date.now();
|
|
1814
|
+
s.currentUnit = null;
|
|
1815
|
+
s.completedUnits = [];
|
|
1816
|
+
s.pendingQuickTasks = [];
|
|
3854
1817
|
}
|
|
3855
1818
|
|
|
3856
1819
|
const hookUnitType = `hook/${hookName}`;
|
|
3857
1820
|
const hookStartedAt = Date.now();
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
// Create a new session for the hook
|
|
3863
|
-
const result = await cmdCtx!.newSession();
|
|
1821
|
+
|
|
1822
|
+
s.currentUnit = { type: triggerUnitType, id: triggerUnitId, startedAt: hookStartedAt };
|
|
1823
|
+
|
|
1824
|
+
const result = await s.cmdCtx!.newSession();
|
|
3864
1825
|
if (result.cancelled) {
|
|
3865
1826
|
await stopAuto(ctx, pi);
|
|
3866
1827
|
return false;
|
|
3867
1828
|
}
|
|
3868
1829
|
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
// Write runtime record
|
|
3873
|
-
writeUnitRuntimeRecord(basePath, hookUnitType, triggerUnitId, hookStartedAt, {
|
|
1830
|
+
s.currentUnit = { type: hookUnitType, id: triggerUnitId, startedAt: hookStartedAt };
|
|
1831
|
+
|
|
1832
|
+
writeUnitRuntimeRecord(s.basePath, hookUnitType, triggerUnitId, hookStartedAt, {
|
|
3874
1833
|
phase: "dispatched",
|
|
3875
1834
|
wrapupWarningSent: false,
|
|
3876
1835
|
timeoutAt: null,
|
|
@@ -3879,7 +1838,6 @@ export async function dispatchHookUnit(
|
|
|
3879
1838
|
lastProgressKind: "dispatch",
|
|
3880
1839
|
});
|
|
3881
1840
|
|
|
3882
|
-
// Switch model if specified
|
|
3883
1841
|
if (hookModel) {
|
|
3884
1842
|
const availableModels = ctx.modelRegistry.getAvailable();
|
|
3885
1843
|
const match = availableModels.find(m =>
|
|
@@ -3888,23 +1846,22 @@ export async function dispatchHookUnit(
|
|
|
3888
1846
|
if (match) {
|
|
3889
1847
|
try {
|
|
3890
1848
|
await pi.setModel(match);
|
|
3891
|
-
} catch { /* non-fatal
|
|
1849
|
+
} catch { /* non-fatal */ }
|
|
3892
1850
|
}
|
|
3893
1851
|
}
|
|
3894
1852
|
|
|
3895
|
-
// Write lock
|
|
3896
1853
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
3897
|
-
|
|
1854
|
+
updateSessionLock(lockBase(), hookUnitType, triggerUnitId, s.completedUnits.length, sessionFile);
|
|
1855
|
+
writeLock(lockBase(), hookUnitType, triggerUnitId, s.completedUnits.length, sessionFile);
|
|
3898
1856
|
|
|
3899
|
-
// Set up timeout
|
|
3900
1857
|
clearUnitTimeout();
|
|
3901
1858
|
const supervisor = resolveAutoSupervisorConfig();
|
|
3902
1859
|
const hookHardTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
|
|
3903
|
-
unitTimeoutHandle = setTimeout(async () => {
|
|
3904
|
-
unitTimeoutHandle = null;
|
|
3905
|
-
if (!active) return;
|
|
3906
|
-
if (currentUnit) {
|
|
3907
|
-
writeUnitRuntimeRecord(basePath, hookUnitType, triggerUnitId, hookStartedAt, {
|
|
1860
|
+
s.unitTimeoutHandle = setTimeout(async () => {
|
|
1861
|
+
s.unitTimeoutHandle = null;
|
|
1862
|
+
if (!s.active) return;
|
|
1863
|
+
if (s.currentUnit) {
|
|
1864
|
+
writeUnitRuntimeRecord(s.basePath, hookUnitType, triggerUnitId, hookStartedAt, {
|
|
3908
1865
|
phase: "timeout",
|
|
3909
1866
|
timeoutAt: Date.now(),
|
|
3910
1867
|
});
|
|
@@ -3917,220 +1874,19 @@ export async function dispatchHookUnit(
|
|
|
3917
1874
|
await pauseAuto(ctx, pi);
|
|
3918
1875
|
}, hookHardTimeoutMs);
|
|
3919
1876
|
|
|
3920
|
-
|
|
3921
|
-
ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
|
|
1877
|
+
ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
|
|
3922
1878
|
ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
|
|
3923
1879
|
|
|
3924
|
-
// Send the hook prompt
|
|
3925
1880
|
console.log(`[dispatchHookUnit] Sending prompt of length ${hookPrompt.length}`);
|
|
3926
1881
|
console.log(`[dispatchHookUnit] Prompt preview: ${hookPrompt.substring(0, 200)}...`);
|
|
3927
1882
|
pi.sendMessage(
|
|
3928
1883
|
{ customType: "gsd-auto", content: hookPrompt, display: true },
|
|
3929
1884
|
{ triggerTurn: true },
|
|
3930
1885
|
);
|
|
3931
|
-
|
|
1886
|
+
|
|
3932
1887
|
return true;
|
|
3933
1888
|
}
|
|
3934
1889
|
|
|
3935
1890
|
|
|
3936
|
-
//
|
|
3937
|
-
|
|
3938
|
-
export async function dispatchDirectPhase(
|
|
3939
|
-
ctx: ExtensionCommandContext,
|
|
3940
|
-
pi: ExtensionAPI,
|
|
3941
|
-
phase: string,
|
|
3942
|
-
base: string,
|
|
3943
|
-
): Promise<void> {
|
|
3944
|
-
const state = await deriveState(base);
|
|
3945
|
-
const mid = state.activeMilestone?.id;
|
|
3946
|
-
const midTitle = state.activeMilestone?.title ?? "";
|
|
3947
|
-
|
|
3948
|
-
if (!mid) {
|
|
3949
|
-
ctx.ui.notify("Cannot dispatch: no active milestone.", "warning");
|
|
3950
|
-
return;
|
|
3951
|
-
}
|
|
3952
|
-
|
|
3953
|
-
const normalized = phase.toLowerCase();
|
|
3954
|
-
let unitType: string;
|
|
3955
|
-
let unitId: string;
|
|
3956
|
-
let prompt: string;
|
|
3957
|
-
|
|
3958
|
-
switch (normalized) {
|
|
3959
|
-
case "research":
|
|
3960
|
-
case "research-milestone":
|
|
3961
|
-
case "research-slice": {
|
|
3962
|
-
const isSlice = normalized === "research-slice" || (normalized === "research" && state.phase !== "pre-planning");
|
|
3963
|
-
if (isSlice) {
|
|
3964
|
-
const sid = state.activeSlice?.id;
|
|
3965
|
-
const sTitle = state.activeSlice?.title ?? "";
|
|
3966
|
-
if (!sid) {
|
|
3967
|
-
ctx.ui.notify("Cannot dispatch research-slice: no active slice.", "warning");
|
|
3968
|
-
return;
|
|
3969
|
-
}
|
|
3970
|
-
|
|
3971
|
-
// When require_slice_discussion is enabled, pause auto-mode before
|
|
3972
|
-
// each new slice so the user can discuss requirements first (#789).
|
|
3973
|
-
const sliceContextFile = resolveSliceFile(base, mid, sid, "CONTEXT");
|
|
3974
|
-
const requireDiscussion = loadEffectiveGSDPreferences()?.preferences?.phases?.require_slice_discussion;
|
|
3975
|
-
if (requireDiscussion && !sliceContextFile) {
|
|
3976
|
-
ctx.ui.notify(
|
|
3977
|
-
`Slice ${sid} requires discussion before planning. Run /gsd discuss to discuss this slice, then /gsd auto to resume.`,
|
|
3978
|
-
"info",
|
|
3979
|
-
);
|
|
3980
|
-
await pauseAuto(ctx, pi);
|
|
3981
|
-
return;
|
|
3982
|
-
}
|
|
3983
|
-
|
|
3984
|
-
unitType = "research-slice";
|
|
3985
|
-
unitId = `${mid}/${sid}`;
|
|
3986
|
-
prompt = await buildResearchSlicePrompt(mid, midTitle, sid, sTitle, base);
|
|
3987
|
-
} else {
|
|
3988
|
-
unitType = "research-milestone";
|
|
3989
|
-
unitId = mid;
|
|
3990
|
-
prompt = await buildResearchMilestonePrompt(mid, midTitle, base);
|
|
3991
|
-
}
|
|
3992
|
-
break;
|
|
3993
|
-
}
|
|
3994
|
-
|
|
3995
|
-
case "plan":
|
|
3996
|
-
case "plan-milestone":
|
|
3997
|
-
case "plan-slice": {
|
|
3998
|
-
const isSlice = normalized === "plan-slice" || (normalized === "plan" && state.phase !== "pre-planning");
|
|
3999
|
-
if (isSlice) {
|
|
4000
|
-
const sid = state.activeSlice?.id;
|
|
4001
|
-
const sTitle = state.activeSlice?.title ?? "";
|
|
4002
|
-
if (!sid) {
|
|
4003
|
-
ctx.ui.notify("Cannot dispatch plan-slice: no active slice.", "warning");
|
|
4004
|
-
return;
|
|
4005
|
-
}
|
|
4006
|
-
unitType = "plan-slice";
|
|
4007
|
-
unitId = `${mid}/${sid}`;
|
|
4008
|
-
prompt = await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, base);
|
|
4009
|
-
} else {
|
|
4010
|
-
unitType = "plan-milestone";
|
|
4011
|
-
unitId = mid;
|
|
4012
|
-
prompt = await buildPlanMilestonePrompt(mid, midTitle, base);
|
|
4013
|
-
}
|
|
4014
|
-
break;
|
|
4015
|
-
}
|
|
4016
|
-
|
|
4017
|
-
case "execute":
|
|
4018
|
-
case "execute-task": {
|
|
4019
|
-
const sid = state.activeSlice?.id;
|
|
4020
|
-
const sTitle = state.activeSlice?.title ?? "";
|
|
4021
|
-
const tid = state.activeTask?.id;
|
|
4022
|
-
const tTitle = state.activeTask?.title ?? "";
|
|
4023
|
-
if (!sid) {
|
|
4024
|
-
ctx.ui.notify("Cannot dispatch execute-task: no active slice.", "warning");
|
|
4025
|
-
return;
|
|
4026
|
-
}
|
|
4027
|
-
if (!tid) {
|
|
4028
|
-
ctx.ui.notify("Cannot dispatch execute-task: no active task.", "warning");
|
|
4029
|
-
return;
|
|
4030
|
-
}
|
|
4031
|
-
unitType = "execute-task";
|
|
4032
|
-
unitId = `${mid}/${sid}/${tid}`;
|
|
4033
|
-
prompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base);
|
|
4034
|
-
break;
|
|
4035
|
-
}
|
|
4036
|
-
|
|
4037
|
-
case "complete":
|
|
4038
|
-
case "complete-slice":
|
|
4039
|
-
case "complete-milestone": {
|
|
4040
|
-
const isSlice = normalized === "complete-slice" || (normalized === "complete" && state.phase === "summarizing");
|
|
4041
|
-
if (isSlice) {
|
|
4042
|
-
const sid = state.activeSlice?.id;
|
|
4043
|
-
const sTitle = state.activeSlice?.title ?? "";
|
|
4044
|
-
if (!sid) {
|
|
4045
|
-
ctx.ui.notify("Cannot dispatch complete-slice: no active slice.", "warning");
|
|
4046
|
-
return;
|
|
4047
|
-
}
|
|
4048
|
-
unitType = "complete-slice";
|
|
4049
|
-
unitId = `${mid}/${sid}`;
|
|
4050
|
-
prompt = await buildCompleteSlicePrompt(mid, midTitle, sid, sTitle, base);
|
|
4051
|
-
} else {
|
|
4052
|
-
unitType = "complete-milestone";
|
|
4053
|
-
unitId = mid;
|
|
4054
|
-
prompt = await buildCompleteMilestonePrompt(mid, midTitle, base);
|
|
4055
|
-
}
|
|
4056
|
-
break;
|
|
4057
|
-
}
|
|
4058
|
-
|
|
4059
|
-
case "reassess":
|
|
4060
|
-
case "reassess-roadmap": {
|
|
4061
|
-
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
4062
|
-
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
|
|
4063
|
-
if (!roadmapContent) {
|
|
4064
|
-
ctx.ui.notify("Cannot dispatch reassess-roadmap: no roadmap found.", "warning");
|
|
4065
|
-
return;
|
|
4066
|
-
}
|
|
4067
|
-
const roadmap = parseRoadmap(roadmapContent);
|
|
4068
|
-
const completedSlices = roadmap.slices.filter(s => s.done);
|
|
4069
|
-
if (completedSlices.length === 0) {
|
|
4070
|
-
ctx.ui.notify("Cannot dispatch reassess-roadmap: no completed slices.", "warning");
|
|
4071
|
-
return;
|
|
4072
|
-
}
|
|
4073
|
-
const completedSliceId = completedSlices[completedSlices.length - 1].id;
|
|
4074
|
-
unitType = "reassess-roadmap";
|
|
4075
|
-
unitId = `${mid}/${completedSliceId}`;
|
|
4076
|
-
prompt = await buildReassessRoadmapPrompt(mid, midTitle, completedSliceId, base);
|
|
4077
|
-
break;
|
|
4078
|
-
}
|
|
4079
|
-
|
|
4080
|
-
case "uat":
|
|
4081
|
-
case "run-uat": {
|
|
4082
|
-
const sid = state.activeSlice?.id;
|
|
4083
|
-
if (!sid) {
|
|
4084
|
-
ctx.ui.notify("Cannot dispatch run-uat: no active slice.", "warning");
|
|
4085
|
-
return;
|
|
4086
|
-
}
|
|
4087
|
-
const uatFile = resolveSliceFile(base, mid, sid, "UAT");
|
|
4088
|
-
if (!uatFile) {
|
|
4089
|
-
ctx.ui.notify("Cannot dispatch run-uat: no UAT file found.", "warning");
|
|
4090
|
-
return;
|
|
4091
|
-
}
|
|
4092
|
-
const uatContent = await loadFile(uatFile);
|
|
4093
|
-
if (!uatContent) {
|
|
4094
|
-
ctx.ui.notify("Cannot dispatch run-uat: UAT file is empty.", "warning");
|
|
4095
|
-
return;
|
|
4096
|
-
}
|
|
4097
|
-
const uatPath = relSliceFile(base, mid, sid, "UAT");
|
|
4098
|
-
unitType = "run-uat";
|
|
4099
|
-
unitId = `${mid}/${sid}`;
|
|
4100
|
-
prompt = await buildRunUatPrompt(mid, sid, uatPath, uatContent, base);
|
|
4101
|
-
break;
|
|
4102
|
-
}
|
|
4103
|
-
|
|
4104
|
-
case "replan":
|
|
4105
|
-
case "replan-slice": {
|
|
4106
|
-
const sid = state.activeSlice?.id;
|
|
4107
|
-
const sTitle = state.activeSlice?.title ?? "";
|
|
4108
|
-
if (!sid) {
|
|
4109
|
-
ctx.ui.notify("Cannot dispatch replan-slice: no active slice.", "warning");
|
|
4110
|
-
return;
|
|
4111
|
-
}
|
|
4112
|
-
unitType = "replan-slice";
|
|
4113
|
-
unitId = `${mid}/${sid}`;
|
|
4114
|
-
prompt = await buildReplanSlicePrompt(mid, midTitle, sid, sTitle, base);
|
|
4115
|
-
break;
|
|
4116
|
-
}
|
|
4117
|
-
|
|
4118
|
-
default:
|
|
4119
|
-
ctx.ui.notify(
|
|
4120
|
-
`Unknown phase "${phase}". Valid phases: research, plan, execute, complete, reassess, uat, replan.`,
|
|
4121
|
-
"warning",
|
|
4122
|
-
);
|
|
4123
|
-
return;
|
|
4124
|
-
}
|
|
4125
|
-
|
|
4126
|
-
ctx.ui.notify(`Dispatching ${unitType} for ${unitId}...`, "info");
|
|
4127
|
-
const result = await ctx.newSession();
|
|
4128
|
-
if (result.cancelled) {
|
|
4129
|
-
ctx.ui.notify("Session creation cancelled.", "warning");
|
|
4130
|
-
return;
|
|
4131
|
-
}
|
|
4132
|
-
pi.sendMessage(
|
|
4133
|
-
{ customType: "gsd-dispatch", content: prompt, display: false },
|
|
4134
|
-
{ triggerTurn: true },
|
|
4135
|
-
);
|
|
4136
|
-
}
|
|
1891
|
+
// Direct phase dispatch → auto-direct-dispatch.ts
|
|
1892
|
+
export { dispatchDirectPhase } from "./auto-direct-dispatch.js";
|