gsd-pi 2.18.0 → 2.20.0
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 +5 -1
- package/dist/cli.js +3 -3
- package/dist/onboarding.d.ts +3 -1
- package/dist/onboarding.js +77 -3
- package/dist/remote-questions-config.d.ts +1 -1
- package/dist/resources/extensions/google-search/index.ts +164 -47
- package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +148 -39
- package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/dist/resources/extensions/gsd/auto.ts +690 -39
- package/dist/resources/extensions/gsd/captures.ts +384 -0
- package/dist/resources/extensions/gsd/commands.ts +654 -36
- package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
- package/dist/resources/extensions/gsd/context-budget.ts +243 -0
- package/dist/resources/extensions/gsd/context-store.ts +195 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +51 -3
- package/dist/resources/extensions/gsd/db-writer.ts +341 -0
- package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
- package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
- package/dist/resources/extensions/gsd/doctor.ts +283 -2
- package/dist/resources/extensions/gsd/export.ts +81 -2
- package/dist/resources/extensions/gsd/files.ts +39 -9
- package/dist/resources/extensions/gsd/git-service.ts +6 -0
- package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
- package/dist/resources/extensions/gsd/history.ts +0 -1
- package/dist/resources/extensions/gsd/index.ts +277 -1
- package/dist/resources/extensions/gsd/md-importer.ts +526 -0
- package/dist/resources/extensions/gsd/metrics.ts +84 -0
- package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/dist/resources/extensions/gsd/model-router.ts +256 -0
- package/dist/resources/extensions/gsd/notifications.ts +0 -1
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +72 -2
- package/dist/resources/extensions/gsd/preferences.ts +198 -150
- package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
- package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
- package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -1
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- package/dist/resources/extensions/gsd/quick.ts +156 -0
- package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
- package/dist/resources/extensions/gsd/skill-health.ts +417 -0
- package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
- package/dist/resources/extensions/gsd/state.ts +30 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
- package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
- package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
- package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
- package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
- package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
- package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
- package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
- package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
- package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
- package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
- package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
- package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
- package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
- package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
- package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
- package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
- package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
- package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
- package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
- package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
- package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
- package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +488 -1
- package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
- package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
- package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
- package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
- package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
- package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
- package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
- package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +290 -0
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
- package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
- package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
- package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
- package/dist/resources/extensions/gsd/types.ts +29 -0
- package/dist/resources/extensions/gsd/undo.ts +0 -1
- package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
- package/dist/resources/extensions/gsd/visualizer-data.ts +505 -0
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +337 -0
- package/dist/resources/extensions/gsd/visualizer-views.ts +755 -0
- package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
- package/dist/resources/extensions/remote-questions/config.ts +4 -2
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +35 -4
- package/dist/resources/extensions/remote-questions/format.ts +166 -14
- package/dist/resources/extensions/remote-questions/manager.ts +14 -4
- package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
- package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
- package/dist/resources/extensions/remote-questions/types.ts +2 -1
- package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
- package/dist/resources/extensions/voice/index.ts +4 -3
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -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 +25 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
- package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -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 +43 -11
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
- package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
- package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
- package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
- package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
- package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
- package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
- package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
- package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
- package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
- package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
- package/src/resources/extensions/google-search/index.ts +164 -47
- package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +148 -39
- package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/src/resources/extensions/gsd/auto.ts +690 -39
- package/src/resources/extensions/gsd/captures.ts +384 -0
- package/src/resources/extensions/gsd/commands.ts +654 -36
- package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
- package/src/resources/extensions/gsd/context-budget.ts +243 -0
- package/src/resources/extensions/gsd/context-store.ts +195 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +51 -3
- package/src/resources/extensions/gsd/db-writer.ts +341 -0
- package/src/resources/extensions/gsd/debug-logger.ts +178 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
- package/src/resources/extensions/gsd/doctor.ts +283 -2
- package/src/resources/extensions/gsd/export.ts +81 -2
- package/src/resources/extensions/gsd/files.ts +39 -9
- package/src/resources/extensions/gsd/git-service.ts +6 -0
- package/src/resources/extensions/gsd/gsd-db.ts +752 -0
- package/src/resources/extensions/gsd/guided-flow.ts +26 -1
- package/src/resources/extensions/gsd/history.ts +0 -1
- package/src/resources/extensions/gsd/index.ts +277 -1
- package/src/resources/extensions/gsd/md-importer.ts +526 -0
- package/src/resources/extensions/gsd/metrics.ts +84 -0
- package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/src/resources/extensions/gsd/model-router.ts +256 -0
- package/src/resources/extensions/gsd/notifications.ts +0 -1
- package/src/resources/extensions/gsd/post-unit-hooks.ts +72 -2
- package/src/resources/extensions/gsd/preferences.ts +198 -150
- package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
- package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
- package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -1
- package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- package/src/resources/extensions/gsd/quick.ts +156 -0
- package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
- package/src/resources/extensions/gsd/skill-health.ts +417 -0
- package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
- package/src/resources/extensions/gsd/state.ts +30 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
- package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
- package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
- package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
- package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
- package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
- package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
- package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
- package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
- package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +488 -1
- package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
- package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
- package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
- package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +290 -0
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
- package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
- package/src/resources/extensions/gsd/triage-ui.ts +175 -0
- package/src/resources/extensions/gsd/types.ts +29 -0
- package/src/resources/extensions/gsd/undo.ts +0 -1
- package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +505 -0
- package/src/resources/extensions/gsd/visualizer-overlay.ts +337 -0
- package/src/resources/extensions/gsd/visualizer-views.ts +755 -0
- package/src/resources/extensions/gsd/worktree-command.ts +18 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
- package/src/resources/extensions/remote-questions/config.ts +4 -2
- package/src/resources/extensions/remote-questions/discord-adapter.ts +35 -4
- package/src/resources/extensions/remote-questions/format.ts +166 -14
- package/src/resources/extensions/remote-questions/manager.ts +14 -4
- package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
- package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
- package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
- package/src/resources/extensions/remote-questions/types.ts +2 -1
- package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
- package/src/resources/extensions/voice/index.ts +4 -3
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
import { deriveState, invalidateStateCache } from "./state.js";
|
|
20
20
|
import type { BudgetEnforcementMode, GSDState } from "./types.js";
|
|
21
21
|
import { loadFile, parseRoadmap, getManifestStatus, resolveAllOverrides } from "./files.js";
|
|
22
|
+
import { loadPrompt } from "./prompt-loader.js";
|
|
22
23
|
export { inlinePriorMilestoneSummary } from "./files.js";
|
|
23
24
|
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
24
25
|
import {
|
|
@@ -39,9 +40,12 @@ import {
|
|
|
39
40
|
readUnitRuntimeRecord,
|
|
40
41
|
writeUnitRuntimeRecord,
|
|
41
42
|
} from "./unit-runtime.js";
|
|
42
|
-
import { resolveAutoSupervisorConfig, resolveModelWithFallbacksForUnit, loadEffectiveGSDPreferences, resolveSkillDiscoveryMode } from "./preferences.js";
|
|
43
|
+
import { resolveAutoSupervisorConfig, resolveModelWithFallbacksForUnit, loadEffectiveGSDPreferences, resolveSkillDiscoveryMode, resolveDynamicRoutingConfig } from "./preferences.js";
|
|
43
44
|
import { sendDesktopNotification } from "./notifications.js";
|
|
44
45
|
import type { GSDPreferences } from "./preferences.js";
|
|
46
|
+
import { classifyUnitComplexity, tierLabel } from "./complexity-classifier.js";
|
|
47
|
+
import { resolveModelForComplexity } from "./model-router.js";
|
|
48
|
+
import { initRoutingHistory, resetRoutingHistory, recordOutcome } from "./routing-history.js";
|
|
45
49
|
import {
|
|
46
50
|
checkPostUnitHooks,
|
|
47
51
|
getActiveHook,
|
|
@@ -60,8 +64,17 @@ import {
|
|
|
60
64
|
formatValidationIssues,
|
|
61
65
|
} from "./observability-validator.js";
|
|
62
66
|
import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
|
|
63
|
-
import { runGSDDoctor, rebuildState } from "./doctor.js";
|
|
67
|
+
import { runGSDDoctor, rebuildState, summarizeDoctorIssues } from "./doctor.js";
|
|
68
|
+
import {
|
|
69
|
+
preDispatchHealthGate,
|
|
70
|
+
recordHealthSnapshot,
|
|
71
|
+
checkHealEscalation,
|
|
72
|
+
resetProactiveHealing,
|
|
73
|
+
formatHealthSummary,
|
|
74
|
+
getConsecutiveErrorUnits,
|
|
75
|
+
} from "./doctor-proactive.js";
|
|
64
76
|
import { snapshotSkills, clearSkillSnapshot } from "./skill-discovery.js";
|
|
77
|
+
import { captureAvailableSkills, getAndClearSkills, resetSkillTelemetry } from "./skill-telemetry.js";
|
|
65
78
|
import {
|
|
66
79
|
initMetrics, resetMetrics, snapshotUnitMetrics, getLedger,
|
|
67
80
|
getProjectTotals, formatCost, formatTokenCount,
|
|
@@ -96,6 +109,7 @@ import {
|
|
|
96
109
|
} from "./auto-worktree.js";
|
|
97
110
|
import { pruneQueueOrder } from "./queue-order.js";
|
|
98
111
|
import { showNextAction } from "../shared/next-action-ui.js";
|
|
112
|
+
import { debugLog, debugTime, debugCount, debugPeak, enableDebug, isDebugEnabled, writeDebugSummary, getDebugLogPath } from "./debug-logger.js";
|
|
99
113
|
import {
|
|
100
114
|
resolveExpectedArtifactPath,
|
|
101
115
|
verifyExpectedArtifact,
|
|
@@ -129,6 +143,8 @@ import {
|
|
|
129
143
|
deregisterSigtermHandler as _deregisterSigtermHandler,
|
|
130
144
|
detectWorkingTreeActivity,
|
|
131
145
|
} from "./auto-supervisor.js";
|
|
146
|
+
import { isDbAvailable } from "./gsd-db.js";
|
|
147
|
+
import { hasPendingCaptures, loadPendingCaptures, countPendingCaptures } from "./captures.js";
|
|
132
148
|
|
|
133
149
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
134
150
|
|
|
@@ -233,6 +249,18 @@ let autoStartTime: number = 0;
|
|
|
233
249
|
let completedUnits: { type: string; id: string; startedAt: number; finishedAt: number }[] = [];
|
|
234
250
|
let currentUnit: { type: string; id: string; startedAt: number } | null = null;
|
|
235
251
|
|
|
252
|
+
/** Track dynamic routing decision for the current unit (for metrics) */
|
|
253
|
+
let currentUnitRouting: { tier: string; modelDowngraded: boolean } | null = null;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Model captured at auto-mode start. Used to prevent model bleed between
|
|
257
|
+
* concurrent GSD instances sharing the same global settings.json (#650).
|
|
258
|
+
* When preferences don't specify a model for a unit type, this ensures
|
|
259
|
+
* the session's original model is re-applied instead of reading from
|
|
260
|
+
* the shared global settings (which another instance may have overwritten).
|
|
261
|
+
*/
|
|
262
|
+
let autoModeStartModel: { provider: string; id: string } | null = null;
|
|
263
|
+
|
|
236
264
|
/** Track current milestone to detect transitions */
|
|
237
265
|
let currentMilestoneId: string | null = null;
|
|
238
266
|
let lastBudgetAlertLevel: BudgetAlertLevel = 0;
|
|
@@ -254,6 +282,10 @@ let idleWatchdogHandle: ReturnType<typeof setInterval> | null = null;
|
|
|
254
282
|
let dispatchGapHandle: ReturnType<typeof setTimeout> | null = null;
|
|
255
283
|
const DISPATCH_GAP_TIMEOUT_MS = 5_000; // 5 seconds
|
|
256
284
|
|
|
285
|
+
/** Prompt character measurement for token savings analysis (R051). */
|
|
286
|
+
let lastPromptCharCount: number | undefined;
|
|
287
|
+
let lastBaselineCharCount: number | undefined;
|
|
288
|
+
|
|
257
289
|
/** SIGTERM handler registered while auto-mode is active — cleared on stop/pause. */
|
|
258
290
|
let _sigtermHandler: (() => void) | null = null;
|
|
259
291
|
|
|
@@ -301,6 +333,15 @@ export { type AutoDashboardData } from "./auto-dashboard.js";
|
|
|
301
333
|
export function getAutoDashboardData(): AutoDashboardData {
|
|
302
334
|
const ledger = getLedger();
|
|
303
335
|
const totals = ledger ? getProjectTotals(ledger.units) : null;
|
|
336
|
+
// Pending capture count — lazy check, non-fatal
|
|
337
|
+
let pendingCaptureCount = 0;
|
|
338
|
+
try {
|
|
339
|
+
if (basePath) {
|
|
340
|
+
pendingCaptureCount = countPendingCaptures(basePath);
|
|
341
|
+
}
|
|
342
|
+
} catch {
|
|
343
|
+
// Non-fatal — captures module may not be loaded
|
|
344
|
+
}
|
|
304
345
|
return {
|
|
305
346
|
active,
|
|
306
347
|
paused,
|
|
@@ -312,6 +353,7 @@ export function getAutoDashboardData(): AutoDashboardData {
|
|
|
312
353
|
basePath,
|
|
313
354
|
totalCost: totals?.cost ?? 0,
|
|
314
355
|
totalTokens: totals?.tokens.total ?? 0,
|
|
356
|
+
pendingCaptureCount,
|
|
315
357
|
};
|
|
316
358
|
}
|
|
317
359
|
|
|
@@ -457,6 +499,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
457
499
|
clearUnitTimeout();
|
|
458
500
|
if (lockBase()) clearLock(lockBase());
|
|
459
501
|
clearSkillSnapshot();
|
|
502
|
+
resetSkillTelemetry();
|
|
460
503
|
_dispatching = false;
|
|
461
504
|
_skipDepth = 0;
|
|
462
505
|
|
|
@@ -464,12 +507,17 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
464
507
|
deregisterSigtermHandler();
|
|
465
508
|
|
|
466
509
|
// ── Auto-worktree: exit worktree and reset basePath on stop ──
|
|
510
|
+
// Preserve the milestone branch so the next /gsd auto can re-enter
|
|
511
|
+
// where it left off. The branch is only deleted during milestone
|
|
512
|
+
// completion (mergeMilestoneToMain) after the work has been squash-merged.
|
|
467
513
|
if (currentMilestoneId && isInAutoWorktree(basePath)) {
|
|
468
514
|
try {
|
|
469
|
-
|
|
515
|
+
// Auto-commit any dirty state before leaving so work isn't lost
|
|
516
|
+
try { autoCommitCurrentBranch(basePath, "stop", currentMilestoneId); } catch { /* non-fatal */ }
|
|
517
|
+
teardownAutoWorktree(originalBasePath, currentMilestoneId, { preserveBranch: true });
|
|
470
518
|
basePath = originalBasePath;
|
|
471
519
|
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
472
|
-
ctx?.ui.notify("Exited auto-worktree.", "info");
|
|
520
|
+
ctx?.ui.notify("Exited auto-worktree (branch preserved for resume).", "info");
|
|
473
521
|
} catch (err) {
|
|
474
522
|
ctx?.ui.notify(
|
|
475
523
|
`Auto-worktree teardown failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -478,6 +526,14 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
478
526
|
}
|
|
479
527
|
}
|
|
480
528
|
|
|
529
|
+
// ── DB cleanup: close the SQLite connection ──
|
|
530
|
+
if (isDbAvailable()) {
|
|
531
|
+
try {
|
|
532
|
+
const { closeDatabase } = await import("./gsd-db.js");
|
|
533
|
+
closeDatabase();
|
|
534
|
+
} catch { /* non-fatal */ }
|
|
535
|
+
}
|
|
536
|
+
|
|
481
537
|
// Always restore cwd to project root on stop (#608).
|
|
482
538
|
// Even if isInAutoWorktree returned false (e.g., module state was already
|
|
483
539
|
// cleared by mergeMilestoneToMain), the process cwd may still be inside
|
|
@@ -503,7 +559,16 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
503
559
|
try { await rebuildState(basePath); } catch { /* non-fatal */ }
|
|
504
560
|
}
|
|
505
561
|
|
|
562
|
+
// Write debug summary before resetting state
|
|
563
|
+
if (isDebugEnabled()) {
|
|
564
|
+
const logPath = writeDebugSummary();
|
|
565
|
+
if (logPath) {
|
|
566
|
+
ctx?.ui.notify(`Debug log written → ${logPath}`, "info");
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
506
570
|
resetMetrics();
|
|
571
|
+
resetRoutingHistory();
|
|
507
572
|
resetHookState();
|
|
508
573
|
if (basePath) clearPersistedHookState(basePath);
|
|
509
574
|
active = false;
|
|
@@ -515,11 +580,13 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
515
580
|
lastBudgetAlertLevel = 0;
|
|
516
581
|
unitLifetimeDispatches.clear();
|
|
517
582
|
currentUnit = null;
|
|
583
|
+
autoModeStartModel = null;
|
|
518
584
|
currentMilestoneId = null;
|
|
519
585
|
originalBasePath = "";
|
|
520
586
|
completedUnits = [];
|
|
521
587
|
clearSliceProgressCache();
|
|
522
588
|
clearActivityLogState();
|
|
589
|
+
resetProactiveHealing();
|
|
523
590
|
pendingCrashRecovery = null;
|
|
524
591
|
_handlingAgentEnd = false;
|
|
525
592
|
ctx?.ui.setStatus("gsd-auto", undefined);
|
|
@@ -706,27 +773,122 @@ export async function startAuto(
|
|
|
706
773
|
clearLock(base);
|
|
707
774
|
}
|
|
708
775
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
776
|
+
// ── Debug mode: env-var activation ──────────────────────────────────────
|
|
777
|
+
if (!isDebugEnabled() && process.env.GSD_DEBUG === "1") {
|
|
778
|
+
enableDebug(base);
|
|
779
|
+
}
|
|
780
|
+
if (isDebugEnabled()) {
|
|
781
|
+
const { isNativeParserAvailable } = await import("./native-parser-bridge.js");
|
|
782
|
+
debugLog("debug-start", {
|
|
783
|
+
platform: process.platform,
|
|
784
|
+
arch: process.arch,
|
|
785
|
+
node: process.version,
|
|
786
|
+
model: ctx.model?.id ?? "unknown",
|
|
787
|
+
provider: ctx.model?.provider ?? "unknown",
|
|
788
|
+
nativeParser: isNativeParserAvailable(),
|
|
789
|
+
cwd: base,
|
|
790
|
+
});
|
|
791
|
+
ctx.ui.notify(`Debug logging enabled → ${getDebugLogPath()}`, "info");
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
let state = await deriveState(base);
|
|
795
|
+
|
|
796
|
+
// ── Milestone branch recovery (#601) ─────────────────────────────────────
|
|
797
|
+
// When auto-mode was previously stopped, the milestone branch is preserved
|
|
798
|
+
// but the worktree is removed. The project root (integration branch) may
|
|
799
|
+
// not have the roadmap/artifacts — they live on the milestone branch.
|
|
800
|
+
// If state looks like pre-planning but a milestone branch exists with prior
|
|
801
|
+
// work, skip the early-return checks and let worktree setup + dispatch
|
|
802
|
+
// handle it correctly from the branch's state.
|
|
803
|
+
let hasSurvivorBranch = false;
|
|
804
|
+
if (
|
|
805
|
+
state.activeMilestone &&
|
|
806
|
+
(state.phase === "pre-planning" || state.phase === "needs-discussion") &&
|
|
807
|
+
shouldUseWorktreeIsolation() &&
|
|
808
|
+
!detectWorktreeName(base) &&
|
|
809
|
+
!base.includes(`${pathSep}.gsd${pathSep}worktrees${pathSep}`)
|
|
810
|
+
) {
|
|
811
|
+
const milestoneBranch = `milestone/${state.activeMilestone.id}`;
|
|
812
|
+
const { nativeBranchExists } = await import("./native-git-bridge.js");
|
|
813
|
+
hasSurvivorBranch = nativeBranchExists(base, milestoneBranch);
|
|
814
|
+
if (hasSurvivorBranch) {
|
|
815
|
+
ctx.ui.notify(
|
|
816
|
+
`Found prior session branch ${milestoneBranch}. Resuming.`,
|
|
817
|
+
"info",
|
|
818
|
+
);
|
|
819
|
+
}
|
|
716
820
|
}
|
|
717
821
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
822
|
+
if (!hasSurvivorBranch) {
|
|
823
|
+
// No active work at all — start a new milestone via the discuss flow.
|
|
824
|
+
// After discussion completes, checkAutoStartAfterDiscuss() (fired from
|
|
825
|
+
// agent_end) will detect the new CONTEXT.md and restart auto mode.
|
|
826
|
+
// If the LLM didn't follow the discussion protocol (e.g. started editing
|
|
827
|
+
// files directly for a simple task), we re-derive state and either proceed
|
|
828
|
+
// with what was created or notify the user clearly (#609).
|
|
829
|
+
if (!state.activeMilestone || state.phase === "complete") {
|
|
725
830
|
const { showSmartEntry } = await import("./guided-flow.js");
|
|
726
831
|
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
727
|
-
|
|
832
|
+
|
|
833
|
+
// Re-derive state after discussion — the LLM may have created artifacts
|
|
834
|
+
// even if it didn't follow the full protocol.
|
|
835
|
+
invalidateAllCaches();
|
|
836
|
+
const postState = await deriveState(base);
|
|
837
|
+
if (postState.activeMilestone && postState.phase !== "complete" && postState.phase !== "pre-planning") {
|
|
838
|
+
state = postState;
|
|
839
|
+
} else if (postState.activeMilestone && postState.phase === "pre-planning") {
|
|
840
|
+
const contextFile = resolveMilestoneFile(base, postState.activeMilestone.id, "CONTEXT");
|
|
841
|
+
const hasContext = !!(contextFile && await loadFile(contextFile));
|
|
842
|
+
if (hasContext) {
|
|
843
|
+
state = postState;
|
|
844
|
+
} else {
|
|
845
|
+
ctx.ui.notify(
|
|
846
|
+
"Discussion completed but no milestone context was written. Run /gsd to try the discussion again, or /gsd auto after creating the milestone manually.",
|
|
847
|
+
"warning",
|
|
848
|
+
);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
} else {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Active milestone exists but has no roadmap — check if context exists.
|
|
857
|
+
// If context was pre-written (multi-milestone planning), auto-mode can
|
|
858
|
+
// research and plan it. If no context either, need user discussion.
|
|
859
|
+
if (state.phase === "pre-planning") {
|
|
860
|
+
const mid = state.activeMilestone!.id;
|
|
861
|
+
const contextFile = resolveMilestoneFile(base, mid, "CONTEXT");
|
|
862
|
+
const hasContext = !!(contextFile && await loadFile(contextFile));
|
|
863
|
+
if (!hasContext) {
|
|
864
|
+
const { showSmartEntry } = await import("./guided-flow.js");
|
|
865
|
+
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
866
|
+
|
|
867
|
+
// Same re-derive pattern as above
|
|
868
|
+
invalidateAllCaches();
|
|
869
|
+
const postState = await deriveState(base);
|
|
870
|
+
if (postState.activeMilestone && postState.phase !== "pre-planning") {
|
|
871
|
+
state = postState;
|
|
872
|
+
} else {
|
|
873
|
+
ctx.ui.notify(
|
|
874
|
+
"Discussion completed but milestone context is still missing. Run /gsd to try again.",
|
|
875
|
+
"warning",
|
|
876
|
+
);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
// Has context, no roadmap — auto-mode will research + plan it
|
|
728
881
|
}
|
|
729
|
-
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// At this point activeMilestone is guaranteed non-null: either
|
|
885
|
+
// hasSurvivorBranch is true (which requires activeMilestone) or
|
|
886
|
+
// the !activeMilestone early-return above would have fired.
|
|
887
|
+
if (!state.activeMilestone) {
|
|
888
|
+
// Unreachable — satisfies TypeScript's null check
|
|
889
|
+
const { showSmartEntry } = await import("./guided-flow.js");
|
|
890
|
+
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
891
|
+
return;
|
|
730
892
|
}
|
|
731
893
|
|
|
732
894
|
active = true;
|
|
@@ -742,6 +904,7 @@ export async function startAuto(
|
|
|
742
904
|
loadPersistedKeys(base, completedKeySet);
|
|
743
905
|
resetHookState();
|
|
744
906
|
restoreHookState(base);
|
|
907
|
+
resetProactiveHealing();
|
|
745
908
|
autoStartTime = Date.now();
|
|
746
909
|
resourceSyncedAtOnStart = readResourceSyncedAt();
|
|
747
910
|
completedUnits = [];
|
|
@@ -806,9 +969,47 @@ export async function startAuto(
|
|
|
806
969
|
}
|
|
807
970
|
}
|
|
808
971
|
|
|
972
|
+
// ── DB lifecycle: auto-migrate or open existing database ──
|
|
973
|
+
const gsdDbPath = join(basePath, ".gsd", "gsd.db");
|
|
974
|
+
const gsdDirPath = join(basePath, ".gsd");
|
|
975
|
+
if (existsSync(gsdDirPath) && !existsSync(gsdDbPath)) {
|
|
976
|
+
const hasDecisions = existsSync(join(gsdDirPath, "DECISIONS.md"));
|
|
977
|
+
const hasRequirements = existsSync(join(gsdDirPath, "REQUIREMENTS.md"));
|
|
978
|
+
const hasMilestones = existsSync(join(gsdDirPath, "milestones"));
|
|
979
|
+
if (hasDecisions || hasRequirements || hasMilestones) {
|
|
980
|
+
try {
|
|
981
|
+
const { openDatabase: openDb } = await import("./gsd-db.js");
|
|
982
|
+
const { migrateFromMarkdown } = await import("./md-importer.js");
|
|
983
|
+
openDb(gsdDbPath);
|
|
984
|
+
migrateFromMarkdown(basePath);
|
|
985
|
+
} catch (err) {
|
|
986
|
+
process.stderr.write(`gsd-migrate: auto-migration failed: ${(err as Error).message}\n`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (existsSync(gsdDbPath) && !isDbAvailable()) {
|
|
991
|
+
try {
|
|
992
|
+
const { openDatabase: openDb } = await import("./gsd-db.js");
|
|
993
|
+
openDb(gsdDbPath);
|
|
994
|
+
} catch (err) {
|
|
995
|
+
process.stderr.write(`gsd-db: failed to open existing database: ${(err as Error).message}\n`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
809
999
|
// Initialize metrics — loads existing ledger from disk
|
|
810
1000
|
initMetrics(base);
|
|
811
1001
|
|
|
1002
|
+
// Initialize routing history for adaptive learning
|
|
1003
|
+
initRoutingHistory(base);
|
|
1004
|
+
|
|
1005
|
+
// Capture the session's current model at auto-mode start (#650).
|
|
1006
|
+
// This prevents model bleed when multiple GSD instances share the
|
|
1007
|
+
// same global settings.json — each instance remembers its own model.
|
|
1008
|
+
const currentModel = ctx.model;
|
|
1009
|
+
if (currentModel) {
|
|
1010
|
+
autoModeStartModel = { provider: currentModel.provider, id: currentModel.id };
|
|
1011
|
+
}
|
|
1012
|
+
|
|
812
1013
|
// Snapshot installed skills so we can detect new ones after research
|
|
813
1014
|
if (resolveSkillDiscoveryMode() !== "off") {
|
|
814
1015
|
snapshotSkills();
|
|
@@ -824,7 +1025,7 @@ export async function startAuto(
|
|
|
824
1025
|
ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
|
|
825
1026
|
|
|
826
1027
|
// Secrets collection gate — collect pending secrets before first dispatch
|
|
827
|
-
const mid = state.activeMilestone
|
|
1028
|
+
const mid = state.activeMilestone!.id;
|
|
828
1029
|
try {
|
|
829
1030
|
const manifestStatus = await getManifestStatus(base, mid);
|
|
830
1031
|
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
@@ -943,6 +1144,35 @@ export async function handleAgentEnd(
|
|
|
943
1144
|
if (report.fixesApplied.length > 0) {
|
|
944
1145
|
ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
|
|
945
1146
|
}
|
|
1147
|
+
|
|
1148
|
+
// ── Proactive health tracking ──────────────────────────────────────
|
|
1149
|
+
// Record health snapshot for trend analysis and escalation logic.
|
|
1150
|
+
const summary = summarizeDoctorIssues(report.issues);
|
|
1151
|
+
recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
|
|
1152
|
+
|
|
1153
|
+
// Check if we should escalate to LLM-assisted heal
|
|
1154
|
+
if (summary.errors > 0) {
|
|
1155
|
+
const unresolvedErrors = report.issues
|
|
1156
|
+
.filter(i => i.severity === "error" && !i.fixable)
|
|
1157
|
+
.map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
|
|
1158
|
+
const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
|
|
1159
|
+
if (escalation.shouldEscalate) {
|
|
1160
|
+
ctx.ui.notify(
|
|
1161
|
+
`Doctor heal escalation: ${escalation.reason}. Dispatching LLM-assisted heal.`,
|
|
1162
|
+
"warning",
|
|
1163
|
+
);
|
|
1164
|
+
try {
|
|
1165
|
+
const { formatDoctorIssuesForPrompt, formatDoctorReport } = await import("./doctor.js");
|
|
1166
|
+
const { dispatchDoctorHeal } = await import("./commands.js");
|
|
1167
|
+
const actionable = report.issues.filter(i => i.severity === "error");
|
|
1168
|
+
const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
|
|
1169
|
+
const structuredIssues = formatDoctorIssuesForPrompt(actionable);
|
|
1170
|
+
dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
|
|
1171
|
+
} catch {
|
|
1172
|
+
// Non-fatal — escalation dispatch failure
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
946
1176
|
} catch {
|
|
947
1177
|
// Non-fatal — doctor failure should never block dispatch
|
|
948
1178
|
}
|
|
@@ -1003,6 +1233,16 @@ export async function handleAgentEnd(
|
|
|
1003
1233
|
}
|
|
1004
1234
|
}
|
|
1005
1235
|
|
|
1236
|
+
// ── DB dual-write: re-import changed markdown files so next unit's prompts use fresh data ──
|
|
1237
|
+
if (isDbAvailable()) {
|
|
1238
|
+
try {
|
|
1239
|
+
const { migrateFromMarkdown } = await import("./md-importer.js");
|
|
1240
|
+
migrateFromMarkdown(basePath);
|
|
1241
|
+
} catch (err) {
|
|
1242
|
+
process.stderr.write(`gsd-db: re-import failed: ${(err as Error).message}\n`);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1006
1246
|
// ── Post-unit hooks: check if a configured hook should run before normal dispatch ──
|
|
1007
1247
|
if (currentUnit && !stepMode) {
|
|
1008
1248
|
const hookUnit = checkPostUnitHooks(currentUnit.type, currentUnit.id, basePath);
|
|
@@ -1011,7 +1251,7 @@ export async function handleAgentEnd(
|
|
|
1011
1251
|
const hookStartedAt = Date.now();
|
|
1012
1252
|
if (currentUnit) {
|
|
1013
1253
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1014
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1254
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
1015
1255
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1016
1256
|
}
|
|
1017
1257
|
currentUnit = { type: hookUnit.unitType, id: hookUnit.unitId, startedAt: hookStartedAt };
|
|
@@ -1106,6 +1346,108 @@ export async function handleAgentEnd(
|
|
|
1106
1346
|
}
|
|
1107
1347
|
}
|
|
1108
1348
|
|
|
1349
|
+
// ── Triage check: dispatch triage unit if pending captures exist ──────────
|
|
1350
|
+
// Fires after hooks complete, before normal dispatch. Follows the same
|
|
1351
|
+
// early-dispatch-and-return pattern as hooks and fix-merge.
|
|
1352
|
+
// Skip for: step mode (shows wizard instead), triage units (prevent triage-on-triage),
|
|
1353
|
+
// hook units (hooks run before triage conceptually).
|
|
1354
|
+
if (
|
|
1355
|
+
!stepMode &&
|
|
1356
|
+
currentUnit &&
|
|
1357
|
+
!currentUnit.type.startsWith("hook/") &&
|
|
1358
|
+
currentUnit.type !== "triage-captures" &&
|
|
1359
|
+
currentUnit.type !== "quick-task"
|
|
1360
|
+
) {
|
|
1361
|
+
try {
|
|
1362
|
+
if (hasPendingCaptures(basePath)) {
|
|
1363
|
+
const pending = loadPendingCaptures(basePath);
|
|
1364
|
+
if (pending.length > 0) {
|
|
1365
|
+
const state = await deriveState(basePath);
|
|
1366
|
+
const mid = state.activeMilestone?.id;
|
|
1367
|
+
const sid = state.activeSlice?.id;
|
|
1368
|
+
|
|
1369
|
+
if (mid && sid) {
|
|
1370
|
+
// Build triage prompt with current context
|
|
1371
|
+
let currentPlan = "";
|
|
1372
|
+
let roadmapContext = "";
|
|
1373
|
+
const planFile = resolveSliceFile(basePath, mid, sid, "PLAN");
|
|
1374
|
+
if (planFile) currentPlan = (await loadFile(planFile)) ?? "";
|
|
1375
|
+
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
1376
|
+
if (roadmapFile) roadmapContext = (await loadFile(roadmapFile)) ?? "";
|
|
1377
|
+
|
|
1378
|
+
const capturesList = pending.map(c =>
|
|
1379
|
+
`- **${c.id}**: "${c.text}" (captured: ${c.timestamp})`
|
|
1380
|
+
).join("\n");
|
|
1381
|
+
|
|
1382
|
+
const prompt = loadPrompt("triage-captures", {
|
|
1383
|
+
pendingCaptures: capturesList,
|
|
1384
|
+
currentPlan: currentPlan || "(no active slice plan)",
|
|
1385
|
+
roadmapContext: roadmapContext || "(no active roadmap)",
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
ctx.ui.notify(
|
|
1389
|
+
`Triaging ${pending.length} pending capture${pending.length === 1 ? "" : "s"}...`,
|
|
1390
|
+
"info",
|
|
1391
|
+
);
|
|
1392
|
+
|
|
1393
|
+
// Close out previous unit metrics
|
|
1394
|
+
if (currentUnit) {
|
|
1395
|
+
const modelId = ctx.model?.id ?? "unknown";
|
|
1396
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1397
|
+
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// Dispatch triage as a new unit (early-dispatch-and-return)
|
|
1401
|
+
const triageUnitType = "triage-captures";
|
|
1402
|
+
const triageUnitId = `${mid}/${sid}/triage`;
|
|
1403
|
+
const triageStartedAt = Date.now();
|
|
1404
|
+
currentUnit = { type: triageUnitType, id: triageUnitId, startedAt: triageStartedAt };
|
|
1405
|
+
writeUnitRuntimeRecord(basePath, triageUnitType, triageUnitId, triageStartedAt, {
|
|
1406
|
+
phase: "dispatched",
|
|
1407
|
+
wrapupWarningSent: false,
|
|
1408
|
+
timeoutAt: null,
|
|
1409
|
+
lastProgressAt: triageStartedAt,
|
|
1410
|
+
progressCount: 0,
|
|
1411
|
+
lastProgressKind: "dispatch",
|
|
1412
|
+
});
|
|
1413
|
+
updateProgressWidget(ctx, triageUnitType, triageUnitId, state);
|
|
1414
|
+
|
|
1415
|
+
const result = await cmdCtx!.newSession();
|
|
1416
|
+
if (result.cancelled) {
|
|
1417
|
+
await stopAuto(ctx, pi);
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1421
|
+
writeLock(basePath, triageUnitType, triageUnitId, completedUnits.length, sessionFile);
|
|
1422
|
+
|
|
1423
|
+
// Start unit timeout for triage (use same supervisor config as hooks)
|
|
1424
|
+
clearUnitTimeout();
|
|
1425
|
+
const supervisor = resolveAutoSupervisorConfig();
|
|
1426
|
+
const triageTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
|
|
1427
|
+
unitTimeoutHandle = setTimeout(async () => {
|
|
1428
|
+
unitTimeoutHandle = null;
|
|
1429
|
+
if (!active) return;
|
|
1430
|
+
ctx.ui.notify(
|
|
1431
|
+
`Triage unit exceeded timeout. Pausing auto-mode.`,
|
|
1432
|
+
"warning",
|
|
1433
|
+
);
|
|
1434
|
+
await pauseAuto(ctx, pi);
|
|
1435
|
+
}, triageTimeoutMs);
|
|
1436
|
+
|
|
1437
|
+
if (!active) return;
|
|
1438
|
+
pi.sendMessage(
|
|
1439
|
+
{ customType: "gsd-auto", content: prompt, display: verbose },
|
|
1440
|
+
{ triggerTurn: true },
|
|
1441
|
+
);
|
|
1442
|
+
return; // handleAgentEnd will fire again when triage session completes
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
} catch {
|
|
1447
|
+
// Triage check failure is non-fatal — proceed to normal dispatch
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1109
1451
|
// In step mode, pause and show a wizard instead of immediately dispatching
|
|
1110
1452
|
if (stepMode) {
|
|
1111
1453
|
await showStepWizard(ctx, pi);
|
|
@@ -1227,7 +1569,10 @@ function updateProgressWidget(
|
|
|
1227
1569
|
unitId: string,
|
|
1228
1570
|
state: GSDState,
|
|
1229
1571
|
): void {
|
|
1230
|
-
|
|
1572
|
+
const badge = currentUnitRouting?.tier
|
|
1573
|
+
? ({ light: "L", standard: "S", heavy: "H" }[currentUnitRouting.tier] ?? undefined)
|
|
1574
|
+
: undefined;
|
|
1575
|
+
_updateProgressWidget(ctx, unitType, unitId, state, widgetStateAccessors, badge);
|
|
1231
1576
|
}
|
|
1232
1577
|
|
|
1233
1578
|
/** State accessors for the widget — closures over module globals. */
|
|
@@ -1294,8 +1639,34 @@ async function dispatchNextUnit(
|
|
|
1294
1639
|
// Parse cache is also cleared — doctor may have re-populated it with
|
|
1295
1640
|
// stale data between handleAgentEnd and this dispatch call (Path B fix).
|
|
1296
1641
|
invalidateAllCaches();
|
|
1642
|
+
lastPromptCharCount = undefined;
|
|
1643
|
+
lastBaselineCharCount = undefined;
|
|
1297
1644
|
|
|
1645
|
+
// ── Pre-dispatch health gate ──────────────────────────────────────────
|
|
1646
|
+
// Lightweight check for critical issues that would cause the next unit
|
|
1647
|
+
// to fail or corrupt state. Auto-heals what it can, blocks on the rest.
|
|
1648
|
+
try {
|
|
1649
|
+
const healthGate = preDispatchHealthGate(basePath);
|
|
1650
|
+
if (healthGate.fixesApplied.length > 0) {
|
|
1651
|
+
ctx.ui.notify(`Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`, "info");
|
|
1652
|
+
}
|
|
1653
|
+
if (!healthGate.proceed) {
|
|
1654
|
+
ctx.ui.notify(healthGate.reason ?? "Pre-dispatch health check failed.", "error");
|
|
1655
|
+
await pauseAuto(ctx, pi);
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
} catch {
|
|
1659
|
+
// Non-fatal — health gate failure should never block dispatch
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
const stopDeriveTimer = debugTime("derive-state");
|
|
1298
1663
|
let state = await deriveState(basePath);
|
|
1664
|
+
stopDeriveTimer({
|
|
1665
|
+
phase: state.phase,
|
|
1666
|
+
milestone: state.activeMilestone?.id,
|
|
1667
|
+
slice: state.activeSlice?.id,
|
|
1668
|
+
task: state.activeTask?.id,
|
|
1669
|
+
});
|
|
1299
1670
|
let mid = state.activeMilestone?.id;
|
|
1300
1671
|
let midTitle = state.activeMilestone?.title;
|
|
1301
1672
|
|
|
@@ -1306,12 +1677,85 @@ async function dispatchNextUnit(
|
|
|
1306
1677
|
"info",
|
|
1307
1678
|
);
|
|
1308
1679
|
sendDesktopNotification("GSD", `Milestone ${currentMilestoneId} complete!`, "success", "milestone");
|
|
1680
|
+
// Hint: visualizer available after milestone transition
|
|
1681
|
+
const vizPrefs = loadEffectiveGSDPreferences()?.preferences;
|
|
1682
|
+
if (vizPrefs?.auto_visualize) {
|
|
1683
|
+
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
1684
|
+
}
|
|
1309
1685
|
// Reset stuck detection for new milestone
|
|
1310
1686
|
unitDispatchCount.clear();
|
|
1311
1687
|
unitRecoveryCount.clear();
|
|
1312
1688
|
unitLifetimeDispatches.clear();
|
|
1313
|
-
//
|
|
1314
|
-
|
|
1689
|
+
// Clear completed-units.json for the finished milestone
|
|
1690
|
+
try {
|
|
1691
|
+
const file = completedKeysPath(basePath);
|
|
1692
|
+
if (existsSync(file)) writeFileSync(file, JSON.stringify([]), "utf-8");
|
|
1693
|
+
completedKeySet.clear();
|
|
1694
|
+
} catch { /* non-fatal */ }
|
|
1695
|
+
|
|
1696
|
+
// ── Worktree lifecycle on milestone transition (#616) ──────────────
|
|
1697
|
+
// When transitioning from M_old to M_new inside a worktree, we must:
|
|
1698
|
+
// 1. Merge the completed milestone's worktree back to main
|
|
1699
|
+
// 2. Re-derive state from the project root
|
|
1700
|
+
// 3. Create a new worktree for the incoming milestone
|
|
1701
|
+
// Without this, M_new runs inside M_old's worktree on the wrong branch,
|
|
1702
|
+
// and artifact paths resolve against the wrong .gsd/ directory.
|
|
1703
|
+
if (isInAutoWorktree(basePath) && originalBasePath && shouldUseWorktreeIsolation()) {
|
|
1704
|
+
try {
|
|
1705
|
+
const roadmapPath = resolveMilestoneFile(originalBasePath, currentMilestoneId, "ROADMAP");
|
|
1706
|
+
if (roadmapPath) {
|
|
1707
|
+
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1708
|
+
const mergeResult = mergeMilestoneToMain(originalBasePath, currentMilestoneId, roadmapContent);
|
|
1709
|
+
ctx.ui.notify(
|
|
1710
|
+
`Milestone ${currentMilestoneId} merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1711
|
+
"info",
|
|
1712
|
+
);
|
|
1713
|
+
} else {
|
|
1714
|
+
// No roadmap found — teardown worktree without merge
|
|
1715
|
+
teardownAutoWorktree(originalBasePath, currentMilestoneId);
|
|
1716
|
+
ctx.ui.notify(`Exited worktree for ${currentMilestoneId} (no roadmap for merge).`, "info");
|
|
1717
|
+
}
|
|
1718
|
+
} catch (err) {
|
|
1719
|
+
ctx.ui.notify(
|
|
1720
|
+
`Milestone merge failed during transition: ${err instanceof Error ? err.message : String(err)}`,
|
|
1721
|
+
"warning",
|
|
1722
|
+
);
|
|
1723
|
+
// Force cwd back to project root even if merge failed
|
|
1724
|
+
if (originalBasePath) {
|
|
1725
|
+
try { process.chdir(originalBasePath); } catch { /* best-effort */ }
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// Update basePath to project root (mergeMilestoneToMain already chdir'd)
|
|
1730
|
+
basePath = originalBasePath;
|
|
1731
|
+
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
1732
|
+
invalidateAllCaches();
|
|
1733
|
+
|
|
1734
|
+
// Re-derive state from project root before creating new worktree
|
|
1735
|
+
state = await deriveState(basePath);
|
|
1736
|
+
mid = state.activeMilestone?.id;
|
|
1737
|
+
midTitle = state.activeMilestone?.title;
|
|
1738
|
+
|
|
1739
|
+
// Create new worktree for the incoming milestone
|
|
1740
|
+
if (mid) {
|
|
1741
|
+
captureIntegrationBranch(basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
|
|
1742
|
+
try {
|
|
1743
|
+
const wtPath = createAutoWorktree(basePath, mid);
|
|
1744
|
+
basePath = wtPath;
|
|
1745
|
+
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
1746
|
+
ctx.ui.notify(`Created auto-worktree for ${mid} at ${wtPath}`, "info");
|
|
1747
|
+
} catch (err) {
|
|
1748
|
+
ctx.ui.notify(
|
|
1749
|
+
`Auto-worktree creation for ${mid} failed: ${err instanceof Error ? err.message : String(err)}. Continuing in project root.`,
|
|
1750
|
+
"warning",
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
} else {
|
|
1755
|
+
// Not in worktree — just capture integration branch for the new milestone
|
|
1756
|
+
captureIntegrationBranch(originalBasePath || basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1315
1759
|
// Prune completed milestone from queue order file
|
|
1316
1760
|
const pendingIds = state.registry
|
|
1317
1761
|
.filter(m => m.status !== "complete")
|
|
@@ -1327,7 +1771,7 @@ async function dispatchNextUnit(
|
|
|
1327
1771
|
// Save final session before stopping
|
|
1328
1772
|
if (currentUnit) {
|
|
1329
1773
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1330
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1774
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
1331
1775
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1332
1776
|
}
|
|
1333
1777
|
sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
|
|
@@ -1355,7 +1799,7 @@ async function dispatchNextUnit(
|
|
|
1355
1799
|
if (!mid || !midTitle) {
|
|
1356
1800
|
if (currentUnit) {
|
|
1357
1801
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1358
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1802
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
1359
1803
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1360
1804
|
}
|
|
1361
1805
|
await stopAuto(ctx, pi);
|
|
@@ -1370,7 +1814,7 @@ async function dispatchNextUnit(
|
|
|
1370
1814
|
if (state.phase === "complete") {
|
|
1371
1815
|
if (currentUnit) {
|
|
1372
1816
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1373
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1817
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
1374
1818
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1375
1819
|
}
|
|
1376
1820
|
// Clear completed-units.json for the finished milestone so it doesn't grow unbounded.
|
|
@@ -1440,7 +1884,7 @@ async function dispatchNextUnit(
|
|
|
1440
1884
|
if (state.phase === "blocked") {
|
|
1441
1885
|
if (currentUnit) {
|
|
1442
1886
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1443
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1887
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
1444
1888
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1445
1889
|
}
|
|
1446
1890
|
await stopAuto(ctx, pi);
|
|
@@ -1548,7 +1992,7 @@ async function dispatchNextUnit(
|
|
|
1548
1992
|
if (dispatchResult.action === "stop") {
|
|
1549
1993
|
if (currentUnit) {
|
|
1550
1994
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1551
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1995
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
1552
1996
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1553
1997
|
}
|
|
1554
1998
|
await stopAuto(ctx, pi);
|
|
@@ -1650,6 +2094,14 @@ async function dispatchNextUnit(
|
|
|
1650
2094
|
const dispatchKey = `${unitType}/${unitId}`;
|
|
1651
2095
|
const prevCount = unitDispatchCount.get(dispatchKey) ?? 0;
|
|
1652
2096
|
|
|
2097
|
+
debugLog("dispatch-unit", {
|
|
2098
|
+
type: unitType,
|
|
2099
|
+
id: unitId,
|
|
2100
|
+
cycle: prevCount + 1,
|
|
2101
|
+
lifetime: (unitLifetimeDispatches.get(dispatchKey) ?? 0) + 1,
|
|
2102
|
+
});
|
|
2103
|
+
debugCount("dispatches");
|
|
2104
|
+
|
|
1653
2105
|
// Hard lifetime cap — survives counter resets from loop-recovery/self-repair.
|
|
1654
2106
|
// Catches the case where reconciliation "succeeds" (artifacts exist) but
|
|
1655
2107
|
// deriveState keeps returning the same unit, creating an infinite cycle.
|
|
@@ -1658,7 +2110,7 @@ async function dispatchNextUnit(
|
|
|
1658
2110
|
if (lifetimeCount > MAX_LIFETIME_DISPATCHES) {
|
|
1659
2111
|
if (currentUnit) {
|
|
1660
2112
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1661
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
2113
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
1662
2114
|
}
|
|
1663
2115
|
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
1664
2116
|
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
@@ -1672,7 +2124,7 @@ async function dispatchNextUnit(
|
|
|
1672
2124
|
if (prevCount >= MAX_UNIT_DISPATCHES) {
|
|
1673
2125
|
if (currentUnit) {
|
|
1674
2126
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1675
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
2127
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
1676
2128
|
}
|
|
1677
2129
|
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
1678
2130
|
|
|
@@ -1830,9 +2282,19 @@ async function dispatchNextUnit(
|
|
|
1830
2282
|
// The session still holds the previous unit's data (newSession hasn't fired yet).
|
|
1831
2283
|
if (currentUnit) {
|
|
1832
2284
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1833
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
2285
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
1834
2286
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1835
2287
|
|
|
2288
|
+
// Record routing outcome for adaptive learning
|
|
2289
|
+
if (currentUnitRouting) {
|
|
2290
|
+
const isRetry = currentUnit.type === unitType && currentUnit.id === unitId;
|
|
2291
|
+
recordOutcome(
|
|
2292
|
+
currentUnit.type,
|
|
2293
|
+
currentUnitRouting.tier as "light" | "standard" | "heavy",
|
|
2294
|
+
!isRetry, // success = not being retried
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
2297
|
+
|
|
1836
2298
|
// Only mark the previous unit as completed if:
|
|
1837
2299
|
// 1. We're not about to re-dispatch the same unit (retry scenario)
|
|
1838
2300
|
// 2. The expected artifact actually exists on disk
|
|
@@ -1866,6 +2328,7 @@ async function dispatchNextUnit(
|
|
|
1866
2328
|
}
|
|
1867
2329
|
}
|
|
1868
2330
|
currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
2331
|
+
captureAvailableSkills(); // Capture skill telemetry at dispatch time (#599)
|
|
1869
2332
|
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
1870
2333
|
phase: "dispatched",
|
|
1871
2334
|
wrapupWarningSent: false,
|
|
@@ -1930,12 +2393,79 @@ async function dispatchNextUnit(
|
|
|
1930
2393
|
finalPrompt = `${finalPrompt}${repairBlock}`;
|
|
1931
2394
|
}
|
|
1932
2395
|
|
|
2396
|
+
// ── Prompt char measurement (R051) ──
|
|
2397
|
+
lastPromptCharCount = finalPrompt.length;
|
|
2398
|
+
lastBaselineCharCount = undefined;
|
|
2399
|
+
if (isDbAvailable()) {
|
|
2400
|
+
try {
|
|
2401
|
+
const { inlineGsdRootFile } = await import("./auto-prompts.js");
|
|
2402
|
+
const [decisionsContent, requirementsContent, projectContent] = await Promise.all([
|
|
2403
|
+
inlineGsdRootFile(basePath, "decisions.md", "Decisions"),
|
|
2404
|
+
inlineGsdRootFile(basePath, "requirements.md", "Requirements"),
|
|
2405
|
+
inlineGsdRootFile(basePath, "project.md", "Project"),
|
|
2406
|
+
]);
|
|
2407
|
+
lastBaselineCharCount =
|
|
2408
|
+
(decisionsContent?.length ?? 0) +
|
|
2409
|
+
(requirementsContent?.length ?? 0) +
|
|
2410
|
+
(projectContent?.length ?? 0);
|
|
2411
|
+
} catch {
|
|
2412
|
+
// Non-fatal — baseline measurement is best-effort
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
|
|
1933
2416
|
// Switch model if preferences specify one for this unit type
|
|
1934
2417
|
// Try primary model, then fallbacks in order if setting fails
|
|
1935
2418
|
const modelConfig = resolveModelWithFallbacksForUnit(unitType);
|
|
1936
2419
|
if (modelConfig) {
|
|
1937
2420
|
const availableModels = ctx.modelRegistry.getAvailable();
|
|
1938
|
-
|
|
2421
|
+
|
|
2422
|
+
// ─── Dynamic Model Routing ─────────────────────────────────────────
|
|
2423
|
+
// If enabled, classify unit complexity and potentially downgrade to a
|
|
2424
|
+
// cheaper model. The user's configured model is the ceiling.
|
|
2425
|
+
const routingConfig = resolveDynamicRoutingConfig();
|
|
2426
|
+
let effectiveModelConfig = modelConfig;
|
|
2427
|
+
let routingTierLabel = "";
|
|
2428
|
+
currentUnitRouting = null;
|
|
2429
|
+
|
|
2430
|
+
if (routingConfig.enabled) {
|
|
2431
|
+
// Compute budget pressure if budget ceiling is set
|
|
2432
|
+
let budgetPct: number | undefined;
|
|
2433
|
+
if (routingConfig.budget_pressure !== false) {
|
|
2434
|
+
const budgetCeiling = prefs?.budget_ceiling;
|
|
2435
|
+
if (budgetCeiling !== undefined && budgetCeiling > 0) {
|
|
2436
|
+
const currentLedger = getLedger();
|
|
2437
|
+
const totalCost = currentLedger ? getProjectTotals(currentLedger.units).cost : 0;
|
|
2438
|
+
budgetPct = totalCost / budgetCeiling;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// Classify complexity (hook routing controlled by config.hooks)
|
|
2443
|
+
const isHook = unitType.startsWith("hook/");
|
|
2444
|
+
const shouldClassify = !isHook || routingConfig.hooks !== false;
|
|
2445
|
+
|
|
2446
|
+
if (shouldClassify) {
|
|
2447
|
+
const classification = classifyUnitComplexity(unitType, unitId, basePath, budgetPct);
|
|
2448
|
+
const availableModelIds = availableModels.map(m => m.id);
|
|
2449
|
+
const routing = resolveModelForComplexity(classification, modelConfig, routingConfig, availableModelIds);
|
|
2450
|
+
|
|
2451
|
+
if (routing.wasDowngraded) {
|
|
2452
|
+
effectiveModelConfig = {
|
|
2453
|
+
primary: routing.modelId,
|
|
2454
|
+
fallbacks: routing.fallbacks,
|
|
2455
|
+
};
|
|
2456
|
+
if (verbose) {
|
|
2457
|
+
ctx.ui.notify(
|
|
2458
|
+
`Dynamic routing [${tierLabel(classification.tier)}]: ${routing.modelId} (${classification.reason})`,
|
|
2459
|
+
"info",
|
|
2460
|
+
);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
routingTierLabel = ` [${tierLabel(classification.tier)}]`;
|
|
2464
|
+
currentUnitRouting = { tier: classification.tier, modelDowngraded: routing.wasDowngraded };
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
const modelsToTry = [effectiveModelConfig.primary, ...effectiveModelConfig.fallbacks];
|
|
1939
2469
|
let modelSet = false;
|
|
1940
2470
|
|
|
1941
2471
|
for (const modelId of modelsToTry) {
|
|
@@ -2000,11 +2530,11 @@ async function dispatchNextUnit(
|
|
|
2000
2530
|
|
|
2001
2531
|
const ok = await pi.setModel(model, { persist: false });
|
|
2002
2532
|
if (ok) {
|
|
2003
|
-
const fallbackNote = modelId ===
|
|
2533
|
+
const fallbackNote = modelId === effectiveModelConfig.primary
|
|
2004
2534
|
? ""
|
|
2005
|
-
: ` (fallback from ${
|
|
2535
|
+
: ` (fallback from ${effectiveModelConfig.primary})`;
|
|
2006
2536
|
const phase = unitPhaseLabel(unitType);
|
|
2007
|
-
ctx.ui.notify(`Model [${phase}]: ${model.provider}/${model.id}${fallbackNote}`, "info");
|
|
2537
|
+
ctx.ui.notify(`Model [${phase}]${routingTierLabel}: ${model.provider}/${model.id}${fallbackNote}`, "info");
|
|
2008
2538
|
modelSet = true;
|
|
2009
2539
|
break;
|
|
2010
2540
|
} else {
|
|
@@ -2018,6 +2548,22 @@ async function dispatchNextUnit(
|
|
|
2018
2548
|
}
|
|
2019
2549
|
|
|
2020
2550
|
// modelSet=false is already handled by the "all fallbacks exhausted" warning above
|
|
2551
|
+
} else if (autoModeStartModel) {
|
|
2552
|
+
// No model preference for this unit type — re-apply the model captured
|
|
2553
|
+
// at auto-mode start to prevent bleed from the shared global settings.json
|
|
2554
|
+
// when multiple GSD instances run concurrently (#650).
|
|
2555
|
+
const availableModels = ctx.modelRegistry.getAvailable();
|
|
2556
|
+
const startModel = availableModels.find(
|
|
2557
|
+
m => m.provider === autoModeStartModel!.provider && m.id === autoModeStartModel!.id,
|
|
2558
|
+
);
|
|
2559
|
+
if (startModel) {
|
|
2560
|
+
const ok = await pi.setModel(startModel, { persist: false });
|
|
2561
|
+
if (!ok) {
|
|
2562
|
+
// Fallback: try matching just by ID across providers
|
|
2563
|
+
const byId = availableModels.find(m => m.id === autoModeStartModel!.id);
|
|
2564
|
+
if (byId) await pi.setModel(byId, { persist: false });
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2021
2567
|
}
|
|
2022
2568
|
|
|
2023
2569
|
// Start progress-aware supervision: a soft warning, an idle watchdog, and
|
|
@@ -2083,7 +2629,7 @@ async function dispatchNextUnit(
|
|
|
2083
2629
|
|
|
2084
2630
|
if (currentUnit) {
|
|
2085
2631
|
const modelId = ctx.model?.id ?? "unknown";
|
|
2086
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
2632
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2087
2633
|
}
|
|
2088
2634
|
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
2089
2635
|
|
|
@@ -2109,7 +2655,7 @@ async function dispatchNextUnit(
|
|
|
2109
2655
|
timeoutAt: Date.now(),
|
|
2110
2656
|
});
|
|
2111
2657
|
const modelId = ctx.model?.id ?? "unknown";
|
|
2112
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
2658
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2113
2659
|
}
|
|
2114
2660
|
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
2115
2661
|
|
|
@@ -2491,3 +3037,108 @@ export {
|
|
|
2491
3037
|
skipExecuteTask,
|
|
2492
3038
|
buildLoopRemediationSteps,
|
|
2493
3039
|
} from "./auto-recovery.js";
|
|
3040
|
+
|
|
3041
|
+
/**
|
|
3042
|
+
* Dispatch a hook unit directly, bypassing normal pre-dispatch hooks.
|
|
3043
|
+
* Used for manual hook triggers via /gsd run-hook.
|
|
3044
|
+
*/
|
|
3045
|
+
export async function dispatchHookUnit(
|
|
3046
|
+
ctx: ExtensionContext,
|
|
3047
|
+
pi: ExtensionAPI,
|
|
3048
|
+
hookName: string,
|
|
3049
|
+
triggerUnitType: string,
|
|
3050
|
+
triggerUnitId: string,
|
|
3051
|
+
hookPrompt: string,
|
|
3052
|
+
hookModel: string | undefined,
|
|
3053
|
+
targetBasePath: string,
|
|
3054
|
+
): Promise<boolean> {
|
|
3055
|
+
// Ensure auto-mode is active
|
|
3056
|
+
if (!active) {
|
|
3057
|
+
// Initialize auto-mode state minimally
|
|
3058
|
+
active = true;
|
|
3059
|
+
stepMode = true;
|
|
3060
|
+
cmdCtx = ctx as ExtensionCommandContext;
|
|
3061
|
+
basePath = targetBasePath;
|
|
3062
|
+
autoStartTime = Date.now();
|
|
3063
|
+
currentUnit = null;
|
|
3064
|
+
completedUnits = [];
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
const hookUnitType = `hook/${hookName}`;
|
|
3068
|
+
const hookStartedAt = Date.now();
|
|
3069
|
+
|
|
3070
|
+
// Set up the trigger unit as the "current" unit so post-unit hooks can reference it
|
|
3071
|
+
currentUnit = { type: triggerUnitType, id: triggerUnitId, startedAt: hookStartedAt };
|
|
3072
|
+
|
|
3073
|
+
// Create a new session for the hook
|
|
3074
|
+
const result = await cmdCtx!.newSession();
|
|
3075
|
+
if (result.cancelled) {
|
|
3076
|
+
await stopAuto(ctx, pi);
|
|
3077
|
+
return false;
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
// Update current unit to the hook unit
|
|
3081
|
+
currentUnit = { type: hookUnitType, id: triggerUnitId, startedAt: hookStartedAt };
|
|
3082
|
+
|
|
3083
|
+
// Write runtime record
|
|
3084
|
+
writeUnitRuntimeRecord(basePath, hookUnitType, triggerUnitId, hookStartedAt, {
|
|
3085
|
+
phase: "dispatched",
|
|
3086
|
+
wrapupWarningSent: false,
|
|
3087
|
+
timeoutAt: null,
|
|
3088
|
+
lastProgressAt: hookStartedAt,
|
|
3089
|
+
progressCount: 0,
|
|
3090
|
+
lastProgressKind: "dispatch",
|
|
3091
|
+
});
|
|
3092
|
+
|
|
3093
|
+
// Switch model if specified
|
|
3094
|
+
if (hookModel) {
|
|
3095
|
+
const availableModels = ctx.modelRegistry.getAvailable();
|
|
3096
|
+
const match = availableModels.find(m =>
|
|
3097
|
+
m.id === hookModel || `${m.provider}/${m.id}` === hookModel,
|
|
3098
|
+
);
|
|
3099
|
+
if (match) {
|
|
3100
|
+
try {
|
|
3101
|
+
await pi.setModel(match);
|
|
3102
|
+
} catch { /* non-fatal — use current model */ }
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
// Write lock
|
|
3107
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
3108
|
+
writeLock(lockBase(), hookUnitType, triggerUnitId, completedUnits.length, sessionFile);
|
|
3109
|
+
|
|
3110
|
+
// Set up timeout
|
|
3111
|
+
clearUnitTimeout();
|
|
3112
|
+
const supervisor = resolveAutoSupervisorConfig();
|
|
3113
|
+
const hookHardTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
|
|
3114
|
+
unitTimeoutHandle = setTimeout(async () => {
|
|
3115
|
+
unitTimeoutHandle = null;
|
|
3116
|
+
if (!active) return;
|
|
3117
|
+
if (currentUnit) {
|
|
3118
|
+
writeUnitRuntimeRecord(basePath, hookUnitType, triggerUnitId, hookStartedAt, {
|
|
3119
|
+
phase: "timeout",
|
|
3120
|
+
timeoutAt: Date.now(),
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
ctx.ui.notify(
|
|
3124
|
+
`Hook ${hookName} exceeded ${supervisor.hard_timeout_minutes ?? 30}min timeout. Pausing auto-mode.`,
|
|
3125
|
+
"warning",
|
|
3126
|
+
);
|
|
3127
|
+
resetHookState();
|
|
3128
|
+
await pauseAuto(ctx, pi);
|
|
3129
|
+
}, hookHardTimeoutMs);
|
|
3130
|
+
|
|
3131
|
+
// Update status
|
|
3132
|
+
ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
|
|
3133
|
+
ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
|
|
3134
|
+
|
|
3135
|
+
// Send the hook prompt
|
|
3136
|
+
console.log(`[dispatchHookUnit] Sending prompt of length ${hookPrompt.length}`);
|
|
3137
|
+
console.log(`[dispatchHookUnit] Prompt preview: ${hookPrompt.substring(0, 200)}...`);
|
|
3138
|
+
pi.sendMessage(
|
|
3139
|
+
{ customType: "gsd-auto", content: hookPrompt, display: true },
|
|
3140
|
+
{ triggerTurn: true },
|
|
3141
|
+
);
|
|
3142
|
+
|
|
3143
|
+
return true;
|
|
3144
|
+
}
|