chore: formatter / linter touch-up (230 files)
Some checks are pending
CI / detect-changes (push) Waiting to run
CI / docs-check (push) Blocked by required conditions
CI / lint (push) Blocked by required conditions
CI / build (push) Blocked by required conditions
CI / integration-tests (push) Blocked by required conditions
CI / windows-portability (push) Blocked by required conditions
CI / rtk-portability (linux, blacksmith-4vcpu-ubuntu-2404) (push) Blocked by required conditions
CI / rtk-portability (macos, macos-15) (push) Blocked by required conditions
CI / rtk-portability (windows, blacksmith-4vcpu-windows-2025) (push) Blocked by required conditions

Pure formatting / lint-fix pass that ran during `npm run build:core`
in the session that landed the agent-runner / quota / coverage /
phase-2 routing work. No logic changes — indentation, trailing
commas, import sort, etc. Captured separately so the actual feature
commits stay scoped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-16 21:19:53 +02:00
parent d80060fec5
commit 365c6bbc3b
230 changed files with 2283 additions and 1454 deletions

View file

@ -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"
}
}
}
}
}

View file

@ -9,6 +9,8 @@ import type { Api, Model } from "./types.js";
*
* Consumer: pi-ai provider implementations when building request payloads.
*/
export function resolveWireModelId<TApi extends Api>(model: Model<TApi>): string {
export function resolveWireModelId<TApi extends Api>(
model: Model<TApi>,
): string {
return model.wireModelId?.trim() || model.id;
}

View file

@ -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,

View file

@ -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,

View file

@ -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, { models: object[] }>): 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, { models: object[] }>,
): 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]",
);
});
});

View file

@ -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);

View file

@ -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(

View file

@ -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) => {

View file

@ -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<ContentGenerator>;
export declare function createGeminiCliContentGenerator(
options: GeminiCliContentGeneratorOptions,
): Promise<ContentGenerator>;
/**
* 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<GeminiAccountSnapshot | null>;
export declare function snapshotGeminiCliAccount(
cwd?: string,
): Promise<GeminiAccountSnapshot | null>;
/**
* 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<string[] | null>;
//# sourceMappingURL=index.d.ts.map
export declare function discoverGeminiCliModels(
cwd?: string,
): Promise<string[] | null>;
//# sourceMappingURL=index.d.ts.map

View file

@ -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
//# sourceMappingURL=index.js.map

View file

@ -1,2 +1,2 @@
export {};
//# sourceMappingURL=index.test.d.ts.map
//# sourceMappingURL=index.test.d.ts.map

View file

@ -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
//# sourceMappingURL=index.test.js.map

View file

@ -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) {

View file

@ -18,11 +18,11 @@
*/
export {
CodexAppServerClient,
clearCodexAppServerClient,
getCodexAppServerClient,
type CodexAppServerClientOptions,
type CodexAppServerNotification,
type CodexAppServerNotificationHandler,
clearCodexAppServerClient,
getCodexAppServerClient,
} from "./codex-app-server-client.js";
export {

View file

@ -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);

View file

@ -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 <provider> [--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);
}

View file

@ -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<string, unknown>;
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

View file

