gsd-pi 2.32.0-dev.1e39869 → 2.32.0-dev.3d7932c
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-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 +28 -27
- 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/context-compression.test.ts +1 -1
- 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 +1 -1
- package/src/resources/extensions/gsd/auto-dashboard.ts +3 -1
- 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 +28 -27
- 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/context-compression.test.ts +1 -1
- 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
|
@@ -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 {
|
|
@@ -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
|
});
|
|
@@ -9,6 +9,7 @@ import { deriveState } from "./state.js";
|
|
|
9
9
|
import { invalidateAllCaches } from "./cache.js";
|
|
10
10
|
import { gsdRoot, resolveTasksDir, resolveSlicePath, buildTaskFileName } from "./paths.js";
|
|
11
11
|
import { sendDesktopNotification } from "./notifications.js";
|
|
12
|
+
import { parseUnitId } from "./unit-id.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Undo the last completed unit: revert git commits, remove from completed-units,
|
|
@@ -62,11 +63,10 @@ export async function handleUndo(args: string, ctx: ExtensionCommandContext, _pi
|
|
|
62
63
|
writeFileSync(completedKeysFile, JSON.stringify(keys), "utf-8");
|
|
63
64
|
|
|
64
65
|
// 3. Delete summary artifact
|
|
65
|
-
const
|
|
66
|
+
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
66
67
|
let summaryRemoved = false;
|
|
67
|
-
if (
|
|
68
|
+
if (mid && sid && tid) {
|
|
68
69
|
// Task-level: M001/S01/T01
|
|
69
|
-
const [mid, sid, tid] = parts;
|
|
70
70
|
const tasksDir = resolveTasksDir(basePath, mid, sid);
|
|
71
71
|
if (tasksDir) {
|
|
72
72
|
const summaryFile = join(tasksDir, buildTaskFileName(tid, "SUMMARY"));
|
|
@@ -75,9 +75,8 @@ export async function handleUndo(args: string, ctx: ExtensionCommandContext, _pi
|
|
|
75
75
|
summaryRemoved = true;
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
} else if (
|
|
78
|
+
} else if (mid && sid) {
|
|
79
79
|
// Slice-level: M001/S01
|
|
80
|
-
const [mid, sid] = parts;
|
|
81
80
|
const slicePath = resolveSlicePath(basePath, mid, sid);
|
|
82
81
|
if (slicePath) {
|
|
83
82
|
// Try common summary filenames
|
|
@@ -93,8 +92,7 @@ export async function handleUndo(args: string, ctx: ExtensionCommandContext, _pi
|
|
|
93
92
|
|
|
94
93
|
// 4. Uncheck task in PLAN if execute-task
|
|
95
94
|
let planUpdated = false;
|
|
96
|
-
if (unitType === "execute-task" &&
|
|
97
|
-
const [mid, sid, tid] = parts;
|
|
95
|
+
if (unitType === "execute-task" && mid && sid && tid) {
|
|
98
96
|
planUpdated = uncheckTaskInPlan(basePath, mid, sid, tid);
|
|
99
97
|
}
|
|
100
98
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// GSD Extension — Unit ID Parsing
|
|
2
|
+
// Centralizes the milestone/slice/task decomposition of unit ID strings.
|
|
3
|
+
|
|
4
|
+
export interface ParsedUnitId {
|
|
5
|
+
milestone: string;
|
|
6
|
+
slice?: string;
|
|
7
|
+
task?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Parse a unit ID string (e.g. "M1/S1/T1") into its milestone, slice, and task components. */
|
|
11
|
+
export function parseUnitId(unitId: string): ParsedUnitId {
|
|
12
|
+
const [milestone, slice, task] = unitId.split("/");
|
|
13
|
+
return { milestone: milestone!, slice, task };
|
|
14
|
+
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "./paths.js";
|
|
10
10
|
import { loadFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
|
|
11
11
|
import { loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
|
|
12
|
+
import { parseUnitId } from "./unit-id.js";
|
|
12
13
|
|
|
13
14
|
export type UnitRuntimePhase =
|
|
14
15
|
| "dispatched"
|
|
@@ -131,7 +132,7 @@ export async function inspectExecuteTaskDurability(
|
|
|
131
132
|
basePath: string,
|
|
132
133
|
unitId: string,
|
|
133
134
|
): Promise<ExecuteTaskRecoveryStatus | null> {
|
|
134
|
-
const
|
|
135
|
+
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
135
136
|
if (!mid || !sid || !tid) return null;
|
|
136
137
|
|
|
137
138
|
const planAbs = resolveSliceFile(basePath, mid, sid, "PLAN");
|
|
@@ -34,6 +34,7 @@ import type { FileLineStat } from "./worktree-manager.js";
|
|
|
34
34
|
import { existsSync, realpathSync, readdirSync, rmSync, unlinkSync } from "node:fs";
|
|
35
35
|
import { nativeMergeAbort } from "./native-git-bridge.js";
|
|
36
36
|
import { join, sep } from "node:path";
|
|
37
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* Tracks the original project root so we can switch back.
|
|
@@ -370,7 +371,7 @@ async function handleCreate(
|
|
|
370
371
|
"info",
|
|
371
372
|
);
|
|
372
373
|
} catch (error) {
|
|
373
|
-
const msg =
|
|
374
|
+
const msg = getErrorMessage(error);
|
|
374
375
|
ctx.ui.notify(`Failed to create worktree: ${msg}`, "error");
|
|
375
376
|
}
|
|
376
377
|
}
|
|
@@ -418,7 +419,7 @@ async function handleSwitch(
|
|
|
418
419
|
"info",
|
|
419
420
|
);
|
|
420
421
|
} catch (error) {
|
|
421
|
-
const msg =
|
|
422
|
+
const msg = getErrorMessage(error);
|
|
422
423
|
ctx.ui.notify(`Failed to switch to worktree: ${msg}`, "error");
|
|
423
424
|
}
|
|
424
425
|
}
|
|
@@ -528,7 +529,7 @@ async function handleList(
|
|
|
528
529
|
|
|
529
530
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
530
531
|
} catch (error) {
|
|
531
|
-
const msg =
|
|
532
|
+
const msg = getErrorMessage(error);
|
|
532
533
|
ctx.ui.notify(`Failed to list worktrees: ${msg}`, "error");
|
|
533
534
|
}
|
|
534
535
|
}
|
|
@@ -646,7 +647,7 @@ async function handleMerge(
|
|
|
646
647
|
);
|
|
647
648
|
return;
|
|
648
649
|
} catch (mergeErr) {
|
|
649
|
-
const mergeMsg =
|
|
650
|
+
const mergeMsg = getErrorMessage(mergeErr);
|
|
650
651
|
const isConflict = /conflict/i.test(mergeMsg);
|
|
651
652
|
|
|
652
653
|
if (isConflict) {
|
|
@@ -703,7 +704,7 @@ async function handleMerge(
|
|
|
703
704
|
"info",
|
|
704
705
|
);
|
|
705
706
|
} catch (error) {
|
|
706
|
-
const msg =
|
|
707
|
+
const msg = getErrorMessage(error);
|
|
707
708
|
ctx.ui.notify(`Failed to start merge: ${msg}`, "error");
|
|
708
709
|
}
|
|
709
710
|
}
|
|
@@ -746,7 +747,7 @@ async function handleRemove(
|
|
|
746
747
|
|
|
747
748
|
ctx.ui.notify(`${CLR.ok("✓")} Worktree ${CLR.name(name)} removed ${CLR.muted("(branch deleted)")}.`, "info");
|
|
748
749
|
} catch (error) {
|
|
749
|
-
const msg =
|
|
750
|
+
const msg = getErrorMessage(error);
|
|
750
751
|
ctx.ui.notify(`Failed to remove worktree: ${msg}`, "error");
|
|
751
752
|
}
|
|
752
753
|
}
|
|
@@ -800,7 +801,7 @@ async function handleRemoveAll(
|
|
|
800
801
|
if (failed.length > 0) lines.push(`${CLR.warn("✗")} Failed: ${failed.map(n => CLR.name(n)).join(", ")}`);
|
|
801
802
|
ctx.ui.notify(lines.join("\n"), failed.length > 0 ? "warning" : "info");
|
|
802
803
|
} catch (error) {
|
|
803
|
-
const msg =
|
|
804
|
+
const msg = getErrorMessage(error);
|
|
804
805
|
ctx.ui.notify(`Failed to remove worktrees: ${msg}`, "error");
|
|
805
806
|
}
|
|
806
807
|
}
|
package/package.json
CHANGED
|
@@ -20,6 +20,7 @@ import { parseRoadmap, parsePlan } from "./files.js";
|
|
|
20
20
|
import { readFileSync, existsSync } from "node:fs";
|
|
21
21
|
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
|
|
22
22
|
import { makeUI, GLYPH, INDENT } from "../shared/mod.js";
|
|
23
|
+
import { parseUnitId } from "./unit-id.js";
|
|
23
24
|
|
|
24
25
|
// ─── Dashboard Data ───────────────────────────────────────────────────────────
|
|
25
26
|
|
|
@@ -372,8 +373,9 @@ export function updateProgressWidget(
|
|
|
372
373
|
lines.push("");
|
|
373
374
|
|
|
374
375
|
const isHook = unitType.startsWith("hook/");
|
|
376
|
+
const hookParsed = isHook ? parseUnitId(unitId) : undefined;
|
|
375
377
|
const target = isHook
|
|
376
|
-
? (
|
|
378
|
+
? (hookParsed!.task ?? hookParsed!.slice ?? unitId)
|
|
377
379
|
: (task ? `${task.id}: ${task.title}` : unitId);
|
|
378
380
|
const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
|
|
379
381
|
const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { resolveMilestoneFile } from "./paths.js";
|
|
19
19
|
import { MAX_CONSECUTIVE_SKIPS, MAX_LIFETIME_DISPATCHES } from "./auto/session.js";
|
|
20
20
|
import type { AutoSession } from "./auto/session.js";
|
|
21
|
+
import { parseUnitId } from "./unit-id.js";
|
|
21
22
|
|
|
22
23
|
export interface IdempotencyContext {
|
|
23
24
|
s: AutoSession;
|
|
@@ -54,7 +55,7 @@ export function checkIdempotency(ictx: IdempotencyContext): IdempotencyResult {
|
|
|
54
55
|
s.unitConsecutiveSkips.set(idempotencyKey, skipCount);
|
|
55
56
|
if (skipCount > MAX_CONSECUTIVE_SKIPS) {
|
|
56
57
|
// Cross-check: verify the unit's milestone is still active (#790)
|
|
57
|
-
const skippedMid = unitId.
|
|
58
|
+
const skippedMid = parseUnitId(unitId).milestone;
|
|
58
59
|
const skippedMilestoneComplete = skippedMid
|
|
59
60
|
? !!resolveMilestoneFile(basePath, skippedMid, "SUMMARY")
|
|
60
61
|
: false;
|
|
@@ -110,7 +111,7 @@ export function checkIdempotency(ictx: IdempotencyContext): IdempotencyResult {
|
|
|
110
111
|
const skipCount2 = (s.unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
|
|
111
112
|
s.unitConsecutiveSkips.set(idempotencyKey, skipCount2);
|
|
112
113
|
if (skipCount2 > MAX_CONSECUTIVE_SKIPS) {
|
|
113
|
-
const skippedMid2 = unitId.
|
|
114
|
+
const skippedMid2 = parseUnitId(unitId).milestone;
|
|
114
115
|
const skippedMilestoneComplete2 = skippedMid2
|
|
115
116
|
? !!resolveMilestoneFile(basePath, skippedMid2, "SUMMARY")
|
|
116
117
|
: false;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
formatValidationIssues,
|
|
13
13
|
} from "./observability-validator.js";
|
|
14
14
|
import type { ValidationIssue } from "./observability-validator.js";
|
|
15
|
+
import { parseUnitId } from "./unit-id.js";
|
|
15
16
|
|
|
16
17
|
export async function collectObservabilityWarnings(
|
|
17
18
|
ctx: ExtensionContext,
|
|
@@ -22,10 +23,7 @@ export async function collectObservabilityWarnings(
|
|
|
22
23
|
// Hook units have custom artifacts — skip standard observability checks
|
|
23
24
|
if (unitType.startsWith("hook/")) return [];
|
|
24
25
|
|
|
25
|
-
const
|
|
26
|
-
const mid = parts[0];
|
|
27
|
-
const sid = parts[1];
|
|
28
|
-
const tid = parts[2];
|
|
26
|
+
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
29
27
|
|
|
30
28
|
if (!mid || !sid) return [];
|
|
31
29
|
|
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
} from "./auto-dashboard.js";
|
|
62
62
|
import { join } from "node:path";
|
|
63
63
|
import { STATE_REBUILD_MIN_INTERVAL_MS } from "./auto-constants.js";
|
|
64
|
+
import { parseUnitId } from "./unit-id.js";
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
67
|
* Initialize a unit dispatch: stamp the current time, set `s.currentUnit`,
|
|
@@ -134,8 +135,7 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
|
|
|
134
135
|
let taskContext: TaskCommitContext | undefined;
|
|
135
136
|
|
|
136
137
|
if (s.currentUnit.type === "execute-task") {
|
|
137
|
-
const
|
|
138
|
-
const [mid, sid, tid] = parts;
|
|
138
|
+
const { milestone: mid, slice: sid, task: tid } = parseUnitId(s.currentUnit.id);
|
|
139
139
|
if (mid && sid && tid) {
|
|
140
140
|
const summaryPath = resolveTaskFile(s.basePath, mid, sid, tid, "SUMMARY");
|
|
141
141
|
if (summaryPath) {
|
|
@@ -167,8 +167,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
|
|
|
167
167
|
|
|
168
168
|
// Doctor: fix mechanical bookkeeping
|
|
169
169
|
try {
|
|
170
|
-
const
|
|
171
|
-
const doctorScope =
|
|
170
|
+
const { milestone, slice } = parseUnitId(s.currentUnit.id);
|
|
171
|
+
const doctorScope = slice ? `${milestone}/${slice}` : milestone;
|
|
172
172
|
const sliceTerminalUnits = new Set(["complete-slice", "run-uat"]);
|
|
173
173
|
const effectiveFixLevel = sliceTerminalUnits.has(s.currentUnit.type) ? "all" as const : "task" as const;
|
|
174
174
|
const report = await runGSDDoctor(s.basePath, { fix: true, scope: doctorScope, fixLevel: effectiveFixLevel });
|
|
@@ -348,7 +348,7 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
|
|
|
348
348
|
// instead of dispatching LLM sessions for complete-slice / validate-milestone.
|
|
349
349
|
if (s.currentUnit?.type === "execute-task" && !s.stepMode) {
|
|
350
350
|
try {
|
|
351
|
-
const
|
|
351
|
+
const { milestone: mid, slice: sid } = parseUnitId(s.currentUnit.id);
|
|
352
352
|
if (mid && sid) {
|
|
353
353
|
const state = await deriveState(s.basePath);
|
|
354
354
|
if (state.phase === "summarizing" && state.activeSlice?.id === sid) {
|