pi-subagents 0.47.0 → 0.48.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 +37 -0
  2. package/README.md +2 -0
  3. package/agents/reviewer.md +3 -4
  4. package/docs/configuration.md +46 -2
  5. package/docs/observability.md +1 -1
  6. package/docs/tool-reference.md +1 -1
  7. package/package.json +1 -1
  8. package/src/agents/agents.ts +9 -3
  9. package/src/extension/config.ts +16 -0
  10. package/src/extension/doctor.ts +40 -0
  11. package/src/extension/index.ts +30 -11
  12. package/src/extension/public-execution.ts +5 -0
  13. package/src/extension/rpc.ts +7 -9
  14. package/src/extension/schemas.ts +4 -4
  15. package/src/intercom/intercom-bridge.ts +4 -1
  16. package/src/intercom/native-supervisor-channel.ts +102 -4
  17. package/src/missions/lifecycle.ts +4 -7
  18. package/src/missions/store.ts +4 -4
  19. package/src/missions/workflow-state.ts +6 -2
  20. package/src/runs/background/active-async-capacity.ts +374 -0
  21. package/src/runs/background/active-run-index.ts +46 -0
  22. package/src/runs/background/async-execution.ts +117 -29
  23. package/src/runs/background/async-job-tracker.ts +279 -134
  24. package/src/runs/background/async-resume.ts +7 -1
  25. package/src/runs/background/async-status.ts +33 -4
  26. package/src/runs/background/chain-append.ts +33 -15
  27. package/src/runs/background/control-channel.ts +55 -17
  28. package/src/runs/background/owned-process-tree.ts +104 -0
  29. package/src/runs/background/process-terminal.ts +17 -3
  30. package/src/runs/background/result-watcher.ts +87 -6
  31. package/src/runs/background/run-status.ts +23 -1
  32. package/src/runs/background/scheduled-runs.ts +23 -0
  33. package/src/runs/background/stale-run-reconciler.ts +10 -3
  34. package/src/runs/background/subagent-runner.ts +75 -33
  35. package/src/runs/foreground/async-dismiss-action.ts +85 -0
  36. package/src/runs/foreground/async-steering-action.ts +6 -7
  37. package/src/runs/foreground/chain-execution.ts +37 -2
  38. package/src/runs/foreground/execution.ts +90 -19
  39. package/src/runs/foreground/foreground-control.ts +12 -0
  40. package/src/runs/foreground/prompt-audit.ts +171 -0
  41. package/src/runs/foreground/subagent-executor.ts +648 -195
  42. package/src/runs/shared/acceptance.ts +13 -4
  43. package/src/runs/shared/llm-intent-arbiter.ts +286 -0
  44. package/src/runs/shared/parallel-utils.ts +2 -0
  45. package/src/runs/shared/pi-args.ts +44 -1
  46. package/src/runs/shared/run-fanout-budget.ts +280 -0
  47. package/src/runs/shared/single-output.ts +4 -2
  48. package/src/runs/shared/subagent-prompt-runtime.ts +24 -5
  49. package/src/runs/shared/task-intent.ts +19 -3
  50. package/src/runs/shared/worktree.ts +17 -5
  51. package/src/shared/artifacts.ts +1 -1
  52. package/src/shared/file-coalescer.ts +9 -0
  53. package/src/shared/types.ts +97 -1
  54. package/src/shared/utils.ts +3 -1
  55. package/src/tui/fleet-status.ts +7 -5
  56. package/src/tui/fleet.ts +225 -12
  57. package/src/workflows/scripted-workflow.ts +26 -6
