@crouton-kit/crouter 0.2.6 → 0.3.2
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/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +9 -9
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +19 -19
- package/dist/cli.js +42 -37
- package/dist/commands/__tests__/human.test.d.ts +1 -0
- package/dist/commands/__tests__/human.test.js +214 -0
- package/dist/commands/__tests__/skill.test.d.ts +1 -0
- package/dist/commands/__tests__/skill.test.js +294 -0
- package/dist/commands/debug.d.ts +3 -0
- package/dist/commands/debug.js +179 -0
- package/dist/commands/flow.d.ts +2 -0
- package/dist/commands/flow.js +24 -0
- package/dist/commands/human.d.ts +2 -0
- package/dist/commands/human.js +480 -0
- package/dist/commands/job.d.ts +2 -0
- package/dist/commands/job.js +669 -0
- package/dist/commands/pkg.d.ts +2 -0
- package/dist/commands/pkg.js +1021 -0
- package/dist/commands/plan.d.ts +4 -2
- package/dist/commands/plan.js +306 -22
- package/dist/commands/skill.d.ts +2 -2
- package/dist/commands/skill.js +613 -456
- package/dist/commands/spec.d.ts +3 -2
- package/dist/commands/spec.js +283 -10
- package/dist/commands/sys.d.ts +2 -0
- package/dist/commands/sys.js +712 -0
- package/dist/core/__tests__/argv-parser.test.d.ts +1 -0
- package/dist/core/__tests__/argv-parser.test.js +199 -0
- package/dist/core/__tests__/flow-leaves.test.d.ts +1 -0
- package/dist/core/__tests__/flow-leaves.test.js +248 -0
- package/dist/core/__tests__/job.test.d.ts +1 -0
- package/dist/core/__tests__/job.test.js +346 -0
- package/dist/core/__tests__/pkg.test.d.ts +1 -0
- package/dist/core/__tests__/pkg.test.js +218 -0
- package/dist/core/__tests__/sys.test.d.ts +1 -0
- package/dist/core/__tests__/sys.test.js +208 -0
- package/dist/core/artifact.d.ts +29 -18
- package/dist/core/artifact.js +78 -221
- package/dist/core/auto-update.js +11 -4
- package/dist/core/command.d.ts +36 -0
- package/dist/core/command.js +287 -0
- package/dist/core/errors.d.ts +3 -0
- package/dist/core/errors.js +5 -0
- package/dist/core/fs-utils.d.ts +1 -0
- package/dist/core/fs-utils.js +4 -0
- package/dist/core/help.d.ts +98 -0
- package/dist/core/help.js +163 -0
- package/dist/core/io.d.ts +29 -0
- package/dist/core/io.js +83 -0
- package/dist/core/jobs.d.ts +87 -0
- package/dist/core/jobs.js +353 -0
- package/dist/core/pagination.d.ts +33 -0
- package/dist/core/pagination.js +89 -0
- package/dist/core/self-update.d.ts +21 -0
- package/dist/core/self-update.js +105 -0
- package/dist/core/spawn.d.ts +47 -65
- package/dist/core/spawn.js +78 -228
- package/dist/prompts/agent.d.ts +10 -5
- package/dist/prompts/agent.js +51 -74
- package/dist/prompts/debug.d.ts +8 -0
- package/dist/prompts/debug.js +37 -0
- package/dist/prompts/review.js +4 -11
- package/dist/prompts/skill.d.ts +0 -1
- package/dist/prompts/skill.js +95 -149
- package/package.json +4 -2
- package/dist/commands/agent.d.ts +0 -2
- package/dist/commands/agent.js +0 -265
- package/dist/commands/config.d.ts +0 -2
- package/dist/commands/config.js +0 -146
- package/dist/commands/doctor.d.ts +0 -2
- package/dist/commands/doctor.js +0 -268
- package/dist/commands/marketplace.d.ts +0 -2
- package/dist/commands/marketplace.js +0 -365
- package/dist/commands/plugin.d.ts +0 -2
- package/dist/commands/plugin.js +0 -367
- package/dist/commands/update.d.ts +0 -4
- package/dist/commands/update.js +0 -140
- package/dist/prompts/plan.d.ts +0 -1
- package/dist/prompts/plan.js +0 -175
- package/dist/prompts/spec.d.ts +0 -1
- package/dist/prompts/spec.js +0 -153
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// Tests for the skill subtree argv-model migration.
|
|
2
|
+
// Run with: node --import tsx/esm --test 'src/commands/__tests__/skill.test.ts'
|
|
3
|
+
//
|
|
4
|
+
// Tests exercise leaf param schemas via parseArgv (framework) — no subprocess
|
|
5
|
+
// spawning, no filesystem side-effects from handler logic.
|
|
6
|
+
import { test, describe } from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import { parseArgv } from '../../core/command.js';
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Shared flag param sets (mirrors the leaf definitions exactly)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
const scopeAllFlag = {
|
|
13
|
+
kind: 'flag', name: 'scope', type: 'enum',
|
|
14
|
+
choices: ['user', 'project', 'all'], required: false, constraint: '',
|
|
15
|
+
};
|
|
16
|
+
const scopeWriteFlag = {
|
|
17
|
+
kind: 'flag', name: 'scope', type: 'enum',
|
|
18
|
+
choices: ['user', 'project'], required: false, constraint: '',
|
|
19
|
+
};
|
|
20
|
+
const pluginFlag = {
|
|
21
|
+
kind: 'flag', name: 'plugin', type: 'string', required: false, constraint: '',
|
|
22
|
+
};
|
|
23
|
+
const includeDisabledFlag = {
|
|
24
|
+
kind: 'flag', name: 'include-disabled', type: 'bool', required: false, constraint: '',
|
|
25
|
+
};
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// skill find list
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
describe('skill find list params', () => {
|
|
30
|
+
const params = [
|
|
31
|
+
scopeAllFlag,
|
|
32
|
+
pluginFlag,
|
|
33
|
+
includeDisabledFlag,
|
|
34
|
+
{ kind: 'flag', name: 'limit', type: 'int', required: false, default: 50, constraint: '' },
|
|
35
|
+
{ kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: '' },
|
|
36
|
+
{ kind: 'flag', name: 'full', type: 'bool', required: false, constraint: '' },
|
|
37
|
+
];
|
|
38
|
+
test('no args: defaults applied', async () => {
|
|
39
|
+
const r = await parseArgv(params, []);
|
|
40
|
+
assert.equal(r['includeDisabled'], false);
|
|
41
|
+
assert.equal(r['limit'], 50);
|
|
42
|
+
assert.equal(r['scope'], undefined);
|
|
43
|
+
assert.equal(r['plugin'], undefined);
|
|
44
|
+
assert.equal(r['cursor'], undefined);
|
|
45
|
+
});
|
|
46
|
+
test('--scope user', async () => {
|
|
47
|
+
const r = await parseArgv(params, ['--scope', 'user']);
|
|
48
|
+
assert.equal(r['scope'], 'user');
|
|
49
|
+
});
|
|
50
|
+
test('--scope invalid rejects', async () => {
|
|
51
|
+
await assert.rejects(() => parseArgv(params, ['--scope', 'bogus']), (e) => { assert.match(e.message, /must be one of/); return true; });
|
|
52
|
+
});
|
|
53
|
+
test('--include-disabled presence = true', async () => {
|
|
54
|
+
const r = await parseArgv(params, ['--include-disabled']);
|
|
55
|
+
assert.equal(r['includeDisabled'], true);
|
|
56
|
+
});
|
|
57
|
+
test('--include-disabled=value rejects', async () => {
|
|
58
|
+
await assert.rejects(() => parseArgv(params, ['--include-disabled=yes']), (e) => { assert.match(e.message, /takes no value/); return true; });
|
|
59
|
+
});
|
|
60
|
+
test('--limit 100', async () => {
|
|
61
|
+
const r = await parseArgv(params, ['--limit', '100']);
|
|
62
|
+
assert.equal(r['limit'], 100);
|
|
63
|
+
});
|
|
64
|
+
test('--limit non-integer rejects', async () => {
|
|
65
|
+
await assert.rejects(() => parseArgv(params, ['--limit', '1.5']), (e) => { assert.match(e.message, /must be an integer/); return true; });
|
|
66
|
+
});
|
|
67
|
+
test('--cursor TOKEN', async () => {
|
|
68
|
+
const r = await parseArgv(params, ['--cursor', 'tok_abc']);
|
|
69
|
+
assert.equal(r['cursor'], 'tok_abc');
|
|
70
|
+
});
|
|
71
|
+
test('--plugin my-plugin', async () => {
|
|
72
|
+
const r = await parseArgv(params, ['--plugin', 'my-plugin']);
|
|
73
|
+
assert.equal(r['plugin'], 'my-plugin');
|
|
74
|
+
});
|
|
75
|
+
test('--full presence = true, absence = false', async () => {
|
|
76
|
+
const present = await parseArgv(params, ['--full']);
|
|
77
|
+
assert.equal(present['full'], true);
|
|
78
|
+
const absent = await parseArgv(params, []);
|
|
79
|
+
assert.equal(absent['full'], false);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// skill find search
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
describe('skill find search params', () => {
|
|
86
|
+
const params = [
|
|
87
|
+
{ kind: 'positional', name: 'query', required: true, constraint: '' },
|
|
88
|
+
scopeAllFlag,
|
|
89
|
+
pluginFlag,
|
|
90
|
+
includeDisabledFlag,
|
|
91
|
+
{ kind: 'flag', name: 'search-body', type: 'bool', required: false, constraint: '' },
|
|
92
|
+
];
|
|
93
|
+
test('query positional required', async () => {
|
|
94
|
+
await assert.rejects(() => parseArgv(params, []), (e) => { assert.match(e.message, /required parameter is missing/); return true; });
|
|
95
|
+
});
|
|
96
|
+
test('query parsed as positional', async () => {
|
|
97
|
+
const r = await parseArgv(params, ['my topic']);
|
|
98
|
+
assert.equal(r['query'], 'my topic');
|
|
99
|
+
});
|
|
100
|
+
test('query + flags', async () => {
|
|
101
|
+
const r = await parseArgv(params, ['debugging', '--scope', 'project', '--include-disabled', '--search-body']);
|
|
102
|
+
assert.equal(r['query'], 'debugging');
|
|
103
|
+
assert.equal(r['scope'], 'project');
|
|
104
|
+
assert.equal(r['includeDisabled'], true);
|
|
105
|
+
assert.equal(r['searchBody'], true);
|
|
106
|
+
});
|
|
107
|
+
test('--search-body presence = true, absence = false', async () => {
|
|
108
|
+
const present = await parseArgv(params, ['q', '--search-body']);
|
|
109
|
+
assert.equal(present['searchBody'], true);
|
|
110
|
+
const absent = await parseArgv(params, ['q']);
|
|
111
|
+
assert.equal(absent['searchBody'], false);
|
|
112
|
+
});
|
|
113
|
+
test('--scope all valid', async () => {
|
|
114
|
+
const r = await parseArgv(params, ['q', '--scope', 'all']);
|
|
115
|
+
assert.equal(r['scope'], 'all');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// skill find grep
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
describe('skill find grep params', () => {
|
|
122
|
+
const params = [
|
|
123
|
+
{ kind: 'positional', name: 'pattern', required: true, constraint: '' },
|
|
124
|
+
scopeAllFlag,
|
|
125
|
+
pluginFlag,
|
|
126
|
+
];
|
|
127
|
+
test('pattern positional required', async () => {
|
|
128
|
+
await assert.rejects(() => parseArgv(params, []), (e) => { assert.match(e.message, /required parameter is missing/); return true; });
|
|
129
|
+
});
|
|
130
|
+
test('pattern parsed as positional', async () => {
|
|
131
|
+
const r = await parseArgv(params, ['foo.*bar']);
|
|
132
|
+
assert.equal(r['pattern'], 'foo.*bar');
|
|
133
|
+
});
|
|
134
|
+
test('pattern + scope + plugin', async () => {
|
|
135
|
+
const r = await parseArgv(params, ['\\btest\\b', '--scope', 'user', '--plugin', 'myplugin']);
|
|
136
|
+
assert.equal(r['pattern'], '\\btest\\b');
|
|
137
|
+
assert.equal(r['scope'], 'user');
|
|
138
|
+
assert.equal(r['plugin'], 'myplugin');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// skill read show
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
describe('skill read show params', () => {
|
|
145
|
+
const params = [
|
|
146
|
+
{ kind: 'positional', name: 'name', required: true, constraint: '' },
|
|
147
|
+
scopeWriteFlag,
|
|
148
|
+
pluginFlag,
|
|
149
|
+
{ kind: 'flag', name: 'frontmatter', type: 'bool', required: false, constraint: '' },
|
|
150
|
+
];
|
|
151
|
+
test('name positional required', async () => {
|
|
152
|
+
await assert.rejects(() => parseArgv(params, []), (e) => { assert.match(e.message, /required parameter is missing/); return true; });
|
|
153
|
+
});
|
|
154
|
+
test('name parsed correctly', async () => {
|
|
155
|
+
const r = await parseArgv(params, ['my-skill']);
|
|
156
|
+
assert.equal(r['name'], 'my-skill');
|
|
157
|
+
});
|
|
158
|
+
test('--frontmatter presence = true', async () => {
|
|
159
|
+
const r = await parseArgv(params, ['my-skill', '--frontmatter']);
|
|
160
|
+
assert.equal(r['frontmatter'], true);
|
|
161
|
+
});
|
|
162
|
+
test('--frontmatter absent = false', async () => {
|
|
163
|
+
const r = await parseArgv(params, ['my-skill']);
|
|
164
|
+
assert.equal(r['frontmatter'], false);
|
|
165
|
+
});
|
|
166
|
+
test('--scope rejects all', async () => {
|
|
167
|
+
await assert.rejects(() => parseArgv(params, ['my-skill', '--scope', 'all']), (e) => { assert.match(e.message, /must be one of/); return true; });
|
|
168
|
+
});
|
|
169
|
+
test('--scope user valid', async () => {
|
|
170
|
+
const r = await parseArgv(params, ['my-skill', '--scope', 'user']);
|
|
171
|
+
assert.equal(r['scope'], 'user');
|
|
172
|
+
});
|
|
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
|
+
test('--scope project valid', async () => {
|
|
191
|
+
const r = await parseArgv(params, ['skillname', '--scope', 'project']);
|
|
192
|
+
assert.equal(r['scope'], 'project');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// skill author guide
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
describe('skill author guide params', () => {
|
|
199
|
+
const VALID_TYPES = ['playbook', 'primer', 'reference', 'runbook', 'freeform'];
|
|
200
|
+
const params = [
|
|
201
|
+
{ kind: 'flag', name: 'type', type: 'enum', choices: VALID_TYPES, required: false, constraint: '' },
|
|
202
|
+
{ kind: 'flag', name: 'topic', type: 'string', required: false, constraint: '' },
|
|
203
|
+
];
|
|
204
|
+
test('no args: both undefined', async () => {
|
|
205
|
+
const r = await parseArgv(params, []);
|
|
206
|
+
assert.equal(r['type'], undefined);
|
|
207
|
+
assert.equal(r['topic'], undefined);
|
|
208
|
+
});
|
|
209
|
+
test('--type playbook', async () => {
|
|
210
|
+
const r = await parseArgv(params, ['--type', 'playbook']);
|
|
211
|
+
assert.equal(r['type'], 'playbook');
|
|
212
|
+
});
|
|
213
|
+
test('--type invalid rejects', async () => {
|
|
214
|
+
await assert.rejects(() => parseArgv(params, ['--type', 'bogus']), (e) => { assert.match(e.message, /must be one of/); return true; });
|
|
215
|
+
});
|
|
216
|
+
test('all valid types accepted', async () => {
|
|
217
|
+
for (const t of VALID_TYPES) {
|
|
218
|
+
const r = await parseArgv(params, ['--type', t]);
|
|
219
|
+
assert.equal(r['type'], t);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
test('--topic string', async () => {
|
|
223
|
+
const r = await parseArgv(params, ['--topic', 'debugging methodology']);
|
|
224
|
+
assert.equal(r['topic'], 'debugging methodology');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// skill author scaffold
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
describe('skill author scaffold params', () => {
|
|
231
|
+
const VALID_TYPES = ['playbook', 'primer', 'reference', 'runbook', 'freeform'];
|
|
232
|
+
const params = [
|
|
233
|
+
{ kind: 'positional', name: 'qualifier', required: true, constraint: '' },
|
|
234
|
+
{ kind: 'flag', name: 'type', type: 'enum', choices: VALID_TYPES, required: false, constraint: '' },
|
|
235
|
+
{ kind: 'flag', name: 'description', type: 'string', required: false, constraint: '' },
|
|
236
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: '' },
|
|
237
|
+
];
|
|
238
|
+
test('qualifier positional required', async () => {
|
|
239
|
+
await assert.rejects(() => parseArgv(params, []), (e) => { assert.match(e.message, /required parameter is missing/); return true; });
|
|
240
|
+
});
|
|
241
|
+
test('qualifier parsed correctly', async () => {
|
|
242
|
+
const r = await parseArgv(params, ['myplugin:myskill']);
|
|
243
|
+
assert.equal(r['qualifier'], 'myplugin:myskill');
|
|
244
|
+
});
|
|
245
|
+
test('full invocation', async () => {
|
|
246
|
+
const r = await parseArgv(params, [
|
|
247
|
+
'myplugin:myskill',
|
|
248
|
+
'--type', 'playbook',
|
|
249
|
+
'--scope', 'project',
|
|
250
|
+
'--description', 'Use when debugging',
|
|
251
|
+
]);
|
|
252
|
+
assert.equal(r['qualifier'], 'myplugin:myskill');
|
|
253
|
+
assert.equal(r['type'], 'playbook');
|
|
254
|
+
assert.equal(r['scope'], 'project');
|
|
255
|
+
assert.equal(r['description'], 'Use when debugging');
|
|
256
|
+
});
|
|
257
|
+
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; });
|
|
259
|
+
});
|
|
260
|
+
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; });
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// skill state enable / disable
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
describe('skill state enable/disable params', () => {
|
|
268
|
+
const params = [
|
|
269
|
+
{ kind: 'positional', name: 'name', required: true, constraint: '' },
|
|
270
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: '' },
|
|
271
|
+
];
|
|
272
|
+
test('name positional required', async () => {
|
|
273
|
+
await assert.rejects(() => parseArgv(params, []), (e) => { assert.match(e.message, /required parameter is missing/); return true; });
|
|
274
|
+
});
|
|
275
|
+
test('name parsed correctly', async () => {
|
|
276
|
+
const r = await parseArgv(params, ['my-skill']);
|
|
277
|
+
assert.equal(r['name'], 'my-skill');
|
|
278
|
+
});
|
|
279
|
+
test('--scope user', async () => {
|
|
280
|
+
const r = await parseArgv(params, ['my-skill', '--scope', 'user']);
|
|
281
|
+
assert.equal(r['scope'], 'user');
|
|
282
|
+
});
|
|
283
|
+
test('--scope project', async () => {
|
|
284
|
+
const r = await parseArgv(params, ['my-skill', '--scope', 'project']);
|
|
285
|
+
assert.equal(r['scope'], 'project');
|
|
286
|
+
});
|
|
287
|
+
test('--scope all rejects', async () => {
|
|
288
|
+
await assert.rejects(() => parseArgv(params, ['my-skill', '--scope', 'all']), (e) => { assert.match(e.message, /must be one of/); return true; });
|
|
289
|
+
});
|
|
290
|
+
test('plugin:skill qualifier as name', async () => {
|
|
291
|
+
const r = await parseArgv(params, ['myplugin:myskill']);
|
|
292
|
+
assert.equal(r['name'], 'myplugin:myskill');
|
|
293
|
+
});
|
|
294
|
+
});
|
|
@@ -0,0 +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.";
|
|
2
|
+
import type { LeafDef } from '../core/command.js';
|
|
3
|
+
export declare function registerDebug(): LeafDef;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// `crtr flow debug` leaf — reproduce-first root-cause workflow.
|
|
2
|
+
//
|
|
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
|
|
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
|
|
7
|
+
// agent returns. Methodology stays in the CLI guide field, like PLAN_NEW_GUIDE;
|
|
8
|
+
// no builtin skill.
|
|
9
|
+
export const FLOW_DEBUG_GUIDE = `## Debug workflow — reproduce first
|
|
10
|
+
|
|
11
|
+
Audience: the agent that ran \`crtr flow debug\`. A reproduction agent is
|
|
12
|
+
already spawned in a sibling pane. It writes ONE failing integration test and
|
|
13
|
+
never fixes anything. You do everything after: gate on the repro, root-cause,
|
|
14
|
+
fix, verify against that same test.
|
|
15
|
+
|
|
16
|
+
### Phase 0: Await the repro agent
|
|
17
|
+
|
|
18
|
+
Run \`crtr job read result <job_id> --wait\` (10-min budget).
|
|
19
|
+
On status:"timeout": re-issue the wait, or run \`crtr job read logs <job_id> --follow\`
|
|
20
|
+
until the job is terminal.
|
|
21
|
+
|
|
22
|
+
### Phase 1: Gate on reproduction
|
|
23
|
+
|
|
24
|
+
\`reproduces:true\`: read \`test_path\`, run \`test_command\` YOURSELF, confirm
|
|
25
|
+
it fails for the stated reason. Do not trust the agent's claim — if it passes
|
|
26
|
+
or fails differently, treat repro as NOT achieved. This test is the regression
|
|
27
|
+
gate; it stays in the suite after the fix.
|
|
28
|
+
\`status:"failed"\` / \`reproduces:false\` / your run disproves it: no repro
|
|
29
|
+
harness. Continue, but record "no reproduction — fix unverified; do not claim
|
|
30
|
+
verified-fixed."
|
|
31
|
+
|
|
32
|
+
### Phase 2: Reconnaissance
|
|
33
|
+
|
|
34
|
+
Read the key files yourself — entry point, failure point, the data flow
|
|
35
|
+
between. \`git log\` / \`git blame\` near the failure: recent changes are
|
|
36
|
+
high-signal.
|
|
37
|
+
|
|
38
|
+
### Phase 3: Assess difficulty, scale investigators
|
|
39
|
+
|
|
40
|
+
Simple → solo (Explore subagents for tracing if the area is large).
|
|
41
|
+
Medium → 2–3 parallel \`devcore:senior-advisor\`: data-flow tracer, assumption
|
|
42
|
+
auditor, change investigator.
|
|
43
|
+
Hard (intermittent, races, "been stuck", many modules) → 3–5 parallel:
|
|
44
|
+
end-to-end tracer, assumption breaker, git archaeologist, boundary inspector.
|
|
45
|
+
Give investigators file paths, observed behavior, and concrete tasks — never
|
|
46
|
+
your hypotheses. Challenge theories against each other; the one that survives
|
|
47
|
+
disconfirmation wins.
|
|
48
|
+
|
|
49
|
+
### Phase 4: Fix
|
|
50
|
+
|
|
51
|
+
Minimal root-cause fix. No scope expansion, no drive-by refactor.
|
|
52
|
+
|
|
53
|
+
### Phase 5: Verify
|
|
54
|
+
|
|
55
|
+
Re-run \`test_command\`: it MUST now pass. Run the broader suite for
|
|
56
|
+
regressions. If there was no repro test, state the fix is unverified by
|
|
57
|
+
reproduction and recommend explicit manual verification.
|
|
58
|
+
|
|
59
|
+
### Phase 6: Report
|
|
60
|
+
|
|
61
|
+
Root cause (exact line + why), evidence, the now-passing repro test path,
|
|
62
|
+
confidence (High/Medium/Low; if not High, name what is uncertain).
|
|
63
|
+
|
|
64
|
+
### Constraints
|
|
65
|
+
|
|
66
|
+
The repro test is the regression guard — it stays; a fix-agent must never
|
|
67
|
+
weaken it. Investigators run in forked contexts; they return summaries, not
|
|
68
|
+
raw output. No code changes during Phases 2–3 except the repro test.`;
|
|
69
|
+
import { defineLeaf } from '../core/command.js';
|
|
70
|
+
import { InputError } from '../core/io.js';
|
|
71
|
+
import { createJob, appendEvent } from '../core/jobs.js';
|
|
72
|
+
import { spawnAgent, isInTmux } from '../core/spawn.js';
|
|
73
|
+
import { readConfig } from '../core/config.js';
|
|
74
|
+
import { reproHandoffPrompt } from '../prompts/debug.js';
|
|
75
|
+
// Inlined from job.ts (module-private there; not exported, per the no-shim
|
|
76
|
+
// convention). Same forms.
|
|
77
|
+
function resolveMaxPanes() {
|
|
78
|
+
const cfg = readConfig('user');
|
|
79
|
+
return cfg.max_panes_per_window;
|
|
80
|
+
}
|
|
81
|
+
function assertTmux() {
|
|
82
|
+
if (!isInTmux()) {
|
|
83
|
+
throw new InputError({
|
|
84
|
+
error: 'not_in_tmux',
|
|
85
|
+
message: 'crtr flow debug requires tmux (TMUX env var not set).',
|
|
86
|
+
next: 'Run inside a tmux session.',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function registerDebug() {
|
|
91
|
+
return defineLeaf({
|
|
92
|
+
name: 'debug',
|
|
93
|
+
help: {
|
|
94
|
+
name: 'flow debug',
|
|
95
|
+
summary: 'reproduce-first root-cause workflow: spawns a reproduction agent, then you root-cause and fix',
|
|
96
|
+
guide: FLOW_DEBUG_GUIDE,
|
|
97
|
+
params: [
|
|
98
|
+
{
|
|
99
|
+
kind: 'stdin',
|
|
100
|
+
name: 'steps_to_reproduce',
|
|
101
|
+
required: true,
|
|
102
|
+
constraint: 'Prose describing how to reproduce the failure. Pipe on stdin.',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
kind: 'flag',
|
|
106
|
+
name: 'summary',
|
|
107
|
+
type: 'string',
|
|
108
|
+
required: true,
|
|
109
|
+
constraint: 'One paragraph summary of the failure: symptom, where observed, expected vs actual.',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
kind: 'flag',
|
|
113
|
+
name: 'cwd',
|
|
114
|
+
type: 'path',
|
|
115
|
+
required: false,
|
|
116
|
+
constraint: 'Working directory for the spawned agent. Defaults to process.cwd().',
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
output: [
|
|
120
|
+
{
|
|
121
|
+
name: 'job_id',
|
|
122
|
+
type: 'string',
|
|
123
|
+
required: true,
|
|
124
|
+
constraint: 'Use with `job read status`, `job read logs`, `job read result`, `job cancel`.',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'follow_up',
|
|
128
|
+
type: 'string',
|
|
129
|
+
required: true,
|
|
130
|
+
constraint: 'Recommended next call.',
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
outputKind: 'object',
|
|
134
|
+
effects: [
|
|
135
|
+
'Spawns a reproduction agent in a sibling tmux pane.',
|
|
136
|
+
'Creates a job entry at $XDG_STATE_HOME/crtr/jobs/<job_id>/.',
|
|
137
|
+
'On completion, result writes atomically to result.json.',
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
run: async (input) => {
|
|
141
|
+
assertTmux();
|
|
142
|
+
const stepsToReproduce = input['steps_to_reproduce'];
|
|
143
|
+
const summary = input['summary'];
|
|
144
|
+
const cwd = input['cwd'] ?? process.cwd();
|
|
145
|
+
const issue = `${summary}\n\n${stepsToReproduce}`;
|
|
146
|
+
const { jobId } = createJob('debug-repro', { cwd });
|
|
147
|
+
const result = spawnAgent({
|
|
148
|
+
prompt: reproHandoffPrompt(issue, jobId),
|
|
149
|
+
cwd,
|
|
150
|
+
jobId,
|
|
151
|
+
maxPanesPerWindow: resolveMaxPanes(),
|
|
152
|
+
});
|
|
153
|
+
if (result.status === 'not-in-tmux') {
|
|
154
|
+
throw new InputError({
|
|
155
|
+
error: 'not_in_tmux',
|
|
156
|
+
message: result.message,
|
|
157
|
+
next: 'Run inside a tmux session.',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (result.status === 'spawn-failed') {
|
|
161
|
+
throw new InputError({
|
|
162
|
+
error: 'spawn_failed',
|
|
163
|
+
message: result.message,
|
|
164
|
+
next: 'Check tmux is running and try again.',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const paneLabel = result.paneId !== undefined ? result.paneId : 'unknown';
|
|
168
|
+
appendEvent(jobId, {
|
|
169
|
+
level: 'info',
|
|
170
|
+
event: 'worker_started',
|
|
171
|
+
message: `repro pane ${paneLabel} spawned`,
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
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.`,
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// `crtr flow` umbrella — groups the spec → plan → debug development process.
|
|
2
|
+
// registerSpec/registerPlan are unchanged (each still defineBranch{name}); they
|
|
3
|
+
// nest under `flow` here instead of registering at root. registerDebug is a
|
|
4
|
+
// leaf — `crtr flow debug` spawns directly and `-h` prints FLOW_DEBUG_GUIDE.
|
|
5
|
+
import { defineBranch } from '../core/command.js';
|
|
6
|
+
import { registerSpec } from './spec.js';
|
|
7
|
+
import { registerPlan } from './plan.js';
|
|
8
|
+
import { registerDebug } from './debug.js';
|
|
9
|
+
export function registerFlow() {
|
|
10
|
+
return defineBranch({
|
|
11
|
+
name: 'flow',
|
|
12
|
+
help: {
|
|
13
|
+
name: 'flow',
|
|
14
|
+
summary: 'the spec → plan → debug development process',
|
|
15
|
+
model: 'spec captures requirements; plan decomposes them; debug root-causes failures reproduce-first.',
|
|
16
|
+
children: [
|
|
17
|
+
{ name: 'spec', desc: 'create, read, list specifications', useWhen: 'capturing requirements before planning' },
|
|
18
|
+
{ name: 'plan', desc: 'create, read, list plans', useWhen: 'shaping or inspecting work' },
|
|
19
|
+
{ name: 'debug', desc: 'reproduce-first root-cause workflow', useWhen: 'a bug, test failure, or unexpected behavior needs root-causing' },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
children: [registerSpec(), registerPlan(), registerDebug()],
|
|
23
|
+
});
|
|
24
|
+
}
|