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
10 changes: 10 additions & 0 deletions src/automation/runnerExecution.followups.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ describe('fileReviewerFollowups (INT-1704)', () => {
expect(create).not.toHaveBeenCalled();
});

it('files regardless of decision when requireApprove is false (INT-1969)', async () => {
const create = vi.fn(async () => ({}));
const filed = await fileReviewerFollowups(mockSource(create), 'INT-1', review({ decision: 'revise' }), {
autoFile: true,
requireApprove: false,
});
expect(filed).toBe(2);
expect(create).toHaveBeenCalledTimes(2);
});

it('caps at 10 actions', async () => {
const create = vi.fn(async () => ({}));
const many = Array.from({ length: 14 }, (_, i) => ({ type: 'test', title: `t${i}` }));
Expand Down
8 changes: 6 additions & 2 deletions src/automation/runnerExecution.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// ============================================
// OpenSwarm - Runner Execution Helpers
// Execution/reporting/integration logic extracted from AutonomousRunner
Expand Down Expand Up @@ -265,9 +265,13 @@
source: ITaskSource | null,
parentIssueId: string | null | undefined,
review: ReviewResult,
opts: { autoFile?: boolean; projectId?: string } = {},
opts: { autoFile?: boolean; projectId?: string; requireApprove?: boolean } = {},
): Promise<number> {
if (!opts.autoFile || !source || review.decision !== 'approve') return 0;
// Autonomous pipeline files only on approve; the manual `review` command files
// regardless of decision (requireApprove: false). (INT-1704 / INT-1969)
const requireApprove = opts.requireApprove ?? true;
if (!opts.autoFile || !source) return 0;
if (requireApprove && review.decision !== 'approve') return 0;
const actions = (review.recommendedActions ?? []).slice(0, 10);
let filed = 0;
for (const a of actions) {
Expand Down
16 changes: 16 additions & 0 deletions src/cli/reviewCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@ describe('runReviewCommand --issues branch inference (INT-1967)', () => {
expect(logs.join('\n')).toContain('inferred from branch');
});

it('warns to connect Linear when nothing is filed (INT-1969)', async () => {
const logs: string[] = [];
await runReviewCommand(
{ fileIssue: true },
{
getChangedFiles: async () => ['x.ts'],
review: approveWithFollowups,
getBranch: async () => 'main',
fileFollowups: async () => 0, // e.g. Linear not configured
startProgress: () => null,
log: (l) => logs.push(l),
},
);
expect(logs.join('\n')).toMatch(/Linear connected|auth login/);
});

it('uses an explicit id over branch inference', async () => {
const fileFollowups = vi.fn(async () => 1);
const getBranch = vi.fn(async () => 'feat/int-9999-x');
Expand Down
59 changes: 52 additions & 7 deletions src/cli/reviewCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
// command shell wires git + reviewer + Linear.

import type { ReviewResult, WorkerResult } from '../agents/agentPair.js';
import type { ITaskSource } from '../automation/taskSource.js';
import { startReviewProgress } from './reviewProgress.js';

/** Synthesize a WorkerResult describing the working-tree changes for the reviewer. */
Expand Down Expand Up @@ -87,6 +88,40 @@ async function resolveProjectId(cwd: string): Promise<string | undefined> {
}
}

/**
* A Linear-backed task source for the standalone `review` CLI. The daemon
* registers one at startup, but a bare `openswarm review` does not — so init
* Linear from config (OAuth profile or apiKey) and build a LinearTaskSource.
* Returns null when Linear isn't configured. (INT-1969)
*/
async function ensureTaskSource(): Promise<ITaskSource | null> {
const { getTaskSource } = await import('../automation/runnerExecution.js');
const existing = getTaskSource();
if (existing) return existing;
try {
const linear = await import('../linear/linear.js');
if (!linear.isLinearInitialized()) {
const { loadConfig } = await import('../core/config.js');
const config = loadConfig();
if (config.linearTeamId) {
const { AuthProfileStore, ensureValidToken } = await import('../auth/index.js');
const authStore = new AuthProfileStore();
if (authStore.getProfile('linear:default')) {
const token = await ensureValidToken(authStore, 'linear:default');
linear.initLinear(token, config.linearTeamId, true);
} else if (config.linearApiKey) {
linear.initLinear(config.linearApiKey, config.linearTeamId);
}
}
}
if (!linear.isLinearInitialized()) return null;
const { LinearTaskSource } = await import('../automation/taskSource.js');
return new LinearTaskSource(async () => []); // fetch unused for filing
} catch {
return null;
}
}

export interface ReviewCommandOptions {
/** Project path (default cwd). */
path?: string;
Expand Down Expand Up @@ -180,19 +215,29 @@ export async function runReviewCommand(
if (parent) log(`Filing follow-ups under ${parent} (inferred from branch "${branch}").`);
}
// No parent → create top-level (standalone) issues rather than refusing. (INT-1968)
// Default path initializes a Linear task source itself (the daemon isn't
// running here), and files regardless of decision. (INT-1969)
const fileFollowups =
deps.fileFollowups ??
(async (p: string | undefined, r: ReviewResult) => {
const { fileReviewerFollowups, getTaskSource } = await import('../automation/runnerExecution.js');
const { fileReviewerFollowups } = await import('../automation/runnerExecution.js');
const source = await ensureTaskSource();
if (!source) return 0;
const projectId = p ? undefined : await resolveProjectId(cwd);
return fileReviewerFollowups(getTaskSource(), p, r, { autoFile: true, projectId });
return fileReviewerFollowups(source, p, r, { autoFile: true, projectId, requireApprove: false });
});
const filed = await fileFollowups(parent, result);
log(
parent
? `Filed ${filed} follow-up sub-issue(s) under ${parent}.`
: `Filed ${filed} standalone follow-up issue(s) (no issue id on the branch — pass \`--issues <id>\` to nest them).`,
);
if (filed > 0) {
log(
parent
? `Filed ${filed} follow-up sub-issue(s) under ${parent}.`
: `Filed ${filed} standalone follow-up issue(s) (pass \`--issues <id>\` to nest them under an issue).`,
);
} else {
log(
`Could not file follow-ups (0 created). Is Linear connected? Run \`openswarm auth login --provider linear\` (or set linearApiKey in config).`,
);
}
} else if (followups) {
// Suggestions were made but nothing was filed — make the flag discoverable. (INT-1966/1967)
log(`\n${followups} follow-up(s) suggested. Re-run with \`--issues\` to create them as Linear sub-issues (parent inferred from the branch, or pass \`--issues <id>\`).`);
Expand Down
Loading