diff --git a/src/agents/start.js b/src/agents/start.js index 05bdc483..6fc0beb5 100644 --- a/src/agents/start.js +++ b/src/agents/start.js @@ -10,7 +10,6 @@ const { resolveAgent } = require('./registry'); const { createAgentSession, listAgentSessions, - sessionFilePath, updateAgentSession, } = require('./sessions'); @@ -157,7 +156,7 @@ function writeAgentSession(repoRoot, options, metadata, status, extra = {}) { } function writeClaimFailedSession(repoRoot, options, metadata, claimResult) { - const session = writeAgentSession(repoRoot, options, metadata, 'claim-failed', { + return writeAgentSession(repoRoot, options, metadata, 'claim-failed', { claimFailure: { claims: options.claims, exitCode: typeof claimResult.status === 'number' ? claimResult.status : 1, @@ -165,7 +164,11 @@ function writeClaimFailedSession(repoRoot, options, metadata, claimResult) { stdout: String(claimResult.stdout || '').trim(), }, }); - return session ? sessionFilePath(repoRoot, session.id) : ''; +} + +function appendSessionId(stdout, session) { + if (!session?.id) return stdout; + return `${stdout}[${TOOL_NAME}] Agent session id: ${session.id}\n`; } function buildBranchStartArgs(options) { @@ -176,14 +179,14 @@ function buildBranchStartArgs(options) { return args; } -function buildRecoveryLines(metadata, claims, sessionPath) { +function buildRecoveryLines(metadata, claims, session) { const quotedClaims = claims.map((claim) => JSON.stringify(claim)).join(' '); const lines = [ `[${TOOL_NAME}] Claim failed after branch/worktree creation.`, `[${TOOL_NAME}] Session status: claim-failed`, ]; - if (sessionPath) { - lines.push(`[${TOOL_NAME}] Session record: ${sessionPath}`); + if (session?.id) { + lines.push(`[${TOOL_NAME}] Agent session id: ${session.id}`); } if (metadata.worktreePath) { lines.push(`[${TOOL_NAME}] Recovery: cd ${JSON.stringify(metadata.worktreePath)}`); @@ -215,8 +218,9 @@ function startAgentLane(repoRoot, options) { } const metadata = extractAgentBranchStartMetadata(stdout); + const session = writeAgentSession(repoRoot, options, metadata, 'active'); + stdout = appendSessionId(stdout, session); if (options.claims.length === 0) { - writeAgentSession(repoRoot, options, metadata, 'active'); return { status: 0, stdout, stderr }; } @@ -236,15 +240,14 @@ function startAgentLane(repoRoot, options) { stdout += String(claimResult.stdout || ''); stderr += String(claimResult.stderr || ''); if (!isSpawnFailure(claimResult) && claimResult.status === 0) { - writeAgentSession(repoRoot, options, metadata, 'active'); return { status: 0, stdout, stderr }; } if (isSpawnFailure(claimResult)) { stderr += `${claimResult.error.message}\n`; } - const sessionPath = writeClaimFailedSession(repoRoot, options, metadata, claimResult); - stdout += buildRecoveryLines(metadata, options.claims, sessionPath); + const failedSession = writeClaimFailedSession(repoRoot, options, metadata, claimResult); + stdout += buildRecoveryLines(metadata, options.claims, failedSession); return { status: typeof claimResult.status === 'number' ? claimResult.status : 1, stdout, diff --git a/test/agents-start-claims.test.js b/test/agents-start-claims.test.js index 1e2df45a..59098ed0 100644 --- a/test/agents-start-claims.test.js +++ b/test/agents-start-claims.test.js @@ -99,6 +99,7 @@ defineSpawnSuite('agents start claim suite', () => { assert.notEqual(result.status, 0, 'claim failure should fail the command'); assert.match(result.stderr, /Path is outside repository/); assert.match(result.stdout, /Session status: claim-failed/); + assert.match(result.stdout, /Agent session id: /); assert.match(result.stdout, /Recovery: cd /); assert.match(result.stdout, /Recovery: gx locks claim --branch /); diff --git a/test/agents-start.test.js b/test/agents-start.test.js new file mode 100644 index 00000000..1d7b4852 --- /dev/null +++ b/test/agents-start.test.js @@ -0,0 +1,216 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const Module = require('node:module'); + +function loadStartWithMocks({ + runPackageAsset, + createAgentSession, + updateAgentSession, + currentBranchName, + listAgentSessions = () => [], +}) { + const startPath = require.resolve('../src/agents/start'); + const runtimePath = require.resolve('../src/core/runtime'); + const sessionsPath = require.resolve('../src/agents/sessions'); + const gitPath = require.resolve('../src/git'); + const originalLoad = Module._load; + + delete require.cache[startPath]; + Module._load = function mockLoad(request, parent, isMain) { + const resolved = Module._resolveFilename(request, parent, isMain); + if (resolved === runtimePath) { + return { runPackageAsset }; + } + if (resolved === sessionsPath) { + return { createAgentSession, updateAgentSession, listAgentSessions }; + } + if (resolved === gitPath) { + return { currentBranchName }; + } + return originalLoad.apply(this, arguments); + }; + + try { + return require(startPath); + } finally { + Module._load = originalLoad; + delete require.cache[startPath]; + } +} + +function branchStartOutput(branch = 'agent/codex/fix-auth', worktreePath = '/repo/.omx/agent-worktrees/repo__codex__fix-auth') { + return [ + `[agent-branch-start] Created branch: ${branch}`, + `[agent-branch-start] Worktree: ${worktreePath}`, + '', + ].join('\n'); +} + +test('agents start creates canonical session after successful branch start', () => { + const runCalls = []; + const created = []; + const start = loadStartWithMocks({ + runPackageAsset(assetKey, args, options) { + runCalls.push({ assetKey, args, options }); + return { status: 0, stdout: branchStartOutput(), stderr: '' }; + }, + createAgentSession(repoRoot, payload) { + created.push({ repoRoot, payload }); + return { + id: 'session-1', + ...payload, + createdAt: '2026-04-29T20:00:00.000Z', + updatedAt: '2026-04-29T20:00:00.000Z', + }; + }, + updateAgentSession() { + throw new Error('unexpected update'); + }, + currentBranchName: () => 'main', + }); + + const result = start.startAgentLane('/repo', { + task: 'fix auth', + agent: 'codex', + base: 'main', + claims: [], + }); + + assert.equal(result.status, 0); + assert.deepEqual(created, [ + { + repoRoot: '/repo', + payload: { + task: 'fix auth', + agent: 'codex', + id: 'agent__codex__fix-auth', + branch: 'agent/codex/fix-auth', + worktreePath: '/repo/.omx/agent-worktrees/repo__codex__fix-auth', + base: 'main', + status: 'active', + }, + }, + ]); + assert.equal(runCalls.length, 1); +}); + +test('agents start branch failure creates no session', () => { + let createCount = 0; + const start = loadStartWithMocks({ + runPackageAsset() { + return { status: 2, stdout: '', stderr: 'branch failed\n' }; + }, + createAgentSession() { + createCount += 1; + }, + updateAgentSession() { + throw new Error('unexpected update'); + }, + currentBranchName: () => 'main', + }); + + const result = start.startAgentLane('/repo', { + task: 'fix auth', + agent: 'codex', + base: 'main', + claims: [], + }); + + assert.equal(result.status, 2); + assert.equal(createCount, 0); +}); + +test('agents start claim failure updates canonical session to claim-failed', () => { + const runCalls = []; + const created = []; + const updates = []; + const start = loadStartWithMocks({ + runPackageAsset(assetKey, args, options) { + runCalls.push({ assetKey, args, options }); + if (assetKey === 'branchStart') { + return { status: 0, stdout: branchStartOutput(), stderr: '' }; + } + return { status: 1, stdout: '', stderr: 'claim failed\n' }; + }, + createAgentSession(repoRoot, payload) { + created.push({ repoRoot, payload }); + return { + ...payload, + createdAt: '2026-04-29T20:00:00.000Z', + updatedAt: '2026-04-29T20:00:00.000Z', + }; + }, + updateAgentSession(repoRoot, sessionId, patch) { + updates.push({ repoRoot, sessionId, patch }); + return { + id: sessionId, + status: patch.status, + }; + }, + currentBranchName: () => 'main', + listAgentSessions: () => created.map((entry) => entry.payload), + }); + + const result = start.startAgentLane('/repo', { + task: 'fix auth', + agent: 'codex', + base: 'main', + claims: ['src/auth.js'], + }); + + assert.equal(result.status, 1); + assert.equal(created.length, 1); + assert.deepEqual(updates, [ + { + repoRoot: '/repo', + sessionId: 'agent__codex__fix-auth', + patch: { + id: 'agent__codex__fix-auth', + task: 'fix auth', + agent: 'codex', + branch: 'agent/codex/fix-auth', + worktreePath: '/repo/.omx/agent-worktrees/repo__codex__fix-auth', + base: 'main', + status: 'claim-failed', + claimFailure: { + claims: ['src/auth.js'], + exitCode: 1, + stderr: 'claim failed', + stdout: '', + }, + }, + }, + ]); + assert.deepEqual(runCalls[1], { + assetKey: 'lockTool', + args: ['claim', '--branch', 'agent/codex/fix-auth', 'src/auth.js'], + options: { cwd: '/repo/.omx/agent-worktrees/repo__codex__fix-auth' }, + }); + assert.match(result.stdout, /Session status: claim-failed/); +}); + +test('agents start output includes canonical session id', () => { + const start = loadStartWithMocks({ + runPackageAsset() { + return { status: 0, stdout: branchStartOutput(), stderr: '' }; + }, + createAgentSession(repoRoot, payload) { + return { + ...payload, + }; + }, + updateAgentSession() { + throw new Error('unexpected update'); + }, + currentBranchName: () => 'main', + }); + + const result = start.startAgentLane('/repo', { + task: 'fix auth', + agent: 'codex', + base: 'main', + claims: [], + }); + + assert.match(result.stdout, /\[gitguardex\] Agent session id: agent__codex__fix-auth/); +});