@crouton-kit/crouter 0.3.3 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +2 -2
  2. package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +1 -1
  3. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +3 -3
  4. package/dist/cli.js +16 -26
  5. package/dist/commands/__tests__/skill.test.js +24 -28
  6. package/dist/commands/agent.d.ts +6 -0
  7. package/dist/commands/agent.js +585 -0
  8. package/dist/commands/debug.d.ts +1 -1
  9. package/dist/commands/debug.js +20 -7
  10. package/dist/commands/human.js +51 -19
  11. package/dist/commands/job.d.ts +9 -0
  12. package/dist/commands/job.js +100 -385
  13. package/dist/commands/{flow.d.ts → mode.d.ts} +1 -1
  14. package/dist/commands/mode.js +231 -0
  15. package/dist/commands/pkg.js +5 -0
  16. package/dist/commands/plan.d.ts +1 -1
  17. package/dist/commands/plan.js +24 -11
  18. package/dist/commands/skill.js +130 -107
  19. package/dist/commands/spec.d.ts +1 -1
  20. package/dist/commands/spec.js +24 -11
  21. package/dist/commands/sys.js +5 -0
  22. package/dist/core/__tests__/job.test.js +38 -74
  23. package/dist/core/__tests__/jobs.test.d.ts +1 -0
  24. package/dist/core/__tests__/jobs.test.js +98 -0
  25. package/dist/core/__tests__/resolver.test.d.ts +1 -0
  26. package/dist/core/__tests__/resolver.test.js +181 -0
  27. package/dist/core/__tests__/spawn.test.d.ts +1 -0
  28. package/dist/core/__tests__/spawn.test.js +138 -0
  29. package/dist/core/__tests__/subagents.test.d.ts +1 -0
  30. package/dist/core/__tests__/subagents.test.js +75 -0
  31. package/dist/core/__tests__/unknown-path.test.d.ts +1 -0
  32. package/dist/core/__tests__/unknown-path.test.js +52 -0
  33. package/dist/core/bootstrap.d.ts +2 -0
  34. package/dist/core/bootstrap.js +66 -0
  35. package/dist/core/command.d.ts +58 -2
  36. package/dist/core/command.js +62 -14
  37. package/dist/core/config.js +20 -2
  38. package/dist/core/frontmatter.d.ts +10 -0
  39. package/dist/core/frontmatter.js +24 -9
  40. package/dist/core/help.d.ts +39 -8
  41. package/dist/core/help.js +64 -32
  42. package/dist/core/jobs.d.ts +33 -13
  43. package/dist/core/jobs.js +259 -47
  44. package/dist/core/resolver.d.ts +1 -2
  45. package/dist/core/resolver.js +111 -47
  46. package/dist/core/spawn.d.ts +150 -10
  47. package/dist/core/spawn.js +493 -41
  48. package/dist/core/subagents.d.ts +18 -0
  49. package/dist/core/subagents.js +163 -0
  50. package/dist/prompts/agent.d.ts +12 -3
  51. package/dist/prompts/agent.js +51 -18
  52. package/dist/prompts/debug.js +14 -7
  53. package/dist/prompts/skill.js +16 -16
  54. package/dist/types.d.ts +22 -1
  55. package/dist/types.js +5 -2
  56. package/package.json +2 -2
  57. package/dist/commands/flow.js +0 -24
@@ -5,6 +5,7 @@ import { listDirs, pathExists, readText, walkFiles, } from './fs-utils.js';
5
5
  import { readMarketplaceManifest, readPluginManifest } from './manifest.js';
6
6
  import { parseFrontmatter } from './frontmatter.js';
7
7
  import { ambiguous, notFound, usage } from './errors.js';
8
+ import { InputError } from './io.js';
8
9
  import { builtinSkillsRoot, marketplacesDir, pluginsDir, projectScopeRoot, scopeSkillsDir, userScopeRoot, } from './scope.js';
