style: apply prettier formatting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
{"sessionId":"56e89c33-b844-4e6a-8df3-2210b2fb4a4d","pid":47993,"acquiredAt":1775696579277}
|
||||
{"sessionId":"bedd47ed-bfa0-41da-9a03-93d41159b4cd","pid":24606,"acquiredAt":1776194767342}
|
||||
@@ -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` +
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
}
|
||||
|
||||
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<void> {
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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> = {}): 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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string, 'A' | 'M' | 'D'>(
|
||||
changes.map((c) => [c.path, c.status]),
|
||||
);
|
||||
const statusByPath = new Map<string, 'A' | 'M' | 'D'>(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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Pick<AgentGroup, 'name' | 'agent_provider'>>,
|
||||
): void {
|
||||
export function updateAgentGroup(id: string, updates: Partial<Pick<AgentGroup, 'name' | 'agent_provider'>>): void {
|
||||
const fields: string[] = [];
|
||||
const values: Record<string, unknown> = { id };
|
||||
|
||||
|
||||
@@ -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 '[]'`);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -155,13 +155,7 @@ export async function routeInbound(event: InboundEvent): Promise<void> {
|
||||
// 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<string, unknown>)
|
||||
: undefined;
|
||||
const author =
|
||||
typeof content.author === 'object' && content.author !== null
|
||||
? (content.author as Record<string, unknown>)
|
||||
: undefined;
|
||||
const authorUserId = typeof author?.userId === 'string' ? (author.userId as string) : undefined;
|
||||
const senderName =
|
||||
(typeof content.senderName === 'string' ? content.senderName : undefined) ??
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user