@fermindi/pwn-cli 0.5.0 → 0.6.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 CHANGED
@@ -52,9 +52,9 @@ pwn notify test # Test notification channels
52
52
  pwn notify send "msg" # Send notification
53
53
 
54
54
  # Batch Execution
55
- pwn batch # Execute next task
56
- pwn batch --count 5 # Execute 5 tasks
57
- pwn batch status # Show progress
55
+ pwn batch run # Run autonomous batch loop
56
+ pwn batch run --dry-run # Preview next story
57
+ pwn batch status # Show progress
58
58
 
59
59
  # Patterns
60
60
  pwn patterns list # List all patterns
@@ -85,7 +85,7 @@ your-project/
85
85
  │ │ └── deadends.md # Failed approaches (DE-XXX)
86
86
  │ ├── tasks/
87
87
  │ │ ├── active.md # Current work
88
- │ │ └── backlog.md # Future work
88
+ │ │ └── prd.json # Stories (structured JSON)
89
89
  │ ├── patterns/
90
90
  │ │ ├── index.md # Trigger mappings
91
91
  │ │ ├── frontend/ # React, Vue, etc.
@@ -142,17 +142,21 @@ decisions tracking patterns cleanup
142
142
 
143
143
  ### Batch Execution
144
144
 
145
- Execute tasks autonomously:
145
+ Execute stories autonomously via `batch_runner.sh`:
146
146
 
147
147
  ```bash
148
- pwn batch --count 5 --priority high
148
+ ./.ai/batch/batch_runner.sh # Run batch loop
149
+ ./.ai/batch/batch_runner.sh --dry-run # Preview next story
150
+ pwn batch run --phase 3 # Via CLI
149
151
  ```
150
152
 
151
153
  Features:
152
- - Quality gates (lint, test, typecheck)
153
- - Checkpoint-based pause/resume
154
- - Auto-commit after each task
155
- - Notifications on completion
154
+ - Reads stories from `.ai/tasks/prd.json`
155
+ - Quality gates (lint, test, typecheck) per task
156
+ - Retry with error context (up to 2x)
157
+ - Circuit breaker (3 consecutive failures)
158
+ - Rate limit detection + auto-wait
159
+ - Per-task logs in `logs/`
156
160
 
157
161
  ### Codespaces Integration
158
162
 
package/cli/batch.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { execSync } from 'child_process';
3
+ import { join } from 'path';
2
4
  import * as batch from '../src/services/batch-service.js';
3
5
  import { hasWorkspace } from '../src/core/state.js';
4
6
 
@@ -27,6 +29,19 @@ export default async function batchCommand(args = []) {
27
29
  return showConfig();
28
30
  }
29
31
 
32
+ if (subcommand === 'run') {
33
+ const runnerPath = join(process.cwd(), '.ai', 'batch', 'batch_runner.sh');
34
+ try {
35
+ execSync(`bash ${runnerPath} ${args.slice(1).join(' ')}`, {
36
+ cwd: process.cwd(),
37
+ stdio: 'inherit'
38
+ });
39
+ } catch (error) {
40
+ process.exit(error.status || 1);
41
+ }
42
+ return;
43
+ }
44
+
30
45
  // Parse options
31
46
  const options = parseOptions(args);
32
47
 
@@ -109,12 +124,8 @@ function showStatus() {
109
124
  // Task counts
110
125
  console.log('\n📝 Tasks\n');
111
126
  console.log(` Active: ${status.tasks.activePending} pending, ${status.tasks.activeCompleted} completed`);
112
- console.log(` Backlog: ${status.tasks.backlogTotal} total`);
113
- if (status.tasks.backlogTotal > 0) {
114
- console.log(` - High: ${status.tasks.backlogHigh}`);
115
- console.log(` - Medium: ${status.tasks.backlogMedium}`);
116
- console.log(` - Low: ${status.tasks.backlogLow}`);
117
- }
127
+ console.log(` Stories: ${status.tasks.storiesDone}/${status.tasks.storiesTotal} done`);
128
+ console.log(` Pending: ${status.tasks.storiesPending}`);
118
129
 
119
130
  // Batch history
120
131
  if (status.batchState?.completed?.length > 0) {
@@ -133,7 +144,7 @@ function showStatus() {
133
144
  console.log(` Priority: ${nextTask.priority}`);
134
145
  }
135
146
  } else {
136
- console.log('\n No tasks available in backlog');
147
+ console.log('\n No tasks available in prd.json');
137
148
  }
138
149
  }
