@ferris1225/pi-subagents 4.1.4 → 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/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 writer → reviewer documenter workflows,
5
- * bounded worker/reviewer fix rounds, and internal step launching. Stable
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 for explicitly authorized cleanup, removal, simplification, and duplicate-code consolidation; documenter for pre-commit diff sync or explicitly requested whole-codebase comment/README/docs maintenance; reviewer for generic read-only assessments and final gates.",
172
- "Work starts in the background; successful top-level writers automatically continue through enabled documenter/reviewer stages and return one final completion. Results resume the main agent and are already shown to the user, so do not poll, duplicate downstream roles, or restate them. Give each child a self-contained brief because it has no conversation memory.",
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: explorer (recon), worker (implementation), cleaner (authorized cleanup/deduplication), documenter (docs sync), reviewer (read-only assessment/gate); enabled post-writer stages run automatically, results resume automatically, and the workflow delivers once. Use direct tools for trivial work.",
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) {
@@ -387,27 +399,90 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
387
399
  const enabled = (name: string): boolean =>
388
400
  request.agents.some((candidate) => candidate.name === name);
389
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
+
390
444
  const launchStep = async (
391
445
  agentName: string,
392
446
  task: string,
393
447
  relation: string,
448
+ projection: {
449
+ stage?: WorkflowStage;
450
+ timelineRelation?: string;
451
+ childRelation?: string;
452
+ } = {},
394
453
  ): Promise<SingleResult> => {
395
454
  if (!enabled(agentName)) {
396
455
  throw new Error(`Managed workflow cannot launch disabled or missing agent "${agentName}".`);
397
456
  }
398
- const step = await launchInWorkflow(request, agentName, task, {
399
- groupId: request.groupId,
400
- relationLabel: relation,
401
- parentRunId: request.parentRunId,
402
- });
403
- request.rememberLatest(step.result);
404
- steps.push({ ...step, relation });
405
- return step.result;
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
+ }
406
481
  };
407
482
 
408
- /** Bounded worker → reviewer fix rounds. Documentation sync deliberately
409
- * stays out of the rounds: code fixes would invalidate it, and one final
410
- * documenter covers the settled diff once. */
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. */
411
486
  const runFixRounds = async (
412
487
  triggeringReviewer: SingleResult,
413
488
  ): Promise<{ lastReview?: SingleResult; lastWorker?: SingleResult }> => {
@@ -415,20 +490,24 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
415
490
  const outcome: { lastReview?: SingleResult; lastWorker?: SingleResult } = {};
416
491
  for (let round = 1; round <= request.config.maxFixRounds; round++) {
417
492
  if (!canContinue()) break;
493
+ const fixRelation = `fix ${round}/${request.config.maxFixRounds}`;
418
494
  const workerResult = await launchStep(
419
495
  "worker",
420
496
  buildFixTaskBrief(lastReviewer, round, request.config.maxFixRounds),
421
497
  `fix round ${round}`,
498
+ { timelineRelation: fixRelation, childRelation: fixRelation },
422
499
  );
423
500
  if (isFailedResult(workerResult) || !canContinue()) break;
424
501
  outcome.lastWorker = workerResult;
425
502
 
503
+ const reReviewRelation = `re-review ${round}/${request.config.maxFixRounds}`;
426
504
  const reviewResult = await launchStep(
427
505
  "reviewer",
428
506
  buildReReviewBrief(lastReviewer, round, workerResult, {
429
507
  documenterPending: enabled("documenter"),
430
508
  }),
431
509
  `re-review round ${round}`,
510
+ { timelineRelation: reReviewRelation, childRelation: reReviewRelation },
432
511
  );
433
512
  if (isFailedResult(reviewResult) || !canContinue()) break;
434
513
  outcome.lastReview = reviewResult;
@@ -441,24 +520,40 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
441
520
  return outcome;
442
521
  };
443
522
 
444
- /** One final documentation sync after the gate settles. It is skipped
445
- * when documenter is disabled, the last writer already was a
446
- * documenter with no code fixes after it, or the chain ended on a
447
- * failing/crashed gate (docs for rejected code would be rework). */
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. */
448
527
  const runFinalDocumentation = async (
449
528
  lastWriterResult: SingleResult | undefined,
450
529
  finalReviewResult: SingleResult | undefined,
451
530
  ): Promise<void> => {
452
531
  if (!canContinue() || !enabled("documenter")) return;
453
- if (lastWriterResult?.agent === "documenter") return;
532
+ if (lastWriterResult?.agent === "documenter") {
533
+ removeDocumentationStage();
534
+ return;
535
+ }
454
536
  if (finalReviewResult) {
455
- if (isFailedResult(finalReviewResult)) return;
456
- if (reviewVerdict(getResultOutput(finalReviewResult)) === "fail") return;
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
+ }
457
549
  }
550
+ documentationStage ??= { agent: "documenter", relation: "docs", status: "pending" };
551
+ if (!workflowStages.includes(documentationStage)) workflowStages.push(documentationStage);
458
552
  await launchStep(
459
553
  "documenter",
460
554
  buildFinalDocumenterBrief(lastWriterResult, finalReviewResult),
461
555
  "final documentation sync",
556
+ { stage: documentationStage },
462
557
  );
463
558
  };
