fix(lint): restore 0 Biome diagnostics and fix web-mode-onboarding test timeout

- Remove/prefix unused imports and variables across 11 src/ files to clear
  74 diagnostics introduced by 37 subsequent commits since run #3
- Fix pre-existing timeout in web-mode-onboarding integration test:
  - Add timeoutMs: 120_000 to launchPackagedWebHost call (was unbounded)
  - Raise AbortSignal.timeout on simple fetches 10s → 30s (under parallel load)
  - Raise overall test timeout 180s → 420s (budget: 120+60+30+30+120+30=390s)
- Log autoresearch run #4 and update lessons in autoresearch.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mikael Hugo 2026-05-10 11:01:43 +02:00
parent b2bcb922de
commit 05953e9599
14 changed files with 127 additions and 80 deletions

View file

@ -2,3 +2,4 @@
{"run": 1, "commit": "15269f4", "metric": 40.0, "metrics": {}, "status": "keep", "description": "baseline measurement", "timestamp": 1778242955776, "segment": 0, "confidence": null, "asi": {"hypothesis": "baseline measurement", "breakdown": "26 errors, 13 warnings, 1 info"}} {"run": 1, "commit": "15269f4", "metric": 40.0, "metrics": {}, "status": "keep", "description": "baseline measurement", "timestamp": 1778242955776, "segment": 0, "confidence": null, "asi": {"hypothesis": "baseline measurement", "breakdown": "26 errors, 13 warnings, 1 info"}}
{"run": 2, "commit": "72e27f9", "metric": 11.0, "metrics": {}, "status": "keep", "description": "auto-fix format + organizeImports: biome check --write src/", "timestamp": 1778243276590, "segment": 0, "confidence": null, "asi": {"hypothesis": "All 26 errors are auto-fixable format/organizeImports; fixing them drops total from 40 to 11", "breakdown": "0 errors, 11 warnings"}} {"run": 2, "commit": "72e27f9", "metric": 11.0, "metrics": {}, "status": "keep", "description": "auto-fix format + organizeImports: biome check --write src/", "timestamp": 1778243276590, "segment": 0, "confidence": null, "asi": {"hypothesis": "All 26 errors are auto-fixable format/organizeImports; fixing them drops total from 40 to 11", "breakdown": "0 errors, 11 warnings"}}
{"run": 3, "commit": "c6ee770", "metric": 0.0, "metrics": {}, "status": "keep", "description": "fix 11 unused imports/variables by removing or prefixing with underscore", "timestamp": 1778243617559, "segment": 0, "confidence": 3.64, "asi": {"hypothesis": "All 11 remaining warnings are unused imports/variables \u2014 removing unused imports and prefixing intentionally kept but unused variables with underscore eliminates all diagnostics", "breakdown": "Removed: injectReasoningGuidance, withQueryTimeout (unused import), getAutoSession, logWarning (2x), debugLog, readFileSync/unlinkSync/writeFileSync. Prefixed: MAX_HISTOGRAM_BUCKETS, REASONING_ASSIST_MAX_CHARS, basePath param."}} {"run": 3, "commit": "c6ee770", "metric": 0.0, "metrics": {}, "status": "keep", "description": "fix 11 unused imports/variables by removing or prefixing with underscore", "timestamp": 1778243617559, "segment": 0, "confidence": 3.64, "asi": {"hypothesis": "All 11 remaining warnings are unused imports/variables \u2014 removing unused imports and prefixing intentionally kept but unused variables with underscore eliminates all diagnostics", "breakdown": "Removed: injectReasoningGuidance, withQueryTimeout (unused import), getAutoSession, logWarning (2x), debugLog, readFileSync/unlinkSync/writeFileSync. Prefixed: MAX_HISTOGRAM_BUCKETS, REASONING_ASSIST_MAX_CHARS, basePath param."}}
{"run": 4, "commit": "b2bcb922d", "metric": 0.0, "metrics": {}, "status": "keep", "description": "re-fix 74 new diagnostics from 37 subsequent commits: biome --write dropped to 16, manual unused-import/var/param cleanup to 0; fixed web-mode-onboarding test timeout (timeoutMs 120s, AbortSignal 30s, test budget 420s)", "timestamp": 1778403638931, "segment": 0, "confidence": null, "asi": {"hypothesis": "37 new commits introduced 74 diagnostics (57 errors, 17 warnings); auto-fix handles format/import errors, manual prefix/removal handles unsafe unused-import warnings", "breakdown": "0 errors, 0 warnings after fix; all 409 test files pass"}}

