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
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { keyText, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  import { Key, matchesKey, type Component, type TUI } from "@earendil-works/pi-tui";
6
- import { BUILTIN_AGENT_NAMES, discoverAgents, discoverAgentsAll, type ChainConfig } from "../agents/agents.ts";
6
+ import { BUILTIN_AGENT_NAMES, discoverAgents } from "../agents/agents.ts";
7
7
  import {
8
8
  DEFAULT_PROVIDER_MODELS_MAX_AGE_DAYS,
9
9
  applySubagentProfile,
@@ -14,14 +14,11 @@ import {
14
14
  refreshProviderModelCatalog,
15
15
  } from "../profiles/profiles.ts";
16
16
  import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
17
- import { isDynamicParallelStep, isParallelStep, type ChainStep } from "../shared/settings.ts";
18
17
  import { findModelInfo, toModelInfo } from "../shared/model-info.ts";
19
18
  import { formatTokens, shortenPath } from "../shared/formatters.ts";
20
19
  import { listAsyncRuns, formatAsyncRunProgressLabel, type AsyncRunSummary } from "../runs/background/async-status.ts";
21
- import { scheduledRunStorePath } from "../runs/background/scheduled-runs.ts";
20
+ import { listScheduledRunSummaries } from "../runs/background/scheduled-runs.ts";
22
21
  import { SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
23
- import { assertJsonSchemaObject } from "../runs/shared/structured-output.ts";
24
- import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
25
22
  import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
26
23
  import { registerPromptWorkflowCommands } from "./prompt-workflows.ts";
27
24
  import { openSubagentsAdmin } from "./subagents-admin.ts";
@@ -41,7 +38,7 @@ import {
41
38
  SLASH_SUBAGENT_RESPONSE_EVENT,
42
39
  SLASH_SUBAGENT_STARTED_EVENT,
43
40
  SLASH_SUBAGENT_UPDATE_EVENT,
44
- ASYNC_DIR,
41
+ DIRS,
45
42
  type Details,
46
43
  type JsonSchemaObject,
47
44
  type SingleResult,
@@ -55,14 +52,6 @@ interface InlineConfig {
55
52
  reads?: string[] | false;
56
53
  model?: string;
57
54
  skill?: string[] | false;
58
- progress?: boolean;
59
- as?: string;
60
- label?: string;
61
- phase?: string;
62
- cwd?: string;
63
- count?: number;
64
- outputSchema?: string;
65
- acceptance?: string;
66
55
  }
67
56
 
68
57
  const parseInlineConfig = (raw: string): InlineConfig => {
@@ -71,10 +60,7 @@ const parseInlineConfig = (raw: string): InlineConfig => {
71
60
  const trimmed = part.trim();
72
61
  if (!trimmed) continue;
73
62
  const eq = trimmed.indexOf("=");
74
- if (eq === -1) {
75
- if (trimmed === "progress") config.progress = true;
76
- continue;
77
- }
63
+ if (eq === -1) continue;
78
64
  const key = trimmed.slice(0, eq).trim();
79
65
  const val = trimmed.slice(eq + 1).trim();
80
66
  switch (key) {
@@ -83,14 +69,6 @@ const parseInlineConfig = (raw: string): InlineConfig => {
83
69
  case "reads": config.reads = val === "false" ? false : val.split("+").filter(Boolean); break;
84
70
  case "model": config.model = val || undefined; break;
85
71
  case "skill": case "skills": config.skill = val === "false" ? false : val.split("+").filter(Boolean); break;
86
- case "progress": config.progress = val !== "false"; break;
87
- case "as": config.as = val || undefined; break;
88
- case "label": config.label = val || undefined; break;
89
- case "phase": config.phase = val || undefined; break;
90
- case "cwd": config.cwd = val || undefined; break;
91
- case "count": { const n = Number(val); if (Number.isInteger(n) && n > 0) config.count = n; break; }
92
- case "outputSchema": config.outputSchema = val || undefined; break;
93
- case "acceptance": config.acceptance = val || undefined; break;
94
72
  }
95
73
  }
96
74
  return config;
@@ -125,69 +103,11 @@ const extractExecutionFlags = (rawArgs: string): { args: string; bg: boolean; fo
125
103
  return { args, bg, fork };
126
104
  };
127
105
 
128
- const makeAgentCompletions = (state: SubagentState, multiAgent: boolean) => (prefix: string) => {
129
- if (!state.baseCwd) return null;
130
- const agents = discoverAgents(state.baseCwd, "both").agents;
131
- if (!multiAgent) {
132
- if (prefix.includes(" ")) return null;
133
- return agents.filter((a) => a.name.startsWith(prefix)).map((a) => ({ value: a.name, label: a.name }));
134
- }
135
-
136
- // Find the start of the current chain step: after the last top-level `->` arrow or `(`,
137
- // or after a `|` *inside* a group. A `|` at depth 0 is plain task text (only `(` opens a
138
- // group), so it must not restart agent completion — otherwise `scout -- do x | wr` would
139
- // wrongly resume suggesting agents past the `--` task. Quotes are tracked so separators
140
- // inside a task are ignored.
141
- let inSingle = false, inDouble = false, depth = 0, segStart = 0;
142
- for (let i = 0; i < prefix.length; i++) {
143
- const ch = prefix[i]!;
144
- if (inSingle) { if (ch === "'") inSingle = false; continue; }
145
- if (inDouble) { if (ch === '"') inDouble = false; continue; }
146
- if (ch === "'") { inSingle = true; continue; }
147
- if (ch === '"') { inDouble = true; continue; }
148
- if (ch === "(") {
149
- if (!prefix.slice(segStart, i).includes(" -- ")) {
150
- depth++;
151
- segStart = i + 1;
152
- }
153
- }
154
- else if (ch === ")") {
155
- if (depth > 0) {
156
- depth--;
157
- segStart = i + 1;
158
- }
159
- }
160
- else if (ch === "|" && depth > 0) segStart = i + 1;
161
- else if (ch === ">" && prefix[i - 1] === "-" && depth === 0) segStart = i + 1;
162
- }
163
- // Inside an open quote, or once the task has started (`--` / a quote), we are no
164
- // longer typing an agent name.
165
- if (inSingle || inDouble) return null;
166
- const segment = prefix.slice(segStart);
167
- if (segment.includes(" -- ") || segment.includes('"') || segment.includes("'")) return null;
168
-
169
- const lastWord = (segment.match(/(\S*)$/) || ["", ""])[1];
170
- let beforeLastWord = prefix.slice(0, prefix.length - lastWord.length);
171
- // A bare `->` or `|` just typed (no trailing space) needs a separating space;
172
- // `(` glues naturally to the agent name.
173
- if (lastWord === "" && /[>|]$/.test(beforeLastWord)) beforeLastWord = `${beforeLastWord} `;
174
-
175
- return agents.filter((a) => a.name.startsWith(lastWord)).map((a) => ({ value: `${beforeLastWord}${a.name}`, label: a.name }));
176
- };
177
-
178
- const discoverSavedChains = (cwd: string): ChainConfig[] => {
179
- const chainsByName = new Map<string, ChainConfig>();
180
- for (const chain of discoverAgentsAll(cwd).chains) {
181
- chainsByName.set(chain.name, chain);
182
- }
183
- return Array.from(chainsByName.values());
184
- };
185
-
186
- const makeChainCompletions = (state: SubagentState) => (prefix: string) => {
187
- if (prefix.includes(" ") || !state.baseCwd) return null;
188
- return discoverSavedChains(state.baseCwd)
189
- .filter((chain) => chain.name.startsWith(prefix))
190
- .map((chain) => ({ value: chain.name, label: chain.name }));
106
+ const makeAgentCompletions = (state: SubagentState) => (prefix: string) => {
107
+ if (!state.baseCwd || prefix.includes(" ")) return null;
108
+ return discoverAgents(state.baseCwd, "both").agents
109
+ .filter((agent) => agent.name.startsWith(prefix))
110
+ .map((agent) => ({ value: agent.name, label: agent.name }));
191
111
  };
192
112
 
193
113
  const makeBuiltinAgentNameCompletions = () => (prefix: string) => {
@@ -241,7 +161,7 @@ type StopSelectorResult = { confirmed: boolean; target?: StopSelectorTarget };
241
161
 
242
162
  function commandForTarget(target: StopSelectorTarget): string {
243
163
  return target.kind === "scheduled"
244
- ? `subagent({ action: "schedule-cancel", id: ${JSON.stringify(target.id)} })`
164
+ ? `subagent({ action: "schedule.pause", id: ${JSON.stringify(target.id)} })`
245
165
  : `subagent({ action: "stop", id: ${JSON.stringify(target.id)} })`;
246
166
  }
247
167
 
@@ -257,35 +177,26 @@ function formatAsyncStopTarget(run: AsyncRunSummary): StopSelectorTarget {
257
177
  };
258
178
  }
259
179
 
260
- function scheduledStopTargets(ctx: ExtensionContext, state: SubagentState): StopSelectorTarget[] {
261
- const sessionId = state.currentSessionId ?? ctx.sessionManager.getSessionId();
262
- if (!sessionId) return [];
263
- const storePath = scheduledRunStorePath(ctx.cwd, sessionId);
264
- if (!fs.existsSync(storePath)) return [];
265
- let parsed: unknown;
180
+ function scheduledStopTargets(ctx: ExtensionContext, _state: SubagentState): StopSelectorTarget[] {
266
181
  try {
267
- parsed = JSON.parse(fs.readFileSync(storePath, "utf-8"));
182
+ return listScheduledRunSummaries(ctx.cwd)
183
+ .filter((schedule) => !schedule.paused && !schedule.activeRunId && schedule.trigger.nextRunAt)
184
+ .sort((left, right) => left.trigger.nextRunAt!.localeCompare(right.trigger.nextRunAt!))
185
+ .map((schedule) => ({
186
+ kind: "scheduled" as const,
187
+ id: schedule.id,
188
+ label: `${schedule.id} · ${schedule.name}`,
189
+ detail: `scheduled · ${schedule.trigger.nextRunAt}`,
190
+ actionLabel: "pause schedule",
191
+ }));
268
192
  } catch {
269
193
  return [];
270
194
  }
271
- const jobs = parsed && typeof parsed === "object" && Array.isArray((parsed as { jobs?: unknown }).jobs)
272
- ? (parsed as { jobs: Array<Record<string, unknown>> }).jobs
273
- : [];
274
- return jobs
275
- .filter((job) => job.state === "scheduled" && typeof job.id === "string" && typeof job.name === "string" && typeof job.runAt === "number")
276
- .sort((left, right) => Number(left.runAt) - Number(right.runAt))
277
- .map((job) => ({
278
- kind: "scheduled" as const,
279
- id: String(job.id),
280
- label: `${String(job.id)} · ${String(job.name)}`,
281
- detail: `scheduled · ${new Date(Number(job.runAt)).toISOString()}`,
282
- actionLabel: "cancel scheduled run",
283
- }));
284
195
  }
285
196
 
286
197
  function discoverStopTargets(ctx: ExtensionContext, state: SubagentState): StopSelectorTarget[] {
287
198
  const sessionId = state.currentSessionId ?? ctx.sessionManager.getSessionId() ?? undefined;
288
- const asyncTargets = listAsyncRuns(ASYNC_DIR, {
199
+ const asyncTargets = listAsyncRuns(DIRS.async, {
289
200
  states: ["queued", "running"],
290
201
  ...(sessionId ? { sessionId } : {}),
291
202
  }).map(formatAsyncStopTarget);
@@ -367,6 +278,8 @@ class SubagentsStopSelector implements Component {
367
278
  }
368
279
  }
369
280
 
281
+ invalidate(): void {}
282
+
370
283
  render(width: number): string[] {
371
284
  const contentWidth = Math.max(40, Math.min(this.width, width || this.width));
372
285
  const lines = [this.theme.bold("Stop subagent run"), this.theme.fg("dim", "Select a current-session async run to stop, or a scheduled run to cancel."), ""];
@@ -514,58 +427,6 @@ function getProfileWorkerModel(profile: { subagents?: { agentOverrides?: Record<
514
427
  return typeof model === "string" && model.trim() ? model.trim() : undefined;
515
428
  }
516
429
 
517
- function loadSavedOutputSchema(chain: ChainConfig, stepAgent: string, outputSchema: unknown): JsonSchemaObject | undefined {
518
- if (outputSchema === undefined) return undefined;
519
- if (typeof outputSchema === "string") {
520
- const schemaPath = path.isAbsolute(outputSchema)
521
- ? outputSchema
522
- : path.join(path.dirname(chain.filePath), outputSchema);
523
- const parsed = JSON.parse(fs.readFileSync(schemaPath, "utf-8")) as unknown;
524
- assertJsonSchemaObject(parsed, `outputSchema for chain '${chain.name}' step '${stepAgent}' (${schemaPath})`);
525
- return parsed;
526
- }
527
- assertJsonSchemaObject(outputSchema, `outputSchema for chain '${chain.name}' step '${stepAgent}'`);
528
- return outputSchema;
529
- }
530
-
531
- const mapSavedChainSteps = (chain: ChainConfig, worktree = false): ChainStep[] => {
532
- return (chain.steps as unknown as Array<ChainStep & { skills?: string[] | false }>).map((step) => {
533
- if (isParallelStep(step)) {
534
- const parallel = step.parallel.map((task) => {
535
- const { outputSchema: rawOutputSchema, ...rest } = task as typeof task & { outputSchema?: unknown };
536
- const outputSchema = loadSavedOutputSchema(chain, task.agent, rawOutputSchema);
537
- return { ...rest, ...(outputSchema ? { outputSchema } : {}) };
538
- });
539
- return { ...step, parallel, ...(worktree ? { worktree: true } : {}) };
540
- }
541
- if (isDynamicParallelStep(step)) {
542
- const { outputSchema: rawOutputSchema, ...parallelRest } = step.parallel as typeof step.parallel & { outputSchema?: unknown };
543
- const outputSchema = loadSavedOutputSchema(chain, step.parallel.agent, rawOutputSchema);
544
- const collectSchema = loadSavedOutputSchema(chain, `${step.collect.as} collection`, step.collect.outputSchema);
545
- return {
546
- ...step,
547
- parallel: { ...parallelRest, ...(outputSchema ? { outputSchema } : {}) },
548
- collect: { ...step.collect, ...(collectSchema ? { outputSchema: collectSchema } : {}) },
549
- };
550
- }
551
- const outputSchema = loadSavedOutputSchema(chain, step.agent, (step as { outputSchema?: unknown }).outputSchema);
552
- return {
553
- agent: step.agent,
554
- task: step.task || undefined,
555
- ...(step.phase ? { phase: step.phase } : {}),
556
- ...(step.label ? { label: step.label } : {}),
557
- ...(step.as ? { as: step.as } : {}),
558
- ...(outputSchema ? { outputSchema } : {}),
559
- ...((step as { acceptance?: unknown }).acceptance !== undefined ? { acceptance: (step as { acceptance?: unknown }).acceptance } : {}),
560
- output: step.output,
561
- outputMode: step.outputMode,
562
- reads: step.reads,
563
- progress: step.progress,
564
- skill: step.skill ?? step.skills,
565
- model: step.model,
566
- };
567
- });
568
- };
569
430
 
570
431
  async function requestSlashRun(
571
432
  pi: ExtensionAPI,
@@ -760,400 +621,6 @@ function launchSlashSubagent(
760
621
  void runSlashSubagent(pi, ctx, params);
761
622
  }
762
623
 
763
- export interface GroupConfig { concurrency?: number; failFast?: boolean; worktree?: boolean }
764
- export interface ParsedStep { kind: "step"; name: string; config: InlineConfig; task?: string }
765
- export interface ParsedGroup { kind: "group"; tasks: ParsedStep[]; config: GroupConfig }
766
- export type ParsedGroupStep = ParsedStep | ParsedGroup;
767
-
768
- export const PARALLEL_GROUP_USAGE =
769
- 'Usage: /chain agent "task" -> (agent2 "task" | agent3 "task") -> agent4';
770
-
771
- export class SlashParseError extends Error {}
772
-
773
- // Walk `input` tracking quote/paren state; returns true if parens are unbalanced.
774
- function findUnmatchedCloseParen(input: string): boolean {
775
- let depth = 0, inSingle = false, inDouble = false;
776
- for (let i = 0; i < input.length; i++) {
777
- const ch = input[i]!;
778
- if (inSingle) { if (ch === "'") inSingle = false; continue; }
779
- if (inDouble) { if (ch === '"') inDouble = false; continue; }
780
- if (ch === "'") { inSingle = true; continue; }
781
- if (ch === '"') { inDouble = true; continue; }
782
- if (ch === "(") depth++;
783
- else if (ch === ")") { depth--; if (depth < 0) return true; }
784
- }
785
- return depth !== 0;
786
- }
787
-
788
- // Split on top-level " -> ", ignoring arrows inside quotes or parentheses.
789
- function splitOnArrow(input: string): string[] {
790
- const segments: string[] = [];
791
- let depth = 0, inSingle = false, inDouble = false, start = 0;
792
- for (let i = 0; i < input.length; i++) {
793
- const ch = input[i]!;
794
- if (inSingle) { if (ch === "'") inSingle = false; continue; }
795
- if (inDouble) { if (ch === '"') inDouble = false; continue; }
796
- if (ch === "'") { inSingle = true; continue; }
797
- if (ch === '"') { inDouble = true; continue; }
798
- if (ch === "(") depth++;
799
- else if (ch === ")") depth--;
800
- else if (depth === 0 && ch === "-" && input[i + 1] === ">" && input[i + 2] === " ") {
801
- segments.push(input.slice(start, i));
802
- i += 2;
803
- start = i + 1;
804
- }
805
- }
806
- segments.push(input.slice(start));
807
- return segments;
808
- }
809
-
810
- // Split a group's inner text on top-level " | ", ignoring pipes inside quotes/parens.
811
- function splitGroupTasks(inner: string): string[] {
812
- const parts: string[] = [];
813
- let depth = 0, inSingle = false, inDouble = false, start = 0;
814
- for (let i = 0; i < inner.length; i++) {
815
- const ch = inner[i]!;
816
- if (inSingle) { if (ch === "'") inSingle = false; continue; }
817
- if (inDouble) { if (ch === '"') inDouble = false; continue; }
818
- if (ch === "'") { inSingle = true; continue; }
819
- if (ch === '"') { inDouble = true; continue; }
820
- if (ch === "(") depth++;
821
- else if (ch === ")") depth--;
822
- else if (ch === "|" && depth === 0) {
823
- parts.push(inner.slice(start, i));
824
- start = i + 1;
825
- }
826
- }
827
- parts.push(inner.slice(start));
828
- return parts;
829
- }
830
-
831
- export function parseSingleTaskToken(token: string): ParsedStep {
832
- let agentPart: string;
833
- let task: string | undefined;
834
- const qMatch = token.match(/^(\S+(?:\[[^\]]*\])?)\s+(?:"([^"]*)"|'([^']*)')$/);
835
- if (qMatch) {
836
- agentPart = qMatch[1]!;
837
- task = (qMatch[2] ?? qMatch[3]) || undefined;
838
- } else {
839
- const dashIdx = token.indexOf(" -- ");
840
- if (dashIdx !== -1) {
841
- agentPart = token.slice(0, dashIdx).trim();
842
- task = token.slice(dashIdx + 4).trim() || undefined;
843
- } else {
844
- agentPart = token;
845
- }
846
- }
847
- return { kind: "step", ...parseAgentToken(agentPart), task };
848
- }
849
-
850
- const parseGroupConfig = (raw: string): GroupConfig => {
851
- const config: GroupConfig = {};
852
- for (const part of raw.split(",")) {
853
- const trimmed = part.trim();
854
- if (!trimmed) continue;
855
- const eq = trimmed.indexOf("=");
856
- const key = eq === -1 ? trimmed : trimmed.slice(0, eq).trim();
857
- const val = eq === -1 ? "" : trimmed.slice(eq + 1).trim();
858
- switch (key) {
859
- case "concurrency": { const n = Number(val); if (Number.isInteger(n) && n > 0) config.concurrency = n; break; }
860
- case "failFast": config.failFast = eq === -1 ? true : val !== "false"; break;
861
- case "worktree": config.worktree = eq === -1 ? true : val !== "false"; break;
862
- }
863
- }
864
- return config;
865
- };
866
-
867
- // Split `(...)` from an optional trailing `[...]` group-config suffix, respecting
868
- // quotes and nested parens. Returns the inner group text and the parsed config.
869
- const splitGroupBody = (trimmed: string): { inner: string; config: GroupConfig } => {
870
- let depth = 0, inSingle = false, inDouble = false, closeIdx = -1;
871
- for (let i = 0; i < trimmed.length; i++) {
872
- const ch = trimmed[i]!;
873
- if (inSingle) { if (ch === "'") inSingle = false; continue; }
874
- if (inDouble) { if (ch === '"') inDouble = false; continue; }
875
- if (ch === "'") { inSingle = true; continue; }
876
- if (ch === '"') { inDouble = true; continue; }
877
- if (ch === "(") depth++;
878
- else if (ch === ")") { depth--; if (depth === 0) { closeIdx = i; break; } }
879
- }
880
- if (closeIdx === -1) throw new SlashParseError(`Unmatched parentheses in group: '${trimmed}'`);
881
- const inner = trimmed.slice(1, closeIdx);
882
- const suffix = trimmed.slice(closeIdx + 1).trim();
883
- if (!suffix) return { inner, config: {} };
884
- if (!suffix.startsWith("[") || !suffix.endsWith("]")) {
885
- throw new SlashParseError(`Group options must be wrapped in [...]: '${suffix}'`);
886
- }
887
- return { inner, config: parseGroupConfig(suffix.slice(1, -1)) };
888
- };
889
-
890
- export function parseGroupSegment(segment: string): ParsedGroup {
891
- const trimmed = segment.trim();
892
- if (!trimmed.startsWith("(")) {
893
- throw new SlashParseError(`Parallel group must be wrapped in parentheses: '${trimmed}'`);
894
- }
895
- const { inner, config } = splitGroupBody(trimmed);
896
- const rawParts = splitGroupTasks(inner).map((p) => p.trim()).filter((p) => p.length > 0);
897
- if (rawParts.length < 2) {
898
- throw new SlashParseError("Parallel group must contain at least two tasks separated by ' | '");
899
- }
900
- return { kind: "group", tasks: rawParts.map((part) => parseSingleTaskToken(part)), config };
901
- }
902
-
903
- // True if `input` uses inline parallel-group syntax. A group is a *step* that begins
904
- // with `(` at the top level, so we split on top-level ` -> ` arrows and look for a
905
- // segment that opens with `(`. Parentheses appearing inside a task (e.g.
906
- // `scout -- inspect auth (backend)`) do not count, which keeps legacy shared-task
907
- // `-- task` parsing — including a bare `|` inside the task — on the single-agent path.
908
- export function hasGroupSyntax(input: string): boolean {
909
- return splitOnArrow(input).some((seg) => seg.trim().startsWith("("));
910
- }
911
-
912
- export function parseChainExpression(input: string): { steps: ParsedGroupStep[] } {
913
- const trimmed = input.trim();
914
- if (!trimmed.includes(" -> ")) {
915
- throw new SlashParseError('Parallel groups in /chain require " -> " between steps');
916
- }
917
- if (findUnmatchedCloseParen(trimmed)) {
918
- throw new SlashParseError("Unmatched parentheses in /chain expression");
919
- }
920
- const steps: ParsedGroupStep[] = [];
921
- for (const seg of splitOnArrow(trimmed)) {
922
- const t = seg.trim();
923
- if (!t) continue;
924
- if (t.startsWith("(")) {
925
- steps.push(parseGroupSegment(t));
926
- continue;
927
- }
928
- if (findUnmatchedCloseParen(t)) {
929
- throw new SlashParseError(`Unmatched parentheses in chain segment: '${t}'`);
930
- }
931
- steps.push(parseSingleTaskToken(t));
932
- }
933
- if (steps.length === 0) {
934
- throw new SlashParseError("/chain expression must include at least one step");
935
- }
936
- return { steps };
937
- }
938
-
939
- const parseAgentArgs = (
940
- state: SubagentState,
941
- args: string,
942
- command: string,
943
- ctx: ExtensionContext,
944
- ): { steps: ParsedStep[]; task: string } | null => {
945
- const input = args.trim();
946
- const usage = `Usage: /${command} agent1 "task1" -> agent2 "task2"`;
947
- let steps: ParsedStep[];
948
- let sharedTask: string;
949
- let perStep = false;
950
-
951
- if (input.includes(" -> ")) {
952
- perStep = true;
953
- const segments = input.split(" -> ");
954
- steps = [];
955
- for (const seg of segments) {
956
- const trimmed = seg.trim();
957
- if (!trimmed) continue;
958
- steps.push(parseSingleTaskToken(trimmed));
959
- }
960
- sharedTask = steps.find((s) => s.task)?.task ?? "";
961
- } else {
962
- const delimiterIndex = input.indexOf(" -- ");
963
- if (delimiterIndex === -1) {
964
- ctx.ui.notify(usage, "error");
965
- return null;
966
- }
967
- const agentsPart = input.slice(0, delimiterIndex).trim();
968
- sharedTask = input.slice(delimiterIndex + 4).trim();
969
- if (!agentsPart || !sharedTask) {
970
- ctx.ui.notify(usage, "error");
971
- return null;
972
- }
973
- steps = agentsPart.split(/\s+/).filter(Boolean).map((t) => parseSingleTaskToken(t));
974
- }
975
-
976
- if (steps.length === 0) {
977
- ctx.ui.notify(usage, "error");
978
- return null;
979
- }
980
- if (!state.baseCwd) {
981
- ctx.ui.notify("Subagent session cwd is not initialized yet", "error");
982
- return null;
983
- }
984
- const agents = discoverAgents(state.baseCwd, "both").agents;
985
- for (const step of steps) {
986
- if (!agents.find((a) => a.name === step.name)) {
987
- ctx.ui.notify(`Unknown agent: ${step.name}`, "error");
988
- return null;
989
- }
990
- }
991
- if (command === "chain" && !steps[0]?.task && (perStep || !sharedTask)) {
992
- ctx.ui.notify(`First step must have a task: /chain agent "task" -> agent2`, "error");
993
- return null;
994
- }
995
- if (command === "parallel" && !steps.some((s) => s.task) && !sharedTask) {
996
- ctx.ui.notify("At least one step must have a task", "error");
997
- return null;
998
- }
999
- return { steps, task: sharedTask };
1000
- };
1001
-
1002
- type ChainStepObject = {
1003
- agent: string;
1004
- task?: string;
1005
- output?: string | false;
1006
- outputMode?: "inline" | "file-only";
1007
- reads?: string[] | false;
1008
- model?: string;
1009
- skill?: string[] | false;
1010
- progress?: boolean;
1011
- as?: string;
1012
- label?: string;
1013
- phase?: string;
1014
- cwd?: string;
1015
- count?: number;
1016
- outputSchema?: JsonSchemaObject;
1017
- acceptance?: string;
1018
- };
1019
-
1020
- const INLINE_ACCEPTANCE_LEVELS = new Set(["auto", "attested", "checked"]);
1021
-
1022
- function validateInlineAcceptanceInput(value: string, agent: string): void {
1023
- const errors = validateAcceptanceInput(value, `acceptance for step '${agent}'`);
1024
- if (errors.length > 0) throw new SlashParseError(errors[0]!);
1025
- if (!INLINE_ACCEPTANCE_LEVELS.has(value)) {
1026
- throw new SlashParseError(`Inline acceptance for step '${agent}' supports auto, attested, or checked. Use the subagent tool API or a saved .chain.json file for none, verified, or review requirements; reviewed is an achieved status, not an input level.`);
1027
- }
1028
- }
1029
-
1030
- // Load an inline `outputSchema=<path>` JSON file, resolved against the session cwd.
1031
- // Throws (SlashParseError / fs / JSON) on a missing or malformed schema.
1032
- function loadInlineOutputSchema(baseCwd: string, agent: string, value: string): JsonSchemaObject {
1033
- const schemaPath = path.isAbsolute(value) ? value : path.join(baseCwd, value);
1034
- const label = `outputSchema for step '${agent}' (${schemaPath})`;
1035
- let parsed: unknown;
1036
- try {
1037
- parsed = JSON.parse(fs.readFileSync(schemaPath, "utf-8"));
1038
- } catch (error) {
1039
- throw new SlashParseError(`Cannot read ${label}: ${error instanceof Error ? error.message : String(error)}`);
1040
- }
1041
- assertJsonSchemaObject(parsed, label);
1042
- return parsed;
1043
- }
1044
-
1045
- // Build a ChainStep object from a parsed token. `inGroup` enables `count` (parallel-only).
1046
- // May throw SlashParseError for an invalid acceptance level or outputSchema path.
1047
- const mapParsedTaskToStepObject = (
1048
- step: ParsedStep,
1049
- fallbackTask: string | undefined,
1050
- isFirst: boolean,
1051
- opts: { baseCwd: string; inGroup: boolean },
1052
- ): ChainStepObject => {
1053
- const { name, config, task: stepTask } = step;
1054
- if (config.acceptance !== undefined) validateInlineAcceptanceInput(config.acceptance, name);
1055
- return {
1056
- agent: name,
1057
- ...(stepTask ? { task: stepTask } : isFirst && fallbackTask ? { task: fallbackTask } : {}),
1058
- ...(config.output !== undefined ? { output: config.output } : {}),
1059
- ...(config.outputMode !== undefined ? { outputMode: config.outputMode } : {}),
1060
- ...(config.reads !== undefined ? { reads: config.reads } : {}),
1061
- ...(config.model ? { model: config.model } : {}),
1062
- ...(config.skill !== undefined ? { skill: config.skill } : {}),
1063
- ...(config.progress !== undefined ? { progress: config.progress } : {}),
1064
- ...(config.as ? { as: config.as } : {}),
1065
- ...(config.label ? { label: config.label } : {}),
1066
- ...(config.phase ? { phase: config.phase } : {}),
1067
- ...(config.cwd ? { cwd: config.cwd } : {}),
1068
- ...(opts.inGroup && config.count !== undefined ? { count: config.count } : {}),
1069
- ...(config.outputSchema ? { outputSchema: loadInlineOutputSchema(opts.baseCwd, name, config.outputSchema) } : {}),
1070
- ...(config.acceptance ? { acceptance: config.acceptance } : {}),
1071
- };
1072
- };
1073
-
1074
- export function buildChainExpressionSteps(
1075
- state: SubagentState,
1076
- input: string,
1077
- ctx: ExtensionContext,
1078
- ): { chain: ChainStep[]; task: string } | null {
1079
- const notify = (message: string) => ctx.ui.notify(message, "error");
1080
- if (!hasGroupSyntax(input)) {
1081
- const parsed = parseAgentArgs(state, input, "chain", ctx);
1082
- if (!parsed) return null;
1083
- const baseCwd = state.baseCwd!; // parseAgentArgs already verified baseCwd is set
1084
- try {
1085
- const chain: ChainStep[] = parsed.steps.map((step, i) =>
1086
- mapParsedTaskToStepObject(step, parsed.task || undefined, i === 0, { baseCwd, inGroup: false }),
1087
- );
1088
- return { chain, task: parsed.task };
1089
- } catch (error) {
1090
- notify(error instanceof Error ? error.message : String(error));
1091
- return null;
1092
- }
1093
- }
1094
-
1095
- let expression: { steps: ParsedGroupStep[] };
1096
- try {
1097
- expression = parseChainExpression(input);
1098
- } catch (error) {
1099
- notify(error instanceof Error ? error.message : String(error));
1100
- return null;
1101
- }
1102
- if (!state.baseCwd) {
1103
- notify("Subagent session cwd is not initialized yet");
1104
- return null;
1105
- }
1106
- const agents = discoverAgents(state.baseCwd, "both").agents;
1107
- const stepAgentNames = expression.steps.flatMap((step) =>
1108
- step.kind === "group" ? step.tasks.map((t) => t.name) : [step.name],
1109
- );
1110
- for (const name of stepAgentNames) {
1111
- if (!agents.find((a) => a.name === name)) {
1112
- notify(`Unknown agent: ${name}`);
1113
- return null;
1114
- }
1115
- }
1116
- // Every task inside a parallel group needs its own task; there is no shared-task fallback.
1117
- for (const step of expression.steps) {
1118
- if (step.kind === "group" && step.tasks.some((t) => !t.task)) {
1119
- notify('Each task in a parallel group needs a task: (agent "a" | agent "b")');
1120
- return null;
1121
- }
1122
- }
1123
- const firstStep = expression.steps[0]!;
1124
- const firstHasTask =
1125
- firstStep.kind === "group"
1126
- ? firstStep.tasks.some((t) => Boolean(t.task))
1127
- : Boolean(firstStep.task);
1128
- if (!firstHasTask) {
1129
- notify('First step must have a task: /chain agent "task" -> agent2');
1130
- return null;
1131
- }
1132
- const sharedTask =
1133
- firstStep.kind === "group"
1134
- ? (firstStep.tasks.find((t) => t.task)?.task ?? "")
1135
- : (firstStep.task ?? "");
1136
- const baseCwd = state.baseCwd;
1137
- let chain: ChainStep[];
1138
- try {
1139
- chain = expression.steps.map((step) => {
1140
- if (step.kind === "group") {
1141
- const parallel = step.tasks.map((t) => mapParsedTaskToStepObject(t, undefined, false, { baseCwd, inGroup: true }));
1142
- return {
1143
- parallel,
1144
- ...(step.config.concurrency !== undefined ? { concurrency: step.config.concurrency } : {}),
1145
- ...(step.config.failFast !== undefined ? { failFast: step.config.failFast } : {}),
1146
- ...(step.config.worktree !== undefined ? { worktree: step.config.worktree } : {}),
1147
- };
1148
- }
1149
- return mapParsedTaskToStepObject(step, sharedTask || undefined, false, { baseCwd, inGroup: false });
1150
- });
1151
- } catch (error) {
1152
- notify(error instanceof Error ? error.message : String(error));
1153
- return null;
1154
- }
1155
- return { chain, task: sharedTask };
1156
- }
1157
624
 
1158
625
  export function registerSlashCommands(
1159
626
  pi: ExtensionAPI,
@@ -1172,7 +639,7 @@ export function registerSlashCommands(
1172
639
  }
1173
640
  fleetOpen = true;
1174
641
  try {
1175
- await openSubagentFleet(ctx, state);
642
+ await openSubagentFleet(ctx, state, { asyncDirRoot: DIRS.async, resultsDir: DIRS.results });
1176
643
  } finally {
1177
644
  fleetOpen = false;
1178
645
  }
@@ -1187,7 +654,7 @@ export function registerSlashCommands(
1187
654
 
1188
655
  pi.registerCommand("run", {
1189
656
  description: "Run a subagent directly: /run agent[output=file] [task] [--bg] [--fork]",
1190
- getArgumentCompletions: makeAgentCompletions(state, false),
657
+ getArgumentCompletions: makeAgentCompletions(state),
1191
658
  handler: async (args, ctx) => {
1192
659
  const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
1193
660
  const input = cleanedArgs.trim();
@@ -1215,74 +682,6 @@ export function registerSlashCommands(
1215
682
  },
1216
683
  });
1217
684
 
1218
- pi.registerCommand("chain", {
1219
- description: "Run agents in sequence: /chain scout \"task\" -> planner [--bg] [--fork]",
1220
- getArgumentCompletions: makeAgentCompletions(state, true),
1221
- handler: async (args, ctx) => {
1222
- const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
1223
- const built = buildChainExpressionSteps(state, cleanedArgs, ctx);
1224
- if (!built) return;
1225
- const params: SubagentParamsLike = { chain: built.chain, task: built.task, clarify: false, agentScope: "both" };
1226
- if (bg) params.async = true;
1227
- if (fork) params.context = "fork";
1228
- launchSlashSubagent(pi, ctx, params);
1229
- },
1230
- });
1231
-
1232
- pi.registerCommand("run-chain", {
1233
- description: "Run a saved chain: /run-chain chainName -- task [--bg] [--fork]",
1234
- getArgumentCompletions: makeChainCompletions(state),
1235
- handler: async (args, ctx) => {
1236
- const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
1237
- const delimiterIndex = cleanedArgs.indexOf(" -- ");
1238
- const usage = "Usage: /run-chain <chainName> -- <task> [--bg] [--fork]";
1239
- if (delimiterIndex === -1) {
1240
- ctx.ui.notify(usage, "error");
1241
- return;
1242
- }
1243
- const chainName = cleanedArgs.slice(0, delimiterIndex).trim();
1244
- const task = cleanedArgs.slice(delimiterIndex + 4).trim();
1245
- if (!chainName || !task) {
1246
- ctx.ui.notify(usage, "error");
1247
- return;
1248
- }
1249
- if (!state.baseCwd) { ctx.ui.notify("Subagent session cwd is not initialized yet", "error"); return; }
1250
- const chain = discoverSavedChains(state.baseCwd).find((candidate) => candidate.name === chainName);
1251
- if (!chain) {
1252
- ctx.ui.notify(`Unknown chain: ${chainName}`, "error");
1253
- return;
1254
- }
1255
- const params: SubagentParamsLike = { chain: mapSavedChainSteps(chain), task, clarify: false, agentScope: "both" };
1256
- if (bg) params.async = true;
1257
- if (fork) params.context = "fork";
1258
- launchSlashSubagent(pi, ctx, params);
1259
- },
1260
- });
1261
-
1262
- pi.registerCommand("parallel", {
1263
- description: "Run agents in parallel: /parallel scout \"task1\" -> reviewer \"task2\" [--bg] [--fork]",
1264
- getArgumentCompletions: makeAgentCompletions(state, true),
1265
- handler: async (args, ctx) => {
1266
- const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
1267
- const parsed = parseAgentArgs(state, cleanedArgs, "parallel", ctx);
1268
- if (!parsed) return;
1269
- const tasks = parsed.steps.map(({ name, config, task: stepTask }) => ({
1270
- agent: name,
1271
- task: stepTask ?? parsed.task,
1272
- ...(config.output !== undefined ? { output: config.output } : {}),
1273
- ...(config.outputMode !== undefined ? { outputMode: config.outputMode } : {}),
1274
- ...(config.reads !== undefined ? { reads: config.reads } : {}),
1275
- ...(config.model ? { model: config.model } : {}),
1276
- ...(config.skill !== undefined ? { skill: config.skill } : {}),
1277
- ...(config.progress !== undefined ? { progress: config.progress } : {}),
1278
- }));
1279
- const params: SubagentParamsLike = { tasks, clarify: false, agentScope: "both" };
1280
- if (bg) params.async = true;
1281
- if (fork) params.context = "fork";
1282
- launchSlashSubagent(pi, ctx, params);
1283
- },
1284
- });
1285
-
1286
685
  pi.registerCommand("subagent-cost", {
1287
686
  description: "Show parent and subagent child usage cost for this session",
1288
687
  handler: async (_args, ctx) => {
@@ -1371,7 +770,7 @@ export function registerSlashCommands(
1371
770
  );
1372
771
  if (!result?.confirmed || !result.target) return;
1373
772
  if (result.target.kind === "scheduled") {
1374
- await runSlashSubagent(pi, ctx, { action: "schedule-cancel", id: result.target.id });
773
+ await runSlashSubagent(pi, ctx, { action: "schedule.pause", id: result.target.id });
1375
774
  return;
1376
775
  }
1377
776
  await runSlashSubagent(pi, ctx, { action: "stop", id: result.target.id });
@@ -1430,7 +829,7 @@ export function registerSlashCommands(
1430
829
  },
1431
830
  handler: async (args, ctx) => {
1432
831
  const parsed = parseSingleRequiredArg(args, "Usage: /subagents-load-profile <name>");
1433
- if (!parsed.ok) {
832
+ if (parsed.ok === false) {
1434
833
  ctx.ui.notify(parsed.message, "error");
1435
834
  return;
1436
835
  }
@@ -1481,7 +880,7 @@ export function registerSlashCommands(
1481
880
  const force = /(?:^|\s)--force$/.test(trimmed) || /(?:^|\s)force$/.test(trimmed);
1482
881
  const withoutForce = trimmed.replace(/(?:^|\s)(?:--force|force)$/, "").trim();
1483
882
  const parsed = parseSingleRequiredArg(withoutForce, "Usage: /subagents-refresh-provider-models <provider> [--force]");
1484
- if (!parsed.ok) {
883
+ if (parsed.ok === false) {
1485
884
  ctx.ui.notify(parsed.message, "error");
1486
885
  return;
1487
886
  }
@@ -1512,7 +911,7 @@ export function registerSlashCommands(
1512
911
  getArgumentCompletions: makeProviderCompletions(state),
1513
912
  handler: async (args, ctx) => {
1514
913
  const parsed = parseSingleRequiredArg(args, "Usage: /subagents-generate-profiles <provider>");
1515
- if (!parsed.ok) {
914
+ if (parsed.ok === false) {
1516
915
  ctx.ui.notify(parsed.message, "error");
1517
916
  return;
1518
917
  }
@@ -1555,7 +954,7 @@ export function registerSlashCommands(
1555
954
  },
1556
955
  handler: async (args, ctx) => {
1557
956
  const parsed = parseSingleRequiredArg(args, "Usage: /subagents-check-profile <name>");
1558
- if (!parsed.ok) {
957
+ if (parsed.ok === false) {
1559
958
  ctx.ui.notify(parsed.message, "error");
1560
959
  return;
1561
960
  }