diff --git a/biome.json b/biome.json index 91247fcf7..e18a65b2d 100644 --- a/biome.json +++ b/biome.json @@ -29,7 +29,8 @@ "recommended": true, "correctness": { "noUnreachable": "off", - "useExhaustiveDependencies": "off" + "useExhaustiveDependencies": "off", + "noUnusedImports": "off" }, "a11y": { "noLabelWithoutControl": "off", @@ -69,12 +70,12 @@ "tailwindDirectives": true } }, - "assist": { - "enabled": true, - "actions": { - "source": { - "organizeImports": "on" + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "off" + } } } } -} diff --git a/packages/ai/src/model-identity.ts b/packages/ai/src/model-identity.ts index c135caf5b..2c28d7ec1 100644 --- a/packages/ai/src/model-identity.ts +++ b/packages/ai/src/model-identity.ts @@ -9,6 +9,8 @@ import type { Api, Model } from "./types.js"; * * Consumer: pi-ai provider implementations when building request payloads. */ -export function resolveWireModelId(model: Model): string { +export function resolveWireModelId( + model: Model, +): string { return model.wireModelId?.trim() || model.id; } diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 49712bfa4..5e2cd4def 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -1,5 +1,9 @@ -import { supportsXhigh } from "../models.js"; +import { + type CodexAppServerNotification, + getCodexAppServerClient, +} from "@singularity-forge/openai-codex-provider"; import { resolveWireModelId } from "../model-identity.js"; +import { supportsXhigh } from "../models.js"; import type { Api, AssistantMessage, @@ -14,10 +18,6 @@ import type { } from "../types.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { parseStreamingJson } from "../utils/json-parse.js"; -import { - type CodexAppServerNotification, - getCodexAppServerClient, -} from "@singularity-forge/openai-codex-provider"; import { convertResponsesMessages } from "./openai-responses-shared.js"; import { buildBaseOptions, diff --git a/packages/ai/src/providers/openai-completions.test.ts b/packages/ai/src/providers/openai-completions.test.ts index 223a34c87..47937a3f7 100644 --- a/packages/ai/src/providers/openai-completions.test.ts +++ b/packages/ai/src/providers/openai-completions.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { describe, it } from "vitest"; -import type { Context, Model, OpenAICompletionsCompat } from "../types.js"; import { resolveWireModelId } from "../model-identity.js"; +import type { Context, Model, OpenAICompletionsCompat } from "../types.js"; import { convertMessages, streamOpenAICompletions, diff --git a/packages/coding-agent/src/cli/list-models.test.ts b/packages/coding-agent/src/cli/list-models.test.ts index 0e7b8a733..684c464ff 100644 --- a/packages/coding-agent/src/cli/list-models.test.ts +++ b/packages/coding-agent/src/cli/list-models.test.ts @@ -73,7 +73,10 @@ describe("listModels", () => { m.provider === "zai" && m.id === "glm-5.1", } as unknown as ModelRegistry; - await listModels(registry, { searchPattern: "zai", _discoveryCacheFilePath: NO_CACHE }); + await listModels(registry, { + searchPattern: "zai", + _discoveryCacheFilePath: NO_CACHE, + }); const rendered = output.join("\n"); assert.match(rendered, /glm-5\.1/); @@ -98,7 +101,10 @@ describe("listModels", () => { isDiscovered: () => false, } as unknown as ModelRegistry; - await listModels(registry, { discover: true, _discoveryCacheFilePath: NO_CACHE }); + await listModels(registry, { + discover: true, + _discoveryCacheFilePath: NO_CACHE, + }); const rendered = output.join("\n"); assert.doesNotMatch(rendered, /zai/); @@ -108,13 +114,14 @@ describe("listModels", () => { describe("listModels – discovery cache merge", () => { /** Write a minimal discovery-cache.json to a temp file and return its path */ - function writeTempCache(entries: Record): string { - const path = join(tmpdir(), `discovery-cache-test-${Date.now()}-${Math.random().toString(36).slice(2)}.json`); - writeFileSync( - path, - JSON.stringify({ version: 1, entries }), - "utf-8", + function writeTempCache( + entries: Record, + ): string { + const path = join( + tmpdir(), + `discovery-cache-test-${Date.now()}-${Math.random().toString(36).slice(2)}.json`, ); + writeFileSync(path, JSON.stringify({ version: 1, entries }), "utf-8"); return path; } @@ -122,8 +129,22 @@ describe("listModels – discovery cache merge", () => { const cachePath = writeTempCache({ opencode: { models: [ - { id: "big-pickle", name: "big-pickle", provider: "opencode", api: "openai-completions", baseUrl: "https://opencode.ai/zen", input: ["text"] }, - { id: "gpt-5-nano", name: "gpt-5-nano", provider: "opencode", api: "openai-completions", baseUrl: "https://opencode.ai/zen", input: ["text"] }, + { + id: "big-pickle", + name: "big-pickle", + provider: "opencode", + api: "openai-completions", + baseUrl: "https://opencode.ai/zen", + input: ["text"], + }, + { + id: "gpt-5-nano", + name: "gpt-5-nano", + provider: "opencode", + api: "openai-completions", + baseUrl: "https://opencode.ai/zen", + input: ["text"], + }, ], }, }); @@ -165,7 +186,11 @@ describe("listModels – discovery cache merge", () => { assert.match(rendered, /live-only-0/); // Total opencode-go rows = 15 (9 static + 6 new from cache) const lines = rendered.split("\n").filter((l) => l.includes("opencode-go")); - assert.equal(lines.length, 15, `expected 15 opencode-go rows, got ${lines.length}`); + assert.equal( + lines.length, + 15, + `expected 15 opencode-go rows, got ${lines.length}`, + ); }); it("gracefully handles missing discovery cache file — falls back to static only", async () => { @@ -196,7 +221,14 @@ describe("listModels – discovery cache merge", () => { it("synthesized cache entries are marked [discovered] in output", async () => { const cachePath = writeTempCache({ "kimi-coding": { - models: [{ id: "kimi-for-coding", provider: "kimi-coding", api: "openai-completions", input: ["text"] }], + models: [ + { + id: "kimi-for-coding", + provider: "kimi-coding", + api: "openai-completions", + input: ["text"], + }, + ], }, }); @@ -206,8 +238,14 @@ describe("listModels – discovery cache merge", () => { }); const rendered = output.join("\n"); - const kimiLine = rendered.split("\n").find((l) => l.includes("kimi-for-coding")); + const kimiLine = rendered + .split("\n") + .find((l) => l.includes("kimi-for-coding")); assert.ok(kimiLine, "kimi-for-coding should appear in output"); - assert.match(kimiLine, /\[discovered\]/, "cache entry should be marked [discovered]"); + assert.match( + kimiLine, + /\[discovered\]/, + "cache entry should be marked [discovered]", + ); }); }); diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index c0d3eb7bd..419cd6370 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -684,9 +684,9 @@ function createExtensionAPI( runtime.setThinkingLevel(level); }, - setFallbackUnitContext(ctx: { unitType: string; unitId: string } | null) { - runtime.setFallbackUnitContext(ctx); - }, + setFallbackUnitContext(ctx: { unitType: string; unitId: string } | null) { + runtime.setFallbackUnitContext(ctx); + }, registerProvider(name: string, config: ProviderConfig) { runtime.registerProvider(name, config); diff --git a/packages/coding-agent/src/modes/interactive/resource-display.ts b/packages/coding-agent/src/modes/interactive/resource-display.ts index 1995424c5..209b5c807 100644 --- a/packages/coding-agent/src/modes/interactive/resource-display.ts +++ b/packages/coding-agent/src/modes/interactive/resource-display.ts @@ -29,9 +29,7 @@ export function formatDisplayPath(p: string): string { * local paths fall back to formatDisplayPath. */ export function getShortPath(fullPath: string, source: string): string { - const npmMatch = fullPath.match( - /node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/, - ); + const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/); if (npmMatch && source.startsWith("npm:")) { return npmMatch[2]; } @@ -170,9 +168,7 @@ export function formatScopeGroups( ); for (const [source, paths] of sortedPackages) { lines.push(` ${theme.fg("mdLink", source)}`); - const sortedPackagePaths = [...paths].sort((a, b) => - a.localeCompare(b), - ); + const sortedPackagePaths = [...paths].sort((a, b) => a.localeCompare(b)); for (const p of sortedPackagePaths) { lines.push( theme.fg("dim", ` ${options.formatPackagePath(p, source)}`), @@ -278,10 +274,7 @@ export function formatDiagnostics( theme.fg(d.type === "error" ? "error" : "warning", ` ${sourceInfo}`), ); lines.push( - theme.fg( - d.type === "error" ? "error" : "warning", - ` ${d.message}`, - ), + theme.fg(d.type === "error" ? "error" : "warning", ` ${d.message}`), ); } else { lines.push( diff --git a/packages/coding-agent/src/modes/rpc/rpc-client.ts b/packages/coding-agent/src/modes/rpc/rpc-client.ts index 3e5b7c801..2757b4136 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-client.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-client.ts @@ -795,7 +795,7 @@ export class RpcClient { `Timeout waiting for response to ${command.type}. Stderr: ${this.stderr}`, ), ); - }, timeoutMs); + }, timeoutMs); this.pendingRequests.set(id, { resolve: (response) => { diff --git a/packages/google-gemini-cli-provider/src/index.d.ts b/packages/google-gemini-cli-provider/src/index.d.ts index 103b4cd58..c1f5b9235 100644 --- a/packages/google-gemini-cli-provider/src/index.d.ts +++ b/packages/google-gemini-cli-provider/src/index.d.ts @@ -1,8 +1,8 @@ import { type ContentGenerator } from "@google/gemini-cli-core/dist/src/core/contentGenerator.js"; export interface GeminiCliContentGeneratorOptions { - modelId: string; - cwd?: string; - targetDir?: string; + modelId: string; + cwd?: string; + targetDir?: string; } /** * Create a Gemini CLI Core content generator for a model. @@ -12,15 +12,17 @@ export interface GeminiCliContentGeneratorOptions { * * Consumer: the Google Gemini provider in pi-ai. */ -export declare function createGeminiCliContentGenerator(options: GeminiCliContentGeneratorOptions): Promise; +export declare function createGeminiCliContentGenerator( + options: GeminiCliContentGeneratorOptions, +): Promise; /** * Per-model quota bucket from CodeAssistServer.retrieveUserQuota. */ export interface GeminiQuotaBucket { - modelId: string; - usedFraction: number; - remainingFraction: number; - resetTime?: string; + modelId: string; + usedFraction: number; + remainingFraction: number; + resetTime?: string; } /** * Snapshot of the active gemini-cli account: tier identity, project, and the @@ -31,22 +33,22 @@ export interface GeminiQuotaBucket { * them together avoids three separate OAuth round trips. */ export interface GeminiAccountSnapshot { - projectId: string; - /** Active tier id from setupUser.userTier (e.g. "free-tier", "standard-tier"). */ - userTierId?: string; - /** Active tier human label from setupUser.userTierName. */ - userTierName?: string; - /** - * Paid tier descriptor when the account has one (e.g. AI Ultra). Carries - * id like "g1-ultra-tier" and the marketing name. Distinct from the - * effective userTier — a free-tier session can still have a paidTier - * marker if the underlying account is subscribed. - */ - paidTier?: { - id?: string; - name?: string; - }; - models: GeminiQuotaBucket[]; + projectId: string; + /** Active tier id from setupUser.userTier (e.g. "free-tier", "standard-tier"). */ + userTierId?: string; + /** Active tier human label from setupUser.userTierName. */ + userTierName?: string; + /** + * Paid tier descriptor when the account has one (e.g. AI Ultra). Carries + * id like "g1-ultra-tier" and the marketing name. Distinct from the + * effective userTier — a free-tier session can still have a paidTier + * marker if the underlying account is subscribed. + */ + paidTier?: { + id?: string; + name?: string; + }; + models: GeminiQuotaBucket[]; } /** * Discover the active gemini-cli account: tier, project, and every model the @@ -57,10 +59,14 @@ export interface GeminiAccountSnapshot { * * Consumer: SF-side background catalog cache, usage UI, capacity diagnostics. */ -export declare function snapshotGeminiCliAccount(cwd?: string): Promise; +export declare function snapshotGeminiCliAccount( + cwd?: string, +): Promise; /** * Convenience wrapper: just the model IDs the active gemini-cli account has * access to. Returns null on failure (same contract as snapshotGeminiCliAccount). */ -export declare function discoverGeminiCliModels(cwd?: string): Promise; -//# sourceMappingURL=index.d.ts.map \ No newline at end of file +export declare function discoverGeminiCliModels( + cwd?: string, +): Promise; +//# sourceMappingURL=index.d.ts.map diff --git a/packages/google-gemini-cli-provider/src/index.js b/packages/google-gemini-cli-provider/src/index.js index f2ec44db9..2426aa955 100644 --- a/packages/google-gemini-cli-provider/src/index.js +++ b/packages/google-gemini-cli-provider/src/index.js @@ -8,8 +8,17 @@ * Consumer: `@singularity-forge/ai` Google Gemini provider, plus SF-side * background catalog discovery. */ -import { AuthType, CodeAssistServer, getOauthClient, makeFakeConfig, setupUser, } from "@google/gemini-cli-core"; -import { createContentGenerator, createContentGeneratorConfig, } from "@google/gemini-cli-core/dist/src/core/contentGenerator.js"; +import { + AuthType, + CodeAssistServer, + getOauthClient, + makeFakeConfig, + setupUser, +} from "@google/gemini-cli-core"; +import { + createContentGenerator, + createContentGeneratorConfig, +} from "@google/gemini-cli-core/dist/src/core/contentGenerator.js"; /** * Create a Gemini CLI Core content generator for a model. * @@ -19,14 +28,17 @@ import { createContentGenerator, createContentGeneratorConfig, } from "@google/g * Consumer: the Google Gemini provider in pi-ai. */ export async function createGeminiCliContentGenerator(options) { - const cwd = options.cwd ?? process.cwd(); - const config = makeFakeConfig({ - model: options.modelId, - cwd, - targetDir: options.targetDir ?? cwd, - }); - const generatorConfig = await createContentGeneratorConfig(config, AuthType.LOGIN_WITH_GOOGLE); - return createContentGenerator(generatorConfig, config); + const cwd = options.cwd ?? process.cwd(); + const config = makeFakeConfig({ + model: options.modelId, + cwd, + targetDir: options.targetDir ?? cwd, + }); + const generatorConfig = await createContentGeneratorConfig( + config, + AuthType.LOGIN_WITH_GOOGLE, + ); + return createContentGenerator(generatorConfig, config); } /** * Discover the active gemini-cli account: tier, project, and every model the @@ -38,62 +50,61 @@ export async function createGeminiCliContentGenerator(options) { * Consumer: SF-side background catalog cache, usage UI, capacity diagnostics. */ export async function snapshotGeminiCliAccount(cwd) { - try { - const config = makeFakeConfig({ cwd: cwd ?? process.cwd() }); - const authClient = await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, config); - const userData = await setupUser(authClient, config); - const projectId = userData?.projectId; - if (!projectId || typeof projectId !== "string") - return null; - const server = new CodeAssistServer(authClient, projectId, { headers: {} }); - const data = await server.retrieveUserQuota({ project: projectId }); - // Dedup buckets per modelId, keeping the WORST quota (lowest - // remainingFraction). Code Assist sometimes returns multiple buckets - // for the same model when more than one quota window applies; the - // pessimistic choice is what every consumer (UI, capacity diagnostics, - // model picker) actually wants to surface. - const byModel = new Map(); - for (const b of data?.buckets ?? []) { - const modelId = typeof b.modelId === "string" ? b.modelId : ""; - if (!modelId) - continue; - const remainingFraction = typeof b.remainingFraction === "number" ? b.remainingFraction : 1; - const bucket = { - modelId, - usedFraction: 1 - remainingFraction, - remainingFraction, - resetTime: typeof b.resetTime === "string" ? b.resetTime : undefined, - }; - const existing = byModel.get(modelId); - if (!existing || bucket.remainingFraction < existing.remainingFraction) { - byModel.set(modelId, bucket); - } - } - const models = Array.from(byModel.values()).sort((a, b) => a.modelId.localeCompare(b.modelId)); - if (models.length === 0) - return null; - return { - projectId, - userTierId: typeof userData?.userTier === "string" ? userData.userTier : undefined, - userTierName: userData?.userTierName, - paidTier: userData?.paidTier - ? { id: userData.paidTier.id, name: userData.paidTier.name } - : undefined, - models, - }; - } - catch { - return null; - } + try { + const config = makeFakeConfig({ cwd: cwd ?? process.cwd() }); + const authClient = await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, config); + const userData = await setupUser(authClient, config); + const projectId = userData?.projectId; + if (!projectId || typeof projectId !== "string") return null; + const server = new CodeAssistServer(authClient, projectId, { headers: {} }); + const data = await server.retrieveUserQuota({ project: projectId }); + // Dedup buckets per modelId, keeping the WORST quota (lowest + // remainingFraction). Code Assist sometimes returns multiple buckets + // for the same model when more than one quota window applies; the + // pessimistic choice is what every consumer (UI, capacity diagnostics, + // model picker) actually wants to surface. + const byModel = new Map(); + for (const b of data?.buckets ?? []) { + const modelId = typeof b.modelId === "string" ? b.modelId : ""; + if (!modelId) continue; + const remainingFraction = + typeof b.remainingFraction === "number" ? b.remainingFraction : 1; + const bucket = { + modelId, + usedFraction: 1 - remainingFraction, + remainingFraction, + resetTime: typeof b.resetTime === "string" ? b.resetTime : undefined, + }; + const existing = byModel.get(modelId); + if (!existing || bucket.remainingFraction < existing.remainingFraction) { + byModel.set(modelId, bucket); + } + } + const models = Array.from(byModel.values()).sort((a, b) => + a.modelId.localeCompare(b.modelId), + ); + if (models.length === 0) return null; + return { + projectId, + userTierId: + typeof userData?.userTier === "string" ? userData.userTier : undefined, + userTierName: userData?.userTierName, + paidTier: userData?.paidTier + ? { id: userData.paidTier.id, name: userData.paidTier.name } + : undefined, + models, + }; + } catch { + return null; + } } /** * Convenience wrapper: just the model IDs the active gemini-cli account has * access to. Returns null on failure (same contract as snapshotGeminiCliAccount). */ export async function discoverGeminiCliModels(cwd) { - const snap = await snapshotGeminiCliAccount(cwd); - if (!snap) - return null; - return snap.models.map((m) => m.modelId); + const snap = await snapshotGeminiCliAccount(cwd); + if (!snap) return null; + return snap.models.map((m) => m.modelId); } -//# sourceMappingURL=index.js.map \ No newline at end of file +//# sourceMappingURL=index.js.map diff --git a/packages/google-gemini-cli-provider/src/index.test.d.ts b/packages/google-gemini-cli-provider/src/index.test.d.ts index 121d59b38..6fb063f68 100644 --- a/packages/google-gemini-cli-provider/src/index.test.d.ts +++ b/packages/google-gemini-cli-provider/src/index.test.d.ts @@ -1,2 +1,2 @@ export {}; -//# sourceMappingURL=index.test.d.ts.map \ No newline at end of file +//# sourceMappingURL=index.test.d.ts.map diff --git a/packages/google-gemini-cli-provider/src/index.test.js b/packages/google-gemini-cli-provider/src/index.test.js index 5a10b0771..8f67f4f82 100644 --- a/packages/google-gemini-cli-provider/src/index.test.js +++ b/packages/google-gemini-cli-provider/src/index.test.js @@ -1,35 +1,38 @@ import assert from "node:assert/strict"; import { describe, test, vi } from "vitest"; + const helperState = vi.hoisted(() => ({ - authType: undefined, - configParams: undefined, + authType: undefined, + configParams: undefined, })); vi.mock("@google/gemini-cli-core", () => ({ - AuthType: { LOGIN_WITH_GOOGLE: "LOGIN_WITH_GOOGLE" }, - makeFakeConfig: vi.fn((params) => { - helperState.configParams = params; - return { params }; - }), + AuthType: { LOGIN_WITH_GOOGLE: "LOGIN_WITH_GOOGLE" }, + makeFakeConfig: vi.fn((params) => { + helperState.configParams = params; + return { params }; + }), })); vi.mock("@google/gemini-cli-core/dist/src/core/contentGenerator.js", () => ({ - createContentGeneratorConfig: vi.fn(async (_config, authType) => { - helperState.authType = authType; - return { authType }; - }), - createContentGenerator: vi.fn(async () => ({ - async generateContentStream() { - return (async function* emptyStream() { })(); - }, - })), + createContentGeneratorConfig: vi.fn(async (_config, authType) => { + helperState.authType = authType; + return { authType }; + }), + createContentGenerator: vi.fn(async () => ({ + async generateContentStream() { + return (async function* emptyStream() {})(); + }, + })), })); + import { createGeminiCliContentGenerator } from "./index.js"; + describe("google-gemini-cli-provider", () => { - test("createGeminiCliContentGenerator_uses_google_login_auth", async () => { - await createGeminiCliContentGenerator({ modelId: "gemini-3-pro" }); - assert.equal(helperState.authType, "LOGIN_WITH_GOOGLE"); - assert.equal(helperState.configParams?.model, "gemini-3-pro"); - assert.equal(helperState.configParams?.cwd, process.cwd()); - assert.equal(helperState.configParams?.targetDir, process.cwd()); - }); + test("createGeminiCliContentGenerator_uses_google_login_auth", async () => { + await createGeminiCliContentGenerator({ modelId: "gemini-3-pro" }); + assert.equal(helperState.authType, "LOGIN_WITH_GOOGLE"); + assert.equal(helperState.configParams?.model, "gemini-3-pro"); + assert.equal(helperState.configParams?.cwd, process.cwd()); + assert.equal(helperState.configParams?.targetDir, process.cwd()); + }); }); -//# sourceMappingURL=index.test.js.map \ No newline at end of file +//# sourceMappingURL=index.test.js.map diff --git a/packages/google-gemini-cli-provider/src/index.ts b/packages/google-gemini-cli-provider/src/index.ts index 81105e817..c1e005c3d 100644 --- a/packages/google-gemini-cli-provider/src/index.ts +++ b/packages/google-gemini-cli-provider/src/index.ts @@ -120,8 +120,7 @@ export async function snapshotGeminiCliAccount( modelId, usedFraction: 1 - remainingFraction, remainingFraction, - resetTime: - typeof b.resetTime === "string" ? b.resetTime : undefined, + resetTime: typeof b.resetTime === "string" ? b.resetTime : undefined, }; const existing = byModel.get(modelId); if (!existing || bucket.remainingFraction < existing.remainingFraction) { diff --git a/packages/openai-codex-provider/src/index.ts b/packages/openai-codex-provider/src/index.ts index fa2fc63fd..900776f0d 100644 --- a/packages/openai-codex-provider/src/index.ts +++ b/packages/openai-codex-provider/src/index.ts @@ -18,11 +18,11 @@ */ export { CodexAppServerClient, - clearCodexAppServerClient, - getCodexAppServerClient, type CodexAppServerClientOptions, type CodexAppServerNotification, type CodexAppServerNotificationHandler, + clearCodexAppServerClient, + getCodexAppServerClient, } from "./codex-app-server-client.js"; export { diff --git a/scripts/validate-model-cost-table.mjs b/scripts/validate-model-cost-table.mjs index ff1009bbf..1e5468764 100644 --- a/scripts/validate-model-cost-table.mjs +++ b/scripts/validate-model-cost-table.mjs @@ -19,8 +19,8 @@ */ import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { resolve, dirname } from "node:path"; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = resolve(__dirname, ".."); @@ -51,7 +51,7 @@ if (!arrayMatch) { let extensionTable; try { - // biome-ignore lint/security/noEval: parsing a static, local-file data literal + // biome-ignore lint/security/noGlobalEval: parsing a static, local-file data literal extensionTable = eval(`(${arrayMatch[1]})`); } catch (err) { console.error("❌ Failed to evaluate BUNDLED_COST_TABLE:", err.message); diff --git a/src/cli-key.ts b/src/cli-key.ts index bd815c212..90d9a7e6f 100644 --- a/src/cli-key.ts +++ b/src/cli-key.ts @@ -154,7 +154,8 @@ function handleKeySet( const existing = auth .getCredentialsForProvider(provider) .find((c) => c.type === "api_key"); - const oldDisplay = existing?.type === "api_key" ? maskKeyLast4(existing.key) : "(none)"; + const oldDisplay = + existing?.type === "api_key" ? maskKeyLast4(existing.key) : "(none)"; // Replace: remove old api_key entries for this provider, then add new one. // We preserve OAuth credentials by reconstructing without api_key entries. @@ -333,8 +334,7 @@ export async function runKeyCommand( process.stderr.write("Usage: sf key remove [--yes]\n"); return 1; } - const yes = - argv.includes("--yes") || argv.includes("-y"); + const yes = argv.includes("--yes") || argv.includes("-y"); return await handleKeyRemove(auth, provider, yes); } diff --git a/src/cli.ts b/src/cli.ts index 07325cbf6..e6812766e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -765,7 +765,10 @@ if (cliFlags.listModels !== undefined) { const { getKeyManagerAuthStorage } = await import( "./resources/extensions/sf/key-manager.js" ); - await refreshSfManagedProviders(process.cwd(), getKeyManagerAuthStorage()); + await refreshSfManagedProviders( + process.cwd(), + getKeyManagerAuthStorage(), + ); } catch { // Non-fatal — never block model listing. } @@ -809,7 +812,10 @@ if (cliFlags.maintain) { await runGeminiCatalogRefreshIfStale(process.cwd()); await runOpenaiCodexCatalogRefreshIfStale(process.cwd()); await runProviderQuotaRefreshIfStale(process.cwd(), auth); - const prefs = (loadEffectiveSFPreferences()?.preferences ?? {}) as Record; + const prefs = (loadEffectiveSFPreferences()?.preferences ?? {}) as Record< + string, + unknown + >; const coverage = computeBenchmarkCoverage(prefs); writeBenchmarkCoverage(coverage); // Self-feedback triage drain. The daemon's 6h maintenance timer fires diff --git a/src/headless-feedback.ts b/src/headless-feedback.ts index c766fdb55..ea479706e 100644 --- a/src/headless-feedback.ts +++ b/src/headless-feedback.ts @@ -85,7 +85,11 @@ function parseIntOrUndefined(s: string | undefined): number | undefined { const VALID_SEVERITIES = new Set(["low", "medium", "high", "critical"]); const DEFAULT_KIND_DOMAIN = "improvement-idea"; -function emit(json: boolean, payload: Record, human: string): void { +function emit( + json: boolean, + payload: Record, + human: string, +): void { if (json) { process.stdout.write(`${JSON.stringify(payload)}\n`); } else { @@ -100,10 +104,15 @@ async function loadDb(basePath: string): Promise { await autoStart.openProjectDbIfPresent(basePath); } -async function handleAdd(basePath: string, options: FeedbackOptions): Promise { +async function handleAdd( + basePath: string, + options: FeedbackOptions, +): Promise { const summary = readFlag(options.args, "--summary"); if (!summary || summary.trim() === "") { - process.stderr.write("[headless] Error: feedback add requires --summary \n"); + process.stderr.write( + "[headless] Error: feedback add requires --summary \n", + ); return { exitCode: 2 }; } const severity = readFlag(options.args, "--severity") ?? "medium"; @@ -120,8 +129,12 @@ async function handleAdd(basePath: string, options: FeedbackOptions): Promise void; }; sfDb.insertSelfFeedbackEntry(entry); - emit(options.json, { - ok: true, - id, - ts, - kind, - severity, - blocking, - impact_score: impactScore, - purpose_anchor: purposeAnchor ?? null, - summary: entry.summary, - }, `${id} ${severity.padEnd(8)} ${kind} ${entry.summary}`); + emit( + options.json, + { + ok: true, + id, + ts, + kind, + severity, + blocking, + impact_score: impactScore, + purpose_anchor: purposeAnchor ?? null, + summary: entry.summary, + }, + `${id} ${severity.padEnd(8)} ${kind} ${entry.summary}`, + ); return { exitCode: 0 }; } -async function handleList(basePath: string, options: FeedbackOptions): Promise { +async function handleList( + basePath: string, + options: FeedbackOptions, +): Promise { const wantUnresolved = readBoolFlag(options.args, "--unresolved"); const severityFilter = readFlag(options.args, "--severity"); if (severityFilter && !VALID_SEVERITIES.has(severityFilter)) { @@ -199,7 +224,10 @@ async function handleList(basePath: string, options: FeedbackOptions): Promise Array<{ id: string; ts: string; @@ -241,9 +269,13 @@ async function handleList(basePath: string, options: FeedbackOptions): Promise { +async function handleResolve( + basePath: string, + options: FeedbackOptions, +): Promise { const positional = options.args.filter( - (a, i, all) => !a.startsWith("--") && (i === 0 || !all[i - 1].startsWith("--")), + (a, i, all) => + !a.startsWith("--") && (i === 0 || !all[i - 1].startsWith("--")), ); const id = positional[0]; if (!id) { @@ -253,10 +285,14 @@ async function handleResolve(basePath: string, options: FeedbackOptions): Promis return { exitCode: 2 }; } const reason = readFlag(options.args, "--reason") ?? ""; - const evidenceKind = readFlag(options.args, "--evidence-kind") ?? "human-clear"; + const evidenceKind = + readFlag(options.args, "--evidence-kind") ?? "human-clear"; await loadDb(basePath); - const sfDb = (await jiti.import(sfExtensionPath("sf-db/sf-db-self-feedback"), {})) as { + const sfDb = (await jiti.import( + sfExtensionPath("sf-db/sf-db-self-feedback"), + {}, + )) as { resolveSelfFeedbackEntry: ( id: string, resolution: { @@ -276,21 +312,29 @@ async function handleResolve(basePath: string, options: FeedbackOptions): Promis // Either id not found OR already resolved. The DB primitive returns // false for both — surface a 1-line note rather than failing hard, // since "already resolved" is the idempotent path. - emit(options.json, { - ok: false, - idempotent: true, - id, - note: "no row updated (already resolved, or id not found)", - }, `${id}: nothing to resolve (already resolved, or id not found)`); + emit( + options.json, + { + ok: false, + idempotent: true, + id, + note: "no row updated (already resolved, or id not found)", + }, + `${id}: nothing to resolve (already resolved, or id not found)`, + ); return { exitCode: 0 }; } - emit(options.json, { - ok: true, - id, - resolved_at: new Date().toISOString(), - evidence_kind: evidenceKind, - reason, - }, `${id}: resolved (${evidenceKind})`); + emit( + options.json, + { + ok: true, + id, + resolved_at: new Date().toISOString(), + evidence_kind: evidenceKind, + reason, + }, + `${id}: resolved (${evidenceKind})`, + ); return { exitCode: 0 }; } diff --git a/src/headless-import-backlog.ts b/src/headless-import-backlog.ts index 45cd38122..94d85af8e 100644 --- a/src/headless-import-backlog.ts +++ b/src/headless-import-backlog.ts @@ -63,7 +63,8 @@ export function parseBacklogMarkdown(text: string): ParsedMilestone[] { if (!current) continue; // Bullet items: -, *, +, or numbered (1. ...) — strip status emoji/markers - const bullet = line.match(/^\s*[-*+]\s+(.+)$/) ?? line.match(/^\s*\d+\.\s+(.+)$/); + const bullet = + line.match(/^\s*[-*+]\s+(.+)$/) ?? line.match(/^\s*\d+\.\s+(.+)$/); if (bullet) { let title = bullet[1].trim(); // Strip leading status markers: ✅, 🟡, ⬜, ✓, x, [x], [ ], etc. @@ -116,9 +117,10 @@ export async function runImportBacklog( // Open the SF database // eslint-disable-next-line @typescript-eslint/no-explicit-any - const dynamicToolsPath = "./resources/extensions/sf/bootstrap/dynamic-tools.js"; + const dynamicToolsPath = + "./resources/extensions/sf/bootstrap/dynamic-tools.js"; // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { ensureDbOpen } = await import(dynamicToolsPath) as any; + const { ensureDbOpen } = (await import(dynamicToolsPath)) as any; const opened = await ensureDbOpen(cwd); if (!opened) { process.stderr.write( @@ -131,7 +133,7 @@ export async function runImportBacklog( const sfDbPath = "./resources/extensions/sf/sf-db.js"; // eslint-disable-next-line @typescript-eslint/no-explicit-any const { insertMilestone, getMilestone, insertSlice, getAllMilestones } = - await import(sfDbPath) as any; + (await import(sfDbPath)) as any; const text = readFileSync(filePath, "utf8"); const parsed = parseBacklogMarkdown(text); @@ -150,7 +152,12 @@ export async function runImportBacklog( const existing = getAllMilestones(); let sequence = existing.length; - const results: { id: string; title: string; slices: number; skipped: boolean }[] = []; + const results: { + id: string; + title: string; + slices: number; + skipped: boolean; + }[] = []; for (const m of parsed) { const id = slugify(m.title); @@ -186,7 +193,12 @@ export async function runImportBacklog( } log(` + ${id}: "${m.title}" (${m.slices.length} slices)`); - results.push({ id, title: m.title, slices: m.slices.length, skipped: false }); + results.push({ + id, + title: m.title, + slices: m.slices.length, + skipped: false, + }); } const imported = results.filter((r) => !r.skipped).length; @@ -194,7 +206,12 @@ export async function runImportBacklog( if (opts.json) { process.stdout.write( - JSON.stringify({ schemaVersion: 1, imported, skipped, milestones: results }) + "\n", + JSON.stringify({ + schemaVersion: 1, + imported, + skipped, + milestones: results, + }) + "\n", ); } else { process.stderr.write( diff --git a/src/headless-mark-state.ts b/src/headless-mark-state.ts index 863c51712..46d3b64d0 100644 --- a/src/headless-mark-state.ts +++ b/src/headless-mark-state.ts @@ -70,7 +70,10 @@ function sfExtensionPath(moduleName: string): string { ); } -function parseRef(ref: string): { milestoneId: string; sliceId: string | null } { +function parseRef(ref: string): { + milestoneId: string; + sliceId: string | null; +} { const trimmed = ref.trim(); const slash = trimmed.indexOf("/"); if (slash < 0) return { milestoneId: trimmed, sliceId: null }; @@ -122,19 +125,42 @@ export async function handleMarkState( const reason = extractReason(options.args); const ref = parseRef(positional[0]); - const autoStartModule = (await jiti.import(sfExtensionPath("auto-start"), {})) as { + const autoStartModule = (await jiti.import( + sfExtensionPath("auto-start"), + {}, + )) as { openProjectDbIfPresent: (basePath: string) => Promise; }; await autoStartModule.openProjectDbIfPresent(basePath); - const slicesModule = (await jiti.import(sfExtensionPath("sf-db/sf-db-slices"), {})) as { + const slicesModule = (await jiti.import( + sfExtensionPath("sf-db/sf-db-slices"), + {}, + )) as { getSlice: (mid: string, sid: string) => { status: string } | null; - updateSliceStatus: (mid: string, sid: string, status: string, completedAt: string | null) => void; - setSliceSummaryMd?: (mid: string, sid: string, summaryMd: string, uatMd: string) => void; + updateSliceStatus: ( + mid: string, + sid: string, + status: string, + completedAt: string | null, + ) => void; + setSliceSummaryMd?: ( + mid: string, + sid: string, + summaryMd: string, + uatMd: string, + ) => void; }; - const milestonesModule = (await jiti.import(sfExtensionPath("sf-db/sf-db-milestones"), {})) as { + const milestonesModule = (await jiti.import( + sfExtensionPath("sf-db/sf-db-milestones"), + {}, + )) as { getMilestone: (id: string) => { status: string } | null; - updateMilestoneStatus: (id: string, status: string, completedAt: string | null) => void; + updateMilestoneStatus: ( + id: string, + status: string, + completedAt: string | null, + ) => void; }; const now = new Date().toISOString(); @@ -154,23 +180,33 @@ export async function handleMarkState( return { exitCode: 1 }; } if (m.status === "complete") { - emit(process.stdout, options.json, { - ok: true, - idempotent: true, - milestone_id: ref.milestoneId, - status: m.status, - }, `${ref.milestoneId} already complete`); + emit( + process.stdout, + options.json, + { + ok: true, + idempotent: true, + milestone_id: ref.milestoneId, + status: m.status, + }, + `${ref.milestoneId} already complete`, + ); return { exitCode: 0 }; } milestonesModule.updateMilestoneStatus(ref.milestoneId, "complete", now); - emit(process.stdout, options.json, { - ok: true, - milestone_id: ref.milestoneId, - previous_status: m.status, - status: "complete", - completed_at: now, - reason, - }, `${ref.milestoneId}: ${m.status} → complete`); + emit( + process.stdout, + options.json, + { + ok: true, + milestone_id: ref.milestoneId, + previous_status: m.status, + status: "complete", + completed_at: now, + reason, + }, + `${ref.milestoneId}: ${m.status} → complete`, + ); return { exitCode: 0 }; } @@ -191,24 +227,39 @@ export async function handleMarkState( const targetStatus = options.command === "complete-slice" ? "complete" : "skipped"; if (slice.status === targetStatus) { - emit(process.stdout, options.json, { - ok: true, - idempotent: true, - milestone_id: ref.milestoneId, - slice_id: ref.sliceId, - status: slice.status, - }, `${ref.milestoneId}/${ref.sliceId} already ${targetStatus}`); + emit( + process.stdout, + options.json, + { + ok: true, + idempotent: true, + milestone_id: ref.milestoneId, + slice_id: ref.sliceId, + status: slice.status, + }, + `${ref.milestoneId}/${ref.sliceId} already ${targetStatus}`, + ); return { exitCode: 0 }; } - slicesModule.updateSliceStatus(ref.milestoneId, ref.sliceId, targetStatus, now); - emit(process.stdout, options.json, { - ok: true, - milestone_id: ref.milestoneId, - slice_id: ref.sliceId, - previous_status: slice.status, - status: targetStatus, - completed_at: now, - reason, - }, `${ref.milestoneId}/${ref.sliceId}: ${slice.status} → ${targetStatus}`); + slicesModule.updateSliceStatus( + ref.milestoneId, + ref.sliceId, + targetStatus, + now, + ); + emit( + process.stdout, + options.json, + { + ok: true, + milestone_id: ref.milestoneId, + slice_id: ref.sliceId, + previous_status: slice.status, + status: targetStatus, + completed_at: now, + reason, + }, + `${ref.milestoneId}/${ref.sliceId}: ${slice.status} → ${targetStatus}`, + ); return { exitCode: 0 }; } diff --git a/src/headless-reflect.ts b/src/headless-reflect.ts index fac9bca46..98c8bc3d4 100644 --- a/src/headless-reflect.ts +++ b/src/headless-reflect.ts @@ -112,7 +112,9 @@ export async function handleReflect( mod = (await jiti.import(sfExtensionPath("reflection"))) as typeof mod; } catch (err) { const msg = err instanceof Error ? err.message : String(err); - process.stderr.write(`[reflect] failed to load reflection module: ${msg}\n`); + process.stderr.write( + `[reflect] failed to load reflection module: ${msg}\n`, + ); return { exitCode: 1 }; } @@ -164,7 +166,9 @@ export async function handleReflect( // --run: dispatch the rendered prompt to gemini-cli, capture the report, // persist to .sf/reflection/-report.md, emit the report path on stdout. - process.stderr.write("[reflect] dispatching to gemini-cli (this can take a few minutes)…\n"); + process.stderr.write( + "[reflect] dispatching to gemini-cli (this can take a few minutes)…\n", + ); const result = await mod.runGeminiReflection(rendered, { model: options.model, }); diff --git a/src/headless-uok-status.ts b/src/headless-uok-status.ts index e15bdf949..5cfbb7121 100644 --- a/src/headless-uok-status.ts +++ b/src/headless-uok-status.ts @@ -153,9 +153,7 @@ function classifyCoverage( // 1+2, no registry exists, so the safer default is "stale". return "stale"; } - const last = entry.lastEvaluatedAt - ? Date.parse(entry.lastEvaluatedAt) - : null; + const last = entry.lastEvaluatedAt ? Date.parse(entry.lastEvaluatedAt) : null; if (last !== null && Date.now() - last > STALE_THRESHOLD_MS) { return "stale"; } @@ -258,11 +256,10 @@ export async function handleUokStatus( // linear scans of every event. const traceEvents = (() => { try { - return traceWriterModule.readTraceEvents?.( - basePath, - "gate_run", - 24 * 30, - ) ?? []; + return ( + traceWriterModule.readTraceEvents?.(basePath, "gate_run", 24 * 30) ?? + [] + ); } catch { return []; } diff --git a/src/headless-usage.ts b/src/headless-usage.ts index 79b2adcf6..d7219f754 100644 --- a/src/headless-usage.ts +++ b/src/headless-usage.ts @@ -34,7 +34,10 @@ const NO_API_PROVIDERS: ReadonlyArray<{ id: string; reason: string }> = [ { id: "ollama-cloud", reason: "WorkOS dashboard only — ollama.com/settings" }, { id: "opencode", reason: "no public quota endpoint" }, { id: "opencode-go", reason: "no public quota endpoint" }, - { id: "xiaomi", reason: "no public quota endpoint — platform.xiaomimimo.com" }, + { + id: "xiaomi", + reason: "no public quota endpoint — platform.xiaomimimo.com", + }, ]; /** @@ -100,10 +103,7 @@ export async function handleUsage( lines.push(" (no windows reported)"); continue; } - const labelW = Math.max( - 16, - ...windows.map((w) => (w.label ?? "").length), - ); + const labelW = Math.max(16, ...windows.map((w) => (w.label ?? "").length)); for (const w of windows as Array<{ label?: string; usedFraction?: number; diff --git a/src/headless.ts b/src/headless.ts index 134a0215a..8c26f3fe3 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -33,11 +33,6 @@ import { hasProjectMilestones, loadContext, } from "./headless-context.js"; -import { - checkPddFields, - formatPddRefusal, -} from "./resources/extensions/sf/headless-pdd-check.js"; - import { classifyUnexpectedChildExit, EXIT_BLOCKED, @@ -61,7 +56,6 @@ import { shouldArmHeadlessIdleTimeout, shouldRestartHeadlessRun, } from "./headless-events.js"; - import type { HeadlessJsonResult, OutputFormat } from "./headless-types.js"; import { VALID_OUTPUT_FORMATS } from "./headless-types.js"; import type { ExtensionUIRequest, ProgressContext } from "./headless-ui.js"; @@ -85,6 +79,10 @@ import { findUnsupportedAutonomousArgs, formatUnsupportedAutonomousArgs, } from "./resources/extensions/sf/autonomous-command-args.js"; +import { + checkPddFields, + formatPddRefusal, +} from "./resources/extensions/sf/headless-pdd-check.js"; import { ensureSfSymlink, externalSfRoot, @@ -779,7 +777,8 @@ async function runHeadlessOnce( // auto-bootstrap path supplies an internally-built seed that is not // meant to be PDD-shaped) and when the operator explicitly opted out // with --skip-pdd-check (migration escape hatch). - const isOperatorInitiatedNewMilestone = requestedCommand === "new-milestone"; + const isOperatorInitiatedNewMilestone = + requestedCommand === "new-milestone"; if (isOperatorInitiatedNewMilestone) { if (options.skipPddCheck) { process.stderr.write( diff --git a/src/help-text.ts b/src/help-text.ts index c4e9d7fa5..33af07b4c 100644 --- a/src/help-text.ts +++ b/src/help-text.ts @@ -300,13 +300,13 @@ const SUBCOMMAND_HELP: Record = { " sf headless reflect Render reflection prompt for piping", " sf headless reflect --run Dispatch reflection + write report", " sf headless complete-slice M010/S03 Flip M010/S03 to status=complete (idempotent)", - " sf headless skip-slice M003/S01 --reason \"migration placeholder\" Mark placeholder slice skipped", + ' sf headless skip-slice M003/S01 --reason "migration placeholder" Mark placeholder slice skipped', " sf headless complete-milestone M010 Flip milestone to status=complete", - " sf headless feedback add --severity high --summary \"30K truncate drops the why\" File self-feedback", - " sf headless feedback add --summary \"...\" --purpose \"M015 vision: ...\" Anchor to a purpose (ADR-0000)", + ' sf headless feedback add --severity high --summary "30K truncate drops the why" File self-feedback', + ' sf headless feedback add --summary "..." --purpose "M015 vision: ..." Anchor to a purpose (ADR-0000)', " sf headless feedback list --unresolved Pending self-feedback entries", - " sf headless feedback list --purpose \"M015 vision\" Triage by purpose anchor", - " sf headless feedback resolve sf-mp4xxx --reason \"shipped in 7b85a6\" Resolve an entry", + ' sf headless feedback list --purpose "M015 vision" Triage by purpose anchor', + ' sf headless feedback resolve sf-mp4xxx --reason "shipped in 7b85a6" Resolve an entry', "", "Exit codes: 0 = success, 1 = error/timeout, 10 = blocked, 11 = cancelled", ].join("\n"), diff --git a/src/resources/extensions/guardrails/extension-manifest.json b/src/resources/extensions/guardrails/extension-manifest.json index f19fdd17a..f63af289b 100644 --- a/src/resources/extensions/guardrails/extension-manifest.json +++ b/src/resources/extensions/guardrails/extension-manifest.json @@ -7,6 +7,14 @@ "requires": { "platform": ">=2.29.0" }, "provides": { "commands": ["safegit", "safegit-level", "safegit-status", "yolo"], - "hooks": ["session_start", "tool_call", "tool_result", "turn_start", "message_update", "turn_end", "agent_end"] + "hooks": [ + "session_start", + "tool_call", + "tool_result", + "turn_start", + "message_update", + "turn_end", + "agent_end" + ] } } diff --git a/src/resources/extensions/guardrails/index.js b/src/resources/extensions/guardrails/index.js index 581d65cf0..fa2b0c0c8 100644 --- a/src/resources/extensions/guardrails/index.js +++ b/src/resources/extensions/guardrails/index.js @@ -11,10 +11,10 @@ * - Registers SF slash commands: /safegit, /safegit-level, /safegit-status, /yolo */ import { readFileSync } from "node:fs"; -import { join } from "node:path"; import * as path from "node:path"; -import { loadRules } from "./ttsr-rule-loader.js"; +import { join } from "node:path"; import { TtsrManager } from "./ttsr-manager.js"; +import { loadRules } from "./ttsr-rule-loader.js"; const SENSITIVE_PATTERNS = [ { @@ -569,15 +569,22 @@ function buildInterruptContent(rule) { } function extractDeltaContext(event) { if (event.type === "text_delta") { - return { delta: event.delta, context: { source: "text", streamKey: "text" } }; + return { + delta: event.delta, + context: { source: "text", streamKey: "text" }, + }; } if (event.type === "thinking_delta") { - return { delta: event.delta, context: { source: "thinking", streamKey: "thinking" } }; + return { + delta: event.delta, + context: { source: "thinking", streamKey: "thinking" }, + }; } if (event.type === "toolcall_delta") { const partial = event.partial; const contentBlock = partial?.content?.[event.contentIndex]; - const toolName = contentBlock && "name" in contentBlock ? contentBlock.name : undefined; + const toolName = + contentBlock && "name" in contentBlock ? contentBlock.name : undefined; const filePaths = []; if (contentBlock && "partialJson" in contentBlock) { const json = contentBlock.partialJson; @@ -588,7 +595,12 @@ function extractDeltaContext(event) { } return { delta: event.delta, - context: { source: "tool", toolName, filePaths: filePaths.length > 0 ? filePaths : undefined, streamKey: `toolcall:${event.contentIndex}` }, + context: { + source: "tool", + toolName, + filePaths: filePaths.length > 0 ? filePaths : undefined, + streamKey: `toolcall:${event.contentIndex}`, + }, }; } return null; diff --git a/src/resources/extensions/search-the-web/extension-manifest.json b/src/resources/extensions/search-the-web/extension-manifest.json index be938fa79..9dc597fcd 100644 --- a/src/resources/extensions/search-the-web/extension-manifest.json +++ b/src/resources/extensions/search-the-web/extension-manifest.json @@ -6,7 +6,13 @@ "tier": "bundled", "requires": { "platform": ">=2.29.0" }, "provides": { - "tools": ["search-the-web", "fetch_page", "search_and_read", "web_search", "google_search"], + "tools": [ + "search-the-web", + "fetch_page", + "search_and_read", + "web_search", + "google_search" + ], "commands": ["search-provider"], "hooks": ["session_start", "model_select", "before_provider_request"] } diff --git a/src/resources/extensions/sf/agentic-docs-scaffold.js b/src/resources/extensions/sf/agentic-docs-scaffold.js index 2fa881d24..37fb95c07 100644 --- a/src/resources/extensions/sf/agentic-docs-scaffold.js +++ b/src/resources/extensions/sf/agentic-docs-scaffold.js @@ -10,7 +10,11 @@ import { import { dirname, join } from "node:path"; import { SCAFFOLD_FILES } from "./scaffold-constants.js"; import { migrateLegacyScaffold } from "./scaffold-drift.js"; -import { resolveActiveProfileSet, readPreferencesProfile, detectRepoProfile } from "./scaffold-profiles.js"; +import { + detectRepoProfile, + readPreferencesProfile, + resolveActiveProfileSet, +} from "./scaffold-profiles.js"; import { bodyHash, extractMarker, @@ -128,7 +132,11 @@ export function ensureAgenticDocsScaffold(basePath) { const manifest = readScaffoldManifest(basePath); // PREFERENCES.md frontmatter takes highest precedence (ADR-022 §6). // If no profile is set anywhere, auto-detect and persist to manifest. - let { profileName: activeProfile, profileSet, warning } = resolveActiveProfileSet(basePath, manifest, null); + const { + profileName: activeProfile, + profileSet, + warning, + } = resolveActiveProfileSet(basePath, manifest, null); if (warning) { logWarning("scaffold", warning, {}); } @@ -137,7 +145,9 @@ export function ensureAgenticDocsScaffold(basePath) { try { writeScaffoldManifest(basePath, { ...manifest, profile: activeProfile }); } catch (err) { - logWarning("scaffold", "failed to write profile to manifest", { error: err.message }); + logWarning("scaffold", "failed to write profile to manifest", { + error: err.message, + }); } } // Step 1: legacy migration — promote unmarked-but-recognised files. diff --git a/src/resources/extensions/sf/auto-dashboard.js b/src/resources/extensions/sf/auto-dashboard.js index a1c23fca4..b4f2df11f 100644 --- a/src/resources/extensions/sf/auto-dashboard.js +++ b/src/resources/extensions/sf/auto-dashboard.js @@ -346,10 +346,7 @@ export function updateSliceProgressCache(_base, mid, activeSid) { } } catch (err) { // Non-fatal — just omit task count - logWarning( - "dashboard", - `operation failed: ${getErrorMessage(err)}`, - ); + logWarning("dashboard", `operation failed: ${getErrorMessage(err)}`); } } cachedSliceProgress = { @@ -361,10 +358,7 @@ export function updateSliceProgressCache(_base, mid, activeSid) { }; } catch (err) { // Non-fatal — widget just won't show progress bar - logWarning( - "dashboard", - `operation failed: ${getErrorMessage(err)}`, - ); + logWarning("dashboard", `operation failed: ${getErrorMessage(err)}`); } } export function getRoadmapSlicesSync() { @@ -395,10 +389,7 @@ function refreshLastCommit(basePath) { lastCommitFetchedAt = Date.now(); } catch (err) { // Non-fatal — just skip last commit display - logWarning( - "dashboard", - `operation failed: ${getErrorMessage(err)}`, - ); + logWarning("dashboard", `operation failed: ${getErrorMessage(err)}`); } } function getLastCommit(basePath) { @@ -519,10 +510,7 @@ function persistWidgetMode( writeFileSync(prefsPath, content, "utf-8"); } catch (err) { /* non-fatal — mode still set in memory */ - logWarning( - "dashboard", - `file write failed: ${getErrorMessage(err)}`, - ); + logWarning("dashboard", `file write failed: ${getErrorMessage(err)}`); } } /** Cycle to the next widget mode. Returns the new mode. */ diff --git a/src/resources/extensions/sf/auto-post-unit.js b/src/resources/extensions/sf/auto-post-unit.js index 93d259d7a..fa8518948 100644 --- a/src/resources/extensions/sf/auto-post-unit.js +++ b/src/resources/extensions/sf/auto-post-unit.js @@ -164,12 +164,12 @@ const LIFECYCLE_ONLY_UNITS = new Set([ import { existsSync, readFileSync, unlinkSync } from "node:fs"; import { join } from "node:path"; -import { insertArtifact } from "./sf-db/sf-db-artifacts.js"; import { getAutoSession } from "./auto/session.js"; import { describeNextUnit } from "./auto-dashboard.js"; -import { _resetHasChangesCache } from "./native-git-bridge.js"; -import { autoCommitCurrentBranch } from "./worktree.js"; import { getErrorMessage } from "./error-utils.js"; +import { _resetHasChangesCache } from "./native-git-bridge.js"; +import { insertArtifact } from "./sf-db/sf-db-artifacts.js"; +import { autoCommitCurrentBranch } from "./worktree.js"; /** * Detect summary files written directly to disk without the LLM calling @@ -569,8 +569,7 @@ export async function postUnitPreVerification(pctx, opts) { } s.lastGitActionStatus = "ok"; } catch (stageErr) { - const stageErrMsg = - getErrorMessage(stageErr); + const stageErrMsg = getErrorMessage(stageErr); s.lastGitActionFailure = stageErrMsg; s.lastGitActionStatus = "failed"; debugLog("postUnit", { diff --git a/src/resources/extensions/sf/auto-prompts.js b/src/resources/extensions/sf/auto-prompts.js index 84b263117..7e477d037 100644 --- a/src/resources/extensions/sf/auto-prompts.js +++ b/src/resources/extensions/sf/auto-prompts.js @@ -1143,7 +1143,8 @@ export async function buildDiscussProjectPrompt( inputs: {}, }, graph: { - build: async (_, b) => inlineGraphSubgraph(b, "project setup", { budget: 3000 }), + build: async (_, b) => + inlineGraphSubgraph(b, "project setup", { budget: 3000 }), inputs: {}, }, }, @@ -1179,32 +1180,35 @@ export async function buildDiscussRequirementsPrompt( base, structuredQuestionsAvailable = "false", ) { - const { inline: composed } = await composeUnitContext("discuss-requirements", { - base, - resolveArtifact: async (key) => { - switch (key) { - case "project": - return inlineProjectFromDb(base); - case "requirements": - return inlineRequirementsFromDb(base); - case "templates": - return inlineTemplate("requirements", "Requirements"); - default: - return null; - } - }, - computed: { - knowledge: { - build: async (_, b) => inlineKnowledgeScoped(b, []), - inputs: {}, + const { inline: composed } = await composeUnitContext( + "discuss-requirements", + { + base, + resolveArtifact: async (key) => { + switch (key) { + case "project": + return inlineProjectFromDb(base); + case "requirements": + return inlineRequirementsFromDb(base); + case "templates": + return inlineTemplate("requirements", "Requirements"); + default: + return null; + } }, - graph: { - build: async (_, b) => - inlineGraphSubgraph(b, "project requirements", { budget: 3000 }), - inputs: {}, + computed: { + knowledge: { + build: async (_, b) => inlineKnowledgeScoped(b, []), + inputs: {}, + }, + graph: { + build: async (_, b) => + inlineGraphSubgraph(b, "project requirements", { budget: 3000 }), + inputs: {}, + }, }, }, - }); + ); const parts = []; if (composed) parts.push(composed); // #M005-remediation: knowledge/graph included via composed (computed registry). diff --git a/src/resources/extensions/sf/auto-recovery.js b/src/resources/extensions/sf/auto-recovery.js index 632348d92..c68966b19 100644 --- a/src/resources/extensions/sf/auto-recovery.js +++ b/src/resources/extensions/sf/auto-recovery.js @@ -553,30 +553,21 @@ function abortAndResetMerge(basePath, hasMergeHead, squashMsgPath) { nativeMergeAbort(basePath); } catch (err) { /* best-effort */ - logWarning( - "recovery", - `git merge-abort failed: ${getErrorMessage(err)}`, - ); + logWarning("recovery", `git merge-abort failed: ${getErrorMessage(err)}`); } } else if (squashMsgPath) { try { unlinkSync(squashMsgPath); } catch (err) { /* best-effort */ - logWarning( - "recovery", - `file unlink failed: ${getErrorMessage(err)}`, - ); + logWarning("recovery", `file unlink failed: ${getErrorMessage(err)}`); } } try { nativeResetHard(basePath); } catch (err) { /* best-effort */ - logError( - "recovery", - `git reset failed: ${getErrorMessage(err)}`, - ); + logError("recovery", `git reset failed: ${getErrorMessage(err)}`); } } /** diff --git a/src/resources/extensions/sf/auto-start.js b/src/resources/extensions/sf/auto-start.js index 2f80b83e3..dfa6847a5 100644 --- a/src/resources/extensions/sf/auto-start.js +++ b/src/resources/extensions/sf/auto-start.js @@ -38,6 +38,7 @@ import { resetProactiveHealing, setLevelChangeCallback, } from "./doctor-proactive.js"; +import { getErrorMessage } from "./error-utils.js"; import { getManifestStatus, loadFile } from "./files.js"; import { GitService } from "./git-service.js"; import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js"; @@ -61,6 +62,7 @@ import { nativeIsRepo, nativeWorktreeRemove, } from "./native-git-bridge.js"; +import { initNotificationStore } from "./notification-store.js"; import { resolveMilestoneFile, sfRoot } from "./paths.js"; import { resetHookState, restoreHookState } from "./post-unit-hooks.js"; import { @@ -103,8 +105,6 @@ import { } from "./uok/unit-runtime.js"; import { safeSetWidget } from "./widget-safe.js"; import { logError, logWarning } from "./workflow-logger.js"; -import { initNotificationStore } from "./notification-store.js"; - import { captureIntegrationBranch, detectWorktreeName, @@ -115,7 +115,6 @@ import { isInsideWorktreesDir, } from "./worktree-manager.js"; import { emitWorktreeOrphaned } from "./worktree-telemetry.js"; -import { getErrorMessage } from "./error-utils.js"; /** * Bootstrap a fresh autonomous mode session. Handles everything from git init @@ -552,10 +551,7 @@ export async function bootstrapAutoSession( nativeCommit(base, "chore: init sf"); } catch (err) { /* nothing to commit */ - logWarning( - "engine", - `mkdir failed: ${getErrorMessage(err)}`, - ); + logWarning("engine", `mkdir failed: ${getErrorMessage(err)}`); } } // Initialize GitService diff --git a/src/resources/extensions/sf/auto-timers.js b/src/resources/extensions/sf/auto-timers.js index 93a2baa39..c63a613fe 100644 --- a/src/resources/extensions/sf/auto-timers.js +++ b/src/resources/extensions/sf/auto-timers.js @@ -21,6 +21,7 @@ import { computeBudgets, resolveExecutorContextWindow, } from "./context-budget.js"; +import { getErrorMessage } from "./error-utils.js"; import { resolveAutoSupervisorConfig } from "./preferences.js"; import { writeRunawayRecoveryArtifact } from "./runaway-recovery.js"; import { recordSelfFeedback } from "./self-feedback.js"; @@ -38,7 +39,6 @@ import { writeUnitRuntimeRecord, } from "./uok/unit-runtime.js"; import { logError, logWarning } from "./workflow-logger.js"; -import { getErrorMessage } from "./error-utils.js"; /** * Set up all four supervision timers for the current unit: * 1. Soft timeout warning (wrapup) @@ -103,10 +103,7 @@ export function startUnitSupervision(sctx) { } } catch (err) { // Non-fatal — fall through with no estimate - logWarning( - "timer", - `operation failed: ${getErrorMessage(err)}`, - ); + logWarning("timer", `operation failed: ${getErrorMessage(err)}`); } } const estimateMinutes = taskEstimate @@ -482,10 +479,7 @@ export function startUnitSupervision(sctx) { ctx.ui.notify(`Idle watchdog error: ${message}`, "warning"); } catch (err) { /* best effort */ - logWarning( - "timer", - `notification failed: ${getErrorMessage(err)}`, - ); + logWarning("timer", `notification failed: ${getErrorMessage(err)}`); } } }, 15000); @@ -543,10 +537,7 @@ export function startUnitSupervision(sctx) { ctx.ui.notify(`Hard timeout error: ${message}`, "warning"); } catch (err) { /* best effort */ - logWarning( - "timer", - `notification failed: ${getErrorMessage(err)}`, - ); + logWarning("timer", `notification failed: ${getErrorMessage(err)}`); } } }, hardTimeoutMs); diff --git a/src/resources/extensions/sf/auto-worktree.js b/src/resources/extensions/sf/auto-worktree.js index 754e99ef3..5087218bc 100644 --- a/src/resources/extensions/sf/auto-worktree.js +++ b/src/resources/extensions/sf/auto-worktree.js @@ -19,10 +19,10 @@ import { statSync, unlinkSync, } from "node:fs"; -import { sfHome } from './sf-home.js'; import { isAbsolute, join, sep as pathSep } from "node:path"; import { atomicWriteSync } from "./atomic-write.js"; import { debugLog } from "./debug-logger.js"; +import { getErrorMessage } from "./error-utils.js"; import { SF_GIT_ERROR, SF_IO_ERROR, SFError } from "./errors.js"; import { MergeConflictError, @@ -59,6 +59,7 @@ import { isDbAvailable, reconcileWorktreeDb, } from "./sf-db.js"; +import { sfHome } from "./sf-home.js"; import { logError, logWarning } from "./workflow-logger.js"; import { detectWorktreeName, nudgeGitBranchCache } from "./worktree.js"; import { @@ -68,7 +69,6 @@ import { resolveGitDir, worktreePath, } from "./worktree-manager.js"; -import { getErrorMessage } from "./error-utils.js"; const PROJECT_PREFERENCES_FILE = "preferences.yaml"; // ─── Shared Constants & Helpers ───────────────────────────────────────────── @@ -167,10 +167,7 @@ function forceOverwriteAssessmentsWithVerdict( } } catch (err) { /* non-fatal */ - logWarning( - "worktree", - `assessment sync failed: ${getErrorMessage(err)}`, - ); + logWarning("worktree", `assessment sync failed: ${getErrorMessage(err)}`); } } // ─── Module State ────────────────────────────────────────────────────────── @@ -189,10 +186,7 @@ function clearProjectRootStateFiles(basePath, milestoneId) { } catch (err) { // ENOENT is expected — file may not exist (#3597) if (err.code !== "ENOENT") { - logWarning( - "worktree", - `file unlink failed: ${getErrorMessage(err)}`, - ); + logWarning("worktree", `file unlink failed: ${getErrorMessage(err)}`); } } } @@ -721,10 +715,7 @@ export function syncWorktreeStateBack(mainBasePath, worktreePath, milestoneId) { synced.push("sf.db (pre-upgrade reconcile)"); } catch (err) { // Non-fatal — file sync below is the fallback - logError( - "worktree", - `DB reconciliation failed: ${getErrorMessage(err)}`, - ); + logError("worktree", `DB reconciliation failed: ${getErrorMessage(err)}`); } } // ── 1. Sync root-level .sf/ files back ────────────────────────────── @@ -798,10 +789,7 @@ function syncDirFiles(srcDir, dstDir, filter, synced, prefix) { } } catch (err) { /* non-fatal — srcDir may not be readable */ - logWarning( - "worktree", - `directory read failed: ${getErrorMessage(err)}`, - ); + logWarning("worktree", `directory read failed: ${getErrorMessage(err)}`); } } function syncMilestoneDir(wtSf, mainSf, mid, synced) { @@ -892,10 +880,7 @@ export function runWorktreePostCreateHook(sourceDir, worktreeDir, hookPath) { resolved = realpathSync.native(resolved); } catch (err) { /* keep original */ - logWarning( - "worktree", - `realpath failed: ${getErrorMessage(err)}`, - ); + logWarning("worktree", `realpath failed: ${getErrorMessage(err)}`); } } try { @@ -1459,10 +1444,7 @@ export function mergeMilestoneToMain( } } catch (err) { /* non-fatal */ - logError( - "worktree", - `DB reconciliation failed: ${getErrorMessage(err)}`, - ); + logError("worktree", `DB reconciliation failed: ${getErrorMessage(err)}`); } } // 2. Get completed slices for commit message @@ -1631,10 +1613,7 @@ export function mergeMilestoneToMain( } catch (err) { // Stash failure is non-fatal — proceed without stash and let the merge // report the dirty tree if it fails. - logWarning( - "worktree", - `git stash failed: ${getErrorMessage(err)}`, - ); + logWarning("worktree", `git stash failed: ${getErrorMessage(err)}`); } // 7a. Shelter queued milestone directories before the squash merge (#2505). // The milestone branch may contain copies of queued milestone dirs (via @@ -1658,20 +1637,14 @@ export function mergeMilestoneToMain( }); } catch (err) { /* best-effort */ - logError( - "worktree", - `shelter restore failed: ${getErrorMessage(err)}`, - ); + logError("worktree", `shelter restore failed: ${getErrorMessage(err)}`); } } try { rmSync(shelterDir, { recursive: true, force: true }); } catch (err) { /* best-effort */ - logWarning( - "worktree", - `shelter cleanup failed: ${getErrorMessage(err)}`, - ); + logWarning("worktree", `shelter cleanup failed: ${getErrorMessage(err)}`); } }; try { @@ -1717,10 +1690,7 @@ export function mergeMilestoneToMain( } } catch (err) { /* best-effort */ - logError( - "worktree", - `merge state cleanup failed: ${getErrorMessage(err)}`, - ); + logError("worktree", `merge state cleanup failed: ${getErrorMessage(err)}`); } // 8. Squash merge — auto-resolve .sf/ state file conflicts (#530) const mergeResult = nativeMergeSquash(originalBasePath_, milestoneBranch); @@ -2021,10 +1991,7 @@ export function mergeMilestoneToMain( pushed = true; } catch (err) { // Push failure is non-fatal - logWarning( - "worktree", - `git push failed: ${getErrorMessage(err)}`, - ); + logWarning("worktree", `git push failed: ${getErrorMessage(err)}`); } } // 9b. Auto-create PR if enabled (#2302: no longer gated on pushed/auto_push) @@ -2064,10 +2031,7 @@ export function mergeMilestoneToMain( prCreated = true; } catch (err) { // PR creation failure is non-fatal — gh may not be installed or authenticated - logWarning( - "worktree", - `PR creation failed: ${getErrorMessage(err)}`, - ); + logWarning("worktree", `PR creation failed: ${getErrorMessage(err)}`); } } // 11. Guard removed — step 9b (#1792) now handles this with a smarter check: @@ -2124,20 +2088,14 @@ export function mergeMilestoneToMain( }); } catch (err) { // Best-effort -- worktree dir may already be gone - logWarning( - "worktree", - `worktree removal failed: ${getErrorMessage(err)}`, - ); + logWarning("worktree", `worktree removal failed: ${getErrorMessage(err)}`); } // 13. Delete milestone branch (after worktree removal so ref is unlocked) try { nativeBranchDelete(originalBasePath_, milestoneBranch); } catch (err) { // Best-effort - logWarning( - "worktree", - `git branch-delete failed: ${getErrorMessage(err)}`, - ); + logWarning("worktree", `git branch-delete failed: ${getErrorMessage(err)}`); } // 14. Clear module state originalBase = null; diff --git a/src/resources/extensions/sf/auto.js b/src/resources/extensions/sf/auto.js index a329738e1..63f7600c7 100644 --- a/src/resources/extensions/sf/auto.js +++ b/src/resources/extensions/sf/auto.js @@ -28,12 +28,6 @@ import { } from "node:fs"; import { isAbsolute, join } from "node:path"; import { pathToFileURL } from "node:url"; -import { sfHome } from "./sf-home.js"; -import { - clearCmuxSidebar, - logCmuxEvent, - syncCmuxSidebar, -} from "./cmux/index.js"; import { collectSecretsFromManifest } from "../get-secrets-from-user.js"; import { getRtkSessionSavings } from "../shared/rtk-session-stats.js"; import { deactivateSF } from "../shared/sf-phase-state.js"; @@ -83,12 +77,20 @@ import { isQueuedUserMessageSkip, isToolInvocationError, } from "./auto-tool-tracking.js"; +import { + clearCmuxSidebar, + logCmuxEvent, + syncCmuxSidebar, +} from "./cmux/index.js"; +import { sfHome } from "./sf-home.js"; + export { isAutoActive, isAutoPaused, markToolEnd, markToolStart, } from "./auto-runtime-state.js"; + import { autoWorktreeBranch, checkResourcesStale, @@ -419,11 +421,9 @@ export function getAutoDashboardData() { } } catch (err) { // Non-fatal — captures module may not be loaded - logWarning( - "engine", - `capture count failed: ${getErrorMessage(err)}`, - { file: "auto.ts" }, - ); + logWarning("engine", `capture count failed: ${getErrorMessage(err)}`, { + file: "auto.ts", + }); } return { active: s.active, @@ -747,7 +747,9 @@ function cleanupAfterLoopExit(ctx) { // that headless.ts waits for is never emitted. Send it here only when // stopAuto() was bypassed so headless mode can detect completion. if (!s.stopAutoCalled && ctx) { - const label = s.stepMode ? "Assisted mode stopped" : "Autonomous mode stopped"; + const label = s.stepMode + ? "Assisted mode stopped" + : "Autonomous mode stopped"; ctx.ui.notify(label, "info", { kind: "terminal", source: "workflow" }); } s.stopAutoCalled = false; @@ -759,11 +761,9 @@ function cleanupAfterLoopExit(ctx) { if (lockBase()) releaseSessionLock(lockBase()); } catch (err) { /* best-effort — mirror stopAuto cleanup */ - logWarning( - "session", - `lock cleanup failed: ${getErrorMessage(err)}`, - { file: "auto.ts" }, - ); + logWarning("session", `lock cleanup failed: ${getErrorMessage(err)}`, { + file: "auto.ts", + }); } // A transient provider-error pause intentionally leaves the paused badge // visible so the user still has a resumable autonomous mode signal on screen. @@ -780,11 +780,9 @@ function cleanupAfterLoopExit(ctx) { process.chdir(s.basePath); } catch (err) { /* best-effort */ - logWarning( - "engine", - `chdir failed: ${getErrorMessage(err)}`, - { file: "auto.ts" }, - ); + logWarning("engine", `chdir failed: ${getErrorMessage(err)}`, { + file: "auto.ts", + }); } } } @@ -935,11 +933,9 @@ export async function stopAuto(ctx, pi, reason) { process.chdir(s.basePath); } catch (err) { /* best-effort */ - logWarning( - "engine", - `chdir failed: ${getErrorMessage(err)}`, - { file: "auto.ts" }, - ); + logWarning("engine", `chdir failed: ${getErrorMessage(err)}`, { + file: "auto.ts", + }); } } } catch (e) { @@ -1063,11 +1059,9 @@ export async function stopAuto(ctx, pi, reason) { if (existsSync(pausedPath)) unlinkSync(pausedPath); } catch (err) { /* non-fatal */ - logWarning( - "engine", - `file unlink failed: ${getErrorMessage(err)}`, - { file: "auto.ts" }, - ); + logWarning("engine", `file unlink failed: ${getErrorMessage(err)}`, { + file: "auto.ts", + }); } // ── Step 13: Restore original model (before reset clears IDs) ── try { @@ -1106,11 +1100,9 @@ export async function stopAuto(ctx, pi, reason) { } } catch (err) { /* non-fatal: browser-tools may not be loaded */ - logWarning( - "engine", - `browser teardown failed: ${getErrorMessage(err)}`, - { file: "auto.ts" }, - ); + logWarning("engine", `browser teardown failed: ${getErrorMessage(err)}`, { + file: "auto.ts", + }); } // External cleanup (not covered by session reset) clearInFlightTools(); @@ -1209,7 +1201,8 @@ export async function pauseAuto(ctx, _pi, _errorContext) { // The fresh-start bootstrap checks for this file and restores worktree context. try { const hasResumableUnit = !!(s.currentUnit?.type && s.currentUnit?.id); - const hasResumableCustomEngine = !!s.activeEngineId && s.activeEngineId !== "dev"; + const hasResumableCustomEngine = + !!s.activeEngineId && s.activeEngineId !== "dev"; if (hasResumableUnit || hasResumableCustomEngine) { const pausedMeta = { milestoneId: s.currentMilestoneId, @@ -1799,9 +1792,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) { // tree; deployed extensions live at ~/.sf/agent/extensions/sf/ where the // relative path resolves to ~/.sf/agent/resource-loader.js which doesn't exist. // Using SF_PKG_ROOT constructs a correct absolute path in both contexts (#3949). - const agentDir = - process.env.SF_CODING_AGENT_DIR || - join(sfHome(), "agent"); + const agentDir = process.env.SF_CODING_AGENT_DIR || join(sfHome(), "agent"); const pkgRoot = process.env.SF_PKG_ROOT; const resourceLoaderPath = pkgRoot ? pathToFileURL(join(pkgRoot, "dist", "resource-loader.js")).href @@ -1901,11 +1892,9 @@ export async function startAuto(ctx, pi, base, verboseMode, options) { ); } catch (err) { // Best-effort only — sidebar sync must never block autonomous mode startup - logWarning( - "engine", - `cmux sync failed: ${getErrorMessage(err)}`, - { file: "auto.ts" }, - ); + logWarning("engine", `cmux sync failed: ${getErrorMessage(err)}`, { + file: "auto.ts", + }); } logCmuxEvent( loadEffectiveSFPreferences()?.preferences, diff --git a/src/resources/extensions/sf/auto/auto-post-unit-staging.js b/src/resources/extensions/sf/auto/auto-post-unit-staging.js index e5c63e7db..d00fb210a 100644 --- a/src/resources/extensions/sf/auto/auto-post-unit-staging.js +++ b/src/resources/extensions/sf/auto/auto-post-unit-staging.js @@ -11,8 +11,8 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { debugLog } from "../debug-logger.js"; import { formatDocSyncProposal, getDocSyncProposal } from "../doc-sync.js"; -import { runGit } from "../git-service.js"; import { getErrorMessage } from "../error-utils.js"; +import { runGit } from "../git-service.js"; /** Unit types that mutate code — doc-sync only runs after these. */ const CODE_MUTATING_UNITS = new Set(["execute-task", "complete-slice"]); diff --git a/src/resources/extensions/sf/auto/phases-guards.js b/src/resources/extensions/sf/auto/phases-guards.js index 05d1ed643..4c001d888 100644 --- a/src/resources/extensions/sf/auto/phases-guards.js +++ b/src/resources/extensions/sf/auto/phases-guards.js @@ -37,6 +37,7 @@ import { import { resumeAutoAfterProviderDelay } from "../bootstrap/provider-error-resume.js"; import { debugLog } from "../debug-logger.js"; import { PROJECT_FILES } from "../detection.js"; +import { getErrorMessage } from "../error-utils.js"; import { MergeConflictError } from "../git-service.js"; import { recordLearnedOutcome } from "../learning/runtime.js"; import { sfRoot } from "../paths.js"; @@ -81,11 +82,11 @@ import { countChangedFiles, resetRunawayGuardState, } from "../uok/auto-runaway-guard.js"; -import { resolveUokFlags } from "../uok/flags.js"; import { buildAutonomousUokContext, emitAutonomousGate, } from "../uok/auto-uok-ctx.js"; +import { resolveUokFlags } from "../uok/flags.js"; import { emitModelAutoResolvedEvent } from "../uok/model-route-evidence.js"; import { ensurePlanV2Graph as ensurePlanningFlowGraph, @@ -118,7 +119,6 @@ import { withTimeout, } from "./finalize-timeout.js"; import { runUnit } from "./run-unit.js"; -import { getErrorMessage } from "../error-utils.js"; import { BUDGET_THRESHOLDS, MAX_FINALIZE_TIMEOUTS, diff --git a/src/resources/extensions/sf/auto/phases-pre-dispatch.js b/src/resources/extensions/sf/auto/phases-pre-dispatch.js index 6e66355d4..511d5a610 100644 --- a/src/resources/extensions/sf/auto/phases-pre-dispatch.js +++ b/src/resources/extensions/sf/auto/phases-pre-dispatch.js @@ -37,6 +37,7 @@ import { import { resumeAutoAfterProviderDelay } from "../bootstrap/provider-error-resume.js"; import { debugLog } from "../debug-logger.js"; import { PROJECT_FILES } from "../detection.js"; +import { getErrorMessage } from "../error-utils.js"; import { MergeConflictError } from "../git-service.js"; import { recordLearnedOutcome } from "../learning/runtime.js"; import { sfRoot } from "../paths.js"; @@ -63,8 +64,8 @@ import { rollbackToCheckpoint, } from "../safety/git-checkpoint.js"; import { resolveSafetyHarnessConfig } from "../safety/safety-harness.js"; -import { selectInlineFixCandidates } from "../self-feedback-drain.js"; import { recordSelfFeedback } from "../self-feedback.js"; +import { selectInlineFixCandidates } from "../self-feedback-drain.js"; import { _getAdapter, checkpointWal, @@ -123,14 +124,18 @@ import { FINALIZE_PRE_TIMEOUT_MS, withTimeout, } from "./finalize-timeout.js"; +import { + closeoutAndStop, + generateMilestoneReport, + maybeFireProductAudit, + shouldRunPlanningFlowGate, +} from "./phases-helpers.js"; import { runUnit } from "./run-unit.js"; -import { getErrorMessage } from "../error-utils.js"; import { BUDGET_THRESHOLDS, MAX_FINALIZE_TIMEOUTS, MAX_RECOVERY_CHARS, } from "./types.js"; -import { closeoutAndStop, generateMilestoneReport, maybeFireProductAudit, shouldRunPlanningFlowGate } from "./phases-helpers.js"; /** * Surface the open self-feedback queue to the operator at idle-bail time. diff --git a/src/resources/extensions/sf/auto/phases.js b/src/resources/extensions/sf/auto/phases.js index a7d31b2a9..b4a3bb836 100644 --- a/src/resources/extensions/sf/auto/phases.js +++ b/src/resources/extensions/sf/auto/phases.js @@ -4,10 +4,18 @@ * Each phase lives in its own module; this file preserves the original * import surface for loop.js and other consumers. */ -export { assessUokDiagnosticsDispatchGate, runDispatch } from "./phases-dispatch.js"; -export { runGuards, requiresHumanProductionMutationApproval } from "./phases-guards.js"; -export { _resolveDispatchGuardBasePath } from "./phases-helpers.js"; -export { runPreDispatch } from "./phases-pre-dispatch.js"; -export { runUnitPhase, resetSessionTimeoutState } from "./phases-unit.js"; +export { + assessUokDiagnosticsDispatchGate, + runDispatch, +} from "./phases-dispatch.js"; export { runFinalize } from "./phases-finalize.js"; -export { _resolveReportBasePath } from "./phases-helpers.js"; +export { + requiresHumanProductionMutationApproval, + runGuards, +} from "./phases-guards.js"; +export { + _resolveDispatchGuardBasePath, + _resolveReportBasePath, +} from "./phases-helpers.js"; +export { runPreDispatch } from "./phases-pre-dispatch.js"; +export { resetSessionTimeoutState, runUnitPhase } from "./phases-unit.js"; diff --git a/src/resources/extensions/sf/auto/run-unit.js b/src/resources/extensions/sf/auto/run-unit.js index 8f247cd2d..a03646cbd 100644 --- a/src/resources/extensions/sf/auto/run-unit.js +++ b/src/resources/extensions/sf/auto/run-unit.js @@ -1370,9 +1370,12 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt, options) { phase: "send-message-failed", unitType, unitId, - error: e && typeof e === "object" && "message" in e ? e.message : String(e), + error: + e && typeof e === "object" && "message" in e ? e.message : String(e), stack: - e && typeof e === "object" && "stack" in e ? String(e.stack) : undefined, + e && typeof e === "object" && "stack" in e + ? String(e.stack) + : undefined, elapsedMs: Date.now() - sendStartedAt, }); throw e; diff --git a/src/resources/extensions/sf/autonomous-solver-eval.js b/src/resources/extensions/sf/autonomous-solver-eval.js index 7d0c45da4..61e6e33c1 100644 --- a/src/resources/extensions/sf/autonomous-solver-eval.js +++ b/src/resources/extensions/sf/autonomous-solver-eval.js @@ -19,8 +19,8 @@ import { import { dirname, isAbsolute, join, relative, resolve } from "node:path"; import { atomicWriteSync } from "./atomic-write.js"; import { ensureDbOpen } from "./bootstrap/dynamic-tools.js"; -import { sfRoot } from "./paths.js"; import { getErrorMessage } from "./error-utils.js"; +import { sfRoot } from "./paths.js"; const DEFAULT_TIMEOUT_MS = 5 * 60_000; const MAX_OUTPUT_CHARS = 20_000; diff --git a/src/resources/extensions/sf/benchmark-coverage.d.ts b/src/resources/extensions/sf/benchmark-coverage.d.ts index 82d7eca42..16710345a 100644 --- a/src/resources/extensions/sf/benchmark-coverage.d.ts +++ b/src/resources/extensions/sf/benchmark-coverage.d.ts @@ -17,9 +17,15 @@ export interface BenchmarkCoverageResult { } export declare function normalizeForBenchmarkLookup(modelId: string): string; -export declare function computeBenchmarkCoverage(prefs: Record): BenchmarkCoverageResult; -export declare function writeBenchmarkCoverage(coverage: BenchmarkCoverageResult): void; -export declare function detectCoverageChange(coverage: BenchmarkCoverageResult): boolean; +export declare function computeBenchmarkCoverage( + prefs: Record, +): BenchmarkCoverageResult; +export declare function writeBenchmarkCoverage( + coverage: BenchmarkCoverageResult, +): void; +export declare function detectCoverageChange( + coverage: BenchmarkCoverageResult, +): boolean; export declare function scheduleBenchmarkCoverageAudit( prefs: Record, notify?: (message: string) => void, diff --git a/src/resources/extensions/sf/benchmark-coverage.js b/src/resources/extensions/sf/benchmark-coverage.js index 8b19c551a..23e890ff4 100644 --- a/src/resources/extensions/sf/benchmark-coverage.js +++ b/src/resources/extensions/sf/benchmark-coverage.js @@ -83,8 +83,7 @@ function loadCatalogEntries() { provider, id, cost: typeof raw === "object" ? raw?.cost : undefined, - contextWindow: - typeof raw === "object" ? raw?.contextWindow : undefined, + contextWindow: typeof raw === "object" ? raw?.contextWindow : undefined, }); } } diff --git a/src/resources/extensions/sf/benchmark-selector.js b/src/resources/extensions/sf/benchmark-selector.js index 754fc09af..655e3bffa 100644 --- a/src/resources/extensions/sf/benchmark-selector.js +++ b/src/resources/extensions/sf/benchmark-selector.js @@ -69,7 +69,10 @@ export function quotaHeadroomMultiplier(providerKey, getQuotaState) { } let maxUsed = 0; for (const w of state.windows) { - if (typeof w?.usedFraction === "number" && Number.isFinite(w.usedFraction)) { + if ( + typeof w?.usedFraction === "number" && + Number.isFinite(w.usedFraction) + ) { if (w.usedFraction > maxUsed) maxUsed = w.usedFraction; } } diff --git a/src/resources/extensions/sf/bootstrap/agent-end-recovery.js b/src/resources/extensions/sf/bootstrap/agent-end-recovery.js index cd8aa5177..ba213bb64 100644 --- a/src/resources/extensions/sf/bootstrap/agent-end-recovery.js +++ b/src/resources/extensions/sf/bootstrap/agent-end-recovery.js @@ -5,6 +5,7 @@ import { isTransient, resetRetryState, } from "../error-classifier.js"; +import { getErrorMessage } from "../error-utils.js"; import { resolveNextModelRoute } from "../model-route-failure.js"; import { resolveModelWithFallbacksForUnit, @@ -13,7 +14,6 @@ import { import { pauseAutoForProviderError } from "../provider-error-pause.js"; import { logWarning } from "../workflow-logger.js"; import { clearDiscussionFlowState } from "./write-gate.js"; -import { getErrorMessage } from "../error-utils.js"; const retryState = createRetryState(); const GEMINI_CAPACITY_COOLDOWN_MS = 2 * 60_000; diff --git a/src/resources/extensions/sf/bootstrap/context-board-tool.js b/src/resources/extensions/sf/bootstrap/context-board-tool.js index 2c84407d5..80c0d7fbb 100644 --- a/src/resources/extensions/sf/bootstrap/context-board-tool.js +++ b/src/resources/extensions/sf/bootstrap/context-board-tool.js @@ -8,11 +8,11 @@ * Pattern mirrors bootstrap/memory-tools.js. */ import { Type } from "@sinclair/typebox"; +import { nativeGetCurrentBranch } from "../native-git-bridge.js"; import { executeContextBoardAdd, executeContextBoardPrune, } from "../tools/context-board-tool.js"; -import { nativeGetCurrentBranch } from "../native-git-bridge.js"; import { ensureDbOpen } from "./dynamic-tools.js"; /** Resolve a stable repository identifier (absolute project root path). */ @@ -51,7 +51,8 @@ export function registerContextBoardTool(pi) { ], parameters: Type.Object({ op: Type.Union([Type.Literal("add"), Type.Literal("prune")], { - description: "Operation: 'add' to create an entry, 'prune' to remove one by id", + description: + "Operation: 'add' to create an entry, 'prune' to remove one by id", }), content: Type.Optional( Type.String({ diff --git a/src/resources/extensions/sf/bootstrap/exec-tools.js b/src/resources/extensions/sf/bootstrap/exec-tools.js index 15d26853f..82d4ae305 100644 --- a/src/resources/extensions/sf/bootstrap/exec-tools.js +++ b/src/resources/extensions/sf/bootstrap/exec-tools.js @@ -13,11 +13,11 @@ import { loadEffectiveSFPreferences } from "../preferences.js"; // EXIT_RELOAD in src/headless-events.ts — kept in sync manually. const EXIT_RELOAD = 12; +import { getErrorMessage } from "../error-utils.js"; import { executeExecSearch } from "../tools/exec-search-tool.js"; import { executeSfExec } from "../tools/exec-tool.js"; import { executeResume } from "../tools/resume-tool.js"; import { logWarning } from "../workflow-logger.js"; -import { getErrorMessage } from "../error-utils.js"; export function registerExecTools(pi) { pi.registerTool({ name: "run_command", diff --git a/src/resources/extensions/sf/bootstrap/journal-tools.js b/src/resources/extensions/sf/bootstrap/journal-tools.js index 82a9e3c45..482345c5b 100644 --- a/src/resources/extensions/sf/bootstrap/journal-tools.js +++ b/src/resources/extensions/sf/bootstrap/journal-tools.js @@ -1,7 +1,7 @@ import { Type } from "@sinclair/typebox"; +import { getErrorMessage } from "../error-utils.js"; import { queryJournal } from "../journal.js"; import { logWarning } from "../workflow-logger.js"; -import { getErrorMessage } from "../error-utils.js"; export function registerJournalTools(pi) { pi.registerTool({ name: "query_journal", diff --git a/src/resources/extensions/sf/bootstrap/register-extension.js b/src/resources/extensions/sf/bootstrap/register-extension.js index 9e91d7c6a..c02c459f4 100644 --- a/src/resources/extensions/sf/bootstrap/register-extension.js +++ b/src/resources/extensions/sf/bootstrap/register-extension.js @@ -1,23 +1,23 @@ // SF2 — Extension registration: wires all SF tools, commands, and hooks into pi import { loadEcosystemExtensions } from "../ecosystem/loader.js"; +import { getErrorMessage } from "../error-utils.js"; import { registerExitCommand } from "../exit-command.js"; import { registerSiftSearchTool } from "../tools/sift-search-tool.js"; import { logWarning } from "../workflow-logger.js"; import { registerWorktreeCommand } from "../worktree-command.js"; +import { registerContextBoardTool } from "./context-board-tool.js"; import { writeCrashLog } from "./crash-log.js"; import { registerDbTools } from "./db-tools.js"; import { registerDynamicTools } from "./dynamic-tools.js"; import { registerExecTools } from "./exec-tools.js"; import { registerJournalTools } from "./journal-tools.js"; import { registerJudgmentTools } from "./judgment-tools.js"; -import { registerContextBoardTool } from "./context-board-tool.js"; import { registerMemoryTools } from "./memory-tools.js"; import { registerProductAuditTool } from "./product-audit-tool.js"; import { registerQueryTools } from "./query-tools.js"; import { registerHooks } from "./register-hooks.js"; import { registerShortcuts } from "./register-shortcuts.js"; import { registerSessionTodoTool } from "./session-todo-tools.js"; -import { getErrorMessage } from "../error-utils.js"; export { writeCrashLog } from "./crash-log.js"; export function handleRecoverableExtensionProcessError(err) { diff --git a/src/resources/extensions/sf/bootstrap/register-hooks.js b/src/resources/extensions/sf/bootstrap/register-hooks.js index 913df186b..0943b4ce6 100644 --- a/src/resources/extensions/sf/bootstrap/register-hooks.js +++ b/src/resources/extensions/sf/bootstrap/register-hooks.js @@ -547,9 +547,7 @@ export function registerHooks(pi, ecosystemHandlers = []) { const { scheduleBenchmarkCoverageAudit } = await import( "../benchmark-coverage.js" ); - const { loadEffectiveSFPreferences } = await import( - "../preferences.js" - ); + const { loadEffectiveSFPreferences } = await import("../preferences.js"); const prefs = loadEffectiveSFPreferences()?.preferences ?? {}; scheduleBenchmarkCoverageAudit(prefs, (msg) => ctx.ui?.notify?.(msg, "info", { @@ -571,10 +569,7 @@ export function registerHooks(pi, ecosystemHandlers = []) { "../provider-quota-cache.js" ); const { getKeyManagerAuthStorage } = await import("../key-manager.js"); - scheduleProviderQuotaRefresh( - process.cwd(), - getKeyManagerAuthStorage(), - ); + scheduleProviderQuotaRefresh(process.cwd(), getKeyManagerAuthStorage()); } catch { /* non-fatal — quota refresh must never block session start */ } diff --git a/src/resources/extensions/sf/canonical-milestone-plan.js b/src/resources/extensions/sf/canonical-milestone-plan.js index daf0fb668..7234b69e9 100644 --- a/src/resources/extensions/sf/canonical-milestone-plan.js +++ b/src/resources/extensions/sf/canonical-milestone-plan.js @@ -6,6 +6,7 @@ */ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; +import { getErrorMessage } from "./error-utils.js"; import { getDbPath, getMilestone, @@ -14,7 +15,6 @@ import { openDatabase, readTransaction, } from "./sf-db.js"; -import { getErrorMessage } from "./error-utils.js"; function milestoneDir(basePath, milestoneId) { return join(basePath, ".sf", "milestones", milestoneId); diff --git a/src/resources/extensions/sf/changelog.js b/src/resources/extensions/sf/changelog.js index b953d5a84..cec038b8a 100644 --- a/src/resources/extensions/sf/changelog.js +++ b/src/resources/extensions/sf/changelog.js @@ -9,6 +9,7 @@ */ // ─── Semver comparison ──────────────────────────────────────────────────────── import { getErrorMessage } from "./error-utils.js"; + function compareSemver(a, b) { const pa = a.split(".").map(Number); const pb = b.split(".").map(Number); diff --git a/src/resources/extensions/sf/clean-root-preflight.js b/src/resources/extensions/sf/clean-root-preflight.js index 727732a13..a198bda38 100644 --- a/src/resources/extensions/sf/clean-root-preflight.js +++ b/src/resources/extensions/sf/clean-root-preflight.js @@ -13,10 +13,10 @@ * - Fast-path status check — clean trees pay no extra cost */ import { execFileSync } from "node:child_process"; +import { getErrorMessage } from "./error-utils.js"; import { GIT_NO_PROMPT_ENV } from "./git-constants.js"; import { nativeHasChanges } from "./native-git-bridge.js"; import { logWarning } from "./workflow-logger.js"; -import { getErrorMessage } from "./error-utils.js"; /** * Check the working tree for dirty files before a milestone merge. * diff --git a/src/resources/extensions/sf/code-intelligence.js b/src/resources/extensions/sf/code-intelligence.js index ad9f58d67..41ea224f6 100644 --- a/src/resources/extensions/sf/code-intelligence.js +++ b/src/resources/extensions/sf/code-intelligence.js @@ -46,6 +46,7 @@ export function chooseSiftRetrievers(scopePath, projectRoot) { } return { retrievers: "bm25,phrase,vector", reranking: "position-aware" }; } + import { getErrorMessage } from "./error-utils.js"; const SIFT_BINARY_NAME = process.platform === "win32" ? "sift.exe" : "sift"; @@ -640,7 +641,9 @@ export function ensureSiftIndexWarmup(projectRoot, prefs, options = {}) { let stderrFd = null; if (logPath) { writeFileSync(logPath, "", "utf-8"); - mkdirSync(join(projectRoot, ".sf", "runtime", "sift"), { recursive: true }); + mkdirSync(join(projectRoot, ".sf", "runtime", "sift"), { + recursive: true, + }); stderrFd = openSync(logPath, "a"); } const marker = { diff --git a/src/resources/extensions/sf/commands-add-tests.js b/src/resources/extensions/sf/commands-add-tests.js index e2798a3d6..241cdf666 100644 --- a/src/resources/extensions/sf/commands-add-tests.js +++ b/src/resources/extensions/sf/commands-add-tests.js @@ -6,10 +6,10 @@ */ import { existsSync, readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; +import { getErrorMessage } from "./error-utils.js"; import { resolveSliceFile, sfRoot } from "./paths.js"; import { loadPrompt } from "./prompt-loader.js"; import { deriveState } from "./state.js"; -import { getErrorMessage } from "./error-utils.js"; function findLastCompletedSlice(basePath, milestoneId) { // Scan disk for slices that have a SUMMARY.md (indicating completion) diff --git a/src/resources/extensions/sf/commands-debug.js b/src/resources/extensions/sf/commands-debug.js index 737e5f57c..d56946817 100644 --- a/src/resources/extensions/sf/commands-debug.js +++ b/src/resources/extensions/sf/commands-debug.js @@ -5,8 +5,8 @@ import { loadDebugSession, updateDebugSession, } from "./debug-session-store.js"; -import { loadPrompt } from "./prompt-loader.js"; import { getErrorMessage } from "./error-utils.js"; +import { loadPrompt } from "./prompt-loader.js"; const SUBCOMMANDS = new Set(["list", "status", "continue", "--diagnose"]); function isValidSlugCandidate(input) { diff --git a/src/resources/extensions/sf/commands-eval-review.js b/src/resources/extensions/sf/commands-eval-review.js index 2c86668e8..5f53ac3d6 100644 --- a/src/resources/extensions/sf/commands-eval-review.js +++ b/src/resources/extensions/sf/commands-eval-review.js @@ -27,6 +27,7 @@ import { existsSync } from "node:fs"; import { open, readFile } from "node:fs/promises"; import { join, relative } from "node:path"; import { projectRoot } from "./commands/context.js"; +import { getErrorMessage } from "./error-utils.js"; import { COVERAGE_WEIGHT, DIMENSION_VALUES, @@ -44,7 +45,6 @@ import { resolveSlicePath, } from "./paths.js"; import { deriveState } from "./state.js"; -import { getErrorMessage } from "./error-utils.js"; // ─── Constants ──────────────────────────────────────────────────────────────── /** * Slice-ID format. Must match the canonical `/^S\d+$/` used elsewhere in the diff --git a/src/resources/extensions/sf/commands-extensions.js b/src/resources/extensions/sf/commands-extensions.js index 8880a0233..a86fbfa1e 100644 --- a/src/resources/extensions/sf/commands-extensions.js +++ b/src/resources/extensions/sf/commands-extensions.js @@ -13,8 +13,8 @@ import { renameSync, writeFileSync, } from "node:fs"; -import { sfHome } from './sf-home.js'; import { dirname, join } from "node:path"; +import { sfHome } from "./sf-home.js"; // ─── Registry I/O ─────────────────────────────────────────────────────────── /** diff --git a/src/resources/extensions/sf/commands-handlers.js b/src/resources/extensions/sf/commands-handlers.js index e3b888d7e..5085adb68 100644 --- a/src/resources/extensions/sf/commands-handlers.js +++ b/src/resources/extensions/sf/commands-handlers.js @@ -25,12 +25,12 @@ import { runSFDoctor, selectDoctorScope, } from "./doctor.js"; +import { getErrorMessage } from "./error-utils.js"; import { appendKnowledge, appendOverride } from "./files.js"; import { sfRoot } from "./paths.js"; -import { sfHome } from "./sf-home.js"; import { loadPrompt } from "./prompt-loader.js"; +import { sfHome } from "./sf-home.js"; import { deriveState } from "./state.js"; -import { getErrorMessage } from "./error-utils.js"; const UPDATE_REGISTRY_URL = "https://registry.npmjs.org/singularity-forge/latest"; @@ -59,8 +59,7 @@ async function fetchLatestVersionForCommand() { } export function dispatchDoctorHeal(pi, scope, reportText, structuredIssues) { const workflowPath = - process.env.SF_WORKFLOW_PATH ?? - join(sfHome(), "agent", "SF-WORKFLOW.md"); + process.env.SF_WORKFLOW_PATH ?? join(sfHome(), "agent", "SF-WORKFLOW.md"); const workflow = existsSync(workflowPath) ? readFileSync(workflowPath, "utf-8") : ""; @@ -357,10 +356,7 @@ export async function handleTriage(args, ctx, pi, basePath) { "info", ); } catch (err) { - ctx.ui.notify( - `TODO triage failed: ${getErrorMessage(err)}`, - "warning", - ); + ctx.ui.notify(`TODO triage failed: ${getErrorMessage(err)}`, "warning"); } return; } @@ -413,8 +409,7 @@ export async function handleTriage(args, ctx, pi, basePath) { roadmapContext: roadmapContext || "(no active roadmap)", }); const workflowPath = - process.env.SF_WORKFLOW_PATH ?? - join(sfHome(), "agent", "SF-WORKFLOW.md"); + process.env.SF_WORKFLOW_PATH ?? join(sfHome(), "agent", "SF-WORKFLOW.md"); const workflow = readFileSync(workflowPath, "utf-8"); pi.sendMessage( { diff --git a/src/resources/extensions/sf/commands-maintenance.js b/src/resources/extensions/sf/commands-maintenance.js index 7f3a9181e..aaa3f23c9 100644 --- a/src/resources/extensions/sf/commands-maintenance.js +++ b/src/resources/extensions/sf/commands-maintenance.js @@ -3,6 +3,8 @@ * * Contains: handleCleanupBranches, handleCleanupSnapshots, handleCleanupWorktrees, handleSkip, handleDryRun, handleRecover */ + +import { getErrorMessage } from "./error-utils.js"; import { nativeBranchDelete, nativeBranchList, @@ -13,7 +15,6 @@ import { } from "./native-git-bridge.js"; import { deriveState } from "./state.js"; import { logWarning } from "./workflow-logger.js"; -import { getErrorMessage } from "./error-utils.js"; /** * Clean up merged and stale milestone branches. */ diff --git a/src/resources/extensions/sf/commands-plan.js b/src/resources/extensions/sf/commands-plan.js index fa07d351d..84de8cd18 100644 --- a/src/resources/extensions/sf/commands-plan.js +++ b/src/resources/extensions/sf/commands-plan.js @@ -19,7 +19,8 @@ import { statSync, writeFileSync, } from "node:fs"; -import { basename, +import { + basename, dirname, extname, isAbsolute, diff --git a/src/resources/extensions/sf/commands-pr-branch.js b/src/resources/extensions/sf/commands-pr-branch.js index a15507285..ce117c355 100644 --- a/src/resources/extensions/sf/commands-pr-branch.js +++ b/src/resources/extensions/sf/commands-pr-branch.js @@ -6,12 +6,12 @@ * upstream PRs where planning artifacts should not be included. */ import { execFileSync } from "node:child_process"; +import { getErrorMessage } from "./error-utils.js"; import { nativeBranchExists, nativeDetectMainBranch, nativeGetCurrentBranch, } from "./native-git-bridge.js"; -import { getErrorMessage } from "./error-utils.js"; const EXCLUDED_PATHS = [".sf", ".planning", "PLAN.md"]; function git(basePath, args) { @@ -189,8 +189,7 @@ export async function handlePrBranch(args, ctx) { } catch (pickErr) { gitAllowFail(basePath, ["cherry-pick", "--abort"]); gitAllowFail(basePath, ["reset", "--hard", "HEAD"]); - const detail = - getErrorMessage(pickErr); + const detail = getErrorMessage(pickErr); ctx.ui.notify( `Cherry-pick conflict at ${sha.slice(0, 8)}. Picked ${picked}/${commits.length} commits. Resolve manually.\n${detail}`, "warning", diff --git a/src/resources/extensions/sf/commands-scaffold-migrate.js b/src/resources/extensions/sf/commands-scaffold-migrate.js index b973f8d56..2b906dbae 100644 --- a/src/resources/extensions/sf/commands-scaffold-migrate.js +++ b/src/resources/extensions/sf/commands-scaffold-migrate.js @@ -60,12 +60,16 @@ export function runScaffoldMigrate(basePath, targetProfile, opts = {}) { const { prune = false, dryRun = false } = opts; const sfVersion = process.env.SF_VERSION || "0.0.0"; const manifest = readScaffoldManifest(basePath); - const { profileSet: targetSet, warning } = resolveActiveProfileSet(basePath, manifest, targetProfile); + const { profileSet: targetSet, warning } = resolveActiveProfileSet( + basePath, + manifest, + targetProfile, + ); const result = { - reEnabled: [], // state=disabled → state=pending (re-entered profile) - disabled: [], // state=pending → state=disabled (left profile) - pruned: [], // deleted with --prune - warnings: [], // editing/completed/hash-diverged — left alone + reEnabled: [], // state=disabled → state=pending (re-entered profile) + disabled: [], // state=pending → state=disabled (left profile) + pruned: [], // deleted with --prune + warnings: [], // editing/completed/hash-diverged — left alone }; if (warning) { result.warnings.push({ path: "(profile)", reason: warning }); @@ -92,7 +96,10 @@ export function runScaffoldMigrate(basePath, targetProfile, opts = {}) { }); } } catch (err) { - result.warnings.push({ path: file.path, reason: `read error: ${err.message}` }); + result.warnings.push({ + path: file.path, + reason: `read error: ${err.message}`, + }); } } @@ -118,7 +125,8 @@ export function runScaffoldMigrate(basePath, targetProfile, opts = {}) { // User edited the file but marker still says pending — editing-drift. result.warnings.push({ path: file.path, - reason: "state=pending but hash diverged — not auto-disabled (editing-drift)", + reason: + "state=pending but hash diverged — not auto-disabled (editing-drift)", }); continue; } @@ -135,7 +143,10 @@ export function runScaffoldMigrate(basePath, targetProfile, opts = {}) { } } } catch (err) { - result.warnings.push({ path: file.path, reason: `read error: ${err.message}` }); + result.warnings.push({ + path: file.path, + reason: `read error: ${err.message}`, + }); } } @@ -144,7 +155,10 @@ export function runScaffoldMigrate(basePath, targetProfile, opts = {}) { try { writeScaffoldManifest(basePath, { ...manifest, profile: targetProfile }); } catch (err) { - result.warnings.push({ path: "manifest", reason: `manifest write failed: ${err.message}` }); + result.warnings.push({ + path: "manifest", + reason: `manifest write failed: ${err.message}`, + }); } } @@ -209,7 +223,10 @@ export async function handleScaffoldMigrate(args, ctx) { prune: opts.prune, dryRun: opts.dryRun, }); - ctx.ui.notify(formatMigrateResult(result, targetProfile, opts.dryRun), "info"); + ctx.ui.notify( + formatMigrateResult(result, targetProfile, opts.dryRun), + "info", + ); if (!opts.dryRun) { // Run a drift sync to ensure in-profile files that are now pending get written. diff --git a/src/resources/extensions/sf/commands-scan.js b/src/resources/extensions/sf/commands-scan.js index 1504083b6..c5f29b750 100644 --- a/src/resources/extensions/sf/commands-scan.js +++ b/src/resources/extensions/sf/commands-scan.js @@ -15,8 +15,8 @@ */ import { existsSync, mkdirSync } from "node:fs"; import { join, relative } from "node:path"; -import { loadPrompt } from "./prompt-loader.js"; import { getErrorMessage } from "./error-utils.js"; +import { loadPrompt } from "./prompt-loader.js"; // ─── Constants ──────────────────────────────────────────────────────────────── export const DEFAULT_FOCUS = "tech+arch"; export const VALID_FOCUS_AREAS = [ diff --git a/src/resources/extensions/sf/commands-ship.js b/src/resources/extensions/sf/commands-ship.js index ac128c8ff..8e87a0cf7 100644 --- a/src/resources/extensions/sf/commands-ship.js +++ b/src/resources/extensions/sf/commands-ship.js @@ -7,6 +7,7 @@ import { execFileSync } from "node:child_process"; import { existsSync, readdirSync, readFileSync } from "node:fs"; import { formatDuration } from "@singularity-forge/coding-agent"; +import { getErrorMessage } from "./error-utils.js"; import { aggregateByModel, formatCost, @@ -25,7 +26,6 @@ import { resolveSlicePath, } from "./paths.js"; import { deriveState } from "./state.js"; -import { getErrorMessage } from "./error-utils.js"; function git(basePath, args) { return execFileSync("git", args, { cwd: basePath, encoding: "utf-8" }).trim(); diff --git a/src/resources/extensions/sf/commands-todo.js b/src/resources/extensions/sf/commands-todo.js index 68f160a5a..0b353323e 100644 --- a/src/resources/extensions/sf/commands-todo.js +++ b/src/resources/extensions/sf/commands-todo.js @@ -18,6 +18,7 @@ import { } from "node:fs"; import { dirname, join } from "node:path"; import { projectRoot } from "./commands/context.js"; +import { getErrorMessage } from "./error-utils.js"; import { sfRoot } from "./paths.js"; import { addBacklogItem, @@ -27,7 +28,6 @@ import { insertTriageSkill, openDatabase, } from "./sf-db.js"; -import { getErrorMessage } from "./error-utils.js"; const _EMPTY_TODO = "# TODO\n\nDump anything here.\n"; const MAX_DUMP_CHARS = 48_000; @@ -665,9 +665,6 @@ export async function handleTodo(args, ctx, _pi) { "info", ); } catch (err) { - ctx.ui.notify( - `TODO triage failed: ${getErrorMessage(err)}`, - "warning", - ); + ctx.ui.notify(`TODO triage failed: ${getErrorMessage(err)}`, "warning"); } } diff --git a/src/resources/extensions/sf/commands-worktree.js b/src/resources/extensions/sf/commands-worktree.js index 7b717db75..594ed0830 100644 --- a/src/resources/extensions/sf/commands-worktree.js +++ b/src/resources/extensions/sf/commands-worktree.js @@ -6,6 +6,7 @@ // to the CLI surface. import { existsSync } from "node:fs"; import { projectRoot } from "./commands/context.js"; +import { getErrorMessage } from "./error-utils.js"; import { SF_GIT_ERROR, SFError } from "./errors.js"; import { inferCommitType } from "./git-service.js"; import { @@ -14,7 +15,6 @@ import { nativeHasChanges, } from "./native-git-bridge.js"; import { autoCommitCurrentBranch } from "./worktree.js"; -import { getErrorMessage } from "./error-utils.js"; import { diffWorktreeAll, diffWorktreeNumstat, diff --git a/src/resources/extensions/sf/commands/handlers/workflow.js b/src/resources/extensions/sf/commands/handlers/workflow.js index 40266f28f..b6b3ea7d3 100644 --- a/src/resources/extensions/sf/commands/handlers/workflow.js +++ b/src/resources/extensions/sf/commands/handlers/workflow.js @@ -14,6 +14,7 @@ import { handleTemplates, } from "../../commands-workflow-templates.js"; import { validateDefinition } from "../../definition-loader.js"; +import { getErrorMessage } from "../../error-utils.js"; import { findMilestoneIds, showDiscuss, @@ -32,7 +33,6 @@ import { handleQuick } from "../../quick.js"; import { createRun, listRuns } from "../../run-manager.js"; import { deriveState } from "../../state.js"; import { projectRoot } from "../context.js"; -import { getErrorMessage } from "../../error-utils.js"; // ─── Custom Workflow Subcommands ───────────────────────────────────────── const WORKFLOW_USAGE = [ diff --git a/src/resources/extensions/sf/context-board.js b/src/resources/extensions/sf/context-board.js index 7ce5db66d..6ab376309 100644 --- a/src/resources/extensions/sf/context-board.js +++ b/src/resources/extensions/sf/context-board.js @@ -27,7 +27,12 @@ const DEFAULT_MAX_BYTES = 4096; * @param {string} opts.branch — Git branch name. * @returns {string|null} The new entry id, or null on failure. */ -export function addBoardEntry({ content, category = null, repository, branch }) { +export function addBoardEntry({ + content, + category = null, + repository, + branch, +}) { if (!isDbAvailable()) return null; const db = getDatabase(); const id = randomUUID().replace(/-/g, "").slice(0, 16); @@ -108,7 +113,10 @@ export function getBoardEntries({ repository, branch }) { * @param {number} [opts.maxBytes=4096] * @returns {string} Rendered Markdown block, or empty string if no entries. */ -export function formatBoardForPrompt(entries, { maxBytes = DEFAULT_MAX_BYTES } = {}) { +export function formatBoardForPrompt( + entries, + { maxBytes = DEFAULT_MAX_BYTES } = {}, +) { if (!entries || entries.length === 0) return ""; const header = "### Invariants for this repo/branch\n\n"; @@ -123,7 +131,8 @@ export function formatBoardForPrompt(entries, { maxBytes = DEFAULT_MAX_BYTES } = } const allLines = entries.map(formatEntry); - const TRUNCATION_MARKER = "- *[older entries truncated — board exceeded byte cap]*"; + const TRUNCATION_MARKER = + "- *[older entries truncated — board exceeded byte cap]*"; // Build from newest end first, then reverse to restore oldest-first order const encoder = new TextEncoder(); @@ -138,7 +147,10 @@ export function formatBoardForPrompt(entries, { maxBytes = DEFAULT_MAX_BYTES } = // Walk from newest to oldest so we keep the most recent entries under cap for (let i = allLines.length - 1; i >= 0; i--) { const lineBytes = encoder.encode(allLines[i] + "\n").length; - if (usedBytes + lineBytes + (truncated || i === 0 ? 0 : markerBytes) > budget) { + if ( + usedBytes + lineBytes + (truncated || i === 0 ? 0 : markerBytes) > + budget + ) { truncated = true; continue; } diff --git a/src/resources/extensions/sf/debug-session-store.js b/src/resources/extensions/sf/debug-session-store.js index 0916cabf9..fc526bf3c 100644 --- a/src/resources/extensions/sf/debug-session-store.js +++ b/src/resources/extensions/sf/debug-session-store.js @@ -1,8 +1,8 @@ import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { atomicWriteSync } from "./atomic-write.js"; -import { sfRoot } from "./paths.js"; import { getErrorMessage } from "./error-utils.js"; +import { sfRoot } from "./paths.js"; const DEFAULT_PHASE = "queued"; const DEFAULT_STATUS = "active"; diff --git a/src/resources/extensions/sf/detection.js b/src/resources/extensions/sf/detection.js index 477f90fb3..63859b0d2 100644 --- a/src/resources/extensions/sf/detection.js +++ b/src/resources/extensions/sf/detection.js @@ -15,9 +15,9 @@ import { statSync, } from "node:fs"; import { homedir } from "node:os"; -import { sfHome } from './sf-home.js'; import { join } from "node:path"; import { sfRoot } from "./paths.js"; +import { sfHome } from "./sf-home.js"; // ─── Project File Markers ─────────────────────────────────────────────────────── export const PROJECT_FILES = [ diff --git a/src/resources/extensions/sf/doctor-config-checks.js b/src/resources/extensions/sf/doctor-config-checks.js index d921e745c..0f45551c3 100644 --- a/src/resources/extensions/sf/doctor-config-checks.js +++ b/src/resources/extensions/sf/doctor-config-checks.js @@ -7,6 +7,8 @@ * Severity: varies (error for type mismatches, warning for out-of-range values). * Fixable: varies (some are auto-fixable, others are user-config). */ + +import { getErrorMessage } from "./error-utils.js"; import { getContextCompactThreshold, getContextHardLimit, @@ -17,7 +19,6 @@ import { getWorktreeMode, loadEffectiveSFPreferences, } from "./preferences.js"; -import { getErrorMessage } from "./error-utils.js"; /** * Check that all Tier 1.4 config keys are well-formed and within expected ranges. diff --git a/src/resources/extensions/sf/doctor-engine-checks.js b/src/resources/extensions/sf/doctor-engine-checks.js index a82ecd653..692c26180 100644 --- a/src/resources/extensions/sf/doctor-engine-checks.js +++ b/src/resources/extensions/sf/doctor-engine-checks.js @@ -7,6 +7,7 @@ import { statSync, } from "node:fs"; import { join } from "node:path"; +import { getErrorMessage } from "./error-utils.js"; import { migrateHierarchyToDb } from "./md-importer.js"; import { parseRoadmap } from "./parsers.js"; import { milestonesDir, resolveMilestoneFile } from "./paths.js"; @@ -24,7 +25,6 @@ import { } from "./uok/parity-report.js"; import { readEvents } from "./workflow-events.js"; import { renderAllProjections } from "./workflow-projections.js"; -import { getErrorMessage } from "./error-utils.js"; const LEGACY_MILESTONE_DIR_RE = /^(M\d+)-.+$/; const LEGACY_SLICE_DIR_RE = /^(S\d+)-.+$/; diff --git a/src/resources/extensions/sf/doctor.js b/src/resources/extensions/sf/doctor.js index d639f0fb0..276365274 100644 --- a/src/resources/extensions/sf/doctor.js +++ b/src/resources/extensions/sf/doctor.js @@ -24,6 +24,7 @@ import { import { checkEnvironmentHealth } from "./doctor-environment.js"; import { runProviderChecks } from "./doctor-providers.js"; import { GLOBAL_STATE_CODES } from "./doctor-types.js"; +import { getErrorMessage } from "./error-utils.js"; import { countMustHavesMentionedInSummary, loadFile, @@ -57,7 +58,6 @@ import { getMilestoneSlices, getSliceTasks, isDbAvailable } from "./sf-db.js"; import { deriveState, isMilestoneComplete } from "./state.js"; import { isClosedStatus } from "./status-guards.js"; import { parseUnitId } from "./unit-id.js"; -import { getErrorMessage } from "./error-utils.js"; // ─── Flow Audit Implementation ──────────────────────────────────────────── const DEFAULT_STALE_PROGRESS_MS = 20 * 60 * 1000; diff --git a/src/resources/extensions/sf/ecosystem/loader.js b/src/resources/extensions/sf/ecosystem/loader.js index 88a9e8c10..808e94a01 100644 --- a/src/resources/extensions/sf/ecosystem/loader.js +++ b/src/resources/extensions/sf/ecosystem/loader.js @@ -6,9 +6,9 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { pathToFileURL } from "node:url"; import { getAgentDir } from "@singularity-forge/coding-agent"; +import { getErrorMessage } from "../error-utils.js"; import { logWarning } from "../workflow-logger.js"; import { createSFExtensionAPI } from "./sf-extension-api.js"; -import { getErrorMessage } from "../error-utils.js"; // ─── Trust check (inlined; pi does not export isProjectTrusted from its // package root, and constraint forbids modifying packages/coding-agent/) ─ diff --git a/src/resources/extensions/sf/execution-instruction-guard.js b/src/resources/extensions/sf/execution-instruction-guard.js index b682b171f..f965952d4 100644 --- a/src/resources/extensions/sf/execution-instruction-guard.js +++ b/src/resources/extensions/sf/execution-instruction-guard.js @@ -1,5 +1,6 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; +import { getErrorMessage } from "./error-utils.js"; import { resolveTaskFile } from "./paths.js"; import { updateTaskStatus } from "./sf-db.js"; import { invalidateStateCache } from "./state.js"; @@ -7,7 +8,6 @@ import { appendEvent } from "./workflow-events.js"; import { logWarning } from "./workflow-logger.js"; import { writeManifest } from "./workflow-manifest.js"; import { renderAllProjections } from "./workflow-projections.js"; -import { getErrorMessage } from "./error-utils.js"; const REPO_INSTRUCTION_FILES = [ "AGENTS.md", diff --git a/src/resources/extensions/sf/extension-manifest.json b/src/resources/extensions/sf/extension-manifest.json index 66894f301..957aa79d4 100644 --- a/src/resources/extensions/sf/extension-manifest.json +++ b/src/resources/extensions/sf/extension-manifest.json @@ -136,11 +136,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/forensics.js b/src/resources/extensions/sf/forensics.js index 257ab7075..78310aaf9 100644 --- a/src/resources/extensions/sf/forensics.js +++ b/src/resources/extensions/sf/forensics.js @@ -19,7 +19,6 @@ import { join, relative } from "node:path"; import { formatDuration } from "@singularity-forge/coding-agent"; import { showNextAction } from "../shared/tui.js"; import { atomicWriteSync } from "./atomic-write.js"; -import { sfHome } from "./sf-home.js"; import { isAutoActive } from "./auto.js"; import { verifyExpectedArtifact } from "./auto-recovery.js"; import { getAutoWorktreePath } from "./auto-worktree.js"; @@ -56,6 +55,7 @@ import { getSliceTasks, isDbAvailable, } from "./sf-db.js"; +import { sfHome } from "./sf-home.js"; import { deriveState } from "./state.js"; import { isClosedStatus } from "./status-guards.js"; import { diff --git a/src/resources/extensions/sf/gemini-catalog.d.ts b/src/resources/extensions/sf/gemini-catalog.d.ts index 9e9e76654..418aba772 100644 --- a/src/resources/extensions/sf/gemini-catalog.d.ts +++ b/src/resources/extensions/sf/gemini-catalog.d.ts @@ -1,3 +1,7 @@ -export declare function refreshGeminiCatalog(basePath: string): Promise; -export declare function runGeminiCatalogRefreshIfStale(basePath: string): Promise; +export declare function refreshGeminiCatalog( + basePath: string, +): Promise; +export declare function runGeminiCatalogRefreshIfStale( + basePath: string, +): Promise; export declare function scheduleGeminiCatalogRefresh(basePath: string): void; diff --git a/src/resources/extensions/sf/graph-context.js b/src/resources/extensions/sf/graph-context.js index d4aadaed3..80e3e0999 100644 --- a/src/resources/extensions/sf/graph-context.js +++ b/src/resources/extensions/sf/graph-context.js @@ -7,8 +7,8 @@ */ import { readFileSync } from "node:fs"; import { join } from "node:path"; -import { logWarning } from "./workflow-logger.js"; import { getErrorMessage } from "./error-utils.js"; +import { logWarning } from "./workflow-logger.js"; let cachedGraphApi = null; let resolvedGraphApi = false; diff --git a/src/resources/extensions/sf/guided-flow.js b/src/resources/extensions/sf/guided-flow.js index df370be74..67a0e6d2b 100644 --- a/src/resources/extensions/sf/guided-flow.js +++ b/src/resources/extensions/sf/guided-flow.js @@ -114,8 +114,8 @@ export { reserveMilestoneId, } from "./milestone-ids.js"; -import { logWarning } from "./workflow-logger.js"; import { sfHome } from "./sf-home.js"; +import { logWarning } from "./workflow-logger.js"; // ─── Todo/Spec File Detection ──────────────────────────────────────────────── const TODO_FILE_NAMES = ["todo.md", "TODO.md", "SPEC.md", "spec.md"]; @@ -476,8 +476,7 @@ async function dispatchWorkflow( } } const workflowPath = - process.env.SF_WORKFLOW_PATH ?? - join(sfHome(), "agent", "SF-WORKFLOW.md"); + process.env.SF_WORKFLOW_PATH ?? join(sfHome(), "agent", "SF-WORKFLOW.md"); const workflow = readFileSync(workflowPath, "utf-8"); try { await pi.sendMessage( diff --git a/src/resources/extensions/sf/headless-pdd-check.js b/src/resources/extensions/sf/headless-pdd-check.js index 0e71c3339..6f12cb34b 100644 --- a/src/resources/extensions/sf/headless-pdd-check.js +++ b/src/resources/extensions/sf/headless-pdd-check.js @@ -61,7 +61,10 @@ const PDD_FIELDS = [ ], }, { name: "Evidence", aliases: ["Evidence"] }, - { name: "Non-goals", aliases: ["Non-goals", "Non-Goals", "Non goals", "Nongoals"] }, + { + name: "Non-goals", + aliases: ["Non-goals", "Non-Goals", "Non goals", "Nongoals"], + }, { name: "Invariants", aliases: ["Invariants", "Invariant"] }, { name: "Assumptions", aliases: ["Assumptions", "Assumption"] }, ]; @@ -120,7 +123,9 @@ function findFieldBody(content, aliases) { for (let j = i + 1; j < lines.length; j++) { const next = lines[j]; if (!next.trim()) break; - if (/^\s*(?:[-*]\s+)?(?:\*\*)?[A-Z][A-Za-z -]+(?:\*\*)?\s*:/.test(next)) { + if ( + /^\s*(?:[-*]\s+)?(?:\*\*)?[A-Z][A-Za-z -]+(?:\*\*)?\s*:/.test(next) + ) { // Next labelled field starts here. break; } @@ -195,7 +200,8 @@ export function checkPddFields(content) { if (!spineSatisfied) { parts.push(`spine gap: ${spineMissing.join(", ")}`); } - const summary = parts.length === 0 ? "all 8 PDD fields present" : parts.join("; "); + const summary = + parts.length === 0 ? "all 8 PDD fields present" : parts.join("; "); return { ok, diff --git a/src/resources/extensions/sf/hook-emitter.js b/src/resources/extensions/sf/hook-emitter.js index c79621f3a..5442e534f 100644 --- a/src/resources/extensions/sf/hook-emitter.js +++ b/src/resources/extensions/sf/hook-emitter.js @@ -7,8 +7,9 @@ // Set once from `registerSfExtension`. All emitters are best-effort — a // missing `pi` (e.g. in standalone unit tests) logs a warning so callers know // hooks won't fire, but never throws. -import { logWarning } from "./workflow-logger.js"; + import { getErrorMessage } from "./error-utils.js"; +import { logWarning } from "./workflow-logger.js"; let _pi; let _missingPiWarningLogged = false; diff --git a/src/resources/extensions/sf/key-manager.d.ts b/src/resources/extensions/sf/key-manager.d.ts index 598e3ce99..32ed3a292 100644 --- a/src/resources/extensions/sf/key-manager.d.ts +++ b/src/resources/extensions/sf/key-manager.d.ts @@ -3,5 +3,15 @@ import type { AuthStorage } from "@singularity-forge/coding-agent"; export declare function getKeyManagerAuthStorage(): AuthStorage; export declare function getAuthPath(): string; export declare function maskKey(key: string): string; -export declare function findProvider(idOrLabel: string): { id: string; label: string; category: string; envVar?: string } | undefined; -export declare const PROVIDER_REGISTRY: Array<{ id: string; label: string; category: string; envVar?: string; envVarFallback?: string; hasOAuth?: boolean; dashboardUrl?: string }>; +export declare function findProvider( + idOrLabel: string, +): { id: string; label: string; category: string; envVar?: string } | undefined; +export declare const PROVIDER_REGISTRY: Array<{ + id: string; + label: string; + category: string; + envVar?: string; + envVarFallback?: string; + hasOAuth?: boolean; + dashboardUrl?: string; +}>; diff --git a/src/resources/extensions/sf/learning/runtime.js b/src/resources/extensions/sf/learning/runtime.js index c194c5f12..f980cc7fe 100644 --- a/src/resources/extensions/sf/learning/runtime.js +++ b/src/resources/extensions/sf/learning/runtime.js @@ -1,9 +1,9 @@ +import { getErrorMessage } from "../error-utils.js"; import { getDatabase, getDbPath, insertLlmTaskOutcome } from "../sf-db.js"; import { logWarning } from "../workflow-logger.js"; import { createBeforeModelSelectHandler } from "./hook-handler.mjs"; import { loadCapabilityOverrides } from "./loadCapabilityOverrides.mjs"; import { validateOutcome } from "./outcome-recorder.mjs"; -import { getErrorMessage } from "../error-utils.js"; const DEFAULT_N_PRIOR = 10; const DEFAULT_ROLLING_DAYS = 30; diff --git a/src/resources/extensions/sf/lifecycle-hooks.js b/src/resources/extensions/sf/lifecycle-hooks.js index 4c9775476..b05aa689e 100644 --- a/src/resources/extensions/sf/lifecycle-hooks.js +++ b/src/resources/extensions/sf/lifecycle-hooks.js @@ -7,8 +7,8 @@ * Consumer: unit-runtime.js, dispatch-engine.js, or unit completion handlers. */ -import { flushSyncQueue } from "./sync-scheduler.js"; import { getErrorMessage } from "./error-utils.js"; +import { flushSyncQueue } from "./sync-scheduler.js"; /** * Flush SM sync queue for a project before unit completes. diff --git a/src/resources/extensions/sf/md-file-tracker.js b/src/resources/extensions/sf/md-file-tracker.js index d14ed66d3..38b0cebb2 100644 --- a/src/resources/extensions/sf/md-file-tracker.js +++ b/src/resources/extensions/sf/md-file-tracker.js @@ -7,10 +7,11 @@ * * Consumer: bootstrap/register-hooks.js session_start hook. */ + +import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { join, relative } from "node:path"; -import { spawnSync } from "node:child_process"; import { deactivateTrackedMdFile, getAllTrackedMdFiles, @@ -22,7 +23,11 @@ import { isDbAvailable } from "./sf-db.js"; // ─── Exclusions ─────────────────────────────────────────────────────────────── /** Uppercase-root md files excluded by design: high churn, low signal. */ -const EXCLUDED_ROOT_FILES = new Set(["TODO.md", "CHANGELOG.md", "BUILD_PLAN.md"]); +const EXCLUDED_ROOT_FILES = new Set([ + "TODO.md", + "CHANGELOG.md", + "BUILD_PLAN.md", +]); // ─── Hashing ───────────────────────────────────────────────────────────────── @@ -38,8 +43,11 @@ function hashFile(absPath) { function getCurrentCommit(cwd) { try { - const r = spawnSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8" }); - return r.status === 0 ? (r.stdout.trim() || null) : null; + const r = spawnSync("git", ["rev-parse", "HEAD"], { + cwd, + encoding: "utf-8", + }); + return r.status === 0 ? r.stdout.trim() || null : null; } catch { return null; } @@ -53,7 +61,7 @@ function gitDiffForFile(cwd, relpath, sinceCommit) { ["diff", "--unified=3", sinceCommit, "--", relpath], { cwd, encoding: "utf-8" }, ); - return r.status === 0 ? (r.stdout.trim() || null) : null; + return r.status === 0 ? r.stdout.trim() || null : null; } catch { return null; } @@ -96,15 +104,25 @@ function discoverTrackedFiles(repoRoot) { if (!name.endsWith(".md")) continue; if (EXCLUDED_ROOT_FILES.has(name)) continue; if (/^[A-Z][A-Z_\-0-9]*\.md$/.test(name)) { - results.push({ relpath: name, absPath: join(repoRoot, name), category: "meta" }); + results.push({ + relpath: name, + absPath: join(repoRoot, name), + category: "meta", + }); } } - } catch { /* noop if root unreadable */ } + } catch { + /* noop if root unreadable */ + } // .github/copilot-instructions.md const copilotMd = join(repoRoot, ".github", "copilot-instructions.md"); if (existsSync(copilotMd)) { - results.push({ relpath: ".github/copilot-instructions.md", absPath: copilotMd, category: "meta" }); + results.push({ + relpath: ".github/copilot-instructions.md", + absPath: copilotMd, + category: "meta", + }); } // docs/adr/**/*.md @@ -145,19 +163,46 @@ export function detectMdFileDrift(repoRoot) { const sha = hashFile(absPath); if (!sha) continue; let sizeBytes = 0; - try { sizeBytes = statSync(absPath).size; } catch { /* noop */ } + try { + sizeBytes = statSync(absPath).size; + } catch { + /* noop */ + } const existing = getTrackedMdFile(relpath); if (!existing) { - upsertTrackedMdFile({ relpath, sha256: sha, sizeBytes, lastSeenCommit: headCommit, category }); + upsertTrackedMdFile({ + relpath, + sha256: sha, + sizeBytes, + lastSeenCommit: headCommit, + category, + }); added.push({ relpath, category }); } else if (existing.sha256 !== sha) { const diff = gitDiffForFile(repoRoot, relpath, existing.last_seen_commit); - upsertTrackedMdFile({ relpath, sha256: sha, sizeBytes, lastSeenCommit: headCommit, category }); - changed.push({ relpath, category, prevCommit: existing.last_seen_commit, diff }); + upsertTrackedMdFile({ + relpath, + sha256: sha, + sizeBytes, + lastSeenCommit: headCommit, + category, + }); + changed.push({ + relpath, + category, + prevCommit: existing.last_seen_commit, + diff, + }); } else { // Unchanged — refresh timestamp and commit pointer so we stay current. - upsertTrackedMdFile({ relpath, sha256: sha, sizeBytes, lastSeenCommit: headCommit, category }); + upsertTrackedMdFile({ + relpath, + sha256: sha, + sizeBytes, + lastSeenCommit: headCommit, + category, + }); } } @@ -184,15 +229,21 @@ export function detectMdFileDrift(repoRoot) { export function formatDriftReport({ changed, added, deleted }) { const lines = []; if (changed.length > 0) { - lines.push(`${changed.length} tracked md file${changed.length === 1 ? "" : "s"} changed since last session:`); + lines.push( + `${changed.length} tracked md file${changed.length === 1 ? "" : "s"} changed since last session:`, + ); for (const { relpath } of changed) lines.push(` • ${relpath}`); } if (added.length > 0) { - lines.push(`${added.length} new tracked md file${added.length === 1 ? "" : "s"} now tracked:`); + lines.push( + `${added.length} new tracked md file${added.length === 1 ? "" : "s"} now tracked:`, + ); for (const { relpath } of added) lines.push(` • ${relpath}`); } if (deleted.length > 0) { - lines.push(`${deleted.length} previously tracked md file${deleted.length === 1 ? "" : "s"} removed:`); + lines.push( + `${deleted.length} previously tracked md file${deleted.length === 1 ? "" : "s"} removed:`, + ); for (const relpath of deleted) lines.push(` • ${relpath}`); } return lines.join("\n"); diff --git a/src/resources/extensions/sf/memory-embeddings.js b/src/resources/extensions/sf/memory-embeddings.js index 9b1de678a..ecd7dadd1 100644 --- a/src/resources/extensions/sf/memory-embeddings.js +++ b/src/resources/extensions/sf/memory-embeddings.js @@ -20,8 +20,8 @@ import { isDbAvailable, upsertMemoryEmbedding, } from "./sf-db.js"; -import { logWarning } from "./workflow-logger.js"; import { sfHome } from "./sf-home.js"; +import { logWarning } from "./workflow-logger.js"; /** Read the llm-gateway entry from ~/.sf/agent/auth.json if present. * Falls back to SF_LLM_GATEWAY_KEY env var when auth.json is missing @@ -34,7 +34,11 @@ function readGatewayFromAuthJson() { const data = JSON.parse(readFileSync(authPath, "utf8")); const entry = data["llm-gateway"]; if (entry?.key) { - return { key: entry.key, url: entry.url || null, source: "auth.json:llm-gateway" }; + return { + key: entry.key, + url: entry.url || null, + source: "auth.json:llm-gateway", + }; } } } catch { @@ -76,7 +80,9 @@ export function loadGatewayConfigFromEnv() { url: fromAuth.url ?? "https://llm-gateway.centralcloud.com/v1", apiKey: fromAuth.key, keySource: fromAuth.source ?? "auth.json:llm-gateway", - urlSource: fromAuth.url ? (fromAuth.source ?? "auth.json:llm-gateway") : "default", + urlSource: fromAuth.url + ? (fromAuth.source ?? "auth.json:llm-gateway") + : "default", embeddingModel, rerankModel, queryInstruction, diff --git a/src/resources/extensions/sf/metrics.js b/src/resources/extensions/sf/metrics.js index 59d9aa630..fba422613 100644 --- a/src/resources/extensions/sf/metrics.js +++ b/src/resources/extensions/sf/metrics.js @@ -79,7 +79,9 @@ async function recordUnitOutcome(unit) { // Quick Win #2: Also record to model-learner for per-task-type tracking try { - const { ModelLearner, registryReady } = await import("./model-learner.js"); + const { ModelLearner, registryReady } = await import( + "./model-learner.js" + ); // Await registry load so canonicalIdFor is wired before the first // recordOutcome() call. Without this, the timing window between module // load and registry resolution causes all routes to land in _unmapped. diff --git a/src/resources/extensions/sf/model-catalog-cache.d.ts b/src/resources/extensions/sf/model-catalog-cache.d.ts index 350364c0f..08dd37772 100644 --- a/src/resources/extensions/sf/model-catalog-cache.d.ts +++ b/src/resources/extensions/sf/model-catalog-cache.d.ts @@ -1,7 +1,42 @@ -export declare function readCachedModelIds(basePath: string, providerId: string): string[] | null; -export declare function getCachedModelIds(basePath: string, providerId: string): string[]; -export declare function refreshProviderCatalog(basePath: string, providerId: string, apiKey: string): Promise; -export declare function scheduleModelCatalogRefresh(basePath: string, auth: { getCredentialsForProvider: (id: string) => Array<{ type: string; key?: string }> }): void; -export declare function runModelCatalogRefreshIfStale(basePath: string, auth: { getCredentialsForProvider: (id: string) => Array<{ type: string; key?: string }> }): Promise; -export declare function refreshSfManagedProviders(basePath: string, auth: { getCredentialsForProvider: (id: string) => Array<{ type: string; key?: string }> }): Promise; -export declare function getKnownModelIds(basePath: string, providerId: string, sdkModelIds?: string[]): string[]; +export declare function readCachedModelIds( + basePath: string, + providerId: string, +): string[] | null; +export declare function getCachedModelIds( + basePath: string, + providerId: string, +): string[]; +export declare function refreshProviderCatalog( + basePath: string, + providerId: string, + apiKey: string, +): Promise; +export declare function scheduleModelCatalogRefresh( + basePath: string, + auth: { + getCredentialsForProvider: ( + id: string, + ) => Array<{ type: string; key?: string }>; + }, +): void; +export declare function runModelCatalogRefreshIfStale( + basePath: string, + auth: { + getCredentialsForProvider: ( + id: string, + ) => Array<{ type: string; key?: string }>; + }, +): Promise; +export declare function refreshSfManagedProviders( + basePath: string, + auth: { + getCredentialsForProvider: ( + id: string, + ) => Array<{ type: string; key?: string }>; + }, +): Promise; +export declare function getKnownModelIds( + basePath: string, + providerId: string, + sdkModelIds?: string[], +): string[]; diff --git a/src/resources/extensions/sf/model-catalog-cache.js b/src/resources/extensions/sf/model-catalog-cache.js index 4aec2de00..1f7867b03 100644 --- a/src/resources/extensions/sf/model-catalog-cache.js +++ b/src/resources/extensions/sf/model-catalog-cache.js @@ -16,14 +16,20 @@ * Provider configuration (base URL, auth format, model filter patterns) comes * from provider-catalog-config.js — not hardcoded here. */ -import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdirSync, + readFileSync, + renameSync, + writeFileSync, +} from "node:fs"; import { join } from "node:path"; -import { sfHome } from "./sf-home.js"; import { DISCOVERABLE_PROVIDER_IDS, getProviderCatalogConfig, getProviderModelExcludePatterns, } from "./provider-catalog-config.js"; +import { sfHome } from "./sf-home.js"; /** * @typedef {{ id: string, cost?: { input: number, output: number, cacheRead: number, cacheWrite: number }, contextWindow?: number }} DiscoveredModelEntry @@ -137,7 +143,10 @@ function writeCacheEntry(basePath, providerId, modelEntries) { mkdirSync(cacheDirPath(basePath), { recursive: true }); writeFileSync( cacheFilePath(basePath, providerId), - JSON.stringify({ fetchedAt: new Date().toISOString(), modelIds: modelEntries }), + JSON.stringify({ + fetchedAt: new Date().toISOString(), + modelIds: modelEntries, + }), "utf-8", ); } catch { @@ -178,7 +187,8 @@ function writeSdkDiscoveryCacheEntry(providerId, modelEntries) { const models = modelEntries.map((entry) => { const id = typeof entry === "string" ? entry : entry.id; const cost = typeof entry === "object" ? entry.cost : undefined; - const contextWindow = typeof entry === "object" ? entry.contextWindow : undefined; + const contextWindow = + typeof entry === "object" ? entry.contextWindow : undefined; return { id, name: id, @@ -273,7 +283,9 @@ export function parseDiscoveredModels(cfg, json) { cacheRead: parseFloat(m.pricing.input_cache_read ?? "0") || 0, cacheWrite: parseFloat(m.pricing.input_cache_write ?? "0") || 0, }, - ...(m.context_length != null ? { contextWindow: m.context_length } : {}), + ...(m.context_length != null + ? { contextWindow: m.context_length } + : {}), }; } return { id }; diff --git a/src/resources/extensions/sf/model-learner.js b/src/resources/extensions/sf/model-learner.js index 99c73519b..048acdb1d 100644 --- a/src/resources/extensions/sf/model-learner.js +++ b/src/resources/extensions/sf/model-learner.js @@ -67,7 +67,10 @@ export function setRegistryResolver(fn) { // still null (which routes everything to _unmapped). export const registryReady = import("./model-registry.js") .then((mod) => { - if (_canonicalIdForFn === null && typeof mod?.canonicalIdFor === "function") { + if ( + _canonicalIdForFn === null && + typeof mod?.canonicalIdFor === "function" + ) { _canonicalIdForFn = mod.canonicalIdFor; } }) @@ -232,7 +235,11 @@ class ModelPerformanceTracker { // Check if any unit-type blob is still in old format let needsMigration = false; for (const unitTypeBlob of Object.values(parsed)) { - if (typeof unitTypeBlob === "object" && unitTypeBlob !== null && isOldFormat(unitTypeBlob)) { + if ( + typeof unitTypeBlob === "object" && + unitTypeBlob !== null && + isOldFormat(unitTypeBlob) + ) { needsMigration = true; break; } @@ -269,7 +276,11 @@ class ModelPerformanceTracker { if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } - writeFileSync(this.storagePath, JSON.stringify(migrated, null, 2), "utf-8"); + writeFileSync( + this.storagePath, + JSON.stringify(migrated, null, 2), + "utf-8", + ); } catch { // Non-fatal } @@ -346,7 +357,14 @@ class ModelPerformanceTracker { unmapped.by_route[routeKey] = emptyRouteStats(timestamp); } const rs = unmapped.by_route[routeKey]; - this._applyOutcomeToStats(rs, success, timeout, tokensUsed, costUsd, timestamp); + this._applyOutcomeToStats( + rs, + success, + timeout, + tokensUsed, + costUsd, + timestamp, + ); } else { // Known route → write to by_route + recompute aggregate if (!this.data[taskType][canonicalId]) { @@ -360,7 +378,14 @@ class ModelPerformanceTracker { canonicalEntry.by_route[routeKey] = emptyRouteStats(timestamp); } const rs = canonicalEntry.by_route[routeKey]; - this._applyOutcomeToStats(rs, success, timeout, tokensUsed, costUsd, timestamp); + this._applyOutcomeToStats( + rs, + success, + timeout, + tokensUsed, + costUsd, + timestamp, + ); recomputeAggregate(canonicalEntry); } @@ -370,7 +395,14 @@ class ModelPerformanceTracker { /** * Apply a single outcome event to a stats object in-place. */ - _applyOutcomeToStats(stats, success, timeout, tokensUsed, costUsd, timestamp) { + _applyOutcomeToStats( + stats, + success, + timeout, + tokensUsed, + costUsd, + timestamp, + ) { if (success) { stats.successes += 1; } else if (timeout) { @@ -426,12 +458,20 @@ class ModelPerformanceTracker { if (val?.by_route?.[canonicalOrRouteKey]) { const rs = val.by_route[canonicalOrRouteKey]; const total = rs.successes + rs.failures; - return { ...rs, total, successRate: total > 0 ? rs.successes / total : 0 }; + return { + ...rs, + total, + successRate: total > 0 ? rs.successes / total : 0, + }; } } else if (val?.by_route?.[canonicalOrRouteKey]) { const rs = val.by_route[canonicalOrRouteKey]; const total = rs.successes + rs.failures; - return { ...rs, total, successRate: total > 0 ? rs.successes / total : 0 }; + return { + ...rs, + total, + successRate: total > 0 ? rs.successes / total : 0, + }; } } return null; diff --git a/src/resources/extensions/sf/model-registry.ts b/src/resources/extensions/sf/model-registry.ts index dbd240f30..166f06444 100644 --- a/src/resources/extensions/sf/model-registry.ts +++ b/src/resources/extensions/sf/model-registry.ts @@ -9,6 +9,9 @@ * 3. Generation tag (same-generation routes are direct failover candidates) */ +import { readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; // ─── Upstream data import ───────────────────────────────────────────────────── // Use the public API of @singularity-forge/ai so we get: // 1. Both generated + CUSTOM_MODELS entries (e.g. kimi-coding/kimi-for-coding, @@ -17,9 +20,6 @@ // and runtime (~/.sf/agent/extensions/sf/) — relative paths into the // monorepo can't satisfy the latter. import { getModels, getProviders } from "@singularity-forge/ai"; -import { readFileSync } from "node:fs"; -import { homedir } from "node:os"; -import { join } from "node:path"; // ─── Public types ───────────────────────────────────────────────────────────── @@ -138,8 +138,7 @@ const CANONICAL_BY_ROUTE: Record = { "amazon-bedrock/global.anthropic.claude-sonnet-4-6": "claude-sonnet-4-6", "amazon-bedrock/google.gemma-3-27b-it": "gemma-3-27b-it", "amazon-bedrock/google.gemma-3-4b-it": "gemma-3-4b-it", - "amazon-bedrock/meta.llama3-1-405b-instruct-v1:0": - "llama3-1-405b-instruct", + "amazon-bedrock/meta.llama3-1-405b-instruct-v1:0": "llama3-1-405b-instruct", "amazon-bedrock/meta.llama3-1-70b-instruct-v1:0": "llama3-1-70b-instruct", "amazon-bedrock/meta.llama3-1-8b-instruct-v1:0": "llama3-1-8b-instruct", "amazon-bedrock/meta.llama3-2-11b-instruct-v1:0": "llama3-2-11b-instruct", @@ -495,7 +494,8 @@ const CANONICAL_BY_ROUTE: Record = { "vercel-ai-gateway/moonshotai/kimi-k2": "kimi-k2", "vercel-ai-gateway/moonshotai/kimi-k2-0905": "kimi-k2-0905", "vercel-ai-gateway/moonshotai/kimi-k2-thinking": "kimi-k2-thinking", - "vercel-ai-gateway/moonshotai/kimi-k2-thinking-turbo": "kimi-k2-thinking-turbo", + "vercel-ai-gateway/moonshotai/kimi-k2-thinking-turbo": + "kimi-k2-thinking-turbo", "vercel-ai-gateway/moonshotai/kimi-k2-turbo": "kimi-k2-turbo", "vercel-ai-gateway/moonshotai/kimi-k2.5": "kimi-k2.5", "vercel-ai-gateway/openai/gpt-4-turbo": "gpt-4-turbo", @@ -788,12 +788,15 @@ let _discoveryCacheLoadedAt = 0; const DISCOVERY_CACHE_TTL_MS = 60_000; // re-read at most once a minute /** Override for tests — bypasses file read entirely. Set to `undefined` to restore normal behaviour. */ -let _discoveryCacheOverride: any = undefined; +let _discoveryCacheOverride: any; function getDiscoveryCache(): any { if (_discoveryCacheOverride !== undefined) return _discoveryCacheOverride; const now = Date.now(); - if (_discoveryCache !== null && now - _discoveryCacheLoadedAt < DISCOVERY_CACHE_TTL_MS) { + if ( + _discoveryCache !== null && + now - _discoveryCacheLoadedAt < DISCOVERY_CACHE_TTL_MS + ) { return _discoveryCache; } try { @@ -833,7 +836,10 @@ export function __setDiscoveryCacheForTest(cache: any): void { * * Consumer: canonicalIdFor's fallback path when CANONICAL_BY_ROUTE misses. */ -function canonicalIdFromDiscovery(provider: string, modelId: string): string | null { +function canonicalIdFromDiscovery( + provider: string, + modelId: string, +): string | null { try { const cache = getDiscoveryCache(); const entry = cache?.entries?.[provider]; @@ -859,7 +865,12 @@ const _ENTRY_BY_ROUTE = new Map< reasoning?: boolean; input?: string[]; capabilities?: Record; - cost?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number }; + cost?: { + input?: number; + output?: number; + cacheRead?: number; + cacheWrite?: number; + }; contextWindow?: number; maxTokens?: number; } @@ -884,7 +895,12 @@ const _ROUTES_BY_CANONICAL = new Map(); reasoning?: boolean; input?: string[]; capabilities?: Record; - cost?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number }; + cost?: { + input?: number; + output?: number; + cacheRead?: number; + cacheWrite?: number; + }; contextWindow?: number; maxTokens?: number; }; @@ -911,7 +927,7 @@ const _ROUTES_BY_CANONICAL = new Map(); function resolveEntry( routeKey: RouteKey, - entry: ReturnType, + entry: ReturnType<(typeof _ENTRY_BY_ROUTE)["get"]>, ): ResolvedModel | null { if (!entry) return null; const canonical = CANONICAL_BY_ROUTE[routeKey] ?? entry.id; @@ -937,10 +953,7 @@ function resolveEntry( // ─── Public API ─────────────────────────────────────────────────────────────── /** Look up a (provider, wire_id) pair. Returns null if not in upstream. */ -export function lookup( - provider: string, - wireId: string, -): ResolvedModel | null { +export function lookup(provider: string, wireId: string): ResolvedModel | null { const routeKey = `${provider}/${wireId}` as RouteKey; return lookupRoute(routeKey); } @@ -965,9 +978,7 @@ export function routesFor(canonicalId: CanonicalId): ResolvedModel[] { } /** Map a route key to a canonical id, or null if unmappable. */ -export function canonicalIdFor( - routeKey: RouteKey, -): CanonicalId | null { +export function canonicalIdFor(routeKey: RouteKey): CanonicalId | null { // Fast path 1: static alias table (wins over dynamic — allows canonical // remapping where the wire id differs from the desired canonical id, e.g. // kimi-coding/kimi-for-coding → kimi-k2.6). Identity-strip entries for @@ -1021,9 +1032,6 @@ export function allCanonicalIds(): CanonicalId[] { } /** Build a route key from a resolved model (for metrics aggregation). */ -export function routeKeyOf(m: { - provider: string; - wire_id: string; -}): RouteKey { +export function routeKeyOf(m: { provider: string; wire_id: string }): RouteKey { return `${m.provider}/${m.wire_id}` as RouteKey; } diff --git a/src/resources/extensions/sf/model-route-failure.js b/src/resources/extensions/sf/model-route-failure.js index ef14fd8ea..f9269d5a6 100644 --- a/src/resources/extensions/sf/model-route-failure.js +++ b/src/resources/extensions/sf/model-route-failure.js @@ -20,7 +20,12 @@ const SOLVER_PINNED_UNIT_TYPE = "autonomous-solver"; * @param {string} unitType - active unit type at failover time * @param {string} reason - human-readable reason label */ -export function logGenerationDowngrade(fromCanonical, toCanonical, unitType, reason) { +export function logGenerationDowngrade( + fromCanonical, + toCanonical, + unitType, + reason, +) { logWarning("model-route-failure", "generation-downgrade", { from: fromCanonical, to: toCanonical, @@ -125,7 +130,9 @@ export function resolveNextAvailableModelRoute(args) { const currentRouteKey = args.current ? `${args.current.provider}/${args.current.id}` : undefined; - const currentCanonical = currentRouteKey ? canonicalIdFor(currentRouteKey) : null; + const currentCanonical = currentRouteKey + ? canonicalIdFor(currentRouteKey) + : null; const isSolverPinned = args.unitType === SOLVER_PINNED_UNIT_TYPE; const failedKeys = new Set( diff --git a/src/resources/extensions/sf/model-router.js b/src/resources/extensions/sf/model-router.js index 89ce9db80..ca61e7b8b 100644 --- a/src/resources/extensions/sf/model-router.js +++ b/src/resources/extensions/sf/model-router.js @@ -1381,7 +1381,8 @@ export function resolveModelForComplexity( : stickyHint.id; // Match either "provider/model" or bare model id in the eligible list. const found = scored.find( - (s) => s.modelId === stickyKey || s.modelId.endsWith(`/${stickyHint.id}`), + (s) => + s.modelId === stickyKey || s.modelId.endsWith(`/${stickyHint.id}`), ); if (!found) return null; if (winner.score - found.score > STICKY_WINDOW_POINTS) return null; diff --git a/src/resources/extensions/sf/onboarding-state.js b/src/resources/extensions/sf/onboarding-state.js index 3697e25e9..e1d22046f 100644 --- a/src/resources/extensions/sf/onboarding-state.js +++ b/src/resources/extensions/sf/onboarding-state.js @@ -12,9 +12,9 @@ import { writeFileSync, } from "node:fs"; import { dirname, join } from "node:path"; -import { logWarning } from "./workflow-logger.js"; -import { sfHome } from "./sf-home.js"; import { getErrorMessage } from "./error-utils.js"; +import { sfHome } from "./sf-home.js"; +import { logWarning } from "./workflow-logger.js"; /** * Bump `FLOW_VERSION` whenever a new required step is added to ONBOARDING_STEPS. * Records with an older flowVersion are treated as "needs partial re-onboarding" @@ -26,8 +26,7 @@ const RECORD_VERSION = 1; // Inline agentDir computation — keep this module rootDir-clean for the // resources tsconfig; importing from src/ pulls files outside src/resources // and breaks the build. -const AGENT_DIR = - process.env.SF_CODING_AGENT_DIR || join(sfHome(), "agent"); +const AGENT_DIR = process.env.SF_CODING_AGENT_DIR || join(sfHome(), "agent"); const FILE = join(AGENT_DIR, "onboarding.json"); const DEFAULT = { version: RECORD_VERSION, diff --git a/src/resources/extensions/sf/openai-codex-catalog.d.ts b/src/resources/extensions/sf/openai-codex-catalog.d.ts index 8ef765016..87f33fd8c 100644 --- a/src/resources/extensions/sf/openai-codex-catalog.d.ts +++ b/src/resources/extensions/sf/openai-codex-catalog.d.ts @@ -1,4 +1,10 @@ export declare function readCodexAvailableModels(): Promise; -export declare function refreshOpenaiCodexCatalog(basePath?: string): Promise; -export declare function runOpenaiCodexCatalogRefreshIfStale(basePath?: string): Promise; -export declare function scheduleOpenaiCodexCatalogRefresh(basePath?: string): void; +export declare function refreshOpenaiCodexCatalog( + basePath?: string, +): Promise; +export declare function runOpenaiCodexCatalogRefreshIfStale( + basePath?: string, +): Promise; +export declare function scheduleOpenaiCodexCatalogRefresh( + basePath?: string, +): void; diff --git a/src/resources/extensions/sf/orphan-worktree-sweep.js b/src/resources/extensions/sf/orphan-worktree-sweep.js index b4be78857..5adca85e7 100644 --- a/src/resources/extensions/sf/orphan-worktree-sweep.js +++ b/src/resources/extensions/sf/orphan-worktree-sweep.js @@ -15,13 +15,13 @@ import { randomUUID } from "node:crypto"; import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; +import { getErrorMessage } from "./error-utils.js"; import { emitJournalEvent } from "./journal.js"; import { removeWorktree, worktreePath, worktreesDir, } from "./worktree-manager.js"; -import { getErrorMessage } from "./error-utils.js"; // ─── Internal Helpers ───────────────────────────────────────────────────────── /** diff --git a/src/resources/extensions/sf/planning-depth.js b/src/resources/extensions/sf/planning-depth.js index 66d4292cb..d7cdb4b18 100644 --- a/src/resources/extensions/sf/planning-depth.js +++ b/src/resources/extensions/sf/planning-depth.js @@ -6,9 +6,9 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { parse as parseYaml, stringify as stringifyYaml } from "yaml"; +import { getErrorMessage } from "./error-utils.js"; import { sfRoot } from "./paths.js"; import { logWarning } from "./workflow-logger.js"; -import { getErrorMessage } from "./error-utils.js"; /** * Resolve the path to the project-level preferences file. diff --git a/src/resources/extensions/sf/preferences-models.js b/src/resources/extensions/sf/preferences-models.js index 2126064d3..07e344458 100644 --- a/src/resources/extensions/sf/preferences-models.js +++ b/src/resources/extensions/sf/preferences-models.js @@ -124,7 +124,8 @@ function isModelAllowedByBuiltInProviderPolicy(provider, modelId, model) { // model?.cost is populated for statically-known models from the upstream SDK catalog. // For dynamically-discovered models (absent from the catalog), fall back to the // per-provider discovery cache which now carries pricing from /api/v1/models. - const effectiveCost = model?.cost ?? lookupDiscoveredModelCost("openrouter", modelId); + const effectiveCost = + model?.cost ?? lookupDiscoveredModelCost("openrouter", modelId); return ( providerModelAllowEntryMatches(":free", modelKey) || isZeroCost(effectiveCost) diff --git a/src/resources/extensions/sf/production-mutation-approval.js b/src/resources/extensions/sf/production-mutation-approval.js index 620db1f46..ac8451a14 100644 --- a/src/resources/extensions/sf/production-mutation-approval.js +++ b/src/resources/extensions/sf/production-mutation-approval.js @@ -1,8 +1,8 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { atomicWriteSync } from "./atomic-write.js"; -import { sfRoot } from "./paths.js"; import { getErrorMessage } from "./error-utils.js"; +import { sfRoot } from "./paths.js"; /** Schema version for production mutation approval JSON artifacts. */ export const PRODUCTION_MUTATION_APPROVAL_SCHEMA_VERSION = 1; function unitId(unit) { diff --git a/src/resources/extensions/sf/prompt-loader.js b/src/resources/extensions/sf/prompt-loader.js index 9aca18461..d91cefb2e 100644 --- a/src/resources/extensions/sf/prompt-loader.js +++ b/src/resources/extensions/sf/prompt-loader.js @@ -142,24 +142,27 @@ export function loadPrompt(name, vars = {}) { // visible to the validator below and are substituted in the normal pass. // The include pattern uses ":" which is outside the validator regex [a-zA-Z0-9_], // so unresolved includes never accidentally pass validation. - content = content.replace(/\{\{include:([a-zA-Z][a-zA-Z0-9_-]*)\}\}/g, (_, fragName) => { - let frag = templateCache.get(`frg:${fragName}`); - if (frag === undefined) { - // Lazy-load fallback (mirrors the prompt lazy-load path): allows fragments - // added after warmCache() ran (e.g. in test environments) to be resolved. - try { - frag = readFileSync(join(fragmentsDir, `${fragName}.md`), "utf-8"); - templateCache.set(`frg:${fragName}`, frag); - } catch { - throw new SFError( - SF_PARSE_ERROR, - `loadPrompt("${name}"): unknown fragment "{{include:${fragName}}}". ` + - `Create prompts/fragments/${fragName}.md and rebuild.`, - ); + content = content.replace( + /\{\{include:([a-zA-Z][a-zA-Z0-9_-]*)\}\}/g, + (_, fragName) => { + let frag = templateCache.get(`frg:${fragName}`); + if (frag === undefined) { + // Lazy-load fallback (mirrors the prompt lazy-load path): allows fragments + // added after warmCache() ran (e.g. in test environments) to be resolved. + try { + frag = readFileSync(join(fragmentsDir, `${fragName}.md`), "utf-8"); + templateCache.set(`frg:${fragName}`, frag); + } catch { + throw new SFError( + SF_PARSE_ERROR, + `loadPrompt("${name}"): unknown fragment "{{include:${fragName}}}". ` + + `Create prompts/fragments/${fragName}.md and rebuild.`, + ); + } } - } - return frag; - }); + return frag; + }, + ); // Check BEFORE substitution: find all {{varName}} placeholders the template // declares and verify every one has a value in vars. Checking after substitution // would also flag {{...}} patterns injected by inlined content (e.g. template diff --git a/src/resources/extensions/sf/provider-quota-cache.d.ts b/src/resources/extensions/sf/provider-quota-cache.d.ts index fd2bab846..43cbfacfe 100644 --- a/src/resources/extensions/sf/provider-quota-cache.d.ts +++ b/src/resources/extensions/sf/provider-quota-cache.d.ts @@ -19,7 +19,18 @@ export interface AuthLike { } export declare const QUOTA_CAPABLE_PROVIDER_IDS: readonly string[]; -export declare function getProviderQuotaState(providerId: string): ProviderQuotaEntry | null; -export declare function getAllProviderQuotaEntries(): Record; -export declare function runProviderQuotaRefreshIfStale(basePath: string, auth: AuthLike): Promise; -export declare function scheduleProviderQuotaRefresh(basePath: string, auth: AuthLike): void; +export declare function getProviderQuotaState( + providerId: string, +): ProviderQuotaEntry | null; +export declare function getAllProviderQuotaEntries(): Record< + string, + ProviderQuotaEntry +>; +export declare function runProviderQuotaRefreshIfStale( + basePath: string, + auth: AuthLike, +): Promise; +export declare function scheduleProviderQuotaRefresh( + basePath: string, + auth: AuthLike, +): void; diff --git a/src/resources/extensions/sf/provider-quota-cache.js b/src/resources/extensions/sf/provider-quota-cache.js index d8ac756da..b42e0198e 100644 --- a/src/resources/extensions/sf/provider-quota-cache.js +++ b/src/resources/extensions/sf/provider-quota-cache.js @@ -16,12 +16,7 @@ * - routing layer (phase 2): bias dispatch toward under-used subs to * maximize subscription utilization ("spend the subs"). */ -import { - existsSync, - mkdirSync, - readFileSync, - writeFileSync, -} from "node:fs"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { sfHome } from "./sf-home.js"; @@ -125,7 +120,9 @@ async function fetchKimiCodingQuota(apiKey) { if (typeof used !== "number" || typeof limit !== "number") continue; const winLabel = window?.duration && window?.timeUnit - ? `${window.duration} ${String(window.timeUnit).replace(/^TIME_UNIT_/, "").toLowerCase()}` + ? `${window.duration} ${String(window.timeUnit) + .replace(/^TIME_UNIT_/, "") + .toLowerCase()}` : undefined; windows.push({ label: String(detail?.name ?? winLabel ?? "rolling window"), @@ -278,7 +275,9 @@ async function fetchZaiQuota(apiKey) { ); } const windows = []; - const buckets = Array.isArray(payload?.data?.limits) ? payload.data.limits : []; + const buckets = Array.isArray(payload?.data?.limits) + ? payload.data.limits + : []; for (const bucket of buckets) { if (!bucket || typeof bucket !== "object") continue; const limit = toNum(bucket.usage); diff --git a/src/resources/extensions/sf/quick.js b/src/resources/extensions/sf/quick.js index 71908cf60..8251c2db6 100644 --- a/src/resources/extensions/sf/quick.js +++ b/src/resources/extensions/sf/quick.js @@ -17,12 +17,12 @@ import { writeFileSync, } from "node:fs"; import { join } from "node:path"; +import { getErrorMessage } from "./error-utils.js"; import { GitService, runGit } from "./git-service.js"; import { nativeHasStagedChanges } from "./native-git-bridge.js"; import { sfRoot } from "./paths.js"; import { loadEffectiveSFPreferences } from "./preferences.js"; import { loadPrompt } from "./prompt-loader.js"; -import { getErrorMessage } from "./error-utils.js"; let pendingQuickReturn = null; // ─── Quick Task Helpers ─────────────────────────────────────────────────────── diff --git a/src/resources/extensions/sf/reflection.js b/src/resources/extensions/sf/reflection.js index 2cbdb8d3d..678fd284c 100644 --- a/src/resources/extensions/sf/reflection.js +++ b/src/resources/extensions/sf/reflection.js @@ -19,19 +19,19 @@ import { execFileSync } from "node:child_process"; import { existsSync, mkdirSync, - readFileSync, readdirSync, + readFileSync, statSync, writeFileSync, } from "node:fs"; import { join } from "node:path"; import { sfRoot } from "./paths.js"; import { - listSelfFeedbackEntries, getAllMilestones, getMilestoneSlices, getSliceTasks, isDbAvailable, + listSelfFeedbackEntries, } from "./sf-db.js"; const RECENT_RESOLVED_LOOKBACK_DAYS = 30; @@ -191,9 +191,7 @@ export function assembleReflectionCorpus(basePath = process.cwd()) { Date.now() - RECENT_RESOLVED_LOOKBACK_DAYS * 24 * 60 * 60 * 1000; const open = forgeEntries.filter((e) => !e.resolvedAt); const recentResolved = forgeEntries.filter( - (e) => - e.resolvedAt && - new Date(e.resolvedAt).getTime() >= cutoffMs, + (e) => e.resolvedAt && new Date(e.resolvedAt).getTime() >= cutoffMs, ); return { generatedAt: new Date().toISOString(), @@ -270,9 +268,7 @@ export function renderReflectionCorpusBrief(corpus) { const sections = []; sections.push(`## Corpus snapshot — generated ${corpus.generatedAt}`); sections.push(`Project: ${corpus.basePath}`); - sections.push( - `Lookback for resolved entries: ${corpus.lookbackDays} days`, - ); + sections.push(`Lookback for resolved entries: ${corpus.lookbackDays} days`); sections.push(""); sections.push(`## Open self-feedback entries (${corpus.openEntries.length})`); diff --git a/src/resources/extensions/sf/repo-identity.js b/src/resources/extensions/sf/repo-identity.js index 24b89ecac..bd758d19b 100644 --- a/src/resources/extensions/sf/repo-identity.js +++ b/src/resources/extensions/sf/repo-identity.js @@ -23,6 +23,7 @@ import { } from "node:fs"; import { basename, dirname, join, resolve } from "node:path"; import { sfHome } from "./sf-home.js"; + function isRepoMeta(value) { if (!value || typeof value !== "object") return false; const v = value; diff --git a/src/resources/extensions/sf/scaffold-constants.js b/src/resources/extensions/sf/scaffold-constants.js index 78830434f..5b48b76a2 100644 --- a/src/resources/extensions/sf/scaffold-constants.js +++ b/src/resources/extensions/sf/scaffold-constants.js @@ -503,21 +503,17 @@ const INFRA_INCLUDED = new Set([ ".sf/harness/graders/AGENTS.md", ]); -const DOCS_INCLUDED = new Set([ - "AGENTS.md", - ".sf/STYLE.md", -]); +const DOCS_INCLUDED = new Set(["AGENTS.md", ".sf/STYLE.md"]); -const MINIMAL_INCLUDED = new Set([ - ".siftignore", - "AGENTS.md", -]); +const MINIMAL_INCLUDED = new Set([".siftignore", "AGENTS.md"]); export const PROFILES = { /** Full template set. Default for product and CLI repos with UI and tests. */ app: APP_PROFILE_PATHS, /** App minus frontend/design/product-sense files. For libraries, SDKs, CLI tools. */ - library: new Set([...APP_PROFILE_PATHS].filter((p) => !LIBRARY_EXCLUDED.has(p))), + library: new Set( + [...APP_PROFILE_PATHS].filter((p) => !LIBRARY_EXCLUDED.has(p)), + ), /** Infrastructure/GitOps/Kubernetes repos. Ops-focused subset. */ infra: INFRA_INCLUDED, /** Documentation-only repos. Minimal footprint. */ @@ -528,4 +524,3 @@ export const PROFILES = { /** Names of all built-in profiles. */ export const PROFILE_NAMES = /** @type {const} */ (Object.keys(PROFILES)); - diff --git a/src/resources/extensions/sf/scaffold-drift.js b/src/resources/extensions/sf/scaffold-drift.js index 906edc502..a1dea8563 100644 --- a/src/resources/extensions/sf/scaffold-drift.js +++ b/src/resources/extensions/sf/scaffold-drift.js @@ -66,7 +66,11 @@ function emptyCounts() { * local shim so migrateLegacyScaffold has a consistent call site. */ function resolveProfileSet(profile, manifest, basePath) { - const { profileSet } = resolveActiveProfileSet(basePath ?? null, manifest, profile); + const { profileSet } = resolveActiveProfileSet( + basePath ?? null, + manifest, + profile, + ); return profileSet; } diff --git a/src/resources/extensions/sf/scaffold-keeper.js b/src/resources/extensions/sf/scaffold-keeper.js index 0e8d66480..788212943 100644 --- a/src/resources/extensions/sf/scaffold-keeper.js +++ b/src/resources/extensions/sf/scaffold-keeper.js @@ -26,9 +26,9 @@ import { mkdirSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { SCAFFOLD_FILES } from "./agentic-docs-scaffold.js"; +import { getErrorMessage } from "./error-utils.js"; import { detectScaffoldDrift } from "./scaffold-drift.js"; import { logWarning } from "./workflow-logger.js"; -import { getErrorMessage } from "./error-utils.js"; /** * Build the .proposed body shipped by the Phase-D stub: the current scaffold diff --git a/src/resources/extensions/sf/scaffold-profiles.js b/src/resources/extensions/sf/scaffold-profiles.js index 07b427681..b191bda2e 100644 --- a/src/resources/extensions/sf/scaffold-profiles.js +++ b/src/resources/extensions/sf/scaffold-profiles.js @@ -9,7 +9,11 @@ */ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; -import { PROFILE_NAMES, PROFILES, SCAFFOLD_FILES } from "./scaffold-constants.js"; +import { + PROFILE_NAMES, + PROFILES, + SCAFFOLD_FILES, +} from "./scaffold-constants.js"; import { sfHome } from "./sf-home.js"; const SCAFFOLD_FILE_PATHS = new Set(SCAFFOLD_FILES.map((f) => f.path)); @@ -84,7 +88,10 @@ export function loadCustomProfileSet(name) { if (!line.trim()) continue; // Sequence item if (/^\s+-\s/.test(line)) { - const value = line.replace(/^\s+-\s*/, "").trim().replace(/^["']|["']$/g, ""); + const value = line + .replace(/^\s+-\s*/, "") + .trim() + .replace(/^["']|["']$/g, ""); if (currentList === "add") fields.add.push(value); else if (currentList === "remove") fields.remove.push(value); continue; @@ -147,7 +154,10 @@ export function loadCustomProfileSet(name) { */ export function detectRepoProfile(basePath) { // Nix repos are almost always infrastructure. - if (existsSync(join(basePath, "flake.nix")) || existsSync(join(basePath, "shell.nix"))) { + if ( + existsSync(join(basePath, "flake.nix")) || + existsSync(join(basePath, "shell.nix")) + ) { return "infra"; } // Repos with a Terraform or Pulumi entry point. @@ -172,7 +182,9 @@ export function detectRepoProfile(basePath) { // Node repos: check for UI framework dependency (→ app) vs library. if (existsSync(join(basePath, "package.json"))) { try { - const pkg = JSON.parse(readFileSync(join(basePath, "package.json"), "utf-8")); + const pkg = JSON.parse( + readFileSync(join(basePath, "package.json"), "utf-8"), + ); const allDeps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}), diff --git a/src/resources/extensions/sf/scaffold-versioning.js b/src/resources/extensions/sf/scaffold-versioning.js index 755bdbdd3..224fc320b 100644 --- a/src/resources/extensions/sf/scaffold-versioning.js +++ b/src/resources/extensions/sf/scaffold-versioning.js @@ -193,8 +193,7 @@ export function readScaffoldManifest(basePath) { return { schemaVersion: 1, profile: null, applied: [] }; } const applied = parsed.applied.filter(isScaffoldManifestEntry); - const profile = - typeof parsed.profile === "string" ? parsed.profile : null; + const profile = typeof parsed.profile === "string" ? parsed.profile : null; return { schemaVersion: 1, profile, applied }; } /** diff --git a/src/resources/extensions/sf/schedule/schedule-store.js b/src/resources/extensions/sf/schedule/schedule-store.js index e960fa245..c92859bc8 100644 --- a/src/resources/extensions/sf/schedule/schedule-store.js +++ b/src/resources/extensions/sf/schedule/schedule-store.js @@ -8,7 +8,6 @@ */ import { existsSync, mkdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; -import { sfHome } from "../sf-home.js"; import { sfRoot } from "../paths.js"; import { countScheduleEntries, @@ -16,6 +15,7 @@ import { insertScheduleEntry, openDatabase, } from "../sf-db.js"; +import { sfHome } from "../sf-home.js"; // ─── Constants ────────────────────────────────────────────────────────────── diff --git a/src/resources/extensions/sf/self-feedback.js b/src/resources/extensions/sf/self-feedback.js index 7cfe3cbec..4ae7fe403 100644 --- a/src/resources/extensions/sf/self-feedback.js +++ b/src/resources/extensions/sf/self-feedback.js @@ -37,7 +37,7 @@ import { writeFileSync, } from "node:fs"; import { dirname, join } from "node:path"; -import { sfHome } from "./sf-home.js"; +import { createMemory } from "./memory-store.js"; import { resolveMilestoneFile, sfRuntimeRoot } from "./paths.js"; import { insertSelfFeedbackEntry, @@ -45,7 +45,7 @@ import { listSelfFeedbackEntries, resolveSelfFeedbackEntry, } from "./sf-db.js"; -import { createMemory } from "./memory-store.js"; +import { sfHome } from "./sf-home.js"; import { logWarning } from "./workflow-logger.js"; const SELF_FEEDBACK_HEADER = @@ -260,7 +260,9 @@ const SELF_FEEDBACK_RECORD_RESOLUTION = "resolution"; function isResolutionRecord(record) { return ( - record && record.recordType === SELF_FEEDBACK_RECORD_RESOLUTION && record.entryId + record && + record.recordType === SELF_FEEDBACK_RECORD_RESOLUTION && + record.entryId ); } @@ -674,7 +676,7 @@ function extractFilesFromAcceptanceCriteria(acText) { // least one slash and a dotted extension to avoid sucking in plain // english words. const pathRe = - /(?:^|[\s,(\[])([a-zA-Z][a-zA-Z0-9._\-/]*\/[a-zA-Z0-9._\-/]+\.[a-zA-Z0-9]+)(?=[\s,)\]:;.]|$)/g; + /(?:^|[\s,([])([a-zA-Z][a-zA-Z0-9._\-/]*\/[a-zA-Z0-9._\-/]+\.[a-zA-Z0-9]+)(?=[\s,)\]:;.]|$)/g; while ((m = pathRe.exec(acText)) !== null) { const candidate = m[1].trim(); if (looksLikePath(candidate)) found.add(normalizePath(candidate)); @@ -828,10 +830,7 @@ export function markResolved(entryId, resolution, basePath = process.cwd()) { typeof resolution.evidence.commitSha === "string" && resolution.evidence.commitSha.trim().length > 0 ) { - const status = verifyCommitExists( - resolution.evidence.commitSha, - basePath, - ); + const status = verifyCommitExists(resolution.evidence.commitSha, basePath); if (status === "missing") return false; // Outcomes-verification AC2 (sf-mp4vxusa-pn2tnd): if the entry has // acceptanceCriteria mentioning specific files, the cited commit diff --git a/src/resources/extensions/sf/sf-db.js b/src/resources/extensions/sf/sf-db.js index 5f73f7e89..a2493d2d2 100644 --- a/src/resources/extensions/sf/sf-db.js +++ b/src/resources/extensions/sf/sf-db.js @@ -1,23 +1,22 @@ // sf-db.js — barrel re-export. All implementations live in sf-db/ domain files. // Do NOT add logic here; add it to the appropriate domain file. -export * from './sf-db/sf-db-core.js'; -export * from './sf-db/sf-db-mode-state.js'; -export * from './sf-db/sf-db-decisions.js'; -export * from './sf-db/sf-db-artifacts.js'; -export * from './sf-db/sf-db-milestones.js'; -export * from './sf-db/sf-db-slices.js'; -export * from './sf-db/sf-db-tasks.js'; -export * from './sf-db/sf-db-worktree.js'; -export * from './sf-db/sf-db-evidence.js'; -export * from './sf-db/sf-db-spec.js'; -export * from './sf-db/sf-db-gates.js'; -export * from './sf-db/sf-db-uok.js'; -export * from './sf-db/sf-db-session-store.js'; -export * from './sf-db/sf-db-backlog.js'; -export * from './sf-db/sf-db-learning.js'; -export * from './sf-db/sf-db-memory.js'; -export * from './sf-db/sf-db-profile.js'; -export * from './sf-db/sf-db-self-feedback.js'; -export * from './sf-db/sf-db-md-tracker.js'; - +export * from "./sf-db/sf-db-artifacts.js"; +export * from "./sf-db/sf-db-backlog.js"; +export * from "./sf-db/sf-db-core.js"; +export * from "./sf-db/sf-db-decisions.js"; +export * from "./sf-db/sf-db-evidence.js"; +export * from "./sf-db/sf-db-gates.js"; +export * from "./sf-db/sf-db-learning.js"; +export * from "./sf-db/sf-db-md-tracker.js"; +export * from "./sf-db/sf-db-memory.js"; +export * from "./sf-db/sf-db-milestones.js"; +export * from "./sf-db/sf-db-mode-state.js"; +export * from "./sf-db/sf-db-profile.js"; +export * from "./sf-db/sf-db-self-feedback.js"; +export * from "./sf-db/sf-db-session-store.js"; +export * from "./sf-db/sf-db-slices.js"; +export * from "./sf-db/sf-db-spec.js"; +export * from "./sf-db/sf-db-tasks.js"; +export * from "./sf-db/sf-db-uok.js"; +export * from "./sf-db/sf-db-worktree.js"; diff --git a/src/resources/extensions/sf/sf-db/sf-db-artifacts.js b/src/resources/extensions/sf/sf-db/sf-db-artifacts.js index ec3d62bd1..6911f462b 100644 --- a/src/resources/extensions/sf/sf-db/sf-db-artifacts.js +++ b/src/resources/extensions/sf/sf-db/sf-db-artifacts.js @@ -1,6 +1,6 @@ -import { _getAdapter, rowToArtifact } from './sf-db-core.js'; -import { SF_STALE_STATE, SFError } from '../errors.js'; -import { logWarning } from '../workflow-logger.js'; +import { SF_STALE_STATE, SFError } from "../errors.js"; +import { logWarning } from "../workflow-logger.js"; +import { _getAdapter, rowToArtifact } from "./sf-db-core.js"; export function clearArtifacts() { const currentDb = _getAdapter(); @@ -46,4 +46,3 @@ export function deleteArtifactByPath(path) { .prepare("DELETE FROM artifacts WHERE path = :path") .run({ ":path": path }); } - diff --git a/src/resources/extensions/sf/sf-db/sf-db-backlog.js b/src/resources/extensions/sf/sf-db/sf-db-backlog.js index e6035eeef..900508555 100644 --- a/src/resources/extensions/sf/sf-db/sf-db-backlog.js +++ b/src/resources/extensions/sf/sf-db/sf-db-backlog.js @@ -1,5 +1,5 @@ -import { _getAdapter, rowToBacklogItem } from './sf-db-core.js'; -import { SF_STALE_STATE, SFError } from '../errors.js'; +import { SF_STALE_STATE, SFError } from "../errors.js"; +import { _getAdapter, rowToBacklogItem } from "./sf-db-core.js"; export function listBacklogItems() { const currentDb = _getAdapter(); @@ -98,4 +98,3 @@ export function removeBacklogItem(id) { .run({ ":id": id }); return (result?.changes ?? 0) > 0; } - diff --git a/src/resources/extensions/sf/sf-db/sf-db-core.js b/src/resources/extensions/sf/sf-db/sf-db-core.js index 64fbb2359..06fdda29d 100644 --- a/src/resources/extensions/sf/sf-db/sf-db-core.js +++ b/src/resources/extensions/sf/sf-db/sf-db-core.js @@ -31,6 +31,7 @@ import { } from "node:fs"; import { dirname, join } from "node:path"; import { DatabaseSync } from "node:sqlite"; +import { getErrorMessage } from "../error-utils.js"; import { SF_STALE_STATE, SFError } from "../errors.js"; import { normalizeSchedulerStatus, @@ -38,7 +39,6 @@ import { withTaskFrontmatter, } from "../task-frontmatter.js"; import { logError, logWarning } from "../workflow-logger.js"; -import { getErrorMessage } from "../error-utils.js"; import { initSchema } from "./sf-db-schema.js"; let loadAttempted = false; @@ -207,10 +207,7 @@ function createDatabaseSnapshot(rawDb, path) { renameSync(tmpPath, backupPath); pruneDatabaseBackups(dir); } catch (err) { - logWarning( - "sf-db", - `database snapshot failed: ${getErrorMessage(err)}`, - ); + logWarning("sf-db", `database snapshot failed: ${getErrorMessage(err)}`); try { for (const entry of readdirSync(dir)) { if (entry.startsWith("sf.db.") && entry.endsWith(".tmp")) { @@ -251,10 +248,7 @@ function performDatabaseMaintenance(rawDb, path) { }); } } catch (err) { - logWarning( - "sf-db", - `database maintenance failed: ${getErrorMessage(err)}`, - ); + logWarning("sf-db", `database maintenance failed: ${getErrorMessage(err)}`); } } export function hasPlanningPayload(planning = {}) { @@ -358,7 +352,10 @@ export function openDatabase(path) { ) { try { adapter.exec("VACUUM"); - initSchema(adapter, fileBacked, { currentPath: path, withQueryTimeout }); + initSchema(adapter, fileBacked, { + currentPath: path, + withQueryTimeout, + }); process.stderr.write("sf-db: recovered corrupt database via VACUUM\n"); } catch (retryErr) { try { @@ -411,10 +408,7 @@ export function checkpointWal() { try { currentDb.exec("PRAGMA wal_checkpoint(PASSIVE)"); } catch (e) { - logWarning( - "db", - `WAL checkpoint failed: ${getErrorMessage(e)}`, - ); + logWarning("db", `WAL checkpoint failed: ${getErrorMessage(e)}`); } } @@ -673,7 +667,12 @@ export function hasTaskSpecIntent(planning = {}) { return false; } -export function insertTaskSpecIfAbsent(milestoneId, sliceId, taskId, planning = {}) { +export function insertTaskSpecIfAbsent( + milestoneId, + sliceId, + taskId, + planning = {}, +) { if (!hasTaskSpecIntent(planning)) return; const { normalized: frontmatter, errors } = taskFrontmatterFromRecord(planning); @@ -899,11 +898,11 @@ export function rowToSelfFeedback(row) { impactScore: typeof row["impact_score"] === "number" ? row["impact_score"] - : parsed.impactScore ?? null, + : (parsed.impactScore ?? null), effortEstimate: typeof row["effort_estimate"] === "number" ? row["effort_estimate"] - : parsed.effortEstimate ?? null, + : (parsed.effortEstimate ?? null), purposeAnchor: columnPurposeAnchor ?? parsed.purposeAnchor ?? null, }; } catch { diff --git a/src/resources/extensions/sf/sf-db/sf-db-decisions.js b/src/resources/extensions/sf/sf-db/sf-db-decisions.js index 85cfdd011..8474ea6fc 100644 --- a/src/resources/extensions/sf/sf-db/sf-db-decisions.js +++ b/src/resources/extensions/sf/sf-db/sf-db-decisions.js @@ -1,5 +1,5 @@ -import { _getAdapter } from './sf-db-core.js'; -import { SF_STALE_STATE, SFError } from '../errors.js'; +import { SF_STALE_STATE, SFError } from "../errors.js"; +import { _getAdapter } from "./sf-db-core.js"; export function insertDecision(d) { const currentDb = _getAdapter(); @@ -188,4 +188,3 @@ export function deleteRequirementById(id) { .prepare("DELETE FROM requirements WHERE id = :id") .run({ ":id": id }); } - diff --git a/src/resources/extensions/sf/sf-db/sf-db-evidence.js b/src/resources/extensions/sf/sf-db/sf-db-evidence.js index a2d3953ee..4d9a4d82f 100644 --- a/src/resources/extensions/sf/sf-db/sf-db-evidence.js +++ b/src/resources/extensions/sf/sf-db/sf-db-evidence.js @@ -1,6 +1,6 @@ -import { _getAdapter } from './sf-db-core.js'; -import { SF_STALE_STATE, SFError } from '../errors.js'; -import { logWarning } from '../workflow-logger.js'; +import { SF_STALE_STATE, SFError } from "../errors.js"; +import { logWarning } from "../workflow-logger.js"; +import { _getAdapter } from "./sf-db-core.js"; export function insertVerificationEvidence(e) { const currentDb = _getAdapter(); diff --git a/src/resources/extensions/sf/sf-db/sf-db-learning.js b/src/resources/extensions/sf/sf-db/sf-db-learning.js index 81858c3ea..222c73514 100644 --- a/src/resources/extensions/sf/sf-db/sf-db-learning.js +++ b/src/resources/extensions/sf/sf-db/sf-db-learning.js @@ -1,6 +1,15 @@ -import { _getAdapter, boolToInt, intBool, parseJsonObject, solverEvalRunFromRow, solverEvalCaseFromRow, headlessRunFromRow, transaction } from './sf-db-core.js'; -import { SF_STALE_STATE, SFError } from '../errors.js'; -import { logError, logWarning } from '../workflow-logger.js'; +import { SF_STALE_STATE, SFError } from "../errors.js"; +import { logError, logWarning } from "../workflow-logger.js"; +import { + _getAdapter, + boolToInt, + headlessRunFromRow, + intBool, + parseJsonObject, + solverEvalCaseFromRow, + solverEvalRunFromRow, + transaction, +} from "./sf-db-core.js"; export function insertLlmTaskOutcome(input) { const currentDb = _getAdapter(); diff --git a/src/resources/extensions/sf/sf-db/sf-db-md-tracker.js b/src/resources/extensions/sf/sf-db/sf-db-md-tracker.js index faa1654fd..5c20fba6f 100644 --- a/src/resources/extensions/sf/sf-db/sf-db-md-tracker.js +++ b/src/resources/extensions/sf/sf-db/sf-db-md-tracker.js @@ -19,7 +19,13 @@ import { _getAdapter } from "./sf-db-core.js"; * * Consumer: md-file-tracker.js observeMdFiles(). */ -export function upsertTrackedMdFile({ relpath, sha256, sizeBytes, lastSeenCommit, category }) { +export function upsertTrackedMdFile({ + relpath, + sha256, + sizeBytes, + lastSeenCommit, + category, +}) { const db = _getAdapter(); if (!db) return; db.prepare(` @@ -52,9 +58,13 @@ export function upsertTrackedMdFile({ relpath, sha256, sizeBytes, lastSeenCommit export function getTrackedMdFile(relpath) { const db = _getAdapter(); if (!db) return null; - return db.prepare( - "SELECT * FROM tracked_md_files WHERE relpath = :relpath AND active = 1", - ).get({ ":relpath": relpath }) ?? null; + return ( + db + .prepare( + "SELECT * FROM tracked_md_files WHERE relpath = :relpath AND active = 1", + ) + .get({ ":relpath": relpath }) ?? null + ); } /** @@ -84,7 +94,7 @@ export function deactivateTrackedMdFile(relpath) { export function getAllTrackedMdFiles() { const db = _getAdapter(); if (!db) return []; - return db.prepare( - "SELECT * FROM tracked_md_files WHERE active = 1 ORDER BY relpath", - ).all(); + return db + .prepare("SELECT * FROM tracked_md_files WHERE active = 1 ORDER BY relpath") + .all(); } diff --git a/src/resources/extensions/sf/sf-db/sf-db-milestones.js b/src/resources/extensions/sf/sf-db/sf-db-milestones.js index d0ebd2eb5..e1f2f1d23 100644 --- a/src/resources/extensions/sf/sf-db/sf-db-milestones.js +++ b/src/resources/extensions/sf/sf-db/sf-db-milestones.js @@ -1,6 +1,16 @@ -import { _getAdapter, hasPlanningPayload, isEmptyMilestoneSpec, parseJsonOrFallback, insertMilestoneSpecIfAbsent, rowToMilestone, parseVisionMeeting, parseProductResearch, transaction } from './sf-db-core.js'; -import { SF_STALE_STATE, SFError } from '../errors.js'; -import { existsSync, readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from "node:fs"; +import { SF_STALE_STATE, SFError } from "../errors.js"; +import { + _getAdapter, + hasPlanningPayload, + insertMilestoneSpecIfAbsent, + isEmptyMilestoneSpec, + parseJsonOrFallback, + parseProductResearch, + parseVisionMeeting, + rowToMilestone, + transaction, +} from "./sf-db-core.js"; export function insertMilestone(m) { const currentDb = _getAdapter(); @@ -424,4 +434,3 @@ export function restoreManifest(manifest) { } }); } - diff --git a/src/resources/extensions/sf/sf-db/sf-db-mode-state.js b/src/resources/extensions/sf/sf-db/sf-db-mode-state.js index dfdf36498..f505561dc 100644 --- a/src/resources/extensions/sf/sf-db/sf-db-mode-state.js +++ b/src/resources/extensions/sf/sf-db/sf-db-mode-state.js @@ -1,4 +1,4 @@ -import { _getAdapter } from './sf-db-core.js'; +import { _getAdapter } from "./sf-db-core.js"; export function loadSessionModeState() { const currentDb = _getAdapter(); @@ -46,4 +46,3 @@ export function saveSessionModeState(mode) { }); return true; } - diff --git a/src/resources/extensions/sf/sf-db/sf-db-schema.js b/src/resources/extensions/sf/sf-db/sf-db-schema.js index d53173f77..45f9496e8 100644 --- a/src/resources/extensions/sf/sf-db/sf-db-schema.js +++ b/src/resources/extensions/sf/sf-db/sf-db-schema.js @@ -3657,9 +3657,7 @@ function migrateSchema(db, { currentPath, withQueryTimeout }) { // Idempotent ALTER: probe via columnExists because the fresh-DB CREATE // path already adds the column. if (!columnExists(db, "slices", "traces_vision_fragment")) { - db.exec( - "ALTER TABLE slices ADD COLUMN traces_vision_fragment TEXT", - ); + db.exec("ALTER TABLE slices ADD COLUMN traces_vision_fragment TEXT"); } db.prepare( "INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)", diff --git a/src/resources/extensions/sf/sf-db/sf-db-self-feedback.js b/src/resources/extensions/sf/sf-db/sf-db-self-feedback.js index abec8a08f..b1f476505 100644 --- a/src/resources/extensions/sf/sf-db/sf-db-self-feedback.js +++ b/src/resources/extensions/sf/sf-db/sf-db-self-feedback.js @@ -95,7 +95,9 @@ export function listSelfFeedbackEntries(options) { // match permissive — operators paste a sentence fragment, not the full // canonical anchor text. const purposeFragment = - options && typeof options.purpose === "string" && options.purpose.trim() !== "" + options && + typeof options.purpose === "string" && + options.purpose.trim() !== "" ? options.purpose.trim() : null; if (purposeFragment) { diff --git a/src/resources/extensions/sf/sf-db/sf-db-session-store.js b/src/resources/extensions/sf/sf-db/sf-db-session-store.js index a8fd14eb8..b7e191017 100644 --- a/src/resources/extensions/sf/sf-db/sf-db-session-store.js +++ b/src/resources/extensions/sf/sf-db/sf-db-session-store.js @@ -1,6 +1,6 @@ -import { _getAdapter } from './sf-db-core.js'; -import { SF_STALE_STATE, SFError } from '../errors.js'; -import { logWarning, logError } from '../workflow-logger.js'; +import { SF_STALE_STATE, SFError } from "../errors.js"; +import { logError, logWarning } from "../workflow-logger.js"; +import { _getAdapter } from "./sf-db-core.js"; export function upsertSession(entry) { const currentDb = _getAdapter(); @@ -188,4 +188,3 @@ export function listSessionSnapshots(sessionId) { ) .all({ ":sid": sessionId }); } - diff --git a/src/resources/extensions/sf/sf-db/sf-db-slices.js b/src/resources/extensions/sf/sf-db/sf-db-slices.js index 05f39637f..fe6e79130 100644 --- a/src/resources/extensions/sf/sf-db/sf-db-slices.js +++ b/src/resources/extensions/sf/sf-db/sf-db-slices.js @@ -1,7 +1,15 @@ -import { _getAdapter, insertSliceSpecIfAbsent, parsePlanningMeeting, rowToSlice, safeParseJsonArray, rowToTask, transaction } from './sf-db-core.js'; -import { SF_STALE_STATE, SFError } from '../errors.js'; -import { existsSync, readdirSync, readFileSync } from 'node:fs'; -import { join } from 'node:path'; +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { SF_STALE_STATE, SFError } from "../errors.js"; +import { + _getAdapter, + insertSliceSpecIfAbsent, + parsePlanningMeeting, + rowToSlice, + rowToTask, + safeParseJsonArray, + transaction, +} from "./sf-db-core.js"; export function insertSlice(s) { const currentDb = _getAdapter(); @@ -486,4 +494,3 @@ export function deleteSlice(milestoneId, sliceId) { .run({ ":mid": milestoneId, ":sid": sliceId }); }); } - diff --git a/src/resources/extensions/sf/sf-db/sf-db-spec.js b/src/resources/extensions/sf/sf-db/sf-db-spec.js index 74f641e65..f837e0cf5 100644 --- a/src/resources/extensions/sf/sf-db/sf-db-spec.js +++ b/src/resources/extensions/sf/sf-db/sf-db-spec.js @@ -1,5 +1,5 @@ -import { _getAdapter } from './sf-db-core.js'; -import { SF_STALE_STATE, SFError } from '../errors.js'; +import { SF_STALE_STATE, SFError } from "../errors.js"; +import { _getAdapter } from "./sf-db-core.js"; export function getMilestoneSpec(milestoneId) { const currentDb = _getAdapter(); @@ -160,4 +160,3 @@ export function upsertValidationAttentionMarker(milestoneId, marker) { ":revalidation_requested_at": marker.revalidationRequestedAt ?? null, }); } - diff --git a/src/resources/extensions/sf/sf-db/sf-db-tasks.js b/src/resources/extensions/sf/sf-db/sf-db-tasks.js index 5570a385b..4087d96c5 100644 --- a/src/resources/extensions/sf/sf-db/sf-db-tasks.js +++ b/src/resources/extensions/sf/sf-db/sf-db-tasks.js @@ -1,7 +1,19 @@ -import { _getAdapter, hasTaskSpecIntent, insertTaskSpecIfAbsent, rowToTask, safeParseJsonArray, transaction } from './sf-db-core.js'; -import { SF_STALE_STATE, SFError } from '../errors.js'; -import { normalizeSchedulerStatus, normalizeTaskStatus, taskFrontmatterFromRecord, withTaskFrontmatter } from '../task-frontmatter.js'; -import { logWarning } from '../workflow-logger.js'; +import { SF_STALE_STATE, SFError } from "../errors.js"; +import { + normalizeSchedulerStatus, + normalizeTaskStatus, + taskFrontmatterFromRecord, + withTaskFrontmatter, +} from "../task-frontmatter.js"; +import { logWarning } from "../workflow-logger.js"; +import { + _getAdapter, + hasTaskSpecIntent, + insertTaskSpecIfAbsent, + rowToTask, + safeParseJsonArray, + transaction, +} from "./sf-db-core.js"; export function insertTask(t) { const currentDb = _getAdapter(); @@ -391,12 +403,12 @@ export function upsertTaskPlanning(milestoneId, sliceId, taskId, planning) { taskId, frontmatter.schedulerStatus, ); - } else { - upsertTaskSchedulerStatus(milestoneId, sliceId, taskId, "queued", { - onlyIfAbsent: true, - }); - } + } else { + upsertTaskSchedulerStatus(milestoneId, sliceId, taskId, "queued", { + onlyIfAbsent: true, + }); } +} export function getTask(milestoneId, sliceId, taskId) { const currentDb = _getAdapter(); diff --git a/src/resources/extensions/sf/sf-db/sf-db-uok.js b/src/resources/extensions/sf/sf-db/sf-db-uok.js index 956695361..fff8cb577 100644 --- a/src/resources/extensions/sf/sf-db/sf-db-uok.js +++ b/src/resources/extensions/sf/sf-db/sf-db-uok.js @@ -1,6 +1,11 @@ -import { _getAdapter, capErrorForStorage, parseJsonObject, rowToUnitMetrics } from './sf-db-core.js'; -import { logWarning } from '../workflow-logger.js'; -import { readTraceEvents } from '../uok/trace-writer.js'; +import { readTraceEvents } from "../uok/trace-writer.js"; +import { logWarning } from "../workflow-logger.js"; +import { + _getAdapter, + capErrorForStorage, + parseJsonObject, + rowToUnitMetrics, +} from "./sf-db-core.js"; export function recordUokRunStart(entry) { const currentDb = _getAdapter(); diff --git a/src/resources/extensions/sf/sf-db/sf-db-worktree.js b/src/resources/extensions/sf/sf-db/sf-db-worktree.js index 67adbb941..230c2a7b8 100644 --- a/src/resources/extensions/sf/sf-db/sf-db-worktree.js +++ b/src/resources/extensions/sf/sf-db/sf-db-worktree.js @@ -1,7 +1,7 @@ -import { _getAdapter, openDatabase } from './sf-db-core.js'; -import { copyFileSync, existsSync, mkdirSync, realpathSync } from 'node:fs'; -import { dirname } from 'node:path'; -import { logError, logWarning } from '../workflow-logger.js'; +import { copyFileSync, existsSync, mkdirSync, realpathSync } from "node:fs"; +import { dirname } from "node:path"; +import { logError, logWarning } from "../workflow-logger.js"; +import { _getAdapter, openDatabase } from "./sf-db-core.js"; export function copyWorktreeDb(srcDbPath, destDbPath) { try { @@ -262,4 +262,3 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) { return { ...zero, conflicts }; } } - diff --git a/src/resources/extensions/sf/shared/tui.js b/src/resources/extensions/sf/shared/tui.js index d7dbdd468..e8701403c 100644 --- a/src/resources/extensions/sf/shared/tui.js +++ b/src/resources/extensions/sf/shared/tui.js @@ -6,11 +6,11 @@ * Consumer: context.js, skill-catalog.js, init-wizard.js, guided-flow.js, forensics.js, triage-ui.js, migrate/command.js, guided-flow-queue.js */ export async function showNextAction(ctx, opts) { - // Minimal stub: just return a default value or log - return opts?.default ?? null; + // Minimal stub: just return a default value or log + return opts?.default ?? null; } // Add other stubs as needed for showConfirm, etc. export async function showConfirm(ctx, opts) { - return true; + return true; } diff --git a/src/resources/extensions/sf/skill-health.js b/src/resources/extensions/sf/skill-health.js index 84debdd0b..29e650c22 100644 --- a/src/resources/extensions/sf/skill-health.js +++ b/src/resources/extensions/sf/skill-health.js @@ -15,8 +15,8 @@ import { existsSync, statSync } from "node:fs"; import { join } from "node:path"; import { formatCost, formatTokenCount, loadLedgerFromDisk } from "./metrics.js"; -import { detectStaleSkills } from "./skill-telemetry.js"; import { SKILLS_DIR } from "./skill-discovery.js"; +import { detectStaleSkills } from "./skill-telemetry.js"; // ─── Constants ──────────────────────────────────────────────────────────────── /** Default staleness threshold in days */ diff --git a/src/resources/extensions/sf/skill-telemetry.js b/src/resources/extensions/sf/skill-telemetry.js index 6568974fe..d8e9b9f50 100644 --- a/src/resources/extensions/sf/skill-telemetry.js +++ b/src/resources/extensions/sf/skill-telemetry.js @@ -101,10 +101,7 @@ export function detectStaleSkills(units, thresholdDays) { const legacyDir = join(sfHome(), "agent", "skills"); const legacyMigrated = existsSync(join(legacyDir, ".migrated-to-agents")); const legacyNames = legacyMigrated ? [] : listSkillNames(legacyDir); - const installedSet = new Set([ - ...listSkillNames(SKILLS_DIR), - ...legacyNames, - ]); + const installedSet = new Set([...listSkillNames(SKILLS_DIR), ...legacyNames]); const installed = [...installedSet]; for (const skill of installed) { const lastTs = lastUsed.get(skill); diff --git a/src/resources/extensions/sf/slice-cadence.js b/src/resources/extensions/sf/slice-cadence.js index 90ef79f69..ef7031fff 100644 --- a/src/resources/extensions/sf/slice-cadence.js +++ b/src/resources/extensions/sf/slice-cadence.js @@ -21,6 +21,7 @@ import { execFileSync } from "node:child_process"; import { existsSync, unlinkSync } from "node:fs"; import { join } from "node:path"; +import { getErrorMessage } from "./error-utils.js"; import { SF_GIT_ERROR, SFError } from "./errors.js"; import { MergeConflictError } from "./git-service.js"; import { @@ -38,7 +39,6 @@ import { emitMilestoneResquash, emitSliceMerged, } from "./worktree-telemetry.js"; -import { getErrorMessage } from "./error-utils.js"; /** * Auto-worktree milestone branch name. Must match autoWorktreeBranch() in diff --git a/src/resources/extensions/sf/slice-routing-cache.js b/src/resources/extensions/sf/slice-routing-cache.js index e3ba00cd7..254b4e5d0 100644 --- a/src/resources/extensions/sf/slice-routing-cache.js +++ b/src/resources/extensions/sf/slice-routing-cache.js @@ -103,7 +103,12 @@ export function recordSliceRouting(basePath, unitType, unitId, model) { * @param {number} [options.maxAgeMs=7d] * @returns {{ provider: string, id: string } | null} */ -export function readStickyModelForUnit(basePath, unitType, unitId, options = {}) { +export function readStickyModelForUnit( + basePath, + unitType, + unitId, + options = {}, +) { if (!basePath) return null; const sliceId = extractSliceScope(unitId); if (!sliceId) return null; diff --git a/src/resources/extensions/sf/solver-model.js b/src/resources/extensions/sf/solver-model.js index 849774a27..bc8f0d843 100644 --- a/src/resources/extensions/sf/solver-model.js +++ b/src/resources/extensions/sf/solver-model.js @@ -90,10 +90,7 @@ export function resolveSolverModelCandidates(preferences) { const primary = resolveSolverModel(preferences); const candidates = [primary]; for (const fallback of SOLVER_MODEL_FALLBACKS) { - if ( - fallback.provider === primary.provider && - fallback.id === primary.id - ) { + if (fallback.provider === primary.provider && fallback.id === primary.id) { continue; } candidates.push({ ...fallback }); diff --git a/src/resources/extensions/sf/state-legacy.js b/src/resources/extensions/sf/state-legacy.js index e41c19a30..84d753a11 100644 --- a/src/resources/extensions/sf/state-legacy.js +++ b/src/resources/extensions/sf/state-legacy.js @@ -27,7 +27,6 @@ import { import { getSlicePlanBlockingIssue } from "./plan-quality.js"; import { loadQueueOrder, sortByQueueOrder } from "./queue-order.js"; import { getSliceTasks, isDbAvailable } from "./sf-db.js"; -import { logWarning } from "./workflow-logger.js"; import { extractContextTitle, isGhostMilestone, @@ -36,6 +35,7 @@ import { readMilestoneValidationVerdict, stripMilestonePrefix, } from "./state-shared.js"; +import { logWarning } from "./workflow-logger.js"; export async function _deriveStateImpl(basePath) { const diskIds = findMilestoneIds(basePath); diff --git a/src/resources/extensions/sf/state-reconcile.js b/src/resources/extensions/sf/state-reconcile.js index b30e4571e..b2cad533f 100644 --- a/src/resources/extensions/sf/state-reconcile.js +++ b/src/resources/extensions/sf/state-reconcile.js @@ -14,11 +14,7 @@ import { existsSync, readFileSync } from "node:fs"; import { isValidTaskSummary, parseSummary } from "./files.js"; import { resolveTaskFile } from "./paths.js"; -import { - getTask, - isDbAvailable, - setTaskSummaryFields, -} from "./sf-db.js"; +import { getTask, isDbAvailable, setTaskSummaryFields } from "./sf-db.js"; /** * Apply an on-disk task SUMMARY.md to the DB row. diff --git a/src/resources/extensions/sf/state-shared.js b/src/resources/extensions/sf/state-shared.js index 727df4a0f..a37f089e0 100644 --- a/src/resources/extensions/sf/state-shared.js +++ b/src/resources/extensions/sf/state-shared.js @@ -72,7 +72,11 @@ function getDbMilestoneValidationVerdict(milestoneId) { ? status.trim().toLowerCase() : undefined; } -export async function readMilestoneValidationVerdict(basePath, milestoneId, load) { +export async function readMilestoneValidationVerdict( + basePath, + milestoneId, + load, +) { const dbVerdict = getDbMilestoneValidationVerdict(milestoneId); if (dbVerdict) { return { terminal: true, verdict: dbVerdict }; diff --git a/src/resources/extensions/sf/state.js b/src/resources/extensions/sf/state.js index 9f37e5132..500e41022 100644 --- a/src/resources/extensions/sf/state.js +++ b/src/resources/extensions/sf/state.js @@ -7,15 +7,19 @@ import { debugCount, debugTime } from "./debug-logger.js"; import { loadFile } from "./files.js"; import { findMilestoneIds } from "./milestone-ids.js"; import { parseRoadmap } from "./parsers.js"; -import { clearPathCache, resolveMilestoneFile, resolveSfRootFile } from "./paths.js"; +import { + clearPathCache, + resolveMilestoneFile, + resolveSfRootFile, +} from "./paths.js"; import { loadQueueOrder, sortByQueueOrder } from "./queue-order.js"; import { getAllMilestones, isDbAvailable, wasDbOpenAttempted, } from "./sf-db.js"; -import { isClosedStatus } from "./status-guards.js"; -import { logWarning } from "./workflow-logger.js"; +import { deriveStateFromDb } from "./state-db.js"; +import { _deriveStateImpl } from "./state-legacy.js"; // Sub-module imports import { extractContextTitle, @@ -27,9 +31,12 @@ import { readMilestoneValidationVerdict, stripMilestonePrefix, } from "./state-shared.js"; -import { deriveStateFromDb } from "./state-db.js"; -import { _deriveStateImpl } from "./state-legacy.js"; +import { isClosedStatus } from "./status-guards.js"; +import { logWarning } from "./workflow-logger.js"; + export { + _deriveStateImpl, + deriveStateFromDb, extractContextTitle, isGhostMilestone, isMilestoneComplete, @@ -38,8 +45,6 @@ export { loadTerminalSummary, readMilestoneValidationVerdict, stripMilestonePrefix, - deriveStateFromDb, - _deriveStateImpl, }; const CACHE_TTL_MS = 5000; diff --git a/src/resources/extensions/sf/subagent/index.js b/src/resources/extensions/sf/subagent/index.js index 0a5f60d5d..dfbf37a3b 100644 --- a/src/resources/extensions/sf/subagent/index.js +++ b/src/resources/extensions/sf/subagent/index.js @@ -33,6 +33,7 @@ import { resolveSiftLogging, resolveSiftSearchScope, } from "../code-intelligence.js"; +import { emitJournalEvent } from "../journal.js"; import { loadEffectiveSFPreferences } from "../preferences.js"; import { recordRetrievalEvidence } from "../retrieval-evidence.js"; import { @@ -40,6 +41,7 @@ import { buildSubagentInheritanceEnvelope, validateSubagentDispatch, } from "../subagent-inheritance.js"; +import { swarmDispatchAndWait } from "../uok/swarm-dispatch.js"; import { discoverAgents } from "./agents.js"; import { SubagentBackgroundJobManager } from "./background-jobs.js"; import { @@ -47,8 +49,6 @@ import { mergeDeltaPatches, readIsolationMode, } from "./isolation.js"; -import { emitJournalEvent } from "../journal.js"; -import { swarmDispatchAndWait } from "../uok/swarm-dispatch.js"; import { composeAgentPrompt } from "./prompt-parts.js"; import { registerWorker, updateWorker } from "./worker-registry.js"; @@ -92,7 +92,9 @@ function countVectorSectors() { const files = fs.readdirSync(sectorDir); totalSectors += files.length; for (const f of files) { - const s = fs.statSync(path.join(sectorDir, f), { throwIfNoEntry: false }); + const s = fs.statSync(path.join(sectorDir, f), { + throwIfNoEntry: false, + }); if (s) totalBytes += s.size; } } catch { @@ -1279,7 +1281,9 @@ async function runSingleAgentViaSwarm( if (signal.aborted) { controller.abort(); } else { - signal.addEventListener("abort", () => controller.abort(), { once: true }); + signal.addEventListener("abort", () => controller.abort(), { + once: true, + }); } } liveSubagentControllers.add(controller); @@ -1571,8 +1575,10 @@ async function runSingleAgent( liveSubagentControllers.delete(controller); } } + // Exported for testing — not part of the public extension API. export { runSingleAgent, runSingleAgentViaSwarm }; + async function runSingleAgentInCmuxSplit( cmuxClient, directionOrSurfaceId, @@ -2746,10 +2752,9 @@ export default function (pi) { const runtimeDirs = ensureSiftRuntimeDirs(projectRoot); const { env: logEnv, logPath } = resolveSiftLogging(projectRoot); if (logPath) { - fs.mkdirSync( - path.join(projectRoot, ".sf", "runtime", "sift"), - { recursive: true }, - ); + fs.mkdirSync(path.join(projectRoot, ".sf", "runtime", "sift"), { + recursive: true, + }); fs.writeFileSync(logPath, "", "utf-8"); } @@ -2837,9 +2842,7 @@ export default function (pi) { }); }); if (wasAborted) { - const logHint = logPath - ? `\n(stage diagnostic: ${logPath})` - : ""; + const logHint = logPath ? `\n(stage diagnostic: ${logPath})` : ""; const text = timedOut ? `Code search timed out after ${Math.round(timeoutMs / 1000)}s. Narrow the query or scope and retry.${logHint}` : `Code search aborted.${logHint}`; diff --git a/src/resources/extensions/sf/subagent/isolation.js b/src/resources/extensions/sf/subagent/isolation.js index c0c369a2d..9ed400f21 100644 --- a/src/resources/extensions/sf/subagent/isolation.js +++ b/src/resources/extensions/sf/subagent/isolation.js @@ -9,8 +9,8 @@ import { execFile as execFileCb } from "node:child_process"; import * as fs from "node:fs"; import * as path from "node:path"; import { promisify } from "node:util"; -import { sfHome } from "../sf-home.js"; import { getErrorMessage } from "../error-utils.js"; +import { sfHome } from "../sf-home.js"; const execFile = promisify(execFileCb); // ============================================================================ diff --git a/src/resources/extensions/sf/sync-scheduler.js b/src/resources/extensions/sf/sync-scheduler.js index 2517c387c..dfda8cc20 100644 --- a/src/resources/extensions/sf/sync-scheduler.js +++ b/src/resources/extensions/sf/sync-scheduler.js @@ -15,8 +15,8 @@ */ import { delay } from "./atomic-write.js"; -import { syncMemoryToSm } from "./sm-client.js"; import { getErrorMessage } from "./error-utils.js"; +import { syncMemoryToSm } from "./sm-client.js"; /** * Global sync scheduler state. diff --git a/src/resources/extensions/sf/tests/adr-022-scaffold-profiles.test.mjs b/src/resources/extensions/sf/tests/adr-022-scaffold-profiles.test.mjs index cab52c4a7..08e55a57a 100644 --- a/src/resources/extensions/sf/tests/adr-022-scaffold-profiles.test.mjs +++ b/src/resources/extensions/sf/tests/adr-022-scaffold-profiles.test.mjs @@ -23,12 +23,16 @@ import { detectRepoProfile, ensureAgenticDocsScaffold, } from "../agentic-docs-scaffold.js"; -import { PROFILE_NAMES, PROFILES, SCAFFOLD_FILES } from "../scaffold-constants.js"; -import { detectScaffoldDrift } from "../scaffold-drift.js"; import { parseScaffoldMigrateArgs, runScaffoldMigrate, } from "../commands-scaffold-migrate.js"; +import { + PROFILE_NAMES, + PROFILES, + SCAFFOLD_FILES, +} from "../scaffold-constants.js"; +import { detectScaffoldDrift } from "../scaffold-drift.js"; import { bodyHash, extractMarker, @@ -62,7 +66,10 @@ function writePendingFile(root, relPath, content = `# ${relPath}\n`) { describe("PROFILES shape", () => { test("PROFILE_NAMES_contains_five_built_in_profiles", () => { - assert.deepEqual(PROFILE_NAMES.sort(), ["app", "docs", "infra", "library", "minimal"].sort()); + assert.deepEqual( + PROFILE_NAMES.sort(), + ["app", "docs", "infra", "library", "minimal"].sort(), + ); }); test("app_profile_contains_all_scaffold_files", () => { @@ -72,7 +79,10 @@ describe("PROFILES shape", () => { } // app must be the superset — every file belongs to at least app. for (const path of allPaths) { - assert.ok(PROFILES.app.has(path), `SCAFFOLD_FILES path not in app profile: ${path}`); + assert.ok( + PROFILES.app.has(path), + `SCAFFOLD_FILES path not in app profile: ${path}`, + ); } }); @@ -103,7 +113,8 @@ describe("disabled state", () => { test("parseMarker_accepts_disabled_state", () => { // The disabled state must be parseable — otherwise files stamped disabled // before Phase 1 ships would be read as null → untracked → re-rendered. - const line = ""; + const line = + ""; const marker = parseMarker(line); assert.ok(marker, "parseMarker should not return null for state=disabled"); assert.equal(marker.state, "disabled"); @@ -156,7 +167,10 @@ describe("profile-aware drift", () => { // Use "minimal" profile — only .siftignore and AGENTS.md. // A missing file from "app" profile (e.g. ARCHITECTURE.md) should NOT appear as missing. const archFile = "ARCHITECTURE.md"; - assert.ok(!PROFILES.minimal.has(archFile), "ARCHITECTURE.md should not be in minimal"); + assert.ok( + !PROFILES.minimal.has(archFile), + "ARCHITECTURE.md should not be in minimal", + ); const report = detectScaffoldDrift(root, "minimal"); const item = report.items.find((i) => i.path === archFile); @@ -249,7 +263,10 @@ describe("ensureAgenticDocsScaffold profile-aware", () => { ensureAgenticDocsScaffold(root); // FRONTEND.md is in "app" but not in "infra" — must not be written. - assert.ok(!PROFILES.infra.has("docs/FRONTEND.md"), "precondition: FRONTEND.md not in infra"); + assert.ok( + !PROFILES.infra.has("docs/FRONTEND.md"), + "precondition: FRONTEND.md not in infra", + ); assert.equal(existsSync(join(root, "docs/FRONTEND.md")), false); }); }); diff --git a/src/resources/extensions/sf/tests/auto-prompts-v2-migration.test.mjs b/src/resources/extensions/sf/tests/auto-prompts-v2-migration.test.mjs index 819fb7468..28ec50564 100644 --- a/src/resources/extensions/sf/tests/auto-prompts-v2-migration.test.mjs +++ b/src/resources/extensions/sf/tests/auto-prompts-v2-migration.test.mjs @@ -16,8 +16,8 @@ import { afterEach, describe, expect, test } from "vitest"; import { buildChallengePrompt, buildDeployPrompt, - buildResearchMilestonePrompt, buildReleasePrompt, + buildResearchMilestonePrompt, buildRollbackPrompt, buildRunUatPrompt, buildSmokeProductionPrompt, diff --git a/src/resources/extensions/sf/tests/benchmark-coverage.test.mjs b/src/resources/extensions/sf/tests/benchmark-coverage.test.mjs index 6820b7336..ab4703e2d 100644 --- a/src/resources/extensions/sf/tests/benchmark-coverage.test.mjs +++ b/src/resources/extensions/sf/tests/benchmark-coverage.test.mjs @@ -98,7 +98,10 @@ describe("computeBenchmarkCoverage", () => { test("partitions covered vs uncovered using the real benchmark file", () => { const home = tempSfHome(); // glm-4.5 is in the static benchmark file; bogus-not-in-bench-2026 is not. - writeCatalog(home, "zai", [{ id: "glm-4.5" }, { id: "bogus-not-in-bench-2026" }]); + writeCatalog(home, "zai", [ + { id: "glm-4.5" }, + { id: "bogus-not-in-bench-2026" }, + ]); const result = computeBenchmarkCoverage({ allowed_providers: ["zai"], @@ -172,7 +175,10 @@ describe("computeBenchmarkCoverage", () => { }); const ids = [...result.covered, ...result.uncovered].map((m) => m.id); - assert.ok(!ids.includes("anthropic/claude-opus-4-7"), "paid claude blocked"); + assert.ok( + !ids.includes("anthropic/claude-opus-4-7"), + "paid claude blocked", + ); assert.ok(ids.includes("qwen/qwen3-coder:free"), ":free allowed"); assert.ok(ids.includes("openrouter/zero-cost-model"), "zero-cost allowed"); }); @@ -195,7 +201,12 @@ describe("writeBenchmarkCoverage / detectCoverageChange", () => { const coverage = { covered: [{ provider: "zai", id: "glm-4.5" }], uncovered: [{ provider: "zai", id: "bogus" }], - summary: { total: 2, coveredCount: 1, uncoveredCount: 1, coverageRatio: 0.5 }, + summary: { + total: 2, + coveredCount: 1, + uncoveredCount: 1, + coverageRatio: 0.5, + }, }; writeBenchmarkCoverage(coverage); const path = join(home, "benchmark-coverage.json"); @@ -222,7 +233,12 @@ describe("writeBenchmarkCoverage / detectCoverageChange", () => { { provider: "zai", id: "x" }, { provider: "kimi-coding", id: "y" }, ], - summary: { total: 2, coveredCount: 0, uncoveredCount: 2, coverageRatio: 0 }, + summary: { + total: 2, + coveredCount: 0, + uncoveredCount: 2, + coverageRatio: 0, + }, }; writeBenchmarkCoverage(coverage); assert.equal(detectCoverageChange(coverage), false); @@ -233,7 +249,12 @@ describe("writeBenchmarkCoverage / detectCoverageChange", () => { writeBenchmarkCoverage({ covered: [], uncovered: [{ provider: "zai", id: "x" }], - summary: { total: 1, coveredCount: 0, uncoveredCount: 1, coverageRatio: 0 }, + summary: { + total: 1, + coveredCount: 0, + uncoveredCount: 1, + coverageRatio: 0, + }, }); assert.equal( detectCoverageChange({ diff --git a/src/resources/extensions/sf/tests/benchmark-selector-quota.test.mjs b/src/resources/extensions/sf/tests/benchmark-selector-quota.test.mjs index 3332d8b63..5898f6ea1 100644 --- a/src/resources/extensions/sf/tests/benchmark-selector-quota.test.mjs +++ b/src/resources/extensions/sf/tests/benchmark-selector-quota.test.mjs @@ -33,12 +33,19 @@ function stub(usedFractions) { describe("quotaHeadroomMultiplier", () => { test("unknown provider → neutral 1.0", () => { - assert.equal(quotaHeadroomMultiplier("nope", () => null), 1.0); + assert.equal( + quotaHeadroomMultiplier("nope", () => null), + 1.0, + ); }); test("not-ok entry → neutral 1.0", () => { assert.equal( - quotaHeadroomMultiplier("zai", () => ({ ok: false, error: "x", windows: [] })), + quotaHeadroomMultiplier("zai", () => ({ + ok: false, + error: "x", + windows: [], + })), 1.0, ); }); diff --git a/src/resources/extensions/sf/tests/canonical-id-dynamic.test.mjs b/src/resources/extensions/sf/tests/canonical-id-dynamic.test.mjs index 257832ca2..df8df04ea 100644 --- a/src/resources/extensions/sf/tests/canonical-id-dynamic.test.mjs +++ b/src/resources/extensions/sf/tests/canonical-id-dynamic.test.mjs @@ -138,7 +138,9 @@ describe("canonicalIdFor — removed identity-strip aliases resolve via discover minimax: { models: [{ id: "MiniMax-M2.7-highspeed" }] }, }, }); - expect(canonicalIdFor("minimax/MiniMax-M2.7-highspeed")).toBe("MiniMax-M2.7-highspeed"); + expect(canonicalIdFor("minimax/MiniMax-M2.7-highspeed")).toBe( + "MiniMax-M2.7-highspeed", + ); }); it("mistral/codestral-latest resolves to codestral-latest via discovery cache", () => { @@ -165,7 +167,9 @@ describe("canonicalIdFor — removed identity-strip aliases resolve via discover mistral: { models: [{ id: "devstral-small-2507" }] }, }, }); - expect(canonicalIdFor("mistral/devstral-small-2507")).toBe("devstral-small-2507"); + expect(canonicalIdFor("mistral/devstral-small-2507")).toBe( + "devstral-small-2507", + ); }); it("mistral/mistral-large-latest resolves to mistral-large-latest via discovery cache", () => { @@ -174,7 +178,9 @@ describe("canonicalIdFor — removed identity-strip aliases resolve via discover mistral: { models: [{ id: "mistral-large-latest" }] }, }, }); - expect(canonicalIdFor("mistral/mistral-large-latest")).toBe("mistral-large-latest"); + expect(canonicalIdFor("mistral/mistral-large-latest")).toBe( + "mistral-large-latest", + ); }); it("mistral/mistral-medium-latest resolves to mistral-medium-latest via discovery cache", () => { @@ -183,7 +189,9 @@ describe("canonicalIdFor — removed identity-strip aliases resolve via discover mistral: { models: [{ id: "mistral-medium-latest" }] }, }, }); - expect(canonicalIdFor("mistral/mistral-medium-latest")).toBe("mistral-medium-latest"); + expect(canonicalIdFor("mistral/mistral-medium-latest")).toBe( + "mistral-medium-latest", + ); }); it("mistral/mistral-small-latest resolves to mistral-small-latest via discovery cache", () => { @@ -192,7 +200,9 @@ describe("canonicalIdFor — removed identity-strip aliases resolve via discover mistral: { models: [{ id: "mistral-small-latest" }] }, }, }); - expect(canonicalIdFor("mistral/mistral-small-latest")).toBe("mistral-small-latest"); + expect(canonicalIdFor("mistral/mistral-small-latest")).toBe( + "mistral-small-latest", + ); }); it("zai/glm-4.5 resolves to glm-4.5 via discovery cache", () => { diff --git a/src/resources/extensions/sf/tests/canonical-id-mapping.test.mjs b/src/resources/extensions/sf/tests/canonical-id-mapping.test.mjs index 72e0e9f4f..bd0946803 100644 --- a/src/resources/extensions/sf/tests/canonical-id-mapping.test.mjs +++ b/src/resources/extensions/sf/tests/canonical-id-mapping.test.mjs @@ -6,7 +6,7 @@ * backfillModelPerformanceFromJournal() produces non-_unmapped buckets * when given a mock journal. */ -import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/resources/extensions/sf/tests/complete-slice-evidence.test.mjs b/src/resources/extensions/sf/tests/complete-slice-evidence.test.mjs index 4b44be716..ef6704516 100644 --- a/src/resources/extensions/sf/tests/complete-slice-evidence.test.mjs +++ b/src/resources/extensions/sf/tests/complete-slice-evidence.test.mjs @@ -52,7 +52,8 @@ function makeProject() { status: "complete", // ADR-0000 (SF v70): every task must carry a goal-anchored trace // before complete-slice will close the parent slice. - purposeTrace: "Served the slice-goal clause covered by this evidence fixture.", + purposeTrace: + "Served the slice-goal clause covered by this evidence fixture.", }); return dir; } diff --git a/src/resources/extensions/sf/tests/complete-task-purpose-trace.test.mjs b/src/resources/extensions/sf/tests/complete-task-purpose-trace.test.mjs index e30c60898..1e64fc709 100644 --- a/src/resources/extensions/sf/tests/complete-task-purpose-trace.test.mjs +++ b/src/resources/extensions/sf/tests/complete-task-purpose-trace.test.mjs @@ -46,7 +46,11 @@ function makeProject() { recursive: true, }); openDatabase(join(dir, ".sf", "sf.db")); - insertMilestone({ id: "M001", title: "Doctrine restoration", status: "active" }); + insertMilestone({ + id: "M001", + title: "Doctrine restoration", + status: "active", + }); insertSlice({ milestoneId: "M001", id: "S01", @@ -90,22 +94,14 @@ test("complete_task refuses without purpose_trace and names the slice goal", asy /purpose_trace is required/i, "error must call out purpose_trace requirement", ); - assert.match( - result.error, - /ADR-0000/, - "error must reference the doctrine", - ); + assert.match(result.error, /ADR-0000/, "error must reference the doctrine"); assert.ok( result.error.includes(SLICE_GOAL), `error must quote slice.goal verbatim so the agent can anchor against it; got: ${result.error}`, ); // DB must not have flipped status when the call was refused. const stored = getTask("M001", "S01", "T01"); - assert.equal( - stored, - null, - "refused complete_task must not insert a row", - ); + assert.equal(stored, null, "refused complete_task must not insert a row"); }); test("complete_task persists purpose_trace verbatim when supplied", async () => { @@ -137,7 +133,11 @@ test("complete_slice refuses when any task is missing purpose_trace", async () = baseTaskParams({ purposeTrace: trace }), project, ); - assert.equal(okResult.error, undefined, `T01 setup failed: ${okResult.error}`); + assert.equal( + okResult.error, + undefined, + `T01 setup failed: ${okResult.error}`, + ); // Use the low-level insertTask to forge a complete-but-untraced row, // matching the "legacy NULL row" condition the migration explicitly @@ -164,18 +164,15 @@ test("complete_slice refuses when any task is missing purpose_trace", async () = oneLiner: "Did the thing.", narrative: "Did the thing for the doctrine.", verification: "All gates passed.", - uatContent: "## UAT Type\n\n- UAT mode: artifact-driven\n- Why: tests cover.\n", + uatContent: + "## UAT Type\n\n- UAT mode: artifact-driven\n- Why: tests cover.\n", operationalReadiness: "Ready.", }, project, ); assert.ok(result.error, "expected refusal payload"); - assert.match( - result.error, - /purpose_trace/i, - "error must name purpose_trace", - ); + assert.match(result.error, /purpose_trace/i, "error must name purpose_trace"); assert.match( result.error, /T02/, diff --git a/src/resources/extensions/sf/tests/context-board.test.ts b/src/resources/extensions/sf/tests/context-board.test.ts index 253545eb3..1d2c51ed2 100644 --- a/src/resources/extensions/sf/tests/context-board.test.ts +++ b/src/resources/extensions/sf/tests/context-board.test.ts @@ -88,8 +88,14 @@ describe("addBoardEntry / getBoardEntries", () => { branch: "main", }); - const mainEntries = getBoardEntries({ repository: "/my/repo", branch: "main" }); - const featureEntries = getBoardEntries({ repository: "/my/repo", branch: "feature/x" }); + const mainEntries = getBoardEntries({ + repository: "/my/repo", + branch: "main", + }); + const featureEntries = getBoardEntries({ + repository: "/my/repo", + branch: "feature/x", + }); expect(mainEntries).toHaveLength(1); expect(featureEntries).toHaveLength(0); @@ -103,8 +109,14 @@ describe("addBoardEntry / getBoardEntries", () => { branch: "main", }); - const repoAEntries = getBoardEntries({ repository: "/repo-a", branch: "main" }); - const repoBEntries = getBoardEntries({ repository: "/repo-b", branch: "main" }); + const repoAEntries = getBoardEntries({ + repository: "/repo-a", + branch: "main", + }); + const repoBEntries = getBoardEntries({ + repository: "/repo-b", + branch: "main", + }); expect(repoAEntries).toHaveLength(1); expect(repoBEntries).toHaveLength(0); @@ -139,7 +151,11 @@ describe("addBoardEntry / getBoardEntries", () => { it("addBoardEntry returns null when db is not open", () => { // Do NOT call makeProject() - const id = addBoardEntry({ content: "test", repository: "/repo", branch: "main" }); + const id = addBoardEntry({ + content: "test", + repository: "/repo", + branch: "main", + }); expect(id).toBeNull(); }); }); @@ -149,8 +165,16 @@ describe("addBoardEntry / getBoardEntries", () => { describe("pruneBoardEntry", () => { it("removes only the targeted entry", () => { makeProject(); - const id1 = addBoardEntry({ content: "keep this", repository: "/repo", branch: "main" }); - const id2 = addBoardEntry({ content: "prune this", repository: "/repo", branch: "main" }); + const id1 = addBoardEntry({ + content: "keep this", + repository: "/repo", + branch: "main", + }); + const id2 = addBoardEntry({ + content: "prune this", + repository: "/repo", + branch: "main", + }); expect(id1).toBeTruthy(); expect(id2).toBeTruthy(); @@ -185,7 +209,12 @@ describe("formatBoardForPrompt", () => { it("includes header, entries, and footer", () => { const entries = [ - { id: "abc123", content: "Use vitest not jest", category: "convention", added_at: "2026-05-13T00:00:00Z" }, + { + id: "abc123", + content: "Use vitest not jest", + category: "convention", + added_at: "2026-05-13T00:00:00Z", + }, ]; const result = formatBoardForPrompt(entries); expect(result).toContain("### Invariants for this repo/branch"); @@ -197,7 +226,12 @@ describe("formatBoardForPrompt", () => { it("renders entries without category", () => { const entries = [ - { id: "noCat1", content: "No category entry", category: null, added_at: "2026-05-13T00:00:00Z" }, + { + id: "noCat1", + content: "No category entry", + category: null, + added_at: "2026-05-13T00:00:00Z", + }, ]; const result = formatBoardForPrompt(entries); expect(result).toContain("noCat1"); @@ -224,7 +258,12 @@ describe("formatBoardForPrompt", () => { it("does not truncate when entries fit within cap", () => { const entries = [ - { id: "short1", content: "Short entry", category: null, added_at: "2026-05-13T00:00:00Z" }, + { + id: "short1", + content: "Short entry", + category: null, + added_at: "2026-05-13T00:00:00Z", + }, ]; const result = formatBoardForPrompt(entries, { maxBytes: 4096 }); expect(result).not.toContain("truncated"); diff --git a/src/resources/extensions/sf/tests/db-first-validation.test.mjs b/src/resources/extensions/sf/tests/db-first-validation.test.mjs index c867f28fb..a65308d88 100644 --- a/src/resources/extensions/sf/tests/db-first-validation.test.mjs +++ b/src/resources/extensions/sf/tests/db-first-validation.test.mjs @@ -52,7 +52,11 @@ function makeProject() { tmpDirs.push(dir); mkdirSync(join(dir, ".sf", "milestones", "M001"), { recursive: true }); openDatabase(join(dir, ".sf", "sf.db")); - insertMilestone({ id: "M001", title: "DB-first validation", status: "active" }); + insertMilestone({ + id: "M001", + title: "DB-first validation", + status: "active", + }); insertSlice({ milestoneId: "M001", id: "S01", @@ -89,7 +93,11 @@ test("handleValidateMilestone_regenerates_validation_md_with_db_verdict_in_front { uokGatesEnabled: false }, ); - assert.equal(result.error, undefined, `handleValidateMilestone error: ${result.error}`); + assert.equal( + result.error, + undefined, + `handleValidateMilestone error: ${result.error}`, + ); assert.equal(result.verdict, "needs-attention"); // The file must exist and have the generated header @@ -105,7 +113,11 @@ test("handleValidateMilestone_regenerates_validation_md_with_db_verdict_in_front content.startsWith("