View file

@ -44,4 +44,10 @@ Run until interrupted by the user.
## What's Been Tried ## What's Been Tried
- **#2 (auto-fix)**: `biome check --write` — fixed 26 auto-fixable errors (format/organizeImports), dropped diagnostics from 40 to 11. Status: keep. - **#2 (auto-fix)**: `biome check --write` — fixed 26 auto-fixable errors (format/organizeImports), dropped diagnostics from 40 to 11. Status: keep.
- **#3 (manual fixes)**: Removed 7 unused imports (`injectReasoningGuidance`, `withQueryTimeout`, `getAutoSession`, `logWarning` x3, `debugLog`, `readFileSync/unlinkSync/writeFileSync`) and prefixed 4 intentionally-unused items with underscore (`_MAX_HISTOGRAM_BUCKETS`, `_REASONING_ASSIST_MAX_CHARS`, `_basePath`, `_withQueryTimeout`). Dropped from 11 to 0. Status: keep. - **#3 (manual fixes)**: Removed 7 unused imports and prefixed 4 intentionally-unused items with underscore. Dropped from 11 to 0. Status: keep.
- **#4 (regression re-fix)**: 37 new commits introduced 74 diagnostics. `biome check --write` fixed 58 (auto-safe), manual prefix/removal fixed the remaining 16 unsafe warnings across 11 files. Also fixed pre-existing web-mode-onboarding test timeout: added `timeoutMs: 120_000` to `launchPackagedWebHost`, raised `AbortSignal.timeout` on simple fetches 10s→30s, raised test budget 180s→420s. All 409 test files pass. Diagnostics: 0. Status: keep.
## Lessons
- New development (37 commits) is enough to re-introduce 74 diagnostics. Re-run autoresearch periodically (monthly or after large feature branches land).
- Pattern of new violations: unused imports from refactors, unused function params from stubs, duplicate imports. Auto-fix handles errors; unsafe-fix (unused-import/var) requires manual triage.
- Integration test timeout under parallel load: cold-start Next.js can consume most of a 180s test timeout leaving insufficient budget for multi-step API calls. Fix: bound launch phase separately, raise individual fetch timeouts, increase overall budget to match worst-case sum.

View file

