pi-subagents 0.35.1 → 0.37.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 (89) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +224 -31
  3. package/agents/advisor.md +73 -0
  4. package/agents/delegate.md +2 -0
  5. package/agents/worker.md +2 -0
  6. package/package.json +11 -13
  7. package/skills/pi-subagents/SKILL.md +42 -13
  8. package/src/agents/agent-management.ts +7 -1
  9. package/src/agents/agents.ts +121 -17
  10. package/src/api/capability-ceiling.ts +17 -0
  11. package/src/api/delegation.ts +127 -0
  12. package/src/api/preflight.ts +399 -0
  13. package/src/extension/config.ts +7 -1
  14. package/src/extension/index.ts +52 -38
  15. package/src/extension/rpc.ts +31 -2
  16. package/src/extension/schemas.ts +29 -3
  17. package/src/extension/tool-description.ts +4 -2
  18. package/src/intercom/intercom-bridge.ts +1 -1
  19. package/src/intercom/native-supervisor-channel.ts +45 -6
  20. package/src/intercom/result-intercom.ts +7 -0
  21. package/src/runs/background/async-execution.ts +158 -17
  22. package/src/runs/background/async-job-tracker.ts +4 -0
  23. package/src/runs/background/async-resume.ts +47 -8
  24. package/src/runs/background/async-status.ts +94 -3
  25. package/src/runs/background/chain-append.ts +2 -0
  26. package/src/runs/background/completion-batcher.ts +6 -4
  27. package/src/runs/background/completion-dedupe.ts +2 -11
  28. package/src/runs/background/fleet-view.ts +9 -4
  29. package/src/runs/background/notify.ts +132 -120
  30. package/src/runs/background/process-terminal.ts +280 -0
  31. package/src/runs/background/result-watcher.ts +138 -78
  32. package/src/runs/background/run-status.ts +10 -2
  33. package/src/runs/background/scheduled-runs.ts +6 -1
  34. package/src/runs/background/stale-run-reconciler.ts +6 -0
  35. package/src/runs/background/subagent-runner.ts +406 -54
  36. package/src/runs/background/subagent-wait.ts +130 -4
  37. package/src/runs/background/wait-tool.ts +2 -2
  38. package/src/runs/foreground/chain-execution.ts +181 -111
  39. package/src/runs/foreground/execution.ts +147 -47
  40. package/src/runs/foreground/foreground-control.ts +90 -0
  41. package/src/runs/foreground/subagent-executor.ts +426 -165
  42. package/src/runs/shared/acceptance.ts +94 -41
  43. package/src/runs/shared/agent-contract.ts +38 -0
  44. package/src/runs/shared/capability-ceiling.ts +177 -0
  45. package/src/runs/shared/child-protocol.ts +1 -1
  46. package/src/runs/shared/completion-guard.ts +36 -5
  47. package/src/runs/shared/context-mode.ts +44 -0
  48. package/src/runs/shared/dynamic-fanout.ts +5 -5
  49. package/src/runs/shared/long-running-guard.ts +4 -0
  50. package/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  51. package/src/runs/shared/nested-events.ts +35 -3
  52. package/src/runs/shared/parallel-handoff.ts +154 -0
  53. package/src/runs/shared/parallel-utils.ts +11 -0
  54. package/src/runs/shared/pi-args.ts +148 -56
  55. package/src/runs/shared/run-history.ts +90 -5
  56. package/src/runs/shared/session-lease.ts +25 -5
  57. package/src/runs/shared/structured-output.ts +112 -7
  58. package/src/runs/shared/subagent-control.ts +4 -0
  59. package/src/runs/shared/subagent-prompt-runtime.ts +31 -19
  60. package/src/runs/shared/task-intent.ts +10 -5
  61. package/src/runs/shared/tool-availability.ts +21 -3
  62. package/src/runs/shared/tool-budget.ts +11 -5
  63. package/src/runs/shared/turn-budget.ts +2 -1
  64. package/src/runs/shared/worktree.ts +63 -14
  65. package/src/shared/accessible-dir.ts +25 -0
  66. package/src/shared/artifacts.ts +37 -7
  67. package/src/shared/atomic-json.ts +14 -42
  68. package/src/shared/child-transcript.ts +52 -0
  69. package/src/shared/file-system-retry.ts +47 -0
  70. package/src/shared/launch-contract.ts +123 -0
  71. package/src/shared/settings.ts +9 -1
  72. package/src/shared/types.ts +314 -28
  73. package/src/shared/utils.ts +17 -42
  74. package/src/slash/delegation-adapters.ts +153 -6
  75. package/src/slash/delegation-json.ts +108 -0
  76. package/src/slash/delegation-request.ts +182 -36
  77. package/src/slash/prompt-template-bridge.ts +222 -37
  78. package/src/slash/selector.ts +147 -0
  79. package/src/slash/slash-commands.ts +15 -6
  80. package/src/slash/slash-live-state.ts +2 -2
  81. package/src/slash/subagents-admin.ts +42 -42
  82. package/src/tui/fleet-status.ts +362 -0
  83. package/src/tui/fleet-transcript.ts +472 -0
  84. package/src/tui/fleet.ts +318 -59
  85. package/src/tui/render.ts +17 -15
  86. package/src/watchdog/change-signature.ts +105 -12
  87. package/src/watchdog/review.ts +7 -2
  88. package/src/watchdog/runtime.ts +5 -3
  89. package/src/slash/subagents-editor.ts +0 -86
