@nyxa/nyx-agent 0.7.0 → 0.8.1
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 +19 -8
- package/dist/cli.js +3 -1
- package/dist/commands/init.js +53 -29
- package/dist/config/schema.js +45 -4
- package/dist/runtime/prompts.js +50 -17
- package/dist/runtime/runPipeline.js +580 -121
- package/dist/runtime/schemas.js +108 -22
- package/dist/runtime/selectionConfirmation.js +15 -4
- package/dist/runtime/workItems.js +349 -15
- package/package.json +1 -1
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { execa } from "execa";
|
|
2
3
|
import { loadConfig } from "../config/loadConfig.js";
|
|
3
|
-
import {
|
|
4
|
+
import { globalReviewRoles, } from "../config/schema.js";
|
|
5
|
+
import { ensureDir, pathExists, readText, writeText } from "./files.js";
|
|
4
6
|
import { deleteBranch, removeRunWorktree, setUpRunWorktree, } from "./gitLifecycle.js";
|
|
5
7
|
import { markWorkItemCompleted, readWorkItemLedger, writeWorkItemLedger, } from "./ledger.js";
|
|
6
8
|
import { getNyxDir, relativeToProject } from "./paths.js";
|
|
7
|
-
import { buildContextBlock, buildPhasePrompt, EXECUTION_PROMPT, GLOBAL_REVIEW_PROMPT, GLOBAL_REVISION_PROMPT, REVIEW_PROMPT, REVISION_PROMPT, SELECTION_PROMPT,
|
|
9
|
+
import { buildContextBlock, buildPhasePrompt, EXECUTION_PROMPT, GLOBAL_REVIEW_CHALLENGE_PROMPT, GLOBAL_REVIEW_PROMPT, GLOBAL_REVIEW_VALIDATION_PROMPT, GLOBAL_REVISION_PROMPT, REVIEW_CHALLENGE_PROMPT, REVIEW_PROMPT, REVIEW_VALIDATION_PROMPT, REVISION_PROMPT, SELECTION_PROMPT, } from "./prompts.js";
|
|
8
10
|
import { createRunReporter } from "./reporter.js";
|
|
9
11
|
import { runAgentPhase, } from "./runPhase.js";
|
|
10
|
-
import { GLOBAL_REVIEW_SCHEMA,
|
|
12
|
+
import { REVIEW_CHALLENGE_SCHEMA, REVIEW_DISCOVERY_SCHEMA, GLOBAL_REVIEW_SCHEMA, REVIEW_VALIDATION_SCHEMA, SELECTION_SCHEMA, } from "./schemas.js";
|
|
11
13
|
import { commitAll, commitsAhead, createPullRequest, pushBranch, rangeDiff, stageAllAndDiff, } from "./scm.js";
|
|
12
14
|
import { confirmWorkItemSelection, } from "./selectionConfirmation.js";
|
|
13
15
|
import { createRunId } from "./time.js";
|
|
14
|
-
import { filterAvailable,
|
|
16
|
+
import { filterAvailable, listGitHubWorkItemInventory, resolveSelectedQueue, } from "./workItems.js";
|
|
15
17
|
const MAX_CANDIDATES = 50;
|
|
16
18
|
const EXCERPT_CHARS = 800;
|
|
19
|
+
const CORRECTION_VALIDATION_MAX_ATTEMPTS = 3;
|
|
17
20
|
export function defaultPipelineDependencies() {
|
|
18
21
|
return {
|
|
19
|
-
listIssues:
|
|
22
|
+
listIssues: listGitHubWorkItemInventory,
|
|
20
23
|
runPhase: runAgentPhase,
|
|
21
24
|
pushBranch,
|
|
22
25
|
createPullRequest,
|
|
@@ -37,25 +40,32 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
|
|
|
37
40
|
const nyxDir = getNyxDir(projectRoot);
|
|
38
41
|
const configPath = input.configPath ?? path.join(nyxDir, "config.json");
|
|
39
42
|
const config = await loadConfig(configPath);
|
|
40
|
-
const
|
|
43
|
+
const baseAgent = resolveAgentProfile({
|
|
44
|
+
config,
|
|
45
|
+
cliHarness: input.harness,
|
|
46
|
+
});
|
|
41
47
|
const runId = createRunId();
|
|
42
48
|
const runDir = path.join(nyxDir, "runs", runId);
|
|
43
49
|
await ensureDir(runDir);
|
|
44
50
|
const reporter = input.reporter ?? createRunReporter({ verbose: input.verbose ?? false });
|
|
45
51
|
reporter.heading(`NyxAgent run ${runId}`);
|
|
46
|
-
reporter.info(`Harness: ${harness} · model: ${
|
|
52
|
+
reporter.info(`Harness: ${baseAgent.harness} · model: ${baseAgent.model} · review: ${config.review}`);
|
|
47
53
|
reporter.detail(`Config: ${relativeToProject(projectRoot, configPath)}`);
|
|
48
54
|
reporter.detail(`Artifacts: ${relativeToProject(projectRoot, runDir)}`);
|
|
49
55
|
reporter.detail(`Tracker: ${config.tracker.repo}`);
|
|
56
|
+
if (config.review_max_attempts !== undefined) {
|
|
57
|
+
reporter.warn("review_max_attempts is deprecated and ignored; use review_rounds.each/global instead.");
|
|
58
|
+
}
|
|
50
59
|
const ledger = await readWorkItemLedger(nyxDir);
|
|
51
60
|
reporter.detail(`Completed work items already in ledger: ${ledger.completed_work_item_keys.length}`);
|
|
52
61
|
// 1. Selection runs read-only in the main checkout, before any branch exists.
|
|
62
|
+
const inventory = normalizeWorkItemInventory(await deps.listIssues({
|
|
63
|
+
repo: config.tracker.repo,
|
|
64
|
+
maxCandidates: MAX_CANDIDATES,
|
|
65
|
+
excerptChars: EXCERPT_CHARS,
|
|
66
|
+
}));
|
|
53
67
|
const candidates = filterAvailable({
|
|
54
|
-
candidates:
|
|
55
|
-
repo: config.tracker.repo,
|
|
56
|
-
maxCandidates: MAX_CANDIDATES,
|
|
57
|
-
excerptChars: EXCERPT_CHARS,
|
|
58
|
-
}),
|
|
68
|
+
candidates: inventory.candidates,
|
|
59
69
|
completedKeys: ledger.completed_work_item_keys,
|
|
60
70
|
});
|
|
61
71
|
reporter.detail(`Available work item candidates: ${candidates.length}`);
|
|
@@ -66,7 +76,7 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
|
|
|
66
76
|
const proposed = await runSelection({
|
|
67
77
|
projectRoot,
|
|
68
78
|
runDir,
|
|
69
|
-
harness,
|
|
79
|
+
cliHarness: input.harness,
|
|
70
80
|
config,
|
|
71
81
|
candidates,
|
|
72
82
|
runPhase: deps.runPhase,
|
|
@@ -114,7 +124,7 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
|
|
|
114
124
|
item,
|
|
115
125
|
guidance: executionGuidance,
|
|
116
126
|
git,
|
|
117
|
-
harness,
|
|
127
|
+
cliHarness: input.harness,
|
|
118
128
|
config,
|
|
119
129
|
runPhase: deps.runPhase,
|
|
120
130
|
reporter,
|
|
@@ -124,9 +134,9 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
|
|
|
124
134
|
iterationDir,
|
|
125
135
|
item,
|
|
126
136
|
git,
|
|
127
|
-
harness,
|
|
137
|
+
cliHarness: input.harness,
|
|
128
138
|
config,
|
|
129
|
-
|
|
139
|
+
rounds: config.review_rounds.each,
|
|
130
140
|
runPhase: deps.runPhase,
|
|
131
141
|
reporter,
|
|
132
142
|
});
|
|
@@ -153,9 +163,10 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
|
|
|
153
163
|
const corrections = await runGlobalReviewLoop({
|
|
154
164
|
runDir,
|
|
155
165
|
git,
|
|
156
|
-
|
|
166
|
+
completed,
|
|
167
|
+
cliHarness: input.harness,
|
|
157
168
|
config,
|
|
158
|
-
|
|
169
|
+
rounds: config.review_rounds.global,
|
|
159
170
|
runPhase: deps.runPhase,
|
|
160
171
|
reporter,
|
|
161
172
|
});
|
|
@@ -177,7 +188,10 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
|
|
|
177
188
|
base: git.base,
|
|
178
189
|
head: git.branch,
|
|
179
190
|
title: buildPrTitle(completed),
|
|
180
|
-
body: buildPrBody(
|
|
191
|
+
body: buildPrBody({
|
|
192
|
+
items: completed,
|
|
193
|
+
parents: inventory.parents,
|
|
194
|
+
}),
|
|
181
195
|
});
|
|
182
196
|
reporter.success(`\nPull request opened: ${prUrl}`);
|
|
183
197
|
success = true;
|
|
@@ -193,6 +207,7 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
|
|
|
193
207
|
producedCommits,
|
|
194
208
|
completed,
|
|
195
209
|
config,
|
|
210
|
+
parents: inventory.parents,
|
|
196
211
|
deps,
|
|
197
212
|
reporter,
|
|
198
213
|
});
|
|
@@ -242,7 +257,11 @@ async function salvageFailedRun(input) {
|
|
|
242
257
|
base: input.git.base,
|
|
243
258
|
head: input.git.branch,
|
|
244
259
|
title: buildDraftPrTitle(input.completed),
|
|
245
|
-
body: buildDraftPrBody(
|
|
260
|
+
body: buildDraftPrBody({
|
|
261
|
+
items: input.completed,
|
|
262
|
+
parents: input.parents,
|
|
263
|
+
reason,
|
|
264
|
+
}),
|
|
246
265
|
draft: true,
|
|
247
266
|
});
|
|
248
267
|
input.reporter.warn(`\nRun failed, but the work was salvaged into a DRAFT pull request: ${url}`);
|
|
@@ -257,6 +276,10 @@ async function salvageFailedRun(input) {
|
|
|
257
276
|
}
|
|
258
277
|
}
|
|
259
278
|
async function runSelection(input) {
|
|
279
|
+
const agent = resolveAgentProfile({
|
|
280
|
+
config: input.config,
|
|
281
|
+
cliHarness: input.cliHarness,
|
|
282
|
+
});
|
|
260
283
|
const context = buildContextBlock([
|
|
261
284
|
["Repository", input.config.tracker.repo],
|
|
262
285
|
["Max work items this run", input.config.max_iterations],
|
|
@@ -267,6 +290,7 @@ async function runSelection(input) {
|
|
|
267
290
|
number: candidate.number,
|
|
268
291
|
title: candidate.title,
|
|
269
292
|
labels: candidate.labels,
|
|
293
|
+
parent: candidate.parent,
|
|
270
294
|
excerpt: candidate.excerpt,
|
|
271
295
|
})),
|
|
272
296
|
],
|
|
@@ -275,9 +299,9 @@ async function runSelection(input) {
|
|
|
275
299
|
phaseId: "selection",
|
|
276
300
|
phaseDir: path.join(input.runDir, "selection"),
|
|
277
301
|
workdir: input.projectRoot,
|
|
278
|
-
harness:
|
|
279
|
-
model:
|
|
280
|
-
reasoning:
|
|
302
|
+
harness: agent.harness,
|
|
303
|
+
model: agent.model,
|
|
304
|
+
reasoning: agent.reasoning_effort,
|
|
281
305
|
capability: "readonly",
|
|
282
306
|
prompt: buildPhasePrompt({
|
|
283
307
|
guidance: SELECTION_PROMPT,
|
|
@@ -304,6 +328,11 @@ async function runSelection(input) {
|
|
|
304
328
|
return resolved.queue;
|
|
305
329
|
}
|
|
306
330
|
async function runExecution(input) {
|
|
331
|
+
const agent = resolveAgentProfile({
|
|
332
|
+
config: input.config,
|
|
333
|
+
cliHarness: input.cliHarness,
|
|
334
|
+
phase: "execution",
|
|
335
|
+
});
|
|
307
336
|
const context = buildContextBlock([
|
|
308
337
|
["Work item", workItemSummary(input.item)],
|
|
309
338
|
["Issue description", input.item.excerpt ?? "(no description provided)"],
|
|
@@ -314,9 +343,9 @@ async function runExecution(input) {
|
|
|
314
343
|
phaseId: "execution",
|
|
315
344
|
phaseDir: path.join(input.iterationDir, "execution"),
|
|
316
345
|
workdir: input.git.worktree,
|
|
317
|
-
harness:
|
|
318
|
-
model:
|
|
319
|
-
reasoning:
|
|
346
|
+
harness: agent.harness,
|
|
347
|
+
model: agent.model,
|
|
348
|
+
reasoning: agent.reasoning_effort,
|
|
320
349
|
capability: "write",
|
|
321
350
|
prompt: buildPhasePrompt({ guidance: input.guidance, context }),
|
|
322
351
|
reporter: input.reporter,
|
|
@@ -326,54 +355,275 @@ async function runExecution(input) {
|
|
|
326
355
|
}
|
|
327
356
|
}
|
|
328
357
|
async function runReviewLoop(input) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
358
|
+
const agent = resolveAgentProfile({
|
|
359
|
+
config: input.config,
|
|
360
|
+
cliHarness: input.cliHarness,
|
|
361
|
+
phase: "review",
|
|
362
|
+
});
|
|
363
|
+
const validationHistory = [];
|
|
364
|
+
for (let round = 1; round <= input.rounds; round += 1) {
|
|
365
|
+
const roundDir = path.join(input.iterationDir, `review-round-${round}`);
|
|
366
|
+
const discoveryPack = await createReviewContextPack({
|
|
367
|
+
dir: path.join(roundDir, "discovery", "review-context"),
|
|
368
|
+
git: input.git,
|
|
369
|
+
scope: "item",
|
|
370
|
+
workItems: [input.item],
|
|
371
|
+
validations: validationHistory,
|
|
372
|
+
});
|
|
373
|
+
const discoveryResult = await input.runPhase({
|
|
332
374
|
phaseId: "review",
|
|
333
|
-
phaseDir: path.join(
|
|
375
|
+
phaseDir: path.join(roundDir, "discovery"),
|
|
334
376
|
workdir: input.git.worktree,
|
|
335
|
-
harness:
|
|
336
|
-
model:
|
|
337
|
-
reasoning:
|
|
377
|
+
harness: agent.harness,
|
|
378
|
+
model: agent.model,
|
|
379
|
+
reasoning: agent.reasoning_effort,
|
|
338
380
|
capability: "readonly",
|
|
339
381
|
prompt: buildPhasePrompt({
|
|
340
382
|
guidance: REVIEW_PROMPT,
|
|
341
383
|
context: buildContextBlock([
|
|
342
384
|
["Work item", workItemSummary(input.item)],
|
|
343
|
-
[
|
|
344
|
-
|
|
345
|
-
truncateForPrompt(diff || "(no changes)"),
|
|
346
|
-
],
|
|
385
|
+
["Review round", round],
|
|
386
|
+
["Review context", reviewContextSummary(discoveryPack)],
|
|
347
387
|
]),
|
|
348
|
-
schema:
|
|
388
|
+
schema: REVIEW_DISCOVERY_SCHEMA,
|
|
349
389
|
}),
|
|
350
|
-
schema:
|
|
390
|
+
schema: REVIEW_DISCOVERY_SCHEMA,
|
|
351
391
|
reporter: input.reporter,
|
|
352
392
|
});
|
|
353
|
-
if (!
|
|
354
|
-
throw new Error(
|
|
393
|
+
if (!discoveryResult.ok) {
|
|
394
|
+
throw new Error(discoveryResult.error);
|
|
355
395
|
}
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
396
|
+
const discovery = discoveryResult.result;
|
|
397
|
+
const proposedBlockers = normalizeFindings(discovery.blockers);
|
|
398
|
+
input.reporter.info(` review round ${round}: ${proposedBlockers.length} proposed blocker(s)`);
|
|
399
|
+
const challenge = await runReviewChallenge({
|
|
400
|
+
phaseId: "review_challenge",
|
|
401
|
+
phaseDir: path.join(roundDir, "challenge"),
|
|
402
|
+
workdir: input.git.worktree,
|
|
403
|
+
agent,
|
|
404
|
+
runPhase: input.runPhase,
|
|
405
|
+
reporter: input.reporter,
|
|
406
|
+
guidance: REVIEW_CHALLENGE_PROMPT,
|
|
407
|
+
contextEntries: [
|
|
408
|
+
["Work item", workItemSummary(input.item)],
|
|
409
|
+
["Review round", round],
|
|
410
|
+
["Review context", reviewContextSummary(discoveryPack)],
|
|
411
|
+
["Proposed blockers", proposedBlockers],
|
|
412
|
+
["Rejected findings from discovery", discovery.rejected_findings ?? []],
|
|
413
|
+
],
|
|
414
|
+
});
|
|
415
|
+
input.reporter.info(` review round ${round}: ${challenge.blockers.length} verified blocker(s)`);
|
|
416
|
+
await runCorrectionValidationLoop({
|
|
417
|
+
scope: "item",
|
|
418
|
+
roundDir,
|
|
419
|
+
git: input.git,
|
|
420
|
+
workItems: [input.item],
|
|
421
|
+
validationHistory,
|
|
422
|
+
blockers: challenge.blockers,
|
|
423
|
+
agent,
|
|
424
|
+
runPhase: input.runPhase,
|
|
425
|
+
reporter: input.reporter,
|
|
426
|
+
revisionPhaseId: "revision",
|
|
427
|
+
validationPhaseId: "review_validation",
|
|
428
|
+
revisionGuidance: REVISION_PROMPT,
|
|
429
|
+
validationGuidance: REVIEW_VALIDATION_PROMPT,
|
|
430
|
+
failureMessage: (blockers) => `Review for #${input.item.number} has unresolved blocker(s) after ${CORRECTION_VALIDATION_MAX_ATTEMPTS} correction validation attempts:${formatBlockers(blockers)}`,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async function runGlobalReviewLoop(input) {
|
|
435
|
+
let committedCorrections = false;
|
|
436
|
+
const validationHistory = [];
|
|
437
|
+
for (let round = 1; round <= input.rounds; round += 1) {
|
|
438
|
+
const roundDir = path.join(input.runDir, "final", `global-round-${round}`);
|
|
439
|
+
const discoveryPack = await createReviewContextPack({
|
|
440
|
+
dir: path.join(roundDir, "discovery", "review-context"),
|
|
441
|
+
git: input.git,
|
|
442
|
+
scope: "global",
|
|
443
|
+
workItems: input.completed,
|
|
444
|
+
validations: validationHistory,
|
|
445
|
+
});
|
|
446
|
+
const discoveries = [];
|
|
447
|
+
for (const role of globalReviewRoles) {
|
|
448
|
+
const roleAgent = resolveAgentProfile({
|
|
449
|
+
config: input.config,
|
|
450
|
+
cliHarness: input.cliHarness,
|
|
451
|
+
phase: "global_review",
|
|
452
|
+
role,
|
|
453
|
+
});
|
|
454
|
+
const reviewResult = await input.runPhase({
|
|
455
|
+
phaseId: "global_review",
|
|
456
|
+
phaseDir: path.join(roundDir, "discovery", role),
|
|
457
|
+
workdir: input.git.worktree,
|
|
458
|
+
harness: roleAgent.harness,
|
|
459
|
+
model: roleAgent.model,
|
|
460
|
+
reasoning: roleAgent.reasoning_effort,
|
|
461
|
+
capability: "readonly",
|
|
462
|
+
prompt: buildPhasePrompt({
|
|
463
|
+
guidance: buildGlobalReviewGuidance(role),
|
|
464
|
+
context: buildContextBlock([
|
|
465
|
+
["Run branch", `${input.git.branch} (base ${input.git.base})`],
|
|
466
|
+
["Review round", round],
|
|
467
|
+
["Reviewer role", role],
|
|
468
|
+
["Review context", reviewContextSummary(discoveryPack)],
|
|
469
|
+
]),
|
|
470
|
+
schema: GLOBAL_REVIEW_SCHEMA,
|
|
471
|
+
}),
|
|
472
|
+
schema: GLOBAL_REVIEW_SCHEMA,
|
|
473
|
+
reporter: input.reporter,
|
|
474
|
+
});
|
|
475
|
+
if (!reviewResult.ok) {
|
|
476
|
+
throw new Error(reviewResult.error);
|
|
477
|
+
}
|
|
478
|
+
discoveries.push(reviewResult.result);
|
|
360
479
|
}
|
|
361
|
-
|
|
362
|
-
|
|
480
|
+
const aggregated = aggregateDiscoveries(discoveries);
|
|
481
|
+
input.reporter.info(`global review round ${round}: ${aggregated.blockers.length} proposed blocker(s)`);
|
|
482
|
+
const challengeAgent = resolveAgentProfile({
|
|
483
|
+
config: input.config,
|
|
484
|
+
cliHarness: input.cliHarness,
|
|
485
|
+
phase: "global_review",
|
|
486
|
+
});
|
|
487
|
+
const challenge = await runReviewChallenge({
|
|
488
|
+
phaseId: "global_review_challenge",
|
|
489
|
+
phaseDir: path.join(roundDir, "challenge"),
|
|
490
|
+
workdir: input.git.worktree,
|
|
491
|
+
agent: challengeAgent,
|
|
492
|
+
runPhase: input.runPhase,
|
|
493
|
+
reporter: input.reporter,
|
|
494
|
+
guidance: GLOBAL_REVIEW_CHALLENGE_PROMPT,
|
|
495
|
+
contextEntries: [
|
|
496
|
+
["Run branch", `${input.git.branch} (base ${input.git.base})`],
|
|
497
|
+
["Review round", round],
|
|
498
|
+
["Review context", reviewContextSummary(discoveryPack)],
|
|
499
|
+
["Aggregated proposed blockers", aggregated.blockers],
|
|
500
|
+
["Aggregated rejected findings", aggregated.rejected_findings],
|
|
501
|
+
],
|
|
502
|
+
});
|
|
503
|
+
input.reporter.info(`global review round ${round}: ${challenge.blockers.length} verified blocker(s)`);
|
|
504
|
+
const roundCommittedCorrections = await runCorrectionValidationLoop({
|
|
505
|
+
scope: "global",
|
|
506
|
+
roundDir,
|
|
507
|
+
git: input.git,
|
|
508
|
+
workItems: input.completed,
|
|
509
|
+
validationHistory,
|
|
510
|
+
blockers: challenge.blockers,
|
|
511
|
+
agent: challengeAgent,
|
|
512
|
+
runPhase: input.runPhase,
|
|
513
|
+
reporter: input.reporter,
|
|
514
|
+
revisionPhaseId: "global_revision",
|
|
515
|
+
validationPhaseId: "global_review_validation",
|
|
516
|
+
revisionGuidance: GLOBAL_REVISION_PROMPT,
|
|
517
|
+
validationGuidance: GLOBAL_REVIEW_VALIDATION_PROMPT,
|
|
518
|
+
commitMessage: "Apply global review corrections",
|
|
519
|
+
failureMessage: (blockers) => `Global review has unresolved blocker(s) after ${CORRECTION_VALIDATION_MAX_ATTEMPTS} correction validation attempts:${formatBlockers(blockers)}`,
|
|
520
|
+
});
|
|
521
|
+
if (roundCommittedCorrections) {
|
|
522
|
+
committedCorrections = true;
|
|
363
523
|
}
|
|
524
|
+
}
|
|
525
|
+
return committedCorrections;
|
|
526
|
+
}
|
|
527
|
+
function resolveAgentProfile(input) {
|
|
528
|
+
const profile = {
|
|
529
|
+
harness: input.cliHarness ?? input.config.harness,
|
|
530
|
+
model: input.config.model,
|
|
531
|
+
reasoning_effort: input.config.reasoning_effort,
|
|
532
|
+
};
|
|
533
|
+
const phaseOverride = input.phase
|
|
534
|
+
? phaseAgentOverride(input.config, input.phase)
|
|
535
|
+
: undefined;
|
|
536
|
+
const roleOverride = input.role
|
|
537
|
+
? input.config.agents?.global_review?.roles?.[input.role]
|
|
538
|
+
: undefined;
|
|
539
|
+
return applyAgentOverride(applyAgentOverride(profile, phaseOverride), roleOverride);
|
|
540
|
+
}
|
|
541
|
+
function phaseAgentOverride(config, phase) {
|
|
542
|
+
if (phase === "execution") {
|
|
543
|
+
return config.agents?.execution;
|
|
544
|
+
}
|
|
545
|
+
if (phase === "review") {
|
|
546
|
+
return config.agents?.review;
|
|
547
|
+
}
|
|
548
|
+
return config.agents?.global_review;
|
|
549
|
+
}
|
|
550
|
+
function applyAgentOverride(profile, override) {
|
|
551
|
+
if (!override) {
|
|
552
|
+
return profile;
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
harness: override.harness ?? profile.harness,
|
|
556
|
+
model: override.model ?? profile.model,
|
|
557
|
+
reasoning_effort: override.reasoning_effort ?? profile.reasoning_effort,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function buildGlobalReviewGuidance(role) {
|
|
561
|
+
const focus = {
|
|
562
|
+
"diff-contract": "Focus on the public contract of the diff: APIs, CLI behavior, schemas, config compatibility, and generated artifacts.",
|
|
563
|
+
integration: "Focus on integration across touched modules, phase sequencing, artifact paths, and cross-item behavior.",
|
|
564
|
+
"domain-invariants": "Focus on NyxAgent workflow invariants: engine-owned git side effects, read-only review phases, closed pipeline control flow, and review semantics.",
|
|
565
|
+
"tests-validation": "Focus on test coverage, validation evidence, failure modes, and whether the committed changes are demonstrably safe.",
|
|
566
|
+
};
|
|
567
|
+
return `${GLOBAL_REVIEW_PROMPT}\n\nRole focus (${role}): ${focus[role]}`;
|
|
568
|
+
}
|
|
569
|
+
async function runReviewChallenge(input) {
|
|
570
|
+
const result = await input.runPhase({
|
|
571
|
+
phaseId: input.phaseId,
|
|
572
|
+
phaseDir: input.phaseDir,
|
|
573
|
+
workdir: input.workdir,
|
|
574
|
+
harness: input.agent.harness,
|
|
575
|
+
model: input.agent.model,
|
|
576
|
+
reasoning: input.agent.reasoning_effort,
|
|
577
|
+
capability: "readonly",
|
|
578
|
+
prompt: buildPhasePrompt({
|
|
579
|
+
guidance: input.guidance,
|
|
580
|
+
context: buildContextBlock(input.contextEntries),
|
|
581
|
+
schema: REVIEW_CHALLENGE_SCHEMA,
|
|
582
|
+
}),
|
|
583
|
+
schema: REVIEW_CHALLENGE_SCHEMA,
|
|
584
|
+
reporter: input.reporter,
|
|
585
|
+
});
|
|
586
|
+
if (!result.ok) {
|
|
587
|
+
throw new Error(result.error);
|
|
588
|
+
}
|
|
589
|
+
const challenge = result.result;
|
|
590
|
+
return {
|
|
591
|
+
...challenge,
|
|
592
|
+
blockers: normalizeFindings(challenge.blockers),
|
|
593
|
+
rejected_findings: normalizeFindings(challenge.rejected_findings),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
async function runCorrectionValidationLoop(input) {
|
|
597
|
+
let pending = normalizeFindings(input.blockers);
|
|
598
|
+
let committedCorrections = false;
|
|
599
|
+
if (pending.length === 0) {
|
|
600
|
+
return committedCorrections;
|
|
601
|
+
}
|
|
602
|
+
for (let attempt = 1; attempt <= CORRECTION_VALIDATION_MAX_ATTEMPTS; attempt += 1) {
|
|
603
|
+
const revisionPack = await createReviewContextPack({
|
|
604
|
+
dir: path.join(input.roundDir, `${input.revisionPhaseId}-${attempt}`, "review-context"),
|
|
605
|
+
git: input.git,
|
|
606
|
+
scope: input.scope,
|
|
607
|
+
workItems: input.workItems,
|
|
608
|
+
validations: input.validationHistory,
|
|
609
|
+
});
|
|
364
610
|
const revision = await input.runPhase({
|
|
365
|
-
phaseId:
|
|
366
|
-
phaseDir: path.join(input.
|
|
611
|
+
phaseId: input.revisionPhaseId,
|
|
612
|
+
phaseDir: path.join(input.roundDir, `${input.revisionPhaseId}-${attempt}`),
|
|
367
613
|
workdir: input.git.worktree,
|
|
368
|
-
harness: input.harness,
|
|
369
|
-
model: input.
|
|
370
|
-
reasoning: input.
|
|
614
|
+
harness: input.agent.harness,
|
|
615
|
+
model: input.agent.model,
|
|
616
|
+
reasoning: input.agent.reasoning_effort,
|
|
371
617
|
capability: "write",
|
|
372
618
|
prompt: buildPhasePrompt({
|
|
373
|
-
guidance:
|
|
619
|
+
guidance: input.revisionGuidance,
|
|
374
620
|
context: buildContextBlock([
|
|
375
|
-
["
|
|
376
|
-
[
|
|
621
|
+
["Review context", reviewContextSummary(revisionPack)],
|
|
622
|
+
[
|
|
623
|
+
"Correction attempt",
|
|
624
|
+
`${attempt}/${CORRECTION_VALIDATION_MAX_ATTEMPTS}`,
|
|
625
|
+
],
|
|
626
|
+
["Verified blockers", pending],
|
|
377
627
|
]),
|
|
378
628
|
}),
|
|
379
629
|
reporter: input.reporter,
|
|
@@ -381,74 +631,233 @@ async function runReviewLoop(input) {
|
|
|
381
631
|
if (!revision.ok) {
|
|
382
632
|
throw new Error(revision.error);
|
|
383
633
|
}
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
634
|
+
if (input.commitMessage) {
|
|
635
|
+
const { committed } = await commitAll({
|
|
636
|
+
cwd: input.git.worktree,
|
|
637
|
+
message: input.commitMessage,
|
|
638
|
+
});
|
|
639
|
+
if (committed) {
|
|
640
|
+
committedCorrections = true;
|
|
641
|
+
input.reporter.detail("Committed global review corrections.");
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const validationPack = await createReviewContextPack({
|
|
645
|
+
dir: path.join(input.roundDir, `${input.validationPhaseId}-${attempt}`, "review-context"),
|
|
646
|
+
git: input.git,
|
|
647
|
+
scope: input.scope,
|
|
648
|
+
workItems: input.workItems,
|
|
649
|
+
validations: input.validationHistory,
|
|
650
|
+
});
|
|
651
|
+
const validationResult = await input.runPhase({
|
|
652
|
+
phaseId: input.validationPhaseId,
|
|
653
|
+
phaseDir: path.join(input.roundDir, `${input.validationPhaseId}-${attempt}`),
|
|
393
654
|
workdir: input.git.worktree,
|
|
394
|
-
harness: input.harness,
|
|
395
|
-
model: input.
|
|
396
|
-
reasoning: input.
|
|
655
|
+
harness: input.agent.harness,
|
|
656
|
+
model: input.agent.model,
|
|
657
|
+
reasoning: input.agent.reasoning_effort,
|
|
397
658
|
capability: "readonly",
|
|
398
659
|
prompt: buildPhasePrompt({
|
|
399
|
-
guidance:
|
|
660
|
+
guidance: input.validationGuidance,
|
|
400
661
|
context: buildContextBlock([
|
|
401
|
-
["
|
|
662
|
+
["Review context", reviewContextSummary(validationPack)],
|
|
402
663
|
[
|
|
403
|
-
"
|
|
404
|
-
|
|
664
|
+
"Correction attempt",
|
|
665
|
+
`${attempt}/${CORRECTION_VALIDATION_MAX_ATTEMPTS}`,
|
|
405
666
|
],
|
|
667
|
+
["Validated blockers", pending],
|
|
406
668
|
]),
|
|
407
|
-
schema:
|
|
669
|
+
schema: REVIEW_VALIDATION_SCHEMA,
|
|
408
670
|
}),
|
|
409
|
-
schema:
|
|
671
|
+
schema: REVIEW_VALIDATION_SCHEMA,
|
|
410
672
|
reporter: input.reporter,
|
|
411
673
|
});
|
|
412
|
-
if (!
|
|
413
|
-
throw new Error(
|
|
674
|
+
if (!validationResult.ok) {
|
|
675
|
+
throw new Error(validationResult.error);
|
|
414
676
|
}
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
677
|
+
const validation = validationResult.result;
|
|
678
|
+
const validations = normalizeValidations(validation.validations);
|
|
679
|
+
input.validationHistory.push(...validations);
|
|
680
|
+
pending = blockersNeedingCorrection(pending, validations);
|
|
681
|
+
input.reporter.info(`${input.scope} validation attempt ${attempt}: ${pending.length} unresolved blocker(s)`);
|
|
682
|
+
if (pending.length === 0) {
|
|
418
683
|
return committedCorrections;
|
|
419
684
|
}
|
|
420
|
-
|
|
421
|
-
|
|
685
|
+
}
|
|
686
|
+
throw new Error(input.failureMessage(pending));
|
|
687
|
+
}
|
|
688
|
+
async function createReviewContextPack(input) {
|
|
689
|
+
await ensureDir(input.dir);
|
|
690
|
+
const patch = input.scope === "item"
|
|
691
|
+
? await stageAllAndDiff(input.git.worktree)
|
|
692
|
+
: await rangeDiff(input.git.worktree, input.git.base);
|
|
693
|
+
const diffstat = input.scope === "item"
|
|
694
|
+
? await gitOutput(input.git.worktree, ["diff", "--cached", "--stat"], "diff --cached --stat")
|
|
695
|
+
: await gitOutput(input.git.worktree, ["diff", "--stat", `${input.git.base}..HEAD`], "diff --stat range");
|
|
696
|
+
const files = input.scope === "item"
|
|
697
|
+
? await gitOutput(input.git.worktree, ["diff", "--cached", "--name-only"], "diff --cached --name-only")
|
|
698
|
+
: await gitOutput(input.git.worktree, ["diff", "--name-only", `${input.git.base}..HEAD`], "diff --name-only range");
|
|
699
|
+
const commits = await gitOutput(input.git.worktree, ["log", "--oneline", `${input.git.base}..HEAD`], "log range");
|
|
700
|
+
const pack = {
|
|
701
|
+
dir: input.dir,
|
|
702
|
+
summaryPath: path.join(input.dir, "summary.md"),
|
|
703
|
+
patchPath: path.join(input.dir, "combined.patch"),
|
|
704
|
+
diffstatPath: path.join(input.dir, "diffstat.txt"),
|
|
705
|
+
commitsPath: path.join(input.dir, "commits.txt"),
|
|
706
|
+
filesPath: path.join(input.dir, "modified-files.txt"),
|
|
707
|
+
issuesPath: path.join(input.dir, "issues.json"),
|
|
708
|
+
validationsPath: path.join(input.dir, "validations.json"),
|
|
709
|
+
};
|
|
710
|
+
await writeText(pack.patchPath, textOrPlaceholder(patch, "(no changes)"));
|
|
711
|
+
await writeText(pack.diffstatPath, textOrPlaceholder(diffstat, "(no diffstat)"));
|
|
712
|
+
await writeText(pack.commitsPath, textOrPlaceholder(commits, "(no commits yet)"));
|
|
713
|
+
await writeText(pack.filesPath, textOrPlaceholder(files, "(no modified files)"));
|
|
714
|
+
await writeText(pack.issuesPath, `${JSON.stringify((input.workItems ?? []).map(workItemSummary), null, 2)}\n`);
|
|
715
|
+
await writeText(pack.validationsPath, `${JSON.stringify(input.validations, null, 2)}\n`);
|
|
716
|
+
await writeText(pack.summaryPath, [
|
|
717
|
+
"# NyxAgent review context",
|
|
718
|
+
"",
|
|
719
|
+
`Scope: ${input.scope}`,
|
|
720
|
+
`Branch: ${input.git.branch}`,
|
|
721
|
+
`Base: ${input.git.base}`,
|
|
722
|
+
"",
|
|
723
|
+
"Artifacts:",
|
|
724
|
+
`- combined patch: ${pack.patchPath}`,
|
|
725
|
+
`- diffstat: ${pack.diffstatPath}`,
|
|
726
|
+
`- modified files: ${pack.filesPath}`,
|
|
727
|
+
`- commits: ${pack.commitsPath}`,
|
|
728
|
+
`- issues: ${pack.issuesPath}`,
|
|
729
|
+
`- validations: ${pack.validationsPath}`,
|
|
730
|
+
"",
|
|
731
|
+
"Inspect these files, or run the corresponding git commands in the working directory.",
|
|
732
|
+
].join("\n"));
|
|
733
|
+
return pack;
|
|
734
|
+
}
|
|
735
|
+
async function gitOutput(cwd, args, label) {
|
|
736
|
+
const result = await execa("git", args, { cwd, reject: false });
|
|
737
|
+
if (result.exitCode !== 0) {
|
|
738
|
+
const detail = (result.stderr || result.stdout || "unknown error").trim();
|
|
739
|
+
throw new Error(`git ${label} failed: ${detail}`);
|
|
740
|
+
}
|
|
741
|
+
return result.stdout;
|
|
742
|
+
}
|
|
743
|
+
function reviewContextSummary(pack) {
|
|
744
|
+
return {
|
|
745
|
+
directory: pack.dir,
|
|
746
|
+
summary: pack.summaryPath,
|
|
747
|
+
combined_patch: pack.patchPath,
|
|
748
|
+
diffstat: pack.diffstatPath,
|
|
749
|
+
modified_files: pack.filesPath,
|
|
750
|
+
commits: pack.commitsPath,
|
|
751
|
+
issues: pack.issuesPath,
|
|
752
|
+
validations: pack.validationsPath,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
function aggregateDiscoveries(discoveries) {
|
|
756
|
+
return {
|
|
757
|
+
summary: discoveries
|
|
758
|
+
.map((discovery) => discovery.summary)
|
|
759
|
+
.filter((summary) => Boolean(summary))
|
|
760
|
+
.join("\n"),
|
|
761
|
+
blockers: dedupeFindings(discoveries.flatMap((discovery) => normalizeFindings(discovery.blockers))),
|
|
762
|
+
test_gaps: dedupeFindings(discoveries.flatMap((discovery) => normalizeFindings(discovery.test_gaps))),
|
|
763
|
+
advisory_findings: dedupeFindings(discoveries.flatMap((discovery) => normalizeFindings(discovery.advisory_findings))),
|
|
764
|
+
uncertain_findings: dedupeFindings(discoveries.flatMap((discovery) => normalizeFindings(discovery.uncertain_findings))),
|
|
765
|
+
rejected_findings: dedupeFindings(discoveries.flatMap((discovery) => normalizeFindings(discovery.rejected_findings))),
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
function blockersNeedingCorrection(pending, validations) {
|
|
769
|
+
const byTitle = new Map(pending.map((blocker) => [normalizeTitle(blocker.title), blocker]));
|
|
770
|
+
const seen = new Set();
|
|
771
|
+
const unresolved = [];
|
|
772
|
+
for (const validation of validations) {
|
|
773
|
+
const key = normalizeTitle(validation.blocker_title);
|
|
774
|
+
seen.add(key);
|
|
775
|
+
if (validation.status === "unresolved") {
|
|
776
|
+
const original = byTitle.get(key);
|
|
777
|
+
if (original) {
|
|
778
|
+
unresolved.push({
|
|
779
|
+
...original,
|
|
780
|
+
required_change: validation.required_change ?? original.required_change,
|
|
781
|
+
evidence: normalizeEvidence(validation.evidence, original.evidence),
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
continue;
|
|
422
785
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
prompt: buildPhasePrompt({
|
|
432
|
-
guidance: GLOBAL_REVISION_PROMPT,
|
|
433
|
-
context: buildContextBlock([
|
|
434
|
-
["Required changes", review.required_changes ?? []],
|
|
435
|
-
]),
|
|
436
|
-
}),
|
|
437
|
-
reporter: input.reporter,
|
|
438
|
-
});
|
|
439
|
-
if (!revision.ok) {
|
|
440
|
-
throw new Error(revision.error);
|
|
786
|
+
if (validation.status === "regression_from_correction") {
|
|
787
|
+
unresolved.push({
|
|
788
|
+
title: validation.blocker_title,
|
|
789
|
+
required_change: validation.required_change ??
|
|
790
|
+
`Fix regression from correction: ${validation.blocker_title}`,
|
|
791
|
+
confidence: "high",
|
|
792
|
+
evidence: normalizeEvidence(validation.evidence),
|
|
793
|
+
});
|
|
441
794
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (committed) {
|
|
447
|
-
committedCorrections = true;
|
|
448
|
-
input.reporter.detail("Committed global review corrections.");
|
|
795
|
+
}
|
|
796
|
+
for (const blocker of pending) {
|
|
797
|
+
if (!seen.has(normalizeTitle(blocker.title))) {
|
|
798
|
+
unresolved.push(blocker);
|
|
449
799
|
}
|
|
450
800
|
}
|
|
451
|
-
return
|
|
801
|
+
return dedupeFindings(unresolved);
|
|
802
|
+
}
|
|
803
|
+
function normalizeFindings(findings) {
|
|
804
|
+
return dedupeFindings((findings ?? [])
|
|
805
|
+
.filter((finding) => finding.title && finding.required_change)
|
|
806
|
+
.map((finding) => ({
|
|
807
|
+
title: finding.title,
|
|
808
|
+
required_change: finding.required_change,
|
|
809
|
+
confidence: normalizeConfidence(finding.confidence),
|
|
810
|
+
evidence: normalizeEvidence(finding.evidence),
|
|
811
|
+
})));
|
|
812
|
+
}
|
|
813
|
+
function normalizeValidations(validations) {
|
|
814
|
+
const allowed = new Set([
|
|
815
|
+
"resolved",
|
|
816
|
+
"unresolved",
|
|
817
|
+
"false_positive",
|
|
818
|
+
"regression_from_correction",
|
|
819
|
+
]);
|
|
820
|
+
return (validations ?? [])
|
|
821
|
+
.filter((validation) => validation.blocker_title && allowed.has(validation.status))
|
|
822
|
+
.map((validation) => ({
|
|
823
|
+
...validation,
|
|
824
|
+
evidence: normalizeEvidence(validation.evidence),
|
|
825
|
+
}));
|
|
826
|
+
}
|
|
827
|
+
function dedupeFindings(findings) {
|
|
828
|
+
const seen = new Set();
|
|
829
|
+
const deduped = [];
|
|
830
|
+
for (const finding of findings) {
|
|
831
|
+
const key = `${normalizeTitle(finding.title)}\n${finding.required_change.trim().toLowerCase()}`;
|
|
832
|
+
if (seen.has(key)) {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
seen.add(key);
|
|
836
|
+
deduped.push(finding);
|
|
837
|
+
}
|
|
838
|
+
return deduped;
|
|
839
|
+
}
|
|
840
|
+
function normalizeEvidence(evidence, fallback) {
|
|
841
|
+
if (Array.isArray(evidence) && evidence.length > 0) {
|
|
842
|
+
return evidence;
|
|
843
|
+
}
|
|
844
|
+
if (fallback && fallback.length > 0) {
|
|
845
|
+
return fallback;
|
|
846
|
+
}
|
|
847
|
+
return [{ detail: "No evidence provided." }];
|
|
848
|
+
}
|
|
849
|
+
function normalizeConfidence(value) {
|
|
850
|
+
if (value === "low" || value === "medium" || value === "high") {
|
|
851
|
+
return value;
|
|
852
|
+
}
|
|
853
|
+
return "medium";
|
|
854
|
+
}
|
|
855
|
+
function normalizeTitle(value) {
|
|
856
|
+
return value.trim().toLowerCase();
|
|
857
|
+
}
|
|
858
|
+
function textOrPlaceholder(text, placeholder) {
|
|
859
|
+
const trimmed = text.trim();
|
|
860
|
+
return `${trimmed.length > 0 ? trimmed : placeholder}\n`;
|
|
452
861
|
}
|
|
453
862
|
async function loadExecutionGuidance(nyxDir) {
|
|
454
863
|
const override = path.join(nyxDir, "prompts", "execution.md");
|
|
@@ -468,6 +877,7 @@ function workItemSummary(item) {
|
|
|
468
877
|
locator: item.source.locator,
|
|
469
878
|
url: item.url,
|
|
470
879
|
labels: item.labels,
|
|
880
|
+
parent: item.parent,
|
|
471
881
|
};
|
|
472
882
|
}
|
|
473
883
|
function buildCommitMessage(item) {
|
|
@@ -479,41 +889,90 @@ function buildPrTitle(items) {
|
|
|
479
889
|
}
|
|
480
890
|
return `NyxAgent: ${items.length} work items`;
|
|
481
891
|
}
|
|
482
|
-
function
|
|
483
|
-
|
|
892
|
+
function normalizeWorkItemInventory(result) {
|
|
893
|
+
if (Array.isArray(result)) {
|
|
894
|
+
return { candidates: result, parents: [] };
|
|
895
|
+
}
|
|
896
|
+
return {
|
|
897
|
+
candidates: result.candidates,
|
|
898
|
+
parents: result.parents ?? [],
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function buildPrBody(input) {
|
|
902
|
+
const list = input.items
|
|
484
903
|
.map((item) => `- ${item.title} (#${item.number})`)
|
|
485
904
|
.join("\n");
|
|
486
|
-
const
|
|
487
|
-
|
|
905
|
+
const completedParents = completedParentClosures(input);
|
|
906
|
+
const closeNumbers = [
|
|
907
|
+
...uniqueNumbers(input.items.map((item) => item.number)),
|
|
908
|
+
...uniqueNumbers(completedParents.map((parent) => parent.number)),
|
|
909
|
+
];
|
|
910
|
+
const closes = closeNumbers.map((number) => `Closes #${number}`).join("\n");
|
|
911
|
+
const sections = [
|
|
488
912
|
"Automated changes by NyxAgent.",
|
|
489
913
|
"",
|
|
490
914
|
"## Work items",
|
|
491
915
|
"",
|
|
492
916
|
list,
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
917
|
+
];
|
|
918
|
+
if (completedParents.length > 0) {
|
|
919
|
+
sections.push("", "## Completed plans", "", completedParents
|
|
920
|
+
.map((parent) => `- ${parent.title ?? `Parent issue ${parent.number}`} (#${parent.number})`)
|
|
921
|
+
.join("\n"));
|
|
922
|
+
}
|
|
923
|
+
sections.push("", closes);
|
|
924
|
+
return sections.join("\n");
|
|
496
925
|
}
|
|
497
926
|
function buildDraftPrTitle(items) {
|
|
498
927
|
return `[Draft] ${buildPrTitle(items)}`;
|
|
499
928
|
}
|
|
500
|
-
function buildDraftPrBody(
|
|
929
|
+
function buildDraftPrBody(input) {
|
|
501
930
|
return [
|
|
502
931
|
"> [!WARNING]",
|
|
503
932
|
"> This pull request was opened automatically by NyxAgent after the run",
|
|
504
933
|
"> **failed review**. The work is preserved here for a human to finish.",
|
|
505
934
|
"",
|
|
506
|
-
`**Why the run failed:** ${reason}`,
|
|
935
|
+
`**Why the run failed:** ${input.reason}`,
|
|
507
936
|
"",
|
|
508
|
-
buildPrBody(
|
|
937
|
+
buildPrBody({
|
|
938
|
+
items: input.items,
|
|
939
|
+
parents: input.parents,
|
|
940
|
+
}),
|
|
509
941
|
].join("\n");
|
|
510
942
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
943
|
+
function completedParentClosures(input) {
|
|
944
|
+
const completedKeys = new Set(input.items.map((item) => item.key));
|
|
945
|
+
const completedParentKeys = new Set(input.items
|
|
946
|
+
.map((item) => item.parent?.key)
|
|
947
|
+
.filter((key) => Boolean(key)));
|
|
948
|
+
const completedParents = [];
|
|
949
|
+
for (const parent of input.parents ?? []) {
|
|
950
|
+
if (!parent.closable || !completedParentKeys.has(parent.key)) {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
const openChildren = parent.children.filter((child) => child.state !== "closed");
|
|
954
|
+
const openContainerChild = openChildren.some((child) => !child.executable);
|
|
955
|
+
if (openContainerChild) {
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
const completedChildInThisPr = parent.children.some((child) => completedKeys.has(child.key));
|
|
959
|
+
if (!completedChildInThisPr ||
|
|
960
|
+
openChildren.some((child) => !completedKeys.has(child.key))) {
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
completedParents.push(parent);
|
|
964
|
+
}
|
|
965
|
+
return completedParents;
|
|
966
|
+
}
|
|
967
|
+
function uniqueNumbers(numbers) {
|
|
968
|
+
return [...new Set(numbers)];
|
|
969
|
+
}
|
|
970
|
+
/** Render unresolved blockers as a bullet list to append to a failure message. */
|
|
971
|
+
function formatBlockers(blockers) {
|
|
972
|
+
if (blockers.length === 0) {
|
|
514
973
|
return "";
|
|
515
974
|
}
|
|
516
|
-
return `\n\nUnresolved review
|
|
517
|
-
.map((
|
|
975
|
+
return `\n\nUnresolved review blockers:\n${blockers
|
|
976
|
+
.map((blocker) => `- ${blocker.title}: ${blocker.required_change}`)
|
|
518
977
|
.join("\n")}`;
|
|
519
978
|
}
|