pi-cohort 3.0.2 → 4.1.0

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.
@@ -64,7 +64,7 @@ When asked to review for over-engineering, bloat, or "what can we delete", run a
64
64
 
65
65
  ## Working rules
66
66
  - Read the plan, progress, and relevant files first when available.
67
- - Repo-local `progress.md` files are allowed scratch/memory files. Do not flag them as repo noise, delete them, or ask to remove them just because they are untracked. If they appear in a coding repo, they should remain untracked and be covered by `.gitignore`.
67
+ - Top-level progress files exist only when the caller explicitly enables them. Do not ask consumer repositories to add cohort scratch files to `.gitignore`.
68
68
  - Use `bash` only for read-only inspection (e.g., `git diff`, `git log`, `git show`, test runs).
69
69
  - Do not invent issues. Only report problems you can justify from evidence.
70
70
  - Prefer small corrective edits over broad rewrites.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cohort",
3
- "version": "3.0.2",
3
+ "version": "4.1.0",
4
4
  "description": "Delegate Pi work to focused child agents: code review, scouting, implementation, parallel audits, saved chains, and background jobs.",
5
5
  "author": "Jacek Juraszek",
6
6
  "license": "MIT",
@@ -11,7 +11,8 @@ import { deliverSubagentIntercomMessageEvent } from "../intercom/result-intercom
11
11
  import { resolveSubagentIntercomTarget } from "../intercom/intercom-bridge.ts";
12
12
  import { SubagentParams } from "./schemas.ts";
13
13
  import { loadConfig } from "./config.ts";
14
- import { type Details, type SubagentState } from "../shared/types.ts";
14
+ import { deriveForwardedFlags } from "../runs/shared/forward-flags.ts";
15
+ import type { Details, SubagentState } from "../shared/types.ts";
15
16
 
16
17
  function getSubagentSessionRoot(parentSessionFile: string | null): string {
17
18
  if (parentSessionFile) {
@@ -148,6 +149,7 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
148
149
  getSubagentSessionRoot,
149
150
  expandTilde,
150
151
  discoverAgents,
152
+ forwardedFlags: deriveForwardedFlags(process.argv, config),
151
153
  allowMutatingManagementActions: false,
152
154
  });
153
155
 
@@ -16,7 +16,7 @@ import * as fs from "node:fs";
16
16
  import * as os from "node:os";
17
17
  import * as path from "node:path";
18
18
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
19
- import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
19
+ import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
20
20
  import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
21
21
  import { discoverAgents, discoverAgentsAll } from "../agents/agents.ts";
22
22
  import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
@@ -37,6 +37,7 @@ import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi
37
37
  import registerFanoutChildSubagentExtension from "./fanout-child.ts";
38
38
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
39
39
  import { loadConfig } from "./config.ts";
40
+ import { deriveForwardedFlags } from "../runs/shared/forward-flags.ts";
40
41
  import {
41
42
  type Details,
42
43
  type SubagentState,
@@ -315,6 +316,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
315
316
  getSubagentSessionRoot,
316
317
  expandTilde,
317
318
  discoverAgents,
319
+ forwardedFlags: deriveForwardedFlags(process.argv, config),
318
320
  });
319
321
 
320
322
  pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
@@ -119,7 +119,7 @@ const TaskItem = Type.Object({
119
119
  output: Type.Optional(OutputOverride),
120
120
  outputMode: Type.Optional(OutputModeOverride),
121
121
  reads: Type.Optional(ReadsOverride),
122
- progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking for this task" })),
122
+ progress: Type.Optional(Type.Boolean({ description: "Omit or set false to disable progress.md tracking for this task; true enables it." })),
123
123
  model: Type.Optional(Type.String({ description: "Override model for this task (e.g. 'google/gemini-3-pro')" })),
124
124
  skill: Type.Optional(SkillOverride),
125
125
  acceptance: Type.Optional(AcceptanceOverride),
@@ -297,7 +297,7 @@ export const SubagentParams = Type.Object({
297
297
  { type: "string" },
298
298
  { type: "boolean" },
299
299
  ],
300
- description: "Output file for single agent (string), or false to disable. Relative paths resolve against cwd.",
300
+ description: "Omit or set false to disable file output; true uses the agent-configured filename; a string sets the output path. Relative paths resolve against cwd.",
301
301
  })),
