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:
45
packages/core/src/ai-engine-loader.ts
Normal file
45
packages/core/src/ai-engine-loader.ts
Normal 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;
|
||||
}
|
||||
@@ -11,29 +11,7 @@
|
||||
* - Text length validation (201-2000 characters)
|
||||
*/
|
||||
|
||||
// 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();
|
||||
import { getKbAgent, type AgentMessage } from "./ai-engine-loader.js";
|
||||
|
||||
// ── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -231,9 +209,7 @@ export async function summarizeTitle(
|
||||
return null; // Too short for summarization
|
||||
}
|
||||
|
||||
// Ensure engine is loaded before using createKbAgent
|
||||
await engineReady;
|
||||
|
||||
const createKbAgent = await getKbAgent();
|
||||
if (!createKbAgent) {
|
||||
if (DEBUG) console.log("[ai-summarize] 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...");
|
||||
|
||||
// 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 assistantMessages = messages.filter((m: AgentMessage) => m.role === "assistant");
|
||||
|
||||
|
||||
@@ -1672,7 +1672,9 @@ export class Database {
|
||||
* Safe to call periodically. Returns checkpoint stats.
|
||||
*/
|
||||
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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -185,27 +185,7 @@ export class InsightStore extends EventEmitter<InsightStoreEvents> {
|
||||
* @returns Matching insights, ordered ascending by createdAt then id
|
||||
*/
|
||||
listInsights(options: InsightListOptions = {}): Insight[] {
|
||||
const conditions: string[] = [];
|
||||
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 { whereClause, params } = this.buildInsightFilter(options);
|
||||
const limitClause = options.limit !== undefined ? `LIMIT ${options.limit}` : "";
|
||||
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.
|
||||
*/
|
||||
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 params: string[] = [];
|
||||
const params: (string | number)[] = [];
|
||||
|
||||
if (options.projectId !== undefined) {
|
||||
conditions.push("projectId = ?");
|
||||
@@ -369,12 +360,35 @@ export class InsightStore extends EventEmitter<InsightStoreEvents> {
|
||||
params.push(options.runId);
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
const row = this.db.prepare(`
|
||||
SELECT COUNT(*) as count FROM project_insights ${whereClause}
|
||||
`).get(...params) as { count: number } | undefined;
|
||||
return {
|
||||
whereClause: conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "",
|
||||
params,
|
||||
};
|
||||
}
|
||||
|
||||
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 ────────────────────────────────────────────────
|
||||
@@ -456,23 +470,7 @@ export class InsightStore extends EventEmitter<InsightStoreEvents> {
|
||||
* @returns Matching runs
|
||||
*/
|
||||
listRuns(options: InsightRunListOptions = {}): InsightRun[] {
|
||||
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);
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
const { whereClause, params } = this.buildRunFilter(options);
|
||||
const limitClause = options.limit !== undefined ? `LIMIT ${options.limit}` : "";
|
||||
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.
|
||||
*/
|
||||
countRuns(options: Omit<InsightRunListOptions, "limit" | "offset"> = {}): number {
|
||||
const conditions: string[] = [];
|
||||
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 { whereClause, params } = this.buildRunFilter(options);
|
||||
const row = this.db.prepare(`
|
||||
SELECT COUNT(*) as count FROM project_insight_runs ${whereClause}
|
||||
`).get(...params) as { count: number } | undefined;
|
||||
|
||||
@@ -42,6 +42,3 @@ export function createLogger(prefix: string): Logger {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Logger for the plugin loader subsystem. */
|
||||
export const pluginLoaderLog = createLogger("plugin-loader");
|
||||
|
||||
@@ -15,30 +15,7 @@
|
||||
|
||||
import type { ProjectSettings } from "./types.js";
|
||||
import type { ScheduledTaskCreateInput } from "./automation.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();
|
||||
import { getKbAgent, type AgentMessage } from "./ai-engine-loader.js";
|
||||
|
||||
// ── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -102,9 +79,7 @@ export async function compactMemoryWithAi(
|
||||
provider?: string,
|
||||
modelId?: string
|
||||
): Promise<string> {
|
||||
// Ensure engine is loaded before using createKbAgent
|
||||
await engineReady;
|
||||
|
||||
const createKbAgent = await getKbAgent();
|
||||
if (!createKbAgent) {
|
||||
if (DEBUG) console.log("[memory-compaction] 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...");
|
||||
|
||||
// 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 assistantMessages = messages.filter((m: AgentMessage) => m.role === "assistant");
|
||||
|
||||
|
||||
@@ -194,31 +194,7 @@ export class MessageStore extends EventEmitter<MessageStoreEvents> {
|
||||
ownerType: ParticipantType,
|
||||
filter?: MessageFilter,
|
||||
): Message[] {
|
||||
const whereClauses: string[] = ["toId = ?", "toType = ?"];
|
||||
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));
|
||||
return this.queryMessagesByParticipant("to", ownerId, ownerType, filter);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,7 +209,18 @@ export class MessageStore extends EventEmitter<MessageStoreEvents> {
|
||||
ownerType: ParticipantType,
|
||||
filter?: MessageFilter,
|
||||
): 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];
|
||||
|
||||
if (filter?.type) {
|
||||
|
||||
@@ -17,6 +17,26 @@ import { isAbsolute, join, resolve, basename, dirname } from "node:path";
|
||||
import type { CentralCore } 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 ────────────────────────────────────────────────────────────
|
||||
|
||||
/** First-run state detection results */
|
||||
@@ -274,23 +294,8 @@ export class FirstRunDetector {
|
||||
*/
|
||||
private hasKbProject(dir: string): boolean {
|
||||
// Check for current .fusion/fusion.db or legacy .kb/kb.db
|
||||
return this.hasProjectDbFile(dir, ".fusion", "fusion.db") ||
|
||||
this.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;
|
||||
}
|
||||
return hasProjectDbFile(dir, ".fusion", "fusion.db") ||
|
||||
hasProjectDbFile(dir, ".kb", "kb.db");
|
||||
}
|
||||
|
||||
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).
|
||||
*/
|
||||
private isValidKbProject(dir: string): boolean {
|
||||
return this.hasProjectDbInDir(dir, ".fusion", "fusion.db") ||
|
||||
this.hasProjectDbInDir(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;
|
||||
}
|
||||
return hasProjectDbFile(dir, ".fusion", "fusion.db") ||
|
||||
hasProjectDbFile(dir, ".kb", "kb.db");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -674,22 +666,7 @@ export class BackwardCompat {
|
||||
* Check if a directory contains a current .fusion project or legacy .kb project.
|
||||
*/
|
||||
private hasProjectData(dir: string): boolean {
|
||||
return this.hasProjectDb(dir, ".fusion") || this.hasProjectDb(dir, ".kb");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return hasProjectDbFile(dir, ".fusion", "fusion.db") ||
|
||||
hasProjectDbFile(dir, ".kb", "kb.db");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,7 +520,7 @@ export class PluginLoader extends EventEmitter<{
|
||||
await this.loadPlugin(installation.id);
|
||||
loaded++;
|
||||
} catch (err) {
|
||||
if ((err as any).code !== "PLUGIN_DISABLED") {
|
||||
if ((err as { code?: string }).code !== "PLUGIN_DISABLED") {
|
||||
errors++;
|
||||
log.error(
|
||||
`Failed to load plugin ${installation.id}:`,
|
||||
|
||||
@@ -89,7 +89,7 @@ function formatModelTag(provider?: string | null, modelId?: string | null): stri
|
||||
|
||||
// Gemini models: "gemini-2.5-pro" -> "Gemini 2.5 Pro"
|
||||
if (normalizedModel.includes("gemini")) {
|
||||
let formatted = modelId
|
||||
const formatted = modelId
|
||||
.replace(/^gemini[- ]/i, "Gemini ")
|
||||
.replace(/pro[- ](\d+)[- ](\d+)/i, "Pro $1.$2")
|
||||
.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
|
||||
let formatted = modelId
|
||||
const formatted = modelId
|
||||
.replace(/-/g, " ")
|
||||
.replace(/^\w/, (c) => c.toUpperCase())
|
||||
.replace(/\s+/g, " ")
|
||||
|
||||
@@ -255,8 +255,8 @@ export interface MissionWithHierarchy extends Mission {
|
||||
export type MissionEventType = CoreMissionEventType;
|
||||
|
||||
/** 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. */
|
||||
export interface MissionHealth extends CoreMissionHealth {}
|
||||
export type MissionHealth = CoreMissionHealth
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ export function saveOnboardingState(
|
||||
|
||||
// Determine completed and dismissed flags
|
||||
// completed takes precedence over dismissed
|
||||
let completed = options.completed ?? DEFAULT_COMPLETED;
|
||||
const completed = options.completed ?? DEFAULT_COMPLETED;
|
||||
let dismissed = options.dismissed ?? DEFAULT_DISMISSED;
|
||||
|
||||
// If completed is true, dismissed should be false
|
||||
|
||||
@@ -299,7 +299,7 @@ export function useInsights(projectId?: string): UseInsightsResult {
|
||||
}, [sections]);
|
||||
|
||||
// Initial load - intentionally runs once on mount
|
||||
// eslint-disable-next-line
|
||||
|
||||
useMemo(() => {
|
||||
void refresh();
|
||||
}, []);
|
||||
|
||||
@@ -466,7 +466,7 @@ export class ProjectEngine {
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -488,7 +488,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,
|
||||
@@ -497,7 +497,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(
|
||||
@@ -565,13 +565,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 = () =>
|
||||
@@ -772,7 +772,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;
|
||||
}
|
||||
}
|
||||
@@ -780,7 +780,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)`);
|
||||
@@ -806,7 +806,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);
|
||||
}
|
||||
@@ -864,7 +864,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),
|
||||
@@ -879,7 +879,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);
|
||||
}
|
||||
@@ -907,7 +907,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),
|
||||
@@ -922,7 +922,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);
|
||||
}
|
||||
@@ -951,7 +951,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 (err: unknown) {
|
||||
@@ -982,9 +982,9 @@ export class ProjectEngine {
|
||||
"memoryDreamsSchedule",
|
||||
] as const;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
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]);
|
||||
if ((!changed && !dreamsChanged) || !this.automationStore) return;
|
||||
|
||||
@@ -1022,7 +1022,7 @@ export class ProjectEngine {
|
||||
"memoryAutoSummarizeSchedule",
|
||||
] as const;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
const changed = autoSummarizeKeys.some((key) => (s as any)[key] !== (prev as any)[key]);
|
||||
if (!changed || !this.automationStore) return;
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
* by cleaning oldest idle worktrees when count exceeds 2× maxWorktrees.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { exec } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { existsSync, readdirSync, statSync } from "node:fs";
|
||||
|
||||
@@ -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);
|
||||
|
||||
if (roleFallbackSkills && roleFallbackSkills.length > 0) {
|
||||
const skillSelectionContext: SkillSelectionContext = {
|
||||
projectRootDir,
|
||||
requestedSkillNames: roleFallbackSkills,
|
||||
sessionPurpose,
|
||||
};
|
||||
|
||||
return {
|
||||
skillSelectionContext,
|
||||
skillSelectionContext: {
|
||||
projectRootDir,
|
||||
requestedSkillNames: roleFallbackSkills,
|
||||
sessionPurpose,
|
||||
},
|
||||
resolvedSkillNames: roleFallbackSkills,
|
||||
skillSource: "role-fallback",
|
||||
};
|
||||
}
|
||||
|
||||
// Rule 3: No skills available
|
||||
return {
|
||||
skillSelectionContext: undefined,
|
||||
resolvedSkillNames: [],
|
||||
@@ -263,27 +266,5 @@ export function buildSessionSkillContextSync(
|
||||
}
|
||||
}
|
||||
|
||||
// Rule 2: Use role fallback skills
|
||||
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",
|
||||
};
|
||||
return resolveRoleFallback(sessionPurpose, projectRootDir);
|
||||
}
|
||||
|
||||
@@ -1364,7 +1364,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",
|
||||
);
|
||||
|
||||
@@ -1510,7 +1510,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) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { exec } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { existsSync, readdirSync, rmSync } from "node:fs";
|
||||
|
||||
Reference in New Issue
Block a user