diff --git a/packages/pi-ai/src/providers/anthropic.ts b/packages/pi-ai/src/providers/anthropic.ts index 7a823e3d7..e4f49de58 100644 --- a/packages/pi-ai/src/providers/anthropic.ts +++ b/packages/pi-ai/src/providers/anthropic.ts @@ -637,8 +637,12 @@ function buildParams( options?: AnthropicOptions, ): MessageCreateParamsStreaming { const { cacheControl } = getCacheControl(model.baseUrl, options?.cacheRetention); + // For OAuth (Claude Max/Pro), strip variant suffixes like [1m] from model ID. + // The API only accepts the base model ID (e.g. "claude-opus-4-6"), + // not internal variant identifiers (e.g. "claude-opus-4-6[1m]"). + const apiModelId = isOAuthToken ? model.id.replace(/\[.*\]$/, "") : model.id; const params: MessageCreateParamsStreaming = { - model: model.id, + model: apiModelId, messages: convertMessages(context.messages, model, isOAuthToken, cacheControl), max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0, stream: true, diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index aadb740d5..f23e0fa36 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -3065,7 +3065,11 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s } } - // complete-slice must also produce a UAT file + // complete-slice must also produce a UAT file AND mark the slice [x] in the roadmap. + // Without the roadmap check, a crash after writing SUMMARY+UAT but before updating + // the roadmap causes an infinite skip loop: the idempotency key says "done" but the + // 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]; @@ -3076,6 +3080,17 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s const uatPath = join(dir, buildSliceFileName(sid, "UAT")); if (!existsSync(uatPath)) return false; } + // Verify the roadmap has the slice marked [x]. If not, the completion + // record is stale — the unit must re-run to update the roadmap. + const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP"); + if (roadmapFile && existsSync(roadmapFile)) { + try { + const roadmapContent = readFileSync(roadmapFile, "utf-8"); + const roadmap = parseRoadmap(roadmapContent); + const slice = roadmap.slices.find(s => s.id === sid); + if (slice && !slice.done) return false; + } catch { /* corrupt roadmap — be lenient and treat as verified */ } + } } } diff --git a/src/resources/extensions/gsd/tests/idle-recovery.test.ts b/src/resources/extensions/gsd/tests/idle-recovery.test.ts index ef1492e01..4ca52330e 100644 --- a/src/resources/extensions/gsd/tests/idle-recovery.test.ts +++ b/src/resources/extensions/gsd/tests/idle-recovery.test.ts @@ -409,6 +409,87 @@ function createGitBase(): string { } } +// ═══ verifyExpectedArtifact: complete-slice roadmap check ════════════════════ +// Regression for #indefinite-hang: complete-slice must verify roadmap [x] or +// the idempotency skip loops forever after a crash that wrote SUMMARY+UAT but +// did not mark the roadmap done. + +const ROADMAP_INCOMPLETE = `# M001: Test Milestone + +## Slices + +- [ ] **S01: Test Slice** \`risk:low\` +> After this: something works +`; + +const ROADMAP_COMPLETE = `# M001: Test Milestone + +## Slices + +- [x] **S01: Test Slice** \`risk:low\` +> After this: something works +`; + +{ + console.log("\n=== verifyExpectedArtifact: complete-slice — all artifacts present + roadmap marked [x] returns true ==="); + const base = createFixtureBase(); + try { + const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01"); + writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\n", "utf-8"); + writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\n", "utf-8"); + writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), ROADMAP_COMPLETE, "utf-8"); + const result = verifyExpectedArtifact("complete-slice", "M001/S01", base); + assert(result === true, "SUMMARY + UAT + roadmap [x] should verify as true"); + } finally { + cleanup(base); + } +} + +{ + console.log("\n=== verifyExpectedArtifact: complete-slice — SUMMARY + UAT present but roadmap NOT marked [x] returns false ==="); + const base = createFixtureBase(); + try { + const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01"); + writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\n", "utf-8"); + writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\n", "utf-8"); + writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), ROADMAP_INCOMPLETE, "utf-8"); + const result = verifyExpectedArtifact("complete-slice", "M001/S01", base); + assert(result === false, "roadmap not marked [x] should return false (crash recovery scenario)"); + } finally { + cleanup(base); + } +} + +{ + console.log("\n=== verifyExpectedArtifact: complete-slice — SUMMARY present but UAT missing returns false ==="); + const base = createFixtureBase(); + try { + const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01"); + writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\n", "utf-8"); + // no UAT file + writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), ROADMAP_COMPLETE, "utf-8"); + const result = verifyExpectedArtifact("complete-slice", "M001/S01", base); + assert(result === false, "missing UAT should return false"); + } finally { + cleanup(base); + } +} + +{ + console.log("\n=== verifyExpectedArtifact: complete-slice — no roadmap file present is lenient (returns true) ==="); + const base = createFixtureBase(); + try { + const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01"); + writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\n", "utf-8"); + writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\n", "utf-8"); + // no roadmap file + const result = verifyExpectedArtifact("complete-slice", "M001/S01", base); + assert(result === true, "missing roadmap file should be lenient and return true"); + } finally { + cleanup(base); + } +} + // ═════════════════════════════════════════════════════════════════════════════ // Results // ═════════════════════════════════════════════════════════════════════════════