@@ -34,6 +34,7 @@ import {
34
34
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
35
35
  import { INTERCOM_BRIDGE_MARKER } from "../../intercom/intercom-bridge.ts";
36
36
  import { runSync } from "./execution.ts";
37
+ import { createTaskMutationArbiter } from "../shared/llm-intent-arbiter.ts";
37
38
  import { workflowForegroundSteeringLaunchOptions } from "./workflow-foreground-steering.ts";
38
39
  import {
39
40
  beginForegroundChild,
@@ -42,6 +43,7 @@ import {
42
43
  settleForegroundSchedulingOwner,
43
44
  updateForegroundChild,
44
45
  } from "./foreground-control.ts";
46
+ import { updateLiveEffectivePrompt } from "./prompt-audit.ts";
45
47
  import { buildChainSummary } from "../../shared/formatters.ts";
46
48
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
47
49
  import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
@@ -71,6 +73,7 @@ import {
71
73
  type ResolvedControlConfig,
72
74
  type ResolvedTurnBudget,
73
75
  type ResolvedToolBudget,
76
+ type RunFanoutBudgetDescriptor,
74
77
  type SingleResult,
75
78
  type ToolBudgetConfig,
76
79
  type ChainCheckpointState,
@@ -86,6 +89,7 @@ import { ChainOutputValidationError, outputEntryFromResult, resolveOutputReferen
86
89
  import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
87
90
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection, type DynamicCollectedResult } from "../shared/dynamic-fanout.ts";
88
91
  import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, resolveEffectiveAcceptance } from "../shared/acceptance.ts";
92
+ import { claimRunFanoutBatch, RunFanoutLimitError } from "../shared/run-fanout-budget.ts";
89
93
  import { isAgentContractV1 } from "../shared/agent-contract.ts";
90
94
  import type { ChainOutputMap } from "../../shared/types.ts";
91
95
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
@@ -167,6 +171,7 @@ interface ParallelChainRunInput {
167
171
  configToolBudget?: ToolBudgetConfig;
168
172
  globalSemaphore?: Semaphore;
169
173
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
174
+ runFanoutBudget?: RunFanoutBudgetDescriptor;
170
175
  permissions?: PermissionConfig;
171
176
  dynamic?: boolean;
172
177
  }
@@ -368,7 +373,11 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
368
373
  beginForegroundChild(input.foregroundControl, {
369
374
  index: childIndex,
370
375
  agent: task.agent,
371
- description: cleanTask.trim(),
376
+ authoredTask: cleanTask,
377
+ effectivePrompt: taskStr,
378
+ cwd: taskCwd,
379
+ ...(outputPath ? { outputPath } : {}),
380
+ description: `${task.agent} child`,
372
381
  ...(effectiveModel ? { model: effectiveModel } : {}),
373
382
  ...(thinking ? { thinking } : {}),
374
383
  interrupt: () => {
@@ -388,8 +397,10 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
388
397
  result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
389
398
  permissions: input.permissions,
390
399
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
400
+ llmIntentArbiter: createTaskMutationArbiter(input.ctx),
391
401
  ...workflowForegroundSteeringLaunchOptions(input.foregroundControl, childIndex),
392
402
  capabilityCeiling: input.capabilityCeiling,
403
+ runFanoutBudget: input.runFanoutBudget ? { ...input.runFanoutBudget, parentPath: `${input.runFanoutBudget.parentPath ? `${input.runFanoutBudget.parentPath}/` : ""}chain[${input.stepIndex}]${input.dynamic ? `.expand[${taskIndex}]` : `.parallel[${taskIndex}]`}` } : undefined,
393
404
  context: input.contextForAgent?.(task.agent),
394
405
  cwd: taskCwd,
395
406
  signal: input.signal,
@@ -422,6 +433,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
422
433
  agentContract,
423
434
  acceptance: task.acceptance,
424
435
  acceptanceContext: { mode: "chain", dynamic: input.dynamic && task.acceptance === undefined },
436
+ onEffectivePrompt: input.foregroundControl ? (prompt) => updateLiveEffectivePrompt(input.foregroundControl!, childIndex, prompt) : undefined,
425
437
  timeoutMs: input.timeoutMs,
426
438
  deadlineAt: input.deadlineAt,
427
439
  turnBudget: input.turnBudget,
@@ -540,6 +552,7 @@ interface ChainExecutionParams {
540
552
  /** Global cap on simultaneously-running tasks within this chain. Defaults to DEFAULT_GLOBAL_CONCURRENCY_LIMIT. */
541
553
  globalConcurrencyLimit?: number;
542
554
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
555
+ runFanoutBudget?: RunFanoutBudgetDescriptor;
543
556
  }
544
557
 
545
558
  interface ChainExecutionResult {
@@ -876,6 +889,7 @@ ${step.message}` : ""}` }],
876
889
  configToolBudget: params.configToolBudget,
877
890
  globalSemaphore,
878
891
  permissions: params.permissions,
892
+ runFanoutBudget: params.runFanoutBudget,
879
893
  });
880
894
  globalTaskIndex += step.parallel.length;
881
895
 
@@ -1002,6 +1016,19 @@ ${step.message}` : ""}` }],
1002
1016
  return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
1003
1017
  }
1004
1018
 
1019
+ try {
1020
+ if (params.runFanoutBudget) claimRunFanoutBatch(params.runFanoutBudget, materialized.parallel.map((_, itemIndex) => `chain[${stepIndex}].expand[${itemIndex}]`));
1021
+ } catch (error) {
1022
+ const message = error instanceof Error ? error.message : String(error);
1023
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
1024
+ const result = buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
1025
+ if (error instanceof RunFanoutLimitError) {
1026
+ result.details.runFanoutBudget = error.snapshot;
1027
+ result.details.runFanoutRejection = error.rejection;
1028
+ }
1029
+ return result;
1030
+ }
1031
+
1005
1032
  dynamicChildren[stepIndex] = materialized.items.map((item, itemIndex) => ({
1006
1033
  agent: step.parallel.agent,
1007
1034
  label: materialized.parallel[itemIndex]?.label,
@@ -1137,6 +1164,7 @@ ${step.message}` : ""}` }],
1137
1164
  configToolBudget: params.configToolBudget,
1138
1165
  globalSemaphore,
1139
1166
  permissions: params.permissions,
1167
+ runFanoutBudget: params.runFanoutBudget,
1140
1168
  dynamic: true,
1141
1169
  });