302
302
  outputMode: Type.Optional(OutputModeOverride),
303
303
  skill: Type.Optional(SkillOverride),
@@ -120,6 +120,7 @@ interface AsyncChainParams {
120
120
  controlIntercomTarget?: string;
121
121
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
122
122
  nestedRoute?: NestedRouteInfo;
123
+ forwardedFlags?: string[];
123
124
  acceptance?: AcceptanceInput;
124
125
  }
125
126
 
@@ -147,6 +148,7 @@ interface AsyncSingleParams {
147
148
  controlIntercomTarget?: string;
148
149
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
149
150
  nestedRoute?: NestedRouteInfo;
151
+ forwardedFlags?: string[];
150
152
  acceptance?: AcceptanceInput;
151
153
  }
152
154
 
@@ -249,6 +251,7 @@ export function executeAsyncChain(
249
251
  controlIntercomTarget,
250
252
  childIntercomTarget,
251
253
  nestedRoute,
254
+ forwardedFlags,
252
255
  } = params;
253
256
  const resultMode = params.resultMode ?? "chain";
254
257
  const chainSkills = params.chainSkills ?? [];
@@ -481,6 +484,7 @@ export function executeAsyncChain(
481
484
  sessionId: ctx.currentSessionId,
482
485
  piPackageRoot,
483
486
  piArgv1: process.argv[1],
487
+ forwardedFlags,
484
488
  worktreeSetupHook,
485
489
  worktreeSetupHookTimeoutMs,
486
490
  controlConfig,
@@ -629,6 +633,7 @@ export function executeAsyncSingle(
629
633
  controlIntercomTarget,
630
634
  childIntercomTarget,
631
635
  nestedRoute,
636
+ forwardedFlags,
632
637
  } = params;
633
638
  const task = params.task ?? "";
634
639
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
@@ -716,6 +721,7 @@ export function executeAsyncSingle(
716
721
  sessionId: ctx.currentSessionId,
717
722
  piPackageRoot,
718
723
  piArgv1: process.argv[1],
724
+ forwardedFlags,
719
725
  worktreeSetupHook,
720
726
  worktreeSetupHookTimeoutMs,
721
727
  controlConfig,
@@ -97,6 +97,7 @@ interface SubagentRunConfig {
97
97
  sessionId?: string | null;
98
98
  piPackageRoot?: string;
99
99
  piArgv1?: string;
100
+ forwardedFlags?: string[];
100
101
  worktreeSetupHook?: string;
101
102
  worktreeSetupHookTimeoutMs?: number;
102
103
  controlConfig?: ResolvedControlConfig;
@@ -574,6 +575,7 @@ interface SingleStepContext {
574
575
  outputFile: string;
575
576
  piPackageRoot?: string;
576
577
  piArgv1?: string;
578
+ forwardedFlags?: string[];
577
579
  registerInterrupt?: (interrupt: (() => void) | undefined) => void;
578
580
  childIntercomTarget?: string;
579
581
  orchestratorIntercomTarget?: string;
@@ -677,6 +679,7 @@ async function runSingleStep(
677
679
  parentRootRunId: ctx.nestedRoute?.rootRunId,
678
680
  parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
679
681
  structuredOutput: effectiveStructuredOutput,
682
+ forwardedFlags: ctx.forwardedFlags,
680
683
  });
681
684
  const run = await runPiStreaming(
682
685
  args,
@@ -1610,6 +1613,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1610
1613
  outputFile: path.join(asyncDir, `output-${fi}.log`),
1611
1614
  piPackageRoot: config.piPackageRoot,
1612
1615
  piArgv1: config.piArgv1,
1616
+ forwardedFlags: config.forwardedFlags,
1613
1617
  childIntercomTarget: config.childIntercomTargets?.[fi],
1614
1618
  orchestratorIntercomTarget: config.controlIntercomTarget,
1615
1619
  nestedRoute: config.nestedRoute,
@@ -1857,6 +1861,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1857
1861
  outputFile: path.join(asyncDir, `output-${fi}.log`),
1858
1862
  piPackageRoot: config.piPackageRoot,
1859
1863
  piArgv1: config.piArgv1,
1864
+ forwardedFlags: config.forwardedFlags,
1860
1865
  childIntercomTarget: config.childIntercomTargets?.[fi],
1861
1866
  orchestratorIntercomTarget: config.controlIntercomTarget,
1862
1867
  nestedRoute: config.nestedRoute,
@@ -2024,6 +2029,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2024
2029
  outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
2025
2030
  piPackageRoot: config.piPackageRoot,
2026
2031
  piArgv1: config.piArgv1,
2032
+ forwardedFlags: config.forwardedFlags,
2027
2033
  childIntercomTarget: config.childIntercomTargets?.[flatIndex],
2028
2034
  orchestratorIntercomTarget: config.controlIntercomTarget,
2029
2035
  nestedRoute: config.nestedRoute,
@@ -494,7 +494,7 @@ export class ChainClarifyComponent implements Component {
494
494
  return;
495
495
  }
496
496
 
497
- if (data === "w" && this.mode !== 'parallel') {
497
+ if (data === "w") {
498
498
  this.enterEditMode("output");
499
499
  return;
500
500
  }
@@ -504,11 +504,15 @@ export class ChainClarifyComponent implements Component {
504
504
  return;
505
505
  }
506
506
 
507
- if (data === "p" && this.mode === 'chain') {
508
- const anyEnabled = this.agentConfigs.some((_, i) => this.getEffectiveBehavior(i).progress);
509
- const newState = !anyEnabled;
510
- for (let i = 0; i < this.agentConfigs.length; i++) {
511
- this.updateBehavior(i, "progress", newState);
507
+ if (data === "p" && this.mode !== 'single') {
508
+ if (this.mode === 'chain') {
509
+ const anyEnabled = this.agentConfigs.some((_, i) => this.getEffectiveBehavior(i).progress);
510
+ const newState = !anyEnabled;
511
+ for (let i = 0; i < this.agentConfigs.length; i++) {
512
+ this.updateBehavior(i, "progress", newState);
513
+ }
514
+ } else {
515
+ this.updateBehavior(this.selectedStep, "progress", !this.getEffectiveBehavior(this.selectedStep).progress);
512
516
  }
513
517
  this.tui.requestRender();
514
518
  return;
@@ -532,7 +536,9 @@ export class ChainClarifyComponent implements Component {
532
536
  buffer = template.split("\n")[0] ?? "";
533
537
  } else if (mode === "output") {
534
538
  const behavior = this.getEffectiveBehavior(this.selectedStep);
535
- buffer = behavior.output === false ? "" : (behavior.output || "");
539
+ buffer = behavior.output === false
540
+ ? (this.mode === "chain" ? "" : this.agentConfigs[this.selectedStep]?.output ?? "")
541
+ : (behavior.output || "");
536
542
  } else if (mode === "reads") {
537
543
  const behavior = this.getEffectiveBehavior(this.selectedStep);
538
544
  buffer = behavior.reads === false ? "" : (behavior.reads?.join(", ") || "");
@@ -1108,7 +1114,7 @@ export class ChainClarifyComponent implements Component {
1108
1114
  case 'single':
1109
1115
  return ` [Enter] Run • [Esc] Cancel • e m t w s ${bgLabel} `;
1110
1116
  case 'parallel':
1111
- return ` [Enter] Run • [Esc] Cancel • e m t s ${bgLabel} • ↑↓ Nav `;
1117
+ return ` [Enter] Run • [Esc] Cancel • e m t w p s ${bgLabel} • ↑↓ Nav `;
1112
1118
  case 'chain':
1113
1119
  return ` [Enter] Run • [Esc] Cancel • e m t w r p s ${bgLabel} • ↑↓ Nav `;
1114
1120
  }
@@ -1207,6 +1213,16 @@ export class ChainClarifyComponent implements Component {
1207
1213
  lines.push(this.row(` ${modelLabel}${truncateToWidth(modelValue, innerW - 13)}`));
1208
1214
 
1209
1215
  const behavior = this.getEffectiveBehavior(i);
1216
+ const writesValue = behavior.output === false
1217
+ ? th.fg("dim", "(disabled)")
1218
+ : (behavior.output || th.fg("dim", "(none)"));
1219
+ const writesLabel = th.fg("dim", "writes: ");
1220
+ lines.push(this.row(` ${writesLabel}${truncateToWidth(writesValue, innerW - 14)}`));
1221
+
1222
+ const progressValue = behavior.progress ? th.fg("success", "on") : th.fg("dim", "off");
1223
+ const progressLabel = th.fg("dim", "progress: ");
1224
+ lines.push(this.row(` ${progressLabel}${progressValue}`));
1225
+
1210
1226
  const skillsValue = behavior.skills === false
1211
1227
  ? th.fg("dim", "(disabled)")
1212
1228
  : (behavior.skills?.length ? behavior.skills.join(", ") : th.fg("dim", "(none)"));
@@ -136,6 +136,7 @@ interface ParallelChainRunInput {
136
136
  worktreeSetup?: WorktreeSetup;
137
137
  maxSubagentDepth: number;
138
138
  nestedRoute?: NestedRouteInfo;
139
+ forwardedFlags?: string[];
139
140
  }
140
141
 
141
142
  function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details {
@@ -290,6 +291,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
290
291
  structuredOutput: structuredRuntime,
291
292
  acceptance: task.acceptance,
292
293
  acceptanceContext: { mode: "chain" },
294
+ forwardedFlags: input.forwardedFlags,
293
295
  onUpdate: input.onUpdate
294
296
  ? (progressUpdate) => {
295
297
  const stepResults = progressUpdate.details?.results || [];
@@ -392,6 +394,7 @@ interface ChainExecutionParams {
392
394
  nestedRoute?: NestedRouteInfo;
393
395
  worktreeSetupHook?: string;
394
396
  worktreeSetupHookTimeoutMs?: number;
397
+ forwardedFlags?: string[];
395
398
  }
396
399
 
397
400
  interface ChainExecutionResult {
@@ -644,6 +647,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
644
647
  cwd,
645
648
  runId,
646
649
  globalTaskIndex,
650
+ forwardedFlags: params.forwardedFlags,
647
651
  sessionDirForIndex,
648
652
  sessionFileForIndex,
649
653
  shareEnabled,
@@ -850,6 +854,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
850
854
  cwd,
851
855
  runId,
852
856
  globalTaskIndex,
857
+ forwardedFlags: params.forwardedFlags,
853
858
  sessionDirForIndex,
854
859
  sessionFileForIndex,
855
860
  shareEnabled,
@@ -1062,6 +1067,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1062
1067
  artifactConfig,
1063
1068
  outputPath,
1064
1069
  outputMode: behavior.outputMode,
1070
+ forwardedFlags: params.forwardedFlags,
1065
1071
  maxSubagentDepth,
1066
1072
  controlConfig,
1067
1073
  onControlEvent,
@@ -179,6 +179,7 @@ async function runSingleAttempt(
179
179
  parentRootRunId: options.nestedRoute?.rootRunId,
180
180
  parentCapabilityToken: options.nestedRoute?.capabilityToken,
181
181
  structuredOutput: options.structuredOutput,
182
+ forwardedFlags: options.forwardedFlags,
182
183
  });
183
184
 
184
185
  const result: SingleResult = {
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
5
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
6
- import { type AgentConfig, type AgentScope } from "../../agents/agents.ts";
6
+ import type { AgentConfig, AgentScope } from "../../agents/agents.ts";
7
7
  import { getArtifactsDir } from "../../shared/artifacts.ts";
8
8
  import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
9
9
  import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
@@ -37,7 +37,7 @@ import { createForkContextResolver } from "../../shared/fork-context.ts";
37
37
  import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
38
38
  import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
39
39
  import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
40
- import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
40
+ import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeTopLevelOutput, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
41
41
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd } from "../../shared/utils.ts";
42
42
  import {
43
43
  attachNestedChildrenToResultChildren,
@@ -145,6 +145,7 @@ interface ExecutorDeps {
145
145
  pi: ExtensionAPI;
146
146
  state: SubagentState;
147
147
  config: ExtensionConfig;
148
+ forwardedFlags: string[];
148
149
  asyncByDefault: boolean;
149
150
  tempArtifactsDir: string;
150
151
  getSubagentSessionRoot: (parentSessionFile: string | null) => string;
@@ -665,6 +666,7 @@ async function resumeAsyncRun(input: {
665
666
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
666
667
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
667
668
  availableModels,
669
+ forwardedFlags: input.deps.forwardedFlags,
668
670
  });
669
671
  if (result.isError) return result;
670
672
 
@@ -1102,10 +1104,10 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1102
1104
  cwd: task.cwd,
1103
1105
  ...(modelOverrides[index] ? { model: modelOverrides[index] } : {}),
1104
1106
  ...(skillOverrides[index] !== undefined ? { skill: skillOverrides[index] } : {}),
1105
- ...(task.output === true ? (agentConfigs[index]?.output ? { output: agentConfigs[index]!.output } : {}) : task.output !== undefined ? { output: task.output } : {}),
1107
+ output: normalizeTopLevelOutput(task.output, agentConfigs[index]?.output),
1106
1108
  ...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}),
1107
1109
  ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
1108
- ...(task.progress !== undefined ? { progress: task.progress } : {}),
1110
+ progress: task.progress ?? false,
1109
1111
  ...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}),
1110
1112
  }));
1111
1113
  return executeAsyncChain(id, {
@@ -1133,6 +1135,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1133
1135
  controlIntercomTarget,
1134
1136
  childIntercomTarget,
1135
1137
  nestedRoute,
1138
+ forwardedFlags: deps.forwardedFlags,
1136
1139
  });
1137
1140
  }
1138
1141
 
@@ -1162,6 +1165,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1162
1165
  controlIntercomTarget,
1163
1166
  childIntercomTarget,
1164
1167
  nestedRoute,
1168
+ forwardedFlags: deps.forwardedFlags,
1165
1169
  });
1166
1170
  }
1167
1171
 
@@ -1174,8 +1178,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1174
1178
  details: { mode: "single" as const, results: [] },
1175
1179
  };
1176
1180
  }
1177
- const rawOutput = params.output !== undefined ? params.output : a.output;
1178
- const effectiveOutput = normalizeSingleOutputOverride(rawOutput, a.output);
1181
+ const effectiveOutput = normalizeTopLevelOutput(params.output, a.output);
1179
1182
  const effectiveOutputMode = params.outputMode ?? "inline";
1180
1183
  const normalizedSkills = normalizeSkillInput(params.skill);
1181
1184
  const skills = normalizedSkills === false ? [] : normalizedSkills;
@@ -1206,6 +1209,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1206
1209
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined,
1207
1210
  nestedRoute,
1208
1211
  acceptance: params.acceptance,
1212
+ forwardedFlags: deps.forwardedFlags,
1209
1213
  });
