@crouton-kit/crouter 0.3.8 → 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 (46) hide show
  1. package/dist/cli.js +14 -24
  2. package/dist/commands/agent.d.ts +4 -0
  3. package/dist/commands/agent.js +444 -243
  4. package/dist/commands/debug.d.ts +1 -1
  5. package/dist/commands/debug.js +20 -7
  6. package/dist/commands/human.js +51 -19
  7. package/dist/commands/job.d.ts +9 -0
  8. package/dist/commands/job.js +50 -10
  9. package/dist/commands/mode.d.ts +2 -0
  10. package/dist/commands/mode.js +231 -0
  11. package/dist/commands/pkg.js +5 -0
  12. package/dist/commands/plan.d.ts +1 -1
  13. package/dist/commands/plan.js +24 -11
  14. package/dist/commands/skill.js +20 -4
  15. package/dist/commands/spec.d.ts +1 -1
  16. package/dist/commands/spec.js +24 -11
  17. package/dist/commands/sys.js +5 -0
  18. package/dist/core/__tests__/job.test.js +11 -11
  19. package/dist/core/__tests__/jobs.test.js +33 -1
  20. package/dist/core/__tests__/resolver.test.js +69 -1
  21. package/dist/core/__tests__/spawn.test.d.ts +1 -0
  22. package/dist/core/__tests__/spawn.test.js +138 -0
  23. package/dist/core/__tests__/subagents.test.d.ts +1 -0
  24. package/dist/core/__tests__/subagents.test.js +75 -0
  25. package/dist/core/__tests__/unknown-path.test.d.ts +1 -0
  26. package/dist/core/__tests__/unknown-path.test.js +52 -0
  27. package/dist/core/bootstrap.d.ts +2 -0
  28. package/dist/core/bootstrap.js +66 -0
  29. package/dist/core/command.d.ts +58 -2
  30. package/dist/core/command.js +62 -14
  31. package/dist/core/frontmatter.d.ts +10 -0
  32. package/dist/core/frontmatter.js +24 -9
  33. package/dist/core/help.d.ts +39 -8
  34. package/dist/core/help.js +64 -32
  35. package/dist/core/jobs.d.ts +8 -2
  36. package/dist/core/jobs.js +109 -6
  37. package/dist/core/resolver.js +51 -1
  38. package/dist/core/spawn.d.ts +140 -23
  39. package/dist/core/spawn.js +392 -73
  40. package/dist/core/subagents.d.ts +18 -0
  41. package/dist/core/subagents.js +163 -0
  42. package/dist/prompts/agent.d.ts +10 -1
  43. package/dist/prompts/agent.js +34 -3
  44. package/dist/types.d.ts +21 -0
  45. package/dist/types.js +3 -0
  46. package/package.json +2 -2
@@ -1,34 +1,53 @@
1
- // `crtr agent` umbrella — agentic workflows: spec/plan/debug + spawn primitives.
1
+ // `crtr agent` umbrella — spawn primitives.
2
2
  //
3
- // `agent new {prompt,fork,planner,implementer,reviewer}` are the spawn leaves
4
- // (formerly `job start *`). Spawning creates a job record; monitoring lives at
5
- // `crtr job`. This split keeps the job registry agnostic of producer — agents
6
- // are one producer, future producers compose under their own subtree.
3
+ // `agent new` is the single spawn command (general-purpose by default,
4
+ // `--agent <id>` overlays a defined subagent). `agent fork` carries the
5
+ // current session context into a sibling pane. Spawning creates a job record;
6
+ // monitoring lives at `crtr job`.
7
+ //
8
+ // Spec/plan/debug workflows live under `crtr mode` (src/commands/mode.ts).
7
9
  //
8
10
  // Terminal-write contract for spawned workers:
9
- // Worker calls `crtr job submit` jobs.writeResult(jobId, result, 'done').
10
- // If claude exits without submitting, the wrapper shell calls `crtr job _fail`
11
- // → jobs.writeResult(jobId, {}, 'failed') IF result.json does not yet exist.
12
- // `job read result` watches result.json appearance as the sole completion signal.
11
+ // A worker MAY call `crtr job submit` to deliver a result, but is not
12
+ // required to. A job reaches a terminal state by any of three signals:
13
+ // 1. `crtr job submit` writes result.md (done|failed).
14
+ // 2. The wrapper shell's `crtr job _fail` runs when claude exits normally
15
+ // without a submit (result.md absent → failed).
16
+ // 3. The hosting tmux pane is closed/killed — case 2 never runs (SIGHUP),
17
+ // so the jobs layer reaps the job when its recorded pane disappears.
18
+ // Spawns record their pane id so (3) and `crtr job cancel` can act on it.
13
19
  import { defineBranch, defineLeaf } from '../core/command.js';
14
20
  import { InputError } from '../core/io.js';
