pi-subagents 0.65.1 → 0.67.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 (148) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/researcher.md +23 -13
  5. package/agents/reviewer.md +3 -2
  6. package/docs/agents.md +20 -3
  7. package/docs/configuration.md +25 -5
  8. package/docs/extension-api.md +124 -18
  9. package/docs/missions.md +8 -0
  10. package/docs/models.md +59 -2
  11. package/docs/observability.md +46 -6
  12. package/docs/standalone-background.md +49 -0
  13. package/docs/tool-reference.md +20 -10
  14. package/docs/watchdog.md +35 -4
  15. package/docs/workflows.md +40 -19
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +2 -1
  18. package/prompts/parallel-review.md +1 -1
  19. package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
  20. package/skills/pi-subagents/SKILL.md +14 -0
  21. package/skills/pi-subagents/references/execution-controls.md +20 -5
  22. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  23. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  24. package/src/agents/advertised-agent-prompt.ts +94 -0
  25. package/src/agents/agent-management.ts +14 -1
  26. package/src/agents/agent-serializer.ts +2 -0
  27. package/src/agents/agents.ts +14 -0
  28. package/src/agents/builtin-names.ts +1 -0
  29. package/src/api/delegation.ts +4 -0
  30. package/src/api/preflight.ts +76 -45
  31. package/src/api/shared-types.ts +3 -1
  32. package/src/api/workflow-resources.ts +6 -0
  33. package/src/extension/fanout-child.ts +63 -4
  34. package/src/extension/index.ts +58 -8
  35. package/src/extension/public-execution.ts +4 -3
  36. package/src/extension/rpc.ts +8 -21
  37. package/src/extension/schemas.ts +71 -80
  38. package/src/extension/tool-description.ts +29 -81
  39. package/src/inspectors/actions.ts +148 -0
  40. package/src/inspectors/ghostty/actions.ts +74 -0
  41. package/src/inspectors/ghostty/plugin.ts +17 -0
  42. package/src/inspectors/herdr/actions.ts +99 -179
  43. package/src/inspectors/herdr/plugin.ts +20 -0
  44. package/src/inspectors/herdr/project-panes.ts +1 -1
  45. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  46. package/src/inspectors/plugins.ts +8 -0
  47. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  48. package/src/inspectors/types.ts +51 -0
  49. package/src/intercom/intercom-bridge.ts +50 -8
  50. package/src/intercom/native-supervisor-channel.ts +104 -67
  51. package/src/runs/background/active-async-capacity.ts +22 -18
  52. package/src/runs/background/async-execution.ts +45 -56
  53. package/src/runs/background/async-job-tracker.ts +35 -3
  54. package/src/runs/background/async-resume.ts +5 -9
  55. package/src/runs/background/async-status-snapshot.ts +10 -12
  56. package/src/runs/background/async-status.ts +17 -9
  57. package/src/runs/background/auto-drain.ts +44 -30
  58. package/src/runs/background/binary-bootstrap.ts +33 -0
  59. package/src/runs/background/chain-root-attachment.ts +8 -0
  60. package/src/runs/background/control-channel.ts +78 -44
  61. package/src/runs/background/fleet-view.ts +30 -2
  62. package/src/runs/background/notify.ts +117 -13
  63. package/src/runs/background/owned-process-tree.ts +35 -8
  64. package/src/runs/background/process-terminal.ts +23 -23
  65. package/src/runs/background/run-child-session.ts +121 -36
  66. package/src/runs/background/run-status.ts +78 -5
  67. package/src/runs/background/runner-aliases.ts +28 -9
  68. package/src/runs/background/runner-child-launch.ts +88 -0
  69. package/src/runs/background/runner-child-sessions.ts +5 -4
  70. package/src/runs/background/scheduled-runs.ts +40 -13
  71. package/src/runs/background/stale-run-reconciler.ts +3 -1
  72. package/src/runs/background/steering.ts +20 -2
  73. package/src/runs/background/subagent-runner.ts +458 -239
  74. package/src/runs/background/subagent-wait.ts +54 -8
  75. package/src/runs/background/wait-completions.ts +4 -0
  76. package/src/runs/background/wait-tool.ts +1 -1
  77. package/src/runs/foreground/async-steering-action.ts +37 -7
  78. package/src/runs/foreground/execution.ts +145 -56
  79. package/src/runs/foreground/prompt-audit.ts +3 -1
  80. package/src/runs/foreground/subagent-executor.ts +584 -297
  81. package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
  82. package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
  83. package/src/runs/shared/acceptance.ts +7 -4
  84. package/src/runs/shared/agent-contract.ts +1 -1
  85. package/src/runs/shared/async-status-projection.ts +51 -47
  86. package/src/runs/shared/capability-ceiling.ts +2 -0
  87. package/src/runs/shared/child-hooks.ts +167 -3
  88. package/src/runs/shared/child-launch.ts +28 -13
  89. package/src/runs/shared/child-lifecycle.ts +6 -3
  90. package/src/runs/shared/child-runtime-config.ts +3 -1
  91. package/src/runs/shared/child-session.ts +75 -8
  92. package/src/runs/shared/child-tool-plan.ts +124 -5
  93. package/src/runs/shared/completion-evidence.ts +2 -2
  94. package/src/runs/shared/completion-guard.ts +6 -3
  95. package/src/runs/shared/effective-system-prompt.ts +33 -0
  96. package/src/runs/shared/external-cli-runner.ts +9 -7
  97. package/src/runs/shared/host-step-status.ts +11 -11
  98. package/src/runs/shared/llm-intent-arbiter.ts +21 -11
  99. package/src/runs/shared/model-fallback.ts +12 -6
  100. package/src/runs/shared/nested-events.ts +5 -5
  101. package/src/runs/shared/orca-progress-tabs.ts +7 -1
  102. package/src/runs/shared/parallel-handoff.ts +57 -12
  103. package/src/runs/shared/parallel-utils.ts +2 -2
  104. package/src/runs/shared/pi-spawn.ts +10 -0
  105. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  106. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  107. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  108. package/src/runs/shared/run-fanout-budget.ts +8 -8
  109. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  110. package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
  111. package/src/runs/shared/task-intent.ts +46 -13
  112. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  113. package/src/runs/shared/worktree-setup-command.ts +190 -0
  114. package/src/runs/shared/worktree.ts +366 -208
  115. package/src/shared/fork-context.ts +15 -72
  116. package/src/shared/launch-contract.ts +65 -2
  117. package/src/shared/opencode-session-headers.ts +30 -0
  118. package/src/shared/types.ts +85 -61
  119. package/src/shared/utils.ts +7 -2
  120. package/src/shared/workflow-child-permit.ts +18 -13
  121. package/src/slash/delegation-adapters.ts +3 -1
  122. package/src/slash/delegation-request.ts +14 -0
  123. package/src/slash/slash-commands.ts +2 -1
  124. package/src/slash/subagents-admin.ts +11 -4
  125. package/src/tui/fleet-status.ts +164 -19
  126. package/src/tui/fleet.ts +27 -19
  127. package/src/tui/render.ts +172 -33
  128. package/src/watchdog/child-status.ts +8 -0
  129. package/src/watchdog/model-selection.ts +20 -0
  130. package/src/watchdog/permission-arbiter.ts +3 -1
  131. package/src/watchdog/register-child.ts +1 -0
  132. package/src/watchdog/register-main.ts +31 -27
  133. package/src/watchdog/review.ts +132 -67
  134. package/src/watchdog/runtime.ts +82 -20
  135. package/src/watchdog/scope.ts +1 -1
  136. package/src/watchdog/settings.ts +9 -3
  137. package/src/watchdog/tool-actions.ts +13 -12
  138. package/src/watchdog/turn-delta.ts +23 -0
  139. package/src/watchdog/types.ts +4 -0
  140. package/src/workflows/chat-progress.ts +3 -3
  141. package/src/workflows/scripted-workflow.ts +275 -17
  142. package/src/workflows/workflow-checklist.ts +13 -17
  143. package/src/workflows/workflow-child-summary.ts +57 -8
  144. package/src/workflows/workflow-preflight.ts +19 -19
  145. package/src/workflows/workflow-receipt.ts +3 -3
  146. package/src/workflows/workflow-resources.ts +96 -21
  147. package/src/workflows/workflow-settlement.ts +3 -0
  148. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -7,6 +7,7 @@ import { createHash } from "node:crypto";
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
11
  import {
11
12
  formatUnresolvedMcpDirectToolSelectors,
12
13
  resolveMcpDirectToolResolution,
@@ -16,7 +17,7 @@ import {
16
17
  import {
17
18
  TEMP_ROOT_DIR,
18
19
  type JsonSchemaObject,
19
- type LaunchResolvedChildExtensionsV1,
20
+ type LaunchResolvedChildExtensions,
20
21
  } from "../../shared/types.ts";
21
22
  import { THINKING_LEVELS } from "../../shared/model-info.ts";
22
23
  import { getAgentDir } from "../../shared/utils.ts";
@@ -60,6 +61,41 @@ const FAST_MODE_ALLOWED_MODELS = new Set([
60
61
  "openai-codex/gpt-5.6-sol",
61
62
  ]);
62
63
  const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
64
+ const PI_BUILTIN_TOOL_NAMES = new Set(["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"]);
65
+ const REPOSITORY_INSPECTION_TOOLS = new Set(["read", "grep", "find", "ls", "bash", "powershell"]);
66
+ const REVIEW_OR_SCOUT_AGENT_PATTERN = /\b(?:reviewer|scout)\b/i;
67
+ // These providers come from child hooks, not the host's builtin tool registry.
68
+ const NATIVE_COORDINATION_TOOL_NAMES = new Set(["subagent", "contact_supervisor", "subagent_supervisor"]);
69
+
70
+ export function isReviewOrScoutLaneAgent(agentName: string | undefined): boolean {
71
+ return typeof agentName === "string" && REVIEW_OR_SCOUT_AGENT_PATTERN.test(agentName);
72
+ }
73
+
74
+ export function missingPermittedRepositoryInspectionTools(
75
+ unavailableHostBuiltins: readonly string[],
76
+ excludeTools: readonly string[] = [],
77
+ ): string[] {
78
+ const excluded = new Set(excludeTools);
79
+ return unavailableHostBuiltins.filter((tool) => REPOSITORY_INSPECTION_TOOLS.has(tool) && !excluded.has(tool));
80
+ }
81
+
82
+ export function formatReviewLaneToolContractFailure(input: {
83
+ agentName?: string;
84
+ missingTools: readonly string[];
85
+ requestedTools?: readonly string[];
86
+ effectiveTools: readonly string[];
87
+ ceilingSources?: readonly string[];
88
+ excludeTools?: readonly string[];
89
+ }): string {
90
+ const subject = input.agentName ? `Agent '${input.agentName}'` : "Subagent";
91
+ return [
92
+ `${subject}: tool contract could not be satisfied; host runtime does not provide permitted required repository tools [${input.missingTools.join(", ")}].`,
93
+ `Requested tool names: ${input.requestedTools ? `[${input.requestedTools.join(", ")}]` : "not explicitly specified"}; effective tool allowlist: [${input.effectiveTools.join(", ")}].`,
94
+ ...(input.ceilingSources?.length ? [`Active capability ceiling sources: [${input.ceilingSources.join(", ")}].`] : []),
95
+ ...(input.excludeTools?.length ? [`Explicit excludeTools: [${input.excludeTools.join(", ")}].`] : []),
96
+ "This is a lane infrastructure failure, not a completed review/scout result.",
97
+ ].join(" ");
98
+ }
63
99
 
64
100
  export function deriveForkPromptCacheKey(parentSessionId: string | undefined): string | undefined {
65
101
  const parent = parentSessionId?.trim();
@@ -151,6 +187,15 @@ export interface ResolvePiLaunchToolPlanInput {
151
187
  agentName?: string;
152
188
  permissionRules?: PermissionRules;
153
189
  runtimeSnapshotHost?: McpRuntimeSnapshotHost;
190
+ /**
191
+ * When provided, child tool plans intersect declared builtin tools with
192
+ * this set. Tools the agent declares but the host does not provide are
193
+ * omitted with a non-fatal warning (tracked in `unavailableHostBuiltins`).
194
+ * Review/scout lanes fail closed when a requested, still-permitted
195
+ * repository inspection tool is among those host omissions. Intentionally
196
+ * empty or ceiling-restricted allowlists are not a minimum-tool contract.
197
+ */
198
+ hostAvailableBuiltins?: readonly string[];
154
199
  }
155
200
 
156
201
  export interface PiLaunchToolPlan {
@@ -174,6 +219,8 @@ export interface PiLaunchToolPlan {
174
219
  capabilityAudit?: SubagentCapabilityAudit;
175
220
  /** Non-fatal launch warnings; they do not change behavior. */
176
221
  warnings: string[];
222
+ /** Builtin tools the agent declared but the host runtime does not provide. */
223
+ unavailableHostBuiltins: string[];
177
224
  }
178
225
 
179
226
  function extensionIdentifier(value: string): string {
@@ -214,7 +261,7 @@ export function projectLaunchResolvedChildExtensions(
214
261
  | "extensionArgs"
215
262
  | "disableAmbientExtensions"
216
263
  >,
217
- ): LaunchResolvedChildExtensionsV1 {
264
+ ): LaunchResolvedChildExtensions {
218
265
  const runtime = boundedExtensionIdentifiers(toolPlan.runtimeExtensions);
219
266
  const configured = boundedExtensionIdentifiers(toolPlan.configuredExtensions);
220
267
  const effective = boundedExtensionIdentifiers(toolPlan.extensionArgs);
@@ -289,6 +336,31 @@ export function resolvePermissionSystemExtension(): string | undefined {
289
336
  return undefined;
290
337
  }
291
338
 
339
+ /**
340
+ * Extract the names of builtin tools the host provides. Use this to pass
341
+ * `hostAvailableBuiltins` to `resolvePiLaunchToolPlan` so child tool plans
342
+ * intersect declared agent tools with what the host actually supports.
343
+ *
344
+ * Returns `undefined` when builtin tool discovery fails or yields nothing,
345
+ * so callers skip the intersection (fail-safe to allowing all declared tools).
346
+ * This handles test mocks without proper tool registration and hosts whose
347
+ * getAllTools() throws before extensions load.
348
+ */
349
+ export function getHostBuiltinToolNames(pi: Pick<ExtensionAPI, "getAllTools">): string[] | undefined {
350
+ try {
351
+ const builtins = pi
352
+ .getAllTools()
353
+ .filter((tool) => {
354
+ const source = (tool.sourceInfo as { source?: string } | undefined)?.source;
355
+ return source === "builtin" || (source === "auto" && PI_BUILTIN_TOOL_NAMES.has(tool.name));
356
+ })
357
+ .map((tool) => tool.name);
358
+ return builtins.length > 0 ? builtins : undefined;
359
+ } catch {
360
+ return undefined;
361
+ }
362
+ }
363
+
292
364
  export function resolvePiLaunchToolPlan(
293
365
  input: ResolvePiLaunchToolPlanInput,
294
366
  ): PiLaunchToolPlan {
@@ -300,17 +372,27 @@ export function resolvePiLaunchToolPlan(
300
372
  capabilityCeiling?.allowedTools === undefined
301
373
  ? undefined
302
374
  : new Set(capabilityCeiling.allowedTools);
375
+ const hostAvailableSet =
376
+ input.hostAvailableBuiltins === undefined
377
+ ? undefined
378
+ : new Set(input.hostAvailableBuiltins);
303
379
  const requestedBuiltinTools =
304
380
  input.tools?.filter(
305
381
  (tool) =>
306
382
  !(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")),
307
383
  ) ?? [];
384
+ if (input.requireReadTool && hostAvailableSet && !hostAvailableSet.has("read")) {
385
+ const agentLabel = input.agentName ? ` for agent '${input.agentName}'` : "";
386
+ throw new Error(
387
+ `Host runtime does not provide required tool 'read'${agentLabel} for lazy skill loading.`,
388
+ );
389
+ }
308
390
  if (input.requireReadTool && allowedToolSet && !allowedToolSet.has("read")) {
309
391
  throw new Error(
310
392
  `Capability ceiling from ${capabilityCeiling?.sources.join(", ") || "unknown source"} excludes required tool 'read' for lazy skill loading.`,
311
393
  );
312
394
  }
313
- const declaredBuiltinTools =
395
+ const ceilingFilteredBuiltinTools =
314
396
  input.tools === undefined
315
397
  ? allowedToolSet
316
398
  ? [...allowedToolSet]
@@ -322,6 +404,12 @@ export function resolvePiLaunchToolPlan(
322
404
  ? ["read", ...requestedBuiltinTools]
323
405
  : requestedBuiltinTools
324
406
  ).filter((tool) => !allowedToolSet || allowedToolSet.has(tool));
407
+ const declaredBuiltinTools = hostAvailableSet
408
+ ? ceilingFilteredBuiltinTools.filter((tool) => hostAvailableSet.has(tool) || NATIVE_COORDINATION_TOOL_NAMES.has(tool))
409
+ : ceilingFilteredBuiltinTools;
410
+ const unavailableHostBuiltins = hostAvailableSet
411
+ ? ceilingFilteredBuiltinTools.filter((tool) => !hostAvailableSet.has(tool) && !NATIVE_COORDINATION_TOOL_NAMES.has(tool))
412
+ : [];
325
413
  const excludeTools = [...new Set((input.excludeTools ?? []).map((tool) => tool.trim()).filter(Boolean))];
326
414
  const excludedToolSet = new Set(excludeTools);
327
415
  const effectiveDeclaredBuiltinTools = declaredBuiltinTools.filter((tool) => !excludedToolSet.has(tool));
@@ -330,6 +418,9 @@ export function resolvePiLaunchToolPlan(
330
418
  !excludedToolSet.has("subagent") &&
331
419
  (!allowedToolSet || allowedToolSet.has("subagent"))
332
420
  );
421
+ if (effectiveDeclaredBuiltinTools.includes("subagent_supervisor") && !fanoutAuthorized) {
422
+ throw new Error("Tool 'subagent_supervisor' requires fanout authorization: include 'subagent' in the effective tools allowlist or enable allowNestedSubagents.");
423
+ }
333
424
  const toolExtensionPaths: string[] = capabilityCeiling?.denyExtensions
334
425
  ? []
335
426
  : (input.tools ?? []).filter(
@@ -370,8 +461,8 @@ export function resolvePiLaunchToolPlan(
370
461
  ...internalTools,
371
462
  ]),
372
463
  ];
373
- // Supervisor-coordination names stay in the --tools allowlist but are never
374
- // strict requirements: children register contact_supervisor at runtime through
464
+ // Upward contact stays in the --tools allowlist but is not a strict
465
+ // requirement: children register contact_supervisor at runtime through
375
466
  // the native supervisor channel (or pi-intercom). The pre-0.50 bridge always
376
467
  // appended intercom alongside contact_supervisor, so that exact pairing is
377
468
  // legacy plumbing, not a user demand for an external intercom provider;
@@ -437,6 +528,32 @@ export function resolvePiLaunchToolPlan(
437
528
  ]),
438
529
  ]
439
530
  : undefined;
531
+ const missingPermittedRepositoryTools = input.tools !== undefined
532
+ ? missingPermittedRepositoryInspectionTools(unavailableHostBuiltins, excludeTools)
533
+ : [];
534
+ if (missingPermittedRepositoryTools.length > 0 && isReviewOrScoutLaneAgent(input.agentName)) {
535
+ throw new Error(formatReviewLaneToolContractFailure({
536
+ agentName: input.agentName,
537
+ missingTools: missingPermittedRepositoryTools,
538
+ requestedTools: requestedToolNames,
539
+ effectiveTools: effectiveToolAllowlist,
540
+ ceilingSources: capabilityCeiling?.sources,
541
+ excludeTools,
542
+ }));
543
+ }
544
+ // Host pruning also happens without a ceiling (and therefore without an
545
+ // audit). Use the existing non-fatal launch warnings rather than inventing
546
+ // a ceiling or treating the requested allowlist as a minimum requirement.
547
+ if (unavailableHostBuiltins.length > 0) {
548
+ const subject = input.agentName ? `Agent '${input.agentName}'` : "Subagent";
549
+ warnings.push(
550
+ `${subject}: host runtime tool availability omitted [${unavailableHostBuiltins.join(", ")}]. `
551
+ + `Requested tool names: ${requestedToolNames ? `[${requestedToolNames.join(", ")}]` : "not explicitly specified"}; effective tool allowlist: [${effectiveToolAllowlist.join(", ")}]. `
552
+ + (capabilityCeiling ? `Active capability ceiling sources: [${capabilityCeiling.sources.join(", ") || "unknown source"}]. ` : "")
553
+ + (excludeTools.length > 0 ? `Explicit excludeTools: [${excludeTools.join(", ")}]. ` : "")
554
+ + "This is a non-fatal tool-plan diagnostic, not verification of the child's runtime tool menu.",
555
+ );
556
+ }
440
557
  const capabilityAudit = capabilityCeiling
441
558
  ? ({
442
559
  ceiling: capabilityCeiling,
@@ -474,6 +591,7 @@ export function resolvePiLaunchToolPlan(
474
591
  capabilityCeilingAgentRestrictionSources(capabilityCeiling),
475
592
  }
476
593
  : {}),
594
+ ...(unavailableHostBuiltins.length > 0 ? { unavailableHostBuiltins } : {}),
477
595
  } satisfies SubagentCapabilityAudit)
478
596
  : undefined;
479
597
  return {
@@ -495,6 +613,7 @@ export function resolvePiLaunchToolPlan(
495
613
  extensionArgs,
496
614
  disableAmbientExtensions,
497
615
  warnings,
616
+ unavailableHostBuiltins,
498
617
  ...(capabilityAudit ? { capabilityAudit } : {}),
499
618
  };
500
619
  }
@@ -26,7 +26,7 @@ export function planCompletionEvidence(input: {
26
26
  mutationAttemptObserved: boolean;
27
27
  mutationEvidence?: TrackedMutationEvidence;
28
28
  arbiterRescued?: boolean;
29
- agentContractV1: boolean;
29
+ agentContractEnabled: boolean;
30
30
  }): CompletionEvidencePlan {
31
31
  const guardBlocked = input.guard?.blocked === true;
32
32
  const guardTriggered = input.guardTriggered
@@ -59,7 +59,7 @@ export function planCompletionEvidence(input: {
59
59
  mutationExpected,
60
60
  mutationAttempted,
61
61
  fileMutation,
62
- legacyFailureError: guardTriggered && !input.agentContractV1
62
+ legacyFailureError: guardTriggered && !input.agentContractEnabled
63
63
  ? MISSING_IMPLEMENTATION_MUTATION_ERROR
64
64
  : undefined,
65
65
  };
@@ -13,6 +13,7 @@ const READ_ONLY_BUILTIN_TOOLS = new Set([
13
13
  "web_search",
14
14
  "fetch_content",
15
15
  "get_search_content",
16
+ "source_check",
16
17
  "intercom",
17
18
  "contact_supervisor",
18
19
  "structured_output",
@@ -97,10 +98,12 @@ export function validateImplementationToolContract(input: {
97
98
  const declaredMutationToolsWereRemoved = requestedMutationTools.length > 0 && !hasBuiltinMutationTool(input.tools);
98
99
  const configuredExtensionCapability = (input.configuredExtensions?.length ?? 0) > 0 && !declaredMutationToolsWereRemoved;
99
100
  if (hasMutationToolCapability(input.tools, input.mcpDirectTools) || configuredExtensionCapability) return undefined;
100
- const intent = classifyTaskMutationIntent(input.agent, input.task);
101
+ const intent = classifyTaskMutationIntent(input.acceptanceRole === "writer" ? "worker" : input.agent, input.task);
101
102
  if (intent.kind === "read-only") return undefined;
102
- const writerTaskMayMutate = isWriterRole(input.agent, input.acceptanceRole)
103
- && (taskMayMutate(input.task) || WRITER_DELIVERY_PATTERN.test(input.task));
103
+ const writerTaskMayMutate = input.acceptanceRole === "writer"
104
+ ? true
105
+ : isWriterRole(input.agent, input.acceptanceRole)
106
+ && (taskMayMutate(input.task) || WRITER_DELIVERY_PATTERN.test(input.task));
104
107
  if (intent.kind !== "implementation" && !writerTaskMayMutate && !declaredMutationToolsWereRemoved) return undefined;
105
108
  return `Agent '${input.agent}' was given an implementation task, but its tool allowlist has no mutation-capable tools. Add bash, edit, write, or another mutation-capable tool to the agent, or use a read-only task/agent.`;
106
109
  }
@@ -0,0 +1,33 @@
1
+ import type { AgentConfig } from "../../agents/agents.ts";
2
+ import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
3
+ import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
4
+ import { buildSkillInjection } from "../../agents/skills.ts";
5
+ import { injectOutputPathSystemPrompt } from "./single-output.ts";
6
+
7
+ export interface EffectiveSystemPromptInput {
8
+ /** Agent as handed to the child, including runtime-declared overlays such as the Intercom bridge. */
9
+ agent: AgentConfig;
10
+ resolvedSkills: Parameters<typeof buildSkillInjection>[0];
11
+ /** Directory that scopes memory and refinement lookups. */
12
+ cwd: string;
13
+ /** Omit when the caller injects the output path through another channel. */
14
+ outputPath?: string;
15
+ }
16
+
17
+ function appendSection(prompt: string, section: string): string {
18
+ return prompt ? `${prompt}\n\n${section}` : section;
19
+ }
20
+
21
+ /**
22
+ * Child system prompt in the order preflight and every execution path hash:
23
+ * base prompt, skills, memory, refinement overlay, output path. Runtime
24
+ * acceptance prose is appended later and stays outside launch identity.
25
+ */
26
+ export function buildEffectiveSystemPrompt(input: EffectiveSystemPromptInput): string {
27
+ let prompt = input.agent.systemPrompt?.trim() ?? "";
28
+ if (input.resolvedSkills.length > 0) prompt = appendSection(prompt, buildSkillInjection(input.resolvedSkills));
29
+ const memoryInjection = buildAgentMemoryInjection(input.agent, input.cwd);
30
+ if (memoryInjection) prompt = appendSection(prompt, memoryInjection);
31
+ prompt = appendAgentRefinementOverlay(prompt, { cwd: input.cwd, agentName: input.agent.name });
32
+ return injectOutputPathSystemPrompt(prompt, input.outputPath, input.agent);
33
+ }
@@ -3,7 +3,7 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
5
  import { finished } from "node:stream/promises";
6
- import type { ExternalProcessStatus } from "../../shared/types.ts";
6
+ import type { ExternalProcessStatus, ProcessTreeTerminal } from "../../shared/types.ts";
7
7
  import { createOwnedProcessTreeController, type OwnedProcessTreeController } from "../background/owned-process-tree.ts";
8
8
  import { omitExtensionBindingsEnv } from "./extension-bindings.ts";
9
9
  import {
@@ -141,7 +141,7 @@ function classifyInvalidation(error: string): "auth" | "permission" | "launch" {
141
141
  return "launch";
142
142
  }
143
143
 
144
- function terminateExternalProcessTree(pid: number, controller: OwnedProcessTreeController): Promise<unknown> {
144
+ function terminateExternalProcessTree(pid: number, controller: OwnedProcessTreeController): Promise<ProcessTreeTerminal> {
145
145
  if (process.platform !== "win32") return controller.terminate();
146
146
  return new Promise((resolve) => {
147
147
  const cleanup = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
@@ -239,7 +239,7 @@ export function runExternalCli(input: {
239
239
  let settled = false;
240
240
  let processTree: OwnedProcessTreeController | undefined;
241
241
  let processPid: number | undefined;
242
- let termination: Promise<unknown> | undefined;
242
+ let termination: Promise<ProcessTreeTerminal> | undefined;
243
243
  const flushProgress = () => {
244
244
  if (!latestProgress) return;
245
245
  input.onParserProgress?.(latestProgress);
@@ -381,8 +381,7 @@ export function runExternalCli(input: {
381
381
  input.registerTimeout?.(undefined);
382
382
  input.registerStop?.(undefined);
383
383
  void (async () => {
384
- if (termination) await termination;
385
- else if (processTree) await processTree.finishAfterWriterClose();
384
+ const treeProof = termination ? await termination : processTree ? await processTree.finishAfterWriterClose() : undefined;
386
385
  const endedAt = Date.now();
387
386
  const externalProcess: ExternalProcessStatus = {
388
387
  ...initialProcess,
@@ -398,15 +397,18 @@ export function runExternalCli(input: {
398
397
  input.onProcess?.(externalProcess);
399
398
  const stderr = stderrTail.text().trim();
400
399
  const parserFailure = parserError?.message ?? (parserTerminal?.state === "failed" ? parserTerminal.error ?? "External CLI parser reported terminal failure." : undefined);
400
+ const treeFailure = process.platform !== "win32" && treeProof?.state === "unknown"
401
+ ? `Process-tree cleanup failed: ${treeProof.reason}.`
402
+ : undefined;
401
403
  const error = stopped
402
404
  ? input.stopMessage ?? "Subagent stopped by user."
403
405
  : timedOut
404
406
  ? input.timeoutMessage ?? "Subagent timed out."
405
- : spawnError?.message ?? parserFailure ?? (exitCode === 0 ? undefined : stderr || `External CLI exited with code ${exitCode}.`);
407
+ : spawnError?.message ?? parserFailure ?? treeFailure ?? (exitCode === 0 ? undefined : stderr || `External CLI exited with code ${exitCode}.`);
406
408
  if (error && input.preflight && !parserError) invalidateExternalCliPreflight(input.command, input.preflight, classifyInvalidation(error));
407
409
  const result: ExternalCliRunResult = {
408
410
  output: (!parserError && parserTerminal?.state === "completed" ? parserTerminal.output ?? "" : stdoutTail.text()).trim(),
409
- exitCode: timedOut || stopped || spawnError || parserFailure ? 1 : exitCode,
411
+ exitCode: timedOut || stopped || spawnError || parserFailure || treeFailure ? 1 : exitCode,
410
412
  ...(error ? { error } : {}),
411
413
  ...(timedOut ? { timedOut: true } : {}),
412
414
  ...(stopped ? { stopped: true } : {}),
@@ -1,6 +1,6 @@
1
1
  import type {
2
2
  AsyncStatus,
3
- HostStepNodeV1,
3
+ HostStepNode,
4
4
  HostStepState,
5
5
  HostStepVerdict,
6
6
  WorkflowGraphNode,
@@ -57,7 +57,7 @@ function assertTimestamp(value: unknown, field: string, source: string, required
57
57
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`Invalid host step '${source}': ${field} must be a non-negative safe integer.`);
58
58
  }
59
59
 
60
- function assertFreshness(value: unknown, source: string): asserts value is HostStepNodeV1["freshness"] {
60
+ function assertFreshness(value: unknown, source: string): asserts value is HostStepNode["freshness"] {
61
61
  if (!isRecord(value)) throw new Error(`Invalid host step '${source}': freshness must be an object.`);
62
62
  const unknownFields = Object.keys(value).filter((field) => !FRESHNESS_FIELDS.has(field));
63
63
  if (unknownFields.length > 0) throw new Error(`Invalid host step '${source}': freshness has unsupported fields: ${unknownFields.join(", ")}.`);
@@ -70,7 +70,7 @@ function assertFreshness(value: unknown, source: string): asserts value is HostS
70
70
  * Validate the persisted host-step contract. This deliberately rejects unknown
71
71
  * fields and unbounded values so status and receipt loaders fail closed.
72
72
  */
73
- export function assertHostStepNode(value: unknown, source = "status"): asserts value is HostStepNodeV1 {
73
+ export function assertHostStepNode(value: unknown, source = "status"): asserts value is HostStepNode {
74
74
  if (!isRecord(value)) throw new Error(`Invalid host step '${source}': expected an object.`);
75
75
  const unknownFields = Object.keys(value).filter((field) => !HOST_STEP_FIELDS.has(field));
76
76
  if (unknownFields.length > 0) throw new Error(`Invalid host step '${source}': unsupported fields: ${unknownFields.join(", ")}.`);
@@ -105,7 +105,7 @@ export function assertHostStepNode(value: unknown, source = "status"): asserts v
105
105
  assertTimestamp(value.deadlineAt, "deadlineAt", source);
106
106
  }
107
107
 
108
- export function parseHostStepNode(value: unknown, source = "status"): HostStepNodeV1 {
108
+ export function parseHostStepNode(value: unknown, source = "status"): HostStepNode {
109
109
  assertHostStepNode(value, source);
110
110
  return {
111
111
  ...value,
@@ -113,7 +113,7 @@ export function parseHostStepNode(value: unknown, source = "status"): HostStepNo
113
113
  };
114
114
  }
115
115
 
116
- export function assertUniqueHostStepIds(hostSteps: readonly HostStepNodeV1[], source = "status"): void {
116
+ export function assertUniqueHostStepIds(hostSteps: readonly HostStepNode[], source = "status"): void {
117
117
  const ids = new Set<string>();
118
118
  for (const hostStep of hostSteps) {
119
119
  if (ids.has(hostStep.id)) throw new Error(`Invalid host step '${source}': duplicate host step id '${hostStep.id}'.`);
@@ -122,11 +122,11 @@ export function assertUniqueHostStepIds(hostSteps: readonly HostStepNodeV1[], so
122
122
  }
123
123
 
124
124
  /** Return only valid host nodes so an untrusted in-memory projection fails closed. */
125
- export function validHostStepNodes(graph: WorkflowGraphSnapshot | undefined): HostStepNodeV1[] {
125
+ export function validHostStepNodes(graph: WorkflowGraphSnapshot | undefined): HostStepNode[] {
126
126
  const nodes = graph?.nodes ?? [];
127
127
  const nodeIdCounts = new Map<string, number>();
128
128
  for (const node of nodes) nodeIdCounts.set(node.id, (nodeIdCounts.get(node.id) ?? 0) + 1);
129
- const hostSteps: HostStepNodeV1[] = [];
129
+ const hostSteps: HostStepNode[] = [];
130
130
  for (const [index, node] of nodes.entries()) {
131
131
  if (node.kind !== "host-step") continue;
132
132
  try {
@@ -147,7 +147,7 @@ export function assertWorkflowGraphHostSteps(graph: WorkflowGraphSnapshot | unde
147
147
  if (!Array.isArray(graph.nodes)) throw new Error(`Invalid host step '${source}.workflowGraph': nodes must be an array.`);
148
148
  const hostStepCount = graph.nodes.filter((node) => node.kind === "host-step").length;
149
149
  if (hostStepCount > HOST_STEP_MAX_COUNT) throw new Error(`Invalid host step '${source}': workflowGraph contains more than ${HOST_STEP_MAX_COUNT} host steps.`);
150
- const hostSteps: HostStepNodeV1[] = [];
150
+ const hostSteps: HostStepNode[] = [];
151
151
  for (const [index, node] of graph.nodes.entries()) {
152
152
  if (node.kind !== "host-step") continue;
153
153
  const hostStep = parseHostStepNode(node.hostStep, `${source}.workflowGraph.nodes[${index}].hostStep`);
@@ -161,7 +161,7 @@ export function assertWorkflowGraphHostSteps(graph: WorkflowGraphSnapshot | unde
161
161
  }
162
162
  }
163
163
 
164
- export function hostStepWorkflowNode(hostStep: HostStepNodeV1): WorkflowGraphNode {
164
+ export function hostStepWorkflowNode(hostStep: HostStepNode): WorkflowGraphNode {
165
165
  assertHostStepNode(hostStep, hostStep.id);
166
166
  return {
167
167
  id: hostStep.id,
@@ -172,7 +172,7 @@ export function hostStepWorkflowNode(hostStep: HostStepNodeV1): WorkflowGraphNod
172
172
  };
173
173
  }
174
174
 
175
- function workflowNodeStatus(hostStep: HostStepNodeV1): WorkflowNodeStatus {
175
+ function workflowNodeStatus(hostStep: HostStepNode): WorkflowNodeStatus {
176
176
  if (hostStep.state === "pending") return "pending";
177
177
  if (hostStep.state === "running") return "running";
178
178
  if (hostStep.state === "cancelled") return "stopped";
@@ -187,7 +187,7 @@ function workflowNodeStatus(hostStep: HostStepNodeV1): WorkflowNodeStatus {
187
187
  */
188
188
  export function upsertHostStep(input: {
189
189
  status: AsyncStatus;
190
- hostStep: HostStepNodeV1;
190
+ hostStep: HostStepNode;
191
191
  persist: (status: AsyncStatus) => void;
192
192
  }): AsyncStatus {
193
193
  const hostStep = parseHostStepNode(input.hostStep, input.hostStep.id);
@@ -4,6 +4,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
  import type { ProviderHeaders } from "@earendil-works/pi-ai";
5
5
  import { Type, type Static } from "typebox";
6
6
  import { agentStreamOptions } from "../../shared/agent-stream-options.ts";
7
+ import { opencodeSessionHeaders } from "../../shared/opencode-session-headers.ts";
7
8
 
8
9
  /**
9
10
  * LLM intent arbiter for the completion mutation guard.
@@ -56,6 +57,8 @@ interface ArbiterRuntime {
56
57
  /** Registered provider stream, usable only when its api matches the model. */
57
58
  registeredStreamFn?: StreamFn;
58
59
  registeredApi?: string;
60
+ /** Session id for OpenCode session-routing headers, when the caller has one. */
61
+ sessionId?: string;
59
62
  timeoutMs: number;
60
63
  }
61
64
 
@@ -76,9 +79,14 @@ export interface TaskMutationArbiterOptions {
76
79
  const DEFAULT_ARBITER_TIMEOUT_MS = 10_000;
77
80
 
78
81
  type RegistryModel = ReturnType<NonNullable<ExtensionContext["modelRegistry"]["find"]>>;
82
+ /** Model services the arbiter needs, plus the caller's session id for OpenCode session-routing headers. */
83
+ export type ArbiterModelContext = Pick<ExtensionContext, "model" | "modelRegistry"> & {
84
+ /** Session id the headers attach to (the child's, captured by detached runners; the parent's, passed by foreground callers). */
85
+ sessionId?: string;
86
+ };
79
87
 
80
88
  function resolveArbiterModel(
81
- ctx: ExtensionContext,
89
+ ctx: ArbiterModelContext,
82
90
  options?: TaskMutationArbiterOptions,
83
91
  ): NonNullable<RegistryModel> | null {
84
92
  const registry = ctx.modelRegistry as {
@@ -98,7 +106,7 @@ function resolveArbiterModel(
98
106
  }
99
107
 
100
108
  function resolveArbiterRuntime(
101
- ctx: ExtensionContext,
109
+ ctx: ArbiterModelContext,
102
110
  options?: TaskMutationArbiterOptions,
103
111
  ): ArbiterRuntime | null {
104
112
  const model = resolveArbiterModel(ctx, options);
@@ -112,14 +120,15 @@ function resolveArbiterRuntime(
112
120
  explicitStreamFn: options?.streamFn,
113
121
  registeredStreamFn: registered?.streamSimple,
114
122
  registeredApi: registered?.api,
123
+ sessionId: ctx.sessionId,
115
124
  timeoutMs: options?.timeoutMs ?? DEFAULT_ARBITER_TIMEOUT_MS,
116
125
  };
117
126
  }
118
127
 
119
128
  async function resolveArbiterAuth(
120
- ctx: ExtensionContext,
129
+ ctx: ArbiterModelContext,
121
130
  model: RegistryModel,
122
- ): Promise<ArbiterAuth> {
131
+ ): Promise<ArbiterAuth | undefined> {
123
132
  const registry = ctx.modelRegistry as {
124
133
  getApiKeyAndHeaders?: (m: RegistryModel) => Promise<{
125
134
  ok: boolean;
@@ -135,14 +144,14 @@ async function resolveArbiterAuth(
135
144
  if (!registry.getApiKeyAndHeaders) return {};
136
145
  try {
137
146
  const auth = await registry.getApiKeyAndHeaders(model);
138
- if (auth.ok === false) return {};
147
+ if (auth.ok === false) return undefined;
139
148
  return {
140
149
  ...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
141
150
  ...(auth.headers ? { headers: auth.headers } : {}),
142
151
  ...(auth.env ? { env: auth.env } : {}),
143
152
  };
144
153
  } catch {
145
- return {};
154
+ return undefined;
146
155
  }
147
156
  }
148
157
 
@@ -150,12 +159,13 @@ async function resolveArbiterAuth(
150
159
  function authWrappedStreamFn(
151
160
  base: StreamFn,
152
161
  auth: ArbiterAuth,
162
+ sessionId: string | undefined,
153
163
  ): StreamFn {
154
164
  return (model, context, streamOptions) => base(model, context, {
155
165
  ...(streamOptions ?? {}),
156
166
  ...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
157
167
  ...(auth.env || streamOptions?.env ? { env: { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } } : {}),
158
- headers: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
168
+ headers: { ...opencodeSessionHeaders(model, sessionId), ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
159
169
  });
160
170
  }
161
171
 
@@ -199,7 +209,7 @@ async function runArbitration(
199
209
  tools: [tool],
200
210
  },
201
211
  convertToLlm,
202
- ...agentStreamOptions(authWrappedStreamFn(streamFn, auth)),
212
+ ...agentStreamOptions(authWrappedStreamFn(streamFn, auth, runtime.sessionId)),
203
213
  getApiKey: (providerName) =>
204
214
  providerName === runtime.model.provider ? auth.apiKey : undefined,
205
215
  beforeToolCall: async ({ toolCall }) =>
@@ -230,9 +240,9 @@ async function runArbitration(
230
240
  }
231
241
  }
232
242
 
233
- /** Create a memoized arbiter bound to the parent session's model, or undefined when disabled/unavailable. */
243
+ /** Create a memoized arbiter bound to the supplied model services, or undefined when disabled/unavailable. */
234
244
  export function createTaskMutationArbiter(
235
- ctx: ExtensionContext,
245
+ ctx: ArbiterModelContext,
236
246
  options?: TaskMutationArbiterOptions,
237
247
  ): TaskMutationArbiter | undefined {
238
248
  if (process.env.PI_SUBAGENTS_LLM_INTENT_ARBITER === "0") return undefined;
@@ -244,7 +254,7 @@ export function createTaskMutationArbiter(
244
254
  const cached = cache.get(key);
245
255
  if (cached) return cached;
246
256
  const auth = await resolveArbiterAuth(ctx, runtime.model);
247
- const verdict = await runArbitration(runtime, auth, task);
257
+ const verdict = auth ? await runArbitration(runtime, auth, task) : "unavailable";
248
258
  if (cache.size > 200) cache.clear();
249
259
  cache.set(key, verdict);
250
260
  return verdict;
@@ -1,4 +1,4 @@
1
- import { splitKnownThinkingSuffix, type ModelInfo as AvailableModelInfo } from "../../shared/model-info.ts";
1
+ import { splitKnownThinkingSuffix as splitThinkingSuffix, type ModelInfo as AvailableModelInfo } from "../../shared/model-info.ts";
2
2
  import type { Usage } from "../../shared/types.ts";
3
3
  import { filterFallbackCandidates, findModelExclusion, parseModelKey, recordModelFailure } from "./model-exclusions.ts";
4
4
  import { checkModelScope, type ModelScopeCheckRule, type ModelScopeViolation, type ModelSource } from "./model-scope.ts";
@@ -14,9 +14,7 @@ interface ModelAttemptSummary {
14
14
  usage?: Usage;
15
15
  }
16
16
 
17
- export function splitThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
18
- return splitKnownThinkingSuffix(model);
19
- }
17
+ export { splitThinkingSuffix };
20
18
 
21
19
  /** Aliases apply only to the resolved launch candidate (without its thinking suffix) and the exact raw response ID. */
22
20
  export function formatSubagentModelVerificationError(
@@ -38,7 +36,7 @@ export function formatSubagentModelVerificationError(
38
36
  const expectedFullIdLeaf = expectedEntry.fullId.slice(expectedEntry.fullId.lastIndexOf("/") + 1);
39
37
  if (expectedIdLeaf === observedBase || expectedFullIdLeaf === observedBase) return undefined;
40
38
  }
41
- return `model_verification_failed: child reported a different model than the launch candidate. Expected '${expectedModel}' but observed '${observedModel}'.`;
39
+ return `model_verification_failed: native Pi child reported a different model than the launch candidate. Expected '${expectedModel}' but observed '${observedModel}'. If you have independently verified this response ID identifies the requested model, declare the exact mapping in modelResponseAliases in ~/.pi/agent/extensions/subagent/config.json (see docs/configuration.md#modelresponsealiases). Use the resolved provider/model ID without its thinking suffix as the key. This leaves the outgoing request unchanged. Configuration changes affect new independent native runs; resumed native runs retain their launch-time declaration. External CLI adapters do not use this setting.`;
42
40
  }
43
41
 
44
42
  /** Sentinel model value requesting that a subagent inherit the parent session's model. */
@@ -537,6 +535,7 @@ export function buildModelCandidates(
537
535
  }
538
536
 
539
537
  const RETRYABLE_MODEL_FAILURE_PATTERNS = [
538
+ /^REQUEST_LIMIT_EXCEEDED$/,
540
539
  /rate\s*limit/i,
541
540
  /usage\s*limit/i,
542
541
  /too many requests/i,
@@ -544,6 +543,8 @@ const RETRYABLE_MODEL_FAILURE_PATTERNS = [
544
543
  /quota/i,
545
544
  /billing/i,
546
545
  /credit/i,
546
+ // OpenRouter can return only a status-prefixed body, without auth-related prose.
547
+ /^\s*401\s*:/,
547
548
  /auth(?:entication)?/i,
548
549
  /unauthori[sz]ed/i,
549
550
  /forbidden/i,
@@ -608,8 +609,13 @@ export function isRetryableModelFailureAttempt(input: { error: string | undefine
608
609
  return Boolean(error && input.messages?.some((message) => messageError(message)?.trim() === error));
609
610
  }
610
611
 
612
+ // Request-shape failures can match broad fallback signals such as "upstream",
613
+ // but do not establish that the model is unhealthy for subsequent requests.
614
+ const REQUEST_SHAPE_FAILURE_PATTERN = /\b(?:bad[ _]request|invalid[ _]argument|invalid_request_error)\b/i;
615
+
611
616
  export function recordRetryableModelFailure(model: string | undefined, error: string | undefined): void {
612
- if (!model || !isRetryableModelFailure(error)) return;
617
+ if (!model || !error || !isRetryableModelFailure(error) || isContextOverflow(error)) return;
618
+ if (REQUEST_SHAPE_FAILURE_PATTERN.test(error)) return;
613
619
  const { provider, modelId } = parseModelKey(model);
614
620
  recordModelFailure({ modelId, reason: error, ...(provider ? { provider } : {}) });
615
621
  }