1210
1214
  }
1211
1215
 
@@ -1265,6 +1269,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1265
1269
  maxSubagentDepth: currentMaxSubagentDepth,
1266
1270
  worktreeSetupHook: deps.config.worktreeSetupHook,
1267
1271
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1272
+ forwardedFlags: deps.forwardedFlags,
1268
1273
  });
1269
1274
 
1270
1275
  if (chainResult.requestedAsync) {
@@ -1305,6 +1310,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1305
1310
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
1306
1311
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
1307
1312
  nestedRoute: data.nestedRoute,
1313
+ forwardedFlags: deps.forwardedFlags,
1308
1314
  });
1309
1315
  }
1310
1316
 
@@ -1364,6 +1370,7 @@ interface ForegroundParallelRunInput {
1364
1370
  liveProgress: (AgentProgress | undefined)[];
1365
1371
  onUpdate?: (r: AgentToolResult<Details>) => void;
1366
1372
  worktreeSetup?: WorktreeSetup;
1373
+ forwardedFlags?: string[];
1367
1374
  }
1368
1375
 
1369
1376
  function buildParallelModeError(message: string): AgentToolResult<Details> {
@@ -1524,6 +1531,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1524
1531
  skills: effectiveSkills === false ? [] : effectiveSkills,
1525
1532
  acceptance: task.acceptance,
1526
1533
  acceptanceContext: { mode: "parallel" },
1534
+ forwardedFlags: input.forwardedFlags,
1527
1535
  onUpdate: input.onUpdate
1528
1536
  ? (progressUpdate) => {
1529
1537
  const stepResults = progressUpdate.details?.results || [];
@@ -1630,10 +1638,10 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1630
1638
  normalizeSkillInput(t.skill),
1631
1639
  );
1632
1640
  const behaviorOverrides: StepOverrides[] = tasks.map((task, index) => ({
1633
- ...(task.output !== undefined ? { output: task.output === true ? agentConfigs[index]?.output ?? false : task.output } : {}),
1641
+ output: normalizeTopLevelOutput(task.output, agentConfigs[index]?.output),
1634
1642
  ...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}),
1635
1643
  ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
1636
- ...(task.progress !== undefined ? { progress: task.progress } : {}),
1644
+ progress: task.progress ?? false,
1637
1645
  ...(skillOverrides[index] !== undefined ? { skills: skillOverrides[index] } : {}),
1638
1646
  ...(task.model ? { model: task.model } : {}),
1639
1647
  }));
