pi-orchestrator-config 4.2.0 → 4.2.1
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 +26 -8
- package/agents/code-reviewer-docs.md +11 -1
- package/agents/code-reviewer-guidelines.md +11 -1
- package/agents/code-reviewer-quality.md +11 -1
- package/agents/code-reviewer-security.md +11 -1
- package/agents/code-reviewer-spec.md +11 -1
- package/agents/git-expert.md +38 -3
- package/agents/github-expert.md +8 -0
- package/extensions/orchestrator/async-agents.ts +63 -26
- package/extensions/orchestrator/async-capability.ts +8 -8
- package/extensions/orchestrator/cron.ts +1 -1
- package/extensions/orchestrator/dreaming.ts +1 -1
- package/extensions/orchestrator/enforcement.ts +4 -5
- package/extensions/orchestrator/index.ts +55 -0
- package/extensions/orchestrator/parse-model-override.ts +29 -0
- package/extensions/orchestrator/pi-config-review-state.ts +13 -0
- package/extensions/orchestrator/project-settings.ts +118 -111
- package/extensions/orchestrator/resolve-agent-model.ts +24 -13
- package/extensions/orchestrator/review-ui.ts +56 -14
- package/extensions/orchestrator/rule-placeholders.ts +28 -2
- package/extensions/orchestrator/subagent-tool.ts +53 -10
- package/extensions/pidiff/pidiff.ts +36 -4
- package/package.json +3 -2
- package/rules/10-agent-routing.md +11 -0
- package/rules/20-code-review-loop.md +14 -9
- package/settings-keys.json +102 -0
|
@@ -6,7 +6,7 @@ import { spawn } from "node:child_process";
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
10
|
|
|
11
11
|
import { createRequire } from "node:module";
|
|
12
12
|
|
|
@@ -48,10 +48,20 @@ import {
|
|
|
48
48
|
supportsAsyncLlm,
|
|
49
49
|
} from "./async-capability.js";
|
|
50
50
|
import { autoMarkInProgress, autoCompleteTask } from "./async-agents.js";
|
|
51
|
+
import { getSetting } from "./project-settings.js";
|
|
52
|
+
import { substituteSettingsPlaceholders } from "./rule-placeholders.js";
|
|
51
53
|
import { resolveAgentModelProvider } from "./resolve-agent-model.js";
|
|
54
|
+
import { parseModelOverride, mergeModelOverride } from "./parse-model-override.js";
|
|
52
55
|
import { clockHHMM, getPiInvocation, getProjectTmpDir, djb2Hash } from "./utils.js";
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
const SETTINGS_KEYS_FOR_SUBAGENT = JSON.parse(
|
|
58
|
+
fs.readFileSync(
|
|
59
|
+
path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "settings-keys.json"),
|
|
60
|
+
"utf-8",
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
export { resolveAgentModelProvider, parseModelOverride };
|
|
55
65
|
|
|
56
66
|
// ── Constants ────────────────────────────────────────────────────────────
|
|
57
67
|
|
|
@@ -82,6 +92,7 @@ const TaskItem = Type.Object({
|
|
|
82
92
|
name: Type.Optional(Type.String({ description: "Display name for async status" })),
|
|
83
93
|
estimatedSeconds: Type.Optional(Type.Number({ description: "Estimated task duration in seconds. Required for sync parallel tasks." })),
|
|
84
94
|
taskId: Type.Optional(Type.String({ description: "Task ID to auto-complete when this async agent finishes" })),
|
|
95
|
+
model: Type.Optional(Type.String({ description: "Override the agent's model. Format: 'provider/model-id' or 'model-id'." })),
|
|
85
96
|
});
|
|
86
97
|
const ChainItem = Type.Object({
|
|
87
98
|
agent: Type.String({ description: "Agent name" }),
|
|
@@ -141,6 +152,9 @@ const SubagentParams = Type.Object({
|
|
|
141
152
|
persistSession: Type.Optional(
|
|
142
153
|
Type.Boolean({ description: "Persist agent session to disk for reuse across calls. Default: false (ephemeral). When true, the same agent in the same project reuses its session — keeps context, saves tokens." }),
|
|
143
154
|
),
|
|
155
|
+
model: Type.Optional(
|
|
156
|
+
Type.String({ description: "Override the agent's model. Format: 'provider/model-id' (e.g., 'litellm/claude-opus-4-6-1m') or just 'model-id' (inherits provider). Overrides all other model resolution." }),
|
|
157
|
+
),
|
|
144
158
|
});
|
|
145
159
|
|
|
146
160
|
// ── Interfaces ───────────────────────────────────────────────────────────
|
|
@@ -370,6 +384,7 @@ export async function runSingleAgent(
|
|
|
370
384
|
parentModelId?: string,
|
|
371
385
|
parentProvider?: string,
|
|
372
386
|
persistSession?: boolean,
|
|
387
|
+
explicit?: { model?: string; provider?: string },
|
|
373
388
|
): Promise<SingleResult> {
|
|
374
389
|
const agent = agents.find((a) => a.name === agentName);
|
|
375
390
|
if (!agent) {
|
|
@@ -401,7 +416,7 @@ export async function runSingleAgent(
|
|
|
401
416
|
// Deterministic session ID based on agent + cwd for session reuse
|
|
402
417
|
args.push("--session-id", `sync-${agentName}-${djb2Hash(agentName + ':' + cwd).toString(36)}`);
|
|
403
418
|
}
|
|
404
|
-
const { model: effectiveModel, provider: effectiveProvider } = resolveAgentModelProvider(agentName, agent, parentModelId, parentProvider, cwd);
|
|
419
|
+
const { model: effectiveModel, provider: effectiveProvider } = resolveAgentModelProvider(agentName, agent, parentModelId, parentProvider, cwd, explicit);
|
|
405
420
|
if (effectiveModel) args.push("--model", effectiveModel);
|
|
406
421
|
if (effectiveProvider) args.push("--provider", effectiveProvider);
|
|
407
422
|
if (agent.tools && agent.tools.length > 0)
|
|
@@ -446,7 +461,12 @@ export async function runSingleAgent(
|
|
|
446
461
|
|
|
447
462
|
try {
|
|
448
463
|
if (agent.systemPrompt.trim()) {
|
|
449
|
-
const
|
|
464
|
+
const resolvedPrompt = substituteSettingsPlaceholders(
|
|
465
|
+
agent.systemPrompt,
|
|
466
|
+
(key) => getSetting(cwd, key as any),
|
|
467
|
+
Object.keys(SETTINGS_KEYS_FOR_SUBAGENT),
|
|
468
|
+
);
|
|
469
|
+
const tmp = await writePromptFile(agent.name, resolvedPrompt, cwd);
|
|
450
470
|
tmpDir = tmp.dir;
|
|
451
471
|
tmpFile = tmp.filePath;
|
|
452
472
|
args.push("--append-system-prompt", tmpFile);
|
|
@@ -528,6 +548,7 @@ export async function runSingleAgent(
|
|
|
528
548
|
|
|
529
549
|
cur.exitCode = exitCode;
|
|
530
550
|
cur.durationMs = Date.now() - startTime;
|
|
551
|
+
|
|
531
552
|
if (aborted) throw new Error("Subagent was aborted");
|
|
532
553
|
return cur;
|
|
533
554
|
} finally {
|
|
@@ -575,7 +596,7 @@ function validateTaskId(taskId: string, cwd: string, sessionId?: string): string
|
|
|
575
596
|
|
|
576
597
|
export function registerSubagentTool(
|
|
577
598
|
pi: ExtensionAPI,
|
|
578
|
-
spawnAsyncAgent: (agentName: string, task: string, cwd: string, agents: AgentConfig[], options?: { fireAndForget?: boolean; name?: string; parentModelId?: string; parentProvider?: string; groupId?: string; taskId?: string }) => { id: string; error?: string; model?: string },
|
|
599
|
+
spawnAsyncAgent: (agentName: string, task: string, cwd: string, agents: AgentConfig[], options?: { fireAndForget?: boolean; name?: string; parentModelId?: string; parentProvider?: string; groupId?: string; taskId?: string; persistSession?: boolean; explicit?: { model?: string; provider?: string } }) => { id: string; error?: string; model?: string },
|
|
579
600
|
killAsyncAgent: (target: string) => { killed: string[]; errors: string[] },
|
|
580
601
|
): void {
|
|
581
602
|
// Only the orchestrator (top-level pi) can spawn subagents.
|
|
@@ -606,6 +627,7 @@ export function registerSubagentTool(
|
|
|
606
627
|
parentModelId: string | undefined,
|
|
607
628
|
parentProvider: string | undefined,
|
|
608
629
|
ctx: any,
|
|
630
|
+
explicit?: { model?: string; provider?: string },
|
|
609
631
|
) {
|
|
610
632
|
if (params.chain) {
|
|
611
633
|
return {
|
|
@@ -664,6 +686,7 @@ export function registerSubagentTool(
|
|
|
664
686
|
? `group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
665
687
|
: undefined;
|
|
666
688
|
for (const t of params.tasks) {
|
|
689
|
+
const taskExplicit = mergeModelOverride(t.model, explicit);
|
|
667
690
|
const r = spawnAsyncAgent(t.agent, t.task, t.cwd, agents, {
|
|
668
691
|
fireAndForget: params.fireAndForget,
|
|
669
692
|
name: (t as any).name,
|
|
@@ -672,6 +695,7 @@ export function registerSubagentTool(
|
|
|
672
695
|
groupId,
|
|
673
696
|
taskId: (t as any).taskId,
|
|
674
697
|
persistSession: params.persistSession,
|
|
698
|
+
explicit: taskExplicit,
|
|
675
699
|
});
|
|
676
700
|
if (r.error) {
|
|
677
701
|
errors.push(`${t.agent}: ${r.error}`);
|
|
@@ -742,7 +766,15 @@ export function registerSubagentTool(
|
|
|
742
766
|
};
|
|
743
767
|
}
|
|
744
768
|
}
|
|
745
|
-
const result = spawnAsyncAgent(params.agent, params.task, params.cwd, agents, {
|
|
769
|
+
const result = spawnAsyncAgent(params.agent, params.task, params.cwd, agents, {
|
|
770
|
+
fireAndForget: params.fireAndForget,
|
|
771
|
+
name: params.name,
|
|
772
|
+
parentModelId,
|
|
773
|
+
parentProvider,
|
|
774
|
+
taskId: params.taskId,
|
|
775
|
+
persistSession: params.persistSession,
|
|
776
|
+
explicit: explicit,
|
|
777
|
+
});
|
|
746
778
|
if (result.error) {
|
|
747
779
|
return {
|
|
748
780
|
content: [{ type: "text" as const, text: result.error }],
|
|
@@ -770,6 +802,7 @@ export function registerSubagentTool(
|
|
|
770
802
|
parentProvider: string | undefined,
|
|
771
803
|
asyncOk: boolean,
|
|
772
804
|
sessionId?: string,
|
|
805
|
+
explicit?: { model?: string; provider?: string },
|
|
773
806
|
) {
|
|
774
807
|
const results: SingleResult[] = [];
|
|
775
808
|
let prev = "";
|
|
@@ -848,6 +881,7 @@ export function registerSubagentTool(
|
|
|
848
881
|
parentModelId,
|
|
849
882
|
parentProvider,
|
|
850
883
|
params.persistSession,
|
|
884
|
+
explicit,
|
|
851
885
|
);
|
|
852
886
|
activeAgents.delete(s.agent);
|
|
853
887
|
updateWorking();
|
|
@@ -899,6 +933,7 @@ export function registerSubagentTool(
|
|
|
899
933
|
parentProvider: string | undefined,
|
|
900
934
|
asyncOk: boolean,
|
|
901
935
|
sessionId?: string,
|
|
936
|
+
explicit?: { model?: string; provider?: string },
|
|
902
937
|
) {
|
|
903
938
|
if (params.tasks.length > MAX_PARALLEL_TASKS)
|
|
904
939
|
return {
|
|
@@ -997,6 +1032,7 @@ export function registerSubagentTool(
|
|
|
997
1032
|
const label = t.name || t.agent;
|
|
998
1033
|
activeAgents.add(label);
|
|
999
1034
|
updateWorking();
|
|
1035
|
+
const taskExplicit = mergeModelOverride(t.model, explicit);
|
|
1000
1036
|
const r = await runSingleAgent(
|
|
1001
1037
|
agents,
|
|
1002
1038
|
t.agent,
|
|
@@ -1014,6 +1050,7 @@ export function registerSubagentTool(
|
|
|
1014
1050
|
parentModelId,
|
|
1015
1051
|
parentProvider,
|
|
1016
1052
|
params.persistSession,
|
|
1053
|
+
taskExplicit,
|
|
1017
1054
|
);
|
|
1018
1055
|
all[i] = r;
|
|
1019
1056
|
activeAgents.delete(t.name || t.agent);
|
|
@@ -1062,6 +1099,7 @@ export function registerSubagentTool(
|
|
|
1062
1099
|
parentProvider: string | undefined,
|
|
1063
1100
|
asyncOk: boolean,
|
|
1064
1101
|
sessionId?: string,
|
|
1102
|
+
explicit?: { model?: string; provider?: string },
|
|
1065
1103
|
) {
|
|
1066
1104
|
if (!params.cwd) {
|
|
1067
1105
|
return {
|
|
@@ -1116,6 +1154,7 @@ export function registerSubagentTool(
|
|
|
1116
1154
|
parentModelId,
|
|
1117
1155
|
parentProvider,
|
|
1118
1156
|
params.persistSession,
|
|
1157
|
+
explicit,
|
|
1119
1158
|
);
|
|
1120
1159
|
activeAgents.delete(label);
|
|
1121
1160
|
updateWorking();
|
|
@@ -1451,6 +1490,8 @@ export function registerSubagentTool(
|
|
|
1451
1490
|
const confirm = params.confirmProjectAgents ?? true;
|
|
1452
1491
|
let parentModelId = ctx.model?.id as string | undefined;
|
|
1453
1492
|
let parentProvider = ctx.model?.provider as string | undefined;
|
|
1493
|
+
// Parse explicit model override from params (applies to all execution modes)
|
|
1494
|
+
const explicitOverride = parseModelOverride(params.model);
|
|
1454
1495
|
const asyncOk = supportsAsyncLlm(parentProvider, ctx.cwd);
|
|
1455
1496
|
let capabilityNote = "";
|
|
1456
1497
|
|
|
@@ -1577,9 +1618,11 @@ export function registerSubagentTool(
|
|
|
1577
1618
|
}
|
|
1578
1619
|
}
|
|
1579
1620
|
|
|
1621
|
+
const explicit = explicitOverride;
|
|
1622
|
+
|
|
1580
1623
|
// Async mode — spawn in background and return immediately
|
|
1581
1624
|
if (params.async === true) {
|
|
1582
|
-
const result = executeAsync(params, agents, mkd, parentModelId, parentProvider, ctx);
|
|
1625
|
+
const result = executeAsync(params, agents, mkd, parentModelId, parentProvider, ctx, explicit);
|
|
1583
1626
|
if (capabilityNote && result.content?.[0]?.type === "text") {
|
|
1584
1627
|
result.content[0].text = `${capabilityNote}\n\n${result.content[0].text}`;
|
|
1585
1628
|
}
|
|
@@ -1600,21 +1643,21 @@ export function registerSubagentTool(
|
|
|
1600
1643
|
// Chain mode
|
|
1601
1644
|
if (params.chain && params.chain.length > 0) {
|
|
1602
1645
|
return withCapabilityNote(
|
|
1603
|
-
await executeChain(params, agents, mkd, signal, onUpdate, activeAgents, updateWorking, parentModelId, parentProvider, asyncOk, sessionId),
|
|
1646
|
+
await executeChain(params, agents, mkd, signal, onUpdate, activeAgents, updateWorking, parentModelId, parentProvider, asyncOk, sessionId, explicit),
|
|
1604
1647
|
);
|
|
1605
1648
|
}
|
|
1606
1649
|
|
|
1607
1650
|
// Parallel mode
|
|
1608
1651
|
if (params.tasks && params.tasks.length > 0) {
|
|
1609
1652
|
return withCapabilityNote(
|
|
1610
|
-
await executeParallel(params, agents, mkd, signal, onUpdate, activeAgents, updateWorking, parentModelId, parentProvider, asyncOk, sessionId),
|
|
1653
|
+
await executeParallel(params, agents, mkd, signal, onUpdate, activeAgents, updateWorking, parentModelId, parentProvider, asyncOk, sessionId, explicit),
|
|
1611
1654
|
);
|
|
1612
1655
|
}
|
|
1613
1656
|
|
|
1614
1657
|
// Single mode
|
|
1615
1658
|
if (params.agent && params.task) {
|
|
1616
1659
|
return withCapabilityNote(
|
|
1617
|
-
await executeSingle(params, agents, mkd, signal, onUpdate, activeAgents, updateWorking, parentModelId, parentProvider, asyncOk, sessionId),
|
|
1660
|
+
await executeSingle(params, agents, mkd, signal, onUpdate, activeAgents, updateWorking, parentModelId, parentProvider, asyncOk, sessionId, explicit),
|
|
1618
1661
|
);
|
|
1619
1662
|
}
|
|
1620
1663
|
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
|
+
import * as net from "node:net";
|
|
12
13
|
import * as path from "node:path";
|
|
13
14
|
import { createRequire } from "node:module";
|
|
14
15
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
@@ -48,6 +49,32 @@ function getBranch(cwd: string): string {
|
|
|
48
49
|
} catch { return ""; }
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/** Return true if port can be bound on 127.0.0.1. */
|
|
53
|
+
function isPortFree(port: number): Promise<boolean> {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
const srv = net.createServer();
|
|
56
|
+
srv.once("error", () => { try { srv.close(); } catch {} resolve(false); });
|
|
57
|
+
srv.listen(port, "127.0.0.1", () => { srv.close(() => resolve(true)); });
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Reuse preferred port if free; otherwise allocate a new one. */
|
|
62
|
+
async function resolveSpawnPort(preferred: number | null | undefined): Promise<number> {
|
|
63
|
+
if (!preferred) {
|
|
64
|
+
const port = await findFreePort();
|
|
65
|
+
log(`allocated new port: ${port}`);
|
|
66
|
+
return port;
|
|
67
|
+
}
|
|
68
|
+
if (await isPortFree(preferred)) {
|
|
69
|
+
log(`reusing lockfile port: ${preferred}`);
|
|
70
|
+
return preferred;
|
|
71
|
+
}
|
|
72
|
+
log(`lockfile port ${preferred} in use, allocating new port`);
|
|
73
|
+
const port = await findFreePort();
|
|
74
|
+
log(`allocated new port: ${port}`);
|
|
75
|
+
return port;
|
|
76
|
+
}
|
|
77
|
+
|
|
51
78
|
export function registerPidiff(pi: ExtensionAPI): void {
|
|
52
79
|
if (process.env.PI_SUBAGENT_CHILD === "1") return;
|
|
53
80
|
|
|
@@ -157,8 +184,8 @@ export function registerPidiff(pi: ExtensionAPI): void {
|
|
|
157
184
|
spawning = true;
|
|
158
185
|
try {
|
|
159
186
|
log("spawning daemon...");
|
|
160
|
-
port
|
|
161
|
-
|
|
187
|
+
// Reuse lockfile port if available — keeps URL stable across reload/restart
|
|
188
|
+
port = await resolveSpawnPort(port);
|
|
162
189
|
doSpawn(port, ctx.cwd);
|
|
163
190
|
writeLockfile(lockDir, port, null, log);
|
|
164
191
|
const ready = await waitForDaemon(port, 60, log);
|
|
@@ -288,7 +315,8 @@ export function registerPidiff(pi: ExtensionAPI): void {
|
|
|
288
315
|
if (!connected) connect(ctx);
|
|
289
316
|
return;
|
|
290
317
|
}
|
|
291
|
-
|
|
318
|
+
// Reuse lockfile port to keep URL stable — fall back if taken
|
|
319
|
+
const port = await resolveSpawnPort(lock?.port);
|
|
292
320
|
doSpawn(port, ctx.cwd);
|
|
293
321
|
writeLockfile(lockDir, port, null, log);
|
|
294
322
|
if (ctx.hasUI) ctx.ui.notify("Starting pidiff server...", "info");
|
|
@@ -306,10 +334,14 @@ export function registerPidiff(pi: ExtensionAPI): void {
|
|
|
306
334
|
if (ws) { try { ws.close(); } catch {} ws = null; }
|
|
307
335
|
connected = false;
|
|
308
336
|
const pidFile = path.join(lockDir, "pidiff.pid");
|
|
337
|
+
// Reuse existing port to keep URL stable (read before removeLockfile)
|
|
338
|
+
const lock = readLockfile(lockDir);
|
|
339
|
+
const preferredPort = activePort || lock?.port || 0;
|
|
309
340
|
killDaemonByPid(pidFile, log);
|
|
310
341
|
removeLockfile(lockDir, log);
|
|
311
342
|
await new Promise(r => setTimeout(r, 1000));
|
|
312
|
-
|
|
343
|
+
// Verify preferred port is free after kill — fall back if taken by another process
|
|
344
|
+
const port = await resolveSpawnPort(preferredPort || null);
|
|
313
345
|
doSpawn(port, ctx.cwd);
|
|
314
346
|
writeLockfile(lockDir, port, null, log);
|
|
315
347
|
if (ctx.hasUI) ctx.ui.notify("Restarting pidiff server...", "info");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-orchestrator-config",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.1",
|
|
4
4
|
"description": "Orchestrator pattern for pi: specialist subagents, enforcement, code review loop, workflow commands",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"rules/",
|
|
22
22
|
"prompts/",
|
|
23
23
|
"templates/",
|
|
24
|
-
"skills/"
|
|
24
|
+
"skills/",
|
|
25
|
+
"settings-keys.json"
|
|
25
26
|
],
|
|
26
27
|
"dependencies": {
|
|
27
28
|
"@huggingface/transformers": "^4.2.0",
|
|
@@ -56,3 +56,14 @@ Some agents are dispatched internally by rules or prompt templates, not by the r
|
|
|
56
56
|
## Fallback
|
|
57
57
|
|
|
58
58
|
**Fallback:** No specialist? → `worker` agent
|
|
59
|
+
|
|
60
|
+
## Model Override
|
|
61
|
+
|
|
62
|
+
When the user asks to run an agent with a specific model:
|
|
63
|
+
|
|
64
|
+
1. Use `list_models` tool to discover available providers and models.
|
|
65
|
+
2. Pass the model to `subagent(model="provider/model-id")`.
|
|
66
|
+
3. Format: `provider/model-id` (e.g., `litellm/claude-opus-4-6-1m`) or just `model-id` (overrides model only; provider follows normal resolution chain).
|
|
67
|
+
4. For parallel tasks, set `model` per-task in the tasks array. Top-level `model` is used as fallback for tasks without their own.
|
|
68
|
+
5. Chain steps share the top-level `model` — no per-step override.
|
|
69
|
+
6. Explicit `model` param overrides agent_overrides, frontmatter, and settings.
|
|
@@ -17,7 +17,7 @@ call `addReviewerPending`/`recordReviewerResult` from bash, write to the JSON fi
|
|
|
17
17
|
or use any method to fake a clean state. The enforcement system detects and blocks this.
|
|
18
18
|
The ONLY way to reach `clean` status is to run the actual review agents.
|
|
19
19
|
|
|
20
|
-
Resolution
|
|
20
|
+
Resolution is handled automatically via project settings injection.
|
|
21
21
|
|
|
22
22
|
- MUST loop until clean or the max cycle cap is reached — see "Cycle Definition & Max Cycles" below.
|
|
23
23
|
|
|
@@ -46,8 +46,8 @@ Disable the review loop via `review_loop_enforcement: false` — not via max_cyc
|
|
|
46
46
|
Cap check is after 5a; it only blocks `go to 2` (re-dispatch of step 2 / all 6 agents,
|
|
47
47
|
including test-automator), not completing the current cycle's fix/explain.
|
|
48
48
|
|
|
49
|
-
**After 5a completes on the max cycle, two outcomes
|
|
50
|
-
|
|
49
|
+
**After 5a completes on the max cycle, two outcomes** (report them; commit may still be
|
|
50
|
+
allowed via the max-cycle path — see step 6):
|
|
51
51
|
|
|
52
52
|
- **Not fixed** (explained why not) → outstanding — report them.
|
|
53
53
|
- **Fixed** (verification blocked by the cap) → cannot re-dispatch to confirm clean — report that
|
|
@@ -64,6 +64,8 @@ cycle3: dispatch → findings → 5a (fix|explain) → cycle >= max → stop (no
|
|
|
64
64
|
```text
|
|
65
65
|
1. Specialist writes/fixes code
|
|
66
66
|
2. Send ALL 6 agents IN PARALLEL (async): 5 reviewers + test-automator
|
|
67
|
+
**On cycle 2+: MANDATORY — include prior cycle findings + responses in each reviewer's prompt (see Step 5a format).**
|
|
68
|
+
Reviewers without this context will blindly repeat the same findings. This is not optional.
|
|
67
69
|
3. Wait for all 6 to complete
|
|
68
70
|
4. Merge & deduplicate review findings
|
|
69
71
|
5. Has findings OR tests failed?
|
|
@@ -73,14 +75,17 @@ cycle3: dispatch → findings → 5a (fix|explain) → cycle >= max → stop (no
|
|
|
73
75
|
· cycle < {{REVIEW_LOOP_MAX_CYCLES}}? → go to 2 (re-run all 6 with prior findings + responses)
|
|
74
76
|
· cycle >= {{REVIEW_LOOP_MAX_CYCLES}}? → **STOP** (terminal): report the two-outcome result
|
|
75
77
|
(see "Cycle Definition & Max Cycles" above). Do NOT proceed to step 6.
|
|
76
|
-
6. NOW you can commit — the pre-commit hook will pass
|
|
77
|
-
|
|
78
|
+
6. NOW you can commit — the pre-commit hook will pass when:
|
|
79
|
+
- status is `clean` AND `tests_passed: true`, OR
|
|
80
|
+
- max cycles exhausted (`has_findings` or `clean`, no pending reviewers, `cycle >= review_loop_max_cycles` — no `tests_passed` check)
|
|
81
|
+
✅ DONE — commit/push allowed
|
|
78
82
|
```
|
|
79
83
|
|
|
80
|
-
🚨 **Step 6 applies
|
|
84
|
+
🚨 **Step 6 applies when clean OR max cycles exhausted.** Do NOT attempt `git commit` before that.
|
|
81
85
|
The enforcement rule blocks commits until this is satisfied — if it blocks you, run the reviewers.
|
|
82
86
|
**After a cap stop:** report the two-outcome result (**Not fixed** → outstanding, **Fixed** → verification
|
|
83
|
-
blocked);
|
|
87
|
+
blocked); commit is allowed when status is `has_findings` or `clean`, no reviewers pending, and
|
|
88
|
+
`cycle >= review_loop_max_cycles` (no `tests_passed` requirement); do **not**
|
|
84
89
|
return to step 2 (re-dispatch of all 6 agents, including test-automator).
|
|
85
90
|
|
|
86
91
|
## Review Agents
|
|
@@ -100,7 +105,7 @@ Six agents run in parallel for comprehensive coverage:
|
|
|
100
105
|
Do NOT block waiting for results — continue working while they run.**
|
|
101
106
|
|
|
102
107
|
Send reviewers "Review the code changes" — never mention `git diff HEAD` in the task prompt.
|
|
103
|
-
Reviewers get `$PI_REVIEW_BASE_BRANCH` env var and use `git diff origin/$PI_REVIEW_BASE_BRANCH
|
|
108
|
+
Reviewers get `$PI_REVIEW_BASE_BRANCH` env var and use `git diff origin/$PI_REVIEW_BASE_BRANCH` themselves (includes uncommitted changes).
|
|
104
109
|
|
|
105
110
|
Reviewers return structured JSON: `{"findings": [{"severity": "...", "file": "...", "line": N, "description": "...", "suggestion": "..."}]}`.
|
|
106
111
|
The async runner validates the output is valid JSON and retries if not (up to 3 times).
|
|
@@ -137,7 +142,7 @@ The following findings were raised in review cycle {N}. Each has been either fix
|
|
|
137
142
|
explained. Do NOT re-raise findings that were adequately addressed. Only re-raise if:
|
|
138
143
|
- The code fix is incorrect or incomplete
|
|
139
144
|
- The explanation is invalid or factually wrong
|
|
140
|
-
- You disagree with the reasoning (
|
|
145
|
+
- You disagree with the reasoning (state why in the finding description field)
|
|
141
146
|
|
|
142
147
|
Findings with valid explanations that you accept → do not re-raise.
|
|
143
148
|
Findings that were fixed in code → verify the fix, do not re-raise if correct.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dco": {
|
|
3
|
+
"type": "bool",
|
|
4
|
+
"env": "PI_DCO",
|
|
5
|
+
"default": false
|
|
6
|
+
},
|
|
7
|
+
"commit_trailer": {
|
|
8
|
+
"type": "bool_or_string",
|
|
9
|
+
"env": "PI_COMMIT_TRAILER",
|
|
10
|
+
"default": false
|
|
11
|
+
},
|
|
12
|
+
"use_worktrees": {
|
|
13
|
+
"type": "bool",
|
|
14
|
+
"env": "PI_USE_WORKTREES",
|
|
15
|
+
"default": false
|
|
16
|
+
},
|
|
17
|
+
"allow_push_to_protected_branches": {
|
|
18
|
+
"type": "bool",
|
|
19
|
+
"env": "PI_ALLOW_PUSH_TO_PROTECTED_BRANCHES",
|
|
20
|
+
"default": false
|
|
21
|
+
},
|
|
22
|
+
"comment_signature": {
|
|
23
|
+
"type": "bool",
|
|
24
|
+
"default": false
|
|
25
|
+
},
|
|
26
|
+
"review_loop_enforcement": {
|
|
27
|
+
"type": "bool",
|
|
28
|
+
"env": "PI_REVIEW_LOOP_ENFORCEMENT",
|
|
29
|
+
"default": false
|
|
30
|
+
},
|
|
31
|
+
"review_loop_max_cycles": {
|
|
32
|
+
"type": "int",
|
|
33
|
+
"env": "PI_REVIEW_LOOP_MAX_CYCLES",
|
|
34
|
+
"default": 3,
|
|
35
|
+
"min": 1,
|
|
36
|
+
"max": 10,
|
|
37
|
+
"strict_digits": true
|
|
38
|
+
},
|
|
39
|
+
"dream_interval_hours": {
|
|
40
|
+
"type": "number",
|
|
41
|
+
"env": "PI_DREAM_INTERVAL_HOURS",
|
|
42
|
+
"default": 3
|
|
43
|
+
},
|
|
44
|
+
"orchestrator_edit_write_block": {
|
|
45
|
+
"type": "bool",
|
|
46
|
+
"default": false
|
|
47
|
+
},
|
|
48
|
+
"pidash_enable": {
|
|
49
|
+
"type": "bool_enable",
|
|
50
|
+
"env": "PI_PIDASH_ENABLE",
|
|
51
|
+
"default": true
|
|
52
|
+
},
|
|
53
|
+
"pidiff_enable": {
|
|
54
|
+
"type": "bool_enable",
|
|
55
|
+
"env": "PI_PIDIFF_ENABLE",
|
|
56
|
+
"default": true
|
|
57
|
+
},
|
|
58
|
+
"pidash_port": {
|
|
59
|
+
"type": "port",
|
|
60
|
+
"env": "PI_PIDASH_PORT",
|
|
61
|
+
"default": 19190
|
|
62
|
+
},
|
|
63
|
+
"image_model": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"env": "PI_IMAGE_MODEL",
|
|
66
|
+
"default": ""
|
|
67
|
+
},
|
|
68
|
+
"internal_operations_provider": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"env": "PI_INTERNAL_OPERATIONS_PROVIDER",
|
|
71
|
+
"default": ""
|
|
72
|
+
},
|
|
73
|
+
"internal_operations_model": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"env": "PI_INTERNAL_OPERATIONS_MODEL",
|
|
76
|
+
"default": ""
|
|
77
|
+
},
|
|
78
|
+
"acpx_agents": {
|
|
79
|
+
"type": "agent_list",
|
|
80
|
+
"env": "ACPX_AGENTS",
|
|
81
|
+
"default": [],
|
|
82
|
+
"per_key_resolution": true
|
|
83
|
+
},
|
|
84
|
+
"cli_agents": {
|
|
85
|
+
"type": "agent_list",
|
|
86
|
+
"env": "CLI_AGENTS",
|
|
87
|
+
"default": [],
|
|
88
|
+
"per_key_resolution": true
|
|
89
|
+
},
|
|
90
|
+
"agent_provider": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"default": ""
|
|
93
|
+
},
|
|
94
|
+
"agent_model": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"default": ""
|
|
97
|
+
},
|
|
98
|
+
"agent_overrides": {
|
|
99
|
+
"type": "agent_overrides",
|
|
100
|
+
"default": {}
|
|
101
|
+
}
|
|
102
|
+
}
|