@fermindi/pwn-cli 0.5.0 → 0.7.0
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 -10
- package/cli/backlog.js +60 -0
- package/cli/batch.js +112 -12
- package/cli/index.js +9 -31
- package/cli/inject.js +8 -32
- package/cli/status.js +1 -1
- package/cli/update.js +78 -27
- package/package.json +6 -3
- package/src/core/inject.js +41 -45
- package/src/core/state.js +0 -1
- package/src/core/validate.js +14 -1
- package/src/core/workspace.js +13 -11
- package/src/index.js +0 -1
- package/src/services/batch-runner.js +769 -0
- package/src/services/batch-service.js +185 -74
- package/src/ui/backlog-viewer.js +394 -0
- package/templates/workspace/.ai/agents/claude.md +47 -146
- package/templates/workspace/.ai/batch/tasks/.gitkeep +0 -0
- package/templates/workspace/.ai/memory/patterns.md +57 -11
- package/templates/workspace/.ai/tasks/active.md +1 -1
- package/templates/workspace/.ai/workflows/batch-task.md +43 -67
- package/templates/workspace/.claude/commands/save.md +0 -42
- package/cli/codespaces.js +0 -303
- package/cli/migrate.js +0 -466
- package/cli/mode.js +0 -206
- package/cli/notify.js +0 -135
- package/src/services/notification-service.js +0 -342
- package/templates/codespaces/devcontainer.json +0 -52
- package/templates/codespaces/setup.sh +0 -70
- package/templates/workspace/.ai/config/notifications.template.json +0 -20
- package/templates/workspace/.ai/tasks/backlog.md +0 -95
- package/templates/workspace/.claude/commands/mode.md +0 -103
- package/templates/workspace/.claude/settings.json +0 -15
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PWN Backlog Viewer — Interactive TUI
|
|
3
|
+
*
|
|
4
|
+
* Navigable terminal viewer for prd.json stories.
|
|
5
|
+
* Uses raw readline + ANSI escape codes (no extra deps).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import readline from 'readline';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Launch the interactive backlog viewer.
|
|
13
|
+
* @param {object} options
|
|
14
|
+
* @param {string} options.project - Project name
|
|
15
|
+
* @param {Array<object>} options.stories - Stories from prd.json
|
|
16
|
+
* @param {Array<object>} options.taskFiles - Task files from .ai/batch/tasks/
|
|
17
|
+
* @returns {Promise<void>} Resolves when user quits
|
|
18
|
+
*/
|
|
19
|
+
export function startViewer({ project, stories, taskFiles }) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
if (stories.length === 0) {
|
|
22
|
+
console.log(chalk.yellow('No stories found in prd.json'));
|
|
23
|
+
resolve();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let selectedIndex = 0;
|
|
28
|
+
let detailMode = false; // true = full-screen detail view
|
|
29
|
+
let detailScroll = 0; // scroll offset inside detail view
|
|
30
|
+
const taskMap = new Map(taskFiles.map(t => [t.id, t]));
|
|
31
|
+
|
|
32
|
+
function getStatus(story) {
|
|
33
|
+
if (story.passes) return 'done';
|
|
34
|
+
const tf = taskMap.get(story.id);
|
|
35
|
+
if (tf) {
|
|
36
|
+
if (tf.status === 'completed') return 'done';
|
|
37
|
+
if (tf.status === 'planned' || tf.status === 'running') return 'active';
|
|
38
|
+
}
|
|
39
|
+
return 'pending';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function statusIcon(status) {
|
|
43
|
+
if (status === 'done') return chalk.green('✅');
|
|
44
|
+
if (status === 'active') return chalk.yellow('🔄');
|
|
45
|
+
return chalk.dim('⏳');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function statusLabel(status) {
|
|
49
|
+
if (status === 'done') return chalk.green('done');
|
|
50
|
+
if (status === 'active') return chalk.yellow('active');
|
|
51
|
+
return chalk.dim('pending');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function truncate(str, maxLen) {
|
|
55
|
+
if (!str) return '';
|
|
56
|
+
if (str.length <= maxLen) return str;
|
|
57
|
+
return str.slice(0, maxLen - 1) + '…';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildDetailLines(story, cols) {
|
|
61
|
+
const status = getStatus(story);
|
|
62
|
+
const contentLines = [];
|
|
63
|
+
|
|
64
|
+
contentLines.push(chalk.bold(` ${story.id} · ${story.title}`));
|
|
65
|
+
contentLines.push('');
|
|
66
|
+
|
|
67
|
+
const phase = story.phase || '—';
|
|
68
|
+
const effort = story.effort || '—';
|
|
69
|
+
const deps = story.dependencies?.length > 0 ? story.dependencies.join(', ') : 'none';
|
|
70
|
+
contentLines.push(` Phase: ${phase} | Effort: ${effort} | Status: ${statusLabel(status)} | Deps: ${deps}`);
|
|
71
|
+
contentLines.push('');
|
|
72
|
+
|
|
73
|
+
if (story.acceptance_criteria?.length > 0) {
|
|
74
|
+
contentLines.push(chalk.bold(' Acceptance Criteria:'));
|
|
75
|
+
for (const ac of story.acceptance_criteria) {
|
|
76
|
+
const check = status === 'done' ? chalk.green('✓') : chalk.dim('○');
|
|
77
|
+
contentLines.push(` ${check} ${truncate(ac, cols - 6)}`);
|
|
78
|
+
}
|
|
79
|
+
contentLines.push('');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (story.notes) {
|
|
83
|
+
contentLines.push(chalk.bold(' Notes:'));
|
|
84
|
+
// Word-wrap notes to terminal width
|
|
85
|
+
const words = story.notes.split(' ');
|
|
86
|
+
let line = ' ';
|
|
87
|
+
for (const word of words) {
|
|
88
|
+
if (line.length + word.length + 1 > cols - 2) {
|
|
89
|
+
contentLines.push(chalk.dim(line));
|
|
90
|
+
line = ' ' + word;
|
|
91
|
+
} else {
|
|
92
|
+
line += (line.length > 1 ? ' ' : '') + word;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (line.length > 1) contentLines.push(chalk.dim(line));
|
|
96
|
+
contentLines.push('');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Show task file info if available
|
|
100
|
+
const tf = taskMap.get(story.id);
|
|
101
|
+
if (tf) {
|
|
102
|
+
contentLines.push(chalk.bold(' Batch Task Info:'));
|
|
103
|
+
if (tf.complexity) contentLines.push(` Complexity: ${tf.complexity}`);
|
|
104
|
+
if (tf.estimated_time_seconds) contentLines.push(` Estimated: ~${tf.estimated_time_seconds}s`);
|
|
105
|
+
if (tf.plan?.length > 0 && tf.plan[0] !== 'fallback - no plan available') {
|
|
106
|
+
contentLines.push(chalk.bold(' Plan:'));
|
|
107
|
+
for (const step of tf.plan) {
|
|
108
|
+
contentLines.push(` · ${truncate(step, cols - 8)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (tf.failure_reason) {
|
|
112
|
+
contentLines.push(chalk.red(` Failed: ${tf.failure_reason}`));
|
|
113
|
+
}
|
|
114
|
+
contentLines.push('');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return contentLines;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderDetailView() {
|
|
121
|
+
const rows = process.stdout.rows || 24;
|
|
122
|
+
const cols = process.stdout.columns || 80;
|
|
123
|
+
const story = stories[selectedIndex];
|
|
124
|
+
|
|
125
|
+
process.stdout.write('\x1b[H\x1b[2J');
|
|
126
|
+
|
|
127
|
+
const lines = [];
|
|
128
|
+
|
|
129
|
+
// Header
|
|
130
|
+
const header = `📋 ${story.id} — Detail View`;
|
|
131
|
+
const pos = `${selectedIndex + 1}/${stories.length}`;
|
|
132
|
+
const headerLine = `${header}${' '.repeat(Math.max(1, cols - header.length - pos.length - 2))}${pos}`;
|
|
133
|
+
lines.push(chalk.bold(headerLine));
|
|
134
|
+
lines.push(chalk.dim('─'.repeat(cols)));
|
|
135
|
+
|
|
136
|
+
// Build full content and apply scroll
|
|
137
|
+
const contentLines = buildDetailLines(story, cols);
|
|
138
|
+
const maxContent = rows - 4; // header(2) + footer(2)
|
|
139
|
+
const maxScroll = Math.max(0, contentLines.length - maxContent);
|
|
140
|
+
if (detailScroll > maxScroll) detailScroll = maxScroll;
|
|
141
|
+
|
|
142
|
+
const visible = contentLines.slice(detailScroll, detailScroll + maxContent);
|
|
143
|
+
for (const line of visible) {
|
|
144
|
+
lines.push(line);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Scroll indicator
|
|
148
|
+
const scrollInfo = contentLines.length > maxContent
|
|
149
|
+
? ` (${detailScroll + 1}-${Math.min(detailScroll + maxContent, contentLines.length)}/${contentLines.length})`
|
|
150
|
+
: '';
|
|
151
|
+
|
|
152
|
+
// Pad
|
|
153
|
+
while (lines.length < rows - 2) {
|
|
154
|
+
lines.push('');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
lines.push(chalk.dim('─'.repeat(cols)));
|
|
158
|
+
lines.push(chalk.dim(` ↑/k ↓/j scroll ←/→ prev/next story Esc/Backspace back to list q quit${scrollInfo}`));
|
|
159
|
+
|
|
160
|
+
process.stdout.write(lines.join('\n'));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function render() {
|
|
164
|
+
if (detailMode) {
|
|
165
|
+
renderDetailView();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const rows = process.stdout.rows || 24;
|
|
170
|
+
const cols = process.stdout.columns || 80;
|
|
171
|
+
const doneCount = stories.filter(s => getStatus(s) === 'done').length;
|
|
172
|
+
|
|
173
|
+
// Move cursor to top-left and clear screen
|
|
174
|
+
process.stdout.write('\x1b[H\x1b[2J');
|
|
175
|
+
|
|
176
|
+
const lines = [];
|
|
177
|
+
|
|
178
|
+
// Header
|
|
179
|
+
const header = `📋 Product Backlog — ${project}`;
|
|
180
|
+
const progress = `${doneCount}/${stories.length} done`;
|
|
181
|
+
const headerLine = `${header}${' '.repeat(Math.max(1, cols - header.length - progress.length - 2))}${progress}`;
|
|
182
|
+
lines.push(chalk.bold(headerLine));
|
|
183
|
+
lines.push(chalk.dim('─'.repeat(cols)));
|
|
184
|
+
|
|
185
|
+
// Story list — compute how many rows we can show
|
|
186
|
+
const detailReserved = 12; // lines reserved for detail panel + footer
|
|
187
|
+
const maxListRows = Math.max(3, rows - lines.length - detailReserved);
|
|
188
|
+
|
|
189
|
+
// Scrolling window
|
|
190
|
+
let scrollOffset = 0;
|
|
191
|
+
if (selectedIndex >= scrollOffset + maxListRows) {
|
|
192
|
+
scrollOffset = selectedIndex - maxListRows + 1;
|
|
193
|
+
}
|
|
194
|
+
if (selectedIndex < scrollOffset) {
|
|
195
|
+
scrollOffset = selectedIndex;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const visibleStories = stories.slice(scrollOffset, scrollOffset + maxListRows);
|
|
199
|
+
|
|
200
|
+
for (let i = 0; i < visibleStories.length; i++) {
|
|
201
|
+
const story = visibleStories[i];
|
|
202
|
+
const idx = scrollOffset + i;
|
|
203
|
+
const status = getStatus(story);
|
|
204
|
+
const icon = statusIcon(status);
|
|
205
|
+
const pointer = idx === selectedIndex ? chalk.cyan('▸') : ' ';
|
|
206
|
+
const id = chalk.bold(story.id);
|
|
207
|
+
const title = truncate(story.title, cols - story.id.length - 12);
|
|
208
|
+
const line = ` ${pointer} ${id} ${title} ${icon}`;
|
|
209
|
+
lines.push(idx === selectedIndex ? chalk.cyan(line) : line);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Separator
|
|
213
|
+
lines.push(chalk.dim('─'.repeat(cols)));
|
|
214
|
+
|
|
215
|
+
// Detail panel for selected story
|
|
216
|
+
const story = stories[selectedIndex];
|
|
217
|
+
const status = getStatus(story);
|
|
218
|
+
|
|
219
|
+
lines.push(chalk.bold(` ${story.id} · ${story.title}`));
|
|
220
|
+
|
|
221
|
+
const phase = story.phase || '—';
|
|
222
|
+
const effort = story.effort || '—';
|
|
223
|
+
const deps = story.dependencies?.length > 0 ? story.dependencies.join(', ') : 'none';
|
|
224
|
+
lines.push(` Phase: ${phase} | Effort: ${effort} | Status: ${statusLabel(status)} | Deps: ${deps}`);
|
|
225
|
+
lines.push('');
|
|
226
|
+
|
|
227
|
+
// Acceptance criteria
|
|
228
|
+
if (story.acceptance_criteria?.length > 0) {
|
|
229
|
+
lines.push(chalk.bold(' Acceptance Criteria:'));
|
|
230
|
+
const remainingRows = rows - lines.length - 3; // leave room for notes + footer
|
|
231
|
+
const maxAC = Math.max(1, remainingRows - 2);
|
|
232
|
+
const acs = story.acceptance_criteria.slice(0, maxAC);
|
|
233
|
+
for (const ac of acs) {
|
|
234
|
+
const check = status === 'done' ? chalk.green('✓') : chalk.dim('○');
|
|
235
|
+
lines.push(` ${check} ${truncate(ac, cols - 6)}`);
|
|
236
|
+
}
|
|
237
|
+
if (story.acceptance_criteria.length > maxAC) {
|
|
238
|
+
lines.push(chalk.dim(` ... +${story.acceptance_criteria.length - maxAC} more — press Enter for full view`));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Notes
|
|
243
|
+
if (story.notes) {
|
|
244
|
+
lines.push('');
|
|
245
|
+
lines.push(chalk.dim(` Notes: ${truncate(story.notes, cols - 10)}`));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Pad to fill screen
|
|
249
|
+
while (lines.length < rows - 2) {
|
|
250
|
+
lines.push('');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Footer separator + keybindings
|
|
254
|
+
lines.push(chalk.dim('─'.repeat(cols)));
|
|
255
|
+
lines.push(chalk.dim(' ↑/k up ↓/j down Enter detail Home/End first/last q quit'));
|
|
256
|
+
|
|
257
|
+
process.stdout.write(lines.join('\n'));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Enter alternate screen buffer and hide cursor
|
|
261
|
+
process.stdout.write('\x1b[?1049h\x1b[?25l');
|
|
262
|
+
|
|
263
|
+
// Enable raw mode for keypress detection
|
|
264
|
+
readline.emitKeypressEvents(process.stdin);
|
|
265
|
+
if (process.stdin.isTTY) {
|
|
266
|
+
process.stdin.setRawMode(true);
|
|
267
|
+
}
|
|
268
|
+
process.stdin.resume();
|
|
269
|
+
|
|
270
|
+
render();
|
|
271
|
+
|
|
272
|
+
function cleanup() {
|
|
273
|
+
// Restore screen buffer and show cursor
|
|
274
|
+
process.stdout.write('\x1b[?25h\x1b[?1049l');
|
|
275
|
+
if (process.stdin.isTTY) {
|
|
276
|
+
process.stdin.setRawMode(false);
|
|
277
|
+
}
|
|
278
|
+
process.stdin.pause();
|
|
279
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
280
|
+
resolve();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function onKeypress(_str, key) {
|
|
284
|
+
if (!key) return;
|
|
285
|
+
|
|
286
|
+
if (key.name === 'q' || (key.ctrl && key.name === 'c')) {
|
|
287
|
+
cleanup();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (detailMode) {
|
|
292
|
+
// Detail view keybindings
|
|
293
|
+
if (key.name === 'escape' || key.name === 'backspace') {
|
|
294
|
+
detailMode = false;
|
|
295
|
+
detailScroll = 0;
|
|
296
|
+
render();
|
|
297
|
+
} else if (key.name === 'up' || key.name === 'k') {
|
|
298
|
+
if (detailScroll > 0) {
|
|
299
|
+
detailScroll--;
|
|
300
|
+
render();
|
|
301
|
+
}
|
|
302
|
+
} else if (key.name === 'down' || key.name === 'j') {
|
|
303
|
+
detailScroll++;
|
|
304
|
+
render();
|
|
305
|
+
} else if (key.name === 'left') {
|
|
306
|
+
if (selectedIndex > 0) {
|
|
307
|
+
selectedIndex--;
|
|
308
|
+
detailScroll = 0;
|
|
309
|
+
render();
|
|
310
|
+
}
|
|
311
|
+
} else if (key.name === 'right') {
|
|
312
|
+
if (selectedIndex < stories.length - 1) {
|
|
313
|
+
selectedIndex++;
|
|
314
|
+
detailScroll = 0;
|
|
315
|
+
render();
|
|
316
|
+
}
|
|
317
|
+
} else if (key.name === 'home') {
|
|
318
|
+
detailScroll = 0;
|
|
319
|
+
render();
|
|
320
|
+
} else if (key.name === 'end') {
|
|
321
|
+
detailScroll = 9999; // clamped in renderDetailView
|
|
322
|
+
render();
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// List view keybindings
|
|
328
|
+
if (key.name === 'return') {
|
|
329
|
+
detailMode = true;
|
|
330
|
+
detailScroll = 0;
|
|
331
|
+
render();
|
|
332
|
+
} else if (key.name === 'up' || key.name === 'k') {
|
|
333
|
+
if (selectedIndex > 0) {
|
|
334
|
+
selectedIndex--;
|
|
335
|
+
render();
|
|
336
|
+
}
|
|
337
|
+
} else if (key.name === 'down' || key.name === 'j') {
|
|
338
|
+
if (selectedIndex < stories.length - 1) {
|
|
339
|
+
selectedIndex++;
|
|
340
|
+
render();
|
|
341
|
+
}
|
|
342
|
+
} else if (key.name === 'home') {
|
|
343
|
+
selectedIndex = 0;
|
|
344
|
+
render();
|
|
345
|
+
} else if (key.name === 'end') {
|
|
346
|
+
selectedIndex = stories.length - 1;
|
|
347
|
+
render();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
process.stdin.on('keypress', onKeypress);
|
|
352
|
+
|
|
353
|
+
// Handle terminal resize
|
|
354
|
+
process.stdout.on('resize', render);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Print backlog as plain text (non-interactive mode).
|
|
360
|
+
* @param {object} options
|
|
361
|
+
* @param {string} options.project - Project name
|
|
362
|
+
* @param {Array<object>} options.stories - Stories from prd.json
|
|
363
|
+
* @param {Array<object>} options.taskFiles - Task files from .ai/batch/tasks/
|
|
364
|
+
*/
|
|
365
|
+
export function printPlain({ project, stories, taskFiles }) {
|
|
366
|
+
if (stories.length === 0) {
|
|
367
|
+
console.log('No stories found in prd.json');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const taskMap = new Map(taskFiles.map(t => [t.id, t]));
|
|
372
|
+
|
|
373
|
+
function getStatus(story) {
|
|
374
|
+
if (story.passes) return 'done';
|
|
375
|
+
const tf = taskMap.get(story.id);
|
|
376
|
+
if (tf) {
|
|
377
|
+
if (tf.status === 'completed') return 'done';
|
|
378
|
+
if (tf.status === 'planned' || tf.status === 'running') return 'active';
|
|
379
|
+
}
|
|
380
|
+
return 'pending';
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const doneCount = stories.filter(s => getStatus(s) === 'done').length;
|
|
384
|
+
|
|
385
|
+
console.log(`\nProduct Backlog — ${project} (${doneCount}/${stories.length} done)\n`);
|
|
386
|
+
|
|
387
|
+
for (const story of stories) {
|
|
388
|
+
const status = getStatus(story);
|
|
389
|
+
const icon = status === 'done' ? '[x]' : status === 'active' ? '[~]' : '[ ]';
|
|
390
|
+
console.log(` ${icon} ${story.id} ${story.title}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
console.log('');
|
|
394
|
+
}
|
|
@@ -38,9 +38,9 @@ Read these files in order:
|
|
|
38
38
|
- Ask user before deviating from active decisions
|
|
39
39
|
|
|
40
40
|
2. **`/.ai/memory/patterns.md`**
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
- Add
|
|
41
|
+
- Codebase patterns learned by team
|
|
42
|
+
- Apply these patterns to new work
|
|
43
|
+
- Add new patterns when discovered
|
|
44
44
|
|
|
45
45
|
3. **`/.ai/memory/deadends.md`**
|
|
46
46
|
- Failed approaches documented (DE-XXX)
|
|
@@ -71,10 +71,10 @@ If this is your first session:
|
|
|
71
71
|
- Blocked items and blockers
|
|
72
72
|
- Priority ordering
|
|
73
73
|
|
|
74
|
-
2. **`/.ai/tasks/
|
|
75
|
-
- Future work
|
|
74
|
+
2. **`/.ai/tasks/prd.json`** (if needed)
|
|
75
|
+
- Future work as structured JSON stories
|
|
76
76
|
- Use when no active work assigned
|
|
77
|
-
- Pick
|
|
77
|
+
- Pick next story with dependencies satisfied
|
|
78
78
|
|
|
79
79
|
3. **Project-specific notes** (if applicable)
|
|
80
80
|
- Check `/CLAUDE.md` in project root
|
|
@@ -208,42 +208,6 @@ If any gate fails:
|
|
|
208
208
|
- Ask user if unsure how to proceed
|
|
209
209
|
- Don't commit until gates pass
|
|
210
210
|
|
|
211
|
-
### Pattern Checkpoint (Mandatory Before Commit)
|
|
212
|
-
|
|
213
|
-
Before every commit, review the work done and check if any pattern should be documented.
|
|
214
|
-
|
|
215
|
-
**Document a pattern when ANY of these criteria are met:**
|
|
216
|
-
|
|
217
|
-
1. **User corrected your approach** - The user told you to do something differently. Document the correct approach so future sessions don't repeat the mistake.
|
|
218
|
-
2. **You chose between alternatives** - You evaluated 2+ options and picked one. Document what was chosen and why.
|
|
219
|
-
3. **Workaround for limitation** - A library, framework, or tool has a non-obvious limitation. Document the workaround.
|
|
220
|
-
4. **Convention established** - First time implementing a certain type of thing in this codebase (first API route, first test, first component). Document the convention for consistency.
|
|
221
|
-
5. **Repeated approach** - You used the same approach 2+ times in this session. Document it to avoid re-deriving it.
|
|
222
|
-
6. **Non-trivial solution** - The solution wasn't obvious or took investigation to find. Document it so it's immediately available next time.
|
|
223
|
-
|
|
224
|
-
**If a criterion is met:**
|
|
225
|
-
|
|
226
|
-
1. Create pattern file: `.ai/patterns/{category}/{pattern-name}.md`
|
|
227
|
-
```markdown
|
|
228
|
-
# Pattern Name
|
|
229
|
-
|
|
230
|
-
## Context
|
|
231
|
-
When to apply this pattern.
|
|
232
|
-
|
|
233
|
-
## Pattern
|
|
234
|
-
Description or code example.
|
|
235
|
-
|
|
236
|
-
## Rationale
|
|
237
|
-
Why this approach works better.
|
|
238
|
-
|
|
239
|
-
## Examples
|
|
240
|
-
- `src/path/file.ts:42`
|
|
241
|
-
```
|
|
242
|
-
2. Add index entry to `.ai/memory/patterns.md`
|
|
243
|
-
3. Commit pattern files together with the code change
|
|
244
|
-
|
|
245
|
-
**If no criteria are met:** proceed to commit normally. Don't force patterns that aren't there.
|
|
246
|
-
|
|
247
211
|
### Commit Message Format
|
|
248
212
|
|
|
249
213
|
Use conventional commits:
|
|
@@ -281,9 +245,15 @@ When completing work:
|
|
|
281
245
|
- [x] US-042: My completed task (YYYY-MM-DD)
|
|
282
246
|
```
|
|
283
247
|
|
|
284
|
-
2.
|
|
285
|
-
|
|
286
|
-
|
|
248
|
+
2. Update `/.ai/memory/patterns.md` if new pattern discovered
|
|
249
|
+
```
|
|
250
|
+
## New Pattern Category
|
|
251
|
+
|
|
252
|
+
### Pattern Name
|
|
253
|
+
**Context:** When to use
|
|
254
|
+
**Pattern:** Description
|
|
255
|
+
**Rationale:** Why it's better
|
|
256
|
+
```
|
|
287
257
|
|
|
288
258
|
3. Add to `/.ai/memory/deadends.md` if approach failed
|
|
289
259
|
```
|
|
@@ -298,110 +268,41 @@ When completing work:
|
|
|
298
268
|
|
|
299
269
|
## Batch Mode
|
|
300
270
|
|
|
301
|
-
|
|
271
|
+
Autonomous task execution via `.ai/batch/batch_runner.sh`.
|
|
302
272
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
- Follow quality gates strictly
|
|
306
|
-
- Make conservative decisions (ask user when uncertain)
|
|
307
|
-
- Commit and push changes automatically
|
|
308
|
-
- Continue until backlog depleted or limits reached
|
|
273
|
+
The runner reads stories from `.ai/tasks/prd.json`, spawns fresh Claude sessions
|
|
274
|
+
per task, runs quality gates, and tracks progress.
|
|
309
275
|
|
|
310
|
-
###
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
2. Select highest-priority unblocked task
|
|
318
|
-
├─ Check dependencies
|
|
319
|
-
├─ Verify not blocked
|
|
320
|
-
└─ Move to tasks/active.md
|
|
321
|
-
|
|
322
|
-
3. Create feature branch
|
|
323
|
-
└─ Name: feature/{task-id}-{slug}
|
|
324
|
-
|
|
325
|
-
4. Execute work following patterns
|
|
326
|
-
├─ Auto-apply triggers from patterns/index.md
|
|
327
|
-
├─ Keep commits atomic and descriptive
|
|
328
|
-
└─ Test incrementally
|
|
329
|
-
|
|
330
|
-
5. Quality gates must pass
|
|
331
|
-
├─ If gate fails:
|
|
332
|
-
│ ├─ Attempt auto-fix if pattern known
|
|
333
|
-
│ ├─ Pause batch if can't fix
|
|
334
|
-
│ └─ Notify user
|
|
335
|
-
└─ If all pass: proceed to commit
|
|
336
|
-
|
|
337
|
-
6. Commit and push
|
|
338
|
-
├─ Stage all changes
|
|
339
|
-
├─ Commit with conventional format
|
|
340
|
-
├─ Push to origin
|
|
341
|
-
└─ Create PR if configured
|
|
342
|
-
|
|
343
|
-
7. Clean up and continue
|
|
344
|
-
├─ Delete local feature branch
|
|
345
|
-
├─ Update tasks/active.md
|
|
346
|
-
├─ Check if more tasks to execute
|
|
347
|
-
└─ Continue or stop
|
|
348
|
-
|
|
349
|
-
8. Completion report
|
|
350
|
-
└─ Show summary: tasks done, time spent, any issues
|
|
276
|
+
### Usage
|
|
277
|
+
```bash
|
|
278
|
+
./.ai/batch/batch_runner.sh --dry-run # Preview next task
|
|
279
|
+
./.ai/batch/batch_runner.sh # Run (default 20 iterations)
|
|
280
|
+
./.ai/batch/batch_runner.sh 50 # Custom iteration limit
|
|
281
|
+
./.ai/batch/batch_runner.sh --phase 3 # Specific phase only
|
|
351
282
|
```
|
|
352
283
|
|
|
353
|
-
###
|
|
354
|
-
|
|
355
|
-
|
|
284
|
+
### State Files
|
|
285
|
+
| File | Purpose |
|
|
286
|
+
|------|---------|
|
|
287
|
+
| .ai/tasks/prd.json | Single source of truth for task status |
|
|
288
|
+
| .ai/batch/progress.txt | Append-only operational learnings |
|
|
289
|
+
| .ai/batch/prompt.md | Prompt template for Claude sessions |
|
|
290
|
+
| logs/ | Per-task Claude output logs |
|
|
356
291
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
**Important:** Batch mode is conservative:
|
|
374
|
-
- Don't skip quality gates without explicit config
|
|
375
|
-
- Ask user if uncertain about task interpretation
|
|
376
|
-
- Stop on first blocker and notify user
|
|
377
|
-
- Don't force push or destructive operations
|
|
378
|
-
- Log all decisions and timing
|
|
379
|
-
|
|
380
|
-
### Batch Error Handling
|
|
381
|
-
|
|
382
|
-
**Build/Test Fails:**
|
|
383
|
-
- Show error with full context
|
|
384
|
-
- If pattern known: auto-fix
|
|
385
|
-
- If unsure: pause and notify user
|
|
386
|
-
- Don't proceed without resolution
|
|
387
|
-
|
|
388
|
-
**Git Conflicts:**
|
|
389
|
-
- Stop immediately
|
|
390
|
-
- Notify user of conflict
|
|
391
|
-
- Require manual resolution
|
|
392
|
-
- Resume after conflict resolved
|
|
393
|
-
|
|
394
|
-
**Missing Dependencies:**
|
|
395
|
-
- Check if dependency task exists
|
|
396
|
-
- Add to batch queue if available
|
|
397
|
-
- Execute dependency first
|
|
398
|
-
- Resume original task
|
|
399
|
-
|
|
400
|
-
**Resource Limits Reached:**
|
|
401
|
-
- Stop execution gracefully
|
|
402
|
-
- Report progress
|
|
403
|
-
- Create clean state for next batch
|
|
404
|
-
- Notify user of completion status
|
|
292
|
+
### How It Works
|
|
293
|
+
1. Read prd.json → find next incomplete story (deps satisfied)
|
|
294
|
+
2. Spawn: `claude --print --dangerously-skip-permissions -p "implement story X"`
|
|
295
|
+
3. Run quality gates (configurable per project)
|
|
296
|
+
4. If pass → mark story done in prd.json, commit
|
|
297
|
+
5. If fail → retry up to 2x with error context
|
|
298
|
+
6. Append learnings to progress.txt
|
|
299
|
+
7. Repeat until done or max iterations
|
|
300
|
+
|
|
301
|
+
### Safety
|
|
302
|
+
- Circuit breaker: 3 consecutive failures → stop
|
|
303
|
+
- Rate limit detection + auto-wait
|
|
304
|
+
- Graceful Ctrl+C shutdown
|
|
305
|
+
- Per-task logs for debugging
|
|
405
306
|
|
|
406
307
|
---
|
|
407
308
|
|
|
@@ -430,7 +331,7 @@ Reference these files in this priority order:
|
|
|
430
331
|
- Project changelog or git history
|
|
431
332
|
|
|
432
333
|
5. **Fifth Priority: Planning**
|
|
433
|
-
- `/.ai/tasks/
|
|
334
|
+
- `/.ai/tasks/prd.json` - Future work (structured)
|
|
434
335
|
- Roadmap or project documentation
|
|
435
336
|
- Upstream issues or features
|
|
436
337
|
|
|
@@ -620,7 +521,7 @@ When finishing a session:
|
|
|
620
521
|
- Add completion date if done
|
|
621
522
|
|
|
622
523
|
3. **Document learnings**
|
|
623
|
-
-
|
|
524
|
+
- Update `/.ai/memory/patterns.md` if discovered pattern
|
|
624
525
|
- Update `/.ai/memory/deadends.md` if failed approach
|
|
625
526
|
- Reference in commit messages
|
|
626
527
|
|
|
File without changes
|