opencode-magi 0.10.0 → 0.12.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.
Files changed (126) hide show
  1. package/README.ja.md +254 -0
  2. package/README.md +31 -26
  3. package/dist/commands.js +11 -9
  4. package/dist/config/index.js +2 -0
  5. package/dist/config/resolve.js +120 -246
  6. package/dist/config/validate.js +98 -1110
  7. package/dist/constant.js +131 -0
  8. package/dist/graphql/index.generated.js +125 -0
  9. package/dist/graphql/index.js +49 -0
  10. package/dist/index.js +12 -800
  11. package/dist/magi.js +357 -0
  12. package/dist/permissions/common.json +22 -22
  13. package/dist/permissions/editor.json +12 -12
  14. package/dist/prompts/index.js +98 -0
  15. package/dist/prompts/merge/ci-classification/output-contract.md +22 -0
  16. package/dist/prompts/merge/ci-classification/task.md +10 -0
  17. package/dist/prompts/merge/ci-classification/validate.json +20 -0
  18. package/dist/prompts/merge/conflict/output-contract.md +12 -0
  19. package/dist/prompts/merge/conflict/task.md +9 -0
  20. package/dist/prompts/merge/conflict/validate.json +4 -0
  21. package/dist/prompts/merge/edit/output-contract.md +26 -0
  22. package/dist/prompts/merge/edit/task.md +10 -0
  23. package/dist/prompts/merge/edit/validate.json +60 -0
  24. package/dist/prompts/review/ci-classification/output-contract.md +21 -0
  25. package/dist/prompts/review/ci-classification/task.md +7 -0
  26. package/dist/prompts/review/ci-classification/validate.json +20 -0
  27. package/dist/prompts/review/close-reconsideration/output-contract.md +21 -0
  28. package/dist/prompts/review/close-reconsideration/task.md +6 -0
  29. package/dist/prompts/review/close-reconsideration/validate.json +44 -0
  30. package/dist/prompts/review/comment/output-contract.md +11 -0
  31. package/dist/prompts/review/comment/task.md +3 -0
  32. package/dist/prompts/review/comment/validate.json +8 -0
  33. package/dist/prompts/review/finding-validation/output-contract.md +25 -0
  34. package/dist/prompts/{templates/review/finding-validation.md → review/finding-validation/task.md} +2 -3
  35. package/dist/prompts/review/finding-validation/validate.json +21 -0
  36. package/dist/prompts/review/rereview/output-contract.md +30 -0
  37. package/dist/prompts/{templates/review/rereview.md → review/rereview/task.md} +8 -11
  38. package/dist/prompts/review/rereview/validate.json +87 -0
  39. package/dist/prompts/review/review/output-contract.md +25 -0
  40. package/dist/prompts/{templates/review/review.md → review/review/task.md} +2 -5
  41. package/dist/prompts/review/review/validate.json +54 -0
  42. package/dist/prompts/triage/acceptance/output-contract.md +16 -0
  43. package/dist/prompts/triage/acceptance/validate.json +23 -0
  44. package/dist/prompts/triage/category/output-contract.md +16 -0
  45. package/dist/prompts/triage/category/validate.json +23 -0
  46. package/dist/prompts/triage/comment-classification/output-contract.md +15 -0
  47. package/dist/prompts/triage/comment-classification/validate.json +28 -0
  48. package/dist/prompts/triage/create/output-contract.md +28 -0
  49. package/dist/prompts/triage/create/validate.json +73 -0
  50. package/dist/prompts/triage/deplicate/output-contract.md +16 -0
  51. package/dist/prompts/triage/deplicate/validate.json +20 -0
  52. package/dist/prompts/triage/existing/output-contract.md +16 -0
  53. package/dist/prompts/triage/existing/validate.json +13 -0
  54. package/dist/prompts/triage/reconsider/output-contract.md +16 -0
  55. package/dist/prompts/triage/reconsider/validate.json +23 -0
  56. package/dist/prompts/triage/signal/output-contract.md +19 -0
  57. package/dist/prompts/triage/signal/validate.json +18 -0
  58. package/dist/tools/clear/index.js +21 -0
  59. package/dist/tools/index.js +12 -0
  60. package/dist/tools/merge/action.js +47 -0
  61. package/dist/tools/merge/context.js +116 -0
  62. package/dist/tools/merge/editor.js +256 -0
  63. package/dist/tools/merge/index.js +151 -0
  64. package/dist/tools/merge/index.type.js +1 -0
  65. package/dist/tools/merge/merge.js +42 -0
  66. package/dist/tools/merge/report.js +73 -0
  67. package/dist/tools/review/action.js +459 -0
  68. package/dist/tools/review/check.js +335 -0
  69. package/dist/tools/review/context.js +254 -0
  70. package/dist/tools/review/index.js +104 -0
  71. package/dist/tools/review/index.type.js +1 -0
  72. package/dist/tools/review/report.js +143 -0
  73. package/dist/tools/review/review.js +146 -0
  74. package/dist/tools/review/reviewer.js +409 -0
  75. package/dist/tools/triage/index.js +16 -0
  76. package/dist/tools/validate/index.js +28 -0
  77. package/dist/utils/array.js +6 -0
  78. package/dist/utils/assertion.js +38 -0
  79. package/dist/utils/exec.js +14 -0
  80. package/dist/utils/fs.js +24 -0
  81. package/dist/utils/function.js +46 -0
  82. package/dist/utils/github.js +63 -0
  83. package/dist/utils/index.js +10 -0
  84. package/dist/utils/index.type.js +1 -0
  85. package/dist/utils/object.js +21 -0
  86. package/dist/utils/opencode.js +25 -0
  87. package/dist/utils/string.js +38 -0
  88. package/dist/utils/worker.js +30 -0
  89. package/package.json +36 -23
  90. package/schema.json +116 -20
  91. package/dist/config/load.js +0 -62
  92. package/dist/config/output.js +0 -25
  93. package/dist/config/worktree.js +0 -25
  94. package/dist/github/commands.js +0 -905
  95. package/dist/github/retry.js +0 -44
  96. package/dist/orchestrator/abort.js +0 -9
  97. package/dist/orchestrator/ci.js +0 -579
  98. package/dist/orchestrator/findings.js +0 -88
  99. package/dist/orchestrator/inline-comments.js +0 -73
  100. package/dist/orchestrator/majority.js +0 -62
  101. package/dist/orchestrator/merge.js +0 -1260
  102. package/dist/orchestrator/model.js +0 -216
  103. package/dist/orchestrator/pool.js +0 -15
  104. package/dist/orchestrator/report.js +0 -175
  105. package/dist/orchestrator/review-context.js +0 -342
  106. package/dist/orchestrator/review.js +0 -1480
  107. package/dist/orchestrator/run-manager.js +0 -2132
  108. package/dist/orchestrator/safety.js +0 -44
  109. package/dist/orchestrator/triage.js +0 -1370
  110. package/dist/prompts/compose.js +0 -473
  111. package/dist/prompts/contracts.js +0 -300
  112. package/dist/prompts/output.js +0 -401
  113. package/dist/prompts/templates/merge/ci-classification.md +0 -16
  114. package/dist/prompts/templates/merge/conflict.md +0 -10
  115. package/dist/prompts/templates/merge/edit.md +0 -15
  116. package/dist/prompts/templates/review/ci-classification.md +0 -9
  117. package/dist/prompts/templates/review/close-reconsideration.md +0 -6
  118. /package/dist/{types.js → config/index.type.js} +0 -0
  119. /package/dist/prompts/{templates/triage/acceptance.md → triage/acceptance/task.md} +0 -0
  120. /package/dist/prompts/{templates/triage/category.md → triage/category/task.md} +0 -0
  121. /package/dist/prompts/{templates/triage/comment-classification.md → triage/comment-classification/task.md} +0 -0
  122. /package/dist/prompts/{templates/triage/create.md → triage/create/task.md} +0 -0
  123. /package/dist/prompts/{templates/triage/duplicate.md → triage/deplicate/task.md} +0 -0
  124. /package/dist/prompts/{templates/triage/existing-pr.md → triage/existing/task.md} +0 -0
  125. /package/dist/prompts/{templates/triage/reconsider.md → triage/reconsider/task.md} +0 -0
  126. /package/dist/prompts/{templates/triage/signal.md → triage/signal/task.md} +0 -0