1142
1170
  globalTaskIndex = dynamicStartIndex + reservedDynamicItems;
@@ -1332,7 +1360,11 @@ ${step.message}` : ""}` }],
1332
1360
  beginForegroundChild(foregroundControl, {
1333
1361
  index: childIndex,
1334
1362
  agent: seqStep.agent,
1335
- description: cleanTask.trim(),
1363
+ authoredTask: cleanTask,
1364
+ effectivePrompt: stepTask,
1365
+ cwd,
1366
+ ...(outputPath ? { outputPath } : {}),
1367
+ description: `${seqStep.agent} child`,
1336
1368
  ...(effectiveModel ? { model: effectiveModel } : {}),
1337
1369
  ...(thinking ? { thinking } : {}),
1338
1370
  interrupt: () => {
@@ -1369,8 +1401,10 @@ ${step.message}` : ""}` }],
1369
1401
  r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
1370
1402
  permissions: params.permissions,
1371
1403
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
1404
+ llmIntentArbiter: createTaskMutationArbiter(ctx),
1372
1405
  ...workflowForegroundSteeringLaunchOptions(params.foregroundControl, childIndex),
1373
1406
  capabilityCeiling: params.capabilityCeiling,
1407
+ runFanoutBudget: params.runFanoutBudget ? { ...params.runFanoutBudget, parentPath: `${params.runFanoutBudget.parentPath ? `${params.runFanoutBudget.parentPath}/` : ""}chain[${stepIndex}]` } : undefined,
1374
1408
  context: params.contextForAgent?.(seqStep.agent),
1375
1409
  cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
1376
1410
  signal,
@@ -1403,6 +1437,7 @@ ${step.message}` : ""}` }],
1403
1437
  agentContract,
1404
1438
  acceptance: seqStep.acceptance,
1405
1439
  acceptanceContext: { mode: "chain" },
1440
+ onEffectivePrompt: foregroundControl ? (prompt) => updateLiveEffectivePrompt(foregroundControl, childIndex, prompt) : undefined,
1406
1441
  timeoutMs: params.timeoutMs,
1407
1442
  deadlineAt,
1408
1443
  turnBudget: params.turnBudget,
@@ -54,18 +54,19 @@ import {
54
54
  import { buildSkillInjection, resolveSkillsWithFallback } from "../../agents/skills.ts";
55
55
  import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
56
56
  import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
57
+ import { arbitrateCompletionGuardRescue } from "../shared/llm-intent-arbiter.ts";
57
58
  import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
58
59
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
59
60
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
60
61
  import { resolvePermissionRules } from "../shared/permissions.ts";
61
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
62
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
62
63
  import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
63
64
  import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
64
65
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
65
66
  import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput } from "../shared/structured-output.ts";
