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:
parent
65da855c5e
commit
0b5fa75c0d
130 changed files with 2535 additions and 1918 deletions
Binary file not shown.
BIN
.sf/backups/db/sf.db.2026-05-11T01-53-05-325Z
Normal file
BIN
.sf/backups/db/sf.db.2026-05-11T01-53-05-325Z
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }],
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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> = {
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 ?? []
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue