gsd-pi 2.32.0-dev.3d7932c → 2.32.0-dev.f3d5d53
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/dist/resources/extensions/gsd/auto-dashboard.ts +1 -3
- package/dist/resources/extensions/gsd/auto-idempotency.ts +2 -3
- package/dist/resources/extensions/gsd/auto-observability.ts +4 -2
- package/dist/resources/extensions/gsd/auto-post-unit.ts +5 -5
- package/dist/resources/extensions/gsd/auto-recovery.ts +22 -8
- package/dist/resources/extensions/gsd/auto-start.ts +1 -2
- package/dist/resources/extensions/gsd/auto-stuck-detection.ts +2 -3
- package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +1 -2
- package/dist/resources/extensions/gsd/auto-verification.ts +5 -4
- package/dist/resources/extensions/gsd/auto.ts +4 -4
- package/dist/resources/extensions/gsd/complexity-classifier.ts +7 -5
- package/dist/resources/extensions/gsd/dispatch-guard.ts +1 -2
- package/dist/resources/extensions/gsd/metrics.ts +3 -3
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +9 -8
- package/dist/resources/extensions/gsd/undo.ts +7 -5
- package/dist/resources/extensions/gsd/unit-runtime.ts +1 -2
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto-dashboard.ts +1 -3
- package/src/resources/extensions/gsd/auto-idempotency.ts +2 -3
- package/src/resources/extensions/gsd/auto-observability.ts +4 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +5 -5
- package/src/resources/extensions/gsd/auto-recovery.ts +22 -8
- package/src/resources/extensions/gsd/auto-start.ts +1 -2
- package/src/resources/extensions/gsd/auto-stuck-detection.ts +2 -3
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +1 -2
- package/src/resources/extensions/gsd/auto-verification.ts +5 -4
- package/src/resources/extensions/gsd/auto.ts +4 -4
- package/src/resources/extensions/gsd/complexity-classifier.ts +7 -5
- package/src/resources/extensions/gsd/dispatch-guard.ts +1 -2
- package/src/resources/extensions/gsd/metrics.ts +3 -3
- package/src/resources/extensions/gsd/post-unit-hooks.ts +9 -8
- package/src/resources/extensions/gsd/undo.ts +7 -5
- package/src/resources/extensions/gsd/unit-runtime.ts +1 -2
- package/dist/resources/extensions/gsd/unit-id.ts +0 -14
- package/src/resources/extensions/gsd/unit-id.ts +0 -14
|
@@ -20,7 +20,6 @@ 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";
|
|
24
23
|
|
|
25
24
|
// ─── Dashboard Data ───────────────────────────────────────────────────────────
|
|
26
25
|
|
|
@@ -373,9 +372,8 @@ export function updateProgressWidget(
|
|
|
373
372
|
lines.push("");
|
|
374
373
|
|
|
375
374
|
const isHook = unitType.startsWith("hook/");
|
|
376
|
-
const hookParsed = isHook ? parseUnitId(unitId) : undefined;
|
|
377
375
|
const target = isHook
|
|
378
|
-
? (
|
|
376
|
+
? (unitId.split("/").pop() ?? unitId)
|
|
379
377
|
: (task ? `${task.id}: ${task.title}` : unitId);
|
|
380
378
|
const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
|
|
381
379
|
const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
|
|
@@ -18,7 +18,6 @@ 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";
|
|
22
21
|
|
|
23
22
|
export interface IdempotencyContext {
|
|
24
23
|
s: AutoSession;
|
|
@@ -55,7 +54,7 @@ export function checkIdempotency(ictx: IdempotencyContext): IdempotencyResult {
|
|
|
55
54
|
s.unitConsecutiveSkips.set(idempotencyKey, skipCount);
|
|
56
55
|
if (skipCount > MAX_CONSECUTIVE_SKIPS) {
|
|
57
56
|
// Cross-check: verify the unit's milestone is still active (#790)
|
|
58
|
-
const skippedMid =
|
|
57
|
+
const skippedMid = unitId.split("/")[0];
|
|
59
58
|
const skippedMilestoneComplete = skippedMid
|
|
60
59
|
? !!resolveMilestoneFile(basePath, skippedMid, "SUMMARY")
|
|
61
60
|
: false;
|
|
@@ -111,7 +110,7 @@ export function checkIdempotency(ictx: IdempotencyContext): IdempotencyResult {
|
|
|
111
110
|
const skipCount2 = (s.unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
|
|
112
111
|
s.unitConsecutiveSkips.set(idempotencyKey, skipCount2);
|
|
113
112
|
if (skipCount2 > MAX_CONSECUTIVE_SKIPS) {
|
|
114
|
-
const skippedMid2 =
|
|
113
|
+
const skippedMid2 = unitId.split("/")[0];
|
|
115
114
|
const skippedMilestoneComplete2 = skippedMid2
|
|
116
115
|
? !!resolveMilestoneFile(basePath, skippedMid2, "SUMMARY")
|
|
117
116
|
: false;
|
|
@@ -12,7 +12,6 @@ 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";
|
|
16
15
|
|
|
17
16
|
export async function collectObservabilityWarnings(
|
|
18
17
|
ctx: ExtensionContext,
|
|
@@ -23,7 +22,10 @@ export async function collectObservabilityWarnings(
|
|
|
23
22
|
// Hook units have custom artifacts — skip standard observability checks
|
|
24
23
|
if (unitType.startsWith("hook/")) return [];
|
|
25
24
|
|
|
26
|
-
const
|
|
25
|
+
const parts = unitId.split("/");
|
|
26
|
+
const mid = parts[0];
|
|
27
|
+
const sid = parts[1];
|
|
28
|
+
const tid = parts[2];
|
|
27
29
|
|
|
28
30
|
if (!mid || !sid) return [];
|
|
29
31
|
|
|
@@ -61,7 +61,6 @@ 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";
|
|
65
64
|
|
|
66
65
|
/**
|
|
67
66
|
* Initialize a unit dispatch: stamp the current time, set `s.currentUnit`,
|
|
@@ -135,7 +134,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
|
|
|
135
134
|
let taskContext: TaskCommitContext | undefined;
|
|
136
135
|
|
|
137
136
|
if (s.currentUnit.type === "execute-task") {
|
|
138
|
-
const
|
|
137
|
+
const parts = s.currentUnit.id.split("/");
|
|
138
|
+
const [mid, sid, tid] = parts;
|
|
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 scopeParts = s.currentUnit.id.split("/").slice(0, 2);
|
|
171
|
+
const doctorScope = scopeParts.join("/");
|
|
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 [mid, sid] = s.currentUnit.id.split("/");
|
|
352
352
|
if (mid && sid) {
|
|
353
353
|
const state = await deriveState(s.basePath);
|
|
354
354
|
if (state.phase === "summarizing" && state.activeSlice?.id === sid) {
|
|
@@ -42,7 +42,6 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "
|
|
|
42
42
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
43
43
|
import { loadJsonFileOrNull } from "./json-persistence.js";
|
|
44
44
|
import { dirname, join } from "node:path";
|
|
45
|
-
import { parseUnitId } from "./unit-id.js";
|
|
46
45
|
|
|
47
46
|
// ─── Artifact Resolution & Verification ───────────────────────────────────────
|
|
48
47
|
|
|
@@ -50,7 +49,9 @@ import { parseUnitId } from "./unit-id.js";
|
|
|
50
49
|
* Resolve the expected artifact for a unit to an absolute path.
|
|
51
50
|
*/
|
|
52
51
|
export function resolveExpectedArtifactPath(unitType: string, unitId: string, base: string): string | null {
|
|
53
|
-
const
|
|
52
|
+
const parts = unitId.split("/");
|
|
53
|
+
const mid = parts[0]!;
|
|
54
|
+
const sid = parts[1];
|
|
54
55
|
switch (unitType) {
|
|
55
56
|
case "research-milestone": {
|
|
56
57
|
const dir = resolveMilestonePath(base, mid);
|
|
@@ -77,6 +78,7 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
|
|
|
77
78
|
return dir ? join(dir, buildSliceFileName(sid!, "UAT-RESULT")) : null;
|
|
78
79
|
}
|
|
79
80
|
case "execute-task": {
|
|
81
|
+
const tid = parts[2];
|
|
80
82
|
const dir = resolveSlicePath(base, mid, sid!);
|
|
81
83
|
return dir && tid ? join(dir, "tasks", buildTaskFileName(tid, "SUMMARY")) : null;
|
|
82
84
|
}
|
|
@@ -165,7 +167,10 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
|
|
|
165
167
|
|
|
166
168
|
// execute-task must also have its checkbox marked [x] in the slice plan
|
|
167
169
|
if (unitType === "execute-task") {
|
|
168
|
-
const
|
|
170
|
+
const parts = unitId.split("/");
|
|
171
|
+
const mid = parts[0];
|
|
172
|
+
const sid = parts[1];
|
|
173
|
+
const tid = parts[2];
|
|
169
174
|
if (mid && sid && tid) {
|
|
170
175
|
const planAbs = resolveSliceFile(base, mid, sid, "PLAN");
|
|
171
176
|
if (planAbs && existsSync(planAbs)) {
|
|
@@ -182,7 +187,9 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
|
|
|
182
187
|
// but omitted T{tid}-PLAN.md files would be marked complete, causing execute-task
|
|
183
188
|
// to dispatch with a missing task plan (see issue #739).
|
|
184
189
|
if (unitType === "plan-slice") {
|
|
185
|
-
const
|
|
190
|
+
const parts = unitId.split("/");
|
|
191
|
+
const mid = parts[0];
|
|
192
|
+
const sid = parts[1];
|
|
186
193
|
if (mid && sid) {
|
|
187
194
|
try {
|
|
188
195
|
const planContent = readFileSync(absPath, "utf-8");
|
|
@@ -206,8 +213,9 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
|
|
|
206
213
|
// state machine keeps returning the same complete-slice unit (roadmap still shows
|
|
207
214
|
// the slice incomplete), so dispatchNextUnit recurses forever.
|
|
208
215
|
if (unitType === "complete-slice") {
|
|
209
|
-
const
|
|
210
|
-
|
|
216
|
+
const parts = unitId.split("/");
|
|
217
|
+
const mid = parts[0];
|
|
218
|
+
const sid = parts[1];
|
|
211
219
|
if (mid && sid) {
|
|
212
220
|
const dir = resolveSlicePath(base, mid, sid);
|
|
213
221
|
if (dir) {
|
|
@@ -260,7 +268,9 @@ export function writeBlockerPlaceholder(unitType: string, unitId: string, base:
|
|
|
260
268
|
}
|
|
261
269
|
|
|
262
270
|
export function diagnoseExpectedArtifact(unitType: string, unitId: string, base: string): string | null {
|
|
263
|
-
const
|
|
271
|
+
const parts = unitId.split("/");
|
|
272
|
+
const mid = parts[0];
|
|
273
|
+
const sid = parts[1];
|
|
264
274
|
switch (unitType) {
|
|
265
275
|
case "research-milestone":
|
|
266
276
|
return `${relMilestoneFile(base, mid!, "RESEARCH")} (milestone research)`;
|
|
@@ -271,6 +281,7 @@ export function diagnoseExpectedArtifact(unitType: string, unitId: string, base:
|
|
|
271
281
|
case "plan-slice":
|
|
272
282
|
return `${relSliceFile(base, mid!, sid!, "PLAN")} (slice plan)`;
|
|
273
283
|
case "execute-task": {
|
|
284
|
+
const tid = parts[2];
|
|
274
285
|
return `Task ${tid} marked [x] in ${relSliceFile(base, mid!, sid!, "PLAN")} + summary written`;
|
|
275
286
|
}
|
|
276
287
|
case "complete-slice":
|
|
@@ -528,7 +539,10 @@ export async function selfHealRuntimeRecords(
|
|
|
528
539
|
* These are shown when automatic reconciliation is not possible.
|
|
529
540
|
*/
|
|
530
541
|
export function buildLoopRemediationSteps(unitType: string, unitId: string, base: string): string | null {
|
|
531
|
-
const
|
|
542
|
+
const parts = unitId.split("/");
|
|
543
|
+
const mid = parts[0];
|
|
544
|
+
const sid = parts[1];
|
|
545
|
+
const tid = parts[2];
|
|
532
546
|
switch (unitType) {
|
|
533
547
|
case "execute-task": {
|
|
534
548
|
if (!mid || !sid || !tid) break;
|
|
@@ -64,7 +64,6 @@ import type { AutoSession } from "./auto/session.js";
|
|
|
64
64
|
import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
65
65
|
import { join } from "node:path";
|
|
66
66
|
import { getErrorMessage } from "./error-utils.js";
|
|
67
|
-
import { parseUnitId } from "./unit-id.js";
|
|
68
67
|
|
|
69
68
|
export interface BootstrapDeps {
|
|
70
69
|
shouldUseWorktreeIsolation: () => boolean;
|
|
@@ -140,7 +139,7 @@ export async function bootstrapAutoSession(
|
|
|
140
139
|
if (crashLock && crashLock.pid !== process.pid) {
|
|
141
140
|
// We already hold the session lock, so no concurrent session is running.
|
|
142
141
|
// The crash lock is from a dead process — recover context from it.
|
|
143
|
-
const recoveredMid =
|
|
142
|
+
const recoveredMid = crashLock.unitId.split("/")[0];
|
|
144
143
|
const milestoneAlreadyComplete = recoveredMid
|
|
145
144
|
? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
|
|
146
145
|
: false;
|
|
@@ -39,7 +39,6 @@ import {
|
|
|
39
39
|
import type { AutoSession } from "./auto/session.js";
|
|
40
40
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
41
41
|
import { join } from "node:path";
|
|
42
|
-
import { parseUnitId } from "./unit-id.js";
|
|
43
42
|
|
|
44
43
|
export interface StuckContext {
|
|
45
44
|
s: AutoSession;
|
|
@@ -100,7 +99,7 @@ export async function checkStuckAndRecover(sctx: StuckContext): Promise<StuckRes
|
|
|
100
99
|
|
|
101
100
|
// Final reconciliation pass for execute-task
|
|
102
101
|
if (unitType === "execute-task") {
|
|
103
|
-
const
|
|
102
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
104
103
|
if (mid && sid && tid) {
|
|
105
104
|
const status = await inspectExecuteTaskDurability(basePath, unitId);
|
|
106
105
|
if (status) {
|
|
@@ -169,7 +168,7 @@ export async function checkStuckAndRecover(sctx: StuckContext): Promise<StuckRes
|
|
|
169
168
|
// Adaptive self-repair: each retry attempts a different remediation step.
|
|
170
169
|
if (unitType === "execute-task") {
|
|
171
170
|
const status = await inspectExecuteTaskDurability(basePath, unitId);
|
|
172
|
-
const
|
|
171
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
173
172
|
if (status && mid && sid && tid) {
|
|
174
173
|
if (status.summaryExists && !status.taskChecked) {
|
|
175
174
|
const repaired = skipExecuteTask(basePath, mid, sid, tid, status, "self-repair", 0);
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
writeBlockerPlaceholder,
|
|
19
19
|
} from "./auto-recovery.js";
|
|
20
20
|
import { existsSync } from "node:fs";
|
|
21
|
-
import { parseUnitId } from "./unit-id.js";
|
|
22
21
|
|
|
23
22
|
export interface RecoveryContext {
|
|
24
23
|
basePath: string;
|
|
@@ -129,7 +128,7 @@ export async function recoverTimedOutUnit(
|
|
|
129
128
|
|
|
130
129
|
// Retries exhausted — write missing durable artifacts and advance.
|
|
131
130
|
const diagnostic = formatExecuteTaskRecoveryStatus(status);
|
|
132
|
-
const
|
|
131
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
133
132
|
const skipped = mid && sid && tid
|
|
134
133
|
? skipExecuteTask(basePath, mid, sid, tid, status, reason, maxRecoveryAttempts)
|
|
135
134
|
: false;
|
|
@@ -25,7 +25,6 @@ import { removePersistedKey } from "./auto-recovery.js";
|
|
|
25
25
|
import type { AutoSession, PendingVerificationRetry } from "./auto/session.js";
|
|
26
26
|
import { join } from "node:path";
|
|
27
27
|
import { getErrorMessage } from "./error-utils.js";
|
|
28
|
-
import { parseUnitId } from "./unit-id.js";
|
|
29
28
|
|
|
30
29
|
export interface VerificationContext {
|
|
31
30
|
s: AutoSession;
|
|
@@ -59,9 +58,10 @@ export async function runPostUnitVerification(
|
|
|
59
58
|
const prefs = effectivePrefs?.preferences;
|
|
60
59
|
|
|
61
60
|
// Read task plan verify field
|
|
62
|
-
const
|
|
61
|
+
const parts = s.currentUnit.id.split("/");
|
|
63
62
|
let taskPlanVerify: string | undefined;
|
|
64
|
-
if (
|
|
63
|
+
if (parts.length >= 3) {
|
|
64
|
+
const [mid, sid, tid] = parts;
|
|
65
65
|
const planFile = resolveSliceFile(s.basePath, mid, sid, "PLAN");
|
|
66
66
|
if (planFile) {
|
|
67
67
|
const planContent = await loadFile(planFile);
|
|
@@ -153,8 +153,9 @@ export async function runPostUnitVerification(
|
|
|
153
153
|
|
|
154
154
|
// Write verification evidence JSON
|
|
155
155
|
const attempt = s.verificationRetryCount.get(s.currentUnit.id) ?? 0;
|
|
156
|
-
if (
|
|
156
|
+
if (parts.length >= 3) {
|
|
157
157
|
try {
|
|
158
|
+
const [mid, sid, tid] = parts;
|
|
158
159
|
const sDir = resolveSlicePath(s.basePath, mid, sid);
|
|
159
160
|
if (sDir) {
|
|
160
161
|
const tasksDir = join(sDir, "tasks");
|
|
@@ -105,7 +105,6 @@ 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";
|
|
109
108
|
import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync, statSync } from "node:fs";
|
|
110
109
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
111
110
|
import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit } from "./native-git-bridge.js";
|
|
@@ -1749,7 +1748,8 @@ async function dispatchNextUnit(
|
|
|
1749
1748
|
function ensurePreconditions(
|
|
1750
1749
|
unitType: string, unitId: string, base: string, state: GSDState,
|
|
1751
1750
|
): void {
|
|
1752
|
-
const
|
|
1751
|
+
const parts = unitId.split("/");
|
|
1752
|
+
const mid = parts[0]!;
|
|
1753
1753
|
|
|
1754
1754
|
const mDir = resolveMilestonePath(base, mid);
|
|
1755
1755
|
if (!mDir) {
|
|
@@ -1757,8 +1757,8 @@ function ensurePreconditions(
|
|
|
1757
1757
|
mkdirSync(join(newDir, "slices"), { recursive: true });
|
|
1758
1758
|
}
|
|
1759
1759
|
|
|
1760
|
-
|
|
1761
|
-
|
|
1760
|
+
if (parts.length >= 2) {
|
|
1761
|
+
const sid = parts[1]!;
|
|
1762
1762
|
|
|
1763
1763
|
const mDirResolved = resolveMilestonePath(base, mid);
|
|
1764
1764
|
if (mDirResolved) {
|
|
@@ -6,7 +6,6 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { gsdRoot } from "./paths.js";
|
|
8
8
|
import { getAdaptiveTierAdjustment } from "./routing-history.js";
|
|
9
|
-
import { parseUnitId } from "./unit-id.js";
|
|
10
9
|
|
|
11
10
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
12
11
|
|
|
@@ -181,14 +180,15 @@ function analyzePlanComplexity(
|
|
|
181
180
|
basePath: string,
|
|
182
181
|
): TaskAnalysis | null {
|
|
183
182
|
// Check if this is a milestone-level plan (more complex) vs single slice
|
|
184
|
-
const
|
|
185
|
-
if (
|
|
183
|
+
const parts = unitId.split("/");
|
|
184
|
+
if (parts.length === 1) {
|
|
186
185
|
// Milestone-level planning is always at least standard
|
|
187
186
|
return { tier: "standard", reason: "milestone-level planning" };
|
|
188
187
|
}
|
|
189
188
|
|
|
190
189
|
// For slice planning, try to read the context/research to gauge complexity
|
|
191
190
|
// If research exists and is large, bump to heavy
|
|
191
|
+
const [mid, sid] = parts;
|
|
192
192
|
const researchPath = join(gsdRoot(basePath), mid, "slices", sid, "RESEARCH.md");
|
|
193
193
|
try {
|
|
194
194
|
if (existsSync(researchPath)) {
|
|
@@ -210,8 +210,10 @@ function analyzePlanComplexity(
|
|
|
210
210
|
*/
|
|
211
211
|
function extractTaskMetadata(unitId: string, basePath: string): TaskMetadata {
|
|
212
212
|
const meta: TaskMetadata = {};
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
213
|
+
const parts = unitId.split("/");
|
|
214
|
+
if (parts.length !== 3) return meta;
|
|
215
|
+
|
|
216
|
+
const [mid, sid, tid] = parts;
|
|
215
217
|
const taskPlanPath = join(gsdRoot(basePath), mid, "slices", sid, "tasks", `${tid}-PLAN.md`);
|
|
216
218
|
|
|
217
219
|
try {
|
|
@@ -5,7 +5,6 @@ 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";
|
|
9
8
|
|
|
10
9
|
const SLICE_DISPATCH_TYPES = new Set([
|
|
11
10
|
"research-slice",
|
|
@@ -40,7 +39,7 @@ function readRoadmapFromDisk(base: string, milestoneId: string): string | null {
|
|
|
40
39
|
export function getPriorSliceCompletionBlocker(base: string, _mainBranch: string, unitType: string, unitId: string): string | null {
|
|
41
40
|
if (!SLICE_DISPATCH_TYPES.has(unitType)) return null;
|
|
42
41
|
|
|
43
|
-
const
|
|
42
|
+
const [targetMid, targetSid] = unitId.split("/");
|
|
44
43
|
if (!targetMid || !targetSid) return null;
|
|
45
44
|
|
|
46
45
|
// Use findMilestoneIds to respect custom queue order.
|
|
@@ -18,7 +18,6 @@ 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";
|
|
22
21
|
|
|
23
22
|
// Re-export from shared — canonical implementation lives in format-utils.
|
|
24
23
|
export { formatTokenCount } from "../shared/mod.js";
|
|
@@ -291,8 +290,9 @@ export function aggregateByPhase(units: UnitMetrics[]): PhaseAggregate[] {
|
|
|
291
290
|
export function aggregateBySlice(units: UnitMetrics[]): SliceAggregate[] {
|
|
292
291
|
const map = new Map<string, SliceAggregate>();
|
|
293
292
|
for (const u of units) {
|
|
294
|
-
const
|
|
295
|
-
|
|
293
|
+
const parts = u.id.split("/");
|
|
294
|
+
// Slice ID is parts[0]/parts[1] if it exists, else parts[0]
|
|
295
|
+
const sliceId = parts.length >= 2 ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
296
296
|
let agg = map.get(sliceId);
|
|
297
297
|
if (!agg) {
|
|
298
298
|
agg = { sliceId, units: 0, tokens: emptyTokens(), cost: 0, duration: 0 };
|
|
@@ -15,7 +15,6 @@ 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";
|
|
19
18
|
|
|
20
19
|
// ─── Hook Queue State ──────────────────────────────────────────────────────
|
|
21
20
|
|
|
@@ -150,7 +149,7 @@ function dequeueNextHook(basePath: string): HookDispatchResult | null {
|
|
|
150
149
|
};
|
|
151
150
|
|
|
152
151
|
// Build the prompt with variable substitution
|
|
153
|
-
const
|
|
152
|
+
const [mid, sid, tid] = triggerUnitId.split("/");
|
|
154
153
|
const prompt = config.prompt
|
|
155
154
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
156
155
|
.replace(/\{sliceId\}/g, sid ?? "")
|
|
@@ -209,14 +208,16 @@ function handleHookCompletion(basePath: string): HookDispatchResult | null {
|
|
|
209
208
|
* - Milestone-level (M001): .gsd/M001/{artifact}
|
|
210
209
|
*/
|
|
211
210
|
export function resolveHookArtifactPath(basePath: string, unitId: string, artifactName: string): string {
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
211
|
+
const parts = unitId.split("/");
|
|
212
|
+
if (parts.length === 3) {
|
|
213
|
+
const [mid, sid, tid] = parts;
|
|
214
214
|
return join(gsdRoot(basePath), mid, "slices", sid, "tasks", `${tid}-${artifactName}`);
|
|
215
215
|
}
|
|
216
|
-
if (
|
|
216
|
+
if (parts.length === 2) {
|
|
217
|
+
const [mid, sid] = parts;
|
|
217
218
|
return join(gsdRoot(basePath), mid, "slices", sid, artifactName);
|
|
218
219
|
}
|
|
219
|
-
return join(gsdRoot(basePath),
|
|
220
|
+
return join(gsdRoot(basePath), parts[0], artifactName);
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -252,7 +253,7 @@ export function runPreDispatchHooks(
|
|
|
252
253
|
return { action: "proceed", prompt, firedHooks: [] };
|
|
253
254
|
}
|
|
254
255
|
|
|
255
|
-
const
|
|
256
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
256
257
|
const substitute = (text: string): string =>
|
|
257
258
|
text
|
|
258
259
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
@@ -465,7 +466,7 @@ export function triggerHookManually(
|
|
|
465
466
|
activeHook.cycle = currentCycle;
|
|
466
467
|
|
|
467
468
|
// Build the prompt with variable substitution
|
|
468
|
-
const
|
|
469
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
469
470
|
const prompt = hook.prompt
|
|
470
471
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
471
472
|
.replace(/\{sliceId\}/g, sid ?? "")
|
|
@@ -9,7 +9,6 @@ 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";
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* Undo the last completed unit: revert git commits, remove from completed-units,
|
|
@@ -63,10 +62,11 @@ export async function handleUndo(args: string, ctx: ExtensionCommandContext, _pi
|
|
|
63
62
|
writeFileSync(completedKeysFile, JSON.stringify(keys), "utf-8");
|
|
64
63
|
|
|
65
64
|
// 3. Delete summary artifact
|
|
66
|
-
const
|
|
65
|
+
const parts = unitId.split("/");
|
|
67
66
|
let summaryRemoved = false;
|
|
68
|
-
if (
|
|
67
|
+
if (parts.length === 3) {
|
|
69
68
|
// 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,8 +75,9 @@ export async function handleUndo(args: string, ctx: ExtensionCommandContext, _pi
|
|
|
75
75
|
summaryRemoved = true;
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
} else if (
|
|
78
|
+
} else if (parts.length === 2) {
|
|
79
79
|
// Slice-level: M001/S01
|
|
80
|
+
const [mid, sid] = parts;
|
|
80
81
|
const slicePath = resolveSlicePath(basePath, mid, sid);
|
|
81
82
|
if (slicePath) {
|
|
82
83
|
// Try common summary filenames
|
|
@@ -92,7 +93,8 @@ export async function handleUndo(args: string, ctx: ExtensionCommandContext, _pi
|
|
|
92
93
|
|
|
93
94
|
// 4. Uncheck task in PLAN if execute-task
|
|
94
95
|
let planUpdated = false;
|
|
95
|
-
if (unitType === "execute-task" &&
|
|
96
|
+
if (unitType === "execute-task" && parts.length === 3) {
|
|
97
|
+
const [mid, sid, tid] = parts;
|
|
96
98
|
planUpdated = uncheckTaskInPlan(basePath, mid, sid, tid);
|
|
97
99
|
}
|
|
98
100
|
|
|
@@ -9,7 +9,6 @@ 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";
|
|
13
12
|
|
|
14
13
|
export type UnitRuntimePhase =
|
|
15
14
|
| "dispatched"
|
|
@@ -132,7 +131,7 @@ export async function inspectExecuteTaskDurability(
|
|
|
132
131
|
basePath: string,
|
|
133
132
|
unitId: string,
|
|
134
133
|
): Promise<ExecuteTaskRecoveryStatus | null> {
|
|
135
|
-
const
|
|
134
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
136
135
|
if (!mid || !sid || !tid) return null;
|
|
137
136
|
|
|
138
137
|
const planAbs = resolveSliceFile(basePath, mid, sid, "PLAN");
|
package/package.json
CHANGED
|
@@ -20,7 +20,6 @@ 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";
|
|
24
23
|
|
|
25
24
|
// ─── Dashboard Data ───────────────────────────────────────────────────────────
|
|
26
25
|
|
|
@@ -373,9 +372,8 @@ export function updateProgressWidget(
|
|
|
373
372
|
lines.push("");
|
|
374
373
|
|
|
375
374
|
const isHook = unitType.startsWith("hook/");
|
|
376
|
-
const hookParsed = isHook ? parseUnitId(unitId) : undefined;
|
|
377
375
|
const target = isHook
|
|
378
|
-
? (
|
|
376
|
+
? (unitId.split("/").pop() ?? unitId)
|
|
379
377
|
: (task ? `${task.id}: ${task.title}` : unitId);
|
|
380
378
|
const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
|
|
381
379
|
const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
|
|
@@ -18,7 +18,6 @@ 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";
|
|
22
21
|
|
|
23
22
|
export interface IdempotencyContext {
|
|
24
23
|
s: AutoSession;
|
|
@@ -55,7 +54,7 @@ export function checkIdempotency(ictx: IdempotencyContext): IdempotencyResult {
|
|
|
55
54
|
s.unitConsecutiveSkips.set(idempotencyKey, skipCount);
|
|
56
55
|
if (skipCount > MAX_CONSECUTIVE_SKIPS) {
|
|
57
56
|
// Cross-check: verify the unit's milestone is still active (#790)
|
|
58
|
-
const skippedMid =
|
|
57
|
+
const skippedMid = unitId.split("/")[0];
|
|
59
58
|
const skippedMilestoneComplete = skippedMid
|
|
60
59
|
? !!resolveMilestoneFile(basePath, skippedMid, "SUMMARY")
|
|
61
60
|
: false;
|
|
@@ -111,7 +110,7 @@ export function checkIdempotency(ictx: IdempotencyContext): IdempotencyResult {
|
|
|
111
110
|
const skipCount2 = (s.unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
|
|
112
111
|
s.unitConsecutiveSkips.set(idempotencyKey, skipCount2);
|
|
113
112
|
if (skipCount2 > MAX_CONSECUTIVE_SKIPS) {
|
|
114
|
-
const skippedMid2 =
|
|
113
|
+
const skippedMid2 = unitId.split("/")[0];
|
|
115
114
|
const skippedMilestoneComplete2 = skippedMid2
|
|
116
115
|
? !!resolveMilestoneFile(basePath, skippedMid2, "SUMMARY")
|
|
117
116
|
: false;
|
|
@@ -12,7 +12,6 @@ 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";
|
|
16
15
|
|
|
17
16
|
export async function collectObservabilityWarnings(
|
|
18
17
|
ctx: ExtensionContext,
|
|
@@ -23,7 +22,10 @@ export async function collectObservabilityWarnings(
|
|
|
23
22
|
// Hook units have custom artifacts — skip standard observability checks
|
|
24
23
|
if (unitType.startsWith("hook/")) return [];
|
|
25
24
|
|
|
26
|
-
const
|
|
25
|
+
const parts = unitId.split("/");
|
|
26
|
+
const mid = parts[0];
|
|
27
|
+
const sid = parts[1];
|
|
28
|
+
const tid = parts[2];
|
|
27
29
|
|
|
28
30
|
if (!mid || !sid) return [];
|
|
29
31
|
|
|
@@ -61,7 +61,6 @@ 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";
|
|
65
64
|
|
|
66
65
|
/**
|
|
67
66
|
* Initialize a unit dispatch: stamp the current time, set `s.currentUnit`,
|
|
@@ -135,7 +134,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
|
|
|
135
134
|
let taskContext: TaskCommitContext | undefined;
|
|
136
135
|
|
|
137
136
|
if (s.currentUnit.type === "execute-task") {
|
|
138
|
-
const
|
|
137
|
+
const parts = s.currentUnit.id.split("/");
|
|
138
|
+
const [mid, sid, tid] = parts;
|
|
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 scopeParts = s.currentUnit.id.split("/").slice(0, 2);
|
|
171
|
+
const doctorScope = scopeParts.join("/");
|
|
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 [mid, sid] = s.currentUnit.id.split("/");
|
|
352
352
|
if (mid && sid) {
|
|
353
353
|
const state = await deriveState(s.basePath);
|
|
354
354
|
if (state.phase === "summarizing" && state.activeSlice?.id === sid) {
|
|
@@ -42,7 +42,6 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "
|
|
|
42
42
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
43
43
|
import { loadJsonFileOrNull } from "./json-persistence.js";
|
|
44
44
|
import { dirname, join } from "node:path";
|
|
45
|
-
import { parseUnitId } from "./unit-id.js";
|
|
46
45
|
|
|
47
46
|
// ─── Artifact Resolution & Verification ───────────────────────────────────────
|
|
48
47
|
|
|
@@ -50,7 +49,9 @@ import { parseUnitId } from "./unit-id.js";
|
|
|
50
49
|
* Resolve the expected artifact for a unit to an absolute path.
|
|
51
50
|
*/
|
|
52
51
|
export function resolveExpectedArtifactPath(unitType: string, unitId: string, base: string): string | null {
|
|
53
|
-
const
|
|
52
|
+
const parts = unitId.split("/");
|
|
53
|
+
const mid = parts[0]!;
|
|
54
|
+
const sid = parts[1];
|
|
54
55
|
switch (unitType) {
|
|
55
56
|
case "research-milestone": {
|
|
56
57
|
const dir = resolveMilestonePath(base, mid);
|
|
@@ -77,6 +78,7 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
|
|
|
77
78
|
return dir ? join(dir, buildSliceFileName(sid!, "UAT-RESULT")) : null;
|
|
78
79
|
}
|
|
79
80
|
case "execute-task": {
|
|
81
|
+
const tid = parts[2];
|
|
80
82
|
const dir = resolveSlicePath(base, mid, sid!);
|
|
81
83
|
return dir && tid ? join(dir, "tasks", buildTaskFileName(tid, "SUMMARY")) : null;
|
|
82
84
|
}
|
|
@@ -165,7 +167,10 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
|
|
|
165
167
|
|
|
166
168
|
// execute-task must also have its checkbox marked [x] in the slice plan
|
|
167
169
|
if (unitType === "execute-task") {
|
|
168
|
-
const
|
|
170
|
+
const parts = unitId.split("/");
|
|
171
|
+
const mid = parts[0];
|
|
172
|
+
const sid = parts[1];
|
|
173
|
+
const tid = parts[2];
|
|
169
174
|
if (mid && sid && tid) {
|
|
170
175
|
const planAbs = resolveSliceFile(base, mid, sid, "PLAN");
|
|
171
176
|
if (planAbs && existsSync(planAbs)) {
|
|
@@ -182,7 +187,9 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
|
|
|
182
187
|
// but omitted T{tid}-PLAN.md files would be marked complete, causing execute-task
|
|
183
188
|
// to dispatch with a missing task plan (see issue #739).
|
|
184
189
|
if (unitType === "plan-slice") {
|
|
185
|
-
const
|
|
190
|
+
const parts = unitId.split("/");
|
|
191
|
+
const mid = parts[0];
|
|
192
|
+
const sid = parts[1];
|
|
186
193
|
if (mid && sid) {
|
|
187
194
|
try {
|
|
188
195
|
const planContent = readFileSync(absPath, "utf-8");
|
|
@@ -206,8 +213,9 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
|
|
|
206
213
|
// state machine keeps returning the same complete-slice unit (roadmap still shows
|
|
207
214
|
// the slice incomplete), so dispatchNextUnit recurses forever.
|
|
208
215
|
if (unitType === "complete-slice") {
|
|
209
|
-
const
|
|
210
|
-
|
|
216
|
+
const parts = unitId.split("/");
|
|
217
|
+
const mid = parts[0];
|
|
218
|
+
const sid = parts[1];
|
|
211
219
|
if (mid && sid) {
|
|
212
220
|
const dir = resolveSlicePath(base, mid, sid);
|
|
213
221
|
if (dir) {
|
|
@@ -260,7 +268,9 @@ export function writeBlockerPlaceholder(unitType: string, unitId: string, base:
|
|
|
260
268
|
}
|
|
261
269
|
|
|
262
270
|
export function diagnoseExpectedArtifact(unitType: string, unitId: string, base: string): string | null {
|
|
263
|
-
const
|
|
271
|
+
const parts = unitId.split("/");
|
|
272
|
+
const mid = parts[0];
|
|
273
|
+
const sid = parts[1];
|
|
264
274
|
switch (unitType) {
|
|
265
275
|
case "research-milestone":
|
|
266
276
|
return `${relMilestoneFile(base, mid!, "RESEARCH")} (milestone research)`;
|
|
@@ -271,6 +281,7 @@ export function diagnoseExpectedArtifact(unitType: string, unitId: string, base:
|
|
|
271
281
|
case "plan-slice":
|
|
272
282
|
return `${relSliceFile(base, mid!, sid!, "PLAN")} (slice plan)`;
|
|
273
283
|
case "execute-task": {
|
|
284
|
+
const tid = parts[2];
|
|
274
285
|
return `Task ${tid} marked [x] in ${relSliceFile(base, mid!, sid!, "PLAN")} + summary written`;
|
|
275
286
|
}
|
|
276
287
|
case "complete-slice":
|
|
@@ -528,7 +539,10 @@ export async function selfHealRuntimeRecords(
|
|
|
528
539
|
* These are shown when automatic reconciliation is not possible.
|
|
529
540
|
*/
|
|
530
541
|
export function buildLoopRemediationSteps(unitType: string, unitId: string, base: string): string | null {
|
|
531
|
-
const
|
|
542
|
+
const parts = unitId.split("/");
|
|
543
|
+
const mid = parts[0];
|
|
544
|
+
const sid = parts[1];
|
|
545
|
+
const tid = parts[2];
|
|
532
546
|
switch (unitType) {
|
|
533
547
|
case "execute-task": {
|
|
534
548
|
if (!mid || !sid || !tid) break;
|
|
@@ -64,7 +64,6 @@ import type { AutoSession } from "./auto/session.js";
|
|
|
64
64
|
import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
65
65
|
import { join } from "node:path";
|
|
66
66
|
import { getErrorMessage } from "./error-utils.js";
|
|
67
|
-
import { parseUnitId } from "./unit-id.js";
|
|
68
67
|
|
|
69
68
|
export interface BootstrapDeps {
|
|
70
69
|
shouldUseWorktreeIsolation: () => boolean;
|
|
@@ -140,7 +139,7 @@ export async function bootstrapAutoSession(
|
|
|
140
139
|
if (crashLock && crashLock.pid !== process.pid) {
|
|
141
140
|
// We already hold the session lock, so no concurrent session is running.
|
|
142
141
|
// The crash lock is from a dead process — recover context from it.
|
|
143
|
-
const recoveredMid =
|
|
142
|
+
const recoveredMid = crashLock.unitId.split("/")[0];
|
|
144
143
|
const milestoneAlreadyComplete = recoveredMid
|
|
145
144
|
? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
|
|
146
145
|
: false;
|
|
@@ -39,7 +39,6 @@ import {
|
|
|
39
39
|
import type { AutoSession } from "./auto/session.js";
|
|
40
40
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
41
41
|
import { join } from "node:path";
|
|
42
|
-
import { parseUnitId } from "./unit-id.js";
|
|
43
42
|
|
|
44
43
|
export interface StuckContext {
|
|
45
44
|
s: AutoSession;
|
|
@@ -100,7 +99,7 @@ export async function checkStuckAndRecover(sctx: StuckContext): Promise<StuckRes
|
|
|
100
99
|
|
|
101
100
|
// Final reconciliation pass for execute-task
|
|
102
101
|
if (unitType === "execute-task") {
|
|
103
|
-
const
|
|
102
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
104
103
|
if (mid && sid && tid) {
|
|
105
104
|
const status = await inspectExecuteTaskDurability(basePath, unitId);
|
|
106
105
|
if (status) {
|
|
@@ -169,7 +168,7 @@ export async function checkStuckAndRecover(sctx: StuckContext): Promise<StuckRes
|
|
|
169
168
|
// Adaptive self-repair: each retry attempts a different remediation step.
|
|
170
169
|
if (unitType === "execute-task") {
|
|
171
170
|
const status = await inspectExecuteTaskDurability(basePath, unitId);
|
|
172
|
-
const
|
|
171
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
173
172
|
if (status && mid && sid && tid) {
|
|
174
173
|
if (status.summaryExists && !status.taskChecked) {
|
|
175
174
|
const repaired = skipExecuteTask(basePath, mid, sid, tid, status, "self-repair", 0);
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
writeBlockerPlaceholder,
|
|
19
19
|
} from "./auto-recovery.js";
|
|
20
20
|
import { existsSync } from "node:fs";
|
|
21
|
-
import { parseUnitId } from "./unit-id.js";
|
|
22
21
|
|
|
23
22
|
export interface RecoveryContext {
|
|
24
23
|
basePath: string;
|
|
@@ -129,7 +128,7 @@ export async function recoverTimedOutUnit(
|
|
|
129
128
|
|
|
130
129
|
// Retries exhausted — write missing durable artifacts and advance.
|
|
131
130
|
const diagnostic = formatExecuteTaskRecoveryStatus(status);
|
|
132
|
-
const
|
|
131
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
133
132
|
const skipped = mid && sid && tid
|
|
134
133
|
? skipExecuteTask(basePath, mid, sid, tid, status, reason, maxRecoveryAttempts)
|
|
135
134
|
: false;
|
|
@@ -25,7 +25,6 @@ import { removePersistedKey } from "./auto-recovery.js";
|
|
|
25
25
|
import type { AutoSession, PendingVerificationRetry } from "./auto/session.js";
|
|
26
26
|
import { join } from "node:path";
|
|
27
27
|
import { getErrorMessage } from "./error-utils.js";
|
|
28
|
-
import { parseUnitId } from "./unit-id.js";
|
|
29
28
|
|
|
30
29
|
export interface VerificationContext {
|
|
31
30
|
s: AutoSession;
|
|
@@ -59,9 +58,10 @@ export async function runPostUnitVerification(
|
|
|
59
58
|
const prefs = effectivePrefs?.preferences;
|
|
60
59
|
|
|
61
60
|
// Read task plan verify field
|
|
62
|
-
const
|
|
61
|
+
const parts = s.currentUnit.id.split("/");
|
|
63
62
|
let taskPlanVerify: string | undefined;
|
|
64
|
-
if (
|
|
63
|
+
if (parts.length >= 3) {
|
|
64
|
+
const [mid, sid, tid] = parts;
|
|
65
65
|
const planFile = resolveSliceFile(s.basePath, mid, sid, "PLAN");
|
|
66
66
|
if (planFile) {
|
|
67
67
|
const planContent = await loadFile(planFile);
|
|
@@ -153,8 +153,9 @@ export async function runPostUnitVerification(
|
|
|
153
153
|
|
|
154
154
|
// Write verification evidence JSON
|
|
155
155
|
const attempt = s.verificationRetryCount.get(s.currentUnit.id) ?? 0;
|
|
156
|
-
if (
|
|
156
|
+
if (parts.length >= 3) {
|
|
157
157
|
try {
|
|
158
|
+
const [mid, sid, tid] = parts;
|
|
158
159
|
const sDir = resolveSlicePath(s.basePath, mid, sid);
|
|
159
160
|
if (sDir) {
|
|
160
161
|
const tasksDir = join(sDir, "tasks");
|
|
@@ -105,7 +105,6 @@ 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";
|
|
109
108
|
import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync, statSync } from "node:fs";
|
|
110
109
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
111
110
|
import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit } from "./native-git-bridge.js";
|
|
@@ -1749,7 +1748,8 @@ async function dispatchNextUnit(
|
|
|
1749
1748
|
function ensurePreconditions(
|
|
1750
1749
|
unitType: string, unitId: string, base: string, state: GSDState,
|
|
1751
1750
|
): void {
|
|
1752
|
-
const
|
|
1751
|
+
const parts = unitId.split("/");
|
|
1752
|
+
const mid = parts[0]!;
|
|
1753
1753
|
|
|
1754
1754
|
const mDir = resolveMilestonePath(base, mid);
|
|
1755
1755
|
if (!mDir) {
|
|
@@ -1757,8 +1757,8 @@ function ensurePreconditions(
|
|
|
1757
1757
|
mkdirSync(join(newDir, "slices"), { recursive: true });
|
|
1758
1758
|
}
|
|
1759
1759
|
|
|
1760
|
-
|
|
1761
|
-
|
|
1760
|
+
if (parts.length >= 2) {
|
|
1761
|
+
const sid = parts[1]!;
|
|
1762
1762
|
|
|
1763
1763
|
const mDirResolved = resolveMilestonePath(base, mid);
|
|
1764
1764
|
if (mDirResolved) {
|
|
@@ -6,7 +6,6 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { gsdRoot } from "./paths.js";
|
|
8
8
|
import { getAdaptiveTierAdjustment } from "./routing-history.js";
|
|
9
|
-
import { parseUnitId } from "./unit-id.js";
|
|
10
9
|
|
|
11
10
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
12
11
|
|
|
@@ -181,14 +180,15 @@ function analyzePlanComplexity(
|
|
|
181
180
|
basePath: string,
|
|
182
181
|
): TaskAnalysis | null {
|
|
183
182
|
// Check if this is a milestone-level plan (more complex) vs single slice
|
|
184
|
-
const
|
|
185
|
-
if (
|
|
183
|
+
const parts = unitId.split("/");
|
|
184
|
+
if (parts.length === 1) {
|
|
186
185
|
// Milestone-level planning is always at least standard
|
|
187
186
|
return { tier: "standard", reason: "milestone-level planning" };
|
|
188
187
|
}
|
|
189
188
|
|
|
190
189
|
// For slice planning, try to read the context/research to gauge complexity
|
|
191
190
|
// If research exists and is large, bump to heavy
|
|
191
|
+
const [mid, sid] = parts;
|
|
192
192
|
const researchPath = join(gsdRoot(basePath), mid, "slices", sid, "RESEARCH.md");
|
|
193
193
|
try {
|
|
194
194
|
if (existsSync(researchPath)) {
|
|
@@ -210,8 +210,10 @@ function analyzePlanComplexity(
|
|
|
210
210
|
*/
|
|
211
211
|
function extractTaskMetadata(unitId: string, basePath: string): TaskMetadata {
|
|
212
212
|
const meta: TaskMetadata = {};
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
213
|
+
const parts = unitId.split("/");
|
|
214
|
+
if (parts.length !== 3) return meta;
|
|
215
|
+
|
|
216
|
+
const [mid, sid, tid] = parts;
|
|
215
217
|
const taskPlanPath = join(gsdRoot(basePath), mid, "slices", sid, "tasks", `${tid}-PLAN.md`);
|
|
216
218
|
|
|
217
219
|
try {
|
|
@@ -5,7 +5,6 @@ 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";
|
|
9
8
|
|
|
10
9
|
const SLICE_DISPATCH_TYPES = new Set([
|
|
11
10
|
"research-slice",
|
|
@@ -40,7 +39,7 @@ function readRoadmapFromDisk(base: string, milestoneId: string): string | null {
|
|
|
40
39
|
export function getPriorSliceCompletionBlocker(base: string, _mainBranch: string, unitType: string, unitId: string): string | null {
|
|
41
40
|
if (!SLICE_DISPATCH_TYPES.has(unitType)) return null;
|
|
42
41
|
|
|
43
|
-
const
|
|
42
|
+
const [targetMid, targetSid] = unitId.split("/");
|
|
44
43
|
if (!targetMid || !targetSid) return null;
|
|
45
44
|
|
|
46
45
|
// Use findMilestoneIds to respect custom queue order.
|
|
@@ -18,7 +18,6 @@ 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";
|
|
22
21
|
|
|
23
22
|
// Re-export from shared — canonical implementation lives in format-utils.
|
|
24
23
|
export { formatTokenCount } from "../shared/mod.js";
|
|
@@ -291,8 +290,9 @@ export function aggregateByPhase(units: UnitMetrics[]): PhaseAggregate[] {
|
|
|
291
290
|
export function aggregateBySlice(units: UnitMetrics[]): SliceAggregate[] {
|
|
292
291
|
const map = new Map<string, SliceAggregate>();
|
|
293
292
|
for (const u of units) {
|
|
294
|
-
const
|
|
295
|
-
|
|
293
|
+
const parts = u.id.split("/");
|
|
294
|
+
// Slice ID is parts[0]/parts[1] if it exists, else parts[0]
|
|
295
|
+
const sliceId = parts.length >= 2 ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
296
296
|
let agg = map.get(sliceId);
|
|
297
297
|
if (!agg) {
|
|
298
298
|
agg = { sliceId, units: 0, tokens: emptyTokens(), cost: 0, duration: 0 };
|
|
@@ -15,7 +15,6 @@ 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";
|
|
19
18
|
|
|
20
19
|
// ─── Hook Queue State ──────────────────────────────────────────────────────
|
|
21
20
|
|
|
@@ -150,7 +149,7 @@ function dequeueNextHook(basePath: string): HookDispatchResult | null {
|
|
|
150
149
|
};
|
|
151
150
|
|
|
152
151
|
// Build the prompt with variable substitution
|
|
153
|
-
const
|
|
152
|
+
const [mid, sid, tid] = triggerUnitId.split("/");
|
|
154
153
|
const prompt = config.prompt
|
|
155
154
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
156
155
|
.replace(/\{sliceId\}/g, sid ?? "")
|
|
@@ -209,14 +208,16 @@ function handleHookCompletion(basePath: string): HookDispatchResult | null {
|
|
|
209
208
|
* - Milestone-level (M001): .gsd/M001/{artifact}
|
|
210
209
|
*/
|
|
211
210
|
export function resolveHookArtifactPath(basePath: string, unitId: string, artifactName: string): string {
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
211
|
+
const parts = unitId.split("/");
|
|
212
|
+
if (parts.length === 3) {
|
|
213
|
+
const [mid, sid, tid] = parts;
|
|
214
214
|
return join(gsdRoot(basePath), mid, "slices", sid, "tasks", `${tid}-${artifactName}`);
|
|
215
215
|
}
|
|
216
|
-
if (
|
|
216
|
+
if (parts.length === 2) {
|
|
217
|
+
const [mid, sid] = parts;
|
|
217
218
|
return join(gsdRoot(basePath), mid, "slices", sid, artifactName);
|
|
218
219
|
}
|
|
219
|
-
return join(gsdRoot(basePath),
|
|
220
|
+
return join(gsdRoot(basePath), parts[0], artifactName);
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -252,7 +253,7 @@ export function runPreDispatchHooks(
|
|
|
252
253
|
return { action: "proceed", prompt, firedHooks: [] };
|
|
253
254
|
}
|
|
254
255
|
|
|
255
|
-
const
|
|
256
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
256
257
|
const substitute = (text: string): string =>
|
|
257
258
|
text
|
|
258
259
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
@@ -465,7 +466,7 @@ export function triggerHookManually(
|
|
|
465
466
|
activeHook.cycle = currentCycle;
|
|
466
467
|
|
|
467
468
|
// Build the prompt with variable substitution
|
|
468
|
-
const
|
|
469
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
469
470
|
const prompt = hook.prompt
|
|
470
471
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
471
472
|
.replace(/\{sliceId\}/g, sid ?? "")
|
|
@@ -9,7 +9,6 @@ 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";
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* Undo the last completed unit: revert git commits, remove from completed-units,
|
|
@@ -63,10 +62,11 @@ export async function handleUndo(args: string, ctx: ExtensionCommandContext, _pi
|
|
|
63
62
|
writeFileSync(completedKeysFile, JSON.stringify(keys), "utf-8");
|
|
64
63
|
|
|
65
64
|
// 3. Delete summary artifact
|
|
66
|
-
const
|
|
65
|
+
const parts = unitId.split("/");
|
|
67
66
|
let summaryRemoved = false;
|
|
68
|
-
if (
|
|
67
|
+
if (parts.length === 3) {
|
|
69
68
|
// 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,8 +75,9 @@ export async function handleUndo(args: string, ctx: ExtensionCommandContext, _pi
|
|
|
75
75
|
summaryRemoved = true;
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
} else if (
|
|
78
|
+
} else if (parts.length === 2) {
|
|
79
79
|
// Slice-level: M001/S01
|
|
80
|
+
const [mid, sid] = parts;
|
|
80
81
|
const slicePath = resolveSlicePath(basePath, mid, sid);
|
|
81
82
|
if (slicePath) {
|
|
82
83
|
// Try common summary filenames
|
|
@@ -92,7 +93,8 @@ export async function handleUndo(args: string, ctx: ExtensionCommandContext, _pi
|
|
|
92
93
|
|
|
93
94
|
// 4. Uncheck task in PLAN if execute-task
|
|
94
95
|
let planUpdated = false;
|
|
95
|
-
if (unitType === "execute-task" &&
|
|
96
|
+
if (unitType === "execute-task" && parts.length === 3) {
|
|
97
|
+
const [mid, sid, tid] = parts;
|
|
96
98
|
planUpdated = uncheckTaskInPlan(basePath, mid, sid, tid);
|
|
97
99
|
}
|
|
98
100
|
|
|
@@ -9,7 +9,6 @@ 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";
|
|
13
12
|
|
|
14
13
|
export type UnitRuntimePhase =
|
|
15
14
|
| "dispatched"
|
|
@@ -132,7 +131,7 @@ export async function inspectExecuteTaskDurability(
|
|
|
132
131
|
basePath: string,
|
|
133
132
|
unitId: string,
|
|
134
133
|
): Promise<ExecuteTaskRecoveryStatus | null> {
|
|
135
|
-
const
|
|
134
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
136
135
|
if (!mid || !sid || !tid) return null;
|
|
137
136
|
|
|
138
137
|
const planAbs = resolveSliceFile(basePath, mid, sid, "PLAN");
|
|
@@ -1,14 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
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
|
-
}
|