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
12 changes: 12 additions & 0 deletions src/automation/runnerExecution.followups.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ describe('fileReviewerFollowups (INT-1704)', () => {
expect(create.mock.calls[0][3]).toMatchObject({ priority: 3 });
});

it('creates top-level (standalone) issues when no parent is given (INT-1968)', async () => {
const createSubIssue = vi.fn(async () => ({}));
const createTask = vi.fn(async () => ({}));
const src = { createSubIssue, createTask } as unknown as ITaskSource;
const filed = await fileReviewerFollowups(src, undefined, review(), { autoFile: true, projectId: 'proj-1' });
expect(filed).toBe(2);
expect(createSubIssue).not.toHaveBeenCalled();
expect(createTask).toHaveBeenCalledTimes(2);
expect(createTask.mock.calls[0][0]).toBe('[test] add edge-case coverage'); // title
expect(createTask.mock.calls[0][2]).toBe('proj-1'); // projectId
});

it('does nothing when the reviewer did not approve', async () => {
const create = vi.fn(async () => ({}));
expect(await fileReviewerFollowups(mockSource(create), 'INT-1', review({ decision: 'reject' }), { autoFile: true })).toBe(0);
Expand Down
25 changes: 15 additions & 10 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 @@ -254,31 +254,36 @@
* have already created the parent issue (`parentIssueId`).
*/
/**
* File the reviewer's recommendedActions as follow-up sub-issues when it
* approves (INT-1611 restore / INT-1704). Gated by `autoFile` (default OFF);
* File the reviewer's recommendedActions as follow-ups when it approves
* (INT-1611 restore / INT-1704). With a `parentIssueId` they become sub-issues;
* without one (INT-1968) they are created as top-level issues so review can still
* "just file them" off a non-issue branch. Gated by `autoFile` (default OFF);
* caps at 10; each create is best-effort (failures logged, never throw).
* Returns the count filed.
*/
export async function fileReviewerFollowups(
source: ITaskSource | null,
parentIssueId: string,
parentIssueId: string | null | undefined,
review: ReviewResult,
opts: { autoFile?: boolean; projectId?: string } = {},
): Promise<number> {
if (!opts.autoFile || !source || review.decision !== 'approve') return 0;
const actions = (review.recommendedActions ?? []).slice(0, 10);
let filed = 0;
for (const a of actions) {
const title = `[${a.type}] ${a.title}`;
const body = a.location
? `Follow-up from reviewer.\n\nLocation: ${a.location}`
: 'Follow-up recommended by the reviewer.';
try {
await source.createSubIssue(
parentIssueId,
`[${a.type}] ${a.title}`,
a.location ? `Follow-up from reviewer.\n\nLocation: ${a.location}` : 'Follow-up recommended by the reviewer.',
{ priority: 3, projectId: opts.projectId },
);
if (parentIssueId) {
await source.createSubIssue(parentIssueId, title, body, { priority: 3, projectId: opts.projectId });
} else {
await source.createTask(title, body, opts.projectId);
}
filed += 1;
} catch (err) {
console.error(`[Runner] follow-up sub-issue create failed (${a.title}):`, err);
console.error(`[Runner] follow-up issue create failed (${a.title}):`, err);
}
}
return filed;
Expand Down
8 changes: 4 additions & 4 deletions src/cli/reviewCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ describe('runReviewCommand --issues branch inference (INT-1967)', () => {
expect(getBranch).not.toHaveBeenCalled();
});

it('guides when --issues is set but the branch has no issue id', async () => {
const fileFollowups = vi.fn(async () => 1);
it('files standalone issues when --issues is set but the branch has no issue id (INT-1968)', async () => {
const fileFollowups = vi.fn(async () => 2);
const logs: string[] = [];
await runReviewCommand(
{ fileIssue: true },
Expand All @@ -95,8 +95,8 @@ describe('runReviewCommand --issues branch inference (INT-1967)', () => {
log: (l) => logs.push(l),
},
);
expect(fileFollowups).not.toHaveBeenCalled();
expect(logs.join('\n')).toMatch(/no issue could be inferred/);
expect(fileFollowups).toHaveBeenCalledWith(undefined, expect.anything()); // no parent → standalone
expect(logs.join('\n')).toMatch(/standalone follow-up issue/);
});
});

Expand Down
32 changes: 21 additions & 11 deletions src/cli/reviewCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ export function resolveIssueFromBranch(branch: string): string | undefined {
return m ? m[1].toUpperCase() : undefined;
}

/** Best-effort Linear project id from <repo>/openswarm.json, so standalone follow-ups land in the right project. (INT-1968) */
async function resolveProjectId(cwd: string): Promise<string | undefined> {
try {
const { loadRepoMetadata } = await import('../support/repoMetadata.js');
const meta = await loadRepoMetadata(cwd);
return meta?.linear?.projectId;
} catch {
return undefined;
}
}

export interface ReviewCommandOptions {
/** Project path (default cwd). */
path?: string;
Expand All @@ -98,7 +109,7 @@ export async function runReviewCommand(
deps: {
getChangedFiles?: (cwd: string) => Promise<string[]>;
review?: (wr: WorkerResult, cwd: string, onLog?: (line: string) => void) => Promise<ReviewResult>;
fileFollowups?: (parentIssueId: string, review: ReviewResult) => Promise<number>;
fileFollowups?: (parentIssueId: string | undefined, review: ReviewResult) => Promise<number>;
log?: (line: string) => void;
/** Override the progress indicator (default: TTY-gated spinner). Tests pass a stub. */
startProgress?: () => { note: (line: string) => void; stop: () => void } | null;
Expand Down Expand Up @@ -168,21 +179,20 @@ export async function runReviewCommand(
parent = resolveIssueFromBranch(branch);
if (parent) log(`Filing follow-ups under ${parent} (inferred from branch "${branch}").`);
}
if (!parent) {
log(
`\n${followups} follow-up(s) suggested, but no issue could be inferred from the branch. ` +
'Re-run with `--issues <issue-id>` to choose the parent.',
);
return result;
}
// No parent → create top-level (standalone) issues rather than refusing. (INT-1968)
const fileFollowups =
deps.fileFollowups ??
(async (p: string, r: ReviewResult) => {
(async (p: string | undefined, r: ReviewResult) => {
const { fileReviewerFollowups, getTaskSource } = await import('../automation/runnerExecution.js');
return fileReviewerFollowups(getTaskSource(), p, r, { autoFile: true });
const projectId = p ? undefined : await resolveProjectId(cwd);
return fileReviewerFollowups(getTaskSource(), p, r, { autoFile: true, projectId });
});
const filed = await fileFollowups(parent, result);
log(`Filed ${filed} follow-up sub-issue(s) under ${parent}.`);
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).`,
);
} 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