gsd-pi 2.32.0 → 2.33.0-dev.bafba33
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 +27 -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-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 +5 -4
- 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/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 +12 -1
- package/dist/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +14 -11
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/loop-regression.test.ts +839 -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-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 +5 -4
- 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/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 +12 -1
- package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +14 -11
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/loop-regression.test.ts +839 -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
|
@@ -98,11 +98,24 @@ export function isLockProcessAlive(lock: LockData): boolean {
|
|
|
98
98
|
|
|
99
99
|
/** Format crash info for display or injection into a prompt. */
|
|
100
100
|
export function formatCrashInfo(lock: LockData): string {
|
|
101
|
-
|
|
101
|
+
const lines = [
|
|
102
102
|
`Previous auto-mode session was interrupted.`,
|
|
103
103
|
` Was executing: ${lock.unitType} (${lock.unitId})`,
|
|
104
104
|
` Started at: ${lock.unitStartedAt}`,
|
|
105
105
|
` Units completed before crash: ${lock.completedUnits}`,
|
|
106
106
|
` PID: ${lock.pid}`,
|
|
107
|
-
]
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
// Add recovery guidance based on what was happening when it crashed
|
|
110
|
+
if (lock.unitType === "starting" && lock.unitId === "bootstrap" && lock.completedUnits === 0) {
|
|
111
|
+
lines.push(`No work was lost. Run /gsd auto to restart.`);
|
|
112
|
+
} else if (lock.unitType.includes("research") || lock.unitType.includes("plan")) {
|
|
113
|
+
lines.push(`The ${lock.unitType} unit may be incomplete. Run /gsd auto to re-run it.`);
|
|
114
|
+
} else if (lock.unitType.includes("execute")) {
|
|
115
|
+
lines.push(`Task execution was interrupted. Run /gsd auto to resume — completed work is preserved.`);
|
|
116
|
+
} else if (lock.unitType.includes("complete")) {
|
|
117
|
+
lines.push(`Slice/milestone completion was interrupted. Run /gsd auto to finish.`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return lines.join("\n");
|
|
108
121
|
}
|
|
@@ -5,6 +5,7 @@ import { readdirSync } from "node:fs";
|
|
|
5
5
|
import { resolveMilestoneFile, milestonesDir } from "./paths.js";
|
|
6
6
|
import { parseRoadmapSlices } from "./roadmap-slices.js";
|
|
7
7
|
import { findMilestoneIds } from "./guided-flow.js";
|
|
8
|
+
import { parseUnitId } from "./unit-id.js";
|
|
8
9
|
|
|
9
10
|
const SLICE_DISPATCH_TYPES = new Set([
|
|
10
11
|
"research-slice",
|
|
@@ -39,7 +40,7 @@ function readRoadmapFromDisk(base: string, milestoneId: string): string | null {
|
|
|
39
40
|
export function getPriorSliceCompletionBlocker(base: string, _mainBranch: string, unitType: string, unitId: string): string | null {
|
|
40
41
|
if (!SLICE_DISPATCH_TYPES.has(unitType)) return null;
|
|
41
42
|
|
|
42
|
-
const
|
|
43
|
+
const { milestone: targetMid, slice: targetSid } = parseUnitId(unitId);
|
|
43
44
|
if (!targetMid || !targetSid) return null;
|
|
44
45
|
|
|
45
46
|
// Use findMilestoneIds to respect custom queue order.
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import type { UnitMetrics } from "./metrics.js";
|
|
13
13
|
import { gsdRoot } from "./paths.js";
|
|
14
14
|
import { formatDuration, fileLink } from "../shared/mod.js";
|
|
15
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Open a file in the user's default browser.
|
|
@@ -226,7 +227,7 @@ export async function handleExport(args: string, ctx: ExtensionCommandContext, b
|
|
|
226
227
|
}
|
|
227
228
|
} catch (err) {
|
|
228
229
|
ctx.ui.notify(
|
|
229
|
-
`HTML export failed: ${
|
|
230
|
+
`HTML export failed: ${getErrorMessage(err)}`,
|
|
230
231
|
"error",
|
|
231
232
|
);
|
|
232
233
|
}
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
nativeAddPaths,
|
|
34
34
|
} from "./native-git-bridge.js";
|
|
35
35
|
import { GSDError, GSD_MERGE_CONFLICT, GSD_GIT_ERROR } from "./errors.js";
|
|
36
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
36
37
|
|
|
37
38
|
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
38
39
|
|
|
@@ -281,7 +282,7 @@ export function runGit(basePath: string, args: string[], options: { allowFailure
|
|
|
281
282
|
}).trim();
|
|
282
283
|
} catch (error) {
|
|
283
284
|
if (options.allowFailure) return "";
|
|
284
|
-
const message =
|
|
285
|
+
const message = getErrorMessage(error);
|
|
285
286
|
throw new GSDError(GSD_GIT_ERROR, `git ${args.join(" ")} failed in ${basePath}: ${filterGitSvnNoise(message)}`);
|
|
286
287
|
}
|
|
287
288
|
}
|
|
@@ -533,7 +534,7 @@ export class GitServiceImpl {
|
|
|
533
534
|
execSync(command, { cwd: this.basePath, stdio: "pipe", encoding: "utf-8" });
|
|
534
535
|
return { passed: true, skipped: false, command };
|
|
535
536
|
} catch (err) {
|
|
536
|
-
const msg =
|
|
537
|
+
const msg = getErrorMessage(err);
|
|
537
538
|
return { passed: false, skipped: false, command, error: msg };
|
|
538
539
|
}
|
|
539
540
|
}
|
|
@@ -44,6 +44,7 @@ export {
|
|
|
44
44
|
showQueue, handleQueueReorder, showQueueAdd,
|
|
45
45
|
buildExistingMilestonesContext,
|
|
46
46
|
} from "./guided-flow-queue.js";
|
|
47
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
47
48
|
|
|
48
49
|
// ─── Commit Instruction Helpers ──────────────────────────────────────────────
|
|
49
50
|
|
|
@@ -158,9 +159,9 @@ export function checkAutoStartAfterDiscuss(): boolean {
|
|
|
158
159
|
|
|
159
160
|
pendingAutoStart = null;
|
|
160
161
|
startAuto(ctx, pi, basePath, false, { step }).catch((err) => {
|
|
161
|
-
ctx.ui.notify(`Auto-start failed: ${
|
|
162
|
+
ctx.ui.notify(`Auto-start failed: ${getErrorMessage(err)}`, "error");
|
|
162
163
|
if (process.env.GSD_DEBUG) console.error('[gsd] auto start error:', err);
|
|
163
|
-
debugLog("auto-start-failed", { error:
|
|
164
|
+
debugLog("auto-start-failed", { error: getErrorMessage(err) });
|
|
164
165
|
});
|
|
165
166
|
return true;
|
|
166
167
|
}
|
|
@@ -64,6 +64,7 @@ import { pauseAutoForProviderError, classifyProviderError } from "./provider-err
|
|
|
64
64
|
import { toPosixPath } from "../shared/mod.js";
|
|
65
65
|
import { isParallelActive, shutdownParallel } from "./parallel-orchestrator.js";
|
|
66
66
|
import { DEFAULT_BASH_TIMEOUT_SECS } from "./constants.js";
|
|
67
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
67
68
|
|
|
68
69
|
/**
|
|
69
70
|
* Ensure the GSD database is available, auto-initializing if needed.
|
|
@@ -374,7 +375,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
374
375
|
details: { operation: "save_decision", id },
|
|
375
376
|
};
|
|
376
377
|
} catch (err) {
|
|
377
|
-
const msg =
|
|
378
|
+
const msg = getErrorMessage(err);
|
|
378
379
|
process.stderr.write(`gsd-db: gsd_save_decision tool failed: ${msg}\n`);
|
|
379
380
|
return {
|
|
380
381
|
content: [{ type: "text" as const, text: `Error saving decision: ${msg}` }],
|
|
@@ -445,7 +446,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
445
446
|
details: { operation: "update_requirement", id: params.id },
|
|
446
447
|
};
|
|
447
448
|
} catch (err) {
|
|
448
|
-
const msg =
|
|
449
|
+
const msg = getErrorMessage(err);
|
|
449
450
|
process.stderr.write(`gsd-db: gsd_update_requirement tool failed: ${msg}\n`);
|
|
450
451
|
return {
|
|
451
452
|
content: [{ type: "text" as const, text: `Error updating requirement: ${msg}` }],
|
|
@@ -525,7 +526,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
525
526
|
details: { operation: "save_summary", path: relativePath, artifact_type: params.artifact_type },
|
|
526
527
|
};
|
|
527
528
|
} catch (err) {
|
|
528
|
-
const msg =
|
|
529
|
+
const msg = getErrorMessage(err);
|
|
529
530
|
process.stderr.write(`gsd-db: gsd_save_summary tool failed: ${msg}\n`);
|
|
530
531
|
return {
|
|
531
532
|
content: [{ type: "text" as const, text: `Error saving artifact: ${msg}` }],
|
|
@@ -574,7 +575,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
574
575
|
details: { operation: "generate_milestone_id", id: newId, existingCount: existingIds.length, reservedCount: reservedMilestoneIds.size, uniqueEnabled },
|
|
575
576
|
};
|
|
576
577
|
} catch (err) {
|
|
577
|
-
const msg =
|
|
578
|
+
const msg = getErrorMessage(err);
|
|
578
579
|
return {
|
|
579
580
|
content: [{ type: "text" as const, text: `Error generating milestone ID: ${msg}` }],
|
|
580
581
|
isError: true,
|
|
@@ -795,6 +796,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
795
796
|
|
|
796
797
|
// ── agent_end: auto-mode advancement or auto-start after discuss ───────────
|
|
797
798
|
pi.on("agent_end", async (event, ctx: ExtensionContext) => {
|
|
799
|
+
// Clean up quick-task branch if one just completed (#1269)
|
|
800
|
+
try {
|
|
801
|
+
const { cleanupQuickBranch } = await import("./quick.js");
|
|
802
|
+
cleanupQuickBranch();
|
|
803
|
+
} catch { /* non-fatal */ }
|
|
804
|
+
|
|
798
805
|
// If discuss phase just finished, start auto-mode
|
|
799
806
|
if (checkAutoStartAfterDiscuss()) {
|
|
800
807
|
depthVerifiedMilestones.clear();
|
|
@@ -987,7 +994,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
987
994
|
} catch (err) {
|
|
988
995
|
// Safety net: if handleAgentEnd throws despite its internal try-catch,
|
|
989
996
|
// ensure auto-mode stops gracefully instead of silently stalling (#381).
|
|
990
|
-
const message =
|
|
997
|
+
const message = getErrorMessage(err);
|
|
991
998
|
ctx.ui.notify(
|
|
992
999
|
`Auto-mode error in agent_end handler: ${message}. Stopping auto-mode.`,
|
|
993
1000
|
"error",
|
|
@@ -16,6 +16,7 @@ import { getEnvApiKey } from "@gsd/pi-ai";
|
|
|
16
16
|
import { existsSync, statSync, chmodSync } from "node:fs";
|
|
17
17
|
import { join, dirname } from "node:path";
|
|
18
18
|
import { mkdirSync } from "node:fs";
|
|
19
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
19
20
|
|
|
20
21
|
// ─── Provider Registry ─────────────────────────────────────────────────────────
|
|
21
22
|
|
|
@@ -552,7 +553,7 @@ export async function testProviderKey(
|
|
|
552
553
|
return { provider, status: "error", message: `HTTP ${res.status}`, latencyMs };
|
|
553
554
|
} catch (err) {
|
|
554
555
|
const latencyMs = Date.now() - start;
|
|
555
|
-
const msg =
|
|
556
|
+
const msg = getErrorMessage(err);
|
|
556
557
|
if (msg.includes("timeout") || msg.includes("AbortError")) {
|
|
557
558
|
return { provider, status: "error", message: "timeout (15s)", latencyMs };
|
|
558
559
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import * as fs from 'node:fs';
|
|
18
18
|
import * as path from 'node:path';
|
|
19
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
19
20
|
|
|
20
21
|
// ============================================================================
|
|
21
22
|
// Type Definitions
|
|
@@ -194,7 +195,7 @@ export function parseMarketplaceJson(repoRoot: string):
|
|
|
194
195
|
} catch (err) {
|
|
195
196
|
return {
|
|
196
197
|
success: false,
|
|
197
|
-
error: `Failed to read marketplace.json: ${
|
|
198
|
+
error: `Failed to read marketplace.json: ${getErrorMessage(err)}`
|
|
198
199
|
};
|
|
199
200
|
}
|
|
200
201
|
|
|
@@ -204,7 +205,7 @@ export function parseMarketplaceJson(repoRoot: string):
|
|
|
204
205
|
} catch (err) {
|
|
205
206
|
return {
|
|
206
207
|
success: false,
|
|
207
|
-
error: `Failed to parse marketplace.json: ${
|
|
208
|
+
error: `Failed to parse marketplace.json: ${getErrorMessage(err)}`
|
|
208
209
|
};
|
|
209
210
|
}
|
|
210
211
|
|
|
@@ -293,7 +294,7 @@ export function inspectPlugin(
|
|
|
293
294
|
}
|
|
294
295
|
} catch (err) {
|
|
295
296
|
// Fall back to marketplace inline or derived
|
|
296
|
-
result.error = `Failed to parse plugin.json: ${
|
|
297
|
+
result.error = `Failed to parse plugin.json: ${getErrorMessage(err)}`;
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
300
|
|
|
@@ -18,6 +18,7 @@ import type { ExtensionContext } from "@gsd/pi-coding-agent";
|
|
|
18
18
|
import { gsdRoot } from "./paths.js";
|
|
19
19
|
import { getAndClearSkills } from "./skill-telemetry.js";
|
|
20
20
|
import { loadJsonFile, loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
|
|
21
|
+
import { parseUnitId } from "./unit-id.js";
|
|
21
22
|
|
|
22
23
|
// Re-export from shared — canonical implementation lives in format-utils.
|
|
23
24
|
export { formatTokenCount } from "../shared/mod.js";
|
|
@@ -290,9 +291,8 @@ export function aggregateByPhase(units: UnitMetrics[]): PhaseAggregate[] {
|
|
|
290
291
|
export function aggregateBySlice(units: UnitMetrics[]): SliceAggregate[] {
|
|
291
292
|
const map = new Map<string, SliceAggregate>();
|
|
292
293
|
for (const u of units) {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
const sliceId = parts.length >= 2 ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
294
|
+
const { milestone, slice } = parseUnitId(u.id);
|
|
295
|
+
const sliceId = slice ? `${milestone}/${slice}` : milestone;
|
|
296
296
|
let agg = map.get(sliceId);
|
|
297
297
|
if (!agg) {
|
|
298
298
|
agg = { sliceId, units: 0, tokens: emptyTokens(), cost: 0, duration: 0 };
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, cpSync, rmSync, symlinkSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { externalGsdRoot } from "./repo-identity.js";
|
|
12
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
12
13
|
|
|
13
14
|
export interface MigrationResult {
|
|
14
15
|
migrated: boolean;
|
|
@@ -47,7 +48,7 @@ export function migrateToExternalState(basePath: string): MigrationResult {
|
|
|
47
48
|
return { migrated: false, error: ".gsd exists but is not a directory or symlink" };
|
|
48
49
|
}
|
|
49
50
|
} catch (err) {
|
|
50
|
-
return { migrated: false, error: `Cannot stat .gsd: ${
|
|
51
|
+
return { migrated: false, error: `Cannot stat .gsd: ${getErrorMessage(err)}` };
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
const externalPath = externalGsdRoot(basePath);
|
|
@@ -57,8 +58,24 @@ export function migrateToExternalState(basePath: string): MigrationResult {
|
|
|
57
58
|
// mkdir -p the external dir
|
|
58
59
|
mkdirSync(externalPath, { recursive: true });
|
|
59
60
|
|
|
60
|
-
// Rename .gsd -> .gsd.migrating (atomic lock)
|
|
61
|
-
|
|
61
|
+
// Rename .gsd -> .gsd.migrating (atomic lock).
|
|
62
|
+
// On Windows, NTFS may reject rename with EPERM if file descriptors are
|
|
63
|
+
// open (VS Code watchers, antivirus on-access scan). Fall back to
|
|
64
|
+
// copy+delete (#1292).
|
|
65
|
+
try {
|
|
66
|
+
renameSync(localGsd, migratingPath);
|
|
67
|
+
} catch (renameErr: any) {
|
|
68
|
+
if (renameErr?.code === "EPERM" || renameErr?.code === "EBUSY") {
|
|
69
|
+
try {
|
|
70
|
+
cpSync(localGsd, migratingPath, { recursive: true, force: true });
|
|
71
|
+
rmSync(localGsd, { recursive: true, force: true });
|
|
72
|
+
} catch (copyErr) {
|
|
73
|
+
return { migrated: false, error: `Migration rename/copy failed: ${copyErr instanceof Error ? copyErr.message : String(copyErr)}` };
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
throw renameErr;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
62
79
|
|
|
63
80
|
// Copy contents to external dir, skipping worktrees/
|
|
64
81
|
const entries = readdirSync(migratingPath, { withFileTypes: true });
|
|
@@ -98,7 +115,7 @@ export function migrateToExternalState(basePath: string): MigrationResult {
|
|
|
98
115
|
|
|
99
116
|
return {
|
|
100
117
|
migrated: false,
|
|
101
|
-
error: `Migration failed: ${
|
|
118
|
+
error: `Migration failed: ${getErrorMessage(err)}`,
|
|
102
119
|
};
|
|
103
120
|
}
|
|
104
121
|
}
|
|
@@ -9,6 +9,7 @@ import { randomInt } from "node:crypto";
|
|
|
9
9
|
import { readdirSync, existsSync } from "node:fs";
|
|
10
10
|
import { milestonesDir } from "./paths.js";
|
|
11
11
|
import { loadQueueOrder, sortByQueueOrder } from "./queue-order.js";
|
|
12
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
12
13
|
|
|
13
14
|
// ─── Regex ──────────────────────────────────────────────────────────────────
|
|
14
15
|
|
|
@@ -88,7 +89,7 @@ export function findMilestoneIds(basePath: string): string[] {
|
|
|
88
89
|
} catch (err) {
|
|
89
90
|
// Log why milestone scanning failed — silent [] here causes infinite loops (#456)
|
|
90
91
|
if (existsSync(dir)) {
|
|
91
|
-
console.error(`[gsd] findMilestoneIds: .gsd/milestones/ exists but readdirSync failed — ${
|
|
92
|
+
console.error(`[gsd] findMilestoneIds: .gsd/milestones/ exists but readdirSync failed — ${getErrorMessage(err)}`);
|
|
92
93
|
}
|
|
93
94
|
return [];
|
|
94
95
|
}
|
|
@@ -10,6 +10,7 @@ import { existsSync, readFileSync, unlinkSync, rmSync } from "node:fs";
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { GSDError, GSD_GIT_ERROR } from "./errors.js";
|
|
12
12
|
import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
|
|
13
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
13
14
|
|
|
14
15
|
// Issue #453: keep auto-mode bookkeeping on the stable git CLI path unless a
|
|
15
16
|
// caller explicitly opts into the native helper.
|
|
@@ -716,7 +717,7 @@ export function nativeCommit(
|
|
|
716
717
|
try {
|
|
717
718
|
return native.gitCommit(basePath, message, options?.allowEmpty);
|
|
718
719
|
} catch (e) {
|
|
719
|
-
const msg =
|
|
720
|
+
const msg = getErrorMessage(e);
|
|
720
721
|
if (msg.includes("nothing to commit")) return null;
|
|
721
722
|
throw e;
|
|
722
723
|
}
|
|
@@ -11,6 +11,7 @@ import { mergeMilestoneToMain } from "./auto-worktree.js";
|
|
|
11
11
|
import { MergeConflictError } from "./git-service.js";
|
|
12
12
|
import { removeSessionStatus } from "./session-status-io.js";
|
|
13
13
|
import type { WorkerInfo } from "./parallel-orchestrator.js";
|
|
14
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
14
15
|
|
|
15
16
|
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
16
17
|
|
|
@@ -99,7 +100,7 @@ export async function mergeCompletedMilestone(
|
|
|
99
100
|
return {
|
|
100
101
|
milestoneId,
|
|
101
102
|
success: false,
|
|
102
|
-
error:
|
|
103
|
+
error: getErrorMessage(err),
|
|
103
104
|
};
|
|
104
105
|
}
|
|
105
106
|
}
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
analyzeParallelEligibility,
|
|
39
39
|
type ParallelCandidates,
|
|
40
40
|
} from "./parallel-eligibility.js";
|
|
41
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
41
42
|
|
|
42
43
|
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
43
44
|
|
|
@@ -363,7 +364,7 @@ export async function startParallel(
|
|
|
363
364
|
|
|
364
365
|
started.push(mid);
|
|
365
366
|
} catch (err) {
|
|
366
|
-
const message =
|
|
367
|
+
const message = getErrorMessage(err);
|
|
367
368
|
errors.push({ mid, error: message });
|
|
368
369
|
}
|
|
369
370
|
}
|
|
@@ -15,6 +15,7 @@ import { resolvePostUnitHooks, resolvePreDispatchHooks } from "./preferences.js"
|
|
|
15
15
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { gsdRoot } from "./paths.js";
|
|
18
|
+
import { parseUnitId } from "./unit-id.js";
|
|
18
19
|
|
|
19
20
|
// ─── Hook Queue State ──────────────────────────────────────────────────────
|
|
20
21
|
|
|
@@ -149,7 +150,7 @@ function dequeueNextHook(basePath: string): HookDispatchResult | null {
|
|
|
149
150
|
};
|
|
150
151
|
|
|
151
152
|
// Build the prompt with variable substitution
|
|
152
|
-
const
|
|
153
|
+
const { milestone: mid, slice: sid, task: tid } = parseUnitId(triggerUnitId);
|
|
153
154
|
const prompt = config.prompt
|
|
154
155
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
155
156
|
.replace(/\{sliceId\}/g, sid ?? "")
|
|
@@ -208,16 +209,14 @@ function handleHookCompletion(basePath: string): HookDispatchResult | null {
|
|
|
208
209
|
* - Milestone-level (M001): .gsd/M001/{artifact}
|
|
209
210
|
*/
|
|
210
211
|
export function resolveHookArtifactPath(basePath: string, unitId: string, artifactName: string): string {
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
213
|
-
const [mid, sid, tid] = parts;
|
|
212
|
+
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
213
|
+
if (mid && sid && tid) {
|
|
214
214
|
return join(gsdRoot(basePath), mid, "slices", sid, "tasks", `${tid}-${artifactName}`);
|
|
215
215
|
}
|
|
216
|
-
if (
|
|
217
|
-
const [mid, sid] = parts;
|
|
216
|
+
if (mid && sid) {
|
|
218
217
|
return join(gsdRoot(basePath), mid, "slices", sid, artifactName);
|
|
219
218
|
}
|
|
220
|
-
return join(gsdRoot(basePath),
|
|
219
|
+
return join(gsdRoot(basePath), mid, artifactName);
|
|
221
220
|
}
|
|
222
221
|
|
|
223
222
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -253,7 +252,7 @@ export function runPreDispatchHooks(
|
|
|
253
252
|
return { action: "proceed", prompt, firedHooks: [] };
|
|
254
253
|
}
|
|
255
254
|
|
|
256
|
-
const
|
|
255
|
+
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
257
256
|
const substitute = (text: string): string =>
|
|
258
257
|
text
|
|
259
258
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
@@ -466,7 +465,7 @@ export function triggerHookManually(
|
|
|
466
465
|
activeHook.cycle = currentCycle;
|
|
467
466
|
|
|
468
467
|
// Build the prompt with variable substitution
|
|
469
|
-
const
|
|
468
|
+
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
470
469
|
const prompt = hook.prompt
|
|
471
470
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
472
471
|
.replace(/\{sliceId\}/g, sid ?? "")
|
|
@@ -15,6 +15,7 @@ import { join } from "node:path";
|
|
|
15
15
|
import { loadPrompt } from "./prompt-loader.js";
|
|
16
16
|
import { gsdRoot } from "./paths.js";
|
|
17
17
|
import { createGitService, runGit } from "./git-service.js";
|
|
18
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
18
19
|
|
|
19
20
|
// ─── Quick Task Helpers ───────────────────────────────────────────────────────
|
|
20
21
|
|
|
@@ -107,10 +108,11 @@ export async function handleQuick(
|
|
|
107
108
|
const skipBranch = git.prefs.isolation === "none";
|
|
108
109
|
|
|
109
110
|
let branchCreated = false;
|
|
111
|
+
let originalBranch: string | undefined;
|
|
110
112
|
if (!skipBranch) {
|
|
111
113
|
try {
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
+
originalBranch = git.getCurrentBranch();
|
|
115
|
+
if (originalBranch !== branchName) {
|
|
114
116
|
// Auto-commit any dirty state before switching
|
|
115
117
|
try {
|
|
116
118
|
git.autoCommit("quick-task", `Q${taskNum}`, []);
|
|
@@ -121,7 +123,7 @@ export async function handleQuick(
|
|
|
121
123
|
}
|
|
122
124
|
} catch (err) {
|
|
123
125
|
// Branch creation failed — continue on current branch
|
|
124
|
-
const message =
|
|
126
|
+
const message = getErrorMessage(err);
|
|
125
127
|
ctx.ui.notify(`Could not create branch ${branchName}: ${message}. Working on current branch.`, "warning");
|
|
126
128
|
}
|
|
127
129
|
}
|
|
@@ -154,4 +156,57 @@ export async function handleQuick(
|
|
|
154
156
|
},
|
|
155
157
|
{ triggerTurn: true },
|
|
156
158
|
);
|
|
159
|
+
|
|
160
|
+
// Schedule branch merge-back after the quick task agent session ends.
|
|
161
|
+
// Without this, auto-mode resumes on the quick-task branch (#1269).
|
|
162
|
+
if (branchCreated && originalBranch) {
|
|
163
|
+
_pendingQuickBranchReturn = {
|
|
164
|
+
basePath,
|
|
165
|
+
originalBranch,
|
|
166
|
+
quickBranch: branchName,
|
|
167
|
+
taskNum,
|
|
168
|
+
slug,
|
|
169
|
+
description,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Pending quick-task branch return — consumed by cleanupQuickBranch(). */
|
|
175
|
+
let _pendingQuickBranchReturn: {
|
|
176
|
+
basePath: string;
|
|
177
|
+
originalBranch: string;
|
|
178
|
+
quickBranch: string;
|
|
179
|
+
taskNum: number;
|
|
180
|
+
slug: string;
|
|
181
|
+
description: string;
|
|
182
|
+
} | null = null;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Merge the quick-task branch back to the original branch and switch.
|
|
186
|
+
* Called from the agent_end handler after a quick task completes.
|
|
187
|
+
* Returns true if a branch return was performed.
|
|
188
|
+
*/
|
|
189
|
+
export function cleanupQuickBranch(): boolean {
|
|
190
|
+
if (!_pendingQuickBranchReturn) return false;
|
|
191
|
+
const { basePath, originalBranch, quickBranch, taskNum, slug, description } = _pendingQuickBranchReturn;
|
|
192
|
+
_pendingQuickBranchReturn = null;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
// Auto-commit any remaining work
|
|
196
|
+
try { runGit(basePath, ["add", "-A"]); } catch {}
|
|
197
|
+
try { runGit(basePath, ["commit", "-m", `quick(Q${taskNum}): ${slug}`]); } catch {}
|
|
198
|
+
|
|
199
|
+
// Switch back and merge
|
|
200
|
+
runGit(basePath, ["checkout", originalBranch]);
|
|
201
|
+
try {
|
|
202
|
+
runGit(basePath, ["merge", "--squash", quickBranch]);
|
|
203
|
+
runGit(basePath, ["commit", "-m", `quick(Q${taskNum}): ${description.slice(0, 72)}`]);
|
|
204
|
+
} catch { /* merge conflict or nothing — non-fatal */ }
|
|
205
|
+
|
|
206
|
+
// Clean up quick branch
|
|
207
|
+
try { runGit(basePath, ["branch", "-D", quickBranch]); } catch {}
|
|
208
|
+
return true;
|
|
209
|
+
} catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
157
212
|
}
|
|
@@ -10,7 +10,7 @@ import { createHash } from "node:crypto";
|
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
11
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, symlinkSync } from "node:fs";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
|
-
import { join, resolve } from "node:path";
|
|
13
|
+
import { join, resolve, sep } from "node:path";
|
|
14
14
|
|
|
15
15
|
// ─── Repo Identity ──────────────────────────────────────────────────────────
|
|
16
16
|
|
|
@@ -37,6 +37,27 @@ function getRemoteUrl(basePath: string): string {
|
|
|
37
37
|
*/
|
|
38
38
|
function resolveGitRoot(basePath: string): string {
|
|
39
39
|
try {
|
|
40
|
+
// In a worktree, --show-toplevel returns the worktree path, not the main
|
|
41
|
+
// repo root. Use --git-common-dir to find the shared .git directory,
|
|
42
|
+
// then derive the main repo root from it (#1288).
|
|
43
|
+
const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
44
|
+
cwd: basePath,
|
|
45
|
+
encoding: "utf-8",
|
|
46
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
47
|
+
timeout: 5_000,
|
|
48
|
+
}).trim();
|
|
49
|
+
|
|
50
|
+
// If commonDir ends with .git/worktrees/<name>, the main repo is two
|
|
51
|
+
// levels up from the worktrees dir. If it's just .git, resolve normally.
|
|
52
|
+
if (commonDir.includes(`${sep}worktrees${sep}`) || commonDir.includes("/worktrees/")) {
|
|
53
|
+
// e.g., /path/to/project/.gsd/worktrees/M001/.git → /path/to/project
|
|
54
|
+
// or /path/to/project/.git/worktrees/M001 → /path/to/project
|
|
55
|
+
const gitDir = commonDir.replace(/[/\\]worktrees[/\\][^/\\]+$/, "");
|
|
56
|
+
const mainRoot = resolve(gitDir, "..");
|
|
57
|
+
return mainRoot;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Not in a worktree — use --show-toplevel as usual
|
|
40
61
|
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
41
62
|
cwd: basePath,
|
|
42
63
|
encoding: "utf-8",
|
|
@@ -154,12 +154,23 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
|
|
|
154
154
|
// Retry acquisition after cleanup
|
|
155
155
|
const release = lockfile.lockSync(gsdDir, {
|
|
156
156
|
realpath: false,
|
|
157
|
-
stale:
|
|
157
|
+
stale: 1_800_000, // 30 minutes — match primary lock settings
|
|
158
158
|
update: 10_000,
|
|
159
|
+
onCompromised: () => {
|
|
160
|
+
_lockCompromised = true;
|
|
161
|
+
},
|
|
159
162
|
});
|
|
160
163
|
_releaseFunction = release;
|
|
161
164
|
_lockedPath = basePath;
|
|
162
165
|
_lockPid = process.pid;
|
|
166
|
+
|
|
167
|
+
// Safety net for retry path too
|
|
168
|
+
const retryLockDir = join(gsdDir + ".lock");
|
|
169
|
+
process.once("exit", () => {
|
|
170
|
+
try { if (_releaseFunction) { _releaseFunction(); _releaseFunction = null; } } catch {}
|
|
171
|
+
try { if (existsSync(retryLockDir)) rmSync(retryLockDir, { recursive: true, force: true }); } catch {}
|
|
172
|
+
});
|
|
173
|
+
|
|
163
174
|
atomicWriteSync(lp, JSON.stringify(lockData, null, 2));
|
|
164
175
|
return { acquired: true };
|
|
165
176
|
} catch {
|
|
@@ -70,31 +70,34 @@ test("auto.ts 'all milestones complete' path merges before stopping (#962)", ()
|
|
|
70
70
|
const incompleteIdx = autoSrc.indexOf("incomplete.length === 0");
|
|
71
71
|
assert.ok(incompleteIdx > -1, "auto.ts should have 'incomplete.length === 0' check");
|
|
72
72
|
|
|
73
|
-
// The merge call must appear BETWEEN the incomplete check and the stopAuto call
|
|
74
|
-
//
|
|
73
|
+
// The merge call must appear BETWEEN the incomplete check and the stopAuto call.
|
|
74
|
+
// After the #1308 refactor, the merge is delegated to tryMergeMilestone.
|
|
75
75
|
const blockAfterIncomplete = autoSrc.slice(incompleteIdx, incompleteIdx + 3000);
|
|
76
76
|
|
|
77
77
|
assert.ok(
|
|
78
|
-
blockAfterIncomplete.includes("
|
|
79
|
-
"auto.ts should call
|
|
78
|
+
blockAfterIncomplete.includes("tryMergeMilestone"),
|
|
79
|
+
"auto.ts should call tryMergeMilestone in the 'all milestones complete' path",
|
|
80
80
|
);
|
|
81
81
|
|
|
82
82
|
// The merge should come before stopAuto in this block
|
|
83
|
-
const mergePos = blockAfterIncomplete.indexOf("
|
|
83
|
+
const mergePos = blockAfterIncomplete.indexOf("tryMergeMilestone");
|
|
84
84
|
const stopPos = blockAfterIncomplete.indexOf("stopAuto");
|
|
85
85
|
assert.ok(
|
|
86
86
|
mergePos < stopPos,
|
|
87
|
-
"
|
|
87
|
+
"tryMergeMilestone should be called before stopAuto in the 'all complete' path",
|
|
88
88
|
);
|
|
89
89
|
|
|
90
|
-
//
|
|
90
|
+
// Verify tryMergeMilestone handles both worktree and branch isolation
|
|
91
|
+
const helperIdx = autoSrc.indexOf("function tryMergeMilestone");
|
|
92
|
+
assert.ok(helperIdx > -1, "tryMergeMilestone helper should exist");
|
|
93
|
+
const helperBlock = autoSrc.slice(helperIdx, helperIdx + 2000);
|
|
91
94
|
assert.ok(
|
|
92
|
-
|
|
93
|
-
"should check isInAutoWorktree for worktree mode",
|
|
95
|
+
helperBlock.includes("isInAutoWorktree"),
|
|
96
|
+
"tryMergeMilestone should check isInAutoWorktree for worktree mode",
|
|
94
97
|
);
|
|
95
98
|
assert.ok(
|
|
96
|
-
|
|
97
|
-
"should check
|
|
99
|
+
helperBlock.includes("getIsolationMode") || helperBlock.includes("isolationMode"),
|
|
100
|
+
"tryMergeMilestone should check isolation mode for branch mode",
|
|
98
101
|
);
|
|
99
102
|
});
|
|
100
103
|
|
|
@@ -91,7 +91,7 @@ test("compression: buildPlanMilestonePrompt minimal drops project/requirements/d
|
|
|
91
91
|
// The plan-milestone builder should gate root file inlining on inlineLevel
|
|
92
92
|
assert.ok(
|
|
93
93
|
promptsSrc.includes('inlineLevel !== "minimal"') &&
|
|
94
|
-
promptsSrc.includes(
|
|
94
|
+
promptsSrc.includes("inlineProjectFromDb(base)"),
|
|
95
95
|
"plan-milestone should conditionally include project.md based on level",
|
|
96
96
|
);
|
|
97
97
|
});
|