9
10
  function getBuiltinPlugin() {
10
11
  const root = builtinSkillsRoot();
@@ -202,33 +203,99 @@ export function resolveSkill(rawName, opts = {}) {
202
203
  if (parsed.scope && opts.scope && parsed.scope !== opts.scope) {
203
204
  throw usage(`scope conflict: identifier "${rawName}" uses scope "${parsed.scope}" but --scope is "${opts.scope}"`);
204
205
  }
205
- if (parsed.plugin && opts.pluginFilter && parsed.plugin !== opts.pluginFilter) {
206
- throw usage(`plugin conflict: identifier "${rawName}" uses plugin "${parsed.plugin}" but --plugin is "${opts.pluginFilter}"`);
207
- }
208
206
  const effectiveScope = opts.scope ?? parsed.scope;
209
- const effectivePluginFilter = opts.pluginFilter ?? parsed.plugin;
210
- const direct = findSkillMatches(parsed.name, parsed.plugin, effectiveScope, effectivePluginFilter);
211
- if (direct.length > 0)
212
- return pickMatch(direct, parsed.name, parsed.plugin);
213
- // Fallback: bare `plugin/name` (no colon) — try splitting on first `/`.
214
- // Disambiguates "claude-authoring/rules" (which the search/list display also emits as
215
- // "user:claude-authoring/rules") from a nested scope-root skill of the same shape.
216
- if (!parsed.plugin && parsed.name.includes('/')) {
217
- const slashIdx = parsed.name.indexOf('/');
218
- const maybePlugin = parsed.name.slice(0, slashIdx);
219
- const rest = parsed.name.slice(slashIdx + 1);
220
- if (effectivePluginFilter === undefined || effectivePluginFilter === maybePlugin) {
221
- const fallback = findSkillMatches(rest, maybePlugin, effectiveScope, maybePlugin);
222
- if (fallback.length > 0)
223
- return pickMatch(fallback, rest, maybePlugin);
207
+ // Lookup-based disambiguation: if segments[0] matches an installed plugin, treat it as plugin.
208
+ // Otherwise the entire segments array is the skill path under the scope-direct plugin.
209
+ let pluginQualifier;
210
+ let skillName;
211
+ if (parsed.segments.length === 0) {
212
+ throw usage(`skill name required`);
213
+ }
214
+ if (opts.pluginFilter !== undefined) {
215
+ // Explicit plugin filter overrides disambiguation.
216
+ pluginQualifier = opts.pluginFilter;
217
+ skillName = parsed.segments.join('/');
218
+ }
219
+ else if (parsed.segments.length > 1) {
220
+ const maybePlugin = parsed.segments[0];
221
+ const pluginMatch = findPluginByName(maybePlugin, effectiveScope) ??
222
+ (effectiveScope === undefined ? null : findPluginByName(maybePlugin));
223
+ if (pluginMatch !== null) {
224
+ pluginQualifier = maybePlugin;
225
+ skillName = parsed.segments.slice(1).join('/');
224
226
  }
227
+ else {
228
+ pluginQualifier = undefined;
229
+ skillName = parsed.segments.join('/');
230
+ }
231
+ }
232
+ else {
233
+ pluginQualifier = undefined;
234
+ skillName = parsed.segments[0];
225
235
  }
226
- throw notFound(formatNotFoundMessage(rawName, parsed), {
227
- skill: parsed.name,
228
- plugin: parsed.plugin,
236
+ if (pluginQualifier && opts.pluginFilter && pluginQualifier !== opts.pluginFilter) {
237
+ throw usage(`plugin conflict: identifier "${rawName}" uses plugin "${pluginQualifier}" but --plugin is "${opts.pluginFilter}"`);
238
+ }
239
+ const effectivePluginFilter = opts.pluginFilter ?? pluginQualifier;
240
+ const direct = findSkillMatches(skillName, pluginQualifier, effectiveScope, effectivePluginFilter);
241
+ if (direct.length > 0)
242
+ return pickMatch(direct, skillName, pluginQualifier);
243
+ // Leaf-name fallback: the caller supplied only the final path segment
244
+ // (e.g. "cli-design" for "ai/interface/cli-design"). A direct path lookup
245
+ // missed because the skill lives under a nested path. Match by last segment.
246
+ const byLeaf = findSkillsByLeaf(skillName, pluginQualifier, effectiveScope, effectivePluginFilter);
247
+ if (byLeaf.length === 1)
248
+ return byLeaf[0];
249
+ if (byLeaf.length > 1) {
250
+ throw ambiguous(formatLeafAmbiguousMessage(skillName, byLeaf), {
251
+ skill: skillName,
252
+ candidates: byLeaf.map((m) => ({
253
+ id: formatSkillId(m),
254
+ plugin: m.plugin,
255
+ scope: m.scope,
256
+ path: m.path,
257
+ })),
258
+ next: 'Multiple skills share this leaf name. Re-run with one of the full paths in candidates.',
259
+ });
260
+ }
261
+ throw notFound(formatNotFoundMessage(rawName, skillName, pluginQualifier), {
262
+ skill: skillName,
263
+ plugin: pluginQualifier,
229
264
  scope: parsed.scope,
230
265
  });
231
266
  }
267
+ /** Canonical, unambiguous identifier for a skill. Scope-root skills are
268
+ * qualified by scope; plugin skills by plugin name. */
269
+ function formatSkillId(s) {
270
+ return s.plugin === SCOPE_SKILL_PLUGIN ? `${s.scope}/${s.name}` : `${s.plugin}/${s.name}`;
271
+ }
272
+ /** Match skills whose final path segment equals `leaf`. Only meaningful when
273
+ * `leaf` is a bare segment (no slash) — a slashed query can never equal a
274
+ * single segment, so this returns empty and the caller falls through. */
275
+ function findSkillsByLeaf(leaf, pluginQualifier, scope, pluginFilter) {
276
+ if (leaf.includes('/'))
277
+ return [];
278
+ let all;
279
+ try {
280
+ all = scope ? listAllSkills(scope) : listAllSkills();
281
+ }
282
+ catch {
283
+ return [];
284
+ }
285
+ return all.filter((s) => {
286
+ if ((s.name.split('/').pop() ?? s.name) !== leaf)
287
+ return false;
288
+ if (pluginQualifier && s.plugin !== pluginQualifier)
289
+ return false;
290
+ if (pluginFilter && s.plugin !== pluginFilter)
291
+ return false;
292
+ return true;
293
+ });
294
+ }
295
+ function formatLeafAmbiguousMessage(leaf, matches) {
296
+ const ids = matches.map(formatSkillId).join(', ');
297
+ return `ambiguous skill: ${leaf} matches multiple skills: ${ids}`;
298
+ }
232
299
  function findSkillMatches(name, pluginQualifier, scope, pluginFilter) {
233
300
  const plugins = scope ? listInstalledPlugins(scope) : listAllPlugins();
234
301
  const enabledPlugins = plugins.filter((p) => p.enabled);
@@ -304,18 +371,18 @@ function pickMatch(matches, name, pluginQualifier) {
304
371
  })),
305
372
  });
306
373
  }
307
- function formatNotFoundMessage(rawName, parsed) {
308
- const suggestions = suggestSkills(parsed.name, parsed.plugin);
374
+ function formatNotFoundMessage(rawName, skillName, pluginQualifier) {
375
+ const suggestions = suggestSkills(skillName, pluginQualifier);
309
376
  const lines = [`skill not found: ${rawName}`];
310
- lines.push(' expected forms: <name>, <plugin>:<name>, <scope>:<plugin>/<name>');
377
+ lines.push(' expected forms: <name>, <plugin>/<name>, <scope>/<name>, <scope>/<plugin>/<name>');
311
378
  if (suggestions.length > 0) {
312
379
  const formatted = suggestions
313
- .map((s) => s.plugin === SCOPE_SKILL_PLUGIN ? s.name : `${s.plugin}:${s.name}`)
380
+ .map((s) => s.plugin === SCOPE_SKILL_PLUGIN ? s.name : `${s.plugin}/${s.name}`)
314
381
  .slice(0, 3);
315
382
  lines.push(` did you mean: ${formatted.join(', ')}`);
316
383
  }
317
384
  else {
318
- lines.push(' run `crtr skill list` or `crtr skill search <query>` to discover skills');
385
+ lines.push(' run `crtr skill find list` or `crtr skill find search <query>` to discover skills');
319
386
  }
320
387
  return lines.join('\n');
321
388
  }
@@ -386,29 +453,26 @@ function editDistance(a, b) {
386
453
  const SCOPE_QUALIFIERS = new Set(['user', 'project']);
387
454
  // Accepted identifier forms:
388
455
  // <name> — bare name; scope-root first, then plugins
389
- // <plugin>:<name> — explicit plugin
390
- // <scope>:<name> — scope-root in a specific scope
391
- // <scope>:<plugin>/<name> — fully qualified (matches `skill list` / `skill search` display)
392
- // Bare `<plugin>/<name>` (no colon) is handled as a fallback inside resolveSkill.
456
+ // <plugin>/<name> — explicit plugin (plugin may contain slashes)
457
+ // <scope>/<name> — scope-root in a specific scope
458
+ // <scope>/<plugin>/<name> — fully qualified; plugin-vs-path disambiguation is lookup-based in resolveSkill
393
459
  export function parseSkillQualifier(raw) {
394
- const colonIdx = raw.indexOf(':');
395
- if (colonIdx === -1)
396
- return { name: raw };
397
- const before = raw.slice(0, colonIdx);
398
- const after = raw.slice(colonIdx + 1);
399
- if (SCOPE_QUALIFIERS.has(before)) {
400
- const scope = before;
401
- const slashIdx = after.indexOf('/');
402
- if (slashIdx !== -1) {
403
- return {
404
- scope,
405
- plugin: after.slice(0, slashIdx),
406
- name: after.slice(slashIdx + 1),
407
- };
408
- }
409
- return { scope, name: after };
460
+ if (raw.includes(':')) {
461
+ const suggested = raw.replace(/:/g, '/');
462
+ throw new InputError({
463
+ error: 'invalid_qualifier',
464
+ message: "mixed separators ':' and '/' no longer supported; use slashes throughout",
465
+ received: raw,
466
+ field: 'name',
467
+ next: `Use ${suggested}.`,
468
+ });
469
+ }
470
+ const segments = raw.split('/');
471
+ if (SCOPE_QUALIFIERS.has(segments[0])) {
472
+ const scope = segments[0];
473
+ return { scope, segments: segments.slice(1) };
410
474
  }
411
- return { plugin: before, name: after };
475
+ return { segments };
412
476
  }
413
477
  function orderPluginsByResolution(plugins) {
414
478
  const score = (p) => {
@@ -1,5 +1,5 @@
1
1
  export interface SpawnAgentOptions {
2
- /** First user message for the new claude session. */
2
+ /** First user message for the new agent session. */
3
3
  prompt: string;
4
4
  cwd: string;
5
5
  /** crtr job_id injected as CRTR_JOB_ID env var in the pane. */
@@ -10,6 +10,14 @@ export interface SpawnAgentOptions {
10
10
  };
11
11
  /** Max panes per tmux window before overflowing to a new window. */
12
12
  maxPanesPerWindow: number;
13
+ /** Display name passed to the agent's `-n` flag; surfaces in pane title and resume picker. */
14
+ name?: string;
15
+ /** Persona appended via `--append-system-prompt` (subagent body). */
16
+ systemPrompt?: string;
17
+ /** Model pattern/id passed via `--model`. */
18
+ model?: string;
19
+ /** Tool allow-list passed to pi via `--tools`. */
20
+ tools?: string[];
13
21
  }
14
22
  export interface SpawnAgentResult {
15
23
  status: 'spawned' | 'spawn-failed' | 'not-in-tmux';
@@ -20,10 +28,11 @@ export interface SpawnAgentResult {
20
28
  message: string;
21
29
  }
22
30
  export interface DetachOptions {
23
- /** Inner command to run in the pane. If omitted, build `claude <prompt>`. */
31
+ /** Inner command to run in the pane. If omitted, build the detected agent's
32
+ * invocation around `<prompt>`. */
24
33
  command?: string;
25
- /** Full first user message for the new claude session (claude mode only;
26
- * ignored when `command` is set). No custom system prompt. */
34
+ /** Full first user message for the new agent session (ignored when `command`
35
+ * is set). No custom system prompt. */
27
36
  prompt?: string;
28
37
  cwd: string;
29
38
  /** crtr job_id injected as CRTR_JOB_ID env var in the pane and used by the
@@ -40,6 +49,9 @@ export interface DetachOptions {
40
49
  * uses the attached client's currently-focused pane — which drifts if the
41
50
  * user switches windows between kickoff and spawn. */
42
51
  targetPane?: string;
52
+ /** Display name passed to the agent's `-n` flag; ignored when `command` is set
53
+ * (caller controls the full argv in that mode). */
54
+ name?: string;
43
55
  }
44
56
  export interface DetachResult {
45
57
  status: 'spawned' | 'spawn-failed' | 'not-in-tmux';
@@ -48,7 +60,126 @@ export interface DetachResult {
48
60
  }
49
61
  export declare function isInTmux(): boolean;
50
62
  export declare function shellQuote(s: string): string;
63
+ /** Coding-agent CLIs crtr knows how to spawn as a sibling worker. */
64
+ export type AgentKind = 'claude' | 'pi';
65
+ /**
66
+ * Detect which coding-agent CLI is hosting the current crtr process so spawns
67
+ * launch a matching sibling. pi exports `PI_CODING_AGENT=true` into its tool
68
+ * subprocess environment; Claude Code exports `CLAUDECODE` /
69
+ * `CLAUDE_CODE_SESSION_ID`. Defaults to claude when no signal is present
70
+ * (preserves prior behavior).
71
+ */
72
+ export declare function detectAgentKind(): AgentKind;
73
+ /**
74
+ * Normalize a `--model` value for the target agent CLI.
75
+ *
76
+ * Subagent frontmatter uses Claude Code's bare aliases (`sonnet`, `opus`,
77
+ * `haiku`, optionally with a `:thinking` suffix). The `claude` CLI resolves
78
+ * those natively, but `pi` maps a bare alias to its default provider —
79
+ * `amazon-bedrock` — which most users have not authenticated, so the spawn
80
+ * dies with "No API key found for amazon-bedrock". These aliases name Anthropic
81
+ * models, so under pi we pin them to the `anthropic/` provider (preserving any
82
+ * `:thinking` suffix). Values that already carry a `provider/` prefix or are
83
+ * concrete model ids are passed through untouched.
84
+ */
85
+ export declare function normalizeModelForKind(model: string, kind: AgentKind): string;
86
+ export interface AgentCommandOptions {
87
+ /** First user message delivered to the new agent session. */
88
+ prompt: string;
89
+ /** Display name (`-n`); surfaces in the pane title and resume picker. */
90
+ name?: string;
91
+ /** Fork an existing session into a fresh one rather than starting clean. */
92
+ fork?: {
93
+ sessionId: string;
94
+ };
95
+ /** Persona/system prompt appended to the agent's default (`--append-system-prompt`).
96
+ * Used to apply a subagent definition's body. */
97
+ systemPrompt?: string;
98
+ /** Model pattern/id passed via `--model` (both claude and pi). */
99
+ model?: string;
100
+ /** Tool allow-list. Passed to pi via `--tools`; ignored for claude, whose
101
+ * tool names and gating flag differ. */
102
+ tools?: string[];
103
+ }
104
+ /**
105
+ * Build the agent-CLI invocation (no job wrapper) for the given kind.
106
+ *
107
+ * claude: `claude [-n <name>] [--resume <id> --fork-session] \
108
+ * --dangerously-skip-permissions <prompt>`
109
+ * pi: `pi [-n <name>] [--fork <id>] <prompt>`
110
+ *
111
+ * pi has no permission popups, so it needs no skip-permissions flag.
112
+ */
113
+ export declare function buildAgentCommand(opts: AgentCommandOptions, kind?: AgentKind): string;
114
+ export interface AgentPrintArgv {
115
+ cmd: string;
116
+ args: string[];
117
+ }
118
+ /**
119
+ * Argv for a non-interactive print-mode run.
120
+ *
121
+ * claude: `claude [-n <name>] [--resume <id> --fork-session] -p \
122
+ * --dangerously-skip-permissions <prompt>`
123
+ * pi: `pi [-n <name>] [--fork <id>] -p <prompt>`
124
+ *
125
+ * Returned as a cmd + args array so callers can spawn without a shell.
126
+ */
127
+ export declare function buildAgentPrintArgv(opts: AgentCommandOptions, kind?: AgentKind): AgentPrintArgv;
128
+ /** Same as buildAgentPrintArgv but rendered as a single shell-quoted string. */
129
+ export declare function buildAgentPrintCommand(opts: AgentCommandOptions, kind?: AgentKind): string;
130
+ export interface HeadlessRunResult {
131
+ status: 'done' | 'failed';
132
+ /** Captured stdout on success; stdout+stderr (or an error message) on failure. */
133
+ output: string;
134
+ exitCode: number | null;
135
+ }
136
+ /**
137
+ * Run the agent headlessly and resolve once it exits. A blocking caller awaits
138
+ * this. stdout is captured as the result; a non-zero exit yields status
139
+ * 'failed' with the combined output.
140
+ */
141
+ export declare function runAgentHeadless(opts: {
142
+ prompt: string;
143
+ name?: string;
144
+ cwd: string;
145
+ systemPrompt?: string;
146
+ model?: string;
147
+ tools?: string[];
148
+ }): Promise<HeadlessRunResult>;
149
+ export interface HeadlessDetachResult {
150
+ status: 'spawned' | 'spawn-failed';
151
+ pid?: number;
152
+ message: string;
153
+ }
154
+ /**
155
+ * Launch a headless agent detached (background). Its print output is captured
156
+ * and delivered to the job via `crtr job submit`; a non-zero exit marks the job
157
+ * failed. Returns immediately with the wrapper pid (recorded for crash
158
+ * detection). No tmux required.
159
+ */
160
+ export declare function spawnHeadlessDetached(opts: {
161
+ prompt: string;
162
+ name?: string;
163
+ cwd: string;
164
+ jobId: string;
165
+ systemPrompt?: string;
166
+ model?: string;
167
+ tools?: string[];
168
+ }): HeadlessDetachResult;
51
169
  export declare function countPanesInCurrentWindow(): number;
170
+ /**
171
+ * Find a window in the current tmux session with fewer than `maxPanesPerWindow`
172
+ * panes AND where every existing pane hosts an agent (claude or pi) as its
173
+ * foreground process. Prefers the active window so the spawned pane is visible
174
+ * to the user; otherwise falls back to the first other eligible window. Returns
175
+ * the tmux window id (e.g. `@5`) to pass via `-t`, or null if no window qualifies.
176
+ *
177
+ * Windows holding non-agent panes (dashboards, log tails, idle shells, editors,
178
+ * REPLs, etc.) are skipped so spawning never disrupts those workflows. A pane
179
+ * qualifies as long as an agent comm is among its foreground commands —
180
+ * co-resident helpers like `caffeinate` don't disqualify it.
181
+ */
182
+ export declare function findWindowWithSpace(maxPanesPerWindow: number): string | null;
52
183
  /**
53
184
  * Schedule a kill-pane on the *current* tmux pane after `delaySeconds`, detached
54
185
  * so the caller can return normally before the pane dies. No-op outside tmux
@@ -60,18 +191,27 @@ export declare function countPanesInCurrentWindow(): number;
60
191
  */
61
192
  export declare function scheduleKillCurrentPane(delaySeconds: number): boolean;
62
193
  /**
63
- * Fire-and-forget: launch an interactive `claude` in a new pane (or window),
194
+ * Fire-and-forget: launch an interactive agent in a new pane (or window),
64
195
  * then schedule the originating pane to be killed after `killAfterSeconds`.
65
196
  *
66
197
  * No custom system prompt — the task is delivered as the first user message.
67
- * Returns as soon as the new pane is up; does NOT wait for claude to finish.
198
+ * Returns as soon as the new pane is up; does NOT wait for the agent to finish.
68
199
  */
69
200
  export declare function spawnAndDetach(opts: DetachOptions): DetachResult;
201
+ /** Originating pane id + session id of the host (pi/claude) crtr runs under. */
202
+ export declare function originContext(): {
203
+ pane: string;
204
+ sessionId: string;
205
+ } | null;
206
+ /** Deterministic subagent session name for an originating pane id (e.g. `%5`). */
207
+ export declare function subagentSessionName(pane: string): string;
70
208
  /**
71
- * Async sibling spawn. Launches a claude session in a new tmux pane or window
72
- * (depending on current pane count vs maxPanesPerWindow). Returns immediately
73
- * with the pane id; the parent stays alive.
209
+ * Async sibling spawn. Launches an interactive agent (claude or pi, per
210
+ * detectAgentKind) in the dedicated subagent session, progressively filling
211
+ * windows up to `maxPanesPerWindow` before creating a new window. Returns
212
+ * immediately with the pane id; the parent stays alive. Focus is never
213
+ * switched — the user jumps to the subagent session with Alt-o.
74
214
  *
75
- * If `fork` is set, uses `claude --resume <id> --fork-session`.
215
+ * If `fork` is set, forks the host session into a fresh one.
76
216
  */
77
217
  export declare function spawnAgent(opts: SpawnAgentOptions): SpawnAgentResult;