From 33caef89d0e4f04baef1fdf8d51f4f3dec150fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Sat, 21 Mar 2026 09:46:20 -0600 Subject: [PATCH] fix: add missing milestones/ segment in resolveHookArtifactPath (#1779) resolveHookArtifactPath() built paths as .gsd//slices/... instead of .gsd/milestones//slices/..., causing artifact idempotency checks, retry_on detection, and skip_if in pre-dispatch hooks to all fail silently. Closes #1721 Co-authored-by: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/post-unit-hooks.ts | 12 ++++++------ .../extensions/gsd/tests/post-unit-hooks.test.ts | 11 +++++++---- .../extensions/gsd/tests/retry-state-reset.test.ts | 9 ++------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/resources/extensions/gsd/post-unit-hooks.ts b/src/resources/extensions/gsd/post-unit-hooks.ts index 95c978749..1c1964a2a 100644 --- a/src/resources/extensions/gsd/post-unit-hooks.ts +++ b/src/resources/extensions/gsd/post-unit-hooks.ts @@ -206,21 +206,21 @@ function handleHookCompletion(basePath: string): HookDispatchResult | null { /** * Resolve the path where a hook artifact is expected to be written. * Uses the trigger unit's directory context: - * - Task-level (M001/S01/T01): .gsd/M001/slices/S01/tasks/T01-{artifact} - * - Slice-level (M001/S01): .gsd/M001/slices/S01/{artifact} - * - Milestone-level (M001): .gsd/M001/{artifact} + * - Task-level (M001/S01/T01): .gsd/milestones/M001/slices/S01/tasks/T01-{artifact} + * - Slice-level (M001/S01): .gsd/milestones/M001/slices/S01/{artifact} + * - Milestone-level (M001): .gsd/milestones/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; - return join(basePath, ".gsd", mid, "slices", sid, "tasks", `${tid}-${artifactName}`); + return join(basePath, ".gsd", "milestones", mid, "slices", sid, "tasks", `${tid}-${artifactName}`); } if (parts.length === 2) { const [mid, sid] = parts; - return join(basePath, ".gsd", mid, "slices", sid, artifactName); + return join(basePath, ".gsd", "milestones", mid, "slices", sid, artifactName); } - return join(basePath, ".gsd", parts[0], artifactName); + return join(basePath, ".gsd", "milestones", parts[0], artifactName); } // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts b/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts index 881d76700..771af2968 100644 --- a/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +++ b/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts @@ -26,7 +26,7 @@ const { assertEq, assertTrue, assertMatch, report } = createTestContext(); function createFixtureBase(): string { const base = mkdtempSync(join(tmpdir(), "gsd-hook-test-")); - mkdirSync(join(base, ".gsd", "M001", "slices", "S01", "tasks"), { recursive: true }); + mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true }); return base; } @@ -45,7 +45,7 @@ console.log("\n=== resolveHookArtifactPath ==="); const taskPath = resolveHookArtifactPath(base, "M001/S01/T01", "REVIEW-PASS.md"); assertEq( taskPath, - join(base, ".gsd", "M001", "slices", "S01", "tasks", "T01-REVIEW-PASS.md"), + join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01-REVIEW-PASS.md"), "task-level artifact path", ); @@ -53,7 +53,7 @@ console.log("\n=== resolveHookArtifactPath ==="); const slicePath = resolveHookArtifactPath(base, "M001/S01", "REVIEW-PASS.md"); assertEq( slicePath, - join(base, ".gsd", "M001", "slices", "S01", "REVIEW-PASS.md"), + join(base, ".gsd", "milestones", "M001", "slices", "S01", "REVIEW-PASS.md"), "slice-level artifact path", ); @@ -61,7 +61,7 @@ console.log("\n=== resolveHookArtifactPath ==="); const milestonePath = resolveHookArtifactPath(base, "M001", "REVIEW-PASS.md"); assertEq( milestonePath, - join(base, ".gsd", "M001", "REVIEW-PASS.md"), + join(base, ".gsd", "milestones", "M001", "REVIEW-PASS.md"), "milestone-level artifact path", ); } @@ -129,15 +129,18 @@ console.log("\n=== Variable substitution ==="); assertTrue(path3.includes("M002"), "3-part ID extracts milestoneId"); assertTrue(path3.includes("S03"), "3-part ID extracts sliceId"); assertTrue(path3.includes("T05"), "3-part ID extracts taskId"); + assertTrue(path3.includes("milestones"), "3-part ID includes milestones/ segment"); // 2-part ID const path2 = resolveHookArtifactPath(base, "M002/S03", "result.md"); assertTrue(path2.includes("M002"), "2-part ID extracts milestoneId"); assertTrue(path2.includes("S03"), "2-part ID extracts sliceId"); + assertTrue(path2.includes("milestones"), "2-part ID includes milestones/ segment"); // 1-part ID const path1 = resolveHookArtifactPath(base, "M002", "result.md"); assertTrue(path1.includes("M002"), "1-part ID extracts milestoneId"); + assertTrue(path1.includes("milestones"), "1-part ID includes milestones/ segment"); } // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/resources/extensions/gsd/tests/retry-state-reset.test.ts b/src/resources/extensions/gsd/tests/retry-state-reset.test.ts index 86cc9239f..f3c39b117 100644 --- a/src/resources/extensions/gsd/tests/retry-state-reset.test.ts +++ b/src/resources/extensions/gsd/tests/retry-state-reset.test.ts @@ -24,14 +24,9 @@ function createRetryFixture(): { base: string; cleanup: () => void } { const base = mkdtempSync(join(tmpdir(), "gsd-retry-reset-")); // Create the .gsd structure for M001/S01/T01 - // Plan/Summary resolution uses .gsd/milestones/M001/slices/S01/... const milestonesTasksDir = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"); mkdirSync(milestonesTasksDir, { recursive: true }); - // Hook artifact resolution uses .gsd/M001/slices/S01/tasks/... - const hookTasksDir = join(base, ".gsd", "M001", "slices", "S01", "tasks"); - mkdirSync(hookTasksDir, { recursive: true }); - // Write a PLAN.md with T01 checked [x] (as doctor would do) const planFile = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md"); writeFileSync(planFile, [ @@ -57,7 +52,7 @@ function createRetryFixture(): { base: string; cleanup: () => void } { ); // Write the retry_on artifact in the hook artifact path - const retryArtifact = join(hookTasksDir, "T01-NEEDS-REWORK.md"); + const retryArtifact = join(milestonesTasksDir, "T01-NEEDS-REWORK.md"); writeFileSync(retryArtifact, "Rework needed: test coverage insufficient.", "utf-8"); return { @@ -325,7 +320,7 @@ console.log("\n=== resolveHookArtifactPath: correct path for retry artifacts === const path = resolveHookArtifactPath(base, "M001/S01/T01", "NEEDS-REWORK.md"); assertEq( path, - join(base, ".gsd", "M001", "slices", "S01", "tasks", "T01-NEEDS-REWORK.md"), + join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01-NEEDS-REWORK.md"), "retry artifact path resolves to task directory with task prefix", ); }