@ferris1225/pi-subagents 4.1.4 → 4.1.6
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 +129 -116
- 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/config.ts +7 -5
- package/src/dispatch.ts +143 -43
- package/src/fixloop.ts +58 -32
- package/src/monitor.ts +54 -6
- package/src/prompt.ts +24 -26
- package/src/rpc-run.ts +26 -2
- package/src/thread-lifecycle.ts +103 -17
- package/src/tools.ts +49 -5
- package/src/widget.ts +94 -13
- package/src/worktree.ts +12 -0
package/src/dispatch.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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
|
-
* bounded worker/reviewer fix rounds, and internal
|
|
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
|
|
6
7
|
* thread generations, final integration, and completion ownership live in
|
|
7
8
|
* thread-lifecycle.ts.
|
|
8
9
|
*/
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
buildFinalReviewBrief,
|
|
22
23
|
buildFixTaskBrief,
|
|
23
24
|
buildReReviewBrief,
|
|
25
|
+
documentationDisposition,
|
|
24
26
|
type ChainStep,
|
|
25
27
|
type ManagedWorkflowOutcome,
|
|
26
28
|
} from "./fixloop.ts";
|
|
@@ -30,6 +32,8 @@ import {
|
|
|
30
32
|
monitor,
|
|
31
33
|
statusIcon,
|
|
32
34
|
type RunChainMeta,
|
|
35
|
+
type WorkflowStage,
|
|
36
|
+
type WorkflowStageStatus,
|
|
33
37
|
} from "./monitor.ts";
|
|
34
38
|
import type { SubagentRuntime } from "./runtime.ts";
|
|
35
39
|
import {
|
|
@@ -85,6 +89,14 @@ export function defaultIsolationMode(mode: "single" | "parallel", agentName: str
|
|
|
85
89
|
return mode === "parallel" && agentName === "worker" ? "worktree" : "shared";
|
|
86
90
|
}
|
|
87
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
|
+
|
|
88
100
|
const managedRepositoryRootTails = new Map<string, Promise<void>>();
|
|
89
101
|
|
|
90
102
|
async function canonicalManagedRepositoryRoot(cwd: string): Promise<string> {
|
|
@@ -167,15 +179,15 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
167
179
|
name: "subagent",
|
|
168
180
|
label: "Subagent",
|
|
169
181
|
description: [
|
|
170
|
-
"Dispatch enabled specialized agents as isolated leaf Pi child processes, singly or in parallel.",
|
|
171
|
-
"Built-ins: explorer for broad read-only reconnaissance; worker for implementation; cleaner
|
|
172
|
-
"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.",
|
|
173
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.",
|
|
174
186
|
"A selected-model or provider failure continues the retained session on the current main model; ordinary tool/task failures do not.",
|
|
175
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.",
|
|
176
188
|
].join(" "),
|
|
177
189
|
promptSnippet:
|
|
178
|
-
"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.",
|
|
179
191
|
parameters: SubagentParams,
|
|
180
192
|
|
|
181
193
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -196,7 +208,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
196
208
|
if (!run) return; // already finished — stay idempotent
|
|
197
209
|
if (opts?.silent || !runtime.sessionActive) return;
|
|
198
210
|
const icon = status === "done" ? "✓" : "✗";
|
|
199
|
-
ctx.ui.notify(`${icon} ${monitor.summarize(run)}`, status === "done" ? "info" : "error");
|
|
211
|
+
ctx.ui.notify(`${icon} #${run.id} ${monitor.summarize(run)}`, status === "done" ? "info" : "error");
|
|
200
212
|
};
|
|
201
213
|
|
|
202
214
|
// Live sub-agent activity → concise one-line status ("thinking",
|
|
@@ -303,7 +315,11 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
303
315
|
const resolveLiveAgentTools = (candidate: AgentConfig): AgentConfig =>
|
|
304
316
|
resolveAgentTools({ ...candidate, tools: discoveredAgent.tools }, runtime.getActiveTools());
|
|
305
317
|
const agent = resolveLiveAgentTools(discoveredAgent);
|
|
306
|
-
|
|
318
|
+
// Workflow policy (fix-round caps, agents) stays fixed for the chain,
|
|
319
|
+
// but model/thinking routes are re-read per stage so config edits
|
|
320
|
+
// apply to stages that have not launched yet.
|
|
321
|
+
const stageConfig = await loadConfig(runtime.configPath).catch(() => request.config);
|
|
322
|
+
const resolvedRoute = resolveDispatchModelRoute(agent, stageConfig, request.ctx);
|
|
307
323
|
const route = request.isolation === "worktree"
|
|
308
324
|
? { ...resolvedRoute, agent: withWorktreeSystemPrompt(resolvedRoute.agent) }
|
|
309
325
|
: resolvedRoute;
|
|
@@ -311,6 +327,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
311
327
|
const runId = monitor.addRun(agent.name, task, route.agent.model, thinkingLevel, {
|
|
312
328
|
...meta,
|
|
313
329
|
isolation: request.isolation,
|
|
330
|
+
...(request.worktreeId ? { worktreeId: request.worktreeId } : {}),
|
|
314
331
|
});
|
|
315
332
|
const onLive = makeLiveHandler(runId);
|
|
316
333
|
try {
|
|
@@ -327,7 +344,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
327
344
|
signal: request.signal,
|
|
328
345
|
onLive,
|
|
329
346
|
makeDetails: makeDetails("single", true),
|
|
330
|
-
idleTimeoutMs:
|
|
347
|
+
idleTimeoutMs: stageConfig.idleTimeoutSec * 1000,
|
|
331
348
|
},
|
|
332
349
|
route.mainFallbackRef,
|
|
333
350
|
);
|
|
@@ -387,27 +404,90 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
387
404
|
const enabled = (name: string): boolean =>
|
|
388
405
|
request.agents.some((candidate) => candidate.name === name);
|
|
389
406
|
const canContinue = (): boolean => runtime.sessionActive && !request.signal.aborted;
|
|
407
|
+
|
|
408
|
+
// Keep a live parent-owned projection because settled internal rows are
|
|
409
|
+
// intentionally removed. Only real/currently planned stages enter it.
|
|
410
|
+
const initialStageRelation = initialStepResult.agent === "worker"
|
|
411
|
+
? "implement"
|
|
412
|
+
: initialStepResult.agent === "cleaner"
|
|
413
|
+
? "cleanup"
|
|
414
|
+
: "review";
|
|
415
|
+
const workflowStages: WorkflowStage[] = [{
|
|
416
|
+
agent: initialStepResult.agent,
|
|
417
|
+
relation: initialStageRelation,
|
|
418
|
+
status: workflowStageStatus(initialStepResult),
|
|
419
|
+
}];
|
|
420
|
+
let reviewStage: WorkflowStage | undefined;
|
|
421
|
+
if (request.plan.kind === "post-writer" && enabled("reviewer")) {
|
|
422
|
+
reviewStage = { agent: "reviewer", relation: "review", status: "pending" };
|
|
423
|
+
workflowStages.push(reviewStage);
|
|
424
|
+
}
|
|
425
|
+
let documentationStage: WorkflowStage | undefined;
|
|
426
|
+
if (enabled("documenter")) {
|
|
427
|
+
documentationStage = { agent: "documenter", relation: "docs", status: "pending" };
|
|
428
|
+
workflowStages.push(documentationStage);
|
|
429
|
+
}
|
|
430
|
+
const publishWorkflowStages = (): void => {
|
|
431
|
+
monitor.setWorkflowStages(request.parentRunId, workflowStages);
|
|
432
|
+
};
|
|
433
|
+
const insertBeforeDocumentation = (stage: WorkflowStage): void => {
|
|
434
|
+
const documentationIndex = documentationStage
|
|
435
|
+
? workflowStages.indexOf(documentationStage)
|
|
436
|
+
: -1;
|
|
437
|
+
if (documentationIndex === -1) workflowStages.push(stage);
|
|
438
|
+
else workflowStages.splice(documentationIndex, 0, stage);
|
|
439
|
+
};
|
|
440
|
+
const removeDocumentationStage = (): void => {
|
|
441
|
+
if (!documentationStage) return;
|
|
442
|
+
const index = workflowStages.indexOf(documentationStage);
|
|
443
|
+
if (index !== -1) workflowStages.splice(index, 1);
|
|
444
|
+
documentationStage = undefined;
|
|
445
|
+
publishWorkflowStages();
|
|
446
|
+
};
|
|
447
|
+
publishWorkflowStages();
|
|
448
|
+
|
|
390
449
|
const launchStep = async (
|
|
391
450
|
agentName: string,
|
|
392
451
|
task: string,
|
|
393
452
|
relation: string,
|
|
453
|
+
projection: {
|
|
454
|
+
stage?: WorkflowStage;
|
|
455
|
+
timelineRelation?: string;
|
|
456
|
+
childRelation?: string;
|
|
457
|
+
} = {},
|
|
394
458
|
): Promise<SingleResult> => {
|
|
395
459
|
if (!enabled(agentName)) {
|
|
396
460
|
throw new Error(`Managed workflow cannot launch disabled or missing agent "${agentName}".`);
|
|
397
461
|
}
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
462
|
+
const stage: WorkflowStage = projection.stage ?? {
|
|
463
|
+
agent: agentName,
|
|
464
|
+
relation: projection.timelineRelation ?? relation,
|
|
465
|
+
status: "pending",
|
|
466
|
+
};
|
|
467
|
+
if (!projection.stage) insertBeforeDocumentation(stage);
|
|
468
|
+
stage.status = "active";
|
|
469
|
+
publishWorkflowStages();
|
|
470
|
+
try {
|
|
471
|
+
const step = await launchInWorkflow(request, agentName, task, {
|
|
472
|
+
groupId: request.groupId,
|
|
473
|
+
relationLabel: projection.childRelation ?? relation,
|
|
474
|
+
parentRunId: request.parentRunId,
|
|
475
|
+
});
|
|
476
|
+
stage.status = workflowStageStatus(step.result);
|
|
477
|
+
publishWorkflowStages();
|
|
478
|
+
request.rememberLatest(step.result);
|
|
479
|
+
steps.push({ ...step, relation });
|
|
480
|
+
return step.result;
|
|
481
|
+
} catch (error) {
|
|
482
|
+
stage.status = "failed";
|
|
483
|
+
publishWorkflowStages();
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
406
486
|
};
|
|
407
487
|
|
|
408
|
-
/** Bounded worker → reviewer fix rounds.
|
|
409
|
-
* stays out of the rounds: code fixes would invalidate it,
|
|
410
|
-
*
|
|
488
|
+
/** Bounded worker → reviewer fix rounds. A conditional documentation sync
|
|
489
|
+
* deliberately stays out of the rounds: code fixes would invalidate it,
|
|
490
|
+
* and the terminal review classifies whether the settled diff needs one. */
|
|
411
491
|
const runFixRounds = async (
|
|
412
492
|
triggeringReviewer: SingleResult,
|
|
413
493
|
): Promise<{ lastReview?: SingleResult; lastWorker?: SingleResult }> => {
|
|
@@ -415,20 +495,24 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
415
495
|
const outcome: { lastReview?: SingleResult; lastWorker?: SingleResult } = {};
|
|
416
496
|
for (let round = 1; round <= request.config.maxFixRounds; round++) {
|
|
417
497
|
if (!canContinue()) break;
|
|
498
|
+
const fixRelation = `fix ${round}/${request.config.maxFixRounds}`;
|
|
418
499
|
const workerResult = await launchStep(
|
|
419
500
|
"worker",
|
|
420
501
|
buildFixTaskBrief(lastReviewer, round, request.config.maxFixRounds),
|
|
421
502
|
`fix round ${round}`,
|
|
503
|
+
{ timelineRelation: fixRelation, childRelation: fixRelation },
|
|
422
504
|
);
|
|
423
505
|
if (isFailedResult(workerResult) || !canContinue()) break;
|
|
424
506
|
outcome.lastWorker = workerResult;
|
|
425
507
|
|
|
508
|
+
const reReviewRelation = `re-review ${round}/${request.config.maxFixRounds}`;
|
|
426
509
|
const reviewResult = await launchStep(
|
|
427
510
|
"reviewer",
|
|
428
511
|
buildReReviewBrief(lastReviewer, round, workerResult, {
|
|
429
512
|
documenterPending: enabled("documenter"),
|
|
430
513
|
}),
|
|
431
514
|
`re-review round ${round}`,
|
|
515
|
+
{ timelineRelation: reReviewRelation, childRelation: reReviewRelation },
|
|
432
516
|
);
|
|
433
517
|
if (isFailedResult(reviewResult) || !canContinue()) break;
|
|
434
518
|
outcome.lastReview = reviewResult;
|
|
@@ -441,24 +525,40 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
441
525
|
return outcome;
|
|
442
526
|
};
|
|
443
527
|
|
|
444
|
-
/**
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
528
|
+
/** Run the low-cost final documentation sync only when the terminal REVIEW_PASS
|
|
529
|
+
* reports drift or omits the new marker. A failed process, missing verdict, or
|
|
530
|
+
* REVIEW_FAIL never writes docs. With no reviewer, retain the conservative
|
|
531
|
+
* writer → documenter fallback. */
|
|
448
532
|
const runFinalDocumentation = async (
|
|
449
533
|
lastWriterResult: SingleResult | undefined,
|
|
450
534
|
finalReviewResult: SingleResult | undefined,
|
|
451
535
|
): Promise<void> => {
|
|
452
536
|
if (!canContinue() || !enabled("documenter")) return;
|
|
453
|
-
if (lastWriterResult?.agent === "documenter")
|
|
537
|
+
if (lastWriterResult?.agent === "documenter") {
|
|
538
|
+
removeDocumentationStage();
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
454
541
|
if (finalReviewResult) {
|
|
455
|
-
if (isFailedResult(finalReviewResult))
|
|
456
|
-
|
|
542
|
+
if (isFailedResult(finalReviewResult)) {
|
|
543
|
+
removeDocumentationStage();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const reviewOutput = getResultOutput(finalReviewResult);
|
|
547
|
+
if (
|
|
548
|
+
reviewVerdict(reviewOutput) !== "pass" ||
|
|
549
|
+
documentationDisposition(reviewOutput) === "clean"
|
|
550
|
+
) {
|
|
551
|
+
removeDocumentationStage();
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
457
554
|
}
|
|
555
|
+
documentationStage ??= { agent: "documenter", relation: "docs", status: "pending" };
|
|
556
|
+
if (!workflowStages.includes(documentationStage)) workflowStages.push(documentationStage);
|
|
458
557
|
await launchStep(
|
|
459
558
|
"documenter",
|
|
460
559
|
buildFinalDocumenterBrief(lastWriterResult, finalReviewResult),
|
|
461
560
|
"final documentation sync",
|
|
561
|
+
{ stage: documentationStage },
|
|
462
562
|
);
|
|
463
563
|
};
|
|
464
564
|
|
|
@@ -471,30 +571,30 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
471
571
|
const fixOutcome = await runFixRounds(initialStepResult);
|
|
472
572
|
await runFinalDocumentation(fixOutcome.lastWorker, fixOutcome.lastReview ?? initialStepResult);
|
|
473
573
|
} else if (request.plan.kind === "review-pass-sync") {
|
|
474
|
-
// The direct passing review already gated the pending code
|
|
475
|
-
//
|
|
574
|
+
// The direct passing review already gated the pending code. Its
|
|
575
|
+
// disposition requested (or conservatively defaulted to) one docs sync.
|
|
476
576
|
await runFinalDocumentation(undefined, initialStepResult);
|
|
477
577
|
} else if (enabled("reviewer")) {
|
|
478
578
|
const gateReview = await launchStep(
|
|
479
579
|
"reviewer",
|
|
480
580
|
buildFinalReviewBrief(initialStepResult, { documenterPending: enabled("documenter") }),
|
|
481
581
|
"final review",
|
|
582
|
+
{ stage: reviewStage },
|
|
482
583
|
);
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
await runFinalDocumentation(
|
|
494
|
-
fixOutcome.lastWorker ?? initialStepResult,
|
|
495
|
-
fixOutcome.lastReview ?? gateReview,
|
|
496
|
-
);
|
|
584
|
+
let fixOutcome: Awaited<ReturnType<typeof runFixRounds>> = {};
|
|
585
|
+
if (
|
|
586
|
+
!isFailedResult(gateReview) &&
|
|
587
|
+
canContinue() &&
|
|
588
|
+
reviewVerdict(getResultOutput(gateReview)) === "fail" &&
|
|
589
|
+
enabled("worker") &&
|
|
590
|
+
request.config.maxFixRounds > 0
|
|
591
|
+
) {
|
|
592
|
+
fixOutcome = await runFixRounds(gateReview);
|
|
497
593
|
}
|
|
594
|
+
await runFinalDocumentation(
|
|
595
|
+
fixOutcome.lastWorker ?? initialStepResult,
|
|
596
|
+
fixOutcome.lastReview ?? gateReview,
|
|
597
|
+
);
|
|
498
598
|
} else {
|
|
499
599
|
// No gate configured: the documenter is the only downstream stage.
|
|
500
600
|
await runFinalDocumentation(initialStepResult, undefined);
|
package/src/fixloop.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Managed workflow policy and handoff formatting.
|
|
3
3
|
*
|
|
4
|
-
* Successful top-level
|
|
5
|
-
* gate; bounded worker → reviewer fix rounds close its findings
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { isWriteCapableAgent, type AgentConfig } from "./agents.ts";
|
|
@@ -58,6 +58,20 @@ export function workflowAgentAvailability(
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
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
|
+
|
|
61
75
|
export type ManagedWorkflowKind = "auto-fix" | "post-writer" | "review-pass-sync";
|
|
62
76
|
|
|
63
77
|
export interface ManagedWorkflowPlan {
|
|
@@ -101,17 +115,20 @@ export function getManagedWorkflowPlan(
|
|
|
101
115
|
initialRelation: result.agent === "cleaner" ? "initial cleanup" : "initial implementation",
|
|
102
116
|
};
|
|
103
117
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
: undefined;
|
|
108
|
-
}
|
|
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;
|
|
109
121
|
if (result.agent !== "reviewer") return undefined;
|
|
110
122
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
//
|
|
114
|
-
|
|
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
|
+
) {
|
|
115
132
|
return { kind: "review-pass-sync", initialRelation: "pre-documentation review" };
|
|
116
133
|
}
|
|
117
134
|
if (verdict === "fail" && availability.worker && shouldTriggerFixLoop(result, config)) {
|
|
@@ -139,19 +156,21 @@ export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, m
|
|
|
139
156
|
`Fix EVERY finding in the reviewer's findings list — there is no severity triage; all of them get fixed.`,
|
|
140
157
|
`If a finding is factually wrong or clearly out of scope, say so explicitly instead of fixing it.`,
|
|
141
158
|
`Do NOT refactor unrelated code beyond what the findings require.`,
|
|
142
|
-
`
|
|
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.`,
|
|
143
161
|
`After editing, run the project's format/build/tests when they exist and report`,
|
|
144
162
|
`exactly what you changed (paths + short rationale) so a reviewer can verify.`,
|
|
145
163
|
remaining > 0
|
|
146
164
|
? `A reviewer will re-review your changes automatically after you finish.`
|
|
147
|
-
: `This is the last auto-fix round; the workflow runs any
|
|
165
|
+
: `This is the last auto-fix round; the workflow conditionally runs any needed final documentation sync and then delivers.`,
|
|
148
166
|
].join("\n");
|
|
149
167
|
}
|
|
150
168
|
|
|
151
|
-
/** Build the single documentation handoff
|
|
152
|
-
* settles. The last writer's report
|
|
153
|
-
* worker) and the terminal gate review are
|
|
154
|
-
* authoritative. At most one of the two is
|
|
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. */
|
|
155
174
|
export function buildFinalDocumenterBrief(
|
|
156
175
|
lastWriterResult?: SingleResult,
|
|
157
176
|
finalReviewResult?: SingleResult,
|
|
@@ -247,7 +266,7 @@ export function formatChainSummary(
|
|
|
247
266
|
);
|
|
248
267
|
}
|
|
249
268
|
|
|
250
|
-
/** One clear final delivery for
|
|
269
|
+
/** One clear final delivery for post-writer and direct reviewer → documenter workflows. */
|
|
251
270
|
export function formatManagedWorkflowSummary(
|
|
252
271
|
steps: readonly ChainStep[],
|
|
253
272
|
terminalResult: SingleResult = steps[steps.length - 1]!.result,
|
|
@@ -262,8 +281,9 @@ export function formatManagedWorkflowSummary(
|
|
|
262
281
|
}
|
|
263
282
|
|
|
264
283
|
export interface GateBriefOptions {
|
|
265
|
-
/** A final documenter
|
|
266
|
-
* then routed to it as non-gating notes instead of
|
|
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. */
|
|
267
287
|
documenterPending: boolean;
|
|
268
288
|
}
|
|
269
289
|
|
|
@@ -286,11 +306,14 @@ export function buildFinalReviewBrief(
|
|
|
286
306
|
`Remain read-only. Verify correctness, regressions, and tests.`,
|
|
287
307
|
...(options.documenterPending
|
|
288
308
|
? [
|
|
289
|
-
`
|
|
290
|
-
`documentation
|
|
291
|
-
`
|
|
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.`,
|
|
292
313
|
]
|
|
293
|
-
: [
|
|
314
|
+
: [
|
|
315
|
+
`No documenter is pending, so documentation drift is an ordinary gate finding.`,
|
|
316
|
+
]),
|
|
294
317
|
`This is an acceptance gate, not an advisory audit. End with exactly one standalone machine verdict line:`,
|
|
295
318
|
`VERDICT: REVIEW_PASS when no finding remains, otherwise VERDICT: REVIEW_FAIL.`,
|
|
296
319
|
].join("\n");
|
|
@@ -332,9 +355,12 @@ export function buildReReviewBrief(
|
|
|
332
355
|
`Do NOT re-open a finding you verified as resolved.`,
|
|
333
356
|
...(options.documenterPending
|
|
334
357
|
? [
|
|
335
|
-
`Carry
|
|
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.`,
|
|
336
360
|
]
|
|
337
|
-
: [
|
|
361
|
+
: [
|
|
362
|
+
`No documenter is pending, so unresolved documentation drift remains an ordinary gate finding.`,
|
|
363
|
+
]),
|
|
338
364
|
`REQUEST_CHANGES only while an open finding remains; otherwise APPROVE.`,
|
|
339
365
|
`End with your machine-readable verdict line as usual (VERDICT: REVIEW_PASS / REVIEW_FAIL).`,
|
|
340
366
|
].join("\n");
|
package/src/monitor.ts
CHANGED
|
@@ -20,18 +20,32 @@ import type { IsolationMode, WorktreeFinalizationStatus } from "./worktree.ts";
|
|
|
20
20
|
|
|
21
21
|
export type RunStatus = "queued" | "running" | "steering" | "interrupting" | "parked" | "done" | "failed";
|
|
22
22
|
export type ContinuationKind = "resume-retained" | "resume-appended" | "fork-retained" | "fork-appended" | "retarget";
|
|
23
|
+
export type WorkflowStageStatus = "done" | "active" | "pending" | "changes" | "failed";
|
|
24
|
+
|
|
25
|
+
/** Ephemeral projection of one real or currently planned managed stage. It is
|
|
26
|
+
* live monitor state only; durable results remain the per-run chain records. */
|
|
27
|
+
export interface WorkflowStage {
|
|
28
|
+
agent: string;
|
|
29
|
+
relation: string;
|
|
30
|
+
status: WorkflowStageStatus;
|
|
31
|
+
}
|
|
23
32
|
|
|
24
33
|
export function isRunActiveStatus(status: RunStatus): boolean {
|
|
25
34
|
return status === "queued" || status === "running" || status === "steering" || status === "interrupting";
|
|
26
35
|
}
|
|
27
36
|
|
|
37
|
+
/** Durable integration projection of a worktree-isolated run: pending before
|
|
38
|
+
* settlement, finalizing while the patch is applied/cleaned up, then the
|
|
39
|
+
* terminal WorktreeFinalizationStatus. */
|
|
40
|
+
export type RunIntegrationStatus = "pending" | "finalizing" | WorktreeFinalizationStatus;
|
|
41
|
+
|
|
28
42
|
export interface RunView {
|
|
29
43
|
id: number;
|
|
30
44
|
agent: string;
|
|
31
45
|
task: string;
|
|
32
46
|
/** Short content label derived from the task (paths/symbols), shown next to
|
|
33
47
|
* the agent name so concurrent same-agent runs are told apart by what they
|
|
34
|
-
* are doing, not just their run id. */
|
|
48
|
+
* are doing, not just by their run id. */
|
|
35
49
|
label?: string;
|
|
36
50
|
model?: string;
|
|
37
51
|
/** Selected model ref when the run handed off to current main. */
|
|
@@ -39,7 +53,10 @@ export interface RunView {
|
|
|
39
53
|
/** Effective thinking strength this run was launched with (frontmatter/config/global). */
|
|
40
54
|
thinking?: string;
|
|
41
55
|
isolation?: IsolationMode;
|
|
42
|
-
integrationStatus?:
|
|
56
|
+
integrationStatus?: RunIntegrationStatus;
|
|
57
|
+
/** Short worktree-group identity (mkdtemp suffix) shared by every run inside
|
|
58
|
+
* one isolated worktree; changes when a continuation worktree is created. */
|
|
59
|
+
worktreeId?: string;
|
|
43
60
|
forkedFromRunId?: number;
|
|
44
61
|
forkChildRunIds?: number[];
|
|
45
62
|
status: RunStatus;
|
|
@@ -65,6 +82,9 @@ export interface RunView {
|
|
|
65
82
|
/** This stable top-level row currently owns a multi-stage managed workflow.
|
|
66
83
|
* Its elapsed time is workflow-wide; active child rows own stage telemetry. */
|
|
67
84
|
managedWorkflow?: boolean;
|
|
85
|
+
/** Live-only stage timeline retained on the parent while completed internal
|
|
86
|
+
* child rows leave the monitor. */
|
|
87
|
+
workflowStages?: WorkflowStage[];
|
|
68
88
|
}
|
|
69
89
|
|
|
70
90
|
/** Optional metadata for documenter/reviewer/fix children of a stable parent run. */
|
|
@@ -73,6 +93,7 @@ export interface RunChainMeta {
|
|
|
73
93
|
relationLabel?: string;
|
|
74
94
|
parentRunId?: number;
|
|
75
95
|
isolation?: IsolationMode;
|
|
96
|
+
worktreeId?: string;
|
|
76
97
|
forkedFromRunId?: number;
|
|
77
98
|
continuationKind?: ContinuationKind;
|
|
78
99
|
}
|
|
@@ -446,7 +467,7 @@ export class MonitorStore {
|
|
|
446
467
|
...(meta?.groupId ? { groupId: meta.groupId } : {}),
|
|
447
468
|
...(meta?.relationLabel ? { relationLabel: meta.relationLabel } : {}),
|
|
448
469
|
...(meta?.parentRunId !== undefined ? { parentRunId: meta.parentRunId } : {}),
|
|
449
|
-
...(meta?.isolation ? { isolation: meta.isolation, integrationStatus: meta.isolation === "worktree" ? "pending" : undefined } : {}),
|
|
470
|
+
...(meta?.isolation ? { isolation: meta.isolation, integrationStatus: meta.isolation === "worktree" ? "pending" : undefined, ...(meta.worktreeId ? { worktreeId: meta.worktreeId } : {}) } : {}),
|
|
450
471
|
...(meta?.forkedFromRunId !== undefined ? { forkedFromRunId: meta.forkedFromRunId } : {}),
|
|
451
472
|
...(meta?.continuationKind ? { continuationKind: meta.continuationKind } : {}),
|
|
452
473
|
});
|
|
@@ -481,6 +502,18 @@ export class MonitorStore {
|
|
|
481
502
|
const run = this.find(id);
|
|
482
503
|
if (!run) return;
|
|
483
504
|
run.managedWorkflow = active || undefined;
|
|
505
|
+
if (!active) run.workflowStages = undefined;
|
|
506
|
+
this.notify();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Replace the live workflow projection atomically so renderers never observe
|
|
510
|
+
* a half-updated fix/re-review plan. */
|
|
511
|
+
setWorkflowStages(id: number, stages: readonly WorkflowStage[]): void {
|
|
512
|
+
const run = this.find(id);
|
|
513
|
+
if (!run) return;
|
|
514
|
+
run.workflowStages = stages.length > 0
|
|
515
|
+
? stages.map((stage) => ({ ...stage }))
|
|
516
|
+
: undefined;
|
|
484
517
|
this.notify();
|
|
485
518
|
}
|
|
486
519
|
|
|
@@ -535,11 +568,17 @@ export class MonitorStore {
|
|
|
535
568
|
this.notify();
|
|
536
569
|
}
|
|
537
570
|
|
|
538
|
-
setIsolation(
|
|
571
|
+
setIsolation(
|
|
572
|
+
id: number,
|
|
573
|
+
isolation: IsolationMode,
|
|
574
|
+
integrationStatus?: RunIntegrationStatus,
|
|
575
|
+
worktreeId?: string,
|
|
576
|
+
): void {
|
|
539
577
|
const run = this.find(id);
|
|
540
578
|
if (!run) return;
|
|
541
579
|
run.isolation = isolation;
|
|
542
580
|
run.integrationStatus = integrationStatus;
|
|
581
|
+
if (worktreeId) run.worktreeId = worktreeId;
|
|
543
582
|
this.notify();
|
|
544
583
|
}
|
|
545
584
|
|
|
@@ -584,7 +623,7 @@ export class MonitorStore {
|
|
|
584
623
|
model?: string,
|
|
585
624
|
thinking?: string,
|
|
586
625
|
isolation?: IsolationMode,
|
|
587
|
-
meta?: { elapsedMs?: number; continuationKind?: ContinuationKind },
|
|
626
|
+
meta?: { elapsedMs?: number; continuationKind?: ContinuationKind; worktreeId?: string },
|
|
588
627
|
): void {
|
|
589
628
|
const run = this.find(id);
|
|
590
629
|
if (!run) {
|
|
@@ -595,7 +634,13 @@ export class MonitorStore {
|
|
|
595
634
|
label: runLabel(task),
|
|
596
635
|
model,
|
|
597
636
|
thinking,
|
|
598
|
-
...(isolation
|
|
637
|
+
...(isolation
|
|
638
|
+
? {
|
|
639
|
+
isolation,
|
|
640
|
+
integrationStatus: isolation === "worktree" ? "pending" as const : undefined,
|
|
641
|
+
...(isolation === "worktree" && meta?.worktreeId ? { worktreeId: meta.worktreeId } : {}),
|
|
642
|
+
}
|
|
643
|
+
: {}),
|
|
599
644
|
status: "queued",
|
|
600
645
|
usage: emptyUsage(),
|
|
601
646
|
elapsedMs: meta?.elapsedMs ?? 0,
|
|
@@ -611,10 +656,13 @@ export class MonitorStore {
|
|
|
611
656
|
run.thinking = thinking;
|
|
612
657
|
if (isolation) run.isolation = isolation;
|
|
613
658
|
run.integrationStatus = isolation === "worktree" ? "pending" : undefined;
|
|
659
|
+
if (isolation === "worktree" && meta?.worktreeId) run.worktreeId = meta.worktreeId;
|
|
660
|
+
else if (isolation !== "worktree") run.worktreeId = undefined;
|
|
614
661
|
run.status = "queued";
|
|
615
662
|
run.usage = emptyUsage();
|
|
616
663
|
run.activity = undefined;
|
|
617
664
|
run.managedWorkflow = undefined;
|
|
665
|
+
run.workflowStages = undefined;
|
|
618
666
|
run.activeSince = undefined;
|
|
619
667
|
run.endedAt = undefined;
|
|
620
668
|
run.elapsedMs = Math.max(run.elapsedMs, meta?.elapsedMs ?? 0);
|