gsd-pi 2.38.0-dev.96dc7fb → 2.38.0-dev.98b44dc
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/extension-registry.js +2 -2
- package/dist/remote-questions-config.js +2 -2
- package/dist/resource-loader.js +34 -1
- package/dist/resources/extensions/browser-tools/index.js +3 -1
- package/dist/resources/extensions/browser-tools/tools/verify.js +97 -0
- 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/gsd/auto/session.js +6 -23
- package/dist/resources/extensions/gsd/auto-dispatch.js +8 -9
- package/dist/resources/extensions/gsd/auto-loop.js +636 -594
- 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 +7 -1
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +2 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
- package/dist/resources/extensions/gsd/auto.js +143 -96
- package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +4 -2
- 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-providers.js +30 -11
- package/dist/resources/extensions/gsd/doctor.js +20 -1
- 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/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/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/isolation.js +2 -1
- package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
- 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/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/src/core/extensions/loader.ts +223 -7
- package/packages/pi-coding-agent/src/core/skills.ts +9 -1
- package/packages/pi-coding-agent/src/index.ts +1 -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/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 +526 -545
- 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 +11 -1
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +3 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
- package/src/resources/extensions/gsd/auto.ts +139 -101
- package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +5 -3
- 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-providers.ts +30 -9
- package/src/resources/extensions/gsd/doctor.ts +22 -1
- 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/derive-state.test.ts +43 -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/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/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/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).
|
|
@@ -383,6 +420,7 @@ export interface LoopDeps {
|
|
|
383
420
|
midTitle: string;
|
|
384
421
|
state: GSDState;
|
|
385
422
|
prefs: GSDPreferences | undefined;
|
|
423
|
+
session?: AutoSession;
|
|
386
424
|
}) => Promise<DispatchAction>;
|
|
387
425
|
runPreDispatchHooks: (
|
|
388
426
|
unitType: string,
|
|
@@ -500,6 +538,7 @@ export interface LoopDeps {
|
|
|
500
538
|
// Post-unit processing
|
|
501
539
|
postUnitPreVerification: (
|
|
502
540
|
pctx: PostUnitContext,
|
|
541
|
+
opts?: PreVerificationOpts,
|
|
503
542
|
) => Promise<"dispatched" | "continue">;
|
|
504
543
|
runPostUnitVerification: (
|
|
505
544
|
vctx: VerificationContext,
|
|
@@ -513,6 +552,96 @@ export interface LoopDeps {
|
|
|
513
552
|
getSessionFile: (ctx: ExtensionContext) => string;
|
|
514
553
|
}
|
|
515
554
|
|
|
555
|
+
// ─── generateMilestoneReport ──────────────────────────────────────────────────
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Generate and write an HTML milestone report snapshot.
|
|
559
|
+
* Extracted from the milestone-transition block in autoLoop.
|
|
560
|
+
*/
|
|
561
|
+
async function generateMilestoneReport(
|
|
562
|
+
s: AutoSession,
|
|
563
|
+
ctx: ExtensionContext,
|
|
564
|
+
milestoneId: string,
|
|
565
|
+
): Promise<void> {
|
|
566
|
+
const { loadVisualizerData } = await importExtensionModule<typeof import("./visualizer-data.js")>(import.meta.url, "./visualizer-data.js");
|
|
567
|
+
const { generateHtmlReport } = await importExtensionModule<typeof import("./export-html.js")>(import.meta.url, "./export-html.js");
|
|
568
|
+
const { writeReportSnapshot } = await importExtensionModule<typeof import("./reports.js")>(import.meta.url, "./reports.js");
|
|
569
|
+
const { basename } = await import("node:path");
|
|
570
|
+
|
|
571
|
+
const snapData = await loadVisualizerData(s.basePath);
|
|
572
|
+
const completedMs = snapData.milestones.find(
|
|
573
|
+
(m: { id: string }) => m.id === milestoneId,
|
|
574
|
+
);
|
|
575
|
+
const msTitle = completedMs?.title ?? milestoneId;
|
|
576
|
+
const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
|
|
577
|
+
const projName = basename(s.basePath);
|
|
578
|
+
const doneSlices = snapData.milestones.reduce(
|
|
579
|
+
(acc: number, m: { slices: { done: boolean }[] }) =>
|
|
580
|
+
acc + m.slices.filter((sl: { done: boolean }) => sl.done).length,
|
|
581
|
+
0,
|
|
582
|
+
);
|
|
583
|
+
const totalSlices = snapData.milestones.reduce(
|
|
584
|
+
(acc: number, m: { slices: unknown[] }) => acc + m.slices.length,
|
|
585
|
+
0,
|
|
586
|
+
);
|
|
587
|
+
const outPath = writeReportSnapshot({
|
|
588
|
+
basePath: s.basePath,
|
|
589
|
+
html: generateHtmlReport(snapData, {
|
|
590
|
+
projectName: projName,
|
|
591
|
+
projectPath: s.basePath,
|
|
592
|
+
gsdVersion,
|
|
593
|
+
milestoneId,
|
|
594
|
+
indexRelPath: "index.html",
|
|
595
|
+
}),
|
|
596
|
+
milestoneId,
|
|
597
|
+
milestoneTitle: msTitle,
|
|
598
|
+
kind: "milestone",
|
|
599
|
+
projectName: projName,
|
|
600
|
+
projectPath: s.basePath,
|
|
601
|
+
gsdVersion,
|
|
602
|
+
totalCost: snapData.totals?.cost ?? 0,
|
|
603
|
+
totalTokens: snapData.totals?.tokens.total ?? 0,
|
|
604
|
+
totalDuration: snapData.totals?.duration ?? 0,
|
|
605
|
+
doneSlices,
|
|
606
|
+
totalSlices,
|
|
607
|
+
doneMilestones: snapData.milestones.filter(
|
|
608
|
+
(m: { status: string }) => m.status === "complete",
|
|
609
|
+
).length,
|
|
610
|
+
totalMilestones: snapData.milestones.length,
|
|
611
|
+
phase: snapData.phase,
|
|
612
|
+
});
|
|
613
|
+
ctx.ui.notify(
|
|
614
|
+
`Report saved: .gsd/reports/${basename(outPath)} — open index.html to browse progression.`,
|
|
615
|
+
"info",
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ─── closeoutAndStop ──────────────────────────────────────────────────────────
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* If a unit is in-flight, close it out, then stop auto-mode.
|
|
623
|
+
* Extracted from ~4 identical if-closeout-then-stop sequences in autoLoop.
|
|
624
|
+
*/
|
|
625
|
+
async function closeoutAndStop(
|
|
626
|
+
ctx: ExtensionContext,
|
|
627
|
+
pi: ExtensionAPI,
|
|
628
|
+
s: AutoSession,
|
|
629
|
+
deps: LoopDeps,
|
|
630
|
+
reason: string,
|
|
631
|
+
): Promise<void> {
|
|
632
|
+
if (s.currentUnit) {
|
|
633
|
+
await deps.closeoutUnit(
|
|
634
|
+
ctx,
|
|
635
|
+
s.basePath,
|
|
636
|
+
s.currentUnit.type,
|
|
637
|
+
s.currentUnit.id,
|
|
638
|
+
s.currentUnit.startedAt,
|
|
639
|
+
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
await deps.stopAuto(ctx, pi, reason);
|
|
643
|
+
}
|
|
644
|
+
|
|
516
645
|
// ─── autoLoop ────────────────────────────────────────────────────────────────
|
|
517
646
|
|
|
518
647
|
/**
|
|
@@ -530,10 +659,11 @@ export async function autoLoop(
|
|
|
530
659
|
deps: LoopDeps,
|
|
531
660
|
): Promise<void> {
|
|
532
661
|
debugLog("autoLoop", { phase: "enter" });
|
|
533
|
-
_activeSession = s;
|
|
534
662
|
let iteration = 0;
|
|
535
|
-
|
|
536
|
-
|
|
663
|
+
// ── Sliding-window stuck detection ──
|
|
664
|
+
const recentUnits: Array<{ key: string; error?: string }> = [];
|
|
665
|
+
const STUCK_WINDOW_SIZE = 6;
|
|
666
|
+
let stuckRecoveryAttempts = 0;
|
|
537
667
|
|
|
538
668
|
let consecutiveErrors = 0;
|
|
539
669
|
|
|
@@ -562,6 +692,19 @@ export async function autoLoop(
|
|
|
562
692
|
|
|
563
693
|
try {
|
|
564
694
|
// ── Blanket try/catch: one bad iteration must not kill the session
|
|
695
|
+
const prefs = deps.loadEffectiveGSDPreferences()?.preferences;
|
|
696
|
+
|
|
697
|
+
// ── Check sidecar queue before deriveState ──
|
|
698
|
+
let sidecarItem: SidecarItem | undefined;
|
|
699
|
+
if (s.sidecarQueue.length > 0) {
|
|
700
|
+
sidecarItem = s.sidecarQueue.shift()!;
|
|
701
|
+
debugLog("autoLoop", {
|
|
702
|
+
phase: "sidecar-dequeue",
|
|
703
|
+
kind: sidecarItem.kind,
|
|
704
|
+
unitType: sidecarItem.unitType,
|
|
705
|
+
unitId: sidecarItem.unitId,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
565
708
|
|
|
566
709
|
const sessionLockBase = deps.lockBase();
|
|
567
710
|
if (sessionLockBase) {
|
|
@@ -583,6 +726,17 @@ export async function autoLoop(
|
|
|
583
726
|
}
|
|
584
727
|
}
|
|
585
728
|
|
|
729
|
+
// Variables shared between the sidecar and normal paths
|
|
730
|
+
let unitType: string;
|
|
731
|
+
let unitId: string;
|
|
732
|
+
let prompt: string;
|
|
733
|
+
let pauseAfterUatDispatch = false;
|
|
734
|
+
let state: GSDState;
|
|
735
|
+
let mid: string | undefined;
|
|
736
|
+
let midTitle: string | undefined;
|
|
737
|
+
let observabilityIssues: unknown[] = [];
|
|
738
|
+
|
|
739
|
+
if (!sidecarItem) {
|
|
586
740
|
// ── Phase 1: Pre-dispatch ───────────────────────────────────────────
|
|
587
741
|
|
|
588
742
|
// Resource version guard
|
|
@@ -633,10 +787,10 @@ export async function autoLoop(
|
|
|
633
787
|
}
|
|
634
788
|
|
|
635
789
|
// Derive state
|
|
636
|
-
|
|
637
|
-
deps.syncCmuxSidebar(
|
|
638
|
-
|
|
639
|
-
|
|
790
|
+
state = await deps.deriveState(s.basePath);
|
|
791
|
+
deps.syncCmuxSidebar(prefs, state);
|
|
792
|
+
mid = state.activeMilestone?.id;
|
|
793
|
+
midTitle = state.activeMilestone?.title;
|
|
640
794
|
debugLog("autoLoop", {
|
|
641
795
|
phase: "state-derived",
|
|
642
796
|
iteration,
|
|
@@ -657,68 +811,18 @@ export async function autoLoop(
|
|
|
657
811
|
"milestone",
|
|
658
812
|
);
|
|
659
813
|
deps.logCmuxEvent(
|
|
660
|
-
|
|
814
|
+
prefs,
|
|
661
815
|
`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`,
|
|
662
816
|
"success",
|
|
663
817
|
);
|
|
664
818
|
|
|
665
|
-
const vizPrefs =
|
|
819
|
+
const vizPrefs = prefs;
|
|
666
820
|
if (vizPrefs?.auto_visualize) {
|
|
667
821
|
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
668
822
|
}
|
|
669
823
|
if (vizPrefs?.auto_report !== false) {
|
|
670
824
|
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
|
-
);
|
|
825
|
+
await generateMilestoneReport(s, ctx, s.currentMilestoneId!);
|
|
722
826
|
} catch (err) {
|
|
723
827
|
ctx.ui.notify(
|
|
724
828
|
`Report generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -731,11 +835,30 @@ export async function autoLoop(
|
|
|
731
835
|
s.unitDispatchCount.clear();
|
|
732
836
|
s.unitRecoveryCount.clear();
|
|
733
837
|
s.unitLifetimeDispatches.clear();
|
|
734
|
-
|
|
735
|
-
|
|
838
|
+
recentUnits.length = 0;
|
|
839
|
+
stuckRecoveryAttempts = 0;
|
|
736
840
|
|
|
737
841
|
// Worktree lifecycle on milestone transition — merge current, enter next
|
|
738
842
|
deps.resolver.mergeAndExit(s.currentMilestoneId!, ctx.ui);
|
|
843
|
+
|
|
844
|
+
// Opt-in: create draft PR on milestone completion
|
|
845
|
+
if (prefs?.git?.auto_pr) {
|
|
846
|
+
try {
|
|
847
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
848
|
+
const prUrl = createDraftPR(
|
|
849
|
+
s.basePath,
|
|
850
|
+
s.currentMilestoneId!,
|
|
851
|
+
`[GSD] ${s.currentMilestoneId} complete`,
|
|
852
|
+
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
853
|
+
);
|
|
854
|
+
if (prUrl) {
|
|
855
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
856
|
+
}
|
|
857
|
+
} catch {
|
|
858
|
+
// Non-fatal — PR creation is best-effort
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
739
862
|
deps.invalidateAllCaches();
|
|
740
863
|
|
|
741
864
|
state = await deps.deriveState(s.basePath);
|
|
@@ -745,9 +868,7 @@ export async function autoLoop(
|
|
|
745
868
|
if (mid) {
|
|
746
869
|
if (deps.getIsolationMode() !== "none") {
|
|
747
870
|
deps.captureIntegrationBranch(s.basePath, mid, {
|
|
748
|
-
commitDocs:
|
|
749
|
-
deps.loadEffectiveGSDPreferences()?.preferences?.git
|
|
750
|
-
?.commit_docs,
|
|
871
|
+
commitDocs: prefs?.git?.commit_docs,
|
|
751
872
|
});
|
|
752
873
|
}
|
|
753
874
|
deps.resolver.enterMilestone(mid, ctx.ui);
|
|
@@ -791,6 +912,24 @@ export async function autoLoop(
|
|
|
791
912
|
// All milestones complete — merge milestone branch before stopping
|
|
792
913
|
if (s.currentMilestoneId) {
|
|
793
914
|
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
915
|
+
|
|
916
|
+
// Opt-in: create draft PR on milestone completion
|
|
917
|
+
if (prefs?.git?.auto_pr) {
|
|
918
|
+
try {
|
|
919
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
920
|
+
const prUrl = createDraftPR(
|
|
921
|
+
s.basePath,
|
|
922
|
+
s.currentMilestoneId,
|
|
923
|
+
`[GSD] ${s.currentMilestoneId} complete`,
|
|
924
|
+
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
925
|
+
);
|
|
926
|
+
if (prUrl) {
|
|
927
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
928
|
+
}
|
|
929
|
+
} catch {
|
|
930
|
+
// Non-fatal — PR creation is best-effort
|
|
931
|
+
}
|
|
932
|
+
}
|
|
794
933
|
}
|
|
795
934
|
deps.sendDesktopNotification(
|
|
796
935
|
"GSD",
|
|
@@ -799,7 +938,7 @@ export async function autoLoop(
|
|
|
799
938
|
"milestone",
|
|
800
939
|
);
|
|
801
940
|
deps.logCmuxEvent(
|
|
802
|
-
|
|
941
|
+
prefs,
|
|
803
942
|
"All milestones complete.",
|
|
804
943
|
"success",
|
|
805
944
|
);
|
|
@@ -821,7 +960,7 @@ export async function autoLoop(
|
|
|
821
960
|
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
822
961
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
823
962
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
824
|
-
deps.logCmuxEvent(
|
|
963
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
825
964
|
} else {
|
|
826
965
|
const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
|
|
827
966
|
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
@@ -856,20 +995,10 @@ export async function autoLoop(
|
|
|
856
995
|
}
|
|
857
996
|
|
|
858
997
|
if (!mid || !midTitle) {
|
|
859
|
-
if (s.currentUnit) {
|
|
860
|
-
await deps.closeoutUnit(
|
|
861
|
-
ctx,
|
|
862
|
-
s.basePath,
|
|
863
|
-
s.currentUnit.type,
|
|
864
|
-
s.currentUnit.id,
|
|
865
|
-
s.currentUnit.startedAt,
|
|
866
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
867
|
-
);
|
|
868
|
-
}
|
|
869
998
|
const noMilestoneReason = !mid
|
|
870
999
|
? "No active milestone after merge reconciliation"
|
|
871
1000
|
: `Milestone ${mid} has no title after reconciliation`;
|
|
872
|
-
await
|
|
1001
|
+
await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
|
|
873
1002
|
debugLog("autoLoop", {
|
|
874
1003
|
phase: "exit",
|
|
875
1004
|
reason: "no-milestone-after-reconciliation",
|
|
@@ -879,19 +1008,27 @@ export async function autoLoop(
|
|
|
879
1008
|
|
|
880
1009
|
// Terminal: complete
|
|
881
1010
|
if (state.phase === "complete") {
|
|
882
|
-
|
|
883
|
-
await deps.closeoutUnit(
|
|
884
|
-
ctx,
|
|
885
|
-
s.basePath,
|
|
886
|
-
s.currentUnit.type,
|
|
887
|
-
s.currentUnit.id,
|
|
888
|
-
s.currentUnit.startedAt,
|
|
889
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
890
|
-
);
|
|
891
|
-
}
|
|
892
|
-
// Milestone merge on complete
|
|
1011
|
+
// Milestone merge on complete (before closeout so branch state is clean)
|
|
893
1012
|
if (s.currentMilestoneId) {
|
|
894
1013
|
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
1014
|
+
|
|
1015
|
+
// Opt-in: create draft PR on milestone completion
|
|
1016
|
+
if (prefs?.git?.auto_pr) {
|
|
1017
|
+
try {
|
|
1018
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
1019
|
+
const prUrl = createDraftPR(
|
|
1020
|
+
s.basePath,
|
|
1021
|
+
s.currentMilestoneId,
|
|
1022
|
+
`[GSD] ${s.currentMilestoneId} complete`,
|
|
1023
|
+
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
1024
|
+
);
|
|
1025
|
+
if (prUrl) {
|
|
1026
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
1027
|
+
}
|
|
1028
|
+
} catch {
|
|
1029
|
+
// Non-fatal — PR creation is best-effort
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
895
1032
|
}
|
|
896
1033
|
deps.sendDesktopNotification(
|
|
897
1034
|
"GSD",
|
|
@@ -900,40 +1037,28 @@ export async function autoLoop(
|
|
|
900
1037
|
"milestone",
|
|
901
1038
|
);
|
|
902
1039
|
deps.logCmuxEvent(
|
|
903
|
-
|
|
1040
|
+
prefs,
|
|
904
1041
|
`Milestone ${mid} complete.`,
|
|
905
1042
|
"success",
|
|
906
1043
|
);
|
|
907
|
-
await
|
|
1044
|
+
await closeoutAndStop(ctx, pi, s, deps, `Milestone ${mid} complete`);
|
|
908
1045
|
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
909
1046
|
break;
|
|
910
1047
|
}
|
|
911
1048
|
|
|
912
1049
|
// Terminal: blocked
|
|
913
1050
|
if (state.phase === "blocked") {
|
|
914
|
-
if (s.currentUnit) {
|
|
915
|
-
await deps.closeoutUnit(
|
|
916
|
-
ctx,
|
|
917
|
-
s.basePath,
|
|
918
|
-
s.currentUnit.type,
|
|
919
|
-
s.currentUnit.id,
|
|
920
|
-
s.currentUnit.startedAt,
|
|
921
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
922
|
-
);
|
|
923
|
-
}
|
|
924
1051
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
925
|
-
await
|
|
1052
|
+
await closeoutAndStop(ctx, pi, s, deps, blockerMsg);
|
|
926
1053
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
927
1054
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
928
|
-
deps.logCmuxEvent(
|
|
1055
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
929
1056
|
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
930
1057
|
break;
|
|
931
1058
|
}
|
|
932
1059
|
|
|
933
1060
|
// ── Phase 2: Guards ─────────────────────────────────────────────────
|
|
934
1061
|
|
|
935
|
-
const prefs = deps.loadEffectiveGSDPreferences()?.preferences;
|
|
936
|
-
|
|
937
1062
|
// Budget ceiling guard
|
|
938
1063
|
const budgetCeiling = prefs?.budget_ceiling;
|
|
939
1064
|
if (budgetCeiling !== undefined && budgetCeiling > 0) {
|
|
@@ -953,84 +1078,49 @@ export async function autoLoop(
|
|
|
953
1078
|
budgetPct,
|
|
954
1079
|
);
|
|
955
1080
|
|
|
956
|
-
|
|
957
|
-
|
|
1081
|
+
// Data-driven threshold check — loop descending, fire first match
|
|
1082
|
+
const threshold = BUDGET_THRESHOLDS.find(
|
|
1083
|
+
(t) => newBudgetAlertLevel >= t.pct,
|
|
1084
|
+
);
|
|
1085
|
+
if (threshold) {
|
|
958
1086
|
s.lastBudgetAlertLevel =
|
|
959
1087
|
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
)
|
|
1088
|
+
|
|
1089
|
+
if (threshold.pct === 100 && budgetEnforcementAction !== "none") {
|
|
1090
|
+
// 100% — special enforcement logic (halt/pause/warn)
|
|
1091
|
+
const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
|
|
1092
|
+
if (budgetEnforcementAction === "halt") {
|
|
1093
|
+
deps.sendDesktopNotification("GSD", msg, "error", "budget");
|
|
1094
|
+
await deps.stopAuto(ctx, pi, "Budget ceiling reached");
|
|
1095
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
|
|
1096
|
+
break;
|
|
1097
|
+
}
|
|
1098
|
+
if (budgetEnforcementAction === "pause") {
|
|
1099
|
+
ctx.ui.notify(
|
|
1100
|
+
`${msg} Pausing auto-mode — /gsd auto to override and continue.`,
|
|
1101
|
+
"warning",
|
|
1102
|
+
);
|
|
1103
|
+
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
1104
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
1105
|
+
await deps.pauseAuto(ctx, pi);
|
|
1106
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
1107
|
+
break;
|
|
1108
|
+
}
|
|
1109
|
+
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
971
1110
|
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
972
1111
|
deps.logCmuxEvent(prefs, msg, "warning");
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1112
|
+
} else if (threshold.pct < 100) {
|
|
1113
|
+
// Sub-100% — simple notification
|
|
1114
|
+
const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
|
|
1115
|
+
ctx.ui.notify(msg, threshold.notifyLevel);
|
|
1116
|
+
deps.sendDesktopNotification(
|
|
1117
|
+
"GSD",
|
|
1118
|
+
msg,
|
|
1119
|
+
threshold.notifyLevel,
|
|
1120
|
+
"budget",
|
|
1121
|
+
);
|
|
1122
|
+
deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
|
|
976
1123
|
}
|
|
977
|
-
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
978
|
-
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
979
|
-
deps.logCmuxEvent(prefs, msg, "warning");
|
|
980
|
-
} else if (newBudgetAlertLevel === 90) {
|
|
981
|
-
s.lastBudgetAlertLevel =
|
|
982
|
-
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
983
|
-
ctx.ui.notify(
|
|
984
|
-
`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
985
|
-
"warning",
|
|
986
|
-
);
|
|
987
|
-
deps.sendDesktopNotification(
|
|
988
|
-
"GSD",
|
|
989
|
-
`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
990
|
-
"warning",
|
|
991
|
-
"budget",
|
|
992
|
-
);
|
|
993
|
-
deps.logCmuxEvent(
|
|
994
|
-
prefs,
|
|
995
|
-
`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
996
|
-
"warning",
|
|
997
|
-
);
|
|
998
|
-
} else if (newBudgetAlertLevel === 80) {
|
|
999
|
-
s.lastBudgetAlertLevel =
|
|
1000
|
-
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
1001
|
-
ctx.ui.notify(
|
|
1002
|
-
`Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1003
|
-
"warning",
|
|
1004
|
-
);
|
|
1005
|
-
deps.sendDesktopNotification(
|
|
1006
|
-
"GSD",
|
|
1007
|
-
`Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1008
|
-
"warning",
|
|
1009
|
-
"budget",
|
|
1010
|
-
);
|
|
1011
|
-
deps.logCmuxEvent(
|
|
1012
|
-
prefs,
|
|
1013
|
-
`Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1014
|
-
"warning",
|
|
1015
|
-
);
|
|
1016
|
-
} else if (newBudgetAlertLevel === 75) {
|
|
1017
|
-
s.lastBudgetAlertLevel =
|
|
1018
|
-
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
1019
|
-
ctx.ui.notify(
|
|
1020
|
-
`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1021
|
-
"info",
|
|
1022
|
-
);
|
|
1023
|
-
deps.sendDesktopNotification(
|
|
1024
|
-
"GSD",
|
|
1025
|
-
`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1026
|
-
"info",
|
|
1027
|
-
"budget",
|
|
1028
|
-
);
|
|
1029
|
-
deps.logCmuxEvent(
|
|
1030
|
-
prefs,
|
|
1031
|
-
`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1032
|
-
"progress",
|
|
1033
|
-
);
|
|
1034
1124
|
} else if (budgetAlertLevel === 0) {
|
|
1035
1125
|
s.lastBudgetAlertLevel = 0;
|
|
1036
1126
|
}
|
|
@@ -1103,20 +1193,11 @@ export async function autoLoop(
|
|
|
1103
1193
|
midTitle: midTitle!,
|
|
1104
1194
|
state,
|
|
1105
1195
|
prefs,
|
|
1196
|
+
session: s,
|
|
1106
1197
|
});
|
|
1107
1198
|
|
|
1108
1199
|
if (dispatchResult.action === "stop") {
|
|
1109
|
-
|
|
1110
|
-
await deps.closeoutUnit(
|
|
1111
|
-
ctx,
|
|
1112
|
-
s.basePath,
|
|
1113
|
-
s.currentUnit.type,
|
|
1114
|
-
s.currentUnit.id,
|
|
1115
|
-
s.currentUnit.startedAt,
|
|
1116
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
1117
|
-
);
|
|
1118
|
-
}
|
|
1119
|
-
await deps.stopAuto(ctx, pi, dispatchResult.reason);
|
|
1200
|
+
await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
|
|
1120
1201
|
debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
|
|
1121
1202
|
break;
|
|
1122
1203
|
}
|
|
@@ -1127,76 +1208,84 @@ export async function autoLoop(
|
|
|
1127
1208
|
continue;
|
|
1128
1209
|
}
|
|
1129
1210
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1211
|
+
unitType = dispatchResult.unitType;
|
|
1212
|
+
unitId = dispatchResult.unitId;
|
|
1213
|
+
prompt = dispatchResult.prompt;
|
|
1214
|
+
pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
1134
1215
|
|
|
1135
|
-
// ──
|
|
1216
|
+
// ── Sliding-window stuck detection with graduated recovery ──
|
|
1136
1217
|
const derivedKey = `${unitType}/${unitId}`;
|
|
1137
|
-
if (derivedKey === lastDerivedUnit && !s.pendingVerificationRetry) {
|
|
1138
|
-
sameUnitCount++;
|
|
1139
|
-
debugLog("autoLoop", {
|
|
1140
|
-
phase: "stuck-check",
|
|
1141
|
-
unitType,
|
|
1142
|
-
unitId,
|
|
1143
|
-
sameUnitCount,
|
|
1144
|
-
});
|
|
1145
1218
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1219
|
+
if (!s.pendingVerificationRetry) {
|
|
1220
|
+
recentUnits.push({ key: derivedKey });
|
|
1221
|
+
if (recentUnits.length > STUCK_WINDOW_SIZE) recentUnits.shift();
|
|
1222
|
+
|
|
1223
|
+
const stuckSignal = detectStuck(recentUnits);
|
|
1224
|
+
if (stuckSignal) {
|
|
1225
|
+
debugLog("autoLoop", {
|
|
1226
|
+
phase: "stuck-check",
|
|
1149
1227
|
unitType,
|
|
1150
1228
|
unitId,
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1229
|
+
reason: stuckSignal.reason,
|
|
1230
|
+
recoveryAttempts: stuckRecoveryAttempts,
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
if (stuckRecoveryAttempts === 0) {
|
|
1234
|
+
// Level 1: try verifying the artifact, then cache invalidation + retry
|
|
1235
|
+
stuckRecoveryAttempts++;
|
|
1236
|
+
const artifactExists = deps.verifyExpectedArtifact(
|
|
1237
|
+
unitType,
|
|
1238
|
+
unitId,
|
|
1239
|
+
s.basePath,
|
|
1240
|
+
);
|
|
1241
|
+
if (artifactExists) {
|
|
1242
|
+
debugLog("autoLoop", {
|
|
1243
|
+
phase: "stuck-recovery",
|
|
1244
|
+
level: 1,
|
|
1245
|
+
action: "artifact-found",
|
|
1246
|
+
});
|
|
1247
|
+
ctx.ui.notify(
|
|
1248
|
+
`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`,
|
|
1249
|
+
"info",
|
|
1250
|
+
);
|
|
1251
|
+
deps.invalidateAllCaches();
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
ctx.ui.notify(
|
|
1255
|
+
`Stuck on ${unitType} ${unitId} (${stuckSignal.reason}). Invalidating caches and retrying.`,
|
|
1256
|
+
"warning",
|
|
1257
|
+
);
|
|
1258
|
+
deps.invalidateAllCaches();
|
|
1259
|
+
} else {
|
|
1260
|
+
// Level 2: hard stop — genuinely stuck
|
|
1154
1261
|
debugLog("autoLoop", {
|
|
1155
|
-
phase: "stuck-
|
|
1156
|
-
|
|
1157
|
-
|
|
1262
|
+
phase: "stuck-detected",
|
|
1263
|
+
unitType,
|
|
1264
|
+
unitId,
|
|
1265
|
+
reason: stuckSignal.reason,
|
|
1158
1266
|
});
|
|
1267
|
+
await deps.stopAuto(
|
|
1268
|
+
ctx,
|
|
1269
|
+
pi,
|
|
1270
|
+
`Stuck: ${stuckSignal.reason}`,
|
|
1271
|
+
);
|
|
1159
1272
|
ctx.ui.notify(
|
|
1160
|
-
`Stuck
|
|
1161
|
-
"
|
|
1273
|
+
`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}. The expected artifact was not written.`,
|
|
1274
|
+
"error",
|
|
1162
1275
|
);
|
|
1163
|
-
|
|
1164
|
-
|
|
1276
|
+
break;
|
|
1277
|
+
}
|
|
1278
|
+
} else {
|
|
1279
|
+
// Progress detected — reset recovery counter
|
|
1280
|
+
if (stuckRecoveryAttempts > 0) {
|
|
1281
|
+
debugLog("autoLoop", {
|
|
1282
|
+
phase: "stuck-counter-reset",
|
|
1283
|
+
from: recentUnits[recentUnits.length - 2]?.key ?? "",
|
|
1284
|
+
to: derivedKey,
|
|
1285
|
+
});
|
|
1286
|
+
stuckRecoveryAttempts = 0;
|
|
1165
1287
|
}
|
|
1166
|
-
ctx.ui.notify(
|
|
1167
|
-
`Stuck on ${unitType} ${unitId} (attempt ${sameUnitCount}). Invalidating caches and retrying.`,
|
|
1168
|
-
"warning",
|
|
1169
|
-
);
|
|
1170
|
-
deps.invalidateAllCaches();
|
|
1171
|
-
} else if (sameUnitCount === 5) {
|
|
1172
|
-
// Level 2: hard stop — genuinely stuck
|
|
1173
|
-
debugLog("autoLoop", {
|
|
1174
|
-
phase: "stuck-detected",
|
|
1175
|
-
unitType,
|
|
1176
|
-
unitId,
|
|
1177
|
-
sameUnitCount,
|
|
1178
|
-
});
|
|
1179
|
-
await deps.stopAuto(
|
|
1180
|
-
ctx,
|
|
1181
|
-
pi,
|
|
1182
|
-
`Stuck: ${unitType} ${unitId} derived ${sameUnitCount} consecutive times without progress`,
|
|
1183
|
-
);
|
|
1184
|
-
ctx.ui.notify(
|
|
1185
|
-
`Stuck on ${unitType} ${unitId} — deriveState returns the same unit after ${sameUnitCount} attempts. The expected artifact was not written.`,
|
|
1186
|
-
"error",
|
|
1187
|
-
);
|
|
1188
|
-
break;
|
|
1189
|
-
}
|
|
1190
|
-
} else {
|
|
1191
|
-
if (derivedKey !== lastDerivedUnit) {
|
|
1192
|
-
debugLog("autoLoop", {
|
|
1193
|
-
phase: "stuck-counter-reset",
|
|
1194
|
-
from: lastDerivedUnit,
|
|
1195
|
-
to: derivedKey,
|
|
1196
|
-
});
|
|
1197
1288
|
}
|
|
1198
|
-
lastDerivedUnit = derivedKey;
|
|
1199
|
-
sameUnitCount = 0;
|
|
1200
1289
|
}
|
|
1201
1290
|
|
|
1202
1291
|
// Pre-dispatch hooks
|
|
@@ -1239,13 +1328,27 @@ export async function autoLoop(
|
|
|
1239
1328
|
break;
|
|
1240
1329
|
}
|
|
1241
1330
|
|
|
1242
|
-
|
|
1331
|
+
observabilityIssues = await deps.collectObservabilityWarnings(
|
|
1243
1332
|
ctx,
|
|
1244
1333
|
s.basePath,
|
|
1245
1334
|
unitType,
|
|
1246
1335
|
unitId,
|
|
1247
1336
|
);
|
|
1248
1337
|
|
|
1338
|
+
// Derive state for shared use in execution phase
|
|
1339
|
+
// (state, mid, midTitle already set above)
|
|
1340
|
+
|
|
1341
|
+
} else {
|
|
1342
|
+
// ── Sidecar path: use values from the sidecar item directly ──
|
|
1343
|
+
unitType = sidecarItem.unitType;
|
|
1344
|
+
unitId = sidecarItem.unitId;
|
|
1345
|
+
prompt = sidecarItem.prompt;
|
|
1346
|
+
// Derive minimal state for progress widget / execution context
|
|
1347
|
+
state = await deps.deriveState(s.basePath);
|
|
1348
|
+
mid = state.activeMilestone?.id;
|
|
1349
|
+
midTitle = state.activeMilestone?.title;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1249
1352
|
// ── Phase 4: Unit execution ─────────────────────────────────────────
|
|
1250
1353
|
|
|
1251
1354
|
debugLog("autoLoop", {
|
|
@@ -1263,61 +1366,6 @@ export async function autoLoop(
|
|
|
1263
1366
|
);
|
|
1264
1367
|
const previousTier = s.currentUnitRouting?.tier;
|
|
1265
1368
|
|
|
1266
|
-
// Closeout previous unit
|
|
1267
|
-
if (s.currentUnit) {
|
|
1268
|
-
await deps.closeoutUnit(
|
|
1269
|
-
ctx,
|
|
1270
|
-
s.basePath,
|
|
1271
|
-
s.currentUnit.type,
|
|
1272
|
-
s.currentUnit.id,
|
|
1273
|
-
s.currentUnit.startedAt,
|
|
1274
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
1275
|
-
);
|
|
1276
|
-
|
|
1277
|
-
if (s.currentUnitRouting) {
|
|
1278
|
-
const isRetry =
|
|
1279
|
-
s.currentUnit.type === unitType && s.currentUnit.id === unitId;
|
|
1280
|
-
deps.recordOutcome(
|
|
1281
|
-
s.currentUnit.type,
|
|
1282
|
-
s.currentUnitRouting.tier as "light" | "standard" | "heavy",
|
|
1283
|
-
!isRetry,
|
|
1284
|
-
);
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
const closeoutKey = `${s.currentUnit.type}/${s.currentUnit.id}`;
|
|
1288
|
-
const incomingKey = `${unitType}/${unitId}`;
|
|
1289
|
-
const isHookUnit = s.currentUnit.type.startsWith("hook/");
|
|
1290
|
-
const artifactVerified =
|
|
1291
|
-
isHookUnit ||
|
|
1292
|
-
deps.verifyExpectedArtifact(
|
|
1293
|
-
s.currentUnit.type,
|
|
1294
|
-
s.currentUnit.id,
|
|
1295
|
-
s.basePath,
|
|
1296
|
-
);
|
|
1297
|
-
if (closeoutKey !== incomingKey && artifactVerified) {
|
|
1298
|
-
s.completedUnits.push({
|
|
1299
|
-
type: s.currentUnit.type,
|
|
1300
|
-
id: s.currentUnit.id,
|
|
1301
|
-
startedAt: s.currentUnit.startedAt,
|
|
1302
|
-
finishedAt: Date.now(),
|
|
1303
|
-
});
|
|
1304
|
-
if (s.completedUnits.length > 200) {
|
|
1305
|
-
s.completedUnits = s.completedUnits.slice(-200);
|
|
1306
|
-
}
|
|
1307
|
-
deps.clearUnitRuntimeRecord(
|
|
1308
|
-
s.basePath,
|
|
1309
|
-
s.currentUnit.type,
|
|
1310
|
-
s.currentUnit.id,
|
|
1311
|
-
);
|
|
1312
|
-
s.unitDispatchCount.delete(
|
|
1313
|
-
`${s.currentUnit.type}/${s.currentUnit.id}`,
|
|
1314
|
-
);
|
|
1315
|
-
s.unitRecoveryCount.delete(
|
|
1316
|
-
`${s.currentUnit.type}/${s.currentUnit.id}`,
|
|
1317
|
-
);
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
1369
|
s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
1322
1370
|
deps.captureAvailableSkills();
|
|
1323
1371
|
deps.writeUnitRuntimeRecord(
|
|
@@ -1344,7 +1392,6 @@ export async function autoLoop(
|
|
|
1344
1392
|
deps.ensurePreconditions(unitType, unitId, s.basePath, state);
|
|
1345
1393
|
|
|
1346
1394
|
// Prompt injection
|
|
1347
|
-
const MAX_RECOVERY_CHARS = 50_000;
|
|
1348
1395
|
let finalPrompt = prompt;
|
|
1349
1396
|
|
|
1350
1397
|
if (s.pendingVerificationRetry) {
|
|
@@ -1389,7 +1436,7 @@ export async function autoLoop(
|
|
|
1389
1436
|
s.lastBaselineCharCount = undefined;
|
|
1390
1437
|
if (deps.isDbAvailable()) {
|
|
1391
1438
|
try {
|
|
1392
|
-
const { inlineGsdRootFile } = await import("./auto-prompts.js");
|
|
1439
|
+
const { inlineGsdRootFile } = await importExtensionModule<typeof import("./auto-prompts.js")>(import.meta.url, "./auto-prompts.js");
|
|
1393
1440
|
const [decisionsContent, requirementsContent, projectContent] =
|
|
1394
1441
|
await Promise.all([
|
|
1395
1442
|
inlineGsdRootFile(s.basePath, "decisions.md", "Decisions"),
|
|
@@ -1416,7 +1463,7 @@ export async function autoLoop(
|
|
|
1416
1463
|
);
|
|
1417
1464
|
}
|
|
1418
1465
|
|
|
1419
|
-
// Select and apply model (with tier escalation on retry)
|
|
1466
|
+
// Select and apply model (with tier escalation on retry — normal units only)
|
|
1420
1467
|
const modelResult = await deps.selectAndApplyModel(
|
|
1421
1468
|
ctx,
|
|
1422
1469
|
pi,
|
|
@@ -1426,7 +1473,7 @@ export async function autoLoop(
|
|
|
1426
1473
|
prefs,
|
|
1427
1474
|
s.verbose,
|
|
1428
1475
|
s.autoModeStartModel,
|
|
1429
|
-
{ isRetry, previousTier },
|
|
1476
|
+
sidecarItem ? undefined : { isRetry, previousTier },
|
|
1430
1477
|
);
|
|
1431
1478
|
s.currentUnitRouting =
|
|
1432
1479
|
modelResult.routing as AutoSession["currentUnitRouting"];
|
|
@@ -1475,7 +1522,6 @@ export async function autoLoop(
|
|
|
1475
1522
|
unitType,
|
|
1476
1523
|
unitId,
|
|
1477
1524
|
finalPrompt,
|
|
1478
|
-
prefs,
|
|
1479
1525
|
);
|
|
1480
1526
|
debugLog("autoLoop", {
|
|
1481
1527
|
phase: "runUnit-end",
|
|
@@ -1485,6 +1531,23 @@ export async function autoLoop(
|
|
|
1485
1531
|
status: unitResult.status,
|
|
1486
1532
|
});
|
|
1487
1533
|
|
|
1534
|
+
// Tag the most recent window entry with error info for stuck detection
|
|
1535
|
+
if (unitResult.status === "error" || unitResult.status === "cancelled") {
|
|
1536
|
+
const lastEntry = recentUnits[recentUnits.length - 1];
|
|
1537
|
+
if (lastEntry) {
|
|
1538
|
+
lastEntry.error = `${unitResult.status}:${unitType}/${unitId}`;
|
|
1539
|
+
}
|
|
1540
|
+
} else if (unitResult.event?.messages?.length) {
|
|
1541
|
+
const lastMsg = unitResult.event.messages[unitResult.event.messages.length - 1];
|
|
1542
|
+
const msgStr = typeof lastMsg === "string" ? lastMsg : JSON.stringify(lastMsg);
|
|
1543
|
+
if (/error|fail|exception/i.test(msgStr)) {
|
|
1544
|
+
const lastEntry = recentUnits[recentUnits.length - 1];
|
|
1545
|
+
if (lastEntry) {
|
|
1546
|
+
lastEntry.error = msgStr.slice(0, 200);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1488
1551
|
if (unitResult.status === "cancelled") {
|
|
1489
1552
|
ctx.ui.notify(
|
|
1490
1553
|
`Session creation timed out or was cancelled for ${unitType} ${unitId}. Will retry.`,
|
|
@@ -1495,6 +1558,52 @@ export async function autoLoop(
|
|
|
1495
1558
|
break;
|
|
1496
1559
|
}
|
|
1497
1560
|
|
|
1561
|
+
// ── Immediate unit closeout (metrics, activity log, memory) ────────
|
|
1562
|
+
// Run right after runUnit() returns so telemetry is never lost to a
|
|
1563
|
+
// crash between iterations.
|
|
1564
|
+
await deps.closeoutUnit(
|
|
1565
|
+
ctx,
|
|
1566
|
+
s.basePath,
|
|
1567
|
+
unitType,
|
|
1568
|
+
unitId,
|
|
1569
|
+
s.currentUnit.startedAt,
|
|
1570
|
+
deps.buildSnapshotOpts(unitType, unitId),
|
|
1571
|
+
);
|
|
1572
|
+
|
|
1573
|
+
if (s.currentUnitRouting) {
|
|
1574
|
+
deps.recordOutcome(
|
|
1575
|
+
unitType,
|
|
1576
|
+
s.currentUnitRouting.tier as "light" | "standard" | "heavy",
|
|
1577
|
+
true, // success assumed; dispatch will re-dispatch if artifact missing
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
const isHookUnit = unitType.startsWith("hook/");
|
|
1582
|
+
const artifactVerified =
|
|
1583
|
+
isHookUnit ||
|
|
1584
|
+
deps.verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
1585
|
+
if (artifactVerified) {
|
|
1586
|
+
s.completedUnits.push({
|
|
1587
|
+
type: unitType,
|
|
1588
|
+
id: unitId,
|
|
1589
|
+
startedAt: s.currentUnit.startedAt,
|
|
1590
|
+
finishedAt: Date.now(),
|
|
1591
|
+
});
|
|
1592
|
+
if (s.completedUnits.length > 200) {
|
|
1593
|
+
s.completedUnits = s.completedUnits.slice(-200);
|
|
1594
|
+
}
|
|
1595
|
+
// Flush completed-units to disk so the record survives crashes
|
|
1596
|
+
try {
|
|
1597
|
+
const completedKeysPath = join(gsdRoot(s.basePath), "completed-units.json");
|
|
1598
|
+
const keys = s.completedUnits.map((u) => `${u.type}/${u.id}`);
|
|
1599
|
+
atomicWriteSync(completedKeysPath, JSON.stringify(keys, null, 2));
|
|
1600
|
+
} catch { /* non-fatal: disk flush failure */ }
|
|
1601
|
+
|
|
1602
|
+
deps.clearUnitRuntimeRecord(s.basePath, unitType, unitId);
|
|
1603
|
+
s.unitDispatchCount.delete(`${unitType}/${unitId}`);
|
|
1604
|
+
s.unitRecoveryCount.delete(`${unitType}/${unitId}`);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1498
1607
|
// ── Phase 5: Finalize ───────────────────────────────────────────────
|
|
1499
1608
|
|
|
1500
1609
|
debugLog("autoLoop", { phase: "finalize", iteration });
|
|
@@ -1515,7 +1624,13 @@ export async function autoLoop(
|
|
|
1515
1624
|
};
|
|
1516
1625
|
|
|
1517
1626
|
// Pre-verification processing (commit, doctor, state rebuild, etc.)
|
|
1518
|
-
|
|
1627
|
+
// Sidecar items use lightweight pre-verification opts
|
|
1628
|
+
const preVerificationOpts: PreVerificationOpts | undefined = sidecarItem
|
|
1629
|
+
? sidecarItem.kind === "hook"
|
|
1630
|
+
? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
|
|
1631
|
+
: { skipSettleDelay: true, skipStateRebuild: true }
|
|
1632
|
+
: undefined;
|
|
1633
|
+
const preResult = await deps.postUnitPreVerification(postUnitCtx, preVerificationOpts);
|
|
1519
1634
|
if (preResult === "dispatched") {
|
|
1520
1635
|
debugLog("autoLoop", {
|
|
1521
1636
|
phase: "exit",
|
|
@@ -1534,22 +1649,32 @@ export async function autoLoop(
|
|
|
1534
1649
|
break;
|
|
1535
1650
|
}
|
|
1536
1651
|
|
|
1537
|
-
// Verification gate
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
)
|
|
1652
|
+
// Verification gate
|
|
1653
|
+
// Hook sidecar items skip verification entirely.
|
|
1654
|
+
// Non-hook sidecar items run verification but skip retries (just continue).
|
|
1655
|
+
const skipVerification = sidecarItem?.kind === "hook";
|
|
1656
|
+
if (!skipVerification) {
|
|
1657
|
+
const verificationResult = await deps.runPostUnitVerification(
|
|
1658
|
+
{ s, ctx, pi },
|
|
1659
|
+
deps.pauseAuto,
|
|
1660
|
+
);
|
|
1542
1661
|
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1662
|
+
if (verificationResult === "pause") {
|
|
1663
|
+
debugLog("autoLoop", { phase: "exit", reason: "verification-pause" });
|
|
1664
|
+
break;
|
|
1665
|
+
}
|
|
1547
1666
|
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1667
|
+
if (verificationResult === "retry") {
|
|
1668
|
+
if (sidecarItem) {
|
|
1669
|
+
// Sidecar verification retries are skipped — just continue
|
|
1670
|
+
debugLog("autoLoop", { phase: "sidecar-verification-retry-skipped", iteration });
|
|
1671
|
+
} else {
|
|
1672
|
+
// s.pendingVerificationRetry was set by runPostUnitVerification.
|
|
1673
|
+
// Continue the loop — next iteration will inject the retry context into the prompt.
|
|
1674
|
+
debugLog("autoLoop", { phase: "verification-retry", iteration });
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1553
1678
|
}
|
|
1554
1679
|
|
|
1555
1680
|
// Post-verification processing (DB dual-write, hooks, triage, quick-tasks)
|
|
@@ -1569,150 +1694,6 @@ export async function autoLoop(
|
|
|
1569
1694
|
break;
|
|
1570
1695
|
}
|
|
1571
1696
|
|
|
1572
|
-
// ── Sidecar drain: dispatch enqueued hooks/triage/quick-tasks ──
|
|
1573
|
-
let sidecarBroke = false;
|
|
1574
|
-
while (s.sidecarQueue.length > 0 && s.active) {
|
|
1575
|
-
const item = s.sidecarQueue.shift()!;
|
|
1576
|
-
debugLog("autoLoop", {
|
|
1577
|
-
phase: "sidecar-dequeue",
|
|
1578
|
-
kind: item.kind,
|
|
1579
|
-
unitType: item.unitType,
|
|
1580
|
-
unitId: item.unitId,
|
|
1581
|
-
});
|
|
1582
|
-
|
|
1583
|
-
// Set up as current unit
|
|
1584
|
-
const sidecarStartedAt = Date.now();
|
|
1585
|
-
s.currentUnit = {
|
|
1586
|
-
type: item.unitType,
|
|
1587
|
-
id: item.unitId,
|
|
1588
|
-
startedAt: sidecarStartedAt,
|
|
1589
|
-
};
|
|
1590
|
-
deps.writeUnitRuntimeRecord(
|
|
1591
|
-
s.basePath,
|
|
1592
|
-
item.unitType,
|
|
1593
|
-
item.unitId,
|
|
1594
|
-
sidecarStartedAt,
|
|
1595
|
-
{
|
|
1596
|
-
phase: "dispatched",
|
|
1597
|
-
wrapupWarningSent: false,
|
|
1598
|
-
timeoutAt: null,
|
|
1599
|
-
lastProgressAt: sidecarStartedAt,
|
|
1600
|
-
progressCount: 0,
|
|
1601
|
-
lastProgressKind: "dispatch",
|
|
1602
|
-
},
|
|
1603
|
-
);
|
|
1604
|
-
|
|
1605
|
-
// Model selection (handles hook model override)
|
|
1606
|
-
await deps.selectAndApplyModel(
|
|
1607
|
-
ctx,
|
|
1608
|
-
pi,
|
|
1609
|
-
item.unitType,
|
|
1610
|
-
item.unitId,
|
|
1611
|
-
s.basePath,
|
|
1612
|
-
prefs,
|
|
1613
|
-
s.verbose,
|
|
1614
|
-
s.autoModeStartModel,
|
|
1615
|
-
);
|
|
1616
|
-
|
|
1617
|
-
// Supervision
|
|
1618
|
-
deps.clearUnitTimeout();
|
|
1619
|
-
deps.startUnitSupervision({
|
|
1620
|
-
s,
|
|
1621
|
-
ctx,
|
|
1622
|
-
pi,
|
|
1623
|
-
unitType: item.unitType,
|
|
1624
|
-
unitId: item.unitId,
|
|
1625
|
-
prefs,
|
|
1626
|
-
buildSnapshotOpts: () =>
|
|
1627
|
-
deps.buildSnapshotOpts(item.unitType, item.unitId),
|
|
1628
|
-
buildRecoveryContext: () => ({}),
|
|
1629
|
-
pauseAuto: deps.pauseAuto,
|
|
1630
|
-
});
|
|
1631
|
-
|
|
1632
|
-
// Write lock
|
|
1633
|
-
const sidecarSessionFile = deps.getSessionFile(ctx);
|
|
1634
|
-
deps.writeLock(
|
|
1635
|
-
deps.lockBase(),
|
|
1636
|
-
item.unitType,
|
|
1637
|
-
item.unitId,
|
|
1638
|
-
s.completedUnits.length,
|
|
1639
|
-
sidecarSessionFile,
|
|
1640
|
-
);
|
|
1641
|
-
|
|
1642
|
-
// Execute via standard runUnit
|
|
1643
|
-
const sidecarResult = await runUnit(
|
|
1644
|
-
ctx,
|
|
1645
|
-
pi,
|
|
1646
|
-
s,
|
|
1647
|
-
item.unitType,
|
|
1648
|
-
item.unitId,
|
|
1649
|
-
item.prompt,
|
|
1650
|
-
prefs,
|
|
1651
|
-
);
|
|
1652
|
-
deps.clearUnitTimeout();
|
|
1653
|
-
|
|
1654
|
-
if (sidecarResult.status === "cancelled") {
|
|
1655
|
-
ctx.ui.notify(
|
|
1656
|
-
`Sidecar unit ${item.unitType} ${item.unitId} session cancelled. Stopping.`,
|
|
1657
|
-
"warning",
|
|
1658
|
-
);
|
|
1659
|
-
await deps.stopAuto(ctx, pi, "Sidecar session creation failed");
|
|
1660
|
-
sidecarBroke = true;
|
|
1661
|
-
break;
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
// Run pre-verification for the sidecar unit
|
|
1665
|
-
const sidecarPreResult =
|
|
1666
|
-
await deps.postUnitPreVerification(postUnitCtx);
|
|
1667
|
-
if (sidecarPreResult === "dispatched") {
|
|
1668
|
-
// Pre-verification caused stop/pause
|
|
1669
|
-
debugLog("autoLoop", {
|
|
1670
|
-
phase: "exit",
|
|
1671
|
-
reason: "sidecar-pre-verification-stop",
|
|
1672
|
-
});
|
|
1673
|
-
sidecarBroke = true;
|
|
1674
|
-
break;
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
// Verification gate for non-hook sidecar units (triage, quick-tasks)
|
|
1678
|
-
// Hook units are lightweight and don't need verification.
|
|
1679
|
-
if (item.kind !== "hook") {
|
|
1680
|
-
const sidecarVerification = await deps.runPostUnitVerification(
|
|
1681
|
-
{ s, ctx, pi },
|
|
1682
|
-
deps.pauseAuto,
|
|
1683
|
-
);
|
|
1684
|
-
if (sidecarVerification === "pause") {
|
|
1685
|
-
debugLog("autoLoop", {
|
|
1686
|
-
phase: "exit",
|
|
1687
|
-
reason: "sidecar-verification-pause",
|
|
1688
|
-
});
|
|
1689
|
-
sidecarBroke = true;
|
|
1690
|
-
break;
|
|
1691
|
-
}
|
|
1692
|
-
// "retry" for sidecars — skip retry, just continue (sidecar retries are not worth the complexity)
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
// Post-verification (may enqueue more sidecar items)
|
|
1696
|
-
const sidecarPostResult =
|
|
1697
|
-
await deps.postUnitPostVerification(postUnitCtx);
|
|
1698
|
-
if (sidecarPostResult === "stopped") {
|
|
1699
|
-
debugLog("autoLoop", { phase: "exit", reason: "sidecar-stopped" });
|
|
1700
|
-
sidecarBroke = true;
|
|
1701
|
-
break;
|
|
1702
|
-
}
|
|
1703
|
-
if (sidecarPostResult === "step-wizard") {
|
|
1704
|
-
debugLog("autoLoop", {
|
|
1705
|
-
phase: "exit",
|
|
1706
|
-
reason: "sidecar-step-wizard",
|
|
1707
|
-
});
|
|
1708
|
-
sidecarBroke = true;
|
|
1709
|
-
break;
|
|
1710
|
-
}
|
|
1711
|
-
// "continue" — loop checks sidecarQueue again
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
if (sidecarBroke) break;
|
|
1715
|
-
|
|
1716
1697
|
consecutiveErrors = 0; // Iteration completed successfully
|
|
1717
1698
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|
|
1718
1699
|
} catch (loopErr) {
|
|
@@ -1752,6 +1733,6 @@ export async function autoLoop(
|
|
|
1752
1733
|
}
|
|
1753
1734
|
}
|
|
1754
1735
|
|
|
1755
|
-
|
|
1736
|
+
_currentResolve = null;
|
|
1756
1737
|
debugLog("autoLoop", { phase: "exit", totalIterations: iteration });
|
|
1757
1738
|
}
|