@@ -1736,6 +1744,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1736
1744
  controlConfig,
1737
1745
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
1738
1746
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
1747
+ forwardedFlags: deps.forwardedFlags,
1739
1748
  });
1740
1749
  }
1741
1750
  }
@@ -1819,6 +1828,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1819
1828
  liveProgress,
1820
1829
  onUpdate: onUpdateWithCost,
1821
1830
  worktreeSetup,
1831
+ forwardedFlags: deps.forwardedFlags,
1822
1832
  });
1823
1833
  if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
1824
1834
  recordSyncCost(deps.state.grandTotal, runId, results.reduce((sum, r) => sum + r.usage.cost, 0) + sumNestedCost(foregroundControl?.nestedChildren));
@@ -1939,8 +1949,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1939
1949
  currentProvider,
1940
1950
  );
1941
1951
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
1942
- const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
1943
- let effectiveOutput = normalizeSingleOutputOverride(rawOutput, agentConfig.output);
1952
+ let effectiveOutput = normalizeTopLevelOutput(params.output, agentConfig.output);
1944
1953
  const effectiveOutputMode = params.outputMode ?? "inline";
1945
1954
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
1946
1955
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, agentConfig.maxSubagentDepth);
@@ -1974,7 +1983,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1974
1983
  task = result.templates[0]!;
