opencode-magi 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,26 +61,33 @@ Add the following content to the configuration file.
61
61
  ```json
62
62
  {
63
63
  "$schema": "https://raw.githubusercontent.com/magi-ai/opencode-magi/main/schema.json",
64
- "review": {
65
- "agents": [
66
- {
67
- "account": "your-account-1",
68
- "model": "openai/gpt-5.5"
64
+ "agents": {
65
+ "refs": {
66
+ "account-1": {
67
+ "model": "openai/gpt-5.5",
68
+ "account": "account-1"
69
69
  },
70
- {
71
- "account": "your-account-2",
72
- "model": "anthropic/claude-opus-4-7"
70
+ "account-2": {
71
+ "model": "anthropic/claude-opus-4-7",
72
+ "account": "account-2"
73
73
  },
74
- {
75
- "account": "your-account-3",
76
- "model": "opencode/kimi-k2-6"
74
+ "account-3": {
75
+ "model": "opencode/kimi-k2-6",
76
+ "account": "account-3"
77
77
  }
78
+ }
79
+ },
80
+ "review": {
81
+ "agents": [
82
+ { "ref": "account-1" },
83
+ { "ref": "account-2" },
84
+ { "ref": "account-3" }
78
85
  ]
79
86
  }
80
87
  }
81
88
  ```
82
89
 
83
- `review.agents[].account` is the GitHub account used to post reviews and approvals. Must be authenticated with `gh auth token --user <account>` and must be unique.
90
+ After refs are expanded, `review.agents[].account` is the GitHub account used to post reviews and approvals. Must be authenticated with `gh auth token --user <account>` and must be unique.
84
91
 
85
92
  #### Set project config
86
93
 
@@ -101,53 +108,54 @@ Add the following content to the configuration file.
101
108
  "owner": "your-owner",
102
109
  "repo": "your-repo"
103
110
  },
