@crouton-kit/crouter 0.3.2 → 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/human.js +6 -24
- 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/dist/commands/human.js
CHANGED
|
@@ -21,7 +21,7 @@ import { readConfig } from '../core/config.js';
|
|
|
21
21
|
import { mkdirSync, existsSync } from 'node:fs';
|
|
22
22
|
import { join, resolve } from 'node:path';
|
|
23
23
|
import { randomBytes } from 'node:crypto';
|
|
24
|
-
import { ask, launchReview, display, inbox, scanInbox, validateDeck, parseDeck, deckPath, atomicWriteJson, readJson, } from '@crouton-kit/humanloop';
|
|
24
|
+
import { ask, launchReview, display, inbox, scanInbox, validateDeck, approveDeck, notifyDeck, parseDeck, deckPath, atomicWriteJson, readJson, } from '@crouton-kit/humanloop';
|
|
25
25
|
const DECK_SCHEMA_HINT = 'Deck must match the humanloop deck schema: {title?, ' +
|
|
26
26
|
'source?:{sessionName?,askedBy?,blockedSince?}, ' +
|
|
27
27
|
'interactions:[{id,title,subtitle?,(body?|bodyPath?),options:[{id,label,' +
|
|
@@ -146,20 +146,10 @@ const humanApprove = defineLeaf({
|
|
|
146
146
|
const title = input['title'];
|
|
147
147
|
const subtitle = input['subtitle'];
|
|
148
148
|
const body = input['body'];
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
options: [
|
|
154
|
-
{ id: 'yes', label: 'Yes' },
|
|
155
|
-
{ id: 'no', label: 'No' },
|
|
156
|
-
],
|
|
157
|
-
};
|
|
158
|
-
if (subtitle !== undefined)
|
|
159
|
-
interaction['subtitle'] = subtitle;
|
|
160
|
-
if (body !== undefined)
|
|
161
|
-
interaction['body'] = body;
|
|
162
|
-
const deck = validateDeck({ interactions: [interaction] });
|
|
149
|
+
const deck = approveDeck(title, {
|
|
150
|
+
...(subtitle !== undefined ? { subtitle } : {}),
|
|
151
|
+
...(body !== undefined ? { body } : {}),
|
|
152
|
+
});
|
|
163
153
|
const cwd = process.cwd();
|
|
164
154
|
const { jobId } = createJob('human', { cwd });
|
|
165
155
|
const idir = interactionDir(jobId, cwd);
|
|
@@ -250,15 +240,7 @@ const humanNotify = defineLeaf({
|
|
|
250
240
|
run: async (input) => {
|
|
251
241
|
const title = input['title'];
|
|
252
242
|
const body = input['body'];
|
|
253
|
-
const
|
|
254
|
-
id: 'notify',
|
|
255
|
-
title,
|
|
256
|
-
kind: 'notify',
|
|
257
|
-
options: [{ id: 'ack', label: 'OK' }],
|
|
258
|
-
};
|
|
259
|
-
if (body !== undefined)
|
|
260
|
-
interaction['body'] = body;
|
|
261
|
-
const deck = validateDeck({ interactions: [interaction] });
|
|
243
|
+
const deck = notifyDeck(title, body !== undefined ? { body } : {});
|
|
262
244
|
const cwd = process.cwd();
|
|
263
245
|
const id = `nfy-${randomBytes(4).toString('hex')}`;
|
|
264
246
|
const idir = interactionDir(id, cwd);
|
package/dist/commands/job.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
// `crtr job` subtree —
|
|
1
|
+
// `crtr job` subtree — universal monitoring registry for any ongoing task.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// Producers (agent spawns, future task systems) register jobs and write
|
|
4
|
+
// results; this subtree is the read/cancel/submit surface shared across all
|
|
5
|
+
// producers. Sub-branches: read {list, status, logs, result}, submit, _fail,
|
|
6
|
+
// cancel.
|
|
5
7
|
//
|
|
6
8
|
// Terminal-write contract:
|
|
7
9
|
// Worker calls `crtr job submit` → jobs.writeResult(jobId, result, 'done').
|
|
@@ -13,356 +15,12 @@
|
|
|
13
15
|
import { defineBranch, defineLeaf } from '../core/command.js';
|
|
14
16
|
import { emitLine } from '../core/io.js';
|
|
15
17
|
import { InputError } from '../core/io.js';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { readConfig } from '../core/config.js';
|
|
19
|
-
import { planHandoffPrompt, implementHandoffPrompt, reviewerHandoffPrompt } from '../prompts/agent.js';
|
|
18
|
+
import { writeMarkdownResult, readResult as jobsReadResult, jobStatus, listJobs, readLog, cancelJob, } from '../core/jobs.js';
|
|
19
|
+
import { scheduleKillCurrentPane } from '../core/spawn.js';
|
|
20
20
|
import { paginate } from '../core/pagination.js';
|
|
21
|
-
import { existsSync } from 'node:fs';
|
|
22
21
|
const WAIT_BUDGET_MS = 10 * 60 * 1000;
|
|
23
22
|
const FOLLOW_POLL_MS = 1000;
|
|
24
23
|
const DEFAULT_KILL_SECS = 2;
|
|
25
|
-
function followUpResult(jobId) {
|
|
26
|
-
return `crtr job read result ${jobId} --wait`;
|
|
27
|
-
}
|
|
28
|
-
function resolveMaxPanes() {
|
|
29
|
-
const cfg = readConfig('user');
|
|
30
|
-
return cfg.max_panes_per_window;
|
|
31
|
-
}
|
|
32
|
-
function assertTmux() {
|
|
33
|
-
if (!isInTmux()) {
|
|
34
|
-
throw new InputError({
|
|
35
|
-
error: 'not_in_tmux',
|
|
36
|
-
message: 'crtr job start requires tmux (TMUX env var not set).',
|
|
37
|
-
next: 'Run inside a tmux session.',
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// start sub-branch
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
const startPrompt = defineLeaf({
|
|
45
|
-
name: 'prompt',
|
|
46
|
-
help: {
|
|
47
|
-
name: 'job start prompt',
|
|
48
|
-
summary: 'spawn a fresh Claude agent with a prompt; returns a job handle immediately',
|
|
49
|
-
params: [
|
|
50
|
-
{ kind: 'stdin', name: 'prompt', required: true, constraint: 'Prompt text sent to the spawned agent.' },
|
|
51
|
-
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Working directory for the spawned agent. Defaults to process.cwd().' },
|
|
52
|
-
],
|
|
53
|
-
output: [
|
|
54
|
-
{ name: 'job_id', type: 'string', required: true, constraint: 'Use with `job read status`, `job read logs`, `job read result`, `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 { jobId } = createJob('prompt', { cwd });
|
|
69
|
-
const promptWithSubmit = `${prompt}
|
|
70
|
-
|
|
71
|
-
---
|
|
72
|
-
When your task is complete, submit your result:
|
|
73
|
-
\`\`\`bash
|
|
74
|
-
crtr job submit ${jobId} --context-file /tmp/result.json
|
|
75
|
-
\`\`\`
|
|
76
|
-
If you cannot complete the task, still submit with status "failed" and a reason.`;
|
|
77
|
-
const result = spawnAgent({
|
|
78
|
-
prompt: promptWithSubmit,
|
|
79
|
-
cwd,
|
|
80
|
-
jobId,
|
|
81
|
-
maxPanesPerWindow: resolveMaxPanes(),
|
|
82
|
-
});
|
|
83
|
-
if (result.status === 'not-in-tmux') {
|
|
84
|
-
throw new InputError({
|
|
85
|
-
error: 'not_in_tmux',
|
|
86
|
-
message: result.message,
|
|
87
|
-
next: 'Run inside a tmux session.',
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
if (result.status === 'spawn-failed') {
|
|
91
|
-
throw new InputError({
|
|
92
|
-
error: 'spawn_failed',
|
|
93
|
-
message: result.message,
|
|
94
|
-
next: 'Check tmux is running and try again.',
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
const paneLabel = result.paneId !== undefined ? result.paneId : 'unknown';
|
|
98
|
-
appendEvent(jobId, { level: 'info', event: 'worker_started', message: `pane ${paneLabel} spawned` });
|
|
99
|
-
return { job_id: jobId, follow_up: followUpResult(jobId) };
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
const startFork = defineLeaf({
|
|
103
|
-
name: 'fork',
|
|
104
|
-
help: {
|
|
105
|
-
name: 'job start fork',
|
|
106
|
-
summary: 'fork the current Claude session into a sibling pane; returns a job handle immediately',
|
|
107
|
-
params: [
|
|
108
|
-
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Working directory. Defaults to process.cwd().' },
|
|
109
|
-
],
|
|
110
|
-
output: [
|
|
111
|
-
{ name: 'job_id', type: 'string', required: true, constraint: 'Use with `job read *` and `job cancel`.' },
|
|
112
|
-
{ name: 'follow_up', type: 'string', required: true, constraint: 'Recommended next call.' },
|
|
113
|
-
],
|
|
114
|
-
outputKind: 'object',
|
|
115
|
-
effects: [
|
|
116
|
-
'Requires $CLAUDE_CODE_SESSION_ID — must run inside Claude Code.',
|
|
117
|
-
'Spawns a forked Claude session in a sibling tmux pane.',
|
|
118
|
-
'Creates a job entry and result sidecar as with `job start prompt`.',
|
|
119
|
-
],
|
|
120
|
-
},
|
|
121
|
-
run: async (input) => {
|
|
122
|
-
assertTmux();
|
|
123
|
-
const parentSessionId = process.env['CLAUDE_CODE_SESSION_ID'];
|
|
124
|
-
if (parentSessionId === undefined || parentSessionId === '') {
|
|
125
|
-
throw new InputError({
|
|
126
|
-
error: 'missing_session_id',
|
|
127
|
-
message: 'crtr job start fork requires $CLAUDE_CODE_SESSION_ID — must run inside Claude Code.',
|
|
128
|
-
next: 'Run this command from within a Claude Code session.',
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
const cwd = typeof input['cwd'] === 'string' ? input['cwd'] : process.cwd();
|
|
132
|
-
const { jobId } = createJob('fork', { cwd });
|
|
133
|
-
const promptWithSubmit = `Fork of session ${parentSessionId}
|
|
134
|
-
|
|
135
|
-
---
|
|
136
|
-
When your task is complete, submit your result:
|
|
137
|
-
\`\`\`bash
|
|
138
|
-
crtr job submit ${jobId} --context-file /tmp/result.json
|
|
139
|
-
\`\`\`
|
|
140
|
-
If you cannot complete the task, still submit with status "failed" and a reason.`;
|
|
141
|
-
const result = spawnAgent({
|
|
142
|
-
prompt: promptWithSubmit,
|
|
143
|
-
cwd,
|
|
144
|
-
jobId,
|
|
145
|
-
fork: { sessionId: parentSessionId },
|
|
146
|
-
maxPanesPerWindow: resolveMaxPanes(),
|
|
147
|
-
});
|
|
148
|
-
if (result.status === 'not-in-tmux') {
|
|
149
|
-
throw new InputError({
|
|
150
|
-
error: 'not_in_tmux',
|
|
151
|
-
message: result.message,
|
|
152
|
-
next: 'Run inside a tmux session.',
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
if (result.status === 'spawn-failed') {
|
|
156
|
-
throw new InputError({
|
|
157
|
-
error: 'spawn_failed',
|
|
158
|
-
message: result.message,
|
|
159
|
-
next: 'Check tmux is running and try again.',
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
const forkPaneLabel = result.paneId !== undefined ? result.paneId : 'unknown';
|
|
163
|
-
appendEvent(jobId, { level: 'info', event: 'worker_started', message: `forked pane ${forkPaneLabel} spawned` });
|
|
164
|
-
return { job_id: jobId, follow_up: followUpResult(jobId) };
|
|
165
|
-
},
|
|
166
|
-
});
|
|
167
|
-
const startPlanner = defineLeaf({
|
|
168
|
-
name: 'planner',
|
|
169
|
-
help: {
|
|
170
|
-
name: 'job start planner',
|
|
171
|
-
summary: 'launch a planning agent for an approved spec; closes the originating pane after handoff',
|
|
172
|
-
params: [
|
|
173
|
-
{ kind: 'positional', name: 'spec_path', type: 'path', required: true, constraint: 'Absolute path to the spec file.' },
|
|
174
|
-
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Working directory. Defaults to process.cwd().' },
|
|
175
|
-
],
|
|
176
|
-
output: [
|
|
177
|
-
{ name: 'job_id', type: 'string', required: true, constraint: 'Use with `job read *` and `job cancel`.' },
|
|
178
|
-
{ name: 'follow_up', type: 'string', required: true, constraint: 'Recommended next call.' },
|
|
179
|
-
],
|
|
180
|
-
outputKind: 'object',
|
|
181
|
-
effects: [
|
|
182
|
-
'Spawns a planner agent in a sibling tmux pane.',
|
|
183
|
-
'Closes the originating pane after a short delay.',
|
|
184
|
-
'Creates a job entry and result sidecar.',
|
|
185
|
-
],
|
|
186
|
-
},
|
|
187
|
-
run: async (input) => {
|
|
188
|
-
assertTmux();
|
|
189
|
-
const specPath = input['spec_path'];
|
|
190
|
-
const cwd = typeof input['cwd'] === 'string' ? input['cwd'] : process.cwd();
|
|
191
|
-
if (!existsSync(specPath)) {
|
|
192
|
-
throw new InputError({
|
|
193
|
-
error: 'not_found',
|
|
194
|
-
message: `spec not found: ${specPath}`,
|
|
195
|
-
field: 'spec_path',
|
|
196
|
-
next: 'Provide an absolute path to an existing spec file.',
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
const { jobId } = createJob('planner', { cwd });
|
|
200
|
-
const result = spawnAndDetach({
|
|
201
|
-
prompt: planHandoffPrompt(specPath, jobId),
|
|
202
|
-
cwd,
|
|
203
|
-
jobId,
|
|
204
|
-
placement: 'split-h',
|
|
205
|
-
killAfterSeconds: DEFAULT_KILL_SECS,
|
|
206
|
-
});
|
|
207
|
-
if (result.status === 'not-in-tmux') {
|
|
208
|
-
throw new InputError({
|
|
209
|
-
error: 'not_in_tmux',
|
|
210
|
-
message: result.message,
|
|
211
|
-
next: 'Run inside a tmux session.',
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
if (result.status === 'spawn-failed') {
|
|
215
|
-
throw new InputError({
|
|
216
|
-
error: 'spawn_failed',
|
|
217
|
-
message: result.message,
|
|
218
|
-
next: 'Check tmux is running and try again.',
|
|
219
|
-
});
|
|
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
|
-
const startImplementer = defineLeaf({
|
|
227
|
-
name: 'implementer',
|
|
228
|
-
help: {
|
|
229
|
-
name: 'job start implementer',
|
|
230
|
-
summary: 'launch an implementation agent for an approved plan; closes the originating pane after handoff',
|
|
231
|
-
params: [
|
|
232
|
-
{ kind: 'positional', name: 'plan_path', type: 'path', required: true, constraint: 'Absolute path to the plan file.' },
|
|
233
|
-
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Working directory. Defaults to process.cwd().' },
|
|
234
|
-
],
|
|
235
|
-
output: [
|
|
236
|
-
{ name: 'job_id', type: 'string', required: true, constraint: 'Use with `job read *` and `job cancel`.' },
|
|
237
|
-
{ name: 'follow_up', type: 'string', required: true, constraint: 'Recommended next call.' },
|
|
238
|
-
],
|
|
239
|
-
outputKind: 'object',
|
|
240
|
-
effects: [
|
|
241
|
-
'Spawns an implementer agent in a sibling tmux pane.',
|
|
242
|
-
'Closes the originating pane after a short delay.',
|
|
243
|
-
'Creates a job entry and result sidecar.',
|
|
244
|
-
],
|
|
245
|
-
},
|
|
246
|
-
run: async (input) => {
|
|
247
|
-
assertTmux();
|
|
248
|
-
const planPath = input['plan_path'];
|
|
249
|
-
const cwd = typeof input['cwd'] === 'string' ? input['cwd'] : process.cwd();
|
|
250
|
-
if (!existsSync(planPath)) {
|
|
251
|
-
throw new InputError({
|
|
252
|
-
error: 'not_found',
|
|
253
|
-
message: `plan not found: ${planPath}`,
|
|
254
|
-
field: 'plan_path',
|
|
255
|
-
next: 'Provide an absolute path to an existing plan file.',
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
const { jobId } = createJob('implementer', { cwd });
|
|
259
|
-
const result = spawnAndDetach({
|
|
260
|
-
prompt: implementHandoffPrompt(planPath, jobId),
|
|
261
|
-
cwd,
|
|
262
|
-
jobId,
|
|
263
|
-
placement: 'split-h',
|
|
264
|
-
killAfterSeconds: DEFAULT_KILL_SECS,
|
|
265
|
-
});
|
|
266
|
-
if (result.status === 'not-in-tmux') {
|
|
267
|
-
throw new InputError({
|
|
268
|
-
error: 'not_in_tmux',
|
|
269
|
-
message: result.message,
|
|
270
|
-
next: 'Check tmux is running and try again.',
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
if (result.status === 'spawn-failed') {
|
|
274
|
-
throw new InputError({
|
|
275
|
-
error: 'spawn_failed',
|
|
276
|
-
message: result.message,
|
|
277
|
-
next: 'Check tmux is running and try again.',
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
const implPaneLabel = result.paneId !== undefined ? result.paneId : 'unknown';
|
|
281
|
-
appendEvent(jobId, { level: 'info', event: 'worker_started', message: `implementer pane ${implPaneLabel} spawned` });
|
|
282
|
-
return { job_id: jobId, follow_up: followUpResult(jobId) };
|
|
283
|
-
},
|
|
284
|
-
});
|
|
285
|
-
const startReviewer = defineLeaf({
|
|
286
|
-
name: 'reviewer',
|
|
287
|
-
help: {
|
|
288
|
-
name: 'job start reviewer',
|
|
289
|
-
summary: 'launch a reviewer agent for a plan or spec artifact; the originating pane stays alive to collect the verdict',
|
|
290
|
-
params: [
|
|
291
|
-
{ kind: 'positional', name: 'artifact_path', type: 'path', required: true, constraint: 'Absolute path to the artifact to review.' },
|
|
292
|
-
{ kind: 'flag', name: 'kind', type: 'enum', choices: ['plan', 'spec'], required: true, constraint: 'Artifact kind to review.' },
|
|
293
|
-
{ kind: 'flag', name: 'spec-path', type: 'path', required: false, constraint: 'Absolute path to the spec, for plan reviews. Omit for spec reviews.' },
|
|
294
|
-
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Working directory. Defaults to process.cwd().' },
|
|
295
|
-
],
|
|
296
|
-
output: [
|
|
297
|
-
{ name: 'job_id', type: 'string', required: true, constraint: 'Use with `job read *` and `job cancel`.' },
|
|
298
|
-
{ name: 'follow_up', type: 'string', required: true, constraint: 'Recommended next call.' },
|
|
299
|
-
],
|
|
300
|
-
outputKind: 'object',
|
|
301
|
-
effects: [
|
|
302
|
-
'Spawns a reviewer agent in a sibling tmux pane.',
|
|
303
|
-
'The originating pane stays alive — wait on the result and act on the verdict.',
|
|
304
|
-
'Creates a job entry and result sidecar.',
|
|
305
|
-
],
|
|
306
|
-
},
|
|
307
|
-
run: async (input) => {
|
|
308
|
-
assertTmux();
|
|
309
|
-
const artifactPath = input['artifact_path'];
|
|
310
|
-
const artifactKind = input['kind'];
|
|
311
|
-
const specPath = typeof input['specPath'] === 'string' ? input['specPath'] : undefined;
|
|
312
|
-
const cwd = typeof input['cwd'] === 'string' ? input['cwd'] : process.cwd();
|
|
313
|
-
if (!existsSync(artifactPath)) {
|
|
314
|
-
throw new InputError({
|
|
315
|
-
error: 'not_found',
|
|
316
|
-
message: `artifact not found: ${artifactPath}`,
|
|
317
|
-
field: 'artifact_path',
|
|
318
|
-
next: 'Provide an absolute path to an existing artifact file.',
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
const { jobId } = createJob('reviewer', { cwd });
|
|
322
|
-
// The reviewer is a subordinate the caller waits on (verdict → revise or
|
|
323
|
-
// hand off), NOT a handoff successor. Use spawnAgent so the originating
|
|
324
|
-
// pane (planner/orchestrator) stays alive to collect the result; do not
|
|
325
|
-
// self-kill the caller the way planner/implementer handoffs do.
|
|
326
|
-
const result = spawnAgent({
|
|
327
|
-
prompt: reviewerHandoffPrompt(artifactPath, artifactKind, specPath !== undefined ? specPath : null, jobId),
|
|
328
|
-
cwd,
|
|
329
|
-
jobId,
|
|
330
|
-
maxPanesPerWindow: resolveMaxPanes(),
|
|
331
|
-
});
|
|
332
|
-
if (result.status === 'not-in-tmux') {
|
|
333
|
-
throw new InputError({
|
|
334
|
-
error: 'not_in_tmux',
|
|
335
|
-
message: result.message,
|
|
336
|
-
next: 'Run inside a tmux session.',
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
if (result.status === 'spawn-failed') {
|
|
340
|
-
throw new InputError({
|
|
341
|
-
error: 'spawn_failed',
|
|
342
|
-
message: result.message,
|
|
343
|
-
next: 'Check tmux is running and try again.',
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
const reviewerPaneLabel = result.paneId !== undefined ? result.paneId : 'unknown';
|
|
347
|
-
appendEvent(jobId, { level: 'info', event: 'worker_started', message: `reviewer pane ${reviewerPaneLabel} spawned` });
|
|
348
|
-
return { job_id: jobId, follow_up: followUpResult(jobId) };
|
|
349
|
-
},
|
|
350
|
-
});
|
|
351
|
-
const startBranch = defineBranch({
|
|
352
|
-
name: 'start',
|
|
353
|
-
help: {
|
|
354
|
-
name: 'job start',
|
|
355
|
-
summary: 'spawn agent workers; all return a job handle immediately',
|
|
356
|
-
children: [
|
|
357
|
-
{ name: 'prompt', desc: 'fresh agent with a prompt', useWhen: 'spawning a general-purpose agent' },
|
|
358
|
-
{ name: 'fork', desc: 'fork current session into a sibling pane', useWhen: 'branching the current session\'s context into a new agent' },
|
|
359
|
-
{ name: 'planner', desc: 'planning agent for a spec', useWhen: 'handing off spec → plan decomposition' },
|
|
360
|
-
{ name: 'implementer', desc: 'implementation agent for a plan', useWhen: 'handing off plan → code implementation' },
|
|
361
|
-
{ name: 'reviewer', desc: 'review agent for a plan or spec', useWhen: 'launching a review of a plan or spec artifact' },
|
|
362
|
-
],
|
|
363
|
-
},
|
|
364
|
-
children: [startPrompt, startFork, startPlanner, startImplementer, startReviewer],
|
|
365
|
-
});
|
|
366
24
|
// ---------------------------------------------------------------------------
|
|
367
25
|
// read sub-branch
|
|
368
26
|
// ---------------------------------------------------------------------------
|
|
@@ -406,7 +64,7 @@ const readStatus = defineLeaf({
|
|
|
406
64
|
name: 'job read status',
|
|
407
65
|
summary: 'read the current status of a job',
|
|
408
66
|
params: [
|
|
409
|
-
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Job id from a `
|
|
67
|
+
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Job id from a producer (e.g. `crtr agent new *`).' },
|
|
410
68
|
],
|
|
411
69
|
output: [
|
|
412
70
|
{ name: 'job_id', type: 'string', required: true, constraint: 'Echo of input.' },
|
|
@@ -434,7 +92,7 @@ const readLogs = defineLeaf({
|
|
|
434
92
|
name: 'job read logs',
|
|
435
93
|
summary: 'read log events from a job; emits JSONL — one event object per line',
|
|
436
94
|
params: [
|
|
437
|
-
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Job id from a `
|
|
95
|
+
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Job id from a producer (e.g. `crtr agent new *`).' },
|
|
438
96
|
{ kind: 'flag', name: 'since', type: 'string', required: false, constraint: 'ISO 8601 timestamp. Only emit events at or after this time.' },
|
|
439
97
|
{ kind: 'flag', name: 'until', type: 'string', required: false, constraint: 'ISO 8601 timestamp. Only emit events before this time.' },
|
|
440
98
|
{ kind: 'flag', name: 'level', type: 'enum', choices: ['debug', 'info', 'warn', 'error'], required: false, default: 'info', constraint: 'Minimum severity. Default: info.' },
|
|
@@ -478,9 +136,7 @@ const readLogs = defineLeaf({
|
|
|
478
136
|
const terminalStates = new Set(['done', 'failed', 'canceled']);
|
|
479
137
|
await new Promise((resolve) => {
|
|
480
138
|
const poll = () => {
|
|
481
|
-
// Check terminal state first.
|
|
482
139
|
const status = jobStatus(jobId);
|
|
483
|
-
// Emit any new events since lastTs.
|
|
484
140
|
const newEvents = readLog(jobId, { sinceTs: lastTs !== new Date(0).toISOString() ? lastTs : undefined, minLevel });
|
|
485
141
|
for (const ev of newEvents) {
|
|
486
142
|
const e = ev;
|
|
@@ -505,13 +161,15 @@ const readResult = defineLeaf({
|
|
|
505
161
|
name: 'job read result',
|
|
506
162
|
summary: 'read the final result of a completed job',
|
|
507
163
|
params: [
|
|
508
|
-
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Job id from a `
|
|
509
|
-
{ kind: 'flag', name: 'wait', type: 'bool', required: false, constraint: 'When present, blocks until result
|
|
164
|
+
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Job id from a producer (e.g. `crtr agent new *`).' },
|
|
165
|
+
{ kind: 'flag', name: 'wait', type: 'bool', required: false, constraint: 'When present, blocks until a result file appears (up to 10 min).' },
|
|
510
166
|
],
|
|
511
167
|
output: [
|
|
512
168
|
{ name: 'job_id', type: 'string', required: true, constraint: 'Echo of input.' },
|
|
513
169
|
{ name: 'status', type: 'string', required: true, constraint: 'One of: done, failed, canceled, timeout.' },
|
|
514
|
-
{ name: '
|
|
170
|
+
{ name: 'result_md', type: 'string', required: false, constraint: 'Markdown body submitted by an agent via `crtr job submit`. Present when the job used the agent submit path.' },
|
|
171
|
+
{ name: 'result', type: 'object', required: false, constraint: 'Structured object submitted by a programmatic caller (human/sys). Present when the job used the programmatic submit path.' },
|
|
172
|
+
{ name: 'reason', type: 'string', required: false, constraint: 'Failure reason from frontmatter when status is failed and the agent submit path was used.' },
|
|
515
173
|
],
|
|
516
174
|
outputKind: 'object',
|
|
517
175
|
effects: ['None. Read-only.'],
|
|
@@ -524,6 +182,12 @@ const readResult = defineLeaf({
|
|
|
524
182
|
if (r.result !== undefined) {
|
|
525
183
|
out['result'] = r.result;
|
|
526
184
|
}
|
|
185
|
+
if (r.result_md !== undefined) {
|
|
186
|
+
out['result_md'] = r.result_md;
|
|
187
|
+
}
|
|
188
|
+
if (r.reason !== undefined) {
|
|
189
|
+
out['reason'] = r.reason;
|
|
190
|
+
}
|
|
527
191
|
return out;
|
|
528
192
|
},
|
|
529
193
|
});
|
|
@@ -542,16 +206,19 @@ const readBranch = defineBranch({
|
|
|
542
206
|
children: [readList, readStatus, readLogs, readResult],
|
|
543
207
|
});
|
|
544
208
|
// ---------------------------------------------------------------------------
|
|
545
|
-
// submit — called by the worker inside its pane
|
|
209
|
+
// submit — called by the worker inside its pane (or by any producer that
|
|
210
|
+
// writes a result programmatically)
|
|
546
211
|
// ---------------------------------------------------------------------------
|
|
547
212
|
const jobSubmit = defineLeaf({
|
|
548
213
|
name: 'submit',
|
|
549
214
|
help: {
|
|
550
215
|
name: 'job submit',
|
|
551
|
-
summary: '
|
|
216
|
+
summary: 'deliver a markdown result back to a job record (called by workers, or any producer writing the terminal value)',
|
|
552
217
|
params: [
|
|
553
218
|
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Job id injected as $CRTR_JOB_ID in the spawned pane.' },
|
|
554
|
-
{ kind: '
|
|
219
|
+
{ kind: 'stdin', name: 'body', required: false, constraint: 'Markdown body of the result, piped on stdin. Required when --status is done (the default). When --status failed, stdin is optional; --reason carries the explanation.' },
|
|
220
|
+
{ kind: 'flag', name: 'status', type: 'enum', choices: ['done', 'failed'], required: false, default: 'done', constraint: 'Terminal status to record. Default: done.' },
|
|
221
|
+
{ kind: 'flag', name: 'reason', type: 'string', required: false, constraint: 'Short failure reason. Required when --status failed; ignored otherwise.' },
|
|
555
222
|
{ kind: 'flag', name: 'kill-pane', type: 'bool', required: false, constraint: `When present, schedule the current tmux pane to close ${DEFAULT_KILL_SECS}s after submission so the spawned worker does not linger. Reviewer agents should pass this; planner/implementer handoffs already self-kill on spawn.` },
|
|
556
223
|
],
|
|
557
224
|
output: [
|
|
@@ -560,24 +227,34 @@ const jobSubmit = defineLeaf({
|
|
|
560
227
|
],
|
|
561
228
|
outputKind: 'object',
|
|
562
229
|
effects: [
|
|
563
|
-
'Writes result.
|
|
564
|
-
'Updates meta.json status to
|
|
230
|
+
'Writes <jobdir>/result.md atomically (YAML frontmatter + body), marking the job done or failed.',
|
|
231
|
+
'Updates meta.json status to match.',
|
|
565
232
|
`When --kill-pane is set, schedules \`tmux kill-pane\` on $TMUX_PANE after ${DEFAULT_KILL_SECS}s (detached; submit still returns cleanly).`,
|
|
566
233
|
],
|
|
567
234
|
},
|
|
568
235
|
run: async (input) => {
|
|
569
236
|
const jobId = input['job_id'];
|
|
570
|
-
const
|
|
571
|
-
|
|
237
|
+
const status = (typeof input['status'] === 'string' ? input['status'] : 'done');
|
|
238
|
+
const body = typeof input['body'] === 'string' ? input['body'] : '';
|
|
239
|
+
const reason = typeof input['reason'] === 'string' ? input['reason'] : '';
|
|
240
|
+
const killPane = input['killPane'] === true;
|
|
241
|
+
if (status === 'done' && body.trim() === '') {
|
|
572
242
|
throw new InputError({
|
|
573
243
|
error: 'invalid_field',
|
|
574
|
-
message: '
|
|
575
|
-
field: '
|
|
576
|
-
next:
|
|
244
|
+
message: '--status done requires a markdown body on stdin.',
|
|
245
|
+
field: 'body',
|
|
246
|
+
next: `Pipe the markdown result on stdin, e.g. \`crtr job submit ${jobId} <<'MD' ... MD\`. For failures, use \`--status failed --reason "<why>"\`.`,
|
|
577
247
|
});
|
|
578
248
|
}
|
|
579
|
-
|
|
580
|
-
|
|
249
|
+
if (status === 'failed' && reason.trim() === '') {
|
|
250
|
+
throw new InputError({
|
|
251
|
+
error: 'invalid_field',
|
|
252
|
+
message: '--status failed requires --reason "<text>".',
|
|
253
|
+
field: 'reason',
|
|
254
|
+
next: 'Pass --reason explaining why the task could not complete.',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
writeMarkdownResult(jobId, body, status, status === 'failed' ? reason : undefined);
|
|
581
258
|
const paneKillScheduled = killPane ? scheduleKillCurrentPane(DEFAULT_KILL_SECS) : false;
|
|
582
259
|
return { submitted: true, pane_kill_scheduled: paneKillScheduled };
|
|
583
260
|
},
|
|
@@ -591,20 +268,19 @@ const jobFail = defineLeaf({
|
|
|
591
268
|
name: 'job _fail',
|
|
592
269
|
summary: 'internal: mark a job failed if it has not already been submitted (called by wrapper shell)',
|
|
593
270
|
params: [
|
|
594
|
-
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Job id. If result
|
|
271
|
+
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Job id. If a result file already exists, this is a no-op.' },
|
|
595
272
|
],
|
|
596
273
|
output: [
|
|
597
|
-
{ name: 'recorded', type: 'boolean', required: true, constraint: 'True if failure was recorded; false if result
|
|
274
|
+
{ name: 'recorded', type: 'boolean', required: true, constraint: 'True if failure was recorded; false if a result file already existed (no-op).' },
|
|
598
275
|
],
|
|
599
276
|
outputKind: 'object',
|
|
600
277
|
effects: [
|
|
601
|
-
'Writes result.
|
|
278
|
+
'Writes result.md with status "failed" and a reason if no result file is present.',
|
|
602
279
|
'Updates meta.json status to failed.',
|
|
603
280
|
],
|
|
604
281
|
},
|
|
605
282
|
run: async (input) => {
|
|
606
283
|
const jobId = input['job_id'];
|
|
607
|
-
// No-op if result.json already exists (worker submitted successfully).
|
|
608
284
|
try {
|
|
609
285
|
const existing = await jobsReadResult(jobId, { waitMs: 0 });
|
|
610
286
|
if (existing.status !== 'timeout') {
|
|
@@ -615,7 +291,7 @@ const jobFail = defineLeaf({
|
|
|
615
291
|
// job dir not found — still try to write to surface the failure
|
|
616
292
|
}
|
|
617
293
|
try {
|
|
618
|
-
|
|
294
|
+
writeMarkdownResult(jobId, '', 'failed', 'worker exited without submitting');
|
|
619
295
|
return { recorded: true };
|
|
620
296
|
}
|
|
621
297
|
catch {
|
|
@@ -632,7 +308,7 @@ const jobCancel = defineLeaf({
|
|
|
632
308
|
name: 'job cancel',
|
|
633
309
|
summary: 'send a best-effort cancellation signal to a running job',
|
|
634
310
|
params: [
|
|
635
|
-
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Job id from a `
|
|
311
|
+
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Job id from a producer (e.g. `crtr agent new *`).' },
|
|
636
312
|
],
|
|
637
313
|
output: [
|
|
638
314
|
{ name: 'canceled', type: 'boolean', required: true, constraint: 'True if a signal was delivered or the job was already terminal; false if the job was not live.' },
|
|
@@ -654,16 +330,15 @@ export function registerJob() {
|
|
|
654
330
|
name: 'job',
|
|
655
331
|
help: {
|
|
656
332
|
name: 'job',
|
|
657
|
-
summary: '
|
|
658
|
-
model: '
|
|
333
|
+
summary: 'monitor and collect results from any ongoing task',
|
|
334
|
+
model: 'A job is a producer-agnostic record of an ongoing task: state, logs, terminal result. Producers (`crtr agent new *`, future task systems) create jobs; this subtree is the shared read/cancel/submit surface. States: live | done | failed | canceled.',
|
|
659
335
|
children: [
|
|
660
|
-
{ name: 'start', desc: 'spawn agent workers', useWhen: 'launching a new agent job' },
|
|
661
336
|
{ name: 'read', desc: 'read status, logs, or results', useWhen: 'monitoring or collecting from a running or completed job' },
|
|
662
|
-
{ name: 'submit', desc: 'deliver result from inside a
|
|
337
|
+
{ name: 'submit', desc: 'deliver result from inside a worker pane or any producer', useWhen: 'a worker is ready to return its output' },
|
|
663
338
|
{ name: '_fail', desc: 'internal: mark job failed on unsubmitted exit', useWhen: 'called by the wrapper shell, not manually' },
|
|
664
339
|
{ name: 'cancel', desc: 'best-effort cancel a live job', useWhen: 'stopping a job that is no longer needed' },
|
|
665
340
|
],
|
|
666
341
|
},
|
|
667
|
-
children: [
|
|
342
|
+
children: [readBranch, jobSubmit, jobFail, jobCancel],
|
|
668
343
|
});
|
|
669
344
|
}
|
package/dist/commands/plan.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const PLAN_NEW_GUIDE = "## Planning workflow\n\nBuild and save an implementation plan: a map another agent can execute without\nre-discovering context. Work through these phases before saving.\n\n### Phase 1: Understand\n\nBuild a full picture of the request and the code. Search for reusable\nfunctions, patterns, and existing implementations before proposing new ones.\n\nLaunch up to 3 Explore subagents IN PARALLEL (single message, multiple tool\ncalls). Use 1 agent for isolated, small-scope tasks; use more when scope is\nuncertain or multiple subsystems are involved. Give each a distinct focus so\nthey don't duplicate work.\n\n### Phase 2: Design\n\nDesign the implementation from Phase 1 findings. Default to launching at least\n1 Plan agent to validate your understanding and surface alternatives. Skip only\nfor trivially small tasks (typo fixes, single-line renames). Use up to 3 agents\nfor large refactors or architectural changes.\n\nIn the Plan agent prompt: include file paths, code-path traces, requirements,\nand constraints from Phase 1. Request a detailed implementation plan.\n\n### Phase 3: Review findings\n\nRead the critical files identified by agents. Confirm the plan aligns with the\nuser's request. Use AskUserQuestion ONLY to clarify requirements or choose\nbetween approaches \u2014 never to ask \"is this okay?\" or \"should I proceed?\".\n\n### Phase 4: Compose the plan body\n\nQuality bar \u2014 every item below is cheap to satisfy and saves the implementer\nfrom re-deciding:\n\n- Every decision pinned. No \"if X then Y\" branches, no \"investigate whether\u2026\",\n no deferred choices. If you don't know, find out or ask now.\n- No timelines, no fallbacks, no magic values, no \"for now\" shortcuts.\n- Where the plan creates a new interface, schema, or contract, write the actual\n shape, not \"design a Foo type.\"\n\nRequired sections:\n\n # Plan: <one-line title>\n\n ## Context\n <why this change is being made \u2014 the problem, what prompted it, intended outcome>\n\n ## Recommended approach\n <your chosen approach only. Concise enough to scan, detailed enough to execute.>\n\n ## Files to modify / create\n - `path/to/file.ts` \u2014 <what changes>\n\n ## Existing utilities to reuse\n - `functionName` from `path/to/file.ts:LL` \u2014 <why it fits>\n\n ## Verification\n <how to test end-to-end \u2014 run the code, run tests, etc.>\n\nFor plans touching 4+ files across distinct concerns, structure parallel tasks:\n\n ## Tasks\n - **Task 1**: <name>\n - Files: `a.ts`, `b.ts` (disjoint from other tasks)\n - Depends on: (none) | Task N\n - Integration: <shared types/APIs with exact shape>\n - Changes: <bullets>\n\nSkip the Tasks structure for small plans; it's noise when there's no\nparallelism to unlock.\n\n### Phase 5: Save\n\nRun `crtr
|
|
1
|
+
export declare const PLAN_NEW_GUIDE = "## Planning workflow\n\nBuild and save an implementation plan: a map another agent can execute without\nre-discovering context. Work through these phases before saving.\n\n### Phase 1: Understand\n\nBuild a full picture of the request and the code. Search for reusable\nfunctions, patterns, and existing implementations before proposing new ones.\n\nLaunch up to 3 Explore subagents IN PARALLEL (single message, multiple tool\ncalls). Use 1 agent for isolated, small-scope tasks; use more when scope is\nuncertain or multiple subsystems are involved. Give each a distinct focus so\nthey don't duplicate work.\n\n### Phase 2: Design\n\nDesign the implementation from Phase 1 findings. Default to launching at least\n1 Plan agent to validate your understanding and surface alternatives. Skip only\nfor trivially small tasks (typo fixes, single-line renames). Use up to 3 agents\nfor large refactors or architectural changes.\n\nIn the Plan agent prompt: include file paths, code-path traces, requirements,\nand constraints from Phase 1. Request a detailed implementation plan.\n\n### Phase 3: Review findings\n\nRead the critical files identified by agents. Confirm the plan aligns with the\nuser's request. Use AskUserQuestion ONLY to clarify requirements or choose\nbetween approaches \u2014 never to ask \"is this okay?\" or \"should I proceed?\".\n\n### Phase 4: Compose the plan body\n\nQuality bar \u2014 every item below is cheap to satisfy and saves the implementer\nfrom re-deciding:\n\n- Every decision pinned. No \"if X then Y\" branches, no \"investigate whether\u2026\",\n no deferred choices. If you don't know, find out or ask now.\n- No timelines, no fallbacks, no magic values, no \"for now\" shortcuts.\n- Where the plan creates a new interface, schema, or contract, write the actual\n shape, not \"design a Foo type.\"\n\nRequired sections:\n\n # Plan: <one-line title>\n\n ## Context\n <why this change is being made \u2014 the problem, what prompted it, intended outcome>\n\n ## Recommended approach\n <your chosen approach only. Concise enough to scan, detailed enough to execute.>\n\n ## Files to modify / create\n - `path/to/file.ts` \u2014 <what changes>\n\n ## Existing utilities to reuse\n - `functionName` from `path/to/file.ts:LL` \u2014 <why it fits>\n\n ## Verification\n <how to test end-to-end \u2014 run the code, run tests, etc.>\n\nFor plans touching 4+ files across distinct concerns, structure parallel tasks:\n\n ## Tasks\n - **Task 1**: <name>\n - Files: `a.ts`, `b.ts` (disjoint from other tasks)\n - Depends on: (none) | Task N\n - Integration: <shared types/APIs with exact shape>\n - Changes: <bullets>\n\nSkip the Tasks structure for small plans; it's noise when there's no\nparallelism to unlock.\n\n### Phase 5: Save\n\nRun `crtr agent plan new`:\n\n echo '<plan markdown>' | crtr agent plan new <kebab-case-name> [--spec <spec-name>]\n\n- NAME: short kebab-case slug. Nested names become subdirectories\n (e.g. `auth/jwt-refresh`).\n- Pipe the full plan markdown composed in Phase 4 on stdin.\n- `--spec` (optional): name of the spec this plan implements. Enables alignment\n check by the reviewer.\n\nOutput: `{path, follow_up}`. The `follow_up` field names the exact next call\n\u2014 run it.\n\n### Phase 6: Oversize check\n\nIf `follow_up` contains an oversize advisory (plan exceeds 200 lines), split\ninto a short index plan plus nested part plans, each under the threshold.\nRe-save. The implementer executes parts one at a time; long monolithic plans\nare under-decomposed.\n\n### Phase 7: Done\n\nAfter the reviewer approves the plan, your turn ends. Do not summarize in chat.\nFor a human gate, optionally put the plan in front of a person with `crtr\nhuman review` (anchored comments) and gate the handoff with `crtr human\napprove`. This complements \u2014 it does not replace \u2014 `crtr agent new reviewer`.\nIf the user is ready to build, ask once whether to hand off; if yes, run:\n`crtr agent new implementer` with the plan path.";
|
|
2
2
|
export declare const PLAN_SHOW_GUIDE = "";
|
|
3
3
|
import type { BranchDef } from '../core/command.js';
|
|
4
4
|
export declare function registerPlan(): BranchDef;
|