diff --git a/src/cockpit/actions.js b/src/cockpit/actions.js index 47cc2d41..aaba3354 100644 --- a/src/cockpit/actions.js +++ b/src/cockpit/actions.js @@ -1,5 +1,15 @@ 'use strict'; +function parseAgentBranchStartMetadata(output) { + const outputText = String(output || ''); + const branchMatch = outputText.match(/^\[agent-branch-start\] (?:Created branch|Reusing existing branch): (.+)$/m); + const worktreeMatch = outputText.match(/^\[agent-branch-start\] Worktree: (.+)$/m); + return { + branch: branchMatch ? branchMatch[1].trim() : undefined, + worktreePath: worktreeMatch ? worktreeMatch[1].trim() : undefined, + }; +} + function firstString(...values) { for (const value of values) { if (typeof value === 'string' && value.length > 0) return value; @@ -9,17 +19,21 @@ function firstString(...values) { function normalizeStartResult(result) { const payload = result && typeof result === 'object' ? result : {}; + const metadata = parseAgentBranchStartMetadata(payload.stdout); const ok = Object.prototype.hasOwnProperty.call(payload, 'ok') ? Boolean(payload.ok) + : typeof payload.status === 'number' + ? payload.status === 0 : true; return { ok, sessionId: firstString(payload.sessionId, payload.session?.id, payload.id), - branch: firstString(payload.branch, payload.lane?.branch), - worktreePath: firstString(payload.worktreePath, payload.worktree?.path, payload.path), + branch: firstString(payload.branch, payload.lane?.branch, metadata.branch), + worktreePath: firstString(payload.worktreePath, payload.worktree?.path, payload.path, metadata.worktreePath), message: firstString( payload.message, + ok ? payload.stdout : payload.stderr, ok ? 'Started agent lane.' : 'Failed to start agent lane.', ), }; @@ -40,14 +54,15 @@ function resolveStartImplementation(deps = {}) { } function startAgentLane(options = {}, deps = {}) { - const request = { + const repoRoot = firstString(options.repoRoot, deps.repoRoot, process.cwd()); + const normalizedOptions = { task: options.task, agent: options.agent, base: options.base, - claims: options.claims, + claims: Array.isArray(options.claims) ? options.claims : [], }; const startImplementation = resolveStartImplementation(deps); - const result = startImplementation(request); + const result = startImplementation(repoRoot, normalizedOptions); if (result && typeof result.then === 'function') { return result.then(normalizeStartResult); @@ -59,5 +74,6 @@ function startAgentLane(options = {}, deps = {}) { module.exports = { startAgentLane, normalizeStartResult, + parseAgentBranchStartMetadata, resolveStartImplementation, }; diff --git a/test/cockpit-actions.test.js b/test/cockpit-actions.test.js index 544885d3..39ee5693 100644 --- a/test/cockpit-actions.test.js +++ b/test/cockpit-actions.test.js @@ -20,8 +20,9 @@ test('startAgentLane delegates to the gx agents start implementation', () => { claims: ['src/foo.js', 'test/foo.test.js'], }, { - startImplementation(request) { - calls.push(request); + repoRoot: '/repo', + startImplementation(repoRoot, request) { + calls.push({ repoRoot, request }); return { ok: true, sessionId: 'session-123', @@ -35,10 +36,13 @@ test('startAgentLane delegates to the gx agents start implementation', () => { assert.deepEqual(calls, [ { - task: 'fix flaky test', - agent: 'codex', - base: 'main', - claims: ['src/foo.js', 'test/foo.test.js'], + repoRoot: '/repo', + request: { + task: 'fix flaky test', + agent: 'codex', + base: 'main', + claims: ['src/foo.js', 'test/foo.test.js'], + }, }, ]); assert.deepEqual(result, { @@ -50,11 +54,63 @@ test('startAgentLane delegates to the gx agents start implementation', () => { }); }); +test('startAgentLane falls back to cwd repo root and normalizes missing claims', () => { + const calls = []; + const previousCwd = process.cwd(); + + try { + process.chdir(__dirname); + const result = startAgentLane( + { + task: 'fix auth', + agent: 'codex', + base: 'main', + }, + { + startImplementation(repoRoot, request) { + calls.push({ repoRoot, request }); + return { + status: 0, + stdout: + '[agent-branch-start] Created branch: agent/codex/fix-auth\n' + + '[agent-branch-start] Worktree: /repo/.omx/agent-worktrees/fix-auth\n', + }; + }, + }, + ); + + assert.deepEqual(calls, [ + { + repoRoot: __dirname, + request: { + task: 'fix auth', + agent: 'codex', + base: 'main', + claims: [], + }, + }, + ]); + assert.deepEqual(result, { + ok: true, + sessionId: undefined, + branch: 'agent/codex/fix-auth', + worktreePath: '/repo/.omx/agent-worktrees/fix-auth', + message: + '[agent-branch-start] Created branch: agent/codex/fix-auth\n' + + '[agent-branch-start] Worktree: /repo/.omx/agent-worktrees/fix-auth\n', + }); + } finally { + process.chdir(previousCwd); + } +}); + test('startAgentLane normalizes async implementation results', async () => { + const calls = []; const result = await startAgentLane( - { task: 'ship feature', agent: 'claude', base: 'dev', claims: [] }, + { repoRoot: '/repo', task: 'ship feature', agent: 'claude', base: 'dev', claims: [] }, { - async startAgentLane() { + async startAgentLane(repoRoot, request) { + calls.push({ repoRoot, request }); return { session: { id: 'session-async' }, lane: { branch: 'agent/claude/ship-feature' }, @@ -64,6 +120,17 @@ test('startAgentLane normalizes async implementation results', async () => { }, ); + assert.deepEqual(calls, [ + { + repoRoot: '/repo', + request: { + task: 'ship feature', + agent: 'claude', + base: 'dev', + claims: [], + }, + }, + ]); assert.deepEqual(result, { ok: true, sessionId: 'session-async',