From 1891ccbdcdb23604f182270ddd7103e9ad3da681 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sat, 2 May 2026 17:42:28 +0200 Subject: [PATCH] chore(sf): delete orphaned commands-debug + debug-session-store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `/sf debug` was ported in 360208cba but never wired up: - handleDebug exported but no caller anywhere in the tree - not in commands/catalog.ts - loadPrompt("debug-session-manager") and loadPrompt("debug-diagnose") referenced prompts that never existed in prompts/ — guaranteed runtime crash if the dispatch path were ever hit - debug-session-store.ts only consumed by commands-debug.ts - no tests reference any of it 887 LOC of dead code with a latent crash. Removing both files eliminates the orphan-prompt callsite that gap-audit kept flagging and the broken dispatch path. Resolves sf-moohvyzc-ll5bd0. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/resources/extensions/sf/commands-debug.ts | 510 ------------------ .../extensions/sf/debug-session-store.ts | 377 ------------- 2 files changed, 887 deletions(-) delete mode 100644 src/resources/extensions/sf/commands-debug.ts delete mode 100644 src/resources/extensions/sf/debug-session-store.ts diff --git a/src/resources/extensions/sf/commands-debug.ts b/src/resources/extensions/sf/commands-debug.ts deleted file mode 100644 index 7ca9ba13f..000000000 --- a/src/resources/extensions/sf/commands-debug.ts +++ /dev/null @@ -1,510 +0,0 @@ -import type { ExtensionAPI, ExtensionCommandContext } from "@singularity-forge/pi-coding-agent"; - -import { - assertValidDebugSessionSlug, - createDebugSession, - listDebugSessions, - loadDebugSession, - updateDebugSession, - type DebugTddGate, - type DebugSpecialistReview, -} from "./debug-session-store.js"; -import { loadPrompt } from "./prompt-loader.js"; - -export type DebugCommandIntent - = { type: "usage" } - | { type: "issue-start"; issue: string } - | { type: "list" } - | { type: "status"; slug: string } - | { type: "continue"; slug: string } - | { type: "diagnose"; slug?: string } - | { type: "diagnose-issue"; issue: string } - | { type: "error"; message: string }; - -const SUBCOMMANDS = new Set(["list", "status", "continue", "--diagnose"]); - -function isValidSlugCandidate(input: string): boolean { - try { - assertValidDebugSessionSlug(input); - return true; - } catch { - return false; - } -} - -function formatSessionLine(prefix: string, session: { - slug: string; - mode: string; - status: string; - phase: string; - issue: string; - updatedAt: number; -}): string { - return `${prefix} ${session.slug} [mode=${session.mode} status=${session.status} phase=${session.phase}] — ${session.issue} (updated ${new Date(session.updatedAt).toISOString()})`; -} - -function usageText(): string { - return [ - "Usage: /sf debug ", - " /sf debug list", - " /sf debug status ", - " /sf debug continue ", - " /sf debug --diagnose [ | ]", - ].join("\n"); -} - -export function parseDebugCommand(args: string): DebugCommandIntent { - const raw = args.trim(); - if (!raw) return { type: "usage" }; - - const parts = raw.split(/\s+/).filter(Boolean); - const head = parts[0] ?? ""; - - if (head === "list") { - // Strict match only; otherwise treat as issue text for deterministic fallback behavior. - if (parts.length === 1) return { type: "list" }; - return { type: "issue-start", issue: raw }; - } - - if (head === "status") { - if (parts.length === 1) return { type: "error", message: "Missing slug. Usage: /sf debug status " }; - if (parts.length === 2 && isValidSlugCandidate(parts[1])) return { type: "status", slug: parts[1] }; - return { type: "issue-start", issue: raw }; - } - - if (head === "continue") { - if (parts.length === 1) return { type: "error", message: "Missing slug. Usage: /sf debug continue " }; - if (parts.length === 2 && isValidSlugCandidate(parts[1])) return { type: "continue", slug: parts[1] }; - return { type: "issue-start", issue: raw }; - } - - if (head === "--diagnose") { - if (parts.length === 1) return { type: "diagnose" }; - if (parts.length === 2 && isValidSlugCandidate(parts[1])) return { type: "diagnose", slug: parts[1] }; - if (parts.length >= 3) return { type: "diagnose-issue", issue: parts.slice(1).join(" ") }; - return { type: "error", message: "Invalid diagnose target. Usage: /sf debug --diagnose [ | ]" }; - } - - if (head.startsWith("-") && !SUBCOMMANDS.has(head)) { - return { type: "error", message: `Unknown debug flag: ${head}.\n${usageText()}` }; - } - - return { type: "issue-start", issue: raw }; -} - -export async function handleDebug(args: string, ctx: ExtensionCommandContext, pi?: ExtensionAPI): Promise { - const parsed = parseDebugCommand(args); - const basePath = process.cwd(); - - if (parsed.type === "usage") { - ctx.ui.notify(usageText(), "info"); - return; - } - - if (parsed.type === "error") { - ctx.ui.notify(parsed.message, "warning"); - return; - } - - if (parsed.type === "issue-start") { - const issue = parsed.issue.trim(); - if (!issue) { - ctx.ui.notify(`Issue text is required.\n${usageText()}`, "warning"); - return; - } - - try { - const created = createDebugSession(basePath, { issue }); - const s = created.session; - const canDispatch = pi != null && typeof (pi as ExtensionAPI).sendMessage === "function"; - const dispatchNote = canDispatch ? `\ndispatchMode=find_and_fix` : ""; - ctx.ui.notify( - [ - `Debug session started: ${s.slug}`, - formatSessionLine("Session:", s), - `Artifact: ${created.artifactPath}`, - `Log: ${s.logPath}`, - `Next: /sf debug status ${s.slug} or /sf debug continue ${s.slug}`, - ].join("\n") + dispatchNote, - "info", - ); - if (canDispatch) { - try { - const prompt = loadPrompt("debug-session-manager", { - goal: "find_and_fix", - issue: s.issue, - slug: s.slug, - mode: s.mode, - workingDirectory: basePath, - checkpointContext: "", - tddContext: "", - specialistContext: "", - }); - pi.sendMessage( - { customType: "sf-debug-start", content: prompt, display: false }, - { triggerTurn: true }, - ); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - ctx.ui.notify( - `Debug dispatch failed: ${msg}\nSession '${s.slug}' is persisted; retry with /sf debug continue ${s.slug}`, - "warning", - ); - } - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - ctx.ui.notify( - `Unable to create debug session: ${message}\nTry /sf debug --diagnose for artifact health details.`, - "error", - ); - } - return; - } - - if (parsed.type === "list") { - try { - const listed = listDebugSessions(basePath); - if (listed.sessions.length === 0 && listed.malformed.length === 0) { - ctx.ui.notify("No debug sessions found. Start one with: /sf debug ", "info"); - return; - } - - const lines: string[] = []; - if (listed.sessions.length > 0) { - lines.push("Debug sessions:"); - for (const record of listed.sessions) { - lines.push(formatSessionLine(" -", record.session)); - } - } - - if (listed.malformed.length > 0) { - lines.push(""); - lines.push(`Malformed artifacts: ${listed.malformed.length}`); - for (const bad of listed.malformed.slice(0, 5)) { - lines.push(` - ${bad.artifactPath} :: ${bad.message}`); - } - if (listed.malformed.length > 5) { - lines.push(` ... and ${listed.malformed.length - 5} more`); - } - lines.push("Run /sf debug --diagnose for remediation guidance."); - } - - ctx.ui.notify(lines.join("\n"), "info"); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - ctx.ui.notify( - `Unable to list debug sessions: ${message}\nRun /sf debug --diagnose for details.`, - "warning", - ); - } - return; - } - - if (parsed.type === "status") { - try { - const loaded = loadDebugSession(basePath, parsed.slug); - if (!loaded) { - ctx.ui.notify( - `Unknown debug session slug '${parsed.slug}'. Run /sf debug list to see available sessions.`, - "warning", - ); - return; - } - - const s = loaded.session; - ctx.ui.notify( - [ - `Debug session status: ${s.slug}`, - `mode=${s.mode}`, - `status=${s.status}`, - `phase=${s.phase}`, - `issue=${s.issue}`, - `artifact=${loaded.artifactPath}`, - `log=${s.logPath}`, - `updated=${new Date(s.updatedAt).toISOString()}`, - `lastError=${s.lastError ?? "none"}`, - ].join("\n"), - "info", - ); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - ctx.ui.notify( - `Unable to load debug session '${parsed.slug}': ${message}\nTry /sf debug --diagnose ${parsed.slug}`, - "warning", - ); - } - return; - } - - if (parsed.type === "continue") { - try { - const loaded = loadDebugSession(basePath, parsed.slug); - if (!loaded) { - ctx.ui.notify( - `Unknown debug session slug '${parsed.slug}'. Run /sf debug list to see available sessions.`, - "warning", - ); - return; - } - - if (loaded.session.status === "resolved") { - ctx.ui.notify( - `Session '${parsed.slug}' is resolved. Open a new session with /sf debug for follow-up work.`, - "warning", - ); - return; - } - - // Determine checkpoint/TDD/specialist dispatch context before updating session state. - const checkpoint = loaded.session.checkpoint; - const tddGate = loaded.session.tddGate; - const specialistReview: DebugSpecialistReview | null | undefined = loaded.session.specialistReview; - const hasCheckpoint = checkpoint != null && checkpoint.awaitingResponse; - const hasTddGate = tddGate != null && tddGate.enabled; - - let dispatchTemplate = "debug-diagnose"; - let goal = "find_and_fix"; - let dispatchModeLabel = "find_and_fix"; - let checkpointContext = ""; - let tddContext = ""; - let specialistContext = ""; - let tddGateUpdate: DebugTddGate | undefined; - - if (hasCheckpoint || hasTddGate) { - dispatchTemplate = "debug-session-manager"; - - if (hasCheckpoint) { - const cpLines = [ - `## Active Checkpoint`, - `- type: ${checkpoint.type}`, - `- summary: ${checkpoint.summary}`, - ]; - if (checkpoint.userResponse) { - cpLines.push(`- userResponse:\n\nDATA_START\n${checkpoint.userResponse}\nDATA_END`); - } else { - cpLines.push(`- awaitingResponse: true`); - } - checkpointContext = cpLines.join("\n"); - dispatchModeLabel = `checkpointType=${checkpoint.type}`; - } - - if (hasTddGate) { - if (tddGate.phase === "red") { - goal = "find_and_fix"; - const tddLines = [ - `## TDD Gate`, - `- phase: red → green`, - ]; - if (tddGate.testFile) tddLines.push(`- testFile: ${tddGate.testFile}`); - if (tddGate.testName) tddLines.push(`- testName: ${tddGate.testName}`); - if (tddGate.failureOutput) tddLines.push(`- failureOutput:\n${tddGate.failureOutput}`); - tddLines.push(`The failing test has been confirmed. Proceed to implement the fix that makes this test pass.`); - tddContext = tddLines.join("\n"); - tddGateUpdate = { ...tddGate, phase: "green" }; - dispatchModeLabel = "tddPhase=red→green"; - } else if (tddGate.phase === "green") { - goal = "find_and_fix"; - const tddLines = [ - `## TDD Gate`, - `- phase: green`, - ]; - if (tddGate.testFile) tddLines.push(`- testFile: ${tddGate.testFile}`); - if (tddGate.testName) tddLines.push(`- testName: ${tddGate.testName}`); - tddLines.push(`The test is now passing. Continue verifying the fix.`); - tddContext = tddLines.join("\n"); - dispatchModeLabel = "tddPhase=green"; - } else { - // phase === "pending": investigate only, do not fix yet - goal = "find_root_cause_only"; - const tddLines = [ - `## TDD Gate`, - `- phase: pending`, - `TDD mode is active. Write a failing test that captures this bug first. Do NOT fix the issue yet.`, - ]; - if (tddGate.testFile) tddLines.push(`- testFile: ${tddGate.testFile}`); - tddContext = tddLines.join("\n"); - dispatchModeLabel = "tddPhase=pending"; - } - } else { - // Checkpoint only, no TDD gate — apply fix after human response - goal = "find_and_fix"; - } - } - - // Build specialistContext from session's specialistReview field (null/undefined → empty string). - if (specialistReview != null) { - specialistContext = [ - `## Prior Specialist Review`, - `- hint: ${specialistReview.hint}`, - `- skill: ${specialistReview.skill ?? ""}`, - `- verdict: ${specialistReview.verdict}`, - `- detail: ${specialistReview.detail}`, - ].join("\n"); - dispatchModeLabel += ` specialistHint=${specialistReview.hint}`; - } - - // Update session state BEFORE dispatch — handler returns after sendMessage. - const resumed = updateDebugSession(basePath, parsed.slug, { - status: "active", - phase: "continued", - lastError: null, - ...(tddGateUpdate !== undefined ? { tddGate: tddGateUpdate } : {}), - }); - - const canDispatch = pi != null && typeof (pi as ExtensionAPI).sendMessage === "function"; - const dispatchNote = canDispatch ? `\ndispatchMode=${dispatchModeLabel}` : ""; - ctx.ui.notify( - [ - `Resumed debug session: ${resumed.session.slug}`, - formatSessionLine("Session:", resumed.session), - `Log: ${resumed.session.logPath}`, - `Next: /sf debug status ${resumed.session.slug}`, - ].join("\n") + dispatchNote, - "info", - ); - - if (canDispatch) { - try { - const promptVars: Record = { - goal, - issue: resumed.session.issue, - slug: resumed.session.slug, - mode: resumed.session.mode, - workingDirectory: basePath, - }; - if (dispatchTemplate === "debug-session-manager") { - promptVars.checkpointContext = checkpointContext; - promptVars.tddContext = tddContext; - promptVars.specialistContext = specialistContext; - } - const prompt = loadPrompt(dispatchTemplate, promptVars); - pi.sendMessage( - { customType: "sf-debug-continue", content: prompt, display: false }, - { triggerTurn: true }, - ); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - ctx.ui.notify( - `Continue dispatch failed: ${msg}\nSession '${resumed.session.slug}' is persisted; retry with /sf debug continue ${resumed.session.slug}`, - "warning", - ); - } - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - ctx.ui.notify( - `Unable to continue debug session '${parsed.slug}': ${message}\nTry /sf debug --diagnose ${parsed.slug}`, - "warning", - ); - } - return; - } - - if (parsed.type === "diagnose-issue") { - const issue = parsed.issue.trim(); - if (!issue) { - ctx.ui.notify(`Issue text is required.\n${usageText()}`, "warning"); - return; - } - - try { - const created = createDebugSession(basePath, { issue, mode: "diagnose" }); - const s = created.session; - ctx.ui.notify( - [ - `Diagnose session started: ${s.slug}`, - formatSessionLine("Session:", s), - `Artifact: ${created.artifactPath}`, - `Log: ${s.logPath}`, - `dispatchMode=find_root_cause_only`, - `Next: /sf debug status ${s.slug} or /sf debug --diagnose ${s.slug}`, - ].join("\n"), - "info", - ); - - if (pi && typeof pi.sendMessage === "function") { - try { - const prompt = loadPrompt("debug-diagnose", { - goal: "find_root_cause_only", - issue: s.issue, - slug: s.slug, - mode: s.mode, - workingDirectory: basePath, - }); - pi.sendMessage( - { customType: "sf-debug-diagnose", content: prompt, display: false }, - { triggerTurn: true }, - ); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - ctx.ui.notify( - `Diagnose dispatch failed: ${msg}\nSession '${s.slug}' is persisted; continue manually with /sf debug continue ${s.slug}`, - "warning", - ); - } - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - ctx.ui.notify( - `Unable to create diagnose session: ${message}\nTry /sf debug --diagnose for artifact health details.`, - "error", - ); - } - return; - } - - if (parsed.type === "diagnose") { - try { - const listed = listDebugSessions(basePath); - - if (parsed.slug) { - const loaded = loadDebugSession(basePath, parsed.slug); - if (!loaded) { - ctx.ui.notify( - `Diagnose: session '${parsed.slug}' not found.\nRun /sf debug list to discover valid slugs.`, - "warning", - ); - return; - } - - const s = loaded.session; - ctx.ui.notify( - [ - `Diagnose session: ${s.slug}`, - `mode=${s.mode}`, - `status=${s.status}`, - `phase=${s.phase}`, - `artifact=${loaded.artifactPath}`, - `log=${s.logPath}`, - `lastError=${s.lastError ?? "none"}`, - `malformedArtifactsInStore=${listed.malformed.length}`, - ].join("\n"), - "info", - ); - return; - } - - const lines = [ - "Debug session diagnostics:", - `healthySessions=${listed.sessions.length}`, - `malformedArtifacts=${listed.malformed.length}`, - ]; - - if (listed.malformed.length > 0) { - lines.push(""); - lines.push("Malformed artifacts (first 10):"); - for (const malformed of listed.malformed.slice(0, 10)) { - lines.push(` - ${malformed.artifactPath}`); - lines.push(` ${malformed.message}`); - } - lines.push("Remediation: repair/remove malformed JSON artifacts under .sf/debug/sessions/."); - } - - ctx.ui.notify(lines.join("\n"), listed.malformed.length > 0 ? "warning" : "info"); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - ctx.ui.notify(`Diagnose failed: ${message}`, "error"); - } - } -} diff --git a/src/resources/extensions/sf/debug-session-store.ts b/src/resources/extensions/sf/debug-session-store.ts deleted file mode 100644 index 978dcc9f3..000000000 --- a/src/resources/extensions/sf/debug-session-store.ts +++ /dev/null @@ -1,377 +0,0 @@ -import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs"; -import { join } from "node:path"; -import { atomicWriteSync, type AtomicWriteSyncOps } from "./atomic-write.js"; -import { sfRoot } from "./paths.js"; - -export type DebugSessionStatus = "active" | "paused" | "resolved" | "failed"; - -export interface DebugCheckpoint { - type: "human-verify" | "human-action" | "decision" | "root-cause-found" | "inconclusive"; - summary: string; - awaitingResponse: boolean; - userResponse?: string; -} - -export interface DebugTddGate { - enabled: boolean; - phase: "pending" | "red" | "green"; - testFile?: string; - testName?: string; - failureOutput?: string; -} - -export interface DebugSpecialistReview { - hint: string; - skill: string | null; - verdict: string; - detail: string; - reviewedAt: number; -} - -export interface DebugSessionArtifact { - version: 1; - mode: "debug" | "diagnose"; - slug: string; - issue: string; - status: DebugSessionStatus; - phase: string; - createdAt: number; - updatedAt: number; - logPath: string; - lastError: string | null; - checkpoint?: DebugCheckpoint | null; - tddGate?: DebugTddGate | null; - specialistReview?: DebugSpecialistReview | null; -} - -export interface DebugSessionRecord { - artifactPath: string; - session: DebugSessionArtifact; -} - -export interface DebugMalformedSessionArtifact { - artifactPath: string; - message: string; -} - -export interface DebugSessionListResult { - sessions: DebugSessionRecord[]; - malformed: DebugMalformedSessionArtifact[]; -} - -export interface CreateDebugSessionInput { - issue: string; - mode?: "debug" | "diagnose"; - status?: DebugSessionStatus; - phase?: string; - createdAt?: number; -} - -export interface UpdateDebugSessionInput { - status?: DebugSessionStatus; - phase?: string; - issue?: string; - lastError?: string | null; - updatedAt?: number; - checkpoint?: DebugCheckpoint | null; - tddGate?: DebugTddGate | null; - specialistReview?: DebugSpecialistReview | null; -} - -export interface DebugSessionStoreDeps { - atomicWrite?: (filePath: string, content: string, encoding?: BufferEncoding) => void; - readFile?: (filePath: string, encoding: BufferEncoding) => string; - listDir?: (dirPath: string) => string[]; - exists?: (filePath: string) => boolean; - now?: () => number; -} - -const DEFAULT_PHASE = "queued"; -const DEFAULT_STATUS: DebugSessionStatus = "active"; -const SESSION_FILE_SUFFIX = ".json"; -const MAX_SLUG_LENGTH = 64; -const MAX_COLLISION_ATTEMPTS = 10_000; - -function debugRoot(basePath: string): string { - return join(sfRoot(basePath), "debug"); -} - -export function debugSessionsDir(basePath: string): string { - return join(debugRoot(basePath), "sessions"); -} - -export function debugSessionArtifactPath(basePath: string, slug: string): string { - assertValidDebugSessionSlug(slug); - return join(debugSessionsDir(basePath), `${slug}${SESSION_FILE_SUFFIX}`); -} - -export function debugSessionLogPath(basePath: string, slug: string): string { - assertValidDebugSessionSlug(slug); - return join(debugRoot(basePath), `${slug}.log`); -} - -function ensureSessionsDir(basePath: string): string { - const dir = debugSessionsDir(basePath); - if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); - return dir; -} - -export function slugifyDebugSessionIssue(issue: string): string { - const normalized = issue - .trim() - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, "") - .replace(/-{2,}/g, "-") - .slice(0, MAX_SLUG_LENGTH) - .replace(/-+$/g, ""); - - if (!normalized) { - throw new Error("Issue text must contain at least one alphanumeric character."); - } - return normalized; -} - -export function assertValidDebugSessionSlug(slug: string): void { - if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) { - throw new Error(`Invalid debug session slug: ${slug}`); - } -} - -function isDebugSessionStatus(value: unknown): value is DebugSessionStatus { - return value === "active" || value === "paused" || value === "resolved" || value === "failed"; -} - -function isDebugCheckpointShape(value: unknown): value is DebugCheckpoint { - if (!value || typeof value !== "object") return false; - const o = value as Record; - const validTypes = ["human-verify", "human-action", "decision", "root-cause-found", "inconclusive"]; - return ( - validTypes.includes(o.type as string) - && typeof o.summary === "string" - && typeof o.awaitingResponse === "boolean" - && (o.userResponse === undefined || typeof o.userResponse === "string") - ); -} - -function isDebugTddGateShape(value: unknown): value is DebugTddGate { - if (!value || typeof value !== "object") return false; - const o = value as Record; - const validPhases = ["pending", "red", "green"]; - return ( - typeof o.enabled === "boolean" - && validPhases.includes(o.phase as string) - && (o.testFile === undefined || typeof o.testFile === "string") - && (o.testName === undefined || typeof o.testName === "string") - && (o.failureOutput === undefined || typeof o.failureOutput === "string") - ); -} - -function isDebugSpecialistReviewShape(value: unknown): value is DebugSpecialistReview { - if (!value || typeof value !== "object") return false; - const o = value as Record; - return ( - typeof o.hint === "string" - && (typeof o.skill === "string" || o.skill === null) - && typeof o.verdict === "string" - && typeof o.detail === "string" - && typeof o.reviewedAt === "number" - ); -} - -function isDebugSessionArtifact(value: unknown): value is DebugSessionArtifact { - if (!value || typeof value !== "object") return false; - const o = value as Record; - return ( - o.version === 1 - && (o.mode === "debug" || o.mode === "diagnose") - && typeof o.slug === "string" - && typeof o.issue === "string" - && isDebugSessionStatus(o.status) - && typeof o.phase === "string" - && typeof o.createdAt === "number" - && typeof o.updatedAt === "number" - && typeof o.logPath === "string" - && (typeof o.lastError === "string" || o.lastError === null) - && (o.checkpoint === undefined || o.checkpoint === null || isDebugCheckpointShape(o.checkpoint)) - && (o.tddGate === undefined || o.tddGate === null || isDebugTddGateShape(o.tddGate)) - && (o.specialistReview === undefined || o.specialistReview === null || isDebugSpecialistReviewShape(o.specialistReview)) - ); -} - -function parseDebugSessionArtifact(filePath: string, raw: string): DebugSessionArtifact { - let parsed: unknown; - try { - parsed = JSON.parse(raw); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to parse debug session artifact ${filePath}: ${message}`); - } - - if (!isDebugSessionArtifact(parsed)) { - throw new Error(`Malformed debug session artifact ${filePath}: schema validation failed`); - } - return parsed; -} - -function defaultDeps(deps: DebugSessionStoreDeps) { - return { - atomicWrite: deps.atomicWrite ?? atomicWriteSync, - readFile: deps.readFile ?? ((filePath: string, encoding: BufferEncoding) => readFileSync(filePath, encoding)), - listDir: deps.listDir ?? ((dirPath: string) => readdirSync(dirPath)), - exists: deps.exists ?? ((filePath: string) => existsSync(filePath)), - now: deps.now ?? (() => Date.now()), - }; -} - -function nextSlug(basePath: string, baseSlug: string, deps: ReturnType): string { - const baseArtifactPath = debugSessionArtifactPath(basePath, baseSlug); - if (!deps.exists(baseArtifactPath)) return baseSlug; - - for (let n = 2; n < MAX_COLLISION_ATTEMPTS; n++) { - const candidate = `${baseSlug}-${n}`; - const candidatePath = debugSessionArtifactPath(basePath, candidate); - if (!deps.exists(candidatePath)) return candidate; - } - - throw new Error(`Unable to allocate unique debug session slug for '${baseSlug}'`); -} - -function serializeArtifact(session: DebugSessionArtifact): string { - return JSON.stringify(session, null, 2) + "\n"; -} - -export function createDebugSession( - basePath: string, - input: CreateDebugSessionInput, - deps: DebugSessionStoreDeps = {}, -): DebugSessionRecord { - const d = defaultDeps(deps); - const issue = input.issue?.trim() ?? ""; - if (!issue) { - throw new Error("Issue text is required to create a debug session."); - } - - ensureSessionsDir(basePath); - - const baseSlug = slugifyDebugSessionIssue(issue); - const slug = nextSlug(basePath, baseSlug, d); - const now = input.createdAt ?? d.now(); - const session: DebugSessionArtifact = { - version: 1, - mode: input.mode ?? "debug", - slug, - issue, - status: input.status ?? DEFAULT_STATUS, - phase: input.phase ?? DEFAULT_PHASE, - createdAt: now, - updatedAt: now, - logPath: debugSessionLogPath(basePath, slug), - lastError: null, - }; - - const artifactPath = debugSessionArtifactPath(basePath, slug); - d.atomicWrite(artifactPath, serializeArtifact(session), "utf-8"); - - return { artifactPath, session }; -} - -export function loadDebugSession( - basePath: string, - slug: string, - deps: DebugSessionStoreDeps = {}, -): DebugSessionRecord | null { - assertValidDebugSessionSlug(slug); - const d = defaultDeps(deps); - - const artifactPath = debugSessionArtifactPath(basePath, slug); - if (!d.exists(artifactPath)) return null; - - const raw = d.readFile(artifactPath, "utf-8"); - const session = parseDebugSessionArtifact(artifactPath, raw); - return { artifactPath, session }; -} - -export function listDebugSessions( - basePath: string, - deps: DebugSessionStoreDeps = {}, -): DebugSessionListResult { - const d = defaultDeps(deps); - const dir = debugSessionsDir(basePath); - if (!d.exists(dir)) return { sessions: [], malformed: [] }; - - const entries = d.listDir(dir) - .filter(entry => entry.endsWith(SESSION_FILE_SUFFIX)) - .sort((a, b) => a.localeCompare(b)); - - const sessions: DebugSessionRecord[] = []; - const malformed: DebugMalformedSessionArtifact[] = []; - - for (const entry of entries) { - const artifactPath = join(dir, entry); - try { - const raw = d.readFile(artifactPath, "utf-8"); - const session = parseDebugSessionArtifact(artifactPath, raw); - sessions.push({ artifactPath, session }); - } catch (error) { - malformed.push({ - artifactPath, - message: error instanceof Error ? error.message : String(error), - }); - } - } - - sessions.sort((a, b) => { - if (a.session.updatedAt !== b.session.updatedAt) { - return b.session.updatedAt - a.session.updatedAt; - } - if (a.session.createdAt !== b.session.createdAt) { - return b.session.createdAt - a.session.createdAt; - } - return a.session.slug.localeCompare(b.session.slug); - }); - - return { sessions, malformed }; -} - -export function updateDebugSession( - basePath: string, - slug: string, - update: UpdateDebugSessionInput, - deps: DebugSessionStoreDeps = {}, -): DebugSessionRecord { - const d = defaultDeps(deps); - const loaded = loadDebugSession(basePath, slug, d); - if (!loaded) { - throw new Error(`Debug session not found for slug: ${slug}`); - } - - const nextIssue = update.issue?.trim() ?? loaded.session.issue; - if (!nextIssue) { - throw new Error("Issue text cannot be empty."); - } - - const nextStatus = update.status ?? loaded.session.status; - if (!isDebugSessionStatus(nextStatus)) { - throw new Error(`Invalid debug session status: ${String(update.status)}`); - } - - const nextUpdatedAt = update.updatedAt ?? d.now(); - const session: DebugSessionArtifact = { - ...loaded.session, - issue: nextIssue, - status: nextStatus, - phase: update.phase ?? loaded.session.phase, - lastError: update.lastError === undefined ? loaded.session.lastError : update.lastError, - checkpoint: update.checkpoint === undefined ? loaded.session.checkpoint : update.checkpoint, - tddGate: update.tddGate === undefined ? loaded.session.tddGate : update.tddGate, - specialistReview: update.specialistReview === undefined ? loaded.session.specialistReview : update.specialistReview, - updatedAt: nextUpdatedAt, - }; - - d.atomicWrite(loaded.artifactPath, serializeArtifact(session), "utf-8"); - return { artifactPath: loaded.artifactPath, session }; -} - -// Keep this exported for focused fault-injection tests around rename retry behavior. -export type { AtomicWriteSyncOps };