66
67
  import { formatProcessSignalError, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
67
68
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
68
- import { captureSingleOutputSnapshot, extractChildWrittenOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
69
+ import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
69
70
  import {
70
71
  buildModelCandidates,
71
72
  formatModelAttemptNote,
@@ -90,6 +91,7 @@ import {
90
91
  summarizeRecentMutatingFailures,
91
92
  } from "../shared/long-running-guard.ts";
92
93
  import { acceptanceFailureMessage, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport, validateAcceptanceInput } from "../shared/acceptance.ts";
94
+ import { PROMPT_REDACTED } from "../../shared/utils.ts";
93
95
  import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
94
96
  import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
95
97
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
@@ -117,6 +119,12 @@ function withRunContext<T extends SingleResult>(result: T, context: RunSyncOptio
117
119
  return result;
118
120
  }
119
121
 
122
+ function redactResultPrompt<T extends SingleResult>(result: T): T {
123
+ result.task = PROMPT_REDACTED;
124
+ if (result.progress) result.progress.task = PROMPT_REDACTED;
125
+ return result;
126
+ }
127
+
120
128
  function sumUsage(target: Usage, source: Usage): void {
121
129
  target.input += source.input;
122
130
  target.output += source.output;
@@ -139,7 +147,7 @@ function persistSingleResultMetadata(input: {
139
147
  writeMetadata(input.metadataPath, {
140
148
  runId: input.runId,
141
149
  agent: input.agent,
142
- task: input.task,
150
+ task: PROMPT_REDACTED,
143
151
  exitCode: target.exitCode,
144
152
  processSignal: target.processSignal,
145
153
  usage: target.usage,
@@ -231,6 +239,7 @@ function stripAcceptanceReportsFromMessages(messages: Message[] | undefined): vo
231
239
  function snapshotProgress(progress: AgentProgress): AgentProgress {
232
240
  return {
233
241
  ...progress,
242
+ task: PROMPT_REDACTED,
234
243
  skills: progress.skills ? [...progress.skills] : undefined,
235
244
  recentTools: boundStreamedRecentTools(progress.recentTools),
236
245
  recentOutput: boundStreamedRecentOutput(progress.recentOutput),
@@ -240,6 +249,7 @@ function snapshotProgress(progress: AgentProgress): AgentProgress {
240
249
  function snapshotResult(result: SingleResult, progress: AgentProgress): SingleResult {
241
250
  return {
242
251
  ...result,
252
+ task: PROMPT_REDACTED,
243
253
  messages: result.outputMode === "file-only" && result.savedOutputPath ? undefined : result.messages ? [...result.messages] : undefined,
244
254
  usage: { ...result.usage },
245
255
  skills: result.skills ? [...result.skills] : undefined,
@@ -291,6 +301,7 @@ async function runSingleAttempt(
291
301
  attemptNotes: string[];
292
302
  outputSnapshot?: SingleOutputSnapshot;
293
303
  originalTask?: string;
304
+ taskDelivery?: SubagentTaskDelivery;
294
305
  },
295
306
  ): Promise<SingleResult> {
296
307
  const effectiveThinking = options.thinkingOverride ?? agent.thinking;
@@ -312,6 +323,7 @@ async function runSingleAttempt(
312
323
  const { args, env: sharedEnv, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit } = buildPiArgs({
313
324
  baseArgs: ["--mode", "json", "-p"],
314
325
  task,
326
+ taskDelivery: shared.taskDelivery,
315
327
  sessionEnabled: shared.sessionEnabled,
316
328
  sessionDir: options.sessionDir,
317
329
  sessionFile: options.sessionFile,
@@ -337,6 +349,7 @@ async function runSingleAttempt(
337
349
  parentControlInbox: options.nestedRoute?.controlInbox,
338
350
  parentRootRunId: options.nestedRoute?.rootRunId,
339
351
  parentCapabilityToken: options.nestedRoute?.capabilityToken,
352
+ runFanoutBudget: options.runFanoutBudget,
340
353
  parentSessionId: options.parentSessionId,
341
354
  steerInboxDir: options.steerInboxDir,
342
355
  steerCapabilityPath: options.steerCapabilityPath,
@@ -1277,15 +1290,38 @@ async function runSingleAttempt(
1277
1290
  mcpDirectTools: agent.mcpDirectTools,
1278
1291
  })
1279
1292
  : undefined;
1280
- const completionGuardTriggered = completionGuard?.triggered === true && !observedMutationAttempt;
1293
+ let completionGuardTriggered = completionGuard?.triggered === true && !observedMutationAttempt;
1294
+ // The classifier is deliberately narrow, so a read-only review task can
1295
+ // still be misread as implementation. Arbitrate BEFORE any failure side
1296
+ // effect is published (effects, exit code, progress, notifications,
1297
+ // acceptance, output persistence): only a confident read-only verdict
1298
+ // rescues, and the task text alone is evidence — never the child's own
1299
+ // final message.
1300
+ let arbiterRescued = false;
1301
+ if (completionGuardTriggered) {
1302
+ const arbitration = await arbitrateCompletionGuardRescue({
1303
+ guardTriggered: true,
1304
+ task: shared.originalTask ?? task,
1305
+ arbiter: options.llmIntentArbiter,
1306
+ });
1307
+ completionGuardTriggered = arbitration.triggered;
1308
+ arbiterRescued = arbitration.rescued;
1309
+ }
1281
1310
  if (completionGuard) {
1282
1311
  result.effects = {
1283
1312
  ...(result.effects ?? {}),
1284
1313
  fileMutation: {
1285
- status: completionGuard.expectedMutation ? completionGuardTriggered ? "missing" : "observed" : "not-applicable",
1314
+ status: completionGuard.expectedMutation
1315
+ ? completionGuardTriggered
1316
+ ? "missing"
1317
+ : arbiterRescued
1318
+ ? "not-applicable"
1319
+ : "observed"
1320
+ : "not-applicable",
1286
1321
  expected: completionGuard.expectedMutation,
1287
1322
  attempted: completionGuard.attemptedMutation || observedMutationAttempt,
1288
1323
  ...(completionGuardTriggered ? { message: "Subagent completed without making edits for an implementation task." } : {}),
1324
+ ...(arbiterRescued ? { resolvedBy: "llm-intent-arbiter" } : {}),
1289
1325
  },
1290
1326
  };
1291
1327
  }
@@ -1355,7 +1391,7 @@ async function runSyncCompletion(
1355
1391
  };
1356
1392
  const agent = agents.find((a) => a.name === agentName);
1357
1393
  if (!agent) {
1358
- return withRunContext({
1394
+ return redactResultPrompt(withRunContext({
1359
1395
  index: options.index ?? 0,
1360
1396
  agent: agentName,
1361
1397
  task,
@@ -1363,12 +1399,12 @@ async function runSyncCompletion(
1363
1399
  messages: [],
1364
1400
  usage: emptyUsage(),
1365
1401
  error: `Unknown agent: ${agentName}`,
1366
- }, options.context);
1402
+ }, options.context));
1367
1403
  }
1368
1404
  try {
1369
1405
  assertAgentAllowedByCapabilityCeiling(agent.name, options.capabilityCeiling);
1370
1406
  } catch (error) {
1371
- return withRunContext({
1407
+ return redactResultPrompt(withRunContext({
1372
1408
  index: options.index ?? 0,
1373
1409
  agent: agent.name,
1374
1410
  task,
@@ -1377,11 +1413,11 @@ async function runSyncCompletion(
1377
1413
  usage: emptyUsage(),
1378
1414
  error: error instanceof Error ? error.message : String(error),
1379
1415
  ...(options.capabilityCeiling ? { capabilityCeiling: options.capabilityCeiling } : {}),
1380
- }, options.context);
1416
+ }, options.context));
1381
1417
  }
1382
1418
  const acceptanceErrors = validateAcceptanceInput(options.acceptance);
1383
1419
  if (acceptanceErrors.length > 0) {
1384
- return withRunContext({
1420
+ return redactResultPrompt(withRunContext({
1385
1421
  index: options.index ?? 0,
1386
1422
  agent: agentName,
1387
1423
  task,
@@ -1389,11 +1425,11 @@ async function runSyncCompletion(
1389
1425
  messages: [],
1390
1426
  usage: emptyUsage(),
1391
1427
  error: acceptanceErrors.join(" "),
1392
- }, options.context);
1428
+ }, options.context));
1393
1429
  }
1394
1430
  const outputModeValidationError = validateFileOnlyOutputMode(options.outputMode, options.outputPath, `Single run (${agentName})`);
1395
1431
  if (outputModeValidationError) {
1396
- return withRunContext({
1432
+ return redactResultPrompt(withRunContext({
1397
1433
  index: options.index ?? 0,
1398
1434
  agent: agentName,
1399
1435
  task,
@@ -1402,7 +1438,7 @@ async function runSyncCompletion(
1402
1438
  usage: emptyUsage(),
1403
1439
  outputMode: options.outputMode,
1404
1440
  error: outputModeValidationError,
1405
- }, options.context);
1441
+ }, options.context));
1406
1442
  }
1407
1443
 
1408
1444
  const shareEnabled = options.share === true;
@@ -1419,6 +1455,7 @@ async function runSyncCompletion(
1419
1455
  });
1420
1456
  const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance, { reportOptional: isAgentContractV1(options.agentContract) });
1421
1457
  const taskWithAcceptance = acceptancePrompt ? `${task}\n${acceptancePrompt}` : task;
1458
+ options.onEffectivePrompt?.(taskWithAcceptance);
1422
1459
  const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
1423
1460
  if (options.context === "fork" && options.sessionFile && existsSync(options.sessionFile)) {
1424
1461
  alignForkedSessionCwd(options.sessionFile, options.cwd ?? runtimeCwd);
@@ -1433,7 +1470,7 @@ async function runSyncCompletion(
1433
1470
  agent.filePath ? path.dirname(agent.filePath) : skillCwd,
1434
1471
  );
1435
1472
  if (skillNames.some((skill) => skill.trim() === "pi-subagents") && missingSkills.includes("pi-subagents")) {
1436
- return withRunContext({
1473
+ return redactResultPrompt(withRunContext({
1437
1474
  index: options.index ?? 0,
1438
1475
  agent: agentName,
1439
1476
  task,
@@ -1441,7 +1478,7 @@ async function runSyncCompletion(
1441
1478
  messages: [],
1442
1479
  usage: emptyUsage(),
1443
1480
  error: "Skills not found: pi-subagents",
1444
- }, options.context);
1481
+ }, options.context));
1445
1482
  }
1446
1483
  let systemPrompt = agent.systemPrompt?.trim() || "";
1447
1484
  if (resolvedSkills.length > 0) {
@@ -1476,7 +1513,7 @@ async function runSyncCompletion(
1476
1513
  artifactPathsResult = getArtifactPaths(options.artifactsDir, options.runId, agentName, options.index);
1477
1514
  ensureArtifactsDir(options.artifactsDir);
1478
1515
  if (options.artifactConfig?.includeInput !== false) {
1479
- writeArtifact(artifactPathsResult.inputPath, `# Task for ${agentName}\n\n${taskWithAcceptance}`);
1516
+ writeArtifact(artifactPathsResult.inputPath, `# Task for ${agentName}\n\n${PROMPT_REDACTED}; live Prompt Audit only.\n`);
1480
1517
  }
1481
1518
  if (options.artifactConfig?.includeJsonl !== false) {
1482
1519
  jsonlPath = artifactPathsResult.jsonlPath;
@@ -1490,7 +1527,7 @@ async function runSyncCompletion(
1490
1527
  childIndex: options.index,
1491
1528
  cwd: options.cwd ?? runtimeCwd,
1492
1529
  });
1493
- transcriptWriter.writeInitialUserMessage(taskWithAcceptance);
1530
+ transcriptWriter.writeInitialUserMessage(`${PROMPT_REDACTED}; live Prompt Audit only.`);
1494
1531
  }
1495
1532
  }
1496
1533
 
@@ -1526,6 +1563,9 @@ async function runSyncCompletion(
1526
1563
  };
1527
1564
  let lastResult: SingleResult | undefined;
1528
1565
  const modelsToTry = candidates.length > 0 ? candidates : [undefined];
1566
+ // Escalated to "file" after an unexplained zero-activity startup failure so
1567
+ // retries keep the task text out of argv (endpoint pre-exec scans may deny it).
1568
+ let taskDeliveryOverride: SubagentTaskDelivery | undefined;
1529
1569
  modelAttemptsLoop: for (let modelIndex = 0; modelIndex < modelsToTry.length; modelIndex++) {
1530
1570
  const candidate = modelsToTry[modelIndex];
1531
1571
  for (let startupAttemptIndex = 0; ; startupAttemptIndex++) {
@@ -1544,6 +1584,7 @@ async function runSyncCompletion(
1544
1584
  .filter((modelCandidate): modelCandidate is string => Boolean(modelCandidate)),
1545
1585
  outputSnapshot,
1546
1586
  originalTask: task,
1587
+ taskDelivery: taskDeliveryOverride,
1547
1588
  });
1548
1589
  lastResult = result;
1549
1590
  if (startupAttemptIndex === 0) {
@@ -1612,6 +1653,10 @@ async function runSyncCompletion(
1612
1653
  }
1613
1654
  break modelAttemptsLoop;
1614
1655
  }
1656
+ if (!taskDeliveryOverride && result.processSignal === "SIGKILL") {
1657
+ taskDeliveryOverride = "file";
1658
+ attemptNotes.push("[startup-retry] retrying with file task delivery to keep the task text out of the child process argv.");
1659
+ }
1615
1660
  attempt.error = retryNote;
1616
1661
  attemptNotes.push(retryNote);
1617
1662
  continue;
@@ -1726,13 +1771,39 @@ async function runSyncCompletion(
1726
1771
  stripAcceptanceReportsFromMessages(result.messages);
1727
1772
  if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.interrupted && !result.timedOut && !isAgentContractV1(options.agentContract)) {
1728
1773
  result.exitCode = 1;
1774
+ if (result.savedOutputPath) {
1775
+ result.finalOutput = finalizeSingleOutput({
1776
+ fullOutput: result.finalOutput ?? "",
1777
+ outputPath: options.outputPath,
1778
+ outputMode: result.outputMode,
1779
+ exitCode: result.exitCode,
1780
+ preserveSavedOutput: true,
1781
+ savedPath: result.savedOutputPath,
1782
+ outputReference: result.outputReference,
1783
+ }).displayOutput;
1784
+ artifactOutputByResult.set(result, result.finalOutput);
1785
+ }
1729
1786
  result.error = result.error ? `${result.error}\n${acceptanceFailure}` : acceptanceFailure;
1787
+ if (artifactPathsResult && options.artifactConfig?.enabled !== false && options.artifactConfig?.includeOutput !== false) {
1788
+ try {
1789
+ writeArtifact(artifactPathsResult.outputPath, formatOutputArtifactContent({
1790
+ output: artifactOutputByResult.get(result) ?? result.finalOutput ?? "",
1791
+ error: result.error,
1792
+ transcriptPath: result.transcriptPath,
1793
+ metadataPath: options.artifactConfig?.includeMetadata === false ? undefined : artifactPathsResult.metadataPath,
1794
+ }));
1795
+ } catch (error) {
1796
+ const message = `Artifact output post-processing failed: ${error instanceof Error ? error.message : String(error)}`;
1797
+ result.outputSaveError = result.outputSaveError ? `${result.outputSaveError}\n${message}` : message;
1798
+ }
1799
+ }
1730
1800
  if (result.progress) {
1731
1801
  result.progress.status = "failed";
1732
1802
  result.progress.error = result.error;
1733
1803
  }
1734
1804
  }
1735
1805
  if (isAgentContractV1(options.agentContract)) attachContractProjections(result);
1806
+ redactResultPrompt(result);
1736
1807
  try {
1737
1808
  persistResultMetadata(result);
1738
1809
  } catch (error) {
@@ -1817,7 +1888,7 @@ export async function runSync(
1817
1888
  durationMs: publishedReceipt.progressSummary?.durationMs ?? 0,
1818
1889
  error: failureMessage,
1819
1890
  };
1820
- const failedResult: SingleResult = {
1891
+ const failedResult: SingleResult = redactResultPrompt({
1821
1892
  ...publishedReceipt,
1822
1893
  detached: undefined,
1823
1894
  detachedReason,
@@ -1833,7 +1904,7 @@ export async function runSync(
1833
1904
  acceptance: publishedReceipt.acceptance
1834
1905
  ? buildSkippedAcceptanceLedger(publishedReceipt.acceptance.effectiveAcceptance, { id: "completion-pipeline", message: failureMessage })
1835
1906
  : undefined,
1836
- };
1907
+ });
1837
1908
  if (strictContract) attachContractProjections(failedResult);
1838
1909
  try {
1839
1910
  // Replace the provisional detach receipt metadata with the authoritative
@@ -1,9 +1,15 @@
1
1
  import type { AgentProgress, ForegroundChildControl, ForegroundRunControl } from "../../shared/types.ts";
2
+ import { registerLivePromptAudit, removeLivePromptAudit, type PromptAuditRerunContract } from "./prompt-audit.ts";
2
3
 
3
4
  interface BeginForegroundChildInput {
4
5
  index: number;
5
6
  agent: string;
6
7
  description?: string;
8
+ authoredTask: string;
9
+ effectivePrompt: string;
10
+ cwd?: string;
11
+ outputPath?: string;
12
+ rerun?: PromptAuditRerunContract;
7
13
  model?: string;
8
14
  thinking?: string;
9
15
  interrupt: () => boolean;
@@ -109,6 +115,11 @@ export function beginForegroundChild(control: ForegroundRunControl, input: Begin
109
115
  }
110
116
  control.activeChildren ??= new Map();
111
117
  control.activeChildren.set(input.index, child);
118
+ registerLivePromptAudit(control, input.index, input.authoredTask, input.effectivePrompt, {
119
+ ...(input.cwd ? { cwd: input.cwd } : {}),
120
+ ...(input.outputPath ? { outputPath: input.outputPath } : {}),
121
+ ...(input.rerun ? { rerun: input.rerun } : {}),
122
+ });
112
123
  syncCurrentChild(control, child);
113
124
  }
114
125
 
@@ -121,6 +132,7 @@ export function updateForegroundChild(control: ForegroundRunControl, index: numb
121
132
  }
122
133
 
123
134
  export function finishForegroundChild(control: ForegroundRunControl, index: number): void {
135
+ removeLivePromptAudit(control, index);
124
136
  control.activeChildren?.delete(index);
125
137
  if (control.currentIndex === index) {
126
138
  const next = [...(control.activeChildren?.values() ?? [])]
@@ -0,0 +1,171 @@
1
+ import { Agent, type StreamFn, type ThinkingLevel } from "@earendil-works/pi-agent-core";
2
+ import { convertToLlm, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { streamSimple } from "@earendil-works/pi-ai/compat";
4
+ import type { Model } from "@earendil-works/pi-ai";
5
+ import type { ForegroundRunControl } from "../../shared/types.ts";
6
+ export type PromptAuditView = "authored" | "runtime" | "effective";
7
+
8
+ export interface PromptAuditRerunContract {
9
+ params: Record<string, unknown>;
10
+ }
11
+
12
+ export interface LivePromptAudit {
13
+ authoredTask: string;
14
+ runtimeAdditions: string;
15
+ finalEffectivePrompt: string;
16
+ cwd?: string;
17
+ outputPath?: string;
18
+ rerun?: PromptAuditRerunContract;
19
+ }
20
+
21
+ const livePrompts = new WeakMap<ForegroundRunControl, Map<number, LivePromptAudit>>();
22
+
23
+ type RegistryModel = Model<any>;
24
+
25
+ function fullModelId(model: Pick<RegistryModel, "provider" | "id">): string {
26
+ return `${model.provider}/${model.id}`;
27
+ }
28
+
29
+ function textContent(value: unknown): string {
30
+ if (typeof value === "string") return value;
31
+ if (!Array.isArray(value)) return "";
32
+ return value.map((item) => item && typeof item === "object" && (item as { type?: unknown }).type === "text" ? String((item as { text?: unknown }).text ?? "") : "").join("");
33
+ }
34
+
35
+ function finalAssistantText(agent: Agent): string {
36
+ for (let index = agent.state.messages.length - 1; index >= 0; index--) {
37
+ const message = agent.state.messages[index];
38
+ if (message && typeof message === "object" && (message as { role?: unknown }).role === "assistant") {
39
+ return textContent((message as { content?: unknown }).content).trim();
40
+ }
41
+ }
42
+ return "";
43
+ }
44
+
45
+ async function resolveRewriteAuth(ctx: ExtensionContext, model: RegistryModel): Promise<{ apiKey?: string; headers?: Record<string, string>; env?: Record<string, string> }> {
46
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
47
+ if (auth.ok === false) throw new Error(`Prompt redo model auth failed for ${fullModelId(model)}: ${auth.error}`);
48
+ return {
49
+ ...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
50
+ ...(auth.headers ? { headers: auth.headers } : {}),
51
+ ...(auth.env ? { env: auth.env } : {}),
52
+ };
53
+ }
54
+
55
+ export async function rewritePromptWithGuidance(input: {
56
+ ctx: ExtensionContext;
57
+ authoredTask: string;
58
+ runtimeAdditions: string;
59
+ finalEffectivePrompt: string;
60
+ guidance: string;
61
+ signal?: AbortSignal;
62
+ streamFn?: StreamFn;
63
+ }): Promise<string> {
64
+ const model = input.ctx.model;
65
+ if (!model) throw new Error("Prompt redo needs the current session model to rewrite the authored task.");
66
+ const auth = await resolveRewriteAuth(input.ctx, model);
67
+ const registeredProvider = (input.ctx.modelRegistry as {
68
+ getRegisteredProviderConfig?: (provider: string) => { api?: string; streamSimple?: StreamFn } | undefined;
69
+ }).getRegisteredProviderConfig?.(model.provider);
70
+ const baseStreamFn = input.streamFn ?? (registeredProvider?.streamSimple && registeredProvider.api === model.api
71
+ ? registeredProvider.streamSimple
72
+ : streamSimple);
73
+ const streamFn: StreamFn = (nextModel, context, streamOptions) => baseStreamFn(nextModel, context, {
74
+ ...streamOptions,
75
+ ...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
76
+ env: auth.env || streamOptions?.env ? { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } : undefined,
77
+ headers: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
78
+ });
79
+ const ctxThinking = (input.ctx as { getThinkingLevel?: () => ThinkingLevel }).getThinkingLevel?.();
80
+ const agent = new Agent({
81
+ initialState: {
82
+ systemPrompt: [
83
+ "Rewrite one subagent authored task from Prompt Audit context.",
84
+ "Return only the revised authored task text.",
85
+ "Do not add Markdown fences, explanations, labels, or commentary.",
86
+ "Preserve the original intent unless the guidance explicitly changes it.",
87
+ "Do not include runtime additions unless they are needed as ordinary task context.",
88
+ ].join("\n"),
89
+ model,
90
+ thinkingLevel: ctxThinking ?? "off",
91
+ tools: [],
92
+ },
93
+ convertToLlm,
94
+ streamFunction: streamFn,
95
+ getApiKey: (providerName) => providerName === model.provider ? auth.apiKey : undefined,
96
+ toolExecution: "sequential",
97
+ });
98
+ const abort = () => agent.abort();
99
+ input.ctx.signal?.addEventListener("abort", abort, { once: true });
100
+ input.signal?.addEventListener("abort", abort, { once: true });
101
+ try {
102
+ await agent.prompt([
103
+ "Guidance from the human:",
104
+ input.guidance.trim(),
105
+ "",
106
+ "Authored task:",
107
+ input.authoredTask,
108
+ "",
109
+ "Runtime additions for context only:",
110
+ input.runtimeAdditions,
111
+ "",
112
+ "Final effective prompt for context only:",
113
+ input.finalEffectivePrompt,
114
+ ].join("\n"));
115
+ } finally {
116
+ input.ctx.signal?.removeEventListener("abort", abort);
117
+ input.signal?.removeEventListener("abort", abort);
118
+ }
119
+ const rewritten = finalAssistantText(agent);
120
+ if (!rewritten) throw new Error("Prompt redo rewrite returned an empty prompt.");
121
+ return rewritten;
122
+ }
123
+
124
+ function runtimeAdditions(authoredTask: string, effectivePrompt: string): string {
125
+ if (!authoredTask) return effectivePrompt;
126
+ const authoredIndex = effectivePrompt.indexOf(authoredTask);
127
+ if (authoredIndex < 0) return "(runtime additions unavailable)";
128
+ const before = effectivePrompt.slice(0, authoredIndex).trim();
129
+ const after = effectivePrompt.slice(authoredIndex + authoredTask.length).trim();
130
+ return [before, after].filter(Boolean).join("\n\n") || "(none)";
131
+ }
132
+
133
+ export function registerLivePromptAudit(
134
+ control: ForegroundRunControl,
135
+ index: number,
136
+ authoredTask: string,
137
+ effectivePrompt: string,
138
+ metadata: { cwd?: string; outputPath?: string; rerun?: PromptAuditRerunContract } = {},
139
+ ): void {
140
+ let prompts = livePrompts.get(control);
141
+ if (!prompts) {
142
+ prompts = new Map();
143
+ livePrompts.set(control, prompts);
144
+ }
145
+ prompts.set(index, {
146
+ authoredTask,
147
+ runtimeAdditions: runtimeAdditions(authoredTask, effectivePrompt),
148
+ finalEffectivePrompt: effectivePrompt,
149
+ ...(metadata.cwd ? { cwd: metadata.cwd } : {}),
150
+ ...(metadata.outputPath ? { outputPath: metadata.outputPath } : {}),
151
+ ...(metadata.rerun ? { rerun: metadata.rerun } : {}),
152
+ });
153
+ }
154
+
155
+ export function updateLiveEffectivePrompt(control: ForegroundRunControl, index: number, effectivePrompt: string): void {
156
+ const prompt = livePrompts.get(control)?.get(index);
157
+ if (!prompt) return;
158
+ prompt.finalEffectivePrompt = effectivePrompt;
159
+ prompt.runtimeAdditions = runtimeAdditions(prompt.authoredTask, effectivePrompt);
160
+ }
161
+
162
+ export function getLivePromptAudit(control: ForegroundRunControl, index: number): LivePromptAudit | undefined {
163
+ return livePrompts.get(control)?.get(index);
164
+ }
165
+
166
+ export function removeLivePromptAudit(control: ForegroundRunControl, index: number): void {
167
+ const prompts = livePrompts.get(control);
168
+ if (!prompts) return;
169
+ prompts.delete(index);
170
+ if (prompts.size === 0) livePrompts.delete(control);
171
+ }