@@ -9,6 +9,7 @@ import type { Message } from "@earendil-works/pi-ai";
9
9
  import type { AgentConfig } from "../../agents/agents.ts";
10
10
  import {
11
11
  ensureArtifactsDir,
12
+ formatOutputArtifactContent,
12
13
  getArtifactPaths,
13
14
  writeArtifact,
14
15
  writeMetadata,
@@ -41,6 +42,7 @@ import {
41
42
  getFinalOutput,
42
43
  findLatestSessionFile,
43
44
  detectSubagentError,
45
+ hasEmptyTerminalAssistantResponse,
44
46
  extractToolArgsPreview,
45
47
  extractTextFromContent,
46
48
  } from "../../shared/utils.ts";
@@ -50,8 +52,10 @@ import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
50
52
  import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
51
53
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
52
54
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
53
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
54
- import { readStructuredOutput } from "../shared/structured-output.ts";
55
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
56
+ import { decodeSubagentCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
57
+ import { resolveEffectiveThinking } from "../../shared/model-info.ts";
58
+ import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput } from "../shared/structured-output.ts";
55
59
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
56
60
  import { captureSingleOutputSnapshot, extractChildWrittenOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
57
61
  import {
@@ -71,9 +75,11 @@ import {
71
75
  summarizeRecentMutatingFailures,
72
76
  } from "../shared/long-running-guard.ts";
73
77
  import { acceptanceFailureMessage, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
78
+ import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
74
79
  import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
75
80
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
76
81
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
82
+ import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
77
83
  import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, projectChildLifecycle, type ChildLifecycleAction, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
78
84
  import {
79
85
  acceptChildWatchdogEvent,
@@ -90,6 +96,12 @@ function emptyUsage(): Usage {
90
96
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
91
97
  }
92
98
 
99
+ function withRunContext<T extends SingleResult>(result: T, context: RunSyncOptions["context"]): T {
100
+ if (!context) return result;
101
+ result.context = context;
102
+ return result;
103
+ }
104
+
93
105
  function sumUsage(target: Usage, source: Usage): void {
94
106
  target.input += source.input;
95
107
  target.output += source.output;
@@ -116,6 +128,7 @@ function resolveAttemptTimeout(options: RunSyncOptions): { timeoutMs: number; re
116
128
  function buildPendingAcceptanceLedger(acceptance: ResolvedAcceptanceConfig): AcceptanceLedger {
117
129
  return {
118
130
  status: "pending",
131
+ evidenceStatus: "pending",
119
132
  explicit: acceptance.explicit,
120
133
  effectiveAcceptance: acceptance,
121
134
  inferredReason: acceptance.inferredReason,
@@ -185,6 +198,7 @@ async function runSingleAttempt(
185
198
  sessionEnabled: boolean;
186
199
  systemPrompt: string;
187
200
  resolvedSkillNames?: string[];
201
+ modelCandidates?: string[];
188
202
  skillsWarning?: string;
189
203
  jsonlPath?: string;
190
204
  artifactPaths?: ArtifactPaths;
@@ -196,6 +210,7 @@ async function runSingleAttempt(
196
210
  ): Promise<SingleResult> {
197
211
  const effectiveThinking = options.thinkingOverride ?? agent.thinking;
198
212
  const modelArg = applyThinkingSuffix(model, effectiveThinking, options.thinkingOverride !== undefined);
213
+ const resolvedThinking = resolveEffectiveThinking(modelArg, effectiveThinking);
199
214
  const watchdogConfig = resolveWatchdogConfig(options.cwd ?? runtimeCwd);
200
215
  const childWatchdog = watchdogConfig.ok
201
216
  ? resolveChildWatchdogConfig({
@@ -205,7 +220,7 @@ async function runSingleAttempt(
205
220
  childIndex: options.index ?? 0,
206
221
  })
207
222
  : undefined;
208
- const { args, env: sharedEnv, tempDir, toolDiagnosticPath } = buildPiArgs({
223
+ const { args, env: sharedEnv, tempDir, toolDiagnosticPath, capabilityAudit } = buildPiArgs({
209
224
  baseArgs: ["--mode", "json", "-p"],
210
225
  task,
211
226
  sessionEnabled: shared.sessionEnabled,
@@ -236,24 +251,61 @@ async function runSingleAttempt(
236
251
  parentSessionId: options.parentSessionId,
237
252
  structuredOutput: options.structuredOutput,
238
253
  toolBudget: options.toolBudget,
254
+ allowZeroToolBudget: options.allowZeroToolBudget,
239
255
  childWatchdog,
240
256
  waitToolEnabled: options.waitToolEnabled,
257
+ capabilityCeiling: options.capabilityCeiling,
241
258
  });
242
259
 
243
- const result: SingleResult = {
260
+ const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(shared.systemPrompt, options.turnBudget);
261
+ const toolPlan = resolvePiLaunchToolPlan({
262
+ tools: agent.tools,
263
+ extensions: agent.extensions,
264
+ subagentOnlyExtensions: agent.subagentOnlyExtensions,
265
+ mcpDirectTools: agent.mcpDirectTools,
266
+ cwd: options.cwd ?? runtimeCwd,
267
+ requireReadTool: Boolean(shared.resolvedSkillNames?.length),
268
+ structuredOutput: Boolean(options.structuredOutput),
269
+ capabilityCeiling: options.capabilityCeiling,
270
+ inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
271
+ });
272
+ const launchContractDigest = launchBindingDigest({
273
+ definitionDigest: agentDefinitionDigest(agent),
274
+ task: shared.originalTask ?? task,
275
+ ...(modelArg ? { model: modelArg } : {}),
276
+ modelCandidates: shared.modelCandidates,
277
+ ...(resolvedThinking ? { thinking: resolvedThinking } : {}),
278
+ systemPrompt: effectiveSystemPrompt,
279
+ systemPromptMode: agent.systemPromptMode,
280
+ inheritProjectContext: agent.inheritProjectContext,
281
+ inheritSkills: agent.inheritSkills,
282
+ skills: shared.resolvedSkillNames ?? [],
283
+ tools: toolPlan.effectiveToolAllowlist,
284
+ extensions: toolPlan.extensionArgs,
285
+ mcpDirectTools: toolPlan.effectiveMcpTools,
286
+ ...(options.outputPath ? { outputPath: options.outputPath } : {}),
287
+ outputMode: options.outputMode ?? "inline",
288
+ ...(options.structuredOutput ? { structuredOutputSchema: options.structuredOutput.schema } : {}),
289
+ });
290
+ const result: SingleResult = withRunContext({
244
291
  agent: agent.name,
245
292
  task: shared.originalTask ?? task,
293
+ ...(options.agentContract ? { agentContract: options.agentContract } : {}),
294
+ launchContractDigest,
246
295
  exitCode: 0,
247
296
  messages: [],
248
297
  usage: emptyUsage(),
249
298
  model: modelArg,
299
+ ...(resolvedThinking ? { thinking: resolvedThinking } : {}),
250
300
  artifactPaths: shared.artifactPaths,
251
301
  transcriptPath: shared.transcriptWriter ? shared.artifactPaths?.transcriptPath : undefined,
252
302
  skills: shared.resolvedSkillNames,
253
303
  skillsWarning: shared.skillsWarning,
254
304
  ...(options.turnBudget ? { turnBudget: initialTurnBudgetState(options.turnBudget) } : {}),
255
305
  ...(options.toolBudget ? { toolBudget: initialToolBudgetState(options.toolBudget) } : {}),
256
- };
306
+ ...(options.capabilityCeiling ? { capabilityCeiling: options.capabilityCeiling } : {}),
307
+ ...(capabilityAudit ? { capabilityAudit } : {}),
308
+ }, options.context);
257
309
  const startTime = Date.now();
258
310
  if (options.structuredOutput) {
259
311
  try {
@@ -307,6 +359,7 @@ async function runSingleAttempt(
307
359
  }
308
360
  const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
309
361
  let observedMutationAttempt = false;
362
+ let structuredOutputToolInvoked = false;
310
363
 
311
364
  const exitCode = await new Promise<number>((resolve) => {
312
365
  const spawnSpec = getPiSpawnCommand(args);
@@ -592,7 +645,7 @@ async function runSingleAttempt(
592
645
  result.wrapUpRequested = true;
593
646
  appendRecentOutput(progress, [turnBudgetSoftNote(budget, turnCount)]);
594
647
  }
595
- const decision = turnBudgetDecision(budget, turnCount, terminalAssistantStop, toolWorkActiveOrStarting);
648
+ const decision = turnBudgetDecision(budget, turnCount, terminalAssistantStop, toolWorkActiveOrStarting, options.enforceHardTurnLimit);
596
649
  if (decision === "defer") {
597
650
  result.turnBudget = turnBudgetDeferredState(
598
651
  budget,
@@ -646,7 +699,7 @@ async function runSingleAttempt(
646
699
  const fireUpdate = () => {
647
700
  if (!options.onUpdate || processClosed) return;
648
701
  progress.durationMs = Date.now() - startTime;
649
- const output = (result.timedOut || result.turnBudgetExceeded) && result.finalOutput ? result.finalOutput : getFinalOutput(result.messages);
702
+ const output = (result.timedOut || result.turnBudgetExceeded) && result.finalOutput ? result.finalOutput : getFinalOutput(result.messages ?? []);
650
703
  emitUpdateSnapshot(output || "(running...)");
651
704
  };
652
705
 
@@ -696,6 +749,7 @@ async function runSingleAttempt(
696
749
  const toolArgs = evt.args && typeof evt.args === "object" && !Array.isArray(evt.args)
697
750
  ? evt.args as Record<string, unknown>
698
751
  : {};
752
+ if (options.structuredOutput && evt.toolName === "structured_output") structuredOutputToolInvoked = true;
699
753
  if (options.allowIntercomDetach && (evt.toolName === "intercom" || evt.toolName === "contact_supervisor")) {
700
754
  intercomStarted = true;
701
755
  }
@@ -729,15 +783,20 @@ async function runSingleAttempt(
729
783
  }
730
784
 
731
785
  if (evt.type === "message_end" && evt.message) {
732
- result.messages.push(evt.message);
786
+ result.messages!.push(evt.message);
733
787
  if (evt.message.role === "assistant") {
734
788
  result.usage.turns++;
735
789
  progress.turnCount = result.usage.turns;
736
790
  const stopReason = (evt.message as { stopReason?: string }).stopReason;
737
- const hasToolCall = Array.isArray(evt.message.content)
738
- && evt.message.content.some((part) => (part as { type?: string }).type === "toolCall");
791
+ const toolCalls = Array.isArray(evt.message.content)
792
+ ? evt.message.content.filter((part) => (part as { type?: string }).type === "toolCall")
793
+ : [];
794
+ const hasToolCall = toolCalls.length > 0;
739
795
  const terminalAssistantStop = stopReason === "stop" && !hasToolCall;
740
- updateTurnBudget(result.usage.turns, terminalAssistantStop, hasToolCall || Boolean(progress.currentTool));
796
+ const terminalStructuredOutputCall = Boolean(options.structuredOutput)
797
+ && toolCalls.length === 1
798
+ && (toolCalls[0] as { name?: string }).name === "structured_output";
799
+ updateTurnBudget(result.usage.turns, terminalAssistantStop || terminalStructuredOutputCall, hasToolCall || Boolean(progress.currentTool));
741
800
  const u = evt.message.usage;
742
801
  if (u) {
743
802
  result.usage.input += u.input || 0;
@@ -763,7 +822,7 @@ async function runSingleAttempt(
763
822
  }
764
823
 
765
824
  if (evt.type === "tool_result_end" && evt.message) {
766
- result.messages.push(evt.message);
825
+ result.messages!.push(evt.message);
767
826
  const resultText = extractTextFromContent(evt.message.content);
768
827
  if (options.toolBudget && pendingToolResult && resultText.includes("Tool budget hard limit reached")) {
769
828
  result.toolBudgetBlocked = true;
@@ -1021,37 +1080,43 @@ async function runSingleAttempt(
1021
1080
  result.exitCode = 1;
1022
1081
  }
1023
1082
  if (result.exitCode === 0 && !result.error) {
1024
- const errInfo = detectSubagentError(result.messages);
1025
- if (errInfo.hasError) {
1026
- result.exitCode = errInfo.exitCode ?? 1;
1027
- result.error = errInfo.details
1028
- ? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
1029
- : `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
1030
- }
1031
- }
1032
- if (result.exitCode === 0 && !result.error) {
1033
- const finalText = getFinalOutput(result.messages);
1083
+ const messages = result.messages ?? [];
1084
+ const finalText = getFinalOutput(messages);
1034
1085
  const missingStructuredOutput = options.structuredOutput
1035
1086
  ? !existsSync(options.structuredOutput.outputPath)
1036
1087
  : false;
1037
- if (!finalText?.trim() && (!options.structuredOutput || missingStructuredOutput)) {
1088
+ const errInfo = detectSubagentError(messages);
1089
+ const missingOutput = !finalText?.trim() && (!options.structuredOutput || missingStructuredOutput);
1090
+ if (missingOutput && (!errInfo.hasError || hasEmptyTerminalAssistantResponse(messages))) {
1038
1091
  result.exitCode = 1;
1039
1092
  result.error = "Subagent produced no output (possible model cold-start or empty response).";
1093
+ } else if (errInfo.hasError) {
1094
+ result.exitCode = errInfo.exitCode ?? 1;
1095
+ result.error = errInfo.details
1096
+ ? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
1097
+ : `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
1040
1098
  }
1041
1099
  }
1042
1100
  if (options.structuredOutput && result.exitCode === 0 && !result.error) {
1043
- const structured = readStructuredOutput({
1044
- schema: options.structuredOutput.schema,
1045
- schemaPath: options.structuredOutput.schemaPath,
1046
- outputPath: options.structuredOutput.outputPath,
1047
- });
1048
1101
  result.structuredOutputSchemaPath = options.structuredOutput.schemaPath;
1049
1102
  result.structuredOutputPath = options.structuredOutput.outputPath;
1050
- if (structured.error) {
1103
+ if (!structuredOutputToolInvoked) {
1051
1104
  result.exitCode = 1;
1052
- result.error = structured.error;
1105
+ result.error = MISSING_STRUCTURED_OUTPUT_CALL_ERROR;
1106
+ result.structuredOutputFailed = true;
1053
1107
  } else {
1054
- result.structuredOutput = structured.value;
1108
+ const structured = await readStructuredOutput({
1109
+ schema: options.structuredOutput.schema,
1110
+ schemaPath: options.structuredOutput.schemaPath,
1111
+ outputPath: options.structuredOutput.outputPath,
1112
+ });
1113
+ if (structured.error) {
1114
+ result.exitCode = 1;
1115
+ result.error = structured.error;
1116
+ result.structuredOutputFailed = true;
1117
+ } else {
1118
+ result.structuredOutput = structured.value;
1119
+ }
1055
1120
  }
1056
1121
  }
1057
1122
 
@@ -1070,7 +1135,7 @@ async function runSingleAttempt(
1070
1135
  durationMs: progress.durationMs,
1071
1136
  };
1072
1137
 
1073
- const acceptanceOutput = getFinalOutput(result.messages);
1138
+ const acceptanceOutput = getFinalOutput(result.messages ?? []);
1074
1139
  let fullOutput = stripAcceptanceReport(acceptanceOutput);
1075
1140
  if (result.timedOut) {
1076
1141
  const timeoutMessage = formatTimeoutMessage(options.timeoutMs ?? 0);
@@ -1086,16 +1151,29 @@ async function runSingleAttempt(
1086
1151
  const note = turnBudgetSoftNote(result.turnBudget, result.turnBudget.wrapUpRequestedAtTurn ?? result.turnBudget.turnCount);
1087
1152
  fullOutput = fullOutput.trim() ? `${note}\n\n${fullOutput}` : note;
1088
1153
  }
1089
- const completionGuard = result.exitCode === 0 && !result.error && agent.completionGuard !== false
1154
+ const completionGuardEnabled = isAgentContractV1(options.agentContract) ? agent.completionGuard === true : agent.completionGuard !== false;
1155
+ const completionGuard = result.exitCode === 0 && !result.error && completionGuardEnabled
1090
1156
  ? evaluateCompletionMutationGuard({
1091
1157
  agent: agent.name,
1092
1158
  task: shared.originalTask ?? task,
1093
- messages: result.messages,
1159
+ messages: result.messages ?? [],
1094
1160
  tools: agent.tools,
1095
1161
  mcpDirectTools: agent.mcpDirectTools,
1096
1162
  })
1097
1163
  : undefined;
1098
- if (completionGuard?.triggered && !observedMutationAttempt) {
1164
+ const completionGuardTriggered = completionGuard?.triggered === true && !observedMutationAttempt;
1165
+ if (completionGuard) {
1166
+ result.effects = {
1167
+ ...(result.effects ?? {}),
1168
+ fileMutation: {
1169
+ status: completionGuard.expectedMutation ? completionGuardTriggered ? "missing" : "observed" : "not-applicable",
1170
+ expected: completionGuard.expectedMutation,
1171
+ attempted: completionGuard.attemptedMutation || observedMutationAttempt,
1172
+ ...(completionGuardTriggered ? { message: "Subagent completed without making edits for an implementation task." } : {}),
1173
+ },
1174
+ };
1175
+ }
1176
+ if (completionGuardTriggered && !isAgentContractV1(options.agentContract)) {
1099
1177
  result.exitCode = 1;
1100
1178
  result.error = "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes.";
1101
1179
  progress.status = "failed";
@@ -1154,20 +1232,24 @@ export async function runSync(
1154
1232
  task: string,
1155
1233
  options: RunSyncOptions,
1156
1234
  ): Promise<SingleResult> {
1235
+ options = {
1236
+ ...options,
1237
+ capabilityCeiling: options.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(options.parentSessionId),
1238
+ };
1157
1239
  const agent = agents.find((a) => a.name === agentName);
1158
1240
  if (!agent) {
1159
- return {
1241
+ return withRunContext({
1160
1242
  agent: agentName,
1161
1243
  task,
1162
1244
  exitCode: 1,
1163
1245
  messages: [],
1164
1246
  usage: emptyUsage(),
1165
1247
  error: `Unknown agent: ${agentName}`,
1166
- };
1248
+ }, options.context);
1167
1249
  }
1168
1250
  const outputModeValidationError = validateFileOnlyOutputMode(options.outputMode, options.outputPath, `Single run (${agentName})`);
1169
1251
  if (outputModeValidationError) {
1170
- return {
1252
+ return withRunContext({
1171
1253
  agent: agentName,
1172
1254
  task,
1173
1255
  exitCode: 1,
@@ -1175,7 +1257,7 @@ export async function runSync(
1175
1257
  usage: emptyUsage(),
1176
1258
  outputMode: options.outputMode,
1177
1259
  error: outputModeValidationError,
1178
- };
1260
+ }, options.context);
1179
1261
  }
1180
1262
 
1181
1263
  const shareEnabled = options.share === true;
@@ -1188,8 +1270,9 @@ export async function runSync(
1188
1270
  async: options.acceptanceContext?.async,
1189
1271
  dynamic: options.acceptanceContext?.dynamic,
1190
1272
  dynamicGroup: options.acceptanceContext?.dynamicGroup,
1273
+ agentContract: options.agentContract,
1191
1274
  });
1192
- const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance);
1275
+ const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance, { reportOptional: isAgentContractV1(options.agentContract) });
1193
1276
  const taskWithAcceptance = acceptancePrompt ? `${task}\n${acceptancePrompt}` : task;
1194
1277
  const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
1195
1278
  const skillNames = options.skills ?? agent.skills ?? [];
@@ -1202,14 +1285,14 @@ export async function runSync(
1202
1285
  agent.filePath ? path.dirname(agent.filePath) : skillCwd,
1203
1286
  );
1204
1287
  if (skillNames.some((skill) => skill.trim() === "pi-subagents") && missingSkills.includes("pi-subagents")) {
1205
- return {
1288
+ return withRunContext({
1206
1289
  agent: agentName,
1207
1290
  task,
1208
1291
  exitCode: 1,
1209
1292
  messages: [],
1210
1293
  usage: emptyUsage(),
1211
1294
  error: "Skills not found: pi-subagents",
1212
- };
1295
+ }, options.context);
1213
1296
  }
1214
1297
  let systemPrompt = agent.systemPrompt?.trim() || "";
1215
1298
  if (resolvedSkills.length > 0) {
@@ -1275,7 +1358,14 @@ export async function runSync(
1275
1358
  durationMs: target.progressSummary?.durationMs,
1276
1359
  toolCount: target.progressSummary?.toolCount,
1277
1360
  error: target.error,
1361
+ agentContract: target.agentContract,
1362
+ launchContractDigest: target.launchContractDigest,
1363
+ execution: target.execution,
1278
1364
  acceptance: target.acceptance,
1365
+ capabilityCeiling: target.capabilityCeiling,
1366
+ capabilityAudit: target.capabilityAudit,
1367
+ review: target.review,
1368
+ effects: target.effects,
1279
1369
  ...(transcriptWriter ? { transcriptPath: artifactPathsResult.transcriptPath } : {}),
1280
1370
  transcriptError: target.transcriptError,
1281
1371
  skills: target.skills,
@@ -1299,10 +1389,11 @@ export async function runSync(
1299
1389
  ? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only" }
1300
1390
  : undefined,
1301
1391
  cwd: options.cwd ?? runtimeCwd,
1392
+ reportOptional: isAgentContractV1(options.agentContract),
1302
1393
  });
1303
1394
  const acceptanceFailure = acceptanceFailureMessage(recoveredResult.acceptance);
1304
1395
  stripAcceptanceReportsFromMessages(recoveredResult.messages);
1305
- if (acceptanceFailure && recoveredResult.acceptance.explicit && recoveredResult.exitCode === 0) {
1396
+ if (acceptanceFailure && recoveredResult.acceptance.explicit && recoveredResult.exitCode === 0 && !isAgentContractV1(options.agentContract)) {
1306
1397
  recoveredResult.exitCode = 1;
1307
1398
  recoveredResult.error = recoveredResult.error ? `${recoveredResult.error}\n${acceptanceFailure}` : acceptanceFailure;
1308
1399
  if (recoveredResult.progress) {
@@ -1310,6 +1401,7 @@ export async function runSync(
1310
1401
  recoveredResult.progress.error = recoveredResult.error;
1311
1402
  }
1312
1403
  }
1404
+ if (isAgentContractV1(options.agentContract)) attachContractProjections(recoveredResult);
1313
1405
  persistResultMetadata(recoveredResult);
1314
1406
  options.onDetachedExit?.(recoveredResult);
1315
1407
  })().catch((error) => {
@@ -1336,6 +1428,7 @@ export async function runSync(
1336
1428
  artifactPaths: artifactPathsResult,
1337
1429
  transcriptWriter,
1338
1430
  attemptNotes,
1431
+ modelCandidates: candidates.map((candidate) => applyThinkingSuffix(candidate, options.thinkingOverride ?? agent.thinking, options.thinkingOverride !== undefined)),
1339
1432
  outputSnapshot,
1340
1433
  originalTask: task,
1341
1434
  });
@@ -1366,14 +1459,14 @@ export async function runSync(
1366
1459
  attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
1367
1460
  }
1368
1461
 
1369
- const result = lastResult ?? {
1462
+ const result = withRunContext(lastResult ?? {
1370
1463
  agent: agentName,
1371
1464
  task,
1372
1465
  exitCode: 1,
1373
1466
  messages: [],
1374
1467
  usage: emptyUsage(),
1375
1468
  error: "Subagent did not produce a result.",
1376
- } satisfies SingleResult;
1469
+ } satisfies SingleResult, options.context);
1377
1470
 
1378
1471
  result.usage = aggregateUsage;
1379
1472
  result.attemptedModels = attemptedModels.length > 0 ? attemptedModels : undefined;
@@ -1396,7 +1489,12 @@ export async function runSync(
1396
1489
  if (artifactPathsResult && options.artifactConfig?.enabled !== false) {
1397
1490
  result.artifactPaths = artifactPathsResult;
1398
1491
  if (options.artifactConfig?.includeOutput !== false) {
1399
- writeArtifact(artifactPathsResult.outputPath, artifactOutputByResult.get(result) ?? result.finalOutput ?? "");
1492
+ writeArtifact(artifactPathsResult.outputPath, formatOutputArtifactContent({
1493
+ output: artifactOutputByResult.get(result) ?? result.finalOutput ?? "",
1494
+ error: result.error,
1495
+ transcriptPath: result.transcriptPath,
1496
+ metadataPath: options.artifactConfig?.includeMetadata === false ? undefined : artifactPathsResult.metadataPath,
1497
+ }));
1400
1498
  }
1401
1499
  if (options.maxOutput) {
1402
1500
  const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
@@ -1435,11 +1533,12 @@ export async function runSync(
1435
1533
  ? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only" }
1436
1534
  : undefined,
1437
1535
  cwd: options.cwd ?? runtimeCwd,
1536
+ reportOptional: isAgentContractV1(options.agentContract),
1438
1537
  });
1439
1538
  }
1440
1539
  const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
1441
1540
  stripAcceptanceReportsFromMessages(result.messages);
1442
- if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.detached && !result.interrupted && !result.timedOut) {
1541
+ if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.detached && !result.interrupted && !result.timedOut && !isAgentContractV1(options.agentContract)) {
1443
1542
  result.exitCode = 1;
1444
1543
  result.error = result.error ? `${result.error}\n${acceptanceFailure}` : acceptanceFailure;
1445
1544
  if (result.progress) {
@@ -1447,6 +1546,7 @@ export async function runSync(
1447
1546
  result.progress.error = result.error;
1448
1547
  }
1449
1548
  }
1549
+ if (isAgentContractV1(options.agentContract)) attachContractProjections(result);
1450
1550
  persistResultMetadata(result);
1451
1551
 
1452
1552
  return result;
@@ -0,0 +1,90 @@
1
+ import type { AgentProgress, ForegroundChildControl, ForegroundRunControl } from "../../shared/types.ts";
2
+
3
+ interface BeginForegroundChildInput {
4
+ index: number;
5
+ agent: string;
6
+ description?: string;
7
+ interrupt: () => boolean;
8
+ }
9
+
10
+ function copyProgress(target: ForegroundChildControl, progress: AgentProgress | undefined): void {
11
+ if (!progress) return;
12
+ target.currentActivityState = progress.activityState;
13
+ target.lastActivityAt = progress.lastActivityAt;
14
+ target.currentTool = progress.currentTool;
15
+ target.currentToolStartedAt = progress.currentToolStartedAt;
16
+ target.currentPath = progress.currentPath;
17
+ target.turnCount = progress.turnCount;
18
+ target.tokens = progress.tokens;
19
+ target.toolCount = progress.toolCount;
20
+ }
21
+
22
+ function syncCurrentChild(control: ForegroundRunControl, child: ForegroundChildControl): void {
23
+ control.currentAgent = child.agent;
24
+ control.currentIndex = child.index;
25
+ control.description = child.description;
26
+ control.currentActivityState = child.currentActivityState;
27
+ control.lastActivityAt = child.lastActivityAt;
28
+ control.currentTool = child.currentTool;
29
+ control.currentToolStartedAt = child.currentToolStartedAt;
30
+ control.currentPath = child.currentPath;
31
+ control.turnCount = child.turnCount;
32
+ control.tokens = child.tokens;
33
+ control.toolCount = child.toolCount;
34
+ control.interrupt = child.interrupt;
35
+ control.updatedAt = child.updatedAt;
36
+ }
37
+
38
+ function clearCurrentChild(control: ForegroundRunControl): void {
39
+ control.currentAgent = undefined;
40
+ control.currentIndex = undefined;
41
+ control.currentActivityState = undefined;
42
+ control.lastActivityAt = undefined;
43
+ control.currentTool = undefined;
44
+ control.currentToolStartedAt = undefined;
45
+ control.currentPath = undefined;
46
+ control.turnCount = undefined;
47
+ control.tokens = undefined;
48
+ control.toolCount = undefined;
49
+ control.interrupt = undefined;
50
+ }
51
+
52
+ export function beginForegroundChild(control: ForegroundRunControl, input: BeginForegroundChildInput): void {
53
+ const now = Date.now();
54
+ const child: ForegroundChildControl = {
55
+ index: input.index,
56
+ agent: input.agent,
57
+ ...(input.description ? { description: input.description } : {}),
58
+ startedAt: now,
59
+ updatedAt: now,
60
+ };
61
+ child.interrupt = () => {
62
+ if (!input.interrupt()) return false;
63
+ child.currentActivityState = undefined;
64
+ child.updatedAt = Date.now();
65
+ syncCurrentChild(control, child);
66
+ return true;
67
+ };
68
+ control.activeChildren ??= new Map();
69
+ control.activeChildren.set(input.index, child);
70
+ syncCurrentChild(control, child);
71
+ }
72
+
73
+ export function updateForegroundChild(control: ForegroundRunControl, index: number, progress: AgentProgress | undefined): void {
74
+ const child = control.activeChildren?.get(index);
75
+ if (!child) return;
76
+ copyProgress(child, progress);
77
+ child.updatedAt = Date.now();
78
+ syncCurrentChild(control, child);
79
+ }
80
+
81
+ export function finishForegroundChild(control: ForegroundRunControl, index: number): void {
82
+ control.activeChildren?.delete(index);
83
+ if (control.currentIndex === index) {
84
+ const next = [...(control.activeChildren?.values() ?? [])]
85
+ .sort((left, right) => right.updatedAt - left.updatedAt || left.index - right.index)[0];
86
+ if (next) syncCurrentChild(control, next);
87
+ else clearCurrentChild(control);
88
+ }
89
+ control.updatedAt = Date.now();
90
+ }