refactor: low-regret cleanup across core, engine, dashboard

- core: extract ai-engine-loader.ts to share @fusion/engine dynamic-import
  boilerplate between ai-summarize and memory-compaction (incl. AgentMessage
  type); collapse getInbox/getOutbox, listInsights/countInsights,
  listRuns/countRuns, and three hasProjectDb* variants behind shared helpers.
- core: drop unused pluginLoaderLog export; tighten two `any` casts
  (db.walCheckpoint row, plugin-loader error.code).
- engine: extract resolveRoleFallback helper from buildSessionSkillContext/Sync;
  remove 22 stale `eslint-disable no-explicit-any` directives across
  project-engine, self-healing, triage, worktree-pool.
- dashboard: apply ESLint autofix (let→const, empty `interface extends`→type).

All three packages: typecheck clean, full test suites pass (14,414 tests),
builds clean. Net lint: -31 warnings. No public behavior changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-19 13:33:19 -07:00
parent 2dde0b174a
commit fc30dc48fd
18 changed files with 174 additions and 266 deletions

View File

@@ -0,0 +1,45 @@
/**
* Shared lazy loader for `@fusion/engine`'s `createKbAgent`.
*
* @fusion/engine must be imported dynamically (not statically) so that:
* - core can be consumed in test environments where engine isn't resolvable
* - a missing engine package fails soft instead of breaking module load
*
* Using a variable module specifier also prevents bundlers (Vite) from
* statically analysing and trying to resolve the import at build time.
*/
// Engine exports a function type we intentionally don't pull in here — importing
// the type would reintroduce the static resolution this module is designed to avoid.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type CreateKbAgent = any;
let createKbAgent: CreateKbAgent | undefined;
async function initEngine(): Promise<void> {
try {
const engineModule = "@fusion/engine";
const engine = await import(/* @vite-ignore */ engineModule);
createKbAgent = engine.createKbAgent;
} catch {
createKbAgent = undefined;
}
}
/** Shape of a message in an agent session's state. */
export interface AgentMessage {
role: string;
content?: string | Array<{ type: string; text: string }>;
}
/** Promise that resolves once the initial load attempt has completed. */
const engineReady: Promise<void> = initEngine();
/**
* Returns `createKbAgent` from `@fusion/engine`, or `undefined` if the engine
* could not be loaded (typical in tests or when engine isn't installed).
*/
export async function getKbAgent(): Promise<CreateKbAgent> {
await engineReady;
return createKbAgent;
}

View File

