@ferris1225/pi-subagents 4.1.13 → 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 +51 -40
- 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 +1 -1
- 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 +3 -3
- package/src/thread-lifecycle.ts +45 -68
- package/src/tools.ts +1 -1
- package/src/workflow.ts +96 -144
package/src/thread-lifecycle.ts
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
failedStartResult,
|
|
43
43
|
formatCompletionBlock,
|
|
44
44
|
modelLevelTakeoverNote,
|
|
45
|
+
reviewFailFollowUpNote,
|
|
45
46
|
queuedResult,
|
|
46
47
|
} from "./format.ts";
|
|
47
48
|
import {
|
|
@@ -220,16 +221,19 @@ export function ownsResumeReservation(
|
|
|
220
221
|
);
|
|
221
222
|
}
|
|
222
223
|
|
|
223
|
-
/** Fire-and-forget durable checkpoint
|
|
224
|
-
*
|
|
224
|
+
/** Fire-and-forget durable checkpoint. Parked threads stay resumable across
|
|
225
|
+
* reloads; a settled thread drops its record so the manifest only exists
|
|
226
|
+
* while unfinished work needs it. The live session keeps working when the
|
|
227
|
+
* manifest is unwritable; only cross-reload resume is degraded. */
|
|
225
228
|
export function persistThreadCheckpoint(
|
|
226
229
|
runtime: SubagentRuntime,
|
|
227
230
|
thread: SubagentThread,
|
|
228
231
|
state: "parked" | "completed" | "failed",
|
|
229
232
|
): void {
|
|
230
|
-
|
|
231
|
-
(
|
|
232
|
-
|
|
233
|
+
const write = state === "parked"
|
|
234
|
+
? upsertThreadRecord(runtime.configPath, threadRecordFromThread(thread, state))
|
|
235
|
+
: removeThreadRecord(runtime.configPath, thread.id);
|
|
236
|
+
void write.catch(() => undefined);
|
|
233
237
|
}
|
|
234
238
|
|
|
235
239
|
const WORKTREE_ISOLATION_INSTRUCTIONS =
|
|
@@ -246,27 +250,6 @@ export function isWorktreeCapableAgent(agent: AgentConfig): boolean {
|
|
|
246
250
|
return isWriteCapableAgent(agent);
|
|
247
251
|
}
|
|
248
252
|
|
|
249
|
-
/** A direct reviewer otherwise cannot infer enabled-role availability from its
|
|
250
|
-
* isolated task. Managed internal gates receive the same contract in their
|
|
251
|
-
* generated briefs. Advisory reviews still emit neither machine marker. */
|
|
252
|
-
function withEnabledDocumenterReviewContract(agent: AgentConfig): AgentConfig {
|
|
253
|
-
return {
|
|
254
|
-
...agent,
|
|
255
|
-
systemPrompt: `${agent.systemPrompt.trimEnd()}\n\nRuntime workflow context: documenter is enabled. In gate reviews, documentation drift is non-gating: emit DOCUMENTATION: NEEDED with ## Documentation notes, or DOCUMENTATION: CLEAN when no sync is needed. Advisory reviews still emit neither VERDICT nor DOCUMENTATION markers.`.trim(),
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/** The dispatcher explicitly requested a report-only review: forbid the gate
|
|
260
|
-
* markers at the source and (in dispatch) refuse to chain on them anyway. */
|
|
261
|
-
function withAdvisoryReviewContract(agent: AgentConfig): AgentConfig {
|
|
262
|
-
return {
|
|
263
|
-
...agent,
|
|
264
|
-
systemPrompt: `${agent.systemPrompt.trimEnd()}
|
|
265
|
-
|
|
266
|
-
Runtime workflow context: this dispatch is advisory. Report findings only; do not emit VERDICT or DOCUMENTATION markers — the runtime will not act on them.`.trim(),
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
253
|
export interface DispatchEnvironment {
|
|
271
254
|
ctx: ExtensionContext;
|
|
272
255
|
config: SubagentsConfig;
|
|
@@ -296,7 +279,6 @@ export interface StartBackgroundOptions {
|
|
|
296
279
|
environment?: DispatchEnvironment;
|
|
297
280
|
seed?: SessionSeed;
|
|
298
281
|
resumeReservation?: ResumeReservation;
|
|
299
|
-
advisoryReview?: boolean;
|
|
300
282
|
}
|
|
301
283
|
|
|
302
284
|
export type StartBackgroundInternal = (
|
|
@@ -426,15 +408,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
426
408
|
if (!discoveredAgent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
|
|
427
409
|
const resolveLiveAgentTools = (candidate: AgentConfig): AgentConfig =>
|
|
428
410
|
resolveAgentTools({ ...candidate, tools: discoveredAgent.tools }, runtime.getActiveTools());
|
|
429
|
-
const
|
|
430
|
-
const advisoryReview = startOptions.advisoryReview ?? existingThread?.advisoryReview ?? false;
|
|
431
|
-
const agent = agentName === "reviewer"
|
|
432
|
-
? advisoryReview
|
|
433
|
-
? withAdvisoryReviewContract(resolvedAgent)
|
|
434
|
-
: runAgents.some((candidate) => candidate.name === "documenter")
|
|
435
|
-
? withEnabledDocumenterReviewContract(resolvedAgent)
|
|
436
|
-
: resolvedAgent
|
|
437
|
-
: resolvedAgent;
|
|
411
|
+
const agent = resolveLiveAgentTools(discoveredAgent);
|
|
438
412
|
if (isolation === "worktree" && !isWorktreeCapableAgent(agent)) {
|
|
439
413
|
return {
|
|
440
414
|
...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as worker, cleaner, or documenter.`),
|
|
@@ -528,7 +502,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
528
502
|
thread.executionCwd = executionCwd;
|
|
529
503
|
thread.thinkingLevel = thinkingLevel;
|
|
530
504
|
thread.isolation = isolation;
|
|
531
|
-
thread.advisoryReview = advisoryReview;
|
|
532
505
|
thread.worktree = worktree;
|
|
533
506
|
thread.state = "queued";
|
|
534
507
|
thread.control = control;
|
|
@@ -552,7 +525,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
552
525
|
executionCwd,
|
|
553
526
|
thinkingLevel,
|
|
554
527
|
isolation,
|
|
555
|
-
advisoryReview,
|
|
556
528
|
worktree,
|
|
557
529
|
state: "queued",
|
|
558
530
|
control,
|
|
@@ -670,13 +642,13 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
670
642
|
if (thread.lifecycleOperation === "stop") return;
|
|
671
643
|
|
|
672
644
|
// A shutdown can win in the microtask gap after the top-level RPC
|
|
673
|
-
// settles. Do not launch an obsolete
|
|
645
|
+
// settles. Do not launch an obsolete downstream gate or replace the
|
|
674
646
|
// stable top-level session with an aborted downstream attempt.
|
|
675
647
|
if (backgroundSignal.aborted || lifecycleInterrupted() || !runtime.sessionActive) return;
|
|
676
648
|
|
|
677
649
|
if (thread.retireOnSettle) runtime.retireThreadSession(thread);
|
|
678
650
|
let workflowOutcome: ManagedWorkflowOutcome | undefined;
|
|
679
|
-
const workflowPlan = getManagedWorkflowPlan(result, workflowAvailability
|
|
651
|
+
const workflowPlan = getManagedWorkflowPlan(result, workflowAvailability);
|
|
680
652
|
if (workflowPlan && runtime.sessionActive) {
|
|
681
653
|
// The continuation is runtime-initiated (gate review,
|
|
682
654
|
// documentation sync): release this generation's
|
|
@@ -801,6 +773,9 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
801
773
|
if (needsFullFinal) {
|
|
802
774
|
block += `\n\n${formatCompletionBlock(result, runConfig.maxResultLines, originalCwd)}`;
|
|
803
775
|
}
|
|
776
|
+
if (finalVerdict === "fail") {
|
|
777
|
+
block += `\n\n${reviewFailFollowUpNote()}`;
|
|
778
|
+
}
|
|
804
779
|
if (modelLevel) block += `\n\n${modelLevelTakeoverNote(result, { runId })}`;
|
|
805
780
|
runtime.sendCompletionGroup([{
|
|
806
781
|
agent: `managed workflow (${result.agent})`,
|
|
@@ -816,7 +791,9 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
816
791
|
agent: result.agent,
|
|
817
792
|
block: modelLevel
|
|
818
793
|
? `${formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd)}\n\n${modelLevelTakeoverNote(result, { runId })}`
|
|
819
|
-
:
|
|
794
|
+
: result.agent === "reviewer" && reviewVerdict(getResultOutput(result)) === "fail"
|
|
795
|
+
? `${formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd)}\n\n${reviewFailFollowUpNote()}`
|
|
796
|
+
: formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd),
|
|
820
797
|
triggerTurn: completionTriggersTurn(result, runConfig.notifyOnReviewPass),
|
|
821
798
|
usage: result.usage,
|
|
822
799
|
};
|
|
@@ -1290,7 +1267,6 @@ function createRestoredThread(
|
|
|
1290
1267
|
executionCwd: record.executionCwd,
|
|
1291
1268
|
...(record.thinkingLevel ? { thinkingLevel: record.thinkingLevel as ThinkingLevel } : {}),
|
|
1292
1269
|
isolation: record.isolation,
|
|
1293
|
-
advisoryReview: false,
|
|
1294
1270
|
worktree,
|
|
1295
1271
|
state,
|
|
1296
1272
|
control: new RpcRunControl(record.task, record.generation),
|
|
@@ -1320,15 +1296,20 @@ function createRestoredThread(
|
|
|
1320
1296
|
return thread;
|
|
1321
1297
|
}
|
|
1322
1298
|
|
|
1323
|
-
/** Rebuild parked
|
|
1324
|
-
* or restart. Orphaned children recorded by the previous process are
|
|
1325
|
-
* first; records whose retained session vanished
|
|
1326
|
-
*
|
|
1299
|
+
/** Rebuild interrupted (parked) threads from the durable manifest after a
|
|
1300
|
+
* reload or restart. Orphaned children recorded by the previous process are
|
|
1301
|
+
* killed first; records whose retained session vanished — and settled records
|
|
1302
|
+
* left by older versions, which hold no work worth resuming — drop out with
|
|
1303
|
+
* their artifacts. Returns the restored run ids. */
|
|
1327
1304
|
export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<number[]> {
|
|
1328
1305
|
const records = await readThreadRecords(runtime.configPath);
|
|
1329
1306
|
const restoredIds: number[] = [];
|
|
1330
1307
|
for (const record of records) {
|
|
1331
1308
|
if (runtime.threads.has(record.runId) || monitor.findRun(record.runId)) continue;
|
|
1309
|
+
if (record.state !== "parked") {
|
|
1310
|
+
await discardRestoredRecord(runtime, record);
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1332
1313
|
// A child orphaned by reload/crash may still hold the retained session.
|
|
1333
1314
|
// The on-disk session checkpoint is what survives; kill the writer.
|
|
1334
1315
|
for (const pid of record.childPids) {
|
|
@@ -1348,33 +1329,29 @@ export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<n
|
|
|
1348
1329
|
// A worktree thread whose isolated filesystem is gone cannot continue its
|
|
1349
1330
|
// isolation invariant; surface it as failed instead of pretending.
|
|
1350
1331
|
const state: ThreadState = worktree
|
|
1351
|
-
?
|
|
1332
|
+
? "parked"
|
|
1352
1333
|
: record.isolation === "worktree" && record.worktree
|
|
1353
1334
|
? "failed"
|
|
1354
|
-
:
|
|
1335
|
+
: "parked";
|
|
1355
1336
|
const thread = createRestoredThread(runtime, record, worktree, state);
|
|
1356
1337
|
runtime.threads.set(record.runId, thread);
|
|
1357
1338
|
runtime.sessionDirs.add(record.sessionDir!);
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
});
|
|
1375
|
-
} else if (thread.lastResult) {
|
|
1376
|
-
runtime.registerRunResult(record.runId, thread.lastResult);
|
|
1377
|
-
}
|
|
1339
|
+
monitor.restoreRun({
|
|
1340
|
+
id: record.runId,
|
|
1341
|
+
agent: record.agentName,
|
|
1342
|
+
task: record.task,
|
|
1343
|
+
status: "parked",
|
|
1344
|
+
elapsedMs: record.elapsedMs,
|
|
1345
|
+
isolation: record.isolation,
|
|
1346
|
+
...(record.worktree
|
|
1347
|
+
? {
|
|
1348
|
+
integrationStatus: record.worktree.state === "active"
|
|
1349
|
+
? ("pending" as const)
|
|
1350
|
+
: record.worktree.state,
|
|
1351
|
+
...(worktree ? { worktreeId: worktreeGroupId(worktree) } : {}),
|
|
1352
|
+
}
|
|
1353
|
+
: {}),
|
|
1354
|
+
});
|
|
1378
1355
|
restoredIds.push(record.runId);
|
|
1379
1356
|
}
|
|
1380
1357
|
return restoredIds;
|
package/src/tools.ts
CHANGED
|
@@ -106,7 +106,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
106
106
|
const context = hadRetainedSession
|
|
107
107
|
? "the same retained session and prior context are preserved"
|
|
108
108
|
: "no prior child session existed, so only the logical run and objective are continued";
|
|
109
|
-
return { content: [{ type: "text", text: `Resumed run #${thread.id}, ${mode}; ${context}, and cumulative active time is preserved.
|
|
109
|
+
return { content: [{ type: "text", text: `Resumed run #${thread.id}, ${mode}; ${context}, and cumulative active time is preserved. It runs in the background — keep working; the result resumes you automatically.` }], details: {} };
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
} catch (error) {
|
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
|
-
}
|