@fermindi/pwn-cli 0.6.0 → 0.8.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.
@@ -5,12 +5,11 @@
5
5
  * checkpointing, and signal handling.
6
6
  */
7
7
 
8
- import { existsSync, readFileSync, writeFileSync } from 'fs';
8
+ import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
9
9
  import { join } from 'path';
10
10
  import { exec } from 'child_process';
11
11
  import { promisify } from 'util';
12
12
  import { getState, updateState, hasWorkspace } from '../core/state.js';
13
- import * as notifications from './notification-service.js';
14
13
 
15
14
  const execAsync = promisify(exec);
16
15
 
@@ -27,11 +26,110 @@ const DEFAULT_CONFIG = {
27
26
  create_pr: false,
28
27
  branch_format: 'feature/{id}-{slug}',
29
28
  commit_format: 'conventional',
30
- selection_strategy: 'priority', // priority, effort, due_date
31
- notify_on_complete: true,
32
- notify_on_error: true
29
+ selection_strategy: 'priority' // priority, effort, due_date
33
30
  };
34
31
 
32
+ /**
33
+ * Detect which quality gates a project can actually run.
34
+ * Checks for tooling config files and package.json scripts.
35
+ * @param {string} cwd - Working directory
36
+ * @returns {{ available: string[], missing: string[], details: Record<string, string> }}
37
+ */
38
+ export function detectAvailableGates(cwd = process.cwd()) {
39
+ const available = [];
40
+ const missing = [];
41
+ const details = {};
42
+
43
+ let pkg = {};
44
+ const pkgPath = join(cwd, 'package.json');
45
+ if (existsSync(pkgPath)) {
46
+ try { pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); } catch {}
47
+ }
48
+ const scripts = pkg.scripts || {};
49
+
50
+ // --- typecheck ---
51
+ if (scripts.typecheck) {
52
+ available.push('typecheck');
53
+ details.typecheck = 'npm run typecheck';
54
+ } else if (existsSync(join(cwd, 'tsconfig.json'))) {
55
+ available.push('typecheck');
56
+ details.typecheck = 'tsconfig.json (tsc --noEmit)';
57
+ } else if (existsSync(join(cwd, 'jsconfig.json'))) {
58
+ available.push('typecheck');
59
+ details.typecheck = 'jsconfig.json';
60
+ } else {
61
+ missing.push('typecheck');
62
+ }
63
+
64
+ // --- lint ---
65
+ if (scripts.lint) {
66
+ available.push('lint');
67
+ details.lint = 'npm run lint';
68
+ } else if (
69
+ globExists(cwd, '.eslintrc') ||
70
+ globExists(cwd, 'eslint.config') ||
71
+ existsSync(join(cwd, 'biome.json')) ||
72
+ existsSync(join(cwd, 'biome.jsonc'))
73
+ ) {
74
+ available.push('lint');
75
+ const tool = existsSync(join(cwd, 'biome.json')) || existsSync(join(cwd, 'biome.jsonc'))
76
+ ? 'biome' : 'eslint';
77
+ details.lint = tool;
78
+ } else {
79
+ missing.push('lint');
80
+ }
81
+
82
+ // --- test ---
83
+ const defaultTestScript = 'echo "Error: no test specified" && exit 1';
84
+ if (scripts.test && !scripts.test.includes(defaultTestScript)) {
85
+ available.push('test');
86
+ details.test = 'npm test';
87
+ } else if (
88
+ globExists(cwd, 'vitest.config') ||
89
+ globExists(cwd, 'jest.config') ||
90
+ existsSync(join(cwd, 'pytest.ini')) ||
91
+ existsSync(join(cwd, 'pyproject.toml')) ||
92
+ existsSync(join(cwd, 'Cargo.toml')) ||
93
+ existsSync(join(cwd, 'go.mod'))
94
+ ) {
95
+ available.push('test');
96
+ let tool = 'detected';
97
+ if (globExists(cwd, 'vitest.config')) tool = 'vitest';
98
+ else if (globExists(cwd, 'jest.config')) tool = 'jest';
99
+ else if (existsSync(join(cwd, 'pytest.ini')) || existsSync(join(cwd, 'pyproject.toml'))) tool = 'pytest';
100
+ else if (existsSync(join(cwd, 'Cargo.toml'))) tool = 'cargo test';
101
+ else if (existsSync(join(cwd, 'go.mod'))) tool = 'go test';
102
+ details.test = tool;
103
+ } else {
104
+ missing.push('test');
105
+ }
106
+
107
+ // --- build ---
108
+ if (scripts.build) {
109
+ available.push('build');
110
+ details.build = 'npm run build';
111
+ } else {
112
+ missing.push('build');
113
+ }
114
+
115
+ return { available, missing, details };
116
+ }
117
+
118
+ /**
119
+ * Check if any file matching a prefix exists (e.g. "eslint.config" matches eslint.config.js, .mjs, etc.)
120
+ * @param {string} cwd - Directory to check
121
+ * @param {string} prefix - File prefix to match
122
+ * @returns {boolean}
123
+ */
124
+ function globExists(cwd, prefix) {
125
+ try {
126
+ const files = readdirSync(cwd);
127
+ return files.some(f => f.startsWith(prefix));
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+
35
133
  /**
36
134
  * Quality gate commands mapping
37
135
  */
@@ -294,9 +392,14 @@ export function convertBacklogToPrd(backlogContent, projectName = 'my-project')
294
392
  * @returns {object|null} Selected task or null
295
393
  */
296
394
  export function selectNextTask(cwd = process.cwd(), options = {}) {
395
+ const filterRe = options.filter ? new RegExp(options.filter, 'i') : null;
396
+
297
397
  // First check active tasks for incomplete ones
298
398
  const activeTasks = parseActiveTasks(cwd);
299
- const pendingActive = activeTasks.filter(t => !t.completed && !t.blockedBy);
399
+ const pendingActive = activeTasks.filter(t =>
400
+ !t.completed && !t.blockedBy &&
401
+ (!filterRe || filterRe.test(t.id) || filterRe.test(t.title))
402
+ );
300
403
 
301
404
  if (pendingActive.length > 0) {
302
405
  return pendingActive[0];
@@ -309,7 +412,8 @@ export function selectNextTask(cwd = process.cwd(), options = {}) {
309
412
  const eligible = stories.find(s =>
310
413
  !s.passes &&
311
414
  s.dependencies.every(dep => doneIds.includes(dep)) &&
312
- (!options.phase || s.phase === options.phase)
415
+ (!options.phase || s.phase === options.phase) &&
416
+ (!filterRe || filterRe.test(s.id) || filterRe.test(s.title))
313
417
  );
314
418
 
315
419
  return eligible || null;
@@ -345,7 +449,10 @@ export function markStoryDone(taskId, cwd = process.cwd()) {
345
449
  * @returns {Promise<{success: boolean, output?: string, error?: string}>}
346
450
  */
347
451
  export async function runQualityGate(gate, cwd = process.cwd()) {
348
- const commands = GATE_COMMANDS[gate];
452
+ // Check for custom gate commands in config
453
+ const config = loadConfig(cwd);
454
+ const customCmd = config.gate_commands?.[gate];
455
+ const commands = customCmd ? [customCmd] : GATE_COMMANDS[gate];
349
456
 
350
457
  if (!commands) {
351
458
  return { success: false, error: `Unknown gate: ${gate}` };
@@ -699,14 +806,6 @@ export async function executeTask(task, options = {}, cwd = process.cwd()) {
699
806
 
700
807
  pauseBatch(`Quality gates failed: ${failedGates.join(', ')}`, cwd);
701
808
 
702
- if (config.notify_on_error) {
703
- await notifications.notifyError(
704
- 'Batch Paused',
705
- `Quality gates failed for ${task.id}: ${failedGates.join(', ')}`,
706
- { cwd }
707
- );
708
- }
709
-
710
809
  return {
711
810
  success: false,
712
811
  error: 'Quality gates failed',
@@ -753,11 +852,6 @@ export async function executeTask(task, options = {}, cwd = process.cwd()) {
753
852
  last_completed_at: new Date().toISOString()
754
853
  }, cwd);
755
854
 
756
- // Notify completion
757
- if (config.notify_on_complete) {
758
- await notifications.notifyTaskComplete(task.id, task.title, { cwd });
759
- }
760
-
761
855
  return { success: true, details };
762
856
  }
763
857
 
@@ -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
+ }
@@ -111,6 +111,26 @@ Defines:
111
111
  - Commit patterns
112
112
  - Completion signals
113
113
 
114
+ ### Writing stories for `prd.json`
115
+
116
+ Stories run with `--dangerously-skip-permissions` — the agent has full access. Write defensively.
117
+
118
+ **Never put these in batch stories:**
119
+ - Destructive git ops (`git filter-repo`, `BFG`, `push --force`, history rewriting)
120
+ - Destructive file ops (`rm -rf`, wiping directories)
121
+ - Database ops (`DROP TABLE`, prod migrations)
122
+ - Secret rotation (revoking keys, rotating credentials)
123
+ - External side effects (sending emails, creating PRs, publishing packages)
124
+
125
+ **Rule of thumb**: if a mistake needs human intervention to fix, it's not a batch story.
126
+
127
+ **Instead**, ask the agent to **prepare and document** — write the script, the docs, the config — but let a human execute the dangerous part.
128
+
129
+ **Always include in `notes`** what the agent must NOT do:
130
+ ```json
131
+ "notes": "Do NOT run git-filter-repo. Do NOT modify prd.json."
132
+ ```
133
+
114
134
  ## 🤖 Agents
115
135
 
116
136
  ### agent/claude.md
@@ -0,0 +1,36 @@
1
+ You are working on this project autonomously as part of a batch execution run.
2
+
3
+ ## Project Context
4
+ - Read CLAUDE.md (or .ai/agents/claude.md) for full project instructions and conventions
5
+ - Read .ai/memory/decisions.md for architectural decisions
6
+ - Read .ai/memory/patterns.md for established patterns
7
+ - Read .ai/batch/progress.txt for learnings from previous iterations
8
+
9
+ ## Current Task
10
+ **{STORY_ID}**: {STORY_TITLE}
11
+
12
+ ### Acceptance Criteria
13
+ {ACCEPTANCE_CRITERIA}
14
+
15
+ ### Notes
16
+ {NOTES}
17
+
18
+ ### Dependencies (already implemented)
19
+ {DEPENDENCIES}
20
+
21
+ ## Instructions
22
+ 1. Explore the codebase to understand existing patterns for similar features
23
+ 2. Implement the feature following existing conventions
24
+ 3. Write comprehensive tests (see tests/ for patterns)
25
+ 4. Run quality gates and fix any failures before committing
26
+ 5. Commit with: feat({STORY_ID}): {short description}
27
+ 6. Update .ai/tasks/active.md marking this task as done with today's date
28
+
29
+ ## Important
30
+ - Do NOT push to remote
31
+ - Do NOT modify unrelated files
32
+ - Do NOT edit .ai/tasks/prd.json or batch configuration files
33
+ - Do NOT run destructive operations (git filter-repo, rm -rf, DROP TABLE, force push)
34
+ - Do NOT rotate secrets, revoke keys, or modify credentials — those are human tasks
35
+ - Follow existing patterns exactly
36
+ - If you discover useful patterns, note them for progress.txt
File without changes