diff --git a/.sf/backups/db/sf.db.2026-05-09T21-41-14-119Z b/.sf/backups/db/sf.db.2026-05-09T21-41-14-119Z deleted file mode 100644 index 3bf7381bb..000000000 Binary files a/.sf/backups/db/sf.db.2026-05-09T21-41-14-119Z and /dev/null differ diff --git a/.sf/backups/db/sf.db.2026-05-11T01-53-05-325Z b/.sf/backups/db/sf.db.2026-05-11T01-53-05-325Z new file mode 100644 index 000000000..f984667e3 Binary files /dev/null and b/.sf/backups/db/sf.db.2026-05-11T01-53-05-325Z differ diff --git a/.sf/metrics.db-shm b/.sf/metrics.db-shm index 3315b42fa..3b59fbfe5 100644 Binary files a/.sf/metrics.db-shm and b/.sf/metrics.db-shm differ diff --git a/.sf/metrics.db-wal b/.sf/metrics.db-wal index 481019a55..857ff205c 100644 Binary files a/.sf/metrics.db-wal and b/.sf/metrics.db-wal differ diff --git a/packages/agent-core/tsconfig.json b/packages/agent-core/tsconfig.json index 6a2c23cf2..24f67372f 100644 --- a/packages/agent-core/tsconfig.json +++ b/packages/agent-core/tsconfig.json @@ -2,9 +2,7 @@ "compilerOptions": { "target": "ES2024", "module": "Node16", - "lib": [ - "ES2024" - ], + "lib": ["ES2024"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, @@ -19,15 +17,11 @@ "resolveJsonModule": true, "allowImportingTsExtensions": false, "useDefineForClassFields": false, - "types": [ - "node" - ], + "types": ["node"], "outDir": "./dist", "rootDir": "./src" }, - "include": [ - "src/**/*.ts" - ], + "include": ["src/**/*.ts"], "exclude": [ "node_modules", "dist", diff --git a/packages/ai/src/providers/google-gemini-cli.ts b/packages/ai/src/providers/google-gemini-cli.ts index f87e73bc1..af9fea4db 100644 --- a/packages/ai/src/providers/google-gemini-cli.ts +++ b/packages/ai/src/providers/google-gemini-cli.ts @@ -12,6 +12,7 @@ import type { GenerateContentParameters, ThinkingConfig, } from "@google/genai"; +import { createGeminiCliContentGenerator } from "@singularity-forge/google-gemini-cli-provider"; import { calculateCost } from "../models.js"; import type { Api, @@ -43,7 +44,6 @@ import { isAutoReasoning, resolveReasoningLevel, } from "./simple-options.js"; -import { createGeminiCliContentGenerator } from "@singularity-forge/google-gemini-cli-provider"; /** * Thinking level for Gemini 3 models. diff --git a/packages/ai/tsconfig.json b/packages/ai/tsconfig.json index e8a3610d0..e22f0f518 100644 --- a/packages/ai/tsconfig.json +++ b/packages/ai/tsconfig.json @@ -2,9 +2,7 @@ "compilerOptions": { "target": "ES2024", "module": "Node16", - "lib": [ - "ES2024" - ], + "lib": ["ES2024"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, @@ -19,19 +17,10 @@ "resolveJsonModule": true, "allowImportingTsExtensions": false, "useDefineForClassFields": false, - "types": [ - "node" - ], + "types": ["node"], "outDir": "./dist", "rootDir": "./src" }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "dist", - "**/*.d.ts", - "src/**/*.d.ts" - ] + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "**/*.d.ts", "src/**/*.d.ts"] } diff --git a/packages/coding-agent/src/core/chat-controller-ordering.test.ts b/packages/coding-agent/src/core/chat-controller-ordering.test.ts index 882fca70c..7b1c7a1bc 100644 --- a/packages/coding-agent/src/core/chat-controller-ordering.test.ts +++ b/packages/coding-agent/src/core/chat-controller-ordering.test.ts @@ -104,14 +104,13 @@ function createHost() { test("chat-controller renders content blocks in content[] index order (tool-first stream)", async () => { // ToolExecutionComponent uses the global theme singleton. // Install a minimal no-op theme implementation for this unit test. - (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; + (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = { + fg: (_key: string, text: string) => text, + bg: (_key: string, text: string) => text, + bold: (text: string) => text, + italic: (text: string) => text, + truncate: (text: string) => text, + }; const host = createHost(); const toolId = "mcp-tool-1"; @@ -192,14 +191,13 @@ test("chat-controller renders content blocks in content[] index order (tool-firs }); test("chat-controller renders serverToolUse before trailing text matching content[] index order", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; + (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = { + fg: (_key: string, text: string) => text, + bg: (_key: string, text: string) => text, + bold: (text: string) => text, + italic: (text: string) => text, + truncate: (text: string) => text, + }; const host = createHost(); const toolId = "mcp-secure-1"; @@ -282,14 +280,13 @@ test("chat-controller renders serverToolUse before trailing text matching conten }); test("chat-controller keeps pre-tool prose visible until post-tool prose arrives, then prunes it", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; + (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = { + fg: (_key: string, text: string) => text, + bg: (_key: string, text: string) => text, + bold: (text: string) => text, + italic: (text: string) => text, + truncate: (text: string) => text, + }; const host = createHost(); host.getMarkdownThemeWithSettings = () => ({}); @@ -400,14 +397,13 @@ test("chat-controller keeps pre-tool prose visible until post-tool prose arrives }); test("chat-controller keeps pre-tool thinking visible for adapter MCP turns without post-tool prose", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; + (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = { + fg: (_key: string, text: string) => text, + bg: (_key: string, text: string) => text, + bold: (text: string) => text, + italic: (text: string) => text, + truncate: (text: string) => text, + }; const host = createHost(); host.getMarkdownThemeWithSettings = () => ({}); @@ -483,14 +479,13 @@ test("chat-controller keeps pre-tool thinking visible for adapter MCP turns with }); test("chat-controller prunes orphaned provisional text after adapter sub-turn shrink when MCP tools appear", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; + (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = { + fg: (_key: string, text: string) => text, + bg: (_key: string, text: string) => text, + bold: (text: string) => text, + italic: (text: string) => text, + truncate: (text: string) => text, + }; const host = createHost(); host.getMarkdownThemeWithSettings = () => ({}); @@ -625,14 +620,13 @@ test("chat-controller prunes orphaned provisional text after adapter sub-turn sh }); test("chat-controller pins latest assistant text above editor when tool calls are present", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; + (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = { + fg: (_key: string, text: string) => text, + bg: (_key: string, text: string) => text, + bold: (text: string) => text, + italic: (text: string) => text, + truncate: (text: string) => text, + }; const host = createHost(); const toolId = "tool-pin-1"; @@ -697,14 +691,13 @@ test("chat-controller pins latest assistant text above editor when tool calls ar }); test("chat-controller clears pinned zone when a new assistant message starts", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; + (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = { + fg: (_key: string, text: string) => text, + bg: (_key: string, text: string) => text, + bold: (text: string) => text, + italic: (text: string) => text, + truncate: (text: string) => text, + }; const host = createHost(); const toolCall = { @@ -764,14 +757,13 @@ test("chat-controller clears pinned zone when a new assistant message starts", a }); test("chat-controller clears pinned zone when the agent turn ends", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; + (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = { + fg: (_key: string, text: string) => text, + bg: (_key: string, text: string) => text, + bold: (text: string) => text, + italic: (text: string) => text, + truncate: (text: string) => text, + }; const host = createHost(); const toolCall = { @@ -826,14 +818,13 @@ test("chat-controller clears pinned zone when the agent turn ends", async () => }); test("chat-controller clears pinned zone when assistant message ends", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; + (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = { + fg: (_key: string, text: string) => text, + bg: (_key: string, text: string) => text, + bold: (text: string) => text, + italic: (text: string) => text, + truncate: (text: string) => text, + }; const host = createHost(); const toolCall = { @@ -887,14 +878,13 @@ test("chat-controller clears pinned zone when assistant message ends", async () }); test("chat-controller does not pin when there are no tool calls", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; + (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = { + fg: (_key: string, text: string) => text, + bg: (_key: string, text: string) => text, + bold: (text: string) => text, + italic: (text: string) => text, + truncate: (text: string) => text, + }; const host = createHost(); @@ -931,14 +921,13 @@ test("chat-controller does not pin when there are no tool calls", async () => { // Expected chatContainer order: textRun(A), toolExec(T1), textRun(B), toolExec(T2), textRun(C) // Each AssistantMessageComponent must render ONLY its own text — no duplication after message_end. test("chat-controller renders interleaved text and tool blocks in content[] index order (#4144)", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; + (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = { + fg: (_key: string, text: string) => text, + bg: (_key: string, text: string) => text, + bold: (text: string) => text, + italic: (text: string) => text, + truncate: (text: string) => text, + }; const host = createHost(); host.getMarkdownThemeWithSettings = () => ({}); @@ -1117,14 +1106,13 @@ test("chat-controller renders interleaved text and tool blocks in content[] inde }); test("chat-controller does not duplicate text when content is [text, tool, text] (interleaved stream)", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; + (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = { + fg: (_key: string, text: string) => text, + bg: (_key: string, text: string) => text, + bold: (text: string) => text, + italic: (text: string) => text, + truncate: (text: string) => text, + }; const host = createHost(); host.getMarkdownThemeWithSettings = () => ({}); @@ -1237,14 +1225,13 @@ test("chat-controller does not duplicate text when content is [text, tool, text] // sub-turn children must stay frozen; new sub-turn segments must append after // them, and the pinned "Latest Output" mirror must re-evaluate for the new sub-turn. test("chat-controller freezes prior sub-turn and appends new segments when content shrinks", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; + (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = { + fg: (_key: string, text: string) => text, + bg: (_key: string, text: string) => text, + bold: (text: string) => text, + italic: (text: string) => text, + truncate: (text: string) => text, + }; const host = createHost(); host.getMarkdownThemeWithSettings = () => ({}); @@ -1426,14 +1413,13 @@ test("chat-controller freezes prior sub-turn and appends new segments when conte // pinned "Latest Output" mirror can display text from the new sub-turn instead // of staying frozen on a stale snapshot (the "bottom green stays" symptom). test("chat-controller updates pinned zone after sub-turn shrink", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; + (globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = { + fg: (_key: string, text: string) => text, + bg: (_key: string, text: string) => text, + bold: (text: string) => text, + italic: (text: string) => text, + truncate: (text: string) => text, + }; const host = createHost(); host.getMarkdownThemeWithSettings = () => ({}); diff --git a/packages/coding-agent/src/core/compaction-orchestrator.ts b/packages/coding-agent/src/core/compaction-orchestrator.ts index 1fddfa277..3ee476b2b 100644 --- a/packages/coding-agent/src/core/compaction-orchestrator.ts +++ b/packages/coding-agent/src/core/compaction-orchestrator.ts @@ -151,7 +151,8 @@ export class CompactionOrchestrator { if (extensionCompaction) { summary = extensionCompaction.summary; firstKeptEntryId = extensionCompaction.firstKeptEntryId; - tokensBefore = extensionCompaction.tokensBefore ?? preparation.tokensBefore; + tokensBefore = + extensionCompaction.tokensBefore ?? preparation.tokensBefore; details = extensionCompaction.details; } else { const result = await compact( @@ -397,7 +398,8 @@ export class CompactionOrchestrator { if (extensionCompaction) { summary = extensionCompaction.summary; firstKeptEntryId = extensionCompaction.firstKeptEntryId; - tokensBefore = extensionCompaction.tokensBefore ?? preparation.tokensBefore; + tokensBefore = + extensionCompaction.tokensBefore ?? preparation.tokensBefore; details = extensionCompaction.details; } else { const compactResult = await compact( diff --git a/packages/coding-agent/src/core/fallback-resolver.ts b/packages/coding-agent/src/core/fallback-resolver.ts index abaa64dbf..33cfa7f37 100644 --- a/packages/coding-agent/src/core/fallback-resolver.ts +++ b/packages/coding-agent/src/core/fallback-resolver.ts @@ -187,7 +187,9 @@ export class FallbackResolver { if (this.emitBeforeModelSelect) { try { const unitType = this._unitContext?.unitType ?? "execute-task"; - const unitId = this._unitContext?.unitId ?? `fallback:${currentModel.provider}/${currentModel.id}`; + const unitId = + this._unitContext?.unitId ?? + `fallback:${currentModel.provider}/${currentModel.id}`; const result = await this.emitBeforeModelSelect({ unitType, unitId, diff --git a/packages/coding-agent/src/core/image-overflow-recovery.ts b/packages/coding-agent/src/core/image-overflow-recovery.ts index ef83c1ce1..6d9f4b5e7 100644 --- a/packages/coding-agent/src/core/image-overflow-recovery.ts +++ b/packages/coding-agent/src/core/image-overflow-recovery.ts @@ -10,11 +10,7 @@ * @see https://github.com/singularity-forge/sf-run/issues/2874 */ -import type { - ImageContent, - Message, - TextContent, -} from "@singularity-forge/ai"; +import type { ImageContent, Message, TextContent } from "@singularity-forge/ai"; /** * Maximum image dimension (px) that the Anthropic API allows in many-image diff --git a/packages/coding-agent/src/core/mcp/auth.ts b/packages/coding-agent/src/core/mcp/auth.ts index 1fc3457b9..9dc3bcece 100644 --- a/packages/coding-agent/src/core/mcp/auth.ts +++ b/packages/coding-agent/src/core/mcp/auth.ts @@ -10,28 +10,28 @@ import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth. import type { OAuthConfig } from "./config.js"; export interface AuthConfig { - headers?: Record; - oauth?: OAuthConfig; + headers?: Record; + oauth?: OAuthConfig; } export interface HttpTransportOptions { - authProvider?: OAuthClientProvider; - requestInit?: { headers: Record }; + authProvider?: OAuthClientProvider; + requestInit?: { headers: Record }; } function resolveEnvValue(value: string): string { - return value.replace( - /\$\{([^}]+)\}/g, - (_match: string, varName: string) => process.env[varName] ?? "", - ); + return value.replace( + /\$\{([^}]+)\}/g, + (_match: string, varName: string) => process.env[varName] ?? "", + ); } function resolveHeaders(raw: Record): Record { - const resolved: Record = {}; - for (const [key, value] of Object.entries(raw)) { - resolved[key] = typeof value === "string" ? resolveEnvValue(value) : value; - } - return resolved; + const resolved: Record = {}; + for (const [key, value] of Object.entries(raw)) { + resolved[key] = typeof value === "string" ? resolveEnvValue(value) : value; + } + return resolved; } /** @@ -42,44 +42,48 @@ function resolveHeaders(raw: Record): Record { * * Consumer: buildHttpTransportOpts when the server config has an oauth block. */ -export function createCliOAuthProvider(config: OAuthConfig): OAuthClientProvider { - let storedTokens: Parameters[0] | undefined; - let storedCodeVerifier = ""; - return { - get redirectUrl() { - return config.redirectUrl ?? "http://localhost:0/callback"; - }, - get clientMetadata() { - return { - redirect_uris: [config.redirectUrl ?? "http://localhost:0/callback"], - client_name: "sf", - ...(config.scopes ? { scope: config.scopes.join(" ") } : {}), - }; - }, - clientInformation() { - return { - client_id: config.clientId, - ...(config.clientSecret ? { client_secret: config.clientSecret } : {}), - }; - }, - tokens() { - return storedTokens; - }, - saveTokens(tokens) { - storedTokens = tokens; - }, - redirectToAuthorization(authorizationUrl) { - console.error( - `[MCP OAuth] Authorization required. Visit:\n ${authorizationUrl.toString()}`, - ); - }, - saveCodeVerifier(codeVerifier) { - storedCodeVerifier = codeVerifier; - }, - codeVerifier() { - return storedCodeVerifier; - }, - }; +export function createCliOAuthProvider( + config: OAuthConfig, +): OAuthClientProvider { + let storedTokens: + | Parameters[0] + | undefined; + let storedCodeVerifier = ""; + return { + get redirectUrl() { + return config.redirectUrl ?? "http://localhost:0/callback"; + }, + get clientMetadata() { + return { + redirect_uris: [config.redirectUrl ?? "http://localhost:0/callback"], + client_name: "sf", + ...(config.scopes ? { scope: config.scopes.join(" ") } : {}), + }; + }, + clientInformation() { + return { + client_id: config.clientId, + ...(config.clientSecret ? { client_secret: config.clientSecret } : {}), + }; + }, + tokens() { + return storedTokens; + }, + saveTokens(tokens) { + storedTokens = tokens; + }, + redirectToAuthorization(authorizationUrl) { + console.error( + `[MCP OAuth] Authorization required. Visit:\n ${authorizationUrl.toString()}`, + ); + }, + saveCodeVerifier(codeVerifier) { + storedCodeVerifier = codeVerifier; + }, + codeVerifier() { + return storedCodeVerifier; + }, + }; } /** @@ -96,16 +100,18 @@ export function createCliOAuthProvider(config: OAuthConfig): OAuthClientProvider * * Consumer: McpConnectionManager.getOrConnect() for HTTP transport setup. */ -export function buildHttpTransportOpts(authConfig: AuthConfig): HttpTransportOptions { - const opts: HttpTransportOptions = {}; - if (authConfig.oauth) { - opts.authProvider = createCliOAuthProvider(authConfig.oauth); - return opts; - } - if (authConfig.headers && Object.keys(authConfig.headers).length > 0) { - opts.requestInit = { - headers: resolveHeaders(authConfig.headers), - }; - } - return opts; +export function buildHttpTransportOpts( + authConfig: AuthConfig, +): HttpTransportOptions { + const opts: HttpTransportOptions = {}; + if (authConfig.oauth) { + opts.authProvider = createCliOAuthProvider(authConfig.oauth); + return opts; + } + if (authConfig.headers && Object.keys(authConfig.headers).length > 0) { + opts.requestInit = { + headers: resolveHeaders(authConfig.headers), + }; + } + return opts; } diff --git a/packages/coding-agent/src/core/mcp/config.ts b/packages/coding-agent/src/core/mcp/config.ts index d0c4fb8f6..543b8a360 100644 --- a/packages/coding-agent/src/core/mcp/config.ts +++ b/packages/coding-agent/src/core/mcp/config.ts @@ -11,22 +11,22 @@ import { homedir } from "node:os"; import { join } from "node:path"; export interface OAuthConfig { - clientId: string; - clientSecret?: string; - scopes?: string[]; - redirectUrl?: string; + clientId: string; + clientSecret?: string; + scopes?: string[]; + redirectUrl?: string; } export interface McpServerConfig { - name: string; - transport: "stdio" | "http" | "unknown"; - command?: string; - args?: string[]; - env?: Record; - cwd?: string; - url?: string; - headers?: Record; - oauth?: OAuthConfig; + name: string; + transport: "stdio" | "http" | "unknown"; + command?: string; + args?: string[]; + env?: Record; + cwd?: string; + url?: string; + headers?: Record; + oauth?: OAuthConfig; } /** @@ -39,51 +39,66 @@ export interface McpServerConfig { * Consumer: McpConnectionManager.readConfigs(), mcp_servers tool. */ export function readMcpConfigs(): McpServerConfig[] { - const servers: McpServerConfig[] = []; - const seen = new Set(); - const sfHome = process.env["SF_HOME"] ?? join(homedir(), ".sf"); - const configPaths = [ - join(process.cwd(), ".mcp.json"), - join(process.cwd(), ".sf", "mcp.json"), - join(sfHome, "mcp.json"), - join(sfHome, "agent", "mcp.json"), - join(homedir(), ".mcp.json"), - ]; - for (const configPath of configPaths) { - try { - if (!existsSync(configPath)) continue; - const raw = readFileSync(configPath, "utf-8"); - const data = JSON.parse(raw) as Record; - const mcpServers = (data["mcpServers"] ?? data["servers"]) as Record | undefined; - if (!mcpServers || typeof mcpServers !== "object") continue; - for (const [name, config] of Object.entries(mcpServers)) { - if (seen.has(name)) continue; - seen.add(name); - const cfg = config as Record; - const hasCommand = typeof cfg["command"] === "string"; - const hasUrl = typeof cfg["url"] === "string"; - const transport: McpServerConfig["transport"] = hasCommand ? "stdio" : hasUrl ? "http" : "unknown"; - const hasHeaders = hasUrl && cfg["headers"] && typeof cfg["headers"] === "object"; - const hasOAuth = hasUrl && cfg["oauth"] && typeof cfg["oauth"] === "object"; - servers.push({ - name, - transport, - ...(hasCommand && { - command: cfg["command"] as string, - args: Array.isArray(cfg["args"]) ? (cfg["args"] as string[]) : undefined, - env: cfg["env"] && typeof cfg["env"] === "object" ? (cfg["env"] as Record) : undefined, - cwd: typeof cfg["cwd"] === "string" ? cfg["cwd"] : undefined, - }), - ...(hasUrl && { url: cfg["url"] as string }), - headers: hasHeaders ? (cfg["headers"] as Record) : undefined, - oauth: hasOAuth ? (cfg["oauth"] as OAuthConfig) : undefined, - }); - } - } catch { - // Non-fatal — config file may not exist or be malformed - } - } - return servers; + const servers: McpServerConfig[] = []; + const seen = new Set(); + const sfHome = process.env["SF_HOME"] ?? join(homedir(), ".sf"); + const configPaths = [ + join(process.cwd(), ".mcp.json"), + join(process.cwd(), ".sf", "mcp.json"), + join(sfHome, "mcp.json"), + join(sfHome, "agent", "mcp.json"), + join(homedir(), ".mcp.json"), + ]; + for (const configPath of configPaths) { + try { + if (!existsSync(configPath)) continue; + const raw = readFileSync(configPath, "utf-8"); + const data = JSON.parse(raw) as Record; + const mcpServers = (data["mcpServers"] ?? data["servers"]) as + | Record + | undefined; + if (!mcpServers || typeof mcpServers !== "object") continue; + for (const [name, config] of Object.entries(mcpServers)) { + if (seen.has(name)) continue; + seen.add(name); + const cfg = config as Record; + const hasCommand = typeof cfg["command"] === "string"; + const hasUrl = typeof cfg["url"] === "string"; + const transport: McpServerConfig["transport"] = hasCommand + ? "stdio" + : hasUrl + ? "http" + : "unknown"; + const hasHeaders = + hasUrl && cfg["headers"] && typeof cfg["headers"] === "object"; + const hasOAuth = + hasUrl && cfg["oauth"] && typeof cfg["oauth"] === "object"; + servers.push({ + name, + transport, + ...(hasCommand && { + command: cfg["command"] as string, + args: Array.isArray(cfg["args"]) + ? (cfg["args"] as string[]) + : undefined, + env: + cfg["env"] && typeof cfg["env"] === "object" + ? (cfg["env"] as Record) + : undefined, + cwd: typeof cfg["cwd"] === "string" ? cfg["cwd"] : undefined, + }), + ...(hasUrl && { url: cfg["url"] as string }), + headers: hasHeaders + ? (cfg["headers"] as Record) + : undefined, + oauth: hasOAuth ? (cfg["oauth"] as OAuthConfig) : undefined, + }); + } + } catch { + // Non-fatal — config file may not exist or be malformed + } + } + return servers; } /** @@ -95,11 +110,11 @@ export function readMcpConfigs(): McpServerConfig[] { * Consumer: McpConnectionManager.getOrConnect(). */ export function getServerConfig( - name: string, - configs: McpServerConfig[], + name: string, + configs: McpServerConfig[], ): McpServerConfig | undefined { - const trimmed = name.trim(); - return configs.find( - (s) => s.name === trimmed || s.name.toLowerCase() === trimmed.toLowerCase(), - ); + const trimmed = name.trim(); + return configs.find( + (s) => s.name === trimmed || s.name.toLowerCase() === trimmed.toLowerCase(), + ); } diff --git a/packages/coding-agent/src/core/mcp/connection-manager.ts b/packages/coding-agent/src/core/mcp/connection-manager.ts index 183fd2e61..b8838e46f 100644 --- a/packages/coding-agent/src/core/mcp/connection-manager.ts +++ b/packages/coding-agent/src/core/mcp/connection-manager.ts @@ -12,63 +12,67 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { buildHttpTransportOpts } from "./auth.js"; -import { getServerConfig, readMcpConfigs, type McpServerConfig } from "./config.js"; +import { + getServerConfig, + type McpServerConfig, + readMcpConfigs, +} from "./config.js"; -export type { McpServerConfig, OAuthConfig } from "./config.js"; export type { AuthConfig, HttpTransportOptions } from "./auth.js"; +export type { McpServerConfig, OAuthConfig } from "./config.js"; export interface McpToolDefinition { - name: string; - description: string; - inputSchema?: unknown; + name: string; + description: string; + inputSchema?: unknown; } export interface ConnectionStatus { - connected: boolean; - tools: string[]; - error?: string; + connected: boolean; + tools: string[]; + error?: string; } export interface RegisterToolParams { - name: string; - label: string; - description: string; - inputSchemaRaw: unknown; - execute: ( - id: string, - params: Record, - signal?: AbortSignal, - ) => Promise<{ - content: { type: "text"; text: string }[]; - details: Record; - }>; + name: string; + label: string; + description: string; + inputSchemaRaw: unknown; + execute: ( + id: string, + params: Record, + signal?: AbortSignal, + ) => Promise<{ + content: { type: "text"; text: string }[]; + details: Record; + }>; } export type RegisterToolFn = (params: RegisterToolParams) => void; const SAFE_CHILD_ENV_KEYS = new Set([ - "PATH", - "HOME", - "USER", - "LOGNAME", - "SHELL", - "LANG", - "LC_ALL", - "LC_CTYPE", - "LC_MESSAGES", - "LC_NUMERIC", - "LC_TIME", - "TMPDIR", - "TMP", - "TEMP", - "TZ", - "TERM", - "COLORTERM", + "PATH", + "HOME", + "USER", + "LOGNAME", + "SHELL", + "LANG", + "LC_ALL", + "LC_CTYPE", + "LC_MESSAGES", + "LC_NUMERIC", + "LC_TIME", + "TMPDIR", + "TMP", + "TEMP", + "TZ", + "TERM", + "COLORTERM", ]); interface ActiveConnection { - client: Client; - transport: Transport; + client: Client; + transport: Transport; } /** @@ -83,245 +87,267 @@ interface ActiveConnection { * Consumer: mcp-client extension (one instance per extension activation). */ export class McpConnectionManager { - private readonly connections = new Map(); - private configCache: McpServerConfig[] | null = null; - private readonly autoRegisteredServers = new Set(); - private readonly toolCache = new Map(); + private readonly connections = new Map(); + private configCache: McpServerConfig[] | null = null; + private readonly autoRegisteredServers = new Set(); + private readonly toolCache = new Map(); - /** Read (and cache) the full ordered list of configured MCP servers. */ - readConfigs(): McpServerConfig[] { - if (this.configCache) return this.configCache; - this.configCache = readMcpConfigs(); - return this.configCache; - } + /** Read (and cache) the full ordered list of configured MCP servers. */ + readConfigs(): McpServerConfig[] { + if (this.configCache) return this.configCache; + this.configCache = readMcpConfigs(); + return this.configCache; + } - /** Invalidate the config cache so the next readConfigs() re-reads from disk. */ - invalidateConfigCache(): void { - this.configCache = null; - } + /** Invalidate the config cache so the next readConfigs() re-reads from disk. */ + invalidateConfigCache(): void { + this.configCache = null; + } - /** Return the config for a single server by name (case-insensitive fallback). */ - getServerConfig(name: string): McpServerConfig | undefined { - return getServerConfig(name, this.readConfigs()); - } + /** Return the config for a single server by name (case-insensitive fallback). */ + getServerConfig(name: string): McpServerConfig | undefined { + return getServerConfig(name, this.readConfigs()); + } - /** - * Return true if the given server name is currently connected. - * - * Purpose: let the mcp_servers tool show live connection status without - * triggering a connection. - * - * Consumer: formatServerList in the extension wrapper. - */ - isConnected(name: string): boolean { - return this.connections.has(name); - } + /** + * Return true if the given server name is currently connected. + * + * Purpose: let the mcp_servers tool show live connection status without + * triggering a connection. + * + * Consumer: formatServerList in the extension wrapper. + */ + isConnected(name: string): boolean { + return this.connections.has(name); + } - /** Return cached tools for a server, or undefined if not yet discovered. */ - getCachedTools(serverName: string): McpToolDefinition[] | undefined { - return this.toolCache.get(serverName); - } + /** Return cached tools for a server, or undefined if not yet discovered. */ + getCachedTools(serverName: string): McpToolDefinition[] | undefined { + return this.toolCache.get(serverName); + } - /** Store discovered tools in the cache. */ - setCachedTools(serverName: string, tools: McpToolDefinition[]): void { - this.toolCache.set(serverName, tools); - } + /** Store discovered tools in the cache. */ + setCachedTools(serverName: string, tools: McpToolDefinition[]): void { + this.toolCache.set(serverName, tools); + } - /** - * Return a live MCP Client for the named server, connecting lazily on first call. - * - * Purpose: provide a single canonical connect path so every tool (discover, - * call, auto-registered) always gets the same cached client. - * - * Consumer: mcp_discover, mcp_call, registerToolsForServer execute functions. - */ - async getOrConnect(name: string, signal?: AbortSignal): Promise { - const config = this.getServerConfig(name); - if (!config) { - throw new Error( - `Unknown MCP server: "${name}". Use mcp_servers to list available servers.`, - ); - } - const existing = this.connections.get(config.name); - if (existing) return existing.client; + /** + * Return a live MCP Client for the named server, connecting lazily on first call. + * + * Purpose: provide a single canonical connect path so every tool (discover, + * call, auto-registered) always gets the same cached client. + * + * Consumer: mcp_discover, mcp_call, registerToolsForServer execute functions. + */ + async getOrConnect(name: string, signal?: AbortSignal): Promise { + const config = this.getServerConfig(name); + if (!config) { + throw new Error( + `Unknown MCP server: "${name}". Use mcp_servers to list available servers.`, + ); + } + const existing = this.connections.get(config.name); + if (existing) return existing.client; - const client = new Client({ name: "sf", version: "1.0.0" }); - let transport: Transport; + const client = new Client({ name: "sf", version: "1.0.0" }); + let transport: Transport; - if (config.transport === "stdio" && config.command) { - transport = new StdioClientTransport({ - command: config.command, - args: config.args, - env: this.buildChildEnv(config.env), - cwd: config.cwd, - stderr: "pipe", - }); - } else if (config.transport === "http" && config.url) { - const resolvedUrl = config.url.replace( - /\$\{([^}]+)\}/g, - (_: string, varName: string) => process.env[varName] ?? "", - ); - const httpOpts = buildHttpTransportOpts({ - headers: config.headers, - oauth: config.oauth, - }); - transport = new StreamableHTTPClientTransport( - new URL(resolvedUrl), - httpOpts, - ); - } else { - throw new Error( - `Server "${config.name}" has unsupported transport: ${config.transport}`, - ); - } + if (config.transport === "stdio" && config.command) { + transport = new StdioClientTransport({ + command: config.command, + args: config.args, + env: this.buildChildEnv(config.env), + cwd: config.cwd, + stderr: "pipe", + }); + } else if (config.transport === "http" && config.url) { + const resolvedUrl = config.url.replace( + /\$\{([^}]+)\}/g, + (_: string, varName: string) => process.env[varName] ?? "", + ); + const httpOpts = buildHttpTransportOpts({ + headers: config.headers, + oauth: config.oauth, + }); + transport = new StreamableHTTPClientTransport( + new URL(resolvedUrl), + httpOpts, + ); + } else { + throw new Error( + `Server "${config.name}" has unsupported transport: ${config.transport}`, + ); + } - try { - await client.connect(transport, { signal, timeout: 30000 }); - } catch (err) { - try { await transport.close(); } catch { /* best-effort */ } - try { await client.close(); } catch { /* best-effort */ } - throw err; - } + try { + await client.connect(transport, { signal, timeout: 30000 }); + } catch (err) { + try { + await transport.close(); + } catch { + /* best-effort */ + } + try { + await client.close(); + } catch { + /* best-effort */ + } + throw err; + } - this.connections.set(config.name, { client, transport }); - return client; - } + this.connections.set(config.name, { client, transport }); + return client; + } - /** - * Close all active connections and clear the tool cache. - * - * Purpose: ensure clean shutdown on session end so no dangling stdio child - * processes or HTTP keep-alive connections survive the session. - * - * Consumer: session_shutdown and session_switch lifecycle hooks. - */ - async closeAll(): Promise { - const closing = Array.from(this.connections.entries()).map( - async ([name, conn]) => { - try { await conn.transport.close(); } catch { /* best-effort */ } - try { await conn.client.close(); } catch { /* best-effort */ } - this.connections.delete(name); - }, - ); - await Promise.allSettled(closing); - this.toolCache.clear(); - this.autoRegisteredServers.clear(); - } + /** + * Close all active connections and clear the tool cache. + * + * Purpose: ensure clean shutdown on session end so no dangling stdio child + * processes or HTTP keep-alive connections survive the session. + * + * Consumer: session_shutdown and session_switch lifecycle hooks. + */ + async closeAll(): Promise { + const closing = Array.from(this.connections.entries()).map( + async ([name, conn]) => { + try { + await conn.transport.close(); + } catch { + /* best-effort */ + } + try { + await conn.client.close(); + } catch { + /* best-effort */ + } + this.connections.delete(name); + }, + ); + await Promise.allSettled(closing); + this.toolCache.clear(); + this.autoRegisteredServers.clear(); + } - /** - * Alias for closeAll — named for the /mcp reload command surface. - * - * Purpose: allow reload command to disconnect all servers so the next - * mcp_discover or mcp_call lazily reconnects with fresh config. - * - * Consumer: /mcp reload command handler. - */ - async disconnectAll(): Promise { - await this.closeAll(); - } + /** + * Alias for closeAll — named for the /mcp reload command surface. + * + * Purpose: allow reload command to disconnect all servers so the next + * mcp_discover or mcp_call lazily reconnects with fresh config. + * + * Consumer: /mcp reload command handler. + */ + async disconnectAll(): Promise { + await this.closeAll(); + } - /** - * Return the live connection status for a named server. - * Safe to call even when the server has never been connected. - * - * Purpose: provide non-destructive status inspection for the status command. - * - * Consumer: /mcp status command handler. - */ - getConnectionStatus(name: string): ConnectionStatus { - const conn = this.connections.get(name); - const cached = this.toolCache.get(name); - return { - connected: !!conn, - tools: cached ? cached.map((t) => t.name) : [], - error: undefined, - }; - } + /** + * Return the live connection status for a named server. + * Safe to call even when the server has never been connected. + * + * Purpose: provide non-destructive status inspection for the status command. + * + * Consumer: /mcp status command handler. + */ + getConnectionStatus(name: string): ConnectionStatus { + const conn = this.connections.get(name); + const cached = this.toolCache.get(name); + return { + connected: !!conn, + tools: cached ? cached.map((t) => t.name) : [], + error: undefined, + }; + } - /** - * Register MCP tools discovered for a server as first-class agent tools. - * No-op if the server's tools were already registered in this session. - * - * Purpose: surface each MCP tool by its real name so the LLM can call - * tools directly (e.g. serena_find_symbol) without the mcp_call indirection. - * - * Consumer: mcp_discover execute handler, after listTools() succeeds. - * - * @param serverName Canonical server name from the config. - * @param tools Tool list returned by client.listTools(). - * @param registerTool Extension-provided callback that registers one tool. - */ - registerToolsForServer( - serverName: string, - tools: McpToolDefinition[], - registerTool: RegisterToolFn, - ): void { - if (this.autoRegisteredServers.has(serverName)) return; - this.autoRegisteredServers.add(serverName); - for (const tool of tools) { - const piToolName = `${serverName}_${tool.name}`; - const description = tool.description || `MCP tool: ${tool.name} on ${serverName}`; - try { - registerTool({ - name: piToolName, - label: `${serverName}:${tool.name}`, - description, - inputSchemaRaw: tool.inputSchema, - execute: async (_id, params, signal) => { - const client = await this.getOrConnect(serverName, signal); - const result = await client.callTool( - { name: tool.name, arguments: params }, - undefined, - { signal, timeout: 60000 }, - ); - const contentItems = result.content as { type: string; text?: string }[]; - const raw = contentItems - .map((c) => (c.type === "text" ? (c.text ?? "") : JSON.stringify(c))) - .join("\n"); - return { - content: [{ type: "text" as const, text: raw }], - details: { server: serverName, tool: tool.name }, - }; - }, - }); - } catch { - // Non-fatal — tool registration can fail if schema is unconvertible - } - } - } + /** + * Register MCP tools discovered for a server as first-class agent tools. + * No-op if the server's tools were already registered in this session. + * + * Purpose: surface each MCP tool by its real name so the LLM can call + * tools directly (e.g. serena_find_symbol) without the mcp_call indirection. + * + * Consumer: mcp_discover execute handler, after listTools() succeeds. + * + * @param serverName Canonical server name from the config. + * @param tools Tool list returned by client.listTools(). + * @param registerTool Extension-provided callback that registers one tool. + */ + registerToolsForServer( + serverName: string, + tools: McpToolDefinition[], + registerTool: RegisterToolFn, + ): void { + if (this.autoRegisteredServers.has(serverName)) return; + this.autoRegisteredServers.add(serverName); + for (const tool of tools) { + const piToolName = `${serverName}_${tool.name}`; + const description = + tool.description || `MCP tool: ${tool.name} on ${serverName}`; + try { + registerTool({ + name: piToolName, + label: `${serverName}:${tool.name}`, + description, + inputSchemaRaw: tool.inputSchema, + execute: async (_id, params, signal) => { + const client = await this.getOrConnect(serverName, signal); + const result = await client.callTool( + { name: tool.name, arguments: params }, + undefined, + { signal, timeout: 60000 }, + ); + const contentItems = result.content as { + type: string; + text?: string; + }[]; + const raw = contentItems + .map((c) => + c.type === "text" ? (c.text ?? "") : JSON.stringify(c), + ) + .join("\n"); + return { + content: [{ type: "text" as const, text: raw }], + details: { server: serverName, tool: tool.name }, + }; + }, + }); + } catch { + // Non-fatal — tool registration can fail if schema is unconvertible + } + } + } - /** - * Build a sanitised child environment for stdio transport processes. - * - * Purpose: prevent leaking host secrets or unwanted env vars into MCP - * server child processes by allow-listing safe keys and merging config- - * provided env on top. - * - * Consumer: getOrConnect() when creating a StdioClientTransport. - */ - buildChildEnv(configEnv?: Record): Record { - const safe: Record = {}; - for (const key of SAFE_CHILD_ENV_KEYS) { - const val = process.env[key]; - if (val !== undefined) safe[key] = val; - } - return { ...safe, ...this.resolveEnv(configEnv ?? {}) }; - } + /** + * Build a sanitised child environment for stdio transport processes. + * + * Purpose: prevent leaking host secrets or unwanted env vars into MCP + * server child processes by allow-listing safe keys and merging config- + * provided env on top. + * + * Consumer: getOrConnect() when creating a StdioClientTransport. + */ + buildChildEnv(configEnv?: Record): Record { + const safe: Record = {}; + for (const key of SAFE_CHILD_ENV_KEYS) { + const val = process.env[key]; + if (val !== undefined) safe[key] = val; + } + return { ...safe, ...this.resolveEnv(configEnv ?? {}) }; + } - private resolveEnv(env: Record): Record { - const resolved: Record = {}; - for (const [key, value] of Object.entries(env)) { - if (typeof value === "string") { - resolved[key] = value.replace( - /\$\{([^}]+)\}/g, - (_match: string, varName: string) => process.env[varName] ?? "", - ); - } else { - resolved[key] = value; - } - } - return resolved; - } + private resolveEnv(env: Record): Record { + const resolved: Record = {}; + for (const [key, value] of Object.entries(env)) { + if (typeof value === "string") { + resolved[key] = value.replace( + /\$\{([^}]+)\}/g, + (_match: string, varName: string) => process.env[varName] ?? "", + ); + } else { + resolved[key] = value; + } + } + return resolved; + } } /** Export SAFE_CHILD_ENV_KEYS for tests that need to verify env filtering. */ diff --git a/packages/coding-agent/src/core/mcp/index.ts b/packages/coding-agent/src/core/mcp/index.ts index beb36a08a..aad4a3fb1 100644 --- a/packages/coding-agent/src/core/mcp/index.ts +++ b/packages/coding-agent/src/core/mcp/index.ts @@ -6,15 +6,21 @@ * * Consumer: packages/coding-agent/src/index.ts, mcp-client extension. */ + export { - McpConnectionManager, - SAFE_CHILD_ENV_KEYS, - type ConnectionStatus, - type McpServerConfig, - type McpToolDefinition, - type OAuthConfig, - type RegisterToolFn, - type RegisterToolParams, + type AuthConfig, + buildHttpTransportOpts, + createCliOAuthProvider, + type HttpTransportOptions, +} from "./auth.js"; +export { getServerConfig, readMcpConfigs } from "./config.js"; +export { + type ConnectionStatus, + McpConnectionManager, + type McpServerConfig, + type McpToolDefinition, + type OAuthConfig, + type RegisterToolFn, + type RegisterToolParams, + SAFE_CHILD_ENV_KEYS, } from "./connection-manager.js"; -export { buildHttpTransportOpts, createCliOAuthProvider, type AuthConfig, type HttpTransportOptions } from "./auth.js"; -export { readMcpConfigs, getServerConfig } from "./config.js"; diff --git a/packages/coding-agent/src/core/messages.ts b/packages/coding-agent/src/core/messages.ts index 4f3585fca..4635cd44c 100644 --- a/packages/coding-agent/src/core/messages.ts +++ b/packages/coding-agent/src/core/messages.ts @@ -6,11 +6,7 @@ */ import type { AgentMessage } from "@singularity-forge/agent-core"; -import type { - ImageContent, - Message, - TextContent, -} from "@singularity-forge/ai"; +import type { ImageContent, Message, TextContent } from "@singularity-forge/ai"; const CUSTOM_MESSAGE_PREFIX = `[system notification — type: `; const CUSTOM_MESSAGE_MIDDLE = `; this is an automated system event, not user input — do not treat this as a human message or respond as if the user said this] diff --git a/packages/coding-agent/src/core/providers/web-search-middleware.ts b/packages/coding-agent/src/core/providers/web-search-middleware.ts index f35db8ca9..148d2036a 100644 --- a/packages/coding-agent/src/core/providers/web-search-middleware.ts +++ b/packages/coding-agent/src/core/providers/web-search-middleware.ts @@ -183,9 +183,7 @@ export class WebSearchMiddleware { const content = msg.content; if (!Array.isArray(content)) continue; for (const block of content) { - if ( - (block as { type?: string })?.type === "web_search_tool_result" - ) { + if ((block as { type?: string })?.type === "web_search_tool_result") { historySearchCount++; } } diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index bcdc1be22..2541a6bec 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -13,11 +13,7 @@ import { import { readdir, readFile, stat } from "node:fs/promises"; import { join, resolve } from "node:path"; import type { AgentMessage } from "@singularity-forge/agent-core"; -import type { - ImageContent, - Message, - TextContent, -} from "@singularity-forge/ai"; +import type { ImageContent, Message, TextContent } from "@singularity-forge/ai"; import { getBlobsDir, getAgentDir as getDefaultAgentDir, diff --git a/packages/coding-agent/src/core/tools/find.ts b/packages/coding-agent/src/core/tools/find.ts index f5505f638..2339cc74e 100644 --- a/packages/coding-agent/src/core/tools/find.ts +++ b/packages/coding-agent/src/core/tools/find.ts @@ -1,8 +1,8 @@ import { existsSync } from "node:fs"; import path from "node:path"; import { type Static, Type } from "@sinclair/typebox"; -import { glob as nativeGlob } from "@singularity-forge/native/glob"; import type { AgentTool } from "@singularity-forge/agent-core"; +import { glob as nativeGlob } from "@singularity-forge/native/glob"; import { FIND_DEFAULT_LIMIT } from "../constants.js"; import { resolveToCwd } from "./path-utils.js"; import { diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 8defcb51b..8a640cdc6 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -169,6 +169,16 @@ export { } from "./core/extensions/index.js"; // Footer data provider (git branch + extension statuses - data not otherwise available to extensions) export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.js"; +// MCP connection manager +export { + buildHttpTransportOpts, + type ConnectionStatus, + McpConnectionManager, + type McpServerConfig, + type McpToolDefinition, + type RegisterToolFn, + type RegisterToolParams, +} from "./core/mcp/index.js"; export { FederatedMemoryProvider } from "./core/memory/federated-memory.js"; export { convertToLlm } from "./core/messages.js"; export type { @@ -204,6 +214,15 @@ export type { ResolvedResource, } from "./core/package-manager.js"; export { DefaultPackageManager } from "./core/package-manager.js"; +// Native provider middleware +export { + CUSTOM_SEARCH_TOOL_NAMES, + MAX_NATIVE_SEARCHES_PER_SESSION, + setPreferBraveResolver, + stripThinkingFromHistory, + WebSearchMiddleware, + webSearchMiddleware, +} from "./core/providers/web-search-middleware.js"; export { getAllowedCommandPrefixes, SAFE_COMMAND_PREFIXES, @@ -215,15 +234,6 @@ export type { ResourceLoader, } from "./core/resource-loader.js"; export { DefaultResourceLoader } from "./core/resource-loader.js"; -// Native provider middleware -export { - CUSTOM_SEARCH_TOOL_NAMES, - MAX_NATIVE_SEARCHES_PER_SESSION, - setPreferBraveResolver, - stripThinkingFromHistory, - WebSearchMiddleware, - webSearchMiddleware, -} from "./core/providers/web-search-middleware.js"; // SDK for programmatic usage export { type CreateAgentSessionOptions, @@ -442,7 +452,6 @@ export { export { attachJsonlLineReader, serializeJsonLine } from "./modes/rpc/jsonl.js"; // Clipboard utilities export { copyToClipboard } from "./utils/clipboard.js"; -export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.js"; // Pure formatting utilities (duration, token counts, sparklines, ANSI, etc.) export { fileLink, @@ -455,17 +464,8 @@ export { stripAnsi, truncateWithEllipsis, } from "./utils/format.js"; +export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.js"; // Cross-platform path display export { toPosixPath } from "./utils/path-display.js"; // Shell utilities export { getShellConfig, sanitizeCommand } from "./utils/shell.js"; -// MCP connection manager -export { - McpConnectionManager, - buildHttpTransportOpts, - type McpServerConfig, - type ConnectionStatus, - type McpToolDefinition, - type RegisterToolFn, - type RegisterToolParams, -} from "./core/mcp/index.js"; diff --git a/packages/coding-agent/src/modes/interactive/components/armin.ts b/packages/coding-agent/src/modes/interactive/components/armin.ts index 85027d69d..5b929fe9c 100644 --- a/packages/coding-agent/src/modes/interactive/components/armin.ts +++ b/packages/coding-agent/src/modes/interactive/components/armin.ts @@ -2,11 +2,7 @@ * Armin says hi! A fun easter egg with animated XBM art. */ -import { - type Component, - type TUI, - visibleWidth, -} from "@singularity-forge/tui"; +import { type Component, type TUI, visibleWidth } from "@singularity-forge/tui"; import { theme } from "../theme/theme.js"; // XBM image: 31x36 pixels, LSB first, 1=background, 0=foreground diff --git a/packages/coding-agent/src/modes/interactive/components/daxnuts.ts b/packages/coding-agent/src/modes/interactive/components/daxnuts.ts index cbabcd822..3a8e1f9fc 100644 --- a/packages/coding-agent/src/modes/interactive/components/daxnuts.ts +++ b/packages/coding-agent/src/modes/interactive/components/daxnuts.ts @@ -4,11 +4,7 @@ * A heartfelt tribute to dax (@thdxr) for providing free Kimi K2.5 access via OpenCode. */ -import { - type Component, - type TUI, - visibleWidth, -} from "@singularity-forge/tui"; +import { type Component, type TUI, visibleWidth } from "@singularity-forge/tui"; import { theme } from "../theme/theme.js"; // 32x32 RGB image of dax, hex encoded (3 bytes per pixel) diff --git a/packages/coding-agent/src/modes/interactive/components/model-selector.ts b/packages/coding-agent/src/modes/interactive/components/model-selector.ts index fcc19eeae..0adc99f30 100644 --- a/packages/coding-agent/src/modes/interactive/components/model-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/model-selector.ts @@ -1,5 +1,4 @@ import { type Model, modelsAreEqual } from "@singularity-forge/ai"; -import { fuzzyFilter } from "@singularity-forge/tui/fuzzy"; import { Container, type Focusable, @@ -9,6 +8,7 @@ import { Text, type TUI, } from "@singularity-forge/tui"; +import { fuzzyFilter } from "@singularity-forge/tui/fuzzy"; import type { ModelRegistry } from "../../../core/model-registry.js"; import type { SettingsManager } from "../../../core/settings-manager.js"; import { theme } from "../theme/theme.js"; diff --git a/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts b/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts index ab84c41a5..27fd4e903 100644 --- a/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts @@ -1,5 +1,4 @@ import type { Model } from "@singularity-forge/ai"; -import { fuzzyFilter } from "@singularity-forge/tui/fuzzy"; import { Container, type Focusable, @@ -10,6 +9,7 @@ import { Spacer, Text, } from "@singularity-forge/tui"; +import { fuzzyFilter } from "@singularity-forge/tui/fuzzy"; import { theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; import { providerDisplayName } from "./model-selector.js"; diff --git a/packages/coding-agent/src/modes/interactive/components/show-images-selector.ts b/packages/coding-agent/src/modes/interactive/components/show-images-selector.ts index ca616a612..83fe6e84e 100644 --- a/packages/coding-agent/src/modes/interactive/components/show-images-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/show-images-selector.ts @@ -1,8 +1,4 @@ -import { - Container, - type SelectItem, - SelectList, -} from "@singularity-forge/tui"; +import { Container, type SelectItem, SelectList } from "@singularity-forge/tui"; import { getSelectListTheme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; diff --git a/packages/coding-agent/src/modes/interactive/components/theme-selector.ts b/packages/coding-agent/src/modes/interactive/components/theme-selector.ts index 63b0bbc48..dea364b92 100644 --- a/packages/coding-agent/src/modes/interactive/components/theme-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/theme-selector.ts @@ -1,8 +1,4 @@ -import { - Container, - type SelectItem, - SelectList, -} from "@singularity-forge/tui"; +import { Container, type SelectItem, SelectList } from "@singularity-forge/tui"; import { getAvailableThemes, getSelectListTheme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; diff --git a/packages/coding-agent/src/modes/interactive/components/thinking-selector.ts b/packages/coding-agent/src/modes/interactive/components/thinking-selector.ts index 245cca33c..ff2334d98 100644 --- a/packages/coding-agent/src/modes/interactive/components/thinking-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/thinking-selector.ts @@ -1,9 +1,5 @@ import type { ThinkingLevel } from "@singularity-forge/agent-core"; -import { - Container, - type SelectItem, - SelectList, -} from "@singularity-forge/tui"; +import { Container, type SelectItem, SelectList } from "@singularity-forge/tui"; import { getSelectListTheme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index f4e8cd4c0..47c4cda7a 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -9,13 +9,13 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; -import { listDescendants } from "@singularity-forge/native"; import type { AgentMessage } from "@singularity-forge/agent-core"; import type { AssistantMessage, ImageContent, Message, } from "@singularity-forge/ai"; +import { listDescendants } from "@singularity-forge/native"; import type { AutocompleteItem, EditorComponent, @@ -26,7 +26,6 @@ import type { OverlayOptions, SlashCommand, } from "@singularity-forge/tui"; -import { fuzzyFilter } from "@singularity-forge/tui/fuzzy"; import { CombinedAutocompleteProvider, type Component, @@ -42,6 +41,7 @@ import { type Terminal as TuiTerminal, visibleWidth, } from "@singularity-forge/tui"; +import { fuzzyFilter } from "@singularity-forge/tui/fuzzy"; import { APP_NAME, getDebugLogPath, diff --git a/packages/coding-agent/src/resources/extensions/memory/index.ts b/packages/coding-agent/src/resources/extensions/memory/index.ts index e1ee45f29..40eae5273 100644 --- a/packages/coding-agent/src/resources/extensions/memory/index.ts +++ b/packages/coding-agent/src/resources/extensions/memory/index.ts @@ -15,10 +15,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { join } from "node:path"; import { completeSimple } from "@singularity-forge/ai"; import type { ExtensionAPI } from "@singularity-forge/coding-agent"; -import { - getAgentDir, - SettingsManager, -} from "@singularity-forge/coding-agent"; +import { getAgentDir, SettingsManager } from "@singularity-forge/coding-agent"; import { getFullMemory, getMemorySummary, runStartup } from "./pipeline.js"; import { MemoryStorage } from "./storage.js"; diff --git a/packages/coding-agent/src/utils/image-resize.ts b/packages/coding-agent/src/utils/image-resize.ts index 00b14a505..7e2a4c112 100644 --- a/packages/coding-agent/src/utils/image-resize.ts +++ b/packages/coding-agent/src/utils/image-resize.ts @@ -1,10 +1,10 @@ +import type { ImageContent } from "@singularity-forge/ai"; import type { NativeImageHandle } from "@singularity-forge/native/image"; import { ImageFormat, parseImage, SamplingFilter, } from "@singularity-forge/native/image"; -import type { ImageContent } from "@singularity-forge/ai"; export interface ImageResizeOptions { maxWidth?: number; // Default: 2000 diff --git a/packages/coding-agent/tsconfig.json b/packages/coding-agent/tsconfig.json index 8b4a00922..2292f8619 100644 --- a/packages/coding-agent/tsconfig.json +++ b/packages/coding-agent/tsconfig.json @@ -2,9 +2,7 @@ "compilerOptions": { "target": "ES2024", "module": "Node16", - "lib": [ - "ES2024" - ], + "lib": ["ES2024"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, @@ -19,18 +17,10 @@ "resolveJsonModule": true, "allowImportingTsExtensions": false, "useDefineForClassFields": false, - "types": [ - "node" - ], + "types": ["node"], "outDir": "./dist", "rootDir": "./src" }, - "include": [ - "src/**/*.ts", - "src/**/*.d.ts" - ], - "exclude": [ - "node_modules", - "dist" - ] + "include": ["src/**/*.ts", "src/**/*.d.ts"], + "exclude": ["node_modules", "dist"] } diff --git a/packages/google-gemini-cli-provider/src/index.ts b/packages/google-gemini-cli-provider/src/index.ts index d71ff3ed7..4630472c6 100644 --- a/packages/google-gemini-cli-provider/src/index.ts +++ b/packages/google-gemini-cli-provider/src/index.ts @@ -7,14 +7,11 @@ * * Consumer: `@singularity-forge/ai` Google Gemini provider. */ +import { AuthType, makeFakeConfig } from "@google/gemini-cli-core"; import { - AuthType, - makeFakeConfig, -} from "@google/gemini-cli-core"; -import { + type ContentGenerator, createContentGenerator, createContentGeneratorConfig, - type ContentGenerator, } from "@google/gemini-cli-core/dist/src/core/contentGenerator.js"; export interface GeminiCliContentGeneratorOptions { diff --git a/packages/google-gemini-cli-provider/tsconfig.json b/packages/google-gemini-cli-provider/tsconfig.json index e8a3610d0..e22f0f518 100644 --- a/packages/google-gemini-cli-provider/tsconfig.json +++ b/packages/google-gemini-cli-provider/tsconfig.json @@ -2,9 +2,7 @@ "compilerOptions": { "target": "ES2024", "module": "Node16", - "lib": [ - "ES2024" - ], + "lib": ["ES2024"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, @@ -19,19 +17,10 @@ "resolveJsonModule": true, "allowImportingTsExtensions": false, "useDefineForClassFields": false, - "types": [ - "node" - ], + "types": ["node"], "outDir": "./dist", "rootDir": "./src" }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "dist", - "**/*.d.ts", - "src/**/*.d.ts" - ] + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "**/*.d.ts", "src/**/*.d.ts"] } diff --git a/packages/pi-agent-core/src/db/gate-registry.ts b/packages/pi-agent-core/src/db/gate-registry.ts index 5cefb8686..71457da7d 100644 --- a/packages/pi-agent-core/src/db/gate-registry.ts +++ b/packages/pi-agent-core/src/db/gate-registry.ts @@ -7,7 +7,17 @@ */ import { SF_PARSE_ERROR, SFError } from "./errors.js"; -export type GateId = "Q3" | "Q4" | "Q5" | "Q6" | "Q7" | "Q8" | "MV01" | "MV02" | "MV03" | "MV04"; +export type GateId = + | "Q3" + | "Q4" + | "Q5" + | "Q6" + | "Q7" + | "Q8" + | "MV01" + | "MV02" + | "MV03" + | "MV04"; export interface GateDefinition { id: GateId; diff --git a/packages/pi-agent-core/src/db/index.ts b/packages/pi-agent-core/src/db/index.ts index 8b2886a0f..5906c3fe8 100644 --- a/packages/pi-agent-core/src/db/index.ts +++ b/packages/pi-agent-core/src/db/index.ts @@ -1,5 +1,5 @@ -export * from "./sf-db.js"; export * from "./errors.js"; export * from "./gate-registry.js"; +export * from "./sf-db.js"; export * from "./task-frontmatter.js"; export * from "./workflow-logger.js"; diff --git a/packages/pi-agent-core/src/db/sf-db.ts b/packages/pi-agent-core/src/db/sf-db.ts index 0a7af817b..9011007fa 100644 --- a/packages/pi-agent-core/src/db/sf-db.ts +++ b/packages/pi-agent-core/src/db/sf-db.ts @@ -5,7 +5,10 @@ export type DbRow = Record; /** Wrapper around node:sqlite prepared statements. */ export interface DbStatement { - run(...params: unknown[]): { changes?: number; lastInsertRowid?: number | bigint }; + run(...params: unknown[]): { + changes?: number; + lastInsertRowid?: number | bigint; + }; get(...params: unknown[]): DbRow | undefined; all(...params: unknown[]): DbRow[]; } @@ -80,7 +83,11 @@ export interface SlicePlanningPayload { proofLevel?: string; integrationClosure?: string; observabilityImpact?: string; - adversarialReview?: { partner?: string; combatant?: string; architect?: string }; + adversarialReview?: { + partner?: string; + combatant?: string; + architect?: string; + }; planningMeeting?: unknown; [key: string]: unknown; } @@ -203,8 +210,8 @@ import { taskFrontmatterFromRecord, withTaskFrontmatter, } from "./task-frontmatter.js"; -import { logError, logWarning } from "./workflow-logger.js"; import { readTraceEvents } from "./uok/trace-writer.js"; +import { logError, logWarning } from "./workflow-logger.js"; let loadAttempted = false; function loadProvider(): void { @@ -220,17 +227,19 @@ function normalizeRow(row: unknown): Record | undefined { return row as Record; } function normalizeRows(rows: unknown[]): Record[] { - return rows.map((r) => normalizeRow(r)).filter((r): r is Record => r != null); + return rows + .map((r) => normalizeRow(r)) + .filter((r): r is Record => r != null); } const DB_QUERY_TIMEOUT_MS = 30_000; const DB_BACKUP_MIN_INTERVAL_MS = 15 * 60 * 1000; const DB_BACKUP_RETENTION = 24; const DB_FULL_VACUUM_MIN_INTERVAL_MS = 6 * 60 * 60 * 1000; -function createAdapter(rawDb: import('node:sqlite').DatabaseSync): DbAdapter { - const db: import('node:sqlite').DatabaseSync = rawDb; +function createAdapter(rawDb: import("node:sqlite").DatabaseSync): DbAdapter { + const db: import("node:sqlite").DatabaseSync = rawDb; const stmtCache = new Map(); - function wrapStmt(raw: import('node:sqlite').StatementSync): DbStatement { + function wrapStmt(raw: import("node:sqlite").StatementSync): DbStatement { return { run(...params: unknown[]) { return raw.run(...params); @@ -279,7 +288,7 @@ export function withQueryTimeout( return operation(); } catch (err) { const errMsg = (err as Error)?.message; -if (errMsg?.includes("timeout") || errMsg?.includes("busy")) { + if (errMsg?.includes("timeout") || errMsg?.includes("busy")) { logWarning( "sf-db", `Query timed out after ${timeoutMs}ms, returning fallback`, @@ -289,7 +298,7 @@ if (errMsg?.includes("timeout") || errMsg?.includes("busy")) { throw err; } } -function openRawDb(path: string): import('node:sqlite').DatabaseSync { +function openRawDb(path: string): import("node:sqlite").DatabaseSync { loadProvider(); return new DatabaseSync(path); } @@ -346,7 +355,10 @@ function readDatabaseMaintenanceState(path: string): Record { return {}; } } -function writeDatabaseMaintenanceState(path: string, state: Record): void { +function writeDatabaseMaintenanceState( + path: string, + state: Record, +): void { try { writeFileSync( databaseMaintenancePath(path), @@ -357,7 +369,10 @@ function writeDatabaseMaintenanceState(path: string, state: Record = {}): boolean { @@ -1644,14 +1665,17 @@ function isEmptyMilestoneSpec(row: Record): boolean { if (!row) return true; return ( (row["vision"] ?? "") === "" && - (parseJsonOrFallback(row["success_criteria"], []) as unknown[]).length === 0 && + (parseJsonOrFallback(row["success_criteria"], []) as unknown[]).length === + 0 && (parseJsonOrFallback(row["key_risks"], []) as unknown[]).length === 0 && - (parseJsonOrFallback(row["proof_strategy"], []) as unknown[]).length === 0 && + (parseJsonOrFallback(row["proof_strategy"], []) as unknown[]).length === + 0 && (row["verification_contract"] ?? "") === "" && (row["verification_integration"] ?? "") === "" && (row["verification_operational"] ?? "") === "" && (row["verification_uat"] ?? "") === "" && - (parseJsonOrFallback(row["definition_of_done"], []) as unknown[]).length === 0 && + (parseJsonOrFallback(row["definition_of_done"], []) as unknown[]).length === + 0 && (row["requirement_coverage"] ?? "") === "" && (row["boundary_map_markdown"] ?? "") === "" && (row["vision_meeting_json"] ?? "") === "" && @@ -3417,7 +3441,7 @@ export function getDatabase(): DbAdapter | null { /** * Open the database at the specified path. Returns true if successful. */ -export function openDatabase(path: string): void { +export function openDatabase(path: string): boolean { _dbOpenAttempted = true; if (currentDb && currentPath !== path) closeDatabase(); if (currentDb && currentPath === path) return true; @@ -3445,7 +3469,10 @@ export function openDatabase(path: string): void { try { adapter.close(); } catch (e) { - logWarning("db", `close after VACUUM failed: ${(e as Error)?.message}`); + logWarning( + "db", + `close after VACUUM failed: ${(e as Error)?.message}`, + ); } throw retryErr; } @@ -3904,10 +3931,16 @@ export function insertMilestone(m: MilestoneInput): void { ":sequence": m.sequence ?? 0, }); if (hasPlanningPayload(m.planning as Record)) { - insertMilestoneSpecIfAbsent(m.id, (m.planning ?? {}) as Record); + insertMilestoneSpecIfAbsent( + m.id, + (m.planning ?? {}) as Record, + ); } } -function insertMilestoneSpecIfAbsent(milestoneId: string, planning: Record = {}): void { +function insertMilestoneSpecIfAbsent( + milestoneId: string, + planning: Record = {}, +): void { if (!hasPlanningPayload(planning)) return; const existing = (currentDb as DbAdapter) .prepare("SELECT * FROM milestone_specs WHERE id = ?") @@ -3968,7 +4001,10 @@ function insertMilestoneSpecIfAbsent(milestoneId: string, planning: Record): void { +export function upsertMilestonePlanning( + milestoneId: string, + planning: Record, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); insertMilestoneSpecIfAbsent(milestoneId, planning); currentDb @@ -4099,7 +4135,11 @@ export function insertSlice(s: SliceInput): void { }); insertSliceSpecIfAbsent(s.milestoneId, s.id, s.planning ?? {}); } -function insertSliceSpecIfAbsent(milestoneId: string, sliceId: string, planning: Record = {}): void { +function insertSliceSpecIfAbsent( + milestoneId: string, + sliceId: string, + planning: Record = {}, +): void { currentDb .prepare(`INSERT OR IGNORE INTO slice_specs ( milestone_id, slice_id, goal, success_criteria, proof_level, @@ -4142,7 +4182,11 @@ export function clearSliceSketch(milestoneId: string, sliceId: string): void { * mark a slice as a sketch (e.g., a re-plan flow that wants to revert to * sketch-then-refine). */ -export function setSliceSketchFlag(milestoneId: string, sliceId: string, isSketch: boolean): void { +export function setSliceSketchFlag( + milestoneId: string, + sliceId: string, + isSketch: boolean, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); currentDb .prepare( @@ -4160,7 +4204,10 @@ export function setSliceSketchFlag(milestoneId: string, sliceId: string, isSketc * resolver so path logic stays in one place. Safe to call repeatedly — only * flips rows that meet the predicate. */ -export function autoHealSketchFlags(milestoneId: string, hasPlanFile: (sliceId: string) => boolean): void { +export function autoHealSketchFlags( + milestoneId: string, + hasPlanFile: (sliceId: string) => boolean, +): void { if (!currentDb) return; const rows = currentDb .prepare( @@ -4168,8 +4215,8 @@ export function autoHealSketchFlags(milestoneId: string, hasPlanFile: (sliceId: ) .all({ ":mid": milestoneId }); for (const row of rows) { - if (hasPlanFile(row['id'] as string)) { - setSliceSketchFlag(milestoneId, row['id'] as string, false); + if (hasPlanFile(row["id"] as string)) { + setSliceSketchFlag(milestoneId, row["id"] as string, false); } } } @@ -4181,7 +4228,10 @@ export function autoHealSketchFlags(milestoneId: string, hasPlanFile: (sliceId: * * Used by `/escalate list` to enumerate cross-slice escalations. */ -export function listEscalationArtifacts(milestoneId: string, includeResolved = false): DbRow[] { +export function listEscalationArtifacts( + milestoneId: string, + includeResolved = false, +): DbRow[] { if (!currentDb) return []; const filter = includeResolved ? "escalation_artifact_path IS NOT NULL" @@ -4193,7 +4243,11 @@ export function listEscalationArtifacts(milestoneId: string, includeResolved = f .all({ ":mid": milestoneId }); return rows.map(rowToTask); } -export function upsertSlicePlanning(milestoneId: string, sliceId: string, planning: Record): void { +export function upsertSlicePlanning( + milestoneId: string, + sliceId: string, + planning: Record, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); insertSliceSpecIfAbsent(milestoneId, sliceId, planning); currentDb @@ -4334,10 +4388,20 @@ function hasTaskSpecIntent(planning: Record = {}): boolean { } return false; } -function insertTaskSpecIfAbsent(milestoneId: string, sliceId: string, taskId: string, planning: Record = {}): void { +function insertTaskSpecIfAbsent( + milestoneId: string, + sliceId: string, + taskId: string, + planning: Record = {}, +): void { if (!hasTaskSpecIntent(planning)) return; - const { normalized: frontmatter, errors } = taskFrontmatterFromRecord(planning); - if (errors?.length) logWarning("sf-db:insertTaskSpec", `frontmatter validation errors for ${milestoneId}/${sliceId}/${taskId}: ${errors.join(", ")}`); + const { normalized: frontmatter, errors } = + taskFrontmatterFromRecord(planning); + if (errors?.length) + logWarning( + "sf-db:insertTaskSpec", + `frontmatter validation errors for ${milestoneId}/${sliceId}/${taskId}: ${errors.join(", ")}`, + ); currentDb .prepare(`INSERT OR IGNORE INTO task_specs ( milestone_id, slice_id, task_id, verify, inputs, expected_output, @@ -4370,7 +4434,11 @@ function insertTaskSpecIfAbsent(milestoneId: string, sliceId: string, taskId: st ":created_at": new Date().toISOString(), }); } -function insertTaskSchedulerIfAbsent(milestoneId: string, sliceId: string, taskId: string): void { +function insertTaskSchedulerIfAbsent( + milestoneId: string, + sliceId: string, + taskId: string, +): void { upsertTaskSchedulerStatus(milestoneId, sliceId, taskId, "queued", { onlyIfAbsent: true, }); @@ -4485,7 +4553,11 @@ export function setTaskEscalationAwaitingReview( /** SF ADR-011 P2: clear both escalation flags (called when an escalation is * resolved or its artifact is removed). Leaves escalation_artifact_path so * the resolution audit trail survives. */ -export function clearTaskEscalationFlags(milestoneId: string, sliceId: string, taskId: string): void { +export function clearTaskEscalationFlags( + milestoneId: string, + sliceId: string, + taskId: string, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); currentDb .prepare(`UPDATE tasks @@ -4504,7 +4576,10 @@ export function clearTaskEscalationFlags(milestoneId: string, sliceId: string, t * escalation_override_applied=0. The artifact's respondedAt is checked by * the caller (claimOverrideForInjection in escalation.ts) — keeping artifact * schema knowledge out of the DB layer. */ -export function findUnappliedEscalationOverride(milestoneId: string, sliceId: string): DbRow | undefined { +export function findUnappliedEscalationOverride( + milestoneId: string, + sliceId: string, +): DbRow | undefined { if (!currentDb) return null; const row = currentDb .prepare(`SELECT id, escalation_artifact_path @@ -4525,7 +4600,11 @@ export function findUnappliedEscalationOverride(milestoneId: string, sliceId: st * Returns true when this caller successfully flipped 0→1 (race winner) or * false when another caller claimed it first (race loser). Use this to * guarantee the override is injected exactly once. */ -export function claimEscalationOverride(milestoneId: string, sliceId: string, taskId: string): void { +export function claimEscalationOverride( + milestoneId: string, + sliceId: string, + taskId: string, +): boolean { if (!currentDb) return; const result = currentDb .prepare(`UPDATE tasks @@ -4555,11 +4634,21 @@ export function setTaskBlockerDiscovered( ":tid": taskId, }); } -export function upsertTaskPlanning(milestoneId: string, sliceId: string, taskId: string, planning: Record): void { +export function upsertTaskPlanning( + milestoneId: string, + sliceId: string, + taskId: string, + planning: Record, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); insertTaskSpecIfAbsent(milestoneId, sliceId, taskId, planning); - const { normalized: frontmatter, errors: fmErrors } = taskFrontmatterFromRecord(planning); - if (fmErrors?.length) logWarning("sf-db:upsertTaskPlanning", `frontmatter validation errors for ${milestoneId}/${sliceId}/${taskId}: ${fmErrors.join(", ")}`); + const { normalized: frontmatter, errors: fmErrors } = + taskFrontmatterFromRecord(planning); + if (fmErrors?.length) + logWarning( + "sf-db:upsertTaskPlanning", + `frontmatter validation errors for ${milestoneId}/${sliceId}/${taskId}: ${fmErrors.join(", ")}`, + ); const hasTaskStatus = planning.taskStatus !== undefined || planning.task_status !== undefined || @@ -4665,7 +4754,10 @@ function rowToSlice(row: Record): Record { is_sketch: row["is_sketch"] ?? 0, }; } -export function getSlice(milestoneId: string, sliceId: string): DbRow | undefined { +export function getSlice( + milestoneId: string, + sliceId: string, +): DbRow | undefined { if (!currentDb) return null; const row = currentDb .prepare("SELECT * FROM slices WHERE milestone_id = :mid AND id = :sid") @@ -4673,7 +4765,12 @@ export function getSlice(milestoneId: string, sliceId: string): DbRow | undefine if (!row) return null; return rowToSlice(row); } -export function updateSliceStatus(milestoneId: string, sliceId: string, status: string, completedAt: string | null): void { +export function updateSliceStatus( + milestoneId: string, + sliceId: string, + status: string, + completedAt: string | null, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); currentDb .prepare(`UPDATE slices SET status = :status, completed_at = :completed_at @@ -4689,7 +4786,11 @@ export function updateSliceStatus(milestoneId: string, sliceId: string, status: * Store the UAT verdict for a slice. Called when an ASSESSMENT or UAT_RESULT * file is written so the DB is the canonical source for verdict checks. */ -export function setSliceUatVerdict(milestoneId: string, sliceId: string, verdict: string): void { +export function setSliceUatVerdict( + milestoneId: string, + sliceId: string, + verdict: string, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); currentDb .prepare( @@ -4700,7 +4801,10 @@ export function setSliceUatVerdict(milestoneId: string, sliceId: string, verdict /** * Returns the stored UAT verdict for a slice, or null if not yet recorded. */ -export function getSliceUatVerdict(milestoneId: string, sliceId: string): string | null { +export function getSliceUatVerdict( + milestoneId: string, + sliceId: string, +): string | null { if (!currentDb) return null; const row = currentDb .prepare( @@ -4774,7 +4878,12 @@ export function backfillUatVerdicts(basePath: string): void { } } } -export function setTaskSummaryMd(milestoneId: string, sliceId: string, taskId: string, md: string): void { +export function setTaskSummaryMd( + milestoneId: string, + sliceId: string, + taskId: string, + md: string, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); currentDb .prepare( @@ -4782,7 +4891,12 @@ export function setTaskSummaryMd(milestoneId: string, sliceId: string, taskId: s ) .run({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId, ":md": md }); } -export function setSliceSummaryMd(milestoneId: string, sliceId: string, summaryMd: string, uatMd: string): void { +export function setSliceSummaryMd( + milestoneId: string, + sliceId: string, + summaryMd: string, + uatMd: string, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); currentDb .prepare( @@ -4888,7 +5002,11 @@ function rowToTask(row) { escalation_artifact_path: row["escalation_artifact_path"] ?? null, }); } -export function getTask(milestoneId: string, sliceId: string, taskId: string): DbRow | undefined { +export function getTask( + milestoneId: string, + sliceId: string, + taskId: string, +): DbRow | undefined { if (!currentDb) return null; const row = currentDb .prepare( @@ -4936,7 +5054,11 @@ export function insertVerificationEvidence(e: VerificationEvidenceInput): void { ":created_at": new Date().toISOString(), }); } -export function getVerificationEvidence(milestoneId: string, sliceId: string, taskId: string): DbRow[] { +export function getVerificationEvidence( + milestoneId: string, + sliceId: string, + taskId: string, +): DbRow[] { if (!currentDb) return []; const rows = currentDb .prepare( @@ -5041,7 +5163,10 @@ export function listSelfFeedbackEntries(): DbRow[] { .all(); return rows.map(rowToSelfFeedback); } -export function resolveSelfFeedbackEntry(entryId: string, resolution: Record): void { +export function resolveSelfFeedbackEntry( + entryId: string, + resolution: Record, +): boolean { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); const existing = currentDb .prepare("SELECT * FROM self_feedback WHERE id = :id") @@ -5226,7 +5351,11 @@ export function addBacklogItem({ }); return itemId; } -export function updateBacklogItemStatus(id: string, status: string, note = ""): void { +export function updateBacklogItemStatus( + id: string, + status: string, + note = "", +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); const now = new Date().toISOString(); const result = currentDb @@ -5242,21 +5371,24 @@ export function updateBacklogItemStatus(id: string, status: string, note = ""): ":note": note, ":updated_at": now, }); - return (result?.changes ?? 0) > 0; + void ((result?.changes ?? 0) > 0); } export function removeBacklogItem(id: string): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); - const result = currentDb + currentDb .prepare("DELETE FROM backlog_items WHERE id = :id") .run({ ":id": id }); - return (result?.changes ?? 0) > 0; } /** * Update a milestone's status in the database. * Used by park/unpark to keep the DB in sync with the filesystem marker. * See: https://github.com/singularity-forge/sf-run/issues/2694 */ -export function updateMilestoneStatus(milestoneId: string, status: string, completedAt: string | null): void { +export function updateMilestoneStatus( + milestoneId: string, + status: string, + completedAt: string | null, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); currentDb .prepare( @@ -5319,7 +5451,10 @@ export function getActiveSliceFromDb(milestoneId: string): DbRow | undefined { if (!row) return null; return rowToSlice(row); } -export function getActiveTaskFromDb(milestoneId: string, sliceId: string): DbRow | undefined { +export function getActiveTaskFromDb( + milestoneId: string, + sliceId: string, +): DbRow | undefined { if (!currentDb) return null; const row = currentDb .prepare( @@ -5369,7 +5504,10 @@ export function getSliceStatusSummary(milestoneId: string): DbRow[] { .map((r) => ({ id: r["id"], status: r["status"] })); } /** Fast task status check — avoids deserializing JSON arrays and large text fields. */ -export function getActiveTaskIdFromDb(milestoneId: string, sliceId: string): string | null { +export function getActiveTaskIdFromDb( + milestoneId: string, + sliceId: string, +): string | null { if (!currentDb) return null; const row = currentDb .prepare( @@ -5384,7 +5522,10 @@ export function getActiveTaskIdFromDb(milestoneId: string, sliceId: string): str }; } /** Count tasks by status for a slice — useful for progress reporting without full row load. */ -export function getSliceTaskCounts(milestoneId: string, sliceId: string): DbRow | undefined { +export function getSliceTaskCounts( + milestoneId: string, + sliceId: string, +): DbRow | undefined { if (!currentDb) return { total: 0, done: 0, pending: 0 }; const row = currentDb .prepare(`SELECT @@ -5402,7 +5543,11 @@ export function getSliceTaskCounts(milestoneId: string, sliceId: string): DbRow } // ─── Slice Dependencies (junction table) ───────────────────────────────── /** Sync the slice_dependencies junction table from a slice's JSON depends array. */ -export function syncSliceDependencies(milestoneId: string, sliceId: string, depends: string[]): void { +export function syncSliceDependencies( + milestoneId: string, + sliceId: string, + depends: string[], +): void { if (!currentDb) return; currentDb .prepare( @@ -5418,7 +5563,10 @@ export function syncSliceDependencies(milestoneId: string, sliceId: string, depe } } /** Get all slices that depend on a given slice. */ -export function getDependentSlices(milestoneId: string, sliceId: string): DbRow[] { +export function getDependentSlices( + milestoneId: string, + sliceId: string, +): DbRow[] { if (!currentDb) return []; return currentDb .prepare( @@ -5428,7 +5576,7 @@ export function getDependentSlices(milestoneId: string, sliceId: string): DbRow[ .map((r) => r["slice_id"]); } // ─── Worktree DB Helpers ────────────────────────────────────────────────── -export function copyWorktreeDb(srcDbPath: string, destDbPath: string): void { +export function copyWorktreeDb(srcDbPath: string, destDbPath: string): boolean { try { if (!existsSync(srcDbPath)) return false; const destDir = dirname(destDbPath); @@ -6099,7 +6247,9 @@ export function recordUokRunExit(entry) { ":status": entry.status ?? "ok", ":started_at": entry.startedAt ?? now, ":ended_at": now, - ":error": entry.error ? capErrorForStorage(entry.error, entry.runId) : null, + ":error": entry.error + ? capErrorForStorage(entry.error, entry.runId) + : null, ":flags_json": JSON.stringify(entry.flags ?? {}), ":updated_at": now, }); @@ -6420,7 +6570,10 @@ export function getRecentLlmTaskOutcomes(hours = 24, limit = 100): DbRow[] { * * Consumer: uok/cost-guard-gate.js, uok/outcome-learning-gate.js. */ -export function getLlmTaskOutcomeStats(modelId: string, windowHours = 24): DbRow[] { +export function getLlmTaskOutcomeStats( + modelId: string, + windowHours = 24, +): DbRow[] { if (!currentDb) { return { total: 0, @@ -6483,13 +6636,18 @@ export function getLlmTaskOutcomeStats(modelId: string, windowHours = 24): DbRow * * Consumer: uok/diagnostic-synthesis.js, uok/gate-runner.js health checks. */ -export function getGateRunStats(gateId: string, windowHours = 24): DbRow | undefined { +export function getGateRunStats( + gateId: string, + windowHours = 24, +): DbRow | undefined { try { - const basePath = currentPath && currentPath !== ":memory:" - ? dirname(dirname(currentPath)) - : process.cwd(); - const events = readTraceEvents(basePath, "gate_run", windowHours) - .filter((e) => e.gateId === gateId); + const basePath = + currentPath && currentPath !== ":memory:" + ? dirname(dirname(currentPath)) + : process.cwd(); + const events = readTraceEvents(basePath, "gate_run", windowHours).filter( + (e) => e.gateId === gateId, + ); const stats = { total: events.length, pass: 0, @@ -6587,7 +6745,10 @@ export function getGateCircuitBreaker(gateId: string): DbRow | undefined { * * Consumer: uok/gate-runner.js after executing a gate. */ -export function updateGateCircuitBreaker(gateId: string, updates: Record): void { +export function updateGateCircuitBreaker( + gateId: string, + updates: Record, +): void { if (!currentDb) return; currentDb .prepare( @@ -6613,22 +6774,34 @@ export function updateGateCircuitBreaker(gateId: string, updates: Record e.gateId === gateId && typeof e.durationMs === "number") .map((e) => e.durationMs) .sort((a, b) => a - b); - if (durations.length === 0) return { p50: null, p95: null, count: 0, total: 0, avgMs: 0, p50Ms: 0, p95Ms: 0, maxMs: 0 }; + if (durations.length === 0) + return { + p50: null, + p95: null, + count: 0, + total: 0, + avgMs: 0, + p50Ms: 0, + p95Ms: 0, + maxMs: 0, + }; const p50Ms = durations[Math.floor(durations.length * 0.5)] ?? 0; const p95Ms = durations[Math.floor(durations.length * 0.95)] ?? 0; const maxMs = durations[durations.length - 1] ?? 0; - const avgMs = Math.round(durations.reduce((s, v) => s + v, 0) / durations.length); + const avgMs = Math.round( + durations.reduce((s, v) => s + v, 0) / durations.length, + ); return { p50: p50Ms, p95: p95Ms, @@ -6640,14 +6813,24 @@ export function getGateLatencyStats(gateId: string, windowHours = 24): DbRow[] { maxMs, }; } catch { - return { p50: null, p95: null, count: 0, total: 0, avgMs: 0, p50Ms: 0, p95Ms: 0, maxMs: 0 }; + return { + p50: null, + p95: null, + count: 0, + total: 0, + avgMs: 0, + p50Ms: 0, + p95Ms: 0, + maxMs: 0, + }; } } export function getDistinctGateIds(): string[] { try { - const basePath = currentPath && currentPath !== ":memory:" - ? dirname(dirname(currentPath)) - : process.cwd(); + const basePath = + currentPath && currentPath !== ":memory:" + ? dirname(dirname(currentPath)) + : process.cwd(); const events = readTraceEvents(basePath, "gate_run", 24 * 30); // 30 days return [...new Set(events.map((e) => e.gateId).filter(Boolean))]; } catch { @@ -6705,7 +6888,11 @@ export function getUokMessagesForAgent( return []; } } -export function getUokConversation(agentA: string, agentB: string, limit = 1000): DbRow[] { +export function getUokConversation( + agentA: string, + agentB: string, + limit = 1000, +): DbRow[] { if (!currentDb) return []; try { const rows = currentDb @@ -6742,9 +6929,8 @@ export function markUokMessageRead(messageId: string, agentId: string): void { ":agent_id": agentId, ":read_at": new Date().toISOString(), }); - return true; } catch { - return false; + // best-effort } } export function getUokMessageUnreadCount(agentId: string): number { @@ -6765,7 +6951,10 @@ export function getUokMessageUnreadCount(agentId: string): number { return 0; } } -export function compactUokMessages(retentionDays: number): void { +export function compactUokMessages(retentionDays: number): { + before: number; + after: number; +} { if (!currentDb) return { before: 0, after: 0 }; try { const cutoff = new Date( @@ -6847,7 +7036,9 @@ export function getUokMessageBusMetrics(): DbRow | undefined { function normalizeScheduleScope(scope: unknown): string { return scope === "global" ? "global" : "project"; } -function scheduleEntryFromRow(row: Record): Record { +function scheduleEntryFromRow( + row: Record, +): Record { if (!row) return null; const full = parseJsonObject(row.full_json, {}); return { @@ -6872,7 +7063,11 @@ function scheduleEntryFromRow(row: Record): Record, importedFrom: string | null = null): void { +export function insertScheduleEntry( + scope: string, + entry: Record, + importedFrom: string | null = null, +): void { if (!currentDb) return; const normalizedScope = normalizeScheduleScope(scope); const schemaVersion = entry.schemaVersion ?? 1; @@ -7092,14 +7287,19 @@ export function getRepoFileObservations(): DbRow[] { function intBool(value: unknown): boolean { return value ? 1 : 0; } -function parseJsonObject(raw: unknown, fallback: Record = {}): Record { +function parseJsonObject( + raw: unknown, + fallback: Record = {}, +): Record { try { return JSON.parse(raw); } catch { return fallback; } } -function solverEvalRunFromRow(row: Record): Record { +function solverEvalRunFromRow( + row: Record, +): Record { return { runId: row["run_id"], suiteSource: row["suite_source"], @@ -7112,7 +7312,9 @@ function solverEvalRunFromRow(row: Record): Record): Record { +function solverEvalCaseFromRow( + row: Record, +): Record { return { runId: row["run_id"], caseId: row["case_id"], @@ -7131,7 +7333,9 @@ function solverEvalCaseFromRow(row: Record): Record): Record { +function headlessRunFromRow( + row: Record, +): Record { return { runId: row["run_id"], command: row["command"], @@ -7442,7 +7646,9 @@ export function unarchiveSession(sessionId: string): void { * Consumer: session-recorder.js on before_agent_start (user_message) and * agent_end (assistant_response patch). */ -export function insertSessionTurn(entry: Record): void { +export function insertSessionTurn( + entry: Record, +): bigint | null { if (!currentDb) return null; const result = currentDb .prepare(`INSERT INTO turns @@ -7469,7 +7675,11 @@ export function insertSessionTurn(entry: Record): void { * * Consumer: session-recorder.js on agent_end. */ -export function patchTurnResponse(sessionId: string, turnIndex: number, assistantResponse: string): void { +export function patchTurnResponse( + sessionId: string, + turnIndex: number, + assistantResponse: string, +): void { if (!currentDb) return; currentDb .prepare(`UPDATE turns SET assistant_response = :resp @@ -7815,7 +8025,9 @@ export function restoreManifest(manifest: Record): void { * Used by workflow-migration.ts to populate engine tables from parsed ROADMAP/PLAN * files. All operations run inside a single transaction. */ -export function bulkInsertLegacyHierarchy(payload: Record): void { +export function bulkInsertLegacyHierarchy( + payload: Record, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); const db = currentDb; const { milestones, slices, tasks, clearMilestoneIds, createdAt } = payload; @@ -7874,18 +8086,38 @@ export function bulkInsertLegacyHierarchy(payload: Record): voi // All memory writes go through sf-db.ts so the single-writer invariant // holds. These are direct pass-throughs to the SQL previously in // memory-store.ts — same bindings, same behavior. -export function getActiveMemories({ category, limit = 200 }: { category?: string; limit?: number } = {}): DbRow[] { +export function getActiveMemories({ + category, + limit = 200, +}: { + category?: string; + limit?: number; +} = {}): DbRow[] { if (!currentDb) return []; const rows = category - ? currentDb.prepare("SELECT * FROM active_memories WHERE category = ? ORDER BY updated_at DESC LIMIT ?").all(category, limit) - : currentDb.prepare("SELECT * FROM active_memories ORDER BY updated_at DESC LIMIT ?").all(limit); + ? currentDb + .prepare( + "SELECT * FROM active_memories WHERE category = ? ORDER BY updated_at DESC LIMIT ?", + ) + .all(category, limit) + : currentDb + .prepare( + "SELECT * FROM active_memories ORDER BY updated_at DESC LIMIT ?", + ) + .all(limit); return rows.map((r) => ({ id: r["id"], category: r["category"], content: r["content"], confidence: r["confidence"], sourceUnitId: r["source_unit_id"], - tags: (() => { try { return JSON.parse(r["tags"] ?? "[]"); } catch { return []; } })(), + tags: (() => { + try { + return JSON.parse(r["tags"] ?? "[]"); + } catch { + return []; + } + })(), createdAt: r["created_at"], updatedAt: r["updated_at"], })); @@ -7916,7 +8148,12 @@ export function rewriteMemoryId(placeholderId: string, realId: string): void { ":placeholder": placeholderId, }); } -export function updateMemoryContentRow(id: string, content: string, confidence: number, updatedAt: string): void { +export function updateMemoryContentRow( + id: string, + content: string, + confidence: number, + updatedAt: string, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); if (confidence != null) { currentDb @@ -7945,7 +8182,11 @@ export function incrementMemoryHitCount(id: string, updatedAt: string): void { ) .run({ ":updated_at": updatedAt, ":id": id }); } -export function supersedeMemoryRow(oldId: string, newId: string, updatedAt: string): void { +export function supersedeMemoryRow( + oldId: string, + newId: string, + updatedAt: string, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); currentDb .prepare( @@ -7953,7 +8194,11 @@ export function supersedeMemoryRow(oldId: string, newId: string, updatedAt: stri ) .run({ ":new_id": newId, ":updated_at": updatedAt, ":old_id": oldId }); } -export function markMemoryUnitProcessed(unitKey: string, activityFile: string | null, processedAt: string): void { +export function markMemoryUnitProcessed( + unitKey: string, + activityFile: string | null, + processedAt: string, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); currentDb .prepare(`INSERT OR IGNORE INTO memory_processed_units (unit_key, activity_file, processed_at) @@ -7982,7 +8227,10 @@ export function decayMemoriesBefore(cutoffTs: string, now: string): void { * Consumer: called at autonomous mode startup from auto-start.js. * Returns the number of memories superseded. */ -export function expireStaleMemories(unstartedTtlDays = 28, maxTtlDays = 90): void { +export function expireStaleMemories( + unstartedTtlDays = 28, + maxTtlDays = 90, +): number { if (!currentDb) return 0; const now = new Date().toISOString(); const cutoffUnstarted = new Date( @@ -8005,7 +8253,10 @@ export function expireStaleMemories(unstartedTtlDays = 28, maxTtlDays = 90): voi }); return result.changes ?? 0; } -export function supersedeLowestRankedMemories(limit: number, now: string): void { +export function supersedeLowestRankedMemories( + limit: number, + now: string, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); currentDb .prepare(`UPDATE memories SET superseded_by = 'CAP_EXCEEDED', updated_at = :now @@ -8035,7 +8286,7 @@ export function insertMemorySourceRow(args: Record): void { ":tags": JSON.stringify(args.tags ?? []), }); } -export function deleteMemorySourceRow(id: string): void { +export function deleteMemorySourceRow(id: string): boolean { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); const res = currentDb .prepare("DELETE FROM memory_sources WHERE id = :id") @@ -8061,7 +8312,10 @@ export function insertJudgment(entry: Record): void { // Judgment logging is best-effort } } -export function getJudgmentsForUnit(unitIdPrefix: string, limit = 1000): DbRow[] { +export function getJudgmentsForUnit( + unitIdPrefix: string, + limit = 1000, +): DbRow[] { if (!currentDb) return []; try { const rows = currentDb @@ -8175,7 +8429,7 @@ export function upsertMemoryEmbedding(args: Record): void { ":updated_at": args.updatedAt, }); } -export function deleteMemoryEmbedding(memoryId: string): void { +export function deleteMemoryEmbedding(memoryId: string): boolean { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); const res = currentDb .prepare("DELETE FROM memory_embeddings WHERE memory_id = :id") @@ -8296,7 +8550,10 @@ export function getMilestoneAuditTrail(milestoneId: string): DbRow[] { * Purpose: Support data archaeology and decision-tree reconstruction. * Consumer: forensics tools, doctor checks, audit/compliance queries. */ -export function getSliceAuditTrail(milestoneId: string, sliceId: string): DbRow[] { +export function getSliceAuditTrail( + milestoneId: string, + sliceId: string, +): DbRow[] { if (!currentDb) return []; return currentDb .prepare(` @@ -8318,7 +8575,11 @@ export function getSliceAuditTrail(milestoneId: string, sliceId: string): DbRow[ * Purpose: Support data archaeology and decision-tree reconstruction. * Consumer: forensics tools, doctor checks, audit/compliance queries. */ -export function getTaskAuditTrail(milestoneId: string, sliceId: string, taskId: string): DbRow[] { +export function getTaskAuditTrail( + milestoneId: string, + sliceId: string, + taskId: string, +): DbRow[] { if (!currentDb) return []; return currentDb .prepare(` @@ -8352,7 +8613,10 @@ export function getMilestoneSpec(milestoneId: string): DbRow | undefined { * Purpose: Retrieve spec intent for re-planning or spec validation. * Consumer: plan-slice and spec validation tools. */ -export function getSliceSpec(milestoneId: string, sliceId: string): DbRow | undefined { +export function getSliceSpec( + milestoneId: string, + sliceId: string, +): DbRow | undefined { if (!currentDb) return null; return currentDb .prepare( @@ -8366,7 +8630,11 @@ export function getSliceSpec(milestoneId: string, sliceId: string): DbRow | unde * Purpose: Retrieve spec intent for re-planning or spec validation. * Consumer: plan-task and spec validation tools. */ -export function getTaskSpec(milestoneId: string, sliceId: string, taskId: string): DbRow | undefined { +export function getTaskSpec( + milestoneId: string, + sliceId: string, + taskId: string, +): DbRow | undefined { if (!currentDb) return null; return currentDb .prepare( @@ -8387,7 +8655,17 @@ export function getTaskSpec(milestoneId: string, sliceId: string, taskId: string * * Consumer: autonomous-solver, plan-slice, quality gates, eval runners. */ -export function startValidationRun({ milestoneId, sliceId, taskId, contract }: { milestoneId: string; sliceId: string; taskId: string; contract: string }): string { +export function startValidationRun({ + milestoneId, + sliceId, + taskId, + contract, +}: { + milestoneId: string; + sliceId: string; + taskId: string; + contract: string; +}): string { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); const runId = crypto.randomUUID(); currentDb @@ -8452,7 +8730,11 @@ export function completeValidationRun({ * * Consumer: any surface that needs "are we passing?" for a milestone/slice/task. */ -export function getLatestValidationState(milestoneId: string, sliceId: string, taskId: string): DbRow | undefined { +export function getLatestValidationState( + milestoneId: string, + sliceId: string, + taskId: string, +): DbRow | undefined { if (!currentDb) return null; const rows = currentDb .prepare( @@ -8477,7 +8759,12 @@ export function getLatestValidationState(milestoneId: string, sliceId: string, t * * Consumer: forensics, eval review, audit trail queries. */ -export function getValidationHistory(milestoneId: string, sliceId: string, taskId: string, limit = 20): DbRow[] { +export function getValidationHistory( + milestoneId: string, + sliceId: string, + taskId: string, + limit = 20, +): DbRow[] { if (!currentDb) return []; return currentDb .prepare( @@ -8503,7 +8790,11 @@ export function getValidationHistory(milestoneId: string, sliceId: string, taskI * Purpose: replace .sf/triage/evals|inbox|skills JSONL files with queryable DB rows. * Consumer: commands-todo.js triageTodoDump after successful triage. */ -export function insertTriageRun(id: string, sourceFile: string | null, createdAt: string): void { +export function insertTriageRun( + id: string, + sourceFile: string | null, + createdAt: string, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); currentDb .prepare( @@ -8523,7 +8814,12 @@ export function insertTriageRun(id: string, sourceFile: string | null, createdAt * Purpose: store eval candidates in DB instead of .evals.jsonl. * Consumer: commands-todo.js triageTodoDump. */ -export function insertTriageEval(id: string, runId: string, data: Record, createdAt: string): void { +export function insertTriageEval( + id: string, + runId: string, + data: Record, + createdAt: string, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); currentDb .prepare( @@ -8577,7 +8873,12 @@ export function insertTriageItem( * Purpose: store skill proposals in DB instead of .skills.jsonl. * Consumer: commands-todo.js triageTodoDump. */ -export function insertTriageSkill(id: string, runId: string, data: Record, createdAt: string): void { +export function insertTriageSkill( + id: string, + runId: string, + data: Record, + createdAt: string, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); currentDb .prepare( @@ -8658,7 +8959,9 @@ export function incrementRuntimeCounter(key: string): number { * Purpose: replace .sf/runtime/validation-attention/{mid}.json reads. * Consumer: auto-dispatch.js hasActiveValidationAttentionMarker. */ -export function getValidationAttentionMarker(milestoneId: string): DbRow | undefined { +export function getValidationAttentionMarker( + milestoneId: string, +): DbRow | undefined { if (!currentDb) return null; return ( currentDb @@ -8674,7 +8977,10 @@ export function getValidationAttentionMarker(milestoneId: string): DbRow | undef * Purpose: replace .sf/runtime/validation-attention/{mid}.json writes. * Consumer: auto-dispatch.js writeValidationAttentionMarker. */ -export function upsertValidationAttentionMarker(milestoneId: string, marker: Record): void { +export function upsertValidationAttentionMarker( + milestoneId: string, + marker: Record, +): void { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); const now = new Date().toISOString(); currentDb @@ -8705,7 +9011,12 @@ export function upsertValidationAttentionMarker(milestoneId: string, marker: Rec * Purpose: persist adaptive tier learning to DB so routing decisions survive restarts. * Consumer: routing-history.js recordOutcome. */ -export function upsertRoutingOutcome(db: DbAdapter, pattern: string, tier: string, success: boolean): void { +export function upsertRoutingOutcome( + db: DbAdapter, + pattern: string, + tier: string, + success: boolean, +): void { db.prepare( `INSERT INTO routing_history (pattern, tier, success_count, fail_count, updated_at) VALUES (:pattern, :tier, :success_count, :fail_count, :updated_at) @@ -8740,7 +9051,10 @@ export function getAllRoutingHistory(db: DbAdapter): DbRow[] { * Purpose: targeted pattern lookup for adaptive tier queries. * Consumer: routing-history.js getRoutingHistoryForPattern. */ -export function getRoutingHistoryForPattern(db: DbAdapter, pattern: string): DbRow[] { +export function getRoutingHistoryForPattern( + db: DbAdapter, + pattern: string, +): DbRow[] { return db .prepare( "SELECT tier, success_count, fail_count FROM routing_history WHERE pattern = ?", @@ -8753,7 +9067,12 @@ export function getRoutingHistoryForPattern(db: DbAdapter, pattern: string): DbR * Purpose: persist user feedback for later analysis and weighted outcome application. * Consumer: routing-history.js recordFeedback. */ -export function insertRoutingFeedback(db: DbAdapter, pattern: string, tier: string, feedback: string): void { +export function insertRoutingFeedback( + db: DbAdapter, + pattern: string, + tier: string, + feedback: string, +): void { db.prepare( `INSERT INTO routing_feedback (pattern, tier, feedback, recorded_at) VALUES (:pattern, :tier, :feedback, :recorded_at)`, @@ -8777,7 +9096,9 @@ export function clearRoutingHistory(db: DbAdapter): void { // ─── Unit Metrics CRUD ──────────────────────────────────────────────────────── -function rowToUnitMetrics(row: Record): Record { +function rowToUnitMetrics( + row: Record, +): Record { const unit = { type: row["type"], id: row["id"], @@ -8826,7 +9147,10 @@ function rowToUnitMetrics(row: Record): Record * * Consumer: metrics.js saveLedger (called after every unit snapshot). */ -export function upsertUnitMetrics(db: DbAdapter, unit: Record): void { +export function upsertUnitMetrics( + db: DbAdapter, + unit: Record, +): void { db.prepare( `INSERT OR REPLACE INTO unit_metrics ( type, id, started_at, finished_at, model, auto_session_key, diff --git a/packages/pi-agent-core/src/db/task-frontmatter.ts b/packages/pi-agent-core/src/db/task-frontmatter.ts index 547eddc8c..a7e5f3161 100644 --- a/packages/pi-agent-core/src/db/task-frontmatter.ts +++ b/packages/pi-agent-core/src/db/task-frontmatter.ts @@ -9,7 +9,13 @@ * sf-db row mapping, and task state machine. */ -export const RISK_LEVELS = ["none", "low", "medium", "high", "critical"] as const; +export const RISK_LEVELS = [ + "none", + "low", + "medium", + "high", + "critical", +] as const; export type RiskLevel = (typeof RISK_LEVELS)[number]; export const MUTATION_SCOPES = [ @@ -196,7 +202,9 @@ export interface ValidationResult { normalized: TaskFrontmatter; } -export function validateTaskFrontmatter(frontmatter: FrontmatterInput = {}): ValidationResult { +export function validateTaskFrontmatter( + frontmatter: FrontmatterInput = {}, +): ValidationResult { const errors: string[] = []; const normalized: Record = { ...DEFAULT_TASK_FRONTMATTER, @@ -454,7 +462,13 @@ export function computeTaskPriority(task: TaskRecord): number { const fm = task.frontmatter ?? buildTaskRecord(task).frontmatter; let score = 50; - const riskScores: Record = { none: 0, low: 5, medium: 15, high: 30, critical: 50 }; + const riskScores: Record = { + none: 0, + low: 5, + medium: 15, + high: 30, + critical: 50, + }; score += riskScores[fm.risk] ?? 0; const scopeScores: Record = { diff --git a/packages/pi-agent-core/src/db/uok/trace-writer.ts b/packages/pi-agent-core/src/db/uok/trace-writer.ts index 8c6980362..3f86b9362 100644 --- a/packages/pi-agent-core/src/db/uok/trace-writer.ts +++ b/packages/pi-agent-core/src/db/uok/trace-writer.ts @@ -7,12 +7,7 @@ * * Consumer: sf-db.ts gate statistics functions (getGateStats, etc.) */ -import { - existsSync, - readdirSync, - readFileSync, - statSync, -} from "node:fs"; +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { join } from "node:path"; function sfRoot(basePath: string): string { @@ -54,9 +49,7 @@ export function readTraceEvents( try { const filePath = join(dir, file); if (statSync(filePath).mtimeMs < cutoff) continue; - const lines = readFileSync(filePath, "utf-8") - .split("\n") - .filter(Boolean); + const lines = readFileSync(filePath, "utf-8").split("\n").filter(Boolean); for (const line of lines) { try { const ev = JSON.parse(line) as Record; diff --git a/packages/pi-agent-core/tsconfig.json b/packages/pi-agent-core/tsconfig.json index 6a2c23cf2..24f67372f 100644 --- a/packages/pi-agent-core/tsconfig.json +++ b/packages/pi-agent-core/tsconfig.json @@ -2,9 +2,7 @@ "compilerOptions": { "target": "ES2024", "module": "Node16", - "lib": [ - "ES2024" - ], + "lib": ["ES2024"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, @@ -19,15 +17,11 @@ "resolveJsonModule": true, "allowImportingTsExtensions": false, "useDefineForClassFields": false, - "types": [ - "node" - ], + "types": ["node"], "outDir": "./dist", "rootDir": "./src" }, - "include": [ - "src/**/*.ts" - ], + "include": ["src/**/*.ts"], "exclude": [ "node_modules", "dist", diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index 9d7b5085b..370b77136 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -72,8 +72,6 @@ export { } from "./keys.js"; // Render safety — prevents one failing component from blanking the TUI export { tryRender } from "./render-guard.js"; -// TUI input listener — used with `TUI.addInputListener` -export type { InputListener } from "./tui-input-dispatch.js"; // Input buffering for batch splitting export { StdinBuffer, @@ -117,5 +115,7 @@ export { type SizeValue, TUI, } from "./tui.js"; +// TUI input listener — used with `TUI.addInputListener` +export type { InputListener } from "./tui-input-dispatch.js"; // Utilities export { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "./utils.js"; diff --git a/packages/tui/tsconfig.json b/packages/tui/tsconfig.json index 2af393ca5..5274850db 100644 --- a/packages/tui/tsconfig.json +++ b/packages/tui/tsconfig.json @@ -2,9 +2,7 @@ "compilerOptions": { "target": "ES2024", "module": "Node16", - "lib": [ - "ES2024" - ], + "lib": ["ES2024"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, @@ -19,17 +17,10 @@ "resolveJsonModule": true, "allowImportingTsExtensions": false, "useDefineForClassFields": false, - "types": [ - "node" - ], + "types": ["node"], "outDir": "./dist", "rootDir": "./src" }, - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules", - "dist" - ] + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] } diff --git a/scripts/check-circular-deps.mjs b/scripts/check-circular-deps.mjs index 59f036d06..4b7874f83 100644 --- a/scripts/check-circular-deps.mjs +++ b/scripts/check-circular-deps.mjs @@ -1,4 +1,5 @@ #!/usr/bin/env node + /** * check-circular-deps.mjs — detect circular imports across the SF codebase. * @@ -10,9 +11,9 @@ * Exit 0 = no cycles found. Exit 1 = cycles detected (or scan error). */ -import madge from "madge"; -import { resolve, dirname } from "node:path"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import madge from "madge"; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = resolve(__dirname, ".."); @@ -25,7 +26,9 @@ const entries = extOnly ? [resolve(root, "src/resources/extensions/sf")] : [resolve(root, "src"), resolve(root, "packages")]; -console.error(`Scanning: ${entries.map((e) => e.replace(root + "/", "")).join(", ")}`); +console.error( + `Scanning: ${entries.map((e) => e.replace(root + "/", "")).join(", ")}`, +); let result; try { diff --git a/scripts/check-sf-extension-inventory.mjs b/scripts/check-sf-extension-inventory.mjs index 9e55d959a..92df00715 100644 --- a/scripts/check-sf-extension-inventory.mjs +++ b/scripts/check-sf-extension-inventory.mjs @@ -198,7 +198,18 @@ function parseHandledTopLevelCommands() { } function parseDirectRegisteredCommands() { - const files = ["ui/color-band.js", "ui/emoji.js", "subagent/index.js"]; + const files = [ + "commands/legacy/audit.js", + "commands/legacy/create-extension.js", + "commands/legacy/create-slash-command.js", + "guards/inturn.js", + "notifications/notify.js", + "permissions/index.js", + "subagent/index.js", + "ui/color-band.js", + "ui/emoji.js", + "ui/usage-bar.js", + ]; const commands = new Set(); for (const file of files) { const source = read(join(sfRoot, file)); @@ -262,7 +273,9 @@ function main() { catalogCommands.filter( (command) => !BASE_RUNTIME_COMMAND_NAMES.has(command), ), - parseDirectRegisteredCommands(), + parseDirectRegisteredCommands().filter( + (command) => !BASE_RUNTIME_COMMAND_NAMES.has(command), + ), ), ); const missingManifestTools = registeredTools.filter( diff --git a/scripts/generate-features-inventory.mjs b/scripts/generate-features-inventory.mjs index 7b233568c..45ed4cc71 100644 --- a/scripts/generate-features-inventory.mjs +++ b/scripts/generate-features-inventory.mjs @@ -49,9 +49,7 @@ export function parseKnownProviders() { const src = readFileSync(providersPath, "utf8"); const match = src.match(/export type KnownProvider =([\s\S]*?);/); if (!match) - throw new Error( - "Could not find KnownProvider in packages/ai/src/types.ts", - ); + throw new Error("Could not find KnownProvider in packages/ai/src/types.ts"); const providers = [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]); return uniqueSorted(providers); } @@ -84,9 +82,7 @@ export function parseSfNativeTools() { "Could not find provides.tools in src/resources/extensions/sf/extension-manifest.json", ); } - return uniqueSorted( - tools.filter((tool) => typeof tool === "string"), - ); + return uniqueSorted(tools.filter((tool) => typeof tool === "string")); } export function parseSearchProviders() { diff --git a/scripts/validate-pack.js b/scripts/validate-pack.js index 0041f1e91..845b117a5 100644 --- a/scripts/validate-pack.js +++ b/scripts/validate-pack.js @@ -58,13 +58,7 @@ try { console.log( "==> Checking workspace packages for @singularity-forge/* cross-deps...", ); - const workspaces = [ - "native", - "agent-core", - "ai", - "coding-agent", - "tui", - ]; + const workspaces = ["native", "agent-core", "ai", "coding-agent", "tui"]; let crossFailed = false; for (const ws of workspaces) { diff --git a/src/resources/extensions/bg-shell/bg-shell-lifecycle.js b/src/resources/extensions/bg-shell/bg-shell-lifecycle.js index 0e6027bab..01404113a 100644 --- a/src/resources/extensions/bg-shell/bg-shell-lifecycle.js +++ b/src/resources/extensions/bg-shell/bg-shell-lifecycle.js @@ -2,8 +2,9 @@ * bg_shell lifecycle hook registration — session events, compaction awareness, * context injection, process discovery, footer widget, and periodic maintenance. */ -import { truncateToWidth, visibleWidth } from "@singularity-forge/tui"; + import { formatTokenCount } from "@singularity-forge/coding-agent"; +import { truncateToWidth, visibleWidth } from "@singularity-forge/tui"; import { cleanupAll, cleanupSessionProcesses, diff --git a/src/resources/extensions/bg-shell/bg-shell-tool.js b/src/resources/extensions/bg-shell/bg-shell-tool.js index dafb033cc..950f0c257 100644 --- a/src/resources/extensions/bg-shell/bg-shell-tool.js +++ b/src/resources/extensions/bg-shell/bg-shell-tool.js @@ -3,8 +3,8 @@ */ import { Type } from "@sinclair/typebox"; import { StringEnum } from "@singularity-forge/ai"; -import { Text } from "@singularity-forge/tui"; import { toPosixPath } from "@singularity-forge/coding-agent"; +import { Text } from "@singularity-forge/tui"; import { queryShellEnv, runOnSession, sendAndWait } from "./interaction.js"; import { formatDigestText, diff --git a/src/resources/extensions/claude-code-cli/tests/stream-adapter-permissions.test.ts b/src/resources/extensions/claude-code-cli/tests/stream-adapter-permissions.test.ts index 38b20e12c..c9f33ebf7 100644 --- a/src/resources/extensions/claude-code-cli/tests/stream-adapter-permissions.test.ts +++ b/src/resources/extensions/claude-code-cli/tests/stream-adapter-permissions.test.ts @@ -34,9 +34,7 @@ describe("buildBashPermissionPattern", () => { }); it("gh_command_includes_two_subcommand_levels", () => { - expect(buildBashPermissionPattern("gh pr list")).toBe( - "Bash(gh pr list:*)", - ); + expect(buildBashPermissionPattern("gh pr list")).toBe("Bash(gh pr list:*)"); }); it("compound_command_extracts_meaningful_operation", () => { @@ -89,17 +87,15 @@ describe("createClaudeCodeCanUseToolHandler", () => { makeOptions() as never, ); expect(result.behavior).toBe("allow"); - expect((result as { updatedPermissions?: unknown }).updatedPermissions).toBeUndefined(); + expect( + (result as { updatedPermissions?: unknown }).updatedPermissions, + ).toBeUndefined(); }); it("deny_returns_deny_behavior", async () => { const ui = makeUi(["Deny"]); const handler = createClaudeCodeCanUseToolHandler(ui)!; - const result = await handler( - "AskUserQuestion", - {}, - makeOptions() as never, - ); + const result = await handler("AskUserQuestion", {}, makeOptions() as never); expect(result.behavior).toBe("deny"); }); @@ -116,7 +112,8 @@ describe("createClaudeCodeCanUseToolHandler", () => { makeOptions({ suggestions: [] }) as never, ); expect(result.behavior).toBe("allow"); - const perms = (result as { updatedPermissions?: unknown[] }).updatedPermissions; + const perms = (result as { updatedPermissions?: unknown[] }) + .updatedPermissions; expect(Array.isArray(perms)).toBe(true); expect(perms!.length).toBeGreaterThan(0); const rule = perms![0] as { @@ -151,7 +148,8 @@ describe("createClaudeCodeCanUseToolHandler", () => { makeOptions({ suggestions: sdkSuggestions }) as never, ); expect(result.behavior).toBe("allow"); - const perms = (result as { updatedPermissions?: unknown[] }).updatedPermissions; + const perms = (result as { updatedPermissions?: unknown[] }) + .updatedPermissions; expect(perms).toEqual(sdkSuggestions); }); @@ -165,7 +163,8 @@ describe("createClaudeCodeCanUseToolHandler", () => { makeOptions({ suggestions: [] }) as never, ); expect(result.behavior).toBe("allow"); - const perms = (result as { updatedPermissions?: unknown[] }).updatedPermissions; + const perms = (result as { updatedPermissions?: unknown[] }) + .updatedPermissions; expect(Array.isArray(perms)).toBe(true); const rule = perms![0] as { type: string; diff --git a/src/resources/extensions/mcp-client/auth.js b/src/resources/extensions/mcp-client/auth.js index 30463fb75..5941ce939 100644 --- a/src/resources/extensions/mcp-client/auth.js +++ b/src/resources/extensions/mcp-client/auth.js @@ -5,4 +5,7 @@ * This shim keeps backward compatibility for any import of ./auth.js from * within the extension or from tests. */ -export { buildHttpTransportOpts, createCliOAuthProvider } from "@singularity-forge/coding-agent"; +export { + buildHttpTransportOpts, + createCliOAuthProvider, +} from "@singularity-forge/coding-agent"; diff --git a/src/resources/extensions/mcp-client/index.js b/src/resources/extensions/mcp-client/index.js index 28e7b9ca3..cc318214c 100644 --- a/src/resources/extensions/mcp-client/index.js +++ b/src/resources/extensions/mcp-client/index.js @@ -15,11 +15,11 @@ */ import { Type } from "@sinclair/typebox"; import { -DEFAULT_MAX_BYTES, -DEFAULT_MAX_LINES, -McpConnectionManager, -formatSize, -truncateHead, + DEFAULT_MAX_BYTES, + DEFAULT_MAX_LINES, + formatSize, + McpConnectionManager, + truncateHead, } from "@singularity-forge/coding-agent"; import { Text } from "@singularity-forge/tui"; @@ -29,71 +29,71 @@ const manager = new McpConnectionManager(); // ─── JSON Schema → TypeBox converter ───────────────────────────────────────── // eslint-disable-next-line @typescript-eslint/no-explicit-any function jsonSchemaPropToTypeBox(schema) { -if (!schema || typeof schema !== "object") return Type.Any(); -const t = schema.type; -if (t === "string") return Type.String({ description: schema.description }); -if (t === "number" || t === "integer") -return Type.Number({ description: schema.description }); -if (t === "boolean") return Type.Boolean({ description: schema.description }); -if (t === "array") return Type.Array(Type.Any()); -if (t === "object") { -const props = schema.properties; -if (props) { -const entries = {}; -for (const [k, v] of Object.entries(props)) { -entries[k] = jsonSchemaPropToTypeBox(v); -} -return Type.Object(entries); -} -} -return Type.Any(); + if (!schema || typeof schema !== "object") return Type.Any(); + const t = schema.type; + if (t === "string") return Type.String({ description: schema.description }); + if (t === "number" || t === "integer") + return Type.Number({ description: schema.description }); + if (t === "boolean") return Type.Boolean({ description: schema.description }); + if (t === "array") return Type.Array(Type.Any()); + if (t === "object") { + const props = schema.properties; + if (props) { + const entries = {}; + for (const [k, v] of Object.entries(props)) { + entries[k] = jsonSchemaPropToTypeBox(v); + } + return Type.Object(entries); + } + } + return Type.Any(); } // eslint-disable-next-line @typescript-eslint/no-explicit-any function jsonSchemaToTypeBox(schema) { -if (!schema || typeof schema !== "object") return Type.Object({}); -const obj = schema; -const props = obj.properties; -if (!props) return Type.Object({}); -const entries = {}; -for (const [k, v] of Object.entries(props)) { -entries[k] = jsonSchemaPropToTypeBox(v); -} -return Type.Object(entries); + if (!schema || typeof schema !== "object") return Type.Object({}); + const obj = schema; + const props = obj.properties; + if (!props) return Type.Object({}); + const entries = {}; + for (const [k, v] of Object.entries(props)) { + entries[k] = jsonSchemaPropToTypeBox(v); + } + return Type.Object(entries); } // ─── Formatters ─────────────────────────────────────────────────────────────── function formatServerList(servers) { -if (servers.length === 0) -return "No MCP servers configured. Add servers to .mcp.json or .sf/mcp.json."; -const lines = [`${servers.length} MCP servers configured:\n`]; -for (const s of servers) { -const connected = manager.isConnected(s.name) ? "✓" : "○"; -const cached = manager.getCachedTools(s.name); -const toolCount = cached ? ` — ${cached.length} tools` : ""; -lines.push(`${connected} ${s.name} (${s.transport})${toolCount}`); -} -lines.push( -"\nUse mcp_discover to see full tool schemas for a specific server.", -); -lines.push("Use mcp_call to invoke a tool: mcp_call(server, tool, args)."); -return lines.join("\n"); + if (servers.length === 0) + return "No MCP servers configured. Add servers to .mcp.json or .sf/mcp.json."; + const lines = [`${servers.length} MCP servers configured:\n`]; + for (const s of servers) { + const connected = manager.isConnected(s.name) ? "✓" : "○"; + const cached = manager.getCachedTools(s.name); + const toolCount = cached ? ` — ${cached.length} tools` : ""; + lines.push(`${connected} ${s.name} (${s.transport})${toolCount}`); + } + lines.push( + "\nUse mcp_discover to see full tool schemas for a specific server.", + ); + lines.push("Use mcp_call to invoke a tool: mcp_call(server, tool, args)."); + return lines.join("\n"); } function formatToolList(serverName, tools) { -const lines = [`${serverName} — ${tools.length} tools:\n`]; -for (const tool of tools) { -lines.push(`## ${tool.name}`); -if (tool.description) lines.push(tool.description); -if (tool.inputSchema) { -lines.push("```json"); -lines.push(JSON.stringify(tool.inputSchema, null, 2)); -lines.push("```"); -} -lines.push(""); -} -lines.push( -`Call with: mcp_call(server="${serverName}", tool="", args={...})`, -); -return lines.join("\n"); + const lines = [`${serverName} — ${tools.length} tools:\n`]; + for (const tool of tools) { + lines.push(`## ${tool.name}`); + if (tool.description) lines.push(tool.description); + if (tool.inputSchema) { + lines.push("```json"); + lines.push(JSON.stringify(tool.inputSchema, null, 2)); + lines.push("```"); + } + lines.push(""); + } + lines.push( + `Call with: mcp_call(server="${serverName}", tool="", args={...})`, + ); + return lines.join("\n"); } // ─── Status helpers (consumed by /sf mcp) ──────────────────────────────────── @@ -105,7 +105,7 @@ return lines.join("\n"); * Consumer: /mcp reload command handler in commands-mcp-status.js. */ export async function disconnectAll() { -await manager.disconnectAll(); + await manager.disconnectAll(); } /** @@ -113,311 +113,315 @@ await manager.disconnectAll(); * Safe to call even when the server has never been connected. */ export function getConnectionStatus(name) { -return manager.getConnectionStatus(name); + return manager.getConnectionStatus(name); } // ─── Test-exported helpers ──────────────────────────────────────────────────── export function _buildMcpChildEnvForTest(env) { -return manager.buildChildEnv(env); + return manager.buildChildEnv(env); } export function _buildMcpTrustConfirmOptionsForTest(signal) { -return { timeout: 120_000, signal }; + return { timeout: 120_000, signal }; } // ─── Extension ──────────────────────────────────────────────────────────────── export default function (pi) { -// ── mcp_servers ────────────────────────────────────────────────────────── -pi.registerTool({ -name: "mcp_servers", -label: "MCP Servers", -description: -"List all available MCP servers configured in project files (.mcp.json, .sf/mcp.json). " + -"Shows server names, transport type, and connection status. After mcp_discover, each server's " + -"tools are auto-registered as first-class pi tools (e.g. serena_find_symbol).", -promptSnippet: "List available MCP servers from project configuration", -promptGuidelines: [ -"Call mcp_servers to see what MCP servers are available before trying to use one.", -"After mcp_discover(server), the server's tools appear as real pi tools.", -"MCP servers provide external integrations (Twitter, Linear, Railway, etc.) via the Model Context Protocol.", -"After listing, use mcp_discover(server) to get tool schemas, then mcp_call(server, tool, args) to invoke.", -], -parameters: Type.Object({ -refresh: Type.Optional( -Type.Boolean({ -description: "Force refresh the server list (default: use cache)", -}), -), -}), -async execute(_id, params) { -if (params.refresh) manager.invalidateConfigCache(); -const servers = manager.readConfigs(); -return { -content: [{ type: "text", text: formatServerList(servers) }], -details: { -serverCount: servers.length, -cached: !params.refresh, -}, -}; -}, -renderCall(args, theme) { -let text = theme.fg("toolTitle", theme.bold("mcp_servers")); -if (args.refresh) text += theme.fg("warning", " (refresh)"); -return new Text(text, 0, 0); -}, -renderResult(result, { isPartial }, theme) { -if (isPartial) -return new Text(theme.fg("warning", "Reading MCP config..."), 0, 0); -const d = result.details; -return new Text( -theme.fg("success", `${d?.serverCount ?? 0} servers configured`), -0, -0, -); -}, -}); + // ── mcp_servers ────────────────────────────────────────────────────────── + pi.registerTool({ + name: "mcp_servers", + label: "MCP Servers", + description: + "List all available MCP servers configured in project files (.mcp.json, .sf/mcp.json). " + + "Shows server names, transport type, and connection status. After mcp_discover, each server's " + + "tools are auto-registered as first-class pi tools (e.g. serena_find_symbol).", + promptSnippet: "List available MCP servers from project configuration", + promptGuidelines: [ + "Call mcp_servers to see what MCP servers are available before trying to use one.", + "After mcp_discover(server), the server's tools appear as real pi tools.", + "MCP servers provide external integrations (Twitter, Linear, Railway, etc.) via the Model Context Protocol.", + "After listing, use mcp_discover(server) to get tool schemas, then mcp_call(server, tool, args) to invoke.", + ], + parameters: Type.Object({ + refresh: Type.Optional( + Type.Boolean({ + description: "Force refresh the server list (default: use cache)", + }), + ), + }), + async execute(_id, params) { + if (params.refresh) manager.invalidateConfigCache(); + const servers = manager.readConfigs(); + return { + content: [{ type: "text", text: formatServerList(servers) }], + details: { + serverCount: servers.length, + cached: !params.refresh, + }, + }; + }, + renderCall(args, theme) { + let text = theme.fg("toolTitle", theme.bold("mcp_servers")); + if (args.refresh) text += theme.fg("warning", " (refresh)"); + return new Text(text, 0, 0); + }, + renderResult(result, { isPartial }, theme) { + if (isPartial) + return new Text(theme.fg("warning", "Reading MCP config..."), 0, 0); + const d = result.details; + return new Text( + theme.fg("success", `${d?.serverCount ?? 0} servers configured`), + 0, + 0, + ); + }, + }); -// ── mcp_discover ───────────────────────────────────────────────────────── -pi.registerTool({ -name: "mcp_discover", -label: "MCP Discover", -description: -"Get detailed tool signatures and JSON schemas for a specific MCP server. " + -"Connects to the server on first call (lazy connection). " + -"After discovery, each MCP tool is auto-registered as a first-class pi tool " + -"(e.g. serena_find_symbol) — the LLM can call them directly without mcp_call.", -promptSnippet: -"Discover MCP server tools and register them as first-class pi tools", -promptGuidelines: [ -"Call mcp_discover(server) to connect to an MCP server and surface its tools.", -"After discovery, the LLM sees each tool by its real name (e.g. serena_search_for_pattern).", -"Call tools directly by their names instead of going through mcp_call.", -], -parameters: Type.Object({ -server: Type.String({ -description: -"MCP server name (from mcp_servers output), e.g. 'railway', 'twitter-mcp', 'linear'", -}), -}), -async execute(_id, params, signal) { -try { -const cached = manager.getCachedTools(params.server); -if (cached) { -const text = formatToolList(params.server, cached); -const truncation = truncateHead(text, { -maxLines: DEFAULT_MAX_LINES, -maxBytes: DEFAULT_MAX_BYTES, -}); -let finalText = truncation.content; -if (truncation.truncated) { -finalText += `\n\n[Truncated: ${truncation.outputLines}/${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`; -} -return { -content: [{ type: "text", text: finalText }], -details: { -server: params.server, -toolCount: cached.length, -cached: true, -}, -}; -} -const client = await manager.getOrConnect(params.server, signal); -const result = await client.listTools(undefined, { -signal, -timeout: 30000, -}); -const tools = (result.tools ?? []).map((t) => ({ -name: t.name, -description: t.description ?? "", -inputSchema: t.inputSchema, -})); -manager.setCachedTools(params.server, tools); -// Auto-register each MCP tool as a first-class pi tool. -manager.registerToolsForServer(params.server, tools, ({ name, label, description, inputSchemaRaw, execute }) => { -const paramType = inputSchemaRaw -? jsonSchemaToTypeBox(inputSchemaRaw) -: Type.Object({}); -pi.registerTool({ -name, -label, -description, -parameters: paramType, -async execute(id, toolParams, toolSignal) { -const res = await execute(id, toolParams, toolSignal); -const truncation = truncateHead(res.content[0]?.text ?? "", { -maxLines: DEFAULT_MAX_LINES, -maxBytes: DEFAULT_MAX_BYTES, -}); -let finalText = truncation.content; -if (truncation.truncated) { -finalText += `\n\n[Output truncated: ${truncation.outputLines}/${truncation.totalLines} lines]`; -} -return { -content: [{ type: "text", text: finalText }], -details: res.details, -}; -}, -}); -}); -const text = formatToolList(params.server, tools); -const truncation = truncateHead(text, { -maxLines: DEFAULT_MAX_LINES, -maxBytes: DEFAULT_MAX_BYTES, -}); -let finalText = truncation.content; -if (truncation.truncated) { -finalText += `\n\n[Truncated: ${truncation.outputLines}/${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`; -} -return { -content: [{ type: "text", text: finalText }], -details: { -server: params.server, -toolCount: tools.length, -cached: false, -}, -}; -} catch (err) { -const msg = err instanceof Error ? err.message : String(err); -throw new Error( -`Failed to discover tools for "${params.server}": ${msg}`, -); -} -}, -renderCall(args, theme) { -let text = theme.fg("toolTitle", theme.bold("mcp_discover ")); -text += theme.fg("accent", args.server); -return new Text(text, 0, 0); -}, -renderResult(result, { isPartial }, theme) { -if (isPartial) -return new Text(theme.fg("warning", "Discovering tools..."), 0, 0); -const d = result.details; -return new Text( -theme.fg("success", `${d?.toolCount ?? 0} tools`) + -theme.fg("dim", ` · ${d?.server}`), -0, -0, -); -}, -}); + // ── mcp_discover ───────────────────────────────────────────────────────── + pi.registerTool({ + name: "mcp_discover", + label: "MCP Discover", + description: + "Get detailed tool signatures and JSON schemas for a specific MCP server. " + + "Connects to the server on first call (lazy connection). " + + "After discovery, each MCP tool is auto-registered as a first-class pi tool " + + "(e.g. serena_find_symbol) — the LLM can call them directly without mcp_call.", + promptSnippet: + "Discover MCP server tools and register them as first-class pi tools", + promptGuidelines: [ + "Call mcp_discover(server) to connect to an MCP server and surface its tools.", + "After discovery, the LLM sees each tool by its real name (e.g. serena_search_for_pattern).", + "Call tools directly by their names instead of going through mcp_call.", + ], + parameters: Type.Object({ + server: Type.String({ + description: + "MCP server name (from mcp_servers output), e.g. 'railway', 'twitter-mcp', 'linear'", + }), + }), + async execute(_id, params, signal) { + try { + const cached = manager.getCachedTools(params.server); + if (cached) { + const text = formatToolList(params.server, cached); + const truncation = truncateHead(text, { + maxLines: DEFAULT_MAX_LINES, + maxBytes: DEFAULT_MAX_BYTES, + }); + let finalText = truncation.content; + if (truncation.truncated) { + finalText += `\n\n[Truncated: ${truncation.outputLines}/${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`; + } + return { + content: [{ type: "text", text: finalText }], + details: { + server: params.server, + toolCount: cached.length, + cached: true, + }, + }; + } + const client = await manager.getOrConnect(params.server, signal); + const result = await client.listTools(undefined, { + signal, + timeout: 30000, + }); + const tools = (result.tools ?? []).map((t) => ({ + name: t.name, + description: t.description ?? "", + inputSchema: t.inputSchema, + })); + manager.setCachedTools(params.server, tools); + // Auto-register each MCP tool as a first-class pi tool. + manager.registerToolsForServer( + params.server, + tools, + ({ name, label, description, inputSchemaRaw, execute }) => { + const paramType = inputSchemaRaw + ? jsonSchemaToTypeBox(inputSchemaRaw) + : Type.Object({}); + pi.registerTool({ + name, + label, + description, + parameters: paramType, + async execute(id, toolParams, toolSignal) { + const res = await execute(id, toolParams, toolSignal); + const truncation = truncateHead(res.content[0]?.text ?? "", { + maxLines: DEFAULT_MAX_LINES, + maxBytes: DEFAULT_MAX_BYTES, + }); + let finalText = truncation.content; + if (truncation.truncated) { + finalText += `\n\n[Output truncated: ${truncation.outputLines}/${truncation.totalLines} lines]`; + } + return { + content: [{ type: "text", text: finalText }], + details: res.details, + }; + }, + }); + }, + ); + const text = formatToolList(params.server, tools); + const truncation = truncateHead(text, { + maxLines: DEFAULT_MAX_LINES, + maxBytes: DEFAULT_MAX_BYTES, + }); + let finalText = truncation.content; + if (truncation.truncated) { + finalText += `\n\n[Truncated: ${truncation.outputLines}/${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`; + } + return { + content: [{ type: "text", text: finalText }], + details: { + server: params.server, + toolCount: tools.length, + cached: false, + }, + }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + throw new Error( + `Failed to discover tools for "${params.server}": ${msg}`, + ); + } + }, + renderCall(args, theme) { + let text = theme.fg("toolTitle", theme.bold("mcp_discover ")); + text += theme.fg("accent", args.server); + return new Text(text, 0, 0); + }, + renderResult(result, { isPartial }, theme) { + if (isPartial) + return new Text(theme.fg("warning", "Discovering tools..."), 0, 0); + const d = result.details; + return new Text( + theme.fg("success", `${d?.toolCount ?? 0} tools`) + + theme.fg("dim", ` · ${d?.server}`), + 0, + 0, + ); + }, + }); -// ── mcp_call ───────────────────────────────────────────────────────────── -pi.registerTool({ -name: "mcp_call", -label: "MCP Call", -description: -"Call a tool on an MCP server. Provide the server name, tool name, and arguments. " + -"Connects to the server on first call (lazy connection). " + -"Use mcp_discover first to see available tools and their required arguments.", -promptSnippet: "Call a tool on an MCP server", -promptGuidelines: [ -"Always use mcp_discover first to understand the tool's parameters before calling mcp_call.", -"Arguments are passed as a JSON object matching the tool's input schema.", -], -parameters: Type.Object({ -server: Type.String({ -description: "MCP server name, e.g. 'railway', 'twitter-mcp'", -}), -tool: Type.String({ -description: "Tool name on that server, e.g. 'railway_list_projects'", -}), -args: Type.Optional( -Type.Object( -{}, -{ -additionalProperties: true, -description: -"Tool arguments as key-value pairs matching the tool's input schema", -}, -), -), -}), -async execute(_id, params, signal) { -try { -const client = await manager.getOrConnect(params.server, signal); -const result = await client.callTool( -{ name: params.tool, arguments: params.args ?? {} }, -undefined, -{ signal, timeout: 60000 }, -); -const contentItems = result.content; -const raw = contentItems -.map((c) => (c.type === "text" ? (c.text ?? "") : JSON.stringify(c))) -.join("\n"); -const truncation = truncateHead(raw, { -maxLines: DEFAULT_MAX_LINES, -maxBytes: DEFAULT_MAX_BYTES, -}); -let finalText = truncation.content; -if (truncation.truncated) { -finalText += `\n\n[Output truncated: ${truncation.outputLines}/${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`; -} -return { -content: [{ type: "text", text: finalText }], -details: { -server: params.server, -tool: params.tool, -charCount: finalText.length, -truncated: truncation.truncated, -}, -}; -} catch (err) { -const msg = err instanceof Error ? err.message : String(err); -throw new Error( -`MCP call failed: ${params.server}.${params.tool}\n${msg}`, -); -} -}, -renderCall(args, theme) { -let text = theme.fg("toolTitle", theme.bold("mcp_call ")); -text += theme.fg("accent", `${args.server}.${args.tool}`); -if (args.args && Object.keys(args.args).length > 0) { -const preview = Object.entries(args.args) -.slice(0, 3) -.map(([k, v]) => { -const val = typeof v === "string" ? v : JSON.stringify(v); -return `${k}:${val.length > 30 ? val.slice(0, 30) + "…" : val}`; -}) -.join(" "); -text += " " + theme.fg("muted", preview); -} -return new Text(text, 0, 0); -}, -renderResult(result, { isPartial, expanded }, theme) { -if (isPartial) -return new Text(theme.fg("warning", "Calling MCP tool..."), 0, 0); -const d = result.details; -let text = theme.fg("success", `✓ ${d?.server}.${d?.tool}`); -text += theme.fg( -"dim", -` · ${(d?.charCount ?? 0).toLocaleString()} chars`, -); -if (d?.truncated) text += theme.fg("warning", " · truncated"); -if (expanded) { -const content = result.content[0]; -if (content?.type === "text") { -const preview = content.text.split("\n").slice(0, 15).join("\n"); -text += "\n\n" + theme.fg("dim", preview); -} -} -return new Text(text, 0, 0); -}, -}); + // ── mcp_call ───────────────────────────────────────────────────────────── + pi.registerTool({ + name: "mcp_call", + label: "MCP Call", + description: + "Call a tool on an MCP server. Provide the server name, tool name, and arguments. " + + "Connects to the server on first call (lazy connection). " + + "Use mcp_discover first to see available tools and their required arguments.", + promptSnippet: "Call a tool on an MCP server", + promptGuidelines: [ + "Always use mcp_discover first to understand the tool's parameters before calling mcp_call.", + "Arguments are passed as a JSON object matching the tool's input schema.", + ], + parameters: Type.Object({ + server: Type.String({ + description: "MCP server name, e.g. 'railway', 'twitter-mcp'", + }), + tool: Type.String({ + description: "Tool name on that server, e.g. 'railway_list_projects'", + }), + args: Type.Optional( + Type.Object( + {}, + { + additionalProperties: true, + description: + "Tool arguments as key-value pairs matching the tool's input schema", + }, + ), + ), + }), + async execute(_id, params, signal) { + try { + const client = await manager.getOrConnect(params.server, signal); + const result = await client.callTool( + { name: params.tool, arguments: params.args ?? {} }, + undefined, + { signal, timeout: 60000 }, + ); + const contentItems = result.content; + const raw = contentItems + .map((c) => (c.type === "text" ? (c.text ?? "") : JSON.stringify(c))) + .join("\n"); + const truncation = truncateHead(raw, { + maxLines: DEFAULT_MAX_LINES, + maxBytes: DEFAULT_MAX_BYTES, + }); + let finalText = truncation.content; + if (truncation.truncated) { + finalText += `\n\n[Output truncated: ${truncation.outputLines}/${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`; + } + return { + content: [{ type: "text", text: finalText }], + details: { + server: params.server, + tool: params.tool, + charCount: finalText.length, + truncated: truncation.truncated, + }, + }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + throw new Error( + `MCP call failed: ${params.server}.${params.tool}\n${msg}`, + ); + } + }, + renderCall(args, theme) { + let text = theme.fg("toolTitle", theme.bold("mcp_call ")); + text += theme.fg("accent", `${args.server}.${args.tool}`); + if (args.args && Object.keys(args.args).length > 0) { + const preview = Object.entries(args.args) + .slice(0, 3) + .map(([k, v]) => { + const val = typeof v === "string" ? v : JSON.stringify(v); + return `${k}:${val.length > 30 ? val.slice(0, 30) + "…" : val}`; + }) + .join(" "); + text += " " + theme.fg("muted", preview); + } + return new Text(text, 0, 0); + }, + renderResult(result, { isPartial, expanded }, theme) { + if (isPartial) + return new Text(theme.fg("warning", "Calling MCP tool..."), 0, 0); + const d = result.details; + let text = theme.fg("success", `✓ ${d?.server}.${d?.tool}`); + text += theme.fg( + "dim", + ` · ${(d?.charCount ?? 0).toLocaleString()} chars`, + ); + if (d?.truncated) text += theme.fg("warning", " · truncated"); + if (expanded) { + const content = result.content[0]; + if (content?.type === "text") { + const preview = content.text.split("\n").slice(0, 15).join("\n"); + text += "\n\n" + theme.fg("dim", preview); + } + } + return new Text(text, 0, 0); + }, + }); -// ── Lifecycle ───────────────────────────────────────────────────────────── -pi.on("session_start", async (_event, ctx) => { -const servers = manager.readConfigs(); -if (servers.length > 0) { -ctx.ui.notify( -`MCP client ready — ${servers.length} server(s) configured`, -"info", -); -} -}); -pi.on("session_shutdown", async () => { -await manager.closeAll(); -}); -pi.on("session_switch", async () => { -await manager.closeAll(); -manager.invalidateConfigCache(); -}); + // ── Lifecycle ───────────────────────────────────────────────────────────── + pi.on("session_start", async (_event, ctx) => { + const servers = manager.readConfigs(); + if (servers.length > 0) { + ctx.ui.notify( + `MCP client ready — ${servers.length} server(s) configured`, + "info", + ); + } + }); + pi.on("session_shutdown", async () => { + await manager.closeAll(); + }); + pi.on("session_switch", async () => { + await manager.closeAll(); + manager.invalidateConfigCache(); + }); } diff --git a/src/resources/extensions/search-the-web/native-search.js b/src/resources/extensions/search-the-web/native-search.js index c7f66d6ad..4e5f884d6 100644 --- a/src/resources/extensions/search-the-web/native-search.js +++ b/src/resources/extensions/search-the-web/native-search.js @@ -115,7 +115,9 @@ export function registerNativeSearchHooks(pi) { pi.on("before_provider_request", async (event, _ctx) => { let modelHint = event.model; if (!modelHint && isAnthropicProvider !== null) { - modelHint = { provider: isAnthropicProvider ? "anthropic" : "not-anthropic" }; + modelHint = { + provider: isAnthropicProvider ? "anthropic" : "not-anthropic", + }; } return webSearchMiddleware.applyToPayload(event.payload, modelHint); }); diff --git a/src/resources/extensions/search-the-web/provider.js b/src/resources/extensions/search-the-web/provider.js index b429597da..505d175de 100644 --- a/src/resources/extensions/search-the-web/provider.js +++ b/src/resources/extensions/search-the-web/provider.js @@ -111,11 +111,11 @@ export function setSearchProviderPreference(pref, authPath) { * Each entry's `getKey` is evaluated at resolution time so hot-reloaded env vars work. */ const PROVIDER_REGISTRY = [ - { name: "tavily", getKey: getTavilyApiKey }, - { name: "brave", getKey: getBraveApiKey }, - { name: "serper", getKey: getSerperApiKey }, - { name: "exa", getKey: getExaApiKey }, - { name: "ollama", getKey: getOllamaApiKey }, + { name: "tavily", getKey: getTavilyApiKey }, + { name: "brave", getKey: getBraveApiKey }, + { name: "serper", getKey: getSerperApiKey }, + { name: "exa", getKey: getExaApiKey }, + { name: "ollama", getKey: getOllamaApiKey }, { name: "minimax", getKey: getMiniMaxSearchApiKey }, ]; @@ -145,9 +145,11 @@ export function resolveSearchProvider(overridePreference) { /** Returns pref if its key exists, otherwise walks registry in order. */ const resolveWithFallback = (pref) => { if (hasKey(pref)) return pref; - return PROVIDER_REGISTRY - .filter((p) => p.name !== pref) - .find((p) => p.getKey().length > 0)?.name ?? null; + return ( + PROVIDER_REGISTRY.filter((p) => p.name !== pref).find( + (p) => p.getKey().length > 0, + )?.name ?? null + ); }; // Determine effective preference diff --git a/src/resources/extensions/sf/auto-dispatch.js b/src/resources/extensions/sf/auto-dispatch.js index 83fdcd47b..3b90b51e1 100644 --- a/src/resources/extensions/sf/auto-dispatch.js +++ b/src/resources/extensions/sf/auto-dispatch.js @@ -6,15 +6,15 @@ * continue to work without changes. */ export { -DISPATCH_RULES, -enhanceUnitRankingWithMemory, -extractValidationAttentionPlan, -formatTaskCompleteFailurePrompt, -getDispatchRuleNames, -getRewriteCount, -getUatCount, -incrementUatCount, -isVerificationNotApplicable, -resolveDispatch, -setRewriteCount, + DISPATCH_RULES, + enhanceUnitRankingWithMemory, + extractValidationAttentionPlan, + formatTaskCompleteFailurePrompt, + getDispatchRuleNames, + getRewriteCount, + getUatCount, + incrementUatCount, + isVerificationNotApplicable, + resolveDispatch, + setRewriteCount, } from "./uok/auto-dispatch.js"; diff --git a/src/resources/extensions/sf/auto-post-unit.js b/src/resources/extensions/sf/auto-post-unit.js index 08666dc92..62be26013 100644 --- a/src/resources/extensions/sf/auto-post-unit.js +++ b/src/resources/extensions/sf/auto-post-unit.js @@ -19,7 +19,6 @@ import { writeBlockerPlaceholder, } from "./auto-recovery.js"; import { isDeterministicPolicyError } from "./auto-tool-tracking.js"; -import { closeoutUnit } from "./uok/auto-unit-closeout.js"; import { runSafely } from "./auto-utils.js"; import { syncStateToProjectRoot } from "./auto-worktree.js"; import { invalidateAllCaches } from "./cache.js"; @@ -82,6 +81,7 @@ import { } from "./sf-db.js"; import { deriveState } from "./state.js"; import { parseUnitId } from "./unit-id.js"; +import { closeoutUnit } from "./uok/auto-unit-closeout.js"; import { resolveUokFlags } from "./uok/flags.js"; import { UokGateRunner } from "./uok/gate-runner.js"; import { diff --git a/src/resources/extensions/sf/auto-runaway-guard.js b/src/resources/extensions/sf/auto-runaway-guard.js index c509f0f16..99f3575b5 100644 --- a/src/resources/extensions/sf/auto-runaway-guard.js +++ b/src/resources/extensions/sf/auto-runaway-guard.js @@ -6,17 +6,17 @@ * continue to work without changes. */ export { -clearRunawayGuardState, -collectSessionTokenUsage, -collectWorktreeFingerprint, -countChangedFiles, -DEFAULT_RUNAWAY_CHANGED_FILES_WARNING, -DEFAULT_RUNAWAY_DIAGNOSTIC_TURNS, -DEFAULT_RUNAWAY_ELAPSED_MINUTES, -DEFAULT_RUNAWAY_MIN_INTERVAL_MS, -DEFAULT_RUNAWAY_TOOL_CALL_WARNING, -DEFAULT_RUNAWAY_TOKEN_WARNING, -evaluateRunawayGuard, -resetRunawayGuardState, -resolveRunawayGuardConfig, + clearRunawayGuardState, + collectSessionTokenUsage, + collectWorktreeFingerprint, + countChangedFiles, + DEFAULT_RUNAWAY_CHANGED_FILES_WARNING, + DEFAULT_RUNAWAY_DIAGNOSTIC_TURNS, + DEFAULT_RUNAWAY_ELAPSED_MINUTES, + DEFAULT_RUNAWAY_MIN_INTERVAL_MS, + DEFAULT_RUNAWAY_TOKEN_WARNING, + DEFAULT_RUNAWAY_TOOL_CALL_WARNING, + evaluateRunawayGuard, + resetRunawayGuardState, + resolveRunawayGuardConfig, } from "./uok/auto-runaway-guard.js"; diff --git a/src/resources/extensions/sf/auto-timers.js b/src/resources/extensions/sf/auto-timers.js index 75b3b9ba1..bb3bb292a 100644 --- a/src/resources/extensions/sf/auto-timers.js +++ b/src/resources/extensions/sf/auto-timers.js @@ -7,13 +7,6 @@ */ import { saveActivityLog } from "./activity-log.js"; import { resolveAgentEndCancelled } from "./auto/resolve.js"; -import { - collectSessionTokenUsage, - collectWorktreeFingerprint, - countChangedFiles, - evaluateRunawayGuard, - resolveRunawayGuardConfig, -} from "./uok/auto-runaway-guard.js"; import { detectWorkingTreeActivity } from "./auto-supervisor.js"; import { recoverTimedOutUnit } from "./auto-timeout-recovery.js"; import { @@ -24,7 +17,6 @@ import { getTotalToolCallCount, hasInteractiveToolInFlight, } from "./auto-tool-tracking.js"; -import { closeoutUnit } from "./uok/auto-unit-closeout.js"; import { computeBudgets, resolveExecutorContextWindow, @@ -33,6 +25,14 @@ import { resolveAutoSupervisorConfig } from "./preferences.js"; import { writeRunawayRecoveryArtifact } from "./runaway-recovery.js"; import { recordSelfFeedback } from "./self-feedback.js"; import { getMilestoneSlices, getSliceTasks, isDbAvailable } from "./sf-db.js"; +import { + collectSessionTokenUsage, + collectWorktreeFingerprint, + countChangedFiles, + evaluateRunawayGuard, + resolveRunawayGuardConfig, +} from "./uok/auto-runaway-guard.js"; +import { closeoutUnit } from "./uok/auto-unit-closeout.js"; import { readUnitRuntimeRecord, writeUnitRuntimeRecord, diff --git a/src/resources/extensions/sf/auto-worktree.js b/src/resources/extensions/sf/auto-worktree.js index be0ba214c..6ec16281d 100644 --- a/src/resources/extensions/sf/auto-worktree.js +++ b/src/resources/extensions/sf/auto-worktree.js @@ -552,7 +552,9 @@ export function syncSfStateToWorktree(mainBasePath, worktreePath_) { } // Forward-sync project preferences from project root to worktree (additive only). { - const worktreeHasPreferences = existsSync(join(wtSf, PROJECT_PREFERENCES_FILE)); + const worktreeHasPreferences = existsSync( + join(wtSf, PROJECT_PREFERENCES_FILE), + ); if (!worktreeHasPreferences) { const src = join(mainSf, PROJECT_PREFERENCES_FILE); const dst = join(wtSf, PROJECT_PREFERENCES_FILE); @@ -567,7 +569,7 @@ export function syncSfStateToWorktree(mainBasePath, worktreePath_) { `preferences copy failed (${PROJECT_PREFERENCES_FILE}): ${err instanceof Error ? err.message : String(err)}`, ); } - } + } } } // Sync milestones: copy entire milestone directories that are missing diff --git a/src/resources/extensions/sf/auto.js b/src/resources/extensions/sf/auto.js index c29352ca5..a668deb6b 100644 --- a/src/resources/extensions/sf/auto.js +++ b/src/resources/extensions/sf/auto.js @@ -52,7 +52,6 @@ import { clearSliceProgressCache, updateSliceProgressCache, } from "./auto-dashboard.js"; -import { DISPATCH_RULES, resolveDispatch } from "./uok/auto-dispatch.js"; import { _resetPendingResolve, isSessionSwitchInFlight, @@ -86,8 +85,6 @@ import { isQueuedUserMessageSkip, isToolInvocationError, } from "./auto-tool-tracking.js"; -import { closeoutUnit } from "./uok/auto-unit-closeout.js"; -import { runPostUnitVerification } from "./uok/auto-verification.js"; import { autoWorktreeBranch, checkResourcesStale, @@ -174,6 +171,9 @@ import { captureAvailableSkills, resetSkillTelemetry, } from "./skill-telemetry.js"; +import { DISPATCH_RULES, resolveDispatch } from "./uok/auto-dispatch.js"; +import { closeoutUnit } from "./uok/auto-unit-closeout.js"; +import { runPostUnitVerification } from "./uok/auto-verification.js"; import { writeUokDiagnostics } from "./uok/diagnostic-synthesis.js"; import { resolveUokFlags } from "./uok/flags.js"; import { diff --git a/src/resources/extensions/sf/auto/phases.js b/src/resources/extensions/sf/auto/phases.js index 8d73512b5..446c7e086 100644 --- a/src/resources/extensions/sf/auto/phases.js +++ b/src/resources/extensions/sf/auto/phases.js @@ -24,12 +24,6 @@ import { diagnoseExpectedArtifact, verifyExpectedArtifact, } from "../auto-recovery.js"; -import { - collectSessionTokenUsage, - collectWorktreeFingerprint, - countChangedFiles, - resetRunawayGuardState, -} from "../uok/auto-runaway-guard.js"; import { formatToolCallSummary, resetToolCallCounts, @@ -87,6 +81,12 @@ import { getEligibleSlices } from "../slice-parallel-eligibility.js"; import { startSliceParallel } from "../slice-parallel-orchestrator.js"; import { handleProductAudit } from "../tools/product-audit-tool.js"; import { parseUnitId } from "../unit-id.js"; +import { + collectSessionTokenUsage, + collectWorktreeFingerprint, + countChangedFiles, + resetRunawayGuardState, +} from "../uok/auto-runaway-guard.js"; import { resolveUokFlags } from "../uok/flags.js"; import { UokGateRunner } from "../uok/gate-runner.js"; import { emitModelAutoResolvedEvent } from "../uok/model-route-evidence.js"; diff --git a/src/resources/extensions/sf/auto/run-unit.js b/src/resources/extensions/sf/auto/run-unit.js index 5a759dacc..3becec295 100644 --- a/src/resources/extensions/sf/auto/run-unit.js +++ b/src/resources/extensions/sf/auto/run-unit.js @@ -3,18 +3,19 @@ * * Imports from: auto/types, auto/resolve */ -import { - collectSessionTokenUsage, - collectWorktreeFingerprint, - countChangedFiles, - resetRunawayGuardState, -} from "../uok/auto-runaway-guard.js"; + import { scopeActiveToolsForUnitType } from "../constants.js"; import { debugLog } from "../debug-logger.js"; import { resolveAutoSupervisorConfig, resolvePersistModelChanges, } from "../preferences.js"; +import { + collectSessionTokenUsage, + collectWorktreeFingerprint, + countChangedFiles, + resetRunawayGuardState, +} from "../uok/auto-runaway-guard.js"; import { logWarning } from "../workflow-logger.js"; import { _clearCurrentResolve, diff --git a/src/resources/extensions/sf/bootstrap/db-tools.js b/src/resources/extensions/sf/bootstrap/db-tools.js index 95f422c7f..7cccc8297 100644 --- a/src/resources/extensions/sf/bootstrap/db-tools.js +++ b/src/resources/extensions/sf/bootstrap/db-tools.js @@ -142,7 +142,7 @@ export function registerDbTools(pi) { const message = d?.reason ?? textContent ?? d?.error ?? "unknown"; return new Text(theme.fg("error", `Error: ${message}`), 0, 0); } - let text = theme.fg("success", `Decision ${d?.id ?? ""} saved`); + const text = theme.fg("success", `Decision ${d?.id ?? ""} saved`); return new Text(text, 0, 0); }, }; @@ -211,7 +211,8 @@ export function registerDbTools(pi) { description: "Update an existing requirement by ID and return confirmation — only fields you provide are changed. " + "Call this when a requirement's status, validation evidence, description, or owning slice changes after it was first recorded.", - promptSnippet: "Update an existing requirement by ID (only provided fields are changed)", + promptSnippet: + "Update an existing requirement by ID (only provided fields are changed)", promptGuidelines: [ "id is required and must be an existing requirement identifier (e.g. R001).", "All other fields are optional — only the fields you provide are updated.", @@ -257,7 +258,7 @@ export function registerDbTools(pi) { 0, ); } - let text = theme.fg("success", `Requirement ${d?.id ?? ""} updated`); + const text = theme.fg("success", `Requirement ${d?.id ?? ""} updated`); return new Text(text, 0, 0); }, }; @@ -323,8 +324,7 @@ export function registerDbTools(pi) { description: "Record a new requirement and return its auto-assigned ID (e.g. R001). " + "Call this when a functional, non-functional, or operational requirement is identified that the project must satisfy.", - promptSnippet: - "Record a new requirement (auto-assigns ID, persists to DB)", + promptSnippet: "Record a new requirement (auto-assigns ID, persists to DB)", promptGuidelines: [ "Requirement IDs are auto-assigned — never guess or provide one.", "class, description, why, and source are required; all other fields are optional.", @@ -373,7 +373,7 @@ export function registerDbTools(pi) { 0, ); } - let text = theme.fg("success", `Requirement ${d?.id ?? ""} saved`); + const text = theme.fg("success", `Requirement ${d?.id ?? ""} saved`); return new Text(text, 0, 0); }, }; @@ -747,7 +747,12 @@ export function registerDbTools(pi) { const { insertMemoryRow } = await import("../sf-db.js"); const id = `K-${randomUUID()}`; const now = new Date().toISOString(); - const confidenceScore = params.confidence === "medium" ? 0.6 : params.confidence === "low" ? 0.3 : 0.9; + const confidenceScore = + params.confidence === "medium" + ? 0.6 + : params.confidence === "low" + ? 0.3 + : 0.9; insertMemoryRow({ id, category: params.category ?? "knowledge", @@ -791,11 +796,13 @@ export function registerDbTools(pi) { ], parameters: Type.Object({ content: Type.String({ - description: "The knowledge insight to persist — complete and self-contained", + description: + "The knowledge insight to persist — complete and self-contained", }), category: Type.Optional( Type.String({ - description: "Category (default: 'knowledge'). E.g. 'architecture', 'tooling', 'process'", + description: + "Category (default: 'knowledge'). E.g. 'architecture', 'tooling', 'process'", }), ), confidence: Type.Optional( @@ -805,7 +812,8 @@ export function registerDbTools(pi) { ), tags: Type.Optional( Type.Array(Type.String(), { - description: "Optional string tags for filtering (e.g. ['mcp', 'transport'])", + description: + "Optional string tags for filtering (e.g. ['mcp', 'transport'])", }), ), }), @@ -814,13 +822,18 @@ export function registerDbTools(pi) { let text = theme.fg("toolTitle", theme.bold("save_knowledge ")); if (args.category) text += theme.fg("accent", `[${args.category}] `); if (args.content) - text += theme.fg("muted", args.content.slice(0, 60) + (args.content.length > 60 ? "…" : "")); + text += theme.fg( + "muted", + args.content.slice(0, 60) + (args.content.length > 60 ? "…" : ""), + ); return new Text(text, 0, 0); }, renderResult(result, _options, theme) { const d = result.details; if (result.isError || d?.error) { - const textContent = result.content?.find?.((item) => item?.type === "text")?.text; + const textContent = result.content?.find?.( + (item) => item?.type === "text", + )?.text; const message = d?.reason ?? textContent ?? d?.error ?? "unknown"; return new Text(theme.fg("error", `Error: ${message}`), 0, 0); } @@ -2688,7 +2701,8 @@ export function registerDbTools(pi) { description: "Record the agent's intent at the start of a work block so crash-resume can surface it. " + "Call at the top of each autonomous work block with a clear one-sentence intent.", - promptSnippet: "Open an intent chapter before starting a significant block of work", + promptSnippet: + "Open an intent chapter before starting a significant block of work", promptGuidelines: [ "Call chapter_open before starting any significant work block (a task, a multi-step investigation, a refactor).", "Keep the intent concise — one sentence stating what you are about to accomplish.", @@ -2705,11 +2719,13 @@ export function registerDbTools(pi) { }, unit_type: { type: "string", - description: "UOK unit type (e.g. 'execute-task', 'plan-slice'). Optional — defaults to current unit.", + description: + "UOK unit type (e.g. 'execute-task', 'plan-slice'). Optional — defaults to current unit.", }, unit_id: { type: "string", - description: "UOK unit ID (e.g. 'M001/S01/T02'). Optional — defaults to current unit.", + description: + "UOK unit ID (e.g. 'M001/S01/T02'). Optional — defaults to current unit.", }, }, required: ["intent"], @@ -2718,7 +2734,12 @@ export function registerDbTools(pi) { const dbAvailable = await ensureDbOpen(); if (!dbAvailable) { return { - content: [{ type: "text", text: "Error: SF database unavailable — chapter not opened." }], + content: [ + { + type: "text", + text: "Error: SF database unavailable — chapter not opened.", + }, + ], details: { operation: "chapter_open", error: "db_unavailable" }, }; } @@ -2750,8 +2771,17 @@ export function registerDbTools(pi) { }, renderToolResult: (d, theme) => { const { Text } = theme; - if (d?.error) return new Text(theme.fg("error", `chapter_open error: ${d.error}`), 0, 0); - return new Text(theme.fg("success", `chapter opened: ${d?.id ?? ""}`), 0, 0); + if (d?.error) + return new Text( + theme.fg("error", `chapter_open error: ${d.error}`), + 0, + 0, + ); + return new Text( + theme.fg("success", `chapter opened: ${d?.id ?? ""}`), + 0, + 0, + ); }, }); @@ -2785,7 +2815,12 @@ export function registerDbTools(pi) { const dbAvailable = await ensureDbOpen(); if (!dbAvailable) { return { - content: [{ type: "text", text: "Error: SF database unavailable — chapter not closed." }], + content: [ + { + type: "text", + text: "Error: SF database unavailable — chapter not closed.", + }, + ], details: { operation: "chapter_close", error: "db_unavailable" }, }; } @@ -2793,7 +2828,14 @@ export function registerDbTools(pi) { const { closeIntentChapter } = await import("../sf-db.js"); const closed = closeIntentChapter(params.id, params.outcome); return { - content: [{ type: "text", text: closed ? `Chapter ${params.id} closed (${params.outcome}).` : `Chapter ${params.id} not found or already closed.` }], + content: [ + { + type: "text", + text: closed + ? `Chapter ${params.id} closed (${params.outcome}).` + : `Chapter ${params.id} not found or already closed.`, + }, + ], details: { operation: "chapter_close", id: params.id, closed }, }; } catch (err) { @@ -2810,8 +2852,17 @@ export function registerDbTools(pi) { }, renderToolResult: (d, theme) => { const { Text } = theme; - if (d?.error) return new Text(theme.fg("error", `chapter_close error: ${d.error}`), 0, 0); - return new Text(theme.fg("success", `chapter closed: ${d?.id ?? ""}`), 0, 0); + if (d?.error) + return new Text( + theme.fg("error", `chapter_close error: ${d.error}`), + 0, + 0, + ); + return new Text( + theme.fg("success", `chapter closed: ${d?.id ?? ""}`), + 0, + 0, + ); }, }); } diff --git a/src/resources/extensions/sf/bootstrap/register-hooks.js b/src/resources/extensions/sf/bootstrap/register-hooks.js index 4f0affaf8..7873ee50f 100644 --- a/src/resources/extensions/sf/bootstrap/register-hooks.js +++ b/src/resources/extensions/sf/bootstrap/register-hooks.js @@ -8,9 +8,11 @@ import { writeFileSync, } from "node:fs"; import { join, relative, resolve } from "node:path"; -import { isToolCallEventType } from "@singularity-forge/coding-agent"; +import { + formatTokenCount, + isToolCallEventType, +} from "@singularity-forge/coding-agent"; import { resetAskUserQuestionsCache } from "../../ask-user-questions.js"; -import { formatTokenCount } from "@singularity-forge/coding-agent"; import { saveActivityLog } from "../activity-log.js"; import { getAutoDashboardData, @@ -46,11 +48,7 @@ import { resetLearningRuntime, selectLearnedModel, } from "../learning/runtime.js"; -import { - observeToolResult, - resetToolWatchdog, -} from "../tool-watchdog.js"; -import { NOTICE_KIND, initNotificationStore } from "../notification-store.js"; +import { initNotificationStore, NOTICE_KIND } from "../notification-store.js"; import { initNotificationWidget } from "../notification-widget.js"; import { isParallelActive, @@ -77,6 +75,7 @@ import { import { initSessionRecorder } from "../session-recorder.js"; import { deriveState } from "../state.js"; import { countGoogleGeminiCliTokens } from "../token-counter.js"; +import { observeToolResult, resetToolWatchdog } from "../tool-watchdog.js"; import { getSessionTodoCompactionBlock } from "../tools/session-todo-tool.js"; import { parseUnitId } from "../unit-id.js"; import { logWarning as safetyLogWarning } from "../workflow-logger.js"; diff --git a/src/resources/extensions/sf/bootstrap/system-context.js b/src/resources/extensions/sf/bootstrap/system-context.js index b173ffd4a..0a56e5f8c 100644 --- a/src/resources/extensions/sf/bootstrap/system-context.js +++ b/src/resources/extensions/sf/bootstrap/system-context.js @@ -45,6 +45,11 @@ import { resolveModelWithFallbacksForUnit } from "../preferences-models.js"; import { resolveSkillReference } from "../preferences-skills.js"; import { getTemplatesDir, loadPrompt } from "../prompt-loader.js"; import { buildRepositoryVcsContextBlock } from "../repository-vcs-context.js"; +import { + getActiveMemories, + isDbAvailable, + listSelfFeedbackEntries, +} from "../sf-db.js"; import { detectNewSkills, formatSkillsXml, @@ -52,11 +57,6 @@ import { } from "../skill-discovery.js"; import { deriveState } from "../state.js"; import { logWarning } from "../workflow-logger.js"; -import { - getActiveMemories, - isDbAvailable, - listSelfFeedbackEntries, -} from "../sf-db.js"; import { getActiveWorktreeName, getWorktreeOriginalCwd, diff --git a/src/resources/extensions/sf/commands-agent.js b/src/resources/extensions/sf/commands-agent.js index 1904c80cf..a3b3640dd 100644 --- a/src/resources/extensions/sf/commands-agent.js +++ b/src/resources/extensions/sf/commands-agent.js @@ -8,10 +8,10 @@ * Consumer: ops.js dispatcher for the /agent slash command. */ -import { getDatabase, openDatabase } from "./sf-db.js"; -import { sfRoot } from "./paths.js"; import { mkdirSync } from "node:fs"; import { join } from "node:path"; +import { sfRoot } from "./paths.js"; +import { getDatabase, openDatabase } from "./sf-db.js"; import { UokCoordinationStore } from "./uok/coordination-store.js"; const USAGE = `Usage: /agent @@ -135,10 +135,7 @@ async function handleAgentInspect(store, name, ctx) { const identityKey = `agent:${name}:identity`; const identity = store.get(identityKey); if (!identity) { - ctx.ui.notify( - `/agent inspect: no agent named "${name}" found.`, - "warning", - ); + ctx.ui.notify(`/agent inspect: no agent named "${name}" found.`, "warning"); return; } @@ -219,10 +216,7 @@ async function handleAgentDelete(store, name, ctx) { const identityKey = `agent:${name}:identity`; const identity = store.get(identityKey); if (!identity) { - ctx.ui.notify( - `/agent delete: no agent named "${name}" found.`, - "warning", - ); + ctx.ui.notify(`/agent delete: no agent named "${name}" found.`, "warning"); return; } diff --git a/src/resources/extensions/sf/commands-memory.js b/src/resources/extensions/sf/commands-memory.js index e7ffd18f6..b63698f65 100644 --- a/src/resources/extensions/sf/commands-memory.js +++ b/src/resources/extensions/sf/commands-memory.js @@ -218,9 +218,7 @@ async function handleSearch(ctx, parsed) { ctx.ui.notify("No matches.", "info"); return; } - const { loadGatewayConfigFromEnv } = await import( - "./memory-embeddings.js" - ); + const { loadGatewayConfigFromEnv } = await import("./memory-embeddings.js"); const gatewayConfig = loadGatewayConfigFromEnv(); const usingEmbeddings = !!gatewayConfig; const usingRerank = !!gatewayConfig?.rerankModel; @@ -418,9 +416,7 @@ async function handleBackfill(ctx) { return; } const before = readMemoryDbStatus(adapter); - const { loadGatewayConfigFromEnv } = await import( - "./memory-embeddings.js" - ); + const { loadGatewayConfigFromEnv } = await import("./memory-embeddings.js"); const gatewayConfig = loadGatewayConfigFromEnv(); if (!gatewayConfig) { ctx.ui.notify( diff --git a/src/resources/extensions/sf/commands-prefs-wizard.js b/src/resources/extensions/sf/commands-prefs-wizard.js index 7bc52b987..092831404 100644 --- a/src/resources/extensions/sf/commands-prefs-wizard.js +++ b/src/resources/extensions/sf/commands-prefs-wizard.js @@ -8,10 +8,7 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { runClaudeImportFlow } from "./claude-import.js"; -import { - loadFile, - saveFile, -} from "./files.js"; +import { loadFile, saveFile } from "./files.js"; import { getGlobalSFPreferencesPath, getProjectSFPreferencesPath, diff --git a/src/resources/extensions/sf/dashboard-overlay.js b/src/resources/extensions/sf/dashboard-overlay.js index 6da3859a9..3a51f3b2e 100644 --- a/src/resources/extensions/sf/dashboard-overlay.js +++ b/src/resources/extensions/sf/dashboard-overlay.js @@ -21,10 +21,6 @@ import { STATUS_COLOR, STATUS_GLYPH, } from "../shared/mod.js"; -import { - getWorkerBatches, - hasActiveWorkers, -} from "./subagent/worker-registry.js"; import { getAutoDashboardData } from "./auto.js"; import { estimateTimeRemaining } from "./auto-dashboard.js"; import { runEnvironmentChecks } from "./doctor-environment.js"; @@ -46,6 +42,10 @@ import { computeProgressScore } from "./progress-score.js"; import { getMilestoneSlices, getSliceTasks, isDbAvailable } from "./sf-db.js"; import { formattedShortcutPair } from "./shortcut-defs.js"; import { deriveState } from "./state.js"; +import { + getWorkerBatches, + hasActiveWorkers, +} from "./subagent/worker-registry.js"; import { writeUokDiagnostics } from "./uok/diagnostic-synthesis.js"; import { getActiveWorktreeName } from "./worktree-command.js"; diff --git a/src/resources/extensions/sf/deep-project-setup-policy.js b/src/resources/extensions/sf/deep-project-setup-policy.js index 86409ce51..6b41b5c00 100644 --- a/src/resources/extensions/sf/deep-project-setup-policy.js +++ b/src/resources/extensions/sf/deep-project-setup-policy.js @@ -20,9 +20,7 @@ export function researchDecisionPath(basePath) { export function isWorkflowPrefsCaptured(basePath) { // Check yaml (canonical) first, fallback to legacy md const sfRootPath = sfRoot(basePath); - const candidates = [ - join(sfRootPath, "preferences.yaml"), - ]; + const candidates = [join(sfRootPath, "preferences.yaml")]; for (const prefsPath of candidates) { if (!existsSync(prefsPath)) continue; let content; @@ -117,14 +115,16 @@ export function resolveDeepProjectSetupState(prefs, basePath) { }; } // DB-first: check for requirements in DB; fall back to file for unmigrated projects - const hasDbRequirements = isDbAvailable() && getActiveRequirements().length > 0; + const hasDbRequirements = + isDbAvailable() && getActiveRequirements().length > 0; if (!hasDbRequirements) { const requirementsPath = join(root, "REQUIREMENTS.md"); if (!existsSync(requirementsPath)) { return { status: "pending", stage: "requirements", - reason: "No requirements found (DB empty and .sf/REQUIREMENTS.md is missing).", + reason: + "No requirements found (DB empty and .sf/REQUIREMENTS.md is missing).", }; } if (!validateArtifact(requirementsPath, "requirements").ok) { diff --git a/src/resources/extensions/sf/detection.js b/src/resources/extensions/sf/detection.js index 677359bc0..41a55d1c0 100644 --- a/src/resources/extensions/sf/detection.js +++ b/src/resources/extensions/sf/detection.js @@ -310,8 +310,7 @@ export function detectV1Planning(basePath) { function detectV2Sf(basePath) { const sfPath = sfRoot(basePath); if (!existsSync(sfPath)) return null; - const hasPreferences = - existsSync(join(sfPath, "preferences.yaml")); + const hasPreferences = existsSync(join(sfPath, "preferences.yaml")); const hasContext = existsSync(join(sfPath, "CONTEXT.md")); let milestoneCount = 0; const milestonesPath = join(sfPath, "milestones"); diff --git a/src/resources/extensions/sf/dev-workflow-engine.js b/src/resources/extensions/sf/dev-workflow-engine.js index ba02d7bff..18b93180a 100644 --- a/src/resources/extensions/sf/dev-workflow-engine.js +++ b/src/resources/extensions/sf/dev-workflow-engine.js @@ -5,9 +5,10 @@ * and dispatch logic. This is the "dev" engine — it wraps the current SF * autonomous mode behavior behind the engine-polymorphic interface. */ -import { resolveDispatch } from "./uok/auto-dispatch.js"; + import { loadEffectiveSFPreferences } from "./preferences.js"; import { deriveState } from "./state.js"; +import { resolveDispatch } from "./uok/auto-dispatch.js"; // ─── Bridge: DispatchAction → EngineDispatchAction ──────────────────────── /** * Map a SF-specific DispatchAction (which carries `matchedRule`, `unitType`, diff --git a/src/resources/extensions/sf/doctor-providers.js b/src/resources/extensions/sf/doctor-providers.js index 3cfabba12..11c4a2dd7 100644 --- a/src/resources/extensions/sf/doctor-providers.js +++ b/src/resources/extensions/sf/doctor-providers.js @@ -14,7 +14,10 @@ import { existsSync } from "node:fs"; import { AuthStorage } from "@singularity-forge/coding-agent"; import { getAuthPath, PROVIDER_REGISTRY } from "./key-manager.js"; import { loadEffectiveSFPreferences } from "./preferences.js"; -import { getConfiguredEnvApiKey, getGatedEnvVarKey } from "./provider-env-auth.js"; +import { + getConfiguredEnvApiKey, + getGatedEnvVarKey, +} from "./provider-env-auth.js"; import { couldBeVaultUri } from "./vault-credential-resolver.js"; // ── Model → Provider ID mapping ─────────────────────────────────────────────── @@ -336,7 +339,9 @@ function checkOptionalProviders() { // "not configured" noise for alternative search providers when at least // one is already active (e.g. don't warn about missing BRAVE_API_KEY // when Tavily is configured). - const searchProviderIds = PROVIDER_REGISTRY.filter((p) => p.category === "search").map((p) => p.id); + const searchProviderIds = PROVIDER_REGISTRY.filter( + (p) => p.category === "search", + ).map((p) => p.id); const hasAnySearchProvider = searchProviderIds.some( (id) => resolveKey(id).found, ); diff --git a/src/resources/extensions/sf/export-html.js b/src/resources/extensions/sf/export-html.js index ca022b82e..fa573d569 100644 --- a/src/resources/extensions/sf/export-html.js +++ b/src/resources/extensions/sf/export-html.js @@ -19,7 +19,10 @@ * * Design: Linear-inspired — restrained palette, geometric status, no emoji. */ -import { formatDateShort, formatDuration } from "@singularity-forge/coding-agent"; +import { + formatDateShort, + formatDuration, +} from "@singularity-forge/coding-agent"; import { formatCost, formatTokenCount } from "./metrics.js"; export function generateHtmlReport(data, opts) { const generated = new Date().toISOString(); diff --git a/src/resources/extensions/sf/extension-manifest.json b/src/resources/extensions/sf/extension-manifest.json index 220e789f1..3cc72f617 100644 --- a/src/resources/extensions/sf/extension-manifest.json +++ b/src/resources/extensions/sf/extension-manifest.json @@ -64,7 +64,6 @@ "backlog", "capture", "chronicle", - "clear", "cleanup", "cmux", "codebase", @@ -204,11 +203,6 @@ "turn_start", "turn_end" ], - "shortcuts": [ - "Ctrl+Alt+G", - "Ctrl+Alt+H", - "Ctrl+Alt+M", - "Ctrl+Shift+H" - ] + "shortcuts": ["Ctrl+Alt+G", "Ctrl+Alt+H", "Ctrl+Alt+M", "Ctrl+Shift+H"] } } diff --git a/src/resources/extensions/sf/guided-flow.js b/src/resources/extensions/sf/guided-flow.js index 9de98842a..e248158f6 100644 --- a/src/resources/extensions/sf/guided-flow.js +++ b/src/resources/extensions/sf/guided-flow.js @@ -70,7 +70,12 @@ import { isSessionLockProcessAlive, readSessionLockData, } from "./session-lock.js"; -import { getAllMilestones, getMilestone, getMilestoneSlices, isDbAvailable } from "./sf-db.js"; +import { + getAllMilestones, + getMilestone, + getMilestoneSlices, + isDbAvailable, +} from "./sf-db.js"; import { deriveState } from "./state.js"; import { resolveUokFlags } from "./uok/flags.js"; import { UokGateRunner } from "./uok/gate-runner.js"; @@ -271,7 +276,7 @@ export function checkAutoStartAfterDiscuss() { if (isDbAvailable()) { const milestone = getMilestone(milestoneId); const dbSlices = getMilestoneSlices(milestoneId); - const hasContext = !!(milestone?.vision); + const hasContext = !!milestone?.vision; const hasRoadmap = dbSlices.length > 0; if (!hasContext && !hasRoadmap) return false; // Gate 3: Multi-milestone completeness warning (DB version) diff --git a/src/resources/extensions/sf/index.js b/src/resources/extensions/sf/index.js index 6d1000f1f..78122ff56 100644 --- a/src/resources/extensions/sf/index.js +++ b/src/resources/extensions/sf/index.js @@ -44,7 +44,9 @@ export default async function registerExtension(pi) { // Register SF-owned subagent tools after core bootstrap so model, preference, // inheritance, and retrieval-evidence integrations share one extension owner. try { - const { default: registerSFSubagents } = await import("./subagent/index.js"); + const { default: registerSFSubagents } = await import( + "./subagent/index.js" + ); registerSFSubagents(pi); } catch (err) { const { logWarning } = await import("./workflow-logger.js"); @@ -82,7 +84,9 @@ export default async function registerExtension(pi) { // Register SF notifications (completion beep/say/focus/threshold commands). try { - const { default: registerSFNotify } = await import("./notifications/notify.js"); + const { default: registerSFNotify } = await import( + "./notifications/notify.js" + ); registerSFNotify(pi); } catch (err) { const { logWarning } = await import("./workflow-logger.js"); @@ -94,7 +98,9 @@ export default async function registerExtension(pi) { // Register SF in-turn guard (duplicate tool-call loop detection). try { - const { default: registerSFInturnGuard } = await import("./guards/inturn.js"); + const { default: registerSFInturnGuard } = await import( + "./guards/inturn.js" + ); registerSFInturnGuard(pi); } catch (err) { const { logWarning } = await import("./workflow-logger.js"); @@ -106,7 +112,9 @@ export default async function registerExtension(pi) { // Register SF permissions (layered permission enforcement). try { - const { default: registerSFPermissions } = await import("./permissions/index.js"); + const { default: registerSFPermissions } = await import( + "./permissions/index.js" + ); registerSFPermissions(pi); } catch (err) { const { logWarning } = await import("./workflow-logger.js"); @@ -118,7 +126,9 @@ export default async function registerExtension(pi) { // Register SF legacy slash commands (/audit, /clear, /create-extension, /create-slash-command). try { - const { default: registerSFLegacyCommands } = await import("./commands/legacy/index.js"); + const { default: registerSFLegacyCommands } = await import( + "./commands/legacy/index.js" + ); registerSFLegacyCommands(pi); } catch (err) { const { logWarning } = await import("./workflow-logger.js"); diff --git a/src/resources/extensions/sf/init-wizard.js b/src/resources/extensions/sf/init-wizard.js index 301966e4e..27ce4c156 100644 --- a/src/resources/extensions/sf/init-wizard.js +++ b/src/resources/extensions/sf/init-wizard.js @@ -519,7 +519,9 @@ function bootstrapSfDirectory(basePath, prefs, signals) { ensureSiftIndexWarmup(basePath); } function buildPreferencesFile(prefs) { - const lines = ["# SF preferences — see ~/.sf/agent/extensions/sf/docs/preferences-reference.md for docs"]; + const lines = [ + "# SF preferences — see ~/.sf/agent/extensions/sf/docs/preferences-reference.md for docs", + ]; lines.push("version: 1"); lines.push(`mode: ${prefs.mode}`); // Git preferences diff --git a/src/resources/extensions/sf/key-manager.js b/src/resources/extensions/sf/key-manager.js index e0884ee70..c5007fe8e 100644 --- a/src/resources/extensions/sf/key-manager.js +++ b/src/resources/extensions/sf/key-manager.js @@ -292,7 +292,9 @@ export function getAllKeyStatuses(auth) { const rawCreds = auth.getCredentialsForProvider(provider.id); // Filter out empty-key entries (left by legacy removeProviderToken or skipped onboarding) const creds = rawCreds.filter((c) => !(c.type === "api_key" && !c.key)); - const envKey = isEnvAuthAllowed(provider.id) ? getProviderEnvKey(provider) : undefined; + const envKey = isEnvAuthAllowed(provider.id) + ? getProviderEnvKey(provider) + : undefined; if (creds.length > 0) { const firstCred = creds[0]; const desc = diff --git a/src/resources/extensions/sf/knowledge-compounding.js b/src/resources/extensions/sf/knowledge-compounding.js index fade4a555..d57f530ae 100644 --- a/src/resources/extensions/sf/knowledge-compounding.js +++ b/src/resources/extensions/sf/knowledge-compounding.js @@ -12,7 +12,7 @@ */ import { randomUUID } from "node:crypto"; import { readJudgmentLog } from "./judgment-log.js"; -import { insertMemoryRow, isDbAvailable, getActiveMemories } from "./sf-db.js"; +import { getActiveMemories, insertMemoryRow, isDbAvailable } from "./sf-db.js"; /** Map judgment-log string confidence to a numeric score for the REAL column. */ function confidenceScore(s) { diff --git a/src/resources/extensions/sf/learning/bayesian-blender.mjs b/src/resources/extensions/sf/learning/bayesian-blender.mjs index d8d232b6b..b9ff625a8 100644 --- a/src/resources/extensions/sf/learning/bayesian-blender.mjs +++ b/src/resources/extensions/sf/learning/bayesian-blender.mjs @@ -168,7 +168,11 @@ export function computeObservedScore( const sampleCount = stats.sample_count ?? 0; const hardFailureCount = stats.hard_failure_count ?? 0; - if (hardFailureCount > 0 && sampleCount > 0 && hardFailureCount / sampleCount > 0.5) { + if ( + hardFailureCount > 0 && + sampleCount > 0 && + hardFailureCount / sampleCount > 0.5 + ) { scaled *= 0.5; } diff --git a/src/resources/extensions/sf/learning/outcome-recorder.test.mjs b/src/resources/extensions/sf/learning/outcome-recorder.test.mjs index 5586d20f0..94e89d719 100644 --- a/src/resources/extensions/sf/learning/outcome-recorder.test.mjs +++ b/src/resources/extensions/sf/learning/outcome-recorder.test.mjs @@ -9,6 +9,7 @@ import assert from "node:assert/strict"; import { test } from "vitest"; +import { computeObservedScore } from "./bayesian-blender.mjs"; import { aggregateAllForUnitType, aggregateOutcomes, @@ -20,7 +21,6 @@ import { recordOutcomeBatch, validateOutcome, } from "./outcome-recorder.mjs"; -import { computeObservedScore } from "./bayesian-blender.mjs"; // --------------------------------------------------------------------------- // Minimal in-memory fake of the SQLite surface consumed by sf-learning. @@ -214,7 +214,8 @@ function summarize(rows) { rows.reduce((sum, r) => sum + effectiveWeight(r), 0) / rows.length; const hard_failure_count = rows.filter( - (r) => r.failure_mode === "quota_exhausted" || r.failure_mode === "auth_error", + (r) => + r.failure_mode === "quota_exhausted" || r.failure_mode === "auth_error", ).length; return { @@ -616,17 +617,88 @@ test("aggregateOutcomes_rate_limit_failures_rank_higher_than_quota_exhausted", ( // model with only rate_limit failures (weight 0.7) dbRateLimit._rows.push( - { id: 1, model_id: "model-x", provider: "p", unit_type: "execute-task", unit_id: "T01", succeeded: 0, failure_mode: "rate_limit", retries: 0, escalated: 0, verification_passed: null, blocker_discovered: 0, duration_ms: 100, tokens_total: 10, cost_usd: 0, recorded_at: now - 1000 }, - { id: 2, model_id: "model-x", provider: "p", unit_type: "execute-task", unit_id: "T02", succeeded: 0, failure_mode: "rate_limit", retries: 0, escalated: 0, verification_passed: null, blocker_discovered: 0, duration_ms: 100, tokens_total: 10, cost_usd: 0, recorded_at: now - 2000 }, + { + id: 1, + model_id: "model-x", + provider: "p", + unit_type: "execute-task", + unit_id: "T01", + succeeded: 0, + failure_mode: "rate_limit", + retries: 0, + escalated: 0, + verification_passed: null, + blocker_discovered: 0, + duration_ms: 100, + tokens_total: 10, + cost_usd: 0, + recorded_at: now - 1000, + }, + { + id: 2, + model_id: "model-x", + provider: "p", + unit_type: "execute-task", + unit_id: "T02", + succeeded: 0, + failure_mode: "rate_limit", + retries: 0, + escalated: 0, + verification_passed: null, + blocker_discovered: 0, + duration_ms: 100, + tokens_total: 10, + cost_usd: 0, + recorded_at: now - 2000, + }, ); // model with only quota_exhausted failures (weight 0.2) dbQuota._rows.push( - { id: 1, model_id: "model-x", provider: "p", unit_type: "execute-task", unit_id: "T01", succeeded: 0, failure_mode: "quota_exhausted", retries: 0, escalated: 0, verification_passed: null, blocker_discovered: 0, duration_ms: 100, tokens_total: 10, cost_usd: 0, recorded_at: now - 1000 }, - { id: 2, model_id: "model-x", provider: "p", unit_type: "execute-task", unit_id: "T02", succeeded: 0, failure_mode: "quota_exhausted", retries: 0, escalated: 0, verification_passed: null, blocker_discovered: 0, duration_ms: 100, tokens_total: 10, cost_usd: 0, recorded_at: now - 2000 }, + { + id: 1, + model_id: "model-x", + provider: "p", + unit_type: "execute-task", + unit_id: "T01", + succeeded: 0, + failure_mode: "quota_exhausted", + retries: 0, + escalated: 0, + verification_passed: null, + blocker_discovered: 0, + duration_ms: 100, + tokens_total: 10, + cost_usd: 0, + recorded_at: now - 1000, + }, + { + id: 2, + model_id: "model-x", + provider: "p", + unit_type: "execute-task", + unit_id: "T02", + succeeded: 0, + failure_mode: "quota_exhausted", + retries: 0, + escalated: 0, + verification_passed: null, + blocker_discovered: 0, + duration_ms: 100, + tokens_total: 10, + cost_usd: 0, + recorded_at: now - 2000, + }, ); - const statsRateLimit = aggregateOutcomes(dbRateLimit, "model-x", "execute-task", { now }); - const statsQuota = aggregateOutcomes(dbQuota, "model-x", "execute-task", { now }); + const statsRateLimit = aggregateOutcomes( + dbRateLimit, + "model-x", + "execute-task", + { now }, + ); + const statsQuota = aggregateOutcomes(dbQuota, "model-x", "execute-task", { + now, + }); assert.ok( statsRateLimit.effective_success_rate > statsQuota.effective_success_rate, diff --git a/src/resources/extensions/sf/model-catalog-cache.js b/src/resources/extensions/sf/model-catalog-cache.js index ef4243d7e..e90c29014 100644 --- a/src/resources/extensions/sf/model-catalog-cache.js +++ b/src/resources/extensions/sf/model-catalog-cache.js @@ -11,12 +11,12 @@ */ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; +import { sfRuntimeRoot } from "./paths.js"; import { DISCOVERABLE_PROVIDER_IDS, getProviderCatalogConfig, getProviderModelExcludePatterns, } from "./provider-catalog-config.js"; -import { sfRuntimeRoot } from "./paths.js"; const CATALOG_TTL_MS = 6 * 60 * 60 * 1000; @@ -37,7 +37,8 @@ export function readCachedModelIds(basePath, providerId) { if (!existsSync(path)) return null; const entry = JSON.parse(readFileSync(path, "utf-8")); if (!entry?.fetchedAt || !Array.isArray(entry.modelIds)) return null; - if (Date.now() - new Date(entry.fetchedAt).getTime() > CATALOG_TTL_MS) return null; + if (Date.now() - new Date(entry.fetchedAt).getTime() > CATALOG_TTL_MS) + return null; return entry.modelIds; } catch { return null; @@ -85,10 +86,16 @@ function parseModelIds(cfg, json) { // Google: { models: [{name: "models/gemini-..."}] } items = Array.isArray(json?.models) ? json.models : null; if (!items) return null; - return items.map((m) => (m.name ?? "").replace(/^models\//, "")).filter(Boolean); + return items + .map((m) => (m.name ?? "").replace(/^models\//, "")) + .filter(Boolean); } // OpenAI-compatible: { data: [{id}] } or { models: [{id}] } - items = Array.isArray(json?.data) ? json.data : Array.isArray(json?.models) ? json.models : null; + items = Array.isArray(json?.data) + ? json.data + : Array.isArray(json?.models) + ? json.models + : null; if (!items) return null; return items.map((m) => m.id ?? m.name).filter(Boolean); } diff --git a/src/resources/extensions/sf/preferences-models.js b/src/resources/extensions/sf/preferences-models.js index b1cf5aad6..198381031 100644 --- a/src/resources/extensions/sf/preferences-models.js +++ b/src/resources/extensions/sf/preferences-models.js @@ -9,6 +9,8 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { getModels, getProviders } from "@singularity-forge/ai"; +import { selectByBenchmarks } from "./benchmark-selector.js"; +import { defaultRoutingConfig, MODEL_CAPABILITY_TIER } from "./model-router.js"; import { DEFAULT_RUNAWAY_CHANGED_FILES_WARNING, DEFAULT_RUNAWAY_DIAGNOSTIC_TURNS, @@ -16,8 +18,6 @@ import { DEFAULT_RUNAWAY_TOKEN_WARNING, DEFAULT_RUNAWAY_TOOL_CALL_WARNING, } from "./uok/auto-runaway-guard.js"; -import { selectByBenchmarks } from "./benchmark-selector.js"; -import { defaultRoutingConfig, MODEL_CAPABILITY_TIER } from "./model-router.js"; // ─── Lazy loader — breaks the preferences.js ↔ preferences-models.js cycle ── // preferences.js imports resolveProfileDefaults from here, and needs @@ -278,10 +278,12 @@ function resolveAutoBenchmarkPickForUnit(unitType, prefs) { const auth = getKeyManagerAuthStorage(); const configuredProviderIds = new Set( auth.getConfiguredProviders?.() ?? - getProviders().filter((p) => { - const creds = auth.getCredentialsForProvider(p); - return creds.some((c) => c.type === "oauth" || (c.type === "api_key" && c.key)); - }), + getProviders().filter((p) => { + const creds = auth.getCredentialsForProvider(p); + return creds.some( + (c) => c.type === "oauth" || (c.type === "api_key" && c.key), + ); + }), ); const candidates = []; for (const provider of getProviders()) { @@ -873,9 +875,13 @@ export function resolveSearchProviderFromPreferences() { export function isHeavyModelId(modelId) { if (!modelId || typeof modelId !== "string") return false; // Strip provider prefix (e.g. "anthropic/claude-opus-4-6" → "claude-opus-4-6") - const bare = modelId.includes("/") ? modelId.split("/").slice(1).join("/") : modelId; + const bare = modelId.includes("/") + ? modelId.split("/").slice(1).join("/") + : modelId; const tier = MODEL_CAPABILITY_TIER[bare] ?? MODEL_CAPABILITY_TIER[modelId]; if (tier !== undefined) return tier === "heavy"; // Fallback regex for models not yet in the tier map - return /\bopus\b|\bo1\b|\bo3\b|\bgpt-4-turbo\b|\bgpt-5\b|\bdeepseek-reasoner\b/i.test(modelId); + return /\bopus\b|\bo1\b|\bo3\b|\bgpt-4-turbo\b|\bgpt-5\b|\bdeepseek-reasoner\b/i.test( + modelId, + ); } diff --git a/src/resources/extensions/sf/preferences.js b/src/resources/extensions/sf/preferences.js index dc7eb22db..b36eb7988 100644 --- a/src/resources/extensions/sf/preferences.js +++ b/src/resources/extensions/sf/preferences.js @@ -12,8 +12,8 @@ import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { dirname, join, resolve } from "node:path"; -import { parse as parseYaml } from "yaml"; import { normalizeStringArray } from "@singularity-forge/coding-agent"; +import { parse as parseYaml } from "yaml"; import { sfRoot } from "./paths.js"; import { _initPrefsLoader, diff --git a/src/resources/extensions/sf/provider-catalog-config.js b/src/resources/extensions/sf/provider-catalog-config.js index db18e5de1..6db6ade39 100644 --- a/src/resources/extensions/sf/provider-catalog-config.js +++ b/src/resources/extensions/sf/provider-catalog-config.js @@ -169,5 +169,7 @@ export function isProviderScopedRateLimit(providerId) { * Returns the exclude patterns for a provider's model list, or []. */ export function getProviderModelExcludePatterns(providerId) { - return PROVIDER_CATALOG_CONFIG[providerId]?.modelFilter?.excludePatterns ?? []; + return ( + PROVIDER_CATALOG_CONFIG[providerId]?.modelFilter?.excludePatterns ?? [] + ); } diff --git a/src/resources/extensions/sf/record-promoter.js b/src/resources/extensions/sf/record-promoter.js index e89c98c6a..78389e747 100644 --- a/src/resources/extensions/sf/record-promoter.js +++ b/src/resources/extensions/sf/record-promoter.js @@ -14,8 +14,8 @@ import { writeFileSync, } from "node:fs"; import { basename, join } from "node:path"; -import { logWarning } from "./workflow-logger.js"; import { addBacklogItem, isDbAvailable } from "./sf-db.js"; +import { logWarning } from "./workflow-logger.js"; // ─── Frontmatter Parser ────────────────────────────────────────────────────── /** * Parse the YAML frontmatter from a markdown file. @@ -268,7 +268,12 @@ export function promoteActionableRecords(basePath) { // Write to DB backlog (primary) if (isDbAvailable()) { try { - addBacklogItem({ id: milestoneId, title: milestoneId, source: "record-promoter", status: "promoted" }); + addBacklogItem({ + id: milestoneId, + title: milestoneId, + source: "record-promoter", + status: "promoted", + }); } catch { // non-fatal } diff --git a/src/resources/extensions/sf/reports.js b/src/resources/extensions/sf/reports.js index 2806d813f..fc36e63aa 100644 --- a/src/resources/extensions/sf/reports.js +++ b/src/resources/extensions/sf/reports.js @@ -15,7 +15,10 @@ */ import { existsSync, mkdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; -import { formatDateShort, formatDuration } from "@singularity-forge/coding-agent"; +import { + formatDateShort, + formatDuration, +} from "@singularity-forge/coding-agent"; import { atomicWriteSync } from "./atomic-write.js"; import { formatCost, formatTokenCount } from "./metrics.js"; import { sfRoot } from "./paths.js"; diff --git a/src/resources/extensions/sf/requirement-promoter.js b/src/resources/extensions/sf/requirement-promoter.js index 73d5280ed..c9c44dcff 100644 --- a/src/resources/extensions/sf/requirement-promoter.js +++ b/src/resources/extensions/sf/requirement-promoter.js @@ -14,7 +14,11 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { markResolved, readAllSelfFeedback } from "./self-feedback.js"; -import { getActiveRequirements, isDbAvailable, upsertRequirement } from "./sf-db.js"; +import { + getActiveRequirements, + isDbAvailable, + upsertRequirement, +} from "./sf-db.js"; // ─── Constants ─────────────────────────────────────────────────────────────── const COUNT_THRESHOLD = 5; diff --git a/src/resources/extensions/sf/setup-catalog.js b/src/resources/extensions/sf/setup-catalog.js index ea863dc75..f1bb3f443 100644 --- a/src/resources/extensions/sf/setup-catalog.js +++ b/src/resources/extensions/sf/setup-catalog.js @@ -120,7 +120,13 @@ export function getLlmProviderIds() { } // Key-free and meta search provider IDs that are valid as preferences but have // no PROVIDER_REGISTRY entry (no env-var key required). -const KEYLESS_SEARCH_PROVIDER_IDS = ["minimax", "ollama", "combosearch", "native", "auto"]; +const KEYLESS_SEARCH_PROVIDER_IDS = [ + "minimax", + "ollama", + "combosearch", + "native", + "auto", +]; /** * All valid values for the `search_provider` preference. * Derived from PROVIDER_REGISTRY (key-backed) plus keyless/meta providers. diff --git a/src/resources/extensions/sf/sf-db.js b/src/resources/extensions/sf/sf-db.js index 4e1d7f407..2e116b4ec 100644 --- a/src/resources/extensions/sf/sf-db.js +++ b/src/resources/extensions/sf/sf-db.js @@ -39,8 +39,8 @@ import { taskFrontmatterFromRecord, withTaskFrontmatter, } from "./task-frontmatter.js"; -import { logError, logWarning } from "./workflow-logger.js"; import { readTraceEvents } from "./uok/trace-writer.js"; +import { logError, logWarning } from "./workflow-logger.js"; let loadAttempted = false; function loadProvider() { @@ -1443,9 +1443,7 @@ function columnExists(db, table, column) { } function tableExists(db, table) { const row = db - .prepare( - `SELECT name FROM sqlite_master WHERE type='table' AND name=?`, - ) + .prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`) .get(table); return row != null; } @@ -3223,7 +3221,9 @@ function migrateSchema(db) { "failure_mode", "ALTER TABLE llm_task_outcomes ADD COLUMN failure_mode TEXT DEFAULT NULL", ); - db.exec("CREATE INDEX IF NOT EXISTS idx_llm_task_outcomes_failure_mode ON llm_task_outcomes(model_id, failure_mode, recorded_at DESC)"); + db.exec( + "CREATE INDEX IF NOT EXISTS idx_llm_task_outcomes_failure_mode ON llm_task_outcomes(model_id, failure_mode, recorded_at DESC)", + ); db.prepare( "INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)", ).run({ @@ -4242,8 +4242,13 @@ function hasTaskSpecIntent(planning = {}) { } function insertTaskSpecIfAbsent(milestoneId, sliceId, taskId, planning = {}) { if (!hasTaskSpecIntent(planning)) return; - const { normalized: frontmatter, errors } = taskFrontmatterFromRecord(planning); - if (errors?.length) logWarning("sf-db:insertTaskSpec", `frontmatter validation errors for ${milestoneId}/${sliceId}/${taskId}: ${errors.join(", ")}`); + const { normalized: frontmatter, errors } = + taskFrontmatterFromRecord(planning); + if (errors?.length) + logWarning( + "sf-db:insertTaskSpec", + `frontmatter validation errors for ${milestoneId}/${sliceId}/${taskId}: ${errors.join(", ")}`, + ); currentDb .prepare(`INSERT OR IGNORE INTO task_specs ( milestone_id, slice_id, task_id, verify, inputs, expected_output, @@ -4464,8 +4469,13 @@ export function setTaskBlockerDiscovered( export function upsertTaskPlanning(milestoneId, sliceId, taskId, planning) { if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open"); insertTaskSpecIfAbsent(milestoneId, sliceId, taskId, planning); - const { normalized: frontmatter, errors: fmErrors } = taskFrontmatterFromRecord(planning); - if (fmErrors?.length) logWarning("sf-db:upsertTaskPlanning", `frontmatter validation errors for ${milestoneId}/${sliceId}/${taskId}: ${fmErrors.join(", ")}`); + const { normalized: frontmatter, errors: fmErrors } = + taskFrontmatterFromRecord(planning); + if (fmErrors?.length) + logWarning( + "sf-db:upsertTaskPlanning", + `frontmatter validation errors for ${milestoneId}/${sliceId}/${taskId}: ${fmErrors.join(", ")}`, + ); const hasTaskStatus = planning.taskStatus !== undefined || planning.task_status !== undefined || @@ -6035,7 +6045,9 @@ export function recordUokRunExit(entry) { ":status": entry.status ?? "ok", ":started_at": entry.startedAt ?? now, ":ended_at": now, - ":error": entry.error ? capErrorForStorage(entry.error, entry.runId) : null, + ":error": entry.error + ? capErrorForStorage(entry.error, entry.runId) + : null, ":flags_json": JSON.stringify(entry.flags ?? {}), ":updated_at": now, }); @@ -6425,11 +6437,13 @@ export function getLlmTaskOutcomeStats(modelId, windowHours = 24) { */ export function getGateRunStats(gateId, windowHours = 24) { try { - const basePath = currentPath && currentPath !== ":memory:" - ? dirname(dirname(currentPath)) - : process.cwd(); - const events = readTraceEvents(basePath, "gate_run", windowHours) - .filter((e) => e.gateId === gateId); + const basePath = + currentPath && currentPath !== ":memory:" + ? dirname(dirname(currentPath)) + : process.cwd(); + const events = readTraceEvents(basePath, "gate_run", windowHours).filter( + (e) => e.gateId === gateId, + ); const stats = { total: events.length, pass: 0, @@ -6557,18 +6571,31 @@ export function updateGateCircuitBreaker(gateId, updates) { } export function getGateLatencyStats(gateId, windowHours = 24) { try { - const basePath = currentPath && currentPath !== ":memory:" - ? dirname(dirname(currentPath)) - : process.cwd(); + const basePath = + currentPath && currentPath !== ":memory:" + ? dirname(dirname(currentPath)) + : process.cwd(); const durations = readTraceEvents(basePath, "gate_run", windowHours) .filter((e) => e.gateId === gateId && typeof e.durationMs === "number") .map((e) => e.durationMs) .sort((a, b) => a - b); - if (durations.length === 0) return { p50: null, p95: null, count: 0, total: 0, avgMs: 0, p50Ms: 0, p95Ms: 0, maxMs: 0 }; + if (durations.length === 0) + return { + p50: null, + p95: null, + count: 0, + total: 0, + avgMs: 0, + p50Ms: 0, + p95Ms: 0, + maxMs: 0, + }; const p50Ms = durations[Math.floor(durations.length * 0.5)] ?? 0; const p95Ms = durations[Math.floor(durations.length * 0.95)] ?? 0; const maxMs = durations[durations.length - 1] ?? 0; - const avgMs = Math.round(durations.reduce((s, v) => s + v, 0) / durations.length); + const avgMs = Math.round( + durations.reduce((s, v) => s + v, 0) / durations.length, + ); return { p50: p50Ms, p95: p95Ms, @@ -6580,14 +6607,24 @@ export function getGateLatencyStats(gateId, windowHours = 24) { maxMs, }; } catch { - return { p50: null, p95: null, count: 0, total: 0, avgMs: 0, p50Ms: 0, p95Ms: 0, maxMs: 0 }; + return { + p50: null, + p95: null, + count: 0, + total: 0, + avgMs: 0, + p50Ms: 0, + p95Ms: 0, + maxMs: 0, + }; } } export function getDistinctGateIds() { try { - const basePath = currentPath && currentPath !== ":memory:" - ? dirname(dirname(currentPath)) - : process.cwd(); + const basePath = + currentPath && currentPath !== ":memory:" + ? dirname(dirname(currentPath)) + : process.cwd(); const events = readTraceEvents(basePath, "gate_run", 24 * 30); // 30 days return [...new Set(events.map((e) => e.gateId).filter(Boolean))]; } catch { @@ -7817,15 +7854,29 @@ export function bulkInsertLegacyHierarchy(payload) { export function getActiveMemories({ category, limit = 200 } = {}) { if (!currentDb) return []; const rows = category - ? currentDb.prepare("SELECT * FROM active_memories WHERE category = ? ORDER BY updated_at DESC LIMIT ?").all(category, limit) - : currentDb.prepare("SELECT * FROM active_memories ORDER BY updated_at DESC LIMIT ?").all(limit); + ? currentDb + .prepare( + "SELECT * FROM active_memories WHERE category = ? ORDER BY updated_at DESC LIMIT ?", + ) + .all(category, limit) + : currentDb + .prepare( + "SELECT * FROM active_memories ORDER BY updated_at DESC LIMIT ?", + ) + .all(limit); return rows.map((r) => ({ id: r["id"], category: r["category"], content: r["content"], confidence: r["confidence"], sourceUnitId: r["source_unit_id"], - tags: (() => { try { return JSON.parse(r["tags"] ?? "[]"); } catch { return []; } })(), + tags: (() => { + try { + return JSON.parse(r["tags"] ?? "[]"); + } catch { + return []; + } + })(), createdAt: r["created_at"], updatedAt: r["updated_at"], })); diff --git a/src/resources/extensions/sf/subagent-inheritance.js b/src/resources/extensions/sf/subagent-inheritance.js index 775d34653..55febf550 100644 --- a/src/resources/extensions/sf/subagent-inheritance.js +++ b/src/resources/extensions/sf/subagent-inheritance.js @@ -15,7 +15,10 @@ import { resolveRunControlMode, resolveWorkMode, } from "./operating-model.js"; -import { isHeavyModelId, isProviderAllowedByLists } from "./preferences-models.js"; +import { + isHeavyModelId, + isProviderAllowedByLists, +} from "./preferences-models.js"; import { logWarning } from "./workflow-logger.js"; function providerFromModelId(modelId) { @@ -24,7 +27,6 @@ function providerFromModelId(modelId) { return provider && provider !== modelId ? provider : null; } - /** * Build an inheritance envelope from the current parent session. * diff --git a/src/resources/extensions/sf/subagent/background-jobs.js b/src/resources/extensions/sf/subagent/background-jobs.js index e8759cb53..54b0d25fe 100644 --- a/src/resources/extensions/sf/subagent/background-jobs.js +++ b/src/resources/extensions/sf/subagent/background-jobs.js @@ -74,7 +74,8 @@ export class SubagentBackgroundJobManager { const job = this.jobs.get(id); if (!job) return "not_found"; if (job.status === "running") return "already_running"; - if (this.getRunningJobs().length >= this.maxRunning) return "concurrency_limit"; + if (this.getRunningJobs().length >= this.maxRunning) + return "concurrency_limit"; // Cancel pending eviction — job is active again const evTimer = this.evictionTimers.get(id); if (evTimer) { @@ -99,7 +100,12 @@ export class SubagentBackgroundJobManager { appendTurn(id, role, content) { const job = this.jobs.get(id); if (!job) return; - job.turns.push({ turnIndex: job.turns.length, role, content, timestamp: Date.now() }); + job.turns.push({ + turnIndex: job.turns.length, + role, + content, + timestamp: Date.now(), + }); } /** Return all turns for a job, optionally filtered to those after `since` (exclusive). */ getTurns(id, since = 0) { diff --git a/src/resources/extensions/sf/subagent/index.js b/src/resources/extensions/sf/subagent/index.js index 8a31f8180..fd40dc28b 100644 --- a/src/resources/extensions/sf/subagent/index.js +++ b/src/resources/extensions/sf/subagent/index.js @@ -22,6 +22,8 @@ import { StringEnum } from "@singularity-forge/ai"; import { getMarkdownTheme } from "@singularity-forge/coding-agent"; import { Container, Markdown, Spacer, Text } from "@singularity-forge/tui"; import { CmuxClient, shellEscape } from "../../cmux/index.js"; +import { formatTokenCount } from "../../shared/mod.js"; +import { getCurrentPhase } from "../../shared/sf-phase-state.js"; import { buildSiftEnv, ensureSiftRuntimeDirs, @@ -35,8 +37,6 @@ import { buildSubagentInheritanceEnvelope, validateSubagentDispatch, } from "../subagent-inheritance.js"; -import { formatTokenCount } from "../../shared/mod.js"; -import { getCurrentPhase } from "../../shared/sf-phase-state.js"; import { discoverAgents } from "./agents.js"; import { SubagentBackgroundJobManager } from "./background-jobs.js"; import { @@ -1779,7 +1779,9 @@ export default function (pi) { const manager = getBackgroundJobs(); // Build a rerun factory for write_subagent multi-turn follow-ups. // Only single-mode dispatches (params.agent + params.task) support write_subagent. - const isSingleMode = Boolean(params.agent && params.task && !params.tasks && !params.chain); + const isSingleMode = Boolean( + params.agent && params.task && !params.tasks && !params.chain, + ); const dispatchContext = isSingleMode ? { originalTask: params.task, @@ -1831,7 +1833,11 @@ export default function (pi) { } return result; }); - jobId = manager.register(summarizeBackgroundInvocation(params), wrappedRun, dispatchContext); + jobId = manager.register( + summarizeBackgroundInvocation(params), + wrappedRun, + dispatchContext, + ); return { content: [ { @@ -2738,7 +2744,8 @@ export default function (pi) { description: "Background subagent job ID (for example sub_a1b2c3d4)", }), message: Type.String({ - description: "Follow-up message or instruction to send to the subagent.", + description: + "Follow-up message or instruction to send to the subagent.", }), }), async execute(_toolCallId, params) { @@ -2746,7 +2753,12 @@ export default function (pi) { const job = manager.getJob(params.job_id); if (!job) { return { - content: [{ type: "text", text: `Background subagent job not found: ${params.job_id}` }], + content: [ + { + type: "text", + text: `Background subagent job not found: ${params.job_id}`, + }, + ], details: undefined, isError: true, }; @@ -2784,19 +2796,23 @@ export default function (pi) { let enrichedTask = job.dispatchContext.originalTask; if (historyTurns.length > 0) { const historyText = historyTurns - .map((t) => `[${t.role === "agent" ? "Agent" : "User"} — turn ${t.turnIndex}]\n${t.content}`) + .map( + (t) => + `[${t.role === "agent" ? "Agent" : "User"} — turn ${t.turnIndex}]\n${t.content}`, + ) .join("\n\n"); enrichedTask += `\n\n---\nConversation history:\n${historyText}\n---`; } enrichedTask += `\n\nUser follow-up: ${params.message}`; - const resumeResult = manager.resume( - params.job_id, - (signal) => job.dispatchContext.rerunWithTask(enrichedTask, signal).then((result) => { - // Append the new agent response as a turn before the notification fires - const text = getPrimaryTextContent(result); - if (text) manager.appendTurn(params.job_id, "agent", text); - return result; - }), + const resumeResult = manager.resume(params.job_id, (signal) => + job.dispatchContext + .rerunWithTask(enrichedTask, signal) + .then((result) => { + // Append the new agent response as a turn before the notification fires + const text = getPrimaryTextContent(result); + if (text) manager.appendTurn(params.job_id, "agent", text); + return result; + }), ); if (resumeResult !== "resumed") { const messages = { @@ -2805,7 +2821,12 @@ export default function (pi) { already_running: `Background subagent ${params.job_id} is already running.`, }; return { - content: [{ type: "text", text: messages[resumeResult] ?? `Resume failed: ${resumeResult}` }], + content: [ + { + type: "text", + text: messages[resumeResult] ?? `Resume failed: ${resumeResult}`, + }, + ], details: undefined, isError: true, }; @@ -2847,7 +2868,12 @@ export default function (pi) { const job = manager.getJob(params.job_id); if (!job) { return { - content: [{ type: "text", text: `Background subagent job not found: ${params.job_id}` }], + content: [ + { + type: "text", + text: `Background subagent job not found: ${params.job_id}`, + }, + ], details: undefined, isError: true, }; @@ -2855,7 +2881,8 @@ export default function (pi) { const since = params.since_turn ?? 0; const turns = manager.getTurns(params.job_id, since); if (!turns || turns.length === 0) { - const status = job.status === "running" ? " (still running)" : ` (${job.status})`; + const status = + job.status === "running" ? " (still running)" : ` (${job.status})`; return { content: [ { @@ -2872,7 +2899,8 @@ export default function (pi) { return `**${label} [turn ${t.turnIndex}]**\n${t.content}`; }) .join("\n\n---\n\n"); - const statusLine = job.status === "running" ? " *(running)*" : ` *(${job.status})*`; + const statusLine = + job.status === "running" ? " *(running)*" : ` *(${job.status})*`; return { content: [ { diff --git a/src/resources/extensions/sf/summary-helpers.js b/src/resources/extensions/sf/summary-helpers.js index aca5e7675..9fdebb105 100644 --- a/src/resources/extensions/sf/summary-helpers.js +++ b/src/resources/extensions/sf/summary-helpers.js @@ -201,7 +201,9 @@ function escapeRegExpLocal(value) { } function extractMarkdownSectionLocal(content, heading) { - const match = new RegExp(`^## ${escapeRegExpLocal(heading)}\\s*$`, "m").exec(content); + const match = new RegExp(`^## ${escapeRegExpLocal(heading)}\\s*$`, "m").exec( + content, + ); if (!match) return null; const start = match.index + match[0].length; const rest = content.slice(start); @@ -228,11 +230,20 @@ export function extractSliceExecutionExcerpt(content, relPath) { const goalLine = lines.find((line) => line.startsWith("**Goal:**"))?.trim(); const demoLine = lines.find((line) => line.startsWith("**Demo:**"))?.trim(); const verification = extractMarkdownSectionLocal(content, "Verification"); - const observability = extractMarkdownSectionLocal(content, "Observability / Diagnostics"); + const observability = extractMarkdownSectionLocal( + content, + "Observability / Diagnostics", + ); const parts = ["## Slice Plan Excerpt", `Source: \`${relPath}\``]; if (goalLine) parts.push(goalLine); if (demoLine) parts.push(demoLine); - if (verification) parts.push("", "### Slice Verification", verification.trim()); - if (observability) parts.push("", "### Slice Observability / Diagnostics", observability.trim()); + if (verification) + parts.push("", "### Slice Verification", verification.trim()); + if (observability) + parts.push( + "", + "### Slice Observability / Diagnostics", + observability.trim(), + ); return parts.join("\n"); } diff --git a/src/resources/extensions/sf/tests/auto-dispatch-canonical-plan.test.mjs b/src/resources/extensions/sf/tests/auto-dispatch-canonical-plan.test.mjs index ff8bd771f..f625023c8 100644 --- a/src/resources/extensions/sf/tests/auto-dispatch-canonical-plan.test.mjs +++ b/src/resources/extensions/sf/tests/auto-dispatch-canonical-plan.test.mjs @@ -41,13 +41,13 @@ vi.mock("../auto-prompts.js", () => ({ checkNeedsRunUat: vi.fn(async () => null), })); -import { resolveDispatch } from "../uok/auto-dispatch.js"; import { closeDatabase, insertMilestone, insertSlice, openDatabase, } from "../sf-db.js"; +import { resolveDispatch } from "../uok/auto-dispatch.js"; function makeTempDir(prefix) { return mkdtempSync(join(tmpdir(), prefix)); diff --git a/src/resources/extensions/sf/tests/doctor-providers.test.mjs b/src/resources/extensions/sf/tests/doctor-providers.test.mjs index 5dcb8f15c..d2ac85d43 100644 --- a/src/resources/extensions/sf/tests/doctor-providers.test.mjs +++ b/src/resources/extensions/sf/tests/doctor-providers.test.mjs @@ -92,12 +92,9 @@ describe("doctor provider checks", () => { test("runProviderChecks_when_google_env_auth_is_default_off_treats_google_as_missing_required_route", () => { makePreferencesProject( - [ - "version: 1", - "models:", - " planning: google/gemini-2.5-pro", - "", - ].join("\n"), + ["version: 1", "models:", " planning: google/gemini-2.5-pro", ""].join( + "\n", + ), ); process.env.GEMINI_API_KEY = "test-google-key"; @@ -109,12 +106,9 @@ describe("doctor provider checks", () => { test("runProviderChecks_when_google_env_auth_is_enabled_accepts_google_env_key", () => { const project = makePreferencesProject( - [ - "version: 1", - "models:", - " planning: google/gemini-2.5-pro", - "", - ].join("\n"), + ["version: 1", "models:", " planning: google/gemini-2.5-pro", ""].join( + "\n", + ), ); mkdirSync(join(project, ".sf"), { recursive: true }); writeFileSync( diff --git a/src/resources/extensions/sf/tests/memory-embeddings-llm-gateway.test.mjs b/src/resources/extensions/sf/tests/memory-embeddings-llm-gateway.test.mjs index bd5ac774b..4c1496ba4 100644 --- a/src/resources/extensions/sf/tests/memory-embeddings-llm-gateway.test.mjs +++ b/src/resources/extensions/sf/tests/memory-embeddings-llm-gateway.test.mjs @@ -43,7 +43,11 @@ test("loadGatewayConfigFromEnv returns null when auth.json does not exist", () = }); test("loadGatewayConfigFromEnv reads key and url from auth.json", () => { - writeAuthJson({ key: "auth-key", url: "https://example.test/v1", type: "api_key" }); + writeAuthJson({ + key: "auth-key", + url: "https://example.test/v1", + type: "api_key", + }); const cfg = loadGatewayConfigFromEnv(); assert.equal(cfg.apiKey, "auth-key"); @@ -71,4 +75,3 @@ test("loadGatewayConfigFromEnv respects model env var overrides", () => { assert.equal(cfg.embeddingModel, "custom-embed"); assert.equal(cfg.rerankModel, "custom-rerank"); }); - diff --git a/src/resources/extensions/sf/tests/preferences-models.test.mjs b/src/resources/extensions/sf/tests/preferences-models.test.mjs index 7a3e922db..2cee5e096 100644 --- a/src/resources/extensions/sf/tests/preferences-models.test.mjs +++ b/src/resources/extensions/sf/tests/preferences-models.test.mjs @@ -46,13 +46,9 @@ function makePreferencesProject(projectPreferences, projectSettings) { describe("preferences model resolution", () => { test("resolveModelWithFallbacksForUnit_when_google_env_auth_is_default_off_skips_google_auto_benchmark_candidates", () => { makePreferencesProject( - [ - "version: 1", - "allowed_providers:", - " - google", - "models: {}", - "", - ].join("\n"), + ["version: 1", "allowed_providers:", " - google", "models: {}", ""].join( + "\n", + ), ); process.env.GEMINI_API_KEY = "test-google-key"; @@ -60,5 +56,4 @@ describe("preferences model resolution", () => { assert.equal(result, undefined); }); - }); diff --git a/src/resources/extensions/sf/tests/remote-steering-pipeline.test.mjs b/src/resources/extensions/sf/tests/remote-steering-pipeline.test.mjs index 2778e8240..284d7e427 100644 --- a/src/resources/extensions/sf/tests/remote-steering-pipeline.test.mjs +++ b/src/resources/extensions/sf/tests/remote-steering-pipeline.test.mjs @@ -72,7 +72,10 @@ describe("parse stage", () => { }); assert.strictEqual(result.steering, true); assert.strictEqual(result.directives.length, 1); - assert.deepStrictEqual(result.directives[0], { cmd: "mode", value: "build" }); + assert.deepStrictEqual(result.directives[0], { + cmd: "mode", + value: "build", + }); }); test("parseRemoteSteeringDirectives_when_text_has_all_axes_returns_all_four_directives", () => { @@ -131,7 +134,10 @@ describe("apply stage", () => { test("applyRemoteSteeringDirectives_when_control_directive_applies_changes_runControl", () => { const src = uniqueSrc(); - applyRemoteSteeringDirectives([{ cmd: "control", value: "autonomous" }], src); + applyRemoteSteeringDirectives( + [{ cmd: "control", value: "autonomous" }], + src, + ); assert.strictEqual(_modeState.runControl, "autonomous"); }); @@ -209,7 +215,10 @@ describe("format stage", () => { ]; const text = formatRemoteSteeringResults(results); assert.ok(text.includes("[ok] /mode build"), `missing ok mode: ${text}`); - assert.ok(text.includes("[ok] /control autonomous"), `missing ok control: ${text}`); + assert.ok( + text.includes("[ok] /control autonomous"), + `missing ok control: ${text}`, + ); }); test("formatRemoteSteeringResults_when_blocked_renders_blocked_marker_and_error", () => { @@ -217,7 +226,10 @@ describe("format stage", () => { { cmd: "mode", value: "review", applied: false, error: "Throttled" }, ]; const text = formatRemoteSteeringResults(results); - assert.ok(text.includes("[blocked] /mode review"), `missing blocked: ${text}`); + assert.ok( + text.includes("[blocked] /mode review"), + `missing blocked: ${text}`, + ); assert.ok(text.includes("Throttled"), `missing error text: ${text}`); }); @@ -259,7 +271,10 @@ describe("full pipeline: parse → apply → format", () => { assert.strictEqual(parsed.directives.length, 4); const applied = applyRemoteSteeringDirectives(parsed.directives, src); - assert.ok(applied.every((r) => r.applied), `Some failed: ${JSON.stringify(applied)}`); + assert.ok( + applied.every((r) => r.applied), + `Some failed: ${JSON.stringify(applied)}`, + ); assert.strictEqual(_modeState.workMode, "build"); assert.strictEqual(_modeState.runControl, "autonomous"); @@ -279,7 +294,10 @@ describe("full pipeline: parse → apply → format", () => { const formatted = formatRemoteSteeringResults(applied2); assert.strictEqual(applied2[0].applied, false); - assert.ok(formatted.includes("[blocked]"), `expected blocked: ${formatted}`); + assert.ok( + formatted.includes("[blocked]"), + `expected blocked: ${formatted}`, + ); // Mode should not have changed assert.strictEqual(_modeState.workMode, "build"); }); diff --git a/src/resources/extensions/sf/tests/schedule-dispatch.test.mjs b/src/resources/extensions/sf/tests/schedule-dispatch.test.mjs index ab47188c7..ebfc8b6d9 100644 --- a/src/resources/extensions/sf/tests/schedule-dispatch.test.mjs +++ b/src/resources/extensions/sf/tests/schedule-dispatch.test.mjs @@ -14,9 +14,9 @@ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, it } from "vitest"; -import { DISPATCH_RULES } from "../uok/auto-dispatch.js"; import { createScheduleStore } from "../schedule/schedule-store.js"; import { generateULID } from "../schedule/schedule-ulid.js"; +import { DISPATCH_RULES } from "../uok/auto-dispatch.js"; describe("schedule-dispatch", () => { let testDir; diff --git a/src/resources/extensions/sf/tests/sf-db-migration.test.mjs b/src/resources/extensions/sf/tests/sf-db-migration.test.mjs index 16557e82d..ac9effcbd 100644 --- a/src/resources/extensions/sf/tests/sf-db-migration.test.mjs +++ b/src/resources/extensions/sf/tests/sf-db-migration.test.mjs @@ -229,7 +229,10 @@ test("openDatabase_migrates_v27_tasks_without_created_at_through_spec_backfill", "SELECT name FROM sqlite_master WHERE type='table' AND name='intent_chapters'", ) .get(); - assert.ok(chaptersTable, "intent_chapters table should exist after v61 migration"); + assert.ok( + chaptersTable, + "intent_chapters table should exist after v61 migration", + ); const taskSpec = db .prepare( "SELECT milestone_id, slice_id, task_id, verify FROM task_specs WHERE task_id = 'T01'", @@ -292,9 +295,15 @@ test("openDatabase_when_fresh_db_does_not_create_gate_runs_table", () => { // After v58 migration, gate_runs table no longer exists const tableInfo = getDatabase() - .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='gate_runs'") + .prepare( + "SELECT name FROM sqlite_master WHERE type='table' AND name='gate_runs'", + ) .get(); - assert.equal(tableInfo, undefined, "gate_runs table should not exist after v58 migration"); + assert.equal( + tableInfo, + undefined, + "gate_runs table should not exist after v58 migration", + ); }); test("reconcileWorktreeDb_when_worktree_lacks_product_research_column_merges_milestones", () => { @@ -342,9 +351,15 @@ test("openDatabase_migrates_v35_gate_cost_usd_drops_table_in_v58", () => { const db = getDatabase(); // After v58 migration, gate_runs is dropped const tableInfo = db - .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='gate_runs'") + .prepare( + "SELECT name FROM sqlite_master WHERE type='table' AND name='gate_runs'", + ) .get(); - assert.equal(tableInfo, undefined, "gate_runs should be dropped by v58 migration"); + assert.equal( + tableInfo, + undefined, + "gate_runs should be dropped by v58 migration", + ); }); test("openDatabase_memories_table_has_tags_column", () => { diff --git a/src/resources/extensions/sf/tests/sift-retrieval-evidence.test.mjs b/src/resources/extensions/sf/tests/sift-retrieval-evidence.test.mjs index 35b433007..543ac04da 100644 --- a/src/resources/extensions/sf/tests/sift-retrieval-evidence.test.mjs +++ b/src/resources/extensions/sf/tests/sift-retrieval-evidence.test.mjs @@ -15,9 +15,9 @@ import { import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, test } from "vitest"; -import registerSubagentExtension from "../subagent/index.js"; import { registerQueryTools } from "../bootstrap/query-tools.js"; import { closeDatabase, getRetrievalEvidence, openDatabase } from "../sf-db.js"; +import registerSubagentExtension from "../subagent/index.js"; import { registerSiftSearchTool } from "../tools/sift-search-tool.js"; const tmpRoots = []; diff --git a/src/resources/extensions/sf/tests/summary-helpers.test.mjs b/src/resources/extensions/sf/tests/summary-helpers.test.mjs index 7e704fcf9..0d768ff90 100644 --- a/src/resources/extensions/sf/tests/summary-helpers.test.mjs +++ b/src/resources/extensions/sf/tests/summary-helpers.test.mjs @@ -1,8 +1,6 @@ import assert from "node:assert/strict"; import { test } from "vitest"; -import { - extractSliceExecutionExcerpt, -} from "../summary-helpers.js"; +import { extractSliceExecutionExcerpt } from "../summary-helpers.js"; test("extractSliceExecutionExcerpt when content is null returns fallback", () => { const result = extractSliceExecutionExcerpt(null, "S01/PLAN.md"); @@ -59,11 +57,7 @@ test("extractSliceExecutionExcerpt extracts verification section", () => { }); test("extractSliceExecutionExcerpt omits missing sections gracefully", () => { - const content = [ - "# Slice Plan", - "", - "**Goal:** Minimal slice", - ].join("\n"); + const content = ["# Slice Plan", "", "**Goal:** Minimal slice"].join("\n"); const result = extractSliceExecutionExcerpt(content, "S01/PLAN.md"); assert.ok(result.includes("**Goal:** Minimal slice")); diff --git a/src/resources/extensions/sf/tests/uok-metrics-exposition.test.mjs b/src/resources/extensions/sf/tests/uok-metrics-exposition.test.mjs index fdaefcd1d..9e31fdb11 100644 --- a/src/resources/extensions/sf/tests/uok-metrics-exposition.test.mjs +++ b/src/resources/extensions/sf/tests/uok-metrics-exposition.test.mjs @@ -10,11 +10,7 @@ import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, test } from "vitest"; -import { - closeDatabase, - insertUokMessage, - openDatabase, -} from "../sf-db.js"; +import { closeDatabase, insertUokMessage, openDatabase } from "../sf-db.js"; import { invalidateMetricsCache, readUokMetrics, @@ -45,7 +41,11 @@ function makeProject() { return dir; } -function recordGateRun(basePath, outcome, evaluatedAt = new Date().toISOString()) { +function recordGateRun( + basePath, + outcome, + evaluatedAt = new Date().toISOString(), +) { appendTraceEvent(basePath, `trace-${outcome}-${Date.now()}`, { type: "gate_run", traceId: `trace-${outcome}`, diff --git a/src/resources/extensions/sf/triage-self-feedback.js b/src/resources/extensions/sf/triage-self-feedback.js index 017c4888c..d8f0a3d2d 100644 --- a/src/resources/extensions/sf/triage-self-feedback.js +++ b/src/resources/extensions/sf/triage-self-feedback.js @@ -37,11 +37,16 @@ function readRequirementsContent(basePath) { if (isDbAvailable()) { const rows = getActiveRequirements(); if (rows.length > 0) { - return rows.map((r) => `- [${r.id}] ${r.title}: ${r.description ?? ""}`).join("\n"); + return rows + .map((r) => `- [${r.id}] ${r.title}: ${r.description ?? ""}`) + .join("\n"); } } const sfDir = sfRoot(basePath); - for (const p of [join(sfDir, "REQUIREMENTS.md"), join(sfDir, "requirements.md")]) { + for (const p of [ + join(sfDir, "REQUIREMENTS.md"), + join(sfDir, "requirements.md"), + ]) { if (existsSync(p)) return readFileSync(p, "utf-8"); } return "(no requirements found)"; diff --git a/src/resources/extensions/sf/ui/index.js b/src/resources/extensions/sf/ui/index.js index 6f77c5ba8..e6fd927d2 100644 --- a/src/resources/extensions/sf/ui/index.js +++ b/src/resources/extensions/sf/ui/index.js @@ -15,10 +15,7 @@ import { Key } from "@singularity-forge/tui"; import { getAutoSession } from "../auto/session.js"; import { isAutoActive } from "../auto.js"; import { projectRoot } from "../commands/context.js"; -import { - getExperimentalFlag, - setExperimentalFlag, -} from "../experimental.js"; +import { getExperimentalFlag, setExperimentalFlag } from "../experimental.js"; import { registerSessionColor } from "./color-band.js"; import { registerSessionEmoji } from "./emoji.js"; import { renderAutoFooter, renderFooter } from "./footer.js"; diff --git a/src/resources/extensions/sf/uok-parity-summary.js b/src/resources/extensions/sf/uok-parity-summary.js index 7da17eba6..c2c4b6982 100644 --- a/src/resources/extensions/sf/uok-parity-summary.js +++ b/src/resources/extensions/sf/uok-parity-summary.js @@ -1,10 +1,10 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; +import { NOTICE_KIND } from "./notification-store.js"; import { hasCurrentParityWarning, writeParityReport, } from "./uok/parity-report.js"; -import { NOTICE_KIND } from "./notification-store.js"; /** * Read the last UOK parity report from /.sf/runtime/uok-parity-report.json * and surface any divergences or errors via ctx.ui?.notify?.(). diff --git a/src/resources/extensions/sf/uok/a2a-agent-server.js b/src/resources/extensions/sf/uok/a2a-agent-server.js index 746e6845a..0a7fbff2a 100644 --- a/src/resources/extensions/sf/uok/a2a-agent-server.js +++ b/src/resources/extensions/sf/uok/a2a-agent-server.js @@ -18,17 +18,14 @@ import { randomUUID } from "node:crypto"; import { createServer } from "node:http"; -import express from "express"; -import { - DefaultRequestHandler, - InMemoryTaskStore, -} from "@a2a-js/sdk/server"; +import { AGENT_CARD_PATH } from "@a2a-js/sdk"; +import { DefaultRequestHandler, InMemoryTaskStore } from "@a2a-js/sdk/server"; import { agentCardHandler, jsonRpcHandler, UserBuilder, } from "@a2a-js/sdk/server/express"; -import { AGENT_CARD_PATH } from "@a2a-js/sdk"; +import express from "express"; import { buildAgentCard } from "./a2a-transport.js"; const agentName = process.env.SF_A2A_AGENT_NAME; @@ -37,9 +34,7 @@ const port = Number(process.env.SF_A2A_PORT ?? 34501); const basePath = process.env.SF_A2A_BASE_PATH ?? process.cwd(); if (!agentName) { - process.stderr.write( - "a2a-agent-server: SF_A2A_AGENT_NAME is required\n", - ); + process.stderr.write("a2a-agent-server: SF_A2A_AGENT_NAME is required\n"); process.exit(1); } @@ -161,7 +156,12 @@ class SwarmAgentExecutor { transport: "a2a", }; - const messageId = bus.send(from, to, envelope.payload ?? envelope, metadata); + const messageId = bus.send( + from, + to, + envelope.payload ?? envelope, + metadata, + ); return { status: "accepted", @@ -214,7 +214,9 @@ async function main() { }); // Signal readiness to the parent process. - process.stdout.write(JSON.stringify({ ready: true, port, agentName, role: agentRole }) + "\n"); + process.stdout.write( + JSON.stringify({ ready: true, port, agentName, role: agentRole }) + "\n", + ); process.on("SIGTERM", () => { process.exit(0); diff --git a/src/resources/extensions/sf/uok/a2a-transport.js b/src/resources/extensions/sf/uok/a2a-transport.js index 8729664e8..7b96af2fb 100644 --- a/src/resources/extensions/sf/uok/a2a-transport.js +++ b/src/resources/extensions/sf/uok/a2a-transport.js @@ -9,9 +9,9 @@ * Consumer: SwarmDispatchLayer._a2aDispatch() when SF_A2A_ENABLED is set. */ -import { randomUUID } from "node:crypto"; import { spawn } from "node:child_process"; -import { join, dirname, resolve } from "node:path"; +import { randomUUID } from "node:crypto"; +import { dirname, join, resolve } from "node:path"; const A2A_AGENT_SERVER_PATH = new URL("./a2a-agent-server.js", import.meta.url) .pathname; @@ -180,7 +180,9 @@ export class A2ATransport { child.on("error", (err) => { clearTimeout(timeout); reject( - new Error(`A2ATransport: failed to spawn agent ${agentName}: ${err.message}`), + new Error( + `A2ATransport: failed to spawn agent ${agentName}: ${err.message}`, + ), ); }); diff --git a/src/resources/extensions/sf/uok/auto-dispatch.js b/src/resources/extensions/sf/uok/auto-dispatch.js index e681305b1..bf7e8cf31 100644 --- a/src/resources/extensions/sf/uok/auto-dispatch.js +++ b/src/resources/extensions/sf/uok/auto-dispatch.js @@ -92,11 +92,14 @@ import { upsertValidationAttentionMarker, } from "../sf-db.js"; import { isClosedStatus, isInactiveStatus } from "../status-guards.js"; -import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js"; +import { extractVerdict, isAcceptableUatVerdict } from "../verdict-parser.js"; import { - buildDispatchEnvelope, - explainDispatch, -} from "./dispatch-envelope.js"; + checkNeedsReassessment, + checkNeedsRunUat, +} from "../workflow-helpers.js"; +import { logError, logWarning } from "../workflow-logger.js"; +import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js"; +import { buildDispatchEnvelope, explainDispatch } from "./dispatch-envelope.js"; import { selectReactiveDispatchBatch } from "./execution-graph.js"; import { resolveUokFlags } from "./flags.js"; import { UokGateRunner } from "./gate-runner.js"; @@ -105,12 +108,6 @@ import { decideUnitRuntimeDispatch, readUnitRuntimeRecord, } from "./unit-runtime.js"; -import { extractVerdict, isAcceptableUatVerdict } from "../verdict-parser.js"; -import { - checkNeedsReassessment, - checkNeedsRunUat, -} from "../workflow-helpers.js"; -import { logError, logWarning } from "../workflow-logger.js"; const MAX_PARALLEL_RESEARCH_SLICES = 8; const PARALLEL_RESEARCH_BLOCKING_PHASES = new Set([ diff --git a/src/resources/extensions/sf/uok/auto-runaway-guard.js b/src/resources/extensions/sf/uok/auto-runaway-guard.js index 1dc06a184..93179b4f8 100644 --- a/src/resources/extensions/sf/uok/auto-runaway-guard.js +++ b/src/resources/extensions/sf/uok/auto-runaway-guard.js @@ -17,10 +17,7 @@ export const DEFAULT_RUNAWAY_DIAGNOSTIC_TURNS = 2; export const DEFAULT_RUNAWAY_MIN_INTERVAL_MS = 120_000; const EXECUTE_NO_PROGRESS_TOOL_WARNING = 25; const EXECUTE_NO_PROGRESS_TOKEN_WARNING = 500_000; -const DURABLE_SF_ARTIFACT_PATHS = [ - ".sf/milestones", - ".sf/approvals", -]; +const DURABLE_SF_ARTIFACT_PATHS = [".sf/milestones", ".sf/approvals"]; let state = null; export function resetRunawayGuardState(unitType, unitId, baseline) { state = { diff --git a/src/resources/extensions/sf/uok/auto-unit-closeout.js b/src/resources/extensions/sf/uok/auto-unit-closeout.js index 19f9fc48d..7954b0558 100644 --- a/src/resources/extensions/sf/uok/auto-unit-closeout.js +++ b/src/resources/extensions/sf/uok/auto-unit-closeout.js @@ -6,8 +6,8 @@ import { saveActivityLog } from "../activity-log.js"; import { snapshotUnitMetrics } from "../metrics.js"; import { updateSubscriptionTokensUsed } from "../preferences-models.js"; -import { writeTurnGitTransaction } from "./gitops.js"; import { logWarning } from "../workflow-logger.js"; +import { writeTurnGitTransaction } from "./gitops.js"; /** * Snapshot metrics, save activity log, and fire-and-forget memory extraction * for a completed unit. Returns the activity log file path (if any). diff --git a/src/resources/extensions/sf/uok/auto-verification.js b/src/resources/extensions/sf/uok/auto-verification.js index 52a0f11b1..0b9b5c0dc 100644 --- a/src/resources/extensions/sf/uok/auto-verification.js +++ b/src/resources/extensions/sf/uok/auto-verification.js @@ -25,6 +25,15 @@ import { import { isMilestoneComplete } from "../state.js"; import { isClosedStatus } from "../status-guards.js"; import { parseUnitId } from "../unit-id.js"; +import { extractVerdict } from "../verdict-parser.js"; +import { writeVerificationJSON } from "../verification-evidence.js"; +import { + captureRuntimeErrors, + formatFailureContext, + runDependencyAudit, + runVerificationGate, +} from "../verification-gate.js"; +import { logError, logWarning } from "../workflow-logger.js"; import { ChaosMonkeyGate } from "./chaos-monkey.js"; import { CostGuardGate } from "./cost-guard-gate.js"; import { resolveUokFlags } from "./flags.js"; @@ -36,15 +45,6 @@ import { formatExecuteTaskRecoveryStatus, inspectExecuteTaskDurability, } from "./unit-runtime.js"; -import { extractVerdict } from "../verdict-parser.js"; -import { writeVerificationJSON } from "../verification-evidence.js"; -import { - captureRuntimeErrors, - formatFailureContext, - runDependencyAudit, - runVerificationGate, -} from "../verification-gate.js"; -import { logError, logWarning } from "../workflow-logger.js"; function computeTokenCountFromSession(ctx) { const entries = ctx.sessionManager?.getEntries?.() ?? []; diff --git a/src/resources/extensions/sf/uok/index.js b/src/resources/extensions/sf/uok/index.js index 9c3d7c8a1..1f6af1d8d 100644 --- a/src/resources/extensions/sf/uok/index.js +++ b/src/resources/extensions/sf/uok/index.js @@ -36,6 +36,40 @@ export { isAuditEnvelopeEnabled, setAuditEnvelopeEnabled, } from "./audit-toggle.js"; +// ─── Autonomous Dispatch ─────────────────────────────────────────────────── +export { + DISPATCH_RULES, + enhanceUnitRankingWithMemory, + extractValidationAttentionPlan, + formatTaskCompleteFailurePrompt, + getDispatchRuleNames, + getRewriteCount, + getUatCount, + incrementUatCount, + isVerificationNotApplicable, + resolveDispatch, + setRewriteCount, +} from "./auto-dispatch.js"; +// ─── Runaway Guard ──────────────────────────────────────────────────────── +export { + clearRunawayGuardState, + collectSessionTokenUsage, + collectWorktreeFingerprint, + countChangedFiles, + DEFAULT_RUNAWAY_CHANGED_FILES_WARNING, + DEFAULT_RUNAWAY_DIAGNOSTIC_TURNS, + DEFAULT_RUNAWAY_ELAPSED_MINUTES, + DEFAULT_RUNAWAY_MIN_INTERVAL_MS, + DEFAULT_RUNAWAY_TOKEN_WARNING, + DEFAULT_RUNAWAY_TOOL_CALL_WARNING, + evaluateRunawayGuard, + resetRunawayGuardState, + resolveRunawayGuardConfig, +} from "./auto-runaway-guard.js"; +// ─── Unit Closeout ──────────────────────────────────────────────────────── +export { closeoutUnit } from "./auto-unit-closeout.js"; +// ─── Post-Unit Verification ──────────────────────────────────────────────── +export { runPostUnitVerification } from "./auto-verification.js"; // ─── Gates ───────────────────────────────────────────────────────────────── export { ChaosMonkey, ChaosMonkeyGate } from "./chaos-monkey.js"; // ─── Model Policy ────────────────────────────────────────────────────────── @@ -222,37 +256,3 @@ export { nextWriteRecord, releaseWriterToken, } from "./writer.js"; -// ─── Autonomous Dispatch ─────────────────────────────────────────────────── -export { -DISPATCH_RULES, -enhanceUnitRankingWithMemory, -extractValidationAttentionPlan, -formatTaskCompleteFailurePrompt, -getDispatchRuleNames, -getRewriteCount, -getUatCount, -incrementUatCount, -isVerificationNotApplicable, -resolveDispatch, -setRewriteCount, -} from "./auto-dispatch.js"; -// ─── Runaway Guard ──────────────────────────────────────────────────────── -export { -clearRunawayGuardState, -collectSessionTokenUsage, -collectWorktreeFingerprint, -countChangedFiles, -DEFAULT_RUNAWAY_CHANGED_FILES_WARNING, -DEFAULT_RUNAWAY_DIAGNOSTIC_TURNS, -DEFAULT_RUNAWAY_ELAPSED_MINUTES, -DEFAULT_RUNAWAY_MIN_INTERVAL_MS, -DEFAULT_RUNAWAY_TOOL_CALL_WARNING, -DEFAULT_RUNAWAY_TOKEN_WARNING, -evaluateRunawayGuard, -resetRunawayGuardState, -resolveRunawayGuardConfig, -} from "./auto-runaway-guard.js"; -// ─── Unit Closeout ──────────────────────────────────────────────────────── -export { closeoutUnit } from "./auto-unit-closeout.js"; -// ─── Post-Unit Verification ──────────────────────────────────────────────── -export { runPostUnitVerification } from "./auto-verification.js"; diff --git a/src/resources/extensions/sf/uok/kernel.ts b/src/resources/extensions/sf/uok/kernel.ts index 111b5d02d..4d004ba99 100644 --- a/src/resources/extensions/sf/uok/kernel.ts +++ b/src/resources/extensions/sf/uok/kernel.ts @@ -12,15 +12,15 @@ import { randomUUID } from "node:crypto"; import { debugLog } from "../debug-logger.js"; import { -defaultPermissionProfileForRunControl, -resolvePermissionProfile, -resolveRunControlMode, -runControlModeForSession, + defaultPermissionProfileForRunControl, + resolvePermissionProfile, + resolveRunControlMode, + runControlModeForSession, } from "../operating-model.js"; import { -isDbAvailable, -recordUokRunExit, -recordUokRunStart, + isDbAvailable, + recordUokRunExit, + recordUokRunStart, } from "../sf-db.js"; import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js"; import { setAuditEnvelopeEnabled } from "./audit-toggle.js"; @@ -28,14 +28,14 @@ import { writeUokDiagnostics } from "./diagnostic-synthesis.js"; import { resolveUokFlags } from "./flags.js"; import { createTurnObserver } from "./loop-adapter.js"; import { -checkAndDrainMissingExit, -resetParityCommitBlock, -signalKernelEnter, + checkAndDrainMissingExit, + resetParityCommitBlock, + signalKernelEnter, } from "./parity-diff-capture.js"; import { -hasCurrentParityWarning, -writeParityHeartbeat, -writeParityReport, + hasCurrentParityWarning, + writeParityHeartbeat, + writeParityReport, } from "./parity-report.js"; // --------------------------------------------------------------------------- @@ -44,88 +44,92 @@ writeParityReport, /** Flags resolved from user preferences for this UOK run. */ interface UokFlags { -auditEnvelope?: boolean; -gitops?: boolean; -gitopsTurnAction?: string; -gitopsTurnPush?: boolean; -chaosMonkey?: boolean; -enabled?: boolean; -[key: string]: unknown; + auditEnvelope?: boolean; + gitops?: boolean; + gitopsTurnAction?: string; + gitopsTurnPush?: boolean; + chaosMonkey?: boolean; + enabled?: boolean; + [key: string]: unknown; } /** Lifecycle flags written to the DB run ledger and parity heartbeat. */ interface LifecycleFlags extends UokFlags { -runControl?: string; -permissionProfile?: string; -workMode?: string; -modelMode?: string; + runControl?: string; + permissionProfile?: string; + workMode?: string; + modelMode?: string; } export interface UokKernelTerminationArgs { -basePath: string; -runId?: string; -sessionId?: string | null; -flags?: UokFlags; -runControl?: string; -permissionProfile?: string; -/** Exit status — "ok" | "error" | "signal". Defaults to "signal". */ -status?: string; -error?: string; + basePath: string; + runId?: string; + sessionId?: string | null; + flags?: UokFlags; + runControl?: string; + permissionProfile?: string; + /** Exit status — "ok" | "error" | "signal". Defaults to "signal". */ + status?: string; + error?: string; } /** Arguments for {@link runAutoLoopWithUok}. */ export interface RunAutoLoopWithUokArgs { -/** Coding-agent context (opaque; provides sessionManager). */ -ctx: { -sessionManager?: { -getSessionId?: () => string | undefined; -}; -[key: string]: unknown; -}; -/** Pi/provider interface (opaque). */ -pi: unknown; -/** Mutable session state. */ -s: { -basePath: string; -workMode?: string; -modelMode?: string; -autoStartTime?: number; -currentUokRunId?: string; -[key: string]: unknown; -}; -/** Injected dependency bag. */ -deps: { -loadEffectiveSFPreferences?: () => { preferences?: Record } | null; -[key: string]: unknown; -}; -runControl?: string; -permissionProfile?: string; -/** The inner autonomous loop to execute inside the UOK wrapper. */ -runKernelLoop: ( -ctx: RunAutoLoopWithUokArgs["ctx"], -pi: unknown, -s: RunAutoLoopWithUokArgs["s"], -deps: Record, -) => Promise; + /** Coding-agent context (opaque; provides sessionManager). */ + ctx: { + sessionManager?: { + getSessionId?: () => string | undefined; + }; + [key: string]: unknown; + }; + /** Pi/provider interface (opaque). */ + pi: unknown; + /** Mutable session state. */ + s: { + basePath: string; + workMode?: string; + modelMode?: string; + autoStartTime?: number; + currentUokRunId?: string; + [key: string]: unknown; + }; + /** Injected dependency bag. */ + deps: { + loadEffectiveSFPreferences?: () => { + preferences?: Record; + } | null; + [key: string]: unknown; + }; + runControl?: string; + permissionProfile?: string; + /** The inner autonomous loop to execute inside the UOK wrapper. */ + runKernelLoop: ( + ctx: RunAutoLoopWithUokArgs["ctx"], + pi: unknown, + s: RunAutoLoopWithUokArgs["s"], + deps: Record, + ) => Promise; } // --------------------------------------------------------------------------- // Internals // --------------------------------------------------------------------------- -function refreshParityReport(basePath: string): ReturnType | null { -try { -return writeParityReport(basePath); -} catch (err) { -debugLog("uok-parity-report-write-failed", { -error: err instanceof Error ? err.message : String(err), -}); -return null; -} +function refreshParityReport( + basePath: string, +): ReturnType | null { + try { + return writeParityReport(basePath); + } catch (err) { + debugLog("uok-parity-report-write-failed", { + error: err instanceof Error ? err.message : String(err), + }); + return null; + } } function resolveKernelPathLabel(): string { -return "uok-kernel"; + return "uok-kernel"; } // --------------------------------------------------------------------------- @@ -142,53 +146,53 @@ return "uok-kernel"; * Consumer: auto signal cleanup and UOK parity tests. */ export function recordUokKernelTermination({ -basePath, -runId, -sessionId, -flags, -runControl, -permissionProfile, -status = "signal", -error, + basePath, + runId, + sessionId, + flags, + runControl, + permissionProfile, + status = "signal", + error, }: UokKernelTerminationArgs): ReturnType | null { -const endedAt = new Date().toISOString(); -const lifecycleFlags: LifecycleFlags = { -...(flags ?? {}), -...(runControl ? { runControl } : {}), -...(permissionProfile ? { permissionProfile } : {}), -}; -if (runId && isDbAvailable()) { -recordUokRunExit({ -runId, -sessionId, -path: resolveKernelPathLabel(), -flags: lifecycleFlags, -status, -endedAt, -...(error ? { error } : {}), -}); -} -writeParityHeartbeat(basePath, { -ts: endedAt, -...(runId ? { runId } : {}), -sessionId, -path: resolveKernelPathLabel(), -flags: lifecycleFlags, -...(runControl ? { runControl } : {}), -...(permissionProfile ? { permissionProfile } : {}), -phase: "exit", -status, -...(error ? { error } : {}), -}); -const report = refreshParityReport(basePath); -try { -writeUokDiagnostics(basePath); -} catch (err) { -debugLog("uok-diagnostics-write-failed", { -error: err instanceof Error ? err.message : String(err), -}); -} -return report; + const endedAt = new Date().toISOString(); + const lifecycleFlags: LifecycleFlags = { + ...(flags ?? {}), + ...(runControl ? { runControl } : {}), + ...(permissionProfile ? { permissionProfile } : {}), + }; + if (runId && isDbAvailable()) { + recordUokRunExit({ + runId, + sessionId, + path: resolveKernelPathLabel(), + flags: lifecycleFlags, + status, + endedAt, + ...(error ? { error } : {}), + }); + } + writeParityHeartbeat(basePath, { + ts: endedAt, + ...(runId ? { runId } : {}), + sessionId, + path: resolveKernelPathLabel(), + flags: lifecycleFlags, + ...(runControl ? { runControl } : {}), + ...(permissionProfile ? { permissionProfile } : {}), + phase: "exit", + status, + ...(error ? { error } : {}), + }); + const report = refreshParityReport(basePath); + try { + writeUokDiagnostics(basePath); + } catch (err) { + debugLog("uok-diagnostics-write-failed", { + error: err instanceof Error ? err.message : String(err), + }); + } + return report; } /** @@ -200,122 +204,127 @@ return report; * * Consumer: auto/dispatch.ts — called once per autonomous session activation. */ -export async function runAutoLoopWithUok(args: RunAutoLoopWithUokArgs): Promise { -const { ctx, pi, s, deps, runKernelLoop } = args; -const prefs = deps.loadEffectiveSFPreferences?.()?.preferences as Record | undefined; -const flags: UokFlags = { ...resolveUokFlags(prefs), enabled: true }; -const runControl: string = resolveRunControlMode( -args.runControl ?? runControlModeForSession(s), -); -const permissionProfile: string = resolvePermissionProfile( -args.permissionProfile ?? -(prefs?.uok as Record | undefined)?.permission_profile ?? -defaultPermissionProfileForRunControl(runControl), -); -// Include workMode and modelMode from session in lifecycle flags -const workMode: string = (s.workMode as string | undefined) ?? "chat"; -const modelMode: string = (s.modelMode as string | undefined) ?? "smart"; -const lifecycleFlags: LifecycleFlags = { -...flags, -runControl, -permissionProfile, -workMode, -modelMode, -}; +export async function runAutoLoopWithUok( + args: RunAutoLoopWithUokArgs, +): Promise { + const { ctx, pi, s, deps, runKernelLoop } = args; + const prefs = deps.loadEffectiveSFPreferences?.()?.preferences as + | Record + | undefined; + const flags: UokFlags = { ...resolveUokFlags(prefs), enabled: true }; + const runControl: string = resolveRunControlMode( + args.runControl ?? runControlModeForSession(s), + ); + const permissionProfile: string = resolvePermissionProfile( + args.permissionProfile ?? + (prefs?.uok as Record | undefined)?.permission_profile ?? + defaultPermissionProfileForRunControl(runControl), + ); + // Include workMode and modelMode from session in lifecycle flags + const workMode: string = (s.workMode as string | undefined) ?? "chat"; + const modelMode: string = (s.modelMode as string | undefined) ?? "smart"; + const lifecycleFlags: LifecycleFlags = { + ...flags, + runControl, + permissionProfile, + workMode, + modelMode, + }; -const healthVerdict = writeUokDiagnostics(s.basePath); -debugLog("uok-system-health-verdict", healthVerdict); + const healthVerdict = writeUokDiagnostics(s.basePath); + debugLog("uok-system-health-verdict", healthVerdict); -const previousReport = refreshParityReport(s.basePath); -const runId = `uok-${randomUUID()}`; -s.currentUokRunId = runId; -resetParityCommitBlock(); -if ( -previousReport && -(previousReport as { missingExitEvents?: number }).missingExitEvents != null && -(previousReport as { missingExitEvents: number }).missingExitEvents > 0 && -hasCurrentParityWarning(previousReport) -) { -checkAndDrainMissingExit( -(previousReport as { enterEvents: number }).enterEvents, -(previousReport as { exitEvents: number }).exitEvents, -); -} -setAuditEnvelopeEnabled(flags.auditEnvelope ?? false); -signalKernelEnter(); -const startedAt = new Date().toISOString(); -const sessionId = ctx.sessionManager?.getSessionId?.(); -if (isDbAvailable()) { -recordUokRunStart({ -runId, -sessionId, -path: resolveKernelPathLabel(), -flags: lifecycleFlags, -startedAt, -}); -} -writeParityHeartbeat(s.basePath, { -ts: startedAt, -runId, -sessionId, -path: resolveKernelPathLabel(), -flags: lifecycleFlags, -runControl, -permissionProfile, -phase: "enter", -}); -if (flags.auditEnvelope) { -emitUokAuditEvent( -s.basePath, -buildAuditEnvelope({ -traceId: `session:${String(s.autoStartTime || Date.now())}`, -category: "orchestration", -type: "uok-kernel-enter", -payload: { -flags: lifecycleFlags, -runControl, -permissionProfile, -workMode, -modelMode, -sessionId, -}, -}), -); -} -const decoratedDeps = { -...deps, -uokObserver: createTurnObserver({ -basePath: s.basePath, -gitAction: flags.gitopsTurnAction, -gitPush: flags.gitopsTurnPush, -enableAudit: flags.auditEnvelope, -enableGitops: flags.gitops, -enableChaosMonkey: flags.chaosMonkey, -runControl, -permissionProfile, -}), -uokRunControl: runControl, -uokPermissionProfile: permissionProfile, -}; -let status = "ok"; -let error: string | undefined; -try { -await runKernelLoop(ctx, pi, s, decoratedDeps); -} catch (err) { -status = "error"; -error = err instanceof Error ? err.message : String(err); -throw err; -} finally { -recordUokKernelTermination({ -basePath: s.basePath, -runId, -sessionId, -flags: lifecycleFlags, -runControl, -permissionProfile, -status, -...(error ? { error } : {}), -}); -if (s.currentUokRunId === runId) s.currentUokRunId = undefined; -} + const previousReport = refreshParityReport(s.basePath); + const runId = `uok-${randomUUID()}`; + s.currentUokRunId = runId; + resetParityCommitBlock(); + if ( + previousReport && + (previousReport as { missingExitEvents?: number }).missingExitEvents != + null && + (previousReport as { missingExitEvents: number }).missingExitEvents > 0 && + hasCurrentParityWarning(previousReport) + ) { + checkAndDrainMissingExit( + (previousReport as { enterEvents: number }).enterEvents, + (previousReport as { exitEvents: number }).exitEvents, + ); + } + setAuditEnvelopeEnabled(flags.auditEnvelope ?? false); + signalKernelEnter(); + const startedAt = new Date().toISOString(); + const sessionId = ctx.sessionManager?.getSessionId?.(); + if (isDbAvailable()) { + recordUokRunStart({ + runId, + sessionId, + path: resolveKernelPathLabel(), + flags: lifecycleFlags, + startedAt, + }); + } + writeParityHeartbeat(s.basePath, { + ts: startedAt, + runId, + sessionId, + path: resolveKernelPathLabel(), + flags: lifecycleFlags, + runControl, + permissionProfile, + phase: "enter", + }); + if (flags.auditEnvelope) { + emitUokAuditEvent( + s.basePath, + buildAuditEnvelope({ + traceId: `session:${String(s.autoStartTime || Date.now())}`, + category: "orchestration", + type: "uok-kernel-enter", + payload: { + flags: lifecycleFlags, + runControl, + permissionProfile, + workMode, + modelMode, + sessionId, + }, + }), + ); + } + const decoratedDeps = { + ...deps, + uokObserver: createTurnObserver({ + basePath: s.basePath, + gitAction: flags.gitopsTurnAction, + gitPush: flags.gitopsTurnPush, + enableAudit: flags.auditEnvelope, + enableGitops: flags.gitops, + enableChaosMonkey: flags.chaosMonkey, + runControl, + permissionProfile, + }), + uokRunControl: runControl, + uokPermissionProfile: permissionProfile, + }; + let status = "ok"; + let error: string | undefined; + try { + await runKernelLoop(ctx, pi, s, decoratedDeps); + } catch (err) { + status = "error"; + error = err instanceof Error ? err.message : String(err); + throw err; + } finally { + recordUokKernelTermination({ + basePath: s.basePath, + runId, + sessionId, + flags: lifecycleFlags, + runControl, + permissionProfile, + status, + ...(error ? { error } : {}), + }); + if (s.currentUokRunId === runId) s.currentUokRunId = undefined; + } } diff --git a/src/resources/extensions/sf/uok/trace-writer.js b/src/resources/extensions/sf/uok/trace-writer.js index 0ff139ad6..6c746a2da 100644 --- a/src/resources/extensions/sf/uok/trace-writer.js +++ b/src/resources/extensions/sf/uok/trace-writer.js @@ -64,15 +64,12 @@ export function readTraceEvents(basePath, type, windowHours = 24) { try { const filePath = join(dir, file); if (statSync(filePath).mtimeMs < cutoff) continue; - const lines = readFileSync(filePath, "utf-8") - .split("\n") - .filter(Boolean); + const lines = readFileSync(filePath, "utf-8").split("\n").filter(Boolean); for (const line of lines) { try { const ev = JSON.parse(line); if (!type || ev.type === type) { - if (!ev.ts || new Date(ev.ts).getTime() >= cutoff) - results.push(ev); + if (!ev.ts || new Date(ev.ts).getTime() >= cutoff) results.push(ev); } } catch { /* skip malformed lines */ diff --git a/src/resources/extensions/sf/worktree-resolver.js b/src/resources/extensions/sf/worktree-resolver.js index ce31badad..3cc0798e8 100644 --- a/src/resources/extensions/sf/worktree-resolver.js +++ b/src/resources/extensions/sf/worktree-resolver.js @@ -58,10 +58,7 @@ export class WorktreeResolver { rebuildGitService() { const gitConfig = this.deps.loadEffectiveSFPreferences()?.preferences?.git ?? {}; - this.s.gitService = new this.deps.GitService( - this.s.basePath, - gitConfig, - ); + this.s.gitService = new this.deps.GitService(this.s.basePath, gitConfig); } /** Restore basePath to originalBasePath and rebuild GitService. */ restoreToProjectRoot() { diff --git a/src/resources/extensions/shared/mod.js b/src/resources/extensions/shared/mod.js index c2495ca51..38388363e 100644 --- a/src/resources/extensions/shared/mod.js +++ b/src/resources/extensions/shared/mod.js @@ -7,8 +7,8 @@ export { normalizeStringArray, sparkline, stripAnsi, - truncateWithEllipsis, toPosixPath, + truncateWithEllipsis, } from "@singularity-forge/coding-agent"; export { parseFrontmatterMap, splitFrontmatter } from "./frontmatter.js"; export { diff --git a/src/resources/extensions/shared/path-display.js b/src/resources/extensions/shared/path-display.js new file mode 100644 index 000000000..92aa80e21 --- /dev/null +++ b/src/resources/extensions/shared/path-display.js @@ -0,0 +1,8 @@ +/** + * Compatibility shim — re-exports toPosixPath from @singularity-forge/coding-agent. + * + * The canonical implementation lives in packages/coding-agent/src/utils/path-display.ts. + * This file exists so the ~4 consumers that import "../shared/path-display.js" directly + * continue to work without changes. + */ +export { toPosixPath } from "@singularity-forge/coding-agent"; diff --git a/src/tests/app-smoke.test.ts b/src/tests/app-smoke.test.ts index 6154d2873..2c0ff4227 100644 --- a/src/tests/app-smoke.test.ts +++ b/src/tests/app-smoke.test.ts @@ -193,12 +193,7 @@ test("loader sets all 4 SF_ env vars and PI_PACKAGE_DIR", async (_t) => { const rel = p.slice(bundledExtensionsDir.length + 1); return rel.split(/[\\/]/)[0].replace(/\.(?:ts|js)$/, ""); }); - for (const core of [ - "sf", - "bg-shell", - "browser-tools", - "search-the-web", - ]) { + for (const core of ["sf", "bg-shell", "browser-tools", "search-the-web"]) { assert.ok( discoveredNames.includes(core), `core extension '${core}' is discoverable`, diff --git a/src/tests/mcp-client-oauth.test.ts b/src/tests/mcp-client-oauth.test.ts index ac5e9b5bb..2412d531b 100644 --- a/src/tests/mcp-client-oauth.test.ts +++ b/src/tests/mcp-client-oauth.test.ts @@ -12,8 +12,8 @@ */ import assert from "node:assert/strict"; -import { test } from "vitest"; import { buildHttpTransportOpts } from "@singularity-forge/coding-agent"; +import { test } from "vitest"; // ── Transport construction (SDK sanity checks) ─────────────────────────────── diff --git a/src/tests/native-search.test.ts b/src/tests/native-search.test.ts index 7cd45fb48..244197da4 100644 --- a/src/tests/native-search.test.ts +++ b/src/tests/native-search.test.ts @@ -1,11 +1,11 @@ import assert from "node:assert/strict"; -import { afterEach, test } from "vitest"; import { CUSTOM_SEARCH_TOOL_NAMES, MAX_NATIVE_SEARCHES_PER_SESSION, stripThinkingFromHistory, webSearchMiddleware, } from "@singularity-forge/coding-agent"; +import { afterEach, test } from "vitest"; import { BRAVE_TOOL_NAMES, registerNativeSearchHooks, @@ -99,7 +99,9 @@ test("applyToPayload injects web_search for Anthropic provider", async () => { tools: [{ name: "bash", type: "custom" }], }; - const result = webSearchMiddleware.applyToPayload(payload, { provider: "anthropic" }); + const result = webSearchMiddleware.applyToPayload(payload, { + provider: "anthropic", + }); const tools = (result as any)?.tools ?? payload.tools; const nativeTool = (tools as any[]).find( @@ -170,7 +172,9 @@ test("applyToPayload does NOT inject for claude model when provider is non-Anthr tools: [{ name: "bash", type: "custom" }], }; - const result = webSearchMiddleware.applyToPayload(payload, { provider: "copilot" }); + const result = webSearchMiddleware.applyToPayload(payload, { + provider: "copilot", + }); assert.equal( result, @@ -200,7 +204,9 @@ test("applyToPayload does NOT inject when provider is github-copilot", async () tools: [{ name: "bash", type: "custom" }], }; - const result = webSearchMiddleware.applyToPayload(payload, { provider: "github-copilot" }); + const result = webSearchMiddleware.applyToPayload(payload, { + provider: "github-copilot", + }); assert.equal( result, @@ -228,7 +234,9 @@ test("applyToPayload DOES inject when provider is anthropic", async () => { tools: [{ name: "bash", type: "custom" }], }; - const result = webSearchMiddleware.applyToPayload(payload, { provider: "anthropic" }); + const result = webSearchMiddleware.applyToPayload(payload, { + provider: "anthropic", + }); const tools = ((result as any)?.tools ?? payload.tools) as any[]; assert.ok( @@ -246,7 +254,9 @@ test("applyToPayload does not double-inject", async () => { tools: [{ type: "web_search_20250305", name: "web_search" }], }; - const result = webSearchMiddleware.applyToPayload(payload, { provider: "anthropic" }); + const result = webSearchMiddleware.applyToPayload(payload, { + provider: "anthropic", + }); assert.equal(result, undefined, "Should not modify when already injected"); const tools = payload.tools as any[]; @@ -261,7 +271,9 @@ test("applyToPayload creates tools array if missing", async () => { model: "claude-haiku-4-5-20251001", }; - const result = webSearchMiddleware.applyToPayload(payload, { provider: "anthropic" }); + const result = webSearchMiddleware.applyToPayload(payload, { + provider: "anthropic", + }); const tools = (result as any)?.tools ?? payload.tools; assert.ok(Array.isArray(tools), "Should create tools array"); diff --git a/tsconfig.extensions.json b/tsconfig.extensions.json index 4236ca0e9..0960890f5 100644 --- a/tsconfig.extensions.json +++ b/tsconfig.extensions.json @@ -15,9 +15,7 @@ ], "@singularity-forge/ai": ["./packages/ai/src/index.ts"], "@singularity-forge/ai/*": ["./packages/ai/src/*.ts"], - "@singularity-forge/agent-core": [ - "./packages/agent-core/src/index.ts" - ], + "@singularity-forge/agent-core": ["./packages/agent-core/src/index.ts"], "@singularity-forge/tui": ["./packages/tui/src/index.ts"], "@singularity-forge/native": ["./packages/native/src/index.ts"], "@singularity-forge/native/*": ["./packages/rust-engine/src/*/index.ts"], diff --git a/vitest.config.ts b/vitest.config.ts index 81036e14a..c90cfa21f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -62,10 +62,7 @@ export default defineConfig({ __dirname, "packages/ai/src/bedrock-provider.ts", ), - "@singularity-forge/ai": resolve( - __dirname, - "packages/ai/src/index.ts", - ), + "@singularity-forge/ai": resolve(__dirname, "packages/ai/src/index.ts"), "@singularity-forge/agent-core": resolve( __dirname, "packages/agent-core/src/index.ts", @@ -74,10 +71,7 @@ export default defineConfig({ __dirname, "packages/tui/src/fuzzy.ts", ), - "@singularity-forge/tui": resolve( - __dirname, - "packages/tui/src/index.ts", - ), + "@singularity-forge/tui": resolve(__dirname, "packages/tui/src/index.ts"), "@singularity-forge/native/ast": resolve( __dirname, "packages/native/src/ast/index.ts", diff --git a/web/components/sf/app-shell.tsx b/web/components/sf/app-shell.tsx index 2a1049b84..cd0e1c031 100644 --- a/web/components/sf/app-shell.tsx +++ b/web/components/sf/app-shell.tsx @@ -15,6 +15,7 @@ import { ChatMode } from "@/components/sf/chat-mode"; import { CommandSurface } from "@/components/sf/command-surface"; import { Dashboard } from "@/components/sf/dashboard"; import { DualTerminal } from "@/components/sf/dual-terminal"; +import { ErrorBoundary } from "@/components/sf/error-boundary"; import { FilesView } from "@/components/sf/files-view"; import { FocusedPanel } from "@/components/sf/focused-panel"; import { OnboardingGate } from "@/components/sf/onboarding-gate"; @@ -33,7 +34,6 @@ import { import { StatusBar } from "@/components/sf/status-bar"; import { UpdateBanner } from "@/components/sf/update-banner"; import { VisualizerView } from "@/components/sf/visualizer-view"; -import { ErrorBoundary } from "@/components/sf/error-boundary"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { getAuthToken } from "@/lib/auth"; diff --git a/web/components/sf/chat-mode.tsx b/web/components/sf/chat-mode.tsx index 2c14868a2..09beb0003 100644 --- a/web/components/sf/chat-mode.tsx +++ b/web/components/sf/chat-mode.tsx @@ -496,47 +496,48 @@ let chatHighlighterPromise: Promise | null = null; function getChatHighlighter(): Promise { if (!chatHighlighterPromise) { chatHighlighterPromise = import("shiki") - .then((mod) => - mod.createHighlighter({ - themes: ["github-dark-default", "github-light-default"], - langs: [ - "typescript", - "tsx", - "javascript", - "jsx", - "json", - "jsonc", - "markdown", - "mdx", - "css", - "scss", - "less", - "html", - "xml", - "yaml", - "toml", - "bash", - "python", - "ruby", - "rust", - "go", - "java", - "kotlin", - "swift", - "c", - "cpp", - "csharp", - "php", - "sql", - "graphql", - "dockerfile", - "makefile", - "lua", - "diff", - "ini", - "dotenv", - ], - }) as Promise, + .then( + (mod) => + mod.createHighlighter({ + themes: ["github-dark-default", "github-light-default"], + langs: [ + "typescript", + "tsx", + "javascript", + "jsx", + "json", + "jsonc", + "markdown", + "mdx", + "css", + "scss", + "less", + "html", + "xml", + "yaml", + "toml", + "bash", + "python", + "ruby", + "rust", + "go", + "java", + "kotlin", + "swift", + "c", + "cpp", + "csharp", + "php", + "sql", + "graphql", + "dockerfile", + "makefile", + "lua", + "diff", + "ini", + "dotenv", + ], + }) as Promise, ) .catch((err) => { chatHighlighterPromise = null; diff --git a/web/components/sf/file-content-viewer.tsx b/web/components/sf/file-content-viewer.tsx index 000b54ae0..787704eab 100644 --- a/web/components/sf/file-content-viewer.tsx +++ b/web/components/sf/file-content-viewer.tsx @@ -123,47 +123,48 @@ let highlighterPromise: Promise | null = null; async function getHighlighter(): Promise { if (!highlighterPromise) { highlighterPromise = import("shiki") - .then((mod) => - mod.createHighlighter({ - themes: ["github-dark-default", "github-light-default"], - langs: [ - "typescript", - "tsx", - "javascript", - "jsx", - "json", - "jsonc", - "markdown", - "mdx", - "css", - "scss", - "less", - "html", - "xml", - "yaml", - "toml", - "bash", - "python", - "ruby", - "rust", - "go", - "java", - "kotlin", - "swift", - "c", - "cpp", - "csharp", - "php", - "sql", - "graphql", - "dockerfile", - "makefile", - "lua", - "diff", - "ini", - "dotenv", - ], - }) as Promise, + .then( + (mod) => + mod.createHighlighter({ + themes: ["github-dark-default", "github-light-default"], + langs: [ + "typescript", + "tsx", + "javascript", + "jsx", + "json", + "jsonc", + "markdown", + "mdx", + "css", + "scss", + "less", + "html", + "xml", + "yaml", + "toml", + "bash", + "python", + "ruby", + "rust", + "go", + "java", + "kotlin", + "swift", + "c", + "cpp", + "csharp", + "php", + "sql", + "graphql", + "dockerfile", + "makefile", + "lua", + "diff", + "ini", + "dotenv", + ], + }) as Promise, ) .catch((err) => { // Reset so the next call retries instead of returning a rejected promise forever diff --git a/web/components/ui/chart.tsx b/web/components/ui/chart.tsx index ede39246a..101a73ec8 100644 --- a/web/components/ui/chart.tsx +++ b/web/components/ui/chart.tsx @@ -253,7 +253,10 @@ function ChartLegendContent({ verticalAlign = "bottom", nameKey, }: React.ComponentProps<"div"> & - Pick & { + Pick< + RechartsPrimitive.DefaultLegendContentProps, + "payload" | "verticalAlign" + > & { hideIcon?: boolean; nameKey?: string; }) { diff --git a/web/components/ui/resizable.tsx b/web/components/ui/resizable.tsx index 9374fa957..a1009ced6 100644 --- a/web/components/ui/resizable.tsx +++ b/web/components/ui/resizable.tsx @@ -1,7 +1,7 @@ "use client"; import { GripVerticalIcon } from "lucide-react"; -import * as React from "react"; +import type * as React from "react"; import { Group, Panel, Separator } from "react-resizable-panels"; import { cn } from "@/lib/utils"; @@ -22,9 +22,7 @@ function ResizablePanelGroup({ ); } -function ResizablePanel({ - ...props -}: React.ComponentProps) { +function ResizablePanel({ ...props }: React.ComponentProps) { return ; } diff --git a/web/lib/__tests__/onboarding-logic.test.ts b/web/lib/__tests__/onboarding-logic.test.ts index 27aaf6e96..41f4694c0 100644 --- a/web/lib/__tests__/onboarding-logic.test.ts +++ b/web/lib/__tests__/onboarding-logic.test.ts @@ -163,12 +163,17 @@ describe("resolveOnboardingLockReason", () => { describe("redactSensitiveText", () => { test("redact_whenApiKeyInText_replacesWithPlaceholder", () => { const result = redactSensitiveText("key: sk-abcdef1234567890"); - assert.ok(!result.includes("sk-abcdef"), `expected redaction, got: ${result}`); + assert.ok( + !result.includes("sk-abcdef"), + `expected redaction, got: ${result}`, + ); assert.ok(result.includes("[redacted]")); }); test("redact_whenBearerToken_replacesWithPlaceholder", () => { - const result = redactSensitiveText("Authorization: Bearer eyJhbGciOiJSUzI1NiJ9"); + const result = redactSensitiveText( + "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9", + ); assert.ok(result.includes("Bearer [redacted]"), `got: ${result}`); }); @@ -186,14 +191,14 @@ describe("redactSensitiveText", () => { describe("extractErrorDetail", () => { test("extractErrorDetail_whenString_returnsItself", () => { - assert.equal(extractErrorDetail("something went wrong"), "something went wrong"); + assert.equal( + extractErrorDetail("something went wrong"), + "something went wrong", + ); }); test("extractErrorDetail_whenObjectWithMessage_returnsMessage", () => { - assert.equal( - extractErrorDetail({ message: "auth failed" }), - "auth failed", - ); + assert.equal(extractErrorDetail({ message: "auth failed" }), "auth failed"); }); test("extractErrorDetail_whenObjectWithError_returnsError", () => { diff --git a/web/lib/workflow-actions.ts b/web/lib/workflow-actions.ts index 955863916..4118adff3 100644 --- a/web/lib/workflow-actions.ts +++ b/web/lib/workflow-actions.ts @@ -96,7 +96,11 @@ export function deriveWorkflowAction( // Auto is not active if (phase === "complete") { // All milestones done — surface a distinct "New Milestone" action - primary = { label: "New Milestone", command: "/new-milestone", variant: "default" }; + primary = { + label: "New Milestone", + command: "/new-milestone", + variant: "default", + }; isNewMilestone = true; } else if (phase === "planning") { primary = { label: "Plan", command: "/discuss", variant: "default" };