@ -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<string, unknown>, human: string): void {
function emit(
json: boolean,
payload: Record<string, unknown>,
human: string,
): void {
if (json) {
process.stdout.write(`${JSON.stringify(payload)}\n`);
} else {
@ -100,10 +104,15 @@ async function loadDb(basePath: string): Promise<void> {
await autoStart.openProjectDbIfPresent(basePath);
}
async function handleAdd(basePath: string, options: FeedbackOptions): Promise<FeedbackResult> {
async function handleAdd(
basePath: string,
options: FeedbackOptions,
): Promise<FeedbackResult> {
const summary = readFlag(options.args, "--summary");
if (!summary || summary.trim() === "") {
process.stderr.write("[headless] Error: feedback add requires --summary <text>\n");
process.stderr.write(
"[headless] Error: feedback add requires --summary <text>\n",
);
return { exitCode: 2 };
}
const severity = readFlag(options.args, "--severity") ?? "medium";
@ -120,8 +129,12 @@ async function handleAdd(basePath: string, options: FeedbackOptions): Promise<Fe
const slice = readFlag(options.args, "--slice");
const task = readFlag(options.args, "--task");
const unitType = readFlag(options.args, "--unit-type");
const impactScore = parseIntOrUndefined(readFlag(options.args, "--impact-score"));
const effortEstimate = parseIntOrUndefined(readFlag(options.args, "--effort-estimate"));
const impactScore = parseIntOrUndefined(
readFlag(options.args, "--impact-score"),
);
const effortEstimate = parseIntOrUndefined(
readFlag(options.args, "--effort-estimate"),
);
// purpose_anchor (sf-db v71, ADR-0000): free-text fragment of the milestone
// vision or slice goal sentence this feedback is filed against. Optional —
// the CLI accepts omission so legacy callers keep working, but triage can
@ -132,7 +145,9 @@ async function handleAdd(basePath: string, options: FeedbackOptions): Promise<Fe
? purposeAnchorRaw.trim()
: undefined;
const blocking =
readBoolFlag(options.args, "--blocking") || severity === "high" || severity === "critical";
readBoolFlag(options.args, "--blocking") ||
severity === "high" ||
severity === "critical";
const id = newId();
const ts = new Date().toISOString();
@ -161,26 +176,36 @@ async function handleAdd(basePath: string, options: FeedbackOptions): Promise<Fe
};
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 {
insertSelfFeedbackEntry: (e: typeof entry) => 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<FeedbackResult> {
async function handleList(
basePath: string,
options: FeedbackOptions,
): Promise<FeedbackResult> {
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<F
: undefined;
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 {
listSelfFeedbackEntries: (options?: { purpose?: string }) => Array<{
id: string;
ts: string;
@ -241,9 +269,13 @@ async function handleList(basePath: string, options: FeedbackOptions): Promise<F
return { exitCode: 0 };
}
async function handleResolve(basePath: string, options: FeedbackOptions): Promise<FeedbackResult> {
async function handleResolve(
basePath: string,
options: FeedbackOptions,
): Promise<FeedbackResult> {
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 };
}

View file

@ -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(

View file

@ -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<void>;
};
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 };
}

View file

@ -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/<ts>-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,
});

View file

@ -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 [];
}

View file

@ -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;

View file

@ -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(

View file

@ -300,13 +300,13 @@ const SUBCOMMAND_HELP: Record<string, string> = {
" 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"),

View file

@ -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"
]
}
}

View file

@ -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;

View file

@ -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"]
}

View file

@ -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.

View file

@ -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. */

View file

@ -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", {

View file

@ -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).

View file

