@crouton-kit/crouter 0.3.3 → 0.3.8

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/README.md CHANGED
@@ -19,8 +19,8 @@ crtr --help
19
19
  Browse and install plugins from it:
20
20
 
21
21
  ```bash
22
- crtr marketplace browse crouter-official-marketplace
23
- crtr marketplace install crouter-official-marketplace:<plugin>
22
+ crtr pkg market inspect browse --marketplace crouter-official-marketplace
23
+ crtr pkg market manage install --marketplace crouter-official-marketplace --plugin <plugin>
24
24
  ```
25
25
 
26
26
  To opt out of the bootstrap (e.g. in CI), set `CRTR_NO_BOOTSTRAP=1`.
@@ -107,7 +107,7 @@ mkdir -p plugins/my-new-plugin/.crouter-plugin plugins/my-new-plugin/skills
107
107
  $EDITOR plugins/my-new-plugin/.crouter-plugin/plugin.json
108
108
 
109
109
  # Add at least one skill
110
- crtr skill author scaffold my-new-plugin:first-skill --type playbook --description "Use when …"
110
+ crtr skill author scaffold my-new-plugin/first-skill --type playbook --description "Use when …"
111
111
 
112
112
  # Add the plugin to the marketplace index
113
113
  $EDITOR .crouter-marketplace/marketplace.json
@@ -96,7 +96,7 @@ Three ways a plugin lands in a scope:
96
96
  mkdir -p my-plugin/.crouter-plugin my-plugin/skills
97
97
  $EDITOR my-plugin/.crouter-plugin/plugin.json # write the manifest
98
98
  cd my-plugin
99
- crtr skill author scaffold my-plugin:my-first-skill --type playbook --description "Use when …"
99
+ crtr skill author scaffold my-plugin/my-first-skill --type playbook --description "Use when …"
100
100
 
101
101
  # Symlink for fast iteration — no clone, edits land immediately
102
102
  ln -s $(pwd) ~/.crouter/plugins/my-plugin
@@ -124,9 +124,9 @@ Standard semver:
124
124
 
125
125
  ## Enable/disable
126
126
 
127
- `crtr pkg plugin manage disable <name>` flips the per-scope config without removing files. Disabled plugins are hidden from `crtr skill find list` and don't resolve via `crtr skill read show <name>`. Re-enable with `crtr pkg plugin manage enable <name>`.
127
+ `crtr pkg plugin manage disable <name>` flips the per-scope config without removing files. Disabled plugins are hidden from `crtr skill find list` and don't resolve via `crtr skill read <name>`. Re-enable with `crtr pkg plugin manage enable <name>`.
128
128
 
129
- Individual skills inside an enabled plugin can also be disabled: `crtr skill state disable <plugin>:<skill>`.
129
+ Individual skills inside an enabled plugin can also be disabled: `crtr skill state disable <plugin>/<skill>`.
130
130
 
131
131
  ## What goes in a plugin
132
132
 
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { defineRoot, runCli } from './core/command.js';
3
- import { registerFlow } from './commands/flow.js';
3
+ import { registerAgent } from './commands/agent.js';
4
4
  import { registerSkill } from './commands/skill.js';
5
5
  import { registerPkg } from './commands/pkg.js';
6
6
  import { registerJob } from './commands/job.js';
