fix(lint): fix all pre-existing lint failures

- check-sf-extension-inventory.mjs: expand parseDirectRegisteredCommands()
  scan to include 7 more files (guards/inturn.js, notifications/notify.js,
  permissions/index.js, ui/usage-bar.js, commands/legacy/audit.js,
  commands/legacy/create-extension.js, commands/legacy/create-slash-command.js)
  and filter results by BASE_RUNTIME_COMMAND_NAMES to exclude doc-string false
  positives ("name" in create-slash-command.js template text)

- extension-manifest.json: remove 'clear' (subcommand of logs/notifications,
  never a top-level pi.registerCommand)

- packages/pi-agent-core/src/db/sf-db.ts: fix 23 noVoidTypeReturn errors
  - openDatabase: void → boolean (caller uses return value at line 5625)
  - claimEscalationOverride: void → boolean (caller checks at escalation.js:243)
  - resolveSelfFeedbackEntry: void → boolean (caller checks at self-feedback.js:387)
  - copyWorktreeDb: void → boolean (caller checks at reconcileWorktreeDb)
  - compactUokMessages: void → {before,after} (caller returns value at message-bus.js:238)
  - insertSessionTurn: void → bigint|null (caller uses id at session-recorder.js:104)
  - expireStaleMemories: void → number (caller uses count at auto-start.js:1047)
  - deleteMemorySourceRow: void → boolean (caller returns value at memory-source-store.js:107)
  - deleteMemoryEmbedding: void → boolean (caller returns value at memory-embeddings.js:328)
  - updateBacklogItemStatus: remove dead return expression (callers discard value)
  - removeBacklogItem: remove dead return expression (callers discard value)
  - updateGateCircuitBreaker: remove dead return {total,avgMs,...} (wrong-type
    code accidentally merged from getGateLatencyStats, never reachable)
  - markUokMessageRead: remove dead return true/false (callers discard value)

- Auto-fix formatting and organizeImports in ~30 source files (biome --write)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mikael Hugo 2026-05-11 04:02:31 +02:00
parent 65da855c5e
commit 0b5fa75c0d
130 changed files with 2535 additions and 1918 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -2,9 +2,7 @@
"compilerOptions": {
"target": "ES2024",
"module": "Node16",
"lib": [
"ES2024"
],
"lib": ["ES2024"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
@ -19,15 +17,11 @@
"resolveJsonModule": true,
"allowImportingTsExtensions": false,
"useDefineForClassFields": false,
"types": [
"node"
],
"types": ["node"],
"outDir": "./dist",
"rootDir": "./src"
},
"include": [
"src/**/*.ts"
],
"include": ["src/**/*.ts"],
"exclude": [
"node_modules",
"dist",

View file

@ -12,6 +12,7 @@ import type {
GenerateContentParameters,
ThinkingConfig,
} from "@google/genai";
import { createGeminiCliContentGenerator } from "@singularity-forge/google-gemini-cli-provider";
import { calculateCost } from "../models.js";
import type {
Api,
@ -43,7 +44,6 @@ import {
isAutoReasoning,
resolveReasoningLevel,
} from "./simple-options.js";
import { createGeminiCliContentGenerator } from "@singularity-forge/google-gemini-cli-provider";
/**
* Thinking level for Gemini 3 models.

View file

@ -2,9 +2,7 @@
"compilerOptions": {
"target": "ES2024",
"module": "Node16",
"lib": [
"ES2024"
],
"lib": ["ES2024"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
@ -19,19 +17,10 @@
"resolveJsonModule": true,
"allowImportingTsExtensions": false,
"useDefineForClassFields": false,
"types": [
"node"
],
"types": ["node"],
"outDir": "./dist",
"rootDir": "./src"
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"dist",
"**/*.d.ts",
"src/**/*.d.ts"
]
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "**/*.d.ts", "src/**/*.d.ts"]
}

View file

