@ferris1225/pi-subagents 4.1.3 → 4.1.5
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 +110 -99
- package/agents/cleaner.md +3 -2
- package/agents/documenter.md +6 -6
- package/agents/reviewer.md +8 -3
- package/agents/worker.md +4 -4
- package/package.json +1 -1
- package/src/completion.ts +160 -160
- package/src/config.ts +8 -6
- package/src/dispatch.ts +173 -65
- package/src/fixloop.ts +108 -102
- package/src/models.ts +189 -189
- package/src/monitor.ts +25 -0
- package/src/prompt.ts +24 -26
- package/src/recovery.ts +145 -145
- package/src/session-fork.ts +80 -80
- package/src/thread-lifecycle.ts +19 -5
- package/src/widget.ts +59 -8
package/src/dispatch.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `subagent` tool: dispatches explorer/worker/cleaner/documenter/reviewer agents as isolated pi
|
|
3
3
|
* child processes, single or parallel. Owns the public dispatch contract,
|
|
4
|
-
* per-run status tracking, managed
|
|
5
|
-
* reviewer
|
|
6
|
-
*
|
|
4
|
+
* per-run status tracking, managed worker/cleaner → reviewer workflows with a
|
|
5
|
+
* conditional documenter, bounded worker/reviewer fix rounds, and internal
|
|
6
|
+
* step launching. Stable
|
|
7
|
+
* thread generations, final integration, and completion ownership live in
|
|
7
8
|
* thread-lifecycle.ts.
|
|
8
9
|
*/
|
|
9
10
|
|
|
@@ -17,12 +18,11 @@ import { discoverAgents, resolveAgentTools, type AgentConfig } from "./agents.ts
|
|
|
17
18
|
import { loadConfig } from "./config.ts";
|
|
18
19
|
import { formatUsage, queuedResult } from "./format.ts";
|
|
19
20
|
import {
|
|
20
|
-
|
|
21
|
+
buildFinalDocumenterBrief,
|
|
21
22
|
buildFinalReviewBrief,
|
|
22
23
|
buildFixTaskBrief,
|
|
23
|
-
buildPostWriterDocumenterBrief,
|
|
24
24
|
buildReReviewBrief,
|
|
25
|
-
|
|
25
|
+
documentationDisposition,
|
|
26
26
|
type ChainStep,
|
|
27
27
|
type ManagedWorkflowOutcome,
|
|
28
28
|
} from "./fixloop.ts";
|
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
monitor,
|
|
33
33
|
statusIcon,
|
|
34
34
|
type RunChainMeta,
|
|
35
|
+
type WorkflowStage,
|
|
36
|
+
type WorkflowStageStatus,
|
|
35
37
|
} from "./monitor.ts";
|
|
36
38
|
import type { SubagentRuntime } from "./runtime.ts";
|
|
37
39
|
import {
|
|
@@ -87,6 +89,14 @@ export function defaultIsolationMode(mode: "single" | "parallel", agentName: str
|
|
|
87
89
|
return mode === "parallel" && agentName === "worker" ? "worktree" : "shared";
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
function workflowStageStatus(result: SingleResult): WorkflowStageStatus {
|
|
93
|
+
if (isFailedResult(result)) return "failed";
|
|
94
|
+
if (result.agent !== "reviewer") return "done";
|
|
95
|
+
const verdict = reviewVerdict(getResultOutput(result));
|
|
96
|
+
if (verdict === "fail") return "changes";
|
|
97
|
+
return verdict === "pass" ? "done" : "failed";
|
|
98
|
+
}
|
|
99
|
+
|
|
90
100
|
const managedRepositoryRootTails = new Map<string, Promise<void>>();
|
|
91
101
|
|
|
92
102
|
async function canonicalManagedRepositoryRoot(cwd: string): Promise<string> {
|
|
@@ -169,15 +179,15 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
169
179
|
name: "subagent",
|
|
170
180
|
label: "Subagent",
|
|
171
181
|
description: [
|
|
172
|
-
"Dispatch enabled specialized agents as isolated leaf Pi child processes, singly or in parallel.",
|
|
173
|
-
"Built-ins: explorer for broad read-only reconnaissance; worker for implementation; cleaner
|
|
174
|
-
"Work starts in the background
|
|
182
|
+
"Dispatch enabled specialized agents as isolated leaf Pi child processes, singly or in parallel; keep small known-target work in the main thread with direct tools.",
|
|
183
|
+
"Built-ins: explorer for broad read-only reconnaissance (a retrieval index, never a gate); worker for implementation; cleaner as a separate explicitly authorized cleanup/removal/simplification/deduplication entry; documenter for explicit docs/comments work or conditional final diff sync; reviewer for generic read-only assessments and independent code gates.",
|
|
184
|
+
"Work starts in the background. Successful worker/cleaner runs keep one enabled reviewer gate and bounded fix loop; documenter runs afterward only when REVIEW_PASS reports DOCUMENTATION: NEEDED or omits the marker, with a reviewer-disabled fallback. A top-level documenter delivers directly. Results resume the main agent and are already shown, so do not poll, duplicate downstream roles, or restate them.",
|
|
175
185
|
"Single tasks default to shared; parallel workers default to detached Git worktrees. Only write-capable agents can use worktree isolation, and failures never fall back silently to shared.",
|
|
176
186
|
"A selected-model or provider failure continues the retained session on the current main model; ordinary tool/task failures do not.",
|
|
177
187
|
"Use subagent_control to steer/retarget an active top-level child, park/stop a managed downstream stage, or resume/fork retained context by stable run id.",
|
|
178
188
|
].join(" "),
|
|
179
189
|
promptSnippet:
|
|
180
|
-
"Dispatch isolated background agents
|
|
190
|
+
"Dispatch isolated background agents for broad recon, self-contained implementation, authorized cleanup, explicit docs, or independent review; keep small known-target work on direct tools. Worker/cleaner gates and only needed/conservative docs sync run automatically, results resume automatically, and each workflow delivers once.",
|
|
181
191
|
parameters: SubagentParams,
|
|
182
192
|
|
|
183
193
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -389,57 +399,162 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
389
399
|
const enabled = (name: string): boolean =>
|
|
390
400
|
request.agents.some((candidate) => candidate.name === name);
|
|
391
401
|
const canContinue = (): boolean => runtime.sessionActive && !request.signal.aborted;
|
|
402
|
+
|
|
403
|
+
// Keep a live parent-owned projection because settled internal rows are
|
|
404
|
+
// intentionally removed. Only real/currently planned stages enter it.
|
|
405
|
+
const initialStageRelation = initialStepResult.agent === "worker"
|
|
406
|
+
? "implement"
|
|
407
|
+
: initialStepResult.agent === "cleaner"
|
|
408
|
+
? "cleanup"
|
|
409
|
+
: "review";
|
|
410
|
+
const workflowStages: WorkflowStage[] = [{
|
|
411
|
+
agent: initialStepResult.agent,
|
|
412
|
+
relation: initialStageRelation,
|
|
413
|
+
status: workflowStageStatus(initialStepResult),
|
|
414
|
+
}];
|
|
415
|
+
let reviewStage: WorkflowStage | undefined;
|
|
416
|
+
if (request.plan.kind === "post-writer" && enabled("reviewer")) {
|
|
417
|
+
reviewStage = { agent: "reviewer", relation: "review", status: "pending" };
|
|
418
|
+
workflowStages.push(reviewStage);
|
|
419
|
+
}
|
|
420
|
+
let documentationStage: WorkflowStage | undefined;
|
|
421
|
+
if (enabled("documenter")) {
|
|
422
|
+
documentationStage = { agent: "documenter", relation: "docs", status: "pending" };
|
|
423
|
+
workflowStages.push(documentationStage);
|
|
424
|
+
}
|
|
425
|
+
const publishWorkflowStages = (): void => {
|
|
426
|
+
monitor.setWorkflowStages(request.parentRunId, workflowStages);
|
|
427
|
+
};
|
|
428
|
+
const insertBeforeDocumentation = (stage: WorkflowStage): void => {
|
|
429
|
+
const documentationIndex = documentationStage
|
|
430
|
+
? workflowStages.indexOf(documentationStage)
|
|
431
|
+
: -1;
|
|
432
|
+
if (documentationIndex === -1) workflowStages.push(stage);
|
|
433
|
+
else workflowStages.splice(documentationIndex, 0, stage);
|
|
434
|
+
};
|
|
435
|
+
const removeDocumentationStage = (): void => {
|
|
436
|
+
if (!documentationStage) return;
|
|
437
|
+
const index = workflowStages.indexOf(documentationStage);
|
|
438
|
+
if (index !== -1) workflowStages.splice(index, 1);
|
|
439
|
+
documentationStage = undefined;
|
|
440
|
+
publishWorkflowStages();
|
|
441
|
+
};
|
|
442
|
+
publishWorkflowStages();
|
|
443
|
+
|
|
392
444
|
const launchStep = async (
|
|
393
445
|
agentName: string,
|
|
394
446
|
task: string,
|
|
395
447
|
relation: string,
|
|
448
|
+
projection: {
|
|
449
|
+
stage?: WorkflowStage;
|
|
450
|
+
timelineRelation?: string;
|
|
451
|
+
childRelation?: string;
|
|
452
|
+
} = {},
|
|
396
453
|
): Promise<SingleResult> => {
|
|
397
454
|
if (!enabled(agentName)) {
|
|
398
455
|
throw new Error(`Managed workflow cannot launch disabled or missing agent "${agentName}".`);
|
|
399
456
|
}
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
457
|
+
const stage: WorkflowStage = projection.stage ?? {
|
|
458
|
+
agent: agentName,
|
|
459
|
+
relation: projection.timelineRelation ?? relation,
|
|
460
|
+
status: "pending",
|
|
461
|
+
};
|
|
462
|
+
if (!projection.stage) insertBeforeDocumentation(stage);
|
|
463
|
+
stage.status = "active";
|
|
464
|
+
publishWorkflowStages();
|
|
465
|
+
try {
|
|
466
|
+
const step = await launchInWorkflow(request, agentName, task, {
|
|
467
|
+
groupId: request.groupId,
|
|
468
|
+
relationLabel: projection.childRelation ?? relation,
|
|
469
|
+
parentRunId: request.parentRunId,
|
|
470
|
+
});
|
|
471
|
+
stage.status = workflowStageStatus(step.result);
|
|
472
|
+
publishWorkflowStages();
|
|
473
|
+
request.rememberLatest(step.result);
|
|
474
|
+
steps.push({ ...step, relation });
|
|
475
|
+
return step.result;
|
|
476
|
+
} catch (error) {
|
|
477
|
+
stage.status = "failed";
|
|
478
|
+
publishWorkflowStages();
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
408
481
|
};
|
|
409
482
|
|
|
410
|
-
|
|
483
|
+
/** Bounded worker → reviewer fix rounds. A conditional documentation sync
|
|
484
|
+
* deliberately stays out of the rounds: code fixes would invalidate it,
|
|
485
|
+
* and the terminal review classifies whether the settled diff needs one. */
|
|
486
|
+
const runFixRounds = async (
|
|
487
|
+
triggeringReviewer: SingleResult,
|
|
488
|
+
): Promise<{ lastReview?: SingleResult; lastWorker?: SingleResult }> => {
|
|
411
489
|
let lastReviewer = triggeringReviewer;
|
|
490
|
+
const outcome: { lastReview?: SingleResult; lastWorker?: SingleResult } = {};
|
|
412
491
|
for (let round = 1; round <= request.config.maxFixRounds; round++) {
|
|
413
492
|
if (!canContinue()) break;
|
|
493
|
+
const fixRelation = `fix ${round}/${request.config.maxFixRounds}`;
|
|
414
494
|
const workerResult = await launchStep(
|
|
415
495
|
"worker",
|
|
416
496
|
buildFixTaskBrief(lastReviewer, round, request.config.maxFixRounds),
|
|
417
497
|
`fix round ${round}`,
|
|
498
|
+
{ timelineRelation: fixRelation, childRelation: fixRelation },
|
|
418
499
|
);
|
|
419
|
-
if (
|
|
420
|
-
|
|
421
|
-
let documenterResult: SingleResult | undefined;
|
|
422
|
-
if (enabled("documenter")) {
|
|
423
|
-
documenterResult = await launchStep(
|
|
424
|
-
"documenter",
|
|
425
|
-
buildDocumenterTaskBrief(workerResult, round, lastReviewer),
|
|
426
|
-
`docs round ${round}`,
|
|
427
|
-
);
|
|
428
|
-
if (!canContinue() || isFailedResult(documenterResult)) break;
|
|
429
|
-
}
|
|
500
|
+
if (isFailedResult(workerResult) || !canContinue()) break;
|
|
501
|
+
outcome.lastWorker = workerResult;
|
|
430
502
|
|
|
503
|
+
const reReviewRelation = `re-review ${round}/${request.config.maxFixRounds}`;
|
|
431
504
|
const reviewResult = await launchStep(
|
|
432
505
|
"reviewer",
|
|
433
|
-
buildReReviewBrief(lastReviewer, round, workerResult,
|
|
506
|
+
buildReReviewBrief(lastReviewer, round, workerResult, {
|
|
507
|
+
documenterPending: enabled("documenter"),
|
|
508
|
+
}),
|
|
434
509
|
`re-review round ${round}`,
|
|
510
|
+
{ timelineRelation: reReviewRelation, childRelation: reReviewRelation },
|
|
435
511
|
);
|
|
436
|
-
if (
|
|
512
|
+
if (isFailedResult(reviewResult) || !canContinue()) break;
|
|
513
|
+
outcome.lastReview = reviewResult;
|
|
437
514
|
const verdict = reviewVerdict(getResultOutput(reviewResult));
|
|
438
515
|
// REVIEW_PASS settles. No verdict is advisory/malformed and must never
|
|
439
516
|
// trigger another writer. Only an explicit REVIEW_FAIL consumes a fix.
|
|
440
517
|
if (verdict !== "fail") break;
|
|
441
518
|
lastReviewer = reviewResult;
|
|
442
519
|
}
|
|
520
|
+
return outcome;
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
/** Run the low-cost final documentation sync only when the terminal REVIEW_PASS
|
|
524
|
+
* reports drift or omits the new marker. A failed process, missing verdict, or
|
|
525
|
+
* REVIEW_FAIL never writes docs. With no reviewer, retain the conservative
|
|
526
|
+
* writer → documenter fallback. */
|
|
527
|
+
const runFinalDocumentation = async (
|
|
528
|
+
lastWriterResult: SingleResult | undefined,
|
|
529
|
+
finalReviewResult: SingleResult | undefined,
|
|
530
|
+
): Promise<void> => {
|
|
531
|
+
if (!canContinue() || !enabled("documenter")) return;
|
|
532
|
+
if (lastWriterResult?.agent === "documenter") {
|
|
533
|
+
removeDocumentationStage();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (finalReviewResult) {
|
|
537
|
+
if (isFailedResult(finalReviewResult)) {
|
|
538
|
+
removeDocumentationStage();
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const reviewOutput = getResultOutput(finalReviewResult);
|
|
542
|
+
if (
|
|
543
|
+
reviewVerdict(reviewOutput) !== "pass" ||
|
|
544
|
+
documentationDisposition(reviewOutput) === "clean"
|
|
545
|
+
) {
|
|
546
|
+
removeDocumentationStage();
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
documentationStage ??= { agent: "documenter", relation: "docs", status: "pending" };
|
|
551
|
+
if (!workflowStages.includes(documentationStage)) workflowStages.push(documentationStage);
|
|
552
|
+
await launchStep(
|
|
553
|
+
"documenter",
|
|
554
|
+
buildFinalDocumenterBrief(lastWriterResult, finalReviewResult),
|
|
555
|
+
"final documentation sync",
|
|
556
|
+
{ stage: documentationStage },
|
|
557
|
+
);
|
|
443
558
|
};
|
|
444
559
|
|
|
445
560
|
try {
|
|
@@ -448,43 +563,36 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
448
563
|
// never create an already-aborted downstream child.
|
|
449
564
|
if (!canContinue()) return { kind: request.plan.kind, steps };
|
|
450
565
|
if (request.plan.kind === "auto-fix") {
|
|
451
|
-
await runFixRounds(initialStepResult);
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
);
|
|
466
|
-
}
|
|
467
|
-
|
|
566
|
+
const fixOutcome = await runFixRounds(initialStepResult);
|
|
567
|
+
await runFinalDocumentation(fixOutcome.lastWorker, fixOutcome.lastReview ?? initialStepResult);
|
|
568
|
+
} else if (request.plan.kind === "review-pass-sync") {
|
|
569
|
+
// The direct passing review already gated the pending code. Its
|
|
570
|
+
// disposition requested (or conservatively defaulted to) one docs sync.
|
|
571
|
+
await runFinalDocumentation(undefined, initialStepResult);
|
|
572
|
+
} else if (enabled("reviewer")) {
|
|
573
|
+
const gateReview = await launchStep(
|
|
574
|
+
"reviewer",
|
|
575
|
+
buildFinalReviewBrief(initialStepResult, { documenterPending: enabled("documenter") }),
|
|
576
|
+
"final review",
|
|
577
|
+
{ stage: reviewStage },
|
|
578
|
+
);
|
|
579
|
+
let fixOutcome: Awaited<ReturnType<typeof runFixRounds>> = {};
|
|
468
580
|
if (
|
|
581
|
+
!isFailedResult(gateReview) &&
|
|
469
582
|
canContinue() &&
|
|
470
|
-
(
|
|
471
|
-
enabled("
|
|
583
|
+
reviewVerdict(getResultOutput(gateReview)) === "fail" &&
|
|
584
|
+
enabled("worker") &&
|
|
585
|
+
request.config.maxFixRounds > 0
|
|
472
586
|
) {
|
|
473
|
-
|
|
474
|
-
"reviewer",
|
|
475
|
-
buildFinalReviewBrief(initialStepResult, documenterResult),
|
|
476
|
-
"final review",
|
|
477
|
-
);
|
|
478
|
-
if (
|
|
479
|
-
canContinue() &&
|
|
480
|
-
!isFailedResult(reviewResult) &&
|
|
481
|
-
reviewVerdict(getResultOutput(reviewResult)) === "fail" &&
|
|
482
|
-
enabled("worker") &&
|
|
483
|
-
request.config.maxFixRounds > 0
|
|
484
|
-
) {
|
|
485
|
-
await runFixRounds(reviewResult);
|
|
486
|
-
}
|
|
587
|
+
fixOutcome = await runFixRounds(gateReview);
|
|
487
588
|
}
|
|
589
|
+
await runFinalDocumentation(
|
|
590
|
+
fixOutcome.lastWorker ?? initialStepResult,
|
|
591
|
+
fixOutcome.lastReview ?? gateReview,
|
|
592
|
+
);
|
|
593
|
+
} else {
|
|
594
|
+
// No gate configured: the documenter is the only downstream stage.
|
|
595
|
+
await runFinalDocumentation(initialStepResult, undefined);
|
|
488
596
|
}
|
|
489
597
|
return { kind: request.plan.kind, steps };
|
|
490
598
|
} finally {
|
package/src/fixloop.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Managed workflow policy and handoff formatting.
|
|
3
3
|
*
|
|
4
|
-
* Successful top-level
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* reviewer
|
|
9
|
-
*
|
|
4
|
+
* Successful top-level worker/cleaner runs continue through an independent
|
|
5
|
+
* code review gate; bounded worker → reviewer fix rounds close its findings.
|
|
6
|
+
* Reviewers classify documentation drift explicitly, so the low-cost final
|
|
7
|
+
* documenter runs only when needed (or conservatively when an older/custom
|
|
8
|
+
* reviewer omits the marker). Direct passing/failing gates use the same policy.
|
|
9
|
+
* Top-level documenters are explicit standalone writing tasks. Internal steps
|
|
10
|
+
* are launched by dispatch directly, so they never re-enter this policy or wake
|
|
11
|
+
* the main agent mid-chain.
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import { isWriteCapableAgent, type AgentConfig } from "./agents.ts";
|
|
@@ -56,6 +58,20 @@ export function workflowAgentAvailability(
|
|
|
56
58
|
};
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
export type DocumentationDisposition = "clean" | "needed";
|
|
62
|
+
|
|
63
|
+
/** Only the last standalone documentation disposition line counts. Inline
|
|
64
|
+
* examples and prose are ignored so a prompt echo cannot suppress a needed
|
|
65
|
+
* conservative sync. */
|
|
66
|
+
export function documentationDisposition(output: string): DocumentationDisposition | undefined {
|
|
67
|
+
const lines = output.split("\n");
|
|
68
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
69
|
+
const match = /^\s*DOCUMENTATION:\s*(CLEAN|NEEDED)\s*$/i.exec(lines[index]);
|
|
70
|
+
if (match) return match[1].toUpperCase() === "CLEAN" ? "clean" : "needed";
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
59
75
|
export type ManagedWorkflowKind = "auto-fix" | "post-writer" | "review-pass-sync";
|
|
60
76
|
|
|
61
77
|
export interface ManagedWorkflowPlan {
|
|
@@ -99,15 +115,20 @@ export function getManagedWorkflowPlan(
|
|
|
99
115
|
initialRelation: result.agent === "cleaner" ? "initial cleanup" : "initial implementation",
|
|
100
116
|
};
|
|
101
117
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
: undefined;
|
|
106
|
-
}
|
|
118
|
+
// A top-level documenter is already an explicit docs/comments write task. It
|
|
119
|
+
// owns the writer lane but delivers directly without an automatic code gate.
|
|
120
|
+
if (result.agent === "documenter") return undefined;
|
|
107
121
|
if (result.agent !== "reviewer") return undefined;
|
|
108
122
|
|
|
109
|
-
const
|
|
110
|
-
|
|
123
|
+
const output = getResultOutput(result);
|
|
124
|
+
const verdict = reviewVerdict(output);
|
|
125
|
+
// The pass stands as the code gate. Run the conditional documenter only for
|
|
126
|
+
// explicit drift or when an older/custom reviewer omitted the marker.
|
|
127
|
+
if (
|
|
128
|
+
verdict === "pass" &&
|
|
129
|
+
availability.documenter &&
|
|
130
|
+
documentationDisposition(output) !== "clean"
|
|
131
|
+
) {
|
|
111
132
|
return { kind: "review-pass-sync", initialRelation: "pre-documentation review" };
|
|
112
133
|
}
|
|
113
134
|
if (verdict === "fail" && availability.worker && shouldTriggerFixLoop(result, config)) {
|
|
@@ -135,77 +156,58 @@ export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, m
|
|
|
135
156
|
`Fix EVERY finding in the reviewer's findings list — there is no severity triage; all of them get fixed.`,
|
|
136
157
|
`If a finding is factually wrong or clearly out of scope, say so explicitly instead of fixing it.`,
|
|
137
158
|
`Do NOT refactor unrelated code beyond what the findings require.`,
|
|
138
|
-
`
|
|
159
|
+
`Synchronize any existing README/docs/examples/comments directly affected by your fixes; do not broaden into standalone documentation maintenance.`,
|
|
160
|
+
`Do NOT commit, push, publish, tag, or release; do not bump versions. The parent chain still owns re-review and any conditional final documentation sync.`,
|
|
139
161
|
`After editing, run the project's format/build/tests when they exist and report`,
|
|
140
162
|
`exactly what you changed (paths + short rationale) so a reviewer can verify.`,
|
|
141
163
|
remaining > 0
|
|
142
164
|
? `A reviewer will re-review your changes automatically after you finish.`
|
|
143
|
-
: `This is the last auto-fix round;
|
|
165
|
+
: `This is the last auto-fix round; the workflow conditionally runs any needed final documentation sync and then delivers.`,
|
|
144
166
|
].join("\n");
|
|
145
167
|
}
|
|
146
168
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
169
|
+
/** Build the single documentation handoff selected after the review gate
|
|
170
|
+
* settles or as the reviewer-disabled fallback. The last writer's report
|
|
171
|
+
* (top-level writer or final fix-round worker) and the terminal gate review are
|
|
172
|
+
* leads; the pending diff stays authoritative. At most one of the two is
|
|
173
|
+
* undefined in every managed flow. */
|
|
174
|
+
export function buildFinalDocumenterBrief(
|
|
175
|
+
lastWriterResult?: SingleResult,
|
|
176
|
+
finalReviewResult?: SingleResult,
|
|
177
|
+
): string {
|
|
178
|
+
const reportSections = [
|
|
179
|
+
...(lastWriterResult
|
|
180
|
+
? [
|
|
181
|
+
`The last writer (${lastWriterResult.agent}) reported:`,
|
|
182
|
+
`---`,
|
|
183
|
+
getResultOutput(lastWriterResult),
|
|
184
|
+
`---`,
|
|
185
|
+
``,
|
|
186
|
+
]
|
|
187
|
+
: []),
|
|
188
|
+
...(finalReviewResult
|
|
189
|
+
? [
|
|
190
|
+
`The final gate review reported:`,
|
|
191
|
+
`---`,
|
|
192
|
+
getResultOutput(finalReviewResult),
|
|
193
|
+
`---`,
|
|
194
|
+
``,
|
|
195
|
+
]
|
|
196
|
+
: []),
|
|
197
|
+
];
|
|
161
198
|
return [
|
|
162
|
-
|
|
199
|
+
`Final documentation sync: the review gate settled and you are the last managed stage before delivery.`,
|
|
163
200
|
``,
|
|
164
201
|
...reportSections,
|
|
165
|
-
`Inspect the actual git diff (the complete pending diff) and relevant implementation; the
|
|
166
|
-
`
|
|
202
|
+
`Inspect the actual git diff (the complete pending diff) and relevant implementation; the reports are only leads.`,
|
|
203
|
+
`Apply every documentation note the reviews recorded, then synchronize stale README/docs, examples, API comments, docstrings, and explanatory comments with the behavior that will be committed.`,
|
|
167
204
|
`Change documentation surfaces only; never alter runtime behavior or tests to make prose true.`,
|
|
168
205
|
`Make zero edits when the diff creates no documentation drift.`,
|
|
169
|
-
`Do NOT commit, push, publish, tag, or release; do not bump versions.
|
|
206
|
+
`Do NOT commit, push, publish, tag, or release; do not bump versions. The parent workflow delivers directly after you; no fresh reviewer runs.`,
|
|
170
207
|
`Report exact documentation/comment paths changed, or state explicitly that no sync was needed.`,
|
|
171
208
|
].join("\n");
|
|
172
209
|
}
|
|
173
210
|
|
|
174
|
-
/** Build the pre-commit documentation handoff after one auto-fix worker. */
|
|
175
|
-
export function buildDocumenterTaskBrief(
|
|
176
|
-
workerResult: SingleResult,
|
|
177
|
-
round: number,
|
|
178
|
-
reviewerResult?: SingleResult,
|
|
179
|
-
): string {
|
|
180
|
-
return buildDocumentationBrief({
|
|
181
|
-
title: `Documentation sync after auto-fix round ${round}.`,
|
|
182
|
-
reports: [
|
|
183
|
-
...(reviewerResult ? [{ label: "The triggering reviewer reported", result: reviewerResult }] : []),
|
|
184
|
-
{ label: "The worker reported", result: workerResult },
|
|
185
|
-
],
|
|
186
|
-
closing: "a fresh reviewer gate runs after you.",
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Build the automatic documentation stage after a successful top-level writer. */
|
|
191
|
-
export function buildPostWriterDocumenterBrief(writerResult: SingleResult): string {
|
|
192
|
-
return buildDocumentationBrief({
|
|
193
|
-
title: `Documentation sync after successful top-level ${writerResult.agent}.`,
|
|
194
|
-
reports: [{ label: `The ${writerResult.agent} reported`, result: writerResult }],
|
|
195
|
-
closing: "the managed workflow owns any final reviewer and delivery.",
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** A direct passing review cannot be the final gate while documenter is enabled:
|
|
200
|
-
* the preliminary report is context, but the actual pending diff is authoritative. */
|
|
201
|
-
export function buildReviewPassDocumenterBrief(reviewerResult: SingleResult): string {
|
|
202
|
-
return buildDocumentationBrief({
|
|
203
|
-
title: "Documentation sync required before accepting a direct passing review.",
|
|
204
|
-
reports: [{ label: "The preliminary reviewer reported", result: reviewerResult }],
|
|
205
|
-
closing: "the preliminary pass is not final and a fresh reviewer gate runs after you.",
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
211
|
/**
|
|
210
212
|
* One step of an auto-fix chain as delivered: the run id (so the condensed
|
|
211
213
|
* summary can point at per-run detail via subagent_status), the result, and
|
|
@@ -264,7 +266,7 @@ export function formatChainSummary(
|
|
|
264
266
|
);
|
|
265
267
|
}
|
|
266
268
|
|
|
267
|
-
/** One clear final delivery for
|
|
269
|
+
/** One clear final delivery for post-writer and direct reviewer → documenter workflows. */
|
|
268
270
|
export function formatManagedWorkflowSummary(
|
|
269
271
|
steps: readonly ChainStep[],
|
|
270
272
|
terminalResult: SingleResult = steps[steps.length - 1]!.result,
|
|
@@ -278,33 +280,40 @@ export function formatManagedWorkflowSummary(
|
|
|
278
280
|
);
|
|
279
281
|
}
|
|
280
282
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
283
|
+
export interface GateBriefOptions {
|
|
284
|
+
/** A conditional final documenter is available after the gate settles.
|
|
285
|
+
* Documentation drift is then routed to it as non-gating notes instead of
|
|
286
|
+
* failing the code gate. */
|
|
287
|
+
documenterPending: boolean;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Build the code gate that runs directly after a top-level writer, before any
|
|
291
|
+
* documentation. Reports carry intent; the actual pending diff remains
|
|
292
|
+
* authoritative. */
|
|
284
293
|
export function buildFinalReviewBrief(
|
|
285
294
|
initialResult: SingleResult,
|
|
286
|
-
|
|
295
|
+
options: GateBriefOptions,
|
|
287
296
|
): string {
|
|
288
|
-
const documenterSection = documenterResult
|
|
289
|
-
? [
|
|
290
|
-
``,
|
|
291
|
-
`The documenter's full sync report:`,
|
|
292
|
-
`---`,
|
|
293
|
-
getResultOutput(documenterResult),
|
|
294
|
-
`---`,
|
|
295
|
-
]
|
|
296
|
-
: [];
|
|
297
297
|
return [
|
|
298
|
-
`Fresh
|
|
298
|
+
`Fresh code gate for a managed ${initialResult.agent} workflow.`,
|
|
299
299
|
``,
|
|
300
300
|
`The top-level ${initialResult.agent}'s full report:`,
|
|
301
301
|
`---`,
|
|
302
302
|
getResultOutput(initialResult),
|
|
303
303
|
`---`,
|
|
304
|
-
...documenterSection,
|
|
305
304
|
``,
|
|
306
|
-
`Run \`git status\` and \`git diff\` and inspect the actual pending code
|
|
307
|
-
`Remain read-only. Verify correctness, regressions,
|
|
305
|
+
`Run \`git status\` and \`git diff\` and inspect the actual pending code; the report is context, not proof.`,
|
|
306
|
+
`Remain read-only. Verify correctness, regressions, and tests.`,
|
|
307
|
+
...(options.documenterPending
|
|
308
|
+
? [
|
|
309
|
+
`A conditional documentation sync is available AFTER this gate, so documentation drift is not a code-gate finding.`,
|
|
310
|
+
`If documentation is stale, add a separate short "## Documentation notes" list and emit the standalone line`,
|
|
311
|
+
`DOCUMENTATION: NEEDED. If no documentation update is needed, emit DOCUMENTATION: CLEAN instead.`,
|
|
312
|
+
`Always emit exactly one of those standalone documentation lines; fail the gate only for code or test findings.`,
|
|
313
|
+
]
|
|
314
|
+
: [
|
|
315
|
+
`No documenter is pending, so documentation drift is an ordinary gate finding.`,
|
|
316
|
+
]),
|
|
308
317
|
`This is an acceptance gate, not an advisory audit. End with exactly one standalone machine verdict line:`,
|
|
309
318
|
`VERDICT: REVIEW_PASS when no finding remains, otherwise VERDICT: REVIEW_FAIL.`,
|
|
310
319
|
].join("\n");
|
|
@@ -312,29 +321,19 @@ export function buildFinalReviewBrief(
|
|
|
312
321
|
|
|
313
322
|
/**
|
|
314
323
|
* The re-review brief handed to the reviewer after a worker fix round. Includes
|
|
315
|
-
* the prior review
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
* a verified resolution.
|
|
324
|
+
* the prior review and worker report so the reviewer can adjudicate rejections
|
|
325
|
+
* instead of restating findings. The convergence contract keeps rounds from
|
|
326
|
+
* ping-ponging: rule on the open findings once, add only defects this round's
|
|
327
|
+
* edits introduced, never re-open a verified resolution.
|
|
320
328
|
*/
|
|
321
329
|
export function buildReReviewBrief(
|
|
322
330
|
reviewerResult: SingleResult,
|
|
323
331
|
round: number,
|
|
324
332
|
workerResult: SingleResult,
|
|
325
|
-
|
|
333
|
+
options: GateBriefOptions = { documenterPending: false },
|
|
326
334
|
): string {
|
|
327
335
|
const review = getResultOutput(reviewerResult);
|
|
328
336
|
const workerReport = getResultOutput(workerResult);
|
|
329
|
-
const documenterSection = documenterResult
|
|
330
|
-
? [
|
|
331
|
-
``,
|
|
332
|
-
`The documenter's pre-commit sync report:`,
|
|
333
|
-
`---`,
|
|
334
|
-
getResultOutput(documenterResult),
|
|
335
|
-
`---`,
|
|
336
|
-
]
|
|
337
|
-
: [];
|
|
338
337
|
return [
|
|
339
338
|
`Re-review after auto-fix round ${round}.`,
|
|
340
339
|
``,
|
|
@@ -347,7 +346,6 @@ export function buildReReviewBrief(
|
|
|
347
346
|
`---`,
|
|
348
347
|
workerReport,
|
|
349
348
|
`---`,
|
|
350
|
-
...documenterSection,
|
|
351
349
|
``,
|
|
352
350
|
`Rule on EVERY previous finding: resolved, or still open. A finding the worker rejected must be`,
|
|
353
351
|
`adjudicated ONCE — accept the rejection unless you can concretely refute the worker's reasoning;`,
|
|
@@ -355,6 +353,14 @@ export function buildReReviewBrief(
|
|
|
355
353
|
`Run \`git diff\` to see what changed, then add NEW findings only when they are defects this round's`,
|
|
356
354
|
`edits introduced or exposed (or a load-bearing issue the earlier review genuinely missed).`,
|
|
357
355
|
`Do NOT re-open a finding you verified as resolved.`,
|
|
356
|
+
...(options.documenterPending
|
|
357
|
+
? [
|
|
358
|
+
`Carry unresolved "## Documentation notes" forward and add any newly exposed drift there; documentation drift is not a code-gate finding.`,
|
|
359
|
+
`Emit exactly one standalone documentation disposition line: DOCUMENTATION: NEEDED when that notes section is required, otherwise DOCUMENTATION: CLEAN.`,
|
|
360
|
+
]
|
|
361
|
+
: [
|
|
362
|
+
`No documenter is pending, so unresolved documentation drift remains an ordinary gate finding.`,
|
|
363
|
+
]),
|
|
358
364
|
`REQUEST_CHANGES only while an open finding remains; otherwise APPROVE.`,
|
|
359
365
|
`End with your machine-readable verdict line as usual (VERDICT: REVIEW_PASS / REVIEW_FAIL).`,
|
|
360
366
|
].join("\n");
|