pi-subagents 0.61.0 → 0.63.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/docs/agents.md +11 -6
  3. package/docs/configuration.md +31 -5
  4. package/docs/extension-api.md +2 -2
  5. package/docs/models.md +5 -5
  6. package/docs/observability.md +5 -2
  7. package/docs/tool-reference.md +3 -3
  8. package/install.mjs +0 -1
  9. package/package.json +1 -1
  10. package/skills/pi-subagents/references/execution-controls.md +3 -4
  11. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
  12. package/skills/pi-subagents/references/prompting-and-roles.md +3 -3
  13. package/src/agents/agent-management.ts +41 -4
  14. package/src/agents/agent-serializer.ts +3 -0
  15. package/src/agents/agents.ts +120 -124
  16. package/src/agents/runtime-agent-registry.ts +5 -1
  17. package/src/api/preflight.ts +4 -0
  18. package/src/api/shared-types.ts +3 -0
  19. package/src/extension/config.ts +20 -0
  20. package/src/extension/public-execution.ts +1 -0
  21. package/src/extension/schemas.ts +6 -2
  22. package/src/extension/tool-description.ts +1 -1
  23. package/src/inspectors/herdr/inspector-runner.ts +19 -13
  24. package/src/runs/background/active-async-capacity.ts +26 -8
  25. package/src/runs/background/async-execution.ts +100 -23
  26. package/src/runs/background/async-resume.ts +6 -2
  27. package/src/runs/background/async-status.ts +18 -2
  28. package/src/runs/background/notify.ts +13 -1
  29. package/src/runs/background/process-terminal.ts +16 -0
  30. package/src/runs/background/run-status.ts +22 -2
  31. package/src/runs/background/scheduled-runs.ts +63 -6
  32. package/src/runs/background/steering.ts +4 -1
  33. package/src/runs/background/subagent-runner.ts +44 -9
  34. package/src/runs/background/wait-completions.ts +13 -0
  35. package/src/runs/background/wait-tool.ts +1 -7
  36. package/src/runs/foreground/execution.ts +14 -7
  37. package/src/runs/foreground/subagent-executor.ts +38 -5
  38. package/src/runs/shared/acceptance.ts +85 -18
  39. package/src/runs/shared/capability-ceiling.ts +1 -0
  40. package/src/runs/shared/dynamic-fanout.ts +1 -1
  41. package/src/runs/shared/lane-metadata.ts +24 -3
  42. package/src/runs/shared/parallel-handoff.ts +4 -0
  43. package/src/runs/shared/parallel-utils.ts +2 -6
  44. package/src/runs/shared/permissions.ts +1 -1
  45. package/src/runs/shared/pi-args.ts +32 -14
  46. package/src/runs/shared/pi-spawn.ts +69 -35
  47. package/src/runs/shared/structured-output.ts +33 -6
  48. package/src/runs/shared/subagent-prompt-runtime.ts +20 -3
  49. package/src/runs/shared/task-intent.ts +21 -7
  50. package/src/runs/shared/tool-timeout.ts +1 -1
  51. package/src/runs/shared/worktree.ts +467 -63
  52. package/src/shared/atomic-json.ts +3 -1
  53. package/src/shared/fork-context.ts +0 -12
  54. package/src/shared/fork-session-cwd.ts +27 -0
  55. package/src/shared/launch-contract.ts +3 -0
  56. package/src/shared/types.ts +32 -1
  57. package/src/shared/utils.ts +18 -7
  58. package/src/slash/slash-commands.ts +1 -1
  59. package/src/slash/subagents-admin.ts +26 -12
  60. package/src/tui/fleet-status.ts +61 -2
  61. package/src/tui/fleet.ts +12 -7
  62. package/src/tui/render.ts +222 -14
  63. package/src/workflows/workflow-checklist.ts +441 -0