@@ -11,29 +11,7 @@
* - Text length validation (201-2000 characters) * - Text length validation (201-2000 characters)
*/ */
// Dynamic import for @fusion/engine to avoid resolution issues in test environment import { getKbAgent, type AgentMessage } from "./ai-engine-loader.js";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AgentResult = any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let createKbAgent: any;
// Initialize the import (this runs in actual server, mocked in tests)
async function initEngine() {
if (!createKbAgent) {
try {
// Use dynamic import with variable to prevent static analysis
const engineModule = "@fusion/engine";
const engine = await import(/* @vite-ignore */ engineModule);
createKbAgent = engine.createKbAgent;
} catch {
// Allow failure in test environments - agent functionality will be stubbed
createKbAgent = undefined;
}
}
}
// Initialize on module load (will be awaited in actual usage)
const engineReady = initEngine();
// ── Constants ─────────────────────────────────────────────────────────────── // ── Constants ───────────────────────────────────────────────────────────────
@@ -231,9 +209,7 @@ export async function summarizeTitle(
return null; // Too short for summarization return null; // Too short for summarization
} }
// Ensure engine is loaded before using createKbAgent const createKbAgent = await getKbAgent();
await engineReady;
if (!createKbAgent) { if (!createKbAgent) {
if (DEBUG) console.log("[ai-summarize] AI engine not available"); if (DEBUG) console.log("[ai-summarize] AI engine not available");
throw new AiServiceError("AI engine not available"); throw new AiServiceError("AI engine not available");
@@ -280,12 +256,6 @@ export async function summarizeTitle(
if (DEBUG) console.log("[ai-summarize] Prompt sent, extracting response from messages..."); if (DEBUG) console.log("[ai-summarize] Prompt sent, extracting response from messages...");
// Get the response text from the agent's state
interface AgentMessage {
role: string;
content?: string | Array<{ type: string; text: string }>;
}
const messages: AgentMessage[] = agentResult.session.state?.messages ?? []; const messages: AgentMessage[] = agentResult.session.state?.messages ?? [];
const assistantMessages = messages.filter((m: AgentMessage) => m.role === "assistant"); const assistantMessages = messages.filter((m: AgentMessage) => m.role === "assistant");

View File

@@ -1672,7 +1672,9 @@ export class Database {
* Safe to call periodically. Returns checkpoint stats. * Safe to call periodically. Returns checkpoint stats.
*/ */
walCheckpoint(): { busy: number; log: number; checkpointed: number } { walCheckpoint(): { busy: number; log: number; checkpointed: number } {
const row = this.db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get() as any; const row = this.db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get() as
| { busy?: number; log?: number; checkpointed?: number }
| undefined;
return { busy: row?.busy ?? 0, log: row?.log ?? 0, checkpointed: row?.checkpointed ?? 0 }; return { busy: row?.busy ?? 0, log: row?.log ?? 0, checkpointed: row?.checkpointed ?? 0 };
} }

View File

@@ -185,27 +185,7 @@ export class InsightStore extends EventEmitter<InsightStoreEvents> {
* @returns Matching insights, ordered ascending by createdAt then id * @returns Matching insights, ordered ascending by createdAt then id
*/ */
listInsights(options: InsightListOptions = {}): Insight[] { listInsights(options: InsightListOptions = {}): Insight[] {
const conditions: string[] = []; const { whereClause, params } = this.buildInsightFilter(options);
const params: (string | number)[] = [];
if (options.projectId !== undefined) {
conditions.push("projectId = ?");
params.push(options.projectId);
}
if (options.category !== undefined) {
conditions.push("category = ?");
params.push(options.category);
}
if (options.status !== undefined) {
conditions.push("status = ?");
params.push(options.status);
}
if (options.runId !== undefined) {
conditions.push("lastRunId = ?");
params.push(options.runId);
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const limitClause = options.limit !== undefined ? `LIMIT ${options.limit}` : ""; const limitClause = options.limit !== undefined ? `LIMIT ${options.limit}` : "";
const offsetClause = options.offset !== undefined ? `OFFSET ${options.offset}` : ""; const offsetClause = options.offset !== undefined ? `OFFSET ${options.offset}` : "";
@@ -349,8 +329,19 @@ export class InsightStore extends EventEmitter<InsightStoreEvents> {
* Get the count of insights matching the given filter. * Get the count of insights matching the given filter.
*/ */
countInsights(options: Omit<InsightListOptions, "limit" | "offset"> = {}): number { countInsights(options: Omit<InsightListOptions, "limit" | "offset"> = {}): number {
const { whereClause, params } = this.buildInsightFilter(options);
const row = this.db.prepare(`
SELECT COUNT(*) as count FROM project_insights ${whereClause}
`).get(...params) as { count: number } | undefined;
return row?.count ?? 0;
}
private buildInsightFilter(
options: Pick<InsightListOptions, "projectId" | "category" | "status" | "runId">,
): { whereClause: string; params: (string | number)[] } {
const conditions: string[] = []; const conditions: string[] = [];
const params: string[] = []; const params: (string | number)[] = [];
if (options.projectId !== undefined) { if (options.projectId !== undefined) {
conditions.push("projectId = ?"); conditions.push("projectId = ?");
@@ -369,12 +360,35 @@ export class InsightStore extends EventEmitter<InsightStoreEvents> {
params.push(options.runId); params.push(options.runId);
} }
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; return {
const row = this.db.prepare(` whereClause: conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "",
SELECT COUNT(*) as count FROM project_insights ${whereClause} params,
`).get(...params) as { count: number } | undefined; };
}
return row?.count ?? 0; private buildRunFilter(
options: Pick<InsightRunListOptions, "projectId" | "status" | "trigger">,
): { whereClause: string; params: (string | number)[] } {
const conditions: string[] = [];
const params: (string | number)[] = [];
if (options.projectId !== undefined) {
conditions.push("projectId = ?");
params.push(options.projectId);
}
if (options.status !== undefined) {
conditions.push("status = ?");
params.push(options.status);
}
if (options.trigger !== undefined) {
conditions.push("trigger = ?");
params.push(options.trigger);
}
return {
whereClause: conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "",
params,
};
} }
// ── Insight Run CRUD ──────────────────────────────────────────────── // ── Insight Run CRUD ────────────────────────────────────────────────
@@ -456,23 +470,7 @@ export class InsightStore extends EventEmitter<InsightStoreEvents> {
* @returns Matching runs * @returns Matching runs
*/ */
listRuns(options: InsightRunListOptions = {}): InsightRun[] { listRuns(options: InsightRunListOptions = {}): InsightRun[] {
const conditions: string[] = []; const { whereClause, params } = this.buildRunFilter(options);
const params: (string | number)[] = [];
if (options.projectId !== undefined) {
conditions.push("projectId = ?");
params.push(options.projectId);
}
if (options.status !== undefined) {
conditions.push("status = ?");
params.push(options.status);
}
if (options.trigger !== undefined) {
conditions.push("trigger = ?");
params.push(options.trigger);
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const limitClause = options.limit !== undefined ? `LIMIT ${options.limit}` : ""; const limitClause = options.limit !== undefined ? `LIMIT ${options.limit}` : "";
const offsetClause = options.offset !== undefined ? `OFFSET ${options.offset}` : ""; const offsetClause = options.offset !== undefined ? `OFFSET ${options.offset}` : "";
@@ -595,23 +593,7 @@ export class InsightStore extends EventEmitter<InsightStoreEvents> {
* Get the count of runs matching the given filter. * Get the count of runs matching the given filter.
*/ */
countRuns(options: Omit<InsightRunListOptions, "limit" | "offset"> = {}): number { countRuns(options: Omit<InsightRunListOptions, "limit" | "offset"> = {}): number {
const conditions: string[] = []; const { whereClause, params } = this.buildRunFilter(options);
const params: string[] = [];
if (options.projectId !== undefined) {
conditions.push("projectId = ?");
params.push(options.projectId);
}
if (options.status !== undefined) {
conditions.push("status = ?");
params.push(options.status);
}
if (options.trigger !== undefined) {
conditions.push("trigger = ?");
params.push(options.trigger);
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const row = this.db.prepare(` const row = this.db.prepare(`
SELECT COUNT(*) as count FROM project_insight_runs ${whereClause} SELECT COUNT(*) as count FROM project_insight_runs ${whereClause}
`).get(...params) as { count: number } | undefined; `).get(...params) as { count: number } | undefined;

View File

@@ -42,6 +42,3 @@ export function createLogger(prefix: string): Logger {
}, },
}; };
} }
/** Logger for the plugin loader subsystem. */
export const pluginLoaderLog = createLogger("plugin-loader");

View File

@@ -15,30 +15,7 @@
import type { ProjectSettings } from "./types.js"; import type { ProjectSettings } from "./types.js";
import type { ScheduledTaskCreateInput } from "./automation.js"; import type { ScheduledTaskCreateInput } from "./automation.js";
import { getKbAgent, type AgentMessage } from "./ai-engine-loader.js";
// Dynamic import for @fusion/engine to avoid resolution issues in test environment
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AgentResult = any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let createKbAgent: any;
// Initialize the import (this runs in actual server, mocked in tests)
async function initEngine() {
if (!createKbAgent) {
try {
// Use dynamic import with variable to prevent static analysis
const engineModule = "@fusion/engine";
const engine = await import(/* @vite-ignore */ engineModule);
createKbAgent = engine.createKbAgent;
} catch {
// Allow failure in test environments - agent functionality will be stubbed
createKbAgent = undefined;
}
}
}
// Initialize on module load (will be awaited in actual usage)
const engineReady = initEngine();
// ── Constants ─────────────────────────────────────────────────────────────── // ── Constants ───────────────────────────────────────────────────────────────
@@ -102,9 +79,7 @@ export async function compactMemoryWithAi(
provider?: string, provider?: string,
modelId?: string modelId?: string
): Promise<string> { ): Promise<string> {
// Ensure engine is loaded before using createKbAgent const createKbAgent = await getKbAgent();
await engineReady;
if (!createKbAgent) { if (!createKbAgent) {
if (DEBUG) console.log("[memory-compaction] AI engine not available"); if (DEBUG) console.log("[memory-compaction] AI engine not available");
throw new AiServiceError("AI engine not available"); throw new AiServiceError("AI engine not available");
@@ -151,12 +126,6 @@ export async function compactMemoryWithAi(
if (DEBUG) console.log("[memory-compaction] Prompt sent, extracting response from messages..."); if (DEBUG) console.log("[memory-compaction] Prompt sent, extracting response from messages...");
// Get the response text from the agent's state
interface AgentMessage {
role: string;
content?: string | Array<{ type: string; text: string }>;
}
const messages: AgentMessage[] = agentResult.session.state?.messages ?? []; const messages: AgentMessage[] = agentResult.session.state?.messages ?? [];
const assistantMessages = messages.filter((m: AgentMessage) => m.role === "assistant"); const assistantMessages = messages.filter((m: AgentMessage) => m.role === "assistant");

View File

@@ -194,31 +194,7 @@ export class MessageStore extends EventEmitter<MessageStoreEvents> {
ownerType: ParticipantType, ownerType: ParticipantType,
filter?: MessageFilter, filter?: MessageFilter,
): Message[] { ): Message[] {
const whereClauses: string[] = ["toId = ?", "toType = ?"]; return this.queryMessagesByParticipant("to", ownerId, ownerType, filter);
const params: (string | number)[] = [ownerId, ownerType];
if (filter?.type) {
whereClauses.push("type = ?");
params.push(filter.type);
}
if (filter?.read !== undefined) {
whereClauses.push("read = ?");
params.push(filter.read ? 1 : 0);
}
const whereSql = whereClauses.join(" AND ");
const limit = filter?.limit ?? 100;
const offset = filter?.offset ?? 0;
const rows = this.db.prepare(`
SELECT * FROM messages
WHERE ${whereSql}
ORDER BY createdAt DESC, rowid DESC
LIMIT ? OFFSET ?
`).all(...params, limit, offset);
return (rows as any[]).map((row) => this.rowToMessage(row));
} }
/** /**
@@ -233,7 +209,18 @@ export class MessageStore extends EventEmitter<MessageStoreEvents> {
ownerType: ParticipantType, ownerType: ParticipantType,
filter?: MessageFilter, filter?: MessageFilter,
): Message[] { ): Message[] {
const whereClauses: string[] = ["fromId = ?", "fromType = ?"]; return this.queryMessagesByParticipant("from", ownerId, ownerType, filter);
}
private queryMessagesByParticipant(
direction: "to" | "from",
ownerId: string,
ownerType: ParticipantType,
filter?: MessageFilter,
): Message[] {
const idCol = direction === "to" ? "toId" : "fromId";
const typeCol = direction === "to" ? "toType" : "fromType";
const whereClauses: string[] = [`${idCol} = ?`, `${typeCol} = ?`];
const params: (string | number)[] = [ownerId, ownerType]; const params: (string | number)[] = [ownerId, ownerType];
if (filter?.type) { if (filter?.type) {

View File

@@ -17,6 +17,26 @@ import { isAbsolute, join, resolve, basename, dirname } from "node:path";
import type { CentralCore } from "./central-core.js"; import type { CentralCore } from "./central-core.js";
import { CentralCore as CentralCoreClass } from "./central-core.js"; import { CentralCore as CentralCoreClass } from "./central-core.js";
/**
* Check whether `<dir>/<folderName>/<dbName>` exists as a non-empty regular file.
* Used to decide if a directory contains either a legacy (.kb/kb.db) or
* current (.fusion/fusion.db) project database.
*/
function hasProjectDbFile(dir: string, folderName: string, dbName: string): boolean {
const projectDir = join(dir, folderName);
const dbPath = join(projectDir, dbName);
if (!existsSync(projectDir)) return false;
if (!existsSync(dbPath)) return false;
try {
const stat = statSync(dbPath);
return stat.isFile() && stat.size > 0;
} catch {
return false;
}
}
// ── Types ──────────────────────────────────────────────────────────── // ── Types ────────────────────────────────────────────────────────────
/** First-run state detection results */ /** First-run state detection results */
@@ -274,23 +294,8 @@ export class FirstRunDetector {
*/ */
private hasKbProject(dir: string): boolean { private hasKbProject(dir: string): boolean {
// Check for current .fusion/fusion.db or legacy .kb/kb.db // Check for current .fusion/fusion.db or legacy .kb/kb.db
return this.hasProjectDbFile(dir, ".fusion", "fusion.db") || return hasProjectDbFile(dir, ".fusion", "fusion.db") ||
this.hasProjectDbFile(dir, ".kb", "kb.db"); hasProjectDbFile(dir, ".kb", "kb.db");
}
private hasProjectDbFile(dir: string, folderName: string, dbName: string): boolean {
const projectDir = join(dir, folderName);
const dbPath = join(projectDir, dbName);
if (!existsSync(projectDir)) return false;
if (!existsSync(dbPath)) return false;
try {
const stat = statSync(dbPath);
return stat.isFile() && stat.size > 0;
} catch {
return false;
}
} }
private getDefaultGlobalDir(): string { private getDefaultGlobalDir(): string {
@@ -532,21 +537,8 @@ export class MigrationCoordinator {
* Check if a directory is a valid kb project (has .fusion/fusion.db or .kb/kb.db). * Check if a directory is a valid kb project (has .fusion/fusion.db or .kb/kb.db).
*/ */
private isValidKbProject(dir: string): boolean { private isValidKbProject(dir: string): boolean {
return this.hasProjectDbInDir(dir, ".fusion", "fusion.db") || return hasProjectDbFile(dir, ".fusion", "fusion.db") ||
this.hasProjectDbInDir(dir, ".kb", "kb.db"); hasProjectDbFile(dir, ".kb", "kb.db");
}
private hasProjectDbInDir(dir: string, folderName: string, dbName: string): boolean {
const projectDir = join(dir, folderName);
const dbPath = join(projectDir, dbName);
if (!existsSync(projectDir)) return false;
if (!existsSync(dbPath)) return false;
try {
const stat = statSync(dbPath);
return stat.isFile() && stat.size > 0;
} catch {
return false;
}
} }
} }
@@ -674,22 +666,7 @@ export class BackwardCompat {
* Check if a directory contains a current .fusion project or legacy .kb project. * Check if a directory contains a current .fusion project or legacy .kb project.
*/ */
private hasProjectData(dir: string): boolean { private hasProjectData(dir: string): boolean {
return this.hasProjectDb(dir, ".fusion") || this.hasProjectDb(dir, ".kb"); return hasProjectDbFile(dir, ".fusion", "fusion.db") ||
} hasProjectDbFile(dir, ".kb", "kb.db");
private hasProjectDb(dir: string, folderName: ".fusion" | ".kb"): boolean {
const projectDir = join(dir, folderName);
const dbName = folderName === ".fusion" ? "fusion.db" : "kb.db";
const dbPath = join(projectDir, dbName);
if (!existsSync(projectDir)) return false;
if (!existsSync(dbPath)) return false;
try {
const stat = statSync(dbPath);
return stat.isFile() && stat.size > 0;
} catch {
return false;
}
} }
} }

View File

@@ -520,7 +520,7 @@ export class PluginLoader extends EventEmitter<{
await this.loadPlugin(installation.id); await this.loadPlugin(installation.id);
loaded++; loaded++;
} catch (err) { } catch (err) {
if ((err as any).code !== "PLUGIN_DISABLED") { if ((err as { code?: string }).code !== "PLUGIN_DISABLED") {
errors++; errors++;
log.error( log.error(
`Failed to load plugin ${installation.id}:`, `Failed to load plugin ${installation.id}:`,

View File

@@ -89,7 +89,7 @@ function formatModelTag(provider?: string | null, modelId?: string | null): stri
// Gemini models: "gemini-2.5-pro" -> "Gemini 2.5 Pro" // Gemini models: "gemini-2.5-pro" -> "Gemini 2.5 Pro"
if (normalizedModel.includes("gemini")) { if (normalizedModel.includes("gemini")) {
let formatted = modelId const formatted = modelId
.replace(/^gemini[- ]/i, "Gemini ") .replace(/^gemini[- ]/i, "Gemini ")
.replace(/pro[- ](\d+)[- ](\d+)/i, "Pro $1.$2") .replace(/pro[- ](\d+)[- ](\d+)/i, "Pro $1.$2")
.replace(/pro[- ](\d+)/i, "Pro $1") .replace(/pro[- ](\d+)/i, "Pro $1")
@@ -100,7 +100,7 @@ function formatModelTag(provider?: string | null, modelId?: string | null): stri
} }
// Generic fallback: capitalize first letter, replace hyphens with spaces // Generic fallback: capitalize first letter, replace hyphens with spaces
let formatted = modelId const formatted = modelId
.replace(/-/g, " ") .replace(/-/g, " ")
.replace(/^\w/, (c) => c.toUpperCase()) .replace(/^\w/, (c) => c.toUpperCase())
.replace(/\s+/g, " ") .replace(/\s+/g, " ")

View File

@@ -255,8 +255,8 @@ export interface MissionWithHierarchy extends Mission {
export type MissionEventType = CoreMissionEventType; export type MissionEventType = CoreMissionEventType;
/** Mission lifecycle event persisted in the mission event log. */ /** Mission lifecycle event persisted in the mission event log. */
export interface MissionEvent extends CoreMissionEvent {} export type MissionEvent = CoreMissionEvent
/** Computed mission health snapshot returned by observability APIs. */ /** Computed mission health snapshot returned by observability APIs. */
export interface MissionHealth extends CoreMissionHealth {} export type MissionHealth = CoreMissionHealth

View File

@@ -144,7 +144,7 @@ export function saveOnboardingState(
// Determine completed and dismissed flags // Determine completed and dismissed flags
// completed takes precedence over dismissed // completed takes precedence over dismissed
let completed = options.completed ?? DEFAULT_COMPLETED; const completed = options.completed ?? DEFAULT_COMPLETED;
let dismissed = options.dismissed ?? DEFAULT_DISMISSED; let dismissed = options.dismissed ?? DEFAULT_DISMISSED;
// If completed is true, dismissed should be false // If completed is true, dismissed should be false

View File

@@ -299,7 +299,7 @@ export function useInsights(projectId?: string): UseInsightsResult {
}, [sections]); }, [sections]);
// Initial load - intentionally runs once on mount // Initial load - intentionally runs once on mount
// eslint-disable-next-line
useMemo(() => { useMemo(() => {
void refresh(); void refresh();
}, []); }, []);

View File

@@ -466,7 +466,7 @@ export class ProjectEngine {
} }
// Intentional cast to access Task properties needed by merge validation // 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)) { if (!this.canMergeTask(task as any)) {
continue; continue;
} }
@@ -488,7 +488,7 @@ export class ProjectEngine {
} }
// Auto-heal verification buffer failures by resetting retry counter // Auto-heal verification buffer failures by resetting retry counter
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (this.hasAutoHealableVerificationBufferFailure(task as any)) { if (this.hasAutoHealableVerificationBufferFailure(task as any)) {
await store.logEntry( await store.logEntry(
taskId, taskId,
@@ -497,7 +497,7 @@ export class ProjectEngine {
await store.updateTask(taskId, { mergeRetries: 0, error: null, status: null }); await store.updateTask(taskId, { mergeRetries: 0, error: null, status: null });
} else if ( } else if (
(task.mergeRetries ?? 0) >= ProjectEngine.MAX_AUTO_MERGE_RETRIES && (task.mergeRetries ?? 0) >= ProjectEngine.MAX_AUTO_MERGE_RETRIES &&
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.isRetryCooldownElapsed(task as any) this.isRetryCooldownElapsed(task as any)
) { ) {
await store.logEntry( await store.logEntry(
@@ -565,13 +565,13 @@ export class ProjectEngine {
} else { } else {
// Direct merge via AI agent, gated by semaphore // Direct merge via AI agent, gated by semaphore
runtimeLog.log(`${manualResolver ? "Manual" : "Auto"}-merge merging ${taskId}...`); runtimeLog.log(`${manualResolver ? "Manual" : "Auto"}-merge merging ${taskId}...`);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const semaphore = (this.runtime as any).globalSemaphore; const semaphore = (this.runtime as any).globalSemaphore;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pool = (this.runtime as any).worktreePool; const pool = (this.runtime as any).worktreePool;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const agentStore = (this.runtime as any).agentStore; const agentStore = (this.runtime as any).agentStore;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const usageLimitPauser = (this.runtime as any).usageLimitPauser; const usageLimitPauser = (this.runtime as any).usageLimitPauser;
const rawMerge = () => const rawMerge = () =>
@@ -772,7 +772,7 @@ export class ProjectEngine {
runtimeLog.log(`Startup sweep: clearing stale '${t.status}' status on ${t.id}`); runtimeLog.log(`Startup sweep: clearing stale '${t.status}' status on ${t.id}`);
await store.updateTask(t.id, { status: null }); await store.updateTask(t.id, { status: null });
// Update in-memory object so canMergeTask sees the cleared status // Update in-memory object so canMergeTask sees the cleared status
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(t as any).status = null; (t as any).status = null;
} }
} }
@@ -780,7 +780,7 @@ export class ProjectEngine {
const settings = await store.getSettings(); const settings = await store.getSettings();
if (!settings.autoMerge) return; if (!settings.autoMerge) return;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const eligible = tasks.filter((t) => this.canMergeTask(t as any)); const eligible = tasks.filter((t) => this.canMergeTask(t as any));
if (eligible.length > 0) { if (eligible.length > 0) {
runtimeLog.log(`Auto-merge startup sweep: enqueueing ${eligible.length} task(s)`); runtimeLog.log(`Auto-merge startup sweep: enqueueing ${eligible.length} task(s)`);
@@ -806,7 +806,7 @@ export class ProjectEngine {
if (!settings.globalPause && !settings.enginePaused && settings.autoMerge) { if (!settings.globalPause && !settings.enginePaused && settings.autoMerge) {
const tasks = await store.listTasks({ column: "in-review" }); const tasks = await store.listTasks({ column: "in-review" });
for (const t of tasks) { for (const t of tasks) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (this.canMergeTask(t as any)) { if (this.canMergeTask(t as any)) {
this.internalEnqueueMerge(t.id); this.internalEnqueueMerge(t.id);
} }
@@ -864,7 +864,7 @@ export class ProjectEngine {
runtimeLog.log("Global unpause — resuming agentic activity"); runtimeLog.log("Global unpause — resuming agentic activity");
try { try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const executor = (this.runtime as any).executor; const executor = (this.runtime as any).executor;
executor?.resumeOrphaned?.().catch((err: Error) => executor?.resumeOrphaned?.().catch((err: Error) =>
runtimeLog.error("Failed to resume orphaned tasks on unpause:", err), runtimeLog.error("Failed to resume orphaned tasks on unpause:", err),
@@ -879,7 +879,7 @@ export class ProjectEngine {
try { try {
const tasks = await store.listTasks({ column: "in-review" }); const tasks = await store.listTasks({ column: "in-review" });
for (const t of tasks) { for (const t of tasks) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (this.canMergeTask(t as any)) { if (this.canMergeTask(t as any)) {
this.internalEnqueueMerge(t.id); this.internalEnqueueMerge(t.id);
} }
@@ -907,7 +907,7 @@ export class ProjectEngine {
runtimeLog.log("Engine unpaused — resuming agentic activity"); runtimeLog.log("Engine unpaused — resuming agentic activity");
try { try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const executor = (this.runtime as any).executor; const executor = (this.runtime as any).executor;
executor?.resumeOrphaned?.().catch((err: Error) => executor?.resumeOrphaned?.().catch((err: Error) =>
runtimeLog.error("Failed to resume orphaned tasks on engine unpause:", err), runtimeLog.error("Failed to resume orphaned tasks on engine unpause:", err),
@@ -922,7 +922,7 @@ export class ProjectEngine {
try { try {
const tasks = await store.listTasks({ column: "in-review" }); const tasks = await store.listTasks({ column: "in-review" });
for (const t of tasks) { for (const t of tasks) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (this.canMergeTask(t as any)) { if (this.canMergeTask(t as any)) {
this.internalEnqueueMerge(t.id); this.internalEnqueueMerge(t.id);
} }
@@ -951,7 +951,7 @@ export class ProjectEngine {
`Stuck task timeout changed to ${s.taskStuckTimeoutMs}ms — running immediate check`, `Stuck task timeout changed to ${s.taskStuckTimeoutMs}ms — running immediate check`,
); );
try { try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const detector = (this.runtime as any).stuckTaskDetector; const detector = (this.runtime as any).stuckTaskDetector;
await detector?.checkNow?.(); await detector?.checkNow?.();
} catch (err: unknown) { } catch (err: unknown) {
@@ -982,9 +982,9 @@ export class ProjectEngine {
"memoryDreamsSchedule", "memoryDreamsSchedule",
] as const; ] as const;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const changed = insightKeys.some((key) => (s as any)[key] !== (prev as any)[key]); const changed = insightKeys.some((key) => (s as any)[key] !== (prev as any)[key]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dreamsChanged = dreamKeys.some((key) => (s as any)[key] !== (prev as any)[key]); const dreamsChanged = dreamKeys.some((key) => (s as any)[key] !== (prev as any)[key]);
if ((!changed && !dreamsChanged) || !this.automationStore) return; if ((!changed && !dreamsChanged) || !this.automationStore) return;
@@ -1022,7 +1022,7 @@ export class ProjectEngine {
"memoryAutoSummarizeSchedule", "memoryAutoSummarizeSchedule",
] as const; ] as const;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const changed = autoSummarizeKeys.some((key) => (s as any)[key] !== (prev as any)[key]); const changed = autoSummarizeKeys.some((key) => (s as any)[key] !== (prev as any)[key]);
if (!changed || !this.automationStore) return; if (!changed || !this.automationStore) return;

View File

@@ -13,7 +13,6 @@
* by cleaning oldest idle worktrees when count exceeds 2× maxWorktrees. * by cleaning oldest idle worktrees when count exceeds 2× maxWorktrees.
*/ */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { exec } from "node:child_process"; import { exec } from "node:child_process";
import { promisify } from "node:util"; import { promisify } from "node:util";
import { existsSync, readdirSync, statSync } from "node:fs"; import { existsSync, readdirSync, statSync } from "node:fs";

View File

@@ -204,24 +204,27 @@ export async function buildSessionSkillContext(
} }
} }
// Rule 2: Use role fallback skills return resolveRoleFallback(sessionPurpose, projectRootDir);
}
function resolveRoleFallback(
sessionPurpose: SessionPurpose,
projectRootDir: string,
): SessionSkillContextResult {
const roleFallbackSkills = getRoleFallbackSkills(sessionPurpose); const roleFallbackSkills = getRoleFallbackSkills(sessionPurpose);
if (roleFallbackSkills && roleFallbackSkills.length > 0) { if (roleFallbackSkills && roleFallbackSkills.length > 0) {
const skillSelectionContext: SkillSelectionContext = {
projectRootDir,
requestedSkillNames: roleFallbackSkills,
sessionPurpose,
};
return { return {
skillSelectionContext, skillSelectionContext: {
projectRootDir,
requestedSkillNames: roleFallbackSkills,
sessionPurpose,
},
resolvedSkillNames: roleFallbackSkills, resolvedSkillNames: roleFallbackSkills,
skillSource: "role-fallback", skillSource: "role-fallback",
}; };
} }
// Rule 3: No skills available
return { return {
skillSelectionContext: undefined, skillSelectionContext: undefined,
resolvedSkillNames: [], resolvedSkillNames: [],
@@ -263,27 +266,5 @@ export function buildSessionSkillContextSync(
} }
} }
// Rule 2: Use role fallback skills return resolveRoleFallback(sessionPurpose, projectRootDir);
const roleFallbackSkills = getRoleFallbackSkills(sessionPurpose);
if (roleFallbackSkills && roleFallbackSkills.length > 0) {
const skillSelectionContext: SkillSelectionContext = {
projectRootDir,
requestedSkillNames: roleFallbackSkills,
sessionPurpose,
};
return {
skillSelectionContext,
resolvedSkillNames: roleFallbackSkills,
skillSource: "role-fallback",
};
}
// Rule 3: No skills available
return {
skillSelectionContext: undefined,
resolvedSkillNames: [],
skillSource: "none",
};
} }

View File

@@ -1364,7 +1364,7 @@ export class TriageProcessor {
// Re-read task detail to get latest user comments for the reviewer // Re-read task detail to get latest user comments for the reviewer
const currentDetail = await store.getTask(taskId); const currentDetail = await store.getTask(taskId);
const currentUserComments = (currentDetail.comments || []).filter( const currentUserComments = (currentDetail.comments || []).filter(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(c: any) => c.author === "user", (c: any) => c.author === "user",
); );
@@ -1510,7 +1510,7 @@ export class TriageProcessor {
} }
const parsedDeps = await this.store.parseDependenciesFromPrompt(task.id); 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 }; const taskUpdates: Record<string, any> = { status: null, error: null };
if (parsedDeps.length > 0) { if (parsedDeps.length > 0) {

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { exec } from "node:child_process"; import { exec } from "node:child_process";
import { promisify } from "node:util"; import { promisify } from "node:util";
import { existsSync, readdirSync, rmSync } from "node:fs"; import { existsSync, readdirSync, rmSync } from "node:fs";