@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/README.md +57 -43
- package/agents/cleaner.md +3 -3
- package/agents/documenter.md +3 -3
- package/agents/explorer.md +1 -1
- package/agents/reviewer.md +21 -13
- package/agents/worker.md +2 -2
- package/package.json +55 -55
- package/src/announcements.ts +8 -1
- package/src/config.ts +1 -1
- package/src/dispatch.ts +93 -125
- package/src/durable.ts +13 -12
- package/src/format.ts +8 -0
- package/src/prompt.ts +14 -27
- package/src/runtime.ts +15 -11
- package/src/setup.ts +112 -18
- package/src/thread-lifecycle.ts +45 -68
- package/src/tools.ts +1 -1
- package/src/workflow.ts +96 -144
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
|
|
5
|
-
* code review gate. A
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
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
|
|
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
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
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.
|
|
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
|
|
120
|
-
*
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
`
|
|
156
|
-
`
|
|
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"
|
|
165
|
-
*
|
|
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
|
|
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
|
-
}
|