@@ -50,7 +50,7 @@ import { acquireActiveAsyncCapacity, ActiveAsyncCapacityError, getActiveAsyncCap
50
50
  import { isScheduledRunAction } from "../background/scheduled-runs.ts";
51
51
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
52
52
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
53
- import { normalizeGateAcceptance, validateExecutionAcceptance } from "../shared/acceptance.ts";
53
+ import { normalizeGateAcceptance, resolveAcceptanceReportMode, validateExecutionAcceptance } from "../shared/acceptance.ts";
54
54
  import { canPreferFork, createForkContextResolver, forkedChildRequiresThinkingOff, resolveSubagentLaunchContext } from "../../shared/fork-context.ts";
55
55
  import { createPrunedForkSessionWriter } from "../../shared/pruned-fork.ts";
56
56
  import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
@@ -393,6 +393,7 @@ export interface SubagentParamsLike {
393
393
  agentContract?: AgentContract;
394
394
  at?: string;
395
395
  every?: string;
396
+ sessionOnly?: boolean;
396
397
  on?: string | number;
397
398
  timezone?: string;
398
399
  overlap?: "skip";
@@ -1313,6 +1314,9 @@ function appendStepToAsyncChain(input: {
1313
1314
  waitToolEnabled: input.deps.waitToolEnabled,
1314
1315
  waitToolDefaultTimeoutMs: input.deps.waitToolDefaultTimeoutMs,
1315
1316
  contextForAgent: contextPolicy.contextForAgent,
1317
+ worktreeBaseDir: input.deps.config.worktreeBaseDir,
1318
+ worktreeProvider: input.deps.config.worktreeProvider,
1319
+ worktreeBranchPrefix: input.deps.config.worktreeBranchPrefix,
1316
1320
  asyncDir: resolved.location.asyncDir,
1317
1321
  validateOutputBindings: false,
1318
1322
  capabilityCeiling: intersectSubagentCapabilityCeilings(status.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(asyncCtx.currentSessionId)),
@@ -1686,6 +1690,8 @@ async function resumeExternalJobFollowUp(input: {
1686
1690
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
1687
1691
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1688
1692
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
1693
+ worktreeProvider: input.deps.config.worktreeProvider,
1694
+ worktreeBranchPrefix: input.deps.config.worktreeBranchPrefix,
1689
1695
  controlConfig: resolveControlConfig(input.deps.config.control, undefined),
1690
1696
  controlIntercomTarget: input.intercomBridge.active ? input.intercomBridge.orchestratorTarget : undefined,
1691
1697
  childIntercomTarget: input.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
@@ -1942,6 +1948,8 @@ async function resumeAsyncRun(input: {
1942
1948
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
1943
1949
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1944
1950
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
1951
+ worktreeProvider: input.deps.config.worktreeProvider,
1952
+ worktreeBranchPrefix: input.deps.config.worktreeBranchPrefix,
1945
1953
  controlConfig: resolveControlConfig(input.deps.config.control, input.params.control),
1946
1954
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
1947
1955
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
@@ -2055,7 +2063,11 @@ async function resumeAsyncRun(input: {
2055
2063
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
2056
2064
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
2057
2065
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
2058
- worktree: input.params.worktree === true,
2066
+ worktreeProvider: input.deps.config.worktreeProvider,
2067
+ worktreeBranchPrefix: input.deps.config.worktreeBranchPrefix,
2068
+ // A retained async child already owns the recorded worktree. Resume it in
2069
+ // place rather than allocating a second provider worktree around it.
2070
+ worktree: input.params.worktree === true && !("managedWorktree" in target && target.managedWorktree === true),
2059
2071
  lane: input.params.lane ?? recoveryDescriptor?.lane,
2060
2072
  controlConfig: recoveryDescriptor?.controlConfig ?? resolveControlConfig(input.deps.config.control, input.params.control),
2061
2073
  intercomBridge: input.params.intercomBridge ?? recoveryDescriptor?.intercomBridge,
@@ -3254,6 +3266,8 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
3254
3266
  worktreeSetupHook: deps.config.worktreeSetupHook,
3255
3267
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3256
3268
  worktreeBaseDir: deps.config.worktreeBaseDir,
3269
+ worktreeProvider: deps.config.worktreeProvider,
3270
+ worktreeBranchPrefix: deps.config.worktreeBranchPrefix,
3257
3271
  controlConfig,
3258
3272
  intercomBridge: params.intercomBridge,
3259
3273
  controlIntercomTarget,
@@ -3290,6 +3304,10 @@ function createSingleWorktreeSetup(
3290
3304
  setupHook: ExtensionConfig["worktreeSetupHook"],
3291
3305
  setupHookTimeoutMs: ExtensionConfig["worktreeSetupHookTimeoutMs"],
3292
3306
  baseDir: ExtensionConfig["worktreeBaseDir"],
3307
+ provider: ExtensionConfig["worktreeProvider"],
3308
+ branchPrefix: ExtensionConfig["worktreeBranchPrefix"],
3309
+ label?: string,
3310
+ task?: string,
3293
3311
  beforeCreate?: (setup: WorktreeSetup) => void,
3294
3312
  ): { setup?: WorktreeSetup; errorResult?: AgentToolResult<Details> } {
3295
3313
  if (!enabled) return {};
@@ -3301,6 +3319,10 @@ function createSingleWorktreeSetup(
3301
3319
  ? { hookPath: setupHook, ...(setupHookTimeoutMs === undefined ? {} : { timeoutMs: setupHookTimeoutMs }) }
3302
3320
  : undefined,
3303
3321
  baseDir,
3322
+ provider,
3323
+ branchPrefix,
3324
+ labels: [label],
3325
+ tasks: [task],
3304
3326
  beforeCreate,
3305
3327
  })),
3306
3328
  };
@@ -3661,6 +3683,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3661
3683
  deps.config.worktreeSetupHook,
3662
3684
  deps.config.worktreeSetupHookTimeoutMs,
3663
3685
  deps.config.worktreeBaseDir,
3686
+ deps.config.worktreeProvider,
3687
+ deps.config.worktreeBranchPrefix,
3688
+ params.lane?.key ?? params.workflowKey,
3689
+ task,
3664
3690
  (plannedSetup) => {
3665
3691
  pendingHandoff = writePendingParallelHandoff({
3666
3692
  manifestPath: parallelHandoffPath(artifactsDir, runId),
@@ -3690,7 +3716,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3690
3716
  return { content: [{ type: "text", text: validationError }], isError: true, details: { mode: "single", results: [] } };
3691
3717
  }
3692
3718
  const structuredRuntime = params.outputSchema
3693
- ? createStructuredOutputRuntime(params.outputSchema, artifactConfig.enabled ? path.join(artifactsDir, "structured-output", runId) : undefined, { captureAcceptanceReport: params.acceptance !== false })
3719
+ ? createStructuredOutputRuntime(params.outputSchema, artifactConfig.enabled ? path.join(artifactsDir, "structured-output", runId) : undefined, { acceptanceReport: resolveAcceptanceReportMode(params.acceptance) })
3694
3720
  : undefined;
3695
3721
  // Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
3696
3722
  // absolute paths pass through; relative paths resolve against the child cwd.
@@ -4163,6 +4189,10 @@ function workflowOutputPathMappingSummary(children: WorkflowScriptChildResult[])
4163
4189
  return mappings.length > 0 ? ` Output path mappings: ${mappings.join("; ")}.` : "";
4164
4190
  }
4165
4191
 
4192
+ function workflowDetailsResults(children: WorkflowScriptChildResult[]): SingleResult[] {
4193
+ return children.flatMap((child) => (child.results ?? []).map((result) => result.workflowKey ? result : { ...result, workflowKey: child.key }));
4194
+ }
4195
+
4166
4196
  function workflowSteerReceipt(key: string, result: AgentToolResult<Details>): WorkflowSteerResult {
4167
4197
  const steering = result.details.steering;
4168
4198
  const error = result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n") || undefined;
@@ -5518,7 +5548,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5518
5548
  const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
5519
5549
  return attachWorkflowMission(withRunFanoutBudget({
5520
5550
  content: [{ type: "text", text: displayText }],
5521
- details: compactOptional<Details>({ mode: "workflow", runId: _id, results: workflow.children.flatMap((child) => (child.results ?? []) as SingleResult[]), ...(workflowPreflight ? { preflight: workflowPreflight } : {}), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { value: workflow.value, trace: finalPreflightTrace, emits: workflow.emits, console: workflow.console, ...(workflowResource ? { resource: workflowResource.provenance } : {}), ...(finalPreflightWarnings.length ? { preflightWarnings: finalPreflightWarnings } : {}), receipt }, chatProgress }),
5551
+ details: compactOptional<Details>({ mode: "workflow", runId: _id, results: workflowDetailsResults(workflow.children), ...(workflowPreflight ? { preflight: workflowPreflight } : {}), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { value: workflow.value, trace: finalPreflightTrace, emits: workflow.emits, console: workflow.console, ...(workflowResource ? { resource: workflowResource.provenance } : {}), ...(finalPreflightWarnings.length ? { preflightWarnings: finalPreflightWarnings } : {}), receipt }, chatProgress }),
5522
5552
  }, workflowFanoutBudget));
5523
5553
  } catch (error) {
5524
5554
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
@@ -5545,7 +5575,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5545
5575
  return attachWorkflowMission(withRunFanoutBudget({
5546
5576
  content: [{ type: "text", text: displayText }],
5547
5577
  isError: true,
5548
- details: compactOptional<Details>({ mode: "workflow", runId: _id, results: partial.children.flatMap((child) => (child.results ?? []) as SingleResult[]), ...(workflowPreflight ? { preflight: workflowPreflight } : {}), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { trace: finalPreflightTrace, emits: partial.emits, console: partial.console, ...(workflowResource ? { resource: workflowResource.provenance } : {}), ...(finalPreflightWarnings.length ? { preflightWarnings: finalPreflightWarnings } : {}), receipt }, chatProgress }),
5578
+ details: compactOptional<Details>({ mode: "workflow", runId: _id, results: workflowDetailsResults(partial.children), ...(workflowPreflight ? { preflight: workflowPreflight } : {}), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { trace: finalPreflightTrace, emits: partial.emits, console: partial.console, ...(workflowResource ? { resource: workflowResource.provenance } : {}), ...(finalPreflightWarnings.length ? { preflightWarnings: finalPreflightWarnings } : {}), receipt }, chatProgress }),
5549
5579
  }, workflowFanoutBudget));
5550
5580
  }
5551
5581
  }
@@ -6244,6 +6274,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6244
6274
  const canonicalParams = canonicalizeExecutionParams(effectiveParams, discoveredAgents, discovered.agentDiagnostics, unknownAgentDiagnosticContext);
6245
6275
  if (canonicalParams.error) return buildRequestedModeError(effectiveParams, canonicalParams.error);
6246
6276
  effectiveParams = canonicalParams.params!;
6277
+ if (effectiveParams.worktree === undefined && deps.config.worktree !== undefined) {
6278
+ effectiveParams = { ...effectiveParams, worktree: deps.config.worktree };
6279
+ }
6247
6280
  const modelScope = discovered.modelScope;
6248
6281
  effectiveParams = applySingleAgentLaunchDefaults(effectiveParams, discoveredAgents);
6249
6282
  // An agent-level defaultContext is a preference, unlike an explicit request.
@@ -47,7 +47,7 @@ const VALID_EVIDENCE_KINDS: AcceptanceEvidenceKind[] = [
47
47
  const VALID_EVIDENCE = new Set<AcceptanceEvidenceKind>(VALID_EVIDENCE_KINDS);
48
48
  const ACCEPTANCE_EVIDENCE_HELP = `Supported evidence kinds: ${VALID_EVIDENCE_KINDS.join(", ")}. Example: { level: "checked", evidence: ["commands-run", "changed-files"] }.`;
49
49
  const ACCEPTANCE_OBJECT_EXAMPLE = "Example: { level: \"checked\", evidence: [\"commands-run\", \"changed-files\"] }.";
50
- const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "criteria", "evidence", "verify", "review", "stopRules", "reason"]);
50
+ const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "report", "criteria", "evidence", "verify", "review", "stopRules", "reason"]);
51
51
  const ACCEPTANCE_GATE_KEYS = new Set(["id", "must", "evidence", "severity"]);
52
52
  const ACCEPTANCE_VERIFY_KEYS = new Set(["id", "command", "timeoutMs", "cwd", "env", "allowFailure"]);
53
53
  const ACCEPTANCE_REVIEW_KEYS = new Set(["agent", "focus", "required"]);
@@ -100,12 +100,13 @@ function inferLevel(input: {
100
100
  const writeTask = taskMayWrite
101
101
  || (input.acceptanceRole === "writer" && !readOnlyTask)
102
102
  || (input.acceptanceRole === undefined && /\bworker\b/.test(agent) && !readOnlyTask);
103
- const inferredReadOnly = readOnlyTask || (input.acceptanceRole === "read-only" && !taskMayWrite);
103
+ const inferredReadOnly = readOnlyTask || ((readOnlyAgent || input.acceptanceRole === "read-only") && !taskMayWrite);
104
104
  const roleResolvesReadOnly = input.acceptanceRole !== undefined && inferredReadOnly;
105
+ const dynamicResolvesReadOnly = inferredReadOnly && !writeTask;
105
106
  const keywordRiskReadOnly = input.acceptanceRole === undefined ? intent.kind === "read-only" : inferredReadOnly;
106
107
  const risky = Boolean(input.async && writeTask)
107
- || (Boolean(input.dynamic) && !roleResolvesReadOnly)
108
- || (Boolean(input.dynamicGroup) && !roleResolvesReadOnly)
108
+ || (Boolean(input.dynamic) && !roleResolvesReadOnly && !dynamicResolvesReadOnly)
109
+ || (Boolean(input.dynamicGroup) && !roleResolvesReadOnly && !dynamicResolvesReadOnly)
109
110
  || (!keywordRiskReadOnly && /\b(?:release|migration|migrate|security|data[- ]loss|destructive|post-review|fix pass)\b/.test(task));
110
111
 
111
112
  if (risky) {
@@ -131,7 +132,7 @@ function inferLevel(input: {
131
132
  if (readOnlyAgent || readOnlyTask) {
132
133
  reasons.push(input.acceptanceRole === "read-only" && !readOnlyTask ? "declared read-only acceptance role" : readOnlyAgent ? "read-only/reviewer-style agent" : "read-only task wording");
133
134
  return {
134
- level: "attested",
135
+ level: "none",
135
136
  reasons,
136
137
  criteria: ["Return concrete findings with file paths and severity when applicable"],
137
138
  evidence: ["review-findings", "residual-risks"],
@@ -146,11 +147,46 @@ function inferLevel(input: {
146
147
  };
147
148
  }
148
149
 
149
- export function normalizeAcceptanceInput(input: AcceptanceInput | undefined): AcceptanceConfig {
150
+ type AcceptanceInputNormalizationResult = { value: unknown; error?: string };
151
+
152
+ function normalizeAcceptanceValue(input: unknown, pathLabel = "acceptance"): AcceptanceInputNormalizationResult {
153
+ if (typeof input !== "string") return { value: input };
154
+ const trimmed = input.trim();
155
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return { value: input };
156
+ let parsed: unknown;
157
+ try {
158
+ parsed = JSON.parse(trimmed);
159
+ } catch (error) {
160
+ return {
161
+ value: input,
162
+ error: `${pathLabel} JSON string must encode a valid acceptance object: ${error instanceof Error ? error.message : String(error)}`,
163
+ };
164
+ }
165
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
166
+ const kind = parsed === null ? "null" : Array.isArray(parsed) ? "an array" : typeof parsed;
167
+ return { value: input, error: `${pathLabel} JSON string must encode an object; got ${kind}.` };
168
+ }
169
+ return { value: parsed };
170
+ }
171
+
172
+ export function normalizeAcceptanceInput(input: unknown): AcceptanceConfig {
173
+ const normalized = normalizeAcceptanceValue(input);
174
+ if (normalized.error) throw new Error(normalized.error);
175
+ input = normalized.value;
150
176
  if (input === undefined || input === "auto") return { level: "auto" };
151
177
  if (input === false) return { level: "none", reason: "disabled by deprecated false shorthand" };
152
- if (typeof input === "string") return { level: input };
153
- return { ...input };
178
+ if (typeof input === "string") return { level: input as AcceptanceConfig["level"] };
179
+ return { ...(input as AcceptanceConfig) };
180
+ }
181
+
182
+ export type ResolvedAcceptanceReportMode = "off" | "optional" | "required";
183
+
184
+ export function resolveAcceptanceReportMode(input: unknown): ResolvedAcceptanceReportMode {
185
+ const normalized = normalizeAcceptanceValue(input);
186
+ if (normalized.error) throw new Error(normalized.error);
187
+ const value = normalized.value;
188
+ const report = value && typeof value === "object" && !Array.isArray(value) ? (value as AcceptanceConfig).report : undefined;
189
+ return value === false || report === "off" ? "off" : report === "on" ? "required" : "optional";
154
190
  }
155
191
 
156
192
  type GateAcceptanceNormalizationResult =
@@ -158,7 +194,11 @@ type GateAcceptanceNormalizationResult =
158
194
  | { ok: false; error: string };
159
195
 
160
196
  export function normalizeGateAcceptance(gate: unknown, acceptance: AcceptanceInput | undefined): GateAcceptanceNormalizationResult {
161
- if (gate === undefined) return acceptance === undefined ? { ok: true } : { ok: true, acceptance };
197
+ if (gate === undefined) {
198
+ if (acceptance === undefined) return { ok: true };
199
+ const normalized = normalizeAcceptanceValue(acceptance);
200
+ return normalized.error ? { ok: false, error: normalized.error } : { ok: true, acceptance: normalized.value as AcceptanceInput };
201
+ }
162
202
  if (typeof gate !== "string" || !gate.trim()) return { ok: false, error: "gate must be a non-empty command string." };
163
203
  if (acceptance !== undefined) return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." };
164
204
  return { ok: true, acceptance: { level: "verified", verify: [{ id: "gate", command: gate.trim() }] } };
@@ -168,6 +208,10 @@ function explicitAcceptanceCanDisable(explicit: AcceptanceConfig): boolean {
168
208
  return explicit.level === "none" && typeof explicit.reason === "string" && explicit.reason.trim().length > 0;
169
209
  }
170
210
 
211
+ function explicitAcceptanceRequestsPolicy(explicit: AcceptanceConfig): boolean {
212
+ return (explicit.level !== undefined && explicit.level !== "auto") || Object.keys(explicit).some((key) => key !== "level");
213
+ }
214
+
171
215
  function unsupportedEvidenceKindMessage(pathLabel: string, item: unknown): string {
172
216
  const value = typeof item === "string" ? ` "${item}"` : "";
173
217
  return `${pathLabel}${value} is not a supported evidence kind. ${ACCEPTANCE_EVIDENCE_HELP}`;
@@ -177,6 +221,9 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
177
221
  const errors: string[] = [];
178
222
  if (input === undefined) return errors;
179
223
  if (input === false) return errors;
224
+ const normalized = normalizeAcceptanceValue(input, pathLabel);
225
+ if (normalized.error) return [normalized.error];
226
+ input = normalized.value;
180
227
  if (typeof input === "string") {
181
228
  if (input === "reviewed") errors.push(`${pathLabel} ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
182
229
  else if (!VALID_LEVELS.has(input as AcceptanceLevel)) errors.push(`${pathLabel} has invalid level '${input}'.`);
@@ -197,6 +244,9 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
197
244
  } else if (value.level !== undefined && (typeof value.level !== "string" || !VALID_LEVELS.has(value.level as AcceptanceLevel))) {
198
245
  errors.push(`${pathLabel}.level must be one of auto, none, attested, checked, verified.`);
199
246
  }
247
+ if (value.report !== undefined && value.report !== "on" && value.report !== "off") {
248
+ errors.push(`${pathLabel}.report must be on or off.`);
249
+ }
200
250
  if (value.level === "none" && (typeof value.reason !== "string" || !value.reason.trim())) {
201
251
  errors.push(`${pathLabel}.reason is required when level is none.`);
202
252
  }
@@ -304,29 +354,45 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
304
354
 
305
355
  export function validateExecutionAcceptance(input: {
306
356
  acceptance?: unknown;
307
- tasks?: Array<{ acceptance?: unknown }>;
357
+ outputSchema?: unknown;
358
+ tasks?: Array<{ acceptance?: unknown; outputSchema?: unknown }>;
308
359
  chain?: Array<{
309
360
  acceptance?: unknown;
310
- parallel?: Array<{ acceptance?: unknown }> | { acceptance?: unknown };
361
+ outputSchema?: unknown;
362
+ parallel?: Array<{ acceptance?: unknown; outputSchema?: unknown }> | { acceptance?: unknown; outputSchema?: unknown };
311
363
  }>;
312
364
  }): string[] {
313
365
  const errors = validateAcceptanceInput(input.acceptance, "acceptance");
366
+ errors.push(...validateAcceptanceReportMode(input.acceptance, input.outputSchema, "acceptance"));
314
367
  for (const [index, task] of (input.tasks ?? []).entries()) {
315
368
  errors.push(...validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`));
369
+ errors.push(...validateAcceptanceReportMode(task.acceptance, task.outputSchema, `tasks[${index}].acceptance`));
316
370
  }
317
371
  for (const [stepIndex, step] of (input.chain ?? []).entries()) {
318
372
  errors.push(...validateAcceptanceInput(step.acceptance, `chain[${stepIndex}].acceptance`));
373
+ errors.push(...validateAcceptanceReportMode(step.acceptance, step.outputSchema, `chain[${stepIndex}].acceptance`));
319
374
  if (Array.isArray(step.parallel)) {
320
375
  for (const [taskIndex, task] of step.parallel.entries()) {
321
376
  errors.push(...validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
377
+ errors.push(...validateAcceptanceReportMode(task.acceptance, task.outputSchema, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
322
378
  }
323
379
  } else if (step.parallel) {
324
380
  errors.push(...validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`));
381
+ errors.push(...validateAcceptanceReportMode(step.parallel.acceptance, step.parallel.outputSchema, `chain[${stepIndex}].parallel.acceptance`));
325
382
  }
326
383
  }
327
384
  return errors;
328
385
  }
329
386
 
387
+ function validateAcceptanceReportMode(acceptance: unknown, outputSchema: unknown, pathLabel: string): string[] {
388
+ const normalized = normalizeAcceptanceValue(acceptance, pathLabel);
389
+ if (normalized.error) return [];
390
+ acceptance = normalized.value;
391
+ if (!acceptance || typeof acceptance !== "object" || Array.isArray(acceptance)) return [];
392
+ if (!("report" in acceptance)) return [];
393
+ return outputSchema === undefined ? [`${pathLabel}.report requires outputSchema.`] : [];
394
+ }
395
+
330
396
  function normalizeCriteria(criteria: Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }> | undefined, evidence: AcceptanceEvidenceKind[]): ResolvedAcceptanceGate[] {
331
397
  return (criteria ?? []).map((criterion, index): ResolvedAcceptanceGate => {
332
398
  if (typeof criterion === "string") {
@@ -376,12 +442,13 @@ export function resolveEffectiveAcceptance(input: {
376
442
  };
377
443
  }
378
444
  const inferred = inferLevel(input);
445
+ const inferredLevel = inferred.level === "none" && explicitAcceptanceRequestsPolicy(explicit) ? "attested" : inferred.level;
379
446
  const level = explicitAcceptanceCanDisable(explicit)
380
447
  ? "none"
381
448
  : explicitLevel === "auto"
382
- ? inferred.level
383
- : (LEVEL_RANK[explicitLevel] >= LEVEL_RANK[inferred.level] ? explicitLevel : inferred.level);
384
- const evidence = unique([...(level === inferred.level ? inferred.evidence : requiredEvidenceForLevel(level)), ...(explicit.evidence ?? [])]);
449
+ ? inferredLevel
450
+ : (LEVEL_RANK[explicitLevel] >= LEVEL_RANK[inferredLevel] ? explicitLevel : inferredLevel);
451
+ const evidence = unique([...(level === inferredLevel ? inferred.evidence : requiredEvidenceForLevel(level)), ...(explicit.evidence ?? [])]);
385
452
  const criteria = normalizeCriteria(
386
453
  (explicit.criteria?.length ? explicit.criteria : inferred.criteria) as Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }>,
387
454
  evidence,
@@ -391,8 +458,8 @@ export function resolveEffectiveAcceptance(input: {
391
458
  level,
392
459
  explicit: input.explicit !== undefined,
393
460
  inferredReason: inferred.reasons,
394
- criteria,
395
- evidence,
461
+ criteria: level === "none" ? [] : criteria,
462
+ evidence: level === "none" ? [] : evidence,
396
463
  verify: explicit.verify ?? [],
397
464
  review,
398
465
  stopRules: explicit.stopRules ?? [],
@@ -851,7 +918,7 @@ function validateStringArrayField(errors: string[], value: unknown, pathLabel: s
851
918
  }
852
919
  }
853
920
 
854
- function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: AcceptanceReport; errors: string[] } {
921
+ export function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: AcceptanceReport; errors: string[] } {
855
922
  const normalized = normalizeAcceptanceReportValue(value, pathLabel);
856
923
  value = normalized.value;
857
924
  pathLabel = normalized.pathLabel;
@@ -1310,7 +1377,7 @@ export async function evaluateAcceptance(input: {
1310
1377
 
1311
1378
  const parsed: AcceptanceReportParseResult = input.reportError
1312
1379
  ? { error: input.reportError }
1313
- : input.report
1380
+ : input.report !== undefined
1314
1381
  ? (() => {
1315
1382
  const validation = validateAcceptanceReport(input.report);
1316
1383
  return validation.report
@@ -22,6 +22,7 @@ export interface SubagentCapabilityAudit {
22
22
  requestedTools?: string[];
23
23
  effectiveTools: string[];
24
24
  removedTools: string[];
25
+ excludeTools?: string[];
25
26
  internalTools: string[];
26
27
  extensionsDenied: boolean;
27
28
  removedExtensionCount: number;
@@ -50,7 +50,7 @@ const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outpu
50
50
  const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
51
51
  ...DYNAMIC_PARALLEL_KEYS,
52
52
  "outputName", "structured", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills", "outputPath", "namespaceOutputPath", "maxSubagentDepth", "timeoutMs", "waitToolEnabled", "waitToolDefaultTimeoutMs",
53
- "structuredOutput", "structuredOutputSchema", "tools", "allowNestedSubagents", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "capabilityCeiling", "completionGuard", "systemPrompt",
53
+ "structuredOutput", "structuredOutputSchema", "tools", "excludeTools", "allowNestedSubagents", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "capabilityCeiling", "completionGuard", "systemPrompt",
54
54
  "systemPromptMode", "thinking", "modelCandidates", "sessionFile", "effectiveAcceptance", "acceptanceInput", "acceptanceRole", "parentSessionId", "launchResolvedExtensions", "requestedCwd",
55
55
  ]);
56
56
  const DYNAMIC_COLLECT_KEYS = new Set(["as", "outputSchema"]);
@@ -1,4 +1,4 @@
1
- import type { AsyncStatus, WorkflowLaneMetadata, WorkflowLaneMode } from "../../shared/types.ts";
1
+ import type { AsyncStatus, ManagedWorktreeProvider, WorktreeNaming, WorkflowLaneMetadata, WorkflowLaneMode } from "../../shared/types.ts";
2
2
 
3
3
  export const WORKFLOW_LANE_KEY_MAX_BYTES = 128;
4
4
  export const WORKFLOW_LANE_SOURCE_REF_MAX_BYTES = 128;
@@ -8,6 +8,7 @@ export const WORKFLOW_LANE_OUTPUT_PATH_MAX_BYTES = 256;
8
8
  export const WORKFLOW_LANE_OUTPUT_PATHS_MAX = 10;
9
9
  export const WORKTREE_STATUS_PATH_MAX_BYTES = 4096;
10
10
  export const WORKTREE_STATUS_BRANCH_MAX_BYTES = 256;
11
+ export const WORKTREE_STATUS_NAMING_LABEL_MAX_BYTES = 256;
11
12
 
12
13
  const WORKFLOW_LANE_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
13
14
  const WORKFLOW_LANE_MODES = new Set<WorkflowLaneMode>(["mutation", "review", "scout", "gate"]);
@@ -75,16 +76,36 @@ export function assertWorkflowLaneKey(lane: WorkflowLaneMetadata | undefined, wo
75
76
  export interface WorktreeStatusReference {
76
77
  worktreePath: string;
77
78
  branch: string;
79
+ provider?: ManagedWorktreeProvider;
80
+ naming?: WorktreeNaming;
81
+ }
82
+
83
+ function normalizeWorktreeNaming(value: unknown, label: string): WorktreeNaming {
84
+ assertPlainObject(value, label);
85
+ assertKnownFields(value, ["requestedBranch", "branchPrefix", "label", "sanitizedPathComponent", "collision", "collisionSuffix"], label);
86
+ const collision = value.collision;
87
+ if (collision !== undefined && collision !== "branch" && collision !== "path" && collision !== "both") throw new Error(`${label}.collision is invalid.`);
88
+ return {
89
+ requestedBranch: boundedNonEmptyString(value.requestedBranch, `${label}.requestedBranch`, WORKTREE_STATUS_BRANCH_MAX_BYTES),
90
+ branchPrefix: boundedNonEmptyString(value.branchPrefix, `${label}.branchPrefix`, WORKTREE_STATUS_BRANCH_MAX_BYTES),
91
+ label: boundedNonEmptyString(value.label, `${label}.label`, WORKTREE_STATUS_NAMING_LABEL_MAX_BYTES),
92
+ sanitizedPathComponent: boundedNonEmptyString(value.sanitizedPathComponent, `${label}.sanitizedPathComponent`, WORKTREE_STATUS_NAMING_LABEL_MAX_BYTES),
93
+ ...(collision !== undefined ? { collision } : {}),
94
+ ...(value.collisionSuffix !== undefined ? { collisionSuffix: boundedNonEmptyString(value.collisionSuffix, `${label}.collisionSuffix`, WORKTREE_STATUS_NAMING_LABEL_MAX_BYTES) } : {}),
95
+ };
78
96
  }
79
97
 
80
98
  /** Validate the display-only worktree fields copied into status.json. */
81
99
  export function normalizeWorktreeStatusReference(value: unknown, label = "worktree status reference"): WorktreeStatusReference | undefined {
82
100
  if (value === undefined) return undefined;
83
101
  assertPlainObject(value, label);
84
- assertKnownFields(value, ["worktreePath", "branch"], label);
102
+ assertKnownFields(value, ["worktreePath", "branch", "provider", "naming"], label);
103
+ if (value.provider !== undefined && value.provider !== "native" && value.provider !== "worktrunk") throw new Error(`${label}.provider is invalid.`);
85
104
  return {
86
105
  worktreePath: boundedNonEmptyString(value.worktreePath, `${label}.worktreePath`, WORKTREE_STATUS_PATH_MAX_BYTES),
87
106
  branch: boundedNonEmptyString(value.branch, `${label}.branch`, WORKTREE_STATUS_BRANCH_MAX_BYTES),
107
+ ...(value.provider !== undefined ? { provider: value.provider as ManagedWorktreeProvider } : {}),
108
+ ...(value.naming !== undefined ? { naming: normalizeWorktreeNaming(value.naming, `${label}.naming`) } : {}),
88
109
  };
89
110
  }
90
111
 
@@ -100,6 +121,6 @@ export function validateAsyncStatusLaneMetadata(status: Pick<AsyncStatus, "runId
100
121
  const hasPath = step.worktreePath !== undefined;
101
122
  const hasBranch = step.branch !== undefined;
102
123
  if (hasPath !== hasBranch) throw new Error(`${label}.steps[${index}] must include both worktreePath and branch.`);
103
- if (hasPath) normalizeWorktreeStatusReference({ worktreePath: step.worktreePath, branch: step.branch }, `${label}.steps[${index}]`);
124
+ if (hasPath) normalizeWorktreeStatusReference({ worktreePath: step.worktreePath, branch: step.branch, ...(step.provider ? { provider: step.provider } : {}), ...(step.naming ? { naming: step.naming } : {}) }, `${label}.steps[${index}]`);
104
125
  }
105
126
  }
@@ -574,6 +574,8 @@ export function writeParallelHandoffGroup(input: {
574
574
  index: worktree.index,
575
575
  path: worktree.path,
576
576
  branch: worktree.branch,
577
+ ...(worktree.provider ? { provider: worktree.provider } : {}),
578
+ ...(worktree.naming ? { naming: worktree.naming } : {}),
577
579
  worktreeRemoved: false,
578
580
  branchRemoved: false,
579
581
  preserved: true,
@@ -653,6 +655,8 @@ export function discardPreservedWorktrees(
653
655
  path: task.path,
654
656
  agentCwd: task.path,
655
657
  branch: task.branch,
658
+ ...(task.provider ? { provider: task.provider } : {}),
659
+ ...(task.naming ? { naming: task.naming } : {}),
656
660
  index: task.index,
657
661
  nodeModulesLinked: false,
658
662
  syntheticPaths: [],
@@ -42,6 +42,7 @@ export interface RunnerSubagentStep {
42
42
  skipPrimaryModelVerification?: boolean;
43
43
  modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string; contextWindow?: number }>;
44
44
  tools?: string[];
45
+ excludeTools?: string[];
45
46
  allowNestedSubagents?: boolean;
46
47
  extensions?: string[];
47
48
  subagentOnlyExtensions?: string[];
@@ -68,12 +69,7 @@ export interface RunnerSubagentStep {
68
69
  toolTimeoutMs?: number;
69
70
  waitToolEnabled?: boolean;
70
71
  waitToolDefaultTimeoutMs?: number;
71
- structuredOutput?: {
72
- schema: import("../../shared/types.ts").JsonSchemaObject;
73
- schemaPath: string;
74
- outputPath: string;
75
- acceptanceReportPath?: string;
76
- };
72
+ structuredOutput?: import("./structured-output.ts").StructuredOutputRuntime;
77
73
  structuredOutputSchema?: import("../../shared/types.ts").JsonSchemaObject;
78
74
  agentContract?: import("../../shared/types.ts").AgentContract;
79
75
  definitionDigest?: string;
@@ -7,7 +7,7 @@ export interface PermissionConfig { rules?: PermissionRules }
7
7
 
8
8
  export const PERMISSION_POLICY_ENV = "PI_SUBAGENT_PERMISSION_POLICY";
9
9
  export const PERMISSION_AUDIT_PATH_ENV = "PI_SUBAGENT_PERMISSION_AUDIT_PATH";
10
- const INTERNAL_TOOLS = new Set(["contact_supervisor", "intercom", "bg_wait", "subagent_wait", "structured_output"]);
10
+ const INTERNAL_TOOLS = new Set(["contact_supervisor", "intercom", "bg_wait", "structured_output"]);
11
11
  const DECISIONS = new Set<PermissionDecision>(["allow", "ask", "deny"]);
12
12
  const MAX_POLICY_BYTES = 16 * 1024;
13
13
  const MAX_PREVIEW_BYTES = 2048;