@@ -12,18 +12,18 @@ const root = defineRoot({
12
12
  help: {
13
13
  tagline: 'crtr: agentic planning runtime.',
14
14
  concepts: [
15
- { name: 'flow', desc: 'spec → plan → debug: the development process' },
15
+ { name: 'agent', desc: 'agentic workflows: spec → plan → debug, and spawning workers' },
16
16
  { name: 'skill', desc: 'loadable SKILL.md document an agent reads to adopt a workflow' },
17
17
  { name: 'pkg', desc: 'plugins and marketplaces that supply skills' },
18
- { name: 'job', desc: 'a running agent worker and its logs and result' },
18
+ { name: 'job', desc: 'producer-agnostic record of any ongoing task — its logs and result' },
19
19
  { name: 'human', desc: 'human-in-the-loop decisions, document review, and live display' },
20
20
  { name: 'sys', desc: 'crtr configuration, diagnostics, and self-management' },
21
21
  ],
22
22
  subtrees: [
23
- { name: 'flow', desc: 'spec, plan, and debug workflows', useWhen: 'capturing requirements, planning work, or root-causing a bug' },
23
+ { name: 'agent', desc: 'spec/plan/debug and spawn workers', useWhen: 'capturing requirements, planning, debugging, or launching an agent worker' },
24
24
  { name: 'skill', desc: 'discover, read, author, and manage skills', useWhen: 'working with SKILL.md documents' },
25
25
  { name: 'pkg', desc: 'manage plugins and marketplaces', useWhen: 'installing or browsing skill collections' },
26
- { name: 'job', desc: 'spawn, monitor, and collect from agent workers', useWhen: 'running or watching agent jobs' },
26
+ { name: 'job', desc: 'monitor and collect from any ongoing task', useWhen: 'reading status, logs, or result of a job started by any producer' },
27
27
  { name: 'human', desc: 'ask, approve, review, notify, show, inbox, list', useWhen: 'putting a decision or document in front of a person' },
28
28
  { name: 'sys', desc: 'config, doctor, update, version', useWhen: 'managing the crtr installation' },
29
29
  ],
@@ -32,7 +32,7 @@ const root = defineRoot({
32
32
  ],
33
33
  },
34
34
  subtrees: [
35
- registerFlow(),
35
+ registerAgent(),
36
36
  registerSkill(),
37
37
  registerPkg(),
38
38
  registerJob(),
@@ -139,14 +139,15 @@ describe('skill find grep params', () => {
139
139
  });
140
140
  });
141
141
  // ---------------------------------------------------------------------------
142
- // skill read show
142
+ // skill read
143
143
  // ---------------------------------------------------------------------------
144
- describe('skill read show params', () => {
144
+ describe('skill read params', () => {
145
145
  const params = [
146
146
  { kind: 'positional', name: 'name', required: true, constraint: '' },
147
147
  scopeWriteFlag,
148
148
  pluginFlag,
149
149
  { kind: 'flag', name: 'frontmatter', type: 'bool', required: false, constraint: '' },
150
+ { kind: 'flag', name: 'no-body', type: 'bool', required: false, constraint: '' },
150
151
  ];
151
152
  test('name positional required', async () => {
152
153
  await assert.rejects(() => parseArgv(params, []), (e) => { assert.match(e.message, /required parameter is missing/); return true; });
@@ -155,6 +156,10 @@ describe('skill read show params', () => {
155
156
  const r = await parseArgv(params, ['my-skill']);
156
157
  assert.equal(r['name'], 'my-skill');
157
158
  });
159
+ test('nested name parsed correctly', async () => {
160
+ const r = await parseArgv(params, ['some/nested/skill']);
161
+ assert.equal(r['name'], 'some/nested/skill');
162
+ });
158
163
  test('--frontmatter presence = true', async () => {
159
164
  const r = await parseArgv(params, ['my-skill', '--frontmatter']);
160
165
  assert.equal(r['frontmatter'], true);
@@ -163,6 +168,14 @@ describe('skill read show params', () => {
163
168
  const r = await parseArgv(params, ['my-skill']);
164
169
  assert.equal(r['frontmatter'], false);
165
170
  });
171
+ test('--no-body presence = true (kebab to camelCase key)', async () => {
172
+ const r = await parseArgv(params, ['my-skill', '--no-body']);
173
+ assert.equal(r['noBody'], true);
174
+ });
175
+ test('--no-body absent = false', async () => {
176
+ const r = await parseArgv(params, ['my-skill']);
177
+ assert.equal(r['noBody'], false);
178
+ });
166
179
  test('--scope rejects all', async () => {
167
180
  await assert.rejects(() => parseArgv(params, ['my-skill', '--scope', 'all']), (e) => { assert.match(e.message, /must be one of/); return true; });
168
181
  });
@@ -170,23 +183,6 @@ describe('skill read show params', () => {
170
183
  const r = await parseArgv(params, ['my-skill', '--scope', 'user']);
171
184
  assert.equal(r['scope'], 'user');
172
185
  });
173
- });
174
- // ---------------------------------------------------------------------------
175
- // skill read where
176
- // ---------------------------------------------------------------------------
177
- describe('skill read where params', () => {
178
- const params = [
179
- { kind: 'positional', name: 'name', required: true, constraint: '' },
180
- scopeWriteFlag,
181
- pluginFlag,
182
- ];
183
- test('name positional required', async () => {
184
- await assert.rejects(() => parseArgv(params, []), (e) => { assert.match(e.message, /required parameter is missing/); return true; });
185
- });
186
- test('name parsed correctly', async () => {
187
- const r = await parseArgv(params, ['some/nested/skill']);
188
- assert.equal(r['name'], 'some/nested/skill');
189
- });
190
186
  test('--scope project valid', async () => {
191
187
  const r = await parseArgv(params, ['skillname', '--scope', 'project']);
192
188
  assert.equal(r['scope'], 'project');
@@ -239,26 +235,26 @@ describe('skill author scaffold params', () => {
239
235
  await assert.rejects(() => parseArgv(params, []), (e) => { assert.match(e.message, /required parameter is missing/); return true; });
240
236
  });
241
237
  test('qualifier parsed correctly', async () => {
242
- const r = await parseArgv(params, ['myplugin:myskill']);
243
- assert.equal(r['qualifier'], 'myplugin:myskill');
238
+ const r = await parseArgv(params, ['myplugin/myskill']);
239
+ assert.equal(r['qualifier'], 'myplugin/myskill');
244
240
  });
245
241
  test('full invocation', async () => {
246
242
  const r = await parseArgv(params, [
247
- 'myplugin:myskill',
243
+ 'myplugin/myskill',
248
244
  '--type', 'playbook',
249
245
  '--scope', 'project',
250
246
  '--description', 'Use when debugging',
251
247
  ]);
252
- assert.equal(r['qualifier'], 'myplugin:myskill');
248
+ assert.equal(r['qualifier'], 'myplugin/myskill');
253
249
  assert.equal(r['type'], 'playbook');
254
250
  assert.equal(r['scope'], 'project');
255
251
  assert.equal(r['description'], 'Use when debugging');
256
252
  });
257
253
  test('--type invalid rejects', async () => {
258
- await assert.rejects(() => parseArgv(params, ['q:s', '--type', 'invalid']), (e) => { assert.match(e.message, /must be one of/); return true; });
254
+ await assert.rejects(() => parseArgv(params, ['q/s', '--type', 'invalid']), (e) => { assert.match(e.message, /must be one of/); return true; });
259
255
  });
260
256
  test('--scope all rejects', async () => {
261
- await assert.rejects(() => parseArgv(params, ['q:s', '--scope', 'all']), (e) => { assert.match(e.message, /must be one of/); return true; });
257
+ await assert.rejects(() => parseArgv(params, ['q/s', '--scope', 'all']), (e) => { assert.match(e.message, /must be one of/); return true; });
262
258
  });
263
259
  });
264
260
  // ---------------------------------------------------------------------------
@@ -287,8 +283,8 @@ describe('skill state enable/disable params', () => {
287
283
  test('--scope all rejects', async () => {
288
284
  await assert.rejects(() => parseArgv(params, ['my-skill', '--scope', 'all']), (e) => { assert.match(e.message, /must be one of/); return true; });
289
285
  });
290
- test('plugin:skill qualifier as name', async () => {
291
- const r = await parseArgv(params, ['myplugin:myskill']);
292
- assert.equal(r['name'], 'myplugin:myskill');
286
+ test('plugin/skill qualifier as name', async () => {
287
+ const r = await parseArgv(params, ['myplugin/myskill']);
288
+ assert.equal(r['name'], 'myplugin/myskill');
293
289
  });
294
290
  });
@@ -1,2 +1,2 @@
1
1
  import type { BranchDef } from '../core/command.js';
2
- export declare function registerFlow(): BranchDef;
2
+ export declare function registerAgent(): BranchDef;
@@ -0,0 +1,384 @@
1
+ // `crtr agent` umbrella — agentic workflows: spec/plan/debug + spawn primitives.
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.
7
+ //
8
+ // 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.
13
+ import { defineBranch, defineLeaf } from '../core/command.js';
14
+ import { InputError } from '../core/io.js';
15
+ import { createJob, appendEvent } from '../core/jobs.js';
16
+ import { spawnAgent, spawnAndDetach, isInTmux } from '../core/spawn.js';
17
+ 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`;
26
+ }
27
+ function resolveMaxPanes() {
28
+ const cfg = readConfig('user');
29
+ return cfg.max_panes_per_window;
30
+ }
31
+ function assertTmux() {
32
+ if (!isInTmux()) {
33
+ throw new InputError({
34
+ error: 'not_in_tmux',
35
+ message: 'crtr agent new requires tmux (TMUX env var not set).',
36
+ next: 'Run inside a tmux session.',
37
+ });
38
+ }
39
+ }
40
+ // ---------------------------------------------------------------------------
41
+ // agent new prompt
42
+ // ---------------------------------------------------------------------------
43
+ const newPrompt = defineLeaf({
44
+ name: 'prompt',
45
+ help: {
46
+ name: 'agent new prompt',
47
+ summary: 'spawn a fresh Claude agent with a prompt; returns a job handle immediately',
48
+ params: [
49
+ { kind: 'stdin', name: 'prompt', required: true, constraint: 'Prompt text sent to the spawned agent.' },
50
+ { 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.' },
52
+ ],
53
+ output: [
54
+ { 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.' },
56
+ ],
57
+ outputKind: 'object',
58
+ 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.',
62
+ ],
63
+ },
64
+ run: async (input) => {
65
+ assertTmux();
66
+ const prompt = input['prompt'];
67
+ 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.' });
92
+ }
93
+ if (result.status === 'spawn-failed') {
94
+ throw new InputError({ error: 'spawn_failed', message: result.message, next: 'Check tmux is running and try again.' });
95
+ }
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) };
99
+ },
100
+ });
101
+ // ---------------------------------------------------------------------------
102
+ // agent new fork
103
+ // ---------------------------------------------------------------------------
104
+ const newFork = defineLeaf({
105
+ name: 'fork',
106
+ help: {
107
+ name: 'agent new fork',
108
+ summary: 'fork the current Claude session into a sibling pane; returns a job handle immediately',
109
+ params: [
110
+ { 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.' },
112
+ ],
113
+ output: [
114
+ { 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.' },
116
+ ],
117
+ outputKind: 'object',
118
+ 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`.',
122
+ ],
123
+ },
124
+ run: async (input) => {
125
+ assertTmux();
126
+ const parentSessionId = process.env['CLAUDE_CODE_SESSION_ID'];
127
+ if (parentSessionId === undefined || parentSessionId === '') {
128
+ throw new InputError({
129
+ error: 'missing_session_id',
130
+ message: 'crtr agent new fork requires $CLAUDE_CODE_SESSION_ID — must run inside Claude Code.',
131
+ next: 'Run this command from within a Claude Code session.',
132
+ });
133
+ }
134
+ const cwd = typeof input['cwd'] === 'string' ? input['cwd'] : process.cwd();
135
+ const name = input['name'];
136
+ 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
+ const result = spawnAgent({
151
+ prompt: promptWithSubmit,
152
+ cwd,
153
+ jobId,
154
+ fork: { sessionId: parentSessionId },
155
+ maxPanesPerWindow: resolveMaxPanes(),
156
+ name,
157
+ });
158
+ if (result.status === 'not-in-tmux') {
159
+ throw new InputError({ error: 'not_in_tmux', message: result.message, next: 'Run inside a tmux session.' });
160
+ }
161
+ if (result.status === 'spawn-failed') {
162
+ throw new InputError({ error: 'spawn_failed', message: result.message, next: 'Check tmux is running and try again.' });
163
+ }
164
+ const forkPaneLabel = result.paneId !== undefined ? result.paneId : 'unknown';
165
+ appendEvent(jobId, { level: 'info', event: 'worker_started', message: `forked pane ${forkPaneLabel} spawned` });
166
+ return { job_id: jobId, follow_up: followUpResult(jobId) };
167
+ },
168
+ });
169
+ // ---------------------------------------------------------------------------
170
+ // agent new planner
171
+ // ---------------------------------------------------------------------------
172
+ const newPlanner = defineLeaf({
173
+ name: 'planner',
174
+ help: {
175
+ name: 'agent new planner',
176
+ summary: 'launch a planning agent for an approved spec; closes the originating pane after handoff',
177
+ 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.' },
181
+ ],
182
+ 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.' },
185
+ ],
186
+ 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
+ ],
192
+ },
193
+ 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
+ });
205
+ }
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) };
224
+ },
225
+ });
226
+ // ---------------------------------------------------------------------------
227
+ // agent new implementer
228
+ // ---------------------------------------------------------------------------
229
+ const newImplementer = defineLeaf({
230
+ name: 'implementer',
231
+ help: {
232
+ name: 'agent new implementer',
233
+ summary: 'launch an implementation agent for an approved plan; closes the originating pane after handoff',
234
+ 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.' },
238
+ ],
239
+ 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.' },
242
+ ],
243
+ 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
+ ],
249
+ },
250
+ 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
+ });
262
+ }
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) };
281
+ },
282
+ });
283
+ // ---------------------------------------------------------------------------
284
+ // agent new reviewer
285
+ // ---------------------------------------------------------------------------
286
+ const newReviewer = defineLeaf({
287
+ name: 'reviewer',
288
+ 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',
291
+ 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.' },
297
+ ],
298
+ 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.' },
301
+ ],
302
+ outputKind: 'object',
303
+ 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.',
307
+ ],
308
+ },
309
+ 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
+ });
323
+ }
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.' });
338
+ }
339
+ if (result.status === 'spawn-failed') {
340
+ throw new InputError({ error: 'spawn_failed', message: result.message, next: 'Check tmux is running and try again.' });
341
+ }
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) };
345
+ },
346
+ });
347
+ // ---------------------------------------------------------------------------
348
+ // agent new (branch)
349
+ // ---------------------------------------------------------------------------
350
+ const newBranch = defineBranch({
351
+ name: 'new',
352
+ help: {
353
+ name: 'agent new',
354
+ summary: 'spawn agent workers; all return a job handle immediately',
355
+ 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' },
361
+ ],
362
+ },
363
+ children: [newPrompt, newFork, newPlanner, newImplementer, newReviewer],
364
+ });
365
+ // ---------------------------------------------------------------------------
366
+ // agent (root umbrella)
367
+ // ---------------------------------------------------------------------------
368
+ export function registerAgent() {
369
+ return defineBranch({
370
+ name: 'agent',
371
+ help: {
372
+ 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`.',
375
+ 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' },
380
+ ],
381
+ },
382
+ children: [registerSpec(), registerPlan(), registerDebug(), newBranch],
383
+ });
384
+ }
@@ -1,3 +1,3 @@
1
- export declare const FLOW_DEBUG_GUIDE = "## Debug workflow \u2014 reproduce first\n\nAudience: the agent that ran `crtr flow debug`. A reproduction agent is\nalready spawned in a sibling pane. It writes ONE failing integration test and\nnever fixes anything. You do everything after: gate on the repro, root-cause,\nfix, verify against that same test.\n\n### Phase 0: Await the repro agent\n\nRun `crtr job read result <job_id> --wait` (10-min budget).\nOn status:\"timeout\": re-issue the wait, or run `crtr job read logs <job_id> --follow`\nuntil the job is terminal.\n\n### Phase 1: Gate on reproduction\n\n`reproduces:true`: read `test_path`, run `test_command` YOURSELF, confirm\nit fails for the stated reason. Do not trust the agent's claim \u2014 if it passes\nor fails differently, treat repro as NOT achieved. This test is the regression\ngate; it stays in the suite after the fix.\n`status:\"failed\"` / `reproduces:false` / your run disproves it: no repro\nharness. Continue, but record \"no reproduction \u2014 fix unverified; do not claim\nverified-fixed.\"\n\n### Phase 2: Reconnaissance\n\nRead the key files yourself \u2014 entry point, failure point, the data flow\nbetween. `git log` / `git blame` near the failure: recent changes are\nhigh-signal.\n\n### Phase 3: Assess difficulty, scale investigators\n\nSimple \u2192 solo (Explore subagents for tracing if the area is large).\nMedium \u2192 2\u20133 parallel `devcore:senior-advisor`: data-flow tracer, assumption\nauditor, change investigator.\nHard (intermittent, races, \"been stuck\", many modules) \u2192 3\u20135 parallel:\nend-to-end tracer, assumption breaker, git archaeologist, boundary inspector.\nGive investigators file paths, observed behavior, and concrete tasks \u2014 never\nyour hypotheses. Challenge theories against each other; the one that survives\ndisconfirmation wins.\n\n### Phase 4: Fix\n\nMinimal root-cause fix. No scope expansion, no drive-by refactor.\n\n### Phase 5: Verify\n\nRe-run `test_command`: it MUST now pass. Run the broader suite for\nregressions. If there was no repro test, state the fix is unverified by\nreproduction and recommend explicit manual verification.\n\n### Phase 6: Report\n\nRoot cause (exact line + why), evidence, the now-passing repro test path,\nconfidence (High/Medium/Low; if not High, name what is uncertain).\n\n### Constraints\n\nThe repro test is the regression guard \u2014 it stays; a fix-agent must never\nweaken it. Investigators run in forked contexts; they return summaries, not\nraw output. No code changes during Phases 2\u20133 except the repro test.";
1
+ export declare const FLOW_DEBUG_GUIDE = "## Debug workflow \u2014 reproduce first\n\nAudience: the agent that ran `crtr agent debug`. A reproduction agent is\nalready spawned in a sibling pane. It writes ONE failing integration test and\nnever fixes anything. You do everything after: gate on the repro, root-cause,\nfix, verify against that same test.\n\n### Phase 0: Await the repro agent\n\nRun `crtr job read result <job_id> --wait` (10-min budget).\nOn status:\"timeout\": re-issue the wait, or run `crtr job read logs <job_id> --follow`\nuntil the job is terminal.\n\n### Phase 1: Gate on reproduction\n\n`reproduces:true`: read `test_path`, run `test_command` YOURSELF, confirm\nit fails for the stated reason. Do not trust the agent's claim \u2014 if it passes\nor fails differently, treat repro as NOT achieved. This test is the regression\ngate; it stays in the suite after the fix.\n`status:\"failed\"` / `reproduces:false` / your run disproves it: no repro\nharness. Continue, but record \"no reproduction \u2014 fix unverified; do not claim\nverified-fixed.\"\n\n### Phase 2: Reconnaissance\n\nRead the key files yourself \u2014 entry point, failure point, the data flow\nbetween. `git log` / `git blame` near the failure: recent changes are\nhigh-signal.\n\n### Phase 3: Assess difficulty, scale investigators\n\nSimple \u2192 solo (Explore subagents for tracing if the area is large).\nMedium \u2192 2\u20133 parallel `devcore:senior-advisor`: data-flow tracer, assumption\nauditor, change investigator.\nHard (intermittent, races, \"been stuck\", many modules) \u2192 3\u20135 parallel:\nend-to-end tracer, assumption breaker, git archaeologist, boundary inspector.\nGive investigators file paths, observed behavior, and concrete tasks \u2014 never\nyour hypotheses. Challenge theories against each other; the one that survives\ndisconfirmation wins.\n\n### Phase 4: Fix\n\nMinimal root-cause fix. No scope expansion, no drive-by refactor.\n\n### Phase 5: Verify\n\nRe-run `test_command`: it MUST now pass. Run the broader suite for\nregressions. If there was no repro test, state the fix is unverified by\nreproduction and recommend explicit manual verification.\n\n### Phase 6: Report\n\nRoot cause (exact line + why), evidence, the now-passing repro test path,\nconfidence (High/Medium/Low; if not High, name what is uncertain).\n\n### Constraints\n\nThe repro test is the regression guard \u2014 it stays; a fix-agent must never\nweaken it. Investigators run in forked contexts; they return summaries, not\nraw output. No code changes during Phases 2\u20133 except the repro test.";
2
2
  import type { LeafDef } from '../core/command.js';
3
3
  export declare function registerDebug(): LeafDef;
@@ -1,14 +1,14 @@
1
- // `crtr flow debug` leaf — reproduce-first root-cause workflow.
1
+ // `crtr agent debug` leaf — reproduce-first root-cause workflow.
2
2
  //
3
3
  // Running it spawns a reproduction-only agent in a sibling tmux pane (the same
4
- // spawn + job-handle shape as `crtr job start prompt`) and returns a job handle
4
+ // spawn + job-handle shape as `crtr agent new prompt`) and returns a job handle
5
5
  // plus a follow_up. The orchestrator-side methodology lives in FLOW_DEBUG_GUIDE
6
- // (the leaf's help.guide), loaded via `crtr flow debug -h` after the repro
6
+ // (the leaf's help.guide), loaded via `crtr agent debug -h` after the repro
7
7
  // agent returns. Methodology stays in the CLI guide field, like PLAN_NEW_GUIDE;
8
8
  // no builtin skill.
9
9
  export const FLOW_DEBUG_GUIDE = `## Debug workflow — reproduce first
10
10
 
11
- Audience: the agent that ran \`crtr flow debug\`. A reproduction agent is
11
+ Audience: the agent that ran \`crtr agent debug\`. A reproduction agent is
12
12
  already spawned in a sibling pane. It writes ONE failing integration test and
13
13
  never fixes anything. You do everything after: gate on the repro, root-cause,
14
14
  fix, verify against that same test.
@@ -82,7 +82,7 @@ function assertTmux() {
82
82
  if (!isInTmux()) {
83
83
  throw new InputError({
84
84
  error: 'not_in_tmux',
85
- message: 'crtr flow debug requires tmux (TMUX env var not set).',
85
+ message: 'crtr agent debug requires tmux (TMUX env var not set).',
86
86
  next: 'Run inside a tmux session.',
87
87
  });
88
88
  }
@@ -91,7 +91,7 @@ export function registerDebug() {
91
91
  return defineLeaf({
92
92
  name: 'debug',
93
93
  help: {
94
- name: 'flow debug',
94
+ name: 'agent debug',
95
95
  summary: 'reproduce-first root-cause workflow: spawns a reproduction agent, then you root-cause and fix',
96
96
  guide: FLOW_DEBUG_GUIDE,
97
97
  params: [
@@ -172,7 +172,7 @@ export function registerDebug() {
172
172
  });
173
173
  return {
174
174
  job_id: jobId,
175
- follow_up: `Await the reproduction agent: crtr job read result ${jobId} --wait. Then run \`crtr flow debug -h\` and follow the workflow from Phase 1.`,
175
+ follow_up: `Await the reproduction agent: crtr job read result ${jobId} --wait. Then run \`crtr agent debug -h\` and follow the workflow from Phase 1.`,
176
176
  };
177
177
  },
178
178
  });