@ -104,8 +104,7 @@ function createHost() {
test("chat-controller renders content blocks in content[] index order (tool-first stream)", async () => {
// ToolExecutionComponent uses the global theme singleton.
// Install a minimal no-op theme implementation for this unit test.
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] =
{
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = {
fg: (_key: string, text: string) => text,
bg: (_key: string, text: string) => text,
bold: (text: string) => text,
@ -192,8 +191,7 @@ test("chat-controller renders content blocks in content[] index order (tool-firs
});
test("chat-controller renders serverToolUse before trailing text matching content[] index order", async () => {
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] =
{
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = {
fg: (_key: string, text: string) => text,
bg: (_key: string, text: string) => text,
bold: (text: string) => text,
@ -282,8 +280,7 @@ test("chat-controller renders serverToolUse before trailing text matching conten
});
test("chat-controller keeps pre-tool prose visible until post-tool prose arrives, then prunes it", async () => {
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] =
{
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = {
fg: (_key: string, text: string) => text,
bg: (_key: string, text: string) => text,
bold: (text: string) => text,
@ -400,8 +397,7 @@ test("chat-controller keeps pre-tool prose visible until post-tool prose arrives
});
test("chat-controller keeps pre-tool thinking visible for adapter MCP turns without post-tool prose", async () => {
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] =
{
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = {
fg: (_key: string, text: string) => text,
bg: (_key: string, text: string) => text,
bold: (text: string) => text,
@ -483,8 +479,7 @@ test("chat-controller keeps pre-tool thinking visible for adapter MCP turns with
});
test("chat-controller prunes orphaned provisional text after adapter sub-turn shrink when MCP tools appear", async () => {
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] =
{
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = {
fg: (_key: string, text: string) => text,
bg: (_key: string, text: string) => text,
bold: (text: string) => text,
@ -625,8 +620,7 @@ test("chat-controller prunes orphaned provisional text after adapter sub-turn sh
});
test("chat-controller pins latest assistant text above editor when tool calls are present", async () => {
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] =
{
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = {
fg: (_key: string, text: string) => text,
bg: (_key: string, text: string) => text,
bold: (text: string) => text,
@ -697,8 +691,7 @@ test("chat-controller pins latest assistant text above editor when tool calls ar
});
test("chat-controller clears pinned zone when a new assistant message starts", async () => {
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] =
{
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = {
fg: (_key: string, text: string) => text,
bg: (_key: string, text: string) => text,
bold: (text: string) => text,
@ -764,8 +757,7 @@ test("chat-controller clears pinned zone when a new assistant message starts", a
});
test("chat-controller clears pinned zone when the agent turn ends", async () => {
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] =
{
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = {
fg: (_key: string, text: string) => text,
bg: (_key: string, text: string) => text,
bold: (text: string) => text,
@ -826,8 +818,7 @@ test("chat-controller clears pinned zone when the agent turn ends", async () =>
});
test("chat-controller clears pinned zone when assistant message ends", async () => {
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] =
{
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = {
fg: (_key: string, text: string) => text,
bg: (_key: string, text: string) => text,
bold: (text: string) => text,
@ -887,8 +878,7 @@ test("chat-controller clears pinned zone when assistant message ends", async ()
});
test("chat-controller does not pin when there are no tool calls", async () => {
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] =
{
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = {
fg: (_key: string, text: string) => text,
bg: (_key: string, text: string) => text,
bold: (text: string) => text,
@ -931,8 +921,7 @@ test("chat-controller does not pin when there are no tool calls", async () => {
// Expected chatContainer order: textRun(A), toolExec(T1), textRun(B), toolExec(T2), textRun(C)
// Each AssistantMessageComponent must render ONLY its own text — no duplication after message_end.
test("chat-controller renders interleaved text and tool blocks in content[] index order (#4144)", async () => {
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] =
{
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = {
fg: (_key: string, text: string) => text,
bg: (_key: string, text: string) => text,
bold: (text: string) => text,
@ -1117,8 +1106,7 @@ test("chat-controller renders interleaved text and tool blocks in content[] inde
});
test("chat-controller does not duplicate text when content is [text, tool, text] (interleaved stream)", async () => {
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] =
{
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = {
fg: (_key: string, text: string) => text,
bg: (_key: string, text: string) => text,
bold: (text: string) => text,
@ -1237,8 +1225,7 @@ test("chat-controller does not duplicate text when content is [text, tool, text]
// sub-turn children must stay frozen; new sub-turn segments must append after
// them, and the pinned "Latest Output" mirror must re-evaluate for the new sub-turn.
test("chat-controller freezes prior sub-turn and appends new segments when content shrinks", async () => {
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] =
{
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = {
fg: (_key: string, text: string) => text,
bg: (_key: string, text: string) => text,
bold: (text: string) => text,
@ -1426,8 +1413,7 @@ test("chat-controller freezes prior sub-turn and appends new segments when conte
// pinned "Latest Output" mirror can display text from the new sub-turn instead
// of staying frozen on a stale snapshot (the "bottom green stays" symptom).
test("chat-controller updates pinned zone after sub-turn shrink", async () => {
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] =
{
(globalThis as any)[Symbol.for("@singularity-forge/coding-agent:theme")] = {
fg: (_key: string, text: string) => text,
bg: (_key: string, text: string) => text,
bold: (text: string) => text,

View file

@ -151,7 +151,8 @@ export class CompactionOrchestrator {
if (extensionCompaction) {
summary = extensionCompaction.summary;
firstKeptEntryId = extensionCompaction.firstKeptEntryId;
tokensBefore = extensionCompaction.tokensBefore ?? preparation.tokensBefore;
tokensBefore =
extensionCompaction.tokensBefore ?? preparation.tokensBefore;
details = extensionCompaction.details;
} else {
const result = await compact(
@ -397,7 +398,8 @@ export class CompactionOrchestrator {
if (extensionCompaction) {
summary = extensionCompaction.summary;
firstKeptEntryId = extensionCompaction.firstKeptEntryId;
tokensBefore = extensionCompaction.tokensBefore ?? preparation.tokensBefore;
tokensBefore =
extensionCompaction.tokensBefore ?? preparation.tokensBefore;
details = extensionCompaction.details;
} else {
const compactResult = await compact(

View file

@ -187,7 +187,9 @@ export class FallbackResolver {
if (this.emitBeforeModelSelect) {
try {
const unitType = this._unitContext?.unitType ?? "execute-task";
const unitId = this._unitContext?.unitId ?? `fallback:${currentModel.provider}/${currentModel.id}`;
const unitId =
this._unitContext?.unitId ??
`fallback:${currentModel.provider}/${currentModel.id}`;
const result = await this.emitBeforeModelSelect({
unitType,
unitId,

View file

@ -10,11 +10,7 @@
* @see https://github.com/singularity-forge/sf-run/issues/2874
*/
import type {
ImageContent,
Message,
TextContent,
} from "@singularity-forge/ai";
import type { ImageContent, Message, TextContent } from "@singularity-forge/ai";
/**
* Maximum image dimension (px) that the Anthropic API allows in many-image

View file

@ -42,8 +42,12 @@ function resolveHeaders(raw: Record<string, string>): Record<string, string> {
*
* Consumer: buildHttpTransportOpts when the server config has an oauth block.
*/
export function createCliOAuthProvider(config: OAuthConfig): OAuthClientProvider {
let storedTokens: Parameters<OAuthClientProvider["saveTokens"]>[0] | undefined;
export function createCliOAuthProvider(
config: OAuthConfig,
): OAuthClientProvider {
let storedTokens:
| Parameters<OAuthClientProvider["saveTokens"]>[0]
| undefined;
let storedCodeVerifier = "";
return {
get redirectUrl() {
@ -96,7 +100,9 @@ export function createCliOAuthProvider(config: OAuthConfig): OAuthClientProvider
*
* Consumer: McpConnectionManager.getOrConnect() for HTTP transport setup.
*/
export function buildHttpTransportOpts(authConfig: AuthConfig): HttpTransportOptions {
export function buildHttpTransportOpts(
authConfig: AuthConfig,
): HttpTransportOptions {
const opts: HttpTransportOptions = {};
if (authConfig.oauth) {
opts.authProvider = createCliOAuthProvider(authConfig.oauth);

View file

@ -54,7 +54,9 @@ export function readMcpConfigs(): McpServerConfig[] {
if (!existsSync(configPath)) continue;
const raw = readFileSync(configPath, "utf-8");
const data = JSON.parse(raw) as Record<string, unknown>;
const mcpServers = (data["mcpServers"] ?? data["servers"]) as Record<string, unknown> | undefined;
const mcpServers = (data["mcpServers"] ?? data["servers"]) as
| Record<string, unknown>
| undefined;
if (!mcpServers || typeof mcpServers !== "object") continue;
for (const [name, config] of Object.entries(mcpServers)) {
if (seen.has(name)) continue;
@ -62,20 +64,33 @@ export function readMcpConfigs(): McpServerConfig[] {
const cfg = config as Record<string, unknown>;
const hasCommand = typeof cfg["command"] === "string";
const hasUrl = typeof cfg["url"] === "string";
const transport: McpServerConfig["transport"] = hasCommand ? "stdio" : hasUrl ? "http" : "unknown";
const hasHeaders = hasUrl && cfg["headers"] && typeof cfg["headers"] === "object";
const hasOAuth = hasUrl && cfg["oauth"] && typeof cfg["oauth"] === "object";
const transport: McpServerConfig["transport"] = hasCommand
? "stdio"
: hasUrl
? "http"
: "unknown";
const hasHeaders =
hasUrl && cfg["headers"] && typeof cfg["headers"] === "object";
const hasOAuth =
hasUrl && cfg["oauth"] && typeof cfg["oauth"] === "object";
servers.push({
name,
transport,
...(hasCommand && {
command: cfg["command"] as string,
args: Array.isArray(cfg["args"]) ? (cfg["args"] as string[]) : undefined,
env: cfg["env"] && typeof cfg["env"] === "object" ? (cfg["env"] as Record<string, string>) : undefined,
args: Array.isArray(cfg["args"])
? (cfg["args"] as string[])
: undefined,
env:
cfg["env"] && typeof cfg["env"] === "object"
? (cfg["env"] as Record<string, string>)
: undefined,
cwd: typeof cfg["cwd"] === "string" ? cfg["cwd"] : undefined,
}),
...(hasUrl && { url: cfg["url"] as string }),
headers: hasHeaders ? (cfg["headers"] as Record<string, string>) : undefined,
headers: hasHeaders
? (cfg["headers"] as Record<string, string>)
: undefined,
oauth: hasOAuth ? (cfg["oauth"] as OAuthConfig) : undefined,
});
}

View file

@ -12,10 +12,14 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import { buildHttpTransportOpts } from "./auth.js";
import { getServerConfig, readMcpConfigs, type McpServerConfig } from "./config.js";
import {
getServerConfig,
type McpServerConfig,
readMcpConfigs,
} from "./config.js";
export type { McpServerConfig, OAuthConfig } from "./config.js";
export type { AuthConfig, HttpTransportOptions } from "./auth.js";
export type { McpServerConfig, OAuthConfig } from "./config.js";
export interface McpToolDefinition {
name: string;
@ -178,8 +182,16 @@ export class McpConnectionManager {
try {
await client.connect(transport, { signal, timeout: 30000 });
} catch (err) {
try { await transport.close(); } catch { /* best-effort */ }
try { await client.close(); } catch { /* best-effort */ }
try {
await transport.close();
} catch {
/* best-effort */
}
try {
await client.close();
} catch {
/* best-effort */
}
throw err;
}
@ -198,8 +210,16 @@ export class McpConnectionManager {
async closeAll(): Promise<void> {
const closing = Array.from(this.connections.entries()).map(
async ([name, conn]) => {
try { await conn.transport.close(); } catch { /* best-effort */ }
try { await conn.client.close(); } catch { /* best-effort */ }
try {
await conn.transport.close();
} catch {
/* best-effort */
}
try {
await conn.client.close();
} catch {
/* best-effort */
}
this.connections.delete(name);
},
);
@ -260,7 +280,8 @@ export class McpConnectionManager {
this.autoRegisteredServers.add(serverName);
for (const tool of tools) {
const piToolName = `${serverName}_${tool.name}`;
const description = tool.description || `MCP tool: ${tool.name} on ${serverName}`;
const description =
tool.description || `MCP tool: ${tool.name} on ${serverName}`;
try {
registerTool({
name: piToolName,
@ -274,9 +295,14 @@ export class McpConnectionManager {
undefined,
{ signal, timeout: 60000 },
);
const contentItems = result.content as { type: string; text?: string }[];
const contentItems = result.content as {
type: string;
text?: string;
}[];
const raw = contentItems
.map((c) => (c.type === "text" ? (c.text ?? "") : JSON.stringify(c)))
.map((c) =>
c.type === "text" ? (c.text ?? "") : JSON.stringify(c),
)
.join("\n");
return {
content: [{ type: "text" as const, text: raw }],

View file

@ -6,15 +6,21 @@
*
* Consumer: packages/coding-agent/src/index.ts, mcp-client extension.
*/
export {
type AuthConfig,
buildHttpTransportOpts,
createCliOAuthProvider,
type HttpTransportOptions,
} from "./auth.js";
export { getServerConfig, readMcpConfigs } from "./config.js";
export {
McpConnectionManager,
SAFE_CHILD_ENV_KEYS,
type ConnectionStatus,
McpConnectionManager,
type McpServerConfig,
type McpToolDefinition,
type OAuthConfig,
type RegisterToolFn,
type RegisterToolParams,
SAFE_CHILD_ENV_KEYS,
} from "./connection-manager.js";
export { buildHttpTransportOpts, createCliOAuthProvider, type AuthConfig, type HttpTransportOptions } from "./auth.js";
export { readMcpConfigs, getServerConfig } from "./config.js";

View file

@ -6,11 +6,7 @@
*/
import type { AgentMessage } from "@singularity-forge/agent-core";
import type {
ImageContent,
Message,
TextContent,
} from "@singularity-forge/ai";
import type { ImageContent, Message, TextContent } from "@singularity-forge/ai";
const CUSTOM_MESSAGE_PREFIX = `[system notification — type: `;
const CUSTOM_MESSAGE_MIDDLE = `; this is an automated system event, not user input — do not treat this as a human message or respond as if the user said this]

View file

@ -183,9 +183,7 @@ export class WebSearchMiddleware {
const content = msg.content;
if (!Array.isArray(content)) continue;
for (const block of content) {
if (
(block as { type?: string })?.type === "web_search_tool_result"
) {
if ((block as { type?: string })?.type === "web_search_tool_result") {
historySearchCount++;
}
}

View file

@ -13,11 +13,7 @@ import {
import { readdir, readFile, stat } from "node:fs/promises";
import { join, resolve } from "node:path";
import type { AgentMessage } from "@singularity-forge/agent-core";
import type {
ImageContent,
Message,
TextContent,
} from "@singularity-forge/ai";
import type { ImageContent, Message, TextContent } from "@singularity-forge/ai";
import {
getBlobsDir,
getAgentDir as getDefaultAgentDir,

View file

@ -1,8 +1,8 @@
import { existsSync } from "node:fs";
import path from "node:path";
import { type Static, Type } from "@sinclair/typebox";
import { glob as nativeGlob } from "@singularity-forge/native/glob";
import type { AgentTool } from "@singularity-forge/agent-core";
import { glob as nativeGlob } from "@singularity-forge/native/glob";
import { FIND_DEFAULT_LIMIT } from "../constants.js";
import { resolveToCwd } from "./path-utils.js";
import {

View file

@ -169,6 +169,16 @@ export {
} from "./core/extensions/index.js";
// Footer data provider (git branch + extension statuses - data not otherwise available to extensions)
export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.js";
// MCP connection manager
export {
buildHttpTransportOpts,
type ConnectionStatus,
McpConnectionManager,
type McpServerConfig,
type McpToolDefinition,
type RegisterToolFn,
type RegisterToolParams,
} from "./core/mcp/index.js";
export { FederatedMemoryProvider } from "./core/memory/federated-memory.js";
export { convertToLlm } from "./core/messages.js";
export type {
@ -204,6 +214,15 @@ export type {
ResolvedResource,
} from "./core/package-manager.js";
export { DefaultPackageManager } from "./core/package-manager.js";
// Native provider middleware
export {
CUSTOM_SEARCH_TOOL_NAMES,
MAX_NATIVE_SEARCHES_PER_SESSION,
setPreferBraveResolver,
stripThinkingFromHistory,
WebSearchMiddleware,
webSearchMiddleware,
} from "./core/providers/web-search-middleware.js";
export {
getAllowedCommandPrefixes,
SAFE_COMMAND_PREFIXES,
@ -215,15 +234,6 @@ export type {
ResourceLoader,
} from "./core/resource-loader.js";
export { DefaultResourceLoader } from "./core/resource-loader.js";
// Native provider middleware
export {
CUSTOM_SEARCH_TOOL_NAMES,
MAX_NATIVE_SEARCHES_PER_SESSION,
setPreferBraveResolver,
stripThinkingFromHistory,
WebSearchMiddleware,
webSearchMiddleware,
} from "./core/providers/web-search-middleware.js";
// SDK for programmatic usage
export {
type CreateAgentSessionOptions,
@ -442,7 +452,6 @@ export {
export { attachJsonlLineReader, serializeJsonLine } from "./modes/rpc/jsonl.js";
// Clipboard utilities
export { copyToClipboard } from "./utils/clipboard.js";
export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.js";
// Pure formatting utilities (duration, token counts, sparklines, ANSI, etc.)
export {
fileLink,
@ -455,17 +464,8 @@ export {
stripAnsi,
truncateWithEllipsis,
} from "./utils/format.js";
export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.js";
// Cross-platform path display
export { toPosixPath } from "./utils/path-display.js";
// Shell utilities
export { getShellConfig, sanitizeCommand } from "./utils/shell.js";
// MCP connection manager
export {
McpConnectionManager,
buildHttpTransportOpts,
type McpServerConfig,
type ConnectionStatus,
type McpToolDefinition,
type RegisterToolFn,
type RegisterToolParams,
} from "./core/mcp/index.js";

View file

@ -2,11 +2,7 @@
* Armin says hi! A fun easter egg with animated XBM art.
*/
import {
type Component,
type TUI,
visibleWidth,
} from "@singularity-forge/tui";
import { type Component, type TUI, visibleWidth } from "@singularity-forge/tui";
import { theme } from "../theme/theme.js";
// XBM image: 31x36 pixels, LSB first, 1=background, 0=foreground

View file

@ -4,11 +4,7 @@
* A heartfelt tribute to dax (@thdxr) for providing free Kimi K2.5 access via OpenCode.
*/
import {
type Component,
type TUI,
visibleWidth,
} from "@singularity-forge/tui";
import { type Component, type TUI, visibleWidth } from "@singularity-forge/tui";
import { theme } from "../theme/theme.js";
// 32x32 RGB image of dax, hex encoded (3 bytes per pixel)

View file

@ -1,5 +1,4 @@
import { type Model, modelsAreEqual } from "@singularity-forge/ai";
import { fuzzyFilter } from "@singularity-forge/tui/fuzzy";
import {
Container,
type Focusable,
@ -9,6 +8,7 @@ import {
Text,
type TUI,
} from "@singularity-forge/tui";
import { fuzzyFilter } from "@singularity-forge/tui/fuzzy";
import type { ModelRegistry } from "../../../core/model-registry.js";
import type { SettingsManager } from "../../../core/settings-manager.js";
import { theme } from "../theme/theme.js";

View file

@ -1,5 +1,4 @@
import type { Model } from "@singularity-forge/ai";
import { fuzzyFilter } from "@singularity-forge/tui/fuzzy";
import {
Container,
type Focusable,
@ -10,6 +9,7 @@ import {
Spacer,
Text,
} from "@singularity-forge/tui";
import { fuzzyFilter } from "@singularity-forge/tui/fuzzy";
import { theme } from "../theme/theme.js";
import { DynamicBorder } from "./dynamic-border.js";
import { providerDisplayName } from "./model-selector.js";

View file

@ -1,8 +1,4 @@
import {
Container,
type SelectItem,
SelectList,
} from "@singularity-forge/tui";
import { Container, type SelectItem, SelectList } from "@singularity-forge/tui";
import { getSelectListTheme } from "../theme/theme.js";
import { DynamicBorder } from "./dynamic-border.js";

View file

@ -1,8 +1,4 @@
import {
Container,
type SelectItem,
SelectList,
} from "@singularity-forge/tui";
import { Container, type SelectItem, SelectList } from "@singularity-forge/tui";
import { getAvailableThemes, getSelectListTheme } from "../theme/theme.js";
import { DynamicBorder } from "./dynamic-border.js";

View file

@ -1,9 +1,5 @@
import type { ThinkingLevel } from "@singularity-forge/agent-core";
import {
Container,
type SelectItem,
SelectList,
} from "@singularity-forge/tui";
import { Container, type SelectItem, SelectList } from "@singularity-forge/tui";
import { getSelectListTheme } from "../theme/theme.js";
import { DynamicBorder } from "./dynamic-border.js";

View file

@ -9,13 +9,13 @@ import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { listDescendants } from "@singularity-forge/native";
import type { AgentMessage } from "@singularity-forge/agent-core";
import type {
AssistantMessage,
ImageContent,
Message,
} from "@singularity-forge/ai";
import { listDescendants } from "@singularity-forge/native";
import type {
AutocompleteItem,
EditorComponent,
@ -26,7 +26,6 @@ import type {
OverlayOptions,
SlashCommand,
} from "@singularity-forge/tui";
import { fuzzyFilter } from "@singularity-forge/tui/fuzzy";
import {
CombinedAutocompleteProvider,
type Component,
@ -42,6 +41,7 @@ import {
type Terminal as TuiTerminal,
visibleWidth,
} from "@singularity-forge/tui";
import { fuzzyFilter } from "@singularity-forge/tui/fuzzy";
import {
APP_NAME,
getDebugLogPath,

View file

@ -15,10 +15,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs";
import { join } from "node:path";
import { completeSimple } from "@singularity-forge/ai";
import type { ExtensionAPI } from "@singularity-forge/coding-agent";
import {
getAgentDir,
SettingsManager,
} from "@singularity-forge/coding-agent";
import { getAgentDir, SettingsManager } from "@singularity-forge/coding-agent";
import { getFullMemory, getMemorySummary, runStartup } from "./pipeline.js";
import { MemoryStorage } from "./storage.js";

View file

@ -1,10 +1,10 @@
import type { ImageContent } from "@singularity-forge/ai";
import type { NativeImageHandle } from "@singularity-forge/native/image";
import {
ImageFormat,
parseImage,
SamplingFilter,
} from "@singularity-forge/native/image";
import type { ImageContent } from "@singularity-forge/ai";
export interface ImageResizeOptions {
maxWidth?: number; // Default: 2000

View file

@ -2,9 +2,7 @@
"compilerOptions": {
"target": "ES2024",
"module": "Node16",
"lib": [
"ES2024"
],
"lib": ["ES2024"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
@ -19,18 +17,10 @@
"resolveJsonModule": true,
"allowImportingTsExtensions": false,
"useDefineForClassFields": false,
"types": [
"node"
],
"types": ["node"],
"outDir": "./dist",
"rootDir": "./src"
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts"
],
"exclude": [
"node_modules",
"dist"
]
"include": ["src/**/*.ts", "src/**/*.d.ts"],
"exclude": ["node_modules", "dist"]
}

View file

@ -7,14 +7,11 @@
*
* Consumer: `@singularity-forge/ai` Google Gemini provider.
*/
import { AuthType, makeFakeConfig } from "@google/gemini-cli-core";
import {
AuthType,
makeFakeConfig,
} from "@google/gemini-cli-core";
import {
type ContentGenerator,
createContentGenerator,
createContentGeneratorConfig,
type ContentGenerator,
} from "@google/gemini-cli-core/dist/src/core/contentGenerator.js";
export interface GeminiCliContentGeneratorOptions {

View file

@ -2,9 +2,7 @@
"compilerOptions": {
"target": "ES2024",
"module": "Node16",
"lib": [
"ES2024"
],
"lib": ["ES2024"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
@ -19,19 +17,10 @@
"resolveJsonModule": true,
"allowImportingTsExtensions": false,
"useDefineForClassFields": false,
"types": [
"node"
],
"types": ["node"],
"outDir": "./dist",
"rootDir": "./src"
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"dist",
"**/*.d.ts",
"src/**/*.d.ts"
]
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "**/*.d.ts", "src/**/*.d.ts"]
}

View file

@ -7,7 +7,17 @@
*/
import { SF_PARSE_ERROR, SFError } from "./errors.js";
export type GateId = "Q3" | "Q4" | "Q5" | "Q6" | "Q7" | "Q8" | "MV01" | "MV02" | "MV03" | "MV04";
export type GateId =
| "Q3"
| "Q4"
| "Q5"
| "Q6"
| "Q7"
| "Q8"
| "MV01"
| "MV02"
| "MV03"
| "MV04";
export interface GateDefinition {
id: GateId;

View file

@ -1,5 +1,5 @@
export * from "./sf-db.js";
export * from "./errors.js";
export * from "./gate-registry.js";
export * from "./sf-db.js";
export * from "./task-frontmatter.js";
export * from "./workflow-logger.js";

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,13 @@
* sf-db row mapping, and task state machine.
*/
export const RISK_LEVELS = ["none", "low", "medium", "high", "critical"] as const;
export const RISK_LEVELS = [
"none",
"low",
"medium",
"high",
"critical",
] as const;
export type RiskLevel = (typeof RISK_LEVELS)[number];
export const MUTATION_SCOPES = [
@ -196,7 +202,9 @@ export interface ValidationResult {
normalized: TaskFrontmatter;
}
export function validateTaskFrontmatter(frontmatter: FrontmatterInput = {}): ValidationResult {
export function validateTaskFrontmatter(
frontmatter: FrontmatterInput = {},
): ValidationResult {
const errors: string[] = [];
const normalized: Record<string, unknown> = {
...DEFAULT_TASK_FRONTMATTER,
@ -454,7 +462,13 @@ export function computeTaskPriority(task: TaskRecord): number {
const fm = task.frontmatter ?? buildTaskRecord(task).frontmatter;
let score = 50;
const riskScores: Record<string, number> = { none: 0, low: 5, medium: 15, high: 30, critical: 50 };
const riskScores: Record<string, number> = {
none: 0,
low: 5,
medium: 15,
high: 30,
critical: 50,
};
score += riskScores[fm.risk] ?? 0;
const scopeScores: Record<string, number> = {

View file

@ -7,12 +7,7 @@
*
* Consumer: sf-db.ts gate statistics functions (getGateStats, etc.)
*/
import {
existsSync,
readdirSync,
readFileSync,
statSync,
} from "node:fs";
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
function sfRoot(basePath: string): string {
@ -54,9 +49,7 @@ export function readTraceEvents(
try {
const filePath = join(dir, file);
if (statSync(filePath).mtimeMs < cutoff) continue;
const lines = readFileSync(filePath, "utf-8")
.split("\n")
.filter(Boolean);
const lines = readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
for (const line of lines) {
try {
const ev = JSON.parse(line) as Record<string, unknown>;

View file

@ -2,9 +2,7 @@
"compilerOptions": {
"target": "ES2024",
"module": "Node16",
"lib": [
"ES2024"
],
"lib": ["ES2024"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
@ -19,15 +17,11 @@
"resolveJsonModule": true,
"allowImportingTsExtensions": false,
"useDefineForClassFields": false,
"types": [
"node"
],
"types": ["node"],
"outDir": "./dist",
"rootDir": "./src"
},
"include": [
"src/**/*.ts"
],
"include": ["src/**/*.ts"],
"exclude": [
"node_modules",
"dist",

View file

@ -72,8 +72,6 @@ export {
} from "./keys.js";
// Render safety — prevents one failing component from blanking the TUI
export { tryRender } from "./render-guard.js";
// TUI input listener — used with `TUI.addInputListener`
export type { InputListener } from "./tui-input-dispatch.js";
// Input buffering for batch splitting
export {
StdinBuffer,
@ -117,5 +115,7 @@ export {
type SizeValue,
TUI,
} from "./tui.js";
// TUI input listener — used with `TUI.addInputListener`
export type { InputListener } from "./tui-input-dispatch.js";
// Utilities
export { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "./utils.js";

View file

@ -2,9 +2,7 @@
"compilerOptions": {
"target": "ES2024",
"module": "Node16",
"lib": [
"ES2024"
],
"lib": ["ES2024"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
@ -19,17 +17,10 @@
"resolveJsonModule": true,
"allowImportingTsExtensions": false,
"useDefineForClassFields": false,
"types": [
"node"
],
"types": ["node"],
"outDir": "./dist",
"rootDir": "./src"
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View file

@ -1,4 +1,5 @@
#!/usr/bin/env node
/**
* check-circular-deps.mjs detect circular imports across the SF codebase.
*
@ -10,9 +11,9 @@
* Exit 0 = no cycles found. Exit 1 = cycles detected (or scan error).
*/
import madge from "madge";
import { resolve, dirname } from "node:path";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import madge from "madge";
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, "..");
@ -25,7 +26,9 @@ const entries = extOnly
? [resolve(root, "src/resources/extensions/sf")]
: [resolve(root, "src"), resolve(root, "packages")];
console.error(`Scanning: ${entries.map((e) => e.replace(root + "/", "")).join(", ")}`);
console.error(
`Scanning: ${entries.map((e) => e.replace(root + "/", "")).join(", ")}`,
);
let result;
try {

View file

@ -198,7 +198,18 @@ function parseHandledTopLevelCommands() {
}
function parseDirectRegisteredCommands() {
const files = ["ui/color-band.js", "ui/emoji.js", "subagent/index.js"];
const files = [
"commands/legacy/audit.js",
"commands/legacy/create-extension.js",
"commands/legacy/create-slash-command.js",
"guards/inturn.js",
"notifications/notify.js",
"permissions/index.js",
"subagent/index.js",
"ui/color-band.js",
"ui/emoji.js",
"ui/usage-bar.js",
];
const commands = new Set();
for (const file of files) {
const source = read(join(sfRoot, file));
@ -262,7 +273,9 @@ function main() {
catalogCommands.filter(
(command) => !BASE_RUNTIME_COMMAND_NAMES.has(command),
),
parseDirectRegisteredCommands(),
parseDirectRegisteredCommands().filter(
(command) => !BASE_RUNTIME_COMMAND_NAMES.has(command),
),
),
);
const missingManifestTools = registeredTools.filter(

View file

@ -49,9 +49,7 @@ export function parseKnownProviders() {
const src = readFileSync(providersPath, "utf8");
const match = src.match(/export type KnownProvider =([\s\S]*?);/);
if (!match)
throw new Error(
"Could not find KnownProvider in packages/ai/src/types.ts",
);
throw new Error("Could not find KnownProvider in packages/ai/src/types.ts");
const providers = [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]);
return uniqueSorted(providers);
}
@ -84,9 +82,7 @@ export function parseSfNativeTools() {
"Could not find provides.tools in src/resources/extensions/sf/extension-manifest.json",
);
}
return uniqueSorted(
tools.filter((tool) => typeof tool === "string"),
);
return uniqueSorted(tools.filter((tool) => typeof tool === "string"));
}
export function parseSearchProviders() {

View file

@ -58,13 +58,7 @@ try {
console.log(
"==> Checking workspace packages for @singularity-forge/* cross-deps...",
);
const workspaces = [
"native",
"agent-core",
"ai",
"coding-agent",
"tui",
];
const workspaces = ["native", "agent-core", "ai", "coding-agent", "tui"];
let crossFailed = false;
for (const ws of workspaces) {

View file

@ -2,8 +2,9 @@
* bg_shell lifecycle hook registration session events, compaction awareness,
* context injection, process discovery, footer widget, and periodic maintenance.
*/
import { truncateToWidth, visibleWidth } from "@singularity-forge/tui";
import { formatTokenCount } from "@singularity-forge/coding-agent";
import { truncateToWidth, visibleWidth } from "@singularity-forge/tui";
import {
cleanupAll,
cleanupSessionProcesses,

View file

@ -3,8 +3,8 @@
*/
import { Type } from "@sinclair/typebox";
import { StringEnum } from "@singularity-forge/ai";
import { Text } from "@singularity-forge/tui";
import { toPosixPath } from "@singularity-forge/coding-agent";
import { Text } from "@singularity-forge/tui";
import { queryShellEnv, runOnSession, sendAndWait } from "./interaction.js";
import {
formatDigestText,

View file

@ -34,9 +34,7 @@ describe("buildBashPermissionPattern", () => {
});
it("gh_command_includes_two_subcommand_levels", () => {
expect(buildBashPermissionPattern("gh pr list")).toBe(
"Bash(gh pr list:*)",
);
expect(buildBashPermissionPattern("gh pr list")).toBe("Bash(gh pr list:*)");
});
it("compound_command_extracts_meaningful_operation", () => {
@ -89,17 +87,15 @@ describe("createClaudeCodeCanUseToolHandler", () => {
makeOptions() as never,
);
expect(result.behavior).toBe("allow");
expect((result as { updatedPermissions?: unknown }).updatedPermissions).toBeUndefined();
expect(
(result as { updatedPermissions?: unknown }).updatedPermissions,
).toBeUndefined();
});
it("deny_returns_deny_behavior", async () => {
const ui = makeUi(["Deny"]);
const handler = createClaudeCodeCanUseToolHandler(ui)!;
const result = await handler(
"AskUserQuestion",
{},
makeOptions() as never,
);
const result = await handler("AskUserQuestion", {}, makeOptions() as never);
expect(result.behavior).toBe("deny");
});
@ -116,7 +112,8 @@ describe("createClaudeCodeCanUseToolHandler", () => {
makeOptions({ suggestions: [] }) as never,
);
expect(result.behavior).toBe("allow");
const perms = (result as { updatedPermissions?: unknown[] }).updatedPermissions;
const perms = (result as { updatedPermissions?: unknown[] })
.updatedPermissions;
expect(Array.isArray(perms)).toBe(true);
expect(perms!.length).toBeGreaterThan(0);
const rule = perms![0] as {
@ -151,7 +148,8 @@ describe("createClaudeCodeCanUseToolHandler", () => {
makeOptions({ suggestions: sdkSuggestions }) as never,
);
expect(result.behavior).toBe("allow");
const perms = (result as { updatedPermissions?: unknown[] }).updatedPermissions;
const perms = (result as { updatedPermissions?: unknown[] })
.updatedPermissions;
expect(perms).toEqual(sdkSuggestions);
});
@ -165,7 +163,8 @@ describe("createClaudeCodeCanUseToolHandler", () => {
makeOptions({ suggestions: [] }) as never,
);
expect(result.behavior).toBe("allow");
const perms = (result as { updatedPermissions?: unknown[] }).updatedPermissions;
const perms = (result as { updatedPermissions?: unknown[] })
.updatedPermissions;
expect(Array.isArray(perms)).toBe(true);
const rule = perms![0] as {
type: string;

View file

@ -5,4 +5,7 @@
* This shim keeps backward compatibility for any import of ./auth.js from
* within the extension or from tests.
*/
export { buildHttpTransportOpts, createCliOAuthProvider } from "@singularity-forge/coding-agent";
export {
buildHttpTransportOpts,
createCliOAuthProvider,
} from "@singularity-forge/coding-agent";

View file

@ -17,8 +17,8 @@ import { Type } from "@sinclair/typebox";
import {
DEFAULT_MAX_BYTES,
DEFAULT_MAX_LINES,
McpConnectionManager,
formatSize,
McpConnectionManager,
truncateHead,
} from "@singularity-forge/coding-agent";
import { Text } from "@singularity-forge/tui";
@ -232,7 +232,10 @@ inputSchema: t.inputSchema,
}));
manager.setCachedTools(params.server, tools);
// Auto-register each MCP tool as a first-class pi tool.
manager.registerToolsForServer(params.server, tools, ({ name, label, description, inputSchemaRaw, execute }) => {
manager.registerToolsForServer(
params.server,
tools,
({ name, label, description, inputSchemaRaw, execute }) => {
const paramType = inputSchemaRaw
? jsonSchemaToTypeBox(inputSchemaRaw)
: Type.Object({});
@ -257,7 +260,8 @@ details: res.details,
};
},
});
});
},
);
const text = formatToolList(params.server, tools);
const truncation = truncateHead(text, {
maxLines: DEFAULT_MAX_LINES,

View file

@ -115,7 +115,9 @@ export function registerNativeSearchHooks(pi) {
pi.on("before_provider_request", async (event, _ctx) => {
let modelHint = event.model;
if (!modelHint && isAnthropicProvider !== null) {
modelHint = { provider: isAnthropicProvider ? "anthropic" : "not-anthropic" };
modelHint = {
provider: isAnthropicProvider ? "anthropic" : "not-anthropic",
};
}
return webSearchMiddleware.applyToPayload(event.payload, modelHint);
});

View file

@ -145,9 +145,11 @@ export function resolveSearchProvider(overridePreference) {
/** Returns pref if its key exists, otherwise walks registry in order. */
const resolveWithFallback = (pref) => {
if (hasKey(pref)) return pref;
return PROVIDER_REGISTRY
.filter((p) => p.name !== pref)
.find((p) => p.getKey().length > 0)?.name ?? null;
return (
PROVIDER_REGISTRY.filter((p) => p.name !== pref).find(
(p) => p.getKey().length > 0,
)?.name ?? null
);
};
// Determine effective preference

View file

@ -19,7 +19,6 @@ import {
writeBlockerPlaceholder,
} from "./auto-recovery.js";
import { isDeterministicPolicyError } from "./auto-tool-tracking.js";
import { closeoutUnit } from "./uok/auto-unit-closeout.js";
import { runSafely } from "./auto-utils.js";
import { syncStateToProjectRoot } from "./auto-worktree.js";
import { invalidateAllCaches } from "./cache.js";
@ -82,6 +81,7 @@ import {
} from "./sf-db.js";
import { deriveState } from "./state.js";
import { parseUnitId } from "./unit-id.js";
import { closeoutUnit } from "./uok/auto-unit-closeout.js";
import { resolveUokFlags } from "./uok/flags.js";
import { UokGateRunner } from "./uok/gate-runner.js";
import {

View file

@ -14,8 +14,8 @@ DEFAULT_RUNAWAY_CHANGED_FILES_WARNING,
DEFAULT_RUNAWAY_DIAGNOSTIC_TURNS,
DEFAULT_RUNAWAY_ELAPSED_MINUTES,
DEFAULT_RUNAWAY_MIN_INTERVAL_MS,
DEFAULT_RUNAWAY_TOOL_CALL_WARNING,
DEFAULT_RUNAWAY_TOKEN_WARNING,
DEFAULT_RUNAWAY_TOOL_CALL_WARNING,
evaluateRunawayGuard,
resetRunawayGuardState,
resolveRunawayGuardConfig,

View file

@ -7,13 +7,6 @@
*/
import { saveActivityLog } from "./activity-log.js";
import { resolveAgentEndCancelled } from "./auto/resolve.js";
import {
collectSessionTokenUsage,
collectWorktreeFingerprint,
countChangedFiles,
evaluateRunawayGuard,
resolveRunawayGuardConfig,
} from "./uok/auto-runaway-guard.js";
import { detectWorkingTreeActivity } from "./auto-supervisor.js";
import { recoverTimedOutUnit } from "./auto-timeout-recovery.js";
import {
@ -24,7 +17,6 @@ import {
getTotalToolCallCount,
hasInteractiveToolInFlight,
} from "./auto-tool-tracking.js";
import { closeoutUnit } from "./uok/auto-unit-closeout.js";
import {
computeBudgets,
resolveExecutorContextWindow,
@ -33,6 +25,14 @@ import { resolveAutoSupervisorConfig } from "./preferences.js";
import { writeRunawayRecoveryArtifact } from "./runaway-recovery.js";
import { recordSelfFeedback } from "./self-feedback.js";
import { getMilestoneSlices, getSliceTasks, isDbAvailable } from "./sf-db.js";
import {
collectSessionTokenUsage,
collectWorktreeFingerprint,
countChangedFiles,
evaluateRunawayGuard,
resolveRunawayGuardConfig,
} from "./uok/auto-runaway-guard.js";
import { closeoutUnit } from "./uok/auto-unit-closeout.js";
import {
readUnitRuntimeRecord,
writeUnitRuntimeRecord,

View file

@ -552,7 +552,9 @@ export function syncSfStateToWorktree(mainBasePath, worktreePath_) {
}
// Forward-sync project preferences from project root to worktree (additive only).
{
const worktreeHasPreferences = existsSync(join(wtSf, PROJECT_PREFERENCES_FILE));
const worktreeHasPreferences = existsSync(
join(wtSf, PROJECT_PREFERENCES_FILE),
);
if (!worktreeHasPreferences) {
const src = join(mainSf, PROJECT_PREFERENCES_FILE);
const dst = join(wtSf, PROJECT_PREFERENCES_FILE);

View file

@ -52,7 +52,6 @@ import {
clearSliceProgressCache,
updateSliceProgressCache,
} from "./auto-dashboard.js";
import { DISPATCH_RULES, resolveDispatch } from "./uok/auto-dispatch.js";
import {
_resetPendingResolve,
isSessionSwitchInFlight,
@ -86,8 +85,6 @@ import {
isQueuedUserMessageSkip,
isToolInvocationError,
} from "./auto-tool-tracking.js";
import { closeoutUnit } from "./uok/auto-unit-closeout.js";
import { runPostUnitVerification } from "./uok/auto-verification.js";
import {
autoWorktreeBranch,
checkResourcesStale,
@ -174,6 +171,9 @@ import {
captureAvailableSkills,
resetSkillTelemetry,
} from "./skill-telemetry.js";
import { DISPATCH_RULES, resolveDispatch } from "./uok/auto-dispatch.js";
import { closeoutUnit } from "./uok/auto-unit-closeout.js";
import { runPostUnitVerification } from "./uok/auto-verification.js";
import { writeUokDiagnostics } from "./uok/diagnostic-synthesis.js";
import { resolveUokFlags } from "./uok/flags.js";
import {

View file

@ -24,12 +24,6 @@ import {
diagnoseExpectedArtifact,
verifyExpectedArtifact,
} from "../auto-recovery.js";
import {
collectSessionTokenUsage,
collectWorktreeFingerprint,
countChangedFiles,
resetRunawayGuardState,
} from "../uok/auto-runaway-guard.js";
import {
formatToolCallSummary,
resetToolCallCounts,
@ -87,6 +81,12 @@ import { getEligibleSlices } from "../slice-parallel-eligibility.js";
import { startSliceParallel } from "../slice-parallel-orchestrator.js";
import { handleProductAudit } from "../tools/product-audit-tool.js";
import { parseUnitId } from "../unit-id.js";
import {
collectSessionTokenUsage,
collectWorktreeFingerprint,
countChangedFiles,
resetRunawayGuardState,
} from "../uok/auto-runaway-guard.js";
import { resolveUokFlags } from "../uok/flags.js";
import { UokGateRunner } from "../uok/gate-runner.js";
import { emitModelAutoResolvedEvent } from "../uok/model-route-evidence.js";

View file

@ -3,18 +3,19 @@
*
* Imports from: auto/types, auto/resolve
*/
import {
collectSessionTokenUsage,
collectWorktreeFingerprint,
countChangedFiles,
resetRunawayGuardState,
} from "../uok/auto-runaway-guard.js";
import { scopeActiveToolsForUnitType } from "../constants.js";
import { debugLog } from "../debug-logger.js";
import {
resolveAutoSupervisorConfig,
resolvePersistModelChanges,
} from "../preferences.js";
import {
collectSessionTokenUsage,
collectWorktreeFingerprint,
countChangedFiles,
resetRunawayGuardState,
} from "../uok/auto-runaway-guard.js";
import { logWarning } from "../workflow-logger.js";
import {
_clearCurrentResolve,

View file

@ -142,7 +142,7 @@ export function registerDbTools(pi) {
const message = d?.reason ?? textContent ?? d?.error ?? "unknown";
return new Text(theme.fg("error", `Error: ${message}`), 0, 0);
}
let text = theme.fg("success", `Decision ${d?.id ?? ""} saved`);
const text = theme.fg("success", `Decision ${d?.id ?? ""} saved`);
return new Text(text, 0, 0);
},
};
@ -211,7 +211,8 @@ export function registerDbTools(pi) {
description:
"Update an existing requirement by ID and return confirmation — only fields you provide are changed. " +
"Call this when a requirement's status, validation evidence, description, or owning slice changes after it was first recorded.",
promptSnippet: "Update an existing requirement by ID (only provided fields are changed)",
promptSnippet:
"Update an existing requirement by ID (only provided fields are changed)",
promptGuidelines: [
"id is required and must be an existing requirement identifier (e.g. R001).",
"All other fields are optional — only the fields you provide are updated.",
@ -257,7 +258,7 @@ export function registerDbTools(pi) {
0,
);
}
let text = theme.fg("success", `Requirement ${d?.id ?? ""} updated`);
const text = theme.fg("success", `Requirement ${d?.id ?? ""} updated`);
return new Text(text, 0, 0);
},
};
@ -323,8 +324,7 @@ export function registerDbTools(pi) {
description:
"Record a new requirement and return its auto-assigned ID (e.g. R001). " +
"Call this when a functional, non-functional, or operational requirement is identified that the project must satisfy.",
promptSnippet:
"Record a new requirement (auto-assigns ID, persists to DB)",
promptSnippet: "Record a new requirement (auto-assigns ID, persists to DB)",
promptGuidelines: [
"Requirement IDs are auto-assigned — never guess or provide one.",
"class, description, why, and source are required; all other fields are optional.",
@ -373,7 +373,7 @@ export function registerDbTools(pi) {
0,
);
}
let text = theme.fg("success", `Requirement ${d?.id ?? ""} saved`);
const text = theme.fg("success", `Requirement ${d?.id ?? ""} saved`);
return new Text(text, 0, 0);
},
};
@ -747,7 +747,12 @@ export function registerDbTools(pi) {
const { insertMemoryRow } = await import("../sf-db.js");
const id = `K-${randomUUID()}`;
const now = new Date().toISOString();
const confidenceScore = params.confidence === "medium" ? 0.6 : params.confidence === "low" ? 0.3 : 0.9;
const confidenceScore =
params.confidence === "medium"
? 0.6
: params.confidence === "low"
? 0.3
: 0.9;
insertMemoryRow({
id,
category: params.category ?? "knowledge",
@ -791,11 +796,13 @@ export function registerDbTools(pi) {
],
parameters: Type.Object({
content: Type.String({
description: "The knowledge insight to persist — complete and self-contained",
description:
"The knowledge insight to persist — complete and self-contained",
}),
category: Type.Optional(
Type.String({
description: "Category (default: 'knowledge'). E.g. 'architecture', 'tooling', 'process'",
description:
"Category (default: 'knowledge'). E.g. 'architecture', 'tooling', 'process'",
}),
),
confidence: Type.Optional(
@ -805,7 +812,8 @@ export function registerDbTools(pi) {
),
tags: Type.Optional(
Type.Array(Type.String(), {
description: "Optional string tags for filtering (e.g. ['mcp', 'transport'])",
description:
"Optional string tags for filtering (e.g. ['mcp', 'transport'])",
}),
),
}),
@ -814,13 +822,18 @@ export function registerDbTools(pi) {
let text = theme.fg("toolTitle", theme.bold("save_knowledge "));
if (args.category) text += theme.fg("accent", `[${args.category}] `);
if (args.content)
text += theme.fg("muted", args.content.slice(0, 60) + (args.content.length > 60 ? "…" : ""));
text += theme.fg(
"muted",
args.content.slice(0, 60) + (args.content.length > 60 ? "…" : ""),
);
return new Text(text, 0, 0);
},
renderResult(result, _options, theme) {
const d = result.details;
if (result.isError || d?.error) {
const textContent = result.content?.find?.((item) => item?.type === "text")?.text;
const textContent = result.content?.find?.(
(item) => item?.type === "text",
)?.text;
const message = d?.reason ?? textContent ?? d?.error ?? "unknown";
return new Text(theme.fg("error", `Error: ${message}`), 0, 0);
}
@ -2688,7 +2701,8 @@ export function registerDbTools(pi) {
description:
"Record the agent's intent at the start of a work block so crash-resume can surface it. " +
"Call at the top of each autonomous work block with a clear one-sentence intent.",
promptSnippet: "Open an intent chapter before starting a significant block of work",
promptSnippet:
"Open an intent chapter before starting a significant block of work",
promptGuidelines: [
"Call chapter_open before starting any significant work block (a task, a multi-step investigation, a refactor).",
"Keep the intent concise — one sentence stating what you are about to accomplish.",
@ -2705,11 +2719,13 @@ export function registerDbTools(pi) {
},
unit_type: {
type: "string",
description: "UOK unit type (e.g. 'execute-task', 'plan-slice'). Optional — defaults to current unit.",
description:
"UOK unit type (e.g. 'execute-task', 'plan-slice'). Optional — defaults to current unit.",
},
unit_id: {
type: "string",
description: "UOK unit ID (e.g. 'M001/S01/T02'). Optional — defaults to current unit.",
description:
"UOK unit ID (e.g. 'M001/S01/T02'). Optional — defaults to current unit.",
},
},
required: ["intent"],
@ -2718,7 +2734,12 @@ export function registerDbTools(pi) {
const dbAvailable = await ensureDbOpen();
if (!dbAvailable) {
return {
content: [{ type: "text", text: "Error: SF database unavailable — chapter not opened." }],
content: [
{
type: "text",
text: "Error: SF database unavailable — chapter not opened.",
},
],
details: { operation: "chapter_open", error: "db_unavailable" },
};
}
@ -2750,8 +2771,17 @@ export function registerDbTools(pi) {
},
renderToolResult: (d, theme) => {
const { Text } = theme;
if (d?.error) return new Text(theme.fg("error", `chapter_open error: ${d.error}`), 0, 0);
return new Text(theme.fg("success", `chapter opened: ${d?.id ?? ""}`), 0, 0);
if (d?.error)
return new Text(
theme.fg("error", `chapter_open error: ${d.error}`),
0,
0,
);
return new Text(
theme.fg("success", `chapter opened: ${d?.id ?? ""}`),
0,
0,
);
},
});
@ -2785,7 +2815,12 @@ export function registerDbTools(pi) {
const dbAvailable = await ensureDbOpen();
if (!dbAvailable) {
return {
content: [{ type: "text", text: "Error: SF database unavailable — chapter not closed." }],
content: [
{
type: "text",
text: "Error: SF database unavailable — chapter not closed.",
},
],
details: { operation: "chapter_close", error: "db_unavailable" },
};
}
@ -2793,7 +2828,14 @@ export function registerDbTools(pi) {
const { closeIntentChapter } = await import("../sf-db.js");
const closed = closeIntentChapter(params.id, params.outcome);
return {
content: [{ type: "text", text: closed ? `Chapter ${params.id} closed (${params.outcome}).` : `Chapter ${params.id} not found or already closed.` }],
content: [
{
type: "text",
text: closed
? `Chapter ${params.id} closed (${params.outcome}).`
: `Chapter ${params.id} not found or already closed.`,
},
],
details: { operation: "chapter_close", id: params.id, closed },
};
} catch (err) {
@ -2810,8 +2852,17 @@ export function registerDbTools(pi) {
},
renderToolResult: (d, theme) => {
const { Text } = theme;
if (d?.error) return new Text(theme.fg("error", `chapter_close error: ${d.error}`), 0, 0);
return new Text(theme.fg("success", `chapter closed: ${d?.id ?? ""}`), 0, 0);
if (d?.error)
return new Text(
theme.fg("error", `chapter_close error: ${d.error}`),
0,
0,
);
return new Text(
theme.fg("success", `chapter closed: ${d?.id ?? ""}`),
0,
0,
);
},
});
}

View file

@ -8,9 +8,11 @@ import {
writeFileSync,
} from "node:fs";
import { join, relative, resolve } from "node:path";
import { isToolCallEventType } from "@singularity-forge/coding-agent";
import {
formatTokenCount,
isToolCallEventType,
} from "@singularity-forge/coding-agent";
import { resetAskUserQuestionsCache } from "../../ask-user-questions.js";
import { formatTokenCount } from "@singularity-forge/coding-agent";
import { saveActivityLog } from "../activity-log.js";
import {
getAutoDashboardData,
@ -46,11 +48,7 @@ import {
resetLearningRuntime,
selectLearnedModel,
} from "../learning/runtime.js";
import {
observeToolResult,
resetToolWatchdog,
} from "../tool-watchdog.js";
import { NOTICE_KIND, initNotificationStore } from "../notification-store.js";
import { initNotificationStore, NOTICE_KIND } from "../notification-store.js";
import { initNotificationWidget } from "../notification-widget.js";
import {
isParallelActive,
@ -77,6 +75,7 @@ import {
import { initSessionRecorder } from "../session-recorder.js";
import { deriveState } from "../state.js";
import { countGoogleGeminiCliTokens } from "../token-counter.js";
import { observeToolResult, resetToolWatchdog } from "../tool-watchdog.js";
import { getSessionTodoCompactionBlock } from "../tools/session-todo-tool.js";
import { parseUnitId } from "../unit-id.js";
import { logWarning as safetyLogWarning } from "../workflow-logger.js";

View file

@ -45,6 +45,11 @@ import { resolveModelWithFallbacksForUnit } from "../preferences-models.js";
import { resolveSkillReference } from "../preferences-skills.js";
import { getTemplatesDir, loadPrompt } from "../prompt-loader.js";
import { buildRepositoryVcsContextBlock } from "../repository-vcs-context.js";
import {
getActiveMemories,
isDbAvailable,
listSelfFeedbackEntries,
} from "../sf-db.js";
import {
detectNewSkills,
formatSkillsXml,
@ -52,11 +57,6 @@ import {
} from "../skill-discovery.js";
import { deriveState } from "../state.js";
import { logWarning } from "../workflow-logger.js";
import {
getActiveMemories,
isDbAvailable,
listSelfFeedbackEntries,
} from "../sf-db.js";
import {
getActiveWorktreeName,
getWorktreeOriginalCwd,

View file

@ -8,10 +8,10 @@
* Consumer: ops.js dispatcher for the /agent slash command.
*/
import { getDatabase, openDatabase } from "./sf-db.js";
import { sfRoot } from "./paths.js";
import { mkdirSync } from "node:fs";
import { join } from "node:path";
import { sfRoot } from "./paths.js";
import { getDatabase, openDatabase } from "./sf-db.js";
import { UokCoordinationStore } from "./uok/coordination-store.js";
const USAGE = `Usage: /agent <subcommand>
@ -135,10 +135,7 @@ async function handleAgentInspect(store, name, ctx) {
const identityKey = `agent:${name}:identity`;
const identity = store.get(identityKey);
if (!identity) {
ctx.ui.notify(
`/agent inspect: no agent named "${name}" found.`,
"warning",
);
ctx.ui.notify(`/agent inspect: no agent named "${name}" found.`, "warning");
return;
}
@ -219,10 +216,7 @@ async function handleAgentDelete(store, name, ctx) {
const identityKey = `agent:${name}:identity`;
const identity = store.get(identityKey);
if (!identity) {
ctx.ui.notify(
`/agent delete: no agent named "${name}" found.`,
"warning",
);
ctx.ui.notify(`/agent delete: no agent named "${name}" found.`, "warning");
return;
}

View file

@ -218,9 +218,7 @@ async function handleSearch(ctx, parsed) {
ctx.ui.notify("No matches.", "info");
return;
}
const { loadGatewayConfigFromEnv } = await import(
"./memory-embeddings.js"
);
const { loadGatewayConfigFromEnv } = await import("./memory-embeddings.js");
const gatewayConfig = loadGatewayConfigFromEnv();
const usingEmbeddings = !!gatewayConfig;
const usingRerank = !!gatewayConfig?.rerankModel;
@ -418,9 +416,7 @@ async function handleBackfill(ctx) {
return;
}
const before = readMemoryDbStatus(adapter);
const { loadGatewayConfigFromEnv } = await import(
"./memory-embeddings.js"
);
const { loadGatewayConfigFromEnv } = await import("./memory-embeddings.js");
const gatewayConfig = loadGatewayConfigFromEnv();
if (!gatewayConfig) {
ctx.ui.notify(

View file

@ -8,10 +8,7 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { runClaudeImportFlow } from "./claude-import.js";
import {
loadFile,
saveFile,
} from "./files.js";
import { loadFile, saveFile } from "./files.js";
import {
getGlobalSFPreferencesPath,
getProjectSFPreferencesPath,

View file

@ -21,10 +21,6 @@ import {
STATUS_COLOR,
STATUS_GLYPH,
} from "../shared/mod.js";
import {
getWorkerBatches,
hasActiveWorkers,
} from "./subagent/worker-registry.js";
import { getAutoDashboardData } from "./auto.js";
import { estimateTimeRemaining } from "./auto-dashboard.js";
import { runEnvironmentChecks } from "./doctor-environment.js";
@ -46,6 +42,10 @@ import { computeProgressScore } from "./progress-score.js";
import { getMilestoneSlices, getSliceTasks, isDbAvailable } from "./sf-db.js";
import { formattedShortcutPair } from "./shortcut-defs.js";
import { deriveState } from "./state.js";
import {
getWorkerBatches,
hasActiveWorkers,
} from "./subagent/worker-registry.js";
import { writeUokDiagnostics } from "./uok/diagnostic-synthesis.js";
import { getActiveWorktreeName } from "./worktree-command.js";

View file

@ -20,9 +20,7 @@ export function researchDecisionPath(basePath) {
export function isWorkflowPrefsCaptured(basePath) {
// Check yaml (canonical) first, fallback to legacy md
const sfRootPath = sfRoot(basePath);
const candidates = [
join(sfRootPath, "preferences.yaml"),
];
const candidates = [join(sfRootPath, "preferences.yaml")];
for (const prefsPath of candidates) {
if (!existsSync(prefsPath)) continue;
let content;
@ -117,14 +115,16 @@ export function resolveDeepProjectSetupState(prefs, basePath) {
};
}
// DB-first: check for requirements in DB; fall back to file for unmigrated projects
const hasDbRequirements = isDbAvailable() && getActiveRequirements().length > 0;
const hasDbRequirements =
isDbAvailable() && getActiveRequirements().length > 0;
if (!hasDbRequirements) {
const requirementsPath = join(root, "REQUIREMENTS.md");
if (!existsSync(requirementsPath)) {
return {
status: "pending",
stage: "requirements",
reason: "No requirements found (DB empty and .sf/REQUIREMENTS.md is missing).",
reason:
"No requirements found (DB empty and .sf/REQUIREMENTS.md is missing).",
};
}
if (!validateArtifact(requirementsPath, "requirements").ok) {

View file

@ -310,8 +310,7 @@ export function detectV1Planning(basePath) {
function detectV2Sf(basePath) {
const sfPath = sfRoot(basePath);
if (!existsSync(sfPath)) return null;
const hasPreferences =
existsSync(join(sfPath, "preferences.yaml"));
const hasPreferences = existsSync(join(sfPath, "preferences.yaml"));
const hasContext = existsSync(join(sfPath, "CONTEXT.md"));
let milestoneCount = 0;
const milestonesPath = join(sfPath, "milestones");

View file

@ -5,9 +5,10 @@
* and dispatch logic. This is the "dev" engine it wraps the current SF
* autonomous mode behavior behind the engine-polymorphic interface.
*/
import { resolveDispatch } from "./uok/auto-dispatch.js";
import { loadEffectiveSFPreferences } from "./preferences.js";
import { deriveState } from "./state.js";
import { resolveDispatch } from "./uok/auto-dispatch.js";
// ─── Bridge: DispatchAction → EngineDispatchAction ────────────────────────
/**
* Map a SF-specific DispatchAction (which carries `matchedRule`, `unitType`,

View file

@ -14,7 +14,10 @@ import { existsSync } from "node:fs";
import { AuthStorage } from "@singularity-forge/coding-agent";
import { getAuthPath, PROVIDER_REGISTRY } from "./key-manager.js";
import { loadEffectiveSFPreferences } from "./preferences.js";
import { getConfiguredEnvApiKey, getGatedEnvVarKey } from "./provider-env-auth.js";
import {
getConfiguredEnvApiKey,
getGatedEnvVarKey,
} from "./provider-env-auth.js";
import { couldBeVaultUri } from "./vault-credential-resolver.js";
// ── Model → Provider ID mapping ───────────────────────────────────────────────
@ -336,7 +339,9 @@ function checkOptionalProviders() {
// "not configured" noise for alternative search providers when at least
// one is already active (e.g. don't warn about missing BRAVE_API_KEY
// when Tavily is configured).
const searchProviderIds = PROVIDER_REGISTRY.filter((p) => p.category === "search").map((p) => p.id);
const searchProviderIds = PROVIDER_REGISTRY.filter(
(p) => p.category === "search",
).map((p) => p.id);
const hasAnySearchProvider = searchProviderIds.some(
(id) => resolveKey(id).found,
);

View file

@ -19,7 +19,10 @@
*
* Design: Linear-inspired restrained palette, geometric status, no emoji.
*/
import { formatDateShort, formatDuration } from "@singularity-forge/coding-agent";
import {
formatDateShort,
formatDuration,
} from "@singularity-forge/coding-agent";
import { formatCost, formatTokenCount } from "./metrics.js";
export function generateHtmlReport(data, opts) {
const generated = new Date().toISOString();

View file

@ -64,7 +64,6 @@
"backlog",
"capture",
"chronicle",
"clear",
"cleanup",
"cmux",
"codebase",
@ -204,11 +203,6 @@
"turn_start",
"turn_end"
],
"shortcuts": [
"Ctrl+Alt+G",
"Ctrl+Alt+H",
"Ctrl+Alt+M",
"Ctrl+Shift+H"
]
"shortcuts": ["Ctrl+Alt+G", "Ctrl+Alt+H", "Ctrl+Alt+M", "Ctrl+Shift+H"]
}
}

View file

@ -70,7 +70,12 @@ import {
isSessionLockProcessAlive,
readSessionLockData,
} from "./session-lock.js";
import { getAllMilestones, getMilestone, getMilestoneSlices, isDbAvailable } from "./sf-db.js";
import {
getAllMilestones,
getMilestone,
getMilestoneSlices,
isDbAvailable,
} from "./sf-db.js";
import { deriveState } from "./state.js";
import { resolveUokFlags } from "./uok/flags.js";
import { UokGateRunner } from "./uok/gate-runner.js";
@ -271,7 +276,7 @@ export function checkAutoStartAfterDiscuss() {
if (isDbAvailable()) {
const milestone = getMilestone(milestoneId);
const dbSlices = getMilestoneSlices(milestoneId);
const hasContext = !!(milestone?.vision);
const hasContext = !!milestone?.vision;
const hasRoadmap = dbSlices.length > 0;
if (!hasContext && !hasRoadmap) return false;
// Gate 3: Multi-milestone completeness warning (DB version)

View file

@ -44,7 +44,9 @@ export default async function registerExtension(pi) {
// Register SF-owned subagent tools after core bootstrap so model, preference,
// inheritance, and retrieval-evidence integrations share one extension owner.
try {
const { default: registerSFSubagents } = await import("./subagent/index.js");
const { default: registerSFSubagents } = await import(
"./subagent/index.js"
);
registerSFSubagents(pi);
} catch (err) {
const { logWarning } = await import("./workflow-logger.js");
@ -82,7 +84,9 @@ export default async function registerExtension(pi) {
// Register SF notifications (completion beep/say/focus/threshold commands).
try {
const { default: registerSFNotify } = await import("./notifications/notify.js");
const { default: registerSFNotify } = await import(
"./notifications/notify.js"
);
registerSFNotify(pi);
} catch (err) {
const { logWarning } = await import("./workflow-logger.js");
@ -94,7 +98,9 @@ export default async function registerExtension(pi) {
// Register SF in-turn guard (duplicate tool-call loop detection).
try {
const { default: registerSFInturnGuard } = await import("./guards/inturn.js");
const { default: registerSFInturnGuard } = await import(
"./guards/inturn.js"
);
registerSFInturnGuard(pi);
} catch (err) {
const { logWarning } = await import("./workflow-logger.js");
@ -106,7 +112,9 @@ export default async function registerExtension(pi) {
// Register SF permissions (layered permission enforcement).
try {
const { default: registerSFPermissions } = await import("./permissions/index.js");
const { default: registerSFPermissions } = await import(
"./permissions/index.js"
);
registerSFPermissions(pi);
} catch (err) {
const { logWarning } = await import("./workflow-logger.js");
@ -118,7 +126,9 @@ export default async function registerExtension(pi) {
// Register SF legacy slash commands (/audit, /clear, /create-extension, /create-slash-command).
try {
const { default: registerSFLegacyCommands } = await import("./commands/legacy/index.js");
const { default: registerSFLegacyCommands } = await import(
"./commands/legacy/index.js"
);
registerSFLegacyCommands(pi);
} catch (err) {
const { logWarning } = await import("./workflow-logger.js");

View file

@ -519,7 +519,9 @@ function bootstrapSfDirectory(basePath, prefs, signals) {
ensureSiftIndexWarmup(basePath);
}
function buildPreferencesFile(prefs) {
const lines = ["# SF preferences — see ~/.sf/agent/extensions/sf/docs/preferences-reference.md for docs"];
const lines = [
"# SF preferences — see ~/.sf/agent/extensions/sf/docs/preferences-reference.md for docs",
];
lines.push("version: 1");
lines.push(`mode: ${prefs.mode}`);
// Git preferences

View file

@ -292,7 +292,9 @@ export function getAllKeyStatuses(auth) {
const rawCreds = auth.getCredentialsForProvider(provider.id);
// Filter out empty-key entries (left by legacy removeProviderToken or skipped onboarding)
const creds = rawCreds.filter((c) => !(c.type === "api_key" && !c.key));
const envKey = isEnvAuthAllowed(provider.id) ? getProviderEnvKey(provider) : undefined;
const envKey = isEnvAuthAllowed(provider.id)
? getProviderEnvKey(provider)
: undefined;
if (creds.length > 0) {
const firstCred = creds[0];
const desc =

View file

@ -12,7 +12,7 @@
*/
import { randomUUID } from "node:crypto";
import { readJudgmentLog } from "./judgment-log.js";
import { insertMemoryRow, isDbAvailable, getActiveMemories } from "./sf-db.js";
import { getActiveMemories, insertMemoryRow, isDbAvailable } from "./sf-db.js";
/** Map judgment-log string confidence to a numeric score for the REAL column. */
function confidenceScore(s) {

View file

@ -168,7 +168,11 @@ export function computeObservedScore(
const sampleCount = stats.sample_count ?? 0;
const hardFailureCount = stats.hard_failure_count ?? 0;
if (hardFailureCount > 0 && sampleCount > 0 && hardFailureCount / sampleCount > 0.5) {
if (
hardFailureCount > 0 &&
sampleCount > 0 &&
hardFailureCount / sampleCount > 0.5
) {
scaled *= 0.5;
}

View file

@ -9,6 +9,7 @@
import assert from "node:assert/strict";
import { test } from "vitest";
import { computeObservedScore } from "./bayesian-blender.mjs";
import {
aggregateAllForUnitType,
aggregateOutcomes,
@ -20,7 +21,6 @@ import {
recordOutcomeBatch,
validateOutcome,
} from "./outcome-recorder.mjs";
import { computeObservedScore } from "./bayesian-blender.mjs";
// ---------------------------------------------------------------------------
// Minimal in-memory fake of the SQLite surface consumed by sf-learning.
@ -214,7 +214,8 @@ function summarize(rows) {
rows.reduce((sum, r) => sum + effectiveWeight(r), 0) / rows.length;
const hard_failure_count = rows.filter(
(r) => r.failure_mode === "quota_exhausted" || r.failure_mode === "auth_error",
(r) =>
r.failure_mode === "quota_exhausted" || r.failure_mode === "auth_error",
).length;
return {
@ -616,17 +617,88 @@ test("aggregateOutcomes_rate_limit_failures_rank_higher_than_quota_exhausted", (
// model with only rate_limit failures (weight 0.7)
dbRateLimit._rows.push(
{ id: 1, model_id: "model-x", provider: "p", unit_type: "execute-task", unit_id: "T01", succeeded: 0, failure_mode: "rate_limit", retries: 0, escalated: 0, verification_passed: null, blocker_discovered: 0, duration_ms: 100, tokens_total: 10, cost_usd: 0, recorded_at: now - 1000 },
{ id: 2, model_id: "model-x", provider: "p", unit_type: "execute-task", unit_id: "T02", succeeded: 0, failure_mode: "rate_limit", retries: 0, escalated: 0, verification_passed: null, blocker_discovered: 0, duration_ms: 100, tokens_total: 10, cost_usd: 0, recorded_at: now - 2000 },
{
id: 1,
model_id: "model-x",
provider: "p",
unit_type: "execute-task",
unit_id: "T01",
succeeded: 0,
failure_mode: "rate_limit",
retries: 0,
escalated: 0,
verification_passed: null,
blocker_discovered: 0,
duration_ms: 100,
tokens_total: 10,
cost_usd: 0,
recorded_at: now - 1000,
},
{
id: 2,
model_id: "model-x",
provider: "p",
unit_type: "execute-task",
unit_id: "T02",
succeeded: 0,
failure_mode: "rate_limit",
retries: 0,
escalated: 0,
verification_passed: null,
blocker_discovered: 0,
duration_ms: 100,
tokens_total: 10,
cost_usd: 0,
recorded_at: now - 2000,
},
);
// model with only quota_exhausted failures (weight 0.2)
dbQuota._rows.push(
{ id: 1, model_id: "model-x", provider: "p", unit_type: "execute-task", unit_id: "T01", succeeded: 0, failure_mode: "quota_exhausted", retries: 0, escalated: 0, verification_passed: null, blocker_discovered: 0, duration_ms: 100, tokens_total: 10, cost_usd: 0, recorded_at: now - 1000 },
{ id: 2, model_id: "model-x", provider: "p", unit_type: "execute-task", unit_id: "T02", succeeded: 0, failure_mode: "quota_exhausted", retries: 0, escalated: 0, verification_passed: null, blocker_discovered: 0, duration_ms: 100, tokens_total: 10, cost_usd: 0, recorded_at: now - 2000 },
{
id: 1,
model_id: "model-x",
provider: "p",
unit_type: "execute-task",
unit_id: "T01",
succeeded: 0,
failure_mode: "quota_exhausted",
retries: 0,
escalated: 0,
verification_passed: null,
blocker_discovered: 0,
duration_ms: 100,
tokens_total: 10,
cost_usd: 0,
recorded_at: now - 1000,
},
{
id: 2,
model_id: "model-x",
provider: "p",
unit_type: "execute-task",
unit_id: "T02",
succeeded: 0,
failure_mode: "quota_exhausted",
retries: 0,
escalated: 0,
verification_passed: null,
blocker_discovered: 0,
duration_ms: 100,
tokens_total: 10,
cost_usd: 0,
recorded_at: now - 2000,
},
);
const statsRateLimit = aggregateOutcomes(dbRateLimit, "model-x", "execute-task", { now });
const statsQuota = aggregateOutcomes(dbQuota, "model-x", "execute-task", { now });
const statsRateLimit = aggregateOutcomes(
dbRateLimit,
"model-x",
"execute-task",
{ now },
);
const statsQuota = aggregateOutcomes(dbQuota, "model-x", "execute-task", {
now,
});
assert.ok(
statsRateLimit.effective_success_rate > statsQuota.effective_success_rate,

View file

@ -11,12 +11,12 @@
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { sfRuntimeRoot } from "./paths.js";
import {
DISCOVERABLE_PROVIDER_IDS,
getProviderCatalogConfig,
getProviderModelExcludePatterns,
} from "./provider-catalog-config.js";
import { sfRuntimeRoot } from "./paths.js";
const CATALOG_TTL_MS = 6 * 60 * 60 * 1000;
@ -37,7 +37,8 @@ export function readCachedModelIds(basePath, providerId) {
if (!existsSync(path)) return null;
const entry = JSON.parse(readFileSync(path, "utf-8"));
if (!entry?.fetchedAt || !Array.isArray(entry.modelIds)) return null;
if (Date.now() - new Date(entry.fetchedAt).getTime() > CATALOG_TTL_MS) return null;
if (Date.now() - new Date(entry.fetchedAt).getTime() > CATALOG_TTL_MS)
return null;
return entry.modelIds;
} catch {
return null;
@ -85,10 +86,16 @@ function parseModelIds(cfg, json) {
// Google: { models: [{name: "models/gemini-..."}] }
items = Array.isArray(json?.models) ? json.models : null;
if (!items) return null;
return items.map((m) => (m.name ?? "").replace(/^models\//, "")).filter(Boolean);
return items
.map((m) => (m.name ?? "").replace(/^models\//, ""))
.filter(Boolean);
}
// OpenAI-compatible: { data: [{id}] } or { models: [{id}] }
items = Array.isArray(json?.data) ? json.data : Array.isArray(json?.models) ? json.models : null;
items = Array.isArray(json?.data)
? json.data
: Array.isArray(json?.models)
? json.models
: null;
if (!items) return null;
return items.map((m) => m.id ?? m.name).filter(Boolean);
}

View file

@ -9,6 +9,8 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { getModels, getProviders } from "@singularity-forge/ai";
import { selectByBenchmarks } from "./benchmark-selector.js";
import { defaultRoutingConfig, MODEL_CAPABILITY_TIER } from "./model-router.js";
import {
DEFAULT_RUNAWAY_CHANGED_FILES_WARNING,
DEFAULT_RUNAWAY_DIAGNOSTIC_TURNS,
@ -16,8 +18,6 @@ import {
DEFAULT_RUNAWAY_TOKEN_WARNING,
DEFAULT_RUNAWAY_TOOL_CALL_WARNING,
} from "./uok/auto-runaway-guard.js";
import { selectByBenchmarks } from "./benchmark-selector.js";
import { defaultRoutingConfig, MODEL_CAPABILITY_TIER } from "./model-router.js";
// ─── Lazy loader — breaks the preferences.js ↔ preferences-models.js cycle ──
// preferences.js imports resolveProfileDefaults from here, and needs
@ -280,7 +280,9 @@ function resolveAutoBenchmarkPickForUnit(unitType, prefs) {
auth.getConfiguredProviders?.() ??
getProviders().filter((p) => {
const creds = auth.getCredentialsForProvider(p);
return creds.some((c) => c.type === "oauth" || (c.type === "api_key" && c.key));
return creds.some(
(c) => c.type === "oauth" || (c.type === "api_key" && c.key),
);
}),
);
const candidates = [];
@ -873,9 +875,13 @@ export function resolveSearchProviderFromPreferences() {
export function isHeavyModelId(modelId) {
if (!modelId || typeof modelId !== "string") return false;
// Strip provider prefix (e.g. "anthropic/claude-opus-4-6" → "claude-opus-4-6")
const bare = modelId.includes("/") ? modelId.split("/").slice(1).join("/") : modelId;
const bare = modelId.includes("/")
? modelId.split("/").slice(1).join("/")
: modelId;
const tier = MODEL_CAPABILITY_TIER[bare] ?? MODEL_CAPABILITY_TIER[modelId];
if (tier !== undefined) return tier === "heavy";
// Fallback regex for models not yet in the tier map
return /\bopus\b|\bo1\b|\bo3\b|\bgpt-4-turbo\b|\bgpt-5\b|\bdeepseek-reasoner\b/i.test(modelId);
return /\bopus\b|\bo1\b|\bo3\b|\bgpt-4-turbo\b|\bgpt-5\b|\bdeepseek-reasoner\b/i.test(
modelId,
);
}

View file

@ -12,8 +12,8 @@
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { parse as parseYaml } from "yaml";
import { normalizeStringArray } from "@singularity-forge/coding-agent";
import { parse as parseYaml } from "yaml";
import { sfRoot } from "./paths.js";
import {
_initPrefsLoader,

View file

@ -169,5 +169,7 @@ export function isProviderScopedRateLimit(providerId) {
* Returns the exclude patterns for a provider's model list, or [].
*/
export function getProviderModelExcludePatterns(providerId) {
return PROVIDER_CATALOG_CONFIG[providerId]?.modelFilter?.excludePatterns ?? [];
return (
PROVIDER_CATALOG_CONFIG[providerId]?.modelFilter?.excludePatterns ?? []
);
}

View file

@ -14,8 +14,8 @@ import {
writeFileSync,
} from "node:fs";
import { basename, join } from "node:path";
import { logWarning } from "./workflow-logger.js";
import { addBacklogItem, isDbAvailable } from "./sf-db.js";
import { logWarning } from "./workflow-logger.js";
// ─── Frontmatter Parser ──────────────────────────────────────────────────────
/**
* Parse the YAML frontmatter from a markdown file.
@ -268,7 +268,12 @@ export function promoteActionableRecords(basePath) {
// Write to DB backlog (primary)
if (isDbAvailable()) {
try {
addBacklogItem({ id: milestoneId, title: milestoneId, source: "record-promoter", status: "promoted" });
addBacklogItem({
id: milestoneId,
title: milestoneId,
source: "record-promoter",
status: "promoted",
});
} catch {
// non-fatal
}

View file

@ -15,7 +15,10 @@
*/
import { existsSync, mkdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { formatDateShort, formatDuration } from "@singularity-forge/coding-agent";
import {
formatDateShort,
formatDuration,
} from "@singularity-forge/coding-agent";
import { atomicWriteSync } from "./atomic-write.js";
import { formatCost, formatTokenCount } from "./metrics.js";
import { sfRoot } from "./paths.js";

View file

@ -14,7 +14,11 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { markResolved, readAllSelfFeedback } from "./self-feedback.js";
import { getActiveRequirements, isDbAvailable, upsertRequirement } from "./sf-db.js";
import {
getActiveRequirements,
isDbAvailable,
upsertRequirement,
} from "./sf-db.js";
// ─── Constants ───────────────────────────────────────────────────────────────
const COUNT_THRESHOLD = 5;

View file

@ -120,7 +120,13 @@ export function getLlmProviderIds() {
}
// Key-free and meta search provider IDs that are valid as preferences but have
// no PROVIDER_REGISTRY entry (no env-var key required).
const KEYLESS_SEARCH_PROVIDER_IDS = ["minimax", "ollama", "combosearch", "native", "auto"];
const KEYLESS_SEARCH_PROVIDER_IDS = [
"minimax",
"ollama",
"combosearch",
"native",
"auto",
];
/**
* All valid values for the `search_provider` preference.
* Derived from PROVIDER_REGISTRY (key-backed) plus keyless/meta providers.

View file

@ -39,8 +39,8 @@ import {
taskFrontmatterFromRecord,
withTaskFrontmatter,
} from "./task-frontmatter.js";
import { logError, logWarning } from "./workflow-logger.js";
import { readTraceEvents } from "./uok/trace-writer.js";
import { logError, logWarning } from "./workflow-logger.js";
let loadAttempted = false;
function loadProvider() {
@ -1443,9 +1443,7 @@ function columnExists(db, table, column) {
}
function tableExists(db, table) {
const row = db
.prepare(
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
)
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`)
.get(table);
return row != null;
}
@ -3223,7 +3221,9 @@ function migrateSchema(db) {
"failure_mode",
"ALTER TABLE llm_task_outcomes ADD COLUMN failure_mode TEXT DEFAULT NULL",
);
db.exec("CREATE INDEX IF NOT EXISTS idx_llm_task_outcomes_failure_mode ON llm_task_outcomes(model_id, failure_mode, recorded_at DESC)");
db.exec(
"CREATE INDEX IF NOT EXISTS idx_llm_task_outcomes_failure_mode ON llm_task_outcomes(model_id, failure_mode, recorded_at DESC)",
);
db.prepare(
"INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)",
).run({
@ -4242,8 +4242,13 @@ function hasTaskSpecIntent(planning = {}) {
}
function insertTaskSpecIfAbsent(milestoneId, sliceId, taskId, planning = {}) {
if (!hasTaskSpecIntent(planning)) return;
const { normalized: frontmatter, errors } = taskFrontmatterFromRecord(planning);
if (errors?.length) logWarning("sf-db:insertTaskSpec", `frontmatter validation errors for ${milestoneId}/${sliceId}/${taskId}: ${errors.join(", ")}`);
const { normalized: frontmatter, errors } =
taskFrontmatterFromRecord(planning);
if (errors?.length)
logWarning(
"sf-db:insertTaskSpec",
`frontmatter validation errors for ${milestoneId}/${sliceId}/${taskId}: ${errors.join(", ")}`,
);
currentDb
.prepare(`INSERT OR IGNORE INTO task_specs (
milestone_id, slice_id, task_id, verify, inputs, expected_output,
@ -4464,8 +4469,13 @@ export function setTaskBlockerDiscovered(
export function upsertTaskPlanning(milestoneId, sliceId, taskId, planning) {
if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
insertTaskSpecIfAbsent(milestoneId, sliceId, taskId, planning);
const { normalized: frontmatter, errors: fmErrors } = taskFrontmatterFromRecord(planning);
if (fmErrors?.length) logWarning("sf-db:upsertTaskPlanning", `frontmatter validation errors for ${milestoneId}/${sliceId}/${taskId}: ${fmErrors.join(", ")}`);
const { normalized: frontmatter, errors: fmErrors } =
taskFrontmatterFromRecord(planning);
if (fmErrors?.length)
logWarning(
"sf-db:upsertTaskPlanning",
`frontmatter validation errors for ${milestoneId}/${sliceId}/${taskId}: ${fmErrors.join(", ")}`,
);
const hasTaskStatus =
planning.taskStatus !== undefined ||
planning.task_status !== undefined ||
@ -6035,7 +6045,9 @@ export function recordUokRunExit(entry) {
":status": entry.status ?? "ok",
":started_at": entry.startedAt ?? now,
":ended_at": now,
":error": entry.error ? capErrorForStorage(entry.error, entry.runId) : null,
":error": entry.error
? capErrorForStorage(entry.error, entry.runId)
: null,
":flags_json": JSON.stringify(entry.flags ?? {}),
":updated_at": now,
});
@ -6425,11 +6437,13 @@ export function getLlmTaskOutcomeStats(modelId, windowHours = 24) {
*/
export function getGateRunStats(gateId, windowHours = 24) {
try {
const basePath = currentPath && currentPath !== ":memory:"
const basePath =
currentPath && currentPath !== ":memory:"
? dirname(dirname(currentPath))
: process.cwd();
const events = readTraceEvents(basePath, "gate_run", windowHours)
.filter((e) => e.gateId === gateId);
const events = readTraceEvents(basePath, "gate_run", windowHours).filter(
(e) => e.gateId === gateId,
);
const stats = {
total: events.length,
pass: 0,
@ -6557,18 +6571,31 @@ export function updateGateCircuitBreaker(gateId, updates) {
}
export function getGateLatencyStats(gateId, windowHours = 24) {
try {
const basePath = currentPath && currentPath !== ":memory:"
const basePath =
currentPath && currentPath !== ":memory:"
? dirname(dirname(currentPath))
: process.cwd();
const durations = readTraceEvents(basePath, "gate_run", windowHours)
.filter((e) => e.gateId === gateId && typeof e.durationMs === "number")
.map((e) => e.durationMs)
.sort((a, b) => a - b);
if (durations.length === 0) return { p50: null, p95: null, count: 0, total: 0, avgMs: 0, p50Ms: 0, p95Ms: 0, maxMs: 0 };
if (durations.length === 0)
return {
p50: null,
p95: null,
count: 0,
total: 0,
avgMs: 0,
p50Ms: 0,
p95Ms: 0,
maxMs: 0,
};
const p50Ms = durations[Math.floor(durations.length * 0.5)] ?? 0;
const p95Ms = durations[Math.floor(durations.length * 0.95)] ?? 0;
const maxMs = durations[durations.length - 1] ?? 0;
const avgMs = Math.round(durations.reduce((s, v) => s + v, 0) / durations.length);
const avgMs = Math.round(
durations.reduce((s, v) => s + v, 0) / durations.length,
);
return {
p50: p50Ms,
p95: p95Ms,
@ -6580,12 +6607,22 @@ export function getGateLatencyStats(gateId, windowHours = 24) {
maxMs,
};
} catch {
return { p50: null, p95: null, count: 0, total: 0, avgMs: 0, p50Ms: 0, p95Ms: 0, maxMs: 0 };
return {
p50: null,
p95: null,
count: 0,
total: 0,
avgMs: 0,
p50Ms: 0,
p95Ms: 0,
maxMs: 0,
};
}
}
export function getDistinctGateIds() {
try {
const basePath = currentPath && currentPath !== ":memory:"
const basePath =
currentPath && currentPath !== ":memory:"
? dirname(dirname(currentPath))
: process.cwd();
const events = readTraceEvents(basePath, "gate_run", 24 * 30); // 30 days
@ -7817,15 +7854,29 @@ export function bulkInsertLegacyHierarchy(payload) {
export function getActiveMemories({ category, limit = 200 } = {}) {
if (!currentDb) return [];
const rows = category
? currentDb.prepare("SELECT * FROM active_memories WHERE category = ? ORDER BY updated_at DESC LIMIT ?").all(category, limit)
: currentDb.prepare("SELECT * FROM active_memories ORDER BY updated_at DESC LIMIT ?").all(limit);
? currentDb
.prepare(
"SELECT * FROM active_memories WHERE category = ? ORDER BY updated_at DESC LIMIT ?",
)
.all(category, limit)
: currentDb
.prepare(
"SELECT * FROM active_memories ORDER BY updated_at DESC LIMIT ?",
)
.all(limit);
return rows.map((r) => ({
id: r["id"],
category: r["category"],
content: r["content"],
confidence: r["confidence"],
sourceUnitId: r["source_unit_id"],
tags: (() => { try { return JSON.parse(r["tags"] ?? "[]"); } catch { return []; } })(),
tags: (() => {
try {
return JSON.parse(r["tags"] ?? "[]");
} catch {
return [];
}
})(),
createdAt: r["created_at"],
updatedAt: r["updated_at"],
}));

View file

@ -15,7 +15,10 @@ import {
resolveRunControlMode,
resolveWorkMode,
} from "./operating-model.js";
import { isHeavyModelId, isProviderAllowedByLists } from "./preferences-models.js";
import {
isHeavyModelId,
isProviderAllowedByLists,
} from "./preferences-models.js";
import { logWarning } from "./workflow-logger.js";
function providerFromModelId(modelId) {
@ -24,7 +27,6 @@ function providerFromModelId(modelId) {
return provider && provider !== modelId ? provider : null;
}
/**
* Build an inheritance envelope from the current parent session.
*

View file

@ -74,7 +74,8 @@ export class SubagentBackgroundJobManager {
const job = this.jobs.get(id);
if (!job) return "not_found";
if (job.status === "running") return "already_running";
if (this.getRunningJobs().length >= this.maxRunning) return "concurrency_limit";
if (this.getRunningJobs().length >= this.maxRunning)
return "concurrency_limit";
// Cancel pending eviction — job is active again
const evTimer = this.evictionTimers.get(id);
if (evTimer) {
@ -99,7 +100,12 @@ export class SubagentBackgroundJobManager {
appendTurn(id, role, content) {
const job = this.jobs.get(id);
if (!job) return;
job.turns.push({ turnIndex: job.turns.length, role, content, timestamp: Date.now() });
job.turns.push({
turnIndex: job.turns.length,
role,
content,
timestamp: Date.now(),
});
}
/** Return all turns for a job, optionally filtered to those after `since` (exclusive). */
getTurns(id, since = 0) {

View file

@ -22,6 +22,8 @@ import { StringEnum } from "@singularity-forge/ai";
import { getMarkdownTheme } from "@singularity-forge/coding-agent";
import { Container, Markdown, Spacer, Text } from "@singularity-forge/tui";
import { CmuxClient, shellEscape } from "../../cmux/index.js";
import { formatTokenCount } from "../../shared/mod.js";
import { getCurrentPhase } from "../../shared/sf-phase-state.js";
import {
buildSiftEnv,
ensureSiftRuntimeDirs,
@ -35,8 +37,6 @@ import {
buildSubagentInheritanceEnvelope,
validateSubagentDispatch,
} from "../subagent-inheritance.js";
import { formatTokenCount } from "../../shared/mod.js";
import { getCurrentPhase } from "../../shared/sf-phase-state.js";
import { discoverAgents } from "./agents.js";
import { SubagentBackgroundJobManager } from "./background-jobs.js";
import {
@ -1779,7 +1779,9 @@ export default function (pi) {
const manager = getBackgroundJobs();
// Build a rerun factory for write_subagent multi-turn follow-ups.
// Only single-mode dispatches (params.agent + params.task) support write_subagent.
const isSingleMode = Boolean(params.agent && params.task && !params.tasks && !params.chain);
const isSingleMode = Boolean(
params.agent && params.task && !params.tasks && !params.chain,
);
const dispatchContext = isSingleMode
? {
originalTask: params.task,
@ -1831,7 +1833,11 @@ export default function (pi) {
}
return result;
});
jobId = manager.register(summarizeBackgroundInvocation(params), wrappedRun, dispatchContext);
jobId = manager.register(
summarizeBackgroundInvocation(params),
wrappedRun,
dispatchContext,
);
return {
content: [
{
@ -2738,7 +2744,8 @@ export default function (pi) {
description: "Background subagent job ID (for example sub_a1b2c3d4)",
}),
message: Type.String({
description: "Follow-up message or instruction to send to the subagent.",
description:
"Follow-up message or instruction to send to the subagent.",
}),
}),
async execute(_toolCallId, params) {
@ -2746,7 +2753,12 @@ export default function (pi) {
const job = manager.getJob(params.job_id);
if (!job) {
return {
content: [{ type: "text", text: `Background subagent job not found: ${params.job_id}` }],
content: [
{
type: "text",
text: `Background subagent job not found: ${params.job_id}`,
},
],
details: undefined,
isError: true,
};
@ -2784,14 +2796,18 @@ export default function (pi) {
let enrichedTask = job.dispatchContext.originalTask;
if (historyTurns.length > 0) {
const historyText = historyTurns
.map((t) => `[${t.role === "agent" ? "Agent" : "User"} — turn ${t.turnIndex}]\n${t.content}`)
.map(
(t) =>
`[${t.role === "agent" ? "Agent" : "User"} — turn ${t.turnIndex}]\n${t.content}`,
)
.join("\n\n");
enrichedTask += `\n\n---\nConversation history:\n${historyText}\n---`;
}
enrichedTask += `\n\nUser follow-up: ${params.message}`;
const resumeResult = manager.resume(
params.job_id,
(signal) => job.dispatchContext.rerunWithTask(enrichedTask, signal).then((result) => {
const resumeResult = manager.resume(params.job_id, (signal) =>
job.dispatchContext
.rerunWithTask(enrichedTask, signal)
.then((result) => {
// Append the new agent response as a turn before the notification fires
const text = getPrimaryTextContent(result);
if (text) manager.appendTurn(params.job_id, "agent", text);
@ -2805,7 +2821,12 @@ export default function (pi) {
already_running: `Background subagent ${params.job_id} is already running.`,
};
return {
content: [{ type: "text", text: messages[resumeResult] ?? `Resume failed: ${resumeResult}` }],
content: [
{
type: "text",
text: messages[resumeResult] ?? `Resume failed: ${resumeResult}`,
},
],
details: undefined,
isError: true,
};
@ -2847,7 +2868,12 @@ export default function (pi) {
const job = manager.getJob(params.job_id);
if (!job) {
return {
content: [{ type: "text", text: `Background subagent job not found: ${params.job_id}` }],
content: [
{
type: "text",
text: `Background subagent job not found: ${params.job_id}`,
},
],
details: undefined,
isError: true,
};
@ -2855,7 +2881,8 @@ export default function (pi) {
const since = params.since_turn ?? 0;
const turns = manager.getTurns(params.job_id, since);
if (!turns || turns.length === 0) {
const status = job.status === "running" ? " (still running)" : ` (${job.status})`;
const status =
job.status === "running" ? " (still running)" : ` (${job.status})`;
return {
content: [
{
@ -2872,7 +2899,8 @@ export default function (pi) {
return `**${label} [turn ${t.turnIndex}]**\n${t.content}`;
})
.join("\n\n---\n\n");
const statusLine = job.status === "running" ? " *(running)*" : ` *(${job.status})*`;
const statusLine =
job.status === "running" ? " *(running)*" : ` *(${job.status})*`;
return {
content: [
{

View file

@ -201,7 +201,9 @@ function escapeRegExpLocal(value) {
}
function extractMarkdownSectionLocal(content, heading) {
const match = new RegExp(`^## ${escapeRegExpLocal(heading)}\\s*$`, "m").exec(content);
const match = new RegExp(`^## ${escapeRegExpLocal(heading)}\\s*$`, "m").exec(
content,
);
if (!match) return null;
const start = match.index + match[0].length;
const rest = content.slice(start);
@ -228,11 +230,20 @@ export function extractSliceExecutionExcerpt(content, relPath) {
const goalLine = lines.find((line) => line.startsWith("**Goal:**"))?.trim();
const demoLine = lines.find((line) => line.startsWith("**Demo:**"))?.trim();
const verification = extractMarkdownSectionLocal(content, "Verification");
const observability = extractMarkdownSectionLocal(content, "Observability / Diagnostics");
const observability = extractMarkdownSectionLocal(
content,
"Observability / Diagnostics",
);
const parts = ["## Slice Plan Excerpt", `Source: \`${relPath}\``];
if (goalLine) parts.push(goalLine);
if (demoLine) parts.push(demoLine);
if (verification) parts.push("", "### Slice Verification", verification.trim());
if (observability) parts.push("", "### Slice Observability / Diagnostics", observability.trim());
if (verification)
parts.push("", "### Slice Verification", verification.trim());
if (observability)
parts.push(
"",
"### Slice Observability / Diagnostics",
observability.trim(),
);
return parts.join("\n");
}

View file

@ -41,13 +41,13 @@ vi.mock("../auto-prompts.js", () => ({
checkNeedsRunUat: vi.fn(async () => null),
}));
import { resolveDispatch } from "../uok/auto-dispatch.js";
import {
closeDatabase,
insertMilestone,
insertSlice,
openDatabase,
} from "../sf-db.js";
import { resolveDispatch } from "../uok/auto-dispatch.js";
function makeTempDir(prefix) {
return mkdtempSync(join(tmpdir(), prefix));

View file

@ -92,12 +92,9 @@ describe("doctor provider checks", () => {
test("runProviderChecks_when_google_env_auth_is_default_off_treats_google_as_missing_required_route", () => {
makePreferencesProject(
[
"version: 1",
"models:",
" planning: google/gemini-2.5-pro",
"",
].join("\n"),
["version: 1", "models:", " planning: google/gemini-2.5-pro", ""].join(
"\n",
),
);
process.env.GEMINI_API_KEY = "test-google-key";
@ -109,12 +106,9 @@ describe("doctor provider checks", () => {
test("runProviderChecks_when_google_env_auth_is_enabled_accepts_google_env_key", () => {
const project = makePreferencesProject(
[
"version: 1",
"models:",
" planning: google/gemini-2.5-pro",
"",
].join("\n"),
["version: 1", "models:", " planning: google/gemini-2.5-pro", ""].join(
"\n",
),
);
mkdirSync(join(project, ".sf"), { recursive: true });
writeFileSync(

View file

@ -43,7 +43,11 @@ test("loadGatewayConfigFromEnv returns null when auth.json does not exist", () =
});
test("loadGatewayConfigFromEnv reads key and url from auth.json", () => {
writeAuthJson({ key: "auth-key", url: "https://example.test/v1", type: "api_key" });
writeAuthJson({
key: "auth-key",
url: "https://example.test/v1",
type: "api_key",
});
const cfg = loadGatewayConfigFromEnv();
assert.equal(cfg.apiKey, "auth-key");
@ -71,4 +75,3 @@ test("loadGatewayConfigFromEnv respects model env var overrides", () => {
assert.equal(cfg.embeddingModel, "custom-embed");
assert.equal(cfg.rerankModel, "custom-rerank");
});

View file

@ -46,13 +46,9 @@ function makePreferencesProject(projectPreferences, projectSettings) {
describe("preferences model resolution", () => {
test("resolveModelWithFallbacksForUnit_when_google_env_auth_is_default_off_skips_google_auto_benchmark_candidates", () => {
makePreferencesProject(
[
"version: 1",
"allowed_providers:",
" - google",
"models: {}",
"",
].join("\n"),
["version: 1", "allowed_providers:", " - google", "models: {}", ""].join(
"\n",
),
);
process.env.GEMINI_API_KEY = "test-google-key";
@ -60,5 +56,4 @@ describe("preferences model resolution", () => {
assert.equal(result, undefined);
});
});

View file

@ -72,7 +72,10 @@ describe("parse stage", () => {
});
assert.strictEqual(result.steering, true);
assert.strictEqual(result.directives.length, 1);
assert.deepStrictEqual(result.directives[0], { cmd: "mode", value: "build" });
assert.deepStrictEqual(result.directives[0], {
cmd: "mode",
value: "build",
});
});
test("parseRemoteSteeringDirectives_when_text_has_all_axes_returns_all_four_directives", () => {
@ -131,7 +134,10 @@ describe("apply stage", () => {
test("applyRemoteSteeringDirectives_when_control_directive_applies_changes_runControl", () => {
const src = uniqueSrc();
applyRemoteSteeringDirectives([{ cmd: "control", value: "autonomous" }], src);
applyRemoteSteeringDirectives(
[{ cmd: "control", value: "autonomous" }],
src,
);
assert.strictEqual(_modeState.runControl, "autonomous");
});
@ -209,7 +215,10 @@ describe("format stage", () => {
];
const text = formatRemoteSteeringResults(results);
assert.ok(text.includes("[ok] /mode build"), `missing ok mode: ${text}`);
assert.ok(text.includes("[ok] /control autonomous"), `missing ok control: ${text}`);
assert.ok(
text.includes("[ok] /control autonomous"),
`missing ok control: ${text}`,
);
});
test("formatRemoteSteeringResults_when_blocked_renders_blocked_marker_and_error", () => {
@ -217,7 +226,10 @@ describe("format stage", () => {
{ cmd: "mode", value: "review", applied: false, error: "Throttled" },
];
const text = formatRemoteSteeringResults(results);
assert.ok(text.includes("[blocked] /mode review"), `missing blocked: ${text}`);
assert.ok(
text.includes("[blocked] /mode review"),
`missing blocked: ${text}`,
);
assert.ok(text.includes("Throttled"), `missing error text: ${text}`);
});
@ -259,7 +271,10 @@ describe("full pipeline: parse → apply → format", () => {
assert.strictEqual(parsed.directives.length, 4);
const applied = applyRemoteSteeringDirectives(parsed.directives, src);
assert.ok(applied.every((r) => r.applied), `Some failed: ${JSON.stringify(applied)}`);
assert.ok(
applied.every((r) => r.applied),
`Some failed: ${JSON.stringify(applied)}`,
);
assert.strictEqual(_modeState.workMode, "build");
assert.strictEqual(_modeState.runControl, "autonomous");
@ -279,7 +294,10 @@ describe("full pipeline: parse → apply → format", () => {
const formatted = formatRemoteSteeringResults(applied2);
assert.strictEqual(applied2[0].applied, false);
assert.ok(formatted.includes("[blocked]"), `expected blocked: ${formatted}`);
assert.ok(
formatted.includes("[blocked]"),
`expected blocked: ${formatted}`,
);
// Mode should not have changed
assert.strictEqual(_modeState.workMode, "build");
});

View file

@ -14,9 +14,9 @@ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, it } from "vitest";
import { DISPATCH_RULES } from "../uok/auto-dispatch.js";
import { createScheduleStore } from "../schedule/schedule-store.js";
import { generateULID } from "../schedule/schedule-ulid.js";
import { DISPATCH_RULES } from "../uok/auto-dispatch.js";
describe("schedule-dispatch", () => {
let testDir;

View file

@ -229,7 +229,10 @@ test("openDatabase_migrates_v27_tasks_without_created_at_through_spec_backfill",
"SELECT name FROM sqlite_master WHERE type='table' AND name='intent_chapters'",
)
.get();
assert.ok(chaptersTable, "intent_chapters table should exist after v61 migration");
assert.ok(
chaptersTable,
"intent_chapters table should exist after v61 migration",
);
const taskSpec = db
.prepare(
"SELECT milestone_id, slice_id, task_id, verify FROM task_specs WHERE task_id = 'T01'",
@ -292,9 +295,15 @@ test("openDatabase_when_fresh_db_does_not_create_gate_runs_table", () => {
// After v58 migration, gate_runs table no longer exists
const tableInfo = getDatabase()
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='gate_runs'")
.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name='gate_runs'",
)
.get();
assert.equal(tableInfo, undefined, "gate_runs table should not exist after v58 migration");
assert.equal(
tableInfo,
undefined,
"gate_runs table should not exist after v58 migration",
);
});
test("reconcileWorktreeDb_when_worktree_lacks_product_research_column_merges_milestones", () => {
@ -342,9 +351,15 @@ test("openDatabase_migrates_v35_gate_cost_usd_drops_table_in_v58", () => {
const db = getDatabase();
// After v58 migration, gate_runs is dropped
const tableInfo = db
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='gate_runs'")
.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name='gate_runs'",
)
.get();
assert.equal(tableInfo, undefined, "gate_runs should be dropped by v58 migration");
assert.equal(
tableInfo,
undefined,
"gate_runs should be dropped by v58 migration",
);
});
test("openDatabase_memories_table_has_tags_column", () => {

View file

@ -15,9 +15,9 @@ import {
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, test } from "vitest";
import registerSubagentExtension from "../subagent/index.js";
import { registerQueryTools } from "../bootstrap/query-tools.js";
import { closeDatabase, getRetrievalEvidence, openDatabase } from "../sf-db.js";
import registerSubagentExtension from "../subagent/index.js";
import { registerSiftSearchTool } from "../tools/sift-search-tool.js";
const tmpRoots = [];

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