@ferris1225/pi-subagents 4.1.12 → 4.1.15

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/workflow.ts CHANGED
@@ -1,16 +1,15 @@
1
1
  /**
2
2
  * Managed workflow policy and handoff formatting.
3
3
  *
4
- * Successful top-level worker/cleaner runs continue through an independent
5
- * code review gate. A passing gate may run one conditional documentation
6
- * sync; a failing gate is delivered to the main agent, which owns the fix
7
- * decision the runtime never edits code on a reviewer's behalf. Reviewers
8
- * classify documentation drift explicitly, so the low-cost final documenter
9
- * runs only when needed (or conservatively when an older/custom reviewer
10
- * omits the marker). Direct passing gates use the same policy. Top-level
11
- * documenters are explicit standalone writing tasks. Internal steps are
12
- * launched by dispatch directly, so they never re-enter this policy or wake
13
- * the main agent mid-chain.
4
+ * Successful top-level worker/cleaner runs continue through one independent
5
+ * code review gate. A failing managed gate continues into the reviewer fix
6
+ * stage: the same retained reviewer session gets write access and applies its
7
+ * own fix instructions, so nobody outside the gate has to guess what satisfies
8
+ * it. Direct reviewer results never chain a failing direct gate returns its
9
+ * findings to the main agent, which owns that fix decision. Documentation
10
+ * drift is an ordinary gate finding; dispatching a documenter stays the main
11
+ * agent's call. Internal steps are launched by dispatch directly, so they
12
+ * never re-enter this policy or wake the main agent mid-chain.
14
13
  */
15
14
 
16
15
  import { isWriteCapableAgent, type AgentConfig } from "./agents.ts";
@@ -18,8 +17,6 @@ import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } fro
18
17
  import { formatUsageCompact, sumUsage } from "./monitor.ts";
19
18
 
20
19
  export interface WorkflowAgentAvailability {
21
- cleaner: boolean;
22
- documenter: boolean;
23
20
  reviewer: boolean;
24
21
  writer: boolean;
25
22
  }
@@ -29,131 +26,122 @@ export function workflowAgentAvailability(
29
26
  ): WorkflowAgentAvailability {
30
27
  const names = new Set(agents.map((agent) => agent.name));
31
28
  return {
32
- cleaner: names.has("cleaner"),
33
- documenter: names.has("documenter"),
34
29
  reviewer: names.has("reviewer"),
35
30
  writer: agents.some(isWriteCapableAgent),
36
31
  };
37
32
  }
38
33
 
39
- export type DocumentationDisposition = "clean" | "needed";
40
-
41
- /** Only the last standalone documentation disposition line counts. Inline
42
- * examples and prose are ignored so a prompt echo cannot suppress a needed
43
- * conservative sync. */
44
- export function documentationDisposition(output: string): DocumentationDisposition | undefined {
45
- const lines = output.split("\n");
46
- for (let index = lines.length - 1; index >= 0; index--) {
47
- const match = /^\s*DOCUMENTATION:\s*(CLEAN|NEEDED)\s*$/i.exec(lines[index]);
48
- if (match) return match[1].toUpperCase() === "CLEAN" ? "clean" : "needed";
49
- }
50
- return undefined;
51
- }
52
-
53
- export type ManagedWorkflowKind = "post-writer" | "review-pass-sync";
54
-
55
34
  export interface ManagedWorkflowPlan {
56
- kind: ManagedWorkflowKind;
57
35
  initialRelation: string;
58
36
  }
59
37
 
38
+ /** Fixed cap on reviewer fix → re-review rounds inside one managed workflow.
39
+ * The loop converges by itself when reviews pass; the cap only stops
40
+ * pathological burn and hands the still-failing gate back to the main agent. */
41
+ export const MAX_REVIEW_FIX_ROUNDS = 3;
42
+
60
43
  /** Conservative pre-run check used to reserve one shared-repository lane
61
- * around a complete writer workflow or a reviewer that needs a stable diff.
62
- * The actual result is classified again by getManagedWorkflowPlan before a
63
- * downstream child starts. */
44
+ * around a complete writer workflow or a reviewer that needs a stable diff. */
64
45
  export function canStartManagedWorkflow(
65
46
  agent: Pick<AgentConfig, "name" | "tools">,
66
47
  availability: WorkflowAgentAvailability,
67
48
  ): boolean {
68
49
  // Every shared write-capable role—including custom agents—owns the repository
69
50
  // lane even when no downstream role is enabled. Otherwise its edits can race
70
- // a managed writer's documentation snapshot.
51
+ // a managed writer's pending diff.
71
52
  if (isWriteCapableAgent(agent)) return true;
72
53
  if (agent.name === "reviewer") {
73
54
  // Hold a stable diff snapshot against every discoverable writer even when
74
- // this review is advisory. Classification happens only after the read-only
75
- // child returns, too late to acquire the lane safely.
55
+ // this review is advisory: a gate over a moving diff is unsound.
76
56
  return availability.writer;
77
57
  }
78
58
  return false;
79
59
  }
