feat(FN-1761): fix lint errors in executor.test.ts and restart.integration.test.ts

- executor.test.ts: remove unused imports (Column, StuckTaskDetector),
  replace Function type with EventListener, add MockTaskStore interface
- restart.integration.test.ts: replace require() with ESM import,
  replace Function types with proper function signatures
- All tests pass
This commit is contained in:
Fusion
2026-04-15 03:00:54 -07:00
committed by gsxdsm
parent 885f29382c
commit a6c8e2886f
9 changed files with 309 additions and 236 deletions

View File

@@ -143,9 +143,8 @@ import { execSync } from "node:child_process";
import { findWorktreeUser, aiMergeTask } from "./merger.js";
import { WorktreePool } from "./worktree-pool.js";
import { generateWorktreeName, slugify } from "./worktree-names.js";
import type { Column, Task, TaskDetail } from "@fusion/core";
import type { Task, TaskDetail } from "@fusion/core";
import { SessionManager } from "@mariozechner/pi-coding-agent";
import { StuckTaskDetector } from "./stuck-task-detector.js";
import { StepSessionExecutor } from "./step-session-executor.js";
import { executorLog } from "./logger.js";
@@ -155,16 +154,18 @@ const mockedGenerateWorktreeName = vi.mocked(generateWorktreeName);
const mockedFindWorktreeUser = vi.mocked(findWorktreeUser);
const mockedStepSessionExecutor = vi.mocked(StepSessionExecutor);
type EventListener = (...args: unknown[]) => void;
function createMockStore() {
const listeners = new Map<string, Function[]>();
const listeners = new Map<string, EventListener[]>();
const store = {
on: vi.fn((event: string, fn: Function) => {
on: vi.fn((event: string, fn: EventListener) => {
const existing = listeners.get(event) || [];
existing.push(fn);
listeners.set(event, existing);
}),
/** Trigger registered listeners for an event (test helper). */
_trigger(event: string, ...args: any[]) {
_trigger(event: string, ...args: unknown[]) {
for (const fn of listeners.get(event) || []) fn(...args);
},
emit: vi.fn(),

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { exec } from "node:child_process";
import { promisify } from "node:util";
@@ -7,7 +6,6 @@ import { join } from "node:path";
import { existsSync } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import type { TaskStore, Task, TaskDetail, StepStatus, Settings, WorkflowStep, MissionStore, Slice, AgentState, AgentCapability, RunMutationContext } from "@fusion/core";
import { buildExecutionMemoryInstructions, getTaskMergeBlocker, resolveAgentPrompt } from "@fusion/core";
import { findWorktreeUser } from "./merger.js";
import { generateWorktreeName, slugify } from "./worktree-names.js";
@@ -516,9 +514,10 @@ export class TaskExecutor {
} else {
executorLog.log(`${task.id}: model ${newProvider}/${newModelId} not found in registry for hot-swap`);
}
} catch (err: any) {
executorLog.error(`${task.id}: failed to hot-swap model: ${err.message}`);
await this.store.logEntry(task.id, `Model change failed: ${err.message}`, undefined, this.currentRunContext);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
executorLog.error(`${task.id}: failed to hot-swap model: ${errorMessage}`);
await this.store.logEntry(task.id, `Model change failed: ${errorMessage}`, undefined, this.currentRunContext);
}
}
}
@@ -690,8 +689,9 @@ export class TaskExecutor {
this.options.stuckTaskDetector?.untrackTask(task.id);
executorLog.log(`Review handoff complete for ${task.id} — task moved to in-review`);
} catch (err: any) {
executorLog.error(`Failed to execute review handoff for ${task.id}: ${err.message}`);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
executorLog.error(`Failed to execute review handoff for ${task.id}: ${errorMessage}`);
}
}
@@ -729,8 +729,9 @@ export class TaskExecutor {
executorLog.log(`${task.id} auto-recovered completed task → in-review`);
this.options.onComplete?.(task);
return true;
} catch (err: any) {
executorLog.error(`Failed to recover completed task ${task.id}: ${err.message}`);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
executorLog.error(`Failed to recover completed task ${task.id}: ${errorMessage}`);
return false;
}
}
@@ -1007,14 +1008,15 @@ export class TaskExecutor {
} else {
await this.store.logEntry(task.id, `Acquired worktree from pool: ${worktreePath}`, undefined, this.currentRunContext);
}
} catch (poolErr: any) {
} catch (poolErr: unknown) {
// Pool preparation failed — release the worktree back and fall through
// to fresh worktree creation
const poolErrMessage = poolErr instanceof Error ? poolErr.message : String(poolErr);
this.options.pool.release(pooled);
executorLog.log(`Pool prepareForTask failed, falling through to fresh worktree: ${poolErr.message}`);
executorLog.log(`Pool prepareForTask failed, falling through to fresh worktree: ${poolErrMessage}`);
await this.store.logEntry(
task.id,
`Pool worktree preparation failed (${poolErr.message}), creating fresh worktree`,
`Pool worktree preparation failed (${poolErrMessage}), creating fresh worktree`,
undefined,
this.currentRunContext,
);
@@ -1049,8 +1051,11 @@ export class TaskExecutor {
timeout: 120_000,
});
await this.store.logEntry(task.id, "Worktree init command completed", settings.worktreeInitCommand, this.currentRunContext);
} catch (err: any) {
const message = err.stderr?.toString() || err.message || "Unknown error";
} catch (err: unknown) {
const execError = err instanceof Error ? err : new Error(String(err));
const message = "stderr" in execError && typeof (execError as Record<string, unknown>).stderr === "string"
? String((execError as Record<string, unknown>).stderr)
: execError.message;
await this.store.logEntry(task.id, `Worktree init command failed: ${message}`, undefined, this.currentRunContext);
}
}
@@ -1065,8 +1070,11 @@ export class TaskExecutor {
timeout: 120_000,
});
await this.store.logEntry(task.id, `Setup script '${settings.setupScript}' completed`, scriptCommand, this.currentRunContext);
} catch (err: any) {
const message = err.stderr?.toString() || err.message || "Unknown error";
} catch (err: unknown) {
const execError = err instanceof Error ? err : new Error(String(err));
const message = "stderr" in execError && typeof (execError as Record<string, unknown>).stderr === "string"
? String((execError as Record<string, unknown>).stderr)
: execError.message;
await this.store.logEntry(task.id, `Setup script '${settings.setupScript}' failed: ${message}`, undefined, this.currentRunContext);
}
} else {
@@ -1101,8 +1109,9 @@ export class TaskExecutor {
executorLog.log(`${task.id}: captured baseCommitSha ${baseCommitSha.slice(0, 7)}`);
// Audit trail: record base commit capture for later diff computation (FN-1404)
await audit.git({ type: "commit:create", target: baseCommitSha, metadata: { purpose: "base" } });
} catch (err: any) {
executorLog.log(`Failed to capture baseCommitSha for ${task.id}: ${err.message}`);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
executorLog.log(`Failed to capture baseCommitSha for ${task.id}: ${errorMessage}`);
// Non-fatal: task can continue without baseCommitSha
}
}
@@ -1260,7 +1269,8 @@ export class TaskExecutor {
} else {
await retryableStepWork();
}
} catch (err: any) {
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
if (this.depAborted.has(task.id)) {
this.depAborted.delete(task.id);
await this.handleDepAbortCleanup(task.id, worktreePath);
@@ -1271,9 +1281,9 @@ export class TaskExecutor {
} else if (this.stuckAborted.has(task.id)) {
stuckRequeue = this.stuckAborted.get(task.id) ?? true;
this.stuckAborted.delete(task.id);
} else if (this.options.usageLimitPauser && isUsageLimitError(err.message)) {
await this.options.usageLimitPauser.onUsageLimitHit("executor", task.id, err.message);
} else if (isTransientError(err.message)) {
} else if (this.options.usageLimitPauser && isUsageLimitError(errorMessage)) {
await this.options.usageLimitPauser.onUsageLimitHit("executor", task.id, errorMessage);
} else if (isTransientError(errorMessage)) {
const decision = computeRecoveryDecision({
recoveryRetryCount: task.recoveryRetryCount,
nextRecoveryAt: task.nextRecoveryAt,
@@ -1282,17 +1292,17 @@ export class TaskExecutor {
if (decision.shouldRetry) {
const attempt = decision.nextState.recoveryRetryCount;
const delay = formatDelay(decision.delayMs);
if (!isSilentTransientError(err.message)) {
executorLog.warn(`${task.id} transient error — retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay}: ${err.message}`);
await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay}): ${err.message}`, undefined, this.currentRunContext);
if (!isSilentTransientError(errorMessage)) {
executorLog.warn(`${task.id} transient error — retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay}: ${errorMessage}`);
await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay}): ${errorMessage}`, undefined, this.currentRunContext);
}
if (worktreePath && existsSync(worktreePath)) {
try {
await execAsync(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
// Audit trail: record worktree removal (FN-1404)
await audit.git({ type: "worktree:remove", target: worktreePath });
} catch (_err) {
// Ignore errors during worktree cleanup on stuck kill
} catch {
// Worktree removal failed - ignoring since we're cleaning up anyway
}
}
await this.store.updateTask(task.id, {
@@ -1306,23 +1316,23 @@ export class TaskExecutor {
return;
}
executorLog.error(`${task.id} transient error retries exhausted: ${err.message}`);
executorLog.error(`${task.id} transient error retries exhausted: ${errorMessage}`);
await this.store.updateTask(task.id, {
status: "failed",
error: err.message,
error: errorMessage,
recoveryRetryCount: null,
nextRecoveryAt: null,
});
await this.store.moveTask(task.id, "in-review");
executorLog.log(`${task.id} transient retries exhausted → in-review`);
this.options.onError?.(task, err);
this.options.onError?.(task, err instanceof Error ? err : new Error(errorMessage));
} else {
executorLog.error(`${task.id} step-session execution failed:`, err.message);
await this.store.logEntry(task.id, `Step-session execution failed: ${err.message}`, undefined, this.currentRunContext);
await this.store.updateTask(task.id, { status: "failed", error: err.message });
executorLog.error(`${task.id} step-session execution failed:`, errorMessage);
await this.store.logEntry(task.id, `Step-session execution failed: ${errorMessage}`, undefined, this.currentRunContext);
await this.store.updateTask(task.id, { status: "failed", error: errorMessage });
await this.store.moveTask(task.id, "in-review");
executorLog.log(`${task.id} step-session execution failed → in-review`);
this.options.onError?.(task, err);
this.options.onError?.(task, err instanceof Error ? err : new Error(errorMessage));
}
} finally {
this.executing.delete(task.id);
@@ -1342,8 +1352,8 @@ export class TaskExecutor {
if (worktreePath && existsSync(worktreePath)) {
try {
await execAsync(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
} catch (_err) {
// Ignore errors during worktree cleanup on stuck kill
} catch {
// Worktree removal failed - ignoring since we're cleaning up anyway
}
}
await this.store.updateTask(task.id, { status: "stuck-killed", worktree: null, branch: null });
@@ -1351,8 +1361,9 @@ export class TaskExecutor {
await this.store.moveTask(task.id, "todo");
executorLog.log(`${task.id} moved to todo for retry after stuck kill`);
}
} catch (err: any) {
executorLog.error(`Failed to requeue stuck task ${task.id}: ${err.message}`);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
executorLog.error(`Failed to requeue stuck task ${task.id}: ${errorMessage}`);
}
stuckRequeue = null; // Prevent outer finally from re-processing
}
@@ -1438,6 +1449,7 @@ export class TaskExecutor {
executorInstructions,
);
// sessionFile must be let because it's destructured alongside session which is reassigned
// eslint-disable-next-line prefer-const
let { session, sessionFile } = await createKbAgent({
cwd: worktreePath,
@@ -1493,6 +1505,7 @@ export class TaskExecutor {
executorLog.log(`${task.id}: session registered (model=${describeModel(session)}, stuckDetector=${!!stuckDetector})`);
// Invoke plugin onAgentRunStart hook (fire-and-forget)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
void (this.options.pluginRunner as any)?.invokeHook("onAgentRunStart", task.id);
try {
@@ -1783,6 +1796,7 @@ export class TaskExecutor {
this.store.updateTask(task.id, { sessionFile: null }).catch(() => {});
}
// Invoke plugin onAgentRunEnd hook (fire-and-forget)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
void (this.options.pluginRunner as any)?.invokeHook("onAgentRunEnd", task.id);
}
};
@@ -1800,26 +1814,28 @@ export class TaskExecutor {
} else {
await retryableWork();
}
} catch (err: any) {
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
if (this.depAborted.has(task.id)) {
// Dependency added mid-execution — discard worktree and move to triage
this.depAborted.delete(task.id);
await this.handleDepAbortCleanup(task.id, worktreePath);
} else if (err.message?.includes("Invalid transition")) {
} else if (errorMessage.includes("Invalid transition")) {
// Task was moved by user/process while executor was running — already in desired state
// This check must come before pausedAborted since it's more specific
const transitionMatch = err.message.match(/Invalid transition: '([^']+)' → '([^']+)'/);
const transitionMatch = errorMessage.match(/Invalid transition: '([^']+)' → '([^']+)'/);
const fromColumn = transitionMatch?.[1] ?? "unknown";
const toColumn = transitionMatch?.[2] ?? "unknown";
const logMessage = `Task already moved from '${fromColumn}' — skipping transition to '${toColumn}'`;
executorLog.log(`${task.id} ${logMessage}`);
await this.store.logEntry(task.id, logMessage, err.message, this.currentRunContext);
await this.store.logEntry(task.id, logMessage, errorMessage, this.currentRunContext);
if (fromColumn === "in-review" && toColumn === "in-review") {
try {
const finalizeResult = await this.finalizeAlreadyReviewedTask(task.id);
executorLog.log(`${task.id} duplicate in-review finalization result: ${finalizeResult}`);
} catch (finalizeErr: any) {
executorLog.warn(`${task.id} failed to finalize duplicate in-review transition: ${finalizeErr.message}`);
} catch (finalizeErr: unknown) {
const finalizeErrMessage = finalizeErr instanceof Error ? finalizeErr.message : String(finalizeErr);
executorLog.warn(`${task.id} failed to finalize duplicate in-review transition: ${finalizeErrMessage}`);
}
}
// Task finished successfully (just already moved), so call onComplete
@@ -1840,8 +1856,9 @@ export class TaskExecutor {
executorLog.log(`Removed old worktree for paused task: ${worktreePath}`);
// Audit trail: record worktree removal (FN-1404)
await audit.git({ type: "worktree:remove", target: worktreePath });
} catch (cleanupErr: any) {
executorLog.warn(`Failed to remove old worktree ${worktreePath}: ${cleanupErr.message}`);
} catch (cleanupErr: unknown) {
const cleanupErrMessage = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
executorLog.warn(`Failed to remove old worktree ${worktreePath}: ${cleanupErrMessage}`);
}
}
await this.store.updateTask(task.id, { worktree: undefined, branch: undefined });
@@ -1855,10 +1872,6 @@ export class TaskExecutor {
this.stuckAborted.delete(task.id);
executorLog.log(`${task.id} terminated by stuck task detector — will ${stuckRequeue ? "retry" : "not retry (budget exhausted)"}`);
} else {
// Normalize error message for consistent handling across different error types.
// This ensures we don't make assumptions about error object structure.
const errorMessage = typeof err === "string" ? err : err?.message ?? String(err);
// Check if the error is a context-limit error and attempt bounded recovery
// before falling through to the normal failure path. Recovery strategy:
// 1. Try compact-and-resume (compacts session history, then resumes with same prompt)
@@ -1905,9 +1918,9 @@ export class TaskExecutor {
// without marking the task as failed. The agent will continue execution
// and call task_done or complete implicitly.
return;
} catch (resumeErr: any) {
} catch (resumeErr: unknown) {
// Resume after context compaction failed — fall through to reduced-prompt retry
const resumeErrorMessage = resumeErr?.message ?? String(resumeErr);
const resumeErrorMessage = resumeErr instanceof Error ? resumeErr.message : String(resumeErr);
executorLog.error(`${task.id} resume after context compaction failed: ${resumeErrorMessage}`);
await this.store.logEntry(task.id, `Resume after context compaction failed: ${resumeErrorMessage}`, undefined, this.currentRunContext);
// Fall through to reduced-prompt retry below
@@ -1944,8 +1957,8 @@ export class TaskExecutor {
executorLog.log(`${task.id} reduced-prompt recovery succeeded — continuing`);
await this.store.logEntry(task.id, "Reduced-prompt recovery succeeded — continuing execution", undefined, this.currentRunContext);
return;
} catch (reducedErr: any) {
const reducedErrorMessage = reducedErr?.message ?? String(reducedErr);
} catch (reducedErr: unknown) {
const reducedErrorMessage = reducedErr instanceof Error ? reducedErr.message : String(reducedErr);
executorLog.error(`${task.id} reduced-prompt recovery also failed: ${reducedErrorMessage}`);
await this.store.logEntry(task.id, `Reduced-prompt recovery failed: ${reducedErrorMessage}`, undefined, this.currentRunContext);
// Fall through to mark task as failed
@@ -1976,8 +1989,9 @@ export class TaskExecutor {
executorLog.log(`Removed old worktree for transient retry: ${worktreePath}`);
// Audit trail: record worktree removal (FN-1404)
await audit.git({ type: "worktree:remove", target: worktreePath });
} catch (cleanupErr: any) {
executorLog.warn(`Failed to remove old worktree ${worktreePath}: ${cleanupErr.message}`);
} catch (cleanupErr: unknown) {
const cleanupErrMessage = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
executorLog.warn(`Failed to remove old worktree ${worktreePath}: ${cleanupErrMessage}`);
}
}
await this.store.updateTask(task.id, {
@@ -2001,7 +2015,7 @@ export class TaskExecutor {
});
await this.store.moveTask(task.id, "in-review");
executorLog.log(`${task.id} transient retries exhausted → in-review`);
this.options.onError?.(task, err);
this.options.onError?.(task, err instanceof Error ? err : new Error(errorMessage));
return;
}
executorLog.error(`${task.id} execution failed:`, errorMessage);
@@ -2009,7 +2023,7 @@ export class TaskExecutor {
await this.store.updateTask(task.id, { status: "failed", error: errorMessage });
await this.store.moveTask(task.id, "in-review");
executorLog.log(`${task.id} execution failed → in-review`);
this.options.onError?.(task, err);
this.options.onError?.(task, err instanceof Error ? err : new Error(errorMessage));
}
} finally {
this.executing.delete(task.id);
@@ -2038,8 +2052,9 @@ export class TaskExecutor {
executorLog.log(`Removed old worktree for stuck-killed retry: ${worktreePath}`);
// Audit trail: record worktree removal (FN-1404)
await audit.git({ type: "worktree:remove", target: worktreePath });
} catch (cleanupErr: any) {
executorLog.warn(`Failed to remove old worktree ${worktreePath}: ${cleanupErr.message}`);
} catch (cleanupErr: unknown) {
const cleanupErrMessage = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
executorLog.warn(`Failed to remove old worktree ${worktreePath}: ${cleanupErrMessage}`);
}
}
await this.store.updateTask(task.id, { status: "stuck-killed", worktree: null, branch: null });
@@ -2054,8 +2069,9 @@ export class TaskExecutor {
} else {
executorLog.log(`${task.id} already in todo — skipping redundant move`);
}
} catch (err: any) {
executorLog.error(`Failed to requeue stuck task ${task.id}: ${err.message}`);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
executorLog.error(`Failed to requeue stuck task ${task.id}: ${errorMessage}`);
}
}
}
@@ -2389,8 +2405,9 @@ export class TaskExecutor {
try {
await execAsync(`git reset --hard ${baseline}`, { cwd: worktreePath });
executorLog.log(`${taskId}: RETHINK — git reset --hard ${baseline}`);
} catch (gitErr: any) {
executorLog.error(`${taskId}: RETHINK git reset failed: ${gitErr.message}`);
} catch (gitErr: unknown) {
const gitErrMessage = gitErr instanceof Error ? gitErr.message : String(gitErr);
executorLog.error(`${taskId}: RETHINK git reset failed: ${gitErrMessage}`);
}
} else if (reviewType === "code") {
executorLog.log(`${taskId}: RETHINK — no baseline SHA, skipping git reset`);
@@ -2410,8 +2427,9 @@ export class TaskExecutor {
`RETHINK: ${result.summary || "Approach rejected by reviewer"}`,
);
executorLog.log(`${taskId}: RETHINK — branched from checkpoint ${checkpointId}`);
} catch (branchErr: any) {
executorLog.error(`${taskId}: RETHINK session rewind failed: ${branchErr.message}`);
} catch (branchErr: unknown) {
const branchErrMessage = branchErr instanceof Error ? branchErr.message : String(branchErr);
executorLog.error(`${taskId}: RETHINK session rewind failed: ${branchErrMessage}`);
}
}
} else {
@@ -2442,11 +2460,12 @@ export class TaskExecutor {
}
return { content: [{ type: "text" as const, text }], details: {} };
} catch (err: any) {
reviewerLog.error(`${taskId}: review failed: ${err.message}`);
await store.logEntry(taskId, `${reviewType} review failed: ${err.message}`);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
reviewerLog.error(`${taskId}: review failed: ${errorMessage}`);
await store.logEntry(taskId, `${reviewType} review failed: ${errorMessage}`);
return {
content: [{ type: "text" as const, text: `UNAVAILABLE — reviewer error: ${err.message}` }],
content: [{ type: "text" as const, text: `UNAVAILABLE — reviewer error: ${errorMessage}` }],
details: {},
};
}
@@ -2539,8 +2558,9 @@ export class TaskExecutor {
await this.store.moveTask(task.id, "todo");
await this.store.moveTask(task.id, "in-progress");
executorLog.log(`${task.id}: revision rerun scheduled — moved to todo then in-progress`);
} catch (err: any) {
executorLog.error(`${task.id}: failed to schedule revision rerun: ${err.message}`);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
executorLog.error(`${task.id}: failed to schedule revision rerun: ${errorMessage}`);
// Fallback: log entry and let scheduler pick it up on next tick
await this.store.logEntry(
task.id,
@@ -2608,8 +2628,9 @@ ${feedback}
try {
await writeFile(promptPath, newContent);
executorLog.log(`${task.id}: injected workflow revision instructions into PROMPT.md`);
} catch (err: any) {
executorLog.error(`${task.id}: failed to inject revision instructions: ${err.message}`);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
executorLog.error(`${task.id}: failed to inject revision instructions: ${errorMessage}`);
}
}
@@ -2667,8 +2688,9 @@ ${feedback}
await this.store.moveTask(task.id, "todo");
await this.store.moveTask(task.id, "in-progress");
executorLog.log(`${task.id}: workflow step retry scheduled — moved to todo then in-progress`);
} catch (err: any) {
executorLog.error(`${task.id}: failed to schedule workflow step retry: ${err.message}`);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
executorLog.error(`${task.id}: failed to schedule workflow step retry: ${errorMessage}`);
// Fallback: log entry and let scheduler pick it up on next tick
await this.store.logEntry(
task.id,
@@ -2757,8 +2779,9 @@ ${failureFeedback}
try {
await writeFile(promptPath, newContent);
executorLog.log(`${task.id}: injected workflow step failure instructions into PROMPT.md (retry ${retryCount}/${MAX_WORKFLOW_STEP_RETRIES})`);
} catch (err: any) {
executorLog.error(`${task.id}: failed to inject workflow step failure instructions: ${err.message}`);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
executorLog.error(`${task.id}: failed to inject workflow step failure instructions: ${errorMessage}`);
}
}
@@ -2811,8 +2834,9 @@ ${failureFeedback}
}
return output.split("\n").filter(Boolean);
} catch (err: any) {
executorLog.log(`Failed to capture modified files: ${err.message}`);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
executorLog.log(`Failed to capture modified files: ${errorMessage}`);
return [];
}
}
@@ -2981,21 +3005,22 @@ ${failureFeedback}
stepName: ws.name,
};
}
} catch (err: any) {
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
const completedAt = new Date().toISOString();
await this.store.logEntry(
task.id,
`[pre-merge] Workflow step failed: ${ws.name}`,
err.message || "Unknown error",
errorMessage,
);
executorLog.error(`${task.id} — [pre-merge] workflow step error: ${ws.name}${err.message}`);
executorLog.error(`${task.id} — [pre-merge] workflow step error: ${ws.name}${errorMessage}`);
// Update existing pending entry in place
const existingIdx = results.findIndex(r => r.workflowStepId === ws.id);
if (existingIdx >= 0) {
results[existingIdx] = {
...results[existingIdx],
status: "failed",
output: err.message || "Workflow step error",
output: errorMessage || "Workflow step error",
completedAt,
};
}
@@ -3003,7 +3028,7 @@ ${failureFeedback}
return {
allPassed: false,
revisionRequested: false,
feedback: err.message || "Workflow step error",
feedback: errorMessage || "Workflow step error",
stepName: ws.name,
};
}
@@ -3043,15 +3068,16 @@ ${failureFeedback}
timeout: 120_000,
});
return { success: true, output: `Script '${scriptName}' completed successfully` };
} catch (err: any) {
const stderr = err.stderr?.toString()?.trim() || "";
const stdout = err.stdout?.toString()?.trim() || "";
const exitCode = err.code ?? err.status;
} catch (err: unknown) {
const execError = err instanceof Error ? err : new Error(String(err));
const stderr = "stderr" in execError && typeof execError.stderr === "string" ? execError.stderr.trim() : "";
const stdout = "stdout" in execError && typeof execError.stdout === "string" ? execError.stdout.trim() : "";
const exitCode = "code" in execError ? execError.code : ("status" in execError ? execError.status : undefined);
const parts: string[] = [];
if (exitCode !== undefined) parts.push(`Exit code: ${exitCode}`);
if (stdout) parts.push(`stdout: ${truncateWorkflowScriptOutput(stdout)}`);
if (stderr) parts.push(`stderr: ${truncateWorkflowScriptOutput(stderr)}`);
if (!parts.length) parts.push(err.message || "Unknown error");
if (!parts.length) parts.push(execError.message || "Unknown error");
const errorOutput = parts.join("\n");
return { success: false, error: errorOutput };
}
@@ -3196,9 +3222,10 @@ and show an appropriate message to the user.\`
}
return { success: true, output };
} catch (err: any) {
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
await agentLogger.flush();
return { success: false, error: err.message };
return { success: false, error: errorMessage };
}
}
@@ -3227,17 +3254,18 @@ and show an appropriate message to the user.\`
for (let attempt = 0; attempt < this.MAX_WORKTREE_RETRIES; attempt++) {
try {
return await this.tryCreateWorktree(branch, currentPath, taskId, startPoint, attempt);
} catch (error: any) {
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
const isLastAttempt = attempt === this.MAX_WORKTREE_RETRIES - 1;
if (isLastAttempt) {
await this.store.logEntry(
taskId,
`Worktree creation failed after ${this.MAX_WORKTREE_RETRIES} attempts`,
error.message,
errorMessage,
);
throw new Error(
`Failed to create worktree after ${this.MAX_WORKTREE_RETRIES} attempts: ${error.message}`,
`Failed to create worktree after ${this.MAX_WORKTREE_RETRIES} attempts: ${errorMessage}`,
);
}
@@ -3272,8 +3300,9 @@ and show an appropriate message to the user.\`
);
try {
await execAsync(`rm -rf "${path}"`, { cwd: this.rootDir });
} catch (e: any) {
throw new Error(`Failed to remove existing directory ${path}: ${e.message}`);
} catch (e: unknown) {
const eMessage = e instanceof Error ? e.message : String(e);
throw new Error(`Failed to remove existing directory ${path}: ${eMessage}`);
}
} else {
executorLog.log(`Worktree already exists: ${path}`);
@@ -3299,7 +3328,7 @@ and show an appropriate message to the user.\`
await this.store.logEntry(taskId, `Worktree created on attempt ${attemptNumber + 1}`, path);
}
return { path, branch };
} catch (initialError: any) {
} catch (initialError: unknown) {
const conflictInfo = this.extractWorktreeConflictInfo(initialError);
// Handle "already used by worktree" conflict
@@ -3345,7 +3374,8 @@ and show an appropriate message to the user.\`
await createFromExistingBranch();
executorLog.log(`Worktree created from existing branch: ${path}`);
return { path, branch };
} catch (fallbackError: any) {
} catch (fallbackError: unknown) {
const fallbackErrorMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
// Check if the fallback also hit an "already used" conflict
const fallbackConflictInfo = this.extractWorktreeConflictInfo(fallbackError);
if (fallbackConflictInfo.type === "already-used" && fallbackConflictInfo.path) {
@@ -3374,7 +3404,7 @@ and show an appropriate message to the user.\`
}
}
throw new Error(`Failed to create worktree: ${fallbackError.message}`);
throw new Error(`Failed to create worktree: ${fallbackErrorMessage}`);
}
}
}
@@ -3413,7 +3443,7 @@ and show an appropriate message to the user.\`
newPath,
);
return await this.tryCreateWorktree(suffixedBranch, newPath, taskId, startPoint, attemptNumber);
} catch (suffixErr: any) {
} catch (suffixErr: unknown) {
const info = this.extractWorktreeConflictInfo(suffixErr);
if (info.type === "already-used") {
// This suffixed branch is also in use — try next suffix
@@ -3481,18 +3511,15 @@ and show an appropriate message to the user.\`
cwd: this.rootDir,
});
await this.store.logEntry(taskId, `Unlocked worktree`, worktreePath);
} catch (_err) {
} catch {
// Unlock failed - worktree wasn't locked, that's fine
}
// Remove the worktree
try {
await execAsync(`git worktree remove "${worktreePath}" --force`, {
cwd: this.rootDir,
});
} finally {
await this.store.logEntry(taskId, `Removed conflicting worktree`, worktreePath);
}
await execAsync(`git worktree remove "${worktreePath}" --force`, {
cwd: this.rootDir,
});
await this.store.logEntry(taskId, `Removed conflicting worktree`, worktreePath);
// Delete the branch if it exists
try {
@@ -3505,11 +3532,12 @@ and show an appropriate message to the user.\`
}
return true;
} catch (error: any) {
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
await this.store.logEntry(
taskId,
`Failed to clean up conflicting worktree`,
`${worktreePath}: ${error.message}`,
`${worktreePath}: ${errorMessage}`,
);
return false;
}
@@ -3544,11 +3572,12 @@ and show an appropriate message to the user.\`
});
await this.store.logEntry(taskId, `Removed stale branch`, branch);
return true;
} catch (branchDeleteError: any) {
} catch (branchDeleteError: unknown) {
const branchDeleteErrorMessage = branchDeleteError instanceof Error ? branchDeleteError.message : String(branchDeleteError);
await this.store.logEntry(
taskId,
`git branch -D failed for stale branch, trying update-ref`,
`${branch}: ${branchDeleteError.message}`,
`${branch}: ${branchDeleteErrorMessage}`,
);
}
@@ -3560,11 +3589,12 @@ and show an appropriate message to the user.\`
});
await this.store.logEntry(taskId, `Force-removed stale branch reference via update-ref`, refPath);
return true;
} catch (updateRefError: any) {
} catch (updateRefError: unknown) {
const updateRefErrorMessage = updateRefError instanceof Error ? updateRefError.message : String(updateRefError);
await this.store.logEntry(
taskId,
`Failed to remove stale branch reference`,
`${branch}: ${updateRefError.message}`,
`${branch}: ${updateRefErrorMessage}`,
);
return false;
}
@@ -3578,12 +3608,17 @@ and show an appropriate message to the user.\`
* - "could not create leading directories"
* - "working tree already exists"
*/
private extractWorktreeConflictInfo(error: any): {
private extractWorktreeConflictInfo(error: unknown): {
type: "already-used" | "invalid-reference" | "leading-directories" | "already-exists" | "unknown";
path?: string;
message?: string;
} {
const output = [error?.message, error?.stderr?.toString?.(), error?.stdout?.toString?.()]
const execError = error instanceof Error ? error : new Error(String(error));
const output = [
execError.message,
"stderr" in execError && typeof execError.stderr === "string" ? execError.stderr.toString() : undefined,
"stdout" in execError && typeof execError.stdout === "string" ? execError.stdout.toString() : undefined,
]
.filter(Boolean)
.join("\n");
@@ -3639,8 +3674,9 @@ and show an appropriate message to the user.\`
try {
await execAsync(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
executorLog.log(`Cleaned up worktree for ${taskId}`);
} catch (err: any) {
executorLog.error(`Failed to clean up worktree for ${taskId}:`, err.message);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
executorLog.error(`Failed to clean up worktree for ${taskId}:`, errorMessage);
}
}
@@ -3740,8 +3776,9 @@ and show an appropriate message to the user.\`
this.executing.delete(taskId);
this.stuckAborted.delete(taskId);
executorLog.log(`${taskId} force-requeued to todo`);
} catch (err: any) {
executorLog.error(`Failed to force-requeue stuck task ${taskId}: ${err.message}`);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
executorLog.error(`Failed to force-requeue stuck task ${taskId}: ${errorMessage}`);
}
}, FORCE_REQUEUE_GRACE_MS);
}
@@ -3805,8 +3842,9 @@ and show an appropriate message to the user.\`
"approach. Do NOT repeat the same actions. Advance to the next step if the " +
"current work is complete.",
);
} catch (err: any) {
executorLog.error(`${taskId} failed to steer after compaction: ${err.message}`);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
executorLog.error(`${taskId} failed to steer after compaction: ${errorMessage}`);
// Recovery-pending is still set — the execution flow will handle it
}
@@ -3876,12 +3914,13 @@ and show an appropriate message to the user.\`
try {
await this.options.agentStore?.updateAgentState(agentId, "active");
} catch { /* non-critical */ }
} catch (err: any) {
} catch (err: unknown) {
// Error during execution — mark as error
try {
await this.options.agentStore?.updateAgentState(agentId, "error");
} catch { /* non-critical */ }
executorLog.warn(`Child agent ${agentId} failed: ${err.message}`);
const errorMessage = err instanceof Error ? err.message : String(err);
executorLog.warn(`Child agent ${agentId} failed: ${errorMessage}`);
} finally {
this.childSessions.delete(agentId);
this.totalSpawnedCount = Math.max(0, this.totalSpawnedCount - 1);
@@ -3987,8 +4026,9 @@ and show an appropriate message to the user.\`
this.totalSpawnedCount++;
// Run child asynchronously (don't await — parent continues working)
this.runSpawnedChild(agent.id, childSession, taskPrompt).catch((err: any) => {
executorLog.warn(`Child agent ${agent.id} async error: ${err.message}`);
this.runSpawnedChild(agent.id, childSession, taskPrompt).catch((err: unknown) => {
const errorMessage = err instanceof Error ? err.message : String(err);
executorLog.warn(`Child agent ${agent.id} async error: ${errorMessage}`);
});
const result: SpawnAgentResult = {
@@ -4003,10 +4043,11 @@ and show an appropriate message to the user.\`
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
details: result,
};
} catch (err: any) {
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
return {
content: [{ type: "text" as const, text: `Failed to spawn agent: ${err.message}` }],
details: { agentId: "", state: "error", message: err.message },
content: [{ type: "text" as const, text: `Failed to spawn agent: ${errorMessage}` }],
details: { agentId: "", state: "error", message: errorMessage },
};
}
},

View File

@@ -502,7 +502,7 @@ export class PluginRunner {
this.hookTimeoutMs,
`Hook ${hookName} timed out`,
);
} catch (_err) {
} catch {
// Error already logged by invokeHook
}
}

View File

@@ -432,6 +432,8 @@ export class ProjectEngine {
continue;
}
// Intentional cast to access Task properties needed by merge validation
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!this.canMergeTask(task as any)) {
continue;
}
@@ -453,6 +455,7 @@ export class ProjectEngine {
}
// Auto-heal verification buffer failures by resetting retry counter
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (this.hasAutoHealableVerificationBufferFailure(task as any)) {
await store.logEntry(
taskId,
@@ -461,6 +464,7 @@ export class ProjectEngine {
await store.updateTask(taskId, { mergeRetries: 0, error: null, status: null });
} else if (
(task.mergeRetries ?? 0) >= ProjectEngine.MAX_AUTO_MERGE_RETRIES &&
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.isRetryCooldownElapsed(task as any)
) {
await store.logEntry(
@@ -528,9 +532,13 @@ export class ProjectEngine {
} else {
// Direct merge via AI agent, gated by semaphore
runtimeLog.log(`${manualResolver ? "Manual" : "Auto"}-merge merging ${taskId}...`);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const semaphore = (this.runtime as any).globalSemaphore;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pool = (this.runtime as any).worktreePool;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const agentStore = (this.runtime as any).agentStore;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const usageLimitPauser = (this.runtime as any).usageLimitPauser;
const rawMerge = () =>
@@ -564,9 +572,9 @@ export class ProjectEngine {
await store.updateTask(taskId, { mergeRetries: 0 });
}
}
} catch (err: any) {
} catch (err: unknown) {
this.activeMergeSession = null;
const errorMsg = err?.message ?? String(err);
const errorMsg = err instanceof Error ? err.message : String(err);
runtimeLog.error(`${manualResolver ? "Manual" : "Auto"}-merge failed for ${taskId}: ${errorMsg}`);
// If this was a manual merge, reject the promise and skip auto-retry logic
@@ -585,7 +593,7 @@ export class ProjectEngine {
// Deterministic verification failure: move back to in-progress
const isVerificationError =
err?.name === "VerificationError" ||
err instanceof Error && err.name === "VerificationError" ||
errorMsg.includes("Deterministic test verification failed") ||
errorMsg.includes("Deterministic build verification failed");
@@ -723,6 +731,7 @@ export class ProjectEngine {
runtimeLog.log(`Startup sweep: clearing stale '${t.status}' status on ${t.id}`);
await store.updateTask(t.id, { status: null });
// Update in-memory object so canMergeTask sees the cleared status
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(t as any).status = null;
}
}
@@ -730,6 +739,7 @@ export class ProjectEngine {
const settings = await store.getSettings();
if (!settings.autoMerge) return;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const eligible = tasks.filter((t) => this.canMergeTask(t as any));
if (eligible.length > 0) {
runtimeLog.log(`Auto-merge startup sweep: enqueueing ${eligible.length} task(s)`);
@@ -753,6 +763,7 @@ export class ProjectEngine {
if (!settings.globalPause && !settings.enginePaused && settings.autoMerge) {
const tasks = await store.listTasks({ column: "in-review" });
for (const t of tasks) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (this.canMergeTask(t as any)) {
this.internalEnqueueMerge(t.id);
}
@@ -803,6 +814,7 @@ export class ProjectEngine {
runtimeLog.log("Global unpause — resuming agentic activity");
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const executor = (this.runtime as any).executor;
executor?.resumeOrphaned?.().catch((err: Error) =>
runtimeLog.error("Failed to resume orphaned tasks on unpause:", err),
@@ -815,6 +827,7 @@ export class ProjectEngine {
try {
const tasks = await store.listTasks({ column: "in-review" });
for (const t of tasks) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (this.canMergeTask(t as any)) {
this.internalEnqueueMerge(t.id);
}
@@ -840,6 +853,7 @@ export class ProjectEngine {
runtimeLog.log("Engine unpaused — resuming agentic activity");
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const executor = (this.runtime as any).executor;
executor?.resumeOrphaned?.().catch((err: Error) =>
runtimeLog.error("Failed to resume orphaned tasks on engine unpause:", err),
@@ -852,6 +866,7 @@ export class ProjectEngine {
try {
const tasks = await store.listTasks({ column: "in-review" });
for (const t of tasks) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (this.canMergeTask(t as any)) {
this.internalEnqueueMerge(t.id);
}
@@ -878,6 +893,7 @@ export class ProjectEngine {
`Stuck task timeout changed to ${s.taskStuckTimeoutMs}ms — running immediate check`,
);
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const detector = (this.runtime as any).stuckTaskDetector;
await detector?.checkNow?.();
} catch {
@@ -902,6 +918,7 @@ export class ProjectEngine {
"insightExtractionMinIntervalMs",
] as const;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const changed = insightKeys.some((key) => (s as any)[key] !== (prev as any)[key]);
if (!changed || !this.automationStore) return;

View File

@@ -77,8 +77,8 @@ export async function withRateLimitRetry<T>(
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
const error = err instanceof Error ? err : new Error(String(err));
// Non-rate-limit errors: re-throw immediately — no retry

View File

@@ -12,6 +12,8 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { AgentSemaphore } from "./concurrency.js";
type EventListener = (...args: unknown[]) => void;
// ── Module-level mocks (matching existing test patterns) ──────────────────
vi.mock("./pi.js", () => ({
@@ -28,10 +30,10 @@ vi.mock("./pi.js", () => ({
vi.mock("./reviewer.js", () => ({
reviewStep: vi.fn(),
}));
vi.mock("node:child_process", () => {
const { promisify } = require("node:util");
vi.mock("node:child_process", async () => {
const { promisify } = await import("node:util");
const execSyncFn = vi.fn().mockReturnValue(Buffer.from(""));
const execFn: any = vi.fn((cmd: any, opts: any, cb: any) => {
const execFn: any = vi.fn((cmd: string, opts: unknown, cb: unknown) => {
const callback = typeof opts === "function" ? opts : cb;
const forwardedOpts = typeof opts === "function" ? undefined : opts;
try {
@@ -47,7 +49,7 @@ vi.mock("node:child_process", () => {
// Mirror real child_process.exec: promisify resolves to { stdout, stderr }.
execFn[promisify.custom] = (cmd: any, opts?: any) =>
new Promise((resolve, reject) => {
execFn(cmd, opts, (err: any, stdout: any, stderr: any) => {
execFn(cmd, opts, (err: any, stdout: any, stderr: (...args: unknown[]) => void) => {
if (err) {
err.stdout = stdout;
err.stderr = stderr;
@@ -116,9 +118,9 @@ const DEFAULT_SETTINGS: Settings = {
};
function createMockStore(overrides: Record<string, any> = {}) {
const listeners = new Map<string, Function[]>();
const listeners = new Map<string, EventListener[]>();
return {
on: vi.fn((event: string, fn: Function) => {
on: vi.fn((event: string, fn: EventListener) => {
const existing = listeners.get(event) || [];
existing.push(fn);
listeners.set(event, existing);
@@ -141,7 +143,7 @@ function createMockStore(overrides: Record<string, any> = {}) {
updateStep: vi.fn().mockImplementation(async (id: string, step: number, status: StepStatus) => {
return makeTaskDetail(id, "in-progress");
}),
createTask: vi.fn().mockImplementation(async (input: any) => {
createTask: vi.fn().mockImplementation(async (input: (...args: unknown[]) => void) => {
return makeTask("FN-NEW", "triage");
}),
deleteTask: vi.fn().mockResolvedValue(undefined),
@@ -208,7 +210,7 @@ function mockAgentFailure(error = "agent crashed") {
* multiple tasks execute concurrently.
*/
function createAgentWithTaskDone() {
mockedCreateHaiAgent.mockImplementation((async (opts: any) => {
mockedCreateHaiAgent.mockImplementation((async (opts: (...args: unknown[]) => void) => {
// Capture tools per-session to avoid race conditions with concurrent tasks
const localCustomTools = opts.customTools || [];
const session = {
@@ -230,7 +232,7 @@ function createAgentWithTaskDone() {
beforeEach(() => {
vi.clearAllMocks();
mockedExistsSync.mockReturnValue(true); // Default: worktrees exist (resume scenario)
mockedExecSync.mockImplementation((cmd: any) => {
mockedExecSync.mockImplementation((cmd: (...args: unknown[]) => void) => {
if (String(cmd) === "git worktree list --porcelain") {
return [
"worktree /tmp/test",
@@ -534,7 +536,7 @@ describe("In-review merge handling after restart", () => {
store.moveTask.mockResolvedValue(makeTask("FN-051", "done"));
// Branch exists, merge succeeds, no conflicts
mockedExecSync.mockImplementation((cmd: any) => {
mockedExecSync.mockImplementation((cmd: (...args: unknown[]) => void) => {
const cmdStr = String(cmd);
// Post-squash check: squash staged changes → "1"
if (cmdStr.includes("diff --cached --quiet")) return "1" as any;
@@ -561,7 +563,7 @@ describe("In-review merge handling after restart", () => {
store.getTask.mockResolvedValue(makeTaskDetail(taskId, "in-review"));
store.moveTask.mockResolvedValue(makeTask(taskId, "done"));
mockedExecSync.mockImplementation((cmd: any) => {
mockedExecSync.mockImplementation((cmd: (...args: unknown[]) => void) => {
const cmdStr = String(cmd);
if (cmdStr.includes("diff --cached --quiet")) return "1" as any;
if (cmdStr.includes("diff --cached")) return "0" as any;
@@ -580,7 +582,7 @@ describe("In-review merge handling after restart", () => {
store.getTask.mockResolvedValue(makeTaskDetail("FN-055", "in-review"));
// Branch exists, merge starts, agent creates but prompt fails
mockedExecSync.mockImplementation((cmd: any) => {
mockedExecSync.mockImplementation((cmd: (...args: unknown[]) => void) => {
const cmdStr = String(cmd);
// Make merge fail so all attempts exhaust
if (cmdStr.includes("merge --squash") || cmdStr.includes("merge -X")) {
@@ -627,7 +629,7 @@ describe("In-review merge handling after restart", () => {
store.moveTask.mockResolvedValue(makeTask("FN-056", "done"));
// git rev-parse --verify throws (branch not found)
mockedExecSync.mockImplementation((cmd: any) => {
mockedExecSync.mockImplementation((cmd: (...args: unknown[]) => void) => {
if (typeof cmd === "string" && cmd.includes("git rev-parse --verify")) {
throw new Error("branch not found");
}
@@ -677,7 +679,7 @@ describe("Triage re-pick after restart", () => {
store.getTask.mockResolvedValue(makeTaskDetail("FN-062", "triage"));
// Slow agent to keep task in processing
let resolvePrompt: Function;
let resolvePrompt: () => void;
mockedCreateHaiAgent.mockResolvedValue({
session: {
prompt: vi.fn().mockImplementation(() => new Promise((r) => { resolvePrompt = r; })),
@@ -879,7 +881,7 @@ describe("Crash scenario edge cases", () => {
const store = createMockStore();
store.getTask.mockResolvedValue(makeTaskDetail("FN-091", "in-review"));
mockedExecSync.mockImplementation((cmd: any) => {
mockedExecSync.mockImplementation((cmd: (...args: unknown[]) => void) => {
const cmdStr = String(cmd);
// Make merge fail so all attempts exhaust
if (cmdStr.includes("merge --squash") || cmdStr.includes("merge -X")) {
@@ -928,7 +930,7 @@ describe("Crash scenario edge cases", () => {
store.listTasks.mockResolvedValue([task]);
store.getTask.mockResolvedValue(makeTaskDetail("FN-092", "in-progress"));
let resolvePrompt: Function;
let resolvePrompt: () => void;
mockedCreateHaiAgent.mockResolvedValue({
session: {
prompt: vi.fn().mockImplementation(() => new Promise((r) => { resolvePrompt = r; })),
@@ -996,7 +998,7 @@ function makeDirEntry(name: string) {
}
function mockRegisteredWorktrees(rootDir: string, names: string[]) {
mockedExecSync.mockImplementation((cmd: any) => {
mockedExecSync.mockImplementation((cmd: (...args: unknown[]) => void) => {
if (String(cmd) === "git worktree list --porcelain") {
return [
`worktree ${rootDir}`,

View File

@@ -203,8 +203,8 @@ export class SelfHealingManager {
// Note: if the rate limit is still active, the next agent session will
// hit it again → UsageLimitPauser triggers globalPause → our listener
// catches the transition and schedules the next attempt with escalated backoff.
} catch (err: any) {
log.error(`Auto-unpause failed: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Auto-unpause failed: ${errorMessage}`);
}
}
@@ -242,10 +242,11 @@ export class SelfHealingManager {
});
try {
await this.store.moveTask(taskId, "in-review");
} catch (moveErr: any) {
} catch (moveErr: unknown) {
// moveTask may fail if task was concurrently moved (e.g., dep-abort).
// The task is already marked failed — don't allow requeue.
log.warn(`${taskId} moveTask("in-review") failed (${moveErr.message}) — task already marked failed, not re-queuing`);
const moveErrMessage = moveErr instanceof Error ? moveErr.message : String(moveErr);
log.warn(`${taskId} moveTask("in-review") failed (${moveErrMessage}) — task already marked failed, not re-queuing`);
}
await this.store.logEntry(
taskId,
@@ -262,8 +263,8 @@ export class SelfHealingManager {
`Stuck kill ${newCount}/${maxKills} — re-queuing for retry`,
);
return true;
} catch (err: any) {
log.error(`checkStuckBudget failed for ${taskId}: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`checkStuckBudget failed for ${taskId}: ${errorMessage}`);
// On error, allow re-queue — safer than permanently failing
return true;
}
@@ -357,8 +358,8 @@ export class SelfHealingManager {
const elapsedMs = Date.now() - startMs;
log.log(`Maintenance cycle completed in ${elapsedMs}ms`);
} catch (err: any) {
log.error(`Maintenance cycle failed: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Maintenance cycle failed: ${errorMessage}`);
}
}
@@ -399,8 +400,8 @@ export class SelfHealingManager {
try {
await this.store.archiveTask(task.id);
archived++;
} catch (err: any) {
log.error(`Failed to auto-archive ${task.id}: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Failed to auto-archive ${task.id}: ${errorMessage}`);
}
}
@@ -408,8 +409,8 @@ export class SelfHealingManager {
log.log(`Auto-archived ${archived} stale done task(s)`);
}
return archived;
} catch (err: any) {
log.error(`Auto-archive sweep failed: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Auto-archive sweep failed: ${errorMessage}`);
return 0;
}
}
@@ -457,8 +458,8 @@ export class SelfHealingManager {
log.log(`Recovered ${recovered} completed task(s) → in-review`);
}
return recovered;
} catch (err: any) {
log.error(`Completed task recovery failed: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Completed task recovery failed: ${errorMessage}`);
return 0;
}
}
@@ -497,8 +498,8 @@ export class SelfHealingManager {
);
log.log(`Recovered mergeable review task ${task.id}: merged to done`);
recovered++;
} catch (err: any) {
log.error(`Failed to recover mergeable review task ${task.id}: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Failed to recover mergeable review task ${task.id}: ${errorMessage}`);
}
}
@@ -506,8 +507,8 @@ export class SelfHealingManager {
log.log(`Recovered ${recovered} mergeable review task(s) → done`);
}
return recovered;
} catch (err: any) {
log.error(`Mergeable review recovery failed: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Mergeable review recovery failed: ${errorMessage}`);
return 0;
}
}
@@ -551,8 +552,8 @@ export class SelfHealingManager {
);
log.log(`Recovered merged task ${task.id}: moved to done`);
recovered++;
} catch (err: any) {
log.error(`Failed to recover merged task ${task.id}: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Failed to recover merged task ${task.id}: ${errorMessage}`);
}
}
@@ -560,8 +561,8 @@ export class SelfHealingManager {
log.log(`Recovered ${recovered} merged task(s) → done`);
}
return recovered;
} catch (err: any) {
log.error(`Merged review recovery failed: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Merged review recovery failed: ${errorMessage}`);
return 0;
}
}
@@ -605,8 +606,8 @@ export class SelfHealingManager {
);
log.log(`Recovered misclassified failure ${task.id}: ${task.title || task.description?.slice(0, 60) || "(untitled)"}`);
recovered++;
} catch (err: any) {
log.error(`Failed to recover misclassified failure ${task.id}: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Failed to recover misclassified failure ${task.id}: ${errorMessage}`);
}
}
@@ -614,8 +615,8 @@ export class SelfHealingManager {
log.log(`Recovered ${recovered} misclassified failure(s) → cleared for review`);
}
return recovered;
} catch (err: any) {
log.error(`Misclassified failure recovery failed: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Misclassified failure recovery failed: ${errorMessage}`);
return 0;
}
}
@@ -669,8 +670,8 @@ export class SelfHealingManager {
);
await this.store.moveTask(task.id, "todo");
recovered++;
} catch (err: any) {
log.error(`Failed to recover orphaned executor task ${task.id}: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Failed to recover orphaned executor task ${task.id}: ${errorMessage}`);
}
}
@@ -678,8 +679,8 @@ export class SelfHealingManager {
log.log(`Recovered ${recovered} orphaned executor task(s) → todo`);
}
return recovered;
} catch (err: any) {
log.error(`Orphaned executor recovery failed: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Orphaned executor recovery failed: ${errorMessage}`);
return 0;
}
}
@@ -731,8 +732,8 @@ export class SelfHealingManager {
);
await this.store.moveTask(task.id, "todo");
recovered++;
} catch (err: any) {
log.error(`Failed to recover no-progress no-task_done failure ${task.id}: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Failed to recover no-progress no-task_done failure ${task.id}: ${errorMessage}`);
}
}
@@ -740,8 +741,8 @@ export class SelfHealingManager {
log.log(`Recovered ${recovered} no-progress no-task_done failure(s) → todo`);
}
return recovered;
} catch (err: any) {
log.error(`No-progress no-task_done recovery failed: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`No-progress no-task_done recovery failed: ${errorMessage}`);
return 0;
}
}
@@ -822,8 +823,8 @@ export class SelfHealingManager {
log.log(`Recovered ${recovered} approved triage task(s) out of specifying`);
}
return recovered;
} catch (err: any) {
log.error(`Approved triage recovery failed: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Approved triage recovery failed: ${errorMessage}`);
return 0;
}
}
@@ -869,8 +870,8 @@ export class SelfHealingManager {
"Auto-recovered orphaned specifying task — agent session lost, cleared for re-specification",
);
recovered++;
} catch (err: any) {
log.error(`Failed to recover orphaned specifying task ${task.id}: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Failed to recover orphaned specifying task ${task.id}: ${errorMessage}`);
}
}
@@ -878,8 +879,8 @@ export class SelfHealingManager {
log.log(`Recovered ${recovered} orphaned specifying task(s) — cleared for re-specification`);
}
return recovered;
} catch (err: any) {
log.error(`Orphaned specifying task recovery failed: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Orphaned specifying task recovery failed: ${errorMessage}`);
return 0;
}
}
@@ -892,8 +893,8 @@ export class SelfHealingManager {
timeout: 30_000,
});
log.log("Worktree prune completed");
} catch (err: any) {
log.error(`Worktree prune failed: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Worktree prune failed: ${errorMessage}`);
}
}
@@ -926,8 +927,8 @@ export class SelfHealingManager {
log.log(`Cleaned ${cleaned} orphaned worktree(s)`);
}
return cleaned;
} catch (err: any) {
log.error(`Orphan cleanup failed: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Orphan cleanup failed: ${errorMessage}`);
return 0;
}
}
@@ -979,8 +980,8 @@ export class SelfHealingManager {
log.log(`Cleaned ${cleaned} orphaned branch(es)`);
}
return cleaned;
} catch (err: any) {
log.error(`Orphaned branch cleanup failed: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Orphaned branch cleanup failed: ${errorMessage}`);
return 0;
}
}
@@ -993,8 +994,8 @@ export class SelfHealingManager {
log.log(`WAL checkpoint: ${result.checkpointed}/${result.log} pages checkpointed` +
(result.busy > 0 ? ` (${result.busy} busy)` : ""));
}
} catch (err: any) {
log.error(`WAL checkpoint failed: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`WAL checkpoint failed: ${errorMessage}`);
}
}
@@ -1045,8 +1046,8 @@ export class SelfHealingManager {
if (removed > 0) {
log.warn(`Worktree cap: removed ${removed} idle worktree(s) (was ${dirs.length}, cap ${cap})`);
}
} catch (err: any) {
log.error(`Worktree cap enforcement failed: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Worktree cap enforcement failed: ${errorMessage}`);
}
}
}

