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.
- package/CHANGELOG.md +76 -0
- package/README.md +246 -525
- package/agents/oracle.md +1 -0
- package/package.json +12 -4
- package/prompts/parallel-context-build.md +1 -1
- package/prompts/parallel-handoff-plan.md +1 -1
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +6 -6
- package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
- package/skills/pi-subagents/references/execution-controls.md +98 -97
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
- package/src/agents/agent-management.ts +155 -65
- package/src/agents/agent-serializer.ts +19 -0
- package/src/agents/agents.ts +154 -71
- package/src/agents/chain-serializer.ts +10 -7
- package/src/agents/frontmatter.ts +5 -3
- package/src/agents/identity.ts +1 -1
- package/src/agents/proactive-skills.ts +13 -10
- package/src/agents/skills.ts +23 -6
- package/src/api/control-channel.ts +4 -0
- package/src/api/delegation.ts +26 -194
- package/src/api/external-runs.ts +129 -0
- package/src/api/intercom-bridge.ts +3 -0
- package/src/api/pi-args.ts +5 -0
- package/src/api/preflight.ts +3 -3
- package/src/api/shared-types.ts +19 -0
- package/src/extension/config.ts +10 -0
- package/src/extension/control-notices.ts +5 -39
- package/src/extension/doctor.ts +10 -9
- package/src/extension/fanout-child.ts +7 -4
- package/src/extension/index.ts +232 -68
- package/src/extension/rpc.ts +18 -12
- package/src/extension/schemas.ts +48 -37
- package/src/extension/tool-description.ts +36 -86
- package/src/inspectors/herdr/actions.ts +229 -0
- package/src/inspectors/herdr/client.ts +130 -0
- package/src/inspectors/herdr/inspector-runner.ts +141 -0
- package/src/inspectors/herdr/project-panes.ts +154 -0
- package/src/integrations/herdr-status.ts +330 -0
- package/src/intercom/intercom-bridge.ts +3 -2
- package/src/intercom/result-intercom.ts +5 -1
- package/src/missions/actions.ts +372 -0
- package/src/missions/lifecycle.ts +314 -0
- package/src/missions/store.ts +442 -0
- package/src/missions/types.ts +135 -0
- package/src/policy/authority.ts +46 -0
- package/src/profiles/profiles.ts +29 -3
- package/src/runs/background/async-execution.ts +98 -49
- package/src/runs/background/async-job-tracker.ts +10 -2
- package/src/runs/background/async-resume.ts +6 -6
- package/src/runs/background/async-status.ts +29 -1
- package/src/runs/background/auto-drain.ts +3 -3
- package/src/runs/background/chain-append.ts +3 -2
- package/src/runs/background/control-channel.ts +9 -7
- package/src/runs/background/fleet-view.ts +3 -4
- package/src/runs/background/notify.ts +2 -1
- package/src/runs/background/process-terminal.ts +5 -5
- package/src/runs/background/result-watcher.ts +13 -5
- package/src/runs/background/run-id-resolver.ts +3 -3
- package/src/runs/background/run-status.ts +35 -8
- package/src/runs/background/scheduled-runs.ts +602 -375
- package/src/runs/background/stale-run-reconciler.ts +3 -3
- package/src/runs/background/subagent-runner.ts +608 -445
- package/src/runs/background/subagent-wait.ts +50 -9
- package/src/runs/background/wait-subscriptions.ts +253 -0
- package/src/runs/background/wait-tool.ts +12 -4
- package/src/runs/foreground/async-steering-action.ts +3 -3
- package/src/runs/foreground/chain-clarify.ts +8 -4
- package/src/runs/foreground/chain-execution.ts +56 -30
- package/src/runs/foreground/execution.ts +15 -2
- package/src/runs/foreground/subagent-executor.ts +1023 -273
- package/src/runs/shared/acceptance.ts +28 -6
- package/src/runs/shared/child-protocol.ts +302 -22
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/external-cli-runner.ts +130 -0
- package/src/runs/shared/long-running-guard.ts +42 -1
- package/src/runs/shared/nested-events.ts +59 -5
- package/src/runs/shared/nested-render.ts +9 -4
- package/src/runs/shared/parallel-handoff.ts +86 -2
- package/src/runs/shared/parallel-utils.ts +11 -2
- package/src/runs/shared/permissions.ts +95 -0
- package/src/runs/shared/pi-args.ts +11 -1
- package/src/runs/shared/pi-spawn.ts +11 -1
- package/src/runs/shared/run-history.ts +1 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
- package/src/runs/shared/subagent-startup-retry.ts +5 -2
- package/src/runs/shared/turn-budget.ts +6 -6
- package/src/runs/shared/worktree.ts +122 -12
- package/src/shared/accessible-dir.ts +29 -7
- package/src/shared/artifacts.ts +18 -1
- package/src/shared/fork-context.ts +3 -2
- package/src/shared/launch-contract.ts +1 -0
- package/src/shared/settings.ts +10 -0
- package/src/shared/types.ts +156 -14
- package/src/shared/utils.ts +8 -6
- package/src/slash/delegation-adapters.ts +32 -194
- package/src/slash/delegation-request.ts +43 -126
- package/src/slash/prompt-template-bridge.ts +158 -205
- package/src/slash/prompt-workflows.ts +21 -57
- package/src/slash/slash-bridge.ts +14 -0
- package/src/slash/slash-commands.ts +31 -632
- package/src/slash/subagents-admin.ts +18 -14
- package/src/tui/fleet-status.ts +156 -21
- package/src/tui/fleet-transcript.ts +110 -5
- package/src/tui/fleet.ts +56 -24
- package/src/tui/render.ts +291 -109
- package/src/types/pi-runtime-compat.d.ts +14 -0
- package/src/watchdog/lsp-diagnostics.ts +12 -7
- package/src/watchdog/model-selection.ts +2 -2
- package/src/watchdog/permission-arbiter.ts +145 -0
- package/src/watchdog/register-child.ts +1 -1
- package/src/watchdog/register-main.ts +1 -1
- package/src/watchdog/review.ts +4 -1
- package/src/watchdog/runtime.ts +3 -2
- package/src/workflows/chat-progress.ts +140 -0
- package/src/workflows/scripted-workflow.ts +415 -0
- package/agents/advisor.md +0 -73
- 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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
129
|
-
if (!state.baseCwd) return null;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
957
|
+
if (parsed.ok === false) {
|
|
1559
958
|
ctx.ui.notify(parsed.message, "error");
|
|
1560
959
|
return;
|
|
1561
960
|
}
|