@ -11,19 +11,21 @@ import {
sanitizeCommand, sanitizeCommand,
} from "@singularity-forge/pi-coding-agent"; } from "@singularity-forge/pi-coding-agent";
import { rewriteCommandWithRtk } from "../shared/rtk.js"; import { rewriteCommandWithRtk } from "../shared/rtk.js";
import { import { addEvent, pushAlert } from "./bg-events.js";
addEvent,
pendingAlerts,
pushAlert,
setPendingAlerts,
} from "./bg-events.js";
import { analyzeLine } from "./output-formatter.js"; import { analyzeLine } from "./output-formatter.js";
import { startPortProbing, transitionToReady } from "./readiness-detector.js"; import { startPortProbing, transitionToReady } from "./readiness-detector.js";
import { DEAD_PROCESS_TTL, MAX_BUFFER_LINES } from "./types.js"; import { DEAD_PROCESS_TTL, MAX_BUFFER_LINES } from "./types.js";
import { formatUptime, restoreWindowsVTInput } from "./utilities.js"; import { formatUptime, restoreWindowsVTInput } from "./utilities.js";
// Re-export event/alert helpers so existing consumers (bg-shell-lifecycle.js) // Re-export event/alert helpers so existing consumers (bg-shell-lifecycle.js)
// continue to work without changing their import paths. // continue to work without changing their import paths.
export { addEvent, MAX_PENDING_ALERTS, pendingAlerts, pushAlert, setPendingAlerts } from "./bg-events.js"; export {
addEvent,
MAX_PENDING_ALERTS,
pendingAlerts,
pushAlert,
setPendingAlerts,
} from "./bg-events.js";
// ── Process Registry ─────────────────────────────────────────────────────── // ── Process Registry ───────────────────────────────────────────────────────
export const processes = new Map(); export const processes = new Map();
export function addOutputLine(bg, stream, line) { export function addOutputLine(bg, stream, line) {

View file

@ -1,5 +1,5 @@
import { existsSync, readdirSync } from "node:fs";
import { spawnSync } from "node:child_process"; import { spawnSync } from "node:child_process";
import { existsSync, readdirSync } from "node:fs";
import { join, relative, resolve } from "node:path"; import { join, relative, resolve } from "node:path";
import { isToolCallEventType } from "@singularity-forge/pi-coding-agent"; import { isToolCallEventType } from "@singularity-forge/pi-coding-agent";
import { resetAskUserQuestionsCache } from "../../ask-user-questions.js"; import { resetAskUserQuestionsCache } from "../../ask-user-questions.js";
@ -69,6 +69,7 @@ import {
import { initSessionRecorder } from "../session-recorder.js"; import { initSessionRecorder } from "../session-recorder.js";
import { deriveState } from "../state.js"; import { deriveState } from "../state.js";
import { countGoogleGeminiCliTokens } from "../token-counter.js"; import { countGoogleGeminiCliTokens } from "../token-counter.js";
import { getSessionTodoCompactionBlock } from "../tools/session-todo-tool.js";
import { parseUnitId } from "../unit-id.js"; import { parseUnitId } from "../unit-id.js";
import { logWarning as safetyLogWarning } from "../workflow-logger.js"; import { logWarning as safetyLogWarning } from "../workflow-logger.js";
import { import {
@ -79,7 +80,6 @@ import {
import { handleAgentEnd } from "./agent-end-recovery.js"; import { handleAgentEnd } from "./agent-end-recovery.js";
import { installNotifyInterceptor } from "./notify-interceptor.js"; import { installNotifyInterceptor } from "./notify-interceptor.js";
import { buildBeforeAgentStartResult } from "./system-context.js"; import { buildBeforeAgentStartResult } from "./system-context.js";
import { getSessionTodoCompactionBlock } from "../tools/session-todo-tool.js";
import { import {
checkToolCallLoop, checkToolCallLoop,
resetToolCallLoopGuard, resetToolCallLoopGuard,
@ -212,7 +212,7 @@ export function registerHooks(pi, ecosystemHandlers = []) {
resetMemorySleeper(); resetMemorySleeper();
try { try {
const sid = ctx.sessionManager?.getSessionId?.() ?? ""; const sid = ctx.sessionManager?.getSessionId?.() ?? "";
const sfile = ctx.sessionManager?.getSessionFile?.() ?? ""; const _sfile = ctx.sessionManager?.getSessionFile?.() ?? "";
if (sid) { if (sid) {
process.stderr.write(`[forge] session ${sid.slice(0, 8)}\n`); process.stderr.write(`[forge] session ${sid.slice(0, 8)}\n`);
} }
@ -676,9 +676,7 @@ export function registerHooks(pi, ecosystemHandlers = []) {
workState.length > 0 workState.length > 0
? `Work in progress: ${workState.join(". ")}.` ? `Work in progress: ${workState.join(". ")}.`
: "Session compacted. No active work state."; : "Session compacted. No active work state.";
const summary = todoBlock const summary = todoBlock ? `${baseSummary}\n\n${todoBlock}` : baseSummary;
? `${baseSummary}\n\n${todoBlock}`
: baseSummary;
const result = { const result = {
compaction: { compaction: {
summary, summary,

View file

@ -23,15 +23,14 @@ import {
loadProjectSFPreferences, loadProjectSFPreferences,
resolveAllSkillReferences, resolveAllSkillReferences,
} from "./preferences.js"; } from "./preferences.js";
// Re-export from thin shared module so external importers keep working. // Re-export from thin shared module so external importers keep working.
export { export {
serializePreferencesToFrontmatter, serializePreferencesToFrontmatter,
yamlSafeString, yamlSafeString,
} from "./preferences-serializer.js"; } from "./preferences-serializer.js";
import {
serializePreferencesToFrontmatter, import { serializePreferencesToFrontmatter } from "./preferences-serializer.js";
yamlSafeString,
} from "./preferences-serializer.js";
/** Extract body content after frontmatter closing delimiter, or null if none. */ /** Extract body content after frontmatter closing delimiter, or null if none. */
function extractBodyAfterFrontmatter(content) { function extractBodyAfterFrontmatter(content) {

View file

@ -172,7 +172,7 @@ export function renderTriageMarkdown(result, sourcePath) {
section("Unclear Notes", result.unclear_notes), section("Unclear Notes", result.unclear_notes),
].join("\n"); ].join("\n");
} }
function renderEvalJsonl(result) { function _renderEvalJsonl(result) {
return ( return (
result.eval_candidates result.eval_candidates
.map((item) => .map((item) =>
@ -270,7 +270,7 @@ function detectRecurringPatterns(result) {
} }
return proposals; return proposals;
} }
function renderSkillProposals(result) { function _renderSkillProposals(result) {
const proposals = detectRecurringPatterns(result); const proposals = detectRecurringPatterns(result);
if (proposals.length === 0) return "\n"; if (proposals.length === 0) return "\n";
return ( return (
@ -367,7 +367,7 @@ function normalizedItems(result, createdAt) {
for (const item of result.unclear_notes) push("unclear_note", item); for (const item of result.unclear_notes) push("unclear_note", item);
return items; return items;
} }
function renderNormalizedJsonl(result, createdAt) { function _renderNormalizedJsonl(result, createdAt) {
const items = normalizedItems(result, createdAt); const items = normalizedItems(result, createdAt);
return ( return (
items items
@ -565,7 +565,14 @@ export async function triageTodoDump(basePath, llmCall, options = {}) {
insertTriageEval(crypto.randomUUID(), id, item, createdAt); insertTriageEval(crypto.randomUUID(), id, item, createdAt);
} }
for (const item of normalizedItems(result, createdAt)) { for (const item of normalizedItems(result, createdAt)) {
insertTriageItem(crypto.randomUUID(), id, item.kind, item.content, item.evidence, createdAt); insertTriageItem(
crypto.randomUUID(),
id,
item.kind,
item.content,
item.evidence,
createdAt,
);
} }
const skillProposals = detectRecurringPatterns(result); const skillProposals = detectRecurringPatterns(result);
for (const skill of skillProposals) { for (const skill of skillProposals) {

View file

@ -15,6 +15,7 @@ import {
setAllExperimentalFlags, setAllExperimentalFlags,
setExperimentalFlag, setExperimentalFlag,
} from "../../experimental.js"; } from "../../experimental.js";
import { inferPresetName, resolvePreset } from "../../operating-model.js";
import { import {
getGlobalSFPreferencesPath, getGlobalSFPreferencesPath,
getProjectSFPreferencesPath, getProjectSFPreferencesPath,
@ -28,11 +29,6 @@ import { formattedShortcutPair } from "../../shortcut-defs.js";
import { deriveState } from "../../state.js"; import { deriveState } from "../../state.js";
import { writeUokDiagnostics } from "../../uok/diagnostic-synthesis.js"; import { writeUokDiagnostics } from "../../uok/diagnostic-synthesis.js";
import { projectRoot } from "../context.js"; import { projectRoot } from "../context.js";
import {
SF_MODE_PRESET_NAMES,
inferPresetName,
resolvePreset,
} from "../../operating-model.js";
export function showHelp(ctx, args = "") { export function showHelp(ctx, args = "") {
const summaryLines = [ const summaryLines = [
"SF — Singularity Forge\n", "SF — Singularity Forge\n",

View file

@ -432,8 +432,12 @@ function openMetricsDb(basePath) {
) )
`); `);
db.exec(`CREATE INDEX IF NOT EXISTS idx_metrics_name ON metrics(name)`); db.exec(`CREATE INDEX IF NOT EXISTS idx_metrics_name ON metrics(name)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_metrics_session ON metrics(session_id)`); db.exec(
db.exec(`CREATE INDEX IF NOT EXISTS idx_metrics_name_ts ON metrics(name, timestamp DESC)`); `CREATE INDEX IF NOT EXISTS idx_metrics_session ON metrics(session_id)`,
);
db.exec(
`CREATE INDEX IF NOT EXISTS idx_metrics_name_ts ON metrics(name, timestamp DESC)`,
);
_metricsDb = db; _metricsDb = db;
} catch (err) { } catch (err) {
logWarning("metrics-central", `Failed to open metrics.db: ${err.message}`); logWarning("metrics-central", `Failed to open metrics.db: ${err.message}`);
@ -450,7 +454,7 @@ function closeMetricsDb() {
_metricsDb = null; _metricsDb = null;
} }
function ensureMetricsTable(db) { function _ensureMetricsTable(db) {
// no-op — metrics.db is set up by openMetricsDb // no-op — metrics.db is set up by openMetricsDb
void db; void db;
} }
@ -513,11 +517,13 @@ function persistMetricsToDb(registry, sessionId, _ignored) {
try { try {
const row = _metricsDb?.prepare("SELECT count(*) as n FROM metrics").get(); const row = _metricsDb?.prepare("SELECT count(*) as n FROM metrics").get();
if (row && row.n > METRICS_DB_ROW_CAP) { if (row && row.n > METRICS_DB_ROW_CAP) {
_metricsDb.prepare( _metricsDb
`DELETE FROM metrics WHERE rowid NOT IN ( .prepare(
`DELETE FROM metrics WHERE rowid NOT IN (
SELECT rowid FROM metrics ORDER BY timestamp DESC LIMIT ${METRICS_DB_ROW_CAP} SELECT rowid FROM metrics ORDER BY timestamp DESC LIMIT ${METRICS_DB_ROW_CAP}
)`, )`,
).run(); )
.run();
} }
} catch (_) { } catch (_) {
// swallow — prune failure must never surface to the user // swallow — prune failure must never surface to the user

View file

@ -9,21 +9,22 @@
* Consumer: index.js steerableAutonomousExtension(pi) on every startup. * Consumer: index.js steerableAutonomousExtension(pi) on every startup.
*/ */
import {
handleSteerableModeKey,
SteerableAutonomousPanel,
} from "./steerable-autonomous-panel.js";
import { SF_MODE_PRESET_NAMES, inferPresetName, resolvePreset } from "./operating-model.js";
import { getAutoSession } from "./auto/session.js"; import { getAutoSession } from "./auto/session.js";
import { isAutoActive, startAutoDetached } from "./auto.js"; import { isAutoActive, startAutoDetached } from "./auto.js";
import { projectRoot } from "./commands/context.js"; import { projectRoot } from "./commands/context.js";
import {
inferPresetName,
resolvePreset,
SF_MODE_PRESET_NAMES,
} from "./operating-model.js";
import { SteerableAutonomousPanel } from "./steerable-autonomous-panel.js";
export default function steerableAutonomousExtension(api) { export default function steerableAutonomousExtension(api) {
let panel = null; let panel = null;
let isAutonomousActive = false; let isAutonomousActive = false;
// Track autonomous mode state // Track autonomous mode state
api.on("session_start", async (_, ctx) => { api.on("session_start", async (_, _ctx) => {
isAutonomousActive = false; isAutonomousActive = false;
if (panel) { if (panel) {
panel.hide(); panel.hide();
@ -60,7 +61,8 @@ export default function steerableAutonomousExtension(api) {
} }
const current = inferPresetName(s.getMode()) ?? SF_MODE_PRESET_NAMES[0]; const current = inferPresetName(s.getMode()) ?? SF_MODE_PRESET_NAMES[0];
const idx = SF_MODE_PRESET_NAMES.indexOf(current); const idx = SF_MODE_PRESET_NAMES.indexOf(current);
const nextName = SF_MODE_PRESET_NAMES[(idx + 1) % SF_MODE_PRESET_NAMES.length]; const nextName =
SF_MODE_PRESET_NAMES[(idx + 1) % SF_MODE_PRESET_NAMES.length];
const preset = resolvePreset(nextName); const preset = resolvePreset(nextName);
s.setMode({ s.setMode({
workMode: preset.workMode, workMode: preset.workMode,
@ -70,16 +72,14 @@ export default function steerableAutonomousExtension(api) {
}); });
ctx.ui.notify(`Mode → ${nextName} (${preset.description})`, "info"); ctx.ui.notify(`Mode → ${nextName} (${preset.description})`, "info");
} catch { } catch {
ctx.ui.notify( ctx.ui.notify("Mode: use /mode ask|plan|build", "info");
"Mode: use /mode ask|plan|build",
"info",
);
} }
}, },
}); });
api.registerShortcut("ctrl+y", { api.registerShortcut("ctrl+y", {
description: "Toggle YOLO mode (build + autonomous + deep + unrestricted; bypass git prompts). If not running, starts the autonomous loop immediately.", description:
"Toggle YOLO mode (build + autonomous + deep + unrestricted; bypass git prompts). If not running, starts the autonomous loop immediately.",
handler: async (ctx) => { handler: async (ctx) => {
const session = getAutoSession(); const session = getAutoSession();
// Toggle full-autonomy preset in AutoSession (handles mode slam + restore) // Toggle full-autonomy preset in AutoSession (handles mode slam + restore)
@ -109,7 +109,8 @@ export default function steerableAutonomousExtension(api) {
// Handle slash command for panel // Handle slash command for panel
api.registerCommand("steer", { api.registerCommand("steer", {
description: "Open steerable autonomous panel (Shift+Tab during autonomous)", description:
"Open steerable autonomous panel (Shift+Tab during autonomous)",
handler: async (_, ctx) => { handler: async (_, ctx) => {
if (!isAutonomousActive) { if (!isAutonomousActive) {
ctx.ui.notify( ctx.ui.notify(
@ -132,10 +133,7 @@ export default function steerableAutonomousExtension(api) {
// Listen for autonomous mode changes // Listen for autonomous mode changes
api.on("autonomous_start", async (_, ctx) => { api.on("autonomous_start", async (_, ctx) => {
isAutonomousActive = true; isAutonomousActive = true;
ctx.ui.notify( ctx.ui.notify("🤖 Autonomous mode active — Shift+Tab to steer", "info");
"🤖 Autonomous mode active — Shift+Tab to steer",
"info",
);
}); });
api.on("autonomous_stop", async (_, ctx) => { api.on("autonomous_stop", async (_, ctx) => {

View file

@ -7,7 +7,6 @@
*/ */
import { createInterface } from "node:readline"; import { createInterface } from "node:readline";
import { getEditorKeybindings } from "@singularity-forge/pi-tui";
// ─── Constants ────────────────────────────────────────────────────────────── // ─── Constants ──────────────────────────────────────────────────────────────
const PANEL_WIDTH = 60; const PANEL_WIDTH = 60;
@ -216,7 +215,7 @@ const ACTION_HANDLERS = {
// Would trigger immediate reassessment // Would trigger immediate reassessment
}, },
close: async (ctx) => { close: async (_ctx) => {
// Just hide the panel // Just hide the panel
}, },
}; };
@ -247,7 +246,7 @@ export class SteerableAutonomousPanel {
this.render(); this.render();
// Set up key listener // Set up key listener
this.rl.input.on("keypress", (str, key) => { this.rl.input.on("keypress", (_str, key) => {
this.handleKeyPress(key); this.handleKeyPress(key);
}); });
} }

View file

@ -11,7 +11,6 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { test } from "vitest"; import { test } from "vitest";
import { import {
clearRunawayGuardState,
evaluateRunawayGuard, evaluateRunawayGuard,
resetRunawayGuardState, resetRunawayGuardState,
} from "../auto-runaway-guard.js"; } from "../auto-runaway-guard.js";
@ -61,7 +60,15 @@ test("progress check returns none regardless of hard-pause conditions", () => {
evaluateRunawayGuard( evaluateRunawayGuard(
"discuss-milestone", "discuss-milestone",
"M001", "M001",
{ toolCalls: 67, sessionTokens: 1_500_000, elapsedMs: 22 * 60 * 1000, changedFiles: 0, worktreeFingerprint: null, worktreeChangedSinceStart: false, topTools: {} }, {
toolCalls: 67,
sessionTokens: 1_500_000,
elapsedMs: 22 * 60 * 1000,
changedFiles: 0,
worktreeFingerprint: null,
worktreeChangedSinceStart: false,
topTools: {},
},
config, config,
now, now,
); );
@ -71,12 +78,24 @@ test("progress check returns none regardless of hard-pause conditions", () => {
const r = evaluateRunawayGuard( const r = evaluateRunawayGuard(
"discuss-milestone", "discuss-milestone",
"M001", "M001",
{ toolCalls: 67, sessionTokens: 2_000_000, elapsedMs: 25 * 60 * 1000, changedFiles: 1, worktreeFingerprint: null, worktreeChangedSinceStart: false, topTools: {} }, {
toolCalls: 67,
sessionTokens: 2_000_000,
elapsedMs: 25 * 60 * 1000,
changedFiles: 1,
worktreeFingerprint: null,
worktreeChangedSinceStart: false,
topTools: {},
},
config, config,
now + 180_000, now + 180_000,
); );
// The progress check fires BEFORE the hard-pause block, returning 'none' // The progress check fires BEFORE the hard-pause block, returning 'none'
assert.equal(r.action, "none", "progress check should return none even when hardPause conditions are met"); assert.equal(
r.action,
"none",
"progress check should return none even when hardPause conditions are met",
);
}); });
test("returns none when changedFiles > 0 despite token growth and 2 warnings", () => { test("returns none when changedFiles > 0 despite token growth and 2 warnings", () => {
@ -133,7 +152,11 @@ test("returns none when worktreeChangedSinceStart === true despite token growth"
config, config,
now + 180_000, now + 180_000,
); );
assert.equal(r.action, "none", "should not pause when worktreeChangedSinceStart === true"); assert.equal(
r.action,
"none",
"should not pause when worktreeChangedSinceStart === true",
);
}); });
test("returns none when changedFiles is explicitly 0 but worktreeChangedSinceStart is false", () => { test("returns none when changedFiles is explicitly 0 but worktreeChangedSinceStart is false", () => {
@ -154,7 +177,11 @@ test("returns none when changedFiles is explicitly 0 but worktreeChangedSinceSta
const r = evaluateRunawayGuard( const r = evaluateRunawayGuard(
"discuss-milestone", "discuss-milestone",
"M001", "M001",
makeMetrics({ sessionTokens: 2_940_000, changedFiles: 0, worktreeChangedSinceStart: false }), makeMetrics({
sessionTokens: 2_940_000,
changedFiles: 0,
worktreeChangedSinceStart: false,
}),
config, config,
now + 180_000, now + 180_000,
); );
@ -199,5 +226,9 @@ test("discuss-milestone with file changes does not get hard-paused", () => {
config, config,
now + 180_000, now + 180_000,
); );
assert.equal(r.action, "none", "discuss-milestone with file changes should not be paused"); assert.equal(
r.action,
"none",
"discuss-milestone with file changes should not be paused",
);
}); });

View file

@ -12,18 +12,12 @@ import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest"; import { afterEach, describe, expect, test } from "vitest";
import { closeDatabase } from "../sf-db.js";
import { _clearSfRootCache } from "../paths.js"; import { _clearSfRootCache } from "../paths.js";
import { PersistentAgent } from "../uok/persistent-agent.js"; import { closeDatabase } from "../sf-db.js";
import { AgentSwarm } from "../uok/agent-swarm.js"; import { AgentSwarm } from "../uok/agent-swarm.js";
import { import { PersistentAgent } from "../uok/persistent-agent.js";
CoordinatorAgent,
WorkerAgent,
ScoutAgent,
ReviewerAgent,
createDefaultSwarm,
} from "../uok/swarm-roles.js";
import { SwarmDispatchLayer } from "../uok/swarm-dispatch.js"; import { SwarmDispatchLayer } from "../uok/swarm-dispatch.js";
import { createDefaultSwarm } from "../uok/swarm-roles.js";
// ─── Shared Setup ───────────────────────────────────────────────────────────── // ─── Shared Setup ─────────────────────────────────────────────────────────────
@ -86,7 +80,10 @@ describe("PersistentAgent — identity", () => {
describe("PersistentAgent — messaging", () => { describe("PersistentAgent — messaging", () => {
test("send_receive_delivers_message", () => { test("send_receive_delivers_message", () => {
const root = makeProject(); const root = makeProject();
const agentA = new PersistentAgent(root, { name: "sender", role: "worker" }); const agentA = new PersistentAgent(root, {
name: "sender",
role: "worker",
});
const agentB = new PersistentAgent(root, { const agentB = new PersistentAgent(root, {
name: "receiver", name: "receiver",
role: "worker", role: "worker",
@ -132,8 +129,14 @@ describe("PersistentAgent — messaging", () => {
name: "broadcaster", name: "broadcaster",
role: "coordinator", role: "coordinator",
}); });
const agentB = new PersistentAgent(root, { name: "recv-b", role: "worker" }); const agentB = new PersistentAgent(root, {
const agentC = new PersistentAgent(root, { name: "recv-c", role: "worker" }); name: "recv-b",
role: "worker",
});
const agentC = new PersistentAgent(root, {
name: "recv-c",
role: "worker",
});
agentA.broadcast("announcement", {}, ["recv-b", "recv-c"]); agentA.broadcast("announcement", {}, ["recv-b", "recv-c"]);

View file

@ -34,7 +34,7 @@ function saveTodos(baseDir, todos) {
writeFileSync(todoPath(baseDir), JSON.stringify(todos, null, 2), "utf-8"); writeFileSync(todoPath(baseDir), JSON.stringify(todos, null, 2), "utf-8");
} }
function nextId(todos) { function nextId(_todos) {
// Short base-36 timestamp suffix for readable IDs. // Short base-36 timestamp suffix for readable IDs.
return `t${Date.now().toString(36)}`; return `t${Date.now().toString(36)}`;
} }
@ -103,9 +103,7 @@ export function executeSessionTodoList(baseDir) {
details: { operation: "list", todos: [] }, details: { operation: "list", todos: [] },
}; };
} }
const lines = todos.map( const lines = todos.map((t) => `[${t.done ? "x" : " "}] ${t.id}: ${t.text}`);
(t) => `[${t.done ? "x" : " "}] ${t.id}: ${t.text}`,
);
return { return {
content: [{ type: "text", text: lines.join("\n") }], content: [{ type: "text", text: lines.join("\n") }],
details: { operation: "list", todos }, details: { operation: "list", todos },

View file

@ -518,6 +518,9 @@ test("fresh sf --web browser onboarding stays locked on failed validation and un
launchCwd: tempProject, launchCwd: tempProject,
tempHome, tempHome,
browserLogPath, browserLogPath,
// Cap launch at 120s so the remaining budget (~180s+) covers API calls
// (valid-key validation triggers a bridge restart = up to 120s itself).
timeoutMs: 120_000,
env: { env: {
SF_WEB_TEST_FAKE_API_KEY_VALIDATION: "1", SF_WEB_TEST_FAKE_API_KEY_VALIDATION: "1",
ANTHROPIC_API_KEY: "", ANTHROPIC_API_KEY: "",
@ -574,7 +577,7 @@ test("fresh sf --web browser onboarding stays locked on failed validation and un
const bootBefore = await fetch(`${launch.url}/api/boot`, { const bootBefore = await fetch(`${launch.url}/api/boot`, {
method: "GET", method: "GET",
headers: { Accept: "application/json", ...auth }, headers: { Accept: "application/json", ...auth },
signal: AbortSignal.timeout(10_000), signal: AbortSignal.timeout(30_000),
}); });
assert.equal( assert.equal(
bootBefore.ok, bootBefore.ok,
@ -598,7 +601,7 @@ test("fresh sf --web browser onboarding stays locked on failed validation and un
providerId: "openai", providerId: "openai",
apiKey: "invalid-demo-key", apiKey: "invalid-demo-key",
}), }),
signal: AbortSignal.timeout(10_000), signal: AbortSignal.timeout(30_000),
}); });
assert.equal(invalidValidation.status, 422); assert.equal(invalidValidation.status, 422);
const invalidPayload = (await invalidValidation.json()) as any; const invalidPayload = (await invalidValidation.json()) as any;
@ -637,7 +640,7 @@ test("fresh sf --web browser onboarding stays locked on failed validation and un
const bootAfter = await fetch(`${launch.url}/api/boot`, { const bootAfter = await fetch(`${launch.url}/api/boot`, {
method: "GET", method: "GET",
headers: { Accept: "application/json", ...auth }, headers: { Accept: "application/json", ...auth },
signal: AbortSignal.timeout(10_000), signal: AbortSignal.timeout(30_000),
}); });
const bootAfterText = await bootAfter.clone().text(); const bootAfterText = await bootAfter.clone().text();
assert.equal( assert.equal(
@ -648,4 +651,4 @@ test("fresh sf --web browser onboarding stays locked on failed validation and un
const bootAfterPayload = (await bootAfter.json()) as any; const bootAfterPayload = (await bootAfter.json()) as any;
assert.equal(bootAfterPayload.onboarding.locked, false); assert.equal(bootAfterPayload.onboarding.locked, false);
assert.equal(bootAfterPayload.onboarding.lockReason, null); assert.equal(bootAfterPayload.onboarding.lockReason, null);
}, 180_000); }, 420_000);