15
- import { createJob, appendEvent } from '../core/jobs.js';
16
- import { spawnAgent, spawnAndDetach, isInTmux } from '../core/spawn.js';
21
+ import { writeFileSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+ import { stateBlock } from '../core/help.js';
24
+ import { usage, general } from '../core/errors.js';
25
+ import { listSubagents, resolveSubagent, subagentId, scopeAgentsDir, } from '../core/subagents.js';
26
+ import { resolveScopeArg, requireScopeRoot, projectScopeRoot } from '../core/scope.js';
27
+ import { ensureScopeInitialized } from '../core/config.js';
28
+ import { ensureDir, pathExists } from '../core/fs-utils.js';
29
+ import { createJob, appendEvent, recordJobPane, recordJobPid, writeMarkdownResult, readResult as readJobResult, } from '../core/jobs.js';
30
+ import { spawnAgent, isInTmux, detectAgentKind, runAgentHeadless, spawnHeadlessDetached, } from '../core/spawn.js';
31
+ import { agentNewPrompt } from '../prompts/agent.js';
17
32
  import { readConfig } from '../core/config.js';
18
- import { planHandoffPrompt, implementHandoffPrompt, reviewerHandoffPrompt } from '../prompts/agent.js';
19
- import { existsSync } from 'node:fs';
20
- import { registerSpec } from './spec.js';
21
- import { registerPlan } from './plan.js';
22
- import { registerDebug } from './debug.js';
23
- const DEFAULT_KILL_SECS = 2;
24
- function followUpResult(jobId) {
25
- return `crtr job read result ${jobId} --wait`;
33
+ export const DEFAULT_KILL_SECS = 2;
34
+ const WAIT_BUDGET_MS = 10 * 60 * 1000;
35
+ // The built-in, persona-less agent type. `--agent general` (or omitting --agent)
36
+ // spawns the general-purpose worker; any other id overlays a defined subagent.
37
+ const GENERAL_AGENT = 'general';
38
+ // A spawn leaf returns a job handle, not a result. This follow_up is the
39
+ // ORCHESTRATOR's own next call: the agent that spawned the worker collects the
40
+ // result itself and reports the findings to the user. The observed failure mode
41
+ // is relaying these commands to the human ("run this to see the result") or
42
+ // inventing a batch-await that doesn't exist — the phrasing forecloses both.
43
+ export function followUpResult(jobId) {
44
+ return `You spawned this worker — collecting its result is your job, not the user's. When you're ready for it, run \`crtr job read result ${jobId} --wait\` yourself (blocks up to 10 min), then report the worker's findings. Spawned several? Call it once per job_id — there is no batch await. Never print these commands for the user to run.`;
26
45
  }
27
- function resolveMaxPanes() {
46
+ export function resolveMaxPanes() {
28
47
  const cfg = readConfig('user');
29
48
  return cfg.max_panes_per_window;
30
49
  }
31
- function assertTmux() {
50
+ export function assertTmux() {
32
51
  if (!isInTmux()) {
33
52
  throw new InputError({
34
53
  error: 'not_in_tmux',
@@ -38,117 +57,316 @@ function assertTmux() {
38
57
  }
39
58
  }
40
59
  // ---------------------------------------------------------------------------
41
- // agent new prompt
60
+ // Subagent catalog (dynamicState for `agent -h` and `agent subagent -h`)
61
+ // ---------------------------------------------------------------------------
62
+ // Preamble for the subagent catalog. Scoped to ONE job: how to invoke a worker
63
+ // and how to pick which agent runs it. The reach-for-it reflex and the
64
+ // when-to-delegate scenarios live on the root `useWhen` (which renders right
65
+ // above this on the root tool guide); the collection mechanics live on the
66
+ // spawn leaf's `follow_up`. Keeping each fact in one place avoids restating the
67
+ // same delegation pitch three times in the always-loaded root. Travels with the
68
+ // catalog to every surface it renders on (root, `agent -h`, `agent subagent -h`).
69
+ const SUBAGENT_USAGE = 'One spawn command — `agent new` — runs any of these. `--agent <id>` overlays a defined persona (its system prompt, model, and tools); omit it (or pass `--agent general`) for the general-purpose worker. Same command either way. Pick the most specific agent that fits the task, else general. A defined agent earns its keep when a task recurs with a stable shape (a scout, a reviewer); for a fast throwaway pass, general on a cheap model is usually enough.';
70
+ function descLine(a) {
71
+ const meta = [];
72
+ if (a.frontmatter.model !== undefined && a.frontmatter.model !== '')
73
+ meta.push(`model: ${a.frontmatter.model}`);
74
+ if (a.frontmatter.tools !== undefined && a.frontmatter.tools.length > 0)
75
+ meta.push(`tools: ${a.frontmatter.tools.join(',')}`);
76
+ const annot = meta.length > 0 ? ` (${meta.join('; ')})` : '';
77
+ const desc = (a.frontmatter.description !== undefined ? a.frontmatter.description : '').replace(/\s+/g, ' ').trim();
78
+ return `- ${subagentId(a)}${annot}: ${desc}`;
79
+ }
80
+ /** The defined-subagents catalog as a self-named `<subagents count="N">`
81
+ * element: a usage preamble plus one full-description line per agent (the
82
+ * discriminator for picking which to delegate to). Project agents list first.
83
+ * Soft-fails to null when discovery throws or nothing is defined, so the block
84
+ * is simply omitted on a cold path. */
85
+ function buildSubagentCatalog() {
86
+ let agents;
87
+ try {
88
+ agents = listSubagents();
89
+ }
90
+ catch {
91
+ agents = [];
92
+ }
93
+ // Project agents (repo-specific) before user/builtin, then alphabetical.
94
+ const ordered = [...agents].sort((a, b) => {
95
+ const sa = a.scope === 'project' ? 0 : 1;
96
+ const sb = b.scope === 'project' ? 0 : 1;
97
+ return sa !== sb ? sa - sb : subagentId(a).localeCompare(subagentId(b));
98
+ });
99
+ const lines = [
100
+ SUBAGENT_USAGE,
101
+ '',
102
+ 'Agents (select with `agent new --agent <id>`):',
103
+ `- ${GENERAL_AGENT} (default): general-purpose agent for any self-contained task — research, code search, multi-step work. Used when --agent is omitted or \`--agent ${GENERAL_AGENT}\`.`,
104
+ ];
105
+ for (const a of ordered)
106
+ lines.push(descLine(a));
107
+ // count includes the built-in general agent alongside the defined ones.
108
+ return stateBlock('subagents', { count: agents.length + 1 }, lines.join('\n'));
109
+ }
110
+ // Decision guidance surfaced on `agent new -h`. Answers "when do I reach
111
+ // for this, how often, and which mode?" — the questions the flag constraints
112
+ // alone don't.
113
+ const PROMPT_GUIDE = `## How to invoke
114
+
115
+ The task goes on **stdin**, not as an argument; the agent id and flags are argv.
116
+ Three shapes cover almost everything:
117
+
118
+ # Blocking (default): waits and returns the result inline, like a function call
119
+ echo "find where request auth is handled" | crtr agent new
120
+
121
+ # With a persona: --agent overlays a defined subagent (see \`crtr agent subagent list\`)
122
+ echo "map the daemon lifecycle" | crtr agent new --agent explore
123
+
124
+ # Background fan-out: each spawn returns a job_id immediately; collect when ready
125
+ echo "task A" | crtr agent new --background # -> { "job_id": "..." }
126
+ echo "task B" | crtr agent new --background
127
+ crtr job read result <job_id> --wait # blocks until that worker finishes
128
+
129
+ Heredocs work for multi-line prompts: \`crtr agent new <<'EOF' ... EOF\`.
130
+
131
+ ## When to delegate
132
+
133
+ Reach for this constantly — it is the main way to get work done without
134
+ burning your own context. A spawned agent runs in a fresh context window and
135
+ hands back only its conclusion, so your own thread stays lean. Delegate any
136
+ self-contained task you can describe in a prompt: exploring a codebase,
137
+ researching how something works, implementing a change, writing tests,
138
+ refactoring, reproducing a bug, drafting docs. When in doubt, delegate rather
139
+ than do it inline — especially for anything multi-step, file-heavy, or
140
+ parallelizable. Keep doing trivial, single-shot lookups yourself.
141
+
142
+ Two habits worth building. **Scout before you build:** before you start
143
+ working in unfamiliar code, spawn a quick recon agent to map it first — a fast,
144
+ cheap model (e.g. haiku) is plenty for "where does X live / how does Y work,"
145
+ and it keeps the exploration out of your own context. **Fan out independents:**
146
+ when a job splits into tasks that don't depend on each other, launch one
147
+ --background worker per task and let them run concurrently rather than doing
148
+ them in series.
149
+
150
+ This is the single spawn command: it runs the general-purpose agent by default,
151
+ and \`--agent <id>\` swaps in a defined persona instead (see below). Same path
152
+ either way — there is no separate command for custom agents.
153
+
154
+ One worker per independent task. For a big job, fan out several small,
155
+ well-scoped workers instead of one mega-prompt — they run concurrently and each
156
+ stays focused.
157
+
158
+ ## Which mode
159
+
160
+ **Default (blocking)** — use for almost everything. It spawns the worker, waits,
161
+ and returns the result inline, like a function call. Pick this whenever you need
162
+ the answer before you can continue. Inside tmux the worker runs as an interactive
163
+ agent in a pane of the dedicated subagent session WITHOUT stealing your focus —
164
+ jump over to watch or steer it with Alt-o, or just wait for the inline result.
165
+ Outside tmux it runs as a print-mode child process. Either way the result comes
166
+ back inline.
167
+
168
+ **--background** — use when you do NOT need the result right now:
169
+ - Fan out: launch several independent workers, keep working, then collect
170
+ each at \`crtr job read result <id> --wait\` once you need them.
171
+ - Fire-and-continue: kick off a long task and proceed with other work.
172
+ You own the collection — a backgrounded worker is yours to retrieve and report,
173
+ not the user's.
174
+
175
+ **--headed** — legacy/no-op. Workers are always headed (interactive in a pane)
176
+ inside tmux now; this flag is kept so older invocations don't break but changes
177
+ nothing. To watch or steer a worker, spawn normally and press Alt-o.
178
+
179
+ ## Choosing the agent
180
+
181
+ **--agent <id>** selects which agent runs the task. Omit it (or pass
182
+ \`--agent general\`) for the general-purpose agent. Pass a defined id to overlay
183
+ that subagent: a reusable persona defined in markdown with frontmatter (see
184
+ \`crtr agent subagent\`), whose body becomes the worker's appended system prompt
185
+ and whose declared model / (pi) tools are applied for the run. Everything else
186
+ (modes, output) is identical. Reach for a defined agent when a recurring task
187
+ has a stable persona (a scout, a reviewer); use general for one-off delegation.`;
188
+ // ---------------------------------------------------------------------------
189
+ // agent new — the single spawn command. General-purpose by default;
190
+ // `--agent <id>` overlays a defined subagent persona.
42
191
  // ---------------------------------------------------------------------------
43
192
  const newPrompt = defineLeaf({
44
- name: 'prompt',
193
+ name: 'new',
45
194
  help: {
46
- name: 'agent new prompt',
47
- summary: 'spawn a fresh Claude agent with a prompt; returns a job handle immediately',
195
+ name: 'agent new',
196
+ summary: 'spawn a worker (matches the host CLI: claude or pi) — the general-purpose agent by default, or a defined subagent via --agent. Blocking by default, returning the result inline; inside tmux the worker runs in an unfocused pane you can watch with Alt-o',
197
+ guide: PROMPT_GUIDE,
48
198
  params: [
49
- { kind: 'stdin', name: 'prompt', required: true, constraint: 'Prompt text sent to the spawned agent.' },
199
+ { kind: 'stdin', name: 'prompt', required: true, constraint: 'Task/prompt sent to the spawned agent as the first user message.' },
50
200
  { kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Working directory for the spawned agent. Defaults to process.cwd().' },
51
- { kind: 'flag', name: 'name', type: 'string', required: true, constraint: 'Display name passed to `claude -n`; surfaces in pane title and /resume picker.' },
201
+ { kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Display name passed to the agent CLI (`-n`); surfaces in pane title and resume picker. Defaults to the --agent id (or "general").' },
202
+ { kind: 'flag', name: 'agent', type: 'string', required: false, default: 'general', constraint: 'Which agent runs the task. Omit or "general" for the general-purpose agent; any other id (<name> or <plugin>/<name>) overlays that defined subagent\'s persona/model/tools. List with `crtr agent subagent list`.' },
203
+ { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'With --agent, narrows resolution when the subagent name is ambiguous across scopes.' },
204
+ { kind: 'flag', name: 'model', type: 'string', required: false, constraint: 'Model pattern/id passed via `--model`. Overrides a subagent\'s declared model.' },
205
+ { kind: 'flag', name: 'headed', type: 'bool', required: false, constraint: 'Legacy/no-op. Workers are always interactive panes in the dedicated subagent session (inside tmux); this flag is accepted for back-compat but changes nothing. Use Alt-o to watch/steer.' },
206
+ { kind: 'flag', name: 'background', type: 'bool', required: false, constraint: 'Return a job handle immediately instead of blocking for the result. Collect later with `crtr job read result`.' },
52
207
  ],
53
208
  output: [
54
209
  { name: 'job_id', type: 'string', required: true, constraint: 'Use with `crtr job read status|logs|result` and `crtr job cancel`.' },
55
- { name: 'follow_up', type: 'string', required: true, constraint: 'Recommended next call.' },
210
+ { name: 'agent', type: 'string', required: true, constraint: 'The agent that ran the task: "general" or a resolved subagent id.' },
211
+ { name: 'status', type: 'string', required: false, constraint: 'Blocking runs only: done | failed | closed | timeout.' },
212
+ { name: 'result_md', type: 'string', required: false, constraint: 'Blocking runs: the agent\'s output (a submitted markdown result in tmux, or print-mode stdout outside tmux).' },
213
+ { name: 'result', type: 'object', required: false, constraint: 'Blocking runs: a structured result, when one was submitted programmatically.' },
214
+ { name: 'reason', type: 'string', required: false, constraint: 'Blocking runs: explanation when status is failed or closed.' },
215
+ { name: 'follow_up', type: 'string', required: false, constraint: 'Background runs only: your own next call — run it and report the worker\'s result; do not relay it to the user.' },
56
216
  ],
57
217
  outputKind: 'object',
58
218
  effects: [
59
- 'Spawns a Claude agent in a sibling tmux pane.',
60
- 'Creates a job entry at $XDG_STATE_HOME/crtr/jobs/<job_id>/.',
61
- 'On completion, result writes atomically to result.json.',
219
+ 'Default (blocking): inside tmux, runs an interactive agent in a pane of the dedicated subagent session (no focus change) that delivers its result via `crtr job submit`; outside tmux, runs a print-mode child process whose stdout is the result. Set CRTR_SUBAGENT_TMUX=off to force the child-process path. Either way the result is returned inline.',
220
+ 'All spawns land in a per-host-session tmux session (crtr-agents-<pane>); Alt-o toggles between it and the originating pane.',
221
+ '--headed is legacy/no-op (workers are always headed inside tmux).',
222
+ '--background: returns a job handle immediately; the worker runs async and its result is collected via `crtr job`.',
223
+ 'Always creates a job entry at $XDG_STATE_HOME/crtr/jobs/<job_id>/ and records the result there.',
62
224
  ],
63
225
  },
64
226
  run: async (input) => {
65
- assertTmux();
66
227
  const prompt = input['prompt'];
67
228
  const cwd = typeof input['cwd'] === 'string' ? input['cwd'] : process.cwd();
68
- const name = input['name'];
69
- const { jobId } = createJob('prompt', { cwd });
70
- const promptWithSubmit = `${prompt}
71
-
72
- ---
73
- When your task is complete, submit your result (markdown body piped on stdin):
74
- \`\`\`bash
75
- crtr job submit ${jobId} <<'MD'
76
- <your result as markdown>
77
- MD
78
- \`\`\`
79
- If you cannot complete the task, submit a failure with a reason (no stdin needed):
80
- \`\`\`bash
81
- crtr job submit ${jobId} --status failed --reason "<why>"
82
- \`\`\``;
83
- const result = spawnAgent({
84
- prompt: promptWithSubmit,
85
- cwd,
86
- jobId,
87
- maxPanesPerWindow: resolveMaxPanes(),
88
- name,
89
- });
90
- if (result.status === 'not-in-tmux') {
91
- throw new InputError({ error: 'not_in_tmux', message: result.message, next: 'Run inside a tmux session.' });
229
+ const background = input['background'] === true;
230
+ // Optional subagent overlay: --agent loads a persona (markdown + frontmatter)
231
+ // that supplies the appended system prompt, model, and (pi) tools. Without
232
+ // it, this is a plain general-purpose spawn.
233
+ const agentArg = typeof input['agent'] === 'string' && input['agent'] !== '' ? input['agent'] : undefined;
234
+ const scopeStr = input['scope'];
235
+ // `general` (or no --agent) is the built-in general-purpose worker: no
236
+ // persona overlay. Any other id resolves a defined subagent.
237
+ let sub;
238
+ if (agentArg !== undefined && agentArg !== GENERAL_AGENT) {
239
+ const resolveOpts = {};
240
+ if (scopeStr !== undefined) {
241
+ const resolved = resolveScopeArg(scopeStr);
242
+ if (resolved !== 'all')
243
+ resolveOpts.scope = resolved;
244
+ }
245
+ sub = resolveSubagent(agentArg, resolveOpts);
92
246
  }
93
- if (result.status === 'spawn-failed') {
94
- throw new InputError({ error: 'spawn_failed', message: result.message, next: 'Check tmux is running and try again.' });
247
+ const agentId = sub !== undefined ? subagentId(sub) : GENERAL_AGENT;
248
+ const nameArg = typeof input['name'] === 'string' && input['name'] !== '' ? input['name'] : undefined;
249
+ const name = nameArg ?? agentId;
250
+ const systemPrompt = sub?.systemPrompt;
251
+ const tools = sub?.frontmatter.tools;
252
+ const model = (typeof input['model'] === 'string' && input['model'] !== '' ? input['model'] : undefined)
253
+ ?? sub?.frontmatter.model;
254
+ const subOut = { agent: agentId };
255
+ const { jobId } = createJob(sub !== undefined ? 'subagent' : 'general', { cwd });
256
+ // Inside tmux (unless opted out with CRTR_SUBAGENT_TMUX=off), EVERY worker
257
+ // runs as an interactive agent in a pane of the dedicated subagent session.
258
+ // We never steal focus — `--headed` is retained only for back-compat and no
259
+ // longer changes behavior; jump to the session yourself with Alt-o. The pane
260
+ // agent delivers its result via `crtr job submit` (the instruction is
261
+ // appended to the prompt). Outside tmux there is no pane to host an
262
+ // interactive agent, so we fall back to a print-mode child process whose
263
+ // stdout is the result.
264
+ const useTmux = isInTmux() && process.env.CRTR_SUBAGENT_TMUX !== 'off';
265
+ if (useTmux) {
266
+ const result = spawnAgent({
267
+ prompt: agentNewPrompt(prompt, jobId),
268
+ cwd,
269
+ jobId,
270
+ maxPanesPerWindow: resolveMaxPanes(),
271
+ name,
272
+ systemPrompt,
273
+ model,
274
+ tools,
275
+ });
276
+ if (result.status === 'spawned') {
277
+ if (result.paneId !== undefined)
278
+ recordJobPane(jobId, result.paneId);
279
+ const paneLabel = result.paneId !== undefined ? result.paneId : 'unknown';
280
+ appendEvent(jobId, { level: 'info', event: 'worker_started', message: `pane ${paneLabel} spawned (unfocused)` });
281
+ if (background) {
282
+ return { job_id: jobId, ...subOut, follow_up: followUpResult(jobId) };
283
+ }
284
+ const r = await readJobResult(jobId, { waitMs: WAIT_BUDGET_MS });
285
+ const out = { job_id: jobId, ...subOut, status: r.status };
286
+ if (r.result_md !== undefined)
287
+ out['result_md'] = r.result_md;
288
+ if (r.result !== undefined)
289
+ out['result'] = r.result;
290
+ if (r.reason !== undefined)
291
+ out['reason'] = r.reason;
292
+ return out;
293
+ }
294
+ // tmux placement failed → fall through to the child-process path below.
295
+ appendEvent(jobId, { level: 'info', event: 'tmux_spawn_failed', message: `${result.message}; falling back to a print-mode child process` });
95
296
  }
96
- const paneLabel = result.paneId !== undefined ? result.paneId : 'unknown';
97
- appendEvent(jobId, { level: 'info', event: 'worker_started', message: `pane ${paneLabel} spawned` });
98
- return { job_id: jobId, follow_up: followUpResult(jobId) };
297
+ // ---- No tmux: print-mode child process (stdout is the result) ----
298
+ if (background) {
299
+ const res = spawnHeadlessDetached({ prompt, name, cwd, jobId, systemPrompt, model, tools });
300
+ if (res.status === 'spawn-failed') {
301
+ throw new InputError({ error: 'spawn_failed', message: res.message, next: 'Check the agent CLI is installed and on PATH.' });
302
+ }
303
+ if (res.pid !== undefined)
304
+ recordJobPid(jobId, res.pid);
305
+ appendEvent(jobId, { level: 'info', event: 'worker_started', message: res.message });
306
+ return { job_id: jobId, ...subOut, follow_up: followUpResult(jobId) };
307
+ }
308
+ appendEvent(jobId, { level: 'info', event: 'worker_started', message: 'agent started (blocking, no tmux)' });
309
+ const r = await runAgentHeadless({ prompt, name, cwd, systemPrompt, model, tools });
310
+ if (r.status === 'done') {
311
+ writeMarkdownResult(jobId, r.output, 'done');
312
+ }
313
+ else {
314
+ writeMarkdownResult(jobId, r.output, 'failed', `agent exited with code ${r.exitCode ?? 'null'}`);
315
+ }
316
+ appendEvent(jobId, {
317
+ level: r.status === 'done' ? 'info' : 'error',
318
+ event: 'worker_finished',
319
+ message: `agent ${r.status}`,
320
+ });
321
+ return { job_id: jobId, ...subOut, status: r.status, result_md: r.output };
99
322
  },
100
323
  });
101
324
  // ---------------------------------------------------------------------------
102
- // agent new fork
325
+ // agent fork
103
326
  // ---------------------------------------------------------------------------
104
327
  const newFork = defineLeaf({
105
328
  name: 'fork',
106
329
  help: {
107
- name: 'agent new fork',
108
- summary: 'fork the current Claude session into a sibling pane; returns a job handle immediately',
330
+ name: 'agent fork',
331
+ summary: 'fork the current agent session into a sibling pane; returns a job handle immediately',
109
332
  params: [
110
333
  { kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Working directory. Defaults to process.cwd().' },
111
- { kind: 'flag', name: 'name', type: 'string', required: true, constraint: 'Display name passed to `claude -n`; surfaces in pane title and /resume picker.' },
334
+ { kind: 'flag', name: 'name', type: 'string', required: true, constraint: 'Display name passed to the agent CLI (`-n`); surfaces in pane title and resume picker.' },
112
335
  ],
113
336
  output: [
114
337
  { name: 'job_id', type: 'string', required: true, constraint: 'Use with `crtr job read *` and `crtr job cancel`.' },
115
- { name: 'follow_up', type: 'string', required: true, constraint: 'Recommended next call.' },
338
+ { name: 'follow_up', type: 'string', required: true, constraint: 'Your own next call — run it and report the worker\'s result; do not relay it to the user.' },
116
339
  ],
117
340
  outputKind: 'object',
118
341
  effects: [
119
- 'Requires $CLAUDE_CODE_SESSION_ID — must run inside Claude Code.',
120
- 'Spawns a forked Claude session in a sibling tmux pane.',
121
- 'Creates a job entry and result sidecar as with `agent new prompt`.',
342
+ 'Claude Code only: requires $CLAUDE_CODE_SESSION_ID. pi does not expose its session id to subprocesses, so fork is unavailable under pi use `agent new` instead.',
343
+ 'Spawns a forked agent session in a sibling tmux pane.',
344
+ 'Creates a job entry and result sidecar as with `agent new`.',
122
345
  ],
123
346
  },
124
347
  run: async (input) => {
125
348
  assertTmux();
349
+ const agentKind = detectAgentKind();
350
+ if (agentKind === 'pi') {
351
+ throw new InputError({
352
+ error: 'fork_unsupported',
353
+ message: 'crtr agent fork is not supported under pi: pi does not expose the active session id to subprocesses.',
354
+ next: 'Use `crtr agent new` to spawn a fresh pi agent instead.',
355
+ });
356
+ }
126
357
  const parentSessionId = process.env['CLAUDE_CODE_SESSION_ID'];
127
358
  if (parentSessionId === undefined || parentSessionId === '') {
128
359
  throw new InputError({
129
360
  error: 'missing_session_id',
130
- message: 'crtr agent new fork requires $CLAUDE_CODE_SESSION_ID — must run inside Claude Code.',
361
+ message: 'crtr agent fork requires $CLAUDE_CODE_SESSION_ID — must run inside Claude Code.',
131
362
  next: 'Run this command from within a Claude Code session.',
132
363
  });
133
364
  }
134
365
  const cwd = typeof input['cwd'] === 'string' ? input['cwd'] : process.cwd();
135
366
  const name = input['name'];
136
367
  const { jobId } = createJob('fork', { cwd });
137
- const promptWithSubmit = `Fork of session ${parentSessionId}
138
-
139
- ---
140
- When your task is complete, submit your result (markdown body piped on stdin):
141
- \`\`\`bash
142
- crtr job submit ${jobId} <<'MD'
143
- <your result as markdown>
144
- MD
145
- \`\`\`
146
- If you cannot complete the task, submit a failure with a reason (no stdin needed):
147
- \`\`\`bash
148
- crtr job submit ${jobId} --status failed --reason "<why>"
149
- \`\`\``;
150
368
  const result = spawnAgent({
151
- prompt: promptWithSubmit,
369
+ prompt: `Fork of session ${parentSessionId}`,
152
370
  cwd,
153
371
  jobId,
154
372
  fork: { sessionId: parentSessionId },
@@ -161,206 +379,183 @@ crtr job submit ${jobId} --status failed --reason "<why>"
161
379
  if (result.status === 'spawn-failed') {
162
380
  throw new InputError({ error: 'spawn_failed', message: result.message, next: 'Check tmux is running and try again.' });
163
381
  }
382
+ if (result.paneId !== undefined)
383
+ recordJobPane(jobId, result.paneId);
164
384
  const forkPaneLabel = result.paneId !== undefined ? result.paneId : 'unknown';
165
385
  appendEvent(jobId, { level: 'info', event: 'worker_started', message: `forked pane ${forkPaneLabel} spawned` });
166
386
  return { job_id: jobId, follow_up: followUpResult(jobId) };
167
387
  },
168
388
  });
169
389
  // ---------------------------------------------------------------------------
170
- // agent new planner
390
+ // agent subagent (management branch: list / read / scaffold)
171
391
  // ---------------------------------------------------------------------------
172
- const newPlanner = defineLeaf({
173
- name: 'planner',
392
+ const subagentList = defineLeaf({
393
+ name: 'list',
174
394
  help: {
175
- name: 'agent new planner',
176
- summary: 'launch a planning agent for an approved spec; closes the originating pane after handoff',
395
+ name: 'agent subagent list',
396
+ summary: 'list defined subagents (markdown + frontmatter) discoverable from scope roots and plugins',
177
397
  params: [
178
- { kind: 'positional', name: 'spec_path', type: 'path', required: true, constraint: 'Absolute path to the spec file.' },
179
- { kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Working directory. Defaults to process.cwd().' },
180
- { kind: 'flag', name: 'name', type: 'string', required: true, constraint: 'Display name passed to `claude -n`; surfaces in pane title and /resume picker.' },
398
+ { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project', 'all'], required: false, constraint: 'Default: all.' },
399
+ { kind: 'flag', name: 'full', type: 'bool', required: false, constraint: 'When present, includes each subagent\'s model and tools.' },
181
400
  ],
182
401
  output: [
183
- { name: 'job_id', type: 'string', required: true, constraint: 'Use with `crtr job read *` and `crtr job cancel`.' },
184
- { name: 'follow_up', type: 'string', required: true, constraint: 'Recommended next call.' },
402
+ { name: 'items', type: 'object[]', required: true, constraint: 'Each: {id, name, plugin, scope, description}. With --full also {model, tools}. Sorted by name.' },
403
+ { name: 'total', type: 'integer', required: true, constraint: 'Number of subagents returned.' },
404
+ { name: 'follow_up', type: 'string', required: true, constraint: 'Next commands for reading or spawning a subagent.' },
185
405
  ],
186
406
  outputKind: 'object',
187
- effects: [
188
- 'Spawns a planner agent in a sibling tmux pane.',
189
- 'Closes the originating pane after a short delay.',
190
- 'Creates a job entry and result sidecar.',
191
- ],
407
+ effects: ['None. Read-only.'],
192
408
  },
193
409
  run: async (input) => {
194
- assertTmux();
195
- const specPath = input['spec_path'];
196
- const cwd = typeof input['cwd'] === 'string' ? input['cwd'] : process.cwd();
197
- const name = input['name'];
198
- if (!existsSync(specPath)) {
199
- throw new InputError({
200
- error: 'not_found',
201
- message: `spec not found: ${specPath}`,
202
- field: 'spec_path',
203
- next: 'Provide an absolute path to an existing spec file.',
204
- });
410
+ const scopeStr = input['scope'];
411
+ const full = input['full'] === true;
412
+ let scopeFilter;
413
+ if (scopeStr !== undefined) {
414
+ const resolved = resolveScopeArg(scopeStr);
415
+ if (resolved !== 'all')
416
+ scopeFilter = resolved;
205
417
  }
206
- const { jobId } = createJob('planner', { cwd });
207
- const result = spawnAndDetach({
208
- prompt: planHandoffPrompt(specPath, jobId),
209
- cwd,
210
- jobId,
211
- placement: 'split-h',
212
- killAfterSeconds: DEFAULT_KILL_SECS,
213
- name,
214
- });
215
- if (result.status === 'not-in-tmux') {
216
- throw new InputError({ error: 'not_in_tmux', message: result.message, next: 'Run inside a tmux session.' });
217
- }
218
- if (result.status === 'spawn-failed') {
219
- throw new InputError({ error: 'spawn_failed', message: result.message, next: 'Check tmux is running and try again.' });
220
- }
221
- const plannerPaneLabel = result.paneId !== undefined ? result.paneId : 'unknown';
222
- appendEvent(jobId, { level: 'info', event: 'worker_started', message: `planner pane ${plannerPaneLabel} spawned` });
223
- return { job_id: jobId, follow_up: followUpResult(jobId) };
418
+ const agents = listSubagents(scopeFilter);
419
+ return {
420
+ items: agents.map((a) => {
421
+ const base = {
422
+ id: subagentId(a),
423
+ name: a.name,
424
+ plugin: a.plugin,
425
+ scope: a.scope,
426
+ description: a.frontmatter.description !== undefined ? a.frontmatter.description : null,
427
+ };
428
+ if (full) {
429
+ base['model'] = a.frontmatter.model !== undefined ? a.frontmatter.model : null;
430
+ base['tools'] = a.frontmatter.tools !== undefined ? a.frontmatter.tools : null;
431
+ }
432
+ return base;
433
+ }),
434
+ total: agents.length,
435
+ follow_up: 'Read one with `crtr agent subagent read <name>`; delegate with `crtr agent new --agent <name>`.',
436
+ };
224
437
  },
225
438
  });
226
- // ---------------------------------------------------------------------------
227
- // agent new implementer
228
- // ---------------------------------------------------------------------------
229
- const newImplementer = defineLeaf({
230
- name: 'implementer',
439
+ const subagentRead = defineLeaf({
440
+ name: 'read',
231
441
  help: {
232
- name: 'agent new implementer',
233
- summary: 'launch an implementation agent for an approved plan; closes the originating pane after handoff',
442
+ name: 'agent subagent read',
443
+ summary: 'load a subagent\'s system prompt (markdown body) and metadata',
234
444
  params: [
235
- { kind: 'positional', name: 'plan_path', type: 'path', required: true, constraint: 'Absolute path to the plan file.' },
236
- { kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Working directory. Defaults to process.cwd().' },
237
- { kind: 'flag', name: 'name', type: 'string', required: true, constraint: 'Display name passed to `claude -n`; surfaces in pane title and /resume picker.' },
445
+ { kind: 'positional', name: 'name', required: true, constraint: 'Subagent identifier: <name> or <plugin>/<name>.' },
446
+ { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Narrows resolution when the name is ambiguous.' },
447
+ { kind: 'flag', name: 'no-body', type: 'bool', required: false, constraint: 'When present, omits the system prompt body returns metadata only.' },
238
448
  ],
239
449
  output: [
240
- { name: 'job_id', type: 'string', required: true, constraint: 'Use with `crtr job read *` and `crtr job cancel`.' },
241
- { name: 'follow_up', type: 'string', required: true, constraint: 'Recommended next call.' },
450
+ { name: 'id', type: 'string', required: true, constraint: 'Resolved subagent id.' },
451
+ { name: 'name', type: 'string', required: true, constraint: 'Subagent name.' },
452
+ { name: 'plugin', type: 'string', required: true, constraint: 'Plugin the subagent belongs to, or _ for a scope-root agent.' },
453
+ { name: 'scope', type: 'string', required: true, constraint: 'Scope it resolved from.' },
454
+ { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the .md file.' },
455
+ { name: 'description', type: 'string', required: true, constraint: 'Frontmatter description.' },
456
+ { name: 'model', type: 'string | null', required: true, constraint: 'Declared model, or null.' },
457
+ { name: 'tools', type: 'string[] | null', required: true, constraint: 'Declared tool allow-list, or null.' },
458
+ { name: 'system_prompt', type: 'string', required: false, constraint: 'Markdown body applied as the appended system prompt. Omitted with --no-body.' },
242
459
  ],
243
460
  outputKind: 'object',
244
- effects: [
245
- 'Spawns an implementer agent in a sibling tmux pane.',
246
- 'Closes the originating pane after a short delay.',
247
- 'Creates a job entry and result sidecar.',
248
- ],
461
+ effects: ['None. Read-only.'],
249
462
  },
250
463
  run: async (input) => {
251
- assertTmux();
252
- const planPath = input['plan_path'];
253
- const cwd = typeof input['cwd'] === 'string' ? input['cwd'] : process.cwd();
254
- const name = input['name'];
255
- if (!existsSync(planPath)) {
256
- throw new InputError({
257
- error: 'not_found',
258
- message: `plan not found: ${planPath}`,
259
- field: 'plan_path',
260
- next: 'Provide an absolute path to an existing plan file.',
261
- });
464
+ const nameRaw = input['name'];
465
+ const scopeStr = input['scope'];
466
+ const noBody = input['noBody'] === true;
467
+ const resolveOpts = {};
468
+ if (scopeStr !== undefined) {
469
+ const resolved = resolveScopeArg(scopeStr);
470
+ if (resolved !== 'all')
471
+ resolveOpts.scope = resolved;
262
472
  }
263
- const { jobId } = createJob('implementer', { cwd });
264
- const result = spawnAndDetach({
265
- prompt: implementHandoffPrompt(planPath, jobId),
266
- cwd,
267
- jobId,
268
- placement: 'split-h',
269
- killAfterSeconds: DEFAULT_KILL_SECS,
270
- name,
271
- });
272
- if (result.status === 'not-in-tmux') {
273
- throw new InputError({ error: 'not_in_tmux', message: result.message, next: 'Check tmux is running and try again.' });
274
- }
275
- if (result.status === 'spawn-failed') {
276
- throw new InputError({ error: 'spawn_failed', message: result.message, next: 'Check tmux is running and try again.' });
277
- }
278
- const implPaneLabel = result.paneId !== undefined ? result.paneId : 'unknown';
279
- appendEvent(jobId, { level: 'info', event: 'worker_started', message: `implementer pane ${implPaneLabel} spawned` });
280
- return { job_id: jobId, follow_up: followUpResult(jobId) };
473
+ const sub = resolveSubagent(nameRaw, resolveOpts);
474
+ const out = {
475
+ id: subagentId(sub),
476
+ name: sub.name,
477
+ plugin: sub.plugin,
478
+ scope: sub.scope,
479
+ path: sub.path,
480
+ description: sub.frontmatter.description !== undefined ? sub.frontmatter.description : '',
481
+ model: sub.frontmatter.model !== undefined ? sub.frontmatter.model : null,
482
+ tools: sub.frontmatter.tools !== undefined ? sub.frontmatter.tools : null,
483
+ };
484
+ if (!noBody)
485
+ out['system_prompt'] = sub.systemPrompt;
486
+ return out;
281
487
  },
282
488
  });
283
- // ---------------------------------------------------------------------------
284
- // agent new reviewer
285
- // ---------------------------------------------------------------------------
286
- const newReviewer = defineLeaf({
287
- name: 'reviewer',
489
+ const SUBAGENT_STUB = (name, description) => `---\nname: ${name}\ndescription: ${description}\n# model: claude-sonnet-4-5 # optional: model pattern/id passed via --model\n# tools: read, grep, find, ls, bash # optional (pi): tool allow-list passed via --tools\n---\n\nYou are ${name}. Describe the persona, responsibilities, and output format here.\nThis markdown body is applied as the spawned agent's appended system prompt.\n`;
490
+ const subagentScaffold = defineLeaf({
491
+ name: 'scaffold',
288
492
  help: {
289
- name: 'agent new reviewer',
290
- summary: 'launch a reviewer agent for a plan or spec artifact; the originating pane stays alive to collect the verdict',
493
+ name: 'agent subagent scaffold',
494
+ summary: 'create a subagent definition stub (markdown + frontmatter) under <scope>/agents',
291
495
  params: [
292
- { kind: 'positional', name: 'artifact_path', type: 'path', required: true, constraint: 'Absolute path to the artifact to review.' },
293
- { kind: 'flag', name: 'kind', type: 'enum', choices: ['plan', 'spec'], required: true, constraint: 'Artifact kind to review.' },
294
- { kind: 'flag', name: 'spec-path', type: 'path', required: false, constraint: 'Absolute path to the spec, for plan reviews. Omit for spec reviews.' },
295
- { kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Working directory. Defaults to process.cwd().' },
296
- { kind: 'flag', name: 'name', type: 'string', required: true, constraint: 'Display name passed to `claude -n`; surfaces in pane title and /resume picker.' },
496
+ { kind: 'positional', name: 'name', required: true, constraint: 'Subagent name; also the filename stem (<name>.md).' },
497
+ { kind: 'flag', name: 'description', type: 'string', required: false, constraint: 'Short description written to frontmatter. Required for the subagent to appear in listings.' },
498
+ { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
297
499
  ],
298
500
  output: [
299
- { name: 'job_id', type: 'string', required: true, constraint: 'Use with `crtr job read *` and `crtr job cancel`.' },
300
- { name: 'follow_up', type: 'string', required: true, constraint: 'Recommended next call.' },
501
+ { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the scaffolded .md file.' },
502
+ { name: 'id', type: 'string', required: true, constraint: 'Resolved subagent id.' },
503
+ { name: 'follow_up', type: 'string', required: true, constraint: 'Next step to edit and use the subagent.' },
301
504
  ],
302
505
  outputKind: 'object',
303
506
  effects: [
304
- 'Spawns a reviewer agent in a sibling tmux pane.',
305
- 'The originating pane stays alive — wait on the result and act on the verdict.',
306
- 'Creates a job entry and result sidecar.',
507
+ 'Creates `<scope-root>/agents/<name>.md` with a frontmatter + body stub.',
508
+ 'Fails if the file already exists.',
307
509
  ],
308
510
  },
309
511
  run: async (input) => {
310
- assertTmux();
311
- const artifactPath = input['artifact_path'];
312
- const artifactKind = input['kind'];
313
- const specPath = typeof input['specPath'] === 'string' ? input['specPath'] : undefined;
314
- const cwd = typeof input['cwd'] === 'string' ? input['cwd'] : process.cwd();
315
- const name = input['name'];
316
- if (!existsSync(artifactPath)) {
317
- throw new InputError({
318
- error: 'not_found',
319
- message: `artifact not found: ${artifactPath}`,
320
- field: 'artifact_path',
321
- next: 'Provide an absolute path to an existing artifact file.',
322
- });
512
+ const name = input['name'].trim();
513
+ if (name === '' || name.includes('/')) {
514
+ throw usage('subagent name must be a non-empty single segment (no slashes)');
323
515
  }
324
- const { jobId } = createJob('reviewer', { cwd });
325
- // The reviewer is a subordinate the caller waits on (verdict → revise or
326
- // hand off), NOT a handoff successor. Use spawnAgent so the originating
327
- // pane (planner/orchestrator) stays alive to collect the result; do not
328
- // self-kill the caller the way planner/implementer handoffs do.
329
- const result = spawnAgent({
330
- prompt: reviewerHandoffPrompt(artifactPath, artifactKind, specPath !== undefined ? specPath : null, jobId),
331
- cwd,
332
- jobId,
333
- maxPanesPerWindow: resolveMaxPanes(),
334
- name,
335
- });
336
- if (result.status === 'not-in-tmux') {
337
- throw new InputError({ error: 'not_in_tmux', message: result.message, next: 'Run inside a tmux session.' });
516
+ const description = typeof input['description'] === 'string' ? input['description'] : '';
517
+ const scopeStr = input['scope'];
518
+ let scope;
519
+ if (scopeStr !== undefined) {
520
+ const resolved = resolveScopeArg(scopeStr);
521
+ if (resolved === 'all')
522
+ throw usage('scope must be user or project, not all');
523
+ scope = resolved;
338
524
  }
339
- if (result.status === 'spawn-failed') {
340
- throw new InputError({ error: 'spawn_failed', message: result.message, next: 'Check tmux is running and try again.' });
525
+ else {
526
+ scope = projectScopeRoot() !== null ? 'project' : 'user';
341
527
  }
342
- const reviewerPaneLabel = result.paneId !== undefined ? result.paneId : 'unknown';
343
- appendEvent(jobId, { level: 'info', event: 'worker_started', message: `reviewer pane ${reviewerPaneLabel} spawned` });
344
- return { job_id: jobId, follow_up: followUpResult(jobId) };
528
+ const scopeRootPath = requireScopeRoot(scope);
529
+ ensureScopeInitialized(scope, scopeRootPath);
530
+ const dir = scopeAgentsDir(scope);
531
+ if (dir === null)
532
+ throw general(`no agents dir for scope ${scope}`);
533
+ const filePath = join(dir, `${name}.md`);
534
+ if (pathExists(filePath))
535
+ throw general(`subagent already exists: ${filePath}`);
536
+ ensureDir(dir);
537
+ writeFileSync(filePath, SUBAGENT_STUB(name, description), 'utf8');
538
+ return {
539
+ path: filePath,
540
+ id: name,
541
+ follow_up: `Edit ${filePath}, then delegate with \`crtr agent new --agent ${name}\`.`,
542
+ };
345
543
  },
346
544
  });
347
- // ---------------------------------------------------------------------------
348
- // agent new (branch)
349
- // ---------------------------------------------------------------------------
350
- const newBranch = defineBranch({
351
- name: 'new',
545
+ const subagentBranch = defineBranch({
546
+ name: 'subagent',
352
547
  help: {
353
- name: 'agent new',
354
- summary: 'spawn agent workers; all return a job handle immediately',
548
+ name: 'agent subagent',
549
+ summary: 'define and inspect reusable subagent personas (markdown + frontmatter)',
550
+ model: 'A subagent is a markdown file with YAML frontmatter (name, description, optional model/tools) whose body becomes a spawned worker\'s appended system prompt — the same model as the pi subagent extension, surfaced through crtr. Files live under `<scope-root>/agents/*.md` (and plugins\' `agents/`). `list` enumerates them, `read` loads one\'s body + metadata, `scaffold` creates a stub. Spawn one with `crtr agent new --agent <name>`.',
551
+ dynamicState: buildSubagentCatalog,
355
552
  children: [
356
- { name: 'prompt', desc: 'fresh agent with a prompt', useWhen: 'spawning a general-purpose agent' },
357
- { name: 'fork', desc: 'fork current session into a sibling pane', useWhen: 'branching the current session\'s context into a new agent' },
358
- { name: 'planner', desc: 'planning agent for a spec', useWhen: 'handing off spec → plan decomposition' },
359
- { name: 'implementer', desc: 'implementation agent for a plan', useWhen: 'handing off plan → code implementation' },
360
- { name: 'reviewer', desc: 'review agent for a plan or spec', useWhen: 'launching a review of a plan or spec artifact' },
553
+ { name: 'list', desc: 'list defined subagents', useWhen: 'discovering which personas are available' },
554
+ { name: 'read', desc: 'load a subagent\'s system prompt + metadata', useWhen: 'inspecting a persona before using or editing it' },
555
+ { name: 'scaffold', desc: 'create a subagent stub under <scope>/agents', useWhen: 'defining a new subagent' },
361
556
  ],
362
557
  },
363
- children: [newPrompt, newFork, newPlanner, newImplementer, newReviewer],
558
+ children: [subagentList, subagentRead, subagentScaffold],
364
559
  });
365
560
  // ---------------------------------------------------------------------------
366
561
  // agent (root umbrella)
@@ -368,17 +563,23 @@ const newBranch = defineBranch({
368
563
  export function registerAgent() {
369
564
  return defineBranch({
370
565
  name: 'agent',
566
+ rootEntry: {
567
+ concept: 'workers you spawn to offload work to a fresh context. `agent new` runs an agent on any task and hands back just the conclusion; results collected by job handle',
568
+ desc: 'spawn agent workers and manage subagent personas',
569
+ useWhen: 'almost any self-contained task — reach for `agent new` OFTEN, and earlier than feels necessary, to keep work off your own context. Two habits pay off most: (1) before working in unfamiliar code, send a quick recon agent to map it first — a fast, cheap model (e.g. haiku) handles "where does X live / how does Y work" fine and keeps the digging out of your context; (2) when subtasks are independent, fan them out as parallel workers instead of doing them in series. Each worker runs in a fresh context and hands back only its conclusion. Blocking by default (returns inline like a function call); inside tmux the worker runs in an unfocused pane of the dedicated subagent session — press Alt-o to watch or steer it. --background fans out without waiting. Select a persona with `--agent <id>` (see `agent subagent`). Only trivial one-shot lookups are worth doing inline.',
570
+ dynamicState: buildSubagentCatalog,
571
+ },
371
572
  help: {
372
573
  name: 'agent',
373
- summary: 'agentic workflows: spec, plan, debug, and spawning agent workers',
374
- model: 'spec captures requirements; plan decomposes them; debug root-causes failures reproduce-first; new spawns the worker that executes the next phase. Spawned workers register as jobs — monitor and collect at `crtr job`.',
574
+ summary: 'spawn agent workers and manage subagent personas',
575
+ model: '`agent new` spawns a worker (general-purpose by default, or a defined persona via `--agent <id>`); `agent fork` carries the current session context to a new pane; `agent subagent` manages persona definitions. Spawned workers register as jobs — monitor and collect at `crtr job`. Spec, plan, and debug workflows live under `crtr mode`.',
576
+ dynamicState: buildSubagentCatalog,
375
577
  children: [
376
- { name: 'spec', desc: 'create, read, list specifications', useWhen: 'capturing requirements before planning' },
377
- { name: 'plan', desc: 'create, read, list plans', useWhen: 'shaping or inspecting work' },
378
- { name: 'debug', desc: 'reproduce-first root-cause workflow', useWhen: 'a bug, test failure, or unexpected behavior needs root-causing' },
379
- { name: 'new', desc: 'spawn agent workers (prompt, fork, planner, implementer, reviewer)', useWhen: 'launching a new agent worker' },
578
+ { name: 'new', desc: 'spawn a worker — general-purpose by default, or a defined subagent via --agent', useWhen: 'delegating any self-contained task (the main spawn command)' },
579
+ { name: 'fork', desc: 'fork current session into a sibling pane', useWhen: 'branching the current session\'s context into a new agent' },
580
+ { name: 'subagent', desc: 'define and inspect reusable subagent personas', useWhen: 'managing markdown subagent definitions or seeing what personas exist' },
380
581
  ],
381
582
  },
382
- children: [registerSpec(), registerPlan(), registerDebug(), newBranch],
583
+ children: [newPrompt, newFork, subagentBranch],
383
584
  });
384
585
  }