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.
@@ -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
- export { resolveAgentModelProvider };
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 tmp = await writePromptFile(agent.name, agent.systemPrompt, cwd);
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, { fireAndForget: params.fireAndForget, name: params.name, parentModelId, parentProvider, taskId: params.taskId, persistSession: params.persistSession });
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 = await findFreePort();
161
- log(`allocated free port: ${port}`);
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
- const port = await findFreePort();
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
- const port = await findFreePort();
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.0",
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: project `.pi/pi-config-settings.json` → global `~/.pi/pi-config-settings.json` → `PI_REVIEW_LOOP_ENFORCEMENT` env var → `false`.
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 — neither unblocks `git commit`**
50
- (still controlled solely by `review_loop_enforcement`; status must be `clean` + `tests_passed: true`):
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 (only when status is `clean` AND `tests_passed: true`)
77
- ✅ DONE — commit/push allowed (enforcement checks status: clean AND tests_passed: true)
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 only when clean.** Do NOT attempt `git commit` before reaching `clean` + `tests_passed: true`.
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); raise `review_loop_max_cycles` or disable `review_loop_enforcement` if needed — do **not**
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...HEAD` themselves.
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 (explain why)
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
+ }