diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index 064cf16c7..c766829a0 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -1034,6 +1034,14 @@ async function dispatchNextUnit( return; } + // Guard: mid/midTitle must be defined strings from this point onward. + // The !mid check above returns early if mid is falsy; midTitle comes from + // the same object so it should always be present when mid is. + if (!midTitle) { + await stopAuto(ctx, pi); + return; + } + // ── General merge guard: merge completed slice branches before advancing ── // If we're on a gsd/MID/SID branch and that slice is done (roadmap [x]), // merge to main before dispatching the next unit. This handles: @@ -1106,6 +1114,17 @@ async function dispatchNextUnit( } } + // After merge, mid/midTitle may have been re-derived and could be undefined + if (!mid || !midTitle) { + if (currentUnit) { + const modelId = ctx.model?.id ?? "unknown"; + snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId); + saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id); + } + await stopAuto(ctx, pi); + return; + } + // Determine next unit let unitType: string; let unitId: string; @@ -1540,9 +1559,9 @@ async function dispatchNextUnit( // soft timeout; only idle/stalled tasks pause early. clearUnitTimeout(); const supervisor = resolveAutoSupervisorConfig(); - const softTimeoutMs = supervisor.soft_timeout_minutes * 60 * 1000; - const idleTimeoutMs = supervisor.idle_timeout_minutes * 60 * 1000; - const hardTimeoutMs = supervisor.hard_timeout_minutes * 60 * 1000; + const softTimeoutMs = (supervisor.soft_timeout_minutes ?? 0) * 60 * 1000; + const idleTimeoutMs = (supervisor.idle_timeout_minutes ?? 0) * 60 * 1000; + const hardTimeoutMs = (supervisor.hard_timeout_minutes ?? 0) * 60 * 1000; wrapupWarningHandle = setTimeout(() => { wrapupWarningHandle = null; diff --git a/src/resources/extensions/gsd/commands.ts b/src/resources/extensions/gsd/commands.ts index 5e122f729..09829dcbd 100644 --- a/src/resources/extensions/gsd/commands.ts +++ b/src/resources/extensions/gsd/commands.ts @@ -415,7 +415,7 @@ async function handlePrefsWizard( await saveFile(path, content); await ctx.waitForIdle(); await ctx.reload(); - ctx.ui.notify(`Saved ${scope} preferences to ${path}`, "success"); + ctx.ui.notify(`Saved ${scope} preferences to ${path}`, "info"); } /** Wrap a YAML value in double quotes if it contains special characters. */ diff --git a/src/resources/extensions/gsd/doctor.ts b/src/resources/extensions/gsd/doctor.ts index 6910c6071..a1c6ad397 100644 --- a/src/resources/extensions/gsd/doctor.ts +++ b/src/resources/extensions/gsd/doctor.ts @@ -79,7 +79,7 @@ function validatePreferenceShape(preferences: GSDPreferences): string[] { issues.push(`skill_rules[${index}].when must be a string`); } for (const key of ["use", "prefer", "avoid"] as const) { - const value = (rule as Record)[key]; + const value = (rule as unknown as Record)[key]; if (value !== undefined && !Array.isArray(value)) { issues.push(`skill_rules[${index}].${key} must be a list`); } diff --git a/src/resources/extensions/gsd/index.ts b/src/resources/extensions/gsd/index.ts index 6edc7a484..afc113cd8 100644 --- a/src/resources/extensions/gsd/index.ts +++ b/src/resources/extensions/gsd/index.ts @@ -127,7 +127,7 @@ export default function (pi: ExtensionAPI) { ...params, timeout: params.timeout ?? DEFAULT_BASH_TIMEOUT_SECS, }; - return baseBash.execute(toolCallId, paramsWithTimeout, signal, onUpdate, ctx); + return (baseBash as any).execute(toolCallId, paramsWithTimeout, signal, onUpdate, ctx); }, }; pi.registerTool(dynamicBash as any); @@ -148,7 +148,7 @@ export default function (pi: ExtensionAPI) { ctx?: any, ) => { const fresh = createWriteTool(process.cwd()); - return fresh.execute(toolCallId, params, signal, onUpdate, ctx); + return (fresh as any).execute(toolCallId, params, signal, onUpdate, ctx); }, }; pi.registerTool(dynamicWrite as any); @@ -164,7 +164,7 @@ export default function (pi: ExtensionAPI) { ctx?: any, ) => { const fresh = createReadTool(process.cwd()); - return fresh.execute(toolCallId, params, signal, onUpdate, ctx); + return (fresh as any).execute(toolCallId, params, signal, onUpdate, ctx); }, }; pi.registerTool(dynamicRead as any); @@ -180,7 +180,7 @@ export default function (pi: ExtensionAPI) { ctx?: any, ) => { const fresh = createEditTool(process.cwd()); - return fresh.execute(toolCallId, params, signal, onUpdate, ctx); + return (fresh as any).execute(toolCallId, params, signal, onUpdate, ctx); }, }; pi.registerTool(dynamicEdit as any); @@ -339,7 +339,7 @@ export default function (pi: ExtensionAPI) { "errorMessage" in lastMsg && lastMsg.errorMessage ? `: ${lastMsg.errorMessage}` : ""; - ctx.log(`Auto-mode paused due to provider error${errorDetail}`); + (ctx as any).log(`Auto-mode paused due to provider error${errorDetail}`); await pauseAuto(ctx, pi); return; } diff --git a/src/resources/extensions/gsd/preferences.ts b/src/resources/extensions/gsd/preferences.ts index 02cf905f5..e4f96aadd 100644 --- a/src/resources/extensions/gsd/preferences.ts +++ b/src/resources/extensions/gsd/preferences.ts @@ -625,7 +625,7 @@ function validatePreferences(preferences: GSDPreferences): { } const validatedRule: GSDSkillRule = { when }; for (const action of SKILL_ACTIONS) { - const values = normalizeStringList((rule as Record)[action]); + const values = normalizeStringList((rule as unknown as Record)[action]); if (values.length > 0) { validatedRule[action as keyof GSDSkillRule] = values as never; } diff --git a/src/resources/extensions/gsd/tests/migrate-command.test.ts b/src/resources/extensions/gsd/tests/migrate-command.test.ts index ec1f1dd6d..3ec2d8c1f 100644 --- a/src/resources/extensions/gsd/tests/migrate-command.test.ts +++ b/src/resources/extensions/gsd/tests/migrate-command.test.ts @@ -354,8 +354,8 @@ async function main(): Promise { assert(state.phase !== undefined, 'pipeline: deriveState returns phase'); assert(state.activeMilestone !== null, 'pipeline: deriveState has activeMilestone'); assertEq(state.activeMilestone!.id, 'M001', 'pipeline: deriveState activeMilestone is M001'); - assert(state.progress.slices !== undefined, 'pipeline: deriveState has slices progress'); - assert(state.progress.tasks !== undefined, 'pipeline: deriveState has tasks progress'); + assert(state.progress!.slices !== undefined, 'pipeline: deriveState has slices progress'); + assert(state.progress!.tasks !== undefined, 'pipeline: deriveState has tasks progress'); } finally { rmSync(base, { recursive: true, force: true }); diff --git a/src/resources/extensions/gsd/tests/migrate-transformer.test.ts b/src/resources/extensions/gsd/tests/migrate-transformer.test.ts index 9f87479a3..09d3d8d2f 100644 --- a/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +++ b/src/resources/extensions/gsd/tests/migrate-transformer.test.ts @@ -317,7 +317,7 @@ function makeResearch(fileName: string, content: string): PlanningResearch { assertEq(doneSlice?.tasks[0]?.summary?.duration, '2h', 'completion: summary duration from frontmatter'); assertEq(doneSlice?.tasks[0]?.summary?.provides, ['feature-01'], 'completion: summary provides from frontmatter'); assertEq(doneSlice?.tasks[0]?.summary?.keyFiles, ['file-01.ts'], 'completion: summary keyFiles from frontmatter'); - assert(doneSlice?.tasks[0]?.summary?.whatHappened?.includes('Summary body'), 'completion: summary whatHappened from body'); + assert(doneSlice?.tasks[0]?.summary?.whatHappened?.includes('Summary body') ?? false, 'completion: summary whatHappened from body'); assert(doneSlice?.summary !== null, 'completion: done slice has slice summary'); assert(activeSlice?.summary === null, 'completion: active slice has null summary'); assertEq(doneSlice?.tasks[0]?.estimate, '2h', 'completion: task estimate from summary duration'); diff --git a/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts b/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts index 1647a5541..5c414efe5 100644 --- a/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +++ b/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts @@ -234,18 +234,18 @@ async function main(): Promise { assertEq(state.activeSlice!.id, 'S02', 'incomplete: deriveState activeSlice is S02'); assert(state.activeTask !== null, 'incomplete: deriveState has activeTask'); assertEq(state.activeTask!.id, 'T03', 'incomplete: deriveState activeTask is T03'); - assert(state.progress.slices !== undefined, 'incomplete: deriveState has slices progress'); - assertEq(state.progress.slices!.done, 1, 'incomplete: deriveState slices done count'); - assertEq(state.progress.slices!.total, 2, 'incomplete: deriveState slices total count'); - assert(state.progress.tasks !== undefined, 'incomplete: deriveState has tasks progress'); + assert(state.progress!.slices !== undefined, 'incomplete: deriveState has slices progress'); + assertEq(state.progress!.slices!.done, 1, 'incomplete: deriveState slices done count'); + assertEq(state.progress!.slices!.total, 2, 'incomplete: deriveState slices total count'); + assert(state.progress!.tasks !== undefined, 'incomplete: deriveState has tasks progress'); // S02 has 1 task, 0 done (only active slice tasks counted) - assertEq(state.progress.tasks!.done, 0, 'incomplete: deriveState tasks done (in active slice)'); - assertEq(state.progress.tasks!.total, 1, 'incomplete: deriveState tasks total (in active slice)'); + assertEq(state.progress!.tasks!.done, 0, 'incomplete: deriveState tasks done (in active slice)'); + assertEq(state.progress!.tasks!.total, 1, 'incomplete: deriveState tasks total (in active slice)'); // Requirements - assertEq(state.requirements.active, 1, 'incomplete: deriveState requirements active'); - assertEq(state.requirements.validated, 1, 'incomplete: deriveState requirements validated'); - assertEq(state.requirements.deferred, 1, 'incomplete: deriveState requirements deferred'); - assertEq(state.requirements.outOfScope, 1, 'incomplete: deriveState requirements outOfScope'); + assertEq(state.requirements!.active, 1, 'incomplete: deriveState requirements active'); + assertEq(state.requirements!.validated, 1, 'incomplete: deriveState requirements validated'); + assertEq(state.requirements!.deferred, 1, 'incomplete: deriveState requirements deferred'); + assertEq(state.requirements!.outOfScope, 1, 'incomplete: deriveState requirements outOfScope'); // (f) generatePreview console.log(' --- generatePreview ---'); diff --git a/src/resources/extensions/gsd/types.ts b/src/resources/extensions/gsd/types.ts index 839c97506..6ed114844 100644 --- a/src/resources/extensions/gsd/types.ts +++ b/src/resources/extensions/gsd/types.ts @@ -176,6 +176,7 @@ export interface GSDState { blockers: string[]; nextAction: string; activeBranch?: string; + activeWorkspace?: string; registry: MilestoneRegistryEntry[]; requirements?: RequirementCounts; progress?: { diff --git a/tsconfig.extensions.json b/tsconfig.extensions.json new file mode 100644 index 000000000..b2f249376 --- /dev/null +++ b/tsconfig.extensions.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "allowImportingTsExtensions": true, + "rootDir": "." + }, + "include": ["src/resources/extensions/gsd"], + "exclude": [] +}