@ -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)}`);
}
}
/**

View file

@ -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

View file

@ -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);

View file

@ -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;

View file

@ -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,

View file

@ -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"]);

View file

@ -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,

View file

@ -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.

View file

@ -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";

View file

@ -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;

View file

@ -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;

View file

@ -17,9 +17,15 @@ export interface BenchmarkCoverageResult {
}
export declare function normalizeForBenchmarkLookup(modelId: string): string;
export declare function computeBenchmarkCoverage(prefs: Record<string, unknown>): BenchmarkCoverageResult;
export declare function writeBenchmarkCoverage(coverage: BenchmarkCoverageResult): void;
export declare function detectCoverageChange(coverage: BenchmarkCoverageResult): boolean;
export declare function computeBenchmarkCoverage(
prefs: Record<string, unknown>,
): BenchmarkCoverageResult;
export declare function writeBenchmarkCoverage(
coverage: BenchmarkCoverageResult,
): void;
export declare function detectCoverageChange(
coverage: BenchmarkCoverageResult,
): boolean;
export declare function scheduleBenchmarkCoverageAudit(
prefs: Record<string, unknown>,
notify?: (message: string) => void,

View file

@ -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,
});
}
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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({

View file

@ -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",

View file

@ -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",

View file

@ -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) {

View file

@ -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 */
}

View file

@ -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);

View file

@ -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);

View file

@ -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.
*

View file

@ -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 = {

View file

@ -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)

View file

@ -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) {

View file

@ -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

View file

@ -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 ───────────────────────────────────────────────────────────
/**

View file

@ -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(
{

View file

@ -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.
*/

View file

@ -19,7 +19,8 @@ import {
statSync,
writeFileSync,
} from "node:fs";
import { basename,
import {
basename,
dirname,
extname,
isAbsolute,

View file

@ -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",

View file

@ -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.

View file

@ -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 = [

View file

@ -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();

View file

@ -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");
}
}

View file

@ -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,

View file

@ -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 = [

View file

@ -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;
}

View file

@ -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";

View file

@ -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 = [

View file

@ -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.

View file

@ -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+)-.+$/;

View file

@ -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;

View file

@ -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/) ─

View file

@ -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",

View file

@ -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"]
}
}

View file

@ -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 {

View file

@ -1,3 +1,7 @@
export declare function refreshGeminiCatalog(basePath: string): Promise<string[] | null>;
export declare function runGeminiCatalogRefreshIfStale(basePath: string): Promise<string[] | null>;
export declare function refreshGeminiCatalog(
basePath: string,
): Promise<string[] | null>;
export declare function runGeminiCatalogRefreshIfStale(
basePath: string,
): Promise<string[] | null>;
export declare function scheduleGeminiCatalogRefresh(basePath: string): void;

View file

@ -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;

View file

@ -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(

View file

@ -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,

View file

@ -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;

View file

@ -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;
}>;

View file

@ -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;

View file

@ -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.

View file

@ -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");

View file

@ -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,

View file

@ -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.

View file

@ -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<string[] | null>;
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<void>;
export declare function refreshSfManagedProviders(basePath: string, auth: { getCredentialsForProvider: (id: string) => Array<{ type: string; key?: string }> }): Promise<void>;
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<string[] | null>;
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<void>;
export declare function refreshSfManagedProviders(
basePath: string,
auth: {
getCredentialsForProvider: (
id: string,
) => Array<{ type: string; key?: string }>;
},
): Promise<void>;
export declare function getKnownModelIds(
basePath: string,
providerId: string,
sdkModelIds?: string[],
): string[];

View file

@ -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 };

View file

@ -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;

View file

@ -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<RouteKey, CanonicalId> = {
"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<RouteKey, CanonicalId> = {
"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<string, unknown>;
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<CanonicalId, RouteKey[]>();
reasoning?: boolean;
input?: string[];
capabilities?: Record<string, unknown>;
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<CanonicalId, RouteKey[]>();
function resolveEntry(
routeKey: RouteKey,
entry: ReturnType<typeof _ENTRY_BY_ROUTE["get"]>,
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;
}

View file

@ -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(

View file

@ -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;

View file

@ -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,

View file

@ -1,4 +1,10 @@
export declare function readCodexAvailableModels(): Promise<string[] | null>;
export declare function refreshOpenaiCodexCatalog(basePath?: string): Promise<string[] | null>;
export declare function runOpenaiCodexCatalogRefreshIfStale(basePath?: string): Promise<string[] | null>;
export declare function scheduleOpenaiCodexCatalogRefresh(basePath?: string): void;
export declare function refreshOpenaiCodexCatalog(
basePath?: string,
): Promise<string[] | null>;
export declare function runOpenaiCodexCatalogRefreshIfStale(
basePath?: string,
): Promise<string[] | null>;
export declare function scheduleOpenaiCodexCatalogRefresh(
basePath?: string,
): void;

View file

@ -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 ─────────────────────────────────────────────────────────
/**

Some files were not shown because too many files have changed in this diff Show more