pi-subagents 0.40.0 → 0.41.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 (119) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +246 -525
  3. package/agents/oracle.md +1 -0
  4. package/package.json +12 -4
  5. package/prompts/parallel-context-build.md +1 -1
  6. package/prompts/parallel-handoff-plan.md +1 -1
  7. package/prompts/review-loop.md +1 -1
  8. package/skills/pi-subagents/SKILL.md +6 -6
  9. package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
  10. package/skills/pi-subagents/references/execution-controls.md +98 -97
  11. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  12. package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
  13. package/src/agents/agent-management.ts +155 -65
  14. package/src/agents/agent-serializer.ts +19 -0
  15. package/src/agents/agents.ts +154 -71
  16. package/src/agents/chain-serializer.ts +10 -7
  17. package/src/agents/frontmatter.ts +5 -3
  18. package/src/agents/identity.ts +1 -1
  19. package/src/agents/proactive-skills.ts +13 -10
  20. package/src/agents/skills.ts +23 -6
  21. package/src/api/control-channel.ts +4 -0
  22. package/src/api/delegation.ts +26 -194
  23. package/src/api/external-runs.ts +129 -0
  24. package/src/api/intercom-bridge.ts +3 -0
  25. package/src/api/pi-args.ts +5 -0
  26. package/src/api/preflight.ts +3 -3
  27. package/src/api/shared-types.ts +19 -0
  28. package/src/extension/config.ts +10 -0
  29. package/src/extension/control-notices.ts +5 -39
  30. package/src/extension/doctor.ts +10 -9
  31. package/src/extension/fanout-child.ts +7 -4
  32. package/src/extension/index.ts +232 -68
  33. package/src/extension/rpc.ts +18 -12
  34. package/src/extension/schemas.ts +48 -37
  35. package/src/extension/tool-description.ts +36 -86
  36. package/src/inspectors/herdr/actions.ts +229 -0
  37. package/src/inspectors/herdr/client.ts +130 -0
  38. package/src/inspectors/herdr/inspector-runner.ts +141 -0
  39. package/src/inspectors/herdr/project-panes.ts +154 -0
  40. package/src/integrations/herdr-status.ts +330 -0
  41. package/src/intercom/intercom-bridge.ts +3 -2
  42. package/src/intercom/result-intercom.ts +5 -1
  43. package/src/missions/actions.ts +372 -0
  44. package/src/missions/lifecycle.ts +314 -0
  45. package/src/missions/store.ts +442 -0
  46. package/src/missions/types.ts +135 -0
  47. package/src/policy/authority.ts +46 -0
  48. package/src/profiles/profiles.ts +29 -3
  49. package/src/runs/background/async-execution.ts +98 -49
  50. package/src/runs/background/async-job-tracker.ts +10 -2
  51. package/src/runs/background/async-resume.ts +6 -6
  52. package/src/runs/background/async-status.ts +29 -1
  53. package/src/runs/background/auto-drain.ts +3 -3
  54. package/src/runs/background/chain-append.ts +3 -2
  55. package/src/runs/background/control-channel.ts +9 -7
  56. package/src/runs/background/fleet-view.ts +3 -4
  57. package/src/runs/background/notify.ts +2 -1
  58. package/src/runs/background/process-terminal.ts +5 -5
  59. package/src/runs/background/result-watcher.ts +13 -5
  60. package/src/runs/background/run-id-resolver.ts +3 -3
  61. package/src/runs/background/run-status.ts +35 -8
  62. package/src/runs/background/scheduled-runs.ts +602 -375
  63. package/src/runs/background/stale-run-reconciler.ts +3 -3
  64. package/src/runs/background/subagent-runner.ts +608 -445
  65. package/src/runs/background/subagent-wait.ts +50 -9
  66. package/src/runs/background/wait-subscriptions.ts +253 -0
  67. package/src/runs/background/wait-tool.ts +12 -4
  68. package/src/runs/foreground/async-steering-action.ts +3 -3
  69. package/src/runs/foreground/chain-clarify.ts +8 -4
  70. package/src/runs/foreground/chain-execution.ts +56 -30
  71. package/src/runs/foreground/execution.ts +15 -2
  72. package/src/runs/foreground/subagent-executor.ts +1023 -273
  73. package/src/runs/shared/acceptance.ts +28 -6
  74. package/src/runs/shared/child-protocol.ts +302 -22
  75. package/src/runs/shared/dynamic-fanout.ts +1 -1
  76. package/src/runs/shared/external-cli-runner.ts +130 -0
  77. package/src/runs/shared/long-running-guard.ts +42 -1
  78. package/src/runs/shared/nested-events.ts +59 -5
  79. package/src/runs/shared/nested-render.ts +9 -4
  80. package/src/runs/shared/parallel-handoff.ts +86 -2
  81. package/src/runs/shared/parallel-utils.ts +11 -2
  82. package/src/runs/shared/permissions.ts +95 -0
  83. package/src/runs/shared/pi-args.ts +11 -1
  84. package/src/runs/shared/pi-spawn.ts +11 -1
  85. package/src/runs/shared/run-history.ts +1 -1
  86. package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
  87. package/src/runs/shared/subagent-startup-retry.ts +5 -2
  88. package/src/runs/shared/turn-budget.ts +6 -6
  89. package/src/runs/shared/worktree.ts +122 -12
  90. package/src/shared/accessible-dir.ts +29 -7
  91. package/src/shared/artifacts.ts +18 -1
  92. package/src/shared/fork-context.ts +3 -2
  93. package/src/shared/launch-contract.ts +1 -0
  94. package/src/shared/settings.ts +10 -0
  95. package/src/shared/types.ts +156 -14
  96. package/src/shared/utils.ts +8 -6
  97. package/src/slash/delegation-adapters.ts +32 -194
  98. package/src/slash/delegation-request.ts +43 -126
  99. package/src/slash/prompt-template-bridge.ts +158 -205
  100. package/src/slash/prompt-workflows.ts +21 -57
  101. package/src/slash/slash-bridge.ts +14 -0
  102. package/src/slash/slash-commands.ts +31 -632
  103. package/src/slash/subagents-admin.ts +18 -14
  104. package/src/tui/fleet-status.ts +156 -21
  105. package/src/tui/fleet-transcript.ts +110 -5
  106. package/src/tui/fleet.ts +56 -24
  107. package/src/tui/render.ts +291 -109
  108. package/src/types/pi-runtime-compat.d.ts +14 -0
  109. package/src/watchdog/lsp-diagnostics.ts +12 -7
  110. package/src/watchdog/model-selection.ts +2 -2
  111. package/src/watchdog/permission-arbiter.ts +145 -0
  112. package/src/watchdog/register-child.ts +1 -1
  113. package/src/watchdog/register-main.ts +1 -1
  114. package/src/watchdog/review.ts +4 -1
  115. package/src/watchdog/runtime.ts +3 -2
  116. package/src/workflows/chat-progress.ts +140 -0
  117. package/src/workflows/scripted-workflow.ts +415 -0
  118. package/agents/advisor.md +0 -73
  119. package/src/extension/chain-validation.ts +0 -181