464
559
 
@@ -471,30 +566,30 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
471
566
  const fixOutcome = await runFixRounds(initialStepResult);
472
567
  await runFinalDocumentation(fixOutcome.lastWorker, fixOutcome.lastReview ?? initialStepResult);
473
568
  } else if (request.plan.kind === "review-pass-sync") {
474
- // The direct passing review already gated the pending code; sync
475
- // docs once and deliver without a second gate.
569
+ // The direct passing review already gated the pending code. Its
570
+ // disposition requested (or conservatively defaulted to) one docs sync.
476
571
  await runFinalDocumentation(undefined, initialStepResult);
477
572
  } else if (enabled("reviewer")) {
478
573
  const gateReview = await launchStep(
479
574
  "reviewer",
480
575
  buildFinalReviewBrief(initialStepResult, { documenterPending: enabled("documenter") }),
481
576
  "final review",
577
+ { stage: reviewStage },
482
578
  );
483
- if (!isFailedResult(gateReview)) {
484
- let fixOutcome: Awaited<ReturnType<typeof runFixRounds>> = {};
485
- if (
486
- canContinue() &&
487
- reviewVerdict(getResultOutput(gateReview)) === "fail" &&
488
- enabled("worker") &&
489
- request.config.maxFixRounds > 0
490
- ) {
491
- fixOutcome = await runFixRounds(gateReview);
492
- }
493
- await runFinalDocumentation(
494
- fixOutcome.lastWorker ?? initialStepResult,
495
- fixOutcome.lastReview ?? gateReview,
496
- );
579
+ let fixOutcome: Awaited<ReturnType<typeof runFixRounds>> = {};
580
+ if (
581
+ !isFailedResult(gateReview) &&
582
+ canContinue() &&
583
+ reviewVerdict(getResultOutput(gateReview)) === "fail" &&
584
+ enabled("worker") &&
585
+ request.config.maxFixRounds > 0
586
+ ) {
587
+ fixOutcome = await runFixRounds(gateReview);
497
588
  }
589
+ await runFinalDocumentation(
590
+ fixOutcome.lastWorker ?? initialStepResult,
591
+ fixOutcome.lastReview ?? gateReview,
592
+ );
498
593
  } else {
499
594
  // No gate configured: the documenter is the only downstream stage.
500
595
  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 writers continue through an independent code review
5
- * gate; bounded worker → reviewer fix rounds close its findings; one final
6
- * documentation sync runs after the gate settles, so code fixes never
7
- * invalidate an earlier docs pass and reviewers stay focused on code. A direct
8
- * passing reviewer gets that same single final documentation sync instead of a
9
- * second gate; a direct failing reviewer uses the same fix rounds plus the
10
- * final sync. Internal steps are launched by dispatch directly, so they never
11
- * re-enter this top-level policy or wake the main agent mid-chain.
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
- if (result.agent === "documenter") {
105
- return availability.reviewer
106
- ? { kind: "post-writer", initialRelation: "documentation pass" }
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 verdict = reviewVerdict(getResultOutput(result));
112
- // The pass stands as the code gate; documenter then syncs docs once and the
113
- // workflow delivers without a second gate.
114
- if (verdict === "pass" && availability.documenter) {
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
- `Do NOT commit, push, publish, tag, or release; do not bump versions. The parent chain still owns re-review and the final documentation sync.`,
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 enabled final documentation sync and then delivers.`,
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 that runs after the review gate
152
- * settles. The last writer's report (top-level writer or final fix-round
153
- * worker) and the terminal gate review are leads; the pending diff stays
154
- * authoritative. At most one of the two is undefined in every managed flow. */
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 all newly managed writer/documenter workflows. */
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 runs after the gate settles. Documentation drift is
266
- * then routed to it as non-gating notes instead of failing the gate. */
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
- `Documentation sync runs AFTER this gate, so documentation drift is not a gate finding: record needed`,
290
- `documentation updates as a separate short "## Documentation notes" list for the final documenter,`,
291
- `and fail the gate only for code or test findings.`,
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 any unresolved "## Documentation notes" forward verbatim; the final documenter applies them after the chain settles.`,
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,6 +20,15 @@ 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";
@@ -65,6 +74,9 @@ export interface RunView {
65
74
  /** This stable top-level row currently owns a multi-stage managed workflow.
66
75
  * Its elapsed time is workflow-wide; active child rows own stage telemetry. */
67
76
  managedWorkflow?: boolean;
77
+ /** Live-only stage timeline retained on the parent while completed internal
78
+ * child rows leave the monitor. */
79
+ workflowStages?: WorkflowStage[];
68
80
  }
69
81
 
70
82
  /** Optional metadata for documenter/reviewer/fix children of a stable parent run. */
@@ -481,6 +493,18 @@ export class MonitorStore {
481
493
  const run = this.find(id);
482
494
  if (!run) return;
483
495
  run.managedWorkflow = active || undefined;
496
+ if (!active) run.workflowStages = undefined;
497
+ this.notify();
498
+ }
499
+
500
+ /** Replace the live workflow projection atomically so renderers never observe
501
+ * a half-updated fix/re-review plan. */
502
+ setWorkflowStages(id: number, stages: readonly WorkflowStage[]): void {
503
+ const run = this.find(id);
504
+ if (!run) return;
505
+ run.workflowStages = stages.length > 0
506
+ ? stages.map((stage) => ({ ...stage }))
507
+ : undefined;
484
508
  this.notify();
485
509
  }
486
510
 
@@ -615,6 +639,7 @@ export class MonitorStore {
615
639
  run.usage = emptyUsage();
616
640
  run.activity = undefined;
617
641
  run.managedWorkflow = undefined;
642
+ run.workflowStages = undefined;
618
643
  run.activeSince = undefined;
619
644
  run.endedAt = undefined;
620
645
  run.elapsedMs = Math.max(run.elapsedMs, meta?.elapsedMs ?? 0);
package/src/prompt.ts CHANGED
@@ -29,14 +29,7 @@ export function buildDelegationDirective(
29
29
  ...(hasWorker ? ["worker"] : []),
30
30
  ...(hasCleaner ? ["cleaner"] : []),
31
31
  ];
32
- const reviewedWriterNames = [
33
- ...codeWriterNames,
34
- ...(hasDocumenter ? ["documenter"] : []),
35
- ];
36
- const automaticWriterRoute = [
37
- ...(hasReviewer ? ["reviewer"] : []),
38
- ...(hasDocumenter ? ["documenter"] : []),
39
- ].join(" → ");
32
+ const reviewedWriterNames = [...codeWriterNames];
40
33
  const namedWorktreeTargets = [
41
34
  ...(hasWorker ? ["worker"] : []),
42
35
  ...(hasCleaner ? ["cleaner"] : []),
@@ -47,34 +40,43 @@ export function buildDelegationDirective(
47
40
  : namedWorktreeTargets.length === 1
48
41
  ? `${namedWorktreeTargets[0]} or another`
49
42
  : `${namedWorktreeTargets.slice(0, -1).join(", ")}, ${namedWorktreeTargets.at(-1)}, or another`;
43
+ const managedWriterWorkflowRule = reviewedWriterNames.length === 0
44
+ ? undefined
45
+ : hasReviewer && hasDocumenter
46
+ ? `Successful top-level ${reviewedWriterNames.join("/")} runs continue through the enabled reviewer gate. Only REVIEW_PASS can authorize documenter, which runs for DOCUMENTATION: NEEDED or a missing marker; the workflow delivers once. Never duplicate stages.`
47
+ : hasReviewer
48
+ ? `Successful top-level ${reviewedWriterNames.join("/")} runs continue through the enabled reviewer gate and then deliver once; never duplicate the gate.`
49
+ : hasDocumenter
50
+ ? `With reviewer disabled, successful top-level ${reviewedWriterNames.join("/")} runs use documenter as the conservative final fallback and then deliver once; never duplicate the fallback.`
51
+ : undefined;
50
52
 
51
53
  const dispatchRules = [
52
- "Handle simple work inline with direct tools: one-line lookups, known-target reads/edits, and quick questions do not justify a child process.",
54
+ "Keep small, known-target work in the main thread with direct tools: lookups and focused reads/edits do not justify a child context.",
53
55
  ...(hasExplorer
54
56
  ? [
55
- "Use `explorer` proactively when reconnaissance becomes broad or crosses files: mapping unfamiliar code, tracing symbols/dependencies, or answering multi-file location/reference questions. Treat its output only as a retrieval index; re-read load-bearing files before edits or decisions about deletion, security, compatibility, persistence, or dynamic reachability. Use a stronger model/specialist for complex dynamic, concurrent, migration, or security-sensitive analysis.",
57
+ "Use `explorer` proactively only for broad or cross-file reconnaissance: mapping unfamiliar code, tracing symbols/dependencies, or finding multi-file references. It is a lightweight retrieval index, never an automatic gate. Re-read load-bearing files before edits or high-risk decisions. Use a stronger model/specialist for dynamic, concurrent, migration, or security analysis.",
56
58
  ]
57
59
  : []),
58
60
  ...(hasWorker
59
- ? ["Use `worker` for a self-contained implementation, fix, refactor, or test task whose separate context pays for itself."]
61
+ ? ["Use `worker` for a self-contained implementation, fix, refactor, or test whose separate context pays for itself—not a small known-target edit."]
60
62
  : []),
61
63
  ...(hasCleaner
62
64
  ? [
63
- `Use \`cleaner\` only for user-authorized cleanup, removal, simplification, duplicate-code consolidation, or maintenance. Once dispatched, it applies every safe proven in-scope cut without item-by-item approval; zero edits is valid only if none is proved. Generic or read-only audit, review, code-health, plan, or cleanup-candidate assessment goes to ${hasReviewer ? "`reviewer`" : "direct main-context inspection because `reviewer` is disabled"}. Never dispatch cleaner by PR count or as the pre-commit gate.`,
65
+ `Use \`cleaner\` only as the separate evidence-first entry for user-authorized cleanup, removal, simplification, duplicate-code consolidation, or maintenance; never substitute it for \`worker\`. It applies every safe proven in-scope cut without item-by-item approval. Generic or read-only audit, review, code-health, plan, or cleanup-candidate assessment goes to ${hasReviewer ? "`reviewer`" : "direct main-context inspection because `reviewer` is disabled"}. Never dispatch cleaner by PR count or as the pre-commit gate.`,
64
66
  ]
65
67
  : []),
66
68
  ...(hasDocumenter
67
69
  ? [
68
- `Use \`documenter\` directly for explicit whole-codebase maintenance or standalone documentation work.${codeWriterNames.length > 0 ? ` Successful ${codeWriterNames.join("/")} runs already auto-sync the actual diff once after the review gate; never dispatch a duplicate.` : ""} Zero edits is valid and broad mode is never inferred. It changes docs/comments only and never runtime behavior, versions, or release state.`,
70
+ `Use \`documenter\` directly only for explicit whole-codebase maintenance or standalone documentation/comment work; a top-level documenter delivers directly without an automatic reviewer.${codeWriterNames.length > 0 ? ` ${codeWriterNames.join("/")} must sync existing docs they directly affect; runtime runs documenter only after REVIEW_PASS with DOCUMENTATION: NEEDED or a missing marker, or as the reviewer-disabled fallback—never dispatch a duplicate.` : ""} It never changes runtime behavior, versions, or release state.`,
69
71
  ]
70
72
  : []),
71
73
  ...(hasReviewer
72
74
  ? [
73
- `Use \`reviewer\` for read-only assessments or an explicit gate.${reviewedWriterNames.length > 0 ? ` Successful ${reviewedWriterNames.join("/")} runs already get a fresh read-only reviewer gate.` : ""} Advisory output has no VERDICT: it stays read-only and does not authorize follow-up edits.`,
75
+ `Use \`reviewer\` for read-only assessments or a gate.${reviewedWriterNames.length > 0 ? ` Successful ${reviewedWriterNames.join("/")} runs already get one fresh read-only reviewer gate, independent of the writer.` : ""} Advisory output has no VERDICT and cannot authorize follow-up edits${hasDocumenter ? "; gates classify docs separately for the enabled documenter." : "."}`,
74
76
  ]
75
77
  : []),
76
- "Brief every child with the complete goal, exact paths, constraints, and expected output. It has no memory of this conversation.",
77
- "Children are leaf processes without delegation tools. Do not ask them to spawn sub-agents; use `subagent_control fork` on a parked/settled retained thread for an independent continuation.",
78
+ "Brief each child with the complete goal, exact paths, constraints, and expected output; it has no conversation memory.",
79
+ "Children are leaf processes without delegation tools; use `subagent_control fork` on a parked/settled thread for an independent continuation.",
78
80
  ...(hasMultiple
79
81
  ? [
80
82
  "Dispatch independent work in one `tasks` array and let the resumed main agent start dependent work only after prerequisites finish.",
@@ -86,24 +88,20 @@ export function buildDelegationDirective(
86
88
  ];
87
89
 
88
90
  const handoffRules = [
89
- "Dispatch returns immediately and ends this turn. Never sleep, poll, or call `subagent_wait` to hold the turn; results arrive as messages that automatically resume the main agent, even mid-turn.",
90
- "Use `subagent_wait` with explicit `timeoutMs` only when the user specifically asks you to remain in-turn and wait. Its default lookup is non-blocking.",
91
- "A result is already shown to the user. Do not restate, paraphrase, or re-summarize it; add only your conclusion or next action, often one line.",
91
+ "Dispatch ends this turn; results resume the main agent, even mid-turn. Never sleep, poll, or call `subagent_wait` to hold the turn.",
92
+ "Use `subagent_wait` with explicit `timeoutMs` only when the user asks to wait in-turn; its default lookup is non-blocking.",
93
+ "Results are already shown. Do not restate, paraphrase, or re-summarize them; add only your conclusion or next action.",
92
94
  "A delivered result does not mean siblings are finished. Before declaring the overall task done, use `subagent_status` to confirm that no runs remain active.",
93
95
  ];
94
96
 
95
97
  const verificationRules = [
96
98
  "Never report an unrun check as passed; identify unavailable checks and pre-existing failures honestly.",
97
- ...(automaticWriterRoute && reviewedWriterNames.length > 0
98
- ? [
99
- `Successful top-level write roles automatically continue through enabled downstream roles (${automaticWriterRoute}) to one final delivery; never duplicate stages.`,
100
- ]
101
- : []),
99
+ ...(managedWriterWorkflowRule ? [managedWriterWorkflowRule] : []),
102
100
  ...(hasReviewer
103
101
  ? [
104
102
  ...(hasDocumenter
105
103
  ? [
106
- `A direct REVIEW_PASS is final for code: runtime runs the final documentation sync once and delivers. A direct REVIEW_FAIL ${autoFixEnabled ? "keeps auto-fix; maxFixRounds limits worker fixes only, not the final documentation sync." : "cannot start fixes while worker/fix rounds are disabled."}`,
104
+ `A direct REVIEW_PASS with DOCUMENTATION: CLEAN delivers immediately; NEEDED or a missing marker runs one documentation sync. A direct REVIEW_FAIL ${autoFixEnabled ? "keeps bounded worker/reviewer auto-fix, with docs considered only after its terminal REVIEW_PASS." : "cannot start fixes while worker/fix rounds are disabled."}`,
107
105
  ]
108
106
  : []),
109
107
  "Resolve every gate finding; do not bypass the configured auto-fix/re-review cap. A reviewer report without a standalone VERDICT is advisory and cannot trigger writes.",
@@ -116,7 +114,7 @@ export function buildDelegationDirective(
116
114
  return `
117
115
  ## Sub-agent delegation (pi-subagents)
118
116
 
119
- The \`subagent\` tool starts specialized leaf agents in isolated Pi child processes and context windows. It returns immediately; completion messages automatically resume the main agent.
117
+ The \`subagent\` tool starts isolated Pi child processes and context windows. Completions automatically resume the main agent.
120
118
 
121
119
  Available agents:
122
120
  ${catalog}