writethevision 7.0.5 → 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 +543 -5
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.5",
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
@@ -3151,6 +3151,453 @@ Prototype
3151
3151
  return true;
3152
3152
  }
3153
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
+
3154
3601
  // ============================================================================
3155
3602
  // DASHBOARD (Agent Command Center)
3156
3603
  // ============================================================================
@@ -4118,6 +4565,7 @@ function showHelp() {
4118
4565
  console.log(`${pad} ${c.cyan}vision${c.reset} Show VISION.md status`);
4119
4566
  console.log(`${pad} ${c.cyan}log${c.reset} Show task logs`);
4120
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)`);
4121
4569
  console.log(`${pad} ${c.cyan}help${c.reset} Show this help\n`);
4122
4570
 
4123
4571
  console.log(`${pad}${c.bold}Agent Commands${c.reset}`);
@@ -4133,7 +4581,7 @@ function showHelp() {
4133
4581
  console.log(`${pad} ${c.cyan}cry${c.reset} "desc" Enter a problem or need`);
4134
4582
  console.log(`${pad} ${c.cyan}wait${c.reset} <id> Move to waiting (seeking)`);
4135
4583
  console.log(`${pad} ${c.cyan}vision${c.reset} <id> Move to vision (answer received)`);
4136
- 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`);
4137
4585
  console.log(`${pad} ${c.cyan}worship${c.reset} <id> Move to worship (retrospective)`);
4138
4586
  console.log(`${pad} ${c.cyan}note${c.reset} <id> "text" Add note to item`);
4139
4587
  console.log(`${pad} ${c.cyan}item${c.reset} <id> Show item details`);
@@ -4157,6 +4605,7 @@ function showHelp() {
4157
4605
  console.log(`${pad} ${c.green}wtv init --claude${c.reset} ${c.dim}# Install for Claude Code${c.reset}`);
4158
4606
  console.log(`${pad} ${c.green}wtv init --opencode${c.reset} ${c.dim}# Install for OpenCode${c.reset}`);
4159
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}`);
4160
4609
  console.log(`${pad} ${c.green}wtv board --all${c.reset} ${c.dim}# Show full kanban board${c.reset}`);
4161
4610
  console.log(`${pad} ${c.green}wtv uninstall --tool opencode${c.reset} ${c.dim}# Remove OpenCode install${c.reset}`);
4162
4611
  console.log('');
@@ -4175,6 +4624,18 @@ function parseArgs(args) {
4175
4624
  task: null,
4176
4625
  date: null,
4177
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,
4178
4639
  };
4179
4640
 
4180
4641
  let positionalCount = 0;
@@ -4212,6 +4673,77 @@ function parseArgs(args) {
4212
4673
  continue;
4213
4674
  }
4214
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
+
4215
4747
  if (a === '--path') {
4216
4748
  const v = args[i + 1];
4217
4749
  if (!v || v.startsWith('-')) {
@@ -4467,12 +4999,18 @@ export async function run(args) {
4467
4999
  }
4468
5000
 
4469
5001
  case 'run': {
4470
- if (!opts.subcommand) {
4471
- console.log(`\n ${c.red}Error:${c.reset} Item ID or slug required.`);
4472
- 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`);
4473
5010
  process.exit(1);
4474
5011
  }
4475
- habakkukRun(opts.subcommand);
5012
+
5013
+ await visionRunner(opts);
4476
5014
  break;
4477
5015
  }
4478
5016