View File

@@ -823,10 +823,11 @@ export class TriageProcessor {
} else {
await retryableWork();
}
} catch (err: any) {
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
// Race condition: task was deleted (e.g. as a duplicate) between listTasks()
// and specifyTask(). The file is gone, so just log and skip — no point retrying.
if (err.code === "ENOENT") {
if ((err as Record<string, unknown>).code === "ENOENT") {
triageLog.log(`${task.id} no longer exists — skipping`);
} else if (this.pauseAborted.has(task.id)) {
// Pause (global or engine) — clear specifying status without reporting an error
@@ -845,13 +846,13 @@ export class TriageProcessor {
await this.store.updateTask(task.id, { status: restoreStatus }).catch(() => {});
} else {
// Check if the error is a usage-limit error and trigger global pause
if (this.options.usageLimitPauser && isUsageLimitError(err.message)) {
if (this.options.usageLimitPauser && isUsageLimitError(errorMessage)) {
await this.options.usageLimitPauser.onUsageLimitHit(
"triage",
task.id,
err.message,
errorMessage,
);
} else if (isTransientError(err.message)) {
} else if (isTransientError(errorMessage)) {
// Transient network/infrastructure error — use bounded recovery policy
const decision = computeRecoveryDecision({
recoveryRetryCount: task.recoveryRetryCount,
@@ -862,9 +863,9 @@ export class TriageProcessor {
const attempt = decision.nextState.recoveryRetryCount;
const delay = formatDelay(decision.delayMs);
// Silent transient errors (e.g., "request was aborted") are noisy — skip logging
if (!isSilentTransientError(err.message)) {
triageLog.warn(`${task.id} transient error during triage — retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay}: ${err.message}`);
await this.store.logEntry(task.id, `Transient error during specification (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay}): ${err.message}`).catch(() => {});
if (!isSilentTransientError(errorMessage)) {
triageLog.warn(`${task.id} transient error during triage — retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay}: ${errorMessage}`);
await this.store.logEntry(task.id, `Transient error during specification (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay}): ${errorMessage}`).catch(() => {});
}
const restoreStatus = task.status === "needs-respecify" ? "needs-respecify" : null;
await this.store.updateTask(task.id, {
@@ -876,22 +877,22 @@ export class TriageProcessor {
}
// Recovery budget exhausted — freeze in triage with error for manual intervention
triageLog.error(`${task.id} transient error retries exhausted (${MAX_RECOVERY_RETRIES} attempts): ${err.message}`);
await this.store.logEntry(task.id, `Specification failed after ${MAX_RECOVERY_RETRIES} transient errors: ${err.message}`).catch(() => {});
triageLog.error(`${task.id} transient error retries exhausted (${MAX_RECOVERY_RETRIES} attempts): ${errorMessage}`);
await this.store.logEntry(task.id, `Specification failed after ${MAX_RECOVERY_RETRIES} transient errors: ${errorMessage}`).catch(() => {});
await this.store.updateTask(task.id, {
error: `Specification failed after ${MAX_RECOVERY_RETRIES} transient errors: ${err.message}`,
error: `Specification failed after ${MAX_RECOVERY_RETRIES} transient errors: ${errorMessage}`,
recoveryRetryCount: null,
nextRecoveryAt: null,
}).catch(() => {});
this.options.onSpecifyError?.(task, err);
this.options.onSpecifyError?.(task, err instanceof Error ? err : new Error(errorMessage));
return;
}
// For re-specification, restore needs-respecify status so it can be retried;
// otherwise clear to null so the next poll can re-pick the task up.
const restoreStatus = task.status === "needs-respecify" ? "needs-respecify" : null;
await this.store.updateTask(task.id, { status: restoreStatus }).catch(() => {});
triageLog.error(`${task.id} specification failed:`, err.message);
this.options.onSpecifyError?.(task, err);
triageLog.error(`${task.id} specification failed:`, errorMessage);
this.options.onSpecifyError?.(task, err instanceof Error ? err : new Error(errorMessage));
}
} finally {
this.processing.delete(task.id);
@@ -1035,12 +1036,12 @@ export class TriageProcessor {
],
details: { taskId: newTask.id },
};
} catch (err: any) {
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
return {
content: [
{
type: "text" as const,
text: `ERROR: Failed to create task: ${err.message}`,
text: `ERROR: Failed to create task: ${errorMessage}`,
},
],
details: {},
@@ -1131,6 +1132,7 @@ export class TriageProcessor {
// Re-read task detail to get latest user comments for the reviewer
const currentDetail = await store.getTask(taskId);
const currentUserComments = (currentDetail.comments || []).filter(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(c: any) => c.author === "user",
);
@@ -1199,9 +1201,10 @@ export class TriageProcessor {
triageLog.log(
`${taskId}: RETHINK — branched from checkpoint ${checkpointId}`,
);
} catch (branchErr: any) {
} catch (branchErr: unknown) {
const branchErrMessage = branchErr instanceof Error ? branchErr.message : String(branchErr);
triageLog.error(
`${taskId}: RETHINK session rewind failed: ${branchErr.message}`,
`${taskId}: RETHINK session rewind failed: ${branchErrMessage}`,
);
}
}
@@ -1224,14 +1227,14 @@ export class TriageProcessor {
}
return { content: [{ type: "text" as const, text }], details: {} };
} catch (err: any) {
reviewerLog.error(`${taskId}: spec review failed: ${err.message}`);
await store.logEntry(taskId, `Spec review failed: ${err.message}`);
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
reviewerLog.error(`${taskId}: spec review failed: ${errorMessage}`);
await store.logEntry(taskId, `Spec review failed: ${errorMessage}`);
return {
content: [
{
type: "text" as const,
text: `UNAVAILABLE — reviewer error: ${err.message}`,
text: `UNAVAILABLE — reviewer error: ${errorMessage}`,
},
],
details: {},
@@ -1265,6 +1268,7 @@ export class TriageProcessor {
}
const parsedDeps = await this.store.parseDependenciesFromPrompt(task.id);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const taskUpdates: Record<string, any> = { status: null, error: null };
if (parsedDeps.length > 0) {

View File

@@ -186,8 +186,11 @@ export class WorktreePool {
cwd: worktreePath,
});
return branchName;
} catch (err: any) {
const stderr = err?.stderr?.toString() ?? err?.message ?? "";
} catch (err: unknown) {
const execError = err instanceof Error ? err : new Error(String(err));
const stderr = "stderr" in execError && typeof execError.stderr === "string"
? execError.stderr.toString()
: execError.message;
const match = stderr.match(/already used by worktree at '([^']+)'/);
if (!match) {
throw err;
@@ -211,8 +214,11 @@ export class WorktreePool {
try {
await execAsync(suffixedCmd, { cwd: worktreePath });
return suffixedName;
} catch (suffixErr: any) {
const suffixStderr = suffixErr?.stderr?.toString() ?? "";
} catch (suffixErr: unknown) {
const suffixExecError = suffixErr instanceof Error ? suffixErr : new Error(String(suffixErr));
const suffixStderr = "stderr" in suffixExecError && typeof suffixExecError.stderr === "string"
? suffixExecError.stderr.toString()
: "";
if (!suffixStderr.includes("already used by worktree")) {
throw suffixErr;
}
@@ -333,8 +339,9 @@ export async function cleanupOrphanedWorktrees(rootDir: string, store: TaskStore
}
worktreePoolLog.log(`Cleaned up orphaned worktree: ${worktreePath}`);
cleaned++;
} catch (err: any) {
worktreePoolLog.log(`Failed to remove orphaned worktree ${worktreePath}: ${err.message}`);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
worktreePoolLog.log(`Failed to remove orphaned worktree ${worktreePath}: ${errorMessage}`);
}
}