@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 +2 -2
- package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +3 -3
- package/dist/cli.js +6 -6
- package/dist/commands/__tests__/skill.test.js +24 -28
- package/dist/commands/{flow.d.ts → agent.d.ts} +1 -1
- package/dist/commands/agent.js +384 -0
- package/dist/commands/debug.d.ts +1 -1
- package/dist/commands/debug.js +7 -7
- package/dist/commands/job.js +54 -379
- package/dist/commands/plan.d.ts +1 -1
- package/dist/commands/plan.js +11 -11
- package/dist/commands/skill.js +114 -107
- package/dist/commands/spec.d.ts +1 -1
- package/dist/commands/spec.js +11 -11
- package/dist/core/__tests__/job.test.js +38 -74
- package/dist/core/__tests__/jobs.test.d.ts +1 -0
- package/dist/core/__tests__/jobs.test.js +66 -0
- package/dist/core/__tests__/resolver.test.d.ts +1 -0
- package/dist/core/__tests__/resolver.test.js +113 -0
- package/dist/core/config.js +20 -2
- package/dist/core/jobs.d.ts +26 -12
- package/dist/core/jobs.js +151 -42
- package/dist/core/resolver.d.ts +1 -2
- package/dist/core/resolver.js +60 -46
- package/dist/core/spawn.d.ts +26 -3
- package/dist/core/spawn.js +144 -11
- package/dist/prompts/agent.d.ts +3 -3
- package/dist/prompts/agent.js +20 -18
- package/dist/prompts/debug.js +14 -7
- package/dist/prompts/skill.js +16 -16
- package/dist/types.d.ts +1 -1
- package/dist/types.js +2 -2
- package/package.json +2 -2
- package/dist/commands/flow.js +0 -24
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
|
|
23
|
-
crtr
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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
|
|
142
|
+
// skill read
|
|
143
143
|
// ---------------------------------------------------------------------------
|
|
144
|
-
describe('skill read
|
|
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
|
|
243
|
-
assert.equal(r['qualifier'], 'myplugin
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
291
|
-
const r = await parseArgv(params, ['myplugin
|
|
292
|
-
assert.equal(r['name'], 'myplugin
|
|
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
|
|
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
|
+
}
|
package/dist/commands/debug.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare const FLOW_DEBUG_GUIDE = "## Debug workflow \u2014 reproduce first\n\nAudience: the agent that ran `crtr
|
|
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;
|
package/dist/commands/debug.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
// `crtr
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: '
|
|
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
|
|
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
|
});
|