@crouton-kit/crouter 0.1.3 → 0.1.6
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 +14 -1
- package/dist/cli.js +5 -0
- package/dist/commands/agent.d.ts +2 -0
- package/dist/commands/agent.js +265 -0
- package/dist/commands/config.js +13 -1
- package/dist/commands/plan.js +17 -1
- package/dist/commands/skill.js +62 -16
- package/dist/commands/spec.js +4 -0
- package/dist/core/artifact.d.ts +12 -0
- package/dist/core/artifact.js +68 -6
- package/dist/core/bootstrap.d.ts +5 -0
- package/dist/core/bootstrap.js +149 -0
- package/dist/core/config.js +6 -1
- package/dist/core/resolver.d.ts +1 -0
- package/dist/core/resolver.js +66 -6
- package/dist/core/scope.d.ts +1 -0
- package/dist/core/scope.js +4 -0
- package/dist/core/spawn.d.ts +95 -0
- package/dist/core/spawn.js +309 -0
- package/dist/prompts/agent.d.ts +13 -0
- package/dist/prompts/agent.js +114 -0
- package/dist/prompts/plan.js +39 -3
- package/dist/prompts/review.d.ts +2 -0
- package/dist/prompts/review.js +103 -0
- package/dist/prompts/skill.d.ts +1 -0
- package/dist/prompts/skill.js +240 -8
- package/dist/prompts/spec.js +28 -3
- package/dist/types.d.ts +4 -0
- package/dist/types.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { mkdirSync, watch, readFileSync, existsSync, writeFileSync, renameSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { general, notFound } from './errors.js';
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
8
|
+
const PANE_POLL_MS = 2000;
|
|
9
|
+
export const DEFAULT_PANE_OPTS = {
|
|
10
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
11
|
+
};
|
|
12
|
+
function isInTmux() {
|
|
13
|
+
return Boolean(process.env.TMUX);
|
|
14
|
+
}
|
|
15
|
+
function sessionRoot() {
|
|
16
|
+
const root = join(tmpdir(), 'crtr-sessions');
|
|
17
|
+
mkdirSync(root, { recursive: true });
|
|
18
|
+
return root;
|
|
19
|
+
}
|
|
20
|
+
export function createSession() {
|
|
21
|
+
const id = randomUUID();
|
|
22
|
+
const dir = join(sessionRoot(), id);
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
return { id, dir };
|
|
25
|
+
}
|
|
26
|
+
export function submitToSession(sessionDir, content) {
|
|
27
|
+
const tmp = join(sessionDir, '.content.tmp');
|
|
28
|
+
const final = join(sessionDir, 'content');
|
|
29
|
+
writeFileSync(tmp, content, 'utf8');
|
|
30
|
+
renameSync(tmp, final);
|
|
31
|
+
}
|
|
32
|
+
function paneAlive(paneId) {
|
|
33
|
+
const result = spawnSync('tmux', ['list-panes', '-a', '-F', '#{pane_id}'], {
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
});
|
|
36
|
+
if (result.status !== 0)
|
|
37
|
+
return false;
|
|
38
|
+
return result.stdout.split('\n').some((line) => line.trim() === paneId);
|
|
39
|
+
}
|
|
40
|
+
function killPane(paneId) {
|
|
41
|
+
spawnSync('tmux', ['kill-pane', '-t', paneId], { stdio: 'ignore' });
|
|
42
|
+
}
|
|
43
|
+
function shellQuote(s) {
|
|
44
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Fire-and-forget: launch an interactive `claude` in a new pane (or window),
|
|
48
|
+
* then schedule the originating pane to be killed after `killAfterSeconds`.
|
|
49
|
+
*
|
|
50
|
+
* No custom system prompt — the task is delivered as the first user message
|
|
51
|
+
* so the user can `/clear` to fall back to a normal default Claude session.
|
|
52
|
+
*
|
|
53
|
+
* Returns as soon as the new pane is up; does NOT wait for claude to finish.
|
|
54
|
+
*/
|
|
55
|
+
export function spawnAndDetach(opts) {
|
|
56
|
+
if (!isInTmux()) {
|
|
57
|
+
return {
|
|
58
|
+
status: 'not-in-tmux',
|
|
59
|
+
message: 'handoff requires tmux (TMUX env var not set)',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const claudeCmd = [
|
|
63
|
+
'claude',
|
|
64
|
+
'--dangerously-skip-permissions',
|
|
65
|
+
shellQuote(opts.prompt),
|
|
66
|
+
].join(' ');
|
|
67
|
+
const splitArgs = [];
|
|
68
|
+
if (opts.placement === 'new-window') {
|
|
69
|
+
splitArgs.push('new-window');
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
splitArgs.push('split-window');
|
|
73
|
+
splitArgs.push(opts.placement === 'split-h' ? '-h' : '-v');
|
|
74
|
+
}
|
|
75
|
+
splitArgs.push('-P', '-F', '#{pane_id}');
|
|
76
|
+
splitArgs.push('-c', opts.cwd);
|
|
77
|
+
splitArgs.push(claudeCmd);
|
|
78
|
+
const split = spawnSync('tmux', splitArgs, { encoding: 'utf8' });
|
|
79
|
+
if (split.status !== 0) {
|
|
80
|
+
const stderrText = split.stderr.trim();
|
|
81
|
+
const msg = stderrText === '' ? 'tmux split-window/new-window failed' : stderrText;
|
|
82
|
+
return { status: 'spawn-failed', message: msg };
|
|
83
|
+
}
|
|
84
|
+
const paneId = split.stdout.trim();
|
|
85
|
+
// Schedule self-kill of the originating pane. We detach this so it survives
|
|
86
|
+
// crtr's exit.
|
|
87
|
+
const currentPane = process.env.TMUX_PANE;
|
|
88
|
+
if (currentPane !== undefined && currentPane !== '' && opts.killAfterSeconds > 0) {
|
|
89
|
+
const killCmd = `sleep ${opts.killAfterSeconds}; tmux kill-pane -t ${currentPane}`;
|
|
90
|
+
spawnSync('sh', ['-c', `nohup sh -c ${shellQuote(killCmd)} </dev/null >/dev/null 2>&1 &`], {
|
|
91
|
+
stdio: 'ignore',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
status: 'spawned',
|
|
96
|
+
paneId,
|
|
97
|
+
message: `handed off to pane ${paneId}; this pane will close in ${opts.killAfterSeconds}s`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Spawn a side-pane `claude` reviewer. Blocks until the reviewer calls
|
|
102
|
+
* `crtr agent submit <content>`, the 10-min budget elapses, or the pane is closed.
|
|
103
|
+
*
|
|
104
|
+
* No custom system prompt — the task is delivered as the first user message
|
|
105
|
+
* so the reviewer is a normal Claude session running a single task.
|
|
106
|
+
*/
|
|
107
|
+
export async function spawnSidePaneReview(opts) {
|
|
108
|
+
if (!isInTmux()) {
|
|
109
|
+
throw general('side-pane review requires tmux (TMUX env var not set)');
|
|
110
|
+
}
|
|
111
|
+
const session = createSession();
|
|
112
|
+
const timeoutMs = opts.timeoutMs;
|
|
113
|
+
const cwd = opts.cwd;
|
|
114
|
+
const claudeCmd = [
|
|
115
|
+
'claude',
|
|
116
|
+
'-p',
|
|
117
|
+
'--dangerously-skip-permissions',
|
|
118
|
+
shellQuote(opts.prompt),
|
|
119
|
+
].join(' ');
|
|
120
|
+
// After claude exits, sleep briefly so the watcher can confirm submission.
|
|
121
|
+
// The watcher kills the pane anyway once content arrives.
|
|
122
|
+
const fullCmd = `cd ${shellQuote(cwd)} && ${claudeCmd}; sleep 2`;
|
|
123
|
+
const splitArgs = [
|
|
124
|
+
'split-window',
|
|
125
|
+
'-h',
|
|
126
|
+
'-P',
|
|
127
|
+
'-F',
|
|
128
|
+
'#{pane_id}',
|
|
129
|
+
'-e',
|
|
130
|
+
`CRTR_SESSION=${session.id}`,
|
|
131
|
+
'-e',
|
|
132
|
+
`CRTR_PIPE=${session.dir}`,
|
|
133
|
+
fullCmd,
|
|
134
|
+
];
|
|
135
|
+
const split = spawnSync('tmux', splitArgs, { encoding: 'utf8' });
|
|
136
|
+
if (split.status !== 0) {
|
|
137
|
+
rmSync(session.dir, { recursive: true, force: true });
|
|
138
|
+
const stderrText = split.stderr.trim();
|
|
139
|
+
const msg = stderrText === '' ? 'tmux split-window failed' : stderrText;
|
|
140
|
+
return {
|
|
141
|
+
status: 'spawn-failed',
|
|
142
|
+
content: msg,
|
|
143
|
+
sessionDir: session.dir,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const paneId = split.stdout.trim();
|
|
147
|
+
const contentPath = join(session.dir, 'content');
|
|
148
|
+
const result = await waitForResult(session.dir, contentPath, paneId, timeoutMs);
|
|
149
|
+
if (paneAlive(paneId))
|
|
150
|
+
killPane(paneId);
|
|
151
|
+
try {
|
|
152
|
+
rmSync(session.dir, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
/* noop */
|
|
156
|
+
}
|
|
157
|
+
return { ...result, paneId, sessionDir: session.dir };
|
|
158
|
+
}
|
|
159
|
+
function metaPath(sessionDir) {
|
|
160
|
+
return join(sessionDir, 'meta.json');
|
|
161
|
+
}
|
|
162
|
+
function writeSessionMeta(sessionDir, meta) {
|
|
163
|
+
writeFileSync(metaPath(sessionDir), JSON.stringify(meta), 'utf8');
|
|
164
|
+
}
|
|
165
|
+
function readSessionMeta(sessionDir) {
|
|
166
|
+
const p = metaPath(sessionDir);
|
|
167
|
+
if (!existsSync(p))
|
|
168
|
+
return undefined;
|
|
169
|
+
try {
|
|
170
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export function sessionDirForId(sessionId) {
|
|
177
|
+
return join(sessionRoot(), sessionId);
|
|
178
|
+
}
|
|
179
|
+
export function countPanesInCurrentWindow() {
|
|
180
|
+
// -t '' targets the current window of the current session.
|
|
181
|
+
const result = spawnSync('tmux', ['list-panes', '-F', '#{pane_id}'], {
|
|
182
|
+
encoding: 'utf8',
|
|
183
|
+
});
|
|
184
|
+
if (result.status !== 0)
|
|
185
|
+
return 0;
|
|
186
|
+
return result.stdout.split('\n').filter((line) => line.trim() !== '').length;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Async sibling spawn. Launches a claude session in a new tmux pane or window
|
|
190
|
+
* (depending on current pane count vs maxPanesPerWindow). Returns immediately
|
|
191
|
+
* with the crtr session id; the parent stays alive.
|
|
192
|
+
*
|
|
193
|
+
* If `fork` is set, uses `claude --resume <id> --fork-session` so the child
|
|
194
|
+
* gets a fresh session id and does not contend with the parent's JSONL.
|
|
195
|
+
*/
|
|
196
|
+
export function spawnAgent(opts) {
|
|
197
|
+
if (!isInTmux()) {
|
|
198
|
+
return {
|
|
199
|
+
status: 'not-in-tmux',
|
|
200
|
+
message: 'crtr agent requires tmux (TMUX env var not set)',
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const session = createSession();
|
|
204
|
+
const claudeParts = ['claude'];
|
|
205
|
+
if (opts.fork !== undefined) {
|
|
206
|
+
claudeParts.push('--resume', opts.fork.sessionId, '--fork-session');
|
|
207
|
+
}
|
|
208
|
+
claudeParts.push('--dangerously-skip-permissions', shellQuote(opts.prompt));
|
|
209
|
+
const claudeCmd = claudeParts.join(' ');
|
|
210
|
+
const useNewWindow = countPanesInCurrentWindow() >= opts.maxPanesPerWindow;
|
|
211
|
+
const placement = useNewWindow ? 'new-window' : 'split-window';
|
|
212
|
+
const tmuxArgs = [placement];
|
|
213
|
+
if (!useNewWindow)
|
|
214
|
+
tmuxArgs.push('-h');
|
|
215
|
+
tmuxArgs.push('-P', '-F', '#{pane_id}', '-c', opts.cwd, '-e', `CRTR_SESSION=${session.id}`, '-e', `CRTR_PIPE=${session.dir}`, claudeCmd);
|
|
216
|
+
const split = spawnSync('tmux', tmuxArgs, { encoding: 'utf8' });
|
|
217
|
+
if (split.status !== 0) {
|
|
218
|
+
rmSync(session.dir, { recursive: true, force: true });
|
|
219
|
+
const stderrText = split.stderr.trim();
|
|
220
|
+
const msg = stderrText === '' ? `tmux ${placement} failed` : stderrText;
|
|
221
|
+
return { status: 'spawn-failed', message: msg };
|
|
222
|
+
}
|
|
223
|
+
const paneId = split.stdout.trim();
|
|
224
|
+
writeSessionMeta(session.dir, {
|
|
225
|
+
paneId,
|
|
226
|
+
createdAt: Date.now(),
|
|
227
|
+
kind: opts.fork !== undefined ? 'fork' : 'new',
|
|
228
|
+
});
|
|
229
|
+
return {
|
|
230
|
+
status: 'spawned',
|
|
231
|
+
sessionId: session.id,
|
|
232
|
+
paneId,
|
|
233
|
+
placement,
|
|
234
|
+
message: `agent ${session.id} spawned in pane ${paneId} (${placement})`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Block until the agent identified by `sessionId` calls `crtr agent submit`.
|
|
239
|
+
* Returns content + status. Cleans up the session dir on completion.
|
|
240
|
+
*/
|
|
241
|
+
export async function awaitSession(sessionId, opts) {
|
|
242
|
+
const sessionDir = sessionDirForId(sessionId);
|
|
243
|
+
if (!existsSync(sessionDir)) {
|
|
244
|
+
throw notFound(`agent session not found: ${sessionId} (looked at ${sessionDir})`);
|
|
245
|
+
}
|
|
246
|
+
const meta = readSessionMeta(sessionDir);
|
|
247
|
+
let paneId;
|
|
248
|
+
if (meta !== undefined && meta.paneId !== '') {
|
|
249
|
+
paneId = meta.paneId;
|
|
250
|
+
}
|
|
251
|
+
const contentPath = join(sessionDir, 'content');
|
|
252
|
+
const result = await waitForResult(sessionDir, contentPath, paneId, opts.timeoutMs);
|
|
253
|
+
if (opts.killPane && paneId !== undefined && paneAlive(paneId))
|
|
254
|
+
killPane(paneId);
|
|
255
|
+
try {
|
|
256
|
+
rmSync(sessionDir, { recursive: true, force: true });
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
/* noop */
|
|
260
|
+
}
|
|
261
|
+
return { ...result, paneId, sessionDir };
|
|
262
|
+
}
|
|
263
|
+
function waitForResult(sessionDir, contentPath, paneId, timeoutMs) {
|
|
264
|
+
return new Promise((resolve) => {
|
|
265
|
+
let settled = false;
|
|
266
|
+
const finish = (status, content) => {
|
|
267
|
+
if (settled)
|
|
268
|
+
return;
|
|
269
|
+
settled = true;
|
|
270
|
+
clearTimeout(timeoutTimer);
|
|
271
|
+
if (paneTimer !== undefined)
|
|
272
|
+
clearInterval(paneTimer);
|
|
273
|
+
try {
|
|
274
|
+
watcher.close();
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
/* noop */
|
|
278
|
+
}
|
|
279
|
+
resolve({ status, content });
|
|
280
|
+
};
|
|
281
|
+
const watcher = watch(sessionDir, (_event, name) => {
|
|
282
|
+
if (name === 'content' && existsSync(contentPath)) {
|
|
283
|
+
const content = readFileSync(contentPath, 'utf8');
|
|
284
|
+
finish('submitted', content);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
if (existsSync(contentPath)) {
|
|
288
|
+
finish('submitted', readFileSync(contentPath, 'utf8'));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const timeoutTimer = setTimeout(() => {
|
|
292
|
+
finish('timeout', '');
|
|
293
|
+
}, timeoutMs);
|
|
294
|
+
let paneTimer;
|
|
295
|
+
if (paneId !== undefined) {
|
|
296
|
+
const watchedPaneId = paneId;
|
|
297
|
+
paneTimer = setInterval(() => {
|
|
298
|
+
if (!paneAlive(watchedPaneId)) {
|
|
299
|
+
if (existsSync(contentPath)) {
|
|
300
|
+
finish('submitted', readFileSync(contentPath, 'utf8'));
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
finish('pane-closed', '');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}, PANE_POLL_MS);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First user message for a spec → plan handoff.
|
|
3
|
+
* Bundles the full planning workflow with the spec to plan.
|
|
4
|
+
*/
|
|
5
|
+
export declare function planHandoffPrompt(specPath: string, plansDir: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* First user message for a plan → implementation handoff.
|
|
8
|
+
*/
|
|
9
|
+
export declare function implementHandoffPrompt(planPath: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* First user message for a handoff to code review of the working tree.
|
|
12
|
+
*/
|
|
13
|
+
export declare function reviewHandoffPrompt(): string;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { planPrompt } from './plan.js';
|
|
2
|
+
/**
|
|
3
|
+
* First user message for a spec → plan handoff.
|
|
4
|
+
* Bundles the full planning workflow with the spec to plan.
|
|
5
|
+
*/
|
|
6
|
+
export function planHandoffPrompt(specPath, plansDir) {
|
|
7
|
+
return `${planPrompt(plansDir)}
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Your task
|
|
12
|
+
|
|
13
|
+
You were just launched in a new tmux pane via \`crtr agent plan\`. A spec
|
|
14
|
+
has been approved upstream and you are responsible for turning it into a plan.
|
|
15
|
+
|
|
16
|
+
**Spec to plan:** ${specPath}
|
|
17
|
+
|
|
18
|
+
Read the spec end-to-end before anything else. Then proceed through the
|
|
19
|
+
workflow above. When you save the plan, pass \`--spec <spec-name>\` so the
|
|
20
|
+
plan reviewer can check alignment.
|
|
21
|
+
|
|
22
|
+
The originating pane has closed; the user is watching you here. Begin now.`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* First user message for a plan → implementation handoff.
|
|
26
|
+
*/
|
|
27
|
+
export function implementHandoffPrompt(planPath) {
|
|
28
|
+
return `You are an implementation agent. A plan has been approved upstream and your
|
|
29
|
+
job is to execute it: write code, run tests, verify the change works
|
|
30
|
+
end-to-end.
|
|
31
|
+
|
|
32
|
+
**Plan to implement:** ${planPath}
|
|
33
|
+
|
|
34
|
+
## Process
|
|
35
|
+
|
|
36
|
+
1. Read the plan end-to-end before touching code.
|
|
37
|
+
2. If the plan references a spec (\`--spec\` was passed when saving), read it
|
|
38
|
+
too — it has the contract you are realizing.
|
|
39
|
+
3. Read the files listed under "Files to modify / create" and "Existing
|
|
40
|
+
utilities to reuse" to ground yourself in the current code.
|
|
41
|
+
4. Execute the plan step by step. Stay within scope — if the plan does not
|
|
42
|
+
call for a change, do not make it.
|
|
43
|
+
5. After each meaningful change, run the verification described in the plan
|
|
44
|
+
(tests, manual checks). Fix anything that fails before continuing.
|
|
45
|
+
6. When the plan is complete and verification passes, summarize what
|
|
46
|
+
shipped in a single short message: files touched, tests run, what works.
|
|
47
|
+
|
|
48
|
+
## Guardrails
|
|
49
|
+
|
|
50
|
+
- **Do not redesign.** If the plan is wrong, surface the issue and ask;
|
|
51
|
+
do not silently substitute your own approach.
|
|
52
|
+
- **Do not expand scope.** No drive-by refactors, no "while I'm here" cleanup.
|
|
53
|
+
- **Honor existing conventions.** Match the file's style, naming, and
|
|
54
|
+
patterns. Use the utilities the plan named.
|
|
55
|
+
- Commit only if the user asks.
|
|
56
|
+
|
|
57
|
+
When verification passes, your turn ends. The user may then ask for a code
|
|
58
|
+
review via \`crtr agent review\`.
|
|
59
|
+
|
|
60
|
+
You were launched in a new tmux pane via \`crtr agent implement\`. The
|
|
61
|
+
originating pane has closed; the user is watching you here. Begin by reading
|
|
62
|
+
the plan.`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* First user message for a handoff to code review of the working tree.
|
|
66
|
+
*/
|
|
67
|
+
export function reviewHandoffPrompt() {
|
|
68
|
+
return `You are a code reviewer. A change has just been implemented and your job is
|
|
69
|
+
to review it before it lands.
|
|
70
|
+
|
|
71
|
+
## Scope
|
|
72
|
+
|
|
73
|
+
Review the **uncommitted** changes in the working tree of the current
|
|
74
|
+
directory. Use \`git status\` and \`git diff\` (including staged changes via
|
|
75
|
+
\`git diff --cached\`) to enumerate what changed. If there are zero changes,
|
|
76
|
+
say so and stop.
|
|
77
|
+
|
|
78
|
+
## What to check
|
|
79
|
+
|
|
80
|
+
| Category | What to look for |
|
|
81
|
+
|----------|------------------|
|
|
82
|
+
| Correctness | Does the code do what it claims? Off-by-ones, wrong branches, missed cases. |
|
|
83
|
+
| Security | Injection, auth bypass, leaking secrets, unsafe defaults. |
|
|
84
|
+
| Style fit | Matches the file's existing conventions, naming, error-handling style. |
|
|
85
|
+
| Tests | Are there tests for new behavior? Do they actually exercise the change? |
|
|
86
|
+
| Scope | Did the change stay within its mandate, or sneak in unrelated edits? |
|
|
87
|
+
| Reuse | Are there existing utilities that should have been used? |
|
|
88
|
+
|
|
89
|
+
## Calibration
|
|
90
|
+
|
|
91
|
+
Only flag issues that would matter to the next reader, on-call, or future
|
|
92
|
+
maintainer. Nits are fine in a "Recommendations" section, but **do not block
|
|
93
|
+
on style preferences**. Approve unless something is wrong, missing, or risky.
|
|
94
|
+
|
|
95
|
+
## Output
|
|
96
|
+
|
|
97
|
+
\`\`\`
|
|
98
|
+
## Code Review
|
|
99
|
+
|
|
100
|
+
**Status:** Approved | Issues Found
|
|
101
|
+
|
|
102
|
+
**Issues (if any):**
|
|
103
|
+
- [file:line]: [specific issue] — [why it matters]
|
|
104
|
+
|
|
105
|
+
**Recommendations (advisory):**
|
|
106
|
+
- [suggestions]
|
|
107
|
+
\`\`\`
|
|
108
|
+
|
|
109
|
+
After printing the review, your turn ends.
|
|
110
|
+
|
|
111
|
+
You were launched in a new tmux pane via \`crtr agent review\`. The
|
|
112
|
+
originating pane has closed; the user is watching you here. Begin by checking
|
|
113
|
+
the working tree.`;
|
|
114
|
+
}
|
package/dist/prompts/plan.js
CHANGED
|
@@ -87,14 +87,50 @@ EOF
|
|
|
87
87
|
|
|
88
88
|
- Pick a short, descriptive kebab-case name. Names may be nested
|
|
89
89
|
(\`crtr plan --name auth/jwt-refresh\`) — they become subdirectories.
|
|
90
|
+
- If this plan implements a saved spec, pass \`--spec <spec-name>\` so the
|
|
91
|
+
reviewer can check alignment:
|
|
92
|
+
\`crtr plan --name <name> --spec <spec-name> <<'EOF' ... EOF\`
|
|
90
93
|
- The file lands at \`${plansDir}/<name>.md\`.
|
|
91
94
|
- If you are running inside tmux, the saved plan auto-opens in a side pane
|
|
92
95
|
via termrender. No extra step needed.
|
|
93
96
|
|
|
94
|
-
## Phase 5:
|
|
97
|
+
## Phase 5: Review
|
|
95
98
|
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
By default the save command **blocks** while a reviewer agent reads the plan
|
|
100
|
+
(and the spec, if \`--spec\` was passed) in a side pane (10-min budget) and
|
|
101
|
+
returns its findings on stdout under a \`--- review ---\` marker. **Read the
|
|
102
|
+
review** when the command returns:
|
|
103
|
+
|
|
104
|
+
- If \`Status: Approved\`, you are done.
|
|
105
|
+
- If \`Status: Issues Found\`, address the listed issues by editing the plan
|
|
106
|
+
(\`crtr plan edit <name>\` or rewriting via the save command), then save
|
|
107
|
+
again to re-trigger review.
|
|
108
|
+
|
|
109
|
+
Pass \`--no-review\` only when the plan is genuinely trivial (one-line fix,
|
|
110
|
+
typo, single-file rename). For anything substantive, take the review.
|
|
111
|
+
|
|
112
|
+
## Phase 6: Oversize check
|
|
113
|
+
|
|
114
|
+
If the save command emits a \`--- advisory ---\` warning that the plan is
|
|
115
|
+
too long, do not ignore it. Split the plan into a short index plan plus
|
|
116
|
+
one or more nested part plans, each under the threshold, and re-save. The
|
|
117
|
+
implementer will execute parts one at a time; very long plans tend to be
|
|
118
|
+
under-decomposed.
|
|
119
|
+
|
|
120
|
+
## Phase 7: Done
|
|
121
|
+
|
|
122
|
+
After the review returns Approved (or you have addressed its issues), your
|
|
123
|
+
turn ends. No need to summarize the plan in chat — the user can read the file.
|
|
124
|
+
|
|
125
|
+
If the user is ready to start building, ask once whether they want to hand
|
|
126
|
+
off now. If yes, run:
|
|
127
|
+
|
|
128
|
+
\`\`\`bash
|
|
129
|
+
crtr agent implement --plan <name>
|
|
130
|
+
\`\`\`
|
|
131
|
+
|
|
132
|
+
This fires up an implementer in a new tmux pane and closes the current
|
|
133
|
+
pane a few seconds later. Do NOT run this without the user's go-ahead.
|
|
98
134
|
|
|
99
135
|
## See also
|
|
100
136
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const SUBMIT_INSTRUCTIONS = `## Delivering your review
|
|
2
|
+
|
|
3
|
+
When your review is complete, run a single Bash command to submit it back to
|
|
4
|
+
the parent agent:
|
|
5
|
+
|
|
6
|
+
\`\`\`bash
|
|
7
|
+
crtr agent submit "$(cat <<'EOF'
|
|
8
|
+
<your full review markdown here, using the Output Format below>
|
|
9
|
+
EOF
|
|
10
|
+
)"
|
|
11
|
+
\`\`\`
|
|
12
|
+
|
|
13
|
+
The pane will close automatically once your review is delivered. Do NOT
|
|
14
|
+
summarize or chat after submission — \`crtr agent submit\` IS the response.
|
|
15
|
+
|
|
16
|
+
If you cannot complete the review (file missing, totally malformed, etc.),
|
|
17
|
+
still call \`crtr agent submit\` with a brief explanation of why.`;
|
|
18
|
+
export function specReviewPrompt(specPath) {
|
|
19
|
+
return `You are reviewing a spec document. Verify it is complete and ready for planning.
|
|
20
|
+
|
|
21
|
+
**Spec to review:** ${specPath}
|
|
22
|
+
|
|
23
|
+
## What to Check
|
|
24
|
+
|
|
25
|
+
| Category | What to Look For |
|
|
26
|
+
|----------|------------------|
|
|
27
|
+
| Completeness | TODOs, placeholders, "TBD", incomplete sections |
|
|
28
|
+
| Consistency | Internal contradictions, conflicting requirements |
|
|
29
|
+
| Clarity | Requirements ambiguous enough to cause someone to build the wrong thing |
|
|
30
|
+
| Scope | Focused enough for a single plan — not covering multiple independent subsystems |
|
|
31
|
+
| YAGNI | Unrequested features, over-engineering |
|
|
32
|
+
|
|
33
|
+
## Calibration
|
|
34
|
+
|
|
35
|
+
**Only flag issues that would cause real problems during implementation planning.**
|
|
36
|
+
A missing section, a contradiction, or a requirement so ambiguous it could be
|
|
37
|
+
interpreted two different ways — those are issues. Minor wording improvements,
|
|
38
|
+
stylistic preferences, and "sections less detailed than others" are not.
|
|
39
|
+
|
|
40
|
+
Approve unless there are serious gaps that would lead to a flawed plan.
|
|
41
|
+
|
|
42
|
+
## Output Format
|
|
43
|
+
|
|
44
|
+
## Spec Review
|
|
45
|
+
|
|
46
|
+
**Status:** Approved | Issues Found
|
|
47
|
+
|
|
48
|
+
**Issues (if any):**
|
|
49
|
+
- [Section X]: [specific issue] - [why it matters for planning]
|
|
50
|
+
|
|
51
|
+
**Recommendations (advisory, do not block approval):**
|
|
52
|
+
- [suggestions for improvement]
|
|
53
|
+
|
|
54
|
+
${SUBMIT_INSTRUCTIONS}`;
|
|
55
|
+
}
|
|
56
|
+
export function planReviewPrompt(planPath, specPath) {
|
|
57
|
+
const inputs = specPath === null
|
|
58
|
+
? `**Plan to review:** ${planPath}
|
|
59
|
+
|
|
60
|
+
No spec was provided for cross-reference — evaluate the plan on its own
|
|
61
|
+
merits (internal completeness, task decomposition, buildability). Skip the
|
|
62
|
+
"Spec Alignment" check.`
|
|
63
|
+
: `**Plan to review:** ${planPath}
|
|
64
|
+
**Spec for reference:** ${specPath}
|
|
65
|
+
|
|
66
|
+
Read the plan first, then the spec, then evaluate alignment and the other
|
|
67
|
+
criteria below.`;
|
|
68
|
+
return `You are reviewing a plan document. Verify it is complete and ready for implementation.
|
|
69
|
+
|
|
70
|
+
${inputs}
|
|
71
|
+
|
|
72
|
+
## What to Check
|
|
73
|
+
|
|
74
|
+
| Category | What to Look For |
|
|
75
|
+
|----------|------------------|
|
|
76
|
+
| Completeness | TODOs, placeholders, incomplete tasks, missing steps |
|
|
77
|
+
| Spec Alignment | Plan covers spec requirements, no major scope creep |
|
|
78
|
+
| Task Decomposition | Tasks have clear boundaries, steps are actionable |
|
|
79
|
+
| Buildability | Could an engineer follow this plan without getting stuck? |
|
|
80
|
+
|
|
81
|
+
## Calibration
|
|
82
|
+
|
|
83
|
+
**Only flag issues that would cause real problems during implementation.**
|
|
84
|
+
An implementer building the wrong thing or getting stuck is an issue.
|
|
85
|
+
Minor wording, stylistic preferences, and "nice to have" suggestions are not.
|
|
86
|
+
|
|
87
|
+
Approve unless there are serious gaps — missing requirements from the spec,
|
|
88
|
+
contradictory steps, placeholder content, or tasks so vague they can't be acted on.
|
|
89
|
+
|
|
90
|
+
## Output Format
|
|
91
|
+
|
|
92
|
+
## Plan Review
|
|
93
|
+
|
|
94
|
+
**Status:** Approved | Issues Found
|
|
95
|
+
|
|
96
|
+
**Issues (if any):**
|
|
97
|
+
- [Task X, Step Y]: [specific issue] - [why it matters for implementation]
|
|
98
|
+
|
|
99
|
+
**Recommendations (advisory, do not block approval):**
|
|
100
|
+
- [suggestions for improvement]
|
|
101
|
+
|
|
102
|
+
${SUBMIT_INSTRUCTIONS}`;
|
|
103
|
+
}
|
package/dist/prompts/skill.d.ts
CHANGED