opencode-magi 0.0.0-dev-20260721051451 → 0.0.0-dev-20260721055255

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.
@@ -1,7 +1,7 @@
1
1
  import Ajv from "ajv";
2
2
  import { readFile } from "fs/promises";
3
3
  import { join } from "path";
4
- import { isString } from "@/utils";
4
+ import { isString, isUndefined } from "@/utils";
5
5
  export class Prompt {
6
6
  magi;
7
7
  config;
@@ -43,11 +43,16 @@ export class Prompt {
43
43
  ].join("\n\n");
44
44
  return Object.entries(values).reduce((prev, [key, value]) => prev.replaceAll(`{${key}}`, value), content);
45
45
  }
46
- async repair() {
46
+ async repair(e) {
47
47
  const instructions = [
48
- "Your previous output did not match the required schema. Regenerate the result.",
48
+ "Your previous output failed validation. Regenerate the result.",
49
+ !isUndefined(e)
50
+ ? `Validation error: ${e instanceof Error ? e.message : "Invalid output."}`
51
+ : undefined,
49
52
  "Return only a JSON object matching the output contract below. Do not include analysis, explanation, apologies, markdown, or any text before or after the JSON object.",
50
- ].join("\n\n");
53
+ ]
54
+ .filter((instruction) => !isUndefined(instruction))
55
+ .join("\n\n");
51
56
  try {
52
57
  const outputContract = await readFile(join(this.pathname, "output-contract.md"), "utf-8");
53
58
  return [
@@ -1,10 +1,10 @@
1
1
  import { MagiError } from "@/magi";
2
2
  import { retry } from "@/utils";
3
- export async function editCycles(callback) {
3
+ export async function editCycles(cb) {
4
4
  this.context.abort.throwIfAborted();
5
5
  const error = new Error("Continue edit cycle.");
6
6
  const verdict = await retry(async (cycle) => {
7
- const verdict = await callback(cycle);
7
+ const verdict = await cb(cycle);
8
8
  if (verdict === "CHANGES_REQUESTED")
9
9
  throw error;
10
10
  return verdict;
@@ -23,9 +23,8 @@ export async function edit() {
23
23
  repo: this.config.github.repo,
24
24
  worktreePath: this.state.worktree.path,
25
25
  });
26
- const repairMessage = await prompt.repair();
27
- const output = await retry(async (count) => {
28
- const raw = await this.magi.promptSession(this.state.editor.sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
26
+ const output = await retry(async (count, e) => {
27
+ const raw = await this.magi.promptSession(this.state.editor.sessionId, count === 1 ? taskMessage : await prompt.repair(e), this.context.abort);
29
28
  await this.createAgentFile("edit", "editor", raw, count, cycle);
30
29
  const parsed = prompt.parse(raw);
31
30
  if (!prompt.validate(parsed))
@@ -111,9 +110,8 @@ export async function resolveConflict() {
111
110
  repo: this.config.github.repo,
112
111
  worktreePath: this.state.worktree.path,
113
112
  });
114
- const repairMessage = await prompt.repair();
115
- const output = await retry(async (count) => {
116
- const raw = await this.magi.promptSession(this.state.editor.sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
113
+ const output = await retry(async (count, e) => {
114
+ const raw = await this.magi.promptSession(this.state.editor.sessionId, count === 1 ? taskMessage : await prompt.repair(e), this.context.abort);
117
115
  await this.createAgentFile("conflict", "editor", raw, count, cycle);
118
116
  const parsed = prompt.parse(raw);
119
117
  if (!prompt.validate(parsed))
@@ -78,9 +78,8 @@ export async function postReviews() {
78
78
  repo: this.config.github.repo,
79
79
  verdict: this.state.pr.verdict,
80
80
  });
81
- const repairMessage = await prompt.repair();
82
- const output = await retry(async (count) => {
83
- const raw = await this.magi.promptSession(this.state.operator.sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
81
+ const output = await retry(async (count, e) => {
82
+ const raw = await this.magi.promptSession(this.state.operator.sessionId, count === 1 ? taskMessage : await prompt.repair(e), this.context.abort);
84
83
  await this.createAgentFile("comment", "operator", raw, count);
85
84
  const parsed = prompt.parse(raw);
86
85
  if (!prompt.validate(parsed))
@@ -101,13 +101,12 @@ export async function classifyChecks() {
101
101
  repo: this.config.github.repo,
102
102
  worktreePath: this.state.worktree.path,
103
103
  });
104
- const repairMessage = await prompt.repair();
105
104
  await Promise.all(Object.entries(this.state.reviewers ?? {}).map(([id, { sessionId }]) => worker.run(async () => {
106
105
  if (!sessionId)
107
106
  throw new Error(`No session ID found for reviewer ${id}.`);
108
107
  await this.updateEvent(`Classifying CI checks with reviewer ${id}.`);
109
- const output = await retry(async (count) => {
110
- const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
108
+ const output = await retry(async (count, e) => {
109
+ const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : await prompt.repair(e), this.context.abort);
111
110
  await this.createAgentFile("ci-classification", id, raw, count);
112
111
  const parsed = prompt.parse(raw);
113
112
  if (!prompt.validate(parsed))
@@ -70,10 +70,10 @@ export async function review() {
70
70
  }), null, 2);
71
71
  tags.push(["previous_review", previousReviewContext]);
72
72
  }
73
- if (unresolvedThreads.length) {
74
- const unresolvedThreadsContext = JSON.stringify(unresolvedThreads, null, 2);
75
- tags.push(["unresolved_threads", unresolvedThreadsContext]);
76
- }
73
+ tags.push([
74
+ "unresolved_threads",
75
+ JSON.stringify(unresolvedThreads, null, 2),
76
+ ]);
77
77
  if (failedChecks.length) {
78
78
  const ciFailureContext = JSON.stringify(failedChecks, null, 2);
79
79
  tags.push(["ci_failure", ciFailureContext]);
@@ -94,9 +94,8 @@ export async function review() {
94
94
  repo: this.config.github.repo,
95
95
  worktreePath: this.state.worktree.path,
96
96
  }));
97
- const repairMessage = await prompt.repair();
98
- const output = await retry(async (count) => {
99
- const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
97
+ const output = await retry(async (count, e) => {
98
+ const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : await prompt.repair(e), this.context.abort);
100
99
  await this.createAgentFile(rereview ? "rereview" : "review", id, raw, count, cycle);
101
100
  const parsed = prompt.parse(raw);
102
101
  if (!prompt.validate(parsed))
@@ -199,10 +198,9 @@ export async function reconsiderClose() {
199
198
  pr: this.number.toString(),
200
199
  repo: this.config.github.repo,
201
200
  });
202
- const repairMessage = await prompt.repair();
203
201
  await this.updateEvent(`Reconsidering close verdict with reviewer ${id}.`);
204
- const output = await retry(async (count) => {
205
- const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
202
+ const output = await retry(async (count, e) => {
203
+ const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : await prompt.repair(e), this.context.abort);
206
204
  await this.createAgentFile("close-reconsideration", id, raw, count, cycle);
207
205
  const parsed = prompt.parse(raw);
208
206
  if (!prompt.validate(parsed))
@@ -267,9 +265,8 @@ async function collectAcceptedFindings(findings) {
267
265
  pr: this.number.toString(),
268
266
  repo: this.config.github.repo,
269
267
  });
270
- const repairMessage = await prompt.repair();
271
- const output = await retry(async (count) => {
272
- const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
268
+ const output = await retry(async (count, e) => {
269
+ const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : await prompt.repair(e), this.context.abort);
273
270
  await this.createAgentFile("finding-validation", id, raw, count);
274
271
  const parsed = prompt.parse(raw);
275
272
  if (!prompt.validate(parsed))
@@ -386,12 +383,15 @@ function validateInlineCommentTargets(status, output, inlineCommentTargets) {
386
383
  }
387
384
  function validateThreadTargets({ followUps, resolves }, threads) {
388
385
  const targets = new Map(threads.flatMap(({ comments, id }) => comments.flatMap(({ databaseId }) => databaseId == null ? [] : [[databaseId, id]])));
386
+ const allowedTargets = [...targets.entries()]
387
+ .map(([commentId, threadId]) => `- ${commentId}:${threadId}`)
388
+ .join("\n") || "none";
389
389
  if (followUps)
390
390
  for (const [index, { commentId }] of followUps.entries())
391
391
  if (!targets.has(commentId))
392
- throw new Error(`followUps[${index}].commentId must target an unresolved thread owned by the reviewer.`);
392
+ throw new Error(`followUps[${index}].commentId must target an unresolved thread owned by the reviewer.\n\nAllowed targets:\n${allowedTargets}`);
393
393
  if (resolves)
394
394
  for (const [index, { commentId, threadId }] of resolves.entries())
395
395
  if (targets.get(commentId) !== threadId)
396
- throw new Error(`resolves[${index}] must target an unresolved thread owned by the reviewer.`);
396
+ throw new Error(`resolves[${index}] must target an unresolved thread owned by the reviewer.\n\nAllowed targets:\n${allowedTargets}`);
397
397
  }
@@ -12,24 +12,26 @@ export function wait(ms = 0, signal) {
12
12
  signal?.addEventListener("abort", abort, { once: true });
13
13
  });
14
14
  }
15
- export async function loop(callback, ms = 0) {
15
+ export async function loop(cb, ms = 0) {
16
16
  for (;;) {
17
- const value = await callback();
17
+ const value = await cb();
18
18
  if (value != null)
19
19
  return value;
20
20
  await wait(ms);
21
21
  }
22
22
  }
23
- export async function retry(callback, { error, retries = 1, signal }) {
23
+ export async function retry(cb, { error: errorCb, retries = 1, signal }) {
24
24
  let count = 1;
25
+ let error;
25
26
  while (count <= retries)
26
27
  try {
27
28
  signal?.throwIfAborted();
28
- return await callback(count);
29
+ return await cb(count, error);
29
30
  }
30
31
  catch (e) {
31
32
  signal?.throwIfAborted();
32
- await error?.(e, count);
33
+ await errorCb?.(e, count);
34
+ error = e;
33
35
  count += 1;
34
36
  }
35
37
  }
@@ -1,14 +1,25 @@
1
1
  import { isArray } from "./assertion";
2
+ import { createExec } from "./exec";
2
3
  export async function getModels(input) {
3
4
  const providers = await input.client.config
4
5
  .providers({ directory: input.directory })
5
6
  .catch(() => input.client.provider.list({ directory: input.directory }));
6
7
  const data = "data" in providers ? providers.data : undefined;
7
8
  const all = data && "providers" in data ? data.providers : data?.all;
8
- return isArray(all)
9
+ const models = isArray(all)
9
10
  ? all.flatMap((provider) => {
10
11
  const models = Object.keys(provider.models);
11
12
  return provider.id ? models.map((id) => `${provider.id}/${id}`) : models;
12
13
  })
13
14
  : [];
15
+ if (models.length)
16
+ return models;
17
+ else
18
+ try {
19
+ const output = await createExec(input.directory)("opencode models");
20
+ return output.split("\n").filter((model) => model.includes("/"));
21
+ }
22
+ catch {
23
+ return [];
24
+ }
14
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-magi",
3
- "version": "0.0.0-dev-20260721051451",
3
+ "version": "0.0.0-dev-20260721055255",
4
4
  "description": "Multi-agent PR review and merge orchestration plugin for OpenCode.",
5
5
  "license": "MIT",
6
6
  "author": "Hirotomo Yamada <hirotomo.yamada@avap.co.jp>",