pi-subagents 0.62.0 → 0.64.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 (57) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/docs/agents.md +5 -4
  3. package/docs/configuration.md +28 -2
  4. package/docs/models.md +5 -5
  5. package/docs/observability.md +4 -1
  6. package/docs/tool-reference.md +2 -2
  7. package/docs/watchdog.md +92 -114
  8. package/package.json +1 -1
  9. package/skills/pi-subagents/references/execution-controls.md +4 -3
  10. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
  11. package/skills/pi-subagents/references/prompting-and-roles.md +3 -3
  12. package/src/agents/agent-management.ts +22 -4
  13. package/src/agents/agents.ts +107 -125
  14. package/src/api/shared-types.ts +3 -0
  15. package/src/extension/config.ts +20 -0
  16. package/src/inspectors/herdr/inspector-runner.ts +19 -13
  17. package/src/runs/background/active-async-capacity.ts +0 -1
  18. package/src/runs/background/async-execution.ts +56 -7
  19. package/src/runs/background/async-resume.ts +3 -0
  20. package/src/runs/background/async-status.ts +18 -2
  21. package/src/runs/background/notify.ts +54 -3
  22. package/src/runs/background/run-status.ts +22 -2
  23. package/src/runs/background/subagent-runner.ts +53 -3
  24. package/src/runs/background/wait-completions.ts +13 -0
  25. package/src/runs/foreground/execution.ts +7 -1
  26. package/src/runs/foreground/subagent-executor.ts +56 -4
  27. package/src/runs/shared/acceptance.ts +25 -9
  28. package/src/runs/shared/async-status-projection.ts +11 -43
  29. package/src/runs/shared/lane-metadata.ts +24 -3
  30. package/src/runs/shared/parallel-handoff.ts +4 -0
  31. package/src/runs/shared/pi-args.ts +9 -6
  32. package/src/runs/shared/subagent-control.ts +4 -2
  33. package/src/runs/shared/task-intent.ts +5 -2
  34. package/src/runs/shared/worktree.ts +467 -63
  35. package/src/shared/types.ts +38 -2
  36. package/src/shared/utils.ts +18 -7
  37. package/src/slash/subagents-admin.ts +24 -12
  38. package/src/tui/fleet-status.ts +61 -2
  39. package/src/tui/fleet.ts +12 -7
  40. package/src/tui/render.ts +227 -14
  41. package/src/watchdog/child-status.ts +54 -33
  42. package/src/watchdog/diff-tool.ts +77 -0
  43. package/src/watchdog/emission-guard.ts +5 -3
  44. package/src/watchdog/guidance.ts +20 -0
  45. package/src/watchdog/register-child.ts +16 -13
  46. package/src/watchdog/register-main.ts +10 -9
  47. package/src/watchdog/render.ts +4 -5
  48. package/src/watchdog/review.ts +15 -4
  49. package/src/watchdog/rules.ts +70 -0
  50. package/src/watchdog/runtime.ts +75 -92
  51. package/src/watchdog/scope.ts +0 -11
  52. package/src/watchdog/settings.ts +48 -104
  53. package/src/watchdog/types.ts +18 -32
  54. package/src/watchdog/warning-format.ts +0 -1
  55. package/src/workflows/chat-progress.ts +3 -2
  56. package/src/workflows/workflow-checklist.ts +439 -0
  57. package/src/workflows/workflow-preflight.ts +28 -1
@@ -124,6 +124,7 @@ import {
124
124
  findWorktreeTaskCwdConflict,
125
125
  formatWorktreeDiffSummary,
126
126
  formatWorktreeTaskCwdConflict,
127
+ WORKTREE_AGENT_CWD_PLACEHOLDER,
127
128
  type WorktreeSetup,
128
129
  } from "../shared/worktree.ts";
129
130
  import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
