@crouton-kit/crouter 0.2.6 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +9 -9
  2. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +19 -19
  3. package/dist/cli.js +42 -37
  4. package/dist/commands/__tests__/human.test.d.ts +1 -0
  5. package/dist/commands/__tests__/human.test.js +214 -0
  6. package/dist/commands/__tests__/skill.test.d.ts +1 -0
  7. package/dist/commands/__tests__/skill.test.js +287 -0
  8. package/dist/commands/debug.d.ts +3 -0
  9. package/dist/commands/debug.js +179 -0
  10. package/dist/commands/flow.d.ts +2 -0
  11. package/dist/commands/flow.js +24 -0
  12. package/dist/commands/human.d.ts +2 -0
  13. package/dist/commands/human.js +480 -0
  14. package/dist/commands/job.d.ts +2 -0
  15. package/dist/commands/job.js +669 -0
  16. package/dist/commands/pkg.d.ts +2 -0
  17. package/dist/commands/pkg.js +1021 -0
  18. package/dist/commands/plan.d.ts +4 -2
  19. package/dist/commands/plan.js +306 -22
  20. package/dist/commands/skill.d.ts +2 -2
  21. package/dist/commands/skill.js +607 -456
  22. package/dist/commands/spec.d.ts +3 -2
  23. package/dist/commands/spec.js +283 -10
  24. package/dist/commands/sys.d.ts +2 -0
  25. package/dist/commands/sys.js +712 -0
  26. package/dist/core/__tests__/argv-parser.test.d.ts +1 -0
  27. package/dist/core/__tests__/argv-parser.test.js +199 -0
  28. package/dist/core/__tests__/flow-leaves.test.d.ts +1 -0
  29. package/dist/core/__tests__/flow-leaves.test.js +248 -0
  30. package/dist/core/__tests__/job.test.d.ts +1 -0
  31. package/dist/core/__tests__/job.test.js +346 -0
  32. package/dist/core/__tests__/pkg.test.d.ts +1 -0
  33. package/dist/core/__tests__/pkg.test.js +218 -0
  34. package/dist/core/__tests__/sys.test.d.ts +1 -0
  35. package/dist/core/__tests__/sys.test.js +208 -0
  36. package/dist/core/artifact.d.ts +29 -18
  37. package/dist/core/artifact.js +78 -221
  38. package/dist/core/auto-update.js +11 -3
  39. package/dist/core/command.d.ts +36 -0
  40. package/dist/core/command.js +287 -0
  41. package/dist/core/errors.d.ts +3 -0
  42. package/dist/core/errors.js +5 -0
  43. package/dist/core/fs-utils.d.ts +1 -0
  44. package/dist/core/fs-utils.js +4 -0
  45. package/dist/core/help.d.ts +98 -0
  46. package/dist/core/help.js +163 -0
  47. package/dist/core/io.d.ts +29 -0
  48. package/dist/core/io.js +83 -0
  49. package/dist/core/jobs.d.ts +87 -0
  50. package/dist/core/jobs.js +353 -0
  51. package/dist/core/pagination.d.ts +33 -0
  52. package/dist/core/pagination.js +89 -0
  53. package/dist/core/self-update.d.ts +21 -0
  54. package/dist/{commands/update.js → core/self-update.js} +28 -63
  55. package/dist/core/spawn.d.ts +47 -65
  56. package/dist/core/spawn.js +78 -228
  57. package/dist/prompts/agent.d.ts +10 -5
  58. package/dist/prompts/agent.js +51 -74
  59. package/dist/prompts/debug.d.ts +8 -0
  60. package/dist/prompts/debug.js +37 -0
  61. package/dist/prompts/review.js +4 -11
  62. package/dist/prompts/skill.d.ts +0 -1
  63. package/dist/prompts/skill.js +95 -149
  64. package/package.json +4 -2
  65. package/dist/commands/agent.d.ts +0 -2
  66. package/dist/commands/agent.js +0 -265
  67. package/dist/commands/config.d.ts +0 -2
  68. package/dist/commands/config.js +0 -146
  69. package/dist/commands/doctor.d.ts +0 -2
  70. package/dist/commands/doctor.js +0 -268
  71. package/dist/commands/marketplace.d.ts +0 -2
  72. package/dist/commands/marketplace.js +0 -365
  73. package/dist/commands/plugin.d.ts +0 -2
  74. package/dist/commands/plugin.js +0 -367
  75. package/dist/commands/update.d.ts +0 -4
  76. package/dist/prompts/plan.d.ts +0 -1
  77. package/dist/prompts/plan.js +0 -175
  78. package/dist/prompts/spec.d.ts +0 -1
  79. package/dist/prompts/spec.js +0 -153
