writethevision 7.0.7 → 7.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -296,9 +296,34 @@ The Masterbuilder reads your vision document and coordinates the artisans.
296
296
 
297
297
  ## Vision Runner (Ralphy-Style)
298
298
 
299
- `wtv run` can also execute a project vision directly by generating a `PRD.md` (checklist) and iterating until all tasks are complete.
299
+ `wtv run` (with no arguments) launches the **Vision Runner**, an autonomous loop that executes a project vision until completion.
300
300
 
301
- It maintains a `progress.txt` checkpoint log and, by default, creates a single git commit and pushes once at the end.
301
+ It is designed for "Ralphy-style" execution: give it a vision, and it runs until the vision is a reality.
302
+
303
+ ### The Workflow
304
+
305
+ 1. **Pick a Vision**: WTV finds `VISION.md` (root) or files in `vision/*.md` and asks you to select one.
306
+ 2. **Generate Plan**: It prompts the engine (OpenCode, Codex, or Claude) to generate a detailed `PRD.md` with a task checklist.
307
+ 3. **Execute Loop**:
308
+ - Reads `PRD.md` to find the next unchecked task.
309
+ - Creates a focused prompt for the engine.
310
+ - Runs the engine to implement the task.
311
+ - Verifies the task was marked completed in `PRD.md`.
312
+ - Appends a checkpoint to `progress.txt`.
313
+ - Repeats until all tasks are done.
314
+ 4. **Finalize**: Creates a single git commit ("feat: complete <vision>") and pushes to remote (unless disabled).
315
+
316
+ ### File Contracts
317
+
318
+ The Vision Runner relies on strict file conventions:
319
+
320
+ - **Input**:
321
+ - `VISION.md` (or `vision/*.md`): The source of truth for *what* to build.
322
+ - **Artifacts**:
323
+ - `PRD.md`: The execution checklist. Must use `- [ ]` for incomplete and `- [x]` for complete tasks. The runner stops when no `- [ ]` remain.
324
+ - `progress.txt`: An append-only log of every completed task with timestamps.
325
+
326
+ ### Usage
302
327
 
303
328
  ```bash
304
329
  wtv run
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "writethevision",
3
- "version": "7.0.7",
3
+ "version": "7.0.8",
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
@@ -1256,15 +1256,23 @@ async function checkForUpdates() {
1256
1256
 
1257
1257
  try {
1258
1258
  const packageName = getPackageName();
1259
+ const checkEveryRun = process.env.WTV_UPDATE_CHECK_EVERY_RUN === '1';
1259
1260
  const notifier = updateNotifier({
1260
1261
  pkg: {
1261
1262
  name: packageName,
1262
1263
  version: getVersion(),
1263
1264
  },
1264
- updateCheckInterval: 1000 * 60 * 60 * 24 * 7,
1265
+ updateCheckInterval: checkEveryRun ? Number.POSITIVE_INFINITY : 1000 * 60 * 60 * 24 * 7,
1265
1266
  shouldNotifyInNpmScript: false,
1266
1267
  });
1267
1268
 
1269
+ if (checkEveryRun) {
1270
+ try {
1271
+ notifier.update = await notifier.fetchInfo();
1272
+ } catch {
1273
+ }
1274
+ }
1275
+
1268
1276
  const update = notifier.update;
1269
1277
  if (!update) return;
1270
1278
 
@@ -1290,7 +1298,7 @@ Or run latest once:
1290
1298
  },
1291
1299
  });
1292
1300
 
1293
- const shouldPrompt = process.stdin.isTTY && process.env.WTV_NO_AUTO_UPDATE !== '1' && !process.env.CI;
1301
+ const shouldPrompt = process.stdin.isTTY && process.env.WTV_NO_AUTO_UPDATE !== '1' && !process.env.CI && !checkEveryRun;
1294
1302
  if (!shouldPrompt) return;
1295
1303
 
1296
1304
  const isDevCheckout = existsSync(join(PACKAGE_ROOT, '.git'));
@@ -3281,13 +3289,12 @@ async function runEngine(engine, promptText) {
3281
3289
  throw new Error(`Unsupported engine: ${engine}`);
3282
3290
  }
3283
3291
 
3284
- function ensureProgressHeader(progressPath, { visionPath, engine, startSha }) {
3292
+ export function ensureProgressHeader(progressPath, { visionPath, engine, startSha }) {
3285
3293
  if (!existsSync(progressPath)) {
3286
3294
  writeFileSync(progressPath, '');
3287
3295
  }
3288
3296
 
3289
3297
  const existing = readFileSync(progressPath, 'utf8');
3290
- if (existing.trim().length > 0) return;
3291
3298
 
3292
3299
  const startedAt = new Date().toISOString();
3293
3300
  const header = [
@@ -3296,16 +3303,19 @@ function ensureProgressHeader(progressPath, { visionPath, engine, startSha }) {
3296
3303
  `Vision: ${visionPath}`,
3297
3304
  `Engine: ${engine}`,
3298
3305
  startSha ? `Start SHA: ${startSha}` : `Start SHA: (not a git repo)`,
3306
+ startSha ? `Rollback hint: git reset --hard ${startSha}` : '',
3299
3307
  '',
3300
3308
  ].join('\n');
3301
3309
 
3302
- writeFileSync(progressPath, header);
3310
+ if (existing.trim().length > 0) {
3311
+ writeFileSync(progressPath, existing.trimEnd() + '\n\n' + header);
3312
+ } else {
3313
+ writeFileSync(progressPath, header);
3314
+ }
3303
3315
  }
3304
3316
 
3305
- async function generatePrdFromVision({ engine, visionPath, prdPath }) {
3306
- const visionContent = readFileSync(visionPath, 'utf8');
3307
-
3308
- const promptText = `You are generating a PRD for a software project.
3317
+ export function generatePrdPrompt(visionContent) {
3318
+ return `You are generating a PRD for a software project.
3309
3319
 
3310
3320
  Input vision document:
3311
3321
  ---
@@ -3322,6 +3332,12 @@ Requirements:
3322
3332
  - Do NOT implement any code yet.
3323
3333
  - Do NOT run git commit or git push.
3324
3334
  - Output is the updated files on disk (PRD.md).`;
3335
+ }
3336
+
3337
+ async function generatePrdFromVision({ engine, visionPath, prdPath }) {
3338
+ const visionContent = readFileSync(visionPath, 'utf8');
3339
+
3340
+ const promptText = generatePrdPrompt(visionContent);
3325
3341
 
3326
3342
  const code = await runEngine(engine, promptText);
3327
3343
  if (code !== 0) {
@@ -3338,7 +3354,7 @@ Requirements:
3338
3354
  }
3339
3355
  }
