Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions src/cockpit/actions.js
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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.',
),
};
Expand All @@ -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);
Expand All @@ -59,5 +74,6 @@ function startAgentLane(options = {}, deps = {}) {
module.exports = {
startAgentLane,
normalizeStartResult,
parseAgentBranchStartMetadata,
resolveStartImplementation,
};
83 changes: 75 additions & 8 deletions test/cockpit-actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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, {
Expand All @@ -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' },
Expand All @@ -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',
Expand Down
Loading