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.
Files changed (3) hide show
  1. package/README.md +14 -0
  2. package/package.json +1 -1
  3. 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.4",
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: getPackageName(),
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
- notifier.notify({
1284
- message: `Update available ${c.dim}${notifier.update.current}${c.reset} → ${c.green}${notifier.update.latest}${c.reset}\nRun ${c.cyan}npx writethevision update${c.reset} to get the latest version`,
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: 'center',
1287
+ align: 'left',
1290
1288
  borderColor: 'yellow',
1291
1289
  borderStyle: 'round',
1292
1290
  },
1293
1291
  });
1294
- } catch (err) {
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} <id> Move to run (execute)`);
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 (!opts.subcommand) {
4435
- console.log(`\n ${c.red}Error:${c.reset} Item ID or slug required.`);
4436
- console.log(` Usage: wtv run <id|slug>\n`);
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
- habakkukRun(opts.subcommand);
5012
+
5013
+ await visionRunner(opts);
4440
5014
  break;
4441
5015
  }
4442
5016