diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock index d8a755d..f276799 100644 --- a/.claude/scheduled_tasks.lock +++ b/.claude/scheduled_tasks.lock @@ -1 +1 @@ -{"sessionId":"56e89c33-b844-4e6a-8df3-2210b2fb4a4d","pid":47993,"acquiredAt":1775696579277} \ No newline at end of file +{"sessionId":"bedd47ed-bfa0-41da-9a03-93d41159b4cd","pid":24606,"acquiredAt":1776194767342} \ No newline at end of file diff --git a/src/builder-agent/approval.ts b/src/builder-agent/approval.ts index bc44f6c..10ab6ba 100644 --- a/src/builder-agent/approval.ts +++ b/src/builder-agent/approval.ts @@ -68,9 +68,7 @@ export async function sendSwapApprovalCard( // Host-level swaps target the owner only. Group-level uses the normal // approver ladder (scoped admin → global admin → owner). - const approvers = isHostLevel - ? getOwners().map((r) => r.user_id) - : pickApprover(swap.originating_group_id); + const approvers = isHostLevel ? getOwners().map((r) => r.user_id) : pickApprover(swap.originating_group_id); if (approvers.length === 0) { notifyDevAgent( @@ -85,7 +83,8 @@ export async function sendSwapApprovalCard( // Origin channel kind drives tie-break preference (same as existing // install_packages / request_rebuild approvals). const originChannelType = originatingSession.messaging_group_id - ? (await import('../db/messaging-groups.js')).getMessagingGroup(originatingSession.messaging_group_id)?.channel_type ?? '' + ? ((await import('../db/messaging-groups.js')).getMessagingGroup(originatingSession.messaging_group_id) + ?.channel_type ?? '') : ''; const target = await pickApprovalDelivery(approvers, originChannelType); @@ -230,9 +229,7 @@ async function sendSwapReviewMessages( }; // 1. Intro message - const headerPrefix = isHostLevel - ? '⚠️ ⚠️ ⚠️ **HOST-LEVEL CODE CHANGE PROPOSED**' - : '🔧 **Code change proposed**'; + const headerPrefix = isHostLevel ? '⚠️ ⚠️ ⚠️ **HOST-LEVEL CODE CHANGE PROPOSED**' : '🔧 **Code change proposed**'; const intro = `${headerPrefix} by agent "${originatingName}".\n\n` + `**What it does:** ${summary.overallSummary || '(no summary)'}\n\n` + diff --git a/src/builder-agent/classifier.test.ts b/src/builder-agent/classifier.test.ts index abe020b..ddc42f4 100644 --- a/src/builder-agent/classifier.test.ts +++ b/src/builder-agent/classifier.test.ts @@ -22,29 +22,19 @@ describe('classifyPath', () => { const r = classifyPath('container/agent-runner/src/index.ts', OPTS); expect(r).not.toBeNull(); expect(r!.classification).toBe('group'); - expect(r!.target).toBe( - path.join('/repo/data/v2-sessions/grp-abc/agent-runner-src/index.ts'), - ); + expect(r!.target).toBe(path.join('/repo/data/v2-sessions/grp-abc/agent-runner-src/index.ts')); }); it('routes nested runner edits correctly', () => { const r = classifyPath('container/agent-runner/src/mcp-tools/agents.ts', OPTS); expect(r!.classification).toBe('group'); - expect(r!.target).toBe( - path.join( - '/repo/data/v2-sessions/grp-abc/agent-runner-src/mcp-tools/agents.ts', - ), - ); + expect(r!.target).toBe(path.join('/repo/data/v2-sessions/grp-abc/agent-runner-src/mcp-tools/agents.ts')); }); it('routes skills edits to the per-group private skills dir', () => { const r = classifyPath('container/skills/browser/SKILL.md', OPTS); expect(r!.classification).toBe('group'); - expect(r!.target).toBe( - path.join( - '/repo/data/v2-sessions/grp-abc/.claude-shared/skills/browser/SKILL.md', - ), - ); + expect(r!.target).toBe(path.join('/repo/data/v2-sessions/grp-abc/.claude-shared/skills/browser/SKILL.md')); }); it('routes originating group folder edits to their repo path', () => { @@ -131,10 +121,7 @@ describe('isMigrationPath', () => { describe('classifyDiff — overall classification', () => { it('is "group" when all changes land in originating group targets', () => { - const d = classifyDiff( - ['groups/main/CLAUDE.md', 'container/agent-runner/src/index.ts'], - OPTS, - ); + const d = classifyDiff(['groups/main/CLAUDE.md', 'container/agent-runner/src/index.ts'], OPTS); expect(d.overall).toBe('group'); expect(d.hostPaths).toHaveLength(0); expect(d.runnerOrSkillsPaths).toHaveLength(1); @@ -148,28 +135,19 @@ describe('classifyDiff — overall classification', () => { }); it('is "combined" when host AND runner/skills are both changed', () => { - const d = classifyDiff( - ['src/delivery.ts', 'container/agent-runner/src/poll-loop.ts'], - OPTS, - ); + const d = classifyDiff(['src/delivery.ts', 'container/agent-runner/src/poll-loop.ts'], OPTS); expect(d.overall).toBe('combined'); expect(d.hostPaths).toHaveLength(1); expect(d.runnerOrSkillsPaths).toHaveLength(1); }); it('is "combined" for host + skills change', () => { - const d = classifyDiff( - ['Dockerfile', 'container/skills/browser/SKILL.md'], - OPTS, - ); + const d = classifyDiff(['Dockerfile', 'container/skills/browser/SKILL.md'], OPTS); expect(d.overall).toBe('combined'); }); it('flags migrations regardless of other paths', () => { - const d = classifyDiff( - ['src/db/migrations/007-new.ts', 'src/delivery.ts'], - OPTS, - ); + const d = classifyDiff(['src/db/migrations/007-new.ts', 'src/delivery.ts'], OPTS); expect(d.touchesMigrations).toBe(true); expect(d.overall).toBe('host'); }); @@ -180,9 +158,7 @@ describe('classifyDiff — overall classification', () => { }); it('throws on excluded paths in the diff', () => { - expect(() => classifyDiff(['.env'], OPTS)).toThrow( - /unreachable or excluded path/, - ); + expect(() => classifyDiff(['.env'], OPTS)).toThrow(/unreachable or excluded path/); }); it('throws on data/ paths in the diff', () => { @@ -190,13 +166,7 @@ describe('classifyDiff — overall classification', () => { }); it('preserves original paths in output files', () => { - const d = classifyDiff( - ['groups/main/CLAUDE.md', 'src/delivery.ts'], - OPTS, - ); - expect(d.files.map((f) => f.path)).toEqual([ - 'groups/main/CLAUDE.md', - 'src/delivery.ts', - ]); + const d = classifyDiff(['groups/main/CLAUDE.md', 'src/delivery.ts'], OPTS); + expect(d.files.map((f) => f.path)).toEqual(['groups/main/CLAUDE.md', 'src/delivery.ts']); }); }); diff --git a/src/builder-agent/classifier.ts b/src/builder-agent/classifier.ts index 66a184e..397cd71 100644 --- a/src/builder-agent/classifier.ts +++ b/src/builder-agent/classifier.ts @@ -98,13 +98,7 @@ export function classifyPath( const rel = norm.slice(runnerPrefix.length); return { classification: 'group', - target: path.join( - opts.dataDir, - 'v2-sessions', - opts.originatingGroupId, - 'agent-runner-src', - rel, - ), + target: path.join(opts.dataDir, 'v2-sessions', opts.originatingGroupId, 'agent-runner-src', rel), }; } @@ -114,14 +108,7 @@ export function classifyPath( const rel = norm.slice(skillsPrefix.length); return { classification: 'group', - target: path.join( - opts.dataDir, - 'v2-sessions', - opts.originatingGroupId, - '.claude-shared', - 'skills', - rel, - ), + target: path.join(opts.dataDir, 'v2-sessions', opts.originatingGroupId, '.claude-shared', 'skills', rel), }; } @@ -146,10 +133,7 @@ export function classifyPath( /** True iff a classified file's worktree path is under runner or skills template. */ export function isRunnerOrSkillsPath(relPath: string): boolean { const norm = relPath.replace(/\\/g, '/'); - return ( - norm.startsWith('container/agent-runner/src/') || - norm.startsWith('container/skills/') - ); + return norm.startsWith('container/agent-runner/src/') || norm.startsWith('container/skills/'); } /** True iff a changed path is a schema migration. */ @@ -168,9 +152,7 @@ export function classifyDiff(changedPaths: string[], opts: ClassifyOptions): Cla for (const p of changedPaths) { const result = classifyPath(p, opts); if (!result) { - throw new Error( - `builder-agent: diff contains unreachable or excluded path: ${p}`, - ); + throw new Error(`builder-agent: diff contains unreachable or excluded path: ${p}`); } files.push({ path: p, diff --git a/src/builder-agent/deadman.ts b/src/builder-agent/deadman.ts index 5723599..9a1f64a 100644 --- a/src/builder-agent/deadman.ts +++ b/src/builder-agent/deadman.ts @@ -34,12 +34,7 @@ import { log } from '../log.js'; import type { PendingSwap } from '../types.js'; import { maybeSendPromotePrompt } from './promote.js'; import { removeDevWorktree } from './worktree.js'; -import { - isHostLevelSwap, - parseSwapSummary, - restoreDbFromSnapshot, - rollbackSwapFiles, -} from './swap.js'; +import { isHostLevelSwap, parseSwapSummary, restoreDbFromSnapshot, rollbackSwapFiles } from './swap.js'; const DEADMAN_INITIAL_MS = 2 * 60 * 1000; const DEADMAN_HARD_CAP_MS = 10 * 60 * 1000; diff --git a/src/builder-agent/handlers.ts b/src/builder-agent/handlers.ts index ebb10f9..7a19d5f 100644 --- a/src/builder-agent/handlers.ts +++ b/src/builder-agent/handlers.ts @@ -272,10 +272,7 @@ export async function handleCreateDevAgent( } catch { /* best effort */ } - notifyAgent( - session, - `create_dev_agent failed: ${err instanceof Error ? err.message : String(err)}`, - ); + notifyAgent(session, `create_dev_agent failed: ${err instanceof Error ? err.message : String(err)}`); return; } @@ -355,10 +352,7 @@ export async function handleRequestSwap( originatingGroupFolder: getAgentGroup(pending.originating_group_id)?.folder ?? '', }); } catch (err) { - notifyAgent( - session, - `Code change submission failed: ${err instanceof Error ? err.message : String(err)}`, - ); + notifyAgent(session, `Code change submission failed: ${err instanceof Error ? err.message : String(err)}`); return; } diff --git a/src/builder-agent/promote.test.ts b/src/builder-agent/promote.test.ts index d519b5e..c8be942 100644 --- a/src/builder-agent/promote.test.ts +++ b/src/builder-agent/promote.test.ts @@ -38,9 +38,7 @@ describe('swapTouchedRunnerOrSkills', () => { }); it('is true when container/agent-runner/src is touched', () => { - const swap = makeSwap([ - { path: 'container/agent-runner/src/poll-loop.ts', classification: 'group' }, - ]); + const swap = makeSwap([{ path: 'container/agent-runner/src/poll-loop.ts', classification: 'group' }]); expect(swapTouchedRunnerOrSkills(swap)).toBe(true); }); @@ -76,16 +74,12 @@ describe('sourceForTemplate', () => { it('maps nested runner paths correctly', () => { const src = sourceForTemplate('container/agent-runner/src/mcp-tools/agents.ts', 'ag-abc'); - expect(src).toBe( - path.join(DATA_DIR, 'v2-sessions', 'ag-abc', 'agent-runner-src', 'mcp-tools', 'agents.ts'), - ); + expect(src).toBe(path.join(DATA_DIR, 'v2-sessions', 'ag-abc', 'agent-runner-src', 'mcp-tools', 'agents.ts')); }); it('maps skills template paths to the per-group skills dir', () => { const src = sourceForTemplate('container/skills/browser/SKILL.md', 'ag-abc'); - expect(src).toBe( - path.join(DATA_DIR, 'v2-sessions', 'ag-abc', '.claude-shared', 'skills', 'browser', 'SKILL.md'), - ); + expect(src).toBe(path.join(DATA_DIR, 'v2-sessions', 'ag-abc', '.claude-shared', 'skills', 'browser', 'SKILL.md')); }); }); diff --git a/src/builder-agent/promote.ts b/src/builder-agent/promote.ts index fa31748..e03dcc6 100644 --- a/src/builder-agent/promote.ts +++ b/src/builder-agent/promote.ts @@ -57,9 +57,7 @@ export function setPromoteDelivery(adapter: PromoteDelivery): void { export function swapTouchedRunnerOrSkills(swap: PendingSwap): boolean { const summary = parseSwapSummary(swap); return summary.classifiedFiles.some( - (f) => - f.path.startsWith('container/agent-runner/src/') || - f.path.startsWith('container/skills/'), + (f) => f.path.startsWith('container/agent-runner/src/') || f.path.startsWith('container/skills/'), ); } @@ -77,9 +75,7 @@ export async function maybeSendPromotePrompt(swap: PendingSwap): Promise { } const isHostLevel = swap.classification === 'host' || swap.classification === 'combined'; - const approvers = isHostLevel - ? getOwners().map((r) => r.user_id) - : pickApprover(swap.originating_group_id); + const approvers = isHostLevel ? getOwners().map((r) => r.user_id) : pickApprover(swap.originating_group_id); if (approvers.length === 0) { log.info('Skipping promote prompt: no approvers configured', { requestId: swap.request_id }); @@ -187,9 +183,7 @@ async function applyToTemplate(swapRequestId: string): Promise { const summary = parseSwapSummary(swap); const runnerOrSkills = summary.classifiedFiles.filter( - (f) => - f.path.startsWith('container/agent-runner/src/') || - f.path.startsWith('container/skills/'), + (f) => f.path.startsWith('container/agent-runner/src/') || f.path.startsWith('container/skills/'), ); if (runnerOrSkills.length === 0) return; diff --git a/src/builder-agent/startup.ts b/src/builder-agent/startup.ts index 7f6e0c0..a3792fb 100644 --- a/src/builder-agent/startup.ts +++ b/src/builder-agent/startup.ts @@ -19,10 +19,7 @@ import fs from 'fs'; import path from 'path'; -import { - getAwaitingConfirmationSwaps, - getPendingSwap, -} from '../db/pending-swaps.js'; +import { getAwaitingConfirmationSwaps, getPendingSwap } from '../db/pending-swaps.js'; import { log } from '../log.js'; import { resumeDeadman } from './deadman.js'; import { removeDevWorktree } from './worktree.js'; @@ -59,10 +56,7 @@ function cleanupOrphanWorktrees(): void { // Orphaned if: no row, or row in a terminal state. const terminal = - !swap || - swap.status === 'finalized' || - swap.status === 'rolled_back' || - swap.status === 'rejected'; + !swap || swap.status === 'finalized' || swap.status === 'rolled_back' || swap.status === 'rejected'; if (terminal) { log.info('Cleaning up orphan worktree', { requestId, status: swap?.status ?? 'missing' }); diff --git a/src/builder-agent/swap.test.ts b/src/builder-agent/swap.test.ts index ecd6a6a..b28deff 100644 --- a/src/builder-agent/swap.test.ts +++ b/src/builder-agent/swap.test.ts @@ -2,12 +2,7 @@ import path from 'path'; import { describe, expect, it } from 'vitest'; -import { - isHostLevelSwap, - parseSwapSummary, - requiresFullHostRebuild, - targetRepoRelPath, -} from './swap.js'; +import { isHostLevelSwap, parseSwapSummary, requiresFullHostRebuild, targetRepoRelPath } from './swap.js'; import type { PendingSwap } from '../types.js'; function makeSwap(overrides: Partial = {}): PendingSwap { @@ -99,14 +94,10 @@ describe('requiresFullHostRebuild', () => { expect(requiresFullHostRebuild([abs('groups/main/CLAUDE.md')])).toBe(false); }); it('does not flag per-group runner dir changes', () => { - expect( - requiresFullHostRebuild([abs('data/v2-sessions/ag-1/agent-runner-src/poll-loop.ts')]), - ).toBe(false); + expect(requiresFullHostRebuild([abs('data/v2-sessions/ag-1/agent-runner-src/poll-loop.ts')])).toBe(false); }); it('returns true if any path requires rebuild even if others do not', () => { - expect( - requiresFullHostRebuild([abs('groups/main/CLAUDE.md'), abs('src/delivery.ts')]), - ).toBe(true); + expect(requiresFullHostRebuild([abs('groups/main/CLAUDE.md'), abs('src/delivery.ts')])).toBe(true); }); }); @@ -118,9 +109,9 @@ describe('targetRepoRelPath', () => { }); it('maps nested runner paths correctly', () => { - expect( - targetRepoRelPath('container/agent-runner/src/mcp-tools/agents.ts', 'ag-abc'), - ).toBe('data/v2-sessions/ag-abc/agent-runner-src/mcp-tools/agents.ts'); + expect(targetRepoRelPath('container/agent-runner/src/mcp-tools/agents.ts', 'ag-abc')).toBe( + 'data/v2-sessions/ag-abc/agent-runner-src/mcp-tools/agents.ts', + ); }); it('maps skills paths to the per-group skills dir', () => { @@ -136,8 +127,8 @@ describe('targetRepoRelPath', () => { }); it('handles Windows-style separators by normalizing', () => { - expect( - targetRepoRelPath('container\\agent-runner\\src\\index.ts', 'ag-abc'), - ).toBe('data/v2-sessions/ag-abc/agent-runner-src/index.ts'); + expect(targetRepoRelPath('container\\agent-runner\\src\\index.ts', 'ag-abc')).toBe( + 'data/v2-sessions/ag-abc/agent-runner-src/index.ts', + ); }); }); diff --git a/src/builder-agent/swap.ts b/src/builder-agent/swap.ts index 20dfa52..e8d382e 100644 --- a/src/builder-agent/swap.ts +++ b/src/builder-agent/swap.ts @@ -18,11 +18,7 @@ import path from 'path'; import { DATA_DIR } from '../config.js'; import { getAgentGroup } from '../db/agent-groups.js'; import { getDb } from '../db/connection.js'; -import { - getPendingSwap, - resetSwapForRetry, - setSwapPreSwapState, -} from '../db/pending-swaps.js'; +import { getPendingSwap, resetSwapForRetry, setSwapPreSwapState } from '../db/pending-swaps.js'; import { log } from '../log.js'; import type { PendingSwap } from '../types.js'; import { classifyDiff, type ClassifiedFile } from './classifier.js'; @@ -132,10 +128,7 @@ export function applySwapFiles(requestId: string): string[] { // Enumerate every path that changed in the reviewed commit relative // to main. Pairs each path with its A/M/D status. --no-renames keeps // the parsing simple (a rename shows up as D+A). - const nameStatus = git( - ['diff', '--name-status', '--no-renames', `main..${swap.commit_sha}`], - worktreePath, - ); + const nameStatus = git(['diff', '--name-status', '--no-renames', `main..${swap.commit_sha}`], worktreePath); const changes: Array<{ status: 'A' | 'M' | 'D'; path: string }> = []; for (const line of nameStatus.split('\n')) { @@ -158,9 +151,7 @@ export function applySwapFiles(requestId: string): string[] { }, ); - const statusByPath = new Map( - changes.map((c) => [c.path, c.status]), - ); + const statusByPath = new Map(changes.map((c) => [c.path, c.status])); const touchedAbs: string[] = []; for (const file of classified.files) { @@ -182,9 +173,7 @@ export function applySwapFiles(requestId: string): string[] { }); } catch (err) { throw new Error( - `git show ${swap.commit_sha}:${file.path} failed: ${ - err instanceof Error ? err.message : String(err) - }`, + `git show ${swap.commit_sha}:${file.path} failed: ${err instanceof Error ? err.message : String(err)}`, ); } const dir = path.dirname(dst); @@ -269,10 +258,7 @@ export function rollbackSwapFiles(swap: PendingSwap): void { // Record a forward-only revert commit so main's history shows what reverted. try { git(['add', '--', ...relPaths], PROJECT_ROOT); - git( - ['commit', '-m', `rollback ${swap.request_id}: deadman timeout`, '--', ...relPaths], - PROJECT_ROOT, - ); + git(['commit', '-m', `rollback ${swap.request_id}: deadman timeout`, '--', ...relPaths], PROJECT_ROOT); } catch (err) { const msg = err instanceof Error ? err.message : String(err); if (!(msg.includes('nothing to commit') || msg.includes('no changes added'))) { @@ -291,10 +277,7 @@ export function rollbackSwapFiles(swap: PendingSwap): void { * * Exported so tests can lock the mapping against the classifier's rules. */ -export function targetRepoRelPath( - worktreeRelPath: string, - originatingGroupId: string, -): string { +export function targetRepoRelPath(worktreeRelPath: string, originatingGroupId: string): string { const norm = worktreeRelPath.replace(/\\/g, '/'); if (norm.startsWith('container/agent-runner/src/')) { const rel = norm.slice('container/agent-runner/src/'.length); @@ -302,14 +285,7 @@ export function targetRepoRelPath( } if (norm.startsWith('container/skills/')) { const rel = norm.slice('container/skills/'.length); - return path.posix.join( - 'data', - 'v2-sessions', - originatingGroupId, - '.claude-shared', - 'skills', - rel, - ); + return path.posix.join('data', 'v2-sessions', originatingGroupId, '.claude-shared', 'skills', rel); } return norm; } diff --git a/src/builder-agent/worktree.ts b/src/builder-agent/worktree.ts index efdd976..566cd68 100644 --- a/src/builder-agent/worktree.ts +++ b/src/builder-agent/worktree.ts @@ -71,9 +71,7 @@ export function assertGitCleanEnoughForSwap(): void { const rebaseDir = path.join(gitDir, 'rebase-merge'); const rebaseApply = path.join(gitDir, 'rebase-apply'); if (fs.existsSync(rebaseDir) || fs.existsSync(rebaseApply)) { - throw new Error( - 'cannot start swap: git repo is mid-rebase. resolve it in the terminal first.', - ); + throw new Error('cannot start swap: git repo is mid-rebase. resolve it in the terminal first.'); } } @@ -86,10 +84,7 @@ export function assertGitCleanEnoughForSwap(): void { * or crash), it is removed first via `git worktree remove --force` so the * creation is clean. */ -export function createDevWorktree( - requestId: string, - originatingGroupId: string, -): string { +export function createDevWorktree(requestId: string, originatingGroupId: string): string { assertGitCleanEnoughForSwap(); if (!fs.existsSync(WORKTREES_DIR)) { @@ -128,14 +123,8 @@ export function createDevWorktree( // what the originating group is actually running, not the pristine // template. const sessDir = path.join(DATA_DIR, 'v2-sessions', originatingGroupId); - overlayDir( - path.join(sessDir, 'agent-runner-src'), - path.join(worktreePath, 'container', 'agent-runner', 'src'), - ); - overlayDir( - path.join(sessDir, '.claude-shared', 'skills'), - path.join(worktreePath, 'container', 'skills'), - ); + overlayDir(path.join(sessDir, 'agent-runner-src'), path.join(worktreePath, 'container', 'agent-runner', 'src')); + overlayDir(path.join(sessDir, '.claude-shared', 'skills'), path.join(worktreePath, 'container', 'skills')); // Shadow the .env with an empty placeholder so the dev agent can't read // credentials from a committed-but-gitignored file if one snuck into the diff --git a/src/container-config.ts b/src/container-config.ts index 7a758cd..e1366e3 100644 --- a/src/container-config.ts +++ b/src/container-config.ts @@ -95,10 +95,7 @@ export function writeContainerConfig(folder: string, config: ContainerConfig): v * result. Convenient for append-style changes like `install_packages` and * `add_mcp_server` handlers. */ -export function updateContainerConfig( - folder: string, - mutate: (config: ContainerConfig) => void, -): ContainerConfig { +export function updateContainerConfig(folder: string, mutate: (config: ContainerConfig) => void): ContainerConfig { const config = readContainerConfig(folder); mutate(config); writeContainerConfig(folder, config); diff --git a/src/db/agent-destinations.ts b/src/db/agent-destinations.ts index 32c7fd5..9215922 100644 --- a/src/db/agent-destinations.ts +++ b/src/db/agent-destinations.ts @@ -105,9 +105,7 @@ export function deleteDestination(agentGroupId: string, localName: string): void */ export function deleteAllDestinationsTouching(agentGroupId: string): void { getDb() - .prepare( - 'DELETE FROM agent_destinations WHERE agent_group_id = ? OR (target_type = ? AND target_id = ?)', - ) + .prepare('DELETE FROM agent_destinations WHERE agent_group_id = ? OR (target_type = ? AND target_id = ?)') .run(agentGroupId, 'agent', agentGroupId); } diff --git a/src/db/agent-groups.ts b/src/db/agent-groups.ts index c895c4a..db7e402 100644 --- a/src/db/agent-groups.ts +++ b/src/db/agent-groups.ts @@ -22,10 +22,7 @@ export function getAllAgentGroups(): AgentGroup[] { return getDb().prepare('SELECT * FROM agent_groups ORDER BY name').all() as AgentGroup[]; } -export function updateAgentGroup( - id: string, - updates: Partial>, -): void { +export function updateAgentGroup(id: string, updates: Partial>): void { const fields: string[] = []; const values: Record = { id }; diff --git a/src/db/migrations/007-pending-approvals-title-options.ts b/src/db/migrations/007-pending-approvals-title-options.ts index a2b0f8a..9beb978 100644 --- a/src/db/migrations/007-pending-approvals-title-options.ts +++ b/src/db/migrations/007-pending-approvals-title-options.ts @@ -31,13 +31,7 @@ export const migration007: Migration = { void col; }; - addIfMissing( - 'title', - `ALTER TABLE pending_approvals ADD COLUMN title TEXT NOT NULL DEFAULT ''`, - ); - addIfMissing( - 'options_json', - `ALTER TABLE pending_approvals ADD COLUMN options_json TEXT NOT NULL DEFAULT '[]'`, - ); + addIfMissing('title', `ALTER TABLE pending_approvals ADD COLUMN title TEXT NOT NULL DEFAULT ''`); + addIfMissing('options_json', `ALTER TABLE pending_approvals ADD COLUMN options_json TEXT NOT NULL DEFAULT '[]'`); }, }; diff --git a/src/db/pending-swaps.test.ts b/src/db/pending-swaps.test.ts index 51775fe..9f627e6 100644 --- a/src/db/pending-swaps.test.ts +++ b/src/db/pending-swaps.test.ts @@ -189,7 +189,9 @@ describe('pending-swaps bulk lookups', () => { createPendingSwap(makeSwap({ request_id: 'req-t3', status: 'rejected' })); createPendingSwap(makeSwap({ request_id: 'req-active', status: 'awaiting_confirmation' })); - const terminal = getTerminalSwaps().map((s) => s.request_id).sort(); + const terminal = getTerminalSwaps() + .map((s) => s.request_id) + .sort(); expect(terminal).toEqual(['req-t1', 'req-t2', 'req-t3']); }); }); diff --git a/src/db/pending-swaps.ts b/src/db/pending-swaps.ts index da927d4..2f82eb2 100644 --- a/src/db/pending-swaps.ts +++ b/src/db/pending-swaps.ts @@ -18,9 +18,7 @@ export function createPendingSwap(swap: PendingSwap): void { } export function getPendingSwap(requestId: string): PendingSwap | undefined { - return getDb().prepare('SELECT * FROM pending_swaps WHERE request_id = ?').get(requestId) as - | PendingSwap - | undefined; + return getDb().prepare('SELECT * FROM pending_swaps WHERE request_id = ?').get(requestId) as PendingSwap | undefined; } /** @@ -60,9 +58,7 @@ export function getSwapForDevAgent(devAgentId: string): PendingSwap | undefined * unexpected for group-level crashes). */ export function getAwaitingConfirmationSwaps(): PendingSwap[] { - return getDb() - .prepare(`SELECT * FROM pending_swaps WHERE status = 'awaiting_confirmation'`) - .all() as PendingSwap[]; + return getDb().prepare(`SELECT * FROM pending_swaps WHERE status = 'awaiting_confirmation'`).all() as PendingSwap[]; } /** All terminal-status swaps — used by the startup worktree-orphan sweep. */ @@ -76,11 +72,7 @@ export function updatePendingSwapStatus(requestId: string, status: SwapStatus): getDb().prepare('UPDATE pending_swaps SET status = ? WHERE request_id = ?').run(status, requestId); } -export function setSwapPreSwapState( - requestId: string, - preSwapSha: string, - dbSnapshotPath: string, -): void { +export function setSwapPreSwapState(requestId: string, preSwapSha: string, dbSnapshotPath: string): void { getDb() .prepare( `UPDATE pending_swaps @@ -109,17 +101,11 @@ export function startSwapDeadman( } export function extendSwapDeadman(requestId: string, expiresAt: string): void { - getDb().prepare('UPDATE pending_swaps SET deadman_expires_at = ? WHERE request_id = ?').run( - expiresAt, - requestId, - ); + getDb().prepare('UPDATE pending_swaps SET deadman_expires_at = ? WHERE request_id = ?').run(expiresAt, requestId); } export function setSwapHandshakeState(requestId: string, state: SwapHandshakeState): void { - getDb().prepare('UPDATE pending_swaps SET handshake_state = ? WHERE request_id = ?').run( - state, - requestId, - ); + getDb().prepare('UPDATE pending_swaps SET handshake_state = ? WHERE request_id = ?').run(state, requestId); } export function deletePendingSwap(requestId: string): void { diff --git a/src/index.ts b/src/index.ts index ffe8940..7935487 100644 --- a/src/index.ts +++ b/src/index.ts @@ -506,7 +506,9 @@ async function handleSwapRequestApproval( await startDeadman(swapRequestId); if (isHostLevelSwap(swap)) { - notifyDev('Code change applied and committed. Triggering host restart so the new code takes effect. Awaiting user confirmation after restart.'); + notifyDev( + 'Code change applied and committed. Triggering host restart so the new code takes effect. Awaiting user confirmation after restart.', + ); log.warn('Host-level swap triggering process exit for supervisor respawn', { requestId: swapRequestId, }); @@ -520,7 +522,9 @@ async function handleSwapRequestApproval( if (originatingSession) { killContainer(originatingSession.id, 'swap applied'); } - notifyDev('Code change applied and committed. The originating agent will restart on its next message. Awaiting user confirmation.'); + notifyDev( + 'Code change applied and committed. The originating agent will restart on its next message. Awaiting user confirmation.', + ); } } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); diff --git a/src/router.ts b/src/router.ts index 061fa03..9cd8e63 100644 --- a/src/router.ts +++ b/src/router.ts @@ -155,13 +155,7 @@ export async function routeInbound(event: InboundEvent): Promise { // as soon as the agent goes idle — not when the container eventually // exits. Container-runner also calls stopTypingRefresh on exit as a // fast-path cleanup. - startTypingRefresh( - session.id, - session.agent_group_id, - event.channelType, - event.platformId, - event.threadId, - ); + startTypingRefresh(session.id, session.agent_group_id, event.channelType, event.platformId, event.threadId); // 8. Wake container const freshSession = getSession(session.id); @@ -206,9 +200,10 @@ function extractAndUpsertUser(event: InboundEvent): string | null { // `senderId` or `sender` directly at the top level. Check all three. const senderIdField = typeof content.senderId === 'string' ? content.senderId : undefined; const senderField = typeof content.sender === 'string' ? content.sender : undefined; - const author = typeof content.author === 'object' && content.author !== null - ? (content.author as Record) - : undefined; + const author = + typeof content.author === 'object' && content.author !== null + ? (content.author as Record) + : undefined; const authorUserId = typeof author?.userId === 'string' ? (author.userId as string) : undefined; const senderName = (typeof content.senderName === 'string' ? content.senderName : undefined) ?? diff --git a/src/types.ts b/src/types.ts index 0192b76..c87d0e6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -189,12 +189,7 @@ export type SwapClassification = 'group' | 'host' | 'combined'; * pending_approval → awaiting_confirmation → (finalized | rolled_back | rejected) * `rejected` is also reachable directly from pending_approval. */ -export type SwapStatus = - | 'pending_approval' - | 'awaiting_confirmation' - | 'finalized' - | 'rolled_back' - | 'rejected'; +export type SwapStatus = 'pending_approval' | 'awaiting_confirmation' | 'finalized' | 'rolled_back' | 'rejected'; /** * Deadman handshake state — only meaningful while status = awaiting_confirmation.