@ferris1225/pi-subagents 4.1.1 → 4.1.2
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 +506 -481
- package/agents/cleaner.md +14 -4
- package/agents/documenter.md +44 -0
- package/agents/reviewer.md +3 -2
- package/agents/worker.md +4 -1
- package/package.json +2 -2
- package/src/agents.ts +12 -0
- package/src/announcements.ts +18 -1
- package/src/config.ts +43 -13
- package/src/dispatch.ts +637 -704
- package/src/fixloop.ts +266 -52
- package/src/index.ts +3 -3
- package/src/monitor.ts +12 -3
- package/src/prompt.ts +47 -12
- package/src/rpc-run.ts +23 -7
- package/src/runtime.ts +8 -7
- package/src/setup.ts +24 -7
- package/src/spawn.ts +45 -11
- package/src/thread-lifecycle.ts +203 -49
- package/src/tools.ts +23 -9
- package/src/widget.ts +4 -4
- package/src/worktree.ts +27 -4
package/src/fixloop.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* worker (briefed with the review's concrete findings) and then a reviewer
|
|
4
|
-
* re-review, repeating up to maxFixRounds times before waking the main agent with
|
|
5
|
-
* the full chain. The reviewer stays read-only and in its own context; the loop
|
|
6
|
-
* is orchestrated by the extension layer, not by the reviewer itself, so the
|
|
7
|
-
* independence guarantee (no self-confirmation bias) is preserved.
|
|
2
|
+
* Managed workflow policy and handoff formatting.
|
|
8
3
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
4
|
+
* Successful top-level writers can continue through documentation sync and an
|
|
5
|
+
* independent final review. A direct passing reviewer is also forced through
|
|
6
|
+
* documentation sync plus a fresh review when documenter is enabled. Any final
|
|
7
|
+
* gate failure may then use the established worker → optional documenter →
|
|
8
|
+
* reviewer fix rounds. Internal steps are launched by dispatch directly, so
|
|
9
|
+
* they never re-enter this top-level policy or wake the main agent mid-chain.
|
|
12
10
|
*/
|
|
13
11
|
|
|
12
|
+
import { isWriteCapableAgent, type AgentConfig } from "./agents.ts";
|
|
14
13
|
import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
|
|
15
14
|
import { extractKeyFragments, formatUsageCompact, sumUsage } from "./monitor.ts";
|
|
16
15
|
import type { SubagentsConfig } from "./config.ts";
|
|
@@ -31,12 +30,92 @@ export function shouldTriggerFixLoop(result: SingleResult, config: SubagentsConf
|
|
|
31
30
|
// child happened to emit, which could end in a stray `VERDICT: REVIEW_FAIL`.
|
|
32
31
|
// Guard explicitly in addition to isFailedResult so the intent is clear and
|
|
33
32
|
// a future change to isFailedResult can never let a crashed reviewer start a
|
|
34
|
-
// phantom auto-fix chain
|
|
35
|
-
// the catch path).
|
|
33
|
+
// phantom auto-fix chain.
|
|
36
34
|
if (result.dispatchFailed) return false;
|
|
37
35
|
return reviewVerdict(getResultOutput(result)) === "fail";
|
|
38
36
|
}
|
|
39
37
|
|
|
38
|
+
export interface WorkflowAgentAvailability {
|
|
39
|
+
worker: boolean;
|
|
40
|
+
cleaner: boolean;
|
|
41
|
+
documenter: boolean;
|
|
42
|
+
reviewer: boolean;
|
|
43
|
+
writer: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function workflowAgentAvailability(
|
|
47
|
+
agents: readonly Pick<AgentConfig, "name" | "tools">[],
|
|
48
|
+
): WorkflowAgentAvailability {
|
|
49
|
+
const names = new Set(agents.map((agent) => agent.name));
|
|
50
|
+
return {
|
|
51
|
+
worker: names.has("worker"),
|
|
52
|
+
cleaner: names.has("cleaner"),
|
|
53
|
+
documenter: names.has("documenter"),
|
|
54
|
+
reviewer: names.has("reviewer"),
|
|
55
|
+
writer: agents.some(isWriteCapableAgent),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type ManagedWorkflowKind = "auto-fix" | "post-writer" | "review-pass-sync";
|
|
60
|
+
|
|
61
|
+
export interface ManagedWorkflowPlan {
|
|
62
|
+
kind: ManagedWorkflowKind;
|
|
63
|
+
initialRelation: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Conservative pre-run check used to reserve one shared-repository lane
|
|
67
|
+
* around a complete writer workflow or a reviewer that needs a stable diff.
|
|
68
|
+
* The actual result is classified again by getManagedWorkflowPlan before a
|
|
69
|
+
* downstream child starts. */
|
|
70
|
+
export function canStartManagedWorkflow(
|
|
71
|
+
agent: Pick<AgentConfig, "name" | "tools">,
|
|
72
|
+
availability: WorkflowAgentAvailability,
|
|
73
|
+
): boolean {
|
|
74
|
+
// Every shared write-capable role—including custom agents—owns the repository
|
|
75
|
+
// lane even when no downstream role is enabled. Otherwise its edits can race
|
|
76
|
+
// a managed writer's documentation snapshot.
|
|
77
|
+
if (isWriteCapableAgent(agent)) return true;
|
|
78
|
+
if (agent.name === "reviewer") {
|
|
79
|
+
// Hold a stable diff snapshot against every discoverable writer even when
|
|
80
|
+
// this review is advisory or maxFixRounds=0. Classification happens only
|
|
81
|
+
// after the read-only child returns, too late to acquire the lane safely.
|
|
82
|
+
return availability.writer;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Classify only healthy top-level results. In particular, a reviewer without a
|
|
88
|
+
* machine verdict is advisory and cannot start any write-capable child. */
|
|
89
|
+
export function getManagedWorkflowPlan(
|
|
90
|
+
result: SingleResult,
|
|
91
|
+
config: SubagentsConfig,
|
|
92
|
+
availability: WorkflowAgentAvailability,
|
|
93
|
+
): ManagedWorkflowPlan | undefined {
|
|
94
|
+
if (result.parked || result.dispatchFailed || isFailedResult(result)) return undefined;
|
|
95
|
+
if (result.agent === "worker" || result.agent === "cleaner") {
|
|
96
|
+
if (!availability.documenter && !availability.reviewer) return undefined;
|
|
97
|
+
return {
|
|
98
|
+
kind: "post-writer",
|
|
99
|
+
initialRelation: result.agent === "cleaner" ? "initial cleanup" : "initial implementation",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (result.agent === "documenter") {
|
|
103
|
+
return availability.reviewer
|
|
104
|
+
? { kind: "post-writer", initialRelation: "documentation pass" }
|
|
105
|
+
: undefined;
|
|
106
|
+
}
|
|
107
|
+
if (result.agent !== "reviewer") return undefined;
|
|
108
|
+
|
|
109
|
+
const verdict = reviewVerdict(getResultOutput(result));
|
|
110
|
+
if (verdict === "pass" && availability.documenter && availability.reviewer) {
|
|
111
|
+
return { kind: "review-pass-sync", initialRelation: "pre-documentation review" };
|
|
112
|
+
}
|
|
113
|
+
if (verdict === "fail" && availability.worker && shouldTriggerFixLoop(result, config)) {
|
|
114
|
+
return { kind: "auto-fix", initialRelation: "initial review" };
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
40
119
|
/**
|
|
41
120
|
* Build the worker task brief for one fix round from a reviewer's findings.
|
|
42
121
|
* The worker gets the full review text so it can address concrete file:line
|
|
@@ -56,20 +135,83 @@ export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, m
|
|
|
56
135
|
`Fix EVERY finding in the reviewer's findings list — there is no severity triage; all of them get fixed.`,
|
|
57
136
|
`If a finding is factually wrong or clearly out of scope, say so explicitly instead of fixing it.`,
|
|
58
137
|
`Do NOT refactor unrelated code beyond what the findings require.`,
|
|
138
|
+
`Do NOT commit, push, publish, tag, or release; do not bump versions. The parent chain still owns documentation sync and final review.`,
|
|
59
139
|
`After editing, run the project's format/build/tests when they exist and report`,
|
|
60
140
|
`exactly what you changed (paths + short rationale) so a reviewer can verify.`,
|
|
61
141
|
remaining > 0
|
|
62
142
|
? `A reviewer will re-review your changes automatically after you finish.`
|
|
63
|
-
: `This is the last auto-fix round;
|
|
143
|
+
: `This is the last auto-fix round; optional documentation sync and a fresh reviewer still run before final delivery.`,
|
|
64
144
|
].join("\n");
|
|
65
145
|
}
|
|
66
146
|
|
|
147
|
+
interface DocumentationBriefOptions {
|
|
148
|
+
title: string;
|
|
149
|
+
reports: Array<{ label: string; result: SingleResult }>;
|
|
150
|
+
closing: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildDocumentationBrief(options: DocumentationBriefOptions): string {
|
|
154
|
+
const reportSections = options.reports.flatMap(({ label, result }) => [
|
|
155
|
+
`${label}:`,
|
|
156
|
+
`---`,
|
|
157
|
+
getResultOutput(result),
|
|
158
|
+
`---`,
|
|
159
|
+
``,
|
|
160
|
+
]);
|
|
161
|
+
return [
|
|
162
|
+
options.title,
|
|
163
|
+
``,
|
|
164
|
+
...reportSections,
|
|
165
|
+
`Inspect the actual git diff (the complete pending diff) and relevant implementation; the report is only a lead.`,
|
|
166
|
+
`Synchronize stale README/docs, examples, API comments, docstrings, and explanatory comments with the behavior that will be committed.`,
|
|
167
|
+
`Change documentation surfaces only; never alter runtime behavior or tests to make prose true.`,
|
|
168
|
+
`Make zero edits when the diff creates no documentation drift.`,
|
|
169
|
+
`Do NOT commit, push, publish, tag, or release; do not bump versions. ${options.closing}`,
|
|
170
|
+
`Report exact documentation/comment paths changed, or state explicitly that no sync was needed.`,
|
|
171
|
+
].join("\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
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
|
+
|
|
67
209
|
/**
|
|
68
210
|
* One step of an auto-fix chain as delivered: the run id (so the condensed
|
|
69
211
|
* summary can point at per-run detail via subagent_status), the result, and
|
|
70
212
|
* the human-readable role within the chain ("initial review", "fix round 1",
|
|
71
|
-
* "re-review round 2"). runId is
|
|
72
|
-
*
|
|
213
|
+
* "re-review round 2"). runId is optional only for synthetic steps that never
|
|
214
|
+
* spawned a child.
|
|
73
215
|
*/
|
|
74
216
|
export interface ChainStep {
|
|
75
217
|
runId?: number;
|
|
@@ -77,6 +219,11 @@ export interface ChainStep {
|
|
|
77
219
|
relation: string;
|
|
78
220
|
}
|
|
79
221
|
|
|
222
|
+
export interface ManagedWorkflowOutcome {
|
|
223
|
+
kind: ManagedWorkflowKind;
|
|
224
|
+
steps: ChainStep[];
|
|
225
|
+
}
|
|
226
|
+
|
|
80
227
|
/** Max distinguishing fragments kept in a one-line chain summary. */
|
|
81
228
|
export const CHAIN_SUMMARY_FRAGMENTS_MAX = 3;
|
|
82
229
|
|
|
@@ -87,58 +234,124 @@ export function chainKeyFragments(result: SingleResult): string[] {
|
|
|
87
234
|
return extractKeyFragments(getResultOutput(result)).slice(0, CHAIN_SUMMARY_FRAGMENTS_MAX);
|
|
88
235
|
}
|
|
89
236
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
* crash), so the delivered message stays short instead of stacking every
|
|
96
|
-
* round's raw output.
|
|
97
|
-
*/
|
|
98
|
-
export function formatChainSummary(steps: readonly ChainStep[]): string {
|
|
99
|
-
const rounds = steps.filter((step) => step.relation.startsWith("fix round")).length;
|
|
100
|
-
const last = steps[steps.length - 1];
|
|
101
|
-
const stepStatus = (step: ChainStep): string => {
|
|
102
|
-
const { result } = step;
|
|
103
|
-
if (result.agent === "reviewer") {
|
|
104
|
-
const verdict = reviewVerdict(getResultOutput(result));
|
|
105
|
-
if (verdict) return verdict.toUpperCase();
|
|
106
|
-
}
|
|
107
|
-
return isFailedResult(result) ? "failed" : "completed";
|
|
108
|
-
};
|
|
109
|
-
const lines = [
|
|
110
|
-
`## Auto-fix chain: ${Math.max(1, rounds)} round${rounds === 1 ? "" : "s"} — final ${stepStatus(last)}`,
|
|
111
|
-
"",
|
|
112
|
-
];
|
|
113
|
-
for (const step of steps) {
|
|
114
|
-
const fragments = chainKeyFragments(step.result);
|
|
115
|
-
const suffix =
|
|
116
|
-
fragments.length > 0
|
|
117
|
-
? step.result.agent === "worker"
|
|
118
|
-
? ` — changed: ${fragments.join(" · ")}`
|
|
119
|
-
: ` — ${fragments.join(" · ")}`
|
|
120
|
-
: "";
|
|
121
|
-
const id = step.runId !== undefined ? `#${step.runId} ` : "";
|
|
122
|
-
lines.push(`- ${id}${step.result.agent} · ${step.relation} · ${stepStatus(step)}${suffix}`);
|
|
237
|
+
function workflowResultStatus(result: SingleResult): string {
|
|
238
|
+
if (isFailedResult(result)) return "failed";
|
|
239
|
+
if (result.agent === "reviewer") {
|
|
240
|
+
const verdict = reviewVerdict(getResultOutput(result));
|
|
241
|
+
return verdict ? verdict.toUpperCase() : "NO_VERDICT";
|
|
123
242
|
}
|
|
243
|
+
return "completed";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function workflowStepLine(step: ChainStep): string {
|
|
247
|
+
const fragments = chainKeyFragments(step.result);
|
|
248
|
+
const writer = step.result.agent === "worker" || step.result.agent === "cleaner" || step.result.agent === "documenter";
|
|
249
|
+
const suffix = fragments.length > 0
|
|
250
|
+
? writer
|
|
251
|
+
? ` — changed: ${fragments.join(" · ")}`
|
|
252
|
+
: ` — ${fragments.join(" · ")}`
|
|
253
|
+
: "";
|
|
254
|
+
const id = step.runId !== undefined ? `#${step.runId} ` : "";
|
|
255
|
+
return `- ${id}${step.result.agent} · ${step.relation} · ${workflowResultStatus(step.result)}${suffix}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function appendWorkflowFooter(lines: string[], steps: readonly ChainStep[]): void {
|
|
124
259
|
const total = sumUsage(steps.map((step) => step.result.usage));
|
|
125
260
|
const usage = formatUsageCompact(total);
|
|
126
261
|
lines.push("", `Totals: ${steps.length} run${steps.length === 1 ? "" : "s"}${usage ? ` · ${usage}` : ""}`);
|
|
127
262
|
const ids = steps.filter((step) => step.runId !== undefined).map((step) => `#${step.runId}`);
|
|
128
263
|
lines.push(`Full per-run reports (output, usage, failed tools): subagent_status ${ids.join(" ")}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Condensed compatibility summary for a direct REVIEW_FAIL auto-fix chain. */
|
|
267
|
+
export function formatChainSummary(
|
|
268
|
+
steps: readonly ChainStep[],
|
|
269
|
+
terminalResult: SingleResult = steps[steps.length - 1]!.result,
|
|
270
|
+
): string {
|
|
271
|
+
const rounds = steps.filter((step) => step.relation.startsWith("fix round")).length;
|
|
272
|
+
const lines = [
|
|
273
|
+
`## Auto-fix chain: ${Math.max(1, rounds)} round${rounds === 1 ? "" : "s"} — final ${workflowResultStatus(terminalResult)}`,
|
|
274
|
+
"",
|
|
275
|
+
...steps.map(workflowStepLine),
|
|
276
|
+
];
|
|
277
|
+
appendWorkflowFooter(lines, steps);
|
|
278
|
+
return lines.join("\n");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** One clear final delivery for all newly managed writer/documenter workflows. */
|
|
282
|
+
export function formatManagedWorkflowSummary(
|
|
283
|
+
steps: readonly ChainStep[],
|
|
284
|
+
terminalResult: SingleResult = steps[steps.length - 1]!.result,
|
|
285
|
+
): string {
|
|
286
|
+
const route = steps.map((step) => step.result.agent).join(" → ");
|
|
287
|
+
const fixRounds = steps.filter((step) => step.relation.startsWith("fix round")).length;
|
|
288
|
+
const roundNote = fixRounds > 0 ? ` · ${fixRounds} fix round${fixRounds === 1 ? "" : "s"}` : "";
|
|
289
|
+
const lines = [
|
|
290
|
+
`## Managed workflow: ${route}${roundNote} — final ${workflowResultStatus(terminalResult)}`,
|
|
291
|
+
"",
|
|
292
|
+
...steps.map(workflowStepLine),
|
|
293
|
+
];
|
|
294
|
+
appendWorkflowFooter(lines, steps);
|
|
129
295
|
return lines.join("\n");
|
|
130
296
|
}
|
|
131
297
|
|
|
298
|
+
/** Build the first independent final gate after a top-level writer or required
|
|
299
|
+
* post-pass documentation sync. Reports carry intent; the actual pending diff
|
|
300
|
+
* remains authoritative. */
|
|
301
|
+
export function buildFinalReviewBrief(
|
|
302
|
+
initialResult: SingleResult,
|
|
303
|
+
documenterResult?: SingleResult,
|
|
304
|
+
): string {
|
|
305
|
+
const documenterSection = documenterResult
|
|
306
|
+
? [
|
|
307
|
+
``,
|
|
308
|
+
`The documenter's full sync report:`,
|
|
309
|
+
`---`,
|
|
310
|
+
getResultOutput(documenterResult),
|
|
311
|
+
`---`,
|
|
312
|
+
]
|
|
313
|
+
: [];
|
|
314
|
+
return [
|
|
315
|
+
`Fresh final gate for a managed ${initialResult.agent} workflow.`,
|
|
316
|
+
``,
|
|
317
|
+
`The top-level ${initialResult.agent}'s full report:`,
|
|
318
|
+
`---`,
|
|
319
|
+
getResultOutput(initialResult),
|
|
320
|
+
`---`,
|
|
321
|
+
...documenterSection,
|
|
322
|
+
``,
|
|
323
|
+
`Run \`git status\` and \`git diff\` and inspect the actual pending code and documentation; reports are context, not proof.`,
|
|
324
|
+
`Remain read-only. Verify correctness, regressions, tests, documentation drift, and that documenter was the last writer when it ran.`,
|
|
325
|
+
`This is an acceptance gate, not an advisory audit. End with exactly one standalone machine verdict line:`,
|
|
326
|
+
`VERDICT: REVIEW_PASS when no finding remains, otherwise VERDICT: REVIEW_FAIL.`,
|
|
327
|
+
].join("\n");
|
|
328
|
+
}
|
|
329
|
+
|
|
132
330
|
/**
|
|
133
331
|
* The re-review brief handed to the reviewer after a worker fix round. Includes
|
|
134
|
-
* the prior review
|
|
135
|
-
* rejections instead of restating findings. The
|
|
136
|
-
* rounds from ping-ponging: rule on the open
|
|
137
|
-
* this round's edits introduced, never re-open
|
|
332
|
+
* the prior review, worker report, and optional documenter report so the
|
|
333
|
+
* reviewer can adjudicate rejections instead of restating findings. The
|
|
334
|
+
* convergence contract keeps rounds from ping-ponging: rule on the open
|
|
335
|
+
* findings once, add only defects this round's edits introduced, never re-open
|
|
336
|
+
* a verified resolution.
|
|
138
337
|
*/
|
|
139
|
-
export function buildReReviewBrief(
|
|
338
|
+
export function buildReReviewBrief(
|
|
339
|
+
reviewerResult: SingleResult,
|
|
340
|
+
round: number,
|
|
341
|
+
workerResult: SingleResult,
|
|
342
|
+
documenterResult?: SingleResult,
|
|
343
|
+
): string {
|
|
140
344
|
const review = getResultOutput(reviewerResult);
|
|
141
345
|
const workerReport = getResultOutput(workerResult);
|
|
346
|
+
const documenterSection = documenterResult
|
|
347
|
+
? [
|
|
348
|
+
``,
|
|
349
|
+
`The documenter's pre-commit sync report:`,
|
|
350
|
+
`---`,
|
|
351
|
+
getResultOutput(documenterResult),
|
|
352
|
+
`---`,
|
|
353
|
+
]
|
|
354
|
+
: [];
|
|
142
355
|
return [
|
|
143
356
|
`Re-review after auto-fix round ${round}.`,
|
|
144
357
|
``,
|
|
@@ -151,6 +364,7 @@ export function buildReReviewBrief(reviewerResult: SingleResult, round: number,
|
|
|
151
364
|
`---`,
|
|
152
365
|
workerReport,
|
|
153
366
|
`---`,
|
|
367
|
+
...documenterSection,
|
|
154
368
|
``,
|
|
155
369
|
`Rule on EVERY previous finding: resolved, or still open. A finding the worker rejected must be`,
|
|
156
370
|
`adjudicated ONCE — accept the rejection unless you can concretely refute the worker's reasoning;`,
|
package/src/index.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Assembly point: builds the shared runtime and registers everything.
|
|
5
5
|
* The heavy lifting lives in focused modules:
|
|
6
|
-
* - dispatch.ts —
|
|
7
|
-
* - thread-lifecycle.ts —
|
|
6
|
+
* - dispatch.ts — tool contract, managed role policy, internal steps
|
|
7
|
+
* - thread-lifecycle.ts — stable generations, controls, final integration/delivery
|
|
8
8
|
* - tools.ts — subagent_control / subagent_wait / status / stop
|
|
9
9
|
* - announcements.ts — session-start recovery, notices, and widget install
|
|
10
10
|
* - widget.ts — active-only TUI run status
|
|
@@ -86,7 +86,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
86
86
|
enabledNames: config.enabledAgents,
|
|
87
87
|
projectTrusted: ctx.isProjectTrusted?.() === true,
|
|
88
88
|
});
|
|
89
|
-
const directive = buildDelegationDirective(agents);
|
|
89
|
+
const directive = buildDelegationDirective(agents, { maxFixRounds: config.maxFixRounds });
|
|
90
90
|
if (!directive) return undefined;
|
|
91
91
|
return { systemPrompt: `${event.systemPrompt}\n${directive}` };
|
|
92
92
|
});
|
package/src/monitor.ts
CHANGED
|
@@ -49,15 +49,15 @@ export interface RunView {
|
|
|
49
49
|
startedAt?: number;
|
|
50
50
|
/** Epoch ms when the run finished (set on "done"/"failed"). */
|
|
51
51
|
endedAt?: number;
|
|
52
|
-
/** When set, this
|
|
52
|
+
/** When set, this is an internal managed-workflow step. */
|
|
53
53
|
groupId?: string;
|
|
54
54
|
/** Human-readable role within a chain, e.g. "fix round 1" or "re-review round 1". */
|
|
55
55
|
relationLabel?: string;
|
|
56
|
-
/**
|
|
56
|
+
/** Stable owning run whose row represents the whole managed workflow. */
|
|
57
57
|
parentRunId?: number;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/** Optional
|
|
60
|
+
/** Optional metadata for documenter/reviewer/fix children of a stable parent run. */
|
|
61
61
|
export interface RunChainMeta {
|
|
62
62
|
groupId?: string;
|
|
63
63
|
relationLabel?: string;
|
|
@@ -503,6 +503,15 @@ export class MonitorStore {
|
|
|
503
503
|
this.notify();
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
/** Reflect the currently owned internal stage when a managed parent is parked
|
|
507
|
+
* or inspected between children; the stable id and original task stay intact. */
|
|
508
|
+
setAgent(id: number, agent: string): void {
|
|
509
|
+
const run = this.find(id);
|
|
510
|
+
if (!run) return;
|
|
511
|
+
run.agent = agent;
|
|
512
|
+
this.notify();
|
|
513
|
+
}
|
|
514
|
+
|
|
506
515
|
/** Update the objective shown for a queued retarget or resumed generation. */
|
|
507
516
|
setTask(id: number, task: string): void {
|
|
508
517
|
const run = this.find(id);
|
package/src/prompt.ts
CHANGED
|
@@ -11,22 +11,42 @@ function bullets(lines: readonly string[]): string {
|
|
|
11
11
|
return lines.map((line) => `- ${line}`).join("\n");
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export function buildDelegationDirective(
|
|
14
|
+
export function buildDelegationDirective(
|
|
15
|
+
agents: AgentConfig[],
|
|
16
|
+
options: { maxFixRounds?: number } = {},
|
|
17
|
+
): string {
|
|
15
18
|
if (agents.length === 0) return "";
|
|
16
19
|
|
|
17
20
|
const catalog = agents.map(formatCatalogEntry).join("\n");
|
|
18
21
|
const hasExplorer = agents.some((agent) => agent.name === "explorer");
|
|
19
22
|
const hasWorker = agents.some((agent) => agent.name === "worker");
|
|
20
23
|
const hasCleaner = agents.some((agent) => agent.name === "cleaner");
|
|
24
|
+
const hasDocumenter = agents.some((agent) => agent.name === "documenter");
|
|
21
25
|
const hasReviewer = agents.some((agent) => agent.name === "reviewer");
|
|
22
26
|
const hasMultiple = agents.length > 1;
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const autoFixEnabled = hasWorker && (options.maxFixRounds ?? 1) > 0;
|
|
28
|
+
const codeWriterNames = [
|
|
29
|
+
...(hasWorker ? ["worker"] : []),
|
|
30
|
+
...(hasCleaner ? ["cleaner"] : []),
|
|
31
|
+
];
|
|
32
|
+
const reviewedWriterNames = [
|
|
33
|
+
...codeWriterNames,
|
|
34
|
+
...(hasDocumenter ? ["documenter"] : []),
|
|
35
|
+
];
|
|
36
|
+
const automaticWriterRoute = [
|
|
37
|
+
...(hasDocumenter ? ["documenter"] : []),
|
|
38
|
+
...(hasReviewer ? ["reviewer"] : []),
|
|
39
|
+
].join(" → ");
|
|
40
|
+
const namedWorktreeTargets = [
|
|
41
|
+
...(hasWorker ? ["worker"] : []),
|
|
42
|
+
...(hasCleaner ? ["cleaner"] : []),
|
|
43
|
+
...(hasDocumenter ? ["documenter"] : []),
|
|
44
|
+
];
|
|
45
|
+
const worktreeTargets = namedWorktreeTargets.length === 0
|
|
46
|
+
? "a"
|
|
47
|
+
: namedWorktreeTargets.length === 1
|
|
48
|
+
? `${namedWorktreeTargets[0]} or another`
|
|
49
|
+
: `${namedWorktreeTargets.slice(0, -1).join(", ")}, ${namedWorktreeTargets.at(-1)}, or another`;
|
|
30
50
|
|
|
31
51
|
const dispatchRules = [
|
|
32
52
|
"Handle simple work inline with direct tools: one-line lookups, known-target reads/edits, and quick questions do not justify a child process.",
|
|
@@ -40,12 +60,17 @@ export function buildDelegationDirective(agents: AgentConfig[]): string {
|
|
|
40
60
|
: []),
|
|
41
61
|
...(hasCleaner
|
|
42
62
|
? [
|
|
43
|
-
`Use \`cleaner\` only
|
|
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.`,
|
|
64
|
+
]
|
|
65
|
+
: []),
|
|
66
|
+
...(hasDocumenter
|
|
67
|
+
? [
|
|
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; never dispatch a duplicate.` : ""} Zero edits is valid and broad mode is never inferred. It changes docs/comments only and never runtime behavior, versions, release state, or ${hasReviewer ? "the final reviewer gate" : "direct final verification"}.`,
|
|
44
69
|
]
|
|
45
70
|
: []),
|
|
46
71
|
...(hasReviewer
|
|
47
72
|
? [
|
|
48
|
-
`Use \`reviewer\` for
|
|
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.`,
|
|
49
74
|
]
|
|
50
75
|
: []),
|
|
51
76
|
"Brief every child with the complete goal, exact paths, constraints, and expected output. It has no memory of this conversation.",
|
|
@@ -55,7 +80,7 @@ export function buildDelegationDirective(agents: AgentConfig[]): string {
|
|
|
55
80
|
"Dispatch independent work in one `tasks` array and let the resumed main agent start dependent work only after prerequisites finish.",
|
|
56
81
|
]
|
|
57
82
|
: []),
|
|
58
|
-
`Filesystem isolation: single tasks default to shared${hasWorker ? "; parallel worker tasks default to detached Git worktrees" : ""}${hasCleaner ? "; cleaner defaults to shared" : ""}. Request \`isolation: "worktree"\` only for ${worktreeTargets} write-capable agent in a Git repository with committed HEAD. Read-only agents reject it, and setup/integration failure never falls back silently to shared.`,
|
|
83
|
+
`Filesystem isolation: single tasks default to shared${hasWorker ? "; parallel worker tasks default to detached Git worktrees" : ""}${hasCleaner ? "; cleaner defaults to shared" : ""}${hasDocumenter ? "; documenter defaults to shared" : ""}. Request \`isolation: "worktree"\` only for ${worktreeTargets} write-capable agent in a Git repository with committed HEAD. Read-only agents reject it, and setup/integration failure never falls back silently to shared.`,
|
|
59
84
|
"A configured child model/provider failure automatically continues the same retained session on the current main model; do not redispatch. Ordinary tool/task failures stay on the selected model.",
|
|
60
85
|
"Trust but verify: inspect actual changes/results before reporting completion.",
|
|
61
86
|
];
|
|
@@ -69,9 +94,19 @@ export function buildDelegationDirective(agents: AgentConfig[]): string {
|
|
|
69
94
|
|
|
70
95
|
const verificationRules = [
|
|
71
96
|
"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
|
+
: []),
|
|
72
102
|
...(hasReviewer
|
|
73
103
|
? [
|
|
74
|
-
|
|
104
|
+
...(hasDocumenter
|
|
105
|
+
? [
|
|
106
|
+
`A direct REVIEW_PASS is preliminary: runtime runs documenter on the pending diff, then a fresh reviewer. A direct REVIEW_FAIL ${autoFixEnabled ? "keeps auto-fix; maxFixRounds limits worker fixes only, not initial docs/review." : "cannot start fixes while worker/fix rounds are disabled."}`,
|
|
107
|
+
]
|
|
108
|
+
: []),
|
|
109
|
+
"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.",
|
|
75
110
|
"Use multi-model cross-review only when explicitly requested or for genuinely high-risk security, unsafe/FFI, persistence-migration, or concurrency changes.",
|
|
76
111
|
]
|
|
77
112
|
: []),
|
package/src/rpc-run.ts
CHANGED
|
@@ -66,10 +66,13 @@ export interface RpcSingleResult {
|
|
|
66
66
|
* model execution. This remains main-model handoff eligible even when an
|
|
67
67
|
* earlier, aborted objective left assistant text in the session. */
|
|
68
68
|
rpcPromptRejected?: boolean;
|
|
69
|
-
/**
|
|
70
|
-
* transport miss
|
|
69
|
+
/** Startup handshake failed before the initial prompt was dispatched. This
|
|
70
|
+
* transport miss is safe to retry and is not a model/provider failure. */
|
|
71
71
|
rpcStartupFailed?: boolean;
|
|
72
|
-
/** The
|
|
72
|
+
/** The parent dispatched the initial prompt command. Until its response is
|
|
73
|
+
* observed, Pi may already be running it, so startup retries must not replay it. */
|
|
74
|
+
rpcPromptDispatched?: boolean;
|
|
75
|
+
/** The child confirmed prompt acceptance (or emitted agent activity). */
|
|
73
76
|
rpcPromptAccepted?: boolean;
|
|
74
77
|
/** Pi emitted agent/turn/model/tool activity for this attempt. */
|
|
75
78
|
rpcActivity?: boolean;
|
|
@@ -415,6 +418,13 @@ interface RpcResponse {
|
|
|
415
418
|
data?: unknown;
|
|
416
419
|
}
|
|
417
420
|
|
|
421
|
+
class RpcCommandRejectedError extends Error {
|
|
422
|
+
constructor(message: string) {
|
|
423
|
+
super(message);
|
|
424
|
+
this.name = "RpcCommandRejectedError";
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
418
428
|
interface PendingRequest {
|
|
419
429
|
resolve: (response: RpcResponse) => void;
|
|
420
430
|
reject: (error: Error) => void;
|
|
@@ -642,7 +652,9 @@ export async function runRpcAgentAttempt(options: RunRpcAttemptOptions): Promise
|
|
|
642
652
|
},
|
|
643
653
|
);
|
|
644
654
|
}).then((response) => {
|
|
645
|
-
if (!response.success)
|
|
655
|
+
if (!response.success) {
|
|
656
|
+
throw new RpcCommandRejectedError(response.error || `RPC ${response.command} failed.`);
|
|
657
|
+
}
|
|
646
658
|
return response;
|
|
647
659
|
});
|
|
648
660
|
};
|
|
@@ -734,7 +746,7 @@ export async function runRpcAgentAttempt(options: RunRpcAttemptOptions): Promise
|
|
|
734
746
|
result.exitCode = 1;
|
|
735
747
|
result.stopReason = "error";
|
|
736
748
|
result.errorMessage = `Replacement prompt was rejected: ${promptError.message}`;
|
|
737
|
-
if (
|
|
749
|
+
if (promptError instanceof RpcCommandRejectedError) result.rpcPromptRejected = true;
|
|
738
750
|
finish();
|
|
739
751
|
terminate();
|
|
740
752
|
if (!closed) await processClosed.promise;
|
|
@@ -1076,7 +1088,7 @@ export async function runRpcAgentAttempt(options: RunRpcAttemptOptions): Promise
|
|
|
1076
1088
|
result.stopReason = "error";
|
|
1077
1089
|
result.errorMessage = error.message;
|
|
1078
1090
|
if (startup) result.rpcStartupFailed = true;
|
|
1079
|
-
else result.rpcPromptRejected = true;
|
|
1091
|
+
else if (error instanceof RpcCommandRejectedError) result.rpcPromptRejected = true;
|
|
1080
1092
|
finish();
|
|
1081
1093
|
terminate();
|
|
1082
1094
|
};
|
|
@@ -1091,11 +1103,15 @@ export async function runRpcAgentAttempt(options: RunRpcAttemptOptions): Promise
|
|
|
1091
1103
|
}
|
|
1092
1104
|
}
|
|
1093
1105
|
if (!finished && !initialPromptResolved && !control?.isParkRequested() && !control?.isStopRequested()) {
|
|
1106
|
+
// Pi starts the agent immediately after prompt preflight, before its
|
|
1107
|
+
// success response necessarily reaches stdout. From this point on, a
|
|
1108
|
+
// missing ACK is ambiguous and must never be recovered by replay.
|
|
1109
|
+
result.rpcPromptDispatched = true;
|
|
1094
1110
|
void send({ type: "prompt", message: asPlainTextRpcPrompt(options.prompt) }).then(
|
|
1095
1111
|
() => resolveInitialPrompt(true),
|
|
1096
1112
|
(error) => {
|
|
1097
1113
|
const promptError = error instanceof Error ? error : new Error(String(error));
|
|
1098
|
-
failBeforePrompt(promptError,
|
|
1114
|
+
failBeforePrompt(promptError, false);
|
|
1099
1115
|
},
|
|
1100
1116
|
);
|
|
1101
1117
|
}
|