@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 +14 -10
- package/cli/batch.js +23 -10
- package/cli/index.js +2 -2
- package/cli/inject.js +3 -2
- package/cli/migrate.js +2 -2
- package/cli/mode.js +2 -2
- package/cli/status.js +1 -1
- package/cli/update.js +50 -6
- package/package.json +1 -1
- package/src/core/inject.js +25 -8
- package/src/core/validate.js +16 -1
- package/src/core/workspace.js +13 -11
- package/src/services/batch-service.js +78 -55
- package/templates/workspace/.ai/agents/claude.md +47 -146
- 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/mode.md +6 -5
- package/templates/workspace/.claude/commands/save.md +0 -42
- package/templates/workspace/.claude/settings.json +11 -2
- package/templates/workspace/.ai/tasks/backlog.md +0 -95
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
|
|
56
|
-
pwn batch --
|
|
57
|
-
pwn batch status
|
|
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
|
-
│ │ └──
|
|
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
|
|
145
|
+
Execute stories autonomously via `batch_runner.sh`:
|
|
146
146
|
|
|
147
147
|
```bash
|
|
148
|
-
|
|
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
|
-
-
|
|
153
|
-
-
|
|
154
|
-
-
|
|
155
|
-
-
|
|
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(`
|
|
113
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
64
|
+
console.log(' ├── tasks/ (active work, prd.json)');
|
|
65
65
|
console.log(' ├── patterns/ (auto-applied patterns)');
|
|
66
|
-
console.log(' ├──
|
|
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', '
|
|
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
|
|
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
|
|
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
|
|
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(`
|
|
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/
|
|
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
|
-
|
|
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
|
|
175
|
-
if (existsSync(claudeMdPath) && currentContent
|
|
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,
|
|
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
package/src/core/inject.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
package/src/core/validate.js
CHANGED
|
@@ -24,7 +24,13 @@ const REQUIRED_STRUCTURE = {
|
|
|
24
24
|
],
|
|
25
25
|
taskFiles: [
|
|
26
26
|
'tasks/active.md',
|
|
27
|
-
'tasks/
|
|
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
|
];
|
package/src/core/workspace.js
CHANGED
|
@@ -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
|
|
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
|
|
67
|
-
if (existsSync(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
128
|
+
* Parse stories from prd.json
|
|
129
129
|
* @param {string} cwd - Working directory
|
|
130
|
-
* @returns {Array<object>} Array of
|
|
130
|
+
* @returns {Array<object>} Array of stories
|
|
131
131
|
*/
|
|
132
|
-
export function
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
*
|
|
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
|
|
279
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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',
|