3340
3356
 
3341
- async function runVisionTaskIteration({ engine, prdPath, progressPath, taskText, noTests, noLint }) {
3357
+ export async function runVisionTaskIteration({ engine, prdPath, progressPath, taskText, noTests, noLint, _runEngine }) {
3342
3358
  const promptText = `You are an autonomous coding agent executing a PRD.
3343
3359
 
3344
3360
  Files:
@@ -3366,7 +3382,8 @@ Stop after completing this one task.`;
3366
3382
 
3367
3383
  const progressBefore = existsSync(progressPath) ? readFileSync(progressPath, 'utf8') : '';
3368
3384
 
3369
- const code = await runEngine(engine, promptText);
3385
+ const runFn = _runEngine || runEngine;
3386
+ const code = await runFn(engine, promptText);
3370
3387
  if (code !== 0) {
3371
3388
  throw new Error(`Engine exited with code ${code}`);
3372
3389
  }
@@ -3388,6 +3405,71 @@ Stop after completing this one task.`;
3388
3405
  }
3389
3406
  }
3390
3407
 
3408
+ export function finalizeVisionRun({
3409
+ prdPath,
3410
+ visionPath,
3411
+ hasGit,
3412
+ startSha,
3413
+ noPush,
3414
+ _runGit,
3415
+ _readFileSync,
3416
+ _countUncheckedPrdTasks,
3417
+ _getCurrentBranch
3418
+ }) {
3419
+ const runGitFn = _runGit || runGit;
3420
+ const readFileSyncFn = _readFileSync || readFileSync;
3421
+ const countUncheckedFn = _countUncheckedPrdTasks || countUncheckedPrdTasks;
3422
+ const getCurrentBranchFn = _getCurrentBranch || getCurrentBranch;
3423
+
3424
+ if (!hasGit) {
3425
+ console.log(`\n ${c.green}${sym.check}${c.reset} PRD complete (no git repo detected).\n`);
3426
+ return;
3427
+ }
3428
+
3429
+ const prdFinal = readFileSyncFn(prdPath, 'utf8');
3430
+ if (countUncheckedFn(prdFinal) > 0) {
3431
+ console.log(`\n ${c.yellow}${sym.warn}${c.reset} PRD still has remaining tasks; skipping commit/push.\n`);
3432
+ return;
3433
+ }
3434
+
3435
+ const visionName = basename(visionPath).replace(/\.md$/, '');
3436
+ const commitMessage = `feat: run vision (${visionName})`;
3437
+
3438
+ const addRes = runGitFn(['add', '-A']);
3439
+ if (addRes.status !== 0) {
3440
+ console.log(`\n ${c.red}${sym.cross}${c.reset} git add failed.\n`);
3441
+ return;
3442
+ }
3443
+
3444
+ const statusRes = runGitFn(['status', '--porcelain']);
3445
+ if (statusRes.status === 0 && String(statusRes.stdout || '').trim().length === 0) {
3446
+ console.log(`\n ${c.green}${sym.check}${c.reset} Nothing to commit.\n`);
3447
+ return;
3448
+ }
3449
+
3450
+ const commitRes = runGitFn(['commit', '-m', commitMessage]);
3451
+ if (commitRes.status !== 0) {
3452
+ console.log(`\n ${c.red}${sym.cross}${c.reset} git commit failed.\n`);
3453
+ return;
3454
+ }
3455
+
3456
+ if (!noPush) {
3457
+ const pushRes = runGitFn(['push']);
3458
+ if (pushRes.status !== 0) {
3459
+ const branch = getCurrentBranchFn();
3460
+ if (branch) {
3461
+ runGitFn(['push', '-u', 'origin', branch]);
3462
+ }
3463
+ }
3464
+ }
3465
+
3466
+ console.log(`\n ${c.green}${sym.check}${c.reset} Vision complete.`);
3467
+ if (startSha) {
3468
+ console.log(` Started from: ${c.dim}${startSha}${c.reset}`);
3469
+ }
3470
+ console.log('');
3471
+ }
3472
+
3391
3473
  async function visionRunner(opts) {
3392
3474
  const config = loadConfig();
3393
3475
  const interactive = process.stdout.isTTY && process.stdin.isTTY;
@@ -3458,6 +3540,12 @@ async function visionRunner(opts) {
3458
3540
  const hasGit = isGitRepo();
3459
3541
  const startSha = hasGit ? getGitHeadSha() : null;
3460
3542
 
3543
+ if (!hasGit) {
3544
+ console.log(`\n ${c.yellow}${sym.warn} Warning:${c.reset} Not a git repository. Version control features disabled.\n`);
3545
+ } else if (startSha) {
3546
+ console.log(`\n ${c.blue}${sym.info} Rollback hint:${c.reset} git reset --hard ${startSha}\n`);
3547
+ }
3548
+
3461
3549
  if (hasGit && !isWorkingTreeClean() && !opts.dryRun) {
3462
3550
  if (!interactive) {
3463
3551
  console.log(`\n ${c.red}Error:${c.reset} Working tree is not clean (non-interactive mode).\n`);
@@ -3554,53 +3642,13 @@ async function visionRunner(opts) {
3554
3642
  }
3555
3643
  }
3556
3644
 
3557
- if (!hasGit) {
3558
- console.log(`\n ${c.green}${sym.check}${c.reset} PRD complete (no git repo detected).\n`);
3559
- return;
3560
- }
3561
-
3562
- const prdFinal = readFileSync(prdPath, 'utf8');
3563
- if (countUncheckedPrdTasks(prdFinal) > 0) {
3564
- console.log(`\n ${c.yellow}${sym.warn}${c.reset} PRD still has remaining tasks; skipping commit/push.\n`);
3565
- return;
3566
- }
3567
-
3568
- const visionName = basename(visionPath).replace(/\.md$/, '');
3569
- const commitMessage = `feat: run vision (${visionName})`;
3570
-
3571
- const addRes = runGit(['add', '-A']);
3572
- if (addRes.status !== 0) {
3573
- console.log(`\n ${c.red}${sym.cross}${c.reset} git add failed.\n`);
3574
- return;
3575
- }
3576
-
3577
- const statusRes = runGit(['status', '--porcelain']);
3578
- if (statusRes.status === 0 && String(statusRes.stdout || '').trim().length === 0) {
3579
- console.log(`\n ${c.green}${sym.check}${c.reset} Nothing to commit.\n`);
3580
- return;
3581
- }
3582
-
3583
- const commitRes = runGit(['commit', '-m', commitMessage]);
3584
- if (commitRes.status !== 0) {
3585
- console.log(`\n ${c.red}${sym.cross}${c.reset} git commit failed.\n`);
3586
- return;
3587
- }
3588
-
3589
- if (!opts.noPush) {
3590
- const pushRes = runGit(['push']);
3591
- if (pushRes.status !== 0) {
3592
- const branch = getCurrentBranch();
3593
- if (branch) {
3594
- runGit(['push', '-u', 'origin', branch]);
3595
- }
3596
- }
3597
- }
3598
-
3599
- console.log(`\n ${c.green}${sym.check}${c.reset} Vision complete.`);
3600
- if (startSha) {
3601
- console.log(` Started from: ${c.dim}${startSha}${c.reset}`);
3602
- }
3603
- console.log('');
3645
+ finalizeVisionRun({
3646
+ prdPath,
3647
+ visionPath,
3648
+ hasGit,
3649
+ startSha,
3650
+ noPush: opts.noPush
3651
+ });
3604
3652
  }
3605
3653
 
3606
3654
  // ============================================================================
@@ -4570,7 +4618,7 @@ function showHelp() {
4570
4618
  console.log(`${pad} ${c.cyan}vision${c.reset} Show VISION.md status`);
4571
4619
  console.log(`${pad} ${c.cyan}log${c.reset} Show task logs`);
4572
4620
  console.log(`${pad} ${c.cyan}meet${c.reset} Meet Paul and the Artisans`);
4573
- console.log(`${pad} ${c.cyan}run${c.reset} Run the vision (PRD loop)`);
4621
+ console.log(`${pad} ${c.cyan}run${c.reset} Run the vision (Vision Runner)`);
4574
4622
  console.log(`${pad} ${c.cyan}help${c.reset} Show this help\n`);
4575
4623
 
4576
4624
  console.log(`${pad}${c.bold}Agent Commands${c.reset}`);
@@ -4581,12 +4629,17 @@ function showHelp() {
4581
4629
  console.log(`${pad} ${c.cyan}agents fav${c.reset} <name> Toggle favorite`);
4582
4630
  console.log(`${pad} ${c.cyan}agents rm${c.reset} <name> Remove agent\n`);
4583
4631
 
4632
+ console.log(`${pad}${c.bold}Vision Runner${c.reset} ${c.dim}"That he may run that readeth it"${c.reset}`);
4633
+ console.log(`${pad} ${c.cyan}run${c.reset} Launch Vision Runner (interactive)`);
4634
+ console.log(`${pad} ${c.cyan}run${c.reset} --vision <f> Execute specific vision file`);
4635
+ console.log(`${pad} ${c.cyan}run${c.reset} --resume Resume existing PRD.md\n`);
4636
+
4584
4637
  console.log(`${pad}${c.bold}Habakkuk Workflow${c.reset} ${c.dim}"Write the vision, make it plain"${c.reset}`);
4585
4638
  console.log(`${pad} ${c.cyan}board${c.reset} [--all] Show kanban board`);
4586
4639
  console.log(`${pad} ${c.cyan}cry${c.reset} "desc" Enter a problem or need`);
4587
4640
  console.log(`${pad} ${c.cyan}wait${c.reset} <id> Move to waiting (seeking)`);
4588
4641
  console.log(`${pad} ${c.cyan}vision${c.reset} <id> Move to vision (answer received)`);
4589
- console.log(`${pad} ${c.cyan}run${c.reset} [id] Run the vision, or move item to run`);
4642
+ console.log(`${pad} ${c.cyan}run${c.reset} <id> Move to run (execution started)`);
4590
4643
  console.log(`${pad} ${c.cyan}worship${c.reset} <id> Move to worship (retrospective)`);
4591
4644
  console.log(`${pad} ${c.cyan}note${c.reset} <id> "text" Add note to item`);
4592
4645
  console.log(`${pad} ${c.cyan}item${c.reset} <id> Show item details`);
@@ -4604,6 +4657,18 @@ function showHelp() {
4604
4657
  console.log(`${pad} ${c.dim}--gemini${c.reset} Target Gemini CLI`);
4605
4658
  console.log(`${pad} ${c.dim}--antigravity${c.reset} Target Antigravity\n`);
4606
4659
 
4660
+ console.log(`${pad}${c.bold}Vision Runner Options${c.reset}`);
4661
+ console.log(`${pad} ${c.dim}--vision <file>${c.reset} Input vision file (default: discover)`);
4662
+ console.log(`${pad} ${c.dim}--engine <name>${c.reset} Execution engine (opencode|codex|claude)`);
4663
+ console.log(`${pad} ${c.dim}--resume${c.reset} Resume existing PRD.md`);
4664
+ console.log(`${pad} ${c.dim}--regenerate-prd${c.reset} Force regeneration of PRD.md from vision`);
4665
+ console.log(`${pad} ${c.dim}--max-iterations <n>${c.reset} Stop after N tasks`);
4666
+ console.log(`${pad} ${c.dim}--dry-run${c.reset} Show what would happen without running`);
4667
+ console.log(`${pad} ${c.dim}--fast${c.reset} Skip tests and linting`);
4668
+ console.log(`${pad} ${c.dim}--no-tests${c.reset} Skip tests`);
4669
+ console.log(`${pad} ${c.dim}--no-lint${c.reset} Skip linting`);
4670
+ console.log(`${pad} ${c.dim}--no-push${c.reset} Skip git push at end\n`);
4671
+
4607
4672
  console.log(`${pad}${c.bold}Examples${c.reset}`);
4608
4673
  console.log(`${pad} ${c.green}wtv${c.reset} ${c.dim}# Dashboard${c.reset}`);
4609
4674
  console.log(`${pad} ${c.green}wtv agents${c.reset} ${c.dim}# List agents${c.reset}`);
package/src/engine.js ADDED
@@ -0,0 +1,109 @@
1
+ import { spawn } from 'child_process';
2
+
3
+ /**
4
+ * Base class for AI CLI engine adapters.
5
+ */
6
+ class EngineAdapter {
7
+ constructor(cwd = process.cwd()) {
8
+ this.cwd = cwd;
9
+ }
10
+
11
+ /**
12
+ * Get the command and arguments for the engine.
13
+ * @param {string} prompt - The prompt to execute.
14
+ * @returns {{ command: string, args: string[] }}
15
+ */
16
+ getCommand(prompt) {
17
+ throw new Error('getCommand must be implemented by subclass');
18
+ }
19
+
20
+ /**
21
+ * Run the engine with the given prompt.
22
+ * @param {string} prompt - The prompt to execute.
23
+ * @param {object} options - Options for spawn.
24
+ * @returns {Promise<number>} - Exit code.
25
+ */
26
+ async run(prompt, options = {}) {
27
+ const { command, args } = this.getCommand(prompt);
28
+
29
+ console.log(`[Engine] Running: ${command} ${args.join(' ')}`);
30
+
31
+ return new Promise((resolve, reject) => {
32
+ const child = spawn(command, args, {
33
+ cwd: this.cwd,
34
+ stdio: 'inherit', // Stream output to parent stdout/stderr
35
+ shell: true, // Use shell to resolve command paths
36
+ ...options
37
+ });
38
+
39
+ child.on('error', (err) => {
40
+ reject(err);
41
+ });
42
+
43
+ child.on('close', (code) => {
44
+ resolve(code);
45
+ });
46
+ });
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Adapter for OpenCode CLI.
52
+ */
53
+ class OpenCodeEngine extends EngineAdapter {
54
+ getCommand(prompt) {
55
+ // Assumption: opencode takes the prompt as a single argument string
56
+ return {
57
+ command: 'opencode',
58
+ args: [prompt]
59
+ };
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Adapter for Codex CLI.
65
+ */
66
+ class CodexEngine extends EngineAdapter {
67
+ getCommand(prompt) {
68
+ // Assumption: codex takes the prompt as a single argument string
69
+ return {
70
+ command: 'codex',
71
+ args: [prompt]
72
+ };
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Adapter for Claude Code CLI.
78
+ */
79
+ class ClaudeEngine extends EngineAdapter {
80
+ getCommand(prompt) {
81
+ // Assumption: claude takes the prompt as a single argument string
82
+ // Note: Check if it needs -p or similar flag
83
+ return {
84
+ command: 'claude',
85
+ args: [prompt]
86
+ };
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Factory to create the appropriate engine adapter.
92
+ * @param {string} name - The name of the engine ('opencode', 'codex', 'claude').
93
+ * @param {string} cwd - Current working directory.
94
+ * @returns {EngineAdapter}
95
+ */
96
+ export function createEngine(name, cwd) {
97
+ switch (name.toLowerCase()) {
98
+ case 'opencode':
99
+ return new OpenCodeEngine(cwd);
100
+ case 'codex':
101
+ return new CodexEngine(cwd);
102
+ case 'claude':
103
+ return new ClaudeEngine(cwd);
104
+ default:
105
+ throw new Error(`Unsupported engine: ${name}`);
106
+ }
107
+ }
108
+
109
+ export { EngineAdapter, OpenCodeEngine, CodexEngine, ClaudeEngine };