gsd-pi 2.32.0 → 2.33.0-dev.69bff0f
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 +22 -20
- package/dist/resource-loader.js +13 -3
- package/dist/resources/extensions/gsd/auto-dashboard.ts +3 -1
- package/dist/resources/extensions/gsd/auto-dispatch.ts +40 -12
- package/dist/resources/extensions/gsd/auto-idempotency.ts +3 -2
- package/dist/resources/extensions/gsd/auto-observability.ts +2 -4
- package/dist/resources/extensions/gsd/auto-post-unit.ts +5 -5
- package/dist/resources/extensions/gsd/auto-prompts.ts +46 -44
- package/dist/resources/extensions/gsd/auto-recovery.ts +8 -22
- package/dist/resources/extensions/gsd/auto-start.ts +8 -6
- package/dist/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
- package/dist/resources/extensions/gsd/auto-supervisor.ts +10 -5
- package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
- package/dist/resources/extensions/gsd/auto-timers.ts +3 -2
- package/dist/resources/extensions/gsd/auto-verification.ts +6 -6
- package/dist/resources/extensions/gsd/auto-worktree.ts +140 -5
- package/dist/resources/extensions/gsd/auto.ts +108 -182
- package/dist/resources/extensions/gsd/commands-inspect.ts +2 -1
- package/dist/resources/extensions/gsd/commands-workflow-templates.ts +2 -1
- package/dist/resources/extensions/gsd/commands.ts +14 -2
- package/dist/resources/extensions/gsd/complexity-classifier.ts +5 -7
- package/dist/resources/extensions/gsd/crash-recovery.ts +15 -2
- package/dist/resources/extensions/gsd/dispatch-guard.ts +2 -1
- package/dist/resources/extensions/gsd/error-utils.ts +6 -0
- package/dist/resources/extensions/gsd/export.ts +2 -1
- package/dist/resources/extensions/gsd/git-service.ts +3 -2
- package/dist/resources/extensions/gsd/guided-flow.ts +3 -2
- package/dist/resources/extensions/gsd/index.ts +12 -5
- package/dist/resources/extensions/gsd/key-manager.ts +2 -1
- package/dist/resources/extensions/gsd/marketplace-discovery.ts +4 -3
- package/dist/resources/extensions/gsd/metrics.ts +3 -3
- package/dist/resources/extensions/gsd/migrate-external.ts +21 -4
- package/dist/resources/extensions/gsd/milestone-ids.ts +2 -1
- package/dist/resources/extensions/gsd/native-git-bridge.ts +2 -1
- package/dist/resources/extensions/gsd/parallel-merge.ts +2 -1
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +8 -9
- package/dist/resources/extensions/gsd/quick.ts +58 -3
- package/dist/resources/extensions/gsd/repo-identity.ts +22 -1
- package/dist/resources/extensions/gsd/session-lock.ts +86 -11
- package/dist/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +14 -11
- package/dist/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +691 -0
- package/dist/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +317 -0
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/loop-regression.test.ts +877 -0
- package/dist/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +358 -0
- package/dist/resources/extensions/gsd/tests/session-lock-regression.test.ts +216 -0
- package/dist/resources/extensions/gsd/tests/session-lock.test.ts +119 -0
- package/dist/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +206 -0
- package/dist/resources/extensions/gsd/undo.ts +5 -7
- package/dist/resources/extensions/gsd/unit-id.ts +14 -0
- package/dist/resources/extensions/gsd/unit-runtime.ts +2 -1
- package/dist/resources/extensions/gsd/worktree-command.ts +8 -7
- package/package.json +3 -2
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-dashboard.ts +3 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +40 -12
- package/src/resources/extensions/gsd/auto-idempotency.ts +3 -2
- package/src/resources/extensions/gsd/auto-observability.ts +2 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +5 -5
- package/src/resources/extensions/gsd/auto-prompts.ts +46 -44
- package/src/resources/extensions/gsd/auto-recovery.ts +8 -22
- package/src/resources/extensions/gsd/auto-start.ts +8 -6
- package/src/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
- package/src/resources/extensions/gsd/auto-supervisor.ts +10 -5
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
- package/src/resources/extensions/gsd/auto-timers.ts +3 -2
- package/src/resources/extensions/gsd/auto-verification.ts +6 -6
- package/src/resources/extensions/gsd/auto-worktree.ts +140 -5
- package/src/resources/extensions/gsd/auto.ts +108 -182
- package/src/resources/extensions/gsd/commands-inspect.ts +2 -1
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +2 -1
- package/src/resources/extensions/gsd/commands.ts +14 -2
- package/src/resources/extensions/gsd/complexity-classifier.ts +5 -7
- package/src/resources/extensions/gsd/crash-recovery.ts +15 -2
- package/src/resources/extensions/gsd/dispatch-guard.ts +2 -1
- package/src/resources/extensions/gsd/error-utils.ts +6 -0
- package/src/resources/extensions/gsd/export.ts +2 -1
- package/src/resources/extensions/gsd/git-service.ts +3 -2
- package/src/resources/extensions/gsd/guided-flow.ts +3 -2
- package/src/resources/extensions/gsd/index.ts +12 -5
- package/src/resources/extensions/gsd/key-manager.ts +2 -1
- package/src/resources/extensions/gsd/marketplace-discovery.ts +4 -3
- package/src/resources/extensions/gsd/metrics.ts +3 -3
- package/src/resources/extensions/gsd/migrate-external.ts +21 -4
- package/src/resources/extensions/gsd/milestone-ids.ts +2 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +2 -1
- package/src/resources/extensions/gsd/parallel-merge.ts +2 -1
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
- package/src/resources/extensions/gsd/post-unit-hooks.ts +8 -9
- package/src/resources/extensions/gsd/quick.ts +58 -3
- package/src/resources/extensions/gsd/repo-identity.ts +22 -1
- package/src/resources/extensions/gsd/session-lock.ts +86 -11
- package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +14 -11
- package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +691 -0
- package/src/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +317 -0
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/loop-regression.test.ts +877 -0
- package/src/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +358 -0
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +216 -0
- package/src/resources/extensions/gsd/tests/session-lock.test.ts +119 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +206 -0
- package/src/resources/extensions/gsd/undo.ts +5 -7
- package/src/resources/extensions/gsd/unit-id.ts +14 -0
- package/src/resources/extensions/gsd/unit-runtime.ts +2 -1
- package/src/resources/extensions/gsd/worktree-command.ts +8 -7
- package/dist/resources/extensions/mcporter/extension-manifest.json +0 -12
- package/src/resources/extensions/mcporter/extension-manifest.json +0 -12
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* manages create, enter, detect, and teardown for auto-mode worktrees.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { existsSync, readFileSync, realpathSync, unlinkSync, statSync, rmSync } from "node:fs";
|
|
9
|
+
import { existsSync, readFileSync, realpathSync, unlinkSync, statSync, rmSync, readdirSync, cpSync, lstatSync as lstatSyncFn } from "node:fs";
|
|
10
10
|
import { isAbsolute, join, sep } from "node:path";
|
|
11
11
|
import { GSDError, GSD_IO_ERROR, GSD_GIT_ERROR } from "./errors.js";
|
|
12
12
|
import { execSync, execFileSync } from "node:child_process";
|
|
@@ -38,12 +38,129 @@ import {
|
|
|
38
38
|
nativeBranchDelete,
|
|
39
39
|
nativeBranchExists,
|
|
40
40
|
} from "./native-git-bridge.js";
|
|
41
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
41
42
|
|
|
42
43
|
// ─── Module State ──────────────────────────────────────────────────────────
|
|
43
44
|
|
|
44
45
|
/** Original project root before chdir into auto-worktree. */
|
|
45
46
|
let originalBase: string | null = null;
|
|
46
47
|
|
|
48
|
+
// ─── Worktree ↔ Main Repo Sync (#1311) ──────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Sync .gsd/ state from the main repo into the worktree.
|
|
52
|
+
*
|
|
53
|
+
* When .gsd/ is a symlink to the external state directory, both the main
|
|
54
|
+
* repo and worktree share the same directory — no sync needed.
|
|
55
|
+
*
|
|
56
|
+
* When .gsd/ is a real directory (e.g., git-tracked or manage_gitignore:false),
|
|
57
|
+
* the worktree has its own copy that may be stale. This function copies
|
|
58
|
+
* missing milestones, CONTEXT, ROADMAP, DECISIONS, REQUIREMENTS, and
|
|
59
|
+
* PROJECT files from the main repo's .gsd/ into the worktree's .gsd/.
|
|
60
|
+
*
|
|
61
|
+
* Only adds missing content — never overwrites existing files in the worktree
|
|
62
|
+
* (the worktree's execution state is authoritative for in-progress work).
|
|
63
|
+
*/
|
|
64
|
+
export function syncGsdStateToWorktree(mainBasePath: string, worktreePath_: string): { synced: string[] } {
|
|
65
|
+
const mainGsd = gsdRoot(mainBasePath);
|
|
66
|
+
const wtGsd = gsdRoot(worktreePath_);
|
|
67
|
+
const synced: string[] = [];
|
|
68
|
+
|
|
69
|
+
// If both resolve to the same directory (symlink), no sync needed
|
|
70
|
+
try {
|
|
71
|
+
const mainResolved = realpathSync(mainGsd);
|
|
72
|
+
const wtResolved = realpathSync(wtGsd);
|
|
73
|
+
if (mainResolved === wtResolved) return { synced };
|
|
74
|
+
} catch {
|
|
75
|
+
// Can't resolve — proceed with sync as a safety measure
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!existsSync(mainGsd) || !existsSync(wtGsd)) return { synced };
|
|
79
|
+
|
|
80
|
+
// Sync root-level .gsd/ files (DECISIONS, REQUIREMENTS, PROJECT, KNOWLEDGE)
|
|
81
|
+
const rootFiles = ["DECISIONS.md", "REQUIREMENTS.md", "PROJECT.md", "KNOWLEDGE.md", "OVERRIDES.md"];
|
|
82
|
+
for (const f of rootFiles) {
|
|
83
|
+
const src = join(mainGsd, f);
|
|
84
|
+
const dst = join(wtGsd, f);
|
|
85
|
+
if (existsSync(src) && !existsSync(dst)) {
|
|
86
|
+
try {
|
|
87
|
+
cpSync(src, dst);
|
|
88
|
+
synced.push(f);
|
|
89
|
+
} catch { /* non-fatal */ }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Sync milestones: copy entire milestone directories that are missing
|
|
94
|
+
const mainMilestonesDir = join(mainGsd, "milestones");
|
|
95
|
+
const wtMilestonesDir = join(wtGsd, "milestones");
|
|
96
|
+
if (existsSync(mainMilestonesDir) && existsSync(wtMilestonesDir)) {
|
|
97
|
+
try {
|
|
98
|
+
const mainMilestones = readdirSync(mainMilestonesDir, { withFileTypes: true })
|
|
99
|
+
.filter(d => d.isDirectory() && /^M\d{3}/.test(d.name))
|
|
100
|
+
.map(d => d.name);
|
|
101
|
+
|
|
102
|
+
for (const mid of mainMilestones) {
|
|
103
|
+
const srcDir = join(mainMilestonesDir, mid);
|
|
104
|
+
const dstDir = join(wtMilestonesDir, mid);
|
|
105
|
+
|
|
106
|
+
if (!existsSync(dstDir)) {
|
|
107
|
+
// Entire milestone missing from worktree — copy it
|
|
108
|
+
try {
|
|
109
|
+
cpSync(srcDir, dstDir, { recursive: true });
|
|
110
|
+
synced.push(`milestones/${mid}/`);
|
|
111
|
+
} catch { /* non-fatal */ }
|
|
112
|
+
} else {
|
|
113
|
+
// Milestone directory exists but may be missing files (stale snapshot).
|
|
114
|
+
// Sync individual top-level milestone files (CONTEXT, ROADMAP, RESEARCH, etc.)
|
|
115
|
+
try {
|
|
116
|
+
const srcFiles = readdirSync(srcDir).filter(f => f.endsWith(".md") || f.endsWith(".json"));
|
|
117
|
+
for (const f of srcFiles) {
|
|
118
|
+
const srcFile = join(srcDir, f);
|
|
119
|
+
const dstFile = join(dstDir, f);
|
|
120
|
+
if (!existsSync(dstFile)) {
|
|
121
|
+
try {
|
|
122
|
+
const srcStat = lstatSyncFn(srcFile);
|
|
123
|
+
if (srcStat.isFile()) {
|
|
124
|
+
cpSync(srcFile, dstFile);
|
|
125
|
+
synced.push(`milestones/${mid}/${f}`);
|
|
126
|
+
}
|
|
127
|
+
} catch { /* non-fatal */ }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Sync slices directory if it exists in main but not in worktree
|
|
132
|
+
const srcSlicesDir = join(srcDir, "slices");
|
|
133
|
+
const dstSlicesDir = join(dstDir, "slices");
|
|
134
|
+
if (existsSync(srcSlicesDir) && !existsSync(dstSlicesDir)) {
|
|
135
|
+
try {
|
|
136
|
+
cpSync(srcSlicesDir, dstSlicesDir, { recursive: true });
|
|
137
|
+
synced.push(`milestones/${mid}/slices/`);
|
|
138
|
+
} catch { /* non-fatal */ }
|
|
139
|
+
} else if (existsSync(srcSlicesDir) && existsSync(dstSlicesDir)) {
|
|
140
|
+
// Both exist — sync missing slice directories
|
|
141
|
+
const srcSlices = readdirSync(srcSlicesDir, { withFileTypes: true })
|
|
142
|
+
.filter(d => d.isDirectory())
|
|
143
|
+
.map(d => d.name);
|
|
144
|
+
for (const sid of srcSlices) {
|
|
145
|
+
const srcSlice = join(srcSlicesDir, sid);
|
|
146
|
+
const dstSlice = join(dstSlicesDir, sid);
|
|
147
|
+
if (!existsSync(dstSlice)) {
|
|
148
|
+
try {
|
|
149
|
+
cpSync(srcSlice, dstSlice, { recursive: true });
|
|
150
|
+
synced.push(`milestones/${mid}/slices/${sid}/`);
|
|
151
|
+
} catch { /* non-fatal */ }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} catch { /* non-fatal */ }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch { /* non-fatal */ }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { synced };
|
|
162
|
+
}
|
|
163
|
+
|
|
47
164
|
// ─── Worktree Post-Create Hook (#597) ────────────────────────────────────────
|
|
48
165
|
|
|
49
166
|
/**
|
|
@@ -81,7 +198,7 @@ export function runWorktreePostCreateHook(sourceDir: string, worktreeDir: string
|
|
|
81
198
|
});
|
|
82
199
|
return null;
|
|
83
200
|
} catch (err) {
|
|
84
|
-
const msg =
|
|
201
|
+
const msg = getErrorMessage(err);
|
|
85
202
|
return `Worktree post-create hook failed: ${msg}`;
|
|
86
203
|
}
|
|
87
204
|
}
|
|
@@ -124,6 +241,12 @@ export function createAutoWorktree(basePath: string, milestoneId: string): strin
|
|
|
124
241
|
// Ensure worktree shares external state via symlink
|
|
125
242
|
ensureGsdSymlink(info.path);
|
|
126
243
|
|
|
244
|
+
// Sync .gsd/ state from main repo into the worktree (#1311).
|
|
245
|
+
// Even with the symlink, the worktree may have stale git-tracked files
|
|
246
|
+
// if .gsd/ is not gitignored. And on fresh create, the milestone files
|
|
247
|
+
// created on main since the branch point won't be in the worktree.
|
|
248
|
+
syncGsdStateToWorktree(basePath, info.path);
|
|
249
|
+
|
|
127
250
|
// Run user-configured post-create hook (#597) — e.g. copy .env, symlink assets
|
|
128
251
|
const hookError = runWorktreePostCreateHook(basePath, info.path);
|
|
129
252
|
if (hookError) {
|
|
@@ -141,7 +264,7 @@ export function createAutoWorktree(basePath: string, milestoneId: string): strin
|
|
|
141
264
|
// Don't store originalBase -- caller can retry or clean up.
|
|
142
265
|
throw new GSDError(
|
|
143
266
|
GSD_IO_ERROR,
|
|
144
|
-
`Auto-worktree created at ${info.path} but chdir failed: ${
|
|
267
|
+
`Auto-worktree created at ${info.path} but chdir failed: ${getErrorMessage(err)}`,
|
|
145
268
|
);
|
|
146
269
|
}
|
|
147
270
|
|
|
@@ -168,7 +291,7 @@ export function teardownAutoWorktree(
|
|
|
168
291
|
} catch (err) {
|
|
169
292
|
throw new GSDError(
|
|
170
293
|
GSD_IO_ERROR,
|
|
171
|
-
`Failed to chdir back to ${originalBasePath} during teardown: ${
|
|
294
|
+
`Failed to chdir back to ${originalBasePath} during teardown: ${getErrorMessage(err)}`,
|
|
172
295
|
);
|
|
173
296
|
}
|
|
174
297
|
|
|
@@ -266,6 +389,18 @@ export function enterAutoWorktree(basePath: string, milestoneId: string): string
|
|
|
266
389
|
throw new GSDError(GSD_IO_ERROR, `Auto-worktree path ${p} exists but .git is unreadable`);
|
|
267
390
|
}
|
|
268
391
|
|
|
392
|
+
// Ensure worktree shares external state via symlink (#1311).
|
|
393
|
+
// On resume (enterAutoWorktree), the symlink may be missing if it was
|
|
394
|
+
// created before ensureGsdSymlink existed, or the .gsd/ directory may be
|
|
395
|
+
// a stale git-tracked copy instead of a symlink. Refreshing here ensures
|
|
396
|
+
// the worktree sees the same milestone state as the main repo.
|
|
397
|
+
ensureGsdSymlink(p);
|
|
398
|
+
|
|
399
|
+
// Sync .gsd/ state from main repo into worktree (#1311).
|
|
400
|
+
// Covers the case where .gsd/ is a real directory (not symlinked) and
|
|
401
|
+
// milestones were created on main after the worktree was last used.
|
|
402
|
+
syncGsdStateToWorktree(basePath, p);
|
|
403
|
+
|
|
269
404
|
const previousCwd = process.cwd();
|
|
270
405
|
|
|
271
406
|
try {
|
|
@@ -274,7 +409,7 @@ export function enterAutoWorktree(basePath: string, milestoneId: string): string
|
|
|
274
409
|
} catch (err) {
|
|
275
410
|
throw new GSDError(
|
|
276
411
|
GSD_IO_ERROR,
|
|
277
|
-
`Failed to enter auto-worktree at ${p}: ${
|
|
412
|
+
`Failed to enter auto-worktree at ${p}: ${getErrorMessage(err)}`,
|
|
278
413
|
);
|
|
279
414
|
}
|
|
280
415
|
|
|
@@ -105,6 +105,7 @@ import { computeBudgets, resolveExecutorContextWindow } from "./context-budget.j
|
|
|
105
105
|
import { GSDError, GSD_ARTIFACT_MISSING } from "./errors.js";
|
|
106
106
|
import { join } from "node:path";
|
|
107
107
|
import { sep as pathSep } from "node:path";
|
|
108
|
+
import { parseUnitId } from "./unit-id.js";
|
|
108
109
|
import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync, statSync } from "node:fs";
|
|
109
110
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
110
111
|
import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit } from "./native-git-bridge.js";
|
|
@@ -189,6 +190,7 @@ import {
|
|
|
189
190
|
NEW_SESSION_TIMEOUT_MS, DISPATCH_HANG_TIMEOUT_MS,
|
|
190
191
|
} from "./auto/session.js";
|
|
191
192
|
import type { CompletedUnit, CurrentUnit, UnitRouting, StartModel, PendingVerificationRetry } from "./auto/session.js";
|
|
193
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
192
194
|
|
|
193
195
|
// ── ENCAPSULATION INVARIANT ─────────────────────────────────────────────────
|
|
194
196
|
// ALL mutable auto-mode state lives in the AutoSession class (auto/session.ts).
|
|
@@ -213,52 +215,7 @@ export function shouldUseWorktreeIsolation(): boolean {
|
|
|
213
215
|
return true; // default: worktree
|
|
214
216
|
}
|
|
215
217
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
/** Pending verification retry — set when gate fails with retries remaining, consumed by dispatchNextUnit */
|
|
219
|
-
|
|
220
|
-
/** Verification retry count per unitId — separate from s.unitDispatchCount which tracks artifact-missing retries */
|
|
221
|
-
|
|
222
|
-
/** Session file path captured at pause — used to synthesize recovery briefing on resume */
|
|
223
|
-
|
|
224
|
-
/** Dashboard tracking */
|
|
225
|
-
|
|
226
|
-
/** Track dynamic routing decision for the current unit (for metrics) */
|
|
227
|
-
|
|
228
|
-
/** Queue of quick-task captures awaiting dispatch after triage resolution */
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Model captured at auto-mode start. Used to prevent model bleed between
|
|
232
|
-
* concurrent GSD instances sharing the same global settings.json (#650).
|
|
233
|
-
* When preferences don't specify a model for a unit type, this ensures
|
|
234
|
-
* the session's original model is re-applied instead of reading from
|
|
235
|
-
* the shared global settings (which another instance may have overwritten).
|
|
236
|
-
*/
|
|
237
|
-
|
|
238
|
-
/** Track current milestone to detect transitions */
|
|
239
|
-
|
|
240
|
-
/** Model the user had selected before auto-mode started */
|
|
241
|
-
|
|
242
|
-
/** Progress-aware timeout supervision */
|
|
243
|
-
|
|
244
|
-
/** Context-pressure continue-here monitor — fires once when context usage >= 70% */
|
|
245
|
-
|
|
246
|
-
/** Dispatch gap watchdog — detects when the state machine stalls between units.
|
|
247
|
-
* After handleAgentEnd completes, if auto-mode is still active but no new unit
|
|
248
|
-
* has been dispatched (sendMessage not called), this timer fires to force a
|
|
249
|
-
* re-evaluation. Covers the case where dispatchNextUnit silently fails or
|
|
250
|
-
* an unhandled error kills the dispatch chain. */
|
|
251
|
-
|
|
252
|
-
/** Prompt character measurement for token savings analysis (R051). */
|
|
253
|
-
|
|
254
|
-
/** SIGTERM handler registered while auto-mode is active — cleared on stop/pause. */
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Tool calls currently being executed — prevents false idle detection during long-running tools.
|
|
258
|
-
* Maps toolCallId → start timestamp (ms) so the idle watchdog can detect tools that have been
|
|
259
|
-
* running suspiciously long (e.g., a Bash command hung because `&` kept stdout open).
|
|
260
|
-
*/
|
|
261
|
-
|
|
218
|
+
// All mutable state lives in AutoSession (auto/session.ts) — see encapsulation invariant above.
|
|
262
219
|
/** Wrapper: register SIGTERM handler and store reference. */
|
|
263
220
|
function registerSigtermHandler(currentBasePath: string): void {
|
|
264
221
|
s.sigtermHandler = _registerSigtermHandler(currentBasePath, s.sigtermHandler);
|
|
@@ -404,6 +361,79 @@ function buildSnapshotOpts(unitType: string, unitId: string): { continueHereFire
|
|
|
404
361
|
};
|
|
405
362
|
}
|
|
406
363
|
|
|
364
|
+
// ─── Extracted Merge Helper ───────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Attempt to merge the current milestone branch to main.
|
|
368
|
+
* Handles both worktree and branch isolation modes with a single code path.
|
|
369
|
+
* Returns true if merge succeeded, false on error (non-fatal, logged).
|
|
370
|
+
*
|
|
371
|
+
* Extracted from 4 duplicate merge blocks in dispatchNextUnit to eliminate
|
|
372
|
+
* the bug factory where fixing one copy didn't fix the others (#1308).
|
|
373
|
+
*/
|
|
374
|
+
function tryMergeMilestone(ctx: ExtensionContext, milestoneId: string, mode: "transition" | "complete"): boolean {
|
|
375
|
+
const isolationMode = getIsolationMode();
|
|
376
|
+
|
|
377
|
+
// Worktree merge path
|
|
378
|
+
if (isInAutoWorktree(s.basePath) && s.originalBasePath) {
|
|
379
|
+
try {
|
|
380
|
+
const roadmapPath = resolveMilestoneFile(s.originalBasePath, milestoneId, "ROADMAP");
|
|
381
|
+
if (!roadmapPath) {
|
|
382
|
+
teardownAutoWorktree(s.originalBasePath, milestoneId);
|
|
383
|
+
ctx.ui.notify(`Exited worktree for ${milestoneId} (no roadmap for merge).`, "info");
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
387
|
+
const mergeResult = mergeMilestoneToMain(s.originalBasePath, milestoneId, roadmapContent);
|
|
388
|
+
s.basePath = s.originalBasePath;
|
|
389
|
+
s.gitService = createGitService(s.basePath);
|
|
390
|
+
ctx.ui.notify(
|
|
391
|
+
`Milestone ${milestoneId} merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
392
|
+
"info",
|
|
393
|
+
);
|
|
394
|
+
return true;
|
|
395
|
+
} catch (err) {
|
|
396
|
+
ctx.ui.notify(
|
|
397
|
+
`Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
398
|
+
"warning",
|
|
399
|
+
);
|
|
400
|
+
if (s.originalBasePath) {
|
|
401
|
+
s.basePath = s.originalBasePath;
|
|
402
|
+
try { process.chdir(s.basePath); } catch { /* best-effort */ }
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Branch-mode merge path
|
|
409
|
+
if (isolationMode === "branch") {
|
|
410
|
+
try {
|
|
411
|
+
const currentBranch = getCurrentBranch(s.basePath);
|
|
412
|
+
const milestoneBranch = autoWorktreeBranch(milestoneId);
|
|
413
|
+
if (currentBranch === milestoneBranch) {
|
|
414
|
+
const roadmapPath = resolveMilestoneFile(s.basePath, milestoneId, "ROADMAP");
|
|
415
|
+
if (roadmapPath) {
|
|
416
|
+
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
417
|
+
const mergeResult = mergeMilestoneToMain(s.basePath, milestoneId, roadmapContent);
|
|
418
|
+
s.gitService = createGitService(s.basePath);
|
|
419
|
+
ctx.ui.notify(
|
|
420
|
+
`Milestone ${milestoneId} merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
421
|
+
"info",
|
|
422
|
+
);
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} catch (err) {
|
|
427
|
+
ctx.ui.notify(
|
|
428
|
+
`Milestone merge failed (branch mode): ${err instanceof Error ? err.message : String(err)}`,
|
|
429
|
+
"warning",
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
|
|
407
437
|
/**
|
|
408
438
|
* Start a watchdog that fires if no new unit is dispatched within DISPATCH_GAP_TIMEOUT_MS
|
|
409
439
|
* after handleAgentEnd completes. This catches the case where the dispatch chain silently
|
|
@@ -428,7 +458,7 @@ function startDispatchGapWatchdog(ctx: ExtensionContext, pi: ExtensionAPI): void
|
|
|
428
458
|
try {
|
|
429
459
|
await dispatchNextUnit(ctx, pi);
|
|
430
460
|
} catch (retryErr) {
|
|
431
|
-
const message =
|
|
461
|
+
const message = getErrorMessage(retryErr);
|
|
432
462
|
await stopAuto(ctx, pi, `Dispatch gap recovery failed: ${message}`);
|
|
433
463
|
return;
|
|
434
464
|
}
|
|
@@ -458,14 +488,14 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
|
|
|
458
488
|
// ── Auto-worktree: exit worktree and reset s.basePath on stop ──
|
|
459
489
|
if (s.currentMilestoneId && isInAutoWorktree(s.basePath)) {
|
|
460
490
|
try {
|
|
461
|
-
try { autoCommitCurrentBranch(s.basePath, "stop", s.currentMilestoneId); } catch (e) { debugLog("stop-auto-commit-failed", { error:
|
|
491
|
+
try { autoCommitCurrentBranch(s.basePath, "stop", s.currentMilestoneId); } catch (e) { debugLog("stop-auto-commit-failed", { error: getErrorMessage(e) }); }
|
|
462
492
|
teardownAutoWorktree(s.originalBasePath, s.currentMilestoneId, { preserveBranch: true });
|
|
463
493
|
s.basePath = s.originalBasePath;
|
|
464
494
|
s.gitService = createGitService(s.basePath);
|
|
465
495
|
ctx?.ui.notify("Exited auto-worktree (branch preserved for resume).", "info");
|
|
466
496
|
} catch (err) {
|
|
467
497
|
ctx?.ui.notify(
|
|
468
|
-
`Auto-worktree teardown failed: ${
|
|
498
|
+
`Auto-worktree teardown failed: ${getErrorMessage(err)}`,
|
|
469
499
|
"warning",
|
|
470
500
|
);
|
|
471
501
|
}
|
|
@@ -476,7 +506,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
|
|
|
476
506
|
try {
|
|
477
507
|
const { closeDatabase } = await import("./gsd-db.js");
|
|
478
508
|
closeDatabase();
|
|
479
|
-
} catch (e) { debugLog("db-close-failed", { error:
|
|
509
|
+
} catch (e) { debugLog("db-close-failed", { error: getErrorMessage(e) }); }
|
|
480
510
|
}
|
|
481
511
|
|
|
482
512
|
if (s.originalBasePath) {
|
|
@@ -496,7 +526,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
|
|
|
496
526
|
}
|
|
497
527
|
|
|
498
528
|
if (s.basePath) {
|
|
499
|
-
try { await rebuildState(s.basePath); } catch (e) { debugLog("stop-rebuild-state-failed", { error:
|
|
529
|
+
try { await rebuildState(s.basePath); } catch (e) { debugLog("stop-rebuild-state-failed", { error: getErrorMessage(e) }); }
|
|
500
530
|
}
|
|
501
531
|
|
|
502
532
|
if (isDebugEnabled()) {
|
|
@@ -635,7 +665,7 @@ export async function startAuto(
|
|
|
635
665
|
}
|
|
636
666
|
} catch (err) {
|
|
637
667
|
ctx.ui.notify(
|
|
638
|
-
`Auto-worktree re-entry failed: ${
|
|
668
|
+
`Auto-worktree re-entry failed: ${getErrorMessage(err)}. Continuing at current path.`,
|
|
639
669
|
"warning",
|
|
640
670
|
);
|
|
641
671
|
}
|
|
@@ -647,13 +677,13 @@ export async function startAuto(
|
|
|
647
677
|
ctx.ui.setFooter(hideFooter);
|
|
648
678
|
ctx.ui.notify(s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
|
|
649
679
|
restoreHookState(s.basePath);
|
|
650
|
-
try { await rebuildState(s.basePath); } catch (e) { debugLog("resume-rebuild-state-failed", { error:
|
|
680
|
+
try { await rebuildState(s.basePath); } catch (e) { debugLog("resume-rebuild-state-failed", { error: getErrorMessage(e) }); }
|
|
651
681
|
try {
|
|
652
682
|
const report = await runGSDDoctor(s.basePath, { fix: true });
|
|
653
683
|
if (report.fixesApplied.length > 0) {
|
|
654
684
|
ctx.ui.notify(`Resume: applied ${report.fixesApplied.length} fix(es) to state.`, "info");
|
|
655
685
|
}
|
|
656
|
-
} catch (e) { debugLog("resume-doctor-failed", { error:
|
|
686
|
+
} catch (e) { debugLog("resume-doctor-failed", { error: getErrorMessage(e) }); }
|
|
657
687
|
await selfHealRuntimeRecords(s.basePath, ctx, s.completedKeySet);
|
|
658
688
|
invalidateAllCaches();
|
|
659
689
|
|
|
@@ -700,7 +730,7 @@ export async function startAuto(
|
|
|
700
730
|
}
|
|
701
731
|
} catch (err) {
|
|
702
732
|
ctx.ui.notify(
|
|
703
|
-
`Secrets check error: ${
|
|
733
|
+
`Secrets check error: ${getErrorMessage(err)}. Continuing without secrets.`,
|
|
704
734
|
"warning",
|
|
705
735
|
);
|
|
706
736
|
}
|
|
@@ -807,7 +837,7 @@ export async function handleAgentEnd(
|
|
|
807
837
|
try {
|
|
808
838
|
await dispatchNextUnit(ctx, pi);
|
|
809
839
|
} catch (dispatchErr) {
|
|
810
|
-
const message =
|
|
840
|
+
const message = getErrorMessage(dispatchErr);
|
|
811
841
|
ctx.ui.notify(
|
|
812
842
|
`Dispatch error after unit completion: ${message}. Retrying in ${DISPATCH_GAP_TIMEOUT_MS / 1000}s.`,
|
|
813
843
|
"error",
|
|
@@ -838,7 +868,7 @@ export async function handleAgentEnd(
|
|
|
838
868
|
clearDispatchGapWatchdog();
|
|
839
869
|
setImmediate(() => {
|
|
840
870
|
handleAgentEnd(ctx, pi).catch((err) => {
|
|
841
|
-
const msg =
|
|
871
|
+
const msg = getErrorMessage(err);
|
|
842
872
|
ctx.ui.notify(`Deferred agent_end retry failed: ${msg}`, "error");
|
|
843
873
|
pauseAuto(ctx, pi).catch(() => {});
|
|
844
874
|
});
|
|
@@ -1086,7 +1116,7 @@ async function dispatchNextUnit(
|
|
|
1086
1116
|
);
|
|
1087
1117
|
} catch (err) {
|
|
1088
1118
|
ctx.ui.notify(
|
|
1089
|
-
`Report generation failed: ${
|
|
1119
|
+
`Report generation failed: ${getErrorMessage(err)}`,
|
|
1090
1120
|
"warning",
|
|
1091
1121
|
);
|
|
1092
1122
|
}
|
|
@@ -1102,35 +1132,17 @@ async function dispatchNextUnit(
|
|
|
1102
1132
|
atomicWriteSync(file, JSON.stringify([]));
|
|
1103
1133
|
}
|
|
1104
1134
|
s.completedKeySet.clear();
|
|
1105
|
-
} catch (e) { debugLog("completed-keys-reset-failed", { error:
|
|
1135
|
+
} catch (e) { debugLog("completed-keys-reset-failed", { error: getErrorMessage(e) }); }
|
|
1106
1136
|
|
|
1107
1137
|
// ── Worktree lifecycle on milestone transition (#616) ──
|
|
1108
|
-
if (isInAutoWorktree(s.basePath)
|
|
1109
|
-
|
|
1110
|
-
const roadmapPath = resolveMilestoneFile(s.originalBasePath, s.currentMilestoneId, "ROADMAP");
|
|
1111
|
-
if (roadmapPath) {
|
|
1112
|
-
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1113
|
-
const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
|
|
1114
|
-
ctx.ui.notify(
|
|
1115
|
-
`Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1116
|
-
"info",
|
|
1117
|
-
);
|
|
1118
|
-
} else {
|
|
1119
|
-
teardownAutoWorktree(s.originalBasePath, s.currentMilestoneId);
|
|
1120
|
-
ctx.ui.notify(`Exited worktree for ${ s.currentMilestoneId } (no roadmap for merge).`, "info");
|
|
1121
|
-
}
|
|
1122
|
-
} catch (err) {
|
|
1123
|
-
ctx.ui.notify(
|
|
1124
|
-
`Milestone merge failed during transition: ${err instanceof Error ? err.message : String(err)}`,
|
|
1125
|
-
"warning",
|
|
1126
|
-
);
|
|
1127
|
-
if (s.originalBasePath) {
|
|
1128
|
-
try { process.chdir(s.originalBasePath); } catch { /* best-effort */ }
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1138
|
+
if ((isInAutoWorktree(s.basePath) || getIsolationMode() === "branch") && shouldUseWorktreeIsolation()) {
|
|
1139
|
+
tryMergeMilestone(ctx, s.currentMilestoneId, "transition");
|
|
1131
1140
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1141
|
+
// Reset to project root and re-derive state for the new milestone
|
|
1142
|
+
if (s.originalBasePath) {
|
|
1143
|
+
s.basePath = s.originalBasePath;
|
|
1144
|
+
s.gitService = createGitService(s.basePath);
|
|
1145
|
+
}
|
|
1134
1146
|
invalidateAllCaches();
|
|
1135
1147
|
|
|
1136
1148
|
state = await deriveState(s.basePath);
|
|
@@ -1146,7 +1158,7 @@ async function dispatchNextUnit(
|
|
|
1146
1158
|
ctx.ui.notify(`Created auto-worktree for ${mid} at ${wtPath}`, "info");
|
|
1147
1159
|
} catch (err) {
|
|
1148
1160
|
ctx.ui.notify(
|
|
1149
|
-
`Auto-worktree creation for ${mid} failed: ${
|
|
1161
|
+
`Auto-worktree creation for ${mid} failed: ${getErrorMessage(err)}. Continuing in project root.`,
|
|
1150
1162
|
"warning",
|
|
1151
1163
|
);
|
|
1152
1164
|
}
|
|
@@ -1175,51 +1187,8 @@ async function dispatchNextUnit(
|
|
|
1175
1187
|
const incomplete = (state.registry ?? []).filter(m => m.status !== "complete" && m.status !== "parked");
|
|
1176
1188
|
if (incomplete.length === 0) {
|
|
1177
1189
|
// Genuinely all complete (parked milestones excluded) — merge milestone branch to main before stopping (#962)
|
|
1178
|
-
if (s.currentMilestoneId
|
|
1179
|
-
|
|
1180
|
-
const roadmapPath = resolveMilestoneFile(s.originalBasePath, s.currentMilestoneId, "ROADMAP");
|
|
1181
|
-
if (roadmapPath) {
|
|
1182
|
-
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1183
|
-
const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
|
|
1184
|
-
s.basePath = s.originalBasePath;
|
|
1185
|
-
s.gitService = createGitService(s.basePath);
|
|
1186
|
-
ctx.ui.notify(
|
|
1187
|
-
`Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1188
|
-
"info",
|
|
1189
|
-
);
|
|
1190
|
-
}
|
|
1191
|
-
} catch (err) {
|
|
1192
|
-
ctx.ui.notify(
|
|
1193
|
-
`Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1194
|
-
"warning",
|
|
1195
|
-
);
|
|
1196
|
-
if (s.originalBasePath) {
|
|
1197
|
-
s.basePath = s.originalBasePath;
|
|
1198
|
-
try { process.chdir(s.basePath); } catch { /* best-effort */ }
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
} else if (s.currentMilestoneId && !isInAutoWorktree(s.basePath) && getIsolationMode() === "branch") {
|
|
1202
|
-
try {
|
|
1203
|
-
const currentBranch = getCurrentBranch(s.basePath);
|
|
1204
|
-
const milestoneBranch = autoWorktreeBranch(s.currentMilestoneId);
|
|
1205
|
-
if (currentBranch === milestoneBranch) {
|
|
1206
|
-
const roadmapPath = resolveMilestoneFile(s.basePath, s.currentMilestoneId, "ROADMAP");
|
|
1207
|
-
if (roadmapPath) {
|
|
1208
|
-
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1209
|
-
const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
|
|
1210
|
-
s.gitService = createGitService(s.basePath);
|
|
1211
|
-
ctx.ui.notify(
|
|
1212
|
-
`Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1213
|
-
"info",
|
|
1214
|
-
);
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
} catch (err) {
|
|
1218
|
-
ctx.ui.notify(
|
|
1219
|
-
`Milestone merge failed (branch mode): ${err instanceof Error ? err.message : String(err)}`,
|
|
1220
|
-
"warning",
|
|
1221
|
-
);
|
|
1222
|
-
}
|
|
1190
|
+
if (s.currentMilestoneId) {
|
|
1191
|
+
tryMergeMilestone(ctx, s.currentMilestoneId, "complete");
|
|
1223
1192
|
}
|
|
1224
1193
|
sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
|
|
1225
1194
|
await stopAuto(ctx, pi, "All milestones complete");
|
|
@@ -1276,52 +1245,10 @@ async function dispatchNextUnit(
|
|
|
1276
1245
|
atomicWriteSync(file, JSON.stringify([]));
|
|
1277
1246
|
}
|
|
1278
1247
|
s.completedKeySet.clear();
|
|
1279
|
-
} catch (e) { debugLog("completed-keys-reset-failed", { error:
|
|
1248
|
+
} catch (e) { debugLog("completed-keys-reset-failed", { error: getErrorMessage(e) }); }
|
|
1280
1249
|
// ── Milestone merge ──
|
|
1281
|
-
if (s.currentMilestoneId
|
|
1282
|
-
|
|
1283
|
-
const roadmapPath = resolveMilestoneFile(s.originalBasePath, s.currentMilestoneId, "ROADMAP");
|
|
1284
|
-
if (!roadmapPath) throw new GSDError(GSD_ARTIFACT_MISSING, `Cannot resolve ROADMAP file for milestone ${ s.currentMilestoneId }`);
|
|
1285
|
-
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1286
|
-
const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
|
|
1287
|
-
s.basePath = s.originalBasePath;
|
|
1288
|
-
s.gitService = createGitService(s.basePath);
|
|
1289
|
-
ctx.ui.notify(
|
|
1290
|
-
`Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1291
|
-
"info",
|
|
1292
|
-
);
|
|
1293
|
-
} catch (err) {
|
|
1294
|
-
ctx.ui.notify(
|
|
1295
|
-
`Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1296
|
-
"warning",
|
|
1297
|
-
);
|
|
1298
|
-
if (s.originalBasePath) {
|
|
1299
|
-
s.basePath = s.originalBasePath;
|
|
1300
|
-
try { process.chdir(s.basePath); } catch { /* best-effort */ }
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
} else if (s.currentMilestoneId && !isInAutoWorktree(s.basePath) && getIsolationMode() === "branch") {
|
|
1304
|
-
try {
|
|
1305
|
-
const currentBranch = getCurrentBranch(s.basePath);
|
|
1306
|
-
const milestoneBranch = autoWorktreeBranch(s.currentMilestoneId);
|
|
1307
|
-
if (currentBranch === milestoneBranch) {
|
|
1308
|
-
const roadmapPath = resolveMilestoneFile(s.basePath, s.currentMilestoneId, "ROADMAP");
|
|
1309
|
-
if (roadmapPath) {
|
|
1310
|
-
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1311
|
-
const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
|
|
1312
|
-
s.gitService = createGitService(s.basePath);
|
|
1313
|
-
ctx.ui.notify(
|
|
1314
|
-
`Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1315
|
-
"info",
|
|
1316
|
-
);
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
} catch (err) {
|
|
1320
|
-
ctx.ui.notify(
|
|
1321
|
-
`Milestone merge failed (branch mode): ${err instanceof Error ? err.message : String(err)}`,
|
|
1322
|
-
"warning",
|
|
1323
|
-
);
|
|
1324
|
-
}
|
|
1250
|
+
if (s.currentMilestoneId) {
|
|
1251
|
+
tryMergeMilestone(ctx, s.currentMilestoneId, "complete");
|
|
1325
1252
|
}
|
|
1326
1253
|
sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
|
|
1327
1254
|
await stopAuto(ctx, pi, `Milestone ${mid} complete`);
|
|
@@ -1417,7 +1344,7 @@ async function dispatchNextUnit(
|
|
|
1417
1344
|
}
|
|
1418
1345
|
} catch (err) {
|
|
1419
1346
|
ctx.ui.notify(
|
|
1420
|
-
`Secrets collection error: ${
|
|
1347
|
+
`Secrets collection error: ${getErrorMessage(err)}. Continuing with next task.`,
|
|
1421
1348
|
"warning",
|
|
1422
1349
|
);
|
|
1423
1350
|
}
|
|
@@ -1628,7 +1555,7 @@ async function dispatchNextUnit(
|
|
|
1628
1555
|
);
|
|
1629
1556
|
result = await Promise.race([sessionPromise, timeoutPromise]);
|
|
1630
1557
|
} catch (sessionErr) {
|
|
1631
|
-
const msg =
|
|
1558
|
+
const msg = getErrorMessage(sessionErr);
|
|
1632
1559
|
ctx.ui.notify(`Session creation failed: ${msg}. Retrying via watchdog.`, "error");
|
|
1633
1560
|
throw new Error(`newSession() failed: ${msg}`);
|
|
1634
1561
|
}
|
|
@@ -1704,7 +1631,7 @@ async function dispatchNextUnit(
|
|
|
1704
1631
|
const { reorderForCaching } = await import("./prompt-ordering.js");
|
|
1705
1632
|
finalPrompt = reorderForCaching(finalPrompt);
|
|
1706
1633
|
} catch (reorderErr) {
|
|
1707
|
-
const msg =
|
|
1634
|
+
const msg = getErrorMessage(reorderErr);
|
|
1708
1635
|
process.stderr.write(`[gsd] prompt reorder failed (non-fatal): ${msg}\n`);
|
|
1709
1636
|
}
|
|
1710
1637
|
|
|
@@ -1747,8 +1674,7 @@ async function dispatchNextUnit(
|
|
|
1747
1674
|
function ensurePreconditions(
|
|
1748
1675
|
unitType: string, unitId: string, base: string, state: GSDState,
|
|
1749
1676
|
): void {
|
|
1750
|
-
const
|
|
1751
|
-
const mid = parts[0]!;
|
|
1677
|
+
const { milestone: mid } = parseUnitId(unitId);
|
|
1752
1678
|
|
|
1753
1679
|
const mDir = resolveMilestonePath(base, mid);
|
|
1754
1680
|
if (!mDir) {
|
|
@@ -1756,8 +1682,8 @@ function ensurePreconditions(
|
|
|
1756
1682
|
mkdirSync(join(newDir, "slices"), { recursive: true });
|
|
1757
1683
|
}
|
|
1758
1684
|
|
|
1759
|
-
|
|
1760
|
-
|
|
1685
|
+
const sid = parseUnitId(unitId).slice;
|
|
1686
|
+
if (sid) {
|
|
1761
1687
|
|
|
1762
1688
|
const mDirResolved = resolveMilestonePath(base, mid);
|
|
1763
1689
|
if (mDirResolved) {
|