104
- "review": {
105
- "agents": [
106
- {
107
- "account": "your-account-1",
108
- "model": "openai/gpt-5.5"
111
+ "agents": {
112
+ "refs": {
113
+ "account-1": {
114
+ "model": "openai/gpt-5.5",
115
+ "account": "account-1"
109
116
  },
110
- {
111
- "account": "your-account-2",
112
- "model": "anthropic/claude-opus-4-7"
117
+ "account-2": {
118
+ "model": "anthropic/claude-opus-4-7",
119
+ "account": "account-2"
113
120
  },
114
- {
115
- "account": "your-account-3",
116
- "model": "opencode/kimi-k2-6"
121
+ "account-3": {
122
+ "model": "opencode/kimi-k2-6",
123
+ "account": "account-3"
124
+ },
125
+ "account-4": {
126
+ "model": "openai/gpt-5.5",
127
+ "account": "account-4",
128
+ "author": {
129
+ "name": "account-4",
130
+ "email": "your-email@example.com"
131
+ }
117
132
  }
133
+ }
134
+ },
135
+ "review": {
136
+ "agents": [
137
+ { "ref": "account-1" },
138
+ { "ref": "account-2" },
139
+ { "ref": "account-3" }
118
140
  ]
119
141
  },
120
142
  "merge": {
121
- "editor": {
122
- "account": "your-editor-account",
123
- "model": "openai/gpt-5.5",
124
- "author": {
125
- "name": "your-account",
126
- "email": "your-email@example.com"
127
- }
128
- }
143
+ "editor": { "ref": "account-4" }
129
144
  },
130
145
  "triage": {
131
- "account": "your-triage-account",
146
+ "account": "account-5",
132
147
  "agents": [
133
- {
134
- "id": "general",
135
- "model": "openai/gpt-5.5"
136
- },
137
- {
138
- "id": "maintenance",
139
- "model": "anthropic/claude-opus-4-7"
140
- },
141
- {
142
- "id": "product",
143
- "model": "opencode/kimi-k2-6"
144
- }
148
+ { "ref": "account-1" },
149
+ { "ref": "account-2" },
150
+ { "ref": "account-3" }
145
151
  ]
146
152
  }
147
153
  }
148
154
  ```
149
155
 
150
- `review.agents[].account` is the GitHub account used to post reviews and approvals. Must be authenticated with `gh auth token --user <account>` and must be unique. `merge.editor.account` is used by `/magi:merge` to push fixes, close PRs, and merge PRs.
156
+ Entries with `ref` are expanded from `agents.refs`. Fields set alongside `ref` override fields from the preset.
157
+
158
+ After refs are expanded, `review.agents[].account` is the GitHub account used to post reviews and approvals. Must be authenticated with `gh auth token --user <account>` and must be unique. `merge.editor.account` is used by `/magi:merge` to push fixes, close PRs, and merge PRs.
151
159
 
152
160
  #### Validate config
153
161
 
@@ -157,6 +157,56 @@ function ghHostOption(config) {
157
157
  function isPlainObject(value) {
158
158
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
159
159
  }
160
+ function expandAgentRefUse(value, path, refs, refsInvalid, errors) {
161
+ if (!isPlainObject(value) || !Object.hasOwn(value, "ref"))
162
+ return value;
163
+ const use = { ...value };
164
+ const ref = use.ref;
165
+ delete use.ref;
166
+ if (typeof ref !== "string") {
167
+ errors.push(`${path}.ref must be a string`);
168
+ return use;
169
+ }
170
+ if (refsInvalid) {
171
+ errors.push(`agents.refs must be an object to resolve ${path}.ref`);
172
+ return use;
173
+ }
174
+ const preset = refs?.[ref];
175
+ if (preset == null) {
176
+ errors.push(`${path}.ref references unknown agents.refs preset: ${ref}`);
177
+ return use;
178
+ }
179
+ if (!isPlainObject(preset)) {
180
+ errors.push(`agents.refs.${ref} must be an object when referenced by ${path}.ref`);
181
+ return use;
182
+ }
183
+ const presetFields = { ...preset };
184
+ delete presetFields.ref;
185
+ return { ...presetFields, ...use };
186
+ }
187
+ function expandAgentRefs(config, errors) {
188
+ if (!config || typeof config !== "object")
189
+ return;
190
+ const magiConfig = config;
191
+ const agents = magiConfig.agents;
192
+ const refsValue = isPlainObject(agents) ? agents.refs : undefined;
193
+ const refsInvalid = refsValue != null && !isPlainObject(refsValue);
194
+ const refs = isPlainObject(refsValue) ? refsValue : undefined;
195
+ if (Array.isArray(magiConfig.review?.agents)) {
196
+ magiConfig.review.agents = magiConfig.review.agents.map((agent, index) => expandAgentRefUse(agent, `review.agents[${index}]`, refs, refsInvalid, errors));
197
+ }
198
+ if (isPlainObject(magiConfig.merge?.editor)) {
199
+ magiConfig.merge.editor = expandAgentRefUse(magiConfig.merge.editor, "merge.editor", refs, refsInvalid, errors);
200
+ }
201
+ if (Array.isArray(magiConfig.triage?.agents)) {
202
+ magiConfig.triage.agents = magiConfig.triage.agents.map((agent, index) => expandAgentRefUse(agent, `triage.agents[${index}]`, refs, refsInvalid, errors));
203
+ }
204
+ if (isPlainObject(magiConfig.triage?.creator)) {
205
+ magiConfig.triage.creator = expandAgentRefUse(magiConfig.triage.creator, "triage.creator", refs, refsInvalid, errors);
206
+ }
207
+ if (isPlainObject(magiConfig.agents))
208
+ delete magiConfig.agents.refs;
209
+ }
160
210
  function validateKnownKeys(value, path, keys, errors) {
161
211
  if (!isPlainObject(value))
162
212
  return;
@@ -792,6 +842,7 @@ export async function validateConfig(config, options = {}) {
792
842
  const warnings = [];
793
843
  if (!config || typeof config !== "object")
794
844
  errors.push("config must be an object");
845
+ expandAgentRefs(config, errors);
795
846
  if (config && typeof config === "object")
796
847
  validateJsonSchema(config, errors);
797
848
  validateKnownKeys(config, "config", CONFIG_KEYS, errors);
@@ -246,6 +246,12 @@ function duplicateReferences(text) {
246
246
  refs.add(Number(match[1]));
247
247
  return [...refs];
248
248
  }
249
+ function issueTitleSearchQuery(title, fallback) {
250
+ return (title
251
+ .replaceAll(/[^\p{L}\p{N}_]+/gu, " ")
252
+ .replaceAll(/\s+/g, " ")
253
+ .trim() || fallback);
254
+ }
249
255
  async function fetchIssueCandidate(exec, repository, number, whyCandidate) {
250
256
  const raw = await exec(`gh issue view ${number} --repo ${shellQuote(repoSpecifier(repository))} --json number,title,url,state,body,createdAt`).catch(() => undefined);
251
257
  if (!raw)
@@ -254,7 +260,7 @@ async function fetchIssueCandidate(exec, repository, number, whyCandidate) {
254
260
  return { ...data, whyCandidate };
255
261
  }
256
262
  export async function searchDuplicateIssues(exec, repository, issue, limit = 5) {
257
- const query = issue.title;
263
+ const query = issueTitleSearchQuery(issue.title, String(issue.number));
258
264
  const explicitCandidates = await Promise.all(duplicateReferences(issue.body)
259
265
  .filter((number) => number !== issue.number)
260
266
  .map((number) => fetchIssueCandidate(exec, repository, number, "Issue body explicitly references a duplicate target.")));
@@ -522,20 +528,15 @@ function findingComment(finding) {
522
528
  }
523
529
  return comment;
524
530
  }
525
- function requirementFindingSummary(finding) {
526
- return [
527
- `- Missing issue #${finding.issueNumber} requirement: ${finding.requirement}`,
528
- ` Evidence: ${finding.evidence}`,
529
- ` Fix: ${finding.fix}`,
530
- ].join("\n");
531
+ function changesRequestedBody(findings) {
532
+ return findings.length === 1
533
+ ? "Changes requested: 1 inline comment."
534
+ : `Changes requested: ${findings.length} inline comments.`;
531
535
  }
532
- export async function postChangesRequested(exec, repository, pr, account, findings, requirementFindings = []) {
536
+ export async function postChangesRequested(exec, repository, pr, account, findings) {
533
537
  const token = await ghToken(exec, repository, account);
534
538
  const payloadPath = join(tmpdir(), `magi-review-${process.pid}-${Date.now()}.json`);
535
- const body = findings
536
- .map((finding) => `- ${finding.issue.split("\n")[0]}`)
537
- .concat(requirementFindings.map(requirementFindingSummary))
538
- .join("\n");
539
+ const body = changesRequestedBody(findings);
539
540
  await writeFile(payloadPath, JSON.stringify({
540
541
  body,
541
542
  comments: findings.map(findingComment),
package/dist/index.js CHANGED
@@ -95,12 +95,17 @@ export function parseRunArguments(value, dryRun = false, command = "review") {
95
95
  const tokens = value.split(/[\s,]+/).filter(Boolean);
96
96
  const configOverrides = {};
97
97
  const prTokens = [];
98
+ let sync = false;
98
99
  for (let index = 0; index < tokens.length; index++) {
99
100
  const token = tokens[index];
100
101
  if (token === "--dry-run") {
101
102
  dryRun = true;
102
103
  continue;
103
104
  }
105
+ if (token === "--sync") {
106
+ sync = true;
107
+ continue;
108
+ }
104
109
  switch (token) {
105
110
  case "--language":
106
111
  setConfigOverride(configOverrides, ["language"], nextFlagValue(tokens, ++index, token));
@@ -146,18 +151,23 @@ export function parseRunArguments(value, dryRun = false, command = "review") {
146
151
  prTokens.push(token);
147
152
  }
148
153
  }
149
- return { configOverrides, dryRun, prs: parsePrs(prTokens.join(" ")) };
154
+ return { configOverrides, dryRun, prs: parsePrs(prTokens.join(" ")), sync };
150
155
  }
151
156
  export function parseIssueRunArguments(value, dryRun = false) {
152
157
  const tokens = value.split(/[\s,]+/).filter(Boolean);
153
158
  const configOverrides = {};
154
159
  const issueTokens = [];
160
+ let sync = false;
155
161
  for (let index = 0; index < tokens.length; index++) {
156
162
  const token = tokens[index];
157
163
  if (token === "--dry-run") {
158
164
  dryRun = true;
159
165
  continue;
160
166
  }
167
+ if (token === "--sync") {
168
+ sync = true;
169
+ continue;
170
+ }
161
171
  switch (token) {
162
172
  case "--language":
163
173
  setConfigOverride(configOverrides, ["language"], nextFlagValue(tokens, ++index, token));
@@ -195,7 +205,12 @@ export function parseIssueRunArguments(value, dryRun = false) {
195
205
  issueTokens.push(token);
196
206
  }
197
207
  }
198
- return { configOverrides, dryRun, issues: parseIssues(issueTokens.join(" ")) };
208
+ return {
209
+ configOverrides,
210
+ dryRun,
211
+ issues: parseIssues(issueTokens.join(" ")),
212
+ sync,
213
+ };
199
214
  }
200
215
  function nextFlagValue(tokens, index, flag) {
201
216
  const value = tokens[index];
@@ -203,6 +218,15 @@ function nextFlagValue(tokens, index, flag) {
203
218
  throw new Error(`${flag} requires a value.`);
204
219
  return value;
205
220
  }
221
+ async function syncResult(runManager, states) {
222
+ const output = await runManager.formatStatesWithReports(states, {
223
+ verbose: true,
224
+ });
225
+ const failed = states.filter((state) => state.status !== "completed");
226
+ if (failed.length)
227
+ throw new Error(output);
228
+ return output;
229
+ }
206
230
  function parseIntegerFlag(value, flag, minimum) {
207
231
  const parsed = Number.parseInt(value, 10);
208
232
  if (!Number.isInteger(parsed) ||
@@ -432,6 +456,7 @@ export const MagiPlugin = async ({ client, directory }) => {
432
456
  args: {
433
457
  prs: tool.schema.string(),
434
458
  dryRun: tool.schema.boolean().optional(),
459
+ sync: tool.schema.boolean().optional(),
435
460
  },
436
461
  async execute(args, context) {
437
462
  const parsed = parseRunArguments(args.prs, args.dryRun ?? false, "merge");
@@ -448,6 +473,7 @@ export const MagiPlugin = async ({ client, directory }) => {
448
473
  if (!validation.ok)
449
474
  return JSON.stringify(validation, null, 2);
450
475
  const repository = resolveRepository(config);
476
+ const sync = parsed.sync || args.sync === true;
451
477
  const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startMerge({
452
478
  config,
453
479
  dryRun: parsed.dryRun,
@@ -455,7 +481,10 @@ export const MagiPlugin = async ({ client, directory }) => {
455
481
  pr,
456
482
  parentSessionId: context.sessionID,
457
483
  signal: context.abort,
484
+ sync,
458
485
  }), { signal: context.abort });
486
+ if (sync)
487
+ return syncResult(runManager, states);
459
488
  return states
460
489
  .map((state) => `Started reviewing ${prMarkdownLink(repository, state.pr)}.`)
461
490
  .join("\n");
@@ -469,6 +498,7 @@ export const MagiPlugin = async ({ client, directory }) => {
469
498
  args: {
470
499
  prs: tool.schema.string(),
471
500
  dryRun: tool.schema.boolean().optional(),
501
+ sync: tool.schema.boolean().optional(),
472
502
  },
473
503
  async execute(args, context) {
474
504
  const parsed = parseRunArguments(args.prs, args.dryRun ?? false);
@@ -484,6 +514,7 @@ export const MagiPlugin = async ({ client, directory }) => {
484
514
  if (!validation.ok)
485
515
  return JSON.stringify(validation, null, 2);
486
516
  const repository = resolveRepository(config);
517
+ const sync = parsed.sync || args.sync === true;
487
518
  const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startReview({
488
519
  config,
489
520
  dryRun: parsed.dryRun,
@@ -491,7 +522,10 @@ export const MagiPlugin = async ({ client, directory }) => {
491
522
  pr,
492
523
  parentSessionId: context.sessionID,
493
524
  signal: context.abort,
525
+ sync,
494
526
  }), { signal: context.abort });
527
+ if (sync)
528
+ return syncResult(runManager, states);
495
529
  return states
496
530
  .map((state) => `Started reviewing ${prMarkdownLink(repository, state.pr)}.`)
497
531
  .join("\n");
@@ -502,6 +536,7 @@ export const MagiPlugin = async ({ client, directory }) => {
502
536
  args: {
503
537
  issues: tool.schema.string(),
504
538
  dryRun: tool.schema.boolean().optional(),
539
+ sync: tool.schema.boolean().optional(),
505
540
  },
506
541
  async execute(args, context) {
507
542
  const parsed = parseIssueRunArguments(args.issues, args.dryRun ?? false);
@@ -523,6 +558,7 @@ export const MagiPlugin = async ({ client, directory }) => {
523
558
  const repository = resolveRepository(config);
524
559
  if (!repository.triage)
525
560
  return JSON.stringify({ errors: ["triage configuration is required"], ok: false }, null, 2);
561
+ const sync = parsed.sync || args.sync === true;
526
562
  const states = await mapPool(parsed.issues, repository.triage.concurrency.runs, (issue) => runManager.startTriage({
527
563
  config,
528
564
  dryRun: parsed.dryRun,
@@ -530,7 +566,10 @@ export const MagiPlugin = async ({ client, directory }) => {
530
566
  parentSessionId: context.sessionID,
531
567
  repository,
532
568
  signal: context.abort,
569
+ sync,
533
570
  }), { signal: context.abort });
571
+ if (sync)
572
+ return syncResult(runManager, states);
534
573
  return states
535
574
  .map((state) => `Started triaging ${issueMarkdownLink(repository, state.issue)}.`)
536
575
  .join("\n");
@@ -58,10 +58,9 @@ export function applyFindingValidation(input) {
58
58
  discarded.push(target);
59
59
  return false;
60
60
  });
61
- next[reviewer] =
62
- findings.length || output.requirementFindings.length
63
- ? { ...output, findings }
64
- : { findings: [], requirementFindings: [], verdict: "MERGE" };
61
+ next[reviewer] = findings.length
62
+ ? { ...output, findings }
63
+ : { findings: [], verdict: "MERGE" };
65
64
  }
66
65
  return { outputs: next, summary: { discarded, kept } };
67
66
  }
@@ -50,7 +50,7 @@ async function withReviewerFailureProgress(input) {
50
50
  throw error;
51
51
  }
52
52
  }
53
- async function runEditor(input, worktreePath, cycle, unresolvedThreads) {
53
+ async function runEditor(input, worktreePath, cycle, reviewFindings, unresolvedThreads) {
54
54
  const editor = input.repository.agents.editor;
55
55
  if (!editor)
56
56
  throw new Error("agents.editor is required for magi_merge");
@@ -64,6 +64,7 @@ async function runEditor(input, worktreePath, cycle, unresolvedThreads) {
64
64
  directory: input.directory,
65
65
  pr: input.pr,
66
66
  repository: input.repository,
67
+ reviewFindings: JSON.stringify(reviewFindings, null, 2),
67
68
  unresolvedThreads: JSON.stringify(unresolvedThreads, null, 2),
68
69
  worktreePath,
69
70
  });
@@ -149,8 +150,8 @@ async function postRereviewOutput(input, reviewerKey, output) {
149
150
  return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.newFindings.map((finding) => ({
150
151
  fix: "Please address this before merging.",
151
152
  issue: finding.body,
152
- line: finding.line,
153
153
  path: finding.path,
154
+ line: finding.line,
154
155
  startLine: finding.startLine,
155
156
  })));
156
157
  }
@@ -161,6 +162,35 @@ function parseRereviewOutputWithInlineTargets(text, targets) {
161
162
  validateInlineCommentTargets(output.newFindings, targets, "newFindings");
162
163
  return output;
163
164
  }
165
+ function newFindingToEditorFinding(reviewer, finding) {
166
+ return {
167
+ body: finding.body,
168
+ fix: "Please address this before merging.",
169
+ line: finding.line,
170
+ path: finding.path,
171
+ reviewer,
172
+ ...(finding.startLine == null ? {} : { startLine: finding.startLine }),
173
+ type: "inline",
174
+ };
175
+ }
176
+ export function blockingReviewFindings(outputs) {
177
+ return Object.entries(outputs).flatMap(([reviewer, output]) => {
178
+ if (output.verdict !== "CHANGES_REQUESTED")
179
+ return [];
180
+ if ("findings" in output) {
181
+ return output.findings.map((finding) => ({
182
+ fix: finding.fix,
183
+ issue: finding.issue,
184
+ line: finding.line,
185
+ path: finding.path,
186
+ reviewer,
187
+ ...(finding.startLine == null ? {} : { startLine: finding.startLine }),
188
+ type: "inline",
189
+ }));
190
+ }
191
+ return output.newFindings.map((finding) => newFindingToEditorFinding(reviewer, finding));
192
+ });
193
+ }
164
194
  async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionIds, ciFailureContext, options = {}) {
165
195
  throwIfAborted(input.signal);
166
196
  const meta = await fetchPullRequest(input.exec, input.repository, input.pr);
@@ -460,15 +490,38 @@ function syntheticReviewThreads(outputs) {
460
490
  const threads = {};
461
491
  for (const [reviewer, output] of Object.entries(outputs)) {
462
492
  if ("findings" in output) {
463
- threads[reviewer] = output.findings.map((finding) => {
493
+ threads[reviewer] = output.findings.flatMap((finding) => {
464
494
  const commentId = nextCommentId--;
465
- return {
466
- body: `Issue: ${finding.issue}\n\nFix: ${finding.fix}`,
495
+ return [
496
+ {
497
+ body: `Issue: ${finding.issue}\n\nFix: ${finding.fix}`,
498
+ commentId,
499
+ comments: [
500
+ {
501
+ author: reviewer,
502
+ body: `Issue: ${finding.issue}\n\nFix: ${finding.fix}`,
503
+ commentId,
504
+ createdAt: new Date(0).toISOString(),
505
+ },
506
+ ],
507
+ line: finding.line,
508
+ path: finding.path,
509
+ threadId: `dry-run:${reviewer}:${Math.abs(commentId)}`,
510
+ },
511
+ ];
512
+ });
513
+ continue;
514
+ }
515
+ threads[reviewer] = output.newFindings.flatMap((finding) => {
516
+ const commentId = nextCommentId--;
517
+ return [
518
+ {
519
+ body: finding.body,
467
520
  commentId,
468
521
  comments: [
469
522
  {
470
523
  author: reviewer,
471
- body: `Issue: ${finding.issue}\n\nFix: ${finding.fix}`,
524
+ body: finding.body,
472
525
  commentId,
473
526
  createdAt: new Date(0).toISOString(),
474
527
  },
@@ -476,27 +529,8 @@ function syntheticReviewThreads(outputs) {
476
529
  line: finding.line,
477
530
  path: finding.path,
478
531
  threadId: `dry-run:${reviewer}:${Math.abs(commentId)}`,
479
- };
480
- });
481
- continue;
482
- }
483
- threads[reviewer] = output.newFindings.map((finding) => {
484
- const commentId = nextCommentId--;
485
- return {
486
- body: finding.body,
487
- commentId,
488
- comments: [
489
- {
490
- author: reviewer,
491
- body: finding.body,
492
- commentId,
493
- createdAt: new Date(0).toISOString(),
494
- },
495
- ],
496
- line: finding.line,
497
- path: finding.path,
498
- threadId: `dry-run:${reviewer}:${Math.abs(commentId)}`,
499
- };
532
+ },
533
+ ];
500
534
  });
501
535
  }
502
536
  return threads;
@@ -666,7 +700,12 @@ export async function runMerge(input) {
666
700
  maxThreadResolutionCycles: input.repository.merge.maxThreadResolutionCycles,
667
701
  threads: unresolvedThreads,
668
702
  });
669
- if (!editableThreads.length) {
703
+ const editorFindings = blockingReviewFindings(reportOutputs);
704
+ const editableFindings = editableThreads.length ? editorFindings : [];
705
+ const findingAttemptsExhausted = input.repository.merge.maxThreadResolutionCycles !== 0 &&
706
+ cycle > input.repository.merge.maxThreadResolutionCycles;
707
+ if (!editableThreads.length &&
708
+ (!editableFindings.length || findingAttemptsExhausted)) {
670
709
  await input.onProgress?.({
671
710
  status: "changes_unresolved",
672
711
  type: "merge_completed",
@@ -693,7 +732,7 @@ export async function runMerge(input) {
693
732
  });
694
733
  if (!review.worktreePath)
695
734
  throw new Error("Review worktree is missing");
696
- const editorOutput = await runEditor(abortableInput, review.worktreePath, cycle, editableThreads);
735
+ const editorOutput = await runEditor(abortableInput, review.worktreePath, cycle, editableFindings, editableThreads);
697
736
  editorOutputs.push(editorOutput);
698
737
  dryRunThreads = input.dryRun
699
738
  ? appendDryRunEditorResponses({
@@ -27,9 +27,6 @@ function formatRereviewFinding(finding) {
27
27
  : `${finding.path}:${finding.startLine}-${finding.line}`;
28
28
  return `\`${line}\`: ${finding.body}`;
29
29
  }
30
- function formatRequirementFinding(finding) {
31
- return `Issue #${finding.issueNumber}: ${finding.requirement}`;
32
- }
33
30
  function isReviewOutput(output) {
34
31
  return "findings" in output;
35
32
  }
@@ -81,10 +78,7 @@ function reviewerDetailLines(output) {
81
78
  return output.reason ? [output.reason] : [];
82
79
  if (output.verdict !== "CHANGES_REQUESTED")
83
80
  return [];
84
- return [
85
- ...output.findings.map(formatFinding),
86
- ...output.requirementFindings.map(formatRequirementFinding),
87
- ];
81
+ return output.findings.map(formatFinding);
88
82
  }
89
83
  if (output.verdict === "CLOSE")
90
84
  return output.reason ? [output.reason] : [];
@@ -92,7 +86,6 @@ function reviewerDetailLines(output) {
92
86
  return [];
93
87
  return [
94
88
  ...output.newFindings.map(formatRereviewFinding),
95
- ...output.requirementFindings.map(formatRequirementFinding),
96
89
  ...output.followUps.map((item) => `Comment #${item.commentId}: ${item.body}`),
97
90
  ];
98
91
  }