writethevision 7.0.4 → 7.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/package.json +1 -1
- package/src/cli.js +597 -23
package/README.md
CHANGED
|
@@ -253,6 +253,7 @@ wtv board # Show kanban board
|
|
|
253
253
|
wtv cry "description" # Enter a problem or need
|
|
254
254
|
wtv wait <id> # Move to waiting (seeking)
|
|
255
255
|
wtv vision <id> # Move to vision (answer received)
|
|
256
|
+
wtv run # Execute a vision (PRD loop)
|
|
256
257
|
wtv run <id> # Move to execution
|
|
257
258
|
wtv worship <id> # Complete with retrospective
|
|
258
259
|
|
|
@@ -293,6 +294,18 @@ When you move an item to RUN, invoke the Masterbuilder inside your AI CLI:
|
|
|
293
294
|
|
|
294
295
|
The Masterbuilder reads your vision document and coordinates the artisans.
|
|
295
296
|
|
|
297
|
+
## Vision Runner (Ralphy-Style)
|
|
298
|
+
|
|
299
|
+
`wtv run` can also execute a project vision directly by generating a `PRD.md` (checklist) and iterating until all tasks are complete.
|
|
300
|
+
|
|
301
|
+
It maintains a `progress.txt` checkpoint log and, by default, creates a single git commit and pushes once at the end.
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
wtv run
|
|
305
|
+
# or non-interactive
|
|
306
|
+
wtv run --vision vision/VISION.md --engine opencode
|
|
307
|
+
```
|
|
308
|
+
|
|
296
309
|
## Creating Custom Agents
|
|
297
310
|
|
|
298
311
|
```bash
|
|
@@ -327,6 +340,7 @@ wtv board # Show kanban board
|
|
|
327
340
|
wtv cry "description" # Enter a problem
|
|
328
341
|
wtv wait <id> # Move to waiting
|
|
329
342
|
wtv vision <id> # Move to vision
|
|
343
|
+
wtv run # Execute a vision (PRD loop)
|
|
330
344
|
wtv run <id> # Move to execution
|
|
331
345
|
wtv worship <id> # Complete with retrospective
|
|
332
346
|
wtv note <id> "text" # Add note to item
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "writethevision",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.6",
|
|
4
4
|
"description": "Write The Vision (WTV): vision-driven development with the Habakkuk workflow. 10 agents + 21 skills for Claude Code, Codex CLI, and OpenCode.",
|
|
5
5
|
"author": "Christopher Hogg",
|
|
6
6
|
"license": "MIT",
|
package/src/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
writeFileSync,
|
|
15
15
|
} from 'fs';
|
|
16
16
|
import { homedir, platform } from 'os';
|
|
17
|
-
import { spawn } from 'child_process';
|
|
17
|
+
import { spawn, spawnSync } from 'child_process';
|
|
18
18
|
|
|
19
19
|
async function openInEditor(filePath) {
|
|
20
20
|
const editor = process.env.EDITOR || 'vi';
|
|
@@ -1249,15 +1249,16 @@ function habakkukStones() {
|
|
|
1249
1249
|
console.log('');
|
|
1250
1250
|
}
|
|
1251
1251
|
|
|
1252
|
-
function checkForUpdates() {
|
|
1252
|
+
async function checkForUpdates() {
|
|
1253
1253
|
const shouldCheck = process.env.WTV_NO_UPDATE_CHECK !== '1' && process.env.CODEHOGG_NO_UPDATE_CHECK !== '1';
|
|
1254
1254
|
if (!shouldCheck) return;
|
|
1255
1255
|
if (!process.stdout.isTTY) return;
|
|
1256
1256
|
|
|
1257
1257
|
try {
|
|
1258
|
+
const packageName = getPackageName();
|
|
1258
1259
|
const notifier = updateNotifier({
|
|
1259
1260
|
pkg: {
|
|
1260
|
-
name:
|
|
1261
|
+
name: packageName,
|
|
1261
1262
|
version: getVersion(),
|
|
1262
1263
|
},
|
|
1263
1264
|
updateCheckInterval: 1000 * 60 * 60 * 24 * 7,
|
|
@@ -1267,31 +1268,66 @@ function checkForUpdates() {
|
|
|
1267
1268
|
const update = notifier.update;
|
|
1268
1269
|
if (!update) return;
|
|
1269
1270
|
|
|
1271
|
+
const npmGlobalCmd = `npm install -g ${packageName}@latest`;
|
|
1272
|
+
const npxCmd = `npx -y ${packageName}@latest`;
|
|
1273
|
+
|
|
1270
1274
|
notifier.notify({
|
|
1271
1275
|
message: `Update available ${c.dim}${update.current}${c.reset} → ${c.green}${update.latest}${c.reset}
|
|
1272
|
-
Run ${c.cyan}npx writethevision update${c.reset} to get the latest version`,
|
|
1273
|
-
defer: false,
|
|
1274
|
-
boxenOpts: {
|
|
1275
|
-
padding: 1,
|
|
1276
|
-
margin: 1,
|
|
1277
|
-
align: 'center',
|
|
1278
|
-
borderColor: 'yellow',
|
|
1279
|
-
borderStyle: 'round',
|
|
1280
|
-
},
|
|
1281
|
-
});
|
|
1282
1276
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1277
|
+
Update global install:
|
|
1278
|
+
${c.cyan}${npmGlobalCmd}${c.reset}
|
|
1279
|
+
|
|
1280
|
+
Or run latest once:
|
|
1281
|
+
${c.cyan}${npxCmd}${c.reset}
|
|
1282
|
+
`,
|
|
1285
1283
|
defer: false,
|
|
1286
1284
|
boxenOpts: {
|
|
1287
1285
|
padding: 1,
|
|
1288
1286
|
margin: 1,
|
|
1289
|
-
align: '
|
|
1287
|
+
align: 'left',
|
|
1290
1288
|
borderColor: 'yellow',
|
|
1291
1289
|
borderStyle: 'round',
|
|
1292
1290
|
},
|
|
1293
1291
|
});
|
|
1294
|
-
|
|
1292
|
+
|
|
1293
|
+
const shouldPrompt = process.stdin.isTTY && process.env.WTV_NO_AUTO_UPDATE !== '1' && !process.env.CI;
|
|
1294
|
+
if (!shouldPrompt) return;
|
|
1295
|
+
|
|
1296
|
+
const isDevCheckout = existsSync(join(PACKAGE_ROOT, '.git'));
|
|
1297
|
+
if (isDevCheckout) return;
|
|
1298
|
+
|
|
1299
|
+
const wantsUpdate = await confirm(`Update ${packageName} to v${update.latest} now?`, false);
|
|
1300
|
+
if (!wantsUpdate) return;
|
|
1301
|
+
|
|
1302
|
+
const argv1 = process.argv[1] || '';
|
|
1303
|
+
const userAgent = process.env.npm_config_user_agent || '';
|
|
1304
|
+
const runningViaNpx = argv1.includes('_npx') || userAgent.includes('npx/');
|
|
1305
|
+
|
|
1306
|
+
if (runningViaNpx) {
|
|
1307
|
+
console.log(`
|
|
1308
|
+
${c.yellow}${sym.warn}${c.reset} You're running via npx cache.`);
|
|
1309
|
+
console.log(` Next run: ${c.cyan}${npxCmd}${c.reset}
|
|
1310
|
+
`);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
1315
|
+
const result = spawnSync(npmBin, ['install', '-g', `${packageName}@latest`], { stdio: 'inherit' });
|
|
1316
|
+
|
|
1317
|
+
if (result.status !== 0) {
|
|
1318
|
+
console.log(`
|
|
1319
|
+
${c.red}${sym.cross}${c.reset} Update failed.`);
|
|
1320
|
+
console.log(` Try manually: ${c.cyan}${npmGlobalCmd}${c.reset}
|
|
1321
|
+
`);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
console.log(`
|
|
1326
|
+
${c.green}${sym.check}${c.reset} Updated ${packageName} to ${update.latest}.`);
|
|
1327
|
+
console.log(` Re-run ${c.cyan}wtv${c.reset} to use the new version.
|
|
1328
|
+
`);
|
|
1329
|
+
process.exit(0);
|
|
1330
|
+
} catch {
|
|
1295
1331
|
}
|
|
1296
1332
|
}
|
|
1297
1333
|
|
|
@@ -3115,6 +3151,453 @@ Prototype
|
|
|
3115
3151
|
return true;
|
|
3116
3152
|
}
|
|
3117
3153
|
|
|
3154
|
+
// ============================================================================
|
|
3155
|
+
// VISION RUNNER (Ralphy-style execution)
|
|
3156
|
+
// ============================================================================
|
|
3157
|
+
|
|
3158
|
+
const VISION_RUNNER_ENGINES = ['opencode', 'codex', 'claude'];
|
|
3159
|
+
|
|
3160
|
+
function normalizeVisionRunnerEngine(engine) {
|
|
3161
|
+
if (!engine) return null;
|
|
3162
|
+
return String(engine).trim().toLowerCase();
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
function isSupportedVisionRunnerEngine(engine) {
|
|
3166
|
+
return VISION_RUNNER_ENGINES.includes(engine);
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
function discoverVisionDocs() {
|
|
3170
|
+
const visionDirPath = join(process.cwd(), 'vision');
|
|
3171
|
+
const rootVisionPath = join(process.cwd(), 'VISION.md');
|
|
3172
|
+
|
|
3173
|
+
const files = [];
|
|
3174
|
+
if (existsSync(rootVisionPath)) {
|
|
3175
|
+
files.push({
|
|
3176
|
+
path: rootVisionPath,
|
|
3177
|
+
label: 'VISION.md (Root)',
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
if (existsSync(visionDirPath) && lstatSync(visionDirPath).isDirectory()) {
|
|
3182
|
+
const vFiles = readdirSync(visionDirPath)
|
|
3183
|
+
.filter(f => f.endsWith('.md'))
|
|
3184
|
+
.sort((a, b) => a.localeCompare(b))
|
|
3185
|
+
.map(f => ({
|
|
3186
|
+
path: join(visionDirPath, f),
|
|
3187
|
+
label: `vision/${f}`,
|
|
3188
|
+
}));
|
|
3189
|
+
files.push(...vFiles);
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
return files;
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
function countUncheckedPrdTasks(prdContent) {
|
|
3196
|
+
const matches = prdContent.match(/^- \[ \] .+$/gm);
|
|
3197
|
+
return matches ? matches.length : 0;
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
function getNextUncheckedPrdTask(prdContent) {
|
|
3201
|
+
const match = prdContent.match(/^- \[ \] (.+)$/m);
|
|
3202
|
+
return match ? match[1].trim() : null;
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
function runGit(args) {
|
|
3206
|
+
return spawnSync('git', args, {
|
|
3207
|
+
encoding: 'utf8',
|
|
3208
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
3209
|
+
});
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
function isGitRepo() {
|
|
3213
|
+
const res = runGit(['rev-parse', '--is-inside-work-tree']);
|
|
3214
|
+
return res.status === 0 && String(res.stdout || '').trim() === 'true';
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
function getGitHeadSha() {
|
|
3218
|
+
const res = runGit(['rev-parse', 'HEAD']);
|
|
3219
|
+
if (res.status !== 0) return null;
|
|
3220
|
+
const sha = String(res.stdout || '').trim();
|
|
3221
|
+
return sha ? sha : null;
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
function isWorkingTreeClean() {
|
|
3225
|
+
const res = runGit(['status', '--porcelain']);
|
|
3226
|
+
if (res.status !== 0) return false;
|
|
3227
|
+
return String(res.stdout || '').trim().length === 0;
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
function getCurrentBranch() {
|
|
3231
|
+
const res = runGit(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
3232
|
+
if (res.status !== 0) return null;
|
|
3233
|
+
const branch = String(res.stdout || '').trim();
|
|
3234
|
+
return branch ? branch : null;
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
function commandExists(cmd) {
|
|
3238
|
+
const locator = process.platform === 'win32' ? 'where' : 'which';
|
|
3239
|
+
const res = spawnSync(locator, [cmd], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
3240
|
+
return res.status === 0;
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
async function runEngine(engine, promptText) {
|
|
3244
|
+
const normalized = normalizeVisionRunnerEngine(engine);
|
|
3245
|
+
|
|
3246
|
+
if (normalized === 'opencode') {
|
|
3247
|
+
const env = {
|
|
3248
|
+
...process.env,
|
|
3249
|
+
OPENCODE_PERMISSION: '{"*":"allow"}',
|
|
3250
|
+
};
|
|
3251
|
+
|
|
3252
|
+
return await new Promise((resolve, reject) => {
|
|
3253
|
+
const child = spawn('opencode', ['run', '--format', 'json', promptText], { stdio: 'inherit', env });
|
|
3254
|
+
child.on('error', reject);
|
|
3255
|
+
child.on('exit', (code) => resolve(code ?? 1));
|
|
3256
|
+
});
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
if (normalized === 'codex') {
|
|
3260
|
+
return await new Promise((resolve, reject) => {
|
|
3261
|
+
const child = spawn('codex', ['exec', '--full-auto', '--json', promptText], { stdio: 'inherit' });
|
|
3262
|
+
child.on('error', reject);
|
|
3263
|
+
child.on('exit', (code) => resolve(code ?? 1));
|
|
3264
|
+
});
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
if (normalized === 'claude') {
|
|
3268
|
+
return await new Promise((resolve, reject) => {
|
|
3269
|
+
const child = spawn('claude', ['--dangerously-skip-permissions', '--output-format', 'stream-json', '-p', promptText], { stdio: 'inherit' });
|
|
3270
|
+
child.on('error', reject);
|
|
3271
|
+
child.on('exit', (code) => resolve(code ?? 1));
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
throw new Error(`Unsupported engine: ${engine}`);
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
function ensureProgressHeader(progressPath, { visionPath, engine, startSha }) {
|
|
3279
|
+
if (!existsSync(progressPath)) {
|
|
3280
|
+
writeFileSync(progressPath, '');
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
const existing = readFileSync(progressPath, 'utf8');
|
|
3284
|
+
if (existing.trim().length > 0) return;
|
|
3285
|
+
|
|
3286
|
+
const startedAt = new Date().toISOString();
|
|
3287
|
+
const header = [
|
|
3288
|
+
`WTV Vision Runner`,
|
|
3289
|
+
`Started: ${startedAt}`,
|
|
3290
|
+
`Vision: ${visionPath}`,
|
|
3291
|
+
`Engine: ${engine}`,
|
|
3292
|
+
startSha ? `Start SHA: ${startSha}` : `Start SHA: (not a git repo)`,
|
|
3293
|
+
'',
|
|
3294
|
+
].join('\n');
|
|
3295
|
+
|
|
3296
|
+
writeFileSync(progressPath, header);
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
async function generatePrdFromVision({ engine, visionPath, prdPath }) {
|
|
3300
|
+
const visionContent = readFileSync(visionPath, 'utf8');
|
|
3301
|
+
|
|
3302
|
+
const promptText = `You are generating a PRD for a software project.
|
|
3303
|
+
|
|
3304
|
+
Input vision document:
|
|
3305
|
+
---
|
|
3306
|
+
${visionContent}
|
|
3307
|
+
---
|
|
3308
|
+
|
|
3309
|
+
Create or overwrite PRD.md in the project root.
|
|
3310
|
+
|
|
3311
|
+
Requirements:
|
|
3312
|
+
- Must include a section titled "## Tasks".
|
|
3313
|
+
- Under "## Tasks", write a detailed implementation checklist using GitHub-flavored markdown checkboxes:
|
|
3314
|
+
- Each item must use '- [ ] ' (unchecked).
|
|
3315
|
+
- Tasks must be small, sequential, and unambiguous.
|
|
3316
|
+
- Do NOT implement any code yet.
|
|
3317
|
+
- Do NOT run git commit or git push.
|
|
3318
|
+
- Output is the updated files on disk (PRD.md).`;
|
|
3319
|
+
|
|
3320
|
+
const code = await runEngine(engine, promptText);
|
|
3321
|
+
if (code !== 0) {
|
|
3322
|
+
throw new Error(`Engine exited with code ${code} while generating PRD.md`);
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
if (!existsSync(prdPath)) {
|
|
3326
|
+
throw new Error('PRD.md was not created');
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
const prdContent = readFileSync(prdPath, 'utf8');
|
|
3330
|
+
if (countUncheckedPrdTasks(prdContent) === 0) {
|
|
3331
|
+
throw new Error('PRD.md has no unchecked tasks (- [ ])');
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
async function runVisionTaskIteration({ engine, prdPath, progressPath, taskText, noTests, noLint }) {
|
|
3336
|
+
const promptText = `You are an autonomous coding agent executing a PRD.
|
|
3337
|
+
|
|
3338
|
+
Files:
|
|
3339
|
+
- PRD.md (checklist)
|
|
3340
|
+
- progress.txt (checkpoint log)
|
|
3341
|
+
|
|
3342
|
+
Task to implement (ONLY this task):
|
|
3343
|
+
"${taskText}"
|
|
3344
|
+
|
|
3345
|
+
Rules:
|
|
3346
|
+
- Implement ONLY the task above.
|
|
3347
|
+
- Update PRD.md: change that exact task from '- [ ]' to '- [x]'.
|
|
3348
|
+
- Append a short checkpoint entry to progress.txt.
|
|
3349
|
+
- Do NOT run git commit.
|
|
3350
|
+
- Do NOT run git push.
|
|
3351
|
+
|
|
3352
|
+
Verification:
|
|
3353
|
+
${noTests ? '- Skip tests.' : '- Write and run tests; they must pass.'}
|
|
3354
|
+
${noLint ? '- Skip lint.' : '- Run linting; it must pass.'}
|
|
3355
|
+
|
|
3356
|
+
Stop after completing this one task.`;
|
|
3357
|
+
|
|
3358
|
+
const beforePrd = readFileSync(prdPath, 'utf8');
|
|
3359
|
+
const beforeUnchecked = countUncheckedPrdTasks(beforePrd);
|
|
3360
|
+
|
|
3361
|
+
const progressBefore = existsSync(progressPath) ? readFileSync(progressPath, 'utf8') : '';
|
|
3362
|
+
|
|
3363
|
+
const code = await runEngine(engine, promptText);
|
|
3364
|
+
if (code !== 0) {
|
|
3365
|
+
throw new Error(`Engine exited with code ${code}`);
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
const afterPrd = readFileSync(prdPath, 'utf8');
|
|
3369
|
+
const afterUnchecked = countUncheckedPrdTasks(afterPrd);
|
|
3370
|
+
|
|
3371
|
+
const stillUnchecked = afterPrd.includes(`- [ ] ${taskText}`);
|
|
3372
|
+
const nowChecked = afterPrd.includes(`- [x] ${taskText}`);
|
|
3373
|
+
|
|
3374
|
+
if (stillUnchecked || (!nowChecked && afterUnchecked >= beforeUnchecked)) {
|
|
3375
|
+
throw new Error('PRD.md was not updated to mark the task complete');
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
const progressAfter = existsSync(progressPath) ? readFileSync(progressPath, 'utf8') : '';
|
|
3379
|
+
if (progressAfter === progressBefore) {
|
|
3380
|
+
const stamp = new Date().toISOString();
|
|
3381
|
+
writeFileSync(progressPath, progressAfter + `\n[${stamp}] Completed: ${taskText}\n`);
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
async function visionRunner(opts) {
|
|
3386
|
+
const config = loadConfig();
|
|
3387
|
+
const interactive = process.stdout.isTTY && process.stdin.isTTY;
|
|
3388
|
+
|
|
3389
|
+
const prdPath = join(process.cwd(), 'PRD.md');
|
|
3390
|
+
const progressPath = join(process.cwd(), 'progress.txt');
|
|
3391
|
+
|
|
3392
|
+
const defaultEngine = normalizeVisionRunnerEngine(opts.engine || config.defaultTool || 'opencode');
|
|
3393
|
+
let engine = defaultEngine;
|
|
3394
|
+
|
|
3395
|
+
if (!engine || !isSupportedVisionRunnerEngine(engine)) {
|
|
3396
|
+
engine = null;
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
if (!engine) {
|
|
3400
|
+
if (!interactive) {
|
|
3401
|
+
console.log(`\n ${c.red}Error:${c.reset} --engine is required when not running interactively.\n`);
|
|
3402
|
+
return;
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
engine = await select('Engine:', [
|
|
3406
|
+
{ value: 'opencode', label: 'OpenCode (recommended)' },
|
|
3407
|
+
{ value: 'codex', label: 'Codex CLI' },
|
|
3408
|
+
{ value: 'claude', label: 'Claude Code' },
|
|
3409
|
+
]);
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
if (!engine || !isSupportedVisionRunnerEngine(engine)) {
|
|
3413
|
+
console.log(`\n ${c.red}Error:${c.reset} Unsupported engine: ${engine}\n`);
|
|
3414
|
+
return;
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
if (!commandExists(engine === 'claude' ? 'claude' : engine)) {
|
|
3418
|
+
console.log(`\n ${c.red}Error:${c.reset} Required CLI not found in PATH for engine: ${engine}\n`);
|
|
3419
|
+
return;
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
let visionPath = null;
|
|
3423
|
+
if (opts.vision) {
|
|
3424
|
+
visionPath = resolve(process.cwd(), opts.vision);
|
|
3425
|
+
if (!existsSync(visionPath)) {
|
|
3426
|
+
console.log(`\n ${c.red}Error:${c.reset} Vision file not found: ${visionPath}\n`);
|
|
3427
|
+
return;
|
|
3428
|
+
}
|
|
3429
|
+
} else {
|
|
3430
|
+
let docs = discoverVisionDocs();
|
|
3431
|
+
if (docs.length === 0) {
|
|
3432
|
+
if (!interactive) {
|
|
3433
|
+
console.log(`\n ${c.red}Error:${c.reset} No vision documents found. Pass --vision to run non-interactively.\n`);
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
console.log(`\n ${c.yellow}${sym.warn}${c.reset} No vision documents found.`);
|
|
3438
|
+
const create = await confirm('Create VISION.md now?', true);
|
|
3439
|
+
if (!create) return;
|
|
3440
|
+
await initVision('project');
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
docs = discoverVisionDocs();
|
|
3444
|
+
if (docs.length === 0) {
|
|
3445
|
+
console.log(`\n ${c.red}Error:${c.reset} No vision documents available to run.\n`);
|
|
3446
|
+
return;
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
visionPath = await select('Select vision document:', docs.map(d => ({ value: d.path, label: d.label })));
|
|
3450
|
+
if (!visionPath) return;
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
const hasGit = isGitRepo();
|
|
3454
|
+
const startSha = hasGit ? getGitHeadSha() : null;
|
|
3455
|
+
|
|
3456
|
+
if (hasGit && !isWorkingTreeClean() && !opts.dryRun) {
|
|
3457
|
+
if (!interactive) {
|
|
3458
|
+
console.log(`\n ${c.red}Error:${c.reset} Working tree is not clean (non-interactive mode).\n`);
|
|
3459
|
+
return;
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
const proceed = await confirm('Working tree is not clean. Continue anyway?', false);
|
|
3463
|
+
if (!proceed) return;
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
if (interactive && !opts.dryRun) {
|
|
3467
|
+
const proceed = await confirm(`Run vision with ${engine} (will modify files)?`, false);
|
|
3468
|
+
if (!proceed) return;
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
const shouldPush = hasGit && !opts.noPush;
|
|
3472
|
+
|
|
3473
|
+
if (opts.dryRun) {
|
|
3474
|
+
console.log(`\n ${c.bold}Dry run${c.reset}`);
|
|
3475
|
+
console.log(` Vision: ${visionPath}`);
|
|
3476
|
+
console.log(` Engine: ${engine}`);
|
|
3477
|
+
console.log(` PRD: ${prdPath}`);
|
|
3478
|
+
console.log(` Progress: ${progressPath}`);
|
|
3479
|
+
console.log(` Commit+push at end: ${shouldPush ? 'yes' : 'no'}`);
|
|
3480
|
+
console.log('');
|
|
3481
|
+
return;
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
let resume = opts.resume;
|
|
3485
|
+
|
|
3486
|
+
if (!resume && existsSync(prdPath) && !opts.regeneratePrd) {
|
|
3487
|
+
if (!interactive) {
|
|
3488
|
+
// Safe non-interactive default: continue existing PRD.
|
|
3489
|
+
resume = true;
|
|
3490
|
+
} else {
|
|
3491
|
+
const choice = await select('PRD.md already exists. What do you want to do?', [
|
|
3492
|
+
{ value: 'resume', label: 'Resume existing PRD.md (recommended)' },
|
|
3493
|
+
{ value: 'regenerate', label: 'Regenerate PRD.md from vision' },
|
|
3494
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
3495
|
+
]);
|
|
3496
|
+
|
|
3497
|
+
if (choice === 'cancel') return;
|
|
3498
|
+
if (choice === 'resume') resume = true;
|
|
3499
|
+
if (choice === 'regenerate') resume = false;
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
if (!resume) {
|
|
3504
|
+
await generatePrdFromVision({ engine, visionPath, prdPath });
|
|
3505
|
+
} else {
|
|
3506
|
+
if (!existsSync(prdPath)) {
|
|
3507
|
+
console.log(`\n ${c.red}Error:${c.reset} PRD.md not found (use without --resume to generate one).\n`);
|
|
3508
|
+
return;
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
ensureProgressHeader(progressPath, { visionPath, engine, startSha });
|
|
3513
|
+
|
|
3514
|
+
let iteration = 0;
|
|
3515
|
+
|
|
3516
|
+
while (true) {
|
|
3517
|
+
const prdContent = readFileSync(prdPath, 'utf8');
|
|
3518
|
+
const nextTask = getNextUncheckedPrdTask(prdContent);
|
|
3519
|
+
|
|
3520
|
+
if (!nextTask) {
|
|
3521
|
+
break;
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
if (opts.maxIterations > 0 && iteration >= opts.maxIterations) {
|
|
3525
|
+
console.log(`\n ${c.yellow}${sym.warn}${c.reset} Stopped after max iterations (${opts.maxIterations}).`);
|
|
3526
|
+
console.log(` Remaining tasks: ${countUncheckedPrdTasks(prdContent)}\n`);
|
|
3527
|
+
return;
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
iteration++;
|
|
3531
|
+
console.log(`\n ${c.cyan}${sym.bullet}${c.reset} Task ${iteration}: ${nextTask}`);
|
|
3532
|
+
|
|
3533
|
+
try {
|
|
3534
|
+
await runVisionTaskIteration({
|
|
3535
|
+
engine,
|
|
3536
|
+
prdPath,
|
|
3537
|
+
progressPath,
|
|
3538
|
+
taskText: nextTask,
|
|
3539
|
+
noTests: opts.noTests,
|
|
3540
|
+
noLint: opts.noLint,
|
|
3541
|
+
});
|
|
3542
|
+
} catch (err) {
|
|
3543
|
+
console.log(`\n ${c.red}${sym.cross}${c.reset} Vision Runner failed: ${err.message}`);
|
|
3544
|
+
if (startSha) {
|
|
3545
|
+
console.log(` Rollback: ${c.cyan}git reset --hard ${startSha}${c.reset}`);
|
|
3546
|
+
}
|
|
3547
|
+
console.log('');
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
if (!hasGit) {
|
|
3553
|
+
console.log(`\n ${c.green}${sym.check}${c.reset} PRD complete (no git repo detected).\n`);
|
|
3554
|
+
return;
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
const prdFinal = readFileSync(prdPath, 'utf8');
|
|
3558
|
+
if (countUncheckedPrdTasks(prdFinal) > 0) {
|
|
3559
|
+
console.log(`\n ${c.yellow}${sym.warn}${c.reset} PRD still has remaining tasks; skipping commit/push.\n`);
|
|
3560
|
+
return;
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
const visionName = basename(visionPath).replace(/\.md$/, '');
|
|
3564
|
+
const commitMessage = `feat: run vision (${visionName})`;
|
|
3565
|
+
|
|
3566
|
+
const addRes = runGit(['add', '-A']);
|
|
3567
|
+
if (addRes.status !== 0) {
|
|
3568
|
+
console.log(`\n ${c.red}${sym.cross}${c.reset} git add failed.\n`);
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
const statusRes = runGit(['status', '--porcelain']);
|
|
3573
|
+
if (statusRes.status === 0 && String(statusRes.stdout || '').trim().length === 0) {
|
|
3574
|
+
console.log(`\n ${c.green}${sym.check}${c.reset} Nothing to commit.\n`);
|
|
3575
|
+
return;
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
const commitRes = runGit(['commit', '-m', commitMessage]);
|
|
3579
|
+
if (commitRes.status !== 0) {
|
|
3580
|
+
console.log(`\n ${c.red}${sym.cross}${c.reset} git commit failed.\n`);
|
|
3581
|
+
return;
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
if (!opts.noPush) {
|
|
3585
|
+
const pushRes = runGit(['push']);
|
|
3586
|
+
if (pushRes.status !== 0) {
|
|
3587
|
+
const branch = getCurrentBranch();
|
|
3588
|
+
if (branch) {
|
|
3589
|
+
runGit(['push', '-u', 'origin', branch]);
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
console.log(`\n ${c.green}${sym.check}${c.reset} Vision complete.`);
|
|
3595
|
+
if (startSha) {
|
|
3596
|
+
console.log(` Started from: ${c.dim}${startSha}${c.reset}`);
|
|
3597
|
+
}
|
|
3598
|
+
console.log('');
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3118
3601
|
// ============================================================================
|
|
3119
3602
|
// DASHBOARD (Agent Command Center)
|
|
3120
3603
|
// ============================================================================
|
|
@@ -4082,6 +4565,7 @@ function showHelp() {
|
|
|
4082
4565
|
console.log(`${pad} ${c.cyan}vision${c.reset} Show VISION.md status`);
|
|
4083
4566
|
console.log(`${pad} ${c.cyan}log${c.reset} Show task logs`);
|
|
4084
4567
|
console.log(`${pad} ${c.cyan}meet${c.reset} Meet Paul and the Artisans`);
|
|
4568
|
+
console.log(`${pad} ${c.cyan}run${c.reset} Run the vision (PRD loop)`);
|
|
4085
4569
|
console.log(`${pad} ${c.cyan}help${c.reset} Show this help\n`);
|
|
4086
4570
|
|
|
4087
4571
|
console.log(`${pad}${c.bold}Agent Commands${c.reset}`);
|
|
@@ -4097,7 +4581,7 @@ function showHelp() {
|
|
|
4097
4581
|
console.log(`${pad} ${c.cyan}cry${c.reset} "desc" Enter a problem or need`);
|
|
4098
4582
|
console.log(`${pad} ${c.cyan}wait${c.reset} <id> Move to waiting (seeking)`);
|
|
4099
4583
|
console.log(`${pad} ${c.cyan}vision${c.reset} <id> Move to vision (answer received)`);
|
|
4100
|
-
console.log(`${pad} ${c.cyan}run${c.reset}
|
|
4584
|
+
console.log(`${pad} ${c.cyan}run${c.reset} [id] Run the vision, or move item to run`);
|
|
4101
4585
|
console.log(`${pad} ${c.cyan}worship${c.reset} <id> Move to worship (retrospective)`);
|
|
4102
4586
|
console.log(`${pad} ${c.cyan}note${c.reset} <id> "text" Add note to item`);
|
|
4103
4587
|
console.log(`${pad} ${c.cyan}item${c.reset} <id> Show item details`);
|
|
@@ -4121,6 +4605,7 @@ function showHelp() {
|
|
|
4121
4605
|
console.log(`${pad} ${c.green}wtv init --claude${c.reset} ${c.dim}# Install for Claude Code${c.reset}`);
|
|
4122
4606
|
console.log(`${pad} ${c.green}wtv init --opencode${c.reset} ${c.dim}# Install for OpenCode${c.reset}`);
|
|
4123
4607
|
console.log(`${pad} ${c.green}wtv init --codex${c.reset} ${c.dim}# Install for Codex CLI${c.reset}`);
|
|
4608
|
+
console.log(`${pad} ${c.green}wtv run${c.reset} ${c.dim}# Execute a vision with PRD.md${c.reset}`);
|
|
4124
4609
|
console.log(`${pad} ${c.green}wtv board --all${c.reset} ${c.dim}# Show full kanban board${c.reset}`);
|
|
4125
4610
|
console.log(`${pad} ${c.green}wtv uninstall --tool opencode${c.reset} ${c.dim}# Remove OpenCode install${c.reset}`);
|
|
4126
4611
|
console.log('');
|
|
@@ -4139,6 +4624,18 @@ function parseArgs(args) {
|
|
|
4139
4624
|
task: null,
|
|
4140
4625
|
date: null,
|
|
4141
4626
|
all: false,
|
|
4627
|
+
|
|
4628
|
+
// Vision Runner (Ralphy-style)
|
|
4629
|
+
engine: null,
|
|
4630
|
+
vision: null,
|
|
4631
|
+
resume: false,
|
|
4632
|
+
regeneratePrd: false,
|
|
4633
|
+
maxIterations: 0,
|
|
4634
|
+
dryRun: false,
|
|
4635
|
+
fast: false,
|
|
4636
|
+
noTests: false,
|
|
4637
|
+
noLint: false,
|
|
4638
|
+
noPush: false,
|
|
4142
4639
|
};
|
|
4143
4640
|
|
|
4144
4641
|
let positionalCount = 0;
|
|
@@ -4176,6 +4673,77 @@ function parseArgs(args) {
|
|
|
4176
4673
|
continue;
|
|
4177
4674
|
}
|
|
4178
4675
|
|
|
4676
|
+
if (a === '--vision') {
|
|
4677
|
+
const v = args[i + 1];
|
|
4678
|
+
if (!v || v.startsWith('-')) {
|
|
4679
|
+
throw new Error("--vision requires a file path (e.g. '--vision vision/roadmap.md')");
|
|
4680
|
+
}
|
|
4681
|
+
opts.vision = v;
|
|
4682
|
+
i++;
|
|
4683
|
+
continue;
|
|
4684
|
+
}
|
|
4685
|
+
|
|
4686
|
+
if (a === '--engine') {
|
|
4687
|
+
const v = args[i + 1];
|
|
4688
|
+
if (!v || v.startsWith('-')) {
|
|
4689
|
+
throw new Error("--engine requires a value: opencode | codex | claude");
|
|
4690
|
+
}
|
|
4691
|
+
opts.engine = v;
|
|
4692
|
+
i++;
|
|
4693
|
+
continue;
|
|
4694
|
+
}
|
|
4695
|
+
|
|
4696
|
+
if (a === '--resume') {
|
|
4697
|
+
opts.resume = true;
|
|
4698
|
+
continue;
|
|
4699
|
+
}
|
|
4700
|
+
|
|
4701
|
+
if (a === '--regenerate-prd') {
|
|
4702
|
+
opts.regeneratePrd = true;
|
|
4703
|
+
continue;
|
|
4704
|
+
}
|
|
4705
|
+
|
|
4706
|
+
if (a === '--max-iterations') {
|
|
4707
|
+
const v = args[i + 1];
|
|
4708
|
+
if (!v || v.startsWith('-')) {
|
|
4709
|
+
throw new Error("--max-iterations requires a number (e.g. '--max-iterations 3')");
|
|
4710
|
+
}
|
|
4711
|
+
const n = parseInt(v, 10);
|
|
4712
|
+
if (Number.isNaN(n) || n < 0) {
|
|
4713
|
+
throw new Error('--max-iterations must be a non-negative number');
|
|
4714
|
+
}
|
|
4715
|
+
opts.maxIterations = n;
|
|
4716
|
+
i++;
|
|
4717
|
+
continue;
|
|
4718
|
+
}
|
|
4719
|
+
|
|
4720
|
+
if (a === '--dry-run') {
|
|
4721
|
+
opts.dryRun = true;
|
|
4722
|
+
continue;
|
|
4723
|
+
}
|
|
4724
|
+
|
|
4725
|
+
if (a === '--no-push') {
|
|
4726
|
+
opts.noPush = true;
|
|
4727
|
+
continue;
|
|
4728
|
+
}
|
|
4729
|
+
|
|
4730
|
+
if (a === '--no-tests' || a === '--skip-tests') {
|
|
4731
|
+
opts.noTests = true;
|
|
4732
|
+
continue;
|
|
4733
|
+
}
|
|
4734
|
+
|
|
4735
|
+
if (a === '--no-lint' || a === '--skip-lint') {
|
|
4736
|
+
opts.noLint = true;
|
|
4737
|
+
continue;
|
|
4738
|
+
}
|
|
4739
|
+
|
|
4740
|
+
if (a === '--fast') {
|
|
4741
|
+
opts.fast = true;
|
|
4742
|
+
opts.noTests = true;
|
|
4743
|
+
opts.noLint = true;
|
|
4744
|
+
continue;
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4179
4747
|
if (a === '--path') {
|
|
4180
4748
|
const v = args[i + 1];
|
|
4181
4749
|
if (!v || v.startsWith('-')) {
|
|
@@ -4272,7 +4840,7 @@ export async function run(args) {
|
|
|
4272
4840
|
const scope = opts.global ? 'global' : 'project';
|
|
4273
4841
|
|
|
4274
4842
|
if (opts.command !== 'version') {
|
|
4275
|
-
checkForUpdates();
|
|
4843
|
+
await checkForUpdates();
|
|
4276
4844
|
}
|
|
4277
4845
|
|
|
4278
4846
|
switch (opts.command) {
|
|
@@ -4431,12 +4999,18 @@ export async function run(args) {
|
|
|
4431
4999
|
}
|
|
4432
5000
|
|
|
4433
5001
|
case 'run': {
|
|
4434
|
-
if (
|
|
4435
|
-
|
|
4436
|
-
|
|
5002
|
+
if (opts.subcommand) {
|
|
5003
|
+
habakkukRun(opts.subcommand);
|
|
5004
|
+
break;
|
|
5005
|
+
}
|
|
5006
|
+
|
|
5007
|
+
if (!process.stdout.isTTY && (!opts.vision || !opts.engine)) {
|
|
5008
|
+
console.log(`\n ${c.red}Error:${c.reset} run requires interactive TTY, or pass --vision and --engine.`);
|
|
5009
|
+
console.log(` Example: wtv run --vision vision/VISION.md --engine opencode\n`);
|
|
4437
5010
|
process.exit(1);
|
|
4438
5011
|
}
|
|
4439
|
-
|
|
5012
|
+
|
|
5013
|
+
await visionRunner(opts);
|
|
4440
5014
|
break;
|
|
4441
5015
|
}
|
|
4442
5016
|
|