opencode-magi 0.0.0-dev-20260520175008 → 0.0.0-dev-20260520182750
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/dist/orchestrator/run-manager.js +234 -6
- package/dist/orchestrator/triage.js +184 -53
- package/dist/prompts/compose.js +2 -2
- package/dist/prompts/contracts.js +27 -0
- package/dist/prompts/output.js +26 -2
- package/package.json +1 -1
|
@@ -164,6 +164,13 @@ function editorFailureText(input) {
|
|
|
164
164
|
const repairs = repairAttemptsText(input.repairAttempts);
|
|
165
165
|
return `**Editor** failed editing ${input.pr}${repairs}: ${input.error}`;
|
|
166
166
|
}
|
|
167
|
+
function triageCreatorFailureText(input) {
|
|
168
|
+
const repairs = repairAttemptsText(input.repairAttempts);
|
|
169
|
+
return `**Triage creator** failed creating an implementation PR for ${input.issue}${repairs}: ${input.error}`;
|
|
170
|
+
}
|
|
171
|
+
function triageDecisionNotification(input) {
|
|
172
|
+
return `Triage decided ${input.issue}: ${input.result}. Planned action: ${input.action}.`;
|
|
173
|
+
}
|
|
167
174
|
function repairAttemptsText(attempts) {
|
|
168
175
|
if (!attempts)
|
|
169
176
|
return "";
|
|
@@ -536,6 +543,14 @@ export class MagiRunManager {
|
|
|
536
543
|
])),
|
|
537
544
|
runId,
|
|
538
545
|
status: "preparing",
|
|
546
|
+
triageCreator: input.repository.agents.triageCreator
|
|
547
|
+
? {
|
|
548
|
+
account: input.repository.agents.triageCreator.account,
|
|
549
|
+
repairAttempts: 0,
|
|
550
|
+
status: "pending",
|
|
551
|
+
toolCalls: 0,
|
|
552
|
+
}
|
|
553
|
+
: undefined,
|
|
539
554
|
updatedAt: createdAt,
|
|
540
555
|
};
|
|
541
556
|
this.active.set(runId, state);
|
|
@@ -626,6 +641,17 @@ export class MagiRunManager {
|
|
|
626
641
|
.abort?.({ path: { id: state.editor.sessionId } })
|
|
627
642
|
.catch(() => undefined);
|
|
628
643
|
}
|
|
644
|
+
if (state.triageCreator?.status === "pending" ||
|
|
645
|
+
state.triageCreator?.status === "running" ||
|
|
646
|
+
state.triageCreator?.status === "repairing" ||
|
|
647
|
+
state.triageCreator?.status === "blocked") {
|
|
648
|
+
state.triageCreator.status = "cancelled";
|
|
649
|
+
}
|
|
650
|
+
if (state.triageCreator?.sessionId) {
|
|
651
|
+
await this.input.client.session
|
|
652
|
+
.abort?.({ path: { id: state.triageCreator.sessionId } })
|
|
653
|
+
.catch(() => undefined);
|
|
654
|
+
}
|
|
629
655
|
for (const reviewer of Object.values(state.reviewers)) {
|
|
630
656
|
if (reviewer.status === "pending" ||
|
|
631
657
|
reviewer.status === "running" ||
|
|
@@ -919,7 +945,7 @@ export class MagiRunManager {
|
|
|
919
945
|
if (!existing) {
|
|
920
946
|
await this.notify(state, questionWaitText({
|
|
921
947
|
agent: mapping.agent,
|
|
922
|
-
pr:
|
|
948
|
+
pr: runLabel(state),
|
|
923
949
|
question,
|
|
924
950
|
}), { reply: true });
|
|
925
951
|
}
|
|
@@ -938,7 +964,7 @@ export class MagiRunManager {
|
|
|
938
964
|
agent.error = `Permission ${permission?.permission ?? "request"} is waiting for approval.`;
|
|
939
965
|
markUpdated(true);
|
|
940
966
|
dirty = true;
|
|
941
|
-
await this.notify(state, `Magi ${mapping.agent} is waiting for permission on ${
|
|
967
|
+
await this.notify(state, `Magi ${mapping.agent} is waiting for permission on ${runLabel(state)}: ${agent.error}`, { reply: true });
|
|
942
968
|
}
|
|
943
969
|
}
|
|
944
970
|
if (input.event.type === "question.asked") {
|
|
@@ -961,7 +987,7 @@ export class MagiRunManager {
|
|
|
961
987
|
dirty = true;
|
|
962
988
|
await this.notify(state, questionWaitText({
|
|
963
989
|
agent: mapping.agent,
|
|
964
|
-
pr:
|
|
990
|
+
pr: runLabel(state),
|
|
965
991
|
question,
|
|
966
992
|
}), { reply: true });
|
|
967
993
|
}
|
|
@@ -1031,6 +1057,9 @@ export class MagiRunManager {
|
|
|
1031
1057
|
const editorLine = state.editor
|
|
1032
1058
|
? this.formatAgentLine("editor", state.editor, options)
|
|
1033
1059
|
: undefined;
|
|
1060
|
+
const triageCreatorLine = state.triageCreator
|
|
1061
|
+
? this.formatAgentLine("triageCreator", state.triageCreator, options)
|
|
1062
|
+
: undefined;
|
|
1034
1063
|
const reviewerLines = Object.entries(state.reviewers).map(([key, reviewer]) => {
|
|
1035
1064
|
return this.formatAgentLine(key, reviewer, options);
|
|
1036
1065
|
});
|
|
@@ -1038,6 +1067,7 @@ export class MagiRunManager {
|
|
|
1038
1067
|
const lines = [
|
|
1039
1068
|
options.verbose ? `Run: ${state.runId}` : undefined,
|
|
1040
1069
|
state.pr == null ? undefined : `PR: #${state.pr}`,
|
|
1070
|
+
state.issue == null ? undefined : `Issue: #${state.issue}`,
|
|
1041
1071
|
`Command: ${state.command}`,
|
|
1042
1072
|
state.dryRun ? "Dry run: true" : undefined,
|
|
1043
1073
|
`Status: ${state.status}`,
|
|
@@ -1058,6 +1088,7 @@ export class MagiRunManager {
|
|
|
1058
1088
|
? `Report: ${state.reportPath}`
|
|
1059
1089
|
: undefined,
|
|
1060
1090
|
editorLine,
|
|
1091
|
+
triageCreatorLine,
|
|
1061
1092
|
...classifierLines,
|
|
1062
1093
|
...reviewerLines,
|
|
1063
1094
|
];
|
|
@@ -1084,6 +1115,7 @@ export class MagiRunManager {
|
|
|
1084
1115
|
collectSessionIds(state) {
|
|
1085
1116
|
const ids = [
|
|
1086
1117
|
state.editor?.sessionId,
|
|
1118
|
+
state.triageCreator?.sessionId,
|
|
1087
1119
|
...Object.values(state.reviewers).map((reviewer) => reviewer.sessionId),
|
|
1088
1120
|
...Object.values(state.ciClassifiers ?? {}).map((classifier) => classifier.sessionId),
|
|
1089
1121
|
...Object.values(state.sessionIds ?? {}),
|
|
@@ -1126,13 +1158,22 @@ export class MagiRunManager {
|
|
|
1126
1158
|
agentState(state, key) {
|
|
1127
1159
|
if (key.startsWith("ci:"))
|
|
1128
1160
|
return state.ciClassifiers?.[key.slice(3)];
|
|
1129
|
-
|
|
1161
|
+
if (key === "editor")
|
|
1162
|
+
return state.editor;
|
|
1163
|
+
if (key === "triageCreator")
|
|
1164
|
+
return state.triageCreator;
|
|
1165
|
+
return state.reviewers[key];
|
|
1130
1166
|
}
|
|
1131
1167
|
agentEntries(state) {
|
|
1132
1168
|
return [
|
|
1133
1169
|
...(state.editor
|
|
1134
1170
|
? [["editor", state.editor]]
|
|
1135
1171
|
: []),
|
|
1172
|
+
...(state.triageCreator
|
|
1173
|
+
? [
|
|
1174
|
+
["triageCreator", state.triageCreator],
|
|
1175
|
+
]
|
|
1176
|
+
: []),
|
|
1136
1177
|
...Object.entries(state.ciClassifiers ?? {}).map(([key, value]) => [`ci:${key}`, value]),
|
|
1137
1178
|
...Object.entries(state.reviewers),
|
|
1138
1179
|
];
|
|
@@ -1152,10 +1193,10 @@ export class MagiRunManager {
|
|
|
1152
1193
|
if (!matches.length) {
|
|
1153
1194
|
return key
|
|
1154
1195
|
? `No pending ${kind} request found for ${key}.`
|
|
1155
|
-
: `No pending ${kind} request found for ${
|
|
1196
|
+
: `No pending ${kind} request found for ${runLabel(state)}.`;
|
|
1156
1197
|
}
|
|
1157
1198
|
if (matches.length > 1) {
|
|
1158
|
-
return `Multiple pending ${kind} requests found for ${
|
|
1199
|
+
return `Multiple pending ${kind} requests found for ${runLabel(state)}. Specify agent or requestId.`;
|
|
1159
1200
|
}
|
|
1160
1201
|
return { key: matches[0][0], state: matches[0][1] };
|
|
1161
1202
|
}
|
|
@@ -1266,6 +1307,7 @@ export class MagiRunManager {
|
|
|
1266
1307
|
dryRun: input.dryRun,
|
|
1267
1308
|
exec: withGitHubApiRetry(this.input.exec, input.config.github?.apiRetryAttempts ?? 3),
|
|
1268
1309
|
issue: input.issue,
|
|
1310
|
+
onProgress: (progress) => this.applyTriageProgress(input.runId, progress),
|
|
1269
1311
|
repository: input.repository,
|
|
1270
1312
|
runId: input.runId,
|
|
1271
1313
|
signal: input.signal,
|
|
@@ -1284,6 +1326,9 @@ export class MagiRunManager {
|
|
|
1284
1326
|
if (agent.status === "pending")
|
|
1285
1327
|
agent.status = "completed";
|
|
1286
1328
|
}
|
|
1329
|
+
if (completed.triageCreator?.status === "pending") {
|
|
1330
|
+
completed.triageCreator.status = "skipped";
|
|
1331
|
+
}
|
|
1287
1332
|
await this.persist(completed);
|
|
1288
1333
|
await this.notify(completed, [
|
|
1289
1334
|
`Finished triage for ${issueMarkdownLink(completed)}.`,
|
|
@@ -1293,6 +1338,189 @@ export class MagiRunManager {
|
|
|
1293
1338
|
this.active.delete(input.runId);
|
|
1294
1339
|
this.controllers.delete(input.runId);
|
|
1295
1340
|
}
|
|
1341
|
+
async applyTriageProgress(runId, progress) {
|
|
1342
|
+
const state = this.active.get(runId);
|
|
1343
|
+
if (!state)
|
|
1344
|
+
return;
|
|
1345
|
+
const issue = issueMarkdownLink(state);
|
|
1346
|
+
const creatorState = () => state.triageCreator ??
|
|
1347
|
+
(state.triageCreator = {
|
|
1348
|
+
account: "triageCreator",
|
|
1349
|
+
repairAttempts: 0,
|
|
1350
|
+
status: "pending",
|
|
1351
|
+
toolCalls: 0,
|
|
1352
|
+
});
|
|
1353
|
+
state.updatedAt = now();
|
|
1354
|
+
if (progress.type === "phase") {
|
|
1355
|
+
state.phase = progress.phase;
|
|
1356
|
+
state.status = "running";
|
|
1357
|
+
}
|
|
1358
|
+
if (progress.type === "decision") {
|
|
1359
|
+
state.phase = `decision: ${progress.result.disposition}`;
|
|
1360
|
+
state.verdict = JSON.stringify(progress.result);
|
|
1361
|
+
}
|
|
1362
|
+
if (progress.type === "comment_posting") {
|
|
1363
|
+
state.phase = "posting triage comment";
|
|
1364
|
+
state.status = "posting";
|
|
1365
|
+
}
|
|
1366
|
+
if (progress.type === "comment_posted") {
|
|
1367
|
+
state.status = "running";
|
|
1368
|
+
}
|
|
1369
|
+
if (progress.type === "pr_creation_started") {
|
|
1370
|
+
state.phase = "creating implementation PR";
|
|
1371
|
+
state.status = "running";
|
|
1372
|
+
}
|
|
1373
|
+
if (progress.type === "worktree_created") {
|
|
1374
|
+
state.worktreePath = progress.worktreePath;
|
|
1375
|
+
state.worktreeBranch = progress.branch;
|
|
1376
|
+
}
|
|
1377
|
+
if (progress.type === "triage_agent_started") {
|
|
1378
|
+
const reviewer = state.reviewers[progress.reviewer];
|
|
1379
|
+
if (reviewer)
|
|
1380
|
+
reviewer.status = "running";
|
|
1381
|
+
}
|
|
1382
|
+
if (progress.type === "triage_agent_session") {
|
|
1383
|
+
const reviewer = state.reviewers[progress.reviewer];
|
|
1384
|
+
if (reviewer) {
|
|
1385
|
+
if (progress.options)
|
|
1386
|
+
this.input.setSessionOptions?.(progress.sessionId, progress.options);
|
|
1387
|
+
reviewer.sessionId = progress.sessionId;
|
|
1388
|
+
reviewer.status = "running";
|
|
1389
|
+
reviewer.lastUpdate = now();
|
|
1390
|
+
this.sessionToRun.set(progress.sessionId, {
|
|
1391
|
+
agent: progress.reviewer,
|
|
1392
|
+
runId,
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
if (progress.type === "triage_agent_repair") {
|
|
1397
|
+
const reviewer = state.reviewers[progress.reviewer];
|
|
1398
|
+
if (reviewer) {
|
|
1399
|
+
reviewer.status = "repairing";
|
|
1400
|
+
reviewer.repairAttempts += 1;
|
|
1401
|
+
reviewer.lastUpdate = now();
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
if (progress.type === "triage_agent_response") {
|
|
1405
|
+
const reviewer = state.reviewers[progress.reviewer];
|
|
1406
|
+
if (reviewer) {
|
|
1407
|
+
reviewer.sessionId = progress.sessionId;
|
|
1408
|
+
reviewer.lastUpdate = now();
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
if (progress.type === "triage_agent_completed") {
|
|
1412
|
+
const reviewer = state.reviewers[progress.reviewer];
|
|
1413
|
+
if (reviewer) {
|
|
1414
|
+
reviewer.sessionId = progress.sessionId;
|
|
1415
|
+
reviewer.status = "completed";
|
|
1416
|
+
reviewer.verdict = progress.vote;
|
|
1417
|
+
reviewer.rawPath = join(state.outputDir, `${progress.reviewer}.${progress.phase}.raw.txt`);
|
|
1418
|
+
reviewer.parsedPath = join(state.outputDir, `${progress.reviewer}.${progress.phase}.json`);
|
|
1419
|
+
reviewer.lastUpdate = now();
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
if (progress.type === "triage_agent_failed") {
|
|
1423
|
+
const reviewer = state.reviewers[progress.reviewer];
|
|
1424
|
+
if (reviewer) {
|
|
1425
|
+
reviewer.status = "failed";
|
|
1426
|
+
reviewer.error = redactSecrets(progress.error);
|
|
1427
|
+
reviewer.lastUpdate = now();
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
if (progress.type === "triage_creator_started") {
|
|
1431
|
+
creatorState().status = "running";
|
|
1432
|
+
}
|
|
1433
|
+
if (progress.type === "triage_creator_session") {
|
|
1434
|
+
const creator = creatorState();
|
|
1435
|
+
if (progress.options)
|
|
1436
|
+
this.input.setSessionOptions?.(progress.sessionId, progress.options);
|
|
1437
|
+
creator.sessionId = progress.sessionId;
|
|
1438
|
+
creator.status = "running";
|
|
1439
|
+
creator.lastUpdate = now();
|
|
1440
|
+
this.sessionToRun.set(progress.sessionId, {
|
|
1441
|
+
agent: "triageCreator",
|
|
1442
|
+
runId,
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
if (progress.type === "triage_creator_repair") {
|
|
1446
|
+
const creator = creatorState();
|
|
1447
|
+
creator.status = "repairing";
|
|
1448
|
+
creator.repairAttempts += 1;
|
|
1449
|
+
creator.lastUpdate = now();
|
|
1450
|
+
}
|
|
1451
|
+
if (progress.type === "triage_creator_response") {
|
|
1452
|
+
const creator = creatorState();
|
|
1453
|
+
creator.sessionId = progress.sessionId;
|
|
1454
|
+
creator.lastUpdate = now();
|
|
1455
|
+
}
|
|
1456
|
+
if (progress.type === "triage_creator_completed") {
|
|
1457
|
+
const creator = creatorState();
|
|
1458
|
+
creator.sessionId = progress.sessionId;
|
|
1459
|
+
creator.status = "completed";
|
|
1460
|
+
creator.parsedPath = join(state.outputDir, "create-pr.json");
|
|
1461
|
+
creator.lastUpdate = now();
|
|
1462
|
+
}
|
|
1463
|
+
if (progress.type === "triage_creator_failed") {
|
|
1464
|
+
const creator = creatorState();
|
|
1465
|
+
creator.status = "failed";
|
|
1466
|
+
creator.error = redactSecrets(progress.error);
|
|
1467
|
+
creator.lastUpdate = now();
|
|
1468
|
+
}
|
|
1469
|
+
await this.persist(state);
|
|
1470
|
+
if (progress.type === "phase") {
|
|
1471
|
+
await this.notify(state, `Triage phase for ${issue}: ${progress.phase}.`);
|
|
1472
|
+
}
|
|
1473
|
+
if (progress.type === "decision") {
|
|
1474
|
+
await this.notify(state, triageDecisionNotification({
|
|
1475
|
+
action: progress.action,
|
|
1476
|
+
issue,
|
|
1477
|
+
result: JSON.stringify(progress.result),
|
|
1478
|
+
}));
|
|
1479
|
+
}
|
|
1480
|
+
if (progress.type === "triage_agent_started") {
|
|
1481
|
+
await this.notify(state, `**Triage agent ${progress.reviewer}** started ${progress.phase} for ${issue}.`);
|
|
1482
|
+
}
|
|
1483
|
+
if (progress.type === "triage_agent_repair") {
|
|
1484
|
+
await this.notify(state, `**Triage agent ${progress.reviewer}** started JSON regeneration for ${issue}.`);
|
|
1485
|
+
}
|
|
1486
|
+
if (progress.type === "triage_agent_completed") {
|
|
1487
|
+
await this.notify(state, `**Triage agent ${progress.reviewer}** completed ${progress.phase} for ${issue}: ${progress.vote}.`);
|
|
1488
|
+
}
|
|
1489
|
+
if (progress.type === "triage_agent_failed") {
|
|
1490
|
+
await this.notify(state, `**Triage agent ${progress.reviewer}** failed ${progress.phase} for ${issue}: ${redactSecrets(progress.error)}`);
|
|
1491
|
+
}
|
|
1492
|
+
if (progress.type === "comment_posting") {
|
|
1493
|
+
await this.notify(state, `Posting triage comment for ${issue}.`);
|
|
1494
|
+
}
|
|
1495
|
+
if (progress.type === "comment_posted") {
|
|
1496
|
+
await this.notify(state, `Posted triage comment for ${issue}: ${progress.url}`);
|
|
1497
|
+
}
|
|
1498
|
+
if (progress.type === "pr_creation_started") {
|
|
1499
|
+
await this.notify(state, `Started implementation PR creation for ${issue}.`);
|
|
1500
|
+
}
|
|
1501
|
+
if (progress.type === "worktree_created") {
|
|
1502
|
+
await this.notify(state, `Worktree is ready for ${issue}.`);
|
|
1503
|
+
}
|
|
1504
|
+
if (progress.type === "triage_creator_started") {
|
|
1505
|
+
await this.notify(state, `**Triage creator** started creating an implementation PR for ${issue}.`);
|
|
1506
|
+
}
|
|
1507
|
+
if (progress.type === "triage_creator_repair") {
|
|
1508
|
+
await this.notify(state, `**Triage creator** started JSON regeneration for ${issue}.`);
|
|
1509
|
+
}
|
|
1510
|
+
if (progress.type === "triage_creator_completed") {
|
|
1511
|
+
await this.notify(state, `**Triage creator** completed implementation changes for ${issue}.`);
|
|
1512
|
+
}
|
|
1513
|
+
if (progress.type === "triage_creator_failed") {
|
|
1514
|
+
await this.notify(state, triageCreatorFailureText({
|
|
1515
|
+
error: redactSecrets(progress.error),
|
|
1516
|
+
issue,
|
|
1517
|
+
repairAttempts: state.triageCreator?.repairAttempts ?? 0,
|
|
1518
|
+
}));
|
|
1519
|
+
}
|
|
1520
|
+
if (progress.type === "pr_created") {
|
|
1521
|
+
await this.notify(state, `Created implementation PR for ${issue}: ${progress.url}`);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1296
1524
|
async applyReviewProgress(runId, progress) {
|
|
1297
1525
|
const state = this.active.get(runId);
|
|
1298
1526
|
if (!state)
|
|
@@ -6,7 +6,7 @@ import { assignIssue, closeIssue, closePullRequest, configureGitIdentity, create
|
|
|
6
6
|
import { composeTriageAcceptancePrompt, composeTriageActionPrompt, composeTriageCategoryPrompt, composeTriageCommentClassificationPrompt, composeTriageCommentPrompt, composeTriageCreatePrPrompt, composeTriageDuplicatePrompt, composeTriageExistingPrPrompt, composeTriageQuestionPrompt, composeTriageReconsiderPrompt, } from "../prompts/compose";
|
|
7
7
|
import { parseTriageActionOutput, parseTriageBinaryOutput, parseTriageCategoryOutput, parseTriageCommentClassificationOutput, parseTriageCreatePrOutput, parseTriageDuplicateOutput, parseTriageExistingPrOutput, } from "../prompts/output";
|
|
8
8
|
import { aggregateStringMajority, majorityThreshold } from "./majority";
|
|
9
|
-
import { runModelText, runModelWithRepair } from "./model";
|
|
9
|
+
import { runModelText, runModelWithRepair, } from "./model";
|
|
10
10
|
const MARKER_PREFIX = "opencode-magi:triage";
|
|
11
11
|
const BINARY_VOTES = ["ASK", "NO", "YES"];
|
|
12
12
|
const DUPLICATE_VOTES = ["DUPLICATE", "NOT_DUPLICATE"];
|
|
@@ -98,6 +98,35 @@ function issueContext(input) {
|
|
|
98
98
|
async function writeJson(path, value) {
|
|
99
99
|
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
100
100
|
}
|
|
101
|
+
async function emitProgress(input, progress) {
|
|
102
|
+
await input.onProgress?.(progress);
|
|
103
|
+
}
|
|
104
|
+
async function emitTriageModelProgress(input) {
|
|
105
|
+
if (input.progress.type === "session_created") {
|
|
106
|
+
await emitProgress(input.run, {
|
|
107
|
+
options: input.progress.options,
|
|
108
|
+
phase: input.phase,
|
|
109
|
+
reviewer: input.reviewer,
|
|
110
|
+
sessionId: input.progress.sessionId,
|
|
111
|
+
type: "triage_agent_session",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (input.progress.type === "repair") {
|
|
115
|
+
await emitProgress(input.run, {
|
|
116
|
+
phase: input.phase,
|
|
117
|
+
reviewer: input.reviewer,
|
|
118
|
+
type: "triage_agent_repair",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (input.progress.type === "response") {
|
|
122
|
+
await emitProgress(input.run, {
|
|
123
|
+
phase: input.phase,
|
|
124
|
+
reviewer: input.reviewer,
|
|
125
|
+
sessionId: input.progress.sessionId,
|
|
126
|
+
type: "triage_agent_response",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
101
130
|
async function runVote(input) {
|
|
102
131
|
const prompt = await input.prompt({
|
|
103
132
|
context: input.context,
|
|
@@ -106,17 +135,47 @@ async function runVote(input) {
|
|
|
106
135
|
repository: input.repository,
|
|
107
136
|
reviewer: input.agent,
|
|
108
137
|
});
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
138
|
+
await emitProgress(input.run, {
|
|
139
|
+
phase: input.phase,
|
|
140
|
+
reviewer: input.agent.key,
|
|
141
|
+
type: "triage_agent_started",
|
|
142
|
+
});
|
|
143
|
+
let result;
|
|
144
|
+
try {
|
|
145
|
+
result = await runModelWithRepair({
|
|
146
|
+
client: input.client,
|
|
147
|
+
model: input.agent.model,
|
|
148
|
+
onProgress: (progress) => emitTriageModelProgress({
|
|
149
|
+
phase: input.phase,
|
|
150
|
+
progress,
|
|
151
|
+
reviewer: input.agent.key,
|
|
152
|
+
run: input.run,
|
|
153
|
+
}),
|
|
154
|
+
options: input.agent.options,
|
|
155
|
+
parse: input.parse,
|
|
156
|
+
permission: input.agent.permission,
|
|
157
|
+
prompt,
|
|
158
|
+
repairAttempts: 3,
|
|
159
|
+
schemaName: input.schemaName,
|
|
160
|
+
signal: input.signal,
|
|
161
|
+
title: `Magi triage ${input.schemaName} #${input.issue} (${input.agent.key})`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
await emitProgress(input.run, {
|
|
166
|
+
error: error instanceof Error ? error.message : String(error),
|
|
167
|
+
phase: input.phase,
|
|
168
|
+
reviewer: input.agent.key,
|
|
169
|
+
type: "triage_agent_failed",
|
|
170
|
+
});
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
await emitProgress(input.run, {
|
|
174
|
+
phase: input.phase,
|
|
175
|
+
reviewer: input.agent.key,
|
|
176
|
+
sessionId: result.sessionId,
|
|
177
|
+
type: "triage_agent_completed",
|
|
178
|
+
vote: result.value.vote,
|
|
120
179
|
});
|
|
121
180
|
return {
|
|
122
181
|
...result.value,
|
|
@@ -155,6 +214,7 @@ async function runDuplicateVote(input) {
|
|
|
155
214
|
const agents = input.input.repository.agents.triage;
|
|
156
215
|
if (!agents?.length)
|
|
157
216
|
throw new Error("triage.agents is required");
|
|
217
|
+
await emitProgress(input.input, { phase: "duplicate", type: "phase" });
|
|
158
218
|
const outputs = await Promise.all(agents.map((agent) => runVote({
|
|
159
219
|
agent,
|
|
160
220
|
client: input.input.client,
|
|
@@ -162,8 +222,10 @@ async function runDuplicateVote(input) {
|
|
|
162
222
|
directory: input.input.directory,
|
|
163
223
|
issue: input.input.issue,
|
|
164
224
|
parse: parseTriageDuplicateOutput,
|
|
225
|
+
phase: "duplicate",
|
|
165
226
|
prompt: composeTriageDuplicatePrompt,
|
|
166
227
|
repository: input.input.repository,
|
|
228
|
+
run: input.input,
|
|
167
229
|
schemaName: "triage duplicate",
|
|
168
230
|
signal: input.input.signal,
|
|
169
231
|
})));
|
|
@@ -194,6 +256,7 @@ async function runPhaseVote(input) {
|
|
|
194
256
|
const agents = input.input.repository.agents.triage;
|
|
195
257
|
if (!agents?.length)
|
|
196
258
|
throw new Error("triage.agents is required");
|
|
259
|
+
await emitProgress(input.input, { phase: input.phase, type: "phase" });
|
|
197
260
|
const outputs = await Promise.all(agents.map((agent) => runVote({
|
|
198
261
|
agent,
|
|
199
262
|
client: input.input.client,
|
|
@@ -201,8 +264,10 @@ async function runPhaseVote(input) {
|
|
|
201
264
|
directory: input.input.directory,
|
|
202
265
|
issue: input.input.issue,
|
|
203
266
|
parse: input.parse,
|
|
267
|
+
phase: input.phase,
|
|
204
268
|
prompt: input.prompt,
|
|
205
269
|
repository: input.input.repository,
|
|
270
|
+
run: input.input,
|
|
206
271
|
schemaName: input.schemaName,
|
|
207
272
|
signal: input.input.signal,
|
|
208
273
|
})));
|
|
@@ -544,6 +609,11 @@ async function finishWithResult(input) {
|
|
|
544
609
|
if (!triage)
|
|
545
610
|
throw new Error("triage configuration is required");
|
|
546
611
|
const plan = input.plan ?? actionPlan({ result: input.result, triage });
|
|
612
|
+
await emitProgress(input.input, {
|
|
613
|
+
action: plan.action,
|
|
614
|
+
result: input.result,
|
|
615
|
+
type: "decision",
|
|
616
|
+
});
|
|
547
617
|
await runActionPrompt({
|
|
548
618
|
context: input.context,
|
|
549
619
|
input: input.input,
|
|
@@ -565,7 +635,8 @@ async function finishWithResult(input) {
|
|
|
565
635
|
: undefined;
|
|
566
636
|
if (!input.input.dryRun) {
|
|
567
637
|
if (comment) {
|
|
568
|
-
await
|
|
638
|
+
await emitProgress(input.input, { type: "comment_posting" });
|
|
639
|
+
const posted = await postMarkedIssueComment({
|
|
569
640
|
account: triage.account ?? "",
|
|
570
641
|
body: comment,
|
|
571
642
|
exec: input.input.exec,
|
|
@@ -573,6 +644,10 @@ async function finishWithResult(input) {
|
|
|
573
644
|
outputDir: input.outputDir,
|
|
574
645
|
repository: input.input.repository,
|
|
575
646
|
});
|
|
647
|
+
await emitProgress(input.input, {
|
|
648
|
+
type: "comment_posted",
|
|
649
|
+
url: posted.url,
|
|
650
|
+
});
|
|
576
651
|
}
|
|
577
652
|
if (plan.clearLabels) {
|
|
578
653
|
const clearLabels = existingClearLabels(input.issue, triage.automation.clear);
|
|
@@ -597,8 +672,10 @@ async function finishWithResult(input) {
|
|
|
597
672
|
issue: input.issue,
|
|
598
673
|
outputDir: input.outputDir,
|
|
599
674
|
});
|
|
600
|
-
if (prUrl)
|
|
675
|
+
if (prUrl) {
|
|
601
676
|
await writeJson(join(input.outputDir, "pr.json"), { url: prUrl });
|
|
677
|
+
await emitProgress(input.input, { type: "pr_created", url: prUrl });
|
|
678
|
+
}
|
|
602
679
|
}
|
|
603
680
|
if (input.previousMarker && prUrl) {
|
|
604
681
|
await persistProcessedMarker({
|
|
@@ -659,48 +736,89 @@ async function createImplementationPr(input) {
|
|
|
659
736
|
const triage = input.input.repository.triage;
|
|
660
737
|
if (!triage?.account)
|
|
661
738
|
throw new Error("triage.account is required");
|
|
662
|
-
await
|
|
663
|
-
|
|
664
|
-
const worktreePath = join(worktreeBaseDir(input.input.directory, input.input.config, "issue"), `issue-${input.issue.number}`);
|
|
665
|
-
await mkdir(dirname(worktreePath), { recursive: true });
|
|
666
|
-
await input.input.exec(`git worktree add -b ${shellQuote(branch)} ${shellQuote(worktreePath)}`);
|
|
739
|
+
await emitProgress(input.input, { type: "pr_creation_started" });
|
|
740
|
+
await emitProgress(input.input, { type: "triage_creator_started" });
|
|
667
741
|
try {
|
|
668
|
-
await
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
742
|
+
await assignIssue(input.input.exec, input.input.repository, input.issue.number, triage.account);
|
|
743
|
+
const branch = `magi/issue-${input.issue.number}-${Date.now().toString(36)}`;
|
|
744
|
+
const worktreePath = join(worktreeBaseDir(input.input.directory, input.input.config, "issue"), `issue-${input.issue.number}`);
|
|
745
|
+
await mkdir(dirname(worktreePath), { recursive: true });
|
|
746
|
+
await input.input.exec(`git worktree add -b ${shellQuote(branch)} ${shellQuote(worktreePath)}`);
|
|
747
|
+
await emitProgress(input.input, {
|
|
748
|
+
branch,
|
|
749
|
+
type: "worktree_created",
|
|
674
750
|
worktreePath,
|
|
675
751
|
});
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
752
|
+
try {
|
|
753
|
+
await configureGitIdentity(input.input.exec, worktreePath, creator.author);
|
|
754
|
+
const prompt = await composeTriageCreatePrPrompt({
|
|
755
|
+
context: input.context,
|
|
756
|
+
directory: input.input.directory,
|
|
757
|
+
issue: input.issue.number,
|
|
758
|
+
repository: input.input.repository,
|
|
759
|
+
worktreePath,
|
|
760
|
+
});
|
|
761
|
+
const result = await runModelWithRepair({
|
|
762
|
+
client: input.input.client,
|
|
763
|
+
model: creator.model,
|
|
764
|
+
onProgress: async (progress) => {
|
|
765
|
+
if (progress.type === "session_created") {
|
|
766
|
+
await emitProgress(input.input, {
|
|
767
|
+
options: progress.options,
|
|
768
|
+
sessionId: progress.sessionId,
|
|
769
|
+
type: "triage_creator_session",
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
if (progress.type === "repair") {
|
|
773
|
+
await emitProgress(input.input, { type: "triage_creator_repair" });
|
|
774
|
+
}
|
|
775
|
+
if (progress.type === "response") {
|
|
776
|
+
await emitProgress(input.input, {
|
|
777
|
+
sessionId: progress.sessionId,
|
|
778
|
+
type: "triage_creator_response",
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
options: creator.options,
|
|
783
|
+
parse: parseTriageCreatePrOutput,
|
|
784
|
+
permission: creator.permission,
|
|
785
|
+
prompt,
|
|
786
|
+
repairAttempts: 3,
|
|
787
|
+
schemaName: "triage create PR",
|
|
788
|
+
signal: input.input.signal,
|
|
789
|
+
title: `Magi triage create PR #${input.issue.number}`,
|
|
790
|
+
});
|
|
791
|
+
await emitProgress(input.input, {
|
|
792
|
+
sessionId: result.sessionId,
|
|
793
|
+
type: "triage_creator_completed",
|
|
794
|
+
});
|
|
795
|
+
await writeJson(join(input.outputDir, "create-pr.json"), result.value);
|
|
796
|
+
if (result.value.mode !== "EDITED")
|
|
797
|
+
return undefined;
|
|
798
|
+
const pullRequest = result.value.pullRequest;
|
|
799
|
+
if (!pullRequest)
|
|
800
|
+
throw new Error("EDITED requires pullRequest");
|
|
801
|
+
await pushHead(input.input.exec, input.input.repository, worktreePath, creator.account, {
|
|
802
|
+
owner: input.input.repository.github.owner,
|
|
803
|
+
ref: branch,
|
|
804
|
+
repo: input.input.repository.github.repo,
|
|
805
|
+
});
|
|
806
|
+
return createPullRequest(input.input.exec, input.input.repository, creator.account, {
|
|
807
|
+
body: pullRequest.body,
|
|
808
|
+
head: branch,
|
|
809
|
+
title: pullRequest.title,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
finally {
|
|
813
|
+
await removeWorktree(input.input.exec, worktreePath).catch(() => undefined);
|
|
814
|
+
}
|
|
701
815
|
}
|
|
702
|
-
|
|
703
|
-
await
|
|
816
|
+
catch (error) {
|
|
817
|
+
await emitProgress(input.input, {
|
|
818
|
+
error: error instanceof Error ? error.message : String(error),
|
|
819
|
+
type: "triage_creator_failed",
|
|
820
|
+
});
|
|
821
|
+
throw error;
|
|
704
822
|
}
|
|
705
823
|
}
|
|
706
824
|
export async function runTriage(input) {
|
|
@@ -718,7 +836,12 @@ export async function runTriage(input) {
|
|
|
718
836
|
runId,
|
|
719
837
|
});
|
|
720
838
|
await mkdir(outputDir, { recursive: true });
|
|
839
|
+
await emitProgress(input, { phase: "fetching issue", type: "phase" });
|
|
721
840
|
const issue = await fetchIssue(input.exec, input.repository, input.issue);
|
|
841
|
+
await emitProgress(input, {
|
|
842
|
+
phase: "scanning issue relationships",
|
|
843
|
+
type: "phase",
|
|
844
|
+
});
|
|
722
845
|
const relationship = await relationshipScan(input, issue);
|
|
723
846
|
const block = safetyBlocked(input, issue, Boolean(relationship.previousMarker));
|
|
724
847
|
await writeJson(join(outputDir, "issue.json"), issue);
|
|
@@ -738,6 +861,7 @@ export async function runTriage(input) {
|
|
|
738
861
|
}
|
|
739
862
|
let context = issueContext({ issue, relationship });
|
|
740
863
|
await writeFile(join(outputDir, "context.md"), `${context}\n`);
|
|
864
|
+
await emitProgress(input, { phase: "triaging", type: "phase" });
|
|
741
865
|
let processed = relationship.previousMarker?.processed ?? [];
|
|
742
866
|
let result;
|
|
743
867
|
if (relationship.previousMarker) {
|
|
@@ -848,6 +972,11 @@ export async function runTriage(input) {
|
|
|
848
972
|
createPr: false,
|
|
849
973
|
postComment: true,
|
|
850
974
|
};
|
|
975
|
+
await emitProgress(input, {
|
|
976
|
+
action: plan.action,
|
|
977
|
+
result: relatedPrDecision,
|
|
978
|
+
type: "decision",
|
|
979
|
+
});
|
|
851
980
|
await runActionPrompt({
|
|
852
981
|
context,
|
|
853
982
|
input,
|
|
@@ -865,7 +994,8 @@ export async function runTriage(input) {
|
|
|
865
994
|
result: relatedPrDecision,
|
|
866
995
|
});
|
|
867
996
|
if (!input.dryRun) {
|
|
868
|
-
await
|
|
997
|
+
await emitProgress(input, { type: "comment_posting" });
|
|
998
|
+
const posted = await postMarkedIssueComment({
|
|
869
999
|
account: triage.account,
|
|
870
1000
|
body,
|
|
871
1001
|
exec: input.exec,
|
|
@@ -873,6 +1003,7 @@ export async function runTriage(input) {
|
|
|
873
1003
|
outputDir,
|
|
874
1004
|
repository: input.repository,
|
|
875
1005
|
});
|
|
1006
|
+
await emitProgress(input, { type: "comment_posted", url: posted.url });
|
|
876
1007
|
const clearLabels = existingClearLabels(issue, triage.automation.clear);
|
|
877
1008
|
if (clearLabels.length) {
|
|
878
1009
|
await removeIssueLabels(input.exec, input.repository, issue.number, clearLabels, triage.account);
|
package/dist/prompts/compose.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { isAbsolute, join } from "node:path";
|
|
4
|
-
import { ciClassificationAfterEditOutputContract, ciClassificationOutputContract, closeReconsiderationOutputContract, editOutputContract, findingValidationOutputContract, rereviewCloseReconsiderationOutputContract, rereviewOutputContract, reviewOutputContract, triageCommentClassificationOutputContract,
|
|
4
|
+
import { ciClassificationAfterEditOutputContract, ciClassificationOutputContract, closeReconsiderationOutputContract, editOutputContract, findingValidationOutputContract, rereviewCloseReconsiderationOutputContract, rereviewOutputContract, reviewOutputContract, triageActionOutputContract, triageCommentClassificationOutputContract, triageCreatePrOutputContract, triageDuplicateOutputContract, triageVoteOutputContract, } from "./contracts";
|
|
5
5
|
async function readOptionalPrompt(directory, path, values = {}) {
|
|
6
6
|
if (!path)
|
|
7
7
|
return "";
|
|
@@ -387,7 +387,7 @@ export async function composeTriageCreatePrPrompt(input) {
|
|
|
387
387
|
task,
|
|
388
388
|
languageBlock(input.repository.language),
|
|
389
389
|
personaBlock(persona),
|
|
390
|
-
|
|
390
|
+
triageCreatePrOutputContract,
|
|
391
391
|
]
|
|
392
392
|
.filter(Boolean)
|
|
393
393
|
.join("\n\n");
|
|
@@ -148,6 +148,32 @@ Rules:
|
|
|
148
148
|
- responses must include a reply for each thread you addressed.
|
|
149
149
|
- REPLIED requires filesTouched to be empty and at least one DISAGREE or ASK response.
|
|
150
150
|
</output_contract>`.trim();
|
|
151
|
+
export const triageCreatePrOutputContract = `
|
|
152
|
+
<output_contract>
|
|
153
|
+
Return exactly one JSON object and nothing else. Do not wrap it in markdown.
|
|
154
|
+
|
|
155
|
+
The object must match this shape:
|
|
156
|
+
{
|
|
157
|
+
"mode": "EDITED" | "REPLIED",
|
|
158
|
+
"commitSha": "full sha, required only when mode is EDITED; omit when mode is REPLIED",
|
|
159
|
+
"commitMessage": "fix(scope): short description, required only when mode is EDITED; omit when mode is REPLIED",
|
|
160
|
+
"filesTouched": ["relative/path.ext"],
|
|
161
|
+
"pullRequest": {
|
|
162
|
+
"title": "PR title, required only when mode is EDITED; omit when mode is REPLIED",
|
|
163
|
+
"body": "PR body, required only when mode is EDITED; omit when mode is REPLIED"
|
|
164
|
+
},
|
|
165
|
+
"responses": [{ "commentId": 123, "action": "FIXED" | "DISAGREE" | "ASK", "body": "Fixed." }]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
Rules:
|
|
169
|
+
- Use EDITED only when you edited files, staged changes, and committed.
|
|
170
|
+
- Use REPLIED when you only replied without code changes.
|
|
171
|
+
- For EDITED, pullRequest.title and pullRequest.body must be non-empty and follow the repository's PR conventions.
|
|
172
|
+
- Do not push or create the PR. The orchestrator pushes and creates the PR using pullRequest exactly as provided.
|
|
173
|
+
- filesTouched must include every final changed file.
|
|
174
|
+
- responses may be empty when no review threads were addressed.
|
|
175
|
+
- REPLIED requires filesTouched to be empty and at least one DISAGREE or ASK response.
|
|
176
|
+
</output_contract>`.trim();
|
|
151
177
|
export const ciClassificationOutputContract = `
|
|
152
178
|
<output_contract>
|
|
153
179
|
Return exactly one JSON object and nothing else. Do not wrap it in markdown.
|
|
@@ -255,6 +281,7 @@ const outputContractsBySchemaName = {
|
|
|
255
281
|
"triage action": triageActionOutputContract,
|
|
256
282
|
"triage acceptance": triageVoteOutputContract('"YES" | "NO" | "ASK"'),
|
|
257
283
|
"triage category": triageVoteOutputContract('"ASK" or one of the configured category IDs'),
|
|
284
|
+
"triage create PR": triageCreatePrOutputContract,
|
|
258
285
|
"triage comment classification": triageCommentClassificationOutputContract,
|
|
259
286
|
"triage duplicate": triageDuplicateOutputContract,
|
|
260
287
|
"triage existing PR": triageVoteOutputContract('"RELATED_PR_HANDLES_ISSUE" | "RELATED_PR_DOES_NOT_HANDLE_ISSUE"'),
|
package/dist/prompts/output.js
CHANGED
|
@@ -317,6 +317,20 @@ export function parseCiClassificationOutput(text) {
|
|
|
317
317
|
}),
|
|
318
318
|
};
|
|
319
319
|
}
|
|
320
|
+
function parsePullRequest(value, options) {
|
|
321
|
+
if (value == null) {
|
|
322
|
+
if (options.required)
|
|
323
|
+
throw new Error("pullRequest is required");
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
if (typeof value !== "object")
|
|
327
|
+
throw new Error("pullRequest must be an object");
|
|
328
|
+
const pullRequest = value;
|
|
329
|
+
return {
|
|
330
|
+
body: requireString(pullRequest.body, "pullRequest.body"),
|
|
331
|
+
title: requireString(pullRequest.title, "pullRequest.title"),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
320
334
|
function parseEditOutputWithOptions(text, options) {
|
|
321
335
|
const data = extractJson(text);
|
|
322
336
|
if (!data || typeof data !== "object")
|
|
@@ -340,11 +354,15 @@ function parseEditOutputWithOptions(text, options) {
|
|
|
340
354
|
if (data.mode === "EDITED") {
|
|
341
355
|
if (!filesTouched.length)
|
|
342
356
|
throw new Error("EDITED requires filesTouched");
|
|
357
|
+
const pullRequest = parsePullRequest(data.pullRequest, {
|
|
358
|
+
required: options.requirePullRequest,
|
|
359
|
+
});
|
|
343
360
|
return {
|
|
344
361
|
commitMessage: requireString(data.commitMessage, "commitMessage"),
|
|
345
362
|
commitSha: requireString(data.commitSha, "commitSha"),
|
|
346
363
|
filesTouched,
|
|
347
364
|
mode: data.mode,
|
|
365
|
+
...(pullRequest ? { pullRequest } : {}),
|
|
348
366
|
responses,
|
|
349
367
|
};
|
|
350
368
|
}
|
|
@@ -364,8 +382,14 @@ function parseEditOutputWithOptions(text, options) {
|
|
|
364
382
|
};
|
|
365
383
|
}
|
|
366
384
|
export function parseEditOutput(text) {
|
|
367
|
-
return parseEditOutputWithOptions(text, {
|
|
385
|
+
return parseEditOutputWithOptions(text, {
|
|
386
|
+
requirePullRequest: false,
|
|
387
|
+
requireResponses: true,
|
|
388
|
+
});
|
|
368
389
|
}
|
|
369
390
|
export function parseTriageCreatePrOutput(text) {
|
|
370
|
-
return parseEditOutputWithOptions(text, {
|
|
391
|
+
return parseEditOutputWithOptions(text, {
|
|
392
|
+
requirePullRequest: true,
|
|
393
|
+
requireResponses: false,
|
|
394
|
+
});
|
|
371
395
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-magi",
|
|
3
|
-
"version": "0.0.0-dev-
|
|
3
|
+
"version": "0.0.0-dev-20260520182750",
|
|
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>",
|