80
60
 
81
- /** Classify only healthy top-level results. In particular, a reviewer without a
82
- * machine verdict is advisory and cannot start any write-capable child; a
83
- * failing gate is not a workflow at all its findings are delivered to the
84
- * main agent, which decides the fixes. */
61
+ /** Classify only healthy top-level writer results; everything else delivers
62
+ * directly, including every reviewer result a direct reviewer dispatch never
63
+ * starts another child. A failing managed gate is expanded by the workflow
64
+ * itself into the reviewer fix stage. */
85
65
  export function getManagedWorkflowPlan(
86
66
  result: SingleResult,
87
67
  availability: WorkflowAgentAvailability,
88
- advisoryReview = false,
89
68
  ): ManagedWorkflowPlan | undefined {
90
69
  if (result.dispatchFailed || isFailedResult(result)) return undefined;
91
70
  if (result.agent === "worker" || result.agent === "cleaner") {
92
- if (!availability.documenter && !availability.reviewer) return undefined;
71
+ if (!availability.reviewer) return undefined;
93
72
  return {
94
- kind: "post-writer",
95
73
  initialRelation: result.agent === "cleaner" ? "initial cleanup" : "initial implementation",
96
74
  };
97
75
  }
98
- // A top-level documenter is already an explicit docs/comments write task. It
99
- // owns the writer lane but delivers directly without an automatic code gate.
100
- if (result.agent === "documenter") return undefined;
101
- if (result.agent !== "reviewer") return undefined;
102
- // An advisory dispatch never chains: the caller asked for a report, so even
103
- // a stray gate verdict must be delivered rather than acted on.
104
- if (advisoryReview) return undefined;
105
-
106
- const output = getResultOutput(result);
107
- // The pass stands as the code gate. Run the conditional documenter only for
108
- // explicit drift or when an older/custom reviewer omitted the marker.
109
- if (
110
- reviewVerdict(output) === "pass" &&
111
- availability.documenter &&
112
- documentationDisposition(output) !== "clean"
113
- ) {
114
- return { kind: "review-pass-sync", initialRelation: "pre-documentation review" };
115
- }
116
76
  return undefined;
117
77
  }
118
78
 