@@ -0,0 +1,287 @@
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
+ ];
37
+ test('no args: defaults applied', async () => {
38
+ const r = await parseArgv(params, []);
39
+ assert.equal(r['includeDisabled'], false);
40
+ assert.equal(r['limit'], 50);
41
+ assert.equal(r['scope'], undefined);
42
+ assert.equal(r['plugin'], undefined);
43
+ assert.equal(r['cursor'], undefined);
44
+ });
45
+ test('--scope user', async () => {
46
+ const r = await parseArgv(params, ['--scope', 'user']);
47
+ assert.equal(r['scope'], 'user');
48
+ });
49
+ test('--scope invalid rejects', async () => {
50
+ await assert.rejects(() => parseArgv(params, ['--scope', 'bogus']), (e) => { assert.match(e.message, /must be one of/); return true; });
51
+ });
52
+ test('--include-disabled presence = true', async () => {
53
+ const r = await parseArgv(params, ['--include-disabled']);
54
+ assert.equal(r['includeDisabled'], true);
55
+ });
56
+ test('--include-disabled=value rejects', async () => {
57
+ await assert.rejects(() => parseArgv(params, ['--include-disabled=yes']), (e) => { assert.match(e.message, /takes no value/); return true; });
58
+ });
59
+ test('--limit 100', async () => {
60
+ const r = await parseArgv(params, ['--limit', '100']);
61
+ assert.equal(r['limit'], 100);
62
+ });
63
+ test('--limit non-integer rejects', async () => {
64
+ await assert.rejects(() => parseArgv(params, ['--limit', '1.5']), (e) => { assert.match(e.message, /must be an integer/); return true; });
65
+ });
66
+ test('--cursor TOKEN', async () => {
67
+ const r = await parseArgv(params, ['--cursor', 'tok_abc']);
68
+ assert.equal(r['cursor'], 'tok_abc');
69
+ });
70
+ test('--plugin my-plugin', async () => {
71
+ const r = await parseArgv(params, ['--plugin', 'my-plugin']);
72
+ assert.equal(r['plugin'], 'my-plugin');
73
+ });
74
+ });
75
+ // ---------------------------------------------------------------------------
76
+ // skill find search
77
+ // ---------------------------------------------------------------------------
78
+ describe('skill find search params', () => {
79
+ const params = [
80
+ { kind: 'positional', name: 'query', required: true, constraint: '' },
81
+ scopeAllFlag,
82
+ pluginFlag,
83
+ includeDisabledFlag,
84
+ { kind: 'flag', name: 'search-body', type: 'bool', required: false, constraint: '' },
85
+ ];
86
+ test('query positional required', async () => {
87
+ await assert.rejects(() => parseArgv(params, []), (e) => { assert.match(e.message, /required parameter is missing/); return true; });
88
+ });
89
+ test('query parsed as positional', async () => {
90
+ const r = await parseArgv(params, ['my topic']);
91
+ assert.equal(r['query'], 'my topic');
92
+ });
93
+ test('query + flags', async () => {
94
+ const r = await parseArgv(params, ['debugging', '--scope', 'project', '--include-disabled', '--search-body']);
95
+ assert.equal(r['query'], 'debugging');
96
+ assert.equal(r['scope'], 'project');
97
+ assert.equal(r['includeDisabled'], true);
98
+ assert.equal(r['searchBody'], true);
99
+ });
100
+ test('--search-body presence = true, absence = false', async () => {
101
+ const present = await parseArgv(params, ['q', '--search-body']);
102
+ assert.equal(present['searchBody'], true);
103
+ const absent = await parseArgv(params, ['q']);
104
+ assert.equal(absent['searchBody'], false);
105
+ });
106
+ test('--scope all valid', async () => {
107
+ const r = await parseArgv(params, ['q', '--scope', 'all']);
108
+ assert.equal(r['scope'], 'all');
109
+ });
110
+ });
111
+ // ---------------------------------------------------------------------------
112
+ // skill find grep
113
+ // ---------------------------------------------------------------------------
114
+ describe('skill find grep params', () => {
115
+ const params = [
116
+ { kind: 'positional', name: 'pattern', required: true, constraint: '' },
117
+ scopeAllFlag,
118
+ pluginFlag,
119
+ ];
120
+ test('pattern positional required', async () => {
121
+ await assert.rejects(() => parseArgv(params, []), (e) => { assert.match(e.message, /required parameter is missing/); return true; });
122
+ });
123
+ test('pattern parsed as positional', async () => {
124
+ const r = await parseArgv(params, ['foo.*bar']);
125
+ assert.equal(r['pattern'], 'foo.*bar');
126
+ });
127
+ test('pattern + scope + plugin', async () => {
128
+ const r = await parseArgv(params, ['\\btest\\b', '--scope', 'user', '--plugin', 'myplugin']);
129
+ assert.equal(r['pattern'], '\\btest\\b');
130
+ assert.equal(r['scope'], 'user');
131
+ assert.equal(r['plugin'], 'myplugin');
132
+ });
133
+ });
134
+ // ---------------------------------------------------------------------------
135
+ // skill read show
136
+ // ---------------------------------------------------------------------------
137
+ describe('skill read show params', () => {
138
+ const params = [
139
+ { kind: 'positional', name: 'name', required: true, constraint: '' },
140
+ scopeWriteFlag,
141
+ pluginFlag,
142
+ { kind: 'flag', name: 'frontmatter', type: 'bool', required: false, constraint: '' },
143
+ ];
144
+ test('name positional required', async () => {
145
+ await assert.rejects(() => parseArgv(params, []), (e) => { assert.match(e.message, /required parameter is missing/); return true; });
146
+ });
147
+ test('name parsed correctly', async () => {
148
+ const r = await parseArgv(params, ['my-skill']);
149
+ assert.equal(r['name'], 'my-skill');
150
+ });
151
+ test('--frontmatter presence = true', async () => {
152
+ const r = await parseArgv(params, ['my-skill', '--frontmatter']);
153
+ assert.equal(r['frontmatter'], true);
154
+ });
155
+ test('--frontmatter absent = false', async () => {
156
+ const r = await parseArgv(params, ['my-skill']);
157
+ assert.equal(r['frontmatter'], false);
158
+ });
159
+ test('--scope rejects all', async () => {
160
+ await assert.rejects(() => parseArgv(params, ['my-skill', '--scope', 'all']), (e) => { assert.match(e.message, /must be one of/); return true; });
161
+ });
162
+ test('--scope user valid', async () => {
163
+ const r = await parseArgv(params, ['my-skill', '--scope', 'user']);
164
+ assert.equal(r['scope'], 'user');
165
+ });
166
+ });
167
+ // ---------------------------------------------------------------------------
168
+ // skill read where
169
+ // ---------------------------------------------------------------------------
170
+ describe('skill read where params', () => {
171
+ const params = [
172
+ { kind: 'positional', name: 'name', required: true, constraint: '' },
173
+ scopeWriteFlag,
174
+ pluginFlag,
175
+ ];
176
+ test('name positional required', async () => {
177
+ await assert.rejects(() => parseArgv(params, []), (e) => { assert.match(e.message, /required parameter is missing/); return true; });
178
+ });
179
+ test('name parsed correctly', async () => {
180
+ const r = await parseArgv(params, ['some/nested/skill']);
181
+ assert.equal(r['name'], 'some/nested/skill');
182
+ });
183
+ test('--scope project valid', async () => {
184
+ const r = await parseArgv(params, ['skillname', '--scope', 'project']);
185
+ assert.equal(r['scope'], 'project');
186
+ });
187
+ });
188
+ // ---------------------------------------------------------------------------
189
+ // skill author guide
190
+ // ---------------------------------------------------------------------------
191
+ describe('skill author guide params', () => {
192
+ const VALID_TYPES = ['playbook', 'primer', 'reference', 'runbook', 'freeform'];
193
+ const params = [
194
+ { kind: 'flag', name: 'type', type: 'enum', choices: VALID_TYPES, required: false, constraint: '' },
195
+ { kind: 'flag', name: 'topic', type: 'string', required: false, constraint: '' },
196
+ ];
197
+ test('no args: both undefined', async () => {
198
+ const r = await parseArgv(params, []);
199
+ assert.equal(r['type'], undefined);
200
+ assert.equal(r['topic'], undefined);
201
+ });
202
+ test('--type playbook', async () => {
203
+ const r = await parseArgv(params, ['--type', 'playbook']);
204
+ assert.equal(r['type'], 'playbook');
205
+ });
206
+ test('--type invalid rejects', async () => {
207
+ await assert.rejects(() => parseArgv(params, ['--type', 'bogus']), (e) => { assert.match(e.message, /must be one of/); return true; });
208
+ });
209
+ test('all valid types accepted', async () => {
210
+ for (const t of VALID_TYPES) {
211
+ const r = await parseArgv(params, ['--type', t]);
212
+ assert.equal(r['type'], t);
213
+ }
214
+ });
215
+ test('--topic string', async () => {
216
+ const r = await parseArgv(params, ['--topic', 'debugging methodology']);
217
+ assert.equal(r['topic'], 'debugging methodology');
218
+ });
219
+ });
220
+ // ---------------------------------------------------------------------------
221
+ // skill author scaffold
222
+ // ---------------------------------------------------------------------------
223
+ describe('skill author scaffold params', () => {
224
+ const VALID_TYPES = ['playbook', 'primer', 'reference', 'runbook', 'freeform'];
225
+ const params = [
226
+ { kind: 'positional', name: 'qualifier', required: true, constraint: '' },
227
+ { kind: 'flag', name: 'type', type: 'enum', choices: VALID_TYPES, required: false, constraint: '' },
228
+ { kind: 'flag', name: 'description', type: 'string', required: false, constraint: '' },
229
+ { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: '' },
230
+ ];
231
+ test('qualifier positional required', async () => {
232
+ await assert.rejects(() => parseArgv(params, []), (e) => { assert.match(e.message, /required parameter is missing/); return true; });
233
+ });
234
+ test('qualifier parsed correctly', async () => {
235
+ const r = await parseArgv(params, ['myplugin:myskill']);
236
+ assert.equal(r['qualifier'], 'myplugin:myskill');
237
+ });
238
+ test('full invocation', async () => {
239
+ const r = await parseArgv(params, [
240
+ 'myplugin:myskill',
241
+ '--type', 'playbook',
242
+ '--scope', 'project',
243
+ '--description', 'Use when debugging',
244
+ ]);
245
+ assert.equal(r['qualifier'], 'myplugin:myskill');
246
+ assert.equal(r['type'], 'playbook');
247
+ assert.equal(r['scope'], 'project');
248
+ assert.equal(r['description'], 'Use when debugging');
249
+ });
250
+ test('--type invalid rejects', async () => {
251
+ await assert.rejects(() => parseArgv(params, ['q:s', '--type', 'invalid']), (e) => { assert.match(e.message, /must be one of/); return true; });
252
+ });
253
+ test('--scope all rejects', async () => {
254
+ await assert.rejects(() => parseArgv(params, ['q:s', '--scope', 'all']), (e) => { assert.match(e.message, /must be one of/); return true; });
255
+ });
256
+ });
257
+ // ---------------------------------------------------------------------------
258
+ // skill state enable / disable
259
+ // ---------------------------------------------------------------------------
260
+ describe('skill state enable/disable params', () => {
261
+ const params = [
262
+ { kind: 'positional', name: 'name', required: true, constraint: '' },
263
+ { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: '' },
264
+ ];
265
+ test('name positional required', async () => {
266
+ await assert.rejects(() => parseArgv(params, []), (e) => { assert.match(e.message, /required parameter is missing/); return true; });
267
+ });
268
+ test('name parsed correctly', async () => {
269
+ const r = await parseArgv(params, ['my-skill']);
270
+ assert.equal(r['name'], 'my-skill');
271
+ });
272
+ test('--scope user', async () => {
273
+ const r = await parseArgv(params, ['my-skill', '--scope', 'user']);
274
+ assert.equal(r['scope'], 'user');
275
+ });
276
+ test('--scope project', async () => {
277
+ const r = await parseArgv(params, ['my-skill', '--scope', 'project']);
278
+ assert.equal(r['scope'], 'project');
279
+ });
280
+ test('--scope all rejects', async () => {
281
+ await assert.rejects(() => parseArgv(params, ['my-skill', '--scope', 'all']), (e) => { assert.match(e.message, /must be one of/); return true; });
282
+ });
283
+ test('plugin:skill qualifier as name', async () => {
284
+ const r = await parseArgv(params, ['myplugin:myskill']);
285
+ assert.equal(r['name'], 'myplugin:myskill');
286
+ });
287
+ });
@@ -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,2 @@
1
+ import type { BranchDef } from '../core/command.js';
2
+ export declare function registerFlow(): BranchDef;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import type { BranchDef } from '../core/command.js';
2
+ export declare function registerHuman(): BranchDef;