@ferris1225/pi-subagents 4.1.8 → 4.1.11
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 +161 -476
- package/agents/cleaner.md +51 -52
- package/agents/documenter.md +39 -46
- package/agents/explorer.md +40 -50
- package/agents/reviewer.md +29 -31
- package/agents/worker.md +15 -34
- package/package.json +1 -1
- package/src/announcements.ts +8 -0
- package/src/background.ts +23 -5
- package/src/config.ts +4 -2
- package/src/dispatch.ts +645 -746
- package/src/durable.ts +336 -0
- package/src/format.ts +1 -8
- package/src/index.ts +7 -0
- package/src/monitor.ts +29 -30
- package/src/prompt.ts +22 -40
- package/src/rpc-run.ts +22 -228
- package/src/runtime.ts +71 -46
- package/src/session-fork.ts +7 -2
- package/src/setup.ts +25 -193
- package/src/spawn.ts +31 -28
- package/src/temp-hygiene.ts +194 -0
- package/src/thread-lifecycle.ts +1410 -1324
- package/src/tools.ts +21 -108
- package/src/widget.ts +3 -3
- package/src/workflow.ts +248 -0
- package/src/worktree.ts +144 -4
- package/src/fixloop.ts +0 -382
package/src/worktree.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Detached Git worktree isolation for write-capable sub-agents.
|
|
3
3
|
*
|
|
4
4
|
* A handle is created before a child is queued and stays owned by the logical
|
|
5
|
-
* thread across retries, model candidates,
|
|
5
|
+
* thread across retries, model candidates, and resumes. Finalize
|
|
6
6
|
* is idempotent: it records a binary patch, applies it to the original working
|
|
7
7
|
* tree without touching its index, then removes/prunes the temporary worktree.
|
|
8
8
|
* Failed integration deliberately retains both the worktree and patch.
|
|
@@ -19,7 +19,7 @@ export type IsolationMode = "shared" | "worktree";
|
|
|
19
19
|
const WORKTREE_TEMP_DIR_PREFIX = "pi-subagent-worktree-";
|
|
20
20
|
|
|
21
21
|
/** Short stable identity of one isolated worktree group (the mkdtemp suffix).
|
|
22
|
-
* Continuation
|
|
22
|
+
* Continuation generations create a fresh worktree, so the identity
|
|
23
23
|
* visibly changes when the group's filesystem boundary changes. */
|
|
24
24
|
export function worktreeGroupId(worktree: Pick<WorktreeIsolation, "tempDir">): string {
|
|
25
25
|
const base = worktree.tempDir.split(/[\\/]/).filter(Boolean).pop() ?? worktree.tempDir;
|
|
@@ -213,6 +213,28 @@ export interface WorktreeCheckpoint {
|
|
|
213
213
|
patch: Buffer;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
export interface WorktreeCheckpointRef {
|
|
217
|
+
baseHead: string;
|
|
218
|
+
commit: string;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Persistable projection of one worktree handle: enough to rebuild the
|
|
222
|
+
* handle after a reload or restart. Patch bytes are deliberately omitted —
|
|
223
|
+
* only the checkpoint commit, which lives in the shared repository object
|
|
224
|
+
* store, is needed to seed a continuation. */
|
|
225
|
+
export interface WorktreeSnapshot {
|
|
226
|
+
originalCwd: string;
|
|
227
|
+
originalRoot: string;
|
|
228
|
+
cwd: string;
|
|
229
|
+
worktreePath: string;
|
|
230
|
+
tempDir: string;
|
|
231
|
+
patchPath: string;
|
|
232
|
+
head: string;
|
|
233
|
+
integrationBaseHead: string;
|
|
234
|
+
state: "active" | "retained" | "integrated" | "no_changes";
|
|
235
|
+
checkpoint?: WorktreeCheckpointRef;
|
|
236
|
+
}
|
|
237
|
+
|
|
216
238
|
export interface WorktreeCreateOptions {
|
|
217
239
|
runner?: CommandRunner;
|
|
218
240
|
/** Test hook; production uses the OS temp directory. */
|
|
@@ -244,7 +266,12 @@ export interface WorktreeIsolation {
|
|
|
244
266
|
readonly tempDir: string;
|
|
245
267
|
readonly patchPath: string;
|
|
246
268
|
readonly head: string;
|
|
269
|
+
/** Diff base for final integration; a continuation baseline commit when
|
|
270
|
+
* the generation was seeded with already-integrated work. */
|
|
271
|
+
readonly integrationBaseHead: string;
|
|
247
272
|
readonly state: "active" | "finalizing" | WorktreeFinalizationStatus;
|
|
273
|
+
/** Checkpoint retained after finalization for continuation resumes. */
|
|
274
|
+
getContinuationCheckpoint(): WorktreeCheckpoint | undefined;
|
|
248
275
|
/** Capture the complete isolated filesystem state for a fresh continuation.
|
|
249
276
|
* The synthetic commit lets Git merge an already-committed seed without
|
|
250
277
|
* attempting to apply the same patch twice. */
|
|
@@ -410,13 +437,26 @@ class GitWorktreeIsolation implements WorktreeIsolation {
|
|
|
410
437
|
private readonly runner: CommandRunner,
|
|
411
438
|
/** May be a synthetic tree commit representing a seed that the parent
|
|
412
439
|
* checkout already contains. Finalization then integrates only new edits. */
|
|
413
|
-
|
|
414
|
-
|
|
440
|
+
readonly integrationBaseHead: string = head,
|
|
441
|
+
restored?: {
|
|
442
|
+
state: WorktreeIsolation["state"];
|
|
443
|
+
checkpoint?: WorktreeCheckpoint;
|
|
444
|
+
},
|
|
445
|
+
) {
|
|
446
|
+
if (restored) {
|
|
447
|
+
this.currentState = restored.state;
|
|
448
|
+
if (restored.checkpoint) this.continuationCheckpoint = cloneCheckpoint(restored.checkpoint);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
415
451
|
|
|
416
452
|
get state(): WorktreeIsolation["state"] {
|
|
417
453
|
return this.currentState;
|
|
418
454
|
}
|
|
419
455
|
|
|
456
|
+
getContinuationCheckpoint(): WorktreeCheckpoint | undefined {
|
|
457
|
+
return this.continuationCheckpoint ? cloneCheckpoint(this.continuationCheckpoint) : undefined;
|
|
458
|
+
}
|
|
459
|
+
|
|
420
460
|
async snapshotCheckpoint(): Promise<WorktreeCheckpoint> {
|
|
421
461
|
if (this.continuationCheckpoint) return cloneCheckpoint(this.continuationCheckpoint);
|
|
422
462
|
if (this.currentState === "no_changes") {
|
|
@@ -720,3 +760,103 @@ export async function createWorktreeIsolation(
|
|
|
720
760
|
);
|
|
721
761
|
}
|
|
722
762
|
}
|
|
763
|
+
|
|
764
|
+
/** Snapshot only states whose filesystem or repository objects still exist.
|
|
765
|
+
* Transient (`finalizing`) and discarded handles are not persistable. */
|
|
766
|
+
export function worktreeSnapshot(worktree: WorktreeIsolation): WorktreeSnapshot | undefined {
|
|
767
|
+
const state = worktree.state;
|
|
768
|
+
if (state !== "active" && state !== "retained" && state !== "integrated" && state !== "no_changes") {
|
|
769
|
+
return undefined;
|
|
770
|
+
}
|
|
771
|
+
const checkpoint = worktree.getContinuationCheckpoint();
|
|
772
|
+
return {
|
|
773
|
+
originalCwd: worktree.originalCwd,
|
|
774
|
+
originalRoot: worktree.originalRoot,
|
|
775
|
+
cwd: worktree.cwd,
|
|
776
|
+
worktreePath: worktree.worktreePath,
|
|
777
|
+
tempDir: worktree.tempDir,
|
|
778
|
+
patchPath: worktree.patchPath,
|
|
779
|
+
head: worktree.head,
|
|
780
|
+
integrationBaseHead: worktree.integrationBaseHead,
|
|
781
|
+
state,
|
|
782
|
+
...(checkpoint && checkpoint.patch.length > 0
|
|
783
|
+
? { checkpoint: { baseHead: checkpoint.baseHead, commit: checkpoint.commit } }
|
|
784
|
+
: {}),
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** Validate an untrusted persisted snapshot; null when it is unusable. */
|
|
789
|
+
export function normalizeWorktreeSnapshot(value: unknown): WorktreeSnapshot | null {
|
|
790
|
+
if (!value || typeof value !== "object") return null;
|
|
791
|
+
const raw = value as Record<string, unknown>;
|
|
792
|
+
const fields: Record<string, string> = {};
|
|
793
|
+
for (const key of [
|
|
794
|
+
"originalCwd",
|
|
795
|
+
"originalRoot",
|
|
796
|
+
"cwd",
|
|
797
|
+
"worktreePath",
|
|
798
|
+
"tempDir",
|
|
799
|
+
"patchPath",
|
|
800
|
+
"head",
|
|
801
|
+
"integrationBaseHead",
|
|
802
|
+
] as const) {
|
|
803
|
+
if (typeof raw[key] !== "string" || !raw[key]) return null;
|
|
804
|
+
fields[key] = raw[key] as string;
|
|
805
|
+
}
|
|
806
|
+
if (raw.state !== "active" && raw.state !== "retained" && raw.state !== "integrated" && raw.state !== "no_changes") {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
let checkpoint: WorktreeCheckpointRef | undefined;
|
|
810
|
+
if (raw.checkpoint && typeof raw.checkpoint === "object") {
|
|
811
|
+
const rawCheckpoint = raw.checkpoint as Record<string, unknown>;
|
|
812
|
+
if (typeof rawCheckpoint.baseHead !== "string" || !rawCheckpoint.baseHead) return null;
|
|
813
|
+
if (typeof rawCheckpoint.commit !== "string" || !rawCheckpoint.commit) return null;
|
|
814
|
+
checkpoint = { baseHead: rawCheckpoint.baseHead, commit: rawCheckpoint.commit };
|
|
815
|
+
}
|
|
816
|
+
return {
|
|
817
|
+
originalCwd: fields.originalCwd!,
|
|
818
|
+
originalRoot: fields.originalRoot!,
|
|
819
|
+
cwd: fields.cwd!,
|
|
820
|
+
worktreePath: fields.worktreePath!,
|
|
821
|
+
tempDir: fields.tempDir!,
|
|
822
|
+
patchPath: fields.patchPath!,
|
|
823
|
+
head: fields.head!,
|
|
824
|
+
integrationBaseHead: fields.integrationBaseHead!,
|
|
825
|
+
state: raw.state,
|
|
826
|
+
...(checkpoint ? { checkpoint } : {}),
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/** Rebuild a handle from a persisted snapshot. Returns undefined when the
|
|
831
|
+
* on-disk worktree that an active/retained snapshot promises is gone; settled
|
|
832
|
+
* states (integrated/no_changes) intentionally need no filesystem. The
|
|
833
|
+
* restored checkpoint carries no patch bytes — only its commit is consumed by
|
|
834
|
+
* continuation seeds. */
|
|
835
|
+
export async function restoreWorktreeIsolation(
|
|
836
|
+
snapshot: WorktreeSnapshot,
|
|
837
|
+
options: { runner?: CommandRunner } = {},
|
|
838
|
+
): Promise<WorktreeIsolation | undefined> {
|
|
839
|
+
if (
|
|
840
|
+
(snapshot.state === "active" || snapshot.state === "retained") &&
|
|
841
|
+
!existsSync(snapshot.worktreePath)
|
|
842
|
+
) {
|
|
843
|
+
return undefined;
|
|
844
|
+
}
|
|
845
|
+
return new GitWorktreeIsolation(
|
|
846
|
+
snapshot.originalCwd,
|
|
847
|
+
snapshot.originalRoot,
|
|
848
|
+
snapshot.cwd,
|
|
849
|
+
snapshot.worktreePath,
|
|
850
|
+
snapshot.tempDir,
|
|
851
|
+
snapshot.patchPath,
|
|
852
|
+
snapshot.head,
|
|
853
|
+
options.runner ?? runCommand,
|
|
854
|
+
snapshot.integrationBaseHead,
|
|
855
|
+
{
|
|
856
|
+
state: snapshot.state,
|
|
857
|
+
...(snapshot.checkpoint
|
|
858
|
+
? { checkpoint: { ...snapshot.checkpoint, patch: Buffer.alloc(0) } }
|
|
859
|
+
: {}),
|
|
860
|
+
},
|
|
861
|
+
);
|
|
862
|
+
}
|
package/src/fixloop.ts
DELETED
|
@@ -1,382 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Managed workflow policy and handoff formatting.
|
|
3
|
-
*
|
|
4
|
-
* Successful top-level worker/cleaner runs continue through an independent
|
|
5
|
-
* code review gate; bounded worker → reviewer fix rounds close its findings.
|
|
6
|
-
* Gate findings carry concrete fix instructions that the worker implements
|
|
7
|
-
* unless it can justify a sounder fix and push back; re-review adjudicates on
|
|
8
|
-
* the resulting code (open findings plus fix-introduced defects only) so the
|
|
9
|
-
* rounds converge instead of re-auditing from scratch.
|
|
10
|
-
* Reviewers classify documentation drift explicitly, so the low-cost final
|
|
11
|
-
* documenter runs only when needed (or conservatively when an older/custom
|
|
12
|
-
* reviewer omits the marker). Direct passing/failing gates use the same policy.
|
|
13
|
-
* Top-level documenters are explicit standalone writing tasks. Internal steps
|
|
14
|
-
* are launched by dispatch directly, so they never re-enter this policy or wake
|
|
15
|
-
* the main agent mid-chain.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { isWriteCapableAgent, type AgentConfig } from "./agents.ts";
|
|
19
|
-
import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
|
|
20
|
-
import { formatUsageCompact, sumUsage } from "./monitor.ts";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Worker fixes allowed after REVIEW_FAIL. Each fix is followed by a reviewer
|
|
24
|
-
* re-review; this cap does not suppress the post-writer review gate or its
|
|
25
|
-
* conditional/reviewer-disabled documentation fallback.
|
|
26
|
-
*/
|
|
27
|
-
export const MAX_FIX_ROUNDS = 2;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Whether a completed result should trigger the auto-fix loop instead of being
|
|
31
|
-
* delivered to the main agent. Only a REVIEW_FAIL verdict from a healthy
|
|
32
|
-
* reviewer run counts; failed processes and passing reviews are delivered
|
|
33
|
-
* normally. Loop-internal re-review results never reach this path (they are
|
|
34
|
-
* awaited inside the loop, not delivered through the completion flow).
|
|
35
|
-
*/
|
|
36
|
-
export function shouldTriggerFixLoop(result: SingleResult): boolean {
|
|
37
|
-
if (result.agent !== "reviewer") return false;
|
|
38
|
-
if (isFailedResult(result)) return false;
|
|
39
|
-
// A dispatch crash (spawn infra, delivery API, ...) is never a real review
|
|
40
|
-
// verdict: its output is an error message plus whatever partial text the
|
|
41
|
-
// child happened to emit, which could end in a stray `VERDICT: REVIEW_FAIL`.
|
|
42
|
-
// Guard explicitly in addition to isFailedResult so the intent is clear and
|
|
43
|
-
// a future change to isFailedResult can never let a crashed reviewer start a
|
|
44
|
-
// phantom auto-fix chain.
|
|
45
|
-
if (result.dispatchFailed) return false;
|
|
46
|
-
return reviewVerdict(getResultOutput(result)) === "fail";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface WorkflowAgentAvailability {
|
|
50
|
-
worker: boolean;
|
|
51
|
-
cleaner: boolean;
|
|
52
|
-
documenter: boolean;
|
|
53
|
-
reviewer: boolean;
|
|
54
|
-
writer: boolean;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function workflowAgentAvailability(
|
|
58
|
-
agents: readonly Pick<AgentConfig, "name" | "tools">[],
|
|
59
|
-
): WorkflowAgentAvailability {
|
|
60
|
-
const names = new Set(agents.map((agent) => agent.name));
|
|
61
|
-
return {
|
|
62
|
-
worker: names.has("worker"),
|
|
63
|
-
cleaner: names.has("cleaner"),
|
|
64
|
-
documenter: names.has("documenter"),
|
|
65
|
-
reviewer: names.has("reviewer"),
|
|
66
|
-
writer: agents.some(isWriteCapableAgent),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export type DocumentationDisposition = "clean" | "needed";
|
|
71
|
-
|
|
72
|
-
/** Only the last standalone documentation disposition line counts. Inline
|
|
73
|
-
* examples and prose are ignored so a prompt echo cannot suppress a needed
|
|
74
|
-
* conservative sync. */
|
|
75
|
-
export function documentationDisposition(output: string): DocumentationDisposition | undefined {
|
|
76
|
-
const lines = output.split("\n");
|
|
77
|
-
for (let index = lines.length - 1; index >= 0; index--) {
|
|
78
|
-
const match = /^\s*DOCUMENTATION:\s*(CLEAN|NEEDED)\s*$/i.exec(lines[index]);
|
|
79
|
-
if (match) return match[1].toUpperCase() === "CLEAN" ? "clean" : "needed";
|
|
80
|
-
}
|
|
81
|
-
return undefined;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export type ManagedWorkflowKind = "auto-fix" | "post-writer" | "review-pass-sync";
|
|
85
|
-
|
|
86
|
-
export interface ManagedWorkflowPlan {
|
|
87
|
-
kind: ManagedWorkflowKind;
|
|
88
|
-
initialRelation: string;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Conservative pre-run check used to reserve one shared-repository lane
|
|
92
|
-
* around a complete writer workflow or a reviewer that needs a stable diff.
|
|
93
|
-
* The actual result is classified again by getManagedWorkflowPlan before a
|
|
94
|
-
* downstream child starts. */
|
|
95
|
-
export function canStartManagedWorkflow(
|
|
96
|
-
agent: Pick<AgentConfig, "name" | "tools">,
|
|
97
|
-
availability: WorkflowAgentAvailability,
|
|
98
|
-
): boolean {
|
|
99
|
-
// Every shared write-capable role—including custom agents—owns the repository
|
|
100
|
-
// lane even when no downstream role is enabled. Otherwise its edits can race
|
|
101
|
-
// a managed writer's documentation snapshot.
|
|
102
|
-
if (isWriteCapableAgent(agent)) return true;
|
|
103
|
-
if (agent.name === "reviewer") {
|
|
104
|
-
// Hold a stable diff snapshot against every discoverable writer even when
|
|
105
|
-
// this review is advisory. Classification happens only after the read-only
|
|
106
|
-
// child returns, too late to acquire the lane safely.
|
|
107
|
-
return availability.writer;
|
|
108
|
-
}
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Classify only healthy top-level results. In particular, a reviewer without a
|
|
113
|
-
* machine verdict is advisory and cannot start any write-capable child. */
|
|
114
|
-
export function getManagedWorkflowPlan(
|
|
115
|
-
result: SingleResult,
|
|
116
|
-
availability: WorkflowAgentAvailability,
|
|
117
|
-
): ManagedWorkflowPlan | undefined {
|
|
118
|
-
if (result.parked || result.dispatchFailed || isFailedResult(result)) return undefined;
|
|
119
|
-
if (result.agent === "worker" || result.agent === "cleaner") {
|
|
120
|
-
if (!availability.documenter && !availability.reviewer) return undefined;
|
|
121
|
-
return {
|
|
122
|
-
kind: "post-writer",
|
|
123
|
-
initialRelation: result.agent === "cleaner" ? "initial cleanup" : "initial implementation",
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
// A top-level documenter is already an explicit docs/comments write task. It
|
|
127
|
-
// owns the writer lane but delivers directly without an automatic code gate.
|
|
128
|
-
if (result.agent === "documenter") return undefined;
|
|
129
|
-
if (result.agent !== "reviewer") return undefined;
|
|
130
|
-
|
|
131
|
-
const output = getResultOutput(result);
|
|
132
|
-
const verdict = reviewVerdict(output);
|
|
133
|
-
// The pass stands as the code gate. Run the conditional documenter only for
|
|
134
|
-
// explicit drift or when an older/custom reviewer omitted the marker.
|
|
135
|
-
if (
|
|
136
|
-
verdict === "pass" &&
|
|
137
|
-
availability.documenter &&
|
|
138
|
-
documentationDisposition(output) !== "clean"
|
|
139
|
-
) {
|
|
140
|
-
return { kind: "review-pass-sync", initialRelation: "pre-documentation review" };
|
|
141
|
-
}
|
|
142
|
-
if (verdict === "fail" && availability.worker && shouldTriggerFixLoop(result)) {
|
|
143
|
-
return { kind: "auto-fix", initialRelation: "initial review" };
|
|
144
|
-
}
|
|
145
|
-
return undefined;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Build the worker task brief for one fix round from a reviewer's findings.
|
|
150
|
-
* The worker gets the full review text — findings plus their fix instructions
|
|
151
|
-
* — and closes every finding either by implementing the instruction or by
|
|
152
|
-
* shipping a sounder fix with an explicit per-finding pushback.
|
|
153
|
-
*/
|
|
154
|
-
export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, maxRounds: number): string {
|
|
155
|
-
const review = getResultOutput(reviewerResult);
|
|
156
|
-
const remaining = maxRounds - round;
|
|
157
|
-
return [
|
|
158
|
-
`Auto-fix round ${round} of ${maxRounds} (triggered by a failed review).`,
|
|
159
|
-
``,
|
|
160
|
-
`A reviewer ran in an isolated context and returned REQUEST_CHANGES. Its full report:`,
|
|
161
|
-
`---`,
|
|
162
|
-
review,
|
|
163
|
-
`---`,
|
|
164
|
-
``,
|
|
165
|
-
`Each finding in the reviewer's report carries a fix instruction. Close EVERY finding — there is no severity triage; all of them get fixed.`,
|
|
166
|
-
`You own the fix: when an instruction is factually wrong, clearly out of scope, or a sounder fix exists, implement YOUR fix instead,`,
|
|
167
|
-
`then push back explicitly per finding — cite it, refute the instruction's reasoning, and state what you shipped instead.`,
|
|
168
|
-
`A pushback without a working alternative or concrete reasoning will be re-opened.`,
|
|
169
|
-
`Do NOT refactor unrelated code beyond what the findings require.`,
|
|
170
|
-
`Synchronize any existing README/docs/examples/comments directly affected by your fixes; do not broaden into standalone documentation maintenance.`,
|
|
171
|
-
`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.`,
|
|
172
|
-
`After editing, run the project's format/build/tests when they exist and report`,
|
|
173
|
-
`exactly what you changed (paths + short rationale) plus any pushback, so a reviewer can verify.`,
|
|
174
|
-
remaining > 0
|
|
175
|
-
? `A reviewer will re-review your changes automatically after you finish.`
|
|
176
|
-
: `This is the last auto-fix round; the workflow conditionally runs any needed final documentation sync and then delivers.`,
|
|
177
|
-
].join("\n");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** Build the single documentation handoff selected after the review gate
|
|
181
|
-
* settles or as the reviewer-disabled fallback. The last writer's report
|
|
182
|
-
* (top-level writer or final fix-round worker) and the terminal gate review are
|
|
183
|
-
* leads; the pending diff stays authoritative. At most one of the two is
|
|
184
|
-
* undefined in every managed flow. */
|
|
185
|
-
export function buildFinalDocumenterBrief(
|
|
186
|
-
lastWriterResult?: SingleResult,
|
|
187
|
-
finalReviewResult?: SingleResult,
|
|
188
|
-
): string {
|
|
189
|
-
const reportSections = [
|
|
190
|
-
...(lastWriterResult
|
|
191
|
-
? [
|
|
192
|
-
`The last writer (${lastWriterResult.agent}) reported:`,
|
|
193
|
-
`---`,
|
|
194
|
-
getResultOutput(lastWriterResult),
|
|
195
|
-
`---`,
|
|
196
|
-
``,
|
|
197
|
-
]
|
|
198
|
-
: []),
|
|
199
|
-
...(finalReviewResult
|
|
200
|
-
? [
|
|
201
|
-
`The final gate review reported:`,
|
|
202
|
-
`---`,
|
|
203
|
-
getResultOutput(finalReviewResult),
|
|
204
|
-
`---`,
|
|
205
|
-
``,
|
|
206
|
-
]
|
|
207
|
-
: []),
|
|
208
|
-
];
|
|
209
|
-
return [
|
|
210
|
-
`Final documentation sync: the review gate settled and you are the last managed stage before delivery.`,
|
|
211
|
-
``,
|
|
212
|
-
...reportSections,
|
|
213
|
-
`Inspect the actual git diff (the complete pending diff) and relevant implementation; the reports are only leads.`,
|
|
214
|
-
`Apply every documentation note the reviews recorded, then synchronize stale README/docs, examples, API comments, docstrings, and explanatory comments with the behavior that will be committed.`,
|
|
215
|
-
`Change documentation surfaces only; never alter runtime behavior or tests to make prose true.`,
|
|
216
|
-
`Make zero edits when the diff creates no documentation drift.`,
|
|
217
|
-
`Do NOT commit, push, publish, tag, or release; do not bump versions. The parent workflow delivers directly after you; no fresh reviewer runs.`,
|
|
218
|
-
`Report exact documentation/comment paths changed, or state explicitly that no sync was needed.`,
|
|
219
|
-
].join("\n");
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* One step of an auto-fix chain as delivered: the run id (so the condensed
|
|
224
|
-
* summary can point at per-run detail via subagent_status), the result, and
|
|
225
|
-
* the human-readable role within the chain ("initial review", "fix round 1",
|
|
226
|
-
* "re-review round 2"). runId is optional only for synthetic steps that never
|
|
227
|
-
* spawned a child.
|
|
228
|
-
*/
|
|
229
|
-
export interface ChainStep {
|
|
230
|
-
runId?: number;
|
|
231
|
-
result: SingleResult;
|
|
232
|
-
relation: string;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export interface ManagedWorkflowOutcome {
|
|
236
|
-
kind: ManagedWorkflowKind;
|
|
237
|
-
steps: ChainStep[];
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function workflowResultStatus(result: SingleResult): string {
|
|
241
|
-
if (isFailedResult(result)) return "failed";
|
|
242
|
-
if (result.agent === "reviewer") {
|
|
243
|
-
const verdict = reviewVerdict(getResultOutput(result));
|
|
244
|
-
return verdict ? verdict.toUpperCase() : "NO_VERDICT";
|
|
245
|
-
}
|
|
246
|
-
return "completed";
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function workflowStepLine(step: ChainStep): string {
|
|
250
|
-
const id = step.runId !== undefined ? `#${step.runId} ` : "";
|
|
251
|
-
return `- ${id}${step.result.agent} · ${step.relation} · ${workflowResultStatus(step.result)}`;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function appendWorkflowFooter(lines: string[], steps: readonly ChainStep[]): void {
|
|
255
|
-
const total = sumUsage(steps.map((step) => step.result.usage));
|
|
256
|
-
const usage = formatUsageCompact(total);
|
|
257
|
-
lines.push("", `Totals: ${steps.length} run${steps.length === 1 ? "" : "s"}${usage ? ` · ${usage}` : ""}`);
|
|
258
|
-
const ids = steps.filter((step) => step.runId !== undefined).map((step) => `#${step.runId}`);
|
|
259
|
-
lines.push(`Per-run details: subagent_status ${ids.join(" ")}`);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function formatWorkflowSummary(title: string, steps: readonly ChainStep[]): string {
|
|
263
|
-
const lines = [title, "", ...steps.map(workflowStepLine)];
|
|
264
|
-
appendWorkflowFooter(lines, steps);
|
|
265
|
-
return lines.join("\n");
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/** Condensed compatibility summary for a direct REVIEW_FAIL auto-fix chain. */
|
|
269
|
-
export function formatChainSummary(
|
|
270
|
-
steps: readonly ChainStep[],
|
|
271
|
-
terminalResult: SingleResult = steps[steps.length - 1]!.result,
|
|
272
|
-
): string {
|
|
273
|
-
const rounds = steps.filter((step) => step.relation.startsWith("fix round")).length;
|
|
274
|
-
return formatWorkflowSummary(
|
|
275
|
-
`## Auto-fix chain: ${Math.max(1, rounds)} round${rounds === 1 ? "" : "s"} — final ${workflowResultStatus(terminalResult)}`,
|
|
276
|
-
steps,
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/** One clear final delivery for post-writer and direct reviewer → documenter workflows. */
|
|
281
|
-
export function formatManagedWorkflowSummary(
|
|
282
|
-
steps: readonly ChainStep[],
|
|
283
|
-
terminalResult: SingleResult = steps[steps.length - 1]!.result,
|
|
284
|
-
): string {
|
|
285
|
-
const route = steps.map((step) => step.result.agent).join(" → ");
|
|
286
|
-
const fixRounds = steps.filter((step) => step.relation.startsWith("fix round")).length;
|
|
287
|
-
const roundNote = fixRounds > 0 ? ` · ${fixRounds} fix round${fixRounds === 1 ? "" : "s"}` : "";
|
|
288
|
-
return formatWorkflowSummary(
|
|
289
|
-
`## Managed workflow: ${route}${roundNote} — final ${workflowResultStatus(terminalResult)}`,
|
|
290
|
-
steps,
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export interface GateBriefOptions {
|
|
295
|
-
/** A conditional final documenter is available after the gate settles.
|
|
296
|
-
* Documentation drift is then routed to it as non-gating notes instead of
|
|
297
|
-
* failing the code gate. */
|
|
298
|
-
documenterPending: boolean;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/** Build the code gate that runs directly after a top-level writer, before any
|
|
302
|
-
* documentation. Reports carry intent; the actual pending diff remains
|
|
303
|
-
* authoritative. */
|
|
304
|
-
export function buildFinalReviewBrief(
|
|
305
|
-
initialResult: SingleResult,
|
|
306
|
-
options: GateBriefOptions,
|
|
307
|
-
): string {
|
|
308
|
-
return [
|
|
309
|
-
`Fresh code gate for a managed ${initialResult.agent} workflow.`,
|
|
310
|
-
``,
|
|
311
|
-
`The top-level ${initialResult.agent}'s full report:`,
|
|
312
|
-
`---`,
|
|
313
|
-
getResultOutput(initialResult),
|
|
314
|
-
`---`,
|
|
315
|
-
``,
|
|
316
|
-
`Run \`git status\` and \`git diff\` and inspect the actual pending code; the report is context, not proof.`,
|
|
317
|
-
`Remain read-only. Verify correctness, regressions, and tests.`,
|
|
318
|
-
`Attach a concrete fix instruction to EVERY gate finding: what to change, where, and how to verify the fix.`,
|
|
319
|
-
`A worker will implement your instructions unless it can justify a sounder fix and push back, so make each instruction specific enough to act on.`,
|
|
320
|
-
...(options.documenterPending
|
|
321
|
-
? [
|
|
322
|
-
`A conditional documentation sync is available AFTER this gate, so documentation drift is not a code-gate finding.`,
|
|
323
|
-
`If documentation is stale, add a separate short "## Documentation notes" list and emit the standalone line`,
|
|
324
|
-
`DOCUMENTATION: NEEDED. If no documentation update is needed, emit DOCUMENTATION: CLEAN instead.`,
|
|
325
|
-
`Always emit exactly one of those standalone documentation lines; fail the gate only for code or test findings.`,
|
|
326
|
-
]
|
|
327
|
-
: [
|
|
328
|
-
`No documenter is pending, so documentation drift is an ordinary gate finding.`,
|
|
329
|
-
]),
|
|
330
|
-
`This is an acceptance gate, not an advisory audit. End with exactly one standalone machine verdict line:`,
|
|
331
|
-
`VERDICT: REVIEW_PASS when no finding remains, otherwise VERDICT: REVIEW_FAIL.`,
|
|
332
|
-
].join("\n");
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* The re-review brief handed to the reviewer after a worker fix round. Includes
|
|
337
|
-
* the prior review and worker report so the reviewer can adjudicate pushback
|
|
338
|
-
* instead of restating findings. The convergence contract keeps rounds from
|
|
339
|
-
* ping-ponging: judge the resulting code (not instruction obedience), rule on
|
|
340
|
-
* the open findings once, add only defects this round's edits introduced,
|
|
341
|
-
* never re-open a verified resolution.
|
|
342
|
-
*/
|
|
343
|
-
export function buildReReviewBrief(
|
|
344
|
-
reviewerResult: SingleResult,
|
|
345
|
-
round: number,
|
|
346
|
-
workerResult: SingleResult,
|
|
347
|
-
options: GateBriefOptions = { documenterPending: false },
|
|
348
|
-
): string {
|
|
349
|
-
const review = getResultOutput(reviewerResult);
|
|
350
|
-
const workerReport = getResultOutput(workerResult);
|
|
351
|
-
return [
|
|
352
|
-
`Re-review after auto-fix round ${round}.`,
|
|
353
|
-
``,
|
|
354
|
-
`The previous review (REQUEST_CHANGES) found these issues:`,
|
|
355
|
-
`---`,
|
|
356
|
-
review,
|
|
357
|
-
`---`,
|
|
358
|
-
``,
|
|
359
|
-
`The worker's report (what it changed, plus any pushback where it replaced your fix instruction with its own fix):`,
|
|
360
|
-
`---`,
|
|
361
|
-
workerReport,
|
|
362
|
-
`---`,
|
|
363
|
-
``,
|
|
364
|
-
`Rule on EVERY previous finding: resolved, or still open. Judge the code as it now stands — a finding is`,
|
|
365
|
-
`resolved when the pending diff fixes it soundly, whether or not the worker followed your fix instruction.`,
|
|
366
|
-
`A finding the worker pushed back on must be adjudicated ONCE — accept the worker's fix unless you can`,
|
|
367
|
-
`concretely refute its reasoning; never simply restate the finding for another round.`,
|
|
368
|
-
`Run \`git diff\` to see what changed, then add NEW findings only for defects this round's edits introduced or exposed.`,
|
|
369
|
-
`Re-review never opens findings unrelated to this round's edits; issues the earlier review missed belong to a fresh gate.`,
|
|
370
|
-
`Do NOT re-open a finding you verified as resolved.`,
|
|
371
|
-
...(options.documenterPending
|
|
372
|
-
? [
|
|
373
|
-
`Carry unresolved "## Documentation notes" forward and add any newly exposed drift there; documentation drift is not a code-gate finding.`,
|
|
374
|
-
`Emit exactly one standalone documentation disposition line: DOCUMENTATION: NEEDED when that notes section is required, otherwise DOCUMENTATION: CLEAN.`,
|
|
375
|
-
]
|
|
376
|
-
: [
|
|
377
|
-
`No documenter is pending, so unresolved documentation drift remains an ordinary gate finding.`,
|
|
378
|
-
]),
|
|
379
|
-
`REQUEST_CHANGES only while an open finding remains; otherwise APPROVE.`,
|
|
380
|
-
`End with your machine-readable verdict line as usual (VERDICT: REVIEW_PASS / REVIEW_FAIL).`,
|
|
381
|
-
].join("\n");
|
|
382
|
-
}
|