@@ -155,6 +156,7 @@ import { decodeSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type
155
156
  import {
156
157
  CHILD_WATCHDOG_CONFIG_ENV,
157
158
  acceptChildWatchdogEvent,
159
+ applyChildWatchdogMessage,
158
160
  childWatchdogIsActive,
159
161
  decodeChildWatchdogConfig,
160
162
  isChildWatchdogStatusEvent,
@@ -185,6 +187,8 @@ interface SubagentRunConfig {
185
187
  worktreeSetupHook?: string;
186
188
  worktreeSetupHookTimeoutMs?: number;
187
189
  worktreeBaseDir?: string;
190
+ worktreeProvider?: import("../../shared/types.ts").WorktreeProvider;
191
+ worktreeBranchPrefix?: string;
188
192
  controlConfig?: ResolvedControlConfig;
189
193
  controlIntercomTarget?: string;
190
194
  childIntercomTargets?: Array<string | undefined>;
@@ -245,6 +249,7 @@ interface StepResult {
245
249
  sessionFile?: string;
246
250
  intercomTarget?: string;
247
251
  model?: string;
252
+ thinking?: string;
248
253
  attemptedModels?: string[];
249
254
  modelAttempts?: ModelAttempt[];
250
255
  /** True when the dispatch failed because the input exceeded the model's context window. */
@@ -850,6 +855,10 @@ function runPiStreaming(
850
855
  const text = extractTextFromContent(event.message.content);
851
856
  if (text) writeOutputText(text);
852
857
 
858
+ if (childWatchdogConfig && event.type === "message_end") {
859
+ const next = applyChildWatchdogMessage(childWatchdogState, event.message);
860
+ if (next) updateChildWatchdogState(next);
861
+ }
853
862
  if (event.type !== "message_end" || event.message.role !== "assistant") return;
854
863
  const hasToolCall = assistantStartsToolCall(event.message);
855
864
  if (event.message.model) {
@@ -1066,7 +1075,6 @@ function runPiStreaming(
1066
1075
  phase: "stale",
1067
1076
  seq: (childWatchdogState?.seq ?? 0) + 1,
1068
1077
  lastUpdate: Date.now(),
1069
- followUpPending: false,
1070
1078
  reason: "child watchdog tail timeout",
1071
1079
  timedOut: true,
1072
1080
  });
@@ -2164,6 +2172,7 @@ async function runSingleStepInner(
2164
2172
  reportOptional: isAgentContractV1(step.agentContract),
2165
2173
  artifactsDir: ctx.artifactsDir,
2166
2174
  runId: ctx.id,
2175
+ watchdog: finalResult?.watchdog,
2167
2176
  }))
2168
2177
  : undefined;
2169
2178
  const stoppedAfterAcceptance = finalResult?.stopped === true || ctx.stopSignal?.aborted === true;
@@ -2241,6 +2250,7 @@ async function runSingleStepInner(
2241
2250
  sessionFile: step.sessionFile,
2242
2251
  intercomTarget: ctx.childIntercomTarget,
2243
2252
  model: finalResult?.model,
2253
+ thinking: resolveEffectiveThinking(finalResult?.model, step.thinking),
2244
2254
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
2245
2255
  modelAttempts,
2246
2256
  contextOverflow: contextOverflow || undefined,
@@ -2339,6 +2349,8 @@ function requiredStatusStep(statusPayload: RunnerStatusPayload, index: number):
2339
2349
  function setStatusWorktreeReference(statusStep: RunnerStatusStep, worktree: WorktreeSetup["worktrees"][number]): void {
2340
2350
  statusStep.worktreePath = worktree.path;
2341
2351
  statusStep.branch = worktree.branch;
2352
+ if (worktree.provider) statusStep.provider = worktree.provider;
2353
+ if (worktree.naming) statusStep.naming = worktree.naming;
2342
2354
  }
2343
2355
 
2344
2356
  function markParallelGroupSetupFailure(input: {
@@ -2419,6 +2431,18 @@ function markParallelGroupRunning(input: {
2419
2431
  }));
2420
2432
  }
2421
2433
 
2434
+ function bindWorktreeCwd(step: SubagentStep, worktreeCwd: string): SubagentStep {
2435
+ const bind = <T extends string | null | undefined>(value: T): T => value === null || value === undefined ? value : value.replaceAll(WORKTREE_AGENT_CWD_PLACEHOLDER, worktreeCwd) as T;
2436
+ return {
2437
+ ...step,
2438
+ task: bind(step.task) ?? step.task,
2439
+ ...(step.systemPrompt !== undefined ? { systemPrompt: bind(step.systemPrompt) } : {}),
2440
+ ...(step.outputPath !== undefined ? { outputPath: bind(step.outputPath) } : {}),
2441
+ ...(step.launchBindingTask !== undefined ? { launchBindingTask: bind(step.launchBindingTask) } : {}),
2442
+ ...(step.requestedCwd !== undefined ? { requestedCwd: bind(step.requestedCwd) } : {}),
2443
+ };
2444
+ }
2445
+
2422
2446
  function prepareParallelTaskRun(
2423
2447
  task: SubagentStep,
2424
2448
  cwd: string,
@@ -2427,8 +2451,9 @@ function prepareParallelTaskRun(
2427
2451
  ): { taskForRun: SubagentStep; taskCwd: string } {
2428
2452
  if (!worktreeSetup) return { taskForRun: task, taskCwd: cwd };
2429
2453
  const { cwd: _taskCwd, ...taskForRun } = task;
2454
+ const boundTask = bindWorktreeCwd(taskForRun, worktreeSetup.worktrees[taskIndex]!.agentCwd);
2430
2455
  return {
2431
- taskForRun,
2456
+ taskForRun: boundTask,
2432
2457
  taskCwd: worktreeSetup.worktrees[taskIndex]!.agentCwd,
2433
2458
  };
2434
2459
  }
@@ -2887,6 +2912,7 @@ async function runSubagent(
2887
2912
  success: statusResultSuccess(state, step),
2888
2913
  sessionFile: step.sessionFile,
2889
2914
  model: step.model,
2915
+ thinking: step.thinking,
2890
2916
  attemptedModels: step.attemptedModels,
2891
2917
  modelAttempts: step.modelAttempts,
2892
2918
  usage: usageFromAttempts(step.modelAttempts),
@@ -3566,6 +3592,15 @@ async function runSubagent(
3566
3592
  writeStatusPayload(false);
3567
3593
  return;
3568
3594
  }
3595
+ if (event.type === "message_end") {
3596
+ const next = applyChildWatchdogMessage(step.watchdog, event.message, now);
3597
+ if (next) step.watchdog = next;
3598
+ if (next && (event.message as { role?: unknown } | undefined)?.role === "custom") {
3599
+ statusPayload.lastUpdate = now;
3600
+ writeStatusPayload(false);
3601
+ return;
3602
+ }
3603
+ }
3569
3604
  if (event.type === "tool_execution_start" && event.toolName) {
3570
3605
  const mutates = isMutatingTool(event.toolName, event.args, flatSteps[flatIndex]?.mutationTools);
3571
3606
  const currentPath = resolveCurrentPath(event.toolName, event.args);
@@ -3710,6 +3745,7 @@ async function runSubagent(
3710
3745
  config: controlConfig,
3711
3746
  startedAt: step.startedAt ?? overallStartTime,
3712
3747
  lastActivityAt,
3748
+ turnCount: step.turnCount,
3713
3749
  currentTool: step.currentTool,
3714
3750
  thinking: step.thinking,
3715
3751
  now,
@@ -4331,6 +4367,7 @@ async function runSubagent(
4331
4367
  sessionFile: pr.sessionFile,
4332
4368
  intercomTarget: pr.intercomTarget,
4333
4369
  model: pr.model,
4370
+ thinking: pr.thinking,
4334
4371
  attemptedModels: pr.attemptedModels,
4335
4372
  modelAttempts: pr.modelAttempts,
4336
4373
  contextOverflow: pr.contextOverflow,
@@ -4475,6 +4512,10 @@ async function runSubagent(
4475
4512
  try {
4476
4513
  worktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length, omitUndefinedProperties({
4477
4514
  agents: group.parallel.map((task) => task.agent),
4515
+ labels: group.parallel.map((task) => task.lane?.key ?? config.workflowKey ?? task.outputName ?? task.label),
4516
+ tasks: group.parallel.map((task) => task.task),
4517
+ provider: config.worktreeProvider,
4518
+ branchPrefix: config.worktreeBranchPrefix,
4478
4519
  setupHook: config.worktreeSetupHook
4479
4520
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
4480
4521
  : undefined,
@@ -4772,6 +4813,7 @@ async function runSubagent(
4772
4813
  sessionFile: pr.sessionFile,
4773
4814
  intercomTarget: pr.intercomTarget,
4774
4815
  model: pr.model,
4816
+ thinking: pr.thinking,
4775
4817
  attemptedModels: pr.attemptedModels,
4776
4818
  modelAttempts: pr.modelAttempts,
4777
4819
  contextOverflow: pr.contextOverflow,
@@ -4907,6 +4949,10 @@ async function runSubagent(
4907
4949
  try {
4908
4950
  singleWorktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, 1, omitUndefinedProperties({
4909
4951
  agents: [seqStep.agent],
4952
+ labels: [seqStep.lane?.key ?? config.workflowKey ?? seqStep.outputName ?? seqStep.label],
4953
+ tasks: [seqStep.task],
4954
+ provider: config.worktreeProvider,
4955
+ branchPrefix: config.worktreeBranchPrefix,
4910
4956
  setupHook: config.worktreeSetupHook
4911
4957
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
4912
4958
  : undefined,
@@ -4961,7 +5007,9 @@ async function runSubagent(
4961
5007
  }));
4962
5008
 
4963
5009
  flushPendingStepSteers(flatIndex);
4964
- const executionStep = singleWorktreeSetup ? { ...seqStep, cwd: singleCwd } : seqStep;
5010
+ const executionStep = singleWorktreeSetup
5011
+ ? bindWorktreeCwd({ ...seqStep, cwd: singleCwd }, singleCwd)
5012
+ : seqStep;
4965
5013
  let singleResult: Awaited<ReturnType<typeof runSingleStepWithTimeout>>;
4966
5014
  try {
4967
5015
  singleResult = await runSingleStepWithTimeout(executionStep, compactOptional<SingleStepContext>({
@@ -5025,6 +5073,7 @@ async function runSubagent(
5025
5073
  sessionFile: singleResult.sessionFile,
5026
5074
  intercomTarget: singleResult.intercomTarget,
5027
5075
  model: singleResult.model,
5076
+ thinking: singleResult.thinking,
5028
5077
  attemptedModels: singleResult.attemptedModels,
5029
5078
  modelAttempts: singleResult.modelAttempts,
5030
5079
  contextOverflow: singleResult.contextOverflow,
@@ -5388,6 +5437,7 @@ async function runSubagent(
5388
5437
  sessionFile: r.sessionFile,
5389
5438
  intercomTarget: r.intercomTarget,
5390
5439
  model: r.model,
5440
+ thinking: r.thinking,
5391
5441
  attemptedModels: r.attemptedModels,
5392
5442
  modelAttempts: r.modelAttempts,
5393
5443
  contextOverflow: r.contextOverflow,
@@ -42,6 +42,15 @@ function errorMessage(error: unknown): string {
42
42
  return error instanceof Error ? error.message : String(error);
43
43
  }
44
44
 
45
+ const STRUCTURED_OUTPUT_INLINE_LIMIT_BYTES = 4 * 1024;
46
+
47
+ export function projectStructuredOutput(value: unknown): unknown {
48
+ if (value === undefined) return undefined;
49
+ const serialized = JSON.stringify(value);
50
+ if (typeof serialized !== "string") throw new Error("Structured output must be JSON-serializable");
51
+ return Buffer.byteLength(serialized, "utf8") <= STRUCTURED_OUTPUT_INLINE_LIMIT_BYTES ? JSON.parse(serialized) : undefined;
52
+ }
53
+
45
54
  /**
46
55
  * Project a terminal result payload into the slim shape that is safe to surface in
47
56
  * tool_result details: run identity, per-child outcome, and the artifact trail.
@@ -65,6 +74,8 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
65
74
  const sessionFile = asNonEmptyString(child.sessionFile);
66
75
  const error = asNonEmptyString(child.error);
67
76
  const model = asNonEmptyString(child.model);
77
+ const structuredOutput = projectStructuredOutput(child.structuredOutput);
78
+ const structuredOutputPath = asNonEmptyString(child.structuredOutputPath);
68
79
  const contextOverflow = child.contextOverflow === true;
69
80
  const timeoutRecovery = projectTimeoutRecovery(child.timeoutRecovery);
70
81
  return [{
@@ -74,6 +85,8 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
74
85
  ...(sessionFile ? { sessionFile } : {}),
75
86
  ...(typeof child.success === "boolean" ? { success: child.success } : {}),
76
87
  ...(outputState ? { outputState } : {}),
88
+ ...(structuredOutput !== undefined ? { structuredOutput } : {}),
89
+ ...(structuredOutputPath ? { structuredOutputPath } : {}),
77
90
  ...(error ? { error } : {}),
78
91
  ...(model ? { model } : {}),
79
92
  ...(contextOverflow ? { contextOverflow: true } : {}),
@@ -115,6 +115,7 @@ import { consumeWorkflowChildPermit } from "../../shared/workflow-child-permit.t
115
115
  import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, PI_AGGREGATE_EVENT_PROJECTOR, projectChildLifecycle, type ChildLifecycleAction, type ChildLifecycleState, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
116
116
  import {
117
117
  acceptChildWatchdogEvent,
118
+ applyChildWatchdogMessage,
118
119
  childWatchdogIsActive,
119
120
  isChildWatchdogStatusEvent,
120
121
  resolveChildWatchdogConfig,
@@ -779,7 +780,6 @@ async function runSingleAttempt(
779
780
  phase: "stale",
780
781
  seq: (childWatchdogState?.seq ?? 0) + 1,
781
782
  lastUpdate: Date.now(),
782
- followUpPending: false,
783
783
  reason: "child watchdog tail timeout",
784
784
  timedOut: true,
785
785
  });
@@ -965,6 +965,7 @@ async function runSingleAttempt(
965
965
  config: controlConfig,
966
966
  startedAt: startTime,
967
967
  lastActivityAt: progress.lastActivityAt,
968
+ turnCount: progress.turnCount,
968
969
  currentTool: progress.currentTool,
969
970
  thinking: resolvedThinking,
970
971
  now,
@@ -1119,6 +1120,10 @@ async function runSingleAttempt(
1119
1120
 
1120
1121
  if (evt.type === "message_end" && evt.message) {
1121
1122
  result.messages!.push(evt.message);
1123
+ if (childWatchdog) {
1124
+ const next = applyChildWatchdogMessage(childWatchdogState, evt.message);
1125
+ if (next) updateChildWatchdogState(next);
1126
+ }
1122
1127
  if (evt.message.role === "assistant") {
1123
1128
  result.usage.turns++;
1124
1129
  progress.turnCount = result.usage.turns;
@@ -2231,6 +2236,7 @@ async function runSyncCompletionInner(
2231
2236
  reportOptional: isAgentContractV1(options.agentContract),
2232
2237
  artifactsDir: options.artifactsDir,
2233
2238
  runId: options.runId,
2239
+ watchdog: result.watchdog,
2234
2240
  });
2235
2241
  }
2236
2242
  } catch (error) {
@@ -27,6 +27,7 @@ import { normalizePublicSubagentExecution, validateWorkflowCapacityOverrides } f
27
27
  import { runSync } from "./execution.ts";
28
28
  import { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/tool-actions.ts";
29
29
  import type { MainWatchdogRuntime } from "../../watchdog/runtime.ts";
30
+ import { applyWatchdogLaunchRules } from "../../watchdog/rules.ts";
30
31
  import { buildModelCandidates, normalizeParentModel, resolveEffectiveSubagentModel, resolveModelOrigin, type ModelOrigin, type ParentModel } from "../shared/model-fallback.ts";
31
32
  import { formatRetainedChildren, listRetainedChildren } from "../background/retained-children.ts";
32
33
  import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
@@ -1314,6 +1315,9 @@ function appendStepToAsyncChain(input: {
1314
1315
  waitToolEnabled: input.deps.waitToolEnabled,
1315
1316
  waitToolDefaultTimeoutMs: input.deps.waitToolDefaultTimeoutMs,
1316
1317
  contextForAgent: contextPolicy.contextForAgent,
1318
+ worktreeBaseDir: input.deps.config.worktreeBaseDir,
1319
+ worktreeProvider: input.deps.config.worktreeProvider,
1320
+ worktreeBranchPrefix: input.deps.config.worktreeBranchPrefix,
1317
1321
  asyncDir: resolved.location.asyncDir,
1318
1322
  validateOutputBindings: false,
1319
1323
  capabilityCeiling: intersectSubagentCapabilityCeilings(status.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(asyncCtx.currentSessionId)),
@@ -1687,6 +1691,8 @@ async function resumeExternalJobFollowUp(input: {
1687
1691
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
1688
1692
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1689
1693
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
1694
+ worktreeProvider: input.deps.config.worktreeProvider,
1695
+ worktreeBranchPrefix: input.deps.config.worktreeBranchPrefix,
1690
1696
  controlConfig: resolveControlConfig(input.deps.config.control, undefined),
1691
1697
  controlIntercomTarget: input.intercomBridge.active ? input.intercomBridge.orchestratorTarget : undefined,
1692
1698
  childIntercomTarget: input.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
@@ -1943,6 +1949,8 @@ async function resumeAsyncRun(input: {
1943
1949
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
1944
1950
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1945
1951
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
1952
+ worktreeProvider: input.deps.config.worktreeProvider,
1953
+ worktreeBranchPrefix: input.deps.config.worktreeBranchPrefix,
1946
1954
  controlConfig: resolveControlConfig(input.deps.config.control, input.params.control),
1947
1955
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
1948
1956
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
@@ -2056,9 +2064,13 @@ async function resumeAsyncRun(input: {
2056
2064
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
2057
2065
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
2058
2066
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
2059
- worktree: input.params.worktree === true,
2067
+ worktreeProvider: input.deps.config.worktreeProvider,
2068
+ worktreeBranchPrefix: input.deps.config.worktreeBranchPrefix,
2069
+ // A retained async child already owns the recorded worktree. Resume it in
2070
+ // place rather than allocating a second provider worktree around it.
2071
+ worktree: input.params.worktree === true && !("managedWorktree" in target && target.managedWorktree === true),
2060
2072
  lane: input.params.lane ?? recoveryDescriptor?.lane,
2061
- controlConfig: recoveryDescriptor?.controlConfig ?? resolveControlConfig(input.deps.config.control, input.params.control),
2073
+ controlConfig: resolveRevivalControlConfig({ globalConfig: input.deps.config.control, requestedControl: input.params.control, recoveryControlConfig: recoveryDescriptor?.controlConfig }),
2062
2074
  intercomBridge: input.params.intercomBridge ?? recoveryDescriptor?.intercomBridge,
2063
2075
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
2064
2076
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
@@ -3218,6 +3230,8 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
3218
3230
  source: modelOrigin === "explicit" ? "explicit" : "inherited",
3219
3231
  });
3220
3232
  const modelOverrideFromParent = modelOrigin === "inherited";
3233
+ const launchRuleError = applyWatchdogLaunchRules({ cwd: effectiveCwd, agent: a.name, model: modelOverride ?? (parentModel && `${parentModel.provider}/${parentModel.id}`), warn: (violation) => deps.watchdog?.displayRuleWarning(violation) });
3234
+ if (launchRuleError) return toExecutionErrorResult(params, new Error(launchRuleError), data.contextPolicy.contextSummary);
3221
3235
  const asyncResult = executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
3222
3236
  agent: params.agent!,
3223
3237
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
@@ -3255,6 +3269,8 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
3255
3269
  worktreeSetupHook: deps.config.worktreeSetupHook,
3256
3270
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3257
3271
  worktreeBaseDir: deps.config.worktreeBaseDir,
3272
+ worktreeProvider: deps.config.worktreeProvider,
3273
+ worktreeBranchPrefix: deps.config.worktreeBranchPrefix,
3258
3274
  controlConfig,
3259
3275
  intercomBridge: params.intercomBridge,
3260
3276
  controlIntercomTarget,
@@ -3291,6 +3307,10 @@ function createSingleWorktreeSetup(
3291
3307
  setupHook: ExtensionConfig["worktreeSetupHook"],
3292
3308
  setupHookTimeoutMs: ExtensionConfig["worktreeSetupHookTimeoutMs"],
3293
3309
  baseDir: ExtensionConfig["worktreeBaseDir"],
3310
+ provider: ExtensionConfig["worktreeProvider"],
3311
+ branchPrefix: ExtensionConfig["worktreeBranchPrefix"],
3312
+ label?: string,
3313
+ task?: string,
3294
3314
  beforeCreate?: (setup: WorktreeSetup) => void,
3295
3315
  ): { setup?: WorktreeSetup; errorResult?: AgentToolResult<Details> } {
3296
3316
  if (!enabled) return {};
@@ -3302,6 +3322,10 @@ function createSingleWorktreeSetup(
3302
3322
  ? { hookPath: setupHook, ...(setupHookTimeoutMs === undefined ? {} : { timeoutMs: setupHookTimeoutMs }) }
3303
3323
  : undefined,
3304
3324
  baseDir,
3325
+ provider,
3326
+ branchPrefix,
3327
+ labels: [label],
3328
+ tasks: [task],
3305
3329
  beforeCreate,
3306
3330
  })),
3307
3331
  };
@@ -3643,6 +3667,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3643
3667
  },
3644
3668
  );
3645
3669
  const modelOverrideFromParent = modelOrigin === "inherited";
3670
+ const launchRuleError = applyWatchdogLaunchRules({ cwd: effectiveCwd, agent: agentConfig.name, model: modelOverride ?? (parentModel && `${parentModel.provider}/${parentModel.id}`), warn: (violation) => deps.watchdog?.displayRuleWarning(violation) });
3671
+ if (launchRuleError) return toExecutionErrorResult(params, new Error(launchRuleError), data.contextPolicy.contextSummary);
3646
3672
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
3647
3673
  let readsOverride: string[] | false | undefined = params.reads;
3648
3674
  const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
@@ -3662,6 +3688,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3662
3688
  deps.config.worktreeSetupHook,
3663
3689
  deps.config.worktreeSetupHookTimeoutMs,
3664
3690
  deps.config.worktreeBaseDir,
3691
+ deps.config.worktreeProvider,
3692
+ deps.config.worktreeBranchPrefix,
3693
+ params.lane?.key ?? params.workflowKey,
3694
+ task,
3665
3695
  (plannedSetup) => {
3666
3696
  pendingHandoff = writePendingParallelHandoff({
3667
3697
  manifestPath: parallelHandoffPath(artifactsDir, runId),
@@ -4164,6 +4194,10 @@ function workflowOutputPathMappingSummary(children: WorkflowScriptChildResult[])
4164
4194
  return mappings.length > 0 ? ` Output path mappings: ${mappings.join("; ")}.` : "";
4165
4195
  }
4166
4196
 
4197
+ function workflowDetailsResults(children: WorkflowScriptChildResult[]): SingleResult[] {
4198
+ return children.flatMap((child) => (child.results ?? []).map((result) => result.workflowKey ? result : { ...result, workflowKey: child.key }));
4199
+ }
4200
+
4167
4201
  function workflowSteerReceipt(key: string, result: AgentToolResult<Details>): WorkflowSteerResult {
4168
4202
  const steering = result.details.steering;
4169
4203
  const error = result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n") || undefined;
@@ -4409,6 +4443,7 @@ export function prepareWorkflowLaunchParams(
4409
4443
  if (output !== undefined && typeof output !== "string" && typeof output !== "boolean") throw new Error("output must be a path string or boolean.");
4410
4444
  const outputMode = Object.hasOwn(childParams, "outputMode") ? childParams.outputMode : workflowDefaults.outputMode;
4411
4445
  if (outputMode !== undefined && outputMode !== "inline" && outputMode !== "file-only") throw new Error("outputMode must be 'inline' or 'file-only'.");
4446
+ const control = mergeWorkflowControlOverrides(workflowDefaults.control, childParams.control as ControlConfig | undefined);
4412
4447
  return {
4413
4448
  action: "resume",
4414
4449
  id: childParams.resume.trim(),
@@ -4427,14 +4462,17 @@ export function prepareWorkflowLaunchParams(
4427
4462
  ...(options.missionDetached ? { mission: false } : {}),
4428
4463
  ...(timeoutMs !== undefined ? { timeoutMs: timeoutMs as number } : {}),
4429
4464
  ...(toolBudget !== undefined ? { toolBudget: toolBudget as ToolBudgetConfig } : {}),
4465
+ ...(control !== undefined ? { control } : {}),
4430
4466
  ...(intercomBridge !== undefined ? { intercomBridge: intercomBridge as IntercomBridgeConfig } : {}),
4431
4467
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
4432
4468
  };
4433
4469
  }
4470
+ const control = mergeWorkflowControlOverrides(workflowDefaults.control, childParams.control as ControlConfig | undefined);
4434
4471
  const launchParams = {
4435
4472
  ...workflowDefaults,
4436
4473
  async: options.externalAsyncRequired === true && childParams.async === undefined && workflowDefaults.async === undefined ? true : false,
4437
4474
  ...childParams,
4475
+ ...(control !== undefined ? { control } : {}),
4438
4476
  ...(options.externalAsyncRequired === true && childParams.async === undefined && workflowDefaults.async === undefined ? { workflowAwaitAsync: true } : {}),
4439
4477
  ...(options.missionDetached ? { mission: false } : {}),
4440
4478
  workflowParentRunId: parentWorkflowRunId,
@@ -4453,6 +4491,17 @@ export function prepareWorkflowLaunchParams(
4453
4491
  return normalizedGate.params;
4454
4492
  }
4455
4493
 
4494
+ function mergeWorkflowControlOverrides(workflowControl: ControlConfig | undefined, childControl: ControlConfig | undefined): ControlConfig | undefined {
4495
+ if (childControl === undefined) return workflowControl;
4496
+ if (workflowControl === undefined) return childControl;
4497
+ return { ...workflowControl, ...childControl };
4498
+ }
4499
+
4500
+ export function resolveRevivalControlConfig(input: { globalConfig?: ControlConfig; requestedControl?: ControlConfig; recoveryControlConfig?: ResolvedControlConfig }): ResolvedControlConfig {
4501
+ if (input.requestedControl === undefined) return input.recoveryControlConfig ?? resolveControlConfig(input.globalConfig, undefined);
4502
+ return resolveControlConfig(input.recoveryControlConfig ?? input.globalConfig, input.requestedControl);
4503
+ }
4504
+
4456
4505
  type GateParamsNormalizationResult =
4457
4506
  | { ok: true; params: SubagentParamsLike }
4458
4507
  | { ok: false; error: string };
@@ -5519,7 +5568,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5519
5568
  const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
5520
5569
  return attachWorkflowMission(withRunFanoutBudget({
5521
5570
  content: [{ type: "text", text: displayText }],
5522
- 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 }),
5571
+ 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 }),
5523
5572
  }, workflowFanoutBudget));
5524
5573
  } catch (error) {
5525
5574
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
@@ -5546,7 +5595,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5546
5595
  return attachWorkflowMission(withRunFanoutBudget({
5547
5596
  content: [{ type: "text", text: displayText }],
5548
5597
  isError: true,
5549
- 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 }),
5598
+ 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 }),
5550
5599
  }, workflowFanoutBudget));
5551
5600
  }
5552
5601
  }
@@ -6245,6 +6294,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6245
6294
  const canonicalParams = canonicalizeExecutionParams(effectiveParams, discoveredAgents, discovered.agentDiagnostics, unknownAgentDiagnosticContext);
6246
6295
  if (canonicalParams.error) return buildRequestedModeError(effectiveParams, canonicalParams.error);
6247
6296
  effectiveParams = canonicalParams.params!;
6297
+ if (effectiveParams.worktree === undefined && deps.config.worktree !== undefined) {
6298
+ effectiveParams = { ...effectiveParams, worktree: deps.config.worktree };
6299
+ }
6248
6300
  const modelScope = discovered.modelScope;
6249
6301
  effectiveParams = applySingleAgentLaunchDefaults(effectiveParams, discoveredAgents);
6250
6302
  // An agent-level defaultContext is a preference, unlike an explicit request.
@@ -21,7 +21,9 @@ import type {
21
21
  ResolvedAcceptanceGate,
22
22
  SingleResult,
23
23
  SubagentRunMode,
24
+ ChildWatchdogProgress,
24
25
  } from "../../shared/types.ts";
26
+ import { unresolvedChildWatchdogBlockers } from "../../watchdog/child-status.ts";
25
27
  import { isAgentContractV1 } from "./agent-contract.ts";
26
28
  import { classifyTaskMutationIntent, stripSeverityCompounds, taskMayMutate } from "./task-intent.ts";
27
29
 
@@ -100,12 +102,13 @@ function inferLevel(input: {
100
102
  const writeTask = taskMayWrite
101
103
  || (input.acceptanceRole === "writer" && !readOnlyTask)
102
104
  || (input.acceptanceRole === undefined && /\bworker\b/.test(agent) && !readOnlyTask);
103
- const inferredReadOnly = readOnlyTask || (input.acceptanceRole === "read-only" && !taskMayWrite);
105
+ const inferredReadOnly = readOnlyTask || ((readOnlyAgent || input.acceptanceRole === "read-only") && !taskMayWrite);
104
106
  const roleResolvesReadOnly = input.acceptanceRole !== undefined && inferredReadOnly;
107
+ const dynamicResolvesReadOnly = inferredReadOnly && !writeTask;
105
108
  const keywordRiskReadOnly = input.acceptanceRole === undefined ? intent.kind === "read-only" : inferredReadOnly;
106
109
  const risky = Boolean(input.async && writeTask)
107
- || (Boolean(input.dynamic) && !roleResolvesReadOnly)
108
- || (Boolean(input.dynamicGroup) && !roleResolvesReadOnly)
110
+ || (Boolean(input.dynamic) && !roleResolvesReadOnly && !dynamicResolvesReadOnly)
111
+ || (Boolean(input.dynamicGroup) && !roleResolvesReadOnly && !dynamicResolvesReadOnly)
109
112
  || (!keywordRiskReadOnly && /\b(?:release|migration|migrate|security|data[- ]loss|destructive|post-review|fix pass)\b/.test(task));
110
113
 
111
114
  if (risky) {
@@ -131,7 +134,7 @@ function inferLevel(input: {
131
134
  if (readOnlyAgent || readOnlyTask) {
132
135
  reasons.push(input.acceptanceRole === "read-only" && !readOnlyTask ? "declared read-only acceptance role" : readOnlyAgent ? "read-only/reviewer-style agent" : "read-only task wording");
133
136
  return {
134
- level: "attested",
137
+ level: "none",
135
138
  reasons,
136
139
  criteria: ["Return concrete findings with file paths and severity when applicable"],
137
140
  evidence: ["review-findings", "residual-risks"],
@@ -207,6 +210,10 @@ function explicitAcceptanceCanDisable(explicit: AcceptanceConfig): boolean {
207
210
  return explicit.level === "none" && typeof explicit.reason === "string" && explicit.reason.trim().length > 0;
208
211
  }
209
212
 
213
+ function explicitAcceptanceRequestsPolicy(explicit: AcceptanceConfig): boolean {
214
+ return (explicit.level !== undefined && explicit.level !== "auto") || Object.keys(explicit).some((key) => key !== "level");
215
+ }
216
+
210
217
  function unsupportedEvidenceKindMessage(pathLabel: string, item: unknown): string {
211
218
  const value = typeof item === "string" ? ` "${item}"` : "";
212
219
  return `${pathLabel}${value} is not a supported evidence kind. ${ACCEPTANCE_EVIDENCE_HELP}`;
@@ -437,12 +444,13 @@ export function resolveEffectiveAcceptance(input: {
437
444
  };
438
445
  }
439
446
  const inferred = inferLevel(input);
447
+ const inferredLevel = inferred.level === "none" && explicitAcceptanceRequestsPolicy(explicit) ? "attested" : inferred.level;
440
448
  const level = explicitAcceptanceCanDisable(explicit)
441
449
  ? "none"
442
450
  : explicitLevel === "auto"
443
- ? inferred.level
444
- : (LEVEL_RANK[explicitLevel] >= LEVEL_RANK[inferred.level] ? explicitLevel : inferred.level);
445
- const evidence = unique([...(level === inferred.level ? inferred.evidence : requiredEvidenceForLevel(level)), ...(explicit.evidence ?? [])]);
451
+ ? inferredLevel
452
+ : (LEVEL_RANK[explicitLevel] >= LEVEL_RANK[inferredLevel] ? explicitLevel : inferredLevel);
453
+ const evidence = unique([...(level === inferredLevel ? inferred.evidence : requiredEvidenceForLevel(level)), ...(explicit.evidence ?? [])]);
446
454
  const criteria = normalizeCriteria(
447
455
  (explicit.criteria?.length ? explicit.criteria : inferred.criteria) as Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }>,
448
456
  evidence,
@@ -452,8 +460,8 @@ export function resolveEffectiveAcceptance(input: {
452
460
  level,
453
461
  explicit: input.explicit !== undefined,
454
462
  inferredReason: inferred.reasons,
455
- criteria,
456
- evidence,
463
+ criteria: level === "none" ? [] : criteria,
464
+ evidence: level === "none" ? [] : evidence,
457
465
  verify: explicit.verify ?? [],
458
466
  review,
459
467
  stopRules: explicit.stopRules ?? [],
@@ -1354,6 +1362,7 @@ export async function evaluateAcceptance(input: {
1354
1362
  reportOptional?: boolean;
1355
1363
  artifactsDir?: string;
1356
1364
  runId?: string;
1365
+ watchdog?: ChildWatchdogProgress;
1357
1366
  }): Promise<AcceptanceLedger> {
1358
1367
  const acceptance = input.acceptance;
1359
1368
  const initialStatus = acceptance.level === "none" ? "not-required" : "claimed";
@@ -1369,6 +1378,13 @@ export async function evaluateAcceptance(input: {
1369
1378
  };
1370
1379
  if (acceptance.level === "none") return ledger;
1371
1380
 
1381
+ if (input.watchdog) {
1382
+ const unresolved = unresolvedChildWatchdogBlockers(input.watchdog);
1383
+ ledger.runtimeChecks.push(unresolved.length
1384
+ ? { id: "watchdog-blocker", status: "failed", message: `Unresolved watchdog blocker: ${unresolved[0]!.summary}` }
1385
+ : { id: "watchdog-blocker", status: "passed", message: "No unresolved watchdog blockers." });
1386
+ }
1387
+
1372
1388
  const parsed: AcceptanceReportParseResult = input.reportError
1373
1389
  ? { error: input.reportError }
1374
1390
  : input.report !== undefined