@@ -46,11 +46,10 @@ import {
46
46
  type BackgroundWorkSnapshot,
47
47
  type RegisteredBackgroundWorkItem,
48
48
  } from "../../api/background-work.ts";
49
- import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
49
+ import { formatAsyncRunList, listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
50
50
  import {
51
- ASYNC_DIR,
51
+ DIRS,
52
52
  INTERCOM_DETACH_REQUEST_EVENT,
53
- RESULTS_DIR,
54
53
  SUBAGENT_ASYNC_COMPLETE_EVENT,
55
54
  SUBAGENT_FOREGROUND_COMPLETE_EVENT,
56
55
  SUBAGENT_CONTROL_EVENT,
@@ -60,7 +59,7 @@ import {
60
59
  type ForegroundResumeRun,
61
60
  type SubagentState,
62
61
  } from "../../shared/types.ts";
63
- import { formatDuration } from "../../shared/formatters.ts";
62
+ import { formatDuration, shortenPath } from "../../shared/formatters.ts";
64
63
  export { WAIT_TOOL_ENABLED_ENV, resolveWaitToolConfig, type ResolvedWaitToolConfig } from "./wait-config.ts";
65
64
 
66
65
  /** States that mean a run is still in flight (not yet resolved). */
@@ -73,6 +72,8 @@ const DEFAULT_POLL_INTERVAL_MS = 1000;
73
72
  export interface SubagentWaitParams {
74
73
  /** Optional run id/prefix to wait for. When omitted, waits across every active run in this session. */
75
74
  id?: string;
75
+ /** Arm a durable exact-run wake subscription and return immediately. Requires id. */
76
+ nonBlocking?: boolean;
76
77
  /**
77
78
  * When true, block until EVERY active run in this session (or matching `id`)
78
79
  * is terminal. Default false: return as soon as the first run finishes, so a
@@ -106,6 +107,8 @@ export interface SubagentWaitDeps {
106
107
  stopOnAttention?: boolean;
107
108
  /** Internal auto-drain mode surfaces failed terminal subagent runs as errors. */
108
109
  failOnFailedRuns?: boolean;
110
+ /** Arm a durable exact-target wait subscription in a long-lived interactive runtime. */
111
+ subscribe?: (input: { targetKind: "async" | "foreground"; runId: string; requestedId: string; timeoutMs: number }) => { token: string; expiresAt: number };
109
112
  /** Injectable provider protocol surfaces for deterministic tests. */
110
113
  backgroundWork?: {
111
114
  snapshot(sessionId: string, nowMs: number): BackgroundWorkSnapshot;
@@ -253,8 +256,8 @@ function backgroundWorkForSession(deps: SubagentWaitDeps, nowMs: number): Backgr
253
256
 
254
257
  /** Queued/running runs from this session, including runs that need attention. */
255
258
  function activeRunsForSession(params: SubagentWaitParams, deps: SubagentWaitDeps): AsyncRunSummary[] {
256
- const asyncDirRoot = deps.asyncDirRoot ?? ASYNC_DIR;
257
- const resultsDir = deps.resultsDir ?? RESULTS_DIR;
259
+ const asyncDirRoot = deps.asyncDirRoot ?? DIRS.async;
260
+ const resultsDir = deps.resultsDir ?? DIRS.results;
258
261
  const runs = listAsyncRuns(asyncDirRoot, {
259
262
  states: [...ACTIVE_STATES],
260
263
  sessionId: deps.state.currentSessionId ?? undefined,
@@ -273,8 +276,8 @@ function attentionRunsForSession(params: SubagentWaitParams, deps: SubagentWaitD
273
276
 
274
277
  /** All runs (any state) for this session, for the final summary. */
275
278
  function allRunsForSession(params: SubagentWaitParams, deps: SubagentWaitDeps): AsyncRunSummary[] {
276
- const asyncDirRoot = deps.asyncDirRoot ?? ASYNC_DIR;
277
- const resultsDir = deps.resultsDir ?? RESULTS_DIR;
279
+ const asyncDirRoot = deps.asyncDirRoot ?? DIRS.async;
280
+ const resultsDir = deps.resultsDir ?? DIRS.results;
278
281
  const runs = listAsyncRuns(asyncDirRoot, {
279
282
  sessionId: deps.state.currentSessionId ?? undefined,
280
283
  resultsDir,
@@ -289,7 +292,8 @@ function summarizeTerminalRuns(runs: AsyncRunSummary[], providerFinishedCount =
289
292
  if (runs.length === 0 && providerFinishedCount === 0) return "";
290
293
  const counts = { complete: 0, failed: 0, paused: 0 } as Record<string, number>;
291
294
  for (const run of runs) {
292
- if (run.state in counts) counts[run.state] += 1;
295
+ const count = counts[run.state];
296
+ if (count !== undefined) counts[run.state] = count + 1;
293
297
  }
294
298
  const parts: string[] = [];
295
299
  if (counts.complete) parts.push(`${counts.complete} complete`);
@@ -307,6 +311,25 @@ function result(text: string, isError = false): AgentToolResult<Details> {
307
311
  };
308
312
  }
309
313
 
314
+ /** Build the live status shown while async work keeps subagent_wait blocked. */
315
+ function asyncWaitUpdate(runs: AsyncRunSummary[], providerCount: number, elapsedMs: number): AgentToolResult<Details> {
316
+ const activity = runs.flatMap((run) => {
317
+ const activeSteps = run.steps.filter((step) => step.status === "pending" || step.status === "running");
318
+ if (activeSteps.length === 0) {
319
+ return [`${run.id}: ${run.state}`];
320
+ }
321
+ return activeSteps.map((step) => {
322
+ const current = step.currentTool ?? (step.status === "pending" ? "queued" : "thinking…");
323
+ return `${step.agent}: ${current}${step.currentPath ? ` ${shortenPath(step.currentPath)}` : ""}`;
324
+ });
325
+ });
326
+ const headline = [
327
+ `Waiting ${formatDuration(elapsedMs)} for ${runs.length} async run(s) and ${providerCount} provider item(s).`,
328
+ ...activity,
329
+ ].join(" · ");
330
+ return result([headline, runs.length > 0 ? formatAsyncRunList(runs) : ""].filter(Boolean).join("\n"));
331
+ }
332
+
310
333
  const TRANSCRIPT_TAIL_BYTES = 128 * 1024;
311
334
  const TRANSCRIPT_PREVIEW_LINES = 3;
312
335
  const TRANSCRIPT_PREVIEW_WIDTH = 220;
@@ -466,6 +489,12 @@ export async function waitForSubagents(
466
489
  const timeoutMs = params.timeoutMs !== undefined && params.timeoutMs > 0 ? params.timeoutMs : DEFAULT_TIMEOUT_MS;
467
490
  const startedAt = now();
468
491
  const waitForAll = params.id ? true : params.all === true;
492
+ if (params.nonBlocking && !params.id) {
493
+ return result("Non-blocking wait subscriptions require id so the registration can bind one exact run identity.", true);
494
+ }
495
+ if (params.nonBlocking && params.all) {
496
+ return result("nonBlocking cannot be combined with all; subscribe to one exact run id.", true);
497
+ }
469
498
 
470
499
  let active: AsyncRunSummary[];
471
500
  let foreground: ForegroundResumeRun[];
@@ -489,6 +518,17 @@ export async function waitForSubagents(
489
518
  return result(`Ambiguous subagent run id prefix "${params.id}" matched ${matches.length} active runs: ${matches.map((candidate) => candidate.id).join(", ")}. Pass a longer id.`, true);
490
519
  }
491
520
  const selected = matches[0];
521
+ if (selected && params.nonBlocking) {
522
+ if (!deps.subscribe) {
523
+ return result("Non-blocking wait subscriptions require a long-lived interactive subagent runtime; this runtime can only use blocking subagent_wait calls.", true);
524
+ }
525
+ try {
526
+ const registration = deps.subscribe({ targetKind: selected.kind, runId: selected.id, requestedId: params.id, timeoutMs });
527
+ return result(`Armed wait subscription ${registration.token} for exact ${selected.kind} run ${selected.id}. Returning immediately; this session will be woken on completion, failure, attention, reconciliation failure, or timeout. Inspect armed subscriptions with subagent({ action: "status" }).`);
528
+ } catch (error) {
529
+ return result(error instanceof Error ? error.message : String(error), true);
530
+ }
531
+ }
492
532
  if (selected?.kind === "foreground") {
493
533
  return waitForDetachedForegroundRun(selected.run, signal, deps, startedAt, now, pollIntervalMs, timeoutMs);
494
534
  }
@@ -528,6 +568,7 @@ export async function waitForSubagents(
528
568
  ...activeInitialRuns.map((run) => `${run.id} (${run.state})`),
529
569
  ...activeInitialProviderItems.map((item) => `${item.provider}/${item.id}`),
530
570
  ].join(", ");
571
+ deps.onUpdate?.(asyncWaitUpdate(activeInitialRuns, activeInitialProviderItems.length, now() - startedAt));
531
572
  if (signal?.aborted) {
532
573
  return result(`Wait aborted after ${formatDuration(now() - startedAt)}. Still active: ${stillActive}.`, true);
533
574
  }
@@ -0,0 +1,253 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+ import { listAsyncRuns } from "./async-status.ts";
6
+ import { writeAtomicJson } from "../../shared/atomic-json.ts";
7
+ import {
8
+ DIRS,
9
+ INTERCOM_DETACH_REQUEST_EVENT,
10
+ SUBAGENT_ASYNC_COMPLETE_EVENT,
11
+ SUBAGENT_CONTROL_EVENT,
12
+ SUBAGENT_CONTROL_INTERCOM_EVENT,
13
+ SUBAGENT_FOREGROUND_COMPLETE_EVENT,
14
+ SUBAGENT_RESULT_INTERCOM_EVENT,
15
+ type SubagentState,
16
+ type WaitSubscriptionRecord,
17
+ } from "../../shared/types.ts";
18
+
19
+ const SUBSCRIPTION_VERSION = 1;
20
+ const RECONCILE_INTERVAL_MS = 1_000;
21
+
22
+ export interface ArmWaitSubscriptionInput {
23
+ targetKind: "async" | "foreground";
24
+ runId: string;
25
+ requestedId: string;
26
+ timeoutMs: number;
27
+ }
28
+
29
+ export interface WaitSubscriptionManager {
30
+ arm(input: ArmWaitSubscriptionInput): WaitSubscriptionRecord;
31
+ restore(): void;
32
+ reconcile(): void;
33
+ dispose(): void;
34
+ }
35
+
36
+ interface WaitSubscriptionManagerOptions {
37
+ asyncDirRoot?: string;
38
+ resultsDir?: string;
39
+ subscriptionsDir?: string;
40
+ now?: () => number;
41
+ pollIntervalMs?: number;
42
+ kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
43
+ }
44
+
45
+ function isNotFound(error: unknown): boolean {
46
+ return typeof error === "object" && error !== null && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT";
47
+ }
48
+
49
+ function parseRecord(value: unknown): WaitSubscriptionRecord | undefined {
50
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
51
+ const record = value as Partial<WaitSubscriptionRecord>;
52
+ if (record.version !== SUBSCRIPTION_VERSION
53
+ || typeof record.token !== "string"
54
+ || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(record.token)
55
+ || typeof record.sessionId !== "string"
56
+ || (record.targetKind !== "async" && record.targetKind !== "foreground")
57
+ || typeof record.runId !== "string"
58
+ || typeof record.requestedId !== "string"
59
+ || typeof record.createdAt !== "number"
60
+ || typeof record.expiresAt !== "number") return undefined;
61
+ return record as WaitSubscriptionRecord;
62
+ }
63
+
64
+ function needsAttention(run: ReturnType<typeof listAsyncRuns>[number]): boolean {
65
+ return run.activityState === "needs_attention" || run.steps.some((step) => step.activityState === "needs_attention");
66
+ }
67
+
68
+ function subscriptionFile(dir: string, token: string): string {
69
+ return path.join(dir, `${token}.json`);
70
+ }
71
+
72
+ export function formatWaitSubscriptions(state: Pick<SubagentState, "waitSubscriptions">, now = Date.now()): string | undefined {
73
+ const subscriptions = [...(state.waitSubscriptions?.values() ?? [])].sort((left, right) => left.createdAt - right.createdAt);
74
+ if (subscriptions.length === 0) return undefined;
75
+ const lines = [`Armed wait subscriptions (${subscriptions.length}):`];
76
+ for (const record of subscriptions) {
77
+ lines.push(`- ${record.token}: ${record.targetKind} run ${record.runId}, timeout in ${Math.max(0, record.expiresAt - now)}ms`);
78
+ }
79
+ return lines.join("\n");
80
+ }
81
+
82
+ export function createWaitSubscriptionManager(
83
+ pi: Pick<ExtensionAPI, "events" | "sendMessage">,
84
+ state: SubagentState,
85
+ options: WaitSubscriptionManagerOptions = {},
86
+ ): WaitSubscriptionManager {
87
+ const asyncDirRoot = options.asyncDirRoot ?? DIRS.async;
88
+ const resultsDir = options.resultsDir ?? DIRS.results;
89
+ const subscriptionsDir = options.subscriptionsDir ?? path.join(path.dirname(asyncDirRoot), "wait-subscriptions");
90
+ const now = options.now ?? Date.now;
91
+ const subscriptions = state.waitSubscriptions ?? new Map<string, WaitSubscriptionRecord>();
92
+ state.waitSubscriptions = subscriptions;
93
+ const unresolvedRestoredForegroundTokens = new Set<string>();
94
+ let disposed = false;
95
+
96
+ const remove = (record: WaitSubscriptionRecord) => {
97
+ try {
98
+ fs.unlinkSync(subscriptionFile(subscriptionsDir, record.token));
99
+ } catch (error) {
100
+ if (!isNotFound(error)) throw error;
101
+ }
102
+ subscriptions.delete(record.token);
103
+ unresolvedRestoredForegroundTokens.delete(record.token);
104
+ };
105
+
106
+ const settle = (record: WaitSubscriptionRecord, outcome: string, detail: string) => {
107
+ if (disposed || state.currentSessionId !== record.sessionId) return;
108
+ try {
109
+ remove(record);
110
+ } catch (error) {
111
+ console.error(`Failed to clear wait subscription '${record.token}'; it remains armed:`, error);
112
+ return;
113
+ }
114
+ try {
115
+ pi.sendMessage({
116
+ customType: "subagent-wait-subscription",
117
+ content: `Wait subscription ${record.token} fired for run ${record.runId}: ${outcome}. ${detail}`,
118
+ display: true,
119
+ details: { token: record.token, runId: record.runId, outcome },
120
+ }, { triggerTurn: true });
121
+ } catch (error) {
122
+ console.error(`Failed to deliver wait subscription '${record.token}' after clearing it:`, error);
123
+ }
124
+ };
125
+
126
+ const reconcileRecord = (record: WaitSubscriptionRecord) => {
127
+ if (record.sessionId !== state.currentSessionId) return;
128
+ if (now() >= record.expiresAt) {
129
+ settle(record, "timed out", "The targeted run may still be active; inspect its status before taking follow-up action.");
130
+ return;
131
+ }
132
+ if (record.targetKind === "foreground") {
133
+ const run = state.foregroundRuns?.get(record.runId);
134
+ if (!run && unresolvedRestoredForegroundTokens.has(record.token)) return;
135
+ if (run) unresolvedRestoredForegroundTokens.delete(record.token);
136
+ if (!run || run.sessionId !== record.sessionId) {
137
+ settle(record, "could not be reconciled", "The remembered foreground run disappeared before completion was confirmed.");
138
+ return;
139
+ }
140
+ const detached = run.children.filter((child) => child.status === "detached");
141
+ if (detached.some((child) => child.activityState === "needs_attention")) {
142
+ settle(record, "needs attention", "Reply to the pending supervisor request or inspect the run status.");
143
+ return;
144
+ }
145
+ if (detached.length === 0) {
146
+ const failed = run.children.some((child) => child.status === "failed");
147
+ settle(record, failed ? "failed" : "completed", "Inspect the run status for its final output.");
148
+ }
149
+ return;
150
+ }
151
+
152
+ const runs = listAsyncRuns(asyncDirRoot, {
153
+ sessionId: record.sessionId,
154
+ runId: record.runId,
155
+ resultsDir,
156
+ kill: options.kill,
157
+ now,
158
+ });
159
+ const run = runs.find((candidate) => candidate.id === record.runId);
160
+ if (!run) {
161
+ settle(record, "could not be reconciled", "The exact async run disappeared before a terminal result was confirmed.");
162
+ return;
163
+ }
164
+ if (needsAttention(run)) {
165
+ settle(record, "needs attention", "Inspect the run status and answer any pending supervisor request.");
166
+ return;
167
+ }
168
+ if (run.state !== "queued" && run.state !== "running") {
169
+ settle(record, run.state === "complete" ? "completed" : run.state, "Inspect the run status for its final output.");
170
+ }
171
+ };
172
+
173
+ const reconcile = () => {
174
+ if (disposed || !state.currentSessionId) return;
175
+ for (const record of [...subscriptions.values()]) {
176
+ try {
177
+ reconcileRecord(record);
178
+ } catch (error) {
179
+ console.error(`Failed to reconcile wait subscription '${record.token}':`, error);
180
+ settle(record, "reconciliation failed", "The targeted run could not be reconciled. Inspect its status before taking follow-up action.");
181
+ }
182
+ }
183
+ };
184
+
185
+ const wakeChannels = [
186
+ INTERCOM_DETACH_REQUEST_EVENT,
187
+ SUBAGENT_ASYNC_COMPLETE_EVENT,
188
+ SUBAGENT_FOREGROUND_COMPLETE_EVENT,
189
+ SUBAGENT_CONTROL_EVENT,
190
+ SUBAGENT_CONTROL_INTERCOM_EVENT,
191
+ SUBAGENT_RESULT_INTERCOM_EVENT,
192
+ ];
193
+ const unsubscribes = wakeChannels.map((channel) => pi.events.on(channel, reconcile));
194
+ const interval = setInterval(reconcile, options.pollIntervalMs ?? RECONCILE_INTERVAL_MS);
195
+ interval.unref?.();
196
+
197
+ return {
198
+ arm(input) {
199
+ const sessionId = state.currentSessionId;
200
+ if (!sessionId) throw new Error("A wait subscription requires an active session identity.");
201
+ const createdAt = now();
202
+ const record: WaitSubscriptionRecord = {
203
+ version: SUBSCRIPTION_VERSION,
204
+ token: randomUUID(),
205
+ sessionId,
206
+ targetKind: input.targetKind,
207
+ runId: input.runId,
208
+ requestedId: input.requestedId,
209
+ createdAt,
210
+ expiresAt: createdAt + input.timeoutMs,
211
+ };
212
+ fs.mkdirSync(subscriptionsDir, { recursive: true });
213
+ writeAtomicJson(subscriptionFile(subscriptionsDir, record.token), record);
214
+ subscriptions.set(record.token, record);
215
+ return record;
216
+ },
217
+ restore() {
218
+ subscriptions.clear();
219
+ unresolvedRestoredForegroundTokens.clear();
220
+ if (!state.currentSessionId) return;
221
+ let files: string[];
222
+ try {
223
+ files = fs.readdirSync(subscriptionsDir).filter((file) => file.endsWith(".json"));
224
+ } catch (error) {
225
+ if (isNotFound(error)) return;
226
+ throw error;
227
+ }
228
+ for (const file of files) {
229
+ try {
230
+ const record = parseRecord(JSON.parse(fs.readFileSync(path.join(subscriptionsDir, file), "utf-8")));
231
+ if (!record || file !== `${record.token}.json`) continue;
232
+ if (record.sessionId === state.currentSessionId) {
233
+ subscriptions.set(record.token, record);
234
+ if (record.targetKind === "foreground" && !state.foregroundRuns?.has(record.runId)) unresolvedRestoredForegroundTokens.add(record.token);
235
+ }
236
+ } catch (error) {
237
+ console.error(`Ignoring invalid wait subscription '${path.join(subscriptionsDir, file)}':`, error);
238
+ }
239
+ }
240
+ reconcile();
241
+ },
242
+ reconcile,
243
+ dispose() {
244
+ if (disposed) return;
245
+ disposed = true;
246
+ clearInterval(interval);
247
+ for (const unsubscribe of unsubscribes) {
248
+ try { unsubscribe(); } catch { /* best effort */ }
249
+ }
250
+ subscriptions.clear();
251
+ },
252
+ };
253
+ }
@@ -2,8 +2,9 @@ import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-age
2
2
  import { SubagentWaitParams } from "../../extension/schemas.ts";
3
3
  import type { Details, SubagentState } from "../../shared/types.ts";
4
4
  import { resolveWaitToolConfig, waitForSubagents } from "./subagent-wait.ts";
5
+ import type { WaitSubscriptionManager } from "./wait-subscriptions.ts";
5
6
 
6
- export function registerWaitTool(pi: ExtensionAPI, state: SubagentState, enabled = resolveWaitToolConfig().enabled): void {
7
+ export function registerWaitTool(pi: ExtensionAPI, state: SubagentState, enabled = resolveWaitToolConfig().enabled, subscriptions?: Pick<WaitSubscriptionManager, "arm">): void {
7
8
  const tool: ToolDefinition<typeof SubagentWaitParams, Details> = {
8
9
  name: "subagent_wait",
9
10
  label: "Subagent Wait",
@@ -14,12 +15,19 @@ In an interactive chat, do not call this merely to wait: return control to the u
14
15
  • { } — return when the first initially active async run or registered provider item finishes, or when a subagent needs attention.
15
16
  • { all: true } — wait for every async run and provider item that was active when the call began.
16
17
  • { id: "..." } — wait for one async or remembered detached foreground subagent run (id or prefix).
18
+ • { id: "...", nonBlocking: true } — resolve the prefix once, persist an exact-run wake subscription, and return immediately. The originating interactive session wakes on completion, failure, attention, reconciliation failure, or timeout.
17
19
  • { timeoutMs: 600000 } — stop waiting after N ms; active work keeps running.
18
20
 
19
- Provider jobs are session-scoped and identified exactly, so replacing one job with another cannot hide a completion. Provider extensions must be explicitly loaded in this process. In a child agent, keep \`subagent_wait\` in the child tool allowlist and load each provider through the agent's extensions or subagentOnlyExtensions; this tool never loads providers or grants tools itself.${enabled ? "" : "\n\nConfigured behavior: subagent_wait is disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED and returns immediately without blocking."}`,
21
+ Non-blocking subscriptions are visible in subagent status and differ from disabling waitTool: waitTool.enabled=false returns immediately without registering any future wake. Provider jobs are session-scoped and identified exactly, so replacing one job with another cannot hide a completion. Provider extensions must be explicitly loaded in this process. In a child agent, keep \`subagent_wait\` in the child tool allowlist and load each provider through the agent's extensions or subagentOnlyExtensions; this tool never loads providers or grants tools itself.${enabled ? "" : "\n\nConfigured behavior: subagent_wait is disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED and returns immediately without blocking."}`,
20
22
  parameters: SubagentWaitParams,
21
- execute(_id, params, signal, onUpdate) {
22
- return waitForSubagents(params, signal, { state, events: pi.events, enabled, onUpdate });
23
+ execute(_id, params, signal, onUpdate, ctx) {
24
+ return waitForSubagents(params, signal, {
25
+ state,
26
+ events: pi.events,
27
+ enabled,
28
+ onUpdate,
29
+ ...(subscriptions && ctx?.hasUI ? { subscribe: (input) => subscriptions.arm(input) } : {}),
30
+ });
23
31
  },
24
32
  };
25
33
  pi.registerTool(tool);
@@ -121,7 +121,7 @@ export async function steerAsyncRun(input: {
121
121
  if (finalResult?.state === "delivered") {
122
122
  return { content: [{ type: "text", text: `Steering delivered for async run ${status.runId} (request ${requestId}).` }], details: { mode: "management", results: [], steering: finalResult } };
123
123
  }
124
- const running = (finalStatus?.steps ?? status.steps).filter((step) => step.status === "running");
124
+ const running = (finalStatus?.steps ?? status.steps ?? []).filter((step) => step.status === "running");
125
125
  const recoveryAllowed = status.mode === "single" && status.isNested !== true && running.length === 1 && Boolean(finalStatus?.steering) && (input.index === undefined || input.index === 0);
126
126
  if (recoveryAllowed && finalResult?.state !== "scheduled" && input.recover) {
127
127
  const appendSteeringNotice = (state: "failed" | "recovered", message: string): void => {
@@ -194,7 +194,7 @@ export async function steerAsyncRun(input: {
194
194
  const revived = await input.recover(limits);
195
195
  if (revived.isError || !revived.details.asyncId) throw new Error(revived.content[0]?.type === "text" ? revived.content[0].text : "Replacement launch failed; source run remains paused.");
196
196
  const sourceStatus = readStatus(asyncDir);
197
- const targetIndex = input.index ?? status.steps.findIndex((step) => step.status === "running");
197
+ const targetIndex = input.index ?? status.steps?.findIndex((step) => step.status === "running") ?? -1;
198
198
  if (sourceStatus?.state === "paused" && sourceStatus.steering && targetIndex >= 0) {
199
199
  updateSteeringTarget(sourceStatus.steering, requestId, targetIndex, "recovered", Date.now(), { replacementRunId: revived.details.asyncId });
200
200
  const stepSteering = sourceStatus.steps?.[targetIndex]?.steering;
@@ -207,7 +207,7 @@ export async function steerAsyncRun(input: {
207
207
  } catch (error) {
208
208
  const reason = error instanceof Error ? error.message : String(error);
209
209
  const failedStatus = readStatus(asyncDir);
210
- const targetIndex = input.index ?? status.steps.findIndex((step) => step.status === "running");
210
+ const targetIndex = input.index ?? status.steps?.findIndex((step) => step.status === "running") ?? -1;
211
211
  if (failedStatus && failedStatus.state !== "running" && failedStatus.state !== "queued" && failedStatus.steering && targetIndex >= 0) {
212
212
  updateSteeringTarget(failedStatus.steering, requestId, targetIndex, "failed", Date.now(), { reason });
213
213
  const stepSteering = failedStatus.steps?.[targetIndex]?.steering;
@@ -775,22 +775,26 @@ export class ChainClarifyComponent implements Component {
775
775
 
776
776
  private handleEditInput(data: string): void {
777
777
  const textWidth = this.width - 4; // Must match render: innerW - 2 = (width - 2) - 2
778
- if (matchesKey(data, "shift+up") || matchesKey(data, "pageup")) {
778
+ if (matchesKey(data, "shift+up") || matchesKey(data, "pageUp")) {
779
779
  const { lines: wrapped, starts } = wrapText(this.editState.buffer, textWidth);
780
780
  const cursorPos = getCursorDisplayPos(this.editState.cursor, starts);
781
781
  const targetLine = Math.max(0, cursorPos.line - this.EDIT_VIEWPORT_HEIGHT);
782
+ const targetStart = starts[targetLine];
783
+ if (targetStart === undefined) return;
782
784
  const targetCol = Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0);
783
- this.editState = { ...this.editState, cursor: starts[targetLine] + targetCol };
785
+ this.editState = { ...this.editState, cursor: targetStart + targetCol };
784
786
  this.tui.requestRender();
785
787
  return;
786
788
  }
787
789
 
788
- if (matchesKey(data, "shift+down") || matchesKey(data, "pagedown")) {
790
+ if (matchesKey(data, "shift+down") || matchesKey(data, "pageDown")) {
789
791
  const { lines: wrapped, starts } = wrapText(this.editState.buffer, textWidth);
790
792
  const cursorPos = getCursorDisplayPos(this.editState.cursor, starts);
791
793
  const targetLine = Math.min(wrapped.length - 1, cursorPos.line + this.EDIT_VIEWPORT_HEIGHT);
794
+ const targetStart = starts[targetLine];
795
+ if (targetStart === undefined) return;
792
796
  const targetCol = Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0);
793
- this.editState = { ...this.editState, cursor: starts[targetLine] + targetCol };
797
+ this.editState = { ...this.editState, cursor: targetStart + targetCol };
794
798
  this.tui.requestRender();
795
799
  return;
796
800
  }
@@ -44,7 +44,8 @@ import {
44
44
  import { buildChainSummary } from "../../shared/formatters.ts";
45
45
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
46
46
  import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
47
- import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup } from "../shared/parallel-handoff.ts";
47
+ import type { PermissionConfig } from "../shared/permissions.ts";
48
+ import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writePendingParallelHandoff } from "../shared/parallel-handoff.ts";
48
49
  import { recordRun } from "../shared/run-history.ts";
49
50
  import {
50
51
  cleanupWorktrees,
@@ -165,6 +166,7 @@ interface ParallelChainRunInput {
165
166
  configToolBudget?: ToolBudgetConfig;
166
167
  globalSemaphore?: Semaphore;
167
168
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
169
+ permissions?: PermissionConfig;
168
170
  dynamic?: boolean;
169
171
  }
170
172
 
@@ -228,30 +230,32 @@ function finalizeParallelWorktreeHandoff(input: {
228
230
  results: SingleResult[];
229
231
  }): { output: string; reference?: NonNullable<Details["parallelHandoff"]> } {
230
232
  const diffs = diffWorktrees(input.worktreeSetup, input.agents, path.join(input.artifactsDir, "worktree-diffs", input.runId, `step-${input.stepIndex}`));
231
- const cleanup = cleanupWorktrees(input.worktreeSetup);
232
233
  const diffSummary = formatWorktreeDiffSummary(diffs);
234
+ const manifestPath = parallelHandoffPath(input.artifactsDir, input.runId);
235
+ const handoff = {
236
+ manifestPath,
237
+ runId: input.runId,
238
+ mode: "chain" as const,
239
+ source: "foreground" as const,
240
+ cwd: input.cwd,
241
+ stepIndex: input.stepIndex,
242
+ flatStartIndex: input.flatStartIndex,
243
+ setup: input.worktreeSetup,
244
+ diffs,
245
+ results: input.results.map((result) => ({
246
+ agent: result.agent,
247
+ status: result.stopped ? "stopped" as const : result.detached ? "detached" as const : result.interrupted ? "paused" as const : result.exitCode === 0 ? "completed" as const : "failed" as const,
248
+ summary: getSingleResultOutput(result) || result.error || "(no output)",
249
+ ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
250
+ ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
251
+ ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
252
+ ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
253
+ })),
254
+ };
233
255
  try {
234
- const reference = writeParallelHandoffGroup({
235
- manifestPath: parallelHandoffPath(input.artifactsDir, input.runId),
236
- runId: input.runId,
237
- mode: "chain",
238
- source: "foreground",
239
- cwd: input.cwd,
240
- stepIndex: input.stepIndex,
241
- flatStartIndex: input.flatStartIndex,
242
- setup: input.worktreeSetup,
243
- diffs,
244
- cleanup,
245
- results: input.results.map((result) => ({
246
- agent: result.agent,
247
- status: result.stopped ? "stopped" : result.detached ? "detached" : result.interrupted ? "paused" : result.exitCode === 0 ? "completed" : "failed",
248
- summary: getSingleResultOutput(result) || result.error || "(no output)",
249
- ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
250
- ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
251
- ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
252
- ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
253
- })),
254
- });
256
+ writeParallelHandoffGroup(handoff);
257
+ const cleanup = cleanupWorktrees(input.worktreeSetup, { kind: "preserve", capturedDiffs: diffs, handoffManifestPath: manifestPath });
258
+ const reference = writeParallelHandoffGroup({ ...handoff, cleanup });
255
259
  const suffix = [diffSummary, formatParallelHandoffReference(reference)].filter(Boolean).join("\n\n");
256
260
  return { output: suffix ? `${input.output}\n\n${suffix}` : input.output, reference };
257
261
  } catch (error) {
@@ -381,6 +385,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
381
385
  let result!: SingleResult;
382
386
  try {
383
387
  result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
388
+ permissions: input.permissions,
384
389
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
385
390
  capabilityCeiling: input.capabilityCeiling,
386
391
  context: input.contextForAgent?.(task.agent),
@@ -529,6 +534,7 @@ interface ChainExecutionParams {
529
534
  toolBudget?: ResolvedToolBudget;
530
535
  usageBudget?: UsageBudgetConfig;
531
536
  configToolBudget?: ToolBudgetConfig;
537
+ permissions?: PermissionConfig;
532
538
  /** Global cap on simultaneously-running tasks within this chain. Defaults to DEFAULT_GLOBAL_CONCURRENCY_LIMIT. */
533
539
  globalConcurrencyLimit?: number;
534
540
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
@@ -627,10 +633,11 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
627
633
  ?? (isCheckpointStep(firstStep)
628
634
  ? undefined
629
635
  : isParallelStep(firstStep)
630
- ? firstStep.parallel[0]!.task!
636
+ ? firstStep.parallel[0]!.task
631
637
  : isDynamicParallelStep(firstStep)
632
- ? firstStep.parallel.task!
633
- : (firstStep as SequentialStep).task!);
638
+ ? firstStep.parallel.task
639
+ : (firstStep as SequentialStep).task)
640
+ ?? "";
634
641
  try {
635
642
  validateChainOutputBindings(chainSteps, { maxItems: params.dynamicFanoutMaxItems });
636
643
  } catch (error) {
@@ -788,6 +795,18 @@ ${step.message}` : ""}` }],
788
795
  }
789
796
 
790
797
  try {
798
+ if (worktreeSetup) {
799
+ writePendingParallelHandoff({
800
+ manifestPath: parallelHandoffPath(artifactsDir, runId),
801
+ runId,
802
+ mode: "chain",
803
+ source: "foreground",
804
+ cwd: parallelCwd,
805
+ stepIndex,
806
+ flatStartIndex: globalTaskIndex,
807
+ setup: worktreeSetup,
808
+ });
809
+ }
791
810
  const agentNames = step.parallel.map((task) => task.agent);
792
811
  const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex, chainSkills)
793
812
  .map((behavior, taskIndex) => suppressProgressForReadOnlyTask(behavior, parallelTemplates[taskIndex] ?? step.parallel[taskIndex]?.task, originalTask));
@@ -854,6 +873,7 @@ ${step.message}` : ""}` }],
854
873
  toolBudget: params.toolBudget,
855
874
  configToolBudget: params.configToolBudget,
856
875
  globalSemaphore,
876
+ permissions: params.permissions,
857
877
  });
858
878
  globalTaskIndex += step.parallel.length;
859
879
 
@@ -928,7 +948,7 @@ ${step.message}` : ""}` }],
928
948
  .filter(({ result, task }) => isAgentContractV1(task?.agentContract ?? step.agentContract ?? params.agentContract) && (task?.gateOn ?? step.gateOn) === "acceptance" && result.acceptance?.status === "rejected");
929
949
  if (acceptanceFailures.length > 0) {
930
950
  const acceptanceSummary = acceptanceFailures
931
- .map(({ result, originalIndex }) => `- Task ${originalIndex + 1} (${result.agent}): ${acceptanceFailureMessage(result.acceptance) ?? "acceptance rejected"}`)
951
+ .map(({ result, originalIndex }) => `- Task ${originalIndex + 1} (${result.agent}): ${(result.acceptance ? acceptanceFailureMessage(result.acceptance) : undefined) ?? "acceptance rejected"}`)
932
952
  .join("\n");
933
953
  const errorMsg = `Parallel step ${stepIndex + 1} acceptance gate failed:\n${acceptanceSummary}`;
934
954
  const summary = buildChainSummary(chainSteps, results, chainDir, "failed", { index: stepIndex, error: errorMsg });
@@ -1024,7 +1044,9 @@ ${step.message}` : ""}` }],
1024
1044
  cwd: cwd ?? ctx.cwd,
1025
1045
  reportOptional: isAgentContractV1(step.agentContract ?? params.agentContract),
1026
1046
  });
1027
- dynamicGroupStatuses[stepIndex].acceptance = groupAcceptance;
1047
+ const dynamicGroupStatus = dynamicGroupStatuses[stepIndex];
1048
+ if (!dynamicGroupStatus) throw new Error(`Missing dynamic group status at step ${stepIndex}`);
1049
+ dynamicGroupStatus.acceptance = groupAcceptance;
1028
1050
  const groupAcceptanceFailure = !isAgentContractV1(step.agentContract ?? params.agentContract) || step.gateOn === "acceptance" ? acceptanceFailureMessage(groupAcceptance) : undefined;
1029
1051
  if (groupAcceptanceFailure) {
1030
1052
  dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
@@ -1112,6 +1134,7 @@ ${step.message}` : ""}` }],
1112
1134
  toolBudget: params.toolBudget,
1113
1135
  configToolBudget: params.configToolBudget,
1114
1136
  globalSemaphore,
1137
+ permissions: params.permissions,
1115
1138
  dynamic: true,
1116
1139
  });
1117
1140
  globalTaskIndex = dynamicStartIndex + reservedDynamicItems;
@@ -1171,7 +1194,7 @@ ${step.message}` : ""}` }],
1171
1194
  .filter(({ result, task }) => isAgentContractV1(task?.agentContract ?? dynamicParallelStep.agentContract ?? params.agentContract) && (task?.gateOn ?? dynamicParallelStep.gateOn) === "acceptance" && result.acceptance?.status === "rejected");
1172
1195
  if (acceptanceFailures.length > 0) {
1173
1196
  const acceptanceSummary = acceptanceFailures
1174
- .map(({ result, originalIndex }) => `- Item ${originalIndex + 1} (${result.agent}, key ${materialized.items[originalIndex]?.key ?? originalIndex}): ${acceptanceFailureMessage(result.acceptance) ?? "acceptance rejected"}`)
1197
+ .map(({ result, originalIndex }) => `- Item ${originalIndex + 1} (${result.agent}, key ${materialized.items[originalIndex]?.key ?? originalIndex}): ${(result.acceptance ? acceptanceFailureMessage(result.acceptance) : undefined) ?? "acceptance rejected"}`)
1175
1198
  .join("\n");
1176
1199
  const errorMsg = `Dynamic step ${stepIndex + 1} acceptance gate failed:\n${acceptanceSummary}`;
1177
1200
  dynamicGroupStatuses[stepIndex] = { status: "failed", error: errorMsg };
@@ -1217,7 +1240,9 @@ ${step.message}` : ""}` }],
1217
1240
  cwd: cwd ?? ctx.cwd,
1218
1241
  reportOptional: isAgentContractV1(step.agentContract ?? params.agentContract),
1219
1242
  });
1220
- dynamicGroupStatuses[stepIndex].acceptance = groupAcceptance;
1243
+ const dynamicGroupStatus = dynamicGroupStatuses[stepIndex];
1244
+ if (!dynamicGroupStatus) throw new Error(`Missing dynamic group status at step ${stepIndex}`);
1245
+ dynamicGroupStatus.acceptance = groupAcceptance;
1221
1246
  const groupAcceptanceFailure = effectiveGroupAcceptance.explicit && (!isAgentContractV1(step.agentContract ?? params.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(groupAcceptance) : undefined;
1222
1247
  if (groupAcceptanceFailure) {
1223
1248
  dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
@@ -1340,6 +1365,7 @@ ${step.message}` : ""}` }],
1340
1365
  dynamicGroupStatuses,
1341
1366
  });
1342
1367
  r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
1368
+ permissions: params.permissions,
1343
1369
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
1344
1370
  capabilityCeiling: params.capabilityCeiling,
1345
1371
  context: params.contextForAgent?.(seqStep.agent),