119
- /** Build the single documentation handoff selected after the review gate
120
- * settles or as the reviewer-disabled fallback. The top-level writer's report
121
- * and the terminal gate review are leads; the pending diff stays authoritative.
122
- * At most one of the two is undefined in every managed flow. */
123
- export function buildFinalDocumenterBrief(
124
- lastWriterResult?: SingleResult,
125
- finalReviewResult?: SingleResult,
126
- ): string {
127
- const reportSections = [
128
- ...(lastWriterResult
129
- ? [
130
- `The last writer (${lastWriterResult.agent}) reported:`,
131
- `---`,
132
- getResultOutput(lastWriterResult),
133
- `---`,
134
- ``,
135
- ]
136
- : []),
137
- ...(finalReviewResult
138
- ? [
139
- `The final gate review reported:`,
140
- `---`,
141
- getResultOutput(finalReviewResult),
142
- `---`,
143
- ``,
144
- ]
145
- : []),
146
- ];
79
+ /** Build the code gate that runs directly after a top-level writer. Reports
80
+ * carry intent; the actual pending diff remains authoritative. */
81
+ export function buildFinalReviewBrief(initialResult: SingleResult): string {
82
+ return [
83
+ `Fresh code gate for a managed ${initialResult.agent} workflow.`,
84
+ ``,
85
+ `The top-level ${initialResult.agent}'s full report:`,
86
+ `---`,
87
+ getResultOutput(initialResult),
88
+ `---`,
89
+ ``,
90
+ `Run \`git status\` and \`git diff\` and inspect the actual pending code; the report is context, not proof.`,
91
+ `Remain read-only. Attach a concrete fix instruction to EVERY gate finding — including documentation drift —:`,
92
+ `what to change, where, and how to verify the fix. A failing gate continues into your own write-enabled fix stage,`,
93
+ `so make every instruction executable exactly as written.`,
94
+ `Surface the COMPLETE finding set in this one pass — scan the full changed surface before the verdict;`,
95
+ `do not ration findings across later rounds.`,
96
+ `This is an acceptance gate, not an advisory audit. End with exactly one standalone machine verdict line:`,
97
+ `VERDICT: REVIEW_PASS when no finding remains, otherwise VERDICT: REVIEW_FAIL.`,
98
+ ].join("\n");
99
+ }
100
+
101
+ /** Build the follow-up brief for the reviewer fix stage: the same retained
102
+ * reviewer session continues with its read-only boundary lifted and applies
103
+ * its own fix instructions. The workflow continues with a fresh re-review. */
104
+ export function buildReviewerFixBrief(gateOutput: string): string {
105
+ return [
106
+ `Fix stage: your gate review returned REVIEW_FAIL. You now have full write access in this same session.`,
107
+ `Apply every one of your own fix instructions now — exactly the changes you specified, nothing broader.`,
108
+ `Then re-check the code your fixes touch, so the next scan does not open with your own regression, and run`,
109
+ `the narrowest decisive checks (type check, focused tests) to verify.`,
110
+ ``,
111
+ `Your gate review:`,
112
+ `---`,
113
+ gateOutput,
114
+ `---`,
115
+ ``,
116
+ `Report:`,
117
+ `## Fixed`,
118
+ `- each finding → the exact fix applied (path + what changed)`,
119
+ `## Verification`,
120
+ `- checks actually run and their results`,
121
+ `Do not emit another VERDICT; a fresh gate re-reviews the diff after you.`,
122
+ ].join("\n");
123
+ }
124
+
125
+ /** Fresh gate over the updated diff after a fix round. The re-review runs in a
126
+ * brand-new context so it cannot inherit the previous pass's blind spots, and
127
+ * it must rescan the complete surface so new findings surface now, not in a
128
+ * later round. */
129
+ export function buildReReviewBrief(fixResult: SingleResult, round: number): string {
147
130
  return [
148
- `Final documentation sync: the review gate settled and you are the last managed stage before delivery.`,
131
+ `Fresh re-review after fix round ${round}: re-scan the COMPLETE pending diff from scratch.`,
132
+ `Earlier reviews and fix reports are context, not proof — do not inherit their conclusions.`,
149
133
  ``,
150
- ...reportSections,
151
- `Inspect the actual git diff and relevant implementation; the reports are only leads.`,
152
- `Apply every documentation note the reviews recorded, then synchronize stale README/docs, examples, API comments, docstrings,`,
153
- `and explanatory comments with the behavior that will be committed.`,
154
- `Change documentation surfaces only; make zero edits when the diff creates no documentation drift.`,
155
- `The workflow delivers directly after you; no fresh reviewer runs.`,
156
- `Report exact documentation/comment paths changed, or state explicitly that no sync was needed.`,
134
+ `The fix stage reported:`,
135
+ `---`,
136
+ getResultOutput(fixResult),
137
+ `---`,
138
+ ``,
139
+ `Run \`git status\` and \`git diff\` and judge the actual pending code, including side effects of the fixes.`,
140
+ `Surface the complete finding set in this one pass a defect this scan should have caught must not appear later.`,
141
+ `Remain read-only. Attach a concrete fix instruction to EVERY finding; a failing gate continues into another`,
142
+ `write-enabled fix stage.`,
143
+ `End with exactly one standalone machine verdict line:`,
144
+ `VERDICT: REVIEW_PASS when no finding remains, otherwise VERDICT: REVIEW_FAIL.`,
157
145
  ].join("\n");
158
146
  }
159
147
 
@@ -161,8 +149,8 @@ export function buildFinalDocumenterBrief(
161
149
  * One step of a managed workflow as delivered: the run id (so the condensed
162
150
  * summary can point at per-run detail via subagent_status), the result, and
163
151
  * the human-readable role within the workflow ("initial implementation",
164
- * "final review", "final documentation sync"). runId is optional only for
165
- * synthetic steps that never spawned a child.
152
+ * "final review"). runId is optional only for synthetic steps that never
153
+ * spawned a child.
166
154
  */
167
155
  export interface ChainStep {
168
156
  runId?: number;
@@ -171,12 +159,14 @@ export interface ChainStep {
171
159
  }
172
160
 
173
161
  export interface ManagedWorkflowOutcome {
174
- kind: ManagedWorkflowKind;
175
162
  steps: ChainStep[];
176
163
  }
177
164
 
178
- function workflowResultStatus(result: SingleResult): string {
165
+ function workflowResultStatus(result: SingleResult, relation?: string): string {
179
166
  if (isFailedResult(result)) return "failed";
167
+ // The fix stage is reviewer-named writer work: judge it by outcome, not by
168
+ // the verdict contract its review stage was held to.
169
+ if (relation === "review fix") return "completed";
180
170
  if (result.agent === "reviewer") {
181
171
  const verdict = reviewVerdict(getResultOutput(result));
182
172
  return verdict ? verdict.toUpperCase() : "NO_VERDICT";
@@ -186,7 +176,7 @@ function workflowResultStatus(result: SingleResult): string {
186
176
 
187
177
  function workflowStepLine(step: ChainStep): string {
188
178
  const id = step.runId !== undefined ? `#${step.runId} ` : "";
189
- return `- ${id}${step.result.agent} · ${step.relation} · ${workflowResultStatus(step.result)}`;
179
+ return `- ${id}${step.result.agent} · ${step.relation} · ${workflowResultStatus(step.result, step.relation)}`;
190
180
  }
191
181
 
192
182
  function appendWorkflowFooter(lines: string[], steps: readonly ChainStep[]): void {
@@ -197,52 +187,14 @@ function appendWorkflowFooter(lines: string[], steps: readonly ChainStep[]): voi
197
187
  lines.push(`Per-run details: subagent_status ${ids.join(" ")}`);
198
188
  }
199
189
 
200
- /** One clear final delivery for post-writer and direct reviewer documenter workflows. */
190
+ /** One clear final delivery for managed writer → gate workflows. */
201
191
  export function formatManagedWorkflowSummary(
202
192
  steps: readonly ChainStep[],
203
193
  terminalResult: SingleResult = steps[steps.length - 1]!.result,
194
+ terminalRelation: string = steps[steps.length - 1]!.relation,
204
195
  ): string {
205
196
  const route = steps.map((step) => step.result.agent).join(" → ");
206
- const lines = [`## Managed workflow: ${route} — final ${workflowResultStatus(terminalResult)}`, "", ...steps.map(workflowStepLine)];
197
+ const lines = [`## Managed workflow: ${route} — final ${workflowResultStatus(terminalResult, terminalRelation)}`, "", ...steps.map(workflowStepLine)];
207
198
  appendWorkflowFooter(lines, steps);
208
199
  return lines.join("\n");
209
200
  }
210
-
211
- export interface GateBriefOptions {
212
- /** A conditional final documenter is available after the gate settles.
213
- * Documentation drift is then routed to it as non-gating notes instead of
214
- * failing the code gate. */
215
- documenterPending: boolean;
216
- }
217
-
218
- /** Build the code gate that runs directly after a top-level writer, before any
219
- * documentation. Reports carry intent; the actual pending diff remains
220
- * authoritative. */
221
- export function buildFinalReviewBrief(
222
- initialResult: SingleResult,
223
- options: GateBriefOptions,
224
- ): string {
225
- return [
226
- `Fresh code gate for a managed ${initialResult.agent} workflow.`,
227
- ``,
228
- `The top-level ${initialResult.agent}'s full report:`,
229
- `---`,
230
- getResultOutput(initialResult),
231
- `---`,
232
- ``,
233
- `Run \`git status\` and \`git diff\` and inspect the actual pending code; the report is context, not proof.`,
234
- `Remain read-only. Attach a concrete fix instruction to EVERY gate finding: what to change, where, and how to verify the fix`,
235
- `— the report returns to the main agent, which uses your instructions to drive the fix.`,
236
- ...(options.documenterPending
237
- ? [
238
- `A conditional documentation sync is available AFTER this gate, so documentation drift is not a code-gate finding:`,
239
- `record it under "## Documentation notes" and emit the standalone line DOCUMENTATION: NEEDED,`,
240
- `or DOCUMENTATION: CLEAN when no documentation update is needed.`,
241
- ]
242
- : [
243
- `No documenter is pending, so documentation drift is an ordinary gate finding.`,
244
- ]),
245
- `This is an acceptance gate, not an advisory audit. End with exactly one standalone machine verdict line:`,
246
- `VERDICT: REVIEW_PASS when no finding remains, otherwise VERDICT: REVIEW_FAIL.`,
247
- ].join("\n");
248
- }