139
150
 
@@ -185,7 +196,7 @@ async function dryRun(options) {
185
196
  }
186
197
 
187
198
  if (tasks.length === 0) {
188
- console.log(' No tasks available in backlog');
199
+ console.log(' No tasks available in prd.json');
189
200
  return;
190
201
  }
191
202
 
@@ -305,6 +316,7 @@ function showHelp() {
305
316
  console.log('Usage: pwn batch [command] [options]\n');
306
317
  console.log('Commands:');
307
318
  console.log(' (default) Execute next available task(s)');
319
+ console.log(' run Run batch_runner.sh (autonomous loop)');
308
320
  console.log(' status Show batch status');
309
321
  console.log(' config Show batch configuration\n');
310
322
  console.log('Options:');
@@ -320,11 +332,12 @@ function showHelp() {
320
332
  console.log(' --help, -h Show this help\n');
321
333
  console.log('Examples:');
322
334
  console.log(' pwn batch # Execute next task');
335
+ console.log(' pwn batch run # Run autonomous batch loop');
336
+ console.log(' pwn batch run --dry-run # Preview next story');
337
+ console.log(' pwn batch run --phase 3 # Run specific phase');
323
338
  console.log(' pwn batch --count 5 # Execute 5 tasks');
324
339
  console.log(' pwn batch --dry-run # Preview execution');
325
- console.log(' pwn batch --priority high # Only high priority');
326
340
  console.log(' pwn batch --resume # Resume paused batch');
327
- console.log(' pwn batch --resume --skip # Resume, skip current');
328
341
  console.log(' pwn batch status # Show status');
329
342
  console.log(' pwn batch config # Show configuration\n');
330
343
  console.log('Configuration:');
package/cli/index.js CHANGED
@@ -27,7 +27,7 @@ if (!command || command === '--help' || command === '-h') {
27
27
  console.log(' status Show workspace status');
28
28
  console.log(' validate Validate workspace structure');
29
29
  console.log(' notify Send notifications (test, send, config)');
30
- console.log(' batch Execute tasks autonomously');
30
+ console.log(' batch Execute tasks (batch run = autonomous loop)');
31
31
  console.log(' mode Manage session mode (interactive/batch)');
32
32
  console.log(' patterns Manage patterns and triggers');
33
33
  console.log(' knowledge Knowledge lifecycle management');
@@ -41,7 +41,7 @@ if (!command || command === '--help' || command === '-h') {
41
41
  console.log(' save --message=X Save with custom summary');
42
42
  console.log(' validate --verbose Show detailed structure report');
43
43
  console.log(' notify test [ch] Test notification channel');
44
- console.log(' batch --count 5 Execute 5 tasks');
44
+ console.log(' batch run Run autonomous batch loop');
45
45
  console.log(' mode batch --max-tasks=3 Configure batch mode');
46
46
  console.log(' patterns eval <f> Evaluate triggers for file');
47
47
  console.log(' knowledge status Show knowledge system status');
package/cli/inject.js CHANGED
@@ -61,9 +61,10 @@ export default async function injectCommand(args = []) {
61
61
  console.log('📁 Created structure:');
62
62
  console.log(' .ai/');
63
63
  console.log(' ├── memory/ (decisions, patterns, dead-ends)');
64
- console.log(' ├── tasks/ (active work, backlog)');
64
+ console.log(' ├── tasks/ (active work, prd.json)');
65
65
  console.log(' ├── patterns/ (auto-applied patterns)');
66
- console.log(' ├── workflows/ (batch execution)');
66
+ console.log(' ├── batch/ (batch runner, prompts)
67
+ ├── workflows/ (batch execution)');
67
68
  console.log(' ├── agents/ (AI agent configs)');
68
69
  console.log(' └── config/ (notifications, etc)');
69
70
  console.log(' .claude/');
package/cli/migrate.js CHANGED
@@ -300,7 +300,7 @@ function generateMigrationActions(parsed, cwd) {
300
300
 
301
301
  // Tasks
302
302
  if (parsed.tasks.length > 0) {
303
- const targetPath = join(cwd, '.ai', 'tasks', 'backlog.md');
303
+ const targetPath = join(cwd, '.ai', 'tasks', 'active.md');
304
304
  let newContent = '\n## Migrated Tasks\n\n';
305
305
  for (const task of parsed.tasks) {
306
306
  newContent += task.replace(/^##\s+.+\n/, '') + '\n';
@@ -310,7 +310,7 @@ function generateMigrationActions(parsed, cwd) {
310
310
  type: 'append',
311
311
  target: targetPath,
312
312
  content: newContent,
313
- description: `Add ${parsed.tasks.length} task section(s) to backlog.md`
313
+ description: `Add ${parsed.tasks.length} task section(s) to active.md`
314
314
  });
315
315
  }
316
316
 
package/cli/mode.js CHANGED
@@ -71,7 +71,7 @@ function setBatchMode(args) {
71
71
  console.log('✅ Switched to batch mode (config reset to defaults)\n');
72
72
  console.log('Config:');
73
73
  printConfig(DEFAULT_BATCH_CONFIG);
74
- console.log('\nRun \'pwn batch\' to start executing tasks from backlog.');
74
+ console.log('\nRun \'pwn batch run\' to start executing tasks from prd.json.');
75
75
  return;
76
76
  }
77
77
 
@@ -99,7 +99,7 @@ function setBatchMode(args) {
99
99
  printConfig(getBatchConfig());
100
100
  }
101
101
 
102
- console.log('\nRun \'pwn batch\' to start executing tasks from backlog.');
102
+ console.log('\nRun \'pwn batch run\' to start executing tasks from prd.json.');
103
103
  }
104
104
 
105
105
  function parseConfigArgs(args) {
package/cli/status.js CHANGED
@@ -29,7 +29,7 @@ export default async function statusCommand() {
29
29
  // Tasks summary
30
30
  console.log('📋 Tasks');
31
31
  console.log(` Active: ${info.tasks.active.pending} pending, ${info.tasks.active.completed} completed`);
32
- console.log(` Backlog: ${info.tasks.backlog.total} items`);
32
+ console.log(` Stories: ${info.tasks.backlog.done || 0}/${info.tasks.backlog.total} done, ${info.tasks.backlog.pending || 0} pending`);
33
33
  console.log();
34
34
 
35
35
  // Memory summary
package/cli/update.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { existsSync, readFileSync, writeFileSync, cpSync, renameSync, mkdirSync, readdirSync } from 'fs';
3
3
  import { join, dirname } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
+ import { convertBacklogToPrd } from '../src/services/batch-service.js';
5
6
 
6
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
8
 
@@ -17,6 +18,10 @@ const FRAMEWORK_FILES = [
17
18
  'patterns/backend/backend.template.md',
18
19
  'patterns/universal/universal.template.md',
19
20
  'workflows/batch-task.md',
21
+ 'batch/batch_runner.sh',
22
+ 'batch/prompt.md',
23
+ 'batch/progress.txt',
24
+ 'batch/prd_status.sh',
20
25
  'config/README.md',
21
26
  'README.md',
22
27
  ];
@@ -39,7 +44,7 @@ const USER_FILES = [
39
44
  'memory/deadends.md',
40
45
  'memory/archive/',
41
46
  'tasks/active.md',
42
- 'tasks/backlog.md',
47
+ 'tasks/prd.json',
43
48
  'state.json',
44
49
  'config/notifications.json',
45
50
  ];
@@ -122,6 +127,46 @@ export default async function updateCommand(args = []) {
122
127
  }
123
128
  // ============================================
124
129
 
130
+ // ============================================
131
+ // MIGRATION: backlog.md → prd.json
132
+ // Convert legacy backlog.md to structured prd.json
133
+ // ============================================
134
+ const backlogPath = join(aiDir, 'tasks', 'backlog.md');
135
+ const prdPath = join(aiDir, 'tasks', 'prd.json');
136
+
137
+ if (existsSync(backlogPath)) {
138
+ // Check if prd.json is missing or is the default template (empty stories)
139
+ let needsMigration = !existsSync(prdPath);
140
+ if (!needsMigration && existsSync(prdPath)) {
141
+ try {
142
+ const prd = JSON.parse(readFileSync(prdPath, 'utf8'));
143
+ needsMigration = !prd.stories || prd.stories.length === 0;
144
+ } catch {
145
+ needsMigration = true;
146
+ }
147
+ }
148
+
149
+ if (needsMigration) {
150
+ const backlogContent = readFileSync(backlogPath, 'utf8');
151
+ const prd = convertBacklogToPrd(backlogContent);
152
+
153
+ if (prd.stories.length > 0) {
154
+ if (dryRun) {
155
+ console.log(` 🔄 Would migrate: .ai/tasks/backlog.md → prd.json (${prd.stories.length} stories)`);
156
+ console.log(` 📦 Would backup: backlog.md → ~backlog.md`);
157
+ } else {
158
+ writeFileSync(prdPath, JSON.stringify(prd, null, 2));
159
+ renameSync(backlogPath, join(aiDir, 'tasks', '~backlog.md'));
160
+ console.log(` 🔄 Migrated: backlog.md → prd.json (${prd.stories.length} stories)`);
161
+ console.log(` 📦 Backed up: backlog.md → .ai/tasks/~backlog.md`);
162
+ backed_up.push('backlog.md');
163
+ }
164
+ updated.push('tasks/backlog.md → prd.json');
165
+ }
166
+ }
167
+ }
168
+ // ============================================
169
+
125
170
  // Update framework files in .ai/
126
171
  for (const file of FRAMEWORK_FILES) {
127
172
  const templateFile = join(templateDir, file);
@@ -166,13 +211,12 @@ export default async function updateCommand(args = []) {
166
211
  if (templateContent !== currentContent) {
167
212
  if (dryRun) {
168
213
  console.log(` 📝 Would update: CLAUDE.md`);
169
- // Only mention backup if content is actually different (not empty)
170
- if (currentContent && currentContent !== templateContent) {
214
+ if (currentContent) {
171
215
  console.log(` 📦 Would backup: CLAUDE.md → ~CLAUDE.md`);
172
216
  }
173
217
  } else {
174
- // Backup existing CLAUDE.md only if it has custom content (different from template)
175
- if (existsSync(claudeMdPath) && currentContent && currentContent !== templateContent) {
218
+ // Backup existing CLAUDE.md
219
+ if (existsSync(claudeMdPath) && currentContent) {
176
220
  renameSync(claudeMdPath, backupClaudeMdPath);
177
221
  console.log(` 📦 Backed up: CLAUDE.md → ~CLAUDE.md`);
178
222
  backed_up.push('CLAUDE.md');
@@ -273,7 +317,7 @@ export default async function updateCommand(args = []) {
273
317
  // Show what was preserved
274
318
  console.log('🔒 Preserved (user data):');
275
319
  console.log(' .ai/memory/ (decisions, patterns, deadends)');
276
- console.log(' .ai/tasks/ (active, backlog)');
320
+ console.log(' .ai/tasks/ (active, prd.json)');
277
321
  console.log(' .ai/state.json (session state)');
278
322
  console.log(' .claude/ (your custom commands preserved)\n');
279
323
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fermindi/pwn-cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Professional AI Workspace - Inject structured memory and automation into any project for AI-powered development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,6 +3,7 @@ import { join, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { randomUUID } from 'crypto';
5
5
  import { initState } from './state.js';
6
+ import { convertBacklogToPrd } from '../services/batch-service.js';
6
7
 
7
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
9
 
@@ -147,6 +148,20 @@ export async function inject(options = {}) {
147
148
  }
148
149
  }
149
150
 
151
+ // Migrate backlog.md → prd.json before overwriting (force path)
152
+ let migratedPrd = null;
153
+ if (force) {
154
+ const backlogPath = join(targetDir, 'tasks', 'backlog.md');
155
+ if (existsSync(backlogPath)) {
156
+ const backlogContent = readFileSync(backlogPath, 'utf8');
157
+ const prd = convertBacklogToPrd(backlogContent);
158
+ if (prd.stories.length > 0) {
159
+ migratedPrd = prd;
160
+ log(`🔄 Detected backlog.md with ${prd.stories.length} stories, will convert to prd.json`);
161
+ }
162
+ }
163
+ }
164
+
150
165
  try {
151
166
  // Copy workspace template
152
167
  log('📦 Copying workspace template...');
@@ -172,31 +187,33 @@ export async function inject(options = {}) {
172
187
  // Initialize state.json with current user
173
188
  initState(cwd);
174
189
 
190
+ // Write migrated prd.json (from backlog.md) if available
191
+ if (migratedPrd) {
192
+ const prdPath = join(targetDir, 'tasks', 'prd.json');
193
+ writeFileSync(prdPath, JSON.stringify(migratedPrd, null, 2));
194
+ log(`📝 Created prd.json from backlog.md (${migratedPrd.stories.length} stories)`);
195
+ }
196
+
175
197
  // Update .gitignore
176
198
  updateGitignore(cwd, silent);
177
199
 
178
- // Handle CLAUDE.md: backup existing (only if different) and copy PWN template to root
200
+ // Handle CLAUDE.md: backup existing and copy PWN template to root
179
201
  let backupInfo = { backed_up: [] };
180
202
  const claudeMdPath = join(cwd, 'CLAUDE.md');
181
203
  const backupClaudeMdPath = join(cwd, '~CLAUDE.md');
182
204
  const templateClaudeMd = join(targetDir, 'agents', 'claude.md');
183
- const templateContent = existsSync(templateClaudeMd) ? readFileSync(templateClaudeMd, 'utf8') : '';
184
205
 
185
- // Backup existing CLAUDE.md if present AND different from template
206
+ // Backup existing CLAUDE.md if present
186
207
  if (backedUpContent['CLAUDE.md'] || backedUpContent['claude.md']) {
187
208
  const originalName = backedUpContent['CLAUDE.md'] ? 'CLAUDE.md' : 'claude.md';
188
209
  const originalPath = join(cwd, originalName);
189
- const existingContent = backedUpContent[originalName]?.content || '';
190
210
 
191
- // Only backup if content is different from PWN template
192
- if (existsSync(originalPath) && existingContent !== templateContent) {
211
+ if (existsSync(originalPath)) {
193
212
  renameSync(originalPath, backupClaudeMdPath);
194
213
  backupInfo.backed_up.push({ from: originalName, to: '~CLAUDE.md' });
195
214
  if (!silent) {
196
215
  console.log(`📦 Backed up ${originalName} → ~CLAUDE.md`);
197
216
  }
198
- } else if (existsSync(originalPath) && !silent) {
199
- console.log(`⏭️ Skipped backup: ${originalName} is identical to PWN template`);
200
217
  }
201
218
  }
202
219
 
@@ -24,7 +24,13 @@ const REQUIRED_STRUCTURE = {
24
24
  ],
25
25
  taskFiles: [
26
26
  'tasks/active.md',
27
- 'tasks/backlog.md'
27
+ 'tasks/prd.json'
28
+ ],
29
+ batchFiles: [
30
+ 'batch/batch_runner.sh',
31
+ 'batch/prompt.md',
32
+ 'batch/progress.txt',
33
+ 'batch/prd_status.sh'
28
34
  ],
29
35
  agentFiles: [
30
36
  'agents/README.md',
@@ -88,6 +94,14 @@ export function validate(cwd = process.cwd()) {
88
94
  }
89
95
  }
90
96
 
97
+ // Check batch files
98
+ for (const file of REQUIRED_STRUCTURE.batchFiles) {
99
+ const filePath = join(aiDir, file);
100
+ if (!existsSync(filePath)) {
101
+ warnings.push(`Missing batch file: .ai/${file}`);
102
+ }
103
+ }
104
+
91
105
  // Check agent files
92
106
  for (const file of REQUIRED_STRUCTURE.agentFiles) {
93
107
  const filePath = join(aiDir, file);
@@ -189,6 +203,7 @@ export function getStructureReport(cwd = process.cwd()) {
189
203
  ...REQUIRED_STRUCTURE.files,
190
204
  ...REQUIRED_STRUCTURE.memoryFiles,
191
205
  ...REQUIRED_STRUCTURE.taskFiles,
206
+ ...REQUIRED_STRUCTURE.batchFiles,
192
207
  ...REQUIRED_STRUCTURE.agentFiles,
193
208
  ...REQUIRED_STRUCTURE.patternFiles
194
209
  ];
@@ -39,11 +39,11 @@ export function getWorkspaceInfo(cwd = process.cwd()) {
39
39
  */
40
40
  function getTasksSummary(cwd) {
41
41
  const activePath = join(cwd, '.ai', 'tasks', 'active.md');
42
- const backlogPath = join(cwd, '.ai', 'tasks', 'backlog.md');
42
+ const prdPath = join(cwd, '.ai', 'tasks', 'prd.json');
43
43
 
44
44
  const summary = {
45
45
  active: { total: 0, completed: 0, pending: 0 },
46
- backlog: { total: 0 }
46
+ backlog: { total: 0, done: 0, pending: 0 }
47
47
  };
48
48
 
49
49
  // Parse active.md
@@ -63,15 +63,17 @@ function getTasksSummary(cwd) {
63
63
  }
64
64
  }
65
65
 
66
- // Parse backlog.md
67
- if (existsSync(backlogPath)) {
68
- const content = readFileSync(backlogPath, 'utf8');
69
- const lines = content.split('\n');
70
-
71
- for (const line of lines) {
72
- if (line.match(/^- \[[ ]\]/) || line.match(/^\d+\./)) {
73
- summary.backlog.total++;
74
- }
66
+ // Parse prd.json
67
+ if (existsSync(prdPath)) {
68
+ try {
69
+ const prd = JSON.parse(readFileSync(prdPath, 'utf8'));
70
+ summary.backlog = {
71
+ total: prd.stories.length,
72
+ done: prd.stories.filter(s => s.passes).length,
73
+ pending: prd.stories.filter(s => !s.passes).length
74
+ };
75
+ } catch {
76
+ // Invalid JSON, leave defaults
75
77
  }
76
78
  }
77
79
 
@@ -125,19 +125,26 @@ export function parseActiveTasks(cwd = process.cwd()) {
125
125
  }
126
126
 
127
127
  /**
128
- * Parse tasks from backlog.md
128
+ * Parse stories from prd.json
129
129
  * @param {string} cwd - Working directory
130
- * @returns {Array<object>} Array of tasks
130
+ * @returns {Array<object>} Array of stories
131
131
  */
132
- export function parseBacklogTasks(cwd = process.cwd()) {
133
- const backlogPath = join(cwd, '.ai', 'tasks', 'backlog.md');
134
-
135
- if (!existsSync(backlogPath)) {
132
+ export function parsePrdTasks(cwd = process.cwd()) {
133
+ const prdPath = join(cwd, '.ai', 'tasks', 'prd.json');
134
+ if (!existsSync(prdPath)) return [];
135
+ try {
136
+ const prd = JSON.parse(readFileSync(prdPath, 'utf8'));
137
+ return prd.stories || [];
138
+ } catch {
136
139
  return [];
137
140
  }
141
+ }
138
142
 
139
- const content = readFileSync(backlogPath, 'utf8');
140
- return parseBacklogFromMarkdown(content);
143
+ /**
144
+ * @deprecated Use parsePrdTasks instead
145
+ */
146
+ export function parseBacklogTasks(cwd = process.cwd()) {
147
+ return parsePrdTasks(cwd);
141
148
  }
142
149
 
143
150
  /**
@@ -257,16 +264,36 @@ function parseBacklogFromMarkdown(content) {
257
264
  }
258
265
 
259
266
  /**
260
- * Select next task to execute based on strategy
267
+ * Convert backlog.md content to prd.json format
268
+ * @param {string} backlogContent - Raw markdown content from backlog.md
269
+ * @param {string} projectName - Project name for prd.json
270
+ * @returns {object} PRD JSON structure
271
+ */
272
+ export function convertBacklogToPrd(backlogContent, projectName = 'my-project') {
273
+ const tasks = parseBacklogFromMarkdown(backlogContent);
274
+ return {
275
+ project: projectName,
276
+ branch: "main",
277
+ stories: tasks.map(t => ({
278
+ id: t.id,
279
+ title: t.title,
280
+ phase: t.section ? `Phase ${t.section}` : "Phase 1",
281
+ effort: t.effort || "M",
282
+ dependencies: t.dependencies ? t.dependencies.split(',').map(d => d.trim()) : [],
283
+ passes: false,
284
+ acceptance_criteria: [],
285
+ notes: t.description || ""
286
+ }))
287
+ };
288
+ }
289
+
290
+ /**
291
+ * Select next task to execute based on prd.json dependencies
261
292
  * @param {string} cwd - Working directory
262
293
  * @param {object} options - Selection options
263
294
  * @returns {object|null} Selected task or null
264
295
  */
265
296
  export function selectNextTask(cwd = process.cwd(), options = {}) {
266
- const config = loadConfig(cwd);
267
- const strategy = options.strategy || config.selection_strategy;
268
- const priorityFilter = options.priority;
269
-
270
297
  // First check active tasks for incomplete ones
271
298
  const activeTasks = parseActiveTasks(cwd);
272
299
  const pendingActive = activeTasks.filter(t => !t.completed && !t.blockedBy);
@@ -275,47 +302,40 @@ export function selectNextTask(cwd = process.cwd(), options = {}) {
275
302
  return pendingActive[0];
276
303
  }
277
304
 
278
- // Then check backlog
279
- let backlogTasks = parseBacklogTasks(cwd);
305
+ // Then check prd.json stories
306
+ const stories = parsePrdTasks(cwd);
307
+ const doneIds = stories.filter(s => s.passes).map(s => s.id);
280
308
 
281
- // Filter by priority if specified
282
- if (priorityFilter) {
283
- backlogTasks = backlogTasks.filter(t => t.priority === priorityFilter);
284
- }
285
-
286
- // Filter out blocked tasks
287
- backlogTasks = backlogTasks.filter(t => !t.dependencies || t.dependencies === 'None');
288
-
289
- if (backlogTasks.length === 0) {
290
- return null;
291
- }
309
+ const eligible = stories.find(s =>
310
+ !s.passes &&
311
+ s.dependencies.every(dep => doneIds.includes(dep)) &&
312
+ (!options.phase || s.phase === options.phase)
313
+ );
292
314
 
293
- // Sort based on strategy
294
- switch (strategy) {
295
- case 'effort':
296
- // Sort by effort (smallest first)
297
- const effortOrder = { 'XS': 1, 'S': 2, 'M': 3, 'L': 4, 'XL': 5 };
298
- backlogTasks.sort((a, b) => {
299
- const aEffort = effortOrder[a.effort] || 3;
300
- const bEffort = effortOrder[b.effort] || 3;
301
- return aEffort - bEffort;
302
- });
303
- break;
315
+ return eligible || null;
316
+ }
304
317
 
305
- case 'priority':
306
- default:
307
- // Sort by priority (highest first), then by position
308
- const priorityOrder = { 'high': 1, 'medium': 2, 'low': 3 };
309
- backlogTasks.sort((a, b) => {
310
- const aPriority = priorityOrder[a.priority] || 2;
311
- const bPriority = priorityOrder[b.priority] || 2;
312
- if (aPriority !== bPriority) return aPriority - bPriority;
313
- return a.line - b.line;
314
- });
315
- break;
318
+ /**
319
+ * Mark a story as done in prd.json
320
+ * @param {string} taskId - Story ID
321
+ * @param {string} cwd - Working directory
322
+ * @returns {boolean} Success
323
+ */
324
+ export function markStoryDone(taskId, cwd = process.cwd()) {
325
+ const prdPath = join(cwd, '.ai', 'tasks', 'prd.json');
326
+ if (!existsSync(prdPath)) return false;
327
+ try {
328
+ const prd = JSON.parse(readFileSync(prdPath, 'utf8'));
329
+ const story = prd.stories.find(s => s.id === taskId);
330
+ if (story) {
331
+ story.passes = true;
332
+ writeFileSync(prdPath, JSON.stringify(prd, null, 2));
333
+ return true;
334
+ }
335
+ } catch {
336
+ // Invalid JSON
316
337
  }
317
-
318
- return backlogTasks[0];
338
+ return false;
319
339
  }
320
340
 
321
341
  /**
@@ -598,10 +618,12 @@ export function getStatus(cwd = process.cwd()) {
598
618
  const config = loadConfig(cwd);
599
619
  const batchState = getBatchState(cwd);
600
620
  const activeTasks = parseActiveTasks(cwd);
601
- const backlogTasks = parseBacklogTasks(cwd);
621
+ const stories = parsePrdTasks(cwd);
602
622
 
603
623
  const pendingActive = activeTasks.filter(t => !t.completed);
604
624
  const completedActive = activeTasks.filter(t => t.completed);
625
+ const doneStories = stories.filter(s => s.passes);
626
+ const pendingStories = stories.filter(s => !s.passes);
605
627
 
606
628
  return {
607
629
  hasWorkspace: true,
@@ -611,10 +633,11 @@ export function getStatus(cwd = process.cwd()) {
611
633
  activeTotal: activeTasks.length,
612
634
  activePending: pendingActive.length,
613
635
  activeCompleted: completedActive.length,
614
- backlogTotal: backlogTasks.length,
615
- backlogHigh: backlogTasks.filter(t => t.priority === 'high').length,
616
- backlogMedium: backlogTasks.filter(t => t.priority === 'medium').length,
617
- backlogLow: backlogTasks.filter(t => t.priority === 'low').length
636
+ storiesTotal: stories.length,
637
+ storiesDone: doneStories.length,
638
+ storiesPending: pendingStories.length,
639
+ // Legacy compat
640
+ backlogTotal: stories.length
618
641
  },
619
642
  isRunning: batchState?.status === 'running',
620
643
  isPaused: batchState?.status === 'paused',