diff --git a/src/automation/runnerExecution.followups.test.ts b/src/automation/runnerExecution.followups.test.ts index 7574108..d401cae 100644 --- a/src/automation/runnerExecution.followups.test.ts +++ b/src/automation/runnerExecution.followups.test.ts @@ -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}` })); diff --git a/src/automation/runnerExecution.ts b/src/automation/runnerExecution.ts index 9033667..b4bd9c3 100644 --- a/src/automation/runnerExecution.ts +++ b/src/automation/runnerExecution.ts @@ -265,9 +265,13 @@ export async function fileReviewerFollowups( source: ITaskSource | null, parentIssueId: string | null | undefined, review: ReviewResult, - opts: { autoFile?: boolean; projectId?: string } = {}, + opts: { autoFile?: boolean; projectId?: string; requireApprove?: boolean } = {}, ): Promise { - 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) { diff --git a/src/cli/reviewCommand.test.ts b/src/cli/reviewCommand.test.ts index 514ea03..1c408b7 100644 --- a/src/cli/reviewCommand.test.ts +++ b/src/cli/reviewCommand.test.ts @@ -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'); diff --git a/src/cli/reviewCommand.ts b/src/cli/reviewCommand.ts index a2dfc59..8142a32 100644 --- a/src/cli/reviewCommand.ts +++ b/src/cli/reviewCommand.ts @@ -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. */ @@ -87,6 +88,40 @@ async function resolveProjectId(cwd: string): Promise { } } +/** + * 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 { + 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; @@ -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 \` 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 \` 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 \`).`);