@@ -0,0 +1,28 @@
1
+ {
2
+ "type": "object",
3
+ "required": ["comments"],
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "comments": {
7
+ "type": "array",
8
+ "items": {
9
+ "type": "object",
10
+ "required": ["commentId", "classification", "reason"],
11
+ "additionalProperties": false,
12
+ "properties": {
13
+ "commentId": { "type": "integer" },
14
+ "classification": {
15
+ "enum": [
16
+ "OBJECTION",
17
+ "NEW_EVIDENCE",
18
+ "CLARIFICATION",
19
+ "ACKNOWLEDGEMENT",
20
+ "UNRELATED"
21
+ ]
22
+ },
23
+ "reason": { "type": "string", "minLength": 1 }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,28 @@
1
+ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
2
+
3
+ The object must match this shape:
4
+
5
+ ```json
6
+ {
7
+ "mode": "EDITED" | "REPLIED",
8
+ "commitSha": "full sha, required only when mode is EDITED; omit when mode is REPLIED",
9
+ "commitMessage": "fix(scope): short description, required only when mode is EDITED; omit when mode is REPLIED",
10
+ "filesTouched": ["relative/path.ext"],
11
+ "pullRequest": {
12
+ "title": "PR title, required only when mode is EDITED; omit when mode is REPLIED",
13
+ "body": "PR body, required only when mode is EDITED; omit when mode is REPLIED"
14
+ },
15
+ "responses": [{ "commentId": 123, "action": "FIXED" | "DISAGREE" | "ASK", "body": "Fixed." }]
16
+ }
17
+ ```
18
+
19
+ Rules:
20
+
21
+ - Use EDITED only when you edited files, staged changes, and committed.
22
+ - Use REPLIED when you only replied without code changes.
23
+ - For EDITED, pullRequest.title and pullRequest.body must be non-empty and follow the repository's PR conventions.
24
+ - Do not push or create the PR. The orchestrator pushes and creates the PR using pullRequest exactly as provided.
25
+ - filesTouched is required for EDITED and must include every final changed file.
26
+ - responses may be omitted when no review threads were addressed.
27
+ - REPLIED may omit filesTouched. If present, it must be empty.
28
+ - REPLIED requires at least one DISAGREE or ASK response.
@@ -0,0 +1,73 @@
1
+ {
2
+ "type": "object",
3
+ "required": ["mode"],
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "mode": { "enum": ["EDITED", "REPLIED"] },
7
+ "commitSha": { "type": "string", "minLength": 1 },
8
+ "commitMessage": { "type": "string", "minLength": 1 },
9
+ "filesTouched": {
10
+ "type": "array",
11
+ "items": { "type": "string", "minLength": 1 }
12
+ },
13
+ "pullRequest": {
14
+ "type": "object",
15
+ "required": ["title", "body"],
16
+ "additionalProperties": false,
17
+ "properties": {
18
+ "title": { "type": "string", "minLength": 1 },
19
+ "body": { "type": "string", "minLength": 1 }
20
+ }
21
+ },
22
+ "responses": {
23
+ "type": "array",
24
+ "items": {
25
+ "type": "object",
26
+ "required": ["commentId", "action", "body"],
27
+ "additionalProperties": false,
28
+ "properties": {
29
+ "commentId": { "type": "integer" },
30
+ "action": { "enum": ["FIXED", "DISAGREE", "ASK"] },
31
+ "body": { "type": "string", "minLength": 1 }
32
+ }
33
+ }
34
+ }
35
+ },
36
+ "allOf": [
37
+ {
38
+ "if": { "properties": { "mode": { "const": "EDITED" } } },
39
+ "then": {
40
+ "required": [
41
+ "commitSha",
42
+ "commitMessage",
43
+ "filesTouched",
44
+ "pullRequest"
45
+ ],
46
+ "properties": { "filesTouched": { "type": "array", "minItems": 1 } }
47
+ }
48
+ },
49
+ {
50
+ "if": { "properties": { "mode": { "const": "REPLIED" } } },
51
+ "then": {
52
+ "required": ["responses"],
53
+ "not": {
54
+ "anyOf": [
55
+ { "required": ["commitSha"] },
56
+ { "required": ["commitMessage"] },
57
+ { "required": ["pullRequest"] }
58
+ ]
59
+ },
60
+ "properties": {
61
+ "filesTouched": { "type": "array", "maxItems": 0 },
62
+ "responses": {
63
+ "type": "array",
64
+ "minItems": 1,
65
+ "items": {
66
+ "properties": { "action": { "enum": ["DISAGREE", "ASK"] } }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
72
+ ]
73
+ }
@@ -0,0 +1,16 @@
1
+ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
2
+
3
+ The object must match this shape:
4
+
5
+ ```json
6
+ {
7
+ "vote": "DUPLICATE" | "NOT_DUPLICATE",
8
+ "duplicateOf": 123,
9
+ "reason": "Short rationale."
10
+ }
11
+ ```
12
+
13
+ Rules:
14
+
15
+ - duplicateOf is required only when vote is DUPLICATE.
16
+ - duplicateOf must be one of the provided duplicate candidate issue numbers.
@@ -0,0 +1,20 @@
1
+ {
2
+ "type": "object",
3
+ "required": ["vote", "reason"],
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "vote": { "enum": ["DUPLICATE", "NOT_DUPLICATE"] },
7
+ "duplicateOf": { "type": "integer" },
8
+ "reason": { "type": "string", "minLength": 1 }
9
+ },
10
+ "allOf": [
11
+ {
12
+ "if": { "properties": { "vote": { "const": "DUPLICATE" } } },
13
+ "then": { "required": ["duplicateOf"] }
14
+ },
15
+ {
16
+ "if": { "properties": { "vote": { "const": "NOT_DUPLICATE" } } },
17
+ "then": { "not": { "required": ["duplicateOf"] } }
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,16 @@
1
+ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
2
+
3
+ The object must match this shape:
4
+
5
+ ```json
6
+ {
7
+ "vote": "RELATED_PR_HANDLES_ISSUE" | "RELATED_PR_DOES_NOT_HANDLE_ISSUE",
8
+ "reason": "Short rationale.",
9
+ "body": "Required only when vote is ASK. Public issue comment body asking for the missing information."
10
+ }
11
+ ```
12
+
13
+ Rules:
14
+
15
+ - body is required when vote is ASK and must be written for the issue author.
16
+ - Omit body when vote is not ASK.
@@ -0,0 +1,13 @@
1
+ {
2
+ "type": "object",
3
+ "required": ["vote", "reason"],
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "vote": {
7
+ "enum": ["RELATED_PR_HANDLES_ISSUE", "RELATED_PR_DOES_NOT_HANDLE_ISSUE"]
8
+ },
9
+ "reason": { "type": "string", "minLength": 1 },
10
+ "body": { "type": "string", "minLength": 1 }
11
+ },
12
+ "not": { "required": ["body"] }
13
+ }
@@ -0,0 +1,16 @@
1
+ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
2
+
3
+ The object must match this shape:
4
+
5
+ ```json
6
+ {
7
+ "vote": "YES" | "NO" | "ASK",
8
+ "reason": "Short rationale.",
9
+ "body": "Required only when vote is ASK. Public issue comment body asking for the missing information."
10
+ }
11
+ ```
12
+
13
+ Rules:
14
+
15
+ - body is required when vote is ASK and must be written for the issue author.
16
+ - Omit body when vote is not ASK.
@@ -0,0 +1,23 @@
1
+ {
2
+ "type": "object",
3
+ "required": ["vote", "reason"],
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "vote": { "enum": ["YES", "NO", "ASK"] },
7
+ "reason": { "type": "string", "minLength": 1 },
8
+ "body": { "type": "string", "minLength": 1 }
9
+ },
10
+ "allOf": [
11
+ {
12
+ "if": { "properties": { "vote": { "const": "ASK" } } },
13
+ "then": { "required": ["body"] }
14
+ },
15
+ {
16
+ "if": {
17
+ "properties": { "vote": { "not": { "const": "ASK" } } },
18
+ "required": ["vote"]
19
+ },
20
+ "then": { "not": { "required": ["body"] } }
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,19 @@
1
+ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
2
+
3
+ The object must match this shape:
4
+
5
+ ```json
6
+ {
7
+ "signals": [
8
+ {
9
+ "id": "configured_signal_id",
10
+ "reason": "Short rationale."
11
+ }
12
+ ]
13
+ }
14
+ ```
15
+
16
+ Rules:
17
+
18
+ - Return only configured signal IDs that apply.
19
+ - Omit `"signals"` when none apply.
@@ -0,0 +1,18 @@
1
+ {
2
+ "type": "object",
3
+ "additionalProperties": false,
4
+ "properties": {
5
+ "signals": {
6
+ "type": "array",
7
+ "items": {
8
+ "type": "object",
9
+ "required": ["id", "reason"],
10
+ "additionalProperties": false,
11
+ "properties": {
12
+ "id": { "type": "string", "minLength": 1 },
13
+ "reason": { "type": "string", "minLength": 1 }
14
+ }
15
+ }
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,21 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ export const clear = function (magi) {
3
+ return {
4
+ magi_clear: tool({
5
+ args: {},
6
+ description: "Clear all inactive Magi runs by deleting configured sessions, worktrees, branches, and output artifacts.",
7
+ async execute() {
8
+ const config = await magi.getConfig();
9
+ const summary = await magi.clear(config);
10
+ return [
11
+ `Cleared Magi runs: ${summary.run}`,
12
+ `Skipped active runs: ${summary.skipped}`,
13
+ `Sessions deleted: ${summary.session}`,
14
+ `Worktrees deleted: ${summary.worktree}`,
15
+ `Branches deleted: ${summary.branch}`,
16
+ `Outputs deleted: ${summary.output}`,
17
+ ].join("\n");
18
+ },
19
+ }),
20
+ };
21
+ };
@@ -0,0 +1,12 @@
1
+ import { clear } from "./clear";
2
+ import { merge } from "./merge";
3
+ import { review } from "./review";
4
+ import { triage } from "./triage";
5
+ import { validate } from "./validate";
6
+ export const tools = {
7
+ clear,
8
+ merge,
9
+ review,
10
+ triage,
11
+ validate,
12
+ };
@@ -0,0 +1,47 @@
1
+ import { MagiError } from "@/magi";
2
+ import { retry } from "@/utils";
3
+ export async function editCycles(cb) {
4
+ this.context.abort.throwIfAborted();
5
+ const error = new Error("Continue edit cycle.");
6
+ const verdict = await retry(async (cycle) => {
7
+ const verdict = await cb(cycle);
8
+ if (verdict === "CHANGES_REQUESTED")
9
+ throw error;
10
+ return verdict;
11
+ }, {
12
+ error: async (e, count) => {
13
+ if (e !== error)
14
+ throw e;
15
+ await this.updateEvent(`Attempt ${count} failed to edit cycles. Retrying...`);
16
+ },
17
+ retries: this.config.merge.maxThreadResolutionCycles,
18
+ signal: this.context.abort,
19
+ });
20
+ if (!verdict || verdict === "CHANGES_REQUESTED")
21
+ throw new MagiError("blocked", `Reached maximum edit cycles.`);
22
+ }
23
+ export async function postReplies() {
24
+ this.context.abort.throwIfAborted();
25
+ if (this.state.dryRun)
26
+ return;
27
+ const output = this.state.editor?.outputs?.at(-1);
28
+ if (!output)
29
+ throw new MagiError("blocked", "Editor output not found.");
30
+ if (!output.responses.length)
31
+ return;
32
+ await this.updateEvent(`Posting editor replies.`);
33
+ if (!this.state.editor?.account)
34
+ throw new MagiError("blocked", "Editor account not found.");
35
+ const octokit = await this.magi.createOctokit(this.config, this.context.abort, this.state.editor.account);
36
+ const args = {
37
+ owner: this.config.github.owner,
38
+ pull_number: this.number,
39
+ repo: this.config.github.repo,
40
+ };
41
+ await Promise.all(output.responses.map(({ body, commentId }) => octokit.rest.pulls.createReplyForReviewComment({
42
+ ...args,
43
+ body,
44
+ comment_id: commentId,
45
+ })));
46
+ await this.updateEvent(`Finished posting editor replies.`);
47
+ }
@@ -0,0 +1,116 @@
1
+ import { MagiError } from "@/magi";
2
+ import { getClosingIssues, getComments, getInlineCommentTargets, getReviewThreads, } from "@/tools/review/context";
3
+ import { marker } from "@/utils";
4
+ export async function fetchMergeContext() {
5
+ this.context.abort.throwIfAborted();
6
+ const output = this.state.editor?.outputs?.at(-1);
7
+ if (!output)
8
+ throw new MagiError("blocked", "Editor output not found.");
9
+ await this.updateEvent(`Fetching merge context.`);
10
+ const [comments, issues, threads] = await Promise.all([
11
+ getComments.call(this),
12
+ getClosingIssues.call(this),
13
+ getReviewThreads.call(this),
14
+ ]);
15
+ const inlineCommentTargets = await getInlineCommentTargets.call(this);
16
+ await this.updateState({
17
+ pr: {
18
+ comments,
19
+ inlineCommentTargets,
20
+ issues,
21
+ threads: this.state.dryRun
22
+ ? addSyntheticReplies.call(this, [
23
+ ...createSyntheticThreads.call(this),
24
+ ...threads,
25
+ ])
26
+ : threads,
27
+ },
28
+ });
29
+ await this.updateEvent(`Finished fetching merge context.`);
30
+ }
31
+ export async function markRepliedReviewers() {
32
+ this.context.abort.throwIfAborted();
33
+ await this.updateEvent(`Marking replied reviewers.`);
34
+ const output = this.state.editor?.outputs?.at(-1);
35
+ if (!output)
36
+ throw new MagiError("blocked", "Editor output not found.");
37
+ if (!this.state.pr?.threads)
38
+ throw new MagiError("blocked", "PR threads not found.");
39
+ if (!this.state.reviewers)
40
+ throw new MagiError("blocked", "Reviewers not found.");
41
+ const replied = output.responses.flatMap(({ commentId }) => {
42
+ const thread = this.state.pr.threads.find(({ comments }) => comments.some(({ databaseId }) => databaseId === commentId));
43
+ if (!thread)
44
+ return [];
45
+ if (this.config.mode === "single")
46
+ return thread.comments
47
+ .flatMap(({ body }) => marker.parse(body))
48
+ .flatMap(({ reviewer }) => (reviewer ? [reviewer] : []));
49
+ return Object.entries(this.state.reviewers)
50
+ .filter(([, { account }]) => thread.comments.some(({ author }) => account === author?.login))
51
+ .map(([id]) => id);
52
+ });
53
+ if (!replied.length)
54
+ throw new MagiError("blocked", "No replied reviewers found.");
55
+ const reviewers = Object.fromEntries(Object.keys(this.state.reviewers).map((id) => [
56
+ id,
57
+ { status: replied.includes(id) ? "reply" : "skip" },
58
+ ]));
59
+ await this.updateState({ reviewers });
60
+ await this.updateEvent(`Finished marking replied reviewers.`);
61
+ }
62
+ function createSyntheticThreads() {
63
+ const findings = Object.entries(this.state.reviewers ?? {}).flatMap(([reviewer, { account, outputs }]) => {
64
+ const output = outputs?.at(-1);
65
+ if (output?.verdict !== "CHANGES_REQUESTED")
66
+ return [];
67
+ return (output.findings ?? output.newFindings ?? []).map((finding) => ({
68
+ ...finding,
69
+ account,
70
+ reviewer,
71
+ }));
72
+ });
73
+ return findings.map(({ account, body, line, path, reviewer, state }, index) => ({
74
+ comments: [
75
+ {
76
+ author: { login: account ?? reviewer },
77
+ body: [
78
+ body,
79
+ marker.stringify({
80
+ command: "review",
81
+ reviewer,
82
+ verdict: "CHANGES_REQUESTED",
83
+ }),
84
+ ].join("\n\n"),
85
+ createdAt: new Date(0).toISOString(),
86
+ databaseId: -(index + 1),
87
+ url: "",
88
+ },
89
+ ],
90
+ id: `dry-run:${reviewer}:${index + 1}`,
91
+ isResolved: false,
92
+ line,
93
+ path,
94
+ state,
95
+ }));
96
+ }
97
+ function addSyntheticReplies(threads) {
98
+ const output = this.state.editor.outputs.at(-1);
99
+ return threads.map((thread) => ({
100
+ ...thread,
101
+ comments: [
102
+ ...thread.comments,
103
+ ...output.responses.flatMap(({ body, commentId }, index) => thread.comments.some(({ databaseId }) => databaseId === commentId)
104
+ ? [
105
+ {
106
+ author: { login: this.state.editor.account ?? "editor" },
107
+ body,
108
+ createdAt: new Date().toISOString(),
109
+ databaseId: -(threads.length + index + 1),
110
+ url: "",
111
+ },
112
+ ]
113
+ : []),
114
+ ],
115
+ }));
116
+ }