1975
1984
  const override = result.behaviorOverrides[0];
1976
1985
  if (override?.model) modelOverride = override.model;
1977
- if (override?.output !== undefined) effectiveOutput = normalizeSingleOutputOverride(override.output, agentConfig.output);
1986
+ if (override?.output !== undefined) effectiveOutput = normalizeTopLevelOutput(override.output, agentConfig.output);
1978
1987
  if (override?.skills !== undefined) skillOverride = override.skills;
1979
1988
 
1980
1989
  if (result.runInBackground) {
@@ -2015,6 +2024,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2015
2024
  controlConfig,
2016
2025
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2017
2026
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
2027
+ forwardedFlags: deps.forwardedFlags,
2018
2028
  });
2019
2029
  }
2020
2030
  }
@@ -2103,6 +2113,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2103
2113
  skills: effectiveSkills,
2104
2114
  acceptance: params.acceptance,
2105
2115
  acceptanceContext: { mode: "single" },
2116
+ forwardedFlags: deps.forwardedFlags,
2106
2117
  });
2107
2118
  if (foregroundControl?.currentIndex === 0) {
2108
2119
  foregroundControl.interrupt = undefined;
@@ -0,0 +1,82 @@
1
+ export interface ForwardFlagsConfig {
2
+ forwardParentFlags?: boolean;
3
+ }
4
+
5
+ // Core pi flag names -> value arity. This is the UNION of recognized core flags
6
+ // across the supported pi range (0.74-0.80), transcribed from
7
+ // @earendil-works/pi-coding-agent dist/cli/args.js. A name present here is never
8
+ // forwarded to children. Direction matters: an entry that is core in a NEWER pi
9
+ // but absent from an OLDER installed pi is safe (worst case we skip forwarding a
10
+ // hypothetical extension flag of the same name); a MISSING entry is the real
11
+ // hazard - it would leak a real core flag into the child. The drift tripwire in
12
+ // test/unit/forward-flags.test.ts asserts installed-core is a subset of this table.
13
+ type Arity = "boolean" | "value" | "value-guarded" | "value-guarded-print";
14
+ export const RECOGNIZED_PI_FLAGS: Record<string, Arity> = {
15
+ help: "boolean", version: "boolean", continue: "boolean", resume: "boolean",
16
+ "no-session": "boolean", "no-tools": "boolean", "no-builtin-tools": "boolean",
17
+ "no-extensions": "boolean", "no-skills": "boolean", "no-prompt-templates": "boolean",
18
+ "no-themes": "boolean", "no-context-files": "boolean", verbose: "boolean",
19
+ approve: "boolean", "no-approve": "boolean", offline: "boolean",
20
+ mode: "value", provider: "value", model: "value", "api-key": "value",
21
+ "system-prompt": "value", "append-system-prompt": "value", name: "value",
22
+ session: "value", "session-id": "value", fork: "value", "session-dir": "value",
23
+ models: "value", tools: "value", "exclude-tools": "value", thinking: "value",
24
+ export: "value", extension: "value", skill: "value", "prompt-template": "value",
25
+ theme: "value",
26
+ print: "value-guarded-print", "list-models": "value-guarded",
27
+ };
28
+ // Short aliases -> long name, so `-p foo` / `-e x` classify correctly.
29
+ const SHORT_ALIASES: Record<string, string> = {
30
+ h: "help", v: "version", c: "continue", r: "resume", nt: "no-tools",
31
+ nbt: "no-builtin-tools", ne: "no-extensions", ns: "no-skills",
32
+ np: "no-prompt-templates", nc: "no-context-files", a: "approve", na: "no-approve",
33
+ n: "name", t: "tools", xt: "exclude-tools", p: "print", e: "extension",
34
+ };
35
+ const EXTENSION_CUSTOMIZATION = new Set(["--extension", "-e", "--no-extensions", "-ne"]);
36
+
37
+ function consumesNext(arity: Arity, next: string | undefined): boolean {
38
+ if (arity === "value") return next !== undefined;
39
+ if (arity === "value-guarded") return next !== undefined && !next.startsWith("-") && !next.startsWith("@");
40
+ if (arity === "value-guarded-print") return next !== undefined && !next.startsWith("@") && (!next.startsWith("-") || next.startsWith("---"));
41
+ return false;
42
+ }
43
+
44
+ export function deriveForwardedFlags(argv: string[], config: ForwardFlagsConfig): string[] {
45
+ if (config.forwardParentFlags === false) return [];
46
+ if (argv.some((tok) => EXTENSION_CUSTOMIZATION.has(tok))) return [];
47
+
48
+ const collected: Array<{ name: string; tokens: string[] }> = [];
49
+ for (let i = 2; i < argv.length; i++) {
50
+ const tok = argv[i]!;
51
+ if (tok.startsWith("@") || !tok.startsWith("-")) continue;
52
+ if (tok.startsWith("-") && !tok.startsWith("--")) {
53
+ const alias = SHORT_ALIASES[tok.slice(1)];
54
+ if (alias && consumesNext(RECOGNIZED_PI_FLAGS[alias]!, argv[i + 1])) i++;
55
+ continue;
56
+ }
57
+ const body = tok.slice(2);
58
+ const eq = body.indexOf("=");
59
+ const name = eq === -1 ? body : body.slice(0, eq);
60
+ const arity = RECOGNIZED_PI_FLAGS[name];
61
+ if (arity) {
62
+ if (eq === -1 && consumesNext(arity, argv[i + 1])) i++;
63
+ continue;
64
+ }
65
+ if (eq !== -1) {
66
+ collected.push({ name, tokens: [tok] });
67
+ continue;
68
+ }
69
+ const next = argv[i + 1];
70
+ if (next !== undefined && !next.startsWith("-") && !next.startsWith("@")) {
71
+ collected.push({ name, tokens: [tok, next] });
72
+ i++;
73
+ } else {
74
+ collected.push({ name, tokens: [tok] });
75
+ }
76
+ }
77
+
78
+ // Dedup by name, last value wins; first-occurrence position is preserved.
79
+ const lastByName = new Map<string, string[]>();
80
+ for (const { name, tokens } of collected) lastByName.set(name, tokens);
81
+ return [...lastByName.values()].flat();
82
+ }
@@ -38,6 +38,7 @@ interface BuildPiArgsInput {
38
38
  inheritSkills: boolean;
39
39
  tools?: string[];
40
40
  extensions?: string[];
41
+ forwardedFlags?: string[];
41
42
  systemPrompt?: string | null;
42
43
  cwd?: string;
43
44
  promptFileStem?: string;
@@ -76,6 +77,11 @@ export function applyThinkingSuffix(model: string | undefined, thinking: string
76
77
 
77
78
  export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
78
79
  const args = [...input.baseArgs];
80
+ // Forwarded flags are already name-deduped by deriveForwardedFlags; append only
81
+ // when the child inherits full extension discovery (extensions === undefined).
82
+ if (input.extensions === undefined && input.forwardedFlags?.length) {
83
+ args.push(...input.forwardedFlags);
84
+ }
79
85
 
80
86
  if (input.sessionFile) {
81
87
  fs.mkdirSync(path.dirname(input.sessionFile), { recursive: true });
@@ -18,6 +18,15 @@ export function normalizeSingleOutputOverride(
18
18
  return undefined;
19
19
  }
20
20
 
21
+ export function normalizeTopLevelOutput(
22
+ output: string | boolean | undefined,
23
+ defaultOutput: string | undefined,
24
+ ): string | false {
25
+ if (output === undefined || (typeof output === "string" && output.trim().length === 0)) return false;
26
+ const normalizedOutput = normalizeSingleOutputOverride(output, defaultOutput);
27
+ return typeof normalizedOutput === "string" && normalizedOutput.trim().length === 0 ? false : normalizedOutput ?? false;
28
+ }
29
+
21
30
  export function resolveSingleOutputPath(
22
31
  output: string | boolean | undefined,
23
32
  runtimeCwd: string,
@@ -796,6 +796,8 @@ export interface RunSyncOptions {
796
796
  outputMode?: OutputMode;
797
797
  maxSubagentDepth?: number;
798
798
  nestedRoute?: NestedRouteInfo;
799
+ /** Parent extension flags to forward into the child (already derived; empty = none). */
800
+ forwardedFlags?: string[];
799
801
  /** Override the agent's default model (format: "provider/id" or just "id") */
800
802
  modelOverride?: string;
801
803
  /** Registry models available for heuristic bare-model resolution */
@@ -849,6 +851,8 @@ export interface ExtensionConfig {
849
851
  intercomBridge?: IntercomBridgeConfig;
850
852
  /** Print a roster of discovered subagents/chains into chat at session start. Default: true. */
851
853
  showRosterOnStart?: boolean;
854
+ /** Forward the parent pi's extension CLI flags (e.g. --no-autofix) into spawned children. Default: true. */
855
+ forwardParentFlags?: boolean;
852
856
  }
853
857
 
854
858
  // ============================================================================