gsd-pi 2.38.0-dev.eeb3520 → 2.39.0-dev.64cd3ed
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -11
- package/dist/app-paths.js +1 -1
- package/dist/cli.js +9 -0
- package/dist/extension-discovery.d.ts +5 -3
- package/dist/extension-discovery.js +14 -9
- package/dist/extension-registry.js +2 -2
- package/dist/remote-questions-config.js +2 -2
- package/dist/resource-loader.js +34 -1
- package/dist/resources/extensions/async-jobs/index.js +10 -0
- package/dist/resources/extensions/browser-tools/index.js +3 -1
- package/dist/resources/extensions/browser-tools/package.json +3 -1
- package/dist/resources/extensions/browser-tools/tools/verify.js +97 -0
- package/dist/resources/extensions/cmux/index.js +55 -1
- package/dist/resources/extensions/context7/package.json +1 -1
- package/dist/resources/extensions/env-utils.js +29 -0
- package/dist/resources/extensions/get-secrets-from-user.js +5 -24
- package/dist/resources/extensions/github-sync/cli.js +284 -0
- package/dist/resources/extensions/github-sync/index.js +73 -0
- package/dist/resources/extensions/github-sync/mapping.js +67 -0
- package/dist/resources/extensions/github-sync/sync.js +424 -0
- package/dist/resources/extensions/github-sync/templates.js +118 -0
- package/dist/resources/extensions/github-sync/types.js +7 -0
- package/dist/resources/extensions/google-search/package.json +3 -1
- package/dist/resources/extensions/gsd/auto/session.js +6 -23
- package/dist/resources/extensions/gsd/auto-dispatch.js +8 -9
- package/dist/resources/extensions/gsd/auto-loop.js +650 -588
- package/dist/resources/extensions/gsd/auto-post-unit.js +99 -70
- package/dist/resources/extensions/gsd/auto-prompts.js +202 -48
- package/dist/resources/extensions/gsd/auto-start.js +13 -2
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
- package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
- package/dist/resources/extensions/gsd/auto.js +143 -96
- package/dist/resources/extensions/gsd/captures.js +9 -1
- package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
- package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +24 -3
- package/dist/resources/extensions/gsd/context-budget.js +2 -10
- package/dist/resources/extensions/gsd/detection.js +1 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
- package/dist/resources/extensions/gsd/doctor-format.js +15 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +30 -11
- package/dist/resources/extensions/gsd/doctor.js +204 -12
- package/dist/resources/extensions/gsd/exit-command.js +2 -1
- package/dist/resources/extensions/gsd/export.js +1 -1
- package/dist/resources/extensions/gsd/files.js +48 -9
- package/dist/resources/extensions/gsd/forensics.js +1 -1
- package/dist/resources/extensions/gsd/git-service.js +30 -12
- package/dist/resources/extensions/gsd/gitignore.js +16 -3
- package/dist/resources/extensions/gsd/guided-flow.js +149 -38
- package/dist/resources/extensions/gsd/health-widget-core.js +32 -70
- package/dist/resources/extensions/gsd/health-widget.js +3 -86
- package/dist/resources/extensions/gsd/index.js +24 -20
- package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
- package/dist/resources/extensions/gsd/migrate-external.js +18 -1
- package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
- package/dist/resources/extensions/gsd/package.json +1 -1
- package/dist/resources/extensions/gsd/paths.js +3 -0
- package/dist/resources/extensions/gsd/preferences-models.js +0 -12
- package/dist/resources/extensions/gsd/preferences-types.js +1 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +59 -11
- package/dist/resources/extensions/gsd/preferences.js +22 -11
- package/dist/resources/extensions/gsd/prompt-loader.js +6 -2
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -3
- package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/run-uat.md +28 -11
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/dist/resources/extensions/gsd/repo-identity.js +21 -4
- package/dist/resources/extensions/gsd/resource-version.js +2 -1
- package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
- package/dist/resources/extensions/gsd/state.js +42 -23
- package/dist/resources/extensions/gsd/templates/runtime.md +21 -0
- package/dist/resources/extensions/gsd/templates/task-plan.md +3 -0
- package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
- package/dist/resources/extensions/gsd/worktree.js +35 -16
- package/dist/resources/extensions/mcp-client/index.js +14 -1
- package/dist/resources/extensions/remote-questions/status.js +4 -1
- package/dist/resources/extensions/remote-questions/store.js +4 -1
- package/dist/resources/extensions/search-the-web/provider.js +2 -1
- package/dist/resources/extensions/shared/frontmatter.js +1 -1
- package/dist/resources/extensions/subagent/index.js +12 -3
- package/dist/resources/extensions/subagent/isolation.js +2 -1
- package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
- package/dist/resources/extensions/universal-config/package.json +1 -1
- package/dist/welcome-screen.d.ts +12 -0
- package/dist/welcome-screen.js +53 -0
- package/package.json +1 -1
- package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
- package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
- package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.js +6 -1
- package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
- package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
- package/packages/pi-coding-agent/src/core/skills.ts +9 -1
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/async-jobs/index.ts +11 -0
- package/src/resources/extensions/browser-tools/index.ts +3 -0
- package/src/resources/extensions/browser-tools/tools/verify.ts +117 -0
- package/src/resources/extensions/cmux/index.ts +57 -1
- package/src/resources/extensions/env-utils.ts +31 -0
- package/src/resources/extensions/get-secrets-from-user.ts +5 -24
- package/src/resources/extensions/github-sync/cli.ts +364 -0
- package/src/resources/extensions/github-sync/index.ts +93 -0
- package/src/resources/extensions/github-sync/mapping.ts +81 -0
- package/src/resources/extensions/github-sync/sync.ts +556 -0
- package/src/resources/extensions/github-sync/templates.ts +183 -0
- package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
- package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
- package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
- package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
- package/src/resources/extensions/github-sync/types.ts +47 -0
- package/src/resources/extensions/gsd/auto/session.ts +7 -25
- package/src/resources/extensions/gsd/auto-dispatch.ts +7 -9
- package/src/resources/extensions/gsd/auto-loop.ts +553 -546
- package/src/resources/extensions/gsd/auto-post-unit.ts +80 -44
- package/src/resources/extensions/gsd/auto-prompts.ts +247 -50
- package/src/resources/extensions/gsd/auto-start.ts +18 -2
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
- package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
- package/src/resources/extensions/gsd/auto.ts +139 -101
- package/src/resources/extensions/gsd/captures.ts +10 -1
- package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
- package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +26 -4
- package/src/resources/extensions/gsd/context-budget.ts +2 -12
- package/src/resources/extensions/gsd/detection.ts +2 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
- package/src/resources/extensions/gsd/doctor-format.ts +20 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +30 -9
- package/src/resources/extensions/gsd/doctor-types.ts +16 -1
- package/src/resources/extensions/gsd/doctor.ts +199 -14
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/export.ts +1 -1
- package/src/resources/extensions/gsd/files.ts +51 -11
- package/src/resources/extensions/gsd/forensics.ts +1 -1
- package/src/resources/extensions/gsd/git-service.ts +44 -10
- package/src/resources/extensions/gsd/gitignore.ts +17 -3
- package/src/resources/extensions/gsd/guided-flow.ts +177 -44
- package/src/resources/extensions/gsd/health-widget-core.ts +28 -80
- package/src/resources/extensions/gsd/health-widget.ts +3 -89
- package/src/resources/extensions/gsd/index.ts +24 -17
- package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
- package/src/resources/extensions/gsd/migrate-external.ts +18 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
- package/src/resources/extensions/gsd/paths.ts +4 -0
- package/src/resources/extensions/gsd/preferences-models.ts +0 -12
- package/src/resources/extensions/gsd/preferences-types.ts +4 -4
- package/src/resources/extensions/gsd/preferences-validation.ts +51 -11
- package/src/resources/extensions/gsd/preferences.ts +25 -11
- package/src/resources/extensions/gsd/prompt-loader.ts +7 -2
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/src/resources/extensions/gsd/prompts/execute-task.md +5 -3
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +4 -8
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/run-uat.md +28 -11
- package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/src/resources/extensions/gsd/repo-identity.ts +23 -4
- package/src/resources/extensions/gsd/resource-version.ts +3 -1
- package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
- package/src/resources/extensions/gsd/state.ts +39 -21
- package/src/resources/extensions/gsd/templates/runtime.md +21 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +3 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
- package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +16 -54
- package/src/resources/extensions/gsd/tests/parsers.test.ts +131 -14
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +16 -4
- package/src/resources/extensions/gsd/tests/skill-activation.test.ts +140 -0
- package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
- package/src/resources/extensions/gsd/types.ts +18 -1
- package/src/resources/extensions/gsd/verification-evidence.ts +16 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
- package/src/resources/extensions/gsd/worktree.ts +35 -15
- package/src/resources/extensions/mcp-client/index.ts +17 -1
- package/src/resources/extensions/remote-questions/status.ts +5 -1
- package/src/resources/extensions/remote-questions/store.ts +5 -1
- package/src/resources/extensions/search-the-web/provider.ts +2 -1
- package/src/resources/extensions/shared/frontmatter.ts +1 -1
- package/src/resources/extensions/subagent/index.ts +12 -3
- package/src/resources/extensions/subagent/isolation.ts +3 -1
- package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
- package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
- package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
- package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
- package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
- package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
- package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
- package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
- package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
- package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
- package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
- package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
|
@@ -5,20 +5,20 @@
|
|
|
5
5
|
* pattern with a while loop. The agent_end event resolves a promise instead
|
|
6
6
|
* of recursing.
|
|
7
7
|
*
|
|
8
|
-
* MAINTENANCE RULE:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* MAINTENANCE RULE: Module-level mutable state is limited to `_currentResolve`
|
|
9
|
+
* (per-unit one-shot resolver) and `_sessionSwitchInFlight` (guard for
|
|
10
|
+
* session rotation). No queue — stale agent_end events are dropped.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type
|
|
13
|
+
import { importExtensionModule, type ExtensionAPI, type ExtensionContext } from "@gsd/pi-coding-agent";
|
|
14
14
|
|
|
15
|
-
import type { AutoSession } from "./auto/session.js";
|
|
15
|
+
import type { AutoSession, SidecarItem } from "./auto/session.js";
|
|
16
16
|
import { NEW_SESSION_TIMEOUT_MS } from "./auto/session.js";
|
|
17
17
|
import type { GSDPreferences } from "./preferences.js";
|
|
18
18
|
import type { SessionLockStatus } from "./session-lock.js";
|
|
19
19
|
import type { GSDState } from "./types.js";
|
|
20
20
|
import type { CloseoutOptions } from "./auto-unit-closeout.js";
|
|
21
|
-
import type { PostUnitContext } from "./auto-post-unit.js";
|
|
21
|
+
import type { PostUnitContext, PreVerificationOpts } from "./auto-post-unit.js";
|
|
22
22
|
import type {
|
|
23
23
|
VerificationContext,
|
|
24
24
|
VerificationResult,
|
|
@@ -26,6 +26,9 @@ import type {
|
|
|
26
26
|
import type { DispatchAction } from "./auto-dispatch.js";
|
|
27
27
|
import type { WorktreeResolver } from "./worktree-resolver.js";
|
|
28
28
|
import { debugLog } from "./debug-logger.js";
|
|
29
|
+
import { gsdRoot } from "./paths.js";
|
|
30
|
+
import { atomicWriteSync } from "./atomic-write.js";
|
|
31
|
+
import { join } from "node:path";
|
|
29
32
|
import type { CmuxLogLevel } from "../cmux/index.js";
|
|
30
33
|
|
|
31
34
|
/**
|
|
@@ -35,6 +38,23 @@ import type { CmuxLogLevel } from "../cmux/index.js";
|
|
|
35
38
|
* generous headroom including retries and sidecar work.
|
|
36
39
|
*/
|
|
37
40
|
const MAX_LOOP_ITERATIONS = 500;
|
|
41
|
+
/** Maximum characters of failure/crash context included in recovery prompts. */
|
|
42
|
+
const MAX_RECOVERY_CHARS = 50_000;
|
|
43
|
+
|
|
44
|
+
/** Data-driven budget threshold notifications (descending). The 100% entry
|
|
45
|
+
* triggers special enforcement logic (halt/pause/warn); sub-100 entries fire
|
|
46
|
+
* a simple notification. */
|
|
47
|
+
const BUDGET_THRESHOLDS: Array<{
|
|
48
|
+
pct: number;
|
|
49
|
+
label: string;
|
|
50
|
+
notifyLevel: "info" | "warning" | "error";
|
|
51
|
+
cmuxLevel: "progress" | "warning" | "error";
|
|
52
|
+
}> = [
|
|
53
|
+
{ pct: 100, label: "Budget ceiling reached", notifyLevel: "error", cmuxLevel: "error" },
|
|
54
|
+
{ pct: 90, label: "Budget 90%", notifyLevel: "warning", cmuxLevel: "warning" },
|
|
55
|
+
{ pct: 80, label: "Approaching budget ceiling — 80%", notifyLevel: "warning", cmuxLevel: "warning" },
|
|
56
|
+
{ pct: 75, label: "Budget 75%", notifyLevel: "info", cmuxLevel: "progress" },
|
|
57
|
+
];
|
|
38
58
|
|
|
39
59
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
40
60
|
|
|
@@ -54,17 +74,15 @@ export interface UnitResult {
|
|
|
54
74
|
event?: AgentEndEvent;
|
|
55
75
|
}
|
|
56
76
|
|
|
57
|
-
// ───
|
|
77
|
+
// ─── Per-unit one-shot promise state ────────────────────────────────────────
|
|
58
78
|
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
79
|
+
// A single module-level resolve function scoped to the current unit execution.
|
|
80
|
+
// No queue — if an agent_end arrives with no pending resolver, it is dropped
|
|
81
|
+
// (logged as warning). This is simpler and safer than the previous session-
|
|
82
|
+
// scoped pendingResolve + pendingAgentEndQueue pattern.
|
|
61
83
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
* on entry so that the agent_end handler in index.ts can resolve the correct
|
|
65
|
-
* session's promise without needing a direct reference to `s`.
|
|
66
|
-
*/
|
|
67
|
-
let _activeSession: AutoSession | null = null;
|
|
84
|
+
let _currentResolve: ((result: UnitResult) => void) | null = null;
|
|
85
|
+
let _sessionSwitchInFlight = false;
|
|
68
86
|
|
|
69
87
|
// ─── resolveAgentEnd ─────────────────────────────────────────────────────────
|
|
70
88
|
|
|
@@ -73,60 +91,105 @@ let _activeSession: AutoSession | null = null;
|
|
|
73
91
|
* in-flight unit promise. One-shot: the resolver is nulled before calling
|
|
74
92
|
* to prevent double-resolution from model fallback retries.
|
|
75
93
|
*
|
|
76
|
-
* If no
|
|
77
|
-
* the event is
|
|
94
|
+
* If no resolver exists (event arrived between loop iterations or during
|
|
95
|
+
* session switch), the event is dropped with a debug warning.
|
|
78
96
|
*/
|
|
79
97
|
export function resolveAgentEnd(event: AgentEndEvent): void {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
debugLog("resolveAgentEnd", {
|
|
83
|
-
status: "no-active-session",
|
|
84
|
-
warning: "agent_end with no active loop session",
|
|
85
|
-
});
|
|
98
|
+
if (_sessionSwitchInFlight) {
|
|
99
|
+
debugLog("resolveAgentEnd", { status: "ignored-during-switch" });
|
|
86
100
|
return;
|
|
87
101
|
}
|
|
88
|
-
|
|
89
|
-
if (s.pendingResolve) {
|
|
102
|
+
if (_currentResolve) {
|
|
90
103
|
debugLog("resolveAgentEnd", { status: "resolving", hasEvent: true });
|
|
91
|
-
const r =
|
|
92
|
-
|
|
104
|
+
const r = _currentResolve;
|
|
105
|
+
_currentResolve = null;
|
|
93
106
|
r({ status: "completed", event });
|
|
94
107
|
} else {
|
|
95
|
-
// Queue the event so the next runUnit picks it up immediately
|
|
96
108
|
debugLog("resolveAgentEnd", {
|
|
97
|
-
status: "
|
|
98
|
-
|
|
99
|
-
warning:
|
|
100
|
-
"agent_end arrived between loop iterations — queued for next runUnit",
|
|
109
|
+
status: "no-pending-resolve",
|
|
110
|
+
warning: "agent_end with no pending unit",
|
|
101
111
|
});
|
|
102
|
-
s.pendingAgentEndQueue.push(event);
|
|
103
112
|
}
|
|
104
113
|
}
|
|
105
114
|
|
|
106
115
|
export function isSessionSwitchInFlight(): boolean {
|
|
107
|
-
return
|
|
116
|
+
return _sessionSwitchInFlight;
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
// ─── resetPendingResolve (test helper) ───────────────────────────────────────
|
|
111
120
|
|
|
112
121
|
/**
|
|
113
|
-
* Reset
|
|
114
|
-
* should never call this.
|
|
122
|
+
* Reset module-level promise state. Only exported for test cleanup —
|
|
123
|
+
* production code should never call this.
|
|
115
124
|
*/
|
|
116
125
|
export function _resetPendingResolve(): void {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
_activeSession.pendingAgentEndQueue = [];
|
|
120
|
-
}
|
|
121
|
-
_activeSession = null;
|
|
126
|
+
_currentResolve = null;
|
|
127
|
+
_sessionSwitchInFlight = false;
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
131
|
+
* No-op for backward compatibility with tests that previously set the
|
|
132
|
+
* active session. The module no longer holds a session reference.
|
|
127
133
|
*/
|
|
128
|
-
export function _setActiveSession(
|
|
129
|
-
|
|
134
|
+
export function _setActiveSession(_session: AutoSession | null): void {
|
|
135
|
+
// No-op — kept for test backward compatibility
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── detectStuck ─────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
type WindowEntry = { key: string; error?: string };
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Analyze a sliding window of recent unit dispatches for stuck patterns.
|
|
144
|
+
* Returns a signal with reason if stuck, null otherwise.
|
|
145
|
+
*
|
|
146
|
+
* Rule 1: Same error string twice in a row → stuck immediately.
|
|
147
|
+
* Rule 2: Same unit key 3+ consecutive times → stuck (preserves prior behavior).
|
|
148
|
+
* Rule 3: Oscillation A→B→A→B in last 4 entries → stuck.
|
|
149
|
+
*/
|
|
150
|
+
export function detectStuck(
|
|
151
|
+
window: readonly WindowEntry[],
|
|
152
|
+
): { stuck: true; reason: string } | null {
|
|
153
|
+
if (window.length < 2) return null;
|
|
154
|
+
|
|
155
|
+
const last = window[window.length - 1];
|
|
156
|
+
const prev = window[window.length - 2];
|
|
157
|
+
|
|
158
|
+
// Rule 1: Same error repeated consecutively
|
|
159
|
+
if (last.error && prev.error && last.error === prev.error) {
|
|
160
|
+
return {
|
|
161
|
+
stuck: true,
|
|
162
|
+
reason: `Same error repeated: ${last.error.slice(0, 200)}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Rule 2: Same unit 3+ consecutive times
|
|
167
|
+
if (window.length >= 3) {
|
|
168
|
+
const lastThree = window.slice(-3);
|
|
169
|
+
if (lastThree.every((u) => u.key === last.key)) {
|
|
170
|
+
return {
|
|
171
|
+
stuck: true,
|
|
172
|
+
reason: `${last.key} derived 3 consecutive times without progress`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Rule 3: Oscillation (A→B→A→B in last 4)
|
|
178
|
+
if (window.length >= 4) {
|
|
179
|
+
const w = window.slice(-4);
|
|
180
|
+
if (
|
|
181
|
+
w[0].key === w[2].key &&
|
|
182
|
+
w[1].key === w[3].key &&
|
|
183
|
+
w[0].key !== w[1].key
|
|
184
|
+
) {
|
|
185
|
+
return {
|
|
186
|
+
stuck: true,
|
|
187
|
+
reason: `Oscillation detected: ${w[0].key} ↔ ${w[1].key}`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return null;
|
|
130
193
|
}
|
|
131
194
|
|
|
132
195
|
// ─── runUnit ─────────────────────────────────────────────────────────────────
|
|
@@ -146,45 +209,18 @@ export async function runUnit(
|
|
|
146
209
|
unitType: string,
|
|
147
210
|
unitId: string,
|
|
148
211
|
prompt: string,
|
|
149
|
-
_prefs: GSDPreferences | undefined,
|
|
150
212
|
): Promise<UnitResult> {
|
|
151
213
|
debugLog("runUnit", { phase: "start", unitType, unitId });
|
|
152
214
|
|
|
153
|
-
// ── Drain queued events from error-recovery retries ──
|
|
154
|
-
// If an agent_end arrived between iterations (e.g. from a model fallback
|
|
155
|
-
// sendMessage retry), consume it immediately instead of creating a new promise.
|
|
156
|
-
// Cap queue to 3 entries to prevent unbounded growth from stale events.
|
|
157
|
-
if (s.pendingAgentEndQueue.length > 3) {
|
|
158
|
-
debugLog("runUnit", {
|
|
159
|
-
phase: "queue-overflow",
|
|
160
|
-
dropped: s.pendingAgentEndQueue.length - 1,
|
|
161
|
-
unitType,
|
|
162
|
-
unitId,
|
|
163
|
-
});
|
|
164
|
-
s.pendingAgentEndQueue = [
|
|
165
|
-
s.pendingAgentEndQueue[s.pendingAgentEndQueue.length - 1]!,
|
|
166
|
-
];
|
|
167
|
-
}
|
|
168
|
-
if (s.pendingAgentEndQueue.length > 0) {
|
|
169
|
-
const queued = s.pendingAgentEndQueue.shift()!;
|
|
170
|
-
debugLog("runUnit", {
|
|
171
|
-
phase: "drained-queued-event",
|
|
172
|
-
unitType,
|
|
173
|
-
unitId,
|
|
174
|
-
queueRemaining: s.pendingAgentEndQueue.length,
|
|
175
|
-
});
|
|
176
|
-
return { status: "completed", event: queued };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
215
|
// ── Session creation with timeout ──
|
|
180
216
|
debugLog("runUnit", { phase: "session-create", unitType, unitId });
|
|
181
217
|
|
|
182
218
|
let sessionResult: { cancelled: boolean };
|
|
183
219
|
let sessionTimeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
184
|
-
|
|
220
|
+
_sessionSwitchInFlight = true;
|
|
185
221
|
try {
|
|
186
222
|
const sessionPromise = s.cmdCtx!.newSession().finally(() => {
|
|
187
|
-
|
|
223
|
+
_sessionSwitchInFlight = false;
|
|
188
224
|
});
|
|
189
225
|
const timeoutPromise = new Promise<{ cancelled: true }>((resolve) => {
|
|
190
226
|
sessionTimeoutHandle = setTimeout(
|
|
@@ -216,11 +252,12 @@ export async function runUnit(
|
|
|
216
252
|
return { status: "cancelled" };
|
|
217
253
|
}
|
|
218
254
|
|
|
219
|
-
// ── Create the agent_end promise (
|
|
255
|
+
// ── Create the agent_end promise (per-unit one-shot) ──
|
|
220
256
|
// This happens after newSession completes so session-switch agent_end events
|
|
221
257
|
// from the previous session cannot resolve the new unit.
|
|
258
|
+
_sessionSwitchInFlight = false;
|
|
222
259
|
const unitPromise = new Promise<UnitResult>((resolve) => {
|
|
223
|
-
|
|
260
|
+
_currentResolve = resolve;
|
|
224
261
|
});
|
|
225
262
|
|
|
226
263
|
// Ensure cwd matches basePath before dispatch (#1389).
|
|
@@ -250,6 +287,20 @@ export async function runUnit(
|
|
|
250
287
|
status: result.status,
|
|
251
288
|
});
|
|
252
289
|
|
|
290
|
+
// Discard trailing follow-up messages (e.g. async_job_result notifications)
|
|
291
|
+
// from the completed unit. Without this, queued follow-ups trigger wasteful
|
|
292
|
+
// LLM turns before the next session can start (#1642).
|
|
293
|
+
// clearQueue() lives on AgentSession but isn't part of the typed
|
|
294
|
+
// ExtensionCommandContext interface — call it via runtime check.
|
|
295
|
+
try {
|
|
296
|
+
const cmdCtxAny = s.cmdCtx as Record<string, unknown> | null;
|
|
297
|
+
if (typeof cmdCtxAny?.clearQueue === "function") {
|
|
298
|
+
(cmdCtxAny.clearQueue as () => unknown)();
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
// Non-fatal — clearQueue may not be available in all contexts
|
|
302
|
+
}
|
|
303
|
+
|
|
253
304
|
return result;
|
|
254
305
|
}
|
|
255
306
|
|
|
@@ -383,6 +434,7 @@ export interface LoopDeps {
|
|
|
383
434
|
midTitle: string;
|
|
384
435
|
state: GSDState;
|
|
385
436
|
prefs: GSDPreferences | undefined;
|
|
437
|
+
session?: AutoSession;
|
|
386
438
|
}) => Promise<DispatchAction>;
|
|
387
439
|
runPreDispatchHooks: (
|
|
388
440
|
unitType: string,
|
|
@@ -500,6 +552,7 @@ export interface LoopDeps {
|
|
|
500
552
|
// Post-unit processing
|
|
501
553
|
postUnitPreVerification: (
|
|
502
554
|
pctx: PostUnitContext,
|
|
555
|
+
opts?: PreVerificationOpts,
|
|
503
556
|
) => Promise<"dispatched" | "continue">;
|
|
504
557
|
runPostUnitVerification: (
|
|
505
558
|
vctx: VerificationContext,
|
|
@@ -513,6 +566,96 @@ export interface LoopDeps {
|
|
|
513
566
|
getSessionFile: (ctx: ExtensionContext) => string;
|
|
514
567
|
}
|
|
515
568
|
|
|
569
|
+
// ─── generateMilestoneReport ──────────────────────────────────────────────────
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Generate and write an HTML milestone report snapshot.
|
|
573
|
+
* Extracted from the milestone-transition block in autoLoop.
|
|
574
|
+
*/
|
|
575
|
+
async function generateMilestoneReport(
|
|
576
|
+
s: AutoSession,
|
|
577
|
+
ctx: ExtensionContext,
|
|
578
|
+
milestoneId: string,
|
|
579
|
+
): Promise<void> {
|
|
580
|
+
const { loadVisualizerData } = await importExtensionModule<typeof import("./visualizer-data.js")>(import.meta.url, "./visualizer-data.js");
|
|
581
|
+
const { generateHtmlReport } = await importExtensionModule<typeof import("./export-html.js")>(import.meta.url, "./export-html.js");
|
|
582
|
+
const { writeReportSnapshot } = await importExtensionModule<typeof import("./reports.js")>(import.meta.url, "./reports.js");
|
|
583
|
+
const { basename } = await import("node:path");
|
|
584
|
+
|
|
585
|
+
const snapData = await loadVisualizerData(s.basePath);
|
|
586
|
+
const completedMs = snapData.milestones.find(
|
|
587
|
+
(m: { id: string }) => m.id === milestoneId,
|
|
588
|
+
);
|
|
589
|
+
const msTitle = completedMs?.title ?? milestoneId;
|
|
590
|
+
const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
|
|
591
|
+
const projName = basename(s.basePath);
|
|
592
|
+
const doneSlices = snapData.milestones.reduce(
|
|
593
|
+
(acc: number, m: { slices: { done: boolean }[] }) =>
|
|
594
|
+
acc + m.slices.filter((sl: { done: boolean }) => sl.done).length,
|
|
595
|
+
0,
|
|
596
|
+
);
|
|
597
|
+
const totalSlices = snapData.milestones.reduce(
|
|
598
|
+
(acc: number, m: { slices: unknown[] }) => acc + m.slices.length,
|
|
599
|
+
0,
|
|
600
|
+
);
|
|
601
|
+
const outPath = writeReportSnapshot({
|
|
602
|
+
basePath: s.basePath,
|
|
603
|
+
html: generateHtmlReport(snapData, {
|
|
604
|
+
projectName: projName,
|
|
605
|
+
projectPath: s.basePath,
|
|
606
|
+
gsdVersion,
|
|
607
|
+
milestoneId,
|
|
608
|
+
indexRelPath: "index.html",
|
|
609
|
+
}),
|
|
610
|
+
milestoneId,
|
|
611
|
+
milestoneTitle: msTitle,
|
|
612
|
+
kind: "milestone",
|
|
613
|
+
projectName: projName,
|
|
614
|
+
projectPath: s.basePath,
|
|
615
|
+
gsdVersion,
|
|
616
|
+
totalCost: snapData.totals?.cost ?? 0,
|
|
617
|
+
totalTokens: snapData.totals?.tokens.total ?? 0,
|
|
618
|
+
totalDuration: snapData.totals?.duration ?? 0,
|
|
619
|
+
doneSlices,
|
|
620
|
+
totalSlices,
|
|
621
|
+
doneMilestones: snapData.milestones.filter(
|
|
622
|
+
(m: { status: string }) => m.status === "complete",
|
|
623
|
+
).length,
|
|
624
|
+
totalMilestones: snapData.milestones.length,
|
|
625
|
+
phase: snapData.phase,
|
|
626
|
+
});
|
|
627
|
+
ctx.ui.notify(
|
|
628
|
+
`Report saved: .gsd/reports/${basename(outPath)} — open index.html to browse progression.`,
|
|
629
|
+
"info",
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ─── closeoutAndStop ──────────────────────────────────────────────────────────
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* If a unit is in-flight, close it out, then stop auto-mode.
|
|
637
|
+
* Extracted from ~4 identical if-closeout-then-stop sequences in autoLoop.
|
|
638
|
+
*/
|
|
639
|
+
async function closeoutAndStop(
|
|
640
|
+
ctx: ExtensionContext,
|
|
641
|
+
pi: ExtensionAPI,
|
|
642
|
+
s: AutoSession,
|
|
643
|
+
deps: LoopDeps,
|
|
644
|
+
reason: string,
|
|
645
|
+
): Promise<void> {
|
|
646
|
+
if (s.currentUnit) {
|
|
647
|
+
await deps.closeoutUnit(
|
|
648
|
+
ctx,
|
|
649
|
+
s.basePath,
|
|
650
|
+
s.currentUnit.type,
|
|
651
|
+
s.currentUnit.id,
|
|
652
|
+
s.currentUnit.startedAt,
|
|
653
|
+
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
await deps.stopAuto(ctx, pi, reason);
|
|
657
|
+
}
|
|
658
|
+
|
|
516
659
|
// ─── autoLoop ────────────────────────────────────────────────────────────────
|
|
517
660
|
|
|
518
661
|
/**
|
|
@@ -530,10 +673,11 @@ export async function autoLoop(
|
|
|
530
673
|
deps: LoopDeps,
|
|
531
674
|
): Promise<void> {
|
|
532
675
|
debugLog("autoLoop", { phase: "enter" });
|
|
533
|
-
_activeSession = s;
|
|
534
676
|
let iteration = 0;
|
|
535
|
-
|
|
536
|
-
|
|
677
|
+
// ── Sliding-window stuck detection ──
|
|
678
|
+
const recentUnits: Array<{ key: string; error?: string }> = [];
|
|
679
|
+
const STUCK_WINDOW_SIZE = 6;
|
|
680
|
+
let stuckRecoveryAttempts = 0;
|
|
537
681
|
|
|
538
682
|
let consecutiveErrors = 0;
|
|
539
683
|
|
|
@@ -562,6 +706,19 @@ export async function autoLoop(
|
|
|
562
706
|
|
|
563
707
|
try {
|
|
564
708
|
// ── Blanket try/catch: one bad iteration must not kill the session
|
|
709
|
+
const prefs = deps.loadEffectiveGSDPreferences()?.preferences;
|
|
710
|
+
|
|
711
|
+
// ── Check sidecar queue before deriveState ──
|
|
712
|
+
let sidecarItem: SidecarItem | undefined;
|
|
713
|
+
if (s.sidecarQueue.length > 0) {
|
|
714
|
+
sidecarItem = s.sidecarQueue.shift()!;
|
|
715
|
+
debugLog("autoLoop", {
|
|
716
|
+
phase: "sidecar-dequeue",
|
|
717
|
+
kind: sidecarItem.kind,
|
|
718
|
+
unitType: sidecarItem.unitType,
|
|
719
|
+
unitId: sidecarItem.unitId,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
565
722
|
|
|
566
723
|
const sessionLockBase = deps.lockBase();
|
|
567
724
|
if (sessionLockBase) {
|
|
@@ -583,6 +740,17 @@ export async function autoLoop(
|
|
|
583
740
|
}
|
|
584
741
|
}
|
|
585
742
|
|
|
743
|
+
// Variables shared between the sidecar and normal paths
|
|
744
|
+
let unitType: string;
|
|
745
|
+
let unitId: string;
|
|
746
|
+
let prompt: string;
|
|
747
|
+
let pauseAfterUatDispatch = false;
|
|
748
|
+
let state: GSDState;
|
|
749
|
+
let mid: string | undefined;
|
|
750
|
+
let midTitle: string | undefined;
|
|
751
|
+
let observabilityIssues: unknown[] = [];
|
|
752
|
+
|
|
753
|
+
if (!sidecarItem) {
|
|
586
754
|
// ── Phase 1: Pre-dispatch ───────────────────────────────────────────
|
|
587
755
|
|
|
588
756
|
// Resource version guard
|
|
@@ -633,10 +801,10 @@ export async function autoLoop(
|
|
|
633
801
|
}
|
|
634
802
|
|
|
635
803
|
// Derive state
|
|
636
|
-
|
|
637
|
-
deps.syncCmuxSidebar(
|
|
638
|
-
|
|
639
|
-
|
|
804
|
+
state = await deps.deriveState(s.basePath);
|
|
805
|
+
deps.syncCmuxSidebar(prefs, state);
|
|
806
|
+
mid = state.activeMilestone?.id;
|
|
807
|
+
midTitle = state.activeMilestone?.title;
|
|
640
808
|
debugLog("autoLoop", {
|
|
641
809
|
phase: "state-derived",
|
|
642
810
|
iteration,
|
|
@@ -657,68 +825,18 @@ export async function autoLoop(
|
|
|
657
825
|
"milestone",
|
|
658
826
|
);
|
|
659
827
|
deps.logCmuxEvent(
|
|
660
|
-
|
|
828
|
+
prefs,
|
|
661
829
|
`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`,
|
|
662
830
|
"success",
|
|
663
831
|
);
|
|
664
832
|
|
|
665
|
-
const vizPrefs =
|
|
833
|
+
const vizPrefs = prefs;
|
|
666
834
|
if (vizPrefs?.auto_visualize) {
|
|
667
835
|
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
668
836
|
}
|
|
669
837
|
if (vizPrefs?.auto_report !== false) {
|
|
670
838
|
try {
|
|
671
|
-
|
|
672
|
-
const { generateHtmlReport } = await import("./export-html.js");
|
|
673
|
-
const { writeReportSnapshot } = await import("./reports.js");
|
|
674
|
-
const { basename } = await import("node:path");
|
|
675
|
-
const snapData = await loadVisualizerData(s.basePath);
|
|
676
|
-
const completedMs = snapData.milestones.find(
|
|
677
|
-
(m: { id: string }) => m.id === s.currentMilestoneId,
|
|
678
|
-
);
|
|
679
|
-
const msTitle = completedMs?.title ?? s.currentMilestoneId;
|
|
680
|
-
const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
|
|
681
|
-
const projName = basename(s.basePath);
|
|
682
|
-
const doneSlices = snapData.milestones.reduce(
|
|
683
|
-
(acc: number, m: { slices: { done: boolean }[] }) =>
|
|
684
|
-
acc +
|
|
685
|
-
m.slices.filter((sl: { done: boolean }) => sl.done).length,
|
|
686
|
-
0,
|
|
687
|
-
);
|
|
688
|
-
const totalSlices = snapData.milestones.reduce(
|
|
689
|
-
(acc: number, m: { slices: unknown[] }) => acc + m.slices.length,
|
|
690
|
-
0,
|
|
691
|
-
);
|
|
692
|
-
const outPath = writeReportSnapshot({
|
|
693
|
-
basePath: s.basePath,
|
|
694
|
-
html: generateHtmlReport(snapData, {
|
|
695
|
-
projectName: projName,
|
|
696
|
-
projectPath: s.basePath,
|
|
697
|
-
gsdVersion,
|
|
698
|
-
milestoneId: s.currentMilestoneId,
|
|
699
|
-
indexRelPath: "index.html",
|
|
700
|
-
}),
|
|
701
|
-
milestoneId: s.currentMilestoneId!,
|
|
702
|
-
milestoneTitle: msTitle,
|
|
703
|
-
kind: "milestone",
|
|
704
|
-
projectName: projName,
|
|
705
|
-
projectPath: s.basePath,
|
|
706
|
-
gsdVersion,
|
|
707
|
-
totalCost: snapData.totals?.cost ?? 0,
|
|
708
|
-
totalTokens: snapData.totals?.tokens.total ?? 0,
|
|
709
|
-
totalDuration: snapData.totals?.duration ?? 0,
|
|
710
|
-
doneSlices,
|
|
711
|
-
totalSlices,
|
|
712
|
-
doneMilestones: snapData.milestones.filter(
|
|
713
|
-
(m: { status: string }) => m.status === "complete",
|
|
714
|
-
).length,
|
|
715
|
-
totalMilestones: snapData.milestones.length,
|
|
716
|
-
phase: snapData.phase,
|
|
717
|
-
});
|
|
718
|
-
ctx.ui.notify(
|
|
719
|
-
`Report saved: .gsd/reports/${(await import("node:path")).basename(outPath)} — open index.html to browse progression.`,
|
|
720
|
-
"info",
|
|
721
|
-
);
|
|
839
|
+
await generateMilestoneReport(s, ctx, s.currentMilestoneId!);
|
|
722
840
|
} catch (err) {
|
|
723
841
|
ctx.ui.notify(
|
|
724
842
|
`Report generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -731,11 +849,30 @@ export async function autoLoop(
|
|
|
731
849
|
s.unitDispatchCount.clear();
|
|
732
850
|
s.unitRecoveryCount.clear();
|
|
733
851
|
s.unitLifetimeDispatches.clear();
|
|
734
|
-
|
|
735
|
-
|
|
852
|
+
recentUnits.length = 0;
|
|
853
|
+
stuckRecoveryAttempts = 0;
|
|
736
854
|
|
|
737
855
|
// Worktree lifecycle on milestone transition — merge current, enter next
|
|
738
856
|
deps.resolver.mergeAndExit(s.currentMilestoneId!, ctx.ui);
|
|
857
|
+
|
|
858
|
+
// Opt-in: create draft PR on milestone completion
|
|
859
|
+
if (prefs?.git?.auto_pr) {
|
|
860
|
+
try {
|
|
861
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
862
|
+
const prUrl = createDraftPR(
|
|
863
|
+
s.basePath,
|
|
864
|
+
s.currentMilestoneId!,
|
|
865
|
+
`[GSD] ${s.currentMilestoneId} complete`,
|
|
866
|
+
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
867
|
+
);
|
|
868
|
+
if (prUrl) {
|
|
869
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
870
|
+
}
|
|
871
|
+
} catch {
|
|
872
|
+
// Non-fatal — PR creation is best-effort
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
739
876
|
deps.invalidateAllCaches();
|
|
740
877
|
|
|
741
878
|
state = await deps.deriveState(s.basePath);
|
|
@@ -745,9 +882,7 @@ export async function autoLoop(
|
|
|
745
882
|
if (mid) {
|
|
746
883
|
if (deps.getIsolationMode() !== "none") {
|
|
747
884
|
deps.captureIntegrationBranch(s.basePath, mid, {
|
|
748
|
-
commitDocs:
|
|
749
|
-
deps.loadEffectiveGSDPreferences()?.preferences?.git
|
|
750
|
-
?.commit_docs,
|
|
885
|
+
commitDocs: prefs?.git?.commit_docs,
|
|
751
886
|
});
|
|
752
887
|
}
|
|
753
888
|
deps.resolver.enterMilestone(mid, ctx.ui);
|
|
@@ -787,10 +922,28 @@ export async function autoLoop(
|
|
|
787
922
|
(m: { status: string }) =>
|
|
788
923
|
m.status !== "complete" && m.status !== "parked",
|
|
789
924
|
);
|
|
790
|
-
if (incomplete.length === 0) {
|
|
925
|
+
if (incomplete.length === 0 && state.registry.length > 0) {
|
|
791
926
|
// All milestones complete — merge milestone branch before stopping
|
|
792
927
|
if (s.currentMilestoneId) {
|
|
793
928
|
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
929
|
+
|
|
930
|
+
// Opt-in: create draft PR on milestone completion
|
|
931
|
+
if (prefs?.git?.auto_pr) {
|
|
932
|
+
try {
|
|
933
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
934
|
+
const prUrl = createDraftPR(
|
|
935
|
+
s.basePath,
|
|
936
|
+
s.currentMilestoneId,
|
|
937
|
+
`[GSD] ${s.currentMilestoneId} complete`,
|
|
938
|
+
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
939
|
+
);
|
|
940
|
+
if (prUrl) {
|
|
941
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
942
|
+
}
|
|
943
|
+
} catch {
|
|
944
|
+
// Non-fatal — PR creation is best-effort
|
|
945
|
+
}
|
|
946
|
+
}
|
|
794
947
|
}
|
|
795
948
|
deps.sendDesktopNotification(
|
|
796
949
|
"GSD",
|
|
@@ -799,17 +952,29 @@ export async function autoLoop(
|
|
|
799
952
|
"milestone",
|
|
800
953
|
);
|
|
801
954
|
deps.logCmuxEvent(
|
|
802
|
-
|
|
955
|
+
prefs,
|
|
803
956
|
"All milestones complete.",
|
|
804
957
|
"success",
|
|
805
958
|
);
|
|
806
959
|
await deps.stopAuto(ctx, pi, "All milestones complete");
|
|
960
|
+
} else if (incomplete.length === 0 && state.registry.length === 0) {
|
|
961
|
+
// Empty registry — no milestones visible, likely a path resolution bug
|
|
962
|
+
const diag = `basePath=${s.basePath}, phase=${state.phase}`;
|
|
963
|
+
ctx.ui.notify(
|
|
964
|
+
`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`,
|
|
965
|
+
"error",
|
|
966
|
+
);
|
|
967
|
+
await deps.stopAuto(
|
|
968
|
+
ctx,
|
|
969
|
+
pi,
|
|
970
|
+
`No milestones found — check basePath resolution`,
|
|
971
|
+
);
|
|
807
972
|
} else if (state.phase === "blocked") {
|
|
808
973
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
809
974
|
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
810
975
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
811
976
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
812
|
-
deps.logCmuxEvent(
|
|
977
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
813
978
|
} else {
|
|
814
979
|
const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
|
|
815
980
|
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
@@ -844,20 +1009,10 @@ export async function autoLoop(
|
|
|
844
1009
|
}
|
|
845
1010
|
|
|
846
1011
|
if (!mid || !midTitle) {
|
|
847
|
-
if (s.currentUnit) {
|
|
848
|
-
await deps.closeoutUnit(
|
|
849
|
-
ctx,
|
|
850
|
-
s.basePath,
|
|
851
|
-
s.currentUnit.type,
|
|
852
|
-
s.currentUnit.id,
|
|
853
|
-
s.currentUnit.startedAt,
|
|
854
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
855
|
-
);
|
|
856
|
-
}
|
|
857
1012
|
const noMilestoneReason = !mid
|
|
858
1013
|
? "No active milestone after merge reconciliation"
|
|
859
1014
|
: `Milestone ${mid} has no title after reconciliation`;
|
|
860
|
-
await
|
|
1015
|
+
await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
|
|
861
1016
|
debugLog("autoLoop", {
|
|
862
1017
|
phase: "exit",
|
|
863
1018
|
reason: "no-milestone-after-reconciliation",
|
|
@@ -867,19 +1022,27 @@ export async function autoLoop(
|
|
|
867
1022
|
|
|
868
1023
|
// Terminal: complete
|
|
869
1024
|
if (state.phase === "complete") {
|
|
870
|
-
|
|
871
|
-
await deps.closeoutUnit(
|
|
872
|
-
ctx,
|
|
873
|
-
s.basePath,
|
|
874
|
-
s.currentUnit.type,
|
|
875
|
-
s.currentUnit.id,
|
|
876
|
-
s.currentUnit.startedAt,
|
|
877
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
878
|
-
);
|
|
879
|
-
}
|
|
880
|
-
// Milestone merge on complete
|
|
1025
|
+
// Milestone merge on complete (before closeout so branch state is clean)
|
|
881
1026
|
if (s.currentMilestoneId) {
|
|
882
1027
|
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
1028
|
+
|
|
1029
|
+
// Opt-in: create draft PR on milestone completion
|
|
1030
|
+
if (prefs?.git?.auto_pr) {
|
|
1031
|
+
try {
|
|
1032
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
1033
|
+
const prUrl = createDraftPR(
|
|
1034
|
+
s.basePath,
|
|
1035
|
+
s.currentMilestoneId,
|
|
1036
|
+
`[GSD] ${s.currentMilestoneId} complete`,
|
|
1037
|
+
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
1038
|
+
);
|
|
1039
|
+
if (prUrl) {
|
|
1040
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
1041
|
+
}
|
|
1042
|
+
} catch {
|
|
1043
|
+
// Non-fatal — PR creation is best-effort
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
883
1046
|
}
|
|
884
1047
|
deps.sendDesktopNotification(
|
|
885
1048
|
"GSD",
|
|
@@ -888,40 +1051,28 @@ export async function autoLoop(
|
|
|
888
1051
|
"milestone",
|
|
889
1052
|
);
|
|
890
1053
|
deps.logCmuxEvent(
|
|
891
|
-
|
|
1054
|
+
prefs,
|
|
892
1055
|
`Milestone ${mid} complete.`,
|
|
893
1056
|
"success",
|
|
894
1057
|
);
|
|
895
|
-
await
|
|
1058
|
+
await closeoutAndStop(ctx, pi, s, deps, `Milestone ${mid} complete`);
|
|
896
1059
|
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
897
1060
|
break;
|
|
898
1061
|
}
|
|
899
1062
|
|
|
900
1063
|
// Terminal: blocked
|
|
901
1064
|
if (state.phase === "blocked") {
|
|
902
|
-
if (s.currentUnit) {
|
|
903
|
-
await deps.closeoutUnit(
|
|
904
|
-
ctx,
|
|
905
|
-
s.basePath,
|
|
906
|
-
s.currentUnit.type,
|
|
907
|
-
s.currentUnit.id,
|
|
908
|
-
s.currentUnit.startedAt,
|
|
909
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
910
|
-
);
|
|
911
|
-
}
|
|
912
1065
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
913
|
-
await
|
|
1066
|
+
await closeoutAndStop(ctx, pi, s, deps, blockerMsg);
|
|
914
1067
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
915
1068
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
916
|
-
deps.logCmuxEvent(
|
|
1069
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
917
1070
|
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
918
1071
|
break;
|
|
919
1072
|
}
|
|
920
1073
|
|
|
921
1074
|
// ── Phase 2: Guards ─────────────────────────────────────────────────
|
|
922
1075
|
|
|
923
|
-
const prefs = deps.loadEffectiveGSDPreferences()?.preferences;
|
|
924
|
-
|
|
925
1076
|
// Budget ceiling guard
|
|
926
1077
|
const budgetCeiling = prefs?.budget_ceiling;
|
|
927
1078
|
if (budgetCeiling !== undefined && budgetCeiling > 0) {
|
|
@@ -941,84 +1092,49 @@ export async function autoLoop(
|
|
|
941
1092
|
budgetPct,
|
|
942
1093
|
);
|
|
943
1094
|
|
|
944
|
-
|
|
945
|
-
|
|
1095
|
+
// Data-driven threshold check — loop descending, fire first match
|
|
1096
|
+
const threshold = BUDGET_THRESHOLDS.find(
|
|
1097
|
+
(t) => newBudgetAlertLevel >= t.pct,
|
|
1098
|
+
);
|
|
1099
|
+
if (threshold) {
|
|
946
1100
|
s.lastBudgetAlertLevel =
|
|
947
1101
|
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
)
|
|
1102
|
+
|
|
1103
|
+
if (threshold.pct === 100 && budgetEnforcementAction !== "none") {
|
|
1104
|
+
// 100% — special enforcement logic (halt/pause/warn)
|
|
1105
|
+
const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
|
|
1106
|
+
if (budgetEnforcementAction === "halt") {
|
|
1107
|
+
deps.sendDesktopNotification("GSD", msg, "error", "budget");
|
|
1108
|
+
await deps.stopAuto(ctx, pi, "Budget ceiling reached");
|
|
1109
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
|
|
1110
|
+
break;
|
|
1111
|
+
}
|
|
1112
|
+
if (budgetEnforcementAction === "pause") {
|
|
1113
|
+
ctx.ui.notify(
|
|
1114
|
+
`${msg} Pausing auto-mode — /gsd auto to override and continue.`,
|
|
1115
|
+
"warning",
|
|
1116
|
+
);
|
|
1117
|
+
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
1118
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
1119
|
+
await deps.pauseAuto(ctx, pi);
|
|
1120
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
1121
|
+
break;
|
|
1122
|
+
}
|
|
1123
|
+
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
959
1124
|
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
960
1125
|
deps.logCmuxEvent(prefs, msg, "warning");
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1126
|
+
} else if (threshold.pct < 100) {
|
|
1127
|
+
// Sub-100% — simple notification
|
|
1128
|
+
const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
|
|
1129
|
+
ctx.ui.notify(msg, threshold.notifyLevel);
|
|
1130
|
+
deps.sendDesktopNotification(
|
|
1131
|
+
"GSD",
|
|
1132
|
+
msg,
|
|
1133
|
+
threshold.notifyLevel,
|
|
1134
|
+
"budget",
|
|
1135
|
+
);
|
|
1136
|
+
deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
|
|
964
1137
|
}
|
|
965
|
-
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
966
|
-
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
967
|
-
deps.logCmuxEvent(prefs, msg, "warning");
|
|
968
|
-
} else if (newBudgetAlertLevel === 90) {
|
|
969
|
-
s.lastBudgetAlertLevel =
|
|
970
|
-
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
971
|
-
ctx.ui.notify(
|
|
972
|
-
`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
973
|
-
"warning",
|
|
974
|
-
);
|
|
975
|
-
deps.sendDesktopNotification(
|
|
976
|
-
"GSD",
|
|
977
|
-
`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
978
|
-
"warning",
|
|
979
|
-
"budget",
|
|
980
|
-
);
|
|
981
|
-
deps.logCmuxEvent(
|
|
982
|
-
prefs,
|
|
983
|
-
`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
984
|
-
"warning",
|
|
985
|
-
);
|
|
986
|
-
} else if (newBudgetAlertLevel === 80) {
|
|
987
|
-
s.lastBudgetAlertLevel =
|
|
988
|
-
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
989
|
-
ctx.ui.notify(
|
|
990
|
-
`Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
991
|
-
"warning",
|
|
992
|
-
);
|
|
993
|
-
deps.sendDesktopNotification(
|
|
994
|
-
"GSD",
|
|
995
|
-
`Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
996
|
-
"warning",
|
|
997
|
-
"budget",
|
|
998
|
-
);
|
|
999
|
-
deps.logCmuxEvent(
|
|
1000
|
-
prefs,
|
|
1001
|
-
`Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1002
|
-
"warning",
|
|
1003
|
-
);
|
|
1004
|
-
} else if (newBudgetAlertLevel === 75) {
|
|
1005
|
-
s.lastBudgetAlertLevel =
|
|
1006
|
-
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
1007
|
-
ctx.ui.notify(
|
|
1008
|
-
`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1009
|
-
"info",
|
|
1010
|
-
);
|
|
1011
|
-
deps.sendDesktopNotification(
|
|
1012
|
-
"GSD",
|
|
1013
|
-
`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1014
|
-
"info",
|
|
1015
|
-
"budget",
|
|
1016
|
-
);
|
|
1017
|
-
deps.logCmuxEvent(
|
|
1018
|
-
prefs,
|
|
1019
|
-
`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1020
|
-
"progress",
|
|
1021
|
-
);
|
|
1022
1138
|
} else if (budgetAlertLevel === 0) {
|
|
1023
1139
|
s.lastBudgetAlertLevel = 0;
|
|
1024
1140
|
}
|
|
@@ -1091,20 +1207,11 @@ export async function autoLoop(
|
|
|
1091
1207
|
midTitle: midTitle!,
|
|
1092
1208
|
state,
|
|
1093
1209
|
prefs,
|
|
1210
|
+
session: s,
|
|
1094
1211
|
});
|
|
1095
1212
|
|
|
1096
1213
|
if (dispatchResult.action === "stop") {
|
|
1097
|
-
|
|
1098
|
-
await deps.closeoutUnit(
|
|
1099
|
-
ctx,
|
|
1100
|
-
s.basePath,
|
|
1101
|
-
s.currentUnit.type,
|
|
1102
|
-
s.currentUnit.id,
|
|
1103
|
-
s.currentUnit.startedAt,
|
|
1104
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
1105
|
-
);
|
|
1106
|
-
}
|
|
1107
|
-
await deps.stopAuto(ctx, pi, dispatchResult.reason);
|
|
1214
|
+
await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
|
|
1108
1215
|
debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
|
|
1109
1216
|
break;
|
|
1110
1217
|
}
|
|
@@ -1115,76 +1222,84 @@ export async function autoLoop(
|
|
|
1115
1222
|
continue;
|
|
1116
1223
|
}
|
|
1117
1224
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1225
|
+
unitType = dispatchResult.unitType;
|
|
1226
|
+
unitId = dispatchResult.unitId;
|
|
1227
|
+
prompt = dispatchResult.prompt;
|
|
1228
|
+
pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
1122
1229
|
|
|
1123
|
-
// ──
|
|
1230
|
+
// ── Sliding-window stuck detection with graduated recovery ──
|
|
1124
1231
|
const derivedKey = `${unitType}/${unitId}`;
|
|
1125
|
-
if (derivedKey === lastDerivedUnit && !s.pendingVerificationRetry) {
|
|
1126
|
-
sameUnitCount++;
|
|
1127
|
-
debugLog("autoLoop", {
|
|
1128
|
-
phase: "stuck-check",
|
|
1129
|
-
unitType,
|
|
1130
|
-
unitId,
|
|
1131
|
-
sameUnitCount,
|
|
1132
|
-
});
|
|
1133
1232
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1233
|
+
if (!s.pendingVerificationRetry) {
|
|
1234
|
+
recentUnits.push({ key: derivedKey });
|
|
1235
|
+
if (recentUnits.length > STUCK_WINDOW_SIZE) recentUnits.shift();
|
|
1236
|
+
|
|
1237
|
+
const stuckSignal = detectStuck(recentUnits);
|
|
1238
|
+
if (stuckSignal) {
|
|
1239
|
+
debugLog("autoLoop", {
|
|
1240
|
+
phase: "stuck-check",
|
|
1137
1241
|
unitType,
|
|
1138
1242
|
unitId,
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1243
|
+
reason: stuckSignal.reason,
|
|
1244
|
+
recoveryAttempts: stuckRecoveryAttempts,
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
if (stuckRecoveryAttempts === 0) {
|
|
1248
|
+
// Level 1: try verifying the artifact, then cache invalidation + retry
|
|
1249
|
+
stuckRecoveryAttempts++;
|
|
1250
|
+
const artifactExists = deps.verifyExpectedArtifact(
|
|
1251
|
+
unitType,
|
|
1252
|
+
unitId,
|
|
1253
|
+
s.basePath,
|
|
1254
|
+
);
|
|
1255
|
+
if (artifactExists) {
|
|
1256
|
+
debugLog("autoLoop", {
|
|
1257
|
+
phase: "stuck-recovery",
|
|
1258
|
+
level: 1,
|
|
1259
|
+
action: "artifact-found",
|
|
1260
|
+
});
|
|
1261
|
+
ctx.ui.notify(
|
|
1262
|
+
`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`,
|
|
1263
|
+
"info",
|
|
1264
|
+
);
|
|
1265
|
+
deps.invalidateAllCaches();
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
ctx.ui.notify(
|
|
1269
|
+
`Stuck on ${unitType} ${unitId} (${stuckSignal.reason}). Invalidating caches and retrying.`,
|
|
1270
|
+
"warning",
|
|
1271
|
+
);
|
|
1272
|
+
deps.invalidateAllCaches();
|
|
1273
|
+
} else {
|
|
1274
|
+
// Level 2: hard stop — genuinely stuck
|
|
1142
1275
|
debugLog("autoLoop", {
|
|
1143
|
-
phase: "stuck-
|
|
1144
|
-
|
|
1145
|
-
|
|
1276
|
+
phase: "stuck-detected",
|
|
1277
|
+
unitType,
|
|
1278
|
+
unitId,
|
|
1279
|
+
reason: stuckSignal.reason,
|
|
1146
1280
|
});
|
|
1281
|
+
await deps.stopAuto(
|
|
1282
|
+
ctx,
|
|
1283
|
+
pi,
|
|
1284
|
+
`Stuck: ${stuckSignal.reason}`,
|
|
1285
|
+
);
|
|
1147
1286
|
ctx.ui.notify(
|
|
1148
|
-
`Stuck
|
|
1149
|
-
"
|
|
1287
|
+
`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}. The expected artifact was not written.`,
|
|
1288
|
+
"error",
|
|
1150
1289
|
);
|
|
1151
|
-
|
|
1152
|
-
|
|
1290
|
+
break;
|
|
1291
|
+
}
|
|
1292
|
+
} else {
|
|
1293
|
+
// Progress detected — reset recovery counter
|
|
1294
|
+
if (stuckRecoveryAttempts > 0) {
|
|
1295
|
+
debugLog("autoLoop", {
|
|
1296
|
+
phase: "stuck-counter-reset",
|
|
1297
|
+
from: recentUnits[recentUnits.length - 2]?.key ?? "",
|
|
1298
|
+
to: derivedKey,
|
|
1299
|
+
});
|
|
1300
|
+
stuckRecoveryAttempts = 0;
|
|
1153
1301
|
}
|
|
1154
|
-
ctx.ui.notify(
|
|
1155
|
-
`Stuck on ${unitType} ${unitId} (attempt ${sameUnitCount}). Invalidating caches and retrying.`,
|
|
1156
|
-
"warning",
|
|
1157
|
-
);
|
|
1158
|
-
deps.invalidateAllCaches();
|
|
1159
|
-
} else if (sameUnitCount === 5) {
|
|
1160
|
-
// Level 2: hard stop — genuinely stuck
|
|
1161
|
-
debugLog("autoLoop", {
|
|
1162
|
-
phase: "stuck-detected",
|
|
1163
|
-
unitType,
|
|
1164
|
-
unitId,
|
|
1165
|
-
sameUnitCount,
|
|
1166
|
-
});
|
|
1167
|
-
await deps.stopAuto(
|
|
1168
|
-
ctx,
|
|
1169
|
-
pi,
|
|
1170
|
-
`Stuck: ${unitType} ${unitId} derived ${sameUnitCount} consecutive times without progress`,
|
|
1171
|
-
);
|
|
1172
|
-
ctx.ui.notify(
|
|
1173
|
-
`Stuck on ${unitType} ${unitId} — deriveState returns the same unit after ${sameUnitCount} attempts. The expected artifact was not written.`,
|
|
1174
|
-
"error",
|
|
1175
|
-
);
|
|
1176
|
-
break;
|
|
1177
|
-
}
|
|
1178
|
-
} else {
|
|
1179
|
-
if (derivedKey !== lastDerivedUnit) {
|
|
1180
|
-
debugLog("autoLoop", {
|
|
1181
|
-
phase: "stuck-counter-reset",
|
|
1182
|
-
from: lastDerivedUnit,
|
|
1183
|
-
to: derivedKey,
|
|
1184
|
-
});
|
|
1185
1302
|
}
|
|
1186
|
-
lastDerivedUnit = derivedKey;
|
|
1187
|
-
sameUnitCount = 0;
|
|
1188
1303
|
}
|
|
1189
1304
|
|
|
1190
1305
|
// Pre-dispatch hooks
|
|
@@ -1227,13 +1342,27 @@ export async function autoLoop(
|
|
|
1227
1342
|
break;
|
|
1228
1343
|
}
|
|
1229
1344
|
|
|
1230
|
-
|
|
1345
|
+
observabilityIssues = await deps.collectObservabilityWarnings(
|
|
1231
1346
|
ctx,
|
|
1232
1347
|
s.basePath,
|
|
1233
1348
|
unitType,
|
|
1234
1349
|
unitId,
|
|
1235
1350
|
);
|
|
1236
1351
|
|
|
1352
|
+
// Derive state for shared use in execution phase
|
|
1353
|
+
// (state, mid, midTitle already set above)
|
|
1354
|
+
|
|
1355
|
+
} else {
|
|
1356
|
+
// ── Sidecar path: use values from the sidecar item directly ──
|
|
1357
|
+
unitType = sidecarItem.unitType;
|
|
1358
|
+
unitId = sidecarItem.unitId;
|
|
1359
|
+
prompt = sidecarItem.prompt;
|
|
1360
|
+
// Derive minimal state for progress widget / execution context
|
|
1361
|
+
state = await deps.deriveState(s.basePath);
|
|
1362
|
+
mid = state.activeMilestone?.id;
|
|
1363
|
+
midTitle = state.activeMilestone?.title;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1237
1366
|
// ── Phase 4: Unit execution ─────────────────────────────────────────
|
|
1238
1367
|
|
|
1239
1368
|
debugLog("autoLoop", {
|
|
@@ -1251,61 +1380,6 @@ export async function autoLoop(
|
|
|
1251
1380
|
);
|
|
1252
1381
|
const previousTier = s.currentUnitRouting?.tier;
|
|
1253
1382
|
|
|
1254
|
-
// Closeout previous unit
|
|
1255
|
-
if (s.currentUnit) {
|
|
1256
|
-
await deps.closeoutUnit(
|
|
1257
|
-
ctx,
|
|
1258
|
-
s.basePath,
|
|
1259
|
-
s.currentUnit.type,
|
|
1260
|
-
s.currentUnit.id,
|
|
1261
|
-
s.currentUnit.startedAt,
|
|
1262
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
1263
|
-
);
|
|
1264
|
-
|
|
1265
|
-
if (s.currentUnitRouting) {
|
|
1266
|
-
const isRetry =
|
|
1267
|
-
s.currentUnit.type === unitType && s.currentUnit.id === unitId;
|
|
1268
|
-
deps.recordOutcome(
|
|
1269
|
-
s.currentUnit.type,
|
|
1270
|
-
s.currentUnitRouting.tier as "light" | "standard" | "heavy",
|
|
1271
|
-
!isRetry,
|
|
1272
|
-
);
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
const closeoutKey = `${s.currentUnit.type}/${s.currentUnit.id}`;
|
|
1276
|
-
const incomingKey = `${unitType}/${unitId}`;
|
|
1277
|
-
const isHookUnit = s.currentUnit.type.startsWith("hook/");
|
|
1278
|
-
const artifactVerified =
|
|
1279
|
-
isHookUnit ||
|
|
1280
|
-
deps.verifyExpectedArtifact(
|
|
1281
|
-
s.currentUnit.type,
|
|
1282
|
-
s.currentUnit.id,
|
|
1283
|
-
s.basePath,
|
|
1284
|
-
);
|
|
1285
|
-
if (closeoutKey !== incomingKey && artifactVerified) {
|
|
1286
|
-
s.completedUnits.push({
|
|
1287
|
-
type: s.currentUnit.type,
|
|
1288
|
-
id: s.currentUnit.id,
|
|
1289
|
-
startedAt: s.currentUnit.startedAt,
|
|
1290
|
-
finishedAt: Date.now(),
|
|
1291
|
-
});
|
|
1292
|
-
if (s.completedUnits.length > 200) {
|
|
1293
|
-
s.completedUnits = s.completedUnits.slice(-200);
|
|
1294
|
-
}
|
|
1295
|
-
deps.clearUnitRuntimeRecord(
|
|
1296
|
-
s.basePath,
|
|
1297
|
-
s.currentUnit.type,
|
|
1298
|
-
s.currentUnit.id,
|
|
1299
|
-
);
|
|
1300
|
-
s.unitDispatchCount.delete(
|
|
1301
|
-
`${s.currentUnit.type}/${s.currentUnit.id}`,
|
|
1302
|
-
);
|
|
1303
|
-
s.unitRecoveryCount.delete(
|
|
1304
|
-
`${s.currentUnit.type}/${s.currentUnit.id}`,
|
|
1305
|
-
);
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
1383
|
s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
1310
1384
|
deps.captureAvailableSkills();
|
|
1311
1385
|
deps.writeUnitRuntimeRecord(
|
|
@@ -1332,7 +1406,6 @@ export async function autoLoop(
|
|
|
1332
1406
|
deps.ensurePreconditions(unitType, unitId, s.basePath, state);
|
|
1333
1407
|
|
|
1334
1408
|
// Prompt injection
|
|
1335
|
-
const MAX_RECOVERY_CHARS = 50_000;
|
|
1336
1409
|
let finalPrompt = prompt;
|
|
1337
1410
|
|
|
1338
1411
|
if (s.pendingVerificationRetry) {
|
|
@@ -1377,7 +1450,7 @@ export async function autoLoop(
|
|
|
1377
1450
|
s.lastBaselineCharCount = undefined;
|
|
1378
1451
|
if (deps.isDbAvailable()) {
|
|
1379
1452
|
try {
|
|
1380
|
-
const { inlineGsdRootFile } = await import("./auto-prompts.js");
|
|
1453
|
+
const { inlineGsdRootFile } = await importExtensionModule<typeof import("./auto-prompts.js")>(import.meta.url, "./auto-prompts.js");
|
|
1381
1454
|
const [decisionsContent, requirementsContent, projectContent] =
|
|
1382
1455
|
await Promise.all([
|
|
1383
1456
|
inlineGsdRootFile(s.basePath, "decisions.md", "Decisions"),
|
|
@@ -1404,7 +1477,7 @@ export async function autoLoop(
|
|
|
1404
1477
|
);
|
|
1405
1478
|
}
|
|
1406
1479
|
|
|
1407
|
-
// Select and apply model (with tier escalation on retry)
|
|
1480
|
+
// Select and apply model (with tier escalation on retry — normal units only)
|
|
1408
1481
|
const modelResult = await deps.selectAndApplyModel(
|
|
1409
1482
|
ctx,
|
|
1410
1483
|
pi,
|
|
@@ -1414,7 +1487,7 @@ export async function autoLoop(
|
|
|
1414
1487
|
prefs,
|
|
1415
1488
|
s.verbose,
|
|
1416
1489
|
s.autoModeStartModel,
|
|
1417
|
-
{ isRetry, previousTier },
|
|
1490
|
+
sidecarItem ? undefined : { isRetry, previousTier },
|
|
1418
1491
|
);
|
|
1419
1492
|
s.currentUnitRouting =
|
|
1420
1493
|
modelResult.routing as AutoSession["currentUnitRouting"];
|
|
@@ -1463,7 +1536,6 @@ export async function autoLoop(
|
|
|
1463
1536
|
unitType,
|
|
1464
1537
|
unitId,
|
|
1465
1538
|
finalPrompt,
|
|
1466
|
-
prefs,
|
|
1467
1539
|
);
|
|
1468
1540
|
debugLog("autoLoop", {
|
|
1469
1541
|
phase: "runUnit-end",
|
|
@@ -1473,6 +1545,23 @@ export async function autoLoop(
|
|
|
1473
1545
|
status: unitResult.status,
|
|
1474
1546
|
});
|
|
1475
1547
|
|
|
1548
|
+
// Tag the most recent window entry with error info for stuck detection
|
|
1549
|
+
if (unitResult.status === "error" || unitResult.status === "cancelled") {
|
|
1550
|
+
const lastEntry = recentUnits[recentUnits.length - 1];
|
|
1551
|
+
if (lastEntry) {
|
|
1552
|
+
lastEntry.error = `${unitResult.status}:${unitType}/${unitId}`;
|
|
1553
|
+
}
|
|
1554
|
+
} else if (unitResult.event?.messages?.length) {
|
|
1555
|
+
const lastMsg = unitResult.event.messages[unitResult.event.messages.length - 1];
|
|
1556
|
+
const msgStr = typeof lastMsg === "string" ? lastMsg : JSON.stringify(lastMsg);
|
|
1557
|
+
if (/error|fail|exception/i.test(msgStr)) {
|
|
1558
|
+
const lastEntry = recentUnits[recentUnits.length - 1];
|
|
1559
|
+
if (lastEntry) {
|
|
1560
|
+
lastEntry.error = msgStr.slice(0, 200);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1476
1565
|
if (unitResult.status === "cancelled") {
|
|
1477
1566
|
ctx.ui.notify(
|
|
1478
1567
|
`Session creation timed out or was cancelled for ${unitType} ${unitId}. Will retry.`,
|
|
@@ -1483,6 +1572,52 @@ export async function autoLoop(
|
|
|
1483
1572
|
break;
|
|
1484
1573
|
}
|
|
1485
1574
|
|
|
1575
|
+
// ── Immediate unit closeout (metrics, activity log, memory) ────────
|
|
1576
|
+
// Run right after runUnit() returns so telemetry is never lost to a
|
|
1577
|
+
// crash between iterations.
|
|
1578
|
+
await deps.closeoutUnit(
|
|
1579
|
+
ctx,
|
|
1580
|
+
s.basePath,
|
|
1581
|
+
unitType,
|
|
1582
|
+
unitId,
|
|
1583
|
+
s.currentUnit.startedAt,
|
|
1584
|
+
deps.buildSnapshotOpts(unitType, unitId),
|
|
1585
|
+
);
|
|
1586
|
+
|
|
1587
|
+
if (s.currentUnitRouting) {
|
|
1588
|
+
deps.recordOutcome(
|
|
1589
|
+
unitType,
|
|
1590
|
+
s.currentUnitRouting.tier as "light" | "standard" | "heavy",
|
|
1591
|
+
true, // success assumed; dispatch will re-dispatch if artifact missing
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
const isHookUnit = unitType.startsWith("hook/");
|
|
1596
|
+
const artifactVerified =
|
|
1597
|
+
isHookUnit ||
|
|
1598
|
+
deps.verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
1599
|
+
if (artifactVerified) {
|
|
1600
|
+
s.completedUnits.push({
|
|
1601
|
+
type: unitType,
|
|
1602
|
+
id: unitId,
|
|
1603
|
+
startedAt: s.currentUnit.startedAt,
|
|
1604
|
+
finishedAt: Date.now(),
|
|
1605
|
+
});
|
|
1606
|
+
if (s.completedUnits.length > 200) {
|
|
1607
|
+
s.completedUnits = s.completedUnits.slice(-200);
|
|
1608
|
+
}
|
|
1609
|
+
// Flush completed-units to disk so the record survives crashes
|
|
1610
|
+
try {
|
|
1611
|
+
const completedKeysPath = join(gsdRoot(s.basePath), "completed-units.json");
|
|
1612
|
+
const keys = s.completedUnits.map((u) => `${u.type}/${u.id}`);
|
|
1613
|
+
atomicWriteSync(completedKeysPath, JSON.stringify(keys, null, 2));
|
|
1614
|
+
} catch { /* non-fatal: disk flush failure */ }
|
|
1615
|
+
|
|
1616
|
+
deps.clearUnitRuntimeRecord(s.basePath, unitType, unitId);
|
|
1617
|
+
s.unitDispatchCount.delete(`${unitType}/${unitId}`);
|
|
1618
|
+
s.unitRecoveryCount.delete(`${unitType}/${unitId}`);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1486
1621
|
// ── Phase 5: Finalize ───────────────────────────────────────────────
|
|
1487
1622
|
|
|
1488
1623
|
debugLog("autoLoop", { phase: "finalize", iteration });
|
|
@@ -1503,7 +1638,13 @@ export async function autoLoop(
|
|
|
1503
1638
|
};
|
|
1504
1639
|
|
|
1505
1640
|
// Pre-verification processing (commit, doctor, state rebuild, etc.)
|
|
1506
|
-
|
|
1641
|
+
// Sidecar items use lightweight pre-verification opts
|
|
1642
|
+
const preVerificationOpts: PreVerificationOpts | undefined = sidecarItem
|
|
1643
|
+
? sidecarItem.kind === "hook"
|
|
1644
|
+
? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
|
|
1645
|
+
: { skipSettleDelay: true, skipStateRebuild: true }
|
|
1646
|
+
: undefined;
|
|
1647
|
+
const preResult = await deps.postUnitPreVerification(postUnitCtx, preVerificationOpts);
|
|
1507
1648
|
if (preResult === "dispatched") {
|
|
1508
1649
|
debugLog("autoLoop", {
|
|
1509
1650
|
phase: "exit",
|
|
@@ -1522,22 +1663,32 @@ export async function autoLoop(
|
|
|
1522
1663
|
break;
|
|
1523
1664
|
}
|
|
1524
1665
|
|
|
1525
|
-
// Verification gate
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
)
|
|
1666
|
+
// Verification gate
|
|
1667
|
+
// Hook sidecar items skip verification entirely.
|
|
1668
|
+
// Non-hook sidecar items run verification but skip retries (just continue).
|
|
1669
|
+
const skipVerification = sidecarItem?.kind === "hook";
|
|
1670
|
+
if (!skipVerification) {
|
|
1671
|
+
const verificationResult = await deps.runPostUnitVerification(
|
|
1672
|
+
{ s, ctx, pi },
|
|
1673
|
+
deps.pauseAuto,
|
|
1674
|
+
);
|
|
1530
1675
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1676
|
+
if (verificationResult === "pause") {
|
|
1677
|
+
debugLog("autoLoop", { phase: "exit", reason: "verification-pause" });
|
|
1678
|
+
break;
|
|
1679
|
+
}
|
|
1535
1680
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1681
|
+
if (verificationResult === "retry") {
|
|
1682
|
+
if (sidecarItem) {
|
|
1683
|
+
// Sidecar verification retries are skipped — just continue
|
|
1684
|
+
debugLog("autoLoop", { phase: "sidecar-verification-retry-skipped", iteration });
|
|
1685
|
+
} else {
|
|
1686
|
+
// s.pendingVerificationRetry was set by runPostUnitVerification.
|
|
1687
|
+
// Continue the loop — next iteration will inject the retry context into the prompt.
|
|
1688
|
+
debugLog("autoLoop", { phase: "verification-retry", iteration });
|
|
1689
|
+
continue;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1541
1692
|
}
|
|
1542
1693
|
|
|
1543
1694
|
// Post-verification processing (DB dual-write, hooks, triage, quick-tasks)
|
|
@@ -1557,150 +1708,6 @@ export async function autoLoop(
|
|
|
1557
1708
|
break;
|
|
1558
1709
|
}
|
|
1559
1710
|
|
|
1560
|
-
// ── Sidecar drain: dispatch enqueued hooks/triage/quick-tasks ──
|
|
1561
|
-
let sidecarBroke = false;
|
|
1562
|
-
while (s.sidecarQueue.length > 0 && s.active) {
|
|
1563
|
-
const item = s.sidecarQueue.shift()!;
|
|
1564
|
-
debugLog("autoLoop", {
|
|
1565
|
-
phase: "sidecar-dequeue",
|
|
1566
|
-
kind: item.kind,
|
|
1567
|
-
unitType: item.unitType,
|
|
1568
|
-
unitId: item.unitId,
|
|
1569
|
-
});
|
|
1570
|
-
|
|
1571
|
-
// Set up as current unit
|
|
1572
|
-
const sidecarStartedAt = Date.now();
|
|
1573
|
-
s.currentUnit = {
|
|
1574
|
-
type: item.unitType,
|
|
1575
|
-
id: item.unitId,
|
|
1576
|
-
startedAt: sidecarStartedAt,
|
|
1577
|
-
};
|
|
1578
|
-
deps.writeUnitRuntimeRecord(
|
|
1579
|
-
s.basePath,
|
|
1580
|
-
item.unitType,
|
|
1581
|
-
item.unitId,
|
|
1582
|
-
sidecarStartedAt,
|
|
1583
|
-
{
|
|
1584
|
-
phase: "dispatched",
|
|
1585
|
-
wrapupWarningSent: false,
|
|
1586
|
-
timeoutAt: null,
|
|
1587
|
-
lastProgressAt: sidecarStartedAt,
|
|
1588
|
-
progressCount: 0,
|
|
1589
|
-
lastProgressKind: "dispatch",
|
|
1590
|
-
},
|
|
1591
|
-
);
|
|
1592
|
-
|
|
1593
|
-
// Model selection (handles hook model override)
|
|
1594
|
-
await deps.selectAndApplyModel(
|
|
1595
|
-
ctx,
|
|
1596
|
-
pi,
|
|
1597
|
-
item.unitType,
|
|
1598
|
-
item.unitId,
|
|
1599
|
-
s.basePath,
|
|
1600
|
-
prefs,
|
|
1601
|
-
s.verbose,
|
|
1602
|
-
s.autoModeStartModel,
|
|
1603
|
-
);
|
|
1604
|
-
|
|
1605
|
-
// Supervision
|
|
1606
|
-
deps.clearUnitTimeout();
|
|
1607
|
-
deps.startUnitSupervision({
|
|
1608
|
-
s,
|
|
1609
|
-
ctx,
|
|
1610
|
-
pi,
|
|
1611
|
-
unitType: item.unitType,
|
|
1612
|
-
unitId: item.unitId,
|
|
1613
|
-
prefs,
|
|
1614
|
-
buildSnapshotOpts: () =>
|
|
1615
|
-
deps.buildSnapshotOpts(item.unitType, item.unitId),
|
|
1616
|
-
buildRecoveryContext: () => ({}),
|
|
1617
|
-
pauseAuto: deps.pauseAuto,
|
|
1618
|
-
});
|
|
1619
|
-
|
|
1620
|
-
// Write lock
|
|
1621
|
-
const sidecarSessionFile = deps.getSessionFile(ctx);
|
|
1622
|
-
deps.writeLock(
|
|
1623
|
-
deps.lockBase(),
|
|
1624
|
-
item.unitType,
|
|
1625
|
-
item.unitId,
|
|
1626
|
-
s.completedUnits.length,
|
|
1627
|
-
sidecarSessionFile,
|
|
1628
|
-
);
|
|
1629
|
-
|
|
1630
|
-
// Execute via standard runUnit
|
|
1631
|
-
const sidecarResult = await runUnit(
|
|
1632
|
-
ctx,
|
|
1633
|
-
pi,
|
|
1634
|
-
s,
|
|
1635
|
-
item.unitType,
|
|
1636
|
-
item.unitId,
|
|
1637
|
-
item.prompt,
|
|
1638
|
-
prefs,
|
|
1639
|
-
);
|
|
1640
|
-
deps.clearUnitTimeout();
|
|
1641
|
-
|
|
1642
|
-
if (sidecarResult.status === "cancelled") {
|
|
1643
|
-
ctx.ui.notify(
|
|
1644
|
-
`Sidecar unit ${item.unitType} ${item.unitId} session cancelled. Stopping.`,
|
|
1645
|
-
"warning",
|
|
1646
|
-
);
|
|
1647
|
-
await deps.stopAuto(ctx, pi, "Sidecar session creation failed");
|
|
1648
|
-
sidecarBroke = true;
|
|
1649
|
-
break;
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
// Run pre-verification for the sidecar unit
|
|
1653
|
-
const sidecarPreResult =
|
|
1654
|
-
await deps.postUnitPreVerification(postUnitCtx);
|
|
1655
|
-
if (sidecarPreResult === "dispatched") {
|
|
1656
|
-
// Pre-verification caused stop/pause
|
|
1657
|
-
debugLog("autoLoop", {
|
|
1658
|
-
phase: "exit",
|
|
1659
|
-
reason: "sidecar-pre-verification-stop",
|
|
1660
|
-
});
|
|
1661
|
-
sidecarBroke = true;
|
|
1662
|
-
break;
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
// Verification gate for non-hook sidecar units (triage, quick-tasks)
|
|
1666
|
-
// Hook units are lightweight and don't need verification.
|
|
1667
|
-
if (item.kind !== "hook") {
|
|
1668
|
-
const sidecarVerification = await deps.runPostUnitVerification(
|
|
1669
|
-
{ s, ctx, pi },
|
|
1670
|
-
deps.pauseAuto,
|
|
1671
|
-
);
|
|
1672
|
-
if (sidecarVerification === "pause") {
|
|
1673
|
-
debugLog("autoLoop", {
|
|
1674
|
-
phase: "exit",
|
|
1675
|
-
reason: "sidecar-verification-pause",
|
|
1676
|
-
});
|
|
1677
|
-
sidecarBroke = true;
|
|
1678
|
-
break;
|
|
1679
|
-
}
|
|
1680
|
-
// "retry" for sidecars — skip retry, just continue (sidecar retries are not worth the complexity)
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
// Post-verification (may enqueue more sidecar items)
|
|
1684
|
-
const sidecarPostResult =
|
|
1685
|
-
await deps.postUnitPostVerification(postUnitCtx);
|
|
1686
|
-
if (sidecarPostResult === "stopped") {
|
|
1687
|
-
debugLog("autoLoop", { phase: "exit", reason: "sidecar-stopped" });
|
|
1688
|
-
sidecarBroke = true;
|
|
1689
|
-
break;
|
|
1690
|
-
}
|
|
1691
|
-
if (sidecarPostResult === "step-wizard") {
|
|
1692
|
-
debugLog("autoLoop", {
|
|
1693
|
-
phase: "exit",
|
|
1694
|
-
reason: "sidecar-step-wizard",
|
|
1695
|
-
});
|
|
1696
|
-
sidecarBroke = true;
|
|
1697
|
-
break;
|
|
1698
|
-
}
|
|
1699
|
-
// "continue" — loop checks sidecarQueue again
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
if (sidecarBroke) break;
|
|
1703
|
-
|
|
1704
1711
|
consecutiveErrors = 0; // Iteration completed successfully
|
|
1705
1712
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|
|
1706
1713
|
} catch (loopErr) {
|
|
@@ -1740,6 +1747,6 @@ export async function autoLoop(
|
|
|
1740
1747
|
}
|
|
1741
1748
|
}
|
|
1742
1749
|
|
|
1743
|
-
|
|
1750
|
+
_currentResolve = null;
|
|
1744
1751
|
debugLog("autoLoop", { phase: "exit", totalIterations: iteration });
|
|
1745
1752
|
}
|