refactor: extract parseUnitId() to centralize unit ID parsing (#1282)

Replaces 30+ inline `unitId.split("/")` + destructuring patterns across
16 production files with a single `parseUnitId()` helper that returns
`{ milestone, slice?, task? }`. If the unit ID format ever changes,
only one function needs updating.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
TÂCHES 2026-03-18 19:20:08 -06:00 committed by GitHub
parent afb438164e
commit e6ab3b6722
17 changed files with 75 additions and 75 deletions

View file

@ -20,6 +20,7 @@ import { parseRoadmap, parsePlan } from "./files.js";
import { readFileSync, existsSync } from "node:fs";
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
import { makeUI, GLYPH, INDENT } from "../shared/mod.js";
import { parseUnitId } from "./unit-id.js";
// ─── Dashboard Data ───────────────────────────────────────────────────────────
@ -372,8 +373,9 @@ export function updateProgressWidget(
lines.push("");
const isHook = unitType.startsWith("hook/");
const hookParsed = isHook ? parseUnitId(unitId) : undefined;
const target = isHook
? (unitId.split("/").pop() ?? unitId)
? (hookParsed!.task ?? hookParsed!.slice ?? unitId)
: (task ? `${task.id}: ${task.title}` : unitId);
const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";

View file

@ -18,6 +18,7 @@ import {
import { resolveMilestoneFile } from "./paths.js";
import { MAX_CONSECUTIVE_SKIPS, MAX_LIFETIME_DISPATCHES } from "./auto/session.js";
import type { AutoSession } from "./auto/session.js";
import { parseUnitId } from "./unit-id.js";
export interface IdempotencyContext {
s: AutoSession;
@ -54,7 +55,7 @@ export function checkIdempotency(ictx: IdempotencyContext): IdempotencyResult {
s.unitConsecutiveSkips.set(idempotencyKey, skipCount);
if (skipCount > MAX_CONSECUTIVE_SKIPS) {
// Cross-check: verify the unit's milestone is still active (#790)
const skippedMid = unitId.split("/")[0];
const skippedMid = parseUnitId(unitId).milestone;
const skippedMilestoneComplete = skippedMid
? !!resolveMilestoneFile(basePath, skippedMid, "SUMMARY")
: false;
@ -110,7 +111,7 @@ export function checkIdempotency(ictx: IdempotencyContext): IdempotencyResult {
const skipCount2 = (s.unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
s.unitConsecutiveSkips.set(idempotencyKey, skipCount2);
if (skipCount2 > MAX_CONSECUTIVE_SKIPS) {
const skippedMid2 = unitId.split("/")[0];
const skippedMid2 = parseUnitId(unitId).milestone;
const skippedMilestoneComplete2 = skippedMid2
? !!resolveMilestoneFile(basePath, skippedMid2, "SUMMARY")
: false;

View file

@ -12,6 +12,7 @@ import {
formatValidationIssues,
} from "./observability-validator.js";
import type { ValidationIssue } from "./observability-validator.js";
import { parseUnitId } from "./unit-id.js";
export async function collectObservabilityWarnings(
ctx: ExtensionContext,
@ -22,10 +23,7 @@ export async function collectObservabilityWarnings(
// Hook units have custom artifacts — skip standard observability checks
if (unitType.startsWith("hook/")) return [];
const parts = unitId.split("/");
const mid = parts[0];
const sid = parts[1];
const tid = parts[2];
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
if (!mid || !sid) return [];

View file

@ -61,6 +61,7 @@ import {
} from "./auto-dashboard.js";
import { join } from "node:path";
import { STATE_REBUILD_MIN_INTERVAL_MS } from "./auto-constants.js";
import { parseUnitId } from "./unit-id.js";
/**
* Initialize a unit dispatch: stamp the current time, set `s.currentUnit`,
@ -134,8 +135,7 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
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) {
@ -167,8 +167,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
// Doctor: fix mechanical bookkeeping
try {
const scopeParts = s.currentUnit.id.split("/").slice(0, 2);
const doctorScope = scopeParts.join("/");
const { milestone, slice } = parseUnitId(s.currentUnit.id);
const doctorScope = slice ? `${milestone}/${slice}` : milestone;
const sliceTerminalUnits = new Set(["complete-slice", "run-uat"]);
const effectiveFixLevel = sliceTerminalUnits.has(s.currentUnit.type) ? "all" as const : "task" as const;
const report = await runGSDDoctor(s.basePath, { fix: true, scope: doctorScope, fixLevel: effectiveFixLevel });
@ -348,7 +348,7 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
// instead of dispatching LLM sessions for complete-slice / validate-milestone.
if (s.currentUnit?.type === "execute-task" && !s.stepMode) {
try {
const [mid, sid] = s.currentUnit.id.split("/");
const { milestone: mid, slice: sid } = parseUnitId(s.currentUnit.id);
if (mid && sid) {
const state = await deriveState(s.basePath);
if (state.phase === "summarizing" && state.activeSlice?.id === sid) {

View file

@ -42,6 +42,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "
import { atomicWriteSync } from "./atomic-write.js";
import { loadJsonFileOrNull } from "./json-persistence.js";
import { dirname, join } from "node:path";
import { parseUnitId } from "./unit-id.js";
// ─── Artifact Resolution & Verification ───────────────────────────────────────
@ -49,9 +50,7 @@ import { dirname, join } from "node:path";
* Resolve the expected artifact for a unit to an absolute path.
*/
export function resolveExpectedArtifactPath(unitType: string, 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 "research-milestone": {
const dir = resolveMilestonePath(base, mid);
@ -78,7 +77,6 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
return dir ? join(dir, buildSliceFileName(sid!, "UAT-RESULT")) : null;
}
case "execute-task": {
const tid = parts[2];
const dir = resolveSlicePath(base, mid, sid!);
return dir && tid ? join(dir, "tasks", buildTaskFileName(tid, "SUMMARY")) : null;
}
@ -167,10 +165,7 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
// execute-task must also have its checkbox marked [x] in the slice plan
if (unitType === "execute-task") {
const parts = unitId.split("/");
const mid = parts[0];
const sid = parts[1];
const tid = parts[2];
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
if (mid && sid && tid) {
const planAbs = resolveSliceFile(base, mid, sid, "PLAN");
if (planAbs && existsSync(planAbs)) {
@ -187,9 +182,7 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
// but omitted T{tid}-PLAN.md files would be marked complete, causing execute-task
// to dispatch with a missing task plan (see issue #739).
if (unitType === "plan-slice") {
const parts = unitId.split("/");
const mid = parts[0];
const sid = parts[1];
const { milestone: mid, slice: sid } = parseUnitId(unitId);
if (mid && sid) {
try {
const planContent = readFileSync(absPath, "utf-8");
@ -213,9 +206,8 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
// state machine keeps returning the same complete-slice unit (roadmap still shows
// the slice incomplete), so dispatchNextUnit recurses forever.
if (unitType === "complete-slice") {
const parts = unitId.split("/");
const mid = parts[0];
const sid = parts[1];
const { milestone: mid, slice: sid } = parseUnitId(unitId);
if (mid && sid) {
const dir = resolveSlicePath(base, mid, sid);
if (dir) {
@ -268,9 +260,7 @@ export function writeBlockerPlaceholder(unitType: string, unitId: string, base:
}
export function diagnoseExpectedArtifact(unitType: string, 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 "research-milestone":
return `${relMilestoneFile(base, mid!, "RESEARCH")} (milestone research)`;
@ -281,7 +271,6 @@ export function diagnoseExpectedArtifact(unitType: string, unitId: string, base:
case "plan-slice":
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`;
}
case "complete-slice":
@ -539,10 +528,7 @@ export async function selfHealRuntimeRecords(
* These are shown when automatic reconciliation is not possible.
*/
export function buildLoopRemediationSteps(unitType: string, unitId: string, base: string): string | null {
const parts = unitId.split("/");
const mid = parts[0];
const sid = parts[1];
const tid = parts[2];
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
switch (unitType) {
case "execute-task": {
if (!mid || !sid || !tid) break;

View file

@ -64,6 +64,7 @@ import type { AutoSession } from "./auto/session.js";
import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
import { join } from "node:path";
import { getErrorMessage } from "./error-utils.js";
import { parseUnitId } from "./unit-id.js";
export interface BootstrapDeps {
shouldUseWorktreeIsolation: () => boolean;
@ -139,7 +140,7 @@ export async function bootstrapAutoSession(
if (crashLock && crashLock.pid !== process.pid) {
// We already hold the session lock, so no concurrent session is running.
// The crash lock is from a dead process — recover context from it.
const recoveredMid = crashLock.unitId.split("/")[0];
const recoveredMid = parseUnitId(crashLock.unitId).milestone;
const milestoneAlreadyComplete = recoveredMid
? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
: false;

View file

@ -39,6 +39,7 @@ import {
import type { AutoSession } from "./auto/session.js";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { parseUnitId } from "./unit-id.js";
export interface StuckContext {
s: AutoSession;
@ -99,7 +100,7 @@ export async function checkStuckAndRecover(sctx: StuckContext): Promise<StuckRes
// Final reconciliation pass for execute-task
if (unitType === "execute-task") {
const [mid, sid, tid] = unitId.split("/");
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
if (mid && sid && tid) {
const status = await inspectExecuteTaskDurability(basePath, unitId);
if (status) {
@ -168,7 +169,7 @@ export async function checkStuckAndRecover(sctx: StuckContext): Promise<StuckRes
// Adaptive self-repair: each retry attempts a different remediation step.
if (unitType === "execute-task") {
const status = await inspectExecuteTaskDurability(basePath, unitId);
const [mid, sid, tid] = unitId.split("/");
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
if (status && mid && sid && tid) {
if (status.summaryExists && !status.taskChecked) {
const repaired = skipExecuteTask(basePath, mid, sid, tid, status, "self-repair", 0);

View file

@ -18,6 +18,7 @@ import {
writeBlockerPlaceholder,
} from "./auto-recovery.js";
import { existsSync } from "node:fs";
import { parseUnitId } from "./unit-id.js";
export interface RecoveryContext {
basePath: string;
@ -128,7 +129,7 @@ export async function recoverTimedOutUnit(
// Retries exhausted — write missing durable artifacts and advance.
const diagnostic = formatExecuteTaskRecoveryStatus(status);
const [mid, sid, tid] = unitId.split("/");
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
const skipped = mid && sid && tid
? skipExecuteTask(basePath, mid, sid, tid, status, reason, maxRecoveryAttempts)
: false;

View file

@ -25,6 +25,7 @@ import { removePersistedKey } from "./auto-recovery.js";
import type { AutoSession, PendingVerificationRetry } from "./auto/session.js";
import { join } from "node:path";
import { getErrorMessage } from "./error-utils.js";
import { parseUnitId } from "./unit-id.js";
export interface VerificationContext {
s: AutoSession;
@ -58,10 +59,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) {
const planFile = resolveSliceFile(s.basePath, mid, sid, "PLAN");
if (planFile) {
const planContent = await loadFile(planFile);
@ -153,9 +153,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");

View file

@ -105,6 +105,7 @@ import { computeBudgets, resolveExecutorContextWindow } from "./context-budget.j
import { GSDError, GSD_ARTIFACT_MISSING } from "./errors.js";
import { join } from "node:path";
import { sep as pathSep } from "node:path";
import { parseUnitId } from "./unit-id.js";
import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync, statSync } from "node:fs";
import { atomicWriteSync } from "./atomic-write.js";
import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit } from "./native-git-bridge.js";
@ -1748,8 +1749,7 @@ async function dispatchNextUnit(
function ensurePreconditions(
unitType: string, unitId: string, base: string, state: GSDState,
): void {
const parts = unitId.split("/");
const mid = parts[0]!;
const { milestone: mid } = parseUnitId(unitId);
const mDir = resolveMilestonePath(base, mid);
if (!mDir) {
@ -1757,8 +1757,8 @@ function ensurePreconditions(
mkdirSync(join(newDir, "slices"), { recursive: true });
}
if (parts.length >= 2) {
const sid = parts[1]!;
const sid = parseUnitId(unitId).slice;
if (sid) {
const mDirResolved = resolveMilestonePath(base, mid);
if (mDirResolved) {

View file

@ -6,6 +6,7 @@ import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { gsdRoot } from "./paths.js";
import { getAdaptiveTierAdjustment } from "./routing-history.js";
import { parseUnitId } from "./unit-id.js";
// ─── Types ───────────────────────────────────────────────────────────────────
@ -180,15 +181,14 @@ function analyzePlanComplexity(
basePath: string,
): TaskAnalysis | null {
// Check if this is a milestone-level plan (more complex) vs single slice
const parts = unitId.split("/");
if (parts.length === 1) {
const { milestone: mid, slice: sid } = parseUnitId(unitId);
if (!sid) {
// Milestone-level planning is always at least standard
return { tier: "standard", reason: "milestone-level planning" };
}
// For slice planning, try to read the context/research to gauge complexity
// If research exists and is large, bump to heavy
const [mid, sid] = parts;
const researchPath = join(gsdRoot(basePath), mid, "slices", sid, "RESEARCH.md");
try {
if (existsSync(researchPath)) {
@ -210,10 +210,8 @@ function analyzePlanComplexity(
*/
function extractTaskMetadata(unitId: string, basePath: string): TaskMetadata {
const meta: TaskMetadata = {};
const parts = unitId.split("/");
if (parts.length !== 3) return meta;
const [mid, sid, tid] = parts;
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
if (!mid || !sid || !tid) return meta;
const taskPlanPath = join(gsdRoot(basePath), mid, "slices", sid, "tasks", `${tid}-PLAN.md`);
try {

View file

@ -5,6 +5,7 @@ import { readdirSync } from "node:fs";
import { resolveMilestoneFile, milestonesDir } from "./paths.js";
import { parseRoadmapSlices } from "./roadmap-slices.js";
import { findMilestoneIds } from "./guided-flow.js";
import { parseUnitId } from "./unit-id.js";
const SLICE_DISPATCH_TYPES = new Set([
"research-slice",
@ -39,7 +40,7 @@ function readRoadmapFromDisk(base: string, milestoneId: string): string | null {
export function getPriorSliceCompletionBlocker(base: string, _mainBranch: string, unitType: string, unitId: string): string | null {
if (!SLICE_DISPATCH_TYPES.has(unitType)) return null;
const [targetMid, targetSid] = unitId.split("/");
const { milestone: targetMid, slice: targetSid } = parseUnitId(unitId);
if (!targetMid || !targetSid) return null;
// Use findMilestoneIds to respect custom queue order.

View file

@ -18,6 +18,7 @@ import type { ExtensionContext } from "@gsd/pi-coding-agent";
import { gsdRoot } from "./paths.js";
import { getAndClearSkills } from "./skill-telemetry.js";
import { loadJsonFile, loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
import { parseUnitId } from "./unit-id.js";
// Re-export from shared — canonical implementation lives in format-utils.
export { formatTokenCount } from "../shared/mod.js";
@ -290,9 +291,8 @@ export function aggregateByPhase(units: UnitMetrics[]): PhaseAggregate[] {
export function aggregateBySlice(units: UnitMetrics[]): SliceAggregate[] {
const map = new Map<string, SliceAggregate>();
for (const u of units) {
const parts = u.id.split("/");
// Slice ID is parts[0]/parts[1] if it exists, else parts[0]
const sliceId = parts.length >= 2 ? `${parts[0]}/${parts[1]}` : parts[0];
const { milestone, slice } = parseUnitId(u.id);
const sliceId = slice ? `${milestone}/${slice}` : milestone;
let agg = map.get(sliceId);
if (!agg) {
agg = { sliceId, units: 0, tokens: emptyTokens(), cost: 0, duration: 0 };

View file

@ -15,6 +15,7 @@ import { resolvePostUnitHooks, resolvePreDispatchHooks } from "./preferences.js"
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
import { gsdRoot } from "./paths.js";
import { parseUnitId } from "./unit-id.js";
// ─── Hook Queue State ──────────────────────────────────────────────────────
@ -149,7 +150,7 @@ function dequeueNextHook(basePath: string): HookDispatchResult | null {
};
// Build the prompt with variable substitution
const [mid, sid, tid] = triggerUnitId.split("/");
const { milestone: mid, slice: sid, task: tid } = parseUnitId(triggerUnitId);
const prompt = config.prompt
.replace(/\{milestoneId\}/g, mid ?? "")
.replace(/\{sliceId\}/g, sid ?? "")
@ -208,16 +209,14 @@ function handleHookCompletion(basePath: string): HookDispatchResult | null {
* - Milestone-level (M001): .gsd/M001/{artifact}
*/
export function resolveHookArtifactPath(basePath: string, unitId: string, artifactName: string): string {
const parts = unitId.split("/");
if (parts.length === 3) {
const [mid, sid, tid] = parts;
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
if (mid && sid && tid) {
return join(gsdRoot(basePath), mid, "slices", sid, "tasks", `${tid}-${artifactName}`);
}
if (parts.length === 2) {
const [mid, sid] = parts;
if (mid && sid) {
return join(gsdRoot(basePath), mid, "slices", sid, artifactName);
}
return join(gsdRoot(basePath), parts[0], artifactName);
return join(gsdRoot(basePath), mid, artifactName);
}
// ═══════════════════════════════════════════════════════════════════════════
@ -253,7 +252,7 @@ export function runPreDispatchHooks(
return { action: "proceed", prompt, firedHooks: [] };
}
const [mid, sid, tid] = unitId.split("/");
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
const substitute = (text: string): string =>
text
.replace(/\{milestoneId\}/g, mid ?? "")
@ -466,7 +465,7 @@ export function triggerHookManually(
activeHook.cycle = currentCycle;
// Build the prompt with variable substitution
const [mid, sid, tid] = unitId.split("/");
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
const prompt = hook.prompt
.replace(/\{milestoneId\}/g, mid ?? "")
.replace(/\{sliceId\}/g, sid ?? "")

View file

@ -9,6 +9,7 @@ import { deriveState } from "./state.js";
import { invalidateAllCaches } from "./cache.js";
import { gsdRoot, resolveTasksDir, resolveSlicePath, buildTaskFileName } from "./paths.js";
import { sendDesktopNotification } from "./notifications.js";
import { parseUnitId } from "./unit-id.js";
/**
* 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
writeFileSync(completedKeysFile, JSON.stringify(keys), "utf-8");
// 3. Delete summary artifact
const parts = unitId.split("/");
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
let summaryRemoved = false;
if (parts.length === 3) {
if (mid && sid && tid) {
// Task-level: M001/S01/T01
const [mid, sid, tid] = parts;
const tasksDir = resolveTasksDir(basePath, mid, sid);
if (tasksDir) {
const summaryFile = join(tasksDir, buildTaskFileName(tid, "SUMMARY"));
@ -75,9 +75,8 @@ export async function handleUndo(args: string, ctx: ExtensionCommandContext, _pi
summaryRemoved = true;
}
}
} else if (parts.length === 2) {
} else if (mid && sid) {
// Slice-level: M001/S01
const [mid, sid] = parts;
const slicePath = resolveSlicePath(basePath, mid, sid);
if (slicePath) {
// Try common summary filenames
@ -93,8 +92,7 @@ export async function handleUndo(args: string, ctx: ExtensionCommandContext, _pi
// 4. Uncheck task in PLAN if execute-task
let planUpdated = false;
if (unitType === "execute-task" && parts.length === 3) {
const [mid, sid, tid] = parts;
if (unitType === "execute-task" && mid && sid && tid) {
planUpdated = uncheckTaskInPlan(basePath, mid, sid, tid);
}

View file

@ -0,0 +1,14 @@
// GSD Extension — Unit ID Parsing
// Centralizes the milestone/slice/task decomposition of unit ID strings.
export interface ParsedUnitId {
milestone: string;
slice?: string;
task?: string;
}
/** Parse a unit ID string (e.g. "M1/S1/T1") into its milestone, slice, and task components. */
export function parseUnitId(unitId: string): ParsedUnitId {
const [milestone, slice, task] = unitId.split("/");
return { milestone: milestone!, slice, task };
}

View file

@ -9,6 +9,7 @@ import {
} from "./paths.js";
import { loadFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
import { loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
import { parseUnitId } from "./unit-id.js";
export type UnitRuntimePhase =
| "dispatched"
@ -131,7 +132,7 @@ export async function inspectExecuteTaskDurability(
basePath: string,
unitId: string,
): Promise<ExecuteTaskRecoveryStatus | null> {
const [mid, sid, tid] = unitId.split("/");
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
if (!mid || !sid || !tid) return null;
const planAbs = resolveSliceFile(basePath, mid, sid, "PLAN");