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:
@@ -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(),
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
@@ -502,7 +502,7 @@ export class PluginRunner {
|
||||
this.hookTimeoutMs,
|
||||
`Hook ${hookName} timed out`,
|
||||
);
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
// Error already logged by invokeHook
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user