diff --git a/src/resources/extensions/gsd/auto-artifact-paths.ts b/src/resources/extensions/gsd/auto-artifact-paths.ts index 41b72fe6e..808f4819c 100644 --- a/src/resources/extensions/gsd/auto-artifact-paths.ts +++ b/src/resources/extensions/gsd/auto-artifact-paths.ts @@ -13,6 +13,7 @@ import { buildSliceFileName, buildTaskFileName, } from "./paths.js"; +import { parseUnitId } from "./unit-id.js"; import { join } from "node:path"; /** @@ -23,9 +24,7 @@ export function resolveExpectedArtifactPath( unitId: string, base: string, ): string | null { - const parts = unitId.split("/"); - const mid = parts[0]!; - const sid = parts[1]; + const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId); switch (unitType) { case "discuss-milestone": { const dir = resolveMilestonePath(base, mid); @@ -56,7 +55,6 @@ export function resolveExpectedArtifactPath( return dir ? join(dir, buildSliceFileName(sid!, "UAT")) : null; } case "execute-task": { - const tid = parts[2]; const dir = resolveSlicePath(base, mid, sid!); return dir && tid ? join(dir, "tasks", buildTaskFileName(tid, "SUMMARY")) @@ -93,38 +91,35 @@ export function diagnoseExpectedArtifact( unitId: string, base: string, ): string | null { - const parts = unitId.split("/"); - const mid = parts[0]; - const sid = parts[1]; + const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId); switch (unitType) { case "discuss-milestone": - return `${relMilestoneFile(base, mid!, "CONTEXT")} (milestone context from discussion)`; + return `${relMilestoneFile(base, mid, "CONTEXT")} (milestone context from discussion)`; case "research-milestone": - return `${relMilestoneFile(base, mid!, "RESEARCH")} (milestone research)`; + return `${relMilestoneFile(base, mid, "RESEARCH")} (milestone research)`; case "plan-milestone": - return `${relMilestoneFile(base, mid!, "ROADMAP")} (milestone roadmap)`; + return `${relMilestoneFile(base, mid, "ROADMAP")} (milestone roadmap)`; case "research-slice": - return `${relSliceFile(base, mid!, sid!, "RESEARCH")} (slice research)`; + return `${relSliceFile(base, mid, sid!, "RESEARCH")} (slice research)`; case "plan-slice": - return `${relSliceFile(base, mid!, sid!, "PLAN")} (slice plan)`; + return `${relSliceFile(base, mid, sid!, "PLAN")} (slice plan)`; case "execute-task": { - const tid = parts[2]; - return `Task ${tid} marked [x] in ${relSliceFile(base, mid!, sid!, "PLAN")} + summary written`; + return `Task ${tid} marked [x] in ${relSliceFile(base, mid, sid!, "PLAN")} + summary written`; } case "complete-slice": - return `Slice ${sid} marked [x] in ${relMilestoneFile(base, mid!, "ROADMAP")} + summary + UAT written`; + return `Slice ${sid} marked [x] in ${relMilestoneFile(base, mid, "ROADMAP")} + summary + UAT written`; case "replan-slice": - return `${relSliceFile(base, mid!, sid!, "REPLAN")} + updated ${relSliceFile(base, mid!, sid!, "PLAN")}`; + return `${relSliceFile(base, mid, sid!, "REPLAN")} + updated ${relSliceFile(base, mid, sid!, "PLAN")}`; case "rewrite-docs": return "Active overrides resolved in .gsd/OVERRIDES.md + plan documents updated"; case "reassess-roadmap": - return `${relSliceFile(base, mid!, sid!, "ASSESSMENT")} (roadmap reassessment)`; + return `${relSliceFile(base, mid, sid!, "ASSESSMENT")} (roadmap reassessment)`; case "run-uat": - return `${relSliceFile(base, mid!, sid!, "UAT")} (UAT result)`; + return `${relSliceFile(base, mid, sid!, "UAT")} (UAT result)`; case "validate-milestone": - return `${relMilestoneFile(base, mid!, "VALIDATION")} (milestone validation report)`; + return `${relMilestoneFile(base, mid, "VALIDATION")} (milestone validation report)`; case "complete-milestone": - return `${relMilestoneFile(base, mid!, "SUMMARY")} (milestone summary)`; + return `${relMilestoneFile(base, mid, "SUMMARY")} (milestone summary)`; default: return null; } diff --git a/src/resources/extensions/gsd/auto-dashboard.ts b/src/resources/extensions/gsd/auto-dashboard.ts index e926f8253..79751b698 100644 --- a/src/resources/extensions/gsd/auto-dashboard.ts +++ b/src/resources/extensions/gsd/auto-dashboard.ts @@ -25,6 +25,7 @@ import { computeProgressScore } from "./progress-score.js"; import { getActiveWorktreeName } from "./worktree-command.js"; import { loadEffectiveGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js"; import { resolveServiceTierIcon, getEffectiveServiceTier } from "./service-tier.js"; +import { parseUnitId } from "./unit-id.js"; // ─── UAT Slice Extraction ───────────────────────────────────────────────────── @@ -33,8 +34,8 @@ import { resolveServiceTierIcon, getEffectiveServiceTier } from "./service-tier. * Returns null if the format doesn't match. */ export function extractUatSliceId(unitId: string): string | null { - const parts = unitId.split("/"); - if (parts.length >= 2 && parts[1]!.startsWith("S")) return parts[1]!; + const { slice } = parseUnitId(unitId); + if (slice?.startsWith("S")) return slice; return null; } diff --git a/src/resources/extensions/gsd/auto-post-unit.ts b/src/resources/extensions/gsd/auto-post-unit.ts index 1aa4471ad..0704deb8d 100644 --- a/src/resources/extensions/gsd/auto-post-unit.ts +++ b/src/resources/extensions/gsd/auto-post-unit.ts @@ -23,6 +23,7 @@ import { buildTaskFileName, } from "./paths.js"; import { invalidateAllCaches } from "./cache.js"; +import { parseUnitId } from "./unit-id.js"; import { closeoutUnit, type CloseoutOptions } from "./auto-unit-closeout.js"; import { autoCommitCurrentBranch, @@ -91,11 +92,10 @@ export function detectRogueFileWrites( ): RogueFileWrite[] { if (!isDbAvailable()) return []; - const parts = unitId.split("/"); + const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId); const rogues: RogueFileWrite[] = []; if (unitType === "execute-task") { - const [mid, sid, tid] = parts; if (!mid || !sid || !tid) return []; const summaryPath = resolveTaskFile(basePath, mid, sid, tid, "SUMMARY"); @@ -106,7 +106,6 @@ export function detectRogueFileWrites( rogues.push({ path: summaryPath, unitType, unitId }); } } else if (unitType === "complete-slice") { - const [mid, sid] = parts; if (!mid || !sid) return []; const summaryPath = resolveSliceFile(basePath, mid, sid, "SUMMARY"); @@ -117,7 +116,6 @@ export function detectRogueFileWrites( rogues.push({ path: summaryPath, unitType, unitId }); } } else if (unitType === "plan-milestone") { - const [mid] = parts; if (!mid) return []; const roadmapPath = resolveMilestoneFile(basePath, mid, "ROADMAP"); @@ -135,7 +133,6 @@ export function detectRogueFileWrites( rogues.push({ path: roadmapPath, unitType, unitId }); } } else if (unitType === "plan-slice" || unitType === "replan-slice") { - const [mid, sid] = parts; if (!mid || !sid) return []; const planPath = resolveSliceFile(basePath, mid, sid, "PLAN"); @@ -159,7 +156,6 @@ export function detectRogueFileWrites( rogues.push({ path: replanPath, unitType, unitId }); } } else if (unitType === "reassess-roadmap") { - const [mid, sid] = parts; if (!mid || !sid) return []; const assessPath = resolveSliceFile(basePath, mid, sid, "ASSESSMENT"); @@ -176,7 +172,6 @@ export function detectRogueFileWrites( } } } else if (unitType === "plan-task") { - const [mid, sid, tid] = parts; if (!mid || !sid || !tid) return []; const taskPlanPath = resolveTaskFile(basePath, mid, sid, tid, "PLAN"); @@ -249,8 +244,7 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV let taskContext: TaskCommitContext | undefined; if (s.currentUnit.type === "execute-task") { - const parts = s.currentUnit.id.split("/"); - const [mid, sid, tid] = parts; + const { milestone: mid, slice: sid, task: tid } = parseUnitId(s.currentUnit.id); if (mid && sid && tid) { const summaryPath = resolveTaskFile(s.basePath, mid, sid, tid, "SUMMARY"); if (summaryPath) { @@ -354,8 +348,7 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV // Reactive state cleanup on slice completion if (s.currentUnit.type === "complete-slice") { try { - const parts = s.currentUnit.id.split("/"); - const [mid, sid] = parts; + const { milestone: mid, slice: sid } = parseUnitId(s.currentUnit.id); if (mid && sid) { const { clearReactiveState } = await import("./reactive-graph.js"); clearReactiveState(s.basePath, mid, sid); @@ -440,8 +433,7 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV // from DB data before giving up (e.g. research-slice produces PLAN from engine). if (!triggerArtifactVerified) { try { - const parts = s.currentUnit.id.split("/"); - const [mid, sid] = parts; + const { milestone: mid, slice: sid } = parseUnitId(s.currentUnit.id); if (mid && sid) { const regenerated = regenerateIfMissing(s.basePath, mid, sid, "PLAN"); if (regenerated) { @@ -541,8 +533,7 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<" // ── State reset: undo the completion so deriveState re-derives the unit ── try { - const parts = trigger.unitId.split("/"); - const [mid, sid, tid] = parts; + const { milestone: mid, slice: sid, task: tid } = parseUnitId(trigger.unitId); // 1. Reset task status in DB and re-render plan checkboxes if (mid && sid && tid) { diff --git a/src/resources/extensions/gsd/auto-start.ts b/src/resources/extensions/gsd/auto-start.ts index e47dc5069..533edafba 100644 --- a/src/resources/extensions/gsd/auto-start.ts +++ b/src/resources/extensions/gsd/auto-start.ts @@ -66,6 +66,7 @@ import { isDebugEnabled, getDebugLogPath, } from "./debug-logger.js"; +import { parseUnitId } from "./unit-id.js"; import type { AutoSession } from "./auto/session.js"; import { existsSync, @@ -200,7 +201,7 @@ export async function bootstrapAutoSession( ); return releaseLockAndReturn(); } - const recoveredMid = crashLock.unitId.split("/")[0]; + const recoveredMid = parseUnitId(crashLock.unitId).milestone; const milestoneAlreadyComplete = recoveredMid ? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY") : false; diff --git a/src/resources/extensions/gsd/auto-verification.ts b/src/resources/extensions/gsd/auto-verification.ts index 8a0c6ca55..e9b4a6790 100644 --- a/src/resources/extensions/gsd/auto-verification.ts +++ b/src/resources/extensions/gsd/auto-verification.ts @@ -12,6 +12,7 @@ import type { ExtensionContext, ExtensionAPI } from "@gsd/pi-coding-agent"; import { resolveSliceFile, resolveSlicePath } from "./paths.js"; +import { parseUnitId } from "./unit-id.js"; import { isDbAvailable, getTask } from "./gsd-db.js"; import { loadEffectiveGSDPreferences } from "./preferences.js"; import { @@ -60,10 +61,9 @@ export async function runPostUnitVerification( const prefs = effectivePrefs?.preferences; // Read task plan verify field - const parts = s.currentUnit.id.split("/"); + const { milestone: mid, slice: sid, task: tid } = parseUnitId(s.currentUnit.id); let taskPlanVerify: string | undefined; - if (parts.length >= 3) { - const [mid, sid, tid] = parts; + if (mid && sid && tid) { if (isDbAvailable()) { taskPlanVerify = getTask(mid, sid, tid)?.verify; } @@ -141,9 +141,8 @@ export async function runPostUnitVerification( // Write verification evidence JSON const attempt = s.verificationRetryCount.get(s.currentUnit.id) ?? 0; - if (parts.length >= 3) { + if (mid && sid && tid) { try { - const [mid, sid, tid] = parts; const sDir = resolveSlicePath(s.basePath, mid, sid); if (sDir) { const tasksDir = join(sDir, "tasks");