writethevision 7.0.1

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 (37) hide show
  1. package/README.md +382 -0
  2. package/bin/wtv.js +8 -0
  3. package/package.json +51 -0
  4. package/src/cli.js +4452 -0
  5. package/templates/VISION_TEMPLATE.md +22 -0
  6. package/templates/WTV.md +37 -0
  7. package/templates/agents/aholiab.md +58 -0
  8. package/templates/agents/bezaleel.md +58 -0
  9. package/templates/agents/david.md +60 -0
  10. package/templates/agents/ezra.md +57 -0
  11. package/templates/agents/hiram.md +59 -0
  12. package/templates/agents/moses.md +57 -0
  13. package/templates/agents/nehemiah.md +59 -0
  14. package/templates/agents/paul.md +360 -0
  15. package/templates/agents/solomon.md +57 -0
  16. package/templates/agents/zerubbabel.md +57 -0
  17. package/templates/skills/aholiab-seo/SKILL.md +456 -0
  18. package/templates/skills/aholiab-ui/SKILL.md +377 -0
  19. package/templates/skills/aholiab-ux/SKILL.md +393 -0
  20. package/templates/skills/bezaleel-architect/SKILL.md +395 -0
  21. package/templates/skills/bezaleel-stack/SKILL.md +782 -0
  22. package/templates/skills/david-copy/SKILL.md +423 -0
  23. package/templates/skills/ezra-docs/SKILL.md +391 -0
  24. package/templates/skills/ezra-qa/SKILL.md +407 -0
  25. package/templates/skills/hiram-backend/SKILL.md +383 -0
  26. package/templates/skills/hiram-performance/SKILL.md +404 -0
  27. package/templates/skills/moses-product/SKILL.md +413 -0
  28. package/templates/skills/moses-user-testing/SKILL.md +215 -0
  29. package/templates/skills/nehemiah-compliance/SKILL.md +450 -0
  30. package/templates/skills/nehemiah-security/SKILL.md +352 -0
  31. package/templates/skills/paul-artisan-contract/SKILL.md +179 -0
  32. package/templates/skills/paul-quality/SKILL.md +410 -0
  33. package/templates/skills/solomon-database/SKILL.md +390 -0
  34. package/templates/skills/wtv/SKILL.md +397 -0
  35. package/templates/skills/zerubbabel-cost/SKILL.md +389 -0
  36. package/templates/skills/zerubbabel-devops/SKILL.md +389 -0
  37. package/templates/skills/zerubbabel-observability/SKILL.md +483 -0
package/src/cli.js ADDED
@@ -0,0 +1,4452 @@
1
+ import { fileURLToPath } from 'url';
2
+ import { basename, dirname, join, resolve } from 'path';
3
+ import {
4
+ accessSync,
5
+ constants,
6
+ copyFileSync,
7
+ existsSync,
8
+ lstatSync,
9
+ mkdirSync,
10
+ readdirSync,
11
+ readFileSync,
12
+ rmSync,
13
+ statSync,
14
+ writeFileSync,
15
+ } from 'fs';
16
+ import { homedir, platform } from 'os';
17
+ import { spawn } from 'child_process';
18
+
19
+ async function openInEditor(filePath) {
20
+ const editor = process.env.EDITOR || 'vi';
21
+
22
+ // Safety check for raw mode
23
+ const wasRaw = process.stdin.isRaw;
24
+ if (wasRaw) process.stdin.setRawMode(false);
25
+
26
+ const child = spawn(editor, [filePath], {
27
+ stdio: 'inherit'
28
+ });
29
+
30
+ await new Promise((resolve) => {
31
+ child.on('exit', () => {
32
+ if (wasRaw) {
33
+ process.stdin.setRawMode(true);
34
+ process.stdin.resume();
35
+ }
36
+ resolve();
37
+ });
38
+ });
39
+ }
40
+
41
+ import readline, { createInterface, emitKeypressEvents } from 'readline';
42
+ import updateNotifier from 'update-notifier';
43
+
44
+ const __filename = fileURLToPath(import.meta.url);
45
+ const __dirname = dirname(__filename);
46
+ const PACKAGE_ROOT = resolve(__dirname, '..');
47
+ const TEMPLATES_DIR = join(PACKAGE_ROOT, 'templates');
48
+
49
+ const TOOL_IDS = ['claude', 'codex', 'opencode', 'gemini', 'antigravity'];
50
+ const VISION_TEMPLATE_PATH = join(TEMPLATES_DIR, 'VISION_TEMPLATE.md');
51
+ const WTV_LOGS_DIR = '.wtv/logs';
52
+ const LEGACY_LOGS_DIR = '.codehogg/logs';
53
+
54
+ // ANSI colors (works on Windows 10+, macOS, Linux)
55
+ const c = {
56
+ reset: '\x1b[0m',
57
+ bold: '\x1b[1m',
58
+ dim: '\x1b[2m',
59
+ green: '\x1b[32m',
60
+ yellow: '\x1b[33m',
61
+ blue: '\x1b[34m',
62
+ magenta: '\x1b[35m',
63
+ cyan: '\x1b[36m',
64
+ red: '\x1b[31m',
65
+ white: '\x1b[37m',
66
+ bgBlack: '\x1b[40m',
67
+ };
68
+
69
+ // Semantic theme (maps purpose to color)
70
+ const theme = {
71
+ success: c.green,
72
+ warning: c.yellow,
73
+ error: c.red,
74
+ info: c.cyan,
75
+ accent: c.magenta,
76
+ muted: c.dim,
77
+ highlight: c.bold,
78
+ brand: c.magenta,
79
+ };
80
+
81
+ // Symbols (use ASCII fallback on Windows if needed)
82
+ const isWindows = platform() === 'win32';
83
+ const sym = {
84
+ check: isWindows ? '[OK]' : '✓',
85
+ cross: isWindows ? '[X]' : '✗',
86
+ arrow: isWindows ? '>' : '❯',
87
+ bullet: isWindows ? '*' : '•',
88
+ warn: isWindows ? '[!]' : '⚠',
89
+ info: isWindows ? '[i]' : 'ℹ',
90
+ spinner: isWindows
91
+ ? ['|', '/', '-', '\\']
92
+ : ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
93
+ };
94
+
95
+ // Box drawing characters
96
+ const box = {
97
+ round: { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' },
98
+ square: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' },
99
+ double: { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' },
100
+ heavy: { tl: '┏', tr: '┓', bl: '┗', br: '┛', h: '━', v: '┃' },
101
+ };
102
+
103
+ // Layout constants
104
+ const MAX_WIDTH = 70;
105
+ const PADDING = 2;
106
+
107
+ // Get terminal width, constrained to readable max
108
+ function getWidth() {
109
+ const termWidth = process.stdout.columns || 80;
110
+ return Math.min(termWidth - PADDING * 2, MAX_WIDTH);
111
+ }
112
+
113
+ // Center text within terminal
114
+ function centerPad() {
115
+ const termWidth = process.stdout.columns || 80;
116
+ const contentWidth = Math.min(termWidth, MAX_WIDTH + PADDING * 2);
117
+ return ' '.repeat(Math.max(0, Math.floor((termWidth - contentWidth) / 2)));
118
+ }
119
+
120
+ // Draw a box around content
121
+ function drawBox(lines, opts = {}) {
122
+ const { style = 'round', color = '', padding = 1 } = opts;
123
+ const chars = box[style] || box.round;
124
+ const width = getWidth();
125
+ const innerWidth = width - 2;
126
+
127
+ const padLine = ' '.repeat(innerWidth);
128
+ const output = [];
129
+ const prefix = centerPad();
130
+ const col = color || '';
131
+ const reset = color ? c.reset : '';
132
+
133
+ // Top border
134
+ output.push(`${prefix}${col}${chars.tl}${chars.h.repeat(innerWidth)}${chars.tr}${reset}`);
135
+
136
+ // Padding top
137
+ for (let i = 0; i < padding; i++) {
138
+ output.push(`${prefix}${col}${chars.v}${reset}${padLine}${col}${chars.v}${reset}`);
139
+ }
140
+
141
+ // Content lines
142
+ for (const line of lines) {
143
+ const stripped = line.replace(/\x1b\[[0-9;]*m/g, '');
144
+ const padRight = Math.max(0, innerWidth - stripped.length);
145
+ output.push(`${prefix}${col}${chars.v}${reset}${line}${' '.repeat(padRight)}${col}${chars.v}${reset}`);
146
+ }
147
+
148
+ // Padding bottom
149
+ for (let i = 0; i < padding; i++) {
150
+ output.push(`${prefix}${col}${chars.v}${reset}${padLine}${col}${chars.v}${reset}`);
151
+ }
152
+
153
+ // Bottom border
154
+ output.push(`${prefix}${col}${chars.bl}${chars.h.repeat(innerWidth)}${chars.br}${reset}`);
155
+
156
+ return output.join('\n');
157
+ }
158
+
159
+ // Simple spinner for async operations
160
+ function createSpinner(text) {
161
+ if (!process.stdout.isTTY) {
162
+ console.log(` ${sym.bullet} ${text}`);
163
+ return { stop: () => { }, succeed: () => { }, fail: () => { } };
164
+ }
165
+
166
+ let i = 0;
167
+ const frames = sym.spinner;
168
+ const id = setInterval(() => {
169
+ process.stdout.write(`\r ${c.cyan}${frames[i++ % frames.length]}${c.reset} ${text}`);
170
+ }, 80);
171
+
172
+ return {
173
+ stop: (finalText) => {
174
+ clearInterval(id);
175
+ process.stdout.write(`\r ${c.dim}${sym.bullet}${c.reset} ${finalText || text}\n`);
176
+ },
177
+ succeed: (finalText) => {
178
+ clearInterval(id);
179
+ process.stdout.write(`\r ${c.green}${sym.check}${c.reset} ${finalText || text}\n`);
180
+ },
181
+ fail: (finalText) => {
182
+ clearInterval(id);
183
+ process.stdout.write(`\r ${c.red}${sym.cross}${c.reset} ${finalText || text}\n`);
184
+ },
185
+ };
186
+ }
187
+
188
+ function getPackageJson() {
189
+ return JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8'));
190
+ }
191
+
192
+ function getVersion() {
193
+ return getPackageJson().version;
194
+ }
195
+
196
+ function getPackageName() {
197
+ return getPackageJson().name;
198
+ }
199
+
200
+ // ============================================================================
201
+ // CONFIG SYSTEM
202
+ // ============================================================================
203
+
204
+ const WTV_CONFIG_DIR = '.wtv';
205
+ const LEGACY_CONFIG_DIR = '.codehogg';
206
+ const WTV_CONFIG_FILE = 'config.json';
207
+
208
+ const LEGACY_AGENT_NAME_ALIASES = {
209
+ masterbuilder: 'paul',
210
+ 'security-artisan': 'nehemiah',
211
+ 'architecture-artisan': 'bezaleel',
212
+ 'backend-artisan': 'hiram',
213
+ 'frontend-artisan': 'aholiab',
214
+ 'database-artisan': 'solomon',
215
+ 'devops-artisan': 'zerubbabel',
216
+ 'qa-artisan': 'ezra',
217
+ 'product-artisan': 'moses',
218
+ };
219
+
220
+ function normalizeAgentName(name) {
221
+ return LEGACY_AGENT_NAME_ALIASES[name] || name;
222
+ }
223
+
224
+ function normalizeFavorites(favorites) {
225
+ if (!Array.isArray(favorites)) return [];
226
+ const out = [];
227
+
228
+ for (const fav of favorites) {
229
+ if (!fav) continue;
230
+ const normalized = normalizeAgentName(fav);
231
+ if (!normalized) continue;
232
+ if (!out.includes(normalized)) out.push(normalized);
233
+ }
234
+
235
+ return out;
236
+ }
237
+
238
+ function getConfigPaths(scope) {
239
+ if (scope === 'global') {
240
+ const home = getHomedir();
241
+ if (!home) return [];
242
+ return [
243
+ join(home, WTV_CONFIG_DIR, WTV_CONFIG_FILE),
244
+ join(home, LEGACY_CONFIG_DIR, WTV_CONFIG_FILE),
245
+ ];
246
+ }
247
+
248
+ return [
249
+ join(process.cwd(), WTV_CONFIG_DIR, WTV_CONFIG_FILE),
250
+ join(process.cwd(), LEGACY_CONFIG_DIR, WTV_CONFIG_FILE),
251
+ ];
252
+ }
253
+
254
+ function getPrimaryConfigPath(scope) {
255
+ const paths = getConfigPaths(scope);
256
+ return paths.length ? paths[0] : null;
257
+ }
258
+
259
+ function loadConfig() {
260
+ const [globalWtv, globalLegacy] = getConfigPaths('global');
261
+ const [localWtv, localLegacy] = getConfigPaths('project');
262
+
263
+ const globalPath = globalWtv && existsSync(globalWtv)
264
+ ? globalWtv
265
+ : (globalLegacy && existsSync(globalLegacy) ? globalLegacy : null);
266
+
267
+ const localPath = localWtv && existsSync(localWtv)
268
+ ? localWtv
269
+ : (localLegacy && existsSync(localLegacy) ? localLegacy : null);
270
+
271
+ let globalConfig = {};
272
+ let localConfig = {};
273
+
274
+ if (globalPath) {
275
+ try {
276
+ globalConfig = JSON.parse(readFileSync(globalPath, 'utf8'));
277
+ } catch { /* ignore parse errors */ }
278
+ }
279
+
280
+ if (localPath) {
281
+ try {
282
+ localConfig = JSON.parse(readFileSync(localPath, 'utf8'));
283
+ } catch { /* ignore parse errors */ }
284
+ }
285
+
286
+ // Merge: local overrides global, but arrays are replaced not merged
287
+ const rawFavorites = localConfig.favorites || globalConfig.favorites || [];
288
+
289
+ return {
290
+ favorites: normalizeFavorites(rawFavorites),
291
+ defaultTool: localConfig.defaultTool || globalConfig.defaultTool || 'claude',
292
+ editor: localConfig.editor || globalConfig.editor || null,
293
+ };
294
+ }
295
+
296
+ function saveConfig(config, scope = 'project') {
297
+ const configPath = getPrimaryConfigPath(scope);
298
+ if (!configPath) return false;
299
+
300
+ const configDir = dirname(configPath);
301
+ ensureDir(configDir);
302
+
303
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
304
+ return true;
305
+ }
306
+
307
+ function isFavorite(agentName) {
308
+ const config = loadConfig();
309
+ const normalized = normalizeAgentName(agentName);
310
+ return config.favorites.includes(normalized);
311
+ }
312
+
313
+ function toggleFavorite(agentName, scope = 'project') {
314
+ const normalized = normalizeAgentName(agentName);
315
+ const config = loadConfig();
316
+ const idx = config.favorites.indexOf(normalized);
317
+
318
+ if (idx === -1) {
319
+ config.favorites.push(normalized);
320
+ } else {
321
+ config.favorites.splice(idx, 1);
322
+ }
323
+
324
+ saveConfig({ favorites: config.favorites }, scope);
325
+ return idx === -1; // Returns true if now a favorite
326
+ }
327
+
328
+ // ============================================================================
329
+ // AGENT DISCOVERY
330
+ // ============================================================================
331
+
332
+ function getAgentLocations() {
333
+ const home = getHomedir();
334
+ const locations = [];
335
+
336
+ // Local (project-level)
337
+ locations.push({
338
+ label: 'Local',
339
+ path: join(process.cwd(), '.claude', 'agents'),
340
+ tool: 'claude',
341
+ scope: 'local',
342
+ });
343
+ locations.push({
344
+ label: 'Local (OpenCode)',
345
+ path: join(process.cwd(), '.opencode', 'agent'),
346
+ tool: 'opencode',
347
+ scope: 'local',
348
+ });
349
+ locations.push({
350
+ label: 'Local (Gemini)',
351
+ path: join(process.cwd(), '.gemini', 'agents'),
352
+ tool: 'gemini',
353
+ scope: 'local',
354
+ });
355
+ // Antigravity does not use "agent definition files" locally like Claude/OpenCode.
356
+ // However, for visualization, we might want to check for something, or just rely on rules.
357
+ // For now, we will track .agent/rules/ as a proxy for "local configuration".
358
+ locations.push({
359
+ label: 'Local (Antigravity Rules)',
360
+ path: join(process.cwd(), '.agent', 'rules'),
361
+ tool: 'antigravity',
362
+ scope: 'local',
363
+ });
364
+
365
+ // Global
366
+ if (home) {
367
+ locations.push({
368
+ label: 'Global',
369
+ path: join(home, '.claude', 'agents'),
370
+ tool: 'claude',
371
+ scope: 'global',
372
+ });
373
+ locations.push({
374
+ label: 'Global (OpenCode)',
375
+ path: join(home, '.config', 'opencode', 'agent'),
376
+ tool: 'opencode',
377
+ scope: 'global',
378
+ });
379
+ locations.push({
380
+ label: 'Global (Gemini)',
381
+ path: join(home, '.gemini', 'agents'),
382
+ tool: 'gemini',
383
+ scope: 'global',
384
+ });
385
+ // Antigravity global
386
+ locations.push({
387
+ label: 'Global (Antigravity)',
388
+ path: join(home, '.gemini', 'antigravity'),
389
+ tool: 'antigravity',
390
+ scope: 'global',
391
+ });
392
+ }
393
+
394
+ // Templates (Available to be installed)
395
+ locations.push({
396
+ label: 'Available',
397
+ path: join(TEMPLATES_DIR, 'agents'),
398
+ tool: 'all',
399
+ scope: 'available',
400
+ });
401
+
402
+ return locations;
403
+ }
404
+
405
+ function parseAgentFile(filePath) {
406
+ if (!existsSync(filePath)) return null;
407
+
408
+ try {
409
+ const content = readFileSync(filePath, 'utf8');
410
+ const name = filePath.split('/').pop().replace(/\.md$/, '');
411
+
412
+ // Parse frontmatter
413
+ let description = '';
414
+ let model = 'opus';
415
+ let tools = [];
416
+ let skills = [];
417
+
418
+ if (content.startsWith('---')) {
419
+ const endIdx = content.indexOf('\n---\n', 4);
420
+ if (endIdx !== -1) {
421
+ const fm = content.slice(4, endIdx);
422
+ for (const line of fm.split('\n')) {
423
+ const match = line.match(/^([a-z]+):\s*(.+)$/i);
424
+ if (match) {
425
+ const [, key, val] = match;
426
+ if (key === 'description') description = val.replace(/^["']|["']$/g, '');
427
+ if (key === 'model') model = val;
428
+ if (key === 'tools') tools = val.split(',').map(t => t.trim());
429
+ if (key === 'skills') skills = val.split(',').map(s => s.trim());
430
+ }
431
+ }
432
+ }
433
+ }
434
+
435
+ // Fallback: extract first paragraph if no description
436
+ if (!description) {
437
+ const bodyMatch = content.match(/\n---\n\s*\n(.+)/);
438
+ if (bodyMatch) {
439
+ description = bodyMatch[1].slice(0, 80).replace(/[#*_]/g, '').trim();
440
+ }
441
+ }
442
+
443
+ // Extract ASCII Art (looking for text blocks)
444
+ let asciiArt = '';
445
+ const asciiMatch = content.match(/```text\n([\s\S]+?)\n```/);
446
+ if (asciiMatch) {
447
+ asciiArt = asciiMatch[1];
448
+ }
449
+
450
+ return {
451
+ name,
452
+ description: description || 'No description',
453
+ model,
454
+ tools,
455
+ skills,
456
+ path: filePath,
457
+ favorite: isFavorite(name),
458
+ asciiArt,
459
+ };
460
+ } catch {
461
+ return null;
462
+ }
463
+ }
464
+
465
+ function discoverAgents(scopeFilter = null) {
466
+ const locations = getAgentLocations();
467
+ const agents = [];
468
+ const seenNames = new Set();
469
+
470
+ for (const loc of locations) {
471
+ // Filter by scope if specified
472
+ if (scopeFilter && loc.scope !== scopeFilter) continue;
473
+
474
+ if (!existsSync(loc.path)) continue;
475
+
476
+ try {
477
+ const files = readdirSync(loc.path).filter(f => f.endsWith('.md'));
478
+ for (const file of files) {
479
+ const agent = parseAgentFile(join(loc.path, file));
480
+ if (agent && !seenNames.has(agent.name)) {
481
+ seenNames.add(agent.name);
482
+ agents.push({
483
+ ...agent,
484
+ location: loc,
485
+ });
486
+ }
487
+ }
488
+ } catch { /* ignore read errors */ }
489
+ }
490
+
491
+ // Sort: favorites first, then alphabetically
492
+ return agents.sort((a, b) => {
493
+ if (a.favorite && !b.favorite) return -1;
494
+ if (!a.favorite && b.favorite) return 1;
495
+ return a.name.localeCompare(b.name);
496
+ });
497
+ }
498
+
499
+ function findAgent(name) {
500
+ const agents = discoverAgents();
501
+ return agents.find(a => a.name === name || a.name === `${name}-artisan`);
502
+ }
503
+
504
+ // ============================================================================
505
+ // AGENT TEMPLATE
506
+ // ============================================================================
507
+
508
+ const AGENT_TEMPLATE = `---
509
+ name: {{NAME}}
510
+ description: {{DESCRIPTION}}
511
+ tools: Read, Glob, Grep, Edit, Write, Bash, WebFetch, WebSearch
512
+ model: opus
513
+ skills: artisan-contract
514
+ ---
515
+
516
+ # {{TITLE}}
517
+
518
+ You are the **{{TITLE}}**.
519
+
520
+ ## Your Expertise
521
+
522
+ - [Define your areas of expertise]
523
+ - [What domains do you cover?]
524
+ - [What problems do you solve?]
525
+
526
+ ## Mode of Operation
527
+
528
+ Paul (the Masterbuilder) will invoke you in one of two modes:
529
+
530
+ ### Counsel Mode
531
+ Provide domain-specific advice for a mission. Identify concerns, recommend approaches, suggest implementations.
532
+
533
+ ### Execution Mode
534
+ Implement assigned tasks from an approved plan. Build, test, verify, report.
535
+
536
+ ## Follow the Contract
537
+
538
+ Always follow the \`artisan-contract\` skill for:
539
+ - Output format (Counsel or Execution)
540
+ - Evidence citations
541
+ - Vision alignment
542
+ - Distance assessment
543
+ - Confidence levels
544
+
545
+ ## Domain-Specific Guidance
546
+
547
+ When providing counsel or executing:
548
+
549
+ 1. **[First check]** — What to verify first
550
+ 2. **[Second check]** — What to assess next
551
+ 3. **[Third check]** — What to evaluate
552
+ 4. **[Fourth check]** — What to consider
553
+ 5. **[Fifth check]** — What to review
554
+
555
+ ## Your Lane
556
+
557
+ Your domain includes:
558
+ - [List what's in scope]
559
+ - [List related technologies]
560
+ - [List relevant patterns]
561
+
562
+ If you see issues in other domains, note them for Paul but don't attempt to fix them.
563
+ `;
564
+
565
+ function createAgentFromTemplate(name, description) {
566
+ const title = name
567
+ .split('-')
568
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
569
+ .join(' ');
570
+
571
+ return AGENT_TEMPLATE
572
+ .replace(/\{\{NAME\}\}/g, name)
573
+ .replace(/\{\{TITLE\}\}/g, title)
574
+ .replace(/\{\{DESCRIPTION\}\}/g, description);
575
+ }
576
+
577
+ // ============================================================================
578
+ // HABAKKUK WORKFLOW SYSTEM
579
+ // "Write the vision, and make it plain upon tables" — Habakkuk 2:2
580
+ // ============================================================================
581
+
582
+ const HABAKKUK_DIR = '.wtv/habakkuk';
583
+ const LEGACY_HABAKKUK_DIR = '.codehogg/habakkuk';
584
+ const HABAKKUK_STAGES = ['cry', 'wait', 'vision', 'run', 'worship'];
585
+ const HABAKKUK_STAGE_LABELS = {
586
+ cry: 'CRY OUT',
587
+ wait: 'WAIT',
588
+ vision: 'VISION',
589
+ run: 'RUN',
590
+ worship: 'WORSHIP',
591
+ };
592
+ const HABAKKUK_STAGE_VERSES = {
593
+ cry: 'Hab 2:1a',
594
+ wait: 'Hab 2:1b',
595
+ vision: 'Hab 2:2a',
596
+ run: 'Hab 2:2b',
597
+ worship: 'Hab 3',
598
+ };
599
+
600
+ function getHabakkukDir() {
601
+ const wtv = join(process.cwd(), HABAKKUK_DIR);
602
+ if (existsSync(wtv)) return wtv;
603
+
604
+ const legacy = join(process.cwd(), LEGACY_HABAKKUK_DIR);
605
+ if (existsSync(legacy)) return legacy;
606
+
607
+ return wtv;
608
+ }
609
+
610
+ function getHabakkukItemsDir() {
611
+ return join(getHabakkukDir(), 'items');
612
+ }
613
+
614
+ function ensureHabakkukDirs() {
615
+ ensureDir(getHabakkukDir());
616
+ ensureDir(getHabakkukItemsDir());
617
+ }
618
+
619
+ function slugify(text) {
620
+ return text
621
+ .toLowerCase()
622
+ .replace(/[^a-z0-9]+/g, '-')
623
+ .replace(/^-|-$/g, '')
624
+ .slice(0, 50);
625
+ }
626
+
627
+ function loadHabakkukBoard() {
628
+ const boardPath = join(getHabakkukDir(), 'board.json');
629
+ if (!existsSync(boardPath)) {
630
+ return { nextId: 1, items: {} };
631
+ }
632
+ try {
633
+ return JSON.parse(readFileSync(boardPath, 'utf8'));
634
+ } catch {
635
+ return { nextId: 1, items: {} };
636
+ }
637
+ }
638
+
639
+ function saveHabakkukBoard(board) {
640
+ ensureHabakkukDirs();
641
+ const boardPath = join(getHabakkukDir(), 'board.json');
642
+ writeFileSync(boardPath, JSON.stringify(board, null, 2));
643
+ }
644
+
645
+ function loadHabakkukItem(id) {
646
+ const board = loadHabakkukBoard();
647
+ const itemRef = board.items[id];
648
+ if (!itemRef) return null;
649
+
650
+ const itemPath = join(getHabakkukItemsDir(), `${itemRef.file}.json`);
651
+ if (!existsSync(itemPath)) return null;
652
+
653
+ try {
654
+ return JSON.parse(readFileSync(itemPath, 'utf8'));
655
+ } catch {
656
+ return null;
657
+ }
658
+ }
659
+
660
+ function saveHabakkukItem(item) {
661
+ ensureHabakkukDirs();
662
+ const itemPath = join(getHabakkukItemsDir(), `${item.id}-${item.slug}.json`);
663
+ writeFileSync(itemPath, JSON.stringify(item, null, 2));
664
+ }
665
+
666
+ function findHabakkukItem(idOrSlug) {
667
+ const board = loadHabakkukBoard();
668
+
669
+ // Try direct ID match
670
+ if (board.items[idOrSlug]) {
671
+ return loadHabakkukItem(idOrSlug);
672
+ }
673
+
674
+ // Try slug match
675
+ for (const [id, ref] of Object.entries(board.items)) {
676
+ if (ref.slug === idOrSlug || ref.file.includes(idOrSlug)) {
677
+ return loadHabakkukItem(id);
678
+ }
679
+ }
680
+
681
+ return null;
682
+ }
683
+
684
+ function createHabakkukItem(title) {
685
+ const board = loadHabakkukBoard();
686
+ const id = String(board.nextId).padStart(3, '0');
687
+ const slug = slugify(title);
688
+ const now = new Date().toISOString();
689
+
690
+ const item = {
691
+ id,
692
+ slug,
693
+ title,
694
+ stage: 'cry',
695
+ created: now,
696
+ updated: now,
697
+ history: [
698
+ { stage: 'cry', entered: now }
699
+ ],
700
+ notes: [],
701
+ vision: null,
702
+ execution: null,
703
+ worship: null,
704
+ };
705
+
706
+ // Save item
707
+ saveHabakkukItem(item);
708
+
709
+ // Update board
710
+ board.nextId++;
711
+ board.items[id] = { slug, file: `${id}-${slug}` };
712
+ saveHabakkukBoard(board);
713
+
714
+ return item;
715
+ }
716
+
717
+ function moveHabakkukItem(item, newStage) {
718
+ const currentIdx = HABAKKUK_STAGES.indexOf(item.stage);
719
+ const newIdx = HABAKKUK_STAGES.indexOf(newStage);
720
+
721
+ if (newIdx < 0) return null;
722
+ if (newIdx !== currentIdx + 1 && newStage !== 'worship') {
723
+ // Can only move forward one stage (except worship can be reached from run)
724
+ return null;
725
+ }
726
+
727
+ const now = new Date().toISOString();
728
+ item.stage = newStage;
729
+ item.updated = now;
730
+ item.history.push({ stage: newStage, entered: now });
731
+
732
+ saveHabakkukItem(item);
733
+ return item;
734
+ }
735
+
736
+ function addHabakkukNote(item, note) {
737
+ const now = new Date().toISOString();
738
+ item.notes.push({ date: now, text: note });
739
+ item.updated = now;
740
+ saveHabakkukItem(item);
741
+ return item;
742
+ }
743
+
744
+ function getItemsByStage() {
745
+ const board = loadHabakkukBoard();
746
+ const byStage = {
747
+ cry: [],
748
+ wait: [],
749
+ vision: [],
750
+ run: [],
751
+ worship: [],
752
+ };
753
+
754
+ for (const id of Object.keys(board.items)) {
755
+ const item = loadHabakkukItem(id);
756
+ if (item && byStage[item.stage]) {
757
+ byStage[item.stage].push(item);
758
+ }
759
+ }
760
+
761
+ return byStage;
762
+ }
763
+
764
+ function formatTimeAgo(isoDate) {
765
+ const now = Date.now();
766
+ const then = new Date(isoDate).getTime();
767
+ const diffMs = now - then;
768
+ const diffMins = Math.floor(diffMs / 60000);
769
+ const diffHours = Math.floor(diffMs / 3600000);
770
+ const diffDays = Math.floor(diffMs / 86400000);
771
+
772
+ if (diffMins < 60) return `${diffMins}m ago`;
773
+ if (diffHours < 24) return `${diffHours}h ago`;
774
+ return `${diffDays}d ago`;
775
+ }
776
+
777
+ // ============================================================================
778
+ // HABAKKUK CLI COMMANDS
779
+ // ============================================================================
780
+
781
+ function habakkukBoard(showAll = false) {
782
+ const pad = centerPad();
783
+ const byStage = getItemsByStage();
784
+
785
+ console.log('');
786
+ console.log(drawBox([
787
+ ` ${c.bold}THE HABAKKUK BOARD${c.reset}`,
788
+ ` ${c.dim}"Write the vision, make it plain"${c.reset}`,
789
+ ], { style: 'double', color: c.yellow, padding: 0 }));
790
+ console.log('');
791
+
792
+ // Calculate column widths
793
+ const colWidth = 14;
794
+ const stages = showAll ? HABAKKUK_STAGES : HABAKKUK_STAGES.filter(s => s !== 'worship' || byStage.worship.length > 0);
795
+
796
+ // Header row
797
+ let header = pad;
798
+ for (const stage of stages) {
799
+ const label = HABAKKUK_STAGE_LABELS[stage];
800
+ header += ` ${c.bold}${label.padEnd(colWidth)}${c.reset}`;
801
+ }
802
+ console.log(header);
803
+
804
+ // Divider row
805
+ let divider = pad;
806
+ for (const stage of stages) {
807
+ divider += ` ${c.dim}${'─'.repeat(colWidth)}${c.reset}`;
808
+ }
809
+ console.log(divider);
810
+
811
+ // Find max items in any column
812
+ const maxItems = Math.max(...stages.map(s => byStage[s].length), 1);
813
+
814
+ // Item rows
815
+ for (let i = 0; i < maxItems; i++) {
816
+ let row = pad;
817
+ for (const stage of stages) {
818
+ const item = byStage[stage][i];
819
+ if (item) {
820
+ const shortTitle = item.title.slice(0, colWidth - 4);
821
+ row += ` ${c.cyan}#${item.id}${c.reset} ${shortTitle.padEnd(colWidth - 4)}`;
822
+ } else {
823
+ row += ' '.repeat(colWidth + 1);
824
+ }
825
+ }
826
+ console.log(row);
827
+
828
+ // Time ago row
829
+ row = pad;
830
+ for (const stage of stages) {
831
+ const item = byStage[stage][i];
832
+ if (item) {
833
+ const ago = formatTimeAgo(item.updated);
834
+ row += ` ${c.dim}${ago.padEnd(colWidth)}${c.reset}`;
835
+ } else {
836
+ row += ' '.repeat(colWidth + 1);
837
+ }
838
+ }
839
+ console.log(row);
840
+ console.log('');
841
+ }
842
+
843
+ // Empty state
844
+ const totalItems = Object.values(byStage).flat().length;
845
+ if (totalItems === 0) {
846
+ console.log(`${pad}${c.dim}No items yet. Start with:${c.reset}`);
847
+ console.log(`${pad} ${c.green}wtv cry "Your burden or need"${c.reset}`);
848
+ console.log('');
849
+ }
850
+
851
+ // Scripture
852
+ console.log(`${pad}${c.dim}"I will stand upon my watch... and will watch to see${c.reset}`);
853
+ console.log(`${pad}${c.dim} what he will say unto me" — Habakkuk 2:1${c.reset}`);
854
+ console.log('');
855
+ }
856
+
857
+ function habakkukCry(title) {
858
+ const pad = centerPad();
859
+
860
+ if (!title) {
861
+ console.log(`\n${pad}${c.red}${sym.cross}${c.reset} Please provide a description of your burden.`);
862
+ console.log(`${pad}${c.dim}Usage: wtv cry "Description of need"${c.reset}\n`);
863
+ return;
864
+ }
865
+
866
+ const item = createHabakkukItem(title);
867
+
868
+ console.log('');
869
+ console.log(`${pad}${c.yellow}${sym.bullet}${c.reset} ${c.bold}CRY OUT${c.reset} — Item created`);
870
+ console.log(`${pad} ${c.cyan}#${item.id}${c.reset} ${item.title}`);
871
+ console.log('');
872
+ console.log(`${pad}${c.dim}"I will stand upon my watch" — Hab 2:1a${c.reset}`);
873
+ console.log('');
874
+ console.log(`${pad}Next: When ready to seek, run:`);
875
+ console.log(`${pad} ${c.green}wtv wait ${item.id}${c.reset}`);
876
+ console.log('');
877
+ }
878
+
879
+ function habakkukWait(idOrSlug, note) {
880
+ const pad = centerPad();
881
+ const item = findHabakkukItem(idOrSlug);
882
+
883
+ if (!item) {
884
+ console.log(`\n${pad}${c.red}${sym.cross}${c.reset} Item not found: ${idOrSlug}\n`);
885
+ return;
886
+ }
887
+
888
+ // If just adding a note
889
+ if (note && item.stage === 'wait') {
890
+ addHabakkukNote(item, note);
891
+ console.log(`\n${pad}${c.green}${sym.check}${c.reset} Note added to #${item.id}\n`);
892
+ return;
893
+ }
894
+
895
+ // Move from cry to wait
896
+ if (item.stage !== 'cry') {
897
+ console.log(`\n${pad}${c.yellow}${sym.warn}${c.reset} Item #${item.id} is already in ${HABAKKUK_STAGE_LABELS[item.stage]}\n`);
898
+ return;
899
+ }
900
+
901
+ moveHabakkukItem(item, 'wait');
902
+
903
+ console.log('');
904
+ console.log(`${pad}${c.blue}${sym.bullet}${c.reset} ${c.bold}WAIT${c.reset} — Moved to watchtower`);
905
+ console.log(`${pad} ${c.cyan}#${item.id}${c.reset} ${item.title}`);
906
+ console.log('');
907
+ console.log(`${pad}${c.dim}"...and will watch to see what he will say unto me" — Hab 2:1b${c.reset}`);
908
+ console.log('');
909
+ console.log(`${pad}Add notes while waiting:`);
910
+ console.log(`${pad} ${c.green}wtv note ${item.id} "Insight received"${c.reset}`);
911
+ console.log('');
912
+ console.log(`${pad}When the vision comes:`);
913
+ console.log(`${pad} ${c.green}wtv vision ${item.id}${c.reset}`);
914
+ console.log('');
915
+ }
916
+
917
+ function habakkukVision(idOrSlug) {
918
+ const pad = centerPad();
919
+ const item = findHabakkukItem(idOrSlug);
920
+
921
+ if (!item) {
922
+ console.log(`\n${pad}${c.red}${sym.cross}${c.reset} Item not found: ${idOrSlug}\n`);
923
+ return;
924
+ }
925
+
926
+ if (item.stage !== 'wait') {
927
+ console.log(`\n${pad}${c.yellow}${sym.warn}${c.reset} Item must be in WAIT stage to receive vision.`);
928
+ console.log(`${pad}${c.dim}Current stage: ${HABAKKUK_STAGE_LABELS[item.stage]}${c.reset}\n`);
929
+ return;
930
+ }
931
+
932
+ moveHabakkukItem(item, 'vision');
933
+
934
+ // Create vision directory and template
935
+ const visionDir = join(getHabakkukItemsDir(), `${item.id}-${item.slug}`);
936
+ ensureDir(visionDir);
937
+
938
+ const visionPath = join(visionDir, 'VISION.md');
939
+ if (!existsSync(visionPath)) {
940
+ const visionTemplate = `# Vision: ${item.title}
941
+
942
+ > Received: ${new Date().toISOString().split('T')[0]}
943
+
944
+ ## The Answer
945
+
946
+ [What solution or approach has become clear?]
947
+
948
+ ## The Plan
949
+
950
+ 1. [First step]
951
+ 2. [Second step]
952
+ 3. [Third step]
953
+
954
+ ## Success Criteria
955
+
956
+ - [What will be true when complete?]
957
+
958
+ ## Constraints
959
+
960
+ - [Any limitations or boundaries?]
961
+ `;
962
+ writeFileSync(visionPath, visionTemplate);
963
+ }
964
+
965
+ console.log('');
966
+ console.log(`${pad}${c.magenta}${sym.bullet}${c.reset} ${c.bold}VISION${c.reset} — Answer received`);
967
+ console.log(`${pad} ${c.cyan}#${item.id}${c.reset} ${item.title}`);
968
+ console.log('');
969
+ console.log(`${pad}${c.dim}"Write the vision, and make it plain upon tables" — Hab 2:2a${c.reset}`);
970
+ console.log('');
971
+ console.log(`${pad}Vision document created at:`);
972
+ console.log(`${pad} ${c.dim}${visionPath}${c.reset}`);
973
+ console.log('');
974
+ console.log(`${pad}Edit the vision, then run:`);
975
+ console.log(`${pad} ${c.green}wtv run ${item.id}${c.reset}`);
976
+ console.log('');
977
+ }
978
+
979
+ function habakkukRun(idOrSlug) {
980
+ const pad = centerPad();
981
+ const item = findHabakkukItem(idOrSlug);
982
+
983
+ if (!item) {
984
+ console.log(`\n${pad}${c.red}${sym.cross}${c.reset} Item not found: ${idOrSlug}\n`);
985
+ return;
986
+ }
987
+
988
+ if (item.stage !== 'vision') {
989
+ console.log(`\n${pad}${c.yellow}${sym.warn}${c.reset} Item must be in VISION stage to run.`);
990
+ console.log(`${pad}${c.dim}Current stage: ${HABAKKUK_STAGE_LABELS[item.stage]}${c.reset}\n`);
991
+ return;
992
+ }
993
+
994
+ moveHabakkukItem(item, 'run');
995
+
996
+ console.log('');
997
+ console.log(`${pad}${c.green}${sym.bullet}${c.reset} ${c.bold}RUN${c.reset} — Execution started`);
998
+ console.log(`${pad} ${c.cyan}#${item.id}${c.reset} ${item.title}`);
999
+ console.log('');
1000
+ console.log(`${pad}${c.dim}"...that he may run that readeth it" — Hab 2:2b${c.reset}`);
1001
+ console.log('');
1002
+ console.log(`${pad}Inside your AI CLI, invoke Paul (the Masterbuilder):`);
1003
+ console.log(`${pad} ${c.cyan}/wtv "${item.title}"${c.reset}`);
1004
+ console.log('');
1005
+ console.log(`${pad}When complete:`);
1006
+ console.log(`${pad} ${c.green}wtv worship ${item.id}${c.reset}`);
1007
+ console.log('');
1008
+ }
1009
+
1010
+ async function habakkukWorship(idOrSlug) {
1011
+ const pad = centerPad();
1012
+ const item = findHabakkukItem(idOrSlug);
1013
+
1014
+ if (!item) {
1015
+ console.log(`\n${pad}${c.red}${sym.cross}${c.reset} Item not found: ${idOrSlug}\n`);
1016
+ return;
1017
+ }
1018
+
1019
+ if (item.stage !== 'run') {
1020
+ console.log(`\n${pad}${c.yellow}${sym.warn}${c.reset} Item must be in RUN stage to complete.`);
1021
+ console.log(`${pad}${c.dim}Current stage: ${HABAKKUK_STAGE_LABELS[item.stage]}${c.reset}\n`);
1022
+ return;
1023
+ }
1024
+
1025
+ // Create worship document
1026
+ const worshipDir = join(getHabakkukItemsDir(), `${item.id}-${item.slug}`);
1027
+ ensureDir(worshipDir);
1028
+
1029
+ const worshipPath = join(worshipDir, 'WORSHIP.md');
1030
+ if (!existsSync(worshipPath)) {
1031
+ const worshipTemplate = `# Worship: ${item.title}
1032
+
1033
+ > Completed: ${new Date().toISOString().split('T')[0]}
1034
+
1035
+ ## What Was Accomplished
1036
+
1037
+ - [List what was built/fixed/created]
1038
+
1039
+ ## What Was Learned
1040
+
1041
+ - [Insights gained during this work]
1042
+
1043
+ ## Evidence of Faithfulness
1044
+
1045
+ - [How did God provide during this work?]
1046
+
1047
+ ## Gratitude
1048
+
1049
+ Thank you Lord for:
1050
+ - [What are you thankful for?]
1051
+
1052
+ ## Stones of Remembrance
1053
+
1054
+ [What should be remembered about this work?]
1055
+ `;
1056
+ writeFileSync(worshipPath, worshipTemplate);
1057
+ }
1058
+
1059
+ moveHabakkukItem(item, 'worship');
1060
+
1061
+ console.log('');
1062
+ console.log(`${pad}${c.yellow}★${c.reset} ${c.bold}WORSHIP${c.reset} — Complete!`);
1063
+ console.log(`${pad} ${c.cyan}#${item.id}${c.reset} ${item.title}`);
1064
+ console.log('');
1065
+ console.log(`${pad}${c.dim}"Yet I will rejoice in the LORD, I will joy in the${c.reset}`);
1066
+ console.log(`${pad}${c.dim} God of my salvation." — Habakkuk 3:18${c.reset}`);
1067
+ console.log('');
1068
+ console.log(`${pad}Retrospective document created at:`);
1069
+ console.log(`${pad} ${c.dim}${worshipPath}${c.reset}`);
1070
+ console.log('');
1071
+ console.log(`${pad}Edit to capture what God has done.`);
1072
+ console.log('');
1073
+ }
1074
+
1075
+ function habakkukNote(idOrSlug, note) {
1076
+ const pad = centerPad();
1077
+ const item = findHabakkukItem(idOrSlug);
1078
+
1079
+ if (!item) {
1080
+ console.log(`\n${pad}${c.red}${sym.cross}${c.reset} Item not found: ${idOrSlug}\n`);
1081
+ return;
1082
+ }
1083
+
1084
+ if (!note) {
1085
+ // Show existing notes
1086
+ console.log('');
1087
+ console.log(`${pad}${c.bold}Notes for #${item.id}${c.reset} ${item.title}`);
1088
+ console.log('');
1089
+ if (item.notes.length === 0) {
1090
+ console.log(`${pad}${c.dim}No notes yet.${c.reset}`);
1091
+ } else {
1092
+ for (const n of item.notes) {
1093
+ const date = new Date(n.date).toLocaleDateString();
1094
+ console.log(`${pad} ${c.dim}${date}${c.reset} ${n.text}`);
1095
+ }
1096
+ }
1097
+ console.log('');
1098
+ return;
1099
+ }
1100
+
1101
+ addHabakkukNote(item, note);
1102
+ console.log(`\n${pad}${c.green}${sym.check}${c.reset} Note added to #${item.id}\n`);
1103
+ }
1104
+
1105
+ function habakkukItem(idOrSlug) {
1106
+ const pad = centerPad();
1107
+ const item = findHabakkukItem(idOrSlug);
1108
+
1109
+ if (!item) {
1110
+ console.log(`\n${pad}${c.red}${sym.cross}${c.reset} Item not found: ${idOrSlug}\n`);
1111
+ return;
1112
+ }
1113
+
1114
+ const stageLabel = HABAKKUK_STAGE_LABELS[item.stage];
1115
+ const stageVerse = HABAKKUK_STAGE_VERSES[item.stage];
1116
+
1117
+ console.log('');
1118
+ console.log(drawBox([
1119
+ ` ${c.cyan}#${item.id}${c.reset} ${c.bold}${item.title}${c.reset}`,
1120
+ ` ${c.dim}Stage: ${stageLabel} (${stageVerse})${c.reset}`,
1121
+ ], { style: 'round', color: c.cyan, padding: 0 }));
1122
+ console.log('');
1123
+
1124
+ console.log(`${pad}${c.bold}Timeline${c.reset}`);
1125
+ for (const h of item.history) {
1126
+ const date = new Date(h.entered).toLocaleDateString();
1127
+ const label = HABAKKUK_STAGE_LABELS[h.stage];
1128
+ console.log(`${pad} ${c.dim}${date}${c.reset} → ${label}`);
1129
+ }
1130
+ console.log('');
1131
+
1132
+ if (item.notes.length > 0) {
1133
+ console.log(`${pad}${c.bold}Notes${c.reset}`);
1134
+ for (const n of item.notes.slice(-3)) {
1135
+ const date = new Date(n.date).toLocaleDateString();
1136
+ console.log(`${pad} ${c.dim}${date}${c.reset} ${n.text}`);
1137
+ }
1138
+ if (item.notes.length > 3) {
1139
+ console.log(`${pad} ${c.dim}...and ${item.notes.length - 3} more${c.reset}`);
1140
+ }
1141
+ console.log('');
1142
+ }
1143
+
1144
+ // Show next action based on stage
1145
+ const nextActions = {
1146
+ cry: `wtv wait ${item.id}`,
1147
+ wait: `wtv vision ${item.id}`,
1148
+ vision: `wtv run ${item.id}`,
1149
+ run: `wtv worship ${item.id}`,
1150
+ worship: null,
1151
+ };
1152
+
1153
+ if (nextActions[item.stage]) {
1154
+ console.log(`${pad}${c.dim}Next:${c.reset} ${c.green}${nextActions[item.stage]}${c.reset}`);
1155
+ console.log('');
1156
+ }
1157
+ }
1158
+
1159
+ function habakkukStones() {
1160
+ const pad = centerPad();
1161
+ const byStage = getItemsByStage();
1162
+ const completed = byStage.worship;
1163
+
1164
+ console.log('');
1165
+ console.log(drawBox([
1166
+ ` ${c.bold}STONES OF REMEMBRANCE${c.reset}`,
1167
+ ` ${c.dim}Evidence of God's faithfulness${c.reset}`,
1168
+ ], { style: 'round', color: c.yellow, padding: 0 }));
1169
+ console.log('');
1170
+
1171
+ if (completed.length === 0) {
1172
+ console.log(`${pad}${c.dim}No completed items yet.${c.reset}`);
1173
+ console.log(`${pad}${c.dim}As you complete work through the Habakkuk workflow,${c.reset}`);
1174
+ console.log(`${pad}${c.dim}your stones of remembrance will appear here.${c.reset}`);
1175
+ } else {
1176
+ for (const item of completed) {
1177
+ const completed = item.history.find(h => h.stage === 'worship');
1178
+ const date = completed ? new Date(completed.entered).toLocaleDateString() : '';
1179
+ console.log(`${pad}${c.yellow}★${c.reset} ${c.cyan}#${item.id}${c.reset} ${item.title} ${c.dim}(${date})${c.reset}`);
1180
+ }
1181
+ }
1182
+ console.log('');
1183
+ }
1184
+
1185
+ function checkForUpdates() {
1186
+ const shouldCheck = process.env.WTV_NO_UPDATE_CHECK !== '1' && process.env.CODEHOGG_NO_UPDATE_CHECK !== '1';
1187
+ if (!shouldCheck) return;
1188
+ if (!process.stdout.isTTY) return;
1189
+
1190
+ try {
1191
+ const notifier = updateNotifier({
1192
+ pkg: {
1193
+ name: getPackageName(),
1194
+ version: getVersion(),
1195
+ },
1196
+ updateCheckInterval: 1000 * 60 * 60 * 24 * 7,
1197
+ shouldNotifyInNpmScript: false,
1198
+ });
1199
+
1200
+ const update = notifier.update;
1201
+ if (!update) return;
1202
+
1203
+ notifier.notify({
1204
+ message: `Update available ${c.dim}${update.current}${c.reset} → ${c.green}${update.latest}${c.reset}
1205
+ Run ${c.cyan}npx writethevision update${c.reset} to get the latest version`,
1206
+ defer: false,
1207
+ boxenOpts: {
1208
+ padding: 1,
1209
+ margin: 1,
1210
+ align: 'center',
1211
+ borderColor: 'yellow',
1212
+ borderStyle: 'round',
1213
+ },
1214
+ });
1215
+
1216
+ notifier.notify({
1217
+ 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`,
1218
+ defer: false,
1219
+ boxenOpts: {
1220
+ padding: 1,
1221
+ margin: 1,
1222
+ align: 'center',
1223
+ borderColor: 'yellow',
1224
+ borderStyle: 'round',
1225
+ },
1226
+ });
1227
+ } catch (err) {
1228
+ }
1229
+ }
1230
+
1231
+ function getHomedir() {
1232
+ try {
1233
+ const home = homedir();
1234
+ if (!home || home === '') return null;
1235
+ return home;
1236
+ } catch {
1237
+ return null;
1238
+ }
1239
+ }
1240
+
1241
+ function validatePath(dirPath) {
1242
+ try {
1243
+ const parentDir = dirname(dirPath);
1244
+ ensureDir(parentDir);
1245
+ accessSync(parentDir, constants.W_OK);
1246
+ return { valid: true };
1247
+ } catch (err) {
1248
+ return { valid: false, error: `Cannot access directory: ${err.message}` };
1249
+ }
1250
+ }
1251
+
1252
+ function copyDir(src, dest) {
1253
+ if (!existsSync(dest)) {
1254
+ mkdirSync(dest, { recursive: true });
1255
+ }
1256
+
1257
+ const entries = readdirSync(src);
1258
+ let copied = 0;
1259
+
1260
+ for (const entry of entries) {
1261
+ const srcPath = join(src, entry);
1262
+ const destPath = join(dest, entry);
1263
+ const stat = statSync(srcPath);
1264
+
1265
+ if (stat.isDirectory()) {
1266
+ copied += copyDir(srcPath, destPath);
1267
+ } else {
1268
+ copyFileSync(srcPath, destPath);
1269
+ copied++;
1270
+ }
1271
+ }
1272
+
1273
+ return copied;
1274
+ }
1275
+
1276
+ // ============================================================================
1277
+ // WTV SECTION MARKERS (for safe append/remove in tool config files)
1278
+ // ============================================================================
1279
+
1280
+ const WTV_SECTION_START = '<!-- WTV:START -->';
1281
+ const WTV_SECTION_END = '<!-- WTV:END -->';
1282
+
1283
+ const WTV_SECTION_CONTENT = `${WTV_SECTION_START}
1284
+ ## Vision-Driven Development
1285
+
1286
+ This project uses WTV for vision-driven AI development.
1287
+
1288
+ ### Vision Context
1289
+ - Read \`VISION.md\` from project root for core project vision
1290
+ - Read all markdown files in \`vision/\` directory for additional context (roadmap, values, ideas)
1291
+ - If no vision exists, suggest running \`wtv init\` to create one
1292
+
1293
+ ### Agent System
1294
+ - Expert agents are defined in the tool's agents directory
1295
+ - Read \`AGENTS.md\` for agent coordination rules
1296
+ ${WTV_SECTION_END}`;
1297
+
1298
+ function appendWtvSection(filePath) {
1299
+ let content = '';
1300
+ if (existsSync(filePath)) {
1301
+ content = readFileSync(filePath, 'utf8');
1302
+ // Already has WTV section? Skip.
1303
+ if (content.includes(WTV_SECTION_START)) {
1304
+ return false;
1305
+ }
1306
+ }
1307
+
1308
+ // Append with newlines
1309
+ const newContent = content.trimEnd() + '\n\n' + WTV_SECTION_CONTENT + '\n';
1310
+ writeFileSync(filePath, newContent);
1311
+ return true;
1312
+ }
1313
+
1314
+ function removeWtvSection(filePath) {
1315
+ if (!existsSync(filePath)) return false;
1316
+
1317
+ let content = readFileSync(filePath, 'utf8');
1318
+ if (!content.includes(WTV_SECTION_START)) {
1319
+ return false;
1320
+ }
1321
+
1322
+ // Remove WTV section using regex
1323
+ const startEscaped = WTV_SECTION_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1324
+ const endEscaped = WTV_SECTION_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1325
+ const regex = new RegExp(`\n*${startEscaped}[\\s\\S]*?${endEscaped}\n*`, 'g');
1326
+ const newContent = content.replace(regex, '\n');
1327
+ writeFileSync(filePath, newContent.trimEnd() + '\n');
1328
+ return true;
1329
+ }
1330
+
1331
+ function countFiles(dir) {
1332
+ if (!existsSync(dir)) return 0;
1333
+ let count = 0;
1334
+ for (const entry of readdirSync(dir)) {
1335
+ const p = join(dir, entry);
1336
+ const stat = statSync(p);
1337
+ count += stat.isDirectory() ? countFiles(p) : 1;
1338
+ }
1339
+ return count;
1340
+ }
1341
+
1342
+ function countDirs(dir) {
1343
+ if (!existsSync(dir)) return 0;
1344
+ return readdirSync(dir).filter(e => statSync(join(dir, e)).isDirectory()).length;
1345
+ }
1346
+
1347
+ function countFilesFlat(dir, ext = null) {
1348
+ if (!existsSync(dir)) return 0;
1349
+ return readdirSync(dir)
1350
+ .filter(e => {
1351
+ const p = join(dir, e);
1352
+ if (!statSync(p).isFile()) return false;
1353
+ return ext ? e.endsWith(ext) : true;
1354
+ }).length;
1355
+ }
1356
+
1357
+ function prompt(question) {
1358
+ return new Promise((resolve) => {
1359
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1360
+ rl.question(question, (answer) => {
1361
+ rl.close();
1362
+ resolve(answer.trim());
1363
+ });
1364
+ });
1365
+ }
1366
+
1367
+ async function select(question, options) {
1368
+ console.log(`\n ${c.bold}${question}${c.reset} \n`);
1369
+ options.forEach((opt, i) => {
1370
+ console.log(` ${c.cyan}${i + 1}${c.reset}) ${opt.label} `);
1371
+ if (opt.desc) console.log(` ${c.dim}${opt.desc}${c.reset} `);
1372
+ });
1373
+
1374
+ const answer = await prompt(`\n Enter choice(1 - ${options.length}): `);
1375
+ const idx = parseInt(answer, 10) - 1;
1376
+
1377
+ if (idx >= 0 && idx < options.length) {
1378
+ return options[idx].value;
1379
+ }
1380
+ return options[0].value;
1381
+ }
1382
+
1383
+ async function confirm(question, defaultYes = true) {
1384
+ const hint = defaultYes ? 'Y/n' : 'y/N';
1385
+ const answer = await prompt(` ${question} (${hint}): `);
1386
+
1387
+ if (answer === '') return defaultYes;
1388
+ return answer.toLowerCase().startsWith('y');
1389
+ }
1390
+
1391
+ function printBanner() {
1392
+ const version = getVersion();
1393
+ console.log('');
1394
+ console.log(drawBox([
1395
+ ` ${c.bold}${c.magenta}wtv${c.reset} ${c.dim}v${version}${c.reset} `,
1396
+ ` ${c.dim} 10 agents ${sym.bullet} 21 skills${c.reset} `,
1397
+ ` ${c.dim} Claude • Codex • OpenCode • Gemini • Antigravity${c.reset} `,
1398
+ ], { style: 'round', color: c.magenta, padding: 0 }));
1399
+ console.log('');
1400
+ }
1401
+
1402
+ function sleep(ms) {
1403
+ return new Promise(resolve => setTimeout(resolve, ms));
1404
+ }
1405
+
1406
+ // ASCII Art Avatars for the team
1407
+ const AVATARS = {
1408
+ paul: `
1409
+ ${c.yellow}___________${c.reset}
1410
+ ${c.yellow}/ \\${c.reset}
1411
+ ${c.yellow}| ${c.bold}◈ ◈${c.reset}${c.yellow} | ${c.reset}
1412
+ ${c.yellow}| ${c.bold}▽${c.reset}${c.yellow} | ${c.reset}
1413
+ ${c.yellow}| ${c.bold}═════${c.reset}${c.yellow} | ${c.reset}
1414
+ ${c.yellow} \\ ${c.dim}PLAN${c.reset}${c.yellow} /${c.reset}
1415
+ ${c.yellow}╔═══════╗${c.reset}
1416
+ ${c.yellow}║${c.dim}VISION${c.reset}${c.yellow}║${c.reset}
1417
+ ${c.yellow}╚═══════╝${c.reset} `,
1418
+
1419
+ nehemiah: `
1420
+ ${c.red}╔═══════╗${c.reset}
1421
+ ${c.red}║${c.bold} ◉ ◉ ${c.reset}${c.red}║${c.reset}
1422
+ ${c.red}║ ${c.bold}▼${c.reset}${c.red} ║${c.reset}
1423
+ ${c.red}╔╩═══════╩╗${c.reset}
1424
+ ${c.red}║ ${c.bold}◇ ▣ ◇${c.reset}${c.red} ║${c.reset}
1425
+ ${c.red}║ ${c.bold}◇ ◇ ◇${c.reset}${c.red} ║${c.reset}
1426
+ ${c.red}╚════╦════╝${c.reset}
1427
+ ${c.red}║${c.reset}
1428
+ ${c.dim} [SHIELD]${c.reset} `,
1429
+
1430
+ bezaleel: `
1431
+ ${c.blue}△${c.reset}
1432
+ ${c.blue}╱ ╲${c.reset}
1433
+ ${c.blue}╱${c.bold}◈ ◈${c.reset}${c.blue}╲${c.reset}
1434
+ ${c.blue}╱ ${c.bold}▽${c.reset}${c.blue} ╲${c.reset}
1435
+ ${c.blue}╱═══════╲${c.reset}
1436
+ ${c.blue}║ ║ ║ ║${c.reset}
1437
+ ${c.blue}║ ║ ║ ║${c.reset}
1438
+ ${c.blue}╩═╩═══╩═╩${c.reset}
1439
+ ${c.dim} [PILLARS]${c.reset} `,
1440
+
1441
+ hiram: `
1442
+ ${c.green}┌─────────┐${c.reset}
1443
+ ${c.green}│${c.bold}◉${c.reset}${c.green}──┬──${c.bold}◉${c.reset}${c.green}│${c.reset}
1444
+ ${c.green}│ ${c.bold}│${c.reset}${c.green} │${c.reset}
1445
+ ${c.green}├──${c.bold}◇${c.reset}${c.green}──┤${c.reset}
1446
+ ${c.green}│ ${c.bold}│${c.reset}${c.green} │${c.reset}
1447
+ ${c.green}│ ${c.bold}▼${c.reset}${c.green} │${c.reset}
1448
+ ${c.green}└──${c.bold}◎${c.reset}${c.green}──┘${c.reset}
1449
+ ${c.dim} [SERVER]${c.reset} `,
1450
+
1451
+ aholiab: `
1452
+ ${c.magenta}╭─────────╮${c.reset}
1453
+ ${c.magenta}│ ${c.bold}◐ ◑${c.reset}${c.magenta} │${c.reset}
1454
+ ${c.magenta}│ ${c.bold}◡${c.reset}${c.magenta} │${c.reset}
1455
+ ${c.magenta}├─────────┤${c.reset}
1456
+ ${c.magenta}│ ${c.bold}▪ ▪ ▪${c.reset}${c.magenta} │${c.reset}
1457
+ ${c.magenta}│ ${c.bold}█████${c.reset}${c.magenta} │${c.reset}
1458
+ ${c.magenta}╰─────────╯${c.reset}
1459
+ ${c.dim} [SCREEN]${c.reset} `,
1460
+
1461
+ solomon: `
1462
+ ${c.cyan}╭───────╮${c.reset}
1463
+ ${c.cyan}╱ ${c.bold}◉ ◉${c.reset}${c.cyan} ╲${c.reset}
1464
+ ${c.cyan}│ ${c.bold}○${c.reset}${c.cyan} │${c.reset}
1465
+ ${c.cyan}├─────────┤${c.reset}
1466
+ ${c.cyan}│${c.bold}═════════${c.reset}${c.cyan}│${c.reset}
1467
+ ${c.cyan}│${c.bold}═════════${c.reset}${c.cyan}│${c.reset}
1468
+ ${c.cyan}│${c.bold}═════════${c.reset}${c.cyan}│${c.reset}
1469
+ ${c.cyan}╲───────╱${c.reset}
1470
+ ${c.dim} [DATA]${c.reset} `,
1471
+
1472
+ zerubbabel: `
1473
+ ${c.yellow}◎${c.reset}
1474
+ ${c.yellow}╱│╲${c.reset}
1475
+ ${c.yellow}╱ ${c.bold}◉ ◉${c.reset}${c.yellow} ╲${c.reset}
1476
+ ${c.yellow}◁──${c.bold}◡${c.reset}${c.yellow}──▷${c.reset}
1477
+ ${c.yellow}╲ ╱${c.reset}
1478
+ ${c.yellow}◎═══◎${c.reset}
1479
+ ${c.yellow}╱ ╲${c.reset}
1480
+ ${c.yellow}◎ ◎${c.reset}
1481
+ ${c.dim} [PIPELINE]${c.reset} `,
1482
+
1483
+ ezra: `
1484
+ ${c.blue}○${c.reset}
1485
+ ${c.blue}╱ ╲${c.reset}
1486
+ ${c.blue} (${c.bold}◉ ◉${c.reset}${c.blue})${c.reset}
1487
+ ${c.blue}│ ${c.bold}⌓${c.reset}${c.blue} │${c.reset}
1488
+ ${c.blue}╭┴───┴╮${c.reset}
1489
+ ${c.blue} (${c.bold}◎${c.reset}${c.blue} )${c.reset}
1490
+ ${c.blue}╲ ${c.bold}│${c.reset}${c.blue} ╱${c.reset}
1491
+ ${c.blue}╰───╯${c.reset}
1492
+ ${c.dim} [LENS]${c.reset} `,
1493
+
1494
+ moses: `
1495
+ ${c.green}★${c.reset}
1496
+ ${c.green}╱ ╲${c.reset}
1497
+ ${c.green}╱${c.bold}◉ ◉${c.reset}${c.green}╲${c.reset}
1498
+ ${c.green}│ ${c.bold}◡${c.reset}${c.green} │${c.reset}
1499
+ ${c.green}├───────┤${c.reset}
1500
+ ${c.green}│ ${c.bold}☰ ☰${c.reset}${c.green} │${c.reset}
1501
+ ${c.green}│ ${c.bold}☰ ☰${c.reset}${c.green} │${c.reset}
1502
+ ${c.green}╰───────╯${c.reset}
1503
+ ${c.dim} [SCROLL]${c.reset} `,
1504
+
1505
+ david: `
1506
+ ${c.magenta}╭─────────╮${c.reset}
1507
+ ${c.magenta}│ ${c.bold}♪ ♫${c.reset}${c.magenta} │${c.reset}
1508
+ ${c.magenta}│ ${c.bold}╲│╱${c.reset}${c.magenta} │${c.reset}
1509
+ ${c.magenta}│ ${c.bold}╱╲${c.reset}${c.magenta} │${c.reset}
1510
+ ${c.magenta}│ ${c.bold}✎${c.reset}${c.magenta} │${c.reset}
1511
+ ${c.magenta}╰─────────╯${c.reset}
1512
+ ${c.dim} [HARP]${c.reset} `,
1513
+ };
1514
+
1515
+ const ARTISAN_DEFS = [
1516
+ {
1517
+ id: 'nehemiah',
1518
+ name: 'Nehemiah',
1519
+ file: 'nehemiah.md',
1520
+ color: c.red,
1521
+ domain: 'Security — build + guard (trust, secrets, compliance)',
1522
+ voice: 'Builders work; watchers watch. I fortify your wall as you build it.',
1523
+ verse: {
1524
+ ref: 'Ne 4:17',
1525
+ text: 'They which builded on the wall, and they that bare burdens, with those that laded, [every one] with one of his hands wrought in the work, and with the other [hand] held a weapon.',
1526
+ },
1527
+ },
1528
+ {
1529
+ id: 'bezaleel',
1530
+ name: 'Bezaleel',
1531
+ file: 'bezaleel.md',
1532
+ color: c.blue,
1533
+ domain: 'Architecture — craft, structure, and seams',
1534
+ voice: 'Craft matters. I design the joinery so the work can endure.',
1535
+ verse: {
1536
+ ref: 'Ex 31:3',
1537
+ text: 'And I have filled him with the spirit of God, in wisdom, and in understanding, and in knowledge, and in all manner of workmanship,',
1538
+ },
1539
+ },
1540
+ {
1541
+ id: 'hiram',
1542
+ name: 'Hiram',
1543
+ file: 'hiram.md',
1544
+ color: c.green,
1545
+ domain: 'Backend — services, workflows, and integrations',
1546
+ voice: 'Strong internals beat clever shortcuts. I forge durable systems.',
1547
+ verse: {
1548
+ ref: '1Ki 7:14',
1549
+ text: 'He [was] a widow’s son of the tribe of Naphtali, and his father [was] a man of Tyre, a worker in brass: and he was filled with wisdom, and understanding, and cunning to work all works in brass. And he came to king Solomon, and wrought all his work.',
1550
+ },
1551
+ },
1552
+ {
1553
+ id: 'aholiab',
1554
+ name: 'Aholiab',
1555
+ file: 'aholiab.md',
1556
+ color: c.magenta,
1557
+ domain: 'Frontend — make it plain upon tables (UX, clarity)',
1558
+ voice: 'I engrave clarity. What users see should be simple and sure.',
1559
+ verse: {
1560
+ ref: 'Ex 38:23',
1561
+ text: 'And with him [was] Aholiab, son of Ahisamach, of the tribe of Dan, an engraver, and a cunning workman, and an embroiderer in blue, and in purple, and in scarlet, and fine linen.',
1562
+ },
1563
+ },
1564
+ {
1565
+ id: 'solomon',
1566
+ name: 'Solomon',
1567
+ file: 'solomon.md',
1568
+ color: c.cyan,
1569
+ domain: 'Data — integrity, queries, and migrations',
1570
+ voice: 'Wisdom first. Data must remain true as the work grows.',
1571
+ verse: {
1572
+ ref: '1Ki 4:29',
1573
+ text: 'And God gave Solomon wisdom and understanding exceeding much, and largeness of heart, even as the sand that [is] on the sea shore.',
1574
+ },
1575
+ },
1576
+ {
1577
+ id: 'zerubbabel',
1578
+ name: 'Zerubbabel',
1579
+ file: 'zerubbabel.md',
1580
+ color: c.yellow,
1581
+ domain: 'DevOps — releases, installs, and shipping discipline',
1582
+ voice: 'The foundation must finish. I make shipping reliable and repeatable.',
1583
+ verse: {
1584
+ ref: 'Zec 4:9',
1585
+ text: 'The hands of Zerubbabel have laid the foundation of this house; his hands shall also finish it; and thou shalt know that the LORD of hosts hath sent me unto you.',
1586
+ },
1587
+ },
1588
+ {
1589
+ id: 'ezra',
1590
+ name: 'Ezra',
1591
+ file: 'ezra.md',
1592
+ color: c.blue,
1593
+ domain: 'QA — verification, tests, and truth-checking',
1594
+ voice: 'Trust, but verify. I compare what we claim against what we shipped.',
1595
+ verse: {
1596
+ ref: 'Ezr 7:10',
1597
+ text: 'For Ezra had prepared his heart to seek the law of the LORD, and to do [it], and to teach in Israel statutes and judgments.',
1598
+ },
1599
+ },
1600
+ {
1601
+ id: 'moses',
1602
+ name: 'Moses',
1603
+ file: 'moses.md',
1604
+ color: c.green,
1605
+ domain: 'Product — requirements, scope, and the pattern',
1606
+ voice: 'Name the thing. Define the edges. Build to the pattern.',
1607
+ verse: {
1608
+ ref: 'Heb 8:5',
1609
+ text: 'Who serve unto the example and shadow of heavenly things, as Moses was admonished of God when he was about to make the tabernacle: for, See, saith he, [that] thou make all things according to the pattern shewed to thee in the mount.',
1610
+ },
1611
+ },
1612
+ {
1613
+ id: 'david',
1614
+ name: 'David',
1615
+ file: 'david.md',
1616
+ color: c.magenta,
1617
+ domain: 'Voice — copy, tone, worship, and remembrance',
1618
+ voice: 'Cut the slop. Keep the song. Words should carry weight.',
1619
+ verse: {
1620
+ ref: 'Ps 45:1',
1621
+ text: '...my tongue [is] the pen of a ready writer.',
1622
+ },
1623
+ },
1624
+ ];
1625
+
1626
+ async function selectArtisans() {
1627
+ console.log(`\n ${c.bold}Select Your Artisans${c.reset} `);
1628
+ console.log(` ${c.dim}These artisans will counsel Paul and execute tasks.${c.reset} `);
1629
+ console.log(` ${c.dim}Enter numbers separated by spaces, or press Enter for all.${c.reset}\n`);
1630
+
1631
+ ARTISAN_DEFS.forEach((a, i) => {
1632
+ console.log(` ${a.color}${i + 1}${c.reset}) ${a.color}${c.bold}${a.name}${c.reset} `);
1633
+ console.log(` ${c.dim}${a.domain}${c.reset} `);
1634
+ });
1635
+
1636
+ const answer = await prompt(`\n Select(1 - ${ARTISAN_DEFS.length}, space - separated)[${c.dim}all${c.reset}]: `);
1637
+
1638
+ if (!answer.trim()) {
1639
+ return ARTISAN_DEFS.map(a => a.id);
1640
+ }
1641
+
1642
+ const nums = answer
1643
+ .split(/[\s,]+/)
1644
+ .map(n => parseInt(n.trim(), 10))
1645
+ .filter(n => n >= 1 && n <= ARTISAN_DEFS.length);
1646
+ if (nums.length === 0) {
1647
+ return ARTISAN_DEFS.map(a => a.id);
1648
+ }
1649
+
1650
+ return nums.map(n => ARTISAN_DEFS[n - 1].id);
1651
+ }
1652
+
1653
+ async function meetTheTeam() {
1654
+ const fast = !process.stdout.isTTY;
1655
+ const pause = fast ? 0 : 800;
1656
+ const shortPause = fast ? 0 : 400;
1657
+
1658
+ console.log(`
1659
+ ${c.bold}${c.magenta}═══════════════════════════════════════════════════════════════${c.reset}
1660
+ ${c.bold} MEET YOUR TEAM${c.reset}
1661
+ ${c.magenta}═══════════════════════════════════════════════════════════════${c.reset}
1662
+ `);
1663
+
1664
+ await sleep(pause);
1665
+
1666
+ // The Vision
1667
+ console.log(` ${c.dim} "And the LORD answered me, and said,${c.reset}`);
1668
+ console.log(` ${c.dim} Write the vision, and make [it] plain upon tables,${c.reset}`);
1669
+ console.log(` ${c.dim} that he may run that readeth it."${c.reset}`);
1670
+ console.log(` ${c.dim} — Habakkuk 2:2 (KJV PCE)${c.reset}`);
1671
+ console.log('');
1672
+
1673
+ await sleep(pause);
1674
+
1675
+ // The Masterbuilder
1676
+ console.log(` ${c.bold}${c.yellow}PAUL — THE MASTERBUILDER${c.reset}`);
1677
+ console.log(` ${c.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
1678
+ const paulAgent = parseAgentFile(join(TEMPLATES_DIR, 'agents', 'paul.md'));
1679
+ if (paulAgent && paulAgent.asciiArt) {
1680
+ console.log(paulAgent.asciiArt);
1681
+ } else {
1682
+ console.log(AVATARS.paul);
1683
+ }
1684
+ await sleep(shortPause);
1685
+ console.log(`
1686
+ ${c.cyan}"According to the grace of God which is given unto me,${c.reset}
1687
+ ${c.cyan} as a wise masterbuilder, I have laid the foundation,${c.reset}
1688
+ ${c.cyan} and another buildeth thereon."${c.reset}
1689
+ ${c.dim} — 1 Corinthians 3:10 (KJV PCE)${c.reset}
1690
+
1691
+ I am ${c.bold}Paul${c.reset}, the masterbuilder. I read your ${c.bold}VISION.md${c.reset}, consult the artisans,
1692
+ synthesize counsel into a plan, and present it for your approval.
1693
+ Only then do I delegate. I verify. I integrate. I report.
1694
+ `);
1695
+
1696
+ await sleep(pause);
1697
+
1698
+ // The Artisans intro
1699
+ console.log(` ${c.bold}${c.green}THE ARTISANS${c.reset}`);
1700
+ console.log(` ${c.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
1701
+ console.log(`
1702
+ ${c.dim}"Where no counsel [is], the people fall:${c.reset}
1703
+ ${c.dim} but in the multitude of counsellers [there is] safety."${c.reset}
1704
+ ${c.dim} — Proverbs 11:14 (KJV PCE)${c.reset}
1705
+ `);
1706
+
1707
+ await sleep(pause);
1708
+
1709
+ for (const artisan of ARTISAN_DEFS) {
1710
+ console.log(` ${artisan.color}${c.bold}${artisan.name}${c.reset}`);
1711
+ const templatePath = join(TEMPLATES_DIR, 'agents', artisan.file);
1712
+ const agent = parseAgentFile(templatePath);
1713
+ if (agent && agent.asciiArt) {
1714
+ console.log(agent.asciiArt);
1715
+ } else {
1716
+ console.log(AVATARS[artisan.id]);
1717
+ }
1718
+ console.log(` ${c.dim}${artisan.domain}${c.reset}`);
1719
+ if (artisan.verse) {
1720
+ console.log(` ${c.dim}${artisan.verse.ref}${c.reset} ${c.dim}"${artisan.verse.text}"${c.reset}`);
1721
+ }
1722
+ console.log(` ${c.dim}"${artisan.voice}"${c.reset}`);
1723
+ console.log('');
1724
+ await sleep(shortPause);
1725
+ }
1726
+
1727
+ await sleep(pause);
1728
+
1729
+ // The Workflow
1730
+ console.log(` ${c.bold}${c.magenta}THE WORKFLOW${c.reset}`);
1731
+ console.log(` ${c.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
1732
+ console.log(`
1733
+ ┌─────────────┐
1734
+ │ VISION.md │
1735
+ │ ${c.dim}Your intent${c.reset} │
1736
+ └──────┬──────┘
1737
+
1738
+ ┌──────▼──────┐
1739
+ │ MASTERBUILDER│
1740
+ │ ${c.dim}Orchestrates${c.reset} │
1741
+ └──────┬──────┘
1742
+
1743
+ ┌────────────────────┼────────────────────┐
1744
+ │ │ │
1745
+ ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
1746
+ │ ARTISAN │ │ ARTISAN │ │ ARTISAN │
1747
+ │ ${c.dim}Counsel +${c.reset} │ ... │ ${c.dim}Counsel +${c.reset} │ ... │ ${c.dim}Counsel +${c.reset} │
1748
+ │ ${c.dim}Execute${c.reset} │ │ ${c.dim}Execute${c.reset} │ │ ${c.dim}Execute${c.reset} │
1749
+ └───────────┘ └───────────┘ └───────────┘
1750
+ `);
1751
+
1752
+ await sleep(pause);
1753
+
1754
+ // Call to action
1755
+ console.log(` ${c.bold}${c.green}READY TO BUILD${c.reset}`);
1756
+ console.log(` ${c.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
1757
+ console.log(`
1758
+ Your team is installed and ready. Here's how to work with them:
1759
+
1760
+ ${c.cyan}/wtv${c.reset} Strategic review against your VISION.md
1761
+ ${c.cyan}/wtv "your mission"${c.reset} Tactical mission with artisan counsel
1762
+
1763
+ First, define your vision:
1764
+
1765
+ ${c.cyan}npx writethevision init${c.reset} Creates VISION.md in your project
1766
+
1767
+ ${c.dim}"But let every man take heed how he buildeth thereupon."${c.reset}
1768
+ ${c.dim} — 1 Corinthians 3:10 (KJV PCE)${c.reset}
1769
+
1770
+ ${c.bold}${c.magenta}═══════════════════════════════════════════════════════════════${c.reset}
1771
+ `);
1772
+ }
1773
+
1774
+ function yamlQuote(value) {
1775
+ const v = String(value ?? '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
1776
+ return `"${v}"`;
1777
+ }
1778
+
1779
+ function normalizeTools(tools) {
1780
+ const normalized = [];
1781
+
1782
+ for (const raw of tools) {
1783
+ if (!raw) continue;
1784
+ const t = String(raw).toLowerCase();
1785
+ const mapped = t === 'claude-code' ? 'claude'
1786
+ : t === 'codex-cli' ? 'codex'
1787
+ : t;
1788
+
1789
+ if (!TOOL_IDS.includes(mapped)) {
1790
+ console.error(`\n ${c.red}Error:${c.reset} Unknown tool '${raw}'.`);
1791
+ console.log(` Valid tools: ${TOOL_IDS.join(', ')}\n`);
1792
+ process.exit(1);
1793
+ }
1794
+
1795
+ if (!normalized.includes(mapped)) normalized.push(mapped);
1796
+ }
1797
+
1798
+ return normalized;
1799
+ }
1800
+
1801
+ function getRootDirForTool(tool, scope, customPath = null) {
1802
+ if (customPath) {
1803
+ let p = customPath;
1804
+ if (p.startsWith('~')) {
1805
+ const home = getHomedir();
1806
+ if (home) p = p.replace('~', home);
1807
+ }
1808
+ return resolve(p);
1809
+ }
1810
+
1811
+ const home = getHomedir();
1812
+ if (!home && scope === 'global') return null;
1813
+
1814
+ switch (tool) {
1815
+ case 'claude':
1816
+ return scope === 'global' ? join(home, '.claude') : join(process.cwd(), '.claude');
1817
+ case 'codex':
1818
+ return scope === 'global' ? join(home, '.codex') : join(process.cwd(), '.codex');
1819
+ case 'gemini':
1820
+ return scope === 'global' ? join(home, '.gemini') : join(process.cwd(), '.gemini');
1821
+ case 'antigravity':
1822
+ if (scope === 'global') {
1823
+ return join(home, '.gemini', 'antigravity');
1824
+ }
1825
+ return join(process.cwd(), '.agent');
1826
+ default:
1827
+ return null;
1828
+ }
1829
+ }
1830
+
1831
+ function getOpenCodeAgentDir(scope) {
1832
+ const home = getHomedir();
1833
+ if (scope === 'global') {
1834
+ if (!home) return null;
1835
+ return join(home, '.config', 'opencode', 'agent');
1836
+ }
1837
+ return join(process.cwd(), '.opencode', 'agent');
1838
+ }
1839
+
1840
+ function getOpenCodeAgentRoot(scope) {
1841
+ const home = getHomedir();
1842
+ if (scope === 'global') {
1843
+ if (!home) return null;
1844
+ return join(home, '.config', 'opencode');
1845
+ }
1846
+ return join(process.cwd(), '.opencode');
1847
+ }
1848
+
1849
+ function ensureDir(p) {
1850
+ if (!existsSync(p)) mkdirSync(p, { recursive: true });
1851
+ }
1852
+
1853
+ function resetDir(p) {
1854
+ if (existsSync(p)) {
1855
+ rmSync(p, { recursive: true, force: true });
1856
+ }
1857
+ mkdirSync(p, { recursive: true });
1858
+ }
1859
+
1860
+ function writeTimestamp(dir) {
1861
+ ensureDir(dir);
1862
+ const timestampFile = join(dir, '.wtv-updated');
1863
+ writeFileSync(timestampFile, new Date().toISOString().split('T')[0]);
1864
+
1865
+ const legacy = join(dir, '.codehogg-updated');
1866
+ if (existsSync(legacy)) {
1867
+ rmSync(legacy, { force: true });
1868
+ }
1869
+ }
1870
+
1871
+ function parseClaudeAgentTemplate(agentPath) {
1872
+ const raw = readFileSync(agentPath, 'utf8');
1873
+ const parts = raw.split(/\n---\n/);
1874
+
1875
+ if (!raw.startsWith('---\n') || parts.length < 2) {
1876
+ return { frontmatter: {}, body: raw };
1877
+ }
1878
+
1879
+ const fmText = parts[0].replace(/^---\n/, '');
1880
+ const body = raw.slice(parts[0].length + '\n---\n'.length);
1881
+
1882
+ const fm = {};
1883
+ for (const line of fmText.split('\n')) {
1884
+ const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
1885
+ if (m) {
1886
+ fm[m[1]] = m[2];
1887
+ }
1888
+ }
1889
+
1890
+ return { frontmatter: fm, body };
1891
+ }
1892
+
1893
+ function getTemplateSkillDirs() {
1894
+ const source = join(TEMPLATES_DIR, 'skills');
1895
+ if (!existsSync(source)) return [];
1896
+
1897
+ return readdirSync(source).filter(name => {
1898
+ try {
1899
+ return statSync(join(source, name)).isDirectory();
1900
+ } catch {
1901
+ return false;
1902
+ }
1903
+ });
1904
+ }
1905
+
1906
+ function getTemplateAgentFiles() {
1907
+ const source = join(TEMPLATES_DIR, 'agents');
1908
+ if (!existsSync(source)) return [];
1909
+ return readdirSync(source).filter(f => f.endsWith('.md'));
1910
+ }
1911
+
1912
+ function getLegacyAgentFiles() {
1913
+ return Object.keys(LEGACY_AGENT_NAME_ALIASES).map(name => `${name}.md`);
1914
+ }
1915
+
1916
+ function countInstalledDirs(dir, dirNames) {
1917
+ if (!dir || !existsSync(dir)) return 0;
1918
+ let count = 0;
1919
+ for (const name of dirNames) {
1920
+ if (existsSync(join(dir, name))) count++;
1921
+ }
1922
+ return count;
1923
+ }
1924
+
1925
+ function countInstalledFiles(dir, fileNames) {
1926
+ if (!dir || !existsSync(dir)) return 0;
1927
+ let count = 0;
1928
+ for (const name of fileNames) {
1929
+ if (existsSync(join(dir, name))) count++;
1930
+ }
1931
+ return count;
1932
+ }
1933
+
1934
+ function installSkills(destDir, { force, showProgress, label }) {
1935
+ const source = join(TEMPLATES_DIR, 'skills');
1936
+ if (!existsSync(source)) {
1937
+ throw new Error(`Templates missing: ${source}`);
1938
+ }
1939
+
1940
+ const skillDirs = getTemplateSkillDirs();
1941
+ ensureDir(destDir);
1942
+
1943
+ for (const name of skillDirs) {
1944
+ const srcSkillDir = join(source, name);
1945
+ const destSkillDir = join(destDir, name);
1946
+
1947
+ if (existsSync(destSkillDir)) {
1948
+ if (!force) {
1949
+ throw new Error(`Existing skill found at ${destSkillDir}. Use --force to overwrite.`);
1950
+ }
1951
+ rmSync(destSkillDir, { recursive: true, force: true });
1952
+ }
1953
+
1954
+ copyDir(srcSkillDir, destSkillDir);
1955
+ }
1956
+
1957
+ if (force) {
1958
+ const legacyCoreSkillDir = join(destDir, 'codehogg');
1959
+ if (existsSync(legacyCoreSkillDir)) {
1960
+ rmSync(legacyCoreSkillDir, { recursive: true, force: true });
1961
+ }
1962
+ }
1963
+
1964
+ if (showProgress) {
1965
+ console.log(` ${c.green}${sym.check}${c.reset} ${label} (${skillDirs.length} skills)`);
1966
+ }
1967
+
1968
+ return skillDirs.length;
1969
+ }
1970
+
1971
+ function installStandardTool(toolName, targetDir, { force, showProgress, selectedArtisans = null }) {
1972
+ const validation = validatePath(targetDir);
1973
+ if (!validation.valid) {
1974
+ throw new Error(validation.error);
1975
+ }
1976
+
1977
+ ensureDir(targetDir);
1978
+
1979
+ const agentsSource = join(TEMPLATES_DIR, 'agents');
1980
+ const agentsDest = join(targetDir, 'agents');
1981
+
1982
+ if (!existsSync(agentsSource)) {
1983
+ throw new Error(`Templates missing: ${agentsSource}`);
1984
+ }
1985
+
1986
+ ensureDir(agentsDest);
1987
+
1988
+ if (force) {
1989
+ for (const legacyFile of getLegacyAgentFiles()) {
1990
+ const p = join(agentsDest, legacyFile);
1991
+ if (existsSync(p)) {
1992
+ rmSync(p, { force: true });
1993
+ }
1994
+ }
1995
+ }
1996
+
1997
+ // Copy agents - optionally filter by selected artisans
1998
+ let agentCount = 0;
1999
+ const agentFiles = getTemplateAgentFiles();
2000
+ const artisanFiles = new Set(ARTISAN_DEFS.map(a => a.file));
2001
+ const artisanByFile = new Map(ARTISAN_DEFS.map(a => [a.file, a]));
2002
+
2003
+ for (const file of agentFiles) {
2004
+ const srcPath = join(agentsSource, file);
2005
+ const destPath = join(agentsDest, file);
2006
+
2007
+ const isArtisan = artisanFiles.has(file);
2008
+ if (isArtisan && selectedArtisans) {
2009
+ const artisan = artisanByFile.get(file);
2010
+ const selected = artisan && selectedArtisans.includes(artisan.id);
2011
+
2012
+ if (!selected) {
2013
+ if (force && existsSync(destPath)) {
2014
+ rmSync(destPath, { force: true });
2015
+ }
2016
+ continue;
2017
+ }
2018
+ }
2019
+
2020
+ if (existsSync(destPath) && !force) {
2021
+ throw new Error(`Existing agent found at ${destPath}. Use --force to overwrite.`);
2022
+ }
2023
+
2024
+ copyFileSync(srcPath, destPath);
2025
+ agentCount++;
2026
+ }
2027
+
2028
+ const skillsDest = join(targetDir, 'skills');
2029
+ installSkills(skillsDest, { force, showProgress, label: `${toolName} skills` });
2030
+
2031
+ const wtvSrc = join(TEMPLATES_DIR, 'WTV.md');
2032
+ const wtvDest = join(targetDir, 'WTV.md');
2033
+ if (existsSync(wtvSrc)) {
2034
+ copyFileSync(wtvSrc, wtvDest);
2035
+ }
2036
+
2037
+ if (force) {
2038
+ const legacyPluginDoc = join(targetDir, 'CODEHOGG.md');
2039
+ if (existsSync(legacyPluginDoc)) {
2040
+ rmSync(legacyPluginDoc, { force: true });
2041
+ }
2042
+ }
2043
+
2044
+ writeTimestamp(targetDir);
2045
+
2046
+ // Append WTV section to tool's config file (if it exists)
2047
+ const toolConfigMap = {
2048
+ 'Claude': join(process.cwd(), 'CLAUDE.md'),
2049
+ 'Codex': join(process.cwd(), 'AGENTS.md'),
2050
+ 'Gemini': join(process.cwd(), 'GEMINI.md'),
2051
+ 'Antigravity': join(process.cwd(), 'AGENTS.md'),
2052
+ };
2053
+ const configFile = toolConfigMap[toolName];
2054
+ if (configFile) {
2055
+ appendWtvSection(configFile);
2056
+ }
2057
+
2058
+ if (showProgress) {
2059
+ console.log(` ${c.green}${sym.check}${c.reset} ${agentCount} ${toolName} agents`);
2060
+ }
2061
+
2062
+ return true;
2063
+ }
2064
+
2065
+ function installClaude(targetDir, options) {
2066
+ return installStandardTool('Claude', targetDir, options);
2067
+ }
2068
+
2069
+ function installGemini(targetDir, options) {
2070
+ return installStandardTool('Gemini', targetDir, options);
2071
+ }
2072
+
2073
+ function installAntigravity(targetDir, options) {
2074
+ const success = installStandardTool('Antigravity', targetDir, options);
2075
+ if (!success) return false;
2076
+
2077
+ // Create Antigravity Rule file to bootstrap agents and AGENTS.md
2078
+ const rulesDir = join(targetDir, 'rules');
2079
+ ensureDir(rulesDir);
2080
+ const rulePath = join(rulesDir, 'wtv-bootstrap.md');
2081
+
2082
+ if (!existsSync(rulePath) || options.force) {
2083
+ const ruleContent = `# WTV Configuration
2084
+
2085
+ This workspace uses the WTV agent system.
2086
+
2087
+ ## Core Instructions
2088
+ 1. **Source of Truth**: Always read and adhere to \`./AGENTS.md\` in the repository root.
2089
+ - If \`./AGENTS.override.md\` exists in a subdirectory, it takes precedence for that directory.
2090
+
2091
+ ## Expert Agents
2092
+ The following expert agents are available in \`.agent/agents/\`. When acting as or consulting them, read their definition file:
2093
+ - **Paul** (Masterbuilder): \`.agent/agents/paul.md\`
2094
+ - **Nehemiah** (Security): \`.agent/agents/nehemiah.md\`
2095
+ - **Bezaleel** (Architecture): \`.agent/agents/bezaleel.md\`
2096
+ - **Hiram** (Backend): \`.agent/agents/hiram.md\`
2097
+ - **Aholiab** (Frontend): \`.agent/agents/aholiab.md\`
2098
+ - **Solomon** (Data): \`.agent/agents/solomon.md\`
2099
+ - **Zerubbabel** (DevOps): \`.agent/agents/zerubbabel.md\`
2100
+ - **Ezra** (QA): \`.agent/agents/ezra.md\`
2101
+ - **Moses** (Product): \`.agent/agents/moses.md\`
2102
+ - **David** (Voice): \`.agent/agents/david.md\`
2103
+ `;
2104
+ writeFileSync(rulePath, ruleContent);
2105
+ if (options.showProgress) {
2106
+ console.log(` ${c.green}${sym.check}${c.reset} Antigravity bootstrap rule created`);
2107
+ }
2108
+ }
2109
+
2110
+ return true;
2111
+ }
2112
+
2113
+ function installCodex(targetDir, options) {
2114
+ return installStandardTool('Codex', targetDir, options);
2115
+ }
2116
+
2117
+ function installOpenCodeAgents(agentDir, { force, showProgress, selectedArtisans = null }) {
2118
+ const sourceDir = join(TEMPLATES_DIR, 'agents');
2119
+ if (!existsSync(sourceDir)) {
2120
+ throw new Error(`Templates missing: ${sourceDir}`);
2121
+ }
2122
+
2123
+ ensureDir(agentDir);
2124
+
2125
+ if (force) {
2126
+ for (const legacyFile of getLegacyAgentFiles()) {
2127
+ const p = join(agentDir, legacyFile);
2128
+ if (existsSync(p)) {
2129
+ rmSync(p, { force: true });
2130
+ }
2131
+ }
2132
+ }
2133
+
2134
+ const artisanFiles = new Set(ARTISAN_DEFS.map(a => a.file));
2135
+ const artisanByFile = new Map(ARTISAN_DEFS.map(a => [a.file, a]));
2136
+
2137
+ const agentFiles = getTemplateAgentFiles().sort();
2138
+
2139
+ let written = 0;
2140
+ for (const file of agentFiles) {
2141
+ const destPath = join(agentDir, file);
2142
+ const isArtisan = artisanFiles.has(file);
2143
+
2144
+ if (isArtisan && selectedArtisans) {
2145
+ const artisan = artisanByFile.get(file);
2146
+ const selected = artisan && selectedArtisans.includes(artisan.id);
2147
+
2148
+ if (!selected) {
2149
+ if (force && existsSync(destPath)) {
2150
+ rmSync(destPath, { force: true });
2151
+ }
2152
+ continue;
2153
+ }
2154
+ }
2155
+
2156
+ if (existsSync(destPath) && !force) {
2157
+ throw new Error(`Existing agent found at ${destPath}. Use --force to overwrite.`);
2158
+ }
2159
+
2160
+ const inputPath = join(sourceDir, file);
2161
+ const { frontmatter, body } = parseClaudeAgentTemplate(inputPath);
2162
+ const description = frontmatter.description || `WTV agent: ${file.replace(/\.md$/, '')}`;
2163
+
2164
+ const out = [
2165
+ '---',
2166
+ `description: ${yamlQuote(description)}`,
2167
+ 'mode: subagent',
2168
+ 'tools:',
2169
+ ' bash: true',
2170
+ ' read: true',
2171
+ ' grep: true',
2172
+ ' glob: true',
2173
+ ' webfetch: true',
2174
+ ' edit: true',
2175
+ ' write: true',
2176
+ ' skill: true',
2177
+ '---',
2178
+ '',
2179
+ body.trimStart(),
2180
+ ].join('\n');
2181
+
2182
+ writeFileSync(destPath, out);
2183
+ written++;
2184
+ }
2185
+
2186
+ if (showProgress) {
2187
+ console.log(` ${c.green}${sym.check}${c.reset} ${written} OpenCode agents`);
2188
+ }
2189
+
2190
+ return written;
2191
+ }
2192
+
2193
+ function installOpenCode(scope, { force, showProgress, selectedArtisans = null }) {
2194
+ // Skills: use OpenCode's Claude-compatible skill lookup.
2195
+ const claudeRoot = getRootDirForTool('claude', scope);
2196
+ if (!claudeRoot) throw new Error('Cannot determine ~/.claude directory.');
2197
+ const validation = validatePath(claudeRoot);
2198
+ if (!validation.valid) {
2199
+ throw new Error(validation.error);
2200
+ }
2201
+
2202
+ ensureDir(claudeRoot);
2203
+ installSkills(join(claudeRoot, 'skills'), { force, showProgress, label: 'OpenCode skills (via .claude/skills)' });
2204
+ writeTimestamp(claudeRoot);
2205
+
2206
+ // Agents: native OpenCode agent path.
2207
+ const agentRoot = getOpenCodeAgentRoot(scope);
2208
+ const agentDir = getOpenCodeAgentDir(scope);
2209
+ if (!agentRoot || !agentDir) throw new Error('Cannot determine OpenCode agent directory.');
2210
+
2211
+ const agentValidation = validatePath(agentRoot);
2212
+ if (!agentValidation.valid) {
2213
+ throw new Error(agentValidation.error);
2214
+ }
2215
+
2216
+ ensureDir(agentRoot);
2217
+ installOpenCodeAgents(agentDir, { force, showProgress, selectedArtisans });
2218
+ writeTimestamp(agentRoot);
2219
+
2220
+ return true;
2221
+ }
2222
+
2223
+ function detectInstalled(scope) {
2224
+ const home = getHomedir();
2225
+
2226
+ const claudeRoot = scope === 'global'
2227
+ ? (home ? join(home, '.claude') : null)
2228
+ : join(process.cwd(), '.claude');
2229
+
2230
+ const codexRoot = scope === 'global'
2231
+ ? (home ? join(home, '.codex') : null)
2232
+ : join(process.cwd(), '.codex');
2233
+
2234
+ const opencodeAgentsDir = getOpenCodeAgentDir(scope);
2235
+
2236
+ const templateAgentFiles = getTemplateAgentFiles();
2237
+ const legacyAgentFiles = getLegacyAgentFiles();
2238
+
2239
+ const claudeAgentsDir = claudeRoot ? join(claudeRoot, 'agents') : null;
2240
+ const codexSkillsDir = codexRoot ? join(codexRoot, 'skills') : null;
2241
+
2242
+ const hasClaudeAgents =
2243
+ countInstalledFiles(claudeAgentsDir, templateAgentFiles) > 0 ||
2244
+ countInstalledFiles(claudeAgentsDir, legacyAgentFiles) > 0;
2245
+
2246
+ const hasClaudeMarker = !!claudeRoot && (
2247
+ existsSync(join(claudeRoot, 'WTV.md')) ||
2248
+ existsSync(join(claudeRoot, 'CODEHOGG.md'))
2249
+ );
2250
+
2251
+ const hasCodexCoreSkill = !!codexSkillsDir && (
2252
+ existsSync(join(codexSkillsDir, 'wtv')) ||
2253
+ existsSync(join(codexSkillsDir, 'codehogg'))
2254
+ );
2255
+
2256
+ const hasOpenCodeAgents =
2257
+ countInstalledFiles(opencodeAgentsDir, templateAgentFiles) > 0 ||
2258
+ countInstalledFiles(opencodeAgentsDir, legacyAgentFiles) > 0;
2259
+
2260
+ return {
2261
+ claude: !!claudeRoot && (hasClaudeMarker || hasClaudeAgents),
2262
+ codex: !!codexRoot && hasCodexCoreSkill,
2263
+ opencode: !!opencodeAgentsDir && hasOpenCodeAgents,
2264
+ paths: { claudeRoot, codexRoot, opencodeAgentsDir },
2265
+ };
2266
+ }
2267
+
2268
+ async function interactiveInit() {
2269
+ printBanner();
2270
+ console.log(` ${c.cyan}Welcome!${c.reset} Let's set up wtv.\n`);
2271
+
2272
+ const scope = await select('Where would you like to install?', [
2273
+ { value: 'project', label: 'Current project', desc: 'Installs to .claude/.codex/.opencode in this repo' },
2274
+ { value: 'global', label: 'Global', desc: 'Installs to ~/.claude, ~/.codex, and ~/.config/opencode' },
2275
+ ]);
2276
+
2277
+ console.log(`\n ${c.bold}Targets${c.reset}`);
2278
+ const installClaudeFlag = await confirm('Install for Claude Code?', true);
2279
+ const installCodexFlag = await confirm('Install for Codex CLI?', false);
2280
+ const installOpenCodeFlag = await confirm('Install for OpenCode?', false);
2281
+ const installGeminiFlag = await confirm('Install for Gemini Type? (Gemini CLI)', false);
2282
+ const installAntigravityFlag = await confirm('Install for Antigravity?', false);
2283
+
2284
+ const tools = [];
2285
+ if (installClaudeFlag) tools.push('claude');
2286
+ if (installCodexFlag) tools.push('codex');
2287
+ if (installOpenCodeFlag) tools.push('opencode');
2288
+ if (installGeminiFlag) tools.push('gemini');
2289
+ if (installAntigravityFlag) tools.push('antigravity');
2290
+
2291
+ if (tools.length === 0) {
2292
+ console.log(`\n ${c.dim}No tools selected. Nothing to do.${c.reset}\n`);
2293
+ process.exit(0);
2294
+ }
2295
+
2296
+ // Ask which artisans to install (for Claude Code or OpenCode)
2297
+ let selectedArtisans = null;
2298
+ if (tools.includes('claude') || tools.includes('opencode')) {
2299
+ const customizeArtisans = await confirm('\n Customize which Artisans to install?', false);
2300
+ if (customizeArtisans) {
2301
+ selectedArtisans = await selectArtisans();
2302
+ const selectedNames = selectedArtisans.map(id => {
2303
+ const a = ARTISAN_DEFS.find(x => x.id === id);
2304
+ return a ? a.name : id;
2305
+ });
2306
+ console.log(`\n ${c.green}${sym.check}${c.reset} Selected: ${selectedNames.join(', ')}`);
2307
+ } else {
2308
+ console.log(` ${c.dim}Installing all ${ARTISAN_DEFS.length} artisans.${c.reset}`);
2309
+ }
2310
+ }
2311
+
2312
+ const installed = detectInstalled(scope);
2313
+ const existing = tools.filter(t => installed[t]);
2314
+
2315
+ let force = false;
2316
+ if (existing.length > 0) {
2317
+ console.log(`\n ${c.yellow}${sym.warn}${c.reset} Existing installation(s) detected: ${existing.join(', ')}`);
2318
+ force = await confirm('Overwrite existing WTV files? (custom files are preserved)', false);
2319
+ if (!force) {
2320
+ console.log(`\n ${c.dim}Aborted.${c.reset}\n`);
2321
+ process.exit(0);
2322
+ }
2323
+ }
2324
+
2325
+ console.log(`\n ${c.bold}Installing...${c.reset}\n`);
2326
+
2327
+ doInstall({ scope, tools, force, showProgress: true, customPath: null, selectedArtisans });
2328
+
2329
+ console.log(`\n ${c.green}${c.bold}Installation complete!${c.reset}\n`);
2330
+
2331
+ // Offer to create VISION.md for project-scope installs
2332
+ if (scope === 'project') {
2333
+ const visionPath = getVisionPath(scope);
2334
+ if (!existsSync(visionPath)) {
2335
+ console.log(` ${c.bold}Vision-Driven Development${c.reset}`);
2336
+ console.log(` ${c.dim}VISION.md lets /wtv align recommendations with your project goals.${c.reset}\n`);
2337
+
2338
+ const createVision = await confirm('Create VISION.md now?', true);
2339
+ if (createVision) {
2340
+ await initVision(scope);
2341
+ } else {
2342
+ console.log(`\n ${c.dim}You can create it later with 'wtv init' or by creating VISION.md manually.${c.reset}\n`);
2343
+ }
2344
+ }
2345
+ }
2346
+
2347
+ console.log(` ${c.bold}Quick start:${c.reset}`);
2348
+ if (tools.includes('claude')) {
2349
+ console.log(` ${c.cyan}Claude Code:${c.reset} use /wtv or /wtv "your mission"`);
2350
+ }
2351
+ if (tools.includes('codex')) {
2352
+ console.log(` ${c.cyan}Codex CLI:${c.reset} use $wtv`);
2353
+ }
2354
+ if (tools.includes('opencode')) {
2355
+ console.log(` ${c.cyan}OpenCode:${c.reset} use @paul (masterbuilder) and ask for a plan`);
2356
+ }
2357
+ console.log(` ${c.dim}Tip:${c.reset} run 'wtv meet' to see the team`);
2358
+
2359
+ console.log('');
2360
+ }
2361
+
2362
+ function doInstall({ scope, tools, force, showProgress, customPath, selectedArtisans = null }) {
2363
+ const selected = normalizeTools(tools);
2364
+
2365
+ if (customPath && selected.length !== 1) {
2366
+ throw new Error('Custom --path is only supported when installing a single tool.');
2367
+ }
2368
+
2369
+ for (const tool of selected) {
2370
+ if (tool === 'claude') {
2371
+ const targetDir = getRootDirForTool('claude', scope, customPath);
2372
+ if (!targetDir) throw new Error('Cannot determine .claude directory.');
2373
+ installClaude(targetDir, { force, showProgress, selectedArtisans });
2374
+ }
2375
+
2376
+ if (tool === 'codex') {
2377
+ const targetDir = getRootDirForTool('codex', scope, customPath);
2378
+ if (!targetDir) throw new Error('Cannot determine .codex directory.');
2379
+ installCodex(targetDir, { force, showProgress });
2380
+ }
2381
+
2382
+ if (tool === 'opencode') {
2383
+ if (customPath) {
2384
+ throw new Error('Custom --path is not supported for OpenCode installs.');
2385
+ }
2386
+ installOpenCode(scope, { force, showProgress, selectedArtisans });
2387
+ }
2388
+
2389
+ if (tool === 'gemini') {
2390
+ const targetDir = getRootDirForTool('gemini', scope, customPath);
2391
+ if (!targetDir) throw new Error('Cannot determine .gemini directory.');
2392
+ installGemini(targetDir, { force, showProgress, selectedArtisans });
2393
+ }
2394
+
2395
+ if (tool === 'antigravity') {
2396
+ const targetDir = getRootDirForTool('antigravity', scope, customPath);
2397
+ if (!targetDir) throw new Error('Cannot determine .antigravity directory.');
2398
+ installAntigravity(targetDir, { force, showProgress, selectedArtisans });
2399
+ }
2400
+ }
2401
+
2402
+ return true;
2403
+ }
2404
+
2405
+ function initNonInteractive(scope, force, tools, customPath = null) {
2406
+ const selected = normalizeTools(tools.length ? tools : ['claude']);
2407
+
2408
+ const scopeLabel = scope === 'global' ? 'global' : 'project';
2409
+ console.log(`\n ${c.bold}wtv${c.reset} v${getVersion()}`);
2410
+ console.log(` Installing (${scopeLabel}) for: ${selected.join(', ')}\n`);
2411
+
2412
+ doInstall({ scope, tools: selected, force, showProgress: true, customPath });
2413
+
2414
+ console.log(`\n ${c.green}Installation complete!${c.reset}\n`);
2415
+ }
2416
+
2417
+ function update(scope, tools, customPath = null) {
2418
+ if (customPath) {
2419
+ const selected = normalizeTools(tools);
2420
+
2421
+ if (selected.length !== 1) {
2422
+ console.log(`\n ${c.red}Error:${c.reset} Custom --path requires exactly one tool.`);
2423
+ console.log(` Example: wtv update --tool claude --path /tmp/claude-root\n`);
2424
+ process.exit(1);
2425
+ }
2426
+
2427
+ if (selected[0] === 'opencode') {
2428
+ console.log(`\n ${c.red}Error:${c.reset} Custom --path is not supported for OpenCode installs.`);
2429
+ console.log(` OpenCode installs to .opencode/ (project) or ~/.config/opencode (global).\n`);
2430
+ process.exit(1);
2431
+ }
2432
+
2433
+ console.log(`\n ${c.bold}wtv${c.reset} v${getVersion()}`);
2434
+ console.log(` Updating custom installation: ${selected[0]} (${customPath})\n`);
2435
+
2436
+ doInstall({ scope, tools: selected, force: true, showProgress: true, customPath });
2437
+
2438
+ console.log(`\n ${c.green}Update complete!${c.reset}\n`);
2439
+ return;
2440
+ }
2441
+
2442
+ const installed = detectInstalled(scope);
2443
+ const selected = normalizeTools(tools.length ? tools : TOOL_IDS.filter(t => installed[t]));
2444
+
2445
+ if (selected.length === 0) {
2446
+ console.log(`\n ${c.red}Error:${c.reset} No installations found to update.`);
2447
+ console.log(` Run 'wtv init' first.\n`);
2448
+ process.exit(1);
2449
+ }
2450
+
2451
+ console.log(`\n ${c.bold}wtv${c.reset} v${getVersion()}`);
2452
+ console.log(` Updating ${scope} installation(s): ${selected.join(', ')}\n`);
2453
+
2454
+ doInstall({ scope, tools: selected, force: true, showProgress: true, customPath: null });
2455
+
2456
+ console.log(`\n ${c.green}Update complete!${c.reset}\n`);
2457
+ }
2458
+
2459
+ async function uninstallInteractive(scope) {
2460
+ printBanner();
2461
+
2462
+ const installed = detectInstalled(scope);
2463
+ const candidates = TOOL_IDS.filter(t => installed[t]);
2464
+
2465
+ if (candidates.length === 0) {
2466
+ console.log(`\n ${c.dim}Nothing to uninstall.${c.reset}\n`);
2467
+ process.exit(0);
2468
+ }
2469
+
2470
+ console.log(` ${c.bold}Installed:${c.reset} ${candidates.join(', ')}\n`);
2471
+
2472
+ const removeClaude = candidates.includes('claude')
2473
+ ? await confirm('Uninstall Claude Code assets?', true)
2474
+ : false;
2475
+ const removeCodex = candidates.includes('codex')
2476
+ ? await confirm('Uninstall Codex CLI assets?', true)
2477
+ : false;
2478
+ const removeOpenCode = candidates.includes('opencode')
2479
+ ? await confirm('Uninstall OpenCode assets?', true)
2480
+ : false;
2481
+
2482
+ const selected = [];
2483
+ if (removeClaude) selected.push('claude');
2484
+ if (removeCodex) selected.push('codex');
2485
+ if (removeOpenCode) selected.push('opencode');
2486
+
2487
+ if (selected.length === 0) {
2488
+ console.log(`\n ${c.dim}Nothing selected. Aborted.${c.reset}\n`);
2489
+ process.exit(0);
2490
+ }
2491
+
2492
+ const ok = await confirm(`${c.yellow}${sym.warn}${c.reset} This will remove files from disk. Continue?`, false);
2493
+ if (!ok) {
2494
+ console.log(`\n ${c.dim}Aborted.${c.reset}\n`);
2495
+ process.exit(0);
2496
+ }
2497
+
2498
+ uninstall(scope, selected);
2499
+ }
2500
+
2501
+ function uninstall(scope, tools, customPath = null) {
2502
+ const selected = normalizeTools(tools);
2503
+
2504
+ const templateAgentFiles = getTemplateAgentFiles();
2505
+ const templateSkillDirs = getTemplateSkillDirs();
2506
+
2507
+ const removeAgentFiles = (dir) => {
2508
+ if (!dir || !existsSync(dir)) return 0;
2509
+ let removedCount = 0;
2510
+ for (const file of templateAgentFiles) {
2511
+ const p = join(dir, file);
2512
+ if (existsSync(p)) {
2513
+ rmSync(p, { force: true });
2514
+ removedCount++;
2515
+ }
2516
+ }
2517
+ return removedCount;
2518
+ };
2519
+
2520
+ const removeSkillDirs = (dir) => {
2521
+ if (!dir || !existsSync(dir)) return 0;
2522
+ let removedCount = 0;
2523
+ for (const name of templateSkillDirs) {
2524
+ const p = join(dir, name);
2525
+ if (existsSync(p)) {
2526
+ rmSync(p, { recursive: true, force: true });
2527
+ removedCount++;
2528
+ }
2529
+ }
2530
+ return removedCount;
2531
+ };
2532
+
2533
+ if (customPath) {
2534
+ if (selected.length !== 1) {
2535
+ console.log(`\n ${c.red}Error:${c.reset} Custom --path requires exactly one tool.`);
2536
+ console.log(` Example: wtv uninstall --tool claude --path /tmp/claude-root\n`);
2537
+ process.exit(1);
2538
+ }
2539
+
2540
+ if (selected[0] === 'opencode') {
2541
+ console.log(`\n ${c.red}Error:${c.reset} Custom --path is not supported for OpenCode installs.`);
2542
+ console.log(` OpenCode installs to .opencode/ (project) or ~/.config/opencode (global).\n`);
2543
+ process.exit(1);
2544
+ }
2545
+
2546
+ const tool = selected[0];
2547
+ const root = getRootDirForTool(tool, scope, customPath);
2548
+ if (!root) {
2549
+ console.log(`\n ${c.red}Error:${c.reset} Unable to determine directory for ${tool}.\n`);
2550
+ process.exit(1);
2551
+ }
2552
+
2553
+ console.log(`\n ${c.bold}wtv${c.reset} v${getVersion()}`);
2554
+ console.log(` Uninstalling custom installation: ${tool} (${customPath})\n`);
2555
+
2556
+ let removed = false;
2557
+
2558
+ if (tool === 'claude') {
2559
+ const removedAgents = removeAgentFiles(join(root, 'agents'));
2560
+ if (removedAgents > 0) {
2561
+ console.log(` ${c.green}${sym.check}${c.reset} Removed ${removedAgents} Claude agents`);
2562
+ removed = true;
2563
+ }
2564
+
2565
+ const removedSkills = removeSkillDirs(join(root, 'skills'));
2566
+ if (removedSkills > 0) {
2567
+ console.log(` ${c.green}${sym.check}${c.reset} Removed ${removedSkills} Claude skills`);
2568
+ removed = true;
2569
+ }
2570
+
2571
+ for (const file of ['WTV.md', 'CODEHOGG.md', '.wtv-updated', '.codehogg-updated']) {
2572
+ const p = join(root, file);
2573
+ if (existsSync(p)) {
2574
+ rmSync(p, { force: true });
2575
+ removed = true;
2576
+ }
2577
+ }
2578
+ }
2579
+
2580
+ if (tool === 'codex') {
2581
+ const removedSkills = removeSkillDirs(join(root, 'skills'));
2582
+ if (removedSkills > 0) {
2583
+ console.log(` ${c.green}${sym.check}${c.reset} Removed ${removedSkills} Codex skills`);
2584
+ removed = true;
2585
+ }
2586
+
2587
+ for (const file of ['.wtv-updated', '.codehogg-updated']) {
2588
+ const p = join(root, file);
2589
+ if (existsSync(p)) {
2590
+ rmSync(p, { force: true });
2591
+ removed = true;
2592
+ }
2593
+ }
2594
+ }
2595
+
2596
+ if (removed) {
2597
+ console.log(`\n ${c.green}Uninstall complete.${c.reset}\n`);
2598
+ } else {
2599
+ console.log(`\n ${c.dim}Nothing to uninstall.${c.reset}\n`);
2600
+ }
2601
+
2602
+ return;
2603
+ }
2604
+
2605
+ const home = getHomedir();
2606
+ const installed = detectInstalled(scope);
2607
+
2608
+ const claudeRoot = scope === 'global'
2609
+ ? (home ? join(home, '.claude') : null)
2610
+ : join(process.cwd(), '.claude');
2611
+
2612
+ const codexRoot = scope === 'global'
2613
+ ? (home ? join(home, '.codex') : null)
2614
+ : join(process.cwd(), '.codex');
2615
+
2616
+ const opencodeRoot = getOpenCodeAgentRoot(scope);
2617
+ const opencodeAgentsDir = getOpenCodeAgentDir(scope);
2618
+
2619
+ const remainingClaude = installed.claude && !selected.includes('claude');
2620
+ const remainingOpenCode = installed.opencode && !selected.includes('opencode');
2621
+ const keepWtvSkills = remainingClaude || remainingOpenCode;
2622
+
2623
+ console.log(`\n ${c.bold}wtv${c.reset} v${getVersion()}`);
2624
+ console.log(` Uninstalling (${scope}) for: ${selected.join(', ')}\n`);
2625
+
2626
+ let removed = false;
2627
+ let removedWtvSkillsFromClaude = false;
2628
+
2629
+ if (selected.includes('claude') && claudeRoot) {
2630
+ const agentsDir = join(claudeRoot, 'agents');
2631
+ const removedAgents = removeAgentFiles(agentsDir);
2632
+ if (removedAgents > 0) {
2633
+ console.log(` ${c.green}${sym.check}${c.reset} Removed ${removedAgents} Claude agents`);
2634
+ removed = true;
2635
+ }
2636
+
2637
+ if (!keepWtvSkills) {
2638
+ const skillsDir = join(claudeRoot, 'skills');
2639
+ const removedSkills = removeSkillDirs(skillsDir);
2640
+ if (removedSkills > 0) {
2641
+ console.log(` ${c.green}${sym.check}${c.reset} Removed ${removedSkills} Claude skills`);
2642
+ removed = true;
2643
+ }
2644
+ removedWtvSkillsFromClaude = true;
2645
+
2646
+ for (const tsFile of ['.wtv-updated', '.codehogg-updated']) {
2647
+ const ts = join(claudeRoot, tsFile);
2648
+ if (existsSync(ts)) {
2649
+ rmSync(ts, { force: true });
2650
+ removed = true;
2651
+ }
2652
+ }
2653
+ } else {
2654
+ console.log(` ${c.dim}Keeping wtv skills in .claude/skills (still needed)${c.reset}`);
2655
+ }
2656
+
2657
+ for (const file of ['WTV.md', 'CODEHOGG.md']) {
2658
+ const p = join(claudeRoot, file);
2659
+ if (existsSync(p)) {
2660
+ rmSync(p, { force: true });
2661
+ removed = true;
2662
+ }
2663
+ }
2664
+ }
2665
+
2666
+ if (selected.includes('codex') && codexRoot) {
2667
+ const skillsDir = join(codexRoot, 'skills');
2668
+ const removedSkills = removeSkillDirs(skillsDir);
2669
+ if (removedSkills > 0) {
2670
+ console.log(` ${c.green}${sym.check}${c.reset} Removed ${removedSkills} Codex skills`);
2671
+ removed = true;
2672
+ }
2673
+
2674
+ for (const tsFile of ['.wtv-updated', '.codehogg-updated']) {
2675
+ const ts = join(codexRoot, tsFile);
2676
+ if (existsSync(ts)) {
2677
+ rmSync(ts, { force: true });
2678
+ removed = true;
2679
+ }
2680
+ }
2681
+ }
2682
+
2683
+ if (selected.includes('opencode')) {
2684
+ const removedAgents = removeAgentFiles(opencodeAgentsDir);
2685
+ if (removedAgents > 0) {
2686
+ console.log(` ${c.green}${sym.check}${c.reset} Removed ${removedAgents} OpenCode agents`);
2687
+ removed = true;
2688
+ }
2689
+
2690
+ if (opencodeRoot) {
2691
+ for (const tsFile of ['.wtv-updated', '.codehogg-updated']) {
2692
+ const ts = join(opencodeRoot, tsFile);
2693
+ if (existsSync(ts)) {
2694
+ rmSync(ts, { force: true });
2695
+ removed = true;
2696
+ }
2697
+ }
2698
+ }
2699
+
2700
+ // OpenCode skills live in .claude/skills
2701
+ if (!keepWtvSkills && claudeRoot && !removedWtvSkillsFromClaude) {
2702
+ const skillsDir = join(claudeRoot, 'skills');
2703
+ const removedSkills = removeSkillDirs(skillsDir);
2704
+ if (removedSkills > 0) {
2705
+ console.log(` ${c.green}${sym.check}${c.reset} Removed ${removedSkills} OpenCode skills (via .claude/skills)`);
2706
+ removed = true;
2707
+ }
2708
+
2709
+ for (const tsFile of ['.wtv-updated', '.codehogg-updated']) {
2710
+ const ts = join(claudeRoot, tsFile);
2711
+ if (existsSync(ts)) {
2712
+ rmSync(ts, { force: true });
2713
+ removed = true;
2714
+ }
2715
+ }
2716
+ }
2717
+ }
2718
+
2719
+ // Remove WTV section from config files if uninstalling relevant tools
2720
+ const configFilesToClean = [];
2721
+ if (selected.includes('claude')) configFilesToClean.push(join(process.cwd(), 'CLAUDE.md'));
2722
+ if (selected.includes('codex')) configFilesToClean.push(join(process.cwd(), 'AGENTS.md'));
2723
+ if (selected.includes('gemini')) configFilesToClean.push(join(process.cwd(), 'GEMINI.md'));
2724
+ if (selected.includes('antigravity')) configFilesToClean.push(join(process.cwd(), 'AGENTS.md'));
2725
+
2726
+ for (const configFile of [...new Set(configFilesToClean)]) {
2727
+ if (removeWtvSection(configFile)) {
2728
+ console.log(` ${c.green}${sym.check}${c.reset} Removed WTV section from ${basename(configFile)}`);
2729
+ removed = true;
2730
+ }
2731
+ }
2732
+
2733
+ if (removed) {
2734
+ console.log(`\n ${c.green}Uninstall complete.${c.reset}\n`);
2735
+ } else {
2736
+ console.log(`\n ${c.dim}Nothing to uninstall.${c.reset}\n`);
2737
+ }
2738
+ }
2739
+
2740
+ function status(customPath = null) {
2741
+ console.log(`\n ${c.bold}wtv${c.reset} v${getVersion()}\n`);
2742
+
2743
+ const home = getHomedir();
2744
+
2745
+ const templateAgentFiles = getTemplateAgentFiles();
2746
+ const legacyAgentFiles = getLegacyAgentFiles();
2747
+ const templateSkillDirs = getTemplateSkillDirs();
2748
+
2749
+ const showClaude = (label, root) => {
2750
+ const agentsDir = join(root, 'agents');
2751
+ const skillsDir = join(root, 'skills');
2752
+
2753
+ const agents = countInstalledFiles(agentsDir, templateAgentFiles);
2754
+ const skills = countInstalledDirs(skillsDir, templateSkillDirs);
2755
+ const legacyAgents = countInstalledFiles(agentsDir, legacyAgentFiles);
2756
+
2757
+ const hasLegacyCoreSkill = existsSync(join(skillsDir, 'codehogg'));
2758
+ const hasNewCoreSkill = existsSync(join(skillsDir, 'wtv'));
2759
+ const hasTimestamp = existsSync(join(root, '.wtv-updated')) || existsSync(join(root, '.codehogg-updated'));
2760
+ const hasMarkerDoc = existsSync(join(root, 'WTV.md')) || existsSync(join(root, 'CODEHOGG.md'));
2761
+
2762
+ const installed = hasTimestamp || hasMarkerDoc || hasNewCoreSkill || hasLegacyCoreSkill || agents > 0 || legacyAgents > 0;
2763
+
2764
+ console.log(` ${label}:`);
2765
+ if (installed) {
2766
+ console.log(` ${c.green}${sym.check}${c.reset} ${agents}/${templateAgentFiles.length} agents, ${skills}/${templateSkillDirs.length} skills`);
2767
+ if (legacyAgents > 0 && agents === 0) {
2768
+ console.log(` ${c.yellow}${sym.warn}${c.reset} Legacy agents detected (run: wtv update --tool claude)`);
2769
+ } else if (skills > 0 && agents === 0) {
2770
+ console.log(` ${c.yellow}${sym.warn}${c.reset} Agents missing (run: wtv init --claude)`);
2771
+ }
2772
+
2773
+ if (agents > 0 && skills === 0) {
2774
+ console.log(` ${c.yellow}${sym.warn}${c.reset} Skills missing (run: wtv init --claude)`);
2775
+ }
2776
+
2777
+ if (!hasNewCoreSkill && hasLegacyCoreSkill) {
2778
+ console.log(` ${c.yellow}${sym.warn}${c.reset} Legacy core skill detected (codehogg). Run: wtv update --tool claude`);
2779
+ } else if (!hasNewCoreSkill && !hasLegacyCoreSkill) {
2780
+ console.log(` ${c.yellow}${sym.warn}${c.reset} Core skill missing (wtv). Run: wtv update --tool claude`);
2781
+ }
2782
+
2783
+ if (skills > 0 && skills < templateSkillDirs.length) {
2784
+ console.log(` ${c.yellow}${sym.warn}${c.reset} ${templateSkillDirs.length - skills} skills missing (run: wtv update --tool claude)`);
2785
+ }
2786
+
2787
+ if (agents > 0 && agents < templateAgentFiles.length) {
2788
+ console.log(` ${c.dim}${sym.info}${c.reset} Partial agent set installed (custom selection?)`);
2789
+ }
2790
+ } else {
2791
+ console.log(` ${c.dim}Not installed${c.reset}`);
2792
+ if (skills > 0) {
2793
+ console.log(` ${c.dim}${sym.info}${c.reset} Found ${skills}/${templateSkillDirs.length} matching skills (run: wtv init --claude)`);
2794
+ }
2795
+ }
2796
+ };
2797
+
2798
+ const showCodex = (label, root) => {
2799
+ const skillsDir = join(root, 'skills');
2800
+ const skills = countInstalledDirs(skillsDir, templateSkillDirs);
2801
+
2802
+ const hasLegacyCoreSkill = existsSync(join(skillsDir, 'codehogg'));
2803
+ const hasNewCoreSkill = existsSync(join(skillsDir, 'wtv'));
2804
+ const hasTimestamp = existsSync(join(root, '.wtv-updated')) || existsSync(join(root, '.codehogg-updated'));
2805
+
2806
+ const installed = hasTimestamp || hasNewCoreSkill || hasLegacyCoreSkill;
2807
+
2808
+ console.log(` ${label}:`);
2809
+ if (installed) {
2810
+ console.log(` ${c.green}${sym.check}${c.reset} ${skills}/${templateSkillDirs.length} skills`);
2811
+
2812
+ if (!hasNewCoreSkill && hasLegacyCoreSkill) {
2813
+ console.log(` ${c.yellow}${sym.warn}${c.reset} Legacy core skill detected (codehogg). Run: wtv update --tool codex`);
2814
+ } else if (!hasNewCoreSkill && !hasLegacyCoreSkill) {
2815
+ console.log(` ${c.yellow}${sym.warn}${c.reset} Core skill missing (wtv). Run: wtv update --tool codex`);
2816
+ }
2817
+
2818
+ if (skills > 0 && skills < templateSkillDirs.length) {
2819
+ console.log(` ${c.yellow}${sym.warn}${c.reset} ${templateSkillDirs.length - skills} skills missing (run: wtv update --tool codex)`);
2820
+ }
2821
+ } else {
2822
+ console.log(` ${c.dim}Not installed${c.reset}`);
2823
+ if (skills > 0) {
2824
+ console.log(` ${c.dim}${sym.info}${c.reset} Found ${skills}/${templateSkillDirs.length} matching skills (run: wtv init --codex)`);
2825
+ }
2826
+ }
2827
+ };
2828
+
2829
+ const showOpenCode = (label, agentsDir, claudeSkillsDir) => {
2830
+ const agents = countInstalledFiles(agentsDir, templateAgentFiles);
2831
+ const skills = countInstalledDirs(claudeSkillsDir, templateSkillDirs);
2832
+ const legacyAgents = countInstalledFiles(agentsDir, legacyAgentFiles);
2833
+
2834
+ const agentRoot = dirname(agentsDir);
2835
+ const hasTimestamp = existsSync(join(agentRoot, '.wtv-updated')) || existsSync(join(agentRoot, '.codehogg-updated'));
2836
+
2837
+ const hasLegacyCoreSkill = existsSync(join(claudeSkillsDir, 'codehogg'));
2838
+ const hasNewCoreSkill = existsSync(join(claudeSkillsDir, 'wtv'));
2839
+
2840
+ const installed = hasTimestamp || agents > 0 || legacyAgents > 0;
2841
+
2842
+ console.log(` ${label}:`);
2843
+ if (installed) {
2844
+ const suffix = skills > 0 ? ' (via .claude/skills)' : '';
2845
+ console.log(` ${c.green}${sym.check}${c.reset} ${agents}/${templateAgentFiles.length} agents, ${skills}/${templateSkillDirs.length} skills${suffix}`);
2846
+ if (legacyAgents > 0 && agents === 0) {
2847
+ console.log(` ${c.yellow}${sym.warn}${c.reset} Legacy agents detected (run: wtv update --tool opencode)`);
2848
+ } else if (skills > 0 && agents === 0) {
2849
+ console.log(` ${c.yellow}${sym.warn}${c.reset} Agents missing (run: wtv init --opencode)`);
2850
+ }
2851
+
2852
+ if (agents > 0 && skills === 0) {
2853
+ console.log(` ${c.yellow}${sym.warn}${c.reset} Skills missing (run: wtv init --opencode)`);
2854
+ }
2855
+
2856
+ if (!hasNewCoreSkill && hasLegacyCoreSkill) {
2857
+ console.log(` ${c.yellow}${sym.warn}${c.reset} Legacy core skill detected (codehogg). Run: wtv update --tool opencode`);
2858
+ } else if (!hasNewCoreSkill && !hasLegacyCoreSkill) {
2859
+ console.log(` ${c.yellow}${sym.warn}${c.reset} Core skill missing (wtv). Run: wtv update --tool opencode`);
2860
+ }
2861
+
2862
+ if (skills > 0 && skills < templateSkillDirs.length) {
2863
+ console.log(` ${c.yellow}${sym.warn}${c.reset} ${templateSkillDirs.length - skills} skills missing (run: wtv update --tool opencode)`);
2864
+ }
2865
+
2866
+ if (agents > 0 && agents < templateAgentFiles.length) {
2867
+ console.log(` ${c.dim}${sym.info}${c.reset} Partial agent set installed (custom selection?)`);
2868
+ }
2869
+ } else {
2870
+ console.log(` ${c.dim}Not installed${c.reset}`);
2871
+ if (skills > 0 || hasNewCoreSkill || hasLegacyCoreSkill) {
2872
+ console.log(` ${c.dim}${sym.info}${c.reset} Skills present in .claude/skills (run: wtv init --opencode)`);
2873
+ }
2874
+ }
2875
+ };
2876
+
2877
+ if (customPath) {
2878
+ const root = getRootDirForTool('claude', 'project', customPath);
2879
+ if (!root) {
2880
+ console.log(` Custom (${customPath}): ${c.dim}Unable to determine directory${c.reset}`);
2881
+ console.log('');
2882
+ return;
2883
+ }
2884
+ showClaude(`Custom (.claude-like) (${customPath})`, root);
2885
+ console.log('');
2886
+ return;
2887
+ }
2888
+
2889
+ // Project
2890
+ console.log(` ${c.bold}Project${c.reset}`);
2891
+ showClaude('Claude Code (.claude/)', join(process.cwd(), '.claude'));
2892
+ showCodex('Codex CLI (.codex/)', join(process.cwd(), '.codex'));
2893
+ showOpenCode('OpenCode (.opencode/)', join(process.cwd(), '.opencode', 'agent'), join(process.cwd(), '.claude', 'skills'));
2894
+
2895
+ console.log('');
2896
+
2897
+ // Global
2898
+ if (home) {
2899
+ console.log(` ${c.bold}Global${c.reset}`);
2900
+ showClaude('Claude Code (~/.claude/)', join(home, '.claude'));
2901
+ showCodex('Codex CLI (~/.codex/)', join(home, '.codex'));
2902
+ showOpenCode('OpenCode (~/.config/opencode/)', join(home, '.config', 'opencode', 'agent'), join(home, '.claude', 'skills'));
2903
+ }
2904
+
2905
+ console.log('');
2906
+ }
2907
+
2908
+ // ============================================================================
2909
+ // VISION.md Management
2910
+ // ============================================================================
2911
+
2912
+ function getVisionPath(scope) {
2913
+ if (scope === 'global') {
2914
+ const home = getHomedir();
2915
+ return home ? join(home, 'VISION.md') : null;
2916
+ }
2917
+
2918
+ // Project scope: Prefer vision/VISION.md
2919
+ const hasVisionDir = existsSync(join(process.cwd(), 'vision'));
2920
+ const visionDirFile = join(process.cwd(), 'vision', 'VISION.md');
2921
+ const rootFile = join(process.cwd(), 'VISION.md');
2922
+
2923
+ if (existsSync(visionDirFile)) return visionDirFile;
2924
+ if (existsSync(rootFile)) return rootFile;
2925
+
2926
+ // Default for new files: vision/VISION.md
2927
+ return visionDirFile;
2928
+ }
2929
+
2930
+ function parseVisionSections(content) {
2931
+ const sections = {
2932
+ purpose: null,
2933
+ outcomes: null,
2934
+ values: null,
2935
+ constraints: null,
2936
+ stage: null,
2937
+ focus: null,
2938
+ };
2939
+
2940
+ const sectionPatterns = [
2941
+ { key: 'purpose', pattern: /## Purpose\s*\n([\s\S]*?)(?=\n## |$)/ },
2942
+ { key: 'outcomes', pattern: /## Outcomes\s*\n([\s\S]*?)(?=\n## |$)/ },
2943
+ { key: 'values', pattern: /## Values\s*\n([\s\S]*?)(?=\n## |$)/ },
2944
+ { key: 'constraints', pattern: /## Constraints\s*\n([\s\S]*?)(?=\n## |$)/ },
2945
+ { key: 'stage', pattern: /## Stage\s*\n([\s\S]*?)(?=\n## |$)/ },
2946
+ { key: 'focus', pattern: /## Current Focus\s*\n([\s\S]*?)(?=\n## |$)/ },
2947
+ ];
2948
+
2949
+ for (const { key, pattern } of sectionPatterns) {
2950
+ const match = content.match(pattern);
2951
+ if (match) {
2952
+ // Remove HTML comments and trim
2953
+ let text = match[1]
2954
+ .replace(/<!--[\s\S]*?-->/g, '')
2955
+ .trim();
2956
+ // Only mark as filled if there's actual content
2957
+ sections[key] = text.length > 0 ? text : null;
2958
+ }
2959
+ }
2960
+
2961
+ return sections;
2962
+ }
2963
+
2964
+ async function initVision(scope) {
2965
+ const visionPath = getVisionPath(scope);
2966
+ if (!visionPath) {
2967
+ console.log(`\n ${c.red}Error:${c.reset} Cannot determine vision path.\n`);
2968
+ return false;
2969
+ }
2970
+
2971
+ if (existsSync(visionPath)) {
2972
+ console.log(`\n ${c.yellow}${sym.warn}${c.reset} VISION.md already exists at: ${visionPath}`);
2973
+ const overwrite = await confirm('Overwrite existing VISION.md?', false);
2974
+ if (!overwrite) {
2975
+ console.log(`\n ${c.dim}Keeping existing VISION.md.${c.reset}\n`);
2976
+ return false;
2977
+ }
2978
+ }
2979
+
2980
+ console.log(`\n ${c.magenta}${c.bold}Write the Vision${c.reset}\n`);
2981
+ console.log(` ${c.dim}> "And the LORD answered me, and said, Write the vision,${c.reset}`);
2982
+ console.log(` ${c.dim}> and make [it] plain upon tables, that he may run that readeth it."${c.reset}`);
2983
+ console.log(` ${c.dim}> — Habakkuk 2:2 (KJV PCE)${c.reset}\n`);
2984
+
2985
+ // Create template
2986
+ const template = `${c.dim}# Vision
2987
+
2988
+ <!-- Who is this for and what does it do? -->
2989
+ ## Purpose
2990
+ Project Purpose...
2991
+
2992
+ <!-- What does success look like? -->
2993
+ ## Outcomes
2994
+ - Success Metric 1
2995
+ - Success Metric 2
2996
+
2997
+ <!-- What matters most? Tradeoffs? -->
2998
+ ## Values
2999
+ - Value 1
3000
+ - Value 2
3001
+
3002
+ <!-- Off-limits? Time, budget, compliance? -->
3003
+ ## Constraints
3004
+ - Constraint 1
3005
+
3006
+ <!-- Prototype / MVP / Production / Maintenance -->
3007
+ ## Stage
3008
+ Prototype
3009
+
3010
+ <!-- What's the one thing right now? -->
3011
+ ## Current Focus
3012
+ One Thing
3013
+ ${c.reset}`;
3014
+
3015
+ // Clean Markdown for file (no colors)
3016
+ const cleanTemplate = `# Vision
3017
+
3018
+ <!-- Who is this for and what does it do? -->
3019
+ ## Purpose
3020
+ <!-- Summarize the "Why". Who is it for? what problem does it solve? -->
3021
+
3022
+
3023
+ <!-- What does success look like? -->
3024
+ ## Outcomes
3025
+ <!-- Specific, measurable results -->
3026
+ -
3027
+
3028
+ <!-- What matters most? Tradeoffs? -->
3029
+ ## Values
3030
+ <!-- e.g. Speed over Features, Privacy over Convenience -->
3031
+ -
3032
+
3033
+ <!-- Off-limits? Time, budget, compliance? -->
3034
+ ## Constraints
3035
+ <!-- Hard stops and boundaries -->
3036
+ -
3037
+
3038
+ <!-- Prototype / MVP / Production / Maintenance -->
3039
+ ## Stage
3040
+ Prototype
3041
+
3042
+ <!-- What's the one thing right now? -->
3043
+ ## Current Focus
3044
+ <!-- The single most important next step -->
3045
+ `;
3046
+
3047
+ ensureDir(dirname(visionPath));
3048
+ writeFileSync(visionPath, cleanTemplate);
3049
+
3050
+ console.log(` ${c.bold}Opening editor...${c.reset}`);
3051
+ console.log(` ${c.dim}(Close editor to save)${c.reset}\n`);
3052
+
3053
+ await openInEditor(visionPath);
3054
+
3055
+ console.log(`\n ${c.green}${sym.check}${c.reset} Vision saved at: ${visionPath}\n`);
3056
+ return true;
3057
+ }
3058
+
3059
+ // ============================================================================
3060
+ // DASHBOARD (Agent Command Center)
3061
+ // ============================================================================
3062
+
3063
+ async function interactiveVision() {
3064
+ console.clear();
3065
+ console.log(` ${c.magenta}${c.bold}
3066
+ __ ___ _
3067
+ \\ \\ / (_)___(_)___ _ __
3068
+ \\ \\/\\/ /| / __| / _ \\| '_ \\
3069
+ \\_/\\_/ |_|___|_|___/|_| |_|
3070
+ ${c.reset}`);
3071
+ console.log(` ${c.dim}Write the vision, make it plain.${c.reset}\n`);
3072
+
3073
+ const visionDirPath = join(process.cwd(), 'vision');
3074
+ const rootVisionPath = join(process.cwd(), 'VISION.md');
3075
+
3076
+ // Build file list
3077
+ const files = [];
3078
+ if (existsSync(rootVisionPath)) {
3079
+ files.push({
3080
+ path: rootVisionPath,
3081
+ label: 'VISION.md (Root)',
3082
+ type: 'root'
3083
+ });
3084
+ }
3085
+
3086
+ if (existsSync(visionDirPath) && lstatSync(visionDirPath).isDirectory()) {
3087
+ const vFiles = readdirSync(visionDirPath)
3088
+ .filter(f => f.endsWith('.md'))
3089
+ .map(f => ({
3090
+ path: join(visionDirPath, f),
3091
+ label: `vision/${f}`,
3092
+ type: 'vision'
3093
+ }));
3094
+ files.push(...vFiles);
3095
+ }
3096
+
3097
+ // Actions
3098
+ const choices = [];
3099
+
3100
+ if (files.length === 0) {
3101
+ console.log(` ${c.yellow}No vision documents found.${c.reset}\n`);
3102
+ console.log(` ${c.dim}> "Write the vision, and make [it] plain upon tables,${c.reset}`);
3103
+ console.log(` ${c.dim}> that he may run that readeth it."${c.reset}`);
3104
+ console.log(` ${c.dim}> — Habakkuk 2:2 (KJV PCE)${c.reset}\n`);
3105
+ console.log(` ${c.bold}Start by defining the core vision for this project.${c.reset}\n`);
3106
+ choices.push({ value: 'create_root', label: 'Create Project Vision (VISION.md)', desc: 'The main vision for the project' });
3107
+ } else {
3108
+ console.log(` ${c.bold}Existing Documents:${c.reset}`);
3109
+ files.forEach(f => console.log(` - ${f.label}`));
3110
+ console.log('');
3111
+
3112
+ choices.push({ value: 'view', label: 'Read / Edit Existing Vision', desc: 'View or modify a vision document' });
3113
+ choices.push({ value: 'create_sub', label: 'Add New Vision Document', desc: 'Create a new strategy/roadmap file in vision/' });
3114
+ }
3115
+
3116
+ choices.push({ value: 'exit', label: 'Return to Dashboard' });
3117
+
3118
+ const action = await select('Action:', choices);
3119
+
3120
+ if (action === 'exit') return;
3121
+
3122
+ if (action === 'create_root') {
3123
+ await initVision('project');
3124
+ await interactiveVision(); // Loop back
3125
+ return;
3126
+ }
3127
+
3128
+ if (action === 'create_sub') {
3129
+ const name = await prompt(` ${c.cyan}Filename${c.reset} (e.g. roadmap, values):\n > `);
3130
+ if (!name) return interactiveVision();
3131
+
3132
+ const filename = name.endsWith('.md') ? name : `${name}.md`;
3133
+ const filepath = join(visionDirPath, filename);
3134
+
3135
+ ensureDir(visionDirPath);
3136
+
3137
+ if (existsSync(filepath)) {
3138
+ console.log(` ${c.yellow}File already exists.${c.reset}`);
3139
+ // Fall through to edit logic? Or just loop.
3140
+ await new Promise(r => setTimeout(r, 1000));
3141
+ return interactiveVision();
3142
+ }
3143
+
3144
+ // Use a simpler prompt flow for sub-visions
3145
+ console.log(`\n ${c.bold}Opening editor for ${filename}...${c.reset}`);
3146
+
3147
+ const content = `# ${name.replace('.md', '')}\n\n<!-- Vision support document. Describe your thoughts here... -->\n`;
3148
+
3149
+ writeFileSync(filepath, content);
3150
+ await openInEditor(filepath);
3151
+
3152
+ console.log(`\n ${c.green}${sym.check}${c.reset} Created vision/${filename}`);
3153
+ await new Promise(r => setTimeout(r, 1000));
3154
+ return interactiveVision();
3155
+ }
3156
+
3157
+ if (action === 'view') {
3158
+ const fileChoices = files.map(f => ({ value: f.path, label: f.label }));
3159
+ const targetPath = await select('Select document:', fileChoices);
3160
+
3161
+ if (targetPath) {
3162
+ // Read and show content
3163
+ const content = readFileSync(targetPath, 'utf8');
3164
+ console.clear();
3165
+ console.log(` ${c.bold}Reading: ${basename(targetPath)}${c.reset}\n`);
3166
+ console.log(content);
3167
+ console.log('\n ' + '-'.repeat(40) + '\n');
3168
+
3169
+ const subAction = await select('Options:', [
3170
+ { value: 'back', label: 'Back to Vision Board' },
3171
+ { value: 'edit', label: 'Edit File', desc: `Open in ${process.env.EDITOR || 'default editor'}` }
3172
+ ]);
3173
+
3174
+ if (subAction === 'edit') {
3175
+ await openInEditor(targetPath);
3176
+ }
3177
+ }
3178
+ return interactiveVision();
3179
+ }
3180
+ }
3181
+
3182
+ async function dashboard() {
3183
+ // Check if we can run interactive mode
3184
+ if (!process.stdin.isTTY) {
3185
+ return dashboardStatic();
3186
+ }
3187
+
3188
+ const config = loadConfig();
3189
+ const tool = config.defaultTool || 'claude';
3190
+
3191
+ const menuItems = [
3192
+ { label: 'MANAGE AGENTS', action: 'agents' },
3193
+ { label: 'VISION BOARD', action: 'vision' },
3194
+ { label: 'EXIT', action: 'exit' }
3195
+ ];
3196
+
3197
+ let selectedIndex = 1; // Default to VISION BOARD
3198
+
3199
+ function render() {
3200
+ // Clear screen
3201
+ process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
3202
+ process.stdout.write('\x1b[?25l'); // Hide cursor
3203
+
3204
+ // Header
3205
+ console.log(
3206
+ ` ${c.magenta}
3207
+ __ ___
3208
+ \\ \\ / / |_ __
3209
+ \\ \\/\\/ /| | \\/ /
3210
+ \\_/\\_/ \\__|\\/
3211
+ ${c.reset}`
3212
+ );
3213
+
3214
+ console.log(` ${c.dim}v${getVersion()} • ${tool.toUpperCase()} MODE${c.reset}\n`);
3215
+
3216
+ // Menu
3217
+ menuItems.forEach((item, idx) => {
3218
+ if (idx === selectedIndex) {
3219
+ console.log(` ${c.magenta}❯ ${c.bold}${item.label}${c.reset}`);
3220
+ } else {
3221
+ console.log(` ${c.dim}${item.label}${c.reset}`);
3222
+ }
3223
+ });
3224
+
3225
+ console.log(`\n ${c.dim}↑/↓: Navigate • Enter: Select${c.reset}`);
3226
+ }
3227
+
3228
+ render();
3229
+
3230
+ // Input loop
3231
+ process.stdin.setRawMode(true);
3232
+ process.stdin.resume();
3233
+ emitKeypressEvents(process.stdin);
3234
+
3235
+ return new Promise((resolve) => {
3236
+ const handler = async (str, key) => {
3237
+ if (key.ctrl && key.name === 'c') {
3238
+ process.stdin.setRawMode(false);
3239
+ process.stdout.write('\x1b[?25h');
3240
+ process.exit();
3241
+ }
3242
+
3243
+ if (key.name === 'up') {
3244
+ selectedIndex = (selectedIndex - 1 + menuItems.length) % menuItems.length;
3245
+ render();
3246
+ } else if (key.name === 'down') {
3247
+ selectedIndex = (selectedIndex + 1) % menuItems.length;
3248
+ render();
3249
+ } else if (key.name === 'return') {
3250
+ const action = menuItems[selectedIndex].action;
3251
+
3252
+ // Cleanup before action
3253
+ process.stdin.removeListener('keypress', handler);
3254
+ process.stdin.setRawMode(false);
3255
+ process.stdout.write('\x1b[?25h'); // Show cursor
3256
+
3257
+ if (action === 'exit') {
3258
+ process.exit(0);
3259
+ } else if (action === 'agents') {
3260
+ await agentsInteractive();
3261
+ await dashboard();
3262
+ resolve();
3263
+ } else if (action === 'vision') {
3264
+ await interactiveVision();
3265
+ await dashboard(); // Re-render dashboard after return
3266
+ resolve();
3267
+ }
3268
+ } else if (key.name === 'q' || key.name === 'escape') {
3269
+ process.stdin.setRawMode(false);
3270
+ process.stdout.write('\x1b[?25h');
3271
+ process.exit(0);
3272
+ }
3273
+ };
3274
+
3275
+ process.stdin.on('keypress', handler);
3276
+ });
3277
+ }
3278
+
3279
+ function dashboardStatic() {
3280
+ const pad = centerPad();
3281
+
3282
+ // Banner
3283
+ console.log('');
3284
+ const wtvAscii = `
3285
+ ${c.magenta}██ ██ ████████ ██ ██ ${c.reset}
3286
+ ${c.magenta}██ ██ ██ ██ ██ ${c.reset}
3287
+ ${c.magenta}██ █ ██ ██ ██ ██ ${c.reset}
3288
+ ${c.magenta}██ ███ ██ ██ ██ ██ ${c.reset}
3289
+ ${c.magenta} ███ ███ ██ ████ ${c.reset}`;
3290
+
3291
+ console.log(wtvAscii);
3292
+ console.log(drawBox([
3293
+ ` ${c.dim}v${getVersion()}${c.reset}`,
3294
+ ` ${c.dim}By the power of Biblical Artisans${c.reset}`,
3295
+ ``,
3296
+ ` ${c.green}Run 'wtv meet' to be introduced to the team.${c.reset}`,
3297
+ ], { style: 'round', color: c.magenta, padding: 0 }));
3298
+ console.log('');
3299
+
3300
+ // Vision status (if exists)
3301
+ const visionPath = getVisionPath('project');
3302
+ if (visionPath && existsSync(visionPath)) {
3303
+ const content = readFileSync(visionPath, 'utf8');
3304
+ const sections = parseVisionSections(content);
3305
+
3306
+ console.log(`${pad}${c.bold}VISION${c.reset} ${c.dim}./VISION.md${c.reset}`);
3307
+ if (sections.purpose) {
3308
+ const preview = sections.purpose.length > 50 ? sections.purpose.slice(0, 50) + '...' : sections.purpose;
3309
+ console.log(`${pad}├─ Purpose: ${c.dim}${preview}${c.reset}`);
3310
+ }
3311
+ if (sections.stage) {
3312
+ console.log(`${pad}├─ Stage: ${c.cyan}${sections.stage}${c.reset}`);
3313
+ }
3314
+ if (sections.focus) {
3315
+ const preview = sections.focus.length > 50 ? sections.focus.slice(0, 50) + '...' : sections.focus;
3316
+ console.log(`${pad}└─ Focus: ${c.green}${preview}${c.reset}`);
3317
+ }
3318
+ console.log('');
3319
+ }
3320
+
3321
+ // Habakkuk Board summary (if items exist)
3322
+ const habakkukPath = join(process.cwd(), HABAKKUK_DIR);
3323
+ if (existsSync(habakkukPath)) {
3324
+ const byStage = getItemsByStage();
3325
+ const totalItems = Object.values(byStage).flat().length;
3326
+
3327
+ if (totalItems > 0) {
3328
+ // Count items by stage
3329
+ const counts = {};
3330
+ for (const stage of HABAKKUK_STAGES) {
3331
+ counts[stage] = byStage[stage].length;
3332
+ }
3333
+
3334
+ const activeStages = HABAKKUK_STAGES.filter((s) => counts[s] > 0 && s !== 'worship');
3335
+ const worshipCount = counts['worship'] || 0;
3336
+
3337
+ console.log(`${pad}${c.bold}HABAKKUK BOARD${c.reset} ${c.dim}"Write the vision, make it plain"${c.reset}`);
3338
+
3339
+ if (activeStages.length > 0) {
3340
+ const parts = activeStages.map((s) => `${HABAKKUK_STAGE_LABELS[s]}: ${c.cyan}${counts[s]}${c.reset}`);
3341
+ console.log(`${pad}├─ ${parts.join(' • ')}`);
3342
+ }
3343
+
3344
+ if (worshipCount > 0) {
3345
+ console.log(`${pad}└─ ${c.yellow}★${c.reset} ${worshipCount} completed (Stones of Remembrance)`);
3346
+ } else if (activeStages.length > 0) {
3347
+ console.log(`${pad}└─ ${c.dim}View full board:${c.reset} ${c.green}wtv board${c.reset}`);
3348
+ }
3349
+
3350
+ console.log('');
3351
+ }
3352
+ }
3353
+
3354
+ // Discover agents
3355
+ const localAgents = discoverAgents('local');
3356
+ const globalAgents = discoverAgents('global');
3357
+
3358
+ const star = isWindows ? '*' : '★';
3359
+ const noStar = ' ';
3360
+
3361
+ // Local agents
3362
+ if (localAgents.length > 0) {
3363
+ console.log(`${pad}${c.bold}LOCAL AGENTS${c.reset} ${c.dim}(.claude/agents/ or .opencode/agent/)${c.reset}`);
3364
+ for (const agent of localAgents) {
3365
+ const fav = agent.favorite ? `${c.yellow}${star}${c.reset}` : noStar;
3366
+ const desc = agent.description.length > 40 ? agent.description.slice(0, 40) + '...' : agent.description;
3367
+ console.log(`${pad} ${fav} ${c.cyan}${agent.name.padEnd(22)}${c.reset} ${c.dim}${desc}${c.reset}`);
3368
+ }
3369
+ console.log('');
3370
+ }
3371
+
3372
+ // Global agents
3373
+ if (globalAgents.length > 0) {
3374
+ console.log(`${pad}${c.bold}GLOBAL AGENTS${c.reset} ${c.dim}(~/.claude/agents/)${c.reset}`);
3375
+ for (const agent of globalAgents.slice(0, 5)) { // Show top 5
3376
+ const fav = agent.favorite ? `${c.yellow}${star}${c.reset}` : noStar;
3377
+ const desc = agent.description.length > 40 ? agent.description.slice(0, 40) + '...' : agent.description;
3378
+ console.log(`${pad} ${fav} ${c.cyan}${agent.name.padEnd(22)}${c.reset} ${c.dim}${desc}${c.reset}`);
3379
+ }
3380
+ if (globalAgents.length > 5) {
3381
+ console.log(`${pad} ${c.dim}... and ${globalAgents.length - 5} more${c.reset}`);
3382
+ }
3383
+ console.log('');
3384
+ }
3385
+
3386
+ // No agents found
3387
+ if (localAgents.length === 0 && globalAgents.length === 0) {
3388
+ console.log(`${pad}${c.yellow}${sym.warn}${c.reset} No agents found.\n`);
3389
+ console.log(`${pad}Run ${c.cyan}wtv init${c.reset} to install Paul and the Artisans.`);
3390
+ console.log('');
3391
+ return;
3392
+ }
3393
+
3394
+ // Quick actions
3395
+ console.log(`${pad}${c.dim}Quick actions:${c.reset}`);
3396
+ console.log(`${pad} ${c.green}wtv agents${c.reset} ${c.dim}List all agents${c.reset}`);
3397
+ console.log(`${pad} ${c.green}wtv agents add <name>${c.reset} ${c.dim}Create new agent${c.reset}`);
3398
+ console.log(`${pad} ${c.green}wtv agents fav <name>${c.reset} ${c.dim}Toggle favorite${c.reset}`);
3399
+ console.log(`${pad} ${c.green}wtv init${c.reset} ${c.dim}Install/update agents${c.reset}`);
3400
+ console.log('');
3401
+ }
3402
+
3403
+ // ============================================================================
3404
+ // AGENT COMMANDS
3405
+ // ============================================================================
3406
+
3407
+ async function agentsInteractive() {
3408
+ // 1. Gather all potential agents match against scopes
3409
+ const templatesPath = join(TEMPLATES_DIR, 'agents');
3410
+ if (!existsSync(templatesPath)) {
3411
+ console.log(`\n ${c.red}Error:${c.reset} Templates not found at ${templatesPath}`);
3412
+ return;
3413
+ }
3414
+
3415
+ const templateFiles = readdirSync(templatesPath).filter(f => f.endsWith('.md'));
3416
+
3417
+ // Sort logic: Favorites first, then alpha
3418
+ const getSortKey = (name, isFav) => (isFav ? '0-' : '1-') + name;
3419
+
3420
+ const locations = getAgentLocations();
3421
+ // Helper to get installed path for a scope
3422
+ const getInstallPath = (scope) => {
3423
+ const loc = locations.find(l => l.scope === scope);
3424
+ return loc ? loc.path : null;
3425
+ };
3426
+
3427
+ const localPath = getInstallPath('local');
3428
+ const globalPath = getInstallPath('global');
3429
+
3430
+ // Build unified agent list
3431
+ let agents = templateFiles.map(file => {
3432
+ const fullPath = join(templatesPath, file);
3433
+ const data = parseAgentFile(fullPath);
3434
+ if (!data) return null;
3435
+
3436
+ const isLocal = localPath && existsSync(join(localPath, file));
3437
+ const isGlobal = globalPath && existsSync(join(globalPath, file));
3438
+
3439
+ return {
3440
+ ...data,
3441
+ fileName: file,
3442
+ // Source of truth
3443
+ isLocal,
3444
+ isGlobal,
3445
+ // Pending state: Default to TRUE (Auto-Select All)
3446
+ wantsLocal: true,
3447
+ wantsGlobal: true
3448
+ };
3449
+ }).filter(a => a !== null);
3450
+
3451
+ // Initial Sort
3452
+ agents.sort((a, b) => getSortKey(a.name, a.favorite).localeCompare(getSortKey(b.name, b.favorite)));
3453
+
3454
+ let selectedIndex = 0;
3455
+
3456
+ // No more sub-menu!
3457
+
3458
+ const render = () => {
3459
+ // Clear screen
3460
+ process.stdout.write('\x1b[2J\x1b[H');
3461
+
3462
+ console.log(` ${c.bold}${c.magenta}wtv${c.reset} ${c.dim}Agent Manager${c.reset}\n`);
3463
+
3464
+ const sidebarWidth = 35;
3465
+ const star = isWindows ? '*' : '★';
3466
+
3467
+ // Header for columns
3468
+ console.log(` ${c.dim} [L] [G] NAME${c.reset}`);
3469
+
3470
+ for (let i = 0; i < agents.length; i++) {
3471
+ const agent = agents[i];
3472
+ const isSelected = i === selectedIndex;
3473
+ const isFav = agent.favorite;
3474
+
3475
+ const cursor = isSelected ? c.magenta + sym.arrow + c.reset : ' ';
3476
+ const favIcon = isFav ? c.yellow + star + c.reset : ' ';
3477
+
3478
+ // Status indicators with changed state logic
3479
+ // We use specific colors for local vs global columns to differentiate columns visually?
3480
+ // Actually reusing logic is clearer. Let's tweak slightly for "Global" vs "Local" letters.
3481
+ const localIndRaw = agent.wantsLocal ? (agent.isLocal ? c.green + '[L]' : c.green + '[+]') : (agent.isLocal ? c.red + '[-]' : c.dim + '[ ]');
3482
+ const globalIndRaw = agent.wantsGlobal ? (agent.isGlobal ? c.blue + '[G]' : c.blue + '[+]') : (agent.isGlobal ? c.red + '[-]' : c.dim + '[ ]');
3483
+
3484
+ // Restore color reset
3485
+ const localInd = localIndRaw + c.reset;
3486
+ const globalInd = globalIndRaw + c.reset;
3487
+
3488
+ // Formatting
3489
+ const nameColor = isSelected ? c.magenta + c.bold : (isFav ? c.reset : c.dim);
3490
+
3491
+ // Construct line
3492
+ let line = ` ${cursor} ${localInd} ${globalInd} ${nameColor}${agent.name.padEnd(14)}${c.reset}`;
3493
+ line += c.dim + ' │ ' + c.reset;
3494
+
3495
+ if (i < 20) {
3496
+ process.stdout.write(line + '\n');
3497
+ }
3498
+ }
3499
+
3500
+ // --- Detail Pane ---
3501
+ const detailStartY = 4;
3502
+ const detailStartX = sidebarWidth + 5;
3503
+ const agent = agents[selectedIndex];
3504
+
3505
+ // Status Text
3506
+ let statusText = '';
3507
+ if (agent.wantsLocal) statusText += c.green + 'Local ' + c.reset;
3508
+ if (agent.wantsGlobal) statusText += c.blue + 'Global ' + c.reset;
3509
+ if (!agent.wantsLocal && !agent.wantsGlobal) statusText = c.dim + 'None' + c.reset;
3510
+
3511
+ const details = [
3512
+ `${c.magenta}${c.bold}${agent.name.toUpperCase()}${c.reset}`,
3513
+ `Status: ${statusText}`,
3514
+ '',
3515
+ `${c.dim}${agent.description}${c.reset}`,
3516
+ '',
3517
+ agent.asciiArt ? agent.asciiArt : `${c.dim}[ No Portrait ]${c.reset}`
3518
+ ];
3519
+
3520
+ // Draw details
3521
+ const allDetailLines = details.flatMap(d => d.split('\n'));
3522
+ for (let i = 0; i < allDetailLines.length; i++) {
3523
+ process.stdout.write(`\x1b[${detailStartY + i};${detailStartX}H${allDetailLines[i]}`);
3524
+ }
3525
+
3526
+ // Footer
3527
+ process.stdout.write(`\x1b[${Math.max(agents.length + 6, 25)};1H`);
3528
+
3529
+ // Calc pending changes
3530
+ let pendingCount = 0;
3531
+ agents.forEach(a => {
3532
+ if (a.isLocal !== a.wantsLocal) pendingCount++;
3533
+ if (a.isGlobal !== a.wantsGlobal) pendingCount++;
3534
+ });
3535
+
3536
+ const actionPrompt = pendingCount > 0
3537
+ ? `${c.yellow}Enter: Apply ${pendingCount} Changes${c.reset}`
3538
+ : `${c.dim}Enter: Done${c.reset}`;
3539
+
3540
+ console.log(`\n ${c.dim}L: Toggle Local • G: Toggle Global • ${actionPrompt} ${c.dim}• Esc: Cancel${c.reset}`);
3541
+ };
3542
+
3543
+ render();
3544
+
3545
+ // Input Handling
3546
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
3547
+ emitKeypressEvents(process.stdin);
3548
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
3549
+
3550
+ return new Promise((resolve) => {
3551
+ const applyChanges = () => {
3552
+ let changed = false;
3553
+
3554
+ // Process all agents
3555
+ agents.forEach(agent => {
3556
+ // LOCAL
3557
+ if (localPath && agent.isLocal !== agent.wantsLocal) {
3558
+ const dest = join(localPath, agent.fileName);
3559
+ if (agent.wantsLocal) {
3560
+ ensureDir(localPath);
3561
+ copyFileSync(agent.path, dest);
3562
+ } else {
3563
+ if (existsSync(dest)) fs.unlinkSync(dest);
3564
+ }
3565
+ changed = true;
3566
+ }
3567
+
3568
+ // GLOBAL
3569
+ if (globalPath && agent.isGlobal !== agent.wantsGlobal) {
3570
+ const dest = join(globalPath, agent.fileName);
3571
+ if (agent.wantsGlobal) {
3572
+ ensureDir(globalPath);
3573
+ copyFileSync(agent.path, dest);
3574
+ } else {
3575
+ if (existsSync(dest)) fs.unlinkSync(dest);
3576
+ }
3577
+ changed = true;
3578
+ }
3579
+ });
3580
+
3581
+ if (changed) {
3582
+ console.log(`\n ${c.green}${sym.check}${c.reset} Changes applied.`);
3583
+ }
3584
+ };
3585
+
3586
+ const onKey = async (str, key) => {
3587
+ if (key.ctrl && key.name === 'c') {
3588
+ cleanup(); process.exit();
3589
+ }
3590
+
3591
+ if (key.name === 'up') {
3592
+ selectedIndex = (selectedIndex - 1 + agents.length) % agents.length;
3593
+ render();
3594
+ } else if (key.name === 'down') {
3595
+ selectedIndex = (selectedIndex + 1) % agents.length;
3596
+ render();
3597
+ } else if (key.name === 'l') {
3598
+ if (key.shift) {
3599
+ // Toggle ALL based on majority
3600
+ const installCount = agents.filter(a => a.wantsLocal).length;
3601
+ const majorityInstalled = installCount >= (agents.length / 2);
3602
+ const targetState = !majorityInstalled;
3603
+ agents.forEach(a => a.wantsLocal = targetState);
3604
+ } else {
3605
+ const agent = agents[selectedIndex];
3606
+ agent.wantsLocal = !agent.wantsLocal;
3607
+ }
3608
+ render();
3609
+ } else if (key.name === 'g') {
3610
+ if (key.shift) {
3611
+ // Toggle ALL based on majority
3612
+ const installCount = agents.filter(a => a.wantsGlobal).length;
3613
+ const majorityInstalled = installCount >= (agents.length / 2);
3614
+ const targetState = !majorityInstalled;
3615
+ agents.forEach(a => a.wantsGlobal = targetState);
3616
+ } else {
3617
+ const agent = agents[selectedIndex];
3618
+ agent.wantsGlobal = !agent.wantsGlobal;
3619
+ }
3620
+ render();
3621
+ } else if (key.name === 'space') {
3622
+ // Toggle Favorite (Still instant for config)
3623
+ const agent = agents[selectedIndex];
3624
+ agent.favorite = !agent.favorite;
3625
+ toggleFavorite(agent.name);
3626
+ render();
3627
+ } else if (key.name === 'return') {
3628
+ cleanup();
3629
+ applyChanges();
3630
+ resolve();
3631
+ } else if (key.name === 'q' || key.name === 'escape') {
3632
+ cleanup();
3633
+ resolve();
3634
+ }
3635
+ };
3636
+
3637
+ const cleanup = () => {
3638
+ process.stdin.removeListener('keypress', onKey);
3639
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
3640
+ rl.close();
3641
+ process.stdout.write('\x1b[2J\x1b[H');
3642
+ };
3643
+
3644
+ process.stdin.on('keypress', onKey);
3645
+ });
3646
+ }
3647
+
3648
+ async function agentsList() {
3649
+ if (process.stdout.isTTY && !process.env.NON_INTERACTIVE) {
3650
+ await agentsInteractive();
3651
+ return;
3652
+ }
3653
+
3654
+ const pad = centerPad();
3655
+
3656
+ console.log('');
3657
+ console.log(drawBox([
3658
+ ` ${c.bold}${c.magenta}wtv${c.reset} ${c.dim}agents${c.reset}`,
3659
+ ], { style: 'round', color: c.magenta, padding: 0 }));
3660
+ console.log('');
3661
+
3662
+ const localAgents = discoverAgents('local');
3663
+ const globalAgents = discoverAgents('global');
3664
+
3665
+ const star = isWindows ? '*' : '★';
3666
+ const noStar = ' ';
3667
+
3668
+ if (localAgents.length > 0) {
3669
+ console.log(`${pad}${c.bold}LOCAL${c.reset} ${c.dim}(project-level)${c.reset}`);
3670
+ for (const agent of localAgents) {
3671
+ const fav = agent.favorite ? `${c.yellow}${star}${c.reset}` : noStar;
3672
+ console.log(`${pad} ${fav} ${c.cyan}${agent.name.padEnd(24)}${c.reset} ${c.dim}${agent.description.slice(0, 45)}${c.reset}`);
3673
+ }
3674
+ console.log('');
3675
+ }
3676
+
3677
+ if (globalAgents.length > 0) {
3678
+ console.log(`${pad}${c.bold}GLOBAL${c.reset} ${c.dim}(~/.claude/agents/)${c.reset}`);
3679
+ for (const agent of globalAgents) {
3680
+ const fav = agent.favorite ? `${c.yellow}${star}${c.reset}` : noStar;
3681
+ console.log(`${pad} ${fav} ${c.cyan}${agent.name.padEnd(24)}${c.reset} ${c.dim}${agent.description.slice(0, 45)}${c.reset}`);
3682
+ }
3683
+ console.log('');
3684
+ }
3685
+
3686
+ if (localAgents.length === 0 && globalAgents.length === 0) {
3687
+ console.log(`${pad}${c.yellow}${sym.warn}${c.reset} No agents found.`);
3688
+ console.log(`${pad}${c.dim}Run 'wtv init' to install agents.${c.reset}`);
3689
+ console.log('');
3690
+ }
3691
+ }
3692
+
3693
+ async function agentsInfo(name) {
3694
+ const pad = centerPad();
3695
+ const agent = findAgent(name);
3696
+
3697
+ if (!agent) {
3698
+ console.log(`\n${pad}${c.red}${sym.cross}${c.reset} Agent not found: ${name}`);
3699
+ console.log(`${pad}${c.dim}Run 'wtv agents' to see available agents.${c.reset}\n`);
3700
+ return;
3701
+ }
3702
+
3703
+ const star = isWindows ? '*' : '★';
3704
+
3705
+ console.log('');
3706
+ console.log(drawBox([
3707
+ ` ${c.bold}${agent.name}${c.reset}`,
3708
+ agent.favorite ? ` ${c.yellow}${star} Favorite${c.reset}` : '',
3709
+ ].filter(Boolean), { style: 'round', color: c.cyan, padding: 0 }));
3710
+ console.log('');
3711
+
3712
+ console.log(`${pad}${c.bold}Description${c.reset}`);
3713
+ console.log(`${pad} ${agent.description}`);
3714
+ console.log('');
3715
+
3716
+ console.log(`${pad}${c.bold}Details${c.reset}`);
3717
+ console.log(`${pad} Model: ${c.cyan}${agent.model}${c.reset}`);
3718
+ console.log(`${pad} Tools: ${c.dim}${agent.tools.join(', ') || 'none'}${c.reset}`);
3719
+ console.log(`${pad} Skills: ${c.dim}${agent.skills.join(', ') || 'none'}${c.reset}`);
3720
+ console.log('');
3721
+
3722
+ console.log(`${pad}${c.bold}Location${c.reset}`);
3723
+ console.log(`${pad} ${c.dim}${agent.path}${c.reset}`);
3724
+ console.log('');
3725
+
3726
+ console.log(`${pad}${c.dim}Actions:${c.reset}`);
3727
+ console.log(`${pad} ${c.green}wtv agents edit ${agent.name}${c.reset}`);
3728
+ console.log(`${pad} ${c.green}wtv agents fav ${agent.name}${c.reset}`);
3729
+ console.log('');
3730
+ }
3731
+
3732
+ function agentsFav(name, scope = 'project') {
3733
+ const pad = centerPad();
3734
+ const agent = findAgent(name);
3735
+
3736
+ if (!agent) {
3737
+ console.log(`\n${pad}${c.red}${sym.cross}${c.reset} Agent not found: ${name}`);
3738
+ console.log(`${pad}${c.dim}Run 'wtv agents' to see available agents.${c.reset}\n`);
3739
+ return;
3740
+ }
3741
+
3742
+ const nowFavorite = toggleFavorite(agent.name, scope);
3743
+ const star = isWindows ? '*' : '★';
3744
+
3745
+ if (nowFavorite) {
3746
+ console.log(`\n${pad}${c.yellow}${star}${c.reset} ${c.bold}${agent.name}${c.reset} is now a favorite.\n`);
3747
+ } else {
3748
+ console.log(`\n${pad}${c.dim}${sym.bullet}${c.reset} ${c.bold}${agent.name}${c.reset} removed from favorites.\n`);
3749
+ }
3750
+ }
3751
+
3752
+ async function agentsAdd(name, scope = 'project') {
3753
+ const pad = centerPad();
3754
+
3755
+ if (!name) {
3756
+ name = await prompt(`${pad}Agent name (e.g., my-artisan): `);
3757
+ if (!name.trim()) {
3758
+ console.log(`\n${pad}${c.red}${sym.cross}${c.reset} Name required.\n`);
3759
+ return;
3760
+ }
3761
+ name = name.trim().toLowerCase().replace(/\s+/g, '-');
3762
+ }
3763
+
3764
+ // Check if already exists
3765
+ const existing = findAgent(name);
3766
+ if (existing) {
3767
+ console.log(`\n${pad}${c.yellow}${sym.warn}${c.reset} Agent '${name}' already exists at:`);
3768
+ console.log(`${pad} ${c.dim}${existing.path}${c.reset}\n`);
3769
+ return;
3770
+ }
3771
+
3772
+ const description = await prompt(`${pad}Description: `);
3773
+
3774
+ // Determine target directory
3775
+ const targetDir = scope === 'global'
3776
+ ? join(getHomedir(), '.claude', 'agents')
3777
+ : join(process.cwd(), '.claude', 'agents');
3778
+
3779
+ ensureDir(targetDir);
3780
+
3781
+ const content = createAgentFromTemplate(name, description || 'Custom agent');
3782
+ const filePath = join(targetDir, `${name}.md`);
3783
+
3784
+ writeFileSync(filePath, content);
3785
+
3786
+ console.log(`\n${pad}${c.green}${sym.check}${c.reset} Created: ${c.cyan}${name}${c.reset}`);
3787
+ console.log(`${pad} ${c.dim}${filePath}${c.reset}`);
3788
+ console.log(`\n${pad}Edit with: ${c.green}wtv agents edit ${name}${c.reset}\n`);
3789
+ }
3790
+
3791
+ async function agentsEdit(name) {
3792
+ const pad = centerPad();
3793
+ const agent = findAgent(name);
3794
+
3795
+ if (!agent) {
3796
+ console.log(`\n${pad}${c.red}${sym.cross}${c.reset} Agent not found: ${name}`);
3797
+ console.log(`${pad}${c.dim}Run 'wtv agents' to see available agents.${c.reset}\n`);
3798
+ return;
3799
+ }
3800
+
3801
+ const config = loadConfig();
3802
+ const editor = config.editor || process.env.EDITOR || 'vim';
3803
+
3804
+ console.log(`\n${pad}Opening ${c.cyan}${agent.name}${c.reset} in ${editor}...`);
3805
+
3806
+ const { execSync } = await import('child_process');
3807
+ try {
3808
+ execSync(`${editor} "${agent.path}"`, { stdio: 'inherit' });
3809
+ console.log(`${pad}${c.green}${sym.check}${c.reset} Done.\n`);
3810
+ } catch (err) {
3811
+ console.log(`${pad}${c.red}${sym.cross}${c.reset} Failed to open editor: ${err.message}\n`);
3812
+ }
3813
+ }
3814
+
3815
+ async function agentsRemove(name) {
3816
+ const pad = centerPad();
3817
+ const agent = findAgent(name);
3818
+
3819
+ if (!agent) {
3820
+ console.log(`\n${pad}${c.red}${sym.cross}${c.reset} Agent not found: ${name}`);
3821
+ console.log(`${pad}${c.dim}Run 'wtv agents' to see available agents.${c.reset}\n`);
3822
+ return;
3823
+ }
3824
+
3825
+ console.log(`\n${pad}${c.yellow}${sym.warn}${c.reset} About to remove: ${c.bold}${agent.name}${c.reset}`);
3826
+ console.log(`${pad} ${c.dim}${agent.path}${c.reset}\n`);
3827
+
3828
+ const confirmed = await confirm(`${pad}Are you sure?`, false);
3829
+
3830
+ if (!confirmed) {
3831
+ console.log(`\n${pad}${c.dim}Cancelled.${c.reset}\n`);
3832
+ return;
3833
+ }
3834
+
3835
+ rmSync(agent.path);
3836
+ console.log(`\n${pad}${c.green}${sym.check}${c.reset} Removed: ${agent.name}\n`);
3837
+ }
3838
+
3839
+ function showVisionStatus(scope) {
3840
+ const visionPath = getVisionPath(scope);
3841
+
3842
+ console.log(`\n ${c.bold}wtv${c.reset} v${getVersion()}`);
3843
+ console.log(` ${c.bold}Vision Status${c.reset}\n`);
3844
+
3845
+ if (!visionPath || !existsSync(visionPath)) {
3846
+ console.log(` ${c.yellow}${sym.warn}${c.reset} No VISION.md found.`);
3847
+ console.log(` ${c.dim}Run 'wtv init' to create one.${c.reset}\n`);
3848
+ return;
3849
+ }
3850
+
3851
+ const content = readFileSync(visionPath, 'utf8');
3852
+ const sections = parseVisionSections(content);
3853
+
3854
+ console.log(` ${c.dim}Path:${c.reset} ${visionPath}\n`);
3855
+
3856
+ const sectionNames = {
3857
+ purpose: 'Purpose',
3858
+ outcomes: 'Outcomes',
3859
+ values: 'Values',
3860
+ constraints: 'Constraints',
3861
+ stage: 'Stage',
3862
+ focus: 'Current Focus',
3863
+ };
3864
+
3865
+ let filledCount = 0;
3866
+ const totalCount = Object.keys(sections).length;
3867
+
3868
+ for (const [key, value] of Object.entries(sections)) {
3869
+ const name = sectionNames[key];
3870
+ if (value) {
3871
+ filledCount++;
3872
+ const preview = value.length > 60 ? value.substring(0, 60) + '...' : value;
3873
+ console.log(` ${c.green}${sym.check}${c.reset} ${name}: ${c.dim}${preview}${c.reset}`);
3874
+ } else {
3875
+ console.log(` ${c.dim}${sym.bullet} ${name}: (blank)${c.reset}`);
3876
+ }
3877
+ }
3878
+
3879
+ console.log(`\n ${c.bold}Summary:${c.reset} ${filledCount}/${totalCount} sections populated`);
3880
+
3881
+ if (filledCount < totalCount) {
3882
+ console.log(` ${c.dim}Edit VISION.md directly to add more detail.${c.reset}`);
3883
+ }
3884
+
3885
+ console.log('');
3886
+ }
3887
+
3888
+ // ============================================================================
3889
+ // Task Logging
3890
+ // ============================================================================
3891
+
3892
+ function getLogsDir(scope) {
3893
+ const base = scope === 'global'
3894
+ ? (() => {
3895
+ const home = getHomedir();
3896
+ return home || null;
3897
+ })()
3898
+ : process.cwd();
3899
+
3900
+ if (!base) return null;
3901
+
3902
+ const wtv = join(base, WTV_LOGS_DIR);
3903
+ if (existsSync(wtv)) return wtv;
3904
+
3905
+ const legacy = join(base, LEGACY_LOGS_DIR);
3906
+ if (existsSync(legacy)) return legacy;
3907
+
3908
+ // Default to new location (created on first write)
3909
+ return wtv;
3910
+ }
3911
+
3912
+ function showLogs(scope, opts = {}) {
3913
+ const logsDir = getLogsDir(scope);
3914
+
3915
+ console.log(`\n ${c.bold}wtv${c.reset} v${getVersion()}`);
3916
+ console.log(` ${c.bold}Task Logs${c.reset}\n`);
3917
+
3918
+ if (!logsDir || !existsSync(logsDir)) {
3919
+ console.log(` ${c.dim}No logs found.${c.reset}`);
3920
+ console.log(` ${c.dim}Logs are created when you run /wtv commands.${c.reset}\n`);
3921
+ return;
3922
+ }
3923
+
3924
+ // Get date directories
3925
+ const dateDirs = readdirSync(logsDir)
3926
+ .filter(d => {
3927
+ const p = join(logsDir, d);
3928
+ return statSync(p).isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(d);
3929
+ })
3930
+ .sort()
3931
+ .reverse();
3932
+
3933
+ if (dateDirs.length === 0) {
3934
+ console.log(` ${c.dim}No logs found.${c.reset}\n`);
3935
+ return;
3936
+ }
3937
+
3938
+ // Filter by date if specified
3939
+ const targetDate = opts.date;
3940
+ const taskId = opts.task;
3941
+
3942
+ // If task ID specified, find and show that task
3943
+ if (taskId) {
3944
+ for (const dateDir of dateDirs) {
3945
+ const taskPath = join(logsDir, dateDir, `${taskId}.md`);
3946
+ if (existsSync(taskPath)) {
3947
+ console.log(` ${c.dim}Date:${c.reset} ${dateDir}`);
3948
+ console.log(` ${c.dim}Task:${c.reset} ${taskId}\n`);
3949
+ const content = readFileSync(taskPath, 'utf8');
3950
+ console.log(content);
3951
+ return;
3952
+ }
3953
+ }
3954
+ console.log(` ${c.yellow}${sym.warn}${c.reset} Task not found: ${taskId}\n`);
3955
+ return;
3956
+ }
3957
+
3958
+ // Show logs by date
3959
+ const filteredDates = targetDate
3960
+ ? dateDirs.filter(d => d === targetDate)
3961
+ : dateDirs.slice(0, 5); // Show last 5 days by default
3962
+
3963
+ if (filteredDates.length === 0) {
3964
+ console.log(` ${c.dim}No logs found for date: ${targetDate}${c.reset}\n`);
3965
+ return;
3966
+ }
3967
+
3968
+ for (const dateDir of filteredDates) {
3969
+ const datePath = join(logsDir, dateDir);
3970
+ const tasks = readdirSync(datePath)
3971
+ .filter(f => f.endsWith('.md'))
3972
+ .sort();
3973
+
3974
+ if (tasks.length === 0) continue;
3975
+
3976
+ console.log(` ${c.bold}${dateDir}${c.reset}`);
3977
+
3978
+ for (const task of tasks) {
3979
+ const taskId = task.replace('.md', '');
3980
+ const taskPath = join(datePath, task);
3981
+ const content = readFileSync(taskPath, 'utf8');
3982
+
3983
+ // Extract status from log
3984
+ const statusMatch = content.match(/\*\*Status:\*\*\s*(\w+)/);
3985
+ const status = statusMatch ? statusMatch[1] : 'Unknown';
3986
+
3987
+ // Extract mode from log
3988
+ const modeMatch = content.match(/\*\*Mode:\*\*\s*(\w+)/);
3989
+ const mode = modeMatch ? modeMatch[1] : '';
3990
+
3991
+ const statusIcon = status === 'Completed' ? c.green + sym.check : c.yellow + sym.bullet;
3992
+
3993
+ console.log(` ${statusIcon}${c.reset} ${taskId} ${c.dim}(${mode})${c.reset}`);
3994
+ }
3995
+
3996
+ console.log('');
3997
+ }
3998
+
3999
+ console.log(` ${c.dim}Use 'wtv log --task <id>' to view a specific task.${c.reset}\n`);
4000
+ }
4001
+
4002
+ function showHelp() {
4003
+ printBanner();
4004
+
4005
+ const pad = centerPad();
4006
+ console.log(`${pad}${c.bold}Usage${c.reset}`);
4007
+ console.log(`${pad} wtv [command] [options]\n`);
4008
+
4009
+ console.log(`${pad}${c.bold}Commands${c.reset}`);
4010
+ console.log(`${pad} ${c.cyan}(no command)${c.reset} Dashboard - show agents and vision`);
4011
+ console.log(`${pad} ${c.cyan}agents${c.reset} List and manage agents`);
4012
+ console.log(`${pad} ${c.cyan}init${c.reset} Install agents and create VISION.md`);
4013
+ console.log(`${pad} ${c.cyan}update${c.reset} Update installed tools`);
4014
+ console.log(`${pad} ${c.cyan}uninstall${c.reset} Remove installed tools`);
4015
+ console.log(`${pad} ${c.cyan}status${c.reset} Show installation status`);
4016
+ console.log(`${pad} ${c.cyan}vision${c.reset} Show VISION.md status`);
4017
+ console.log(`${pad} ${c.cyan}log${c.reset} Show task logs`);
4018
+ console.log(`${pad} ${c.cyan}meet${c.reset} Meet Paul and the Artisans`);
4019
+ console.log(`${pad} ${c.cyan}help${c.reset} Show this help\n`);
4020
+
4021
+ console.log(`${pad}${c.bold}Agent Commands${c.reset}`);
4022
+ console.log(`${pad} ${c.cyan}agents${c.reset} List all agents`);
4023
+ console.log(`${pad} ${c.cyan}agents info${c.reset} <name> Show agent details`);
4024
+ console.log(`${pad} ${c.cyan}agents add${c.reset} <name> Create new agent`);
4025
+ console.log(`${pad} ${c.cyan}agents edit${c.reset} <name> Open in $EDITOR`);
4026
+ console.log(`${pad} ${c.cyan}agents fav${c.reset} <name> Toggle favorite`);
4027
+ console.log(`${pad} ${c.cyan}agents rm${c.reset} <name> Remove agent\n`);
4028
+
4029
+ console.log(`${pad}${c.bold}Habakkuk Workflow${c.reset} ${c.dim}"Write the vision, make it plain"${c.reset}`);
4030
+ console.log(`${pad} ${c.cyan}board${c.reset} [--all] Show kanban board`);
4031
+ console.log(`${pad} ${c.cyan}cry${c.reset} "desc" Enter a problem or need`);
4032
+ console.log(`${pad} ${c.cyan}wait${c.reset} <id> Move to waiting (seeking)`);
4033
+ console.log(`${pad} ${c.cyan}vision${c.reset} <id> Move to vision (answer received)`);
4034
+ console.log(`${pad} ${c.cyan}run${c.reset} <id> Move to run (execute)`);
4035
+ console.log(`${pad} ${c.cyan}worship${c.reset} <id> Move to worship (retrospective)`);
4036
+ console.log(`${pad} ${c.cyan}note${c.reset} <id> "text" Add note to item`);
4037
+ console.log(`${pad} ${c.cyan}item${c.reset} <id> Show item details`);
4038
+ console.log(`${pad} ${c.cyan}stones${c.reset} View completed works\n`);
4039
+
4040
+ console.log(`${pad}${c.bold}Options${c.reset}`);
4041
+ console.log(`${pad} ${c.dim}--global, -g${c.reset} Global scope`);
4042
+ console.log(`${pad} ${c.dim}--local, -l${c.reset} Project scope (default)`);
4043
+ console.log(`${pad} ${c.dim}--force, -f${c.reset} Overwrite existing`);
4044
+ console.log(`${pad} ${c.dim}--tool <tool>${c.reset} Tool: claude | codex | opencode | gemini | antigravity`);
4045
+ console.log(`${pad} ${c.dim}--path <dir>${c.reset} Install/check a custom directory`);
4046
+ console.log(`${pad} ${c.dim}--claude${c.reset} Target Claude Code`);
4047
+ console.log(`${pad} ${c.dim}--codex${c.reset} Target Codex CLI`);
4048
+ console.log(`${pad} ${c.dim}--opencode${c.reset} Target OpenCode`);
4049
+ console.log(`${pad} ${c.dim}--gemini${c.reset} Target Gemini CLI`);
4050
+ console.log(`${pad} ${c.dim}--antigravity${c.reset} Target Antigravity\n`);
4051
+
4052
+ console.log(`${pad}${c.bold}Examples${c.reset}`);
4053
+ console.log(`${pad} ${c.green}wtv${c.reset} ${c.dim}# Dashboard${c.reset}`);
4054
+ console.log(`${pad} ${c.green}wtv agents${c.reset} ${c.dim}# List agents${c.reset}`);
4055
+ console.log(`${pad} ${c.green}wtv init --claude${c.reset} ${c.dim}# Install for Claude Code${c.reset}`);
4056
+ console.log(`${pad} ${c.green}wtv init --opencode${c.reset} ${c.dim}# Install for OpenCode${c.reset}`);
4057
+ console.log(`${pad} ${c.green}wtv init --codex${c.reset} ${c.dim}# Install for Codex CLI${c.reset}`);
4058
+ console.log(`${pad} ${c.green}wtv board --all${c.reset} ${c.dim}# Show full kanban board${c.reset}`);
4059
+ console.log(`${pad} ${c.green}wtv uninstall --tool opencode${c.reset} ${c.dim}# Remove OpenCode install${c.reset}`);
4060
+ console.log('');
4061
+ }
4062
+
4063
+ function parseArgs(args) {
4064
+ const opts = {
4065
+ command: null,
4066
+ subcommand: null,
4067
+ agentName: null,
4068
+ global: false,
4069
+ local: false,
4070
+ force: false,
4071
+ path: null,
4072
+ tools: [],
4073
+ task: null,
4074
+ date: null,
4075
+ all: false,
4076
+ };
4077
+
4078
+ let positionalCount = 0;
4079
+
4080
+ for (let i = 0; i < args.length; i++) {
4081
+ const a = args[i];
4082
+
4083
+ if (a === '--help' || a === '-h') {
4084
+ opts.command = 'help';
4085
+ continue;
4086
+ }
4087
+
4088
+ if (a === '--version' || a === '-v') {
4089
+ opts.command = 'version';
4090
+ continue;
4091
+ }
4092
+
4093
+ if (a === '--global' || a === '-g') {
4094
+ opts.global = true;
4095
+ continue;
4096
+ }
4097
+
4098
+ if (a === '--local' || a === '-l') {
4099
+ opts.local = true;
4100
+ continue;
4101
+ }
4102
+
4103
+ if (a === '--force' || a === '-f') {
4104
+ opts.force = true;
4105
+ continue;
4106
+ }
4107
+
4108
+ if (a === '--all') {
4109
+ opts.all = true;
4110
+ continue;
4111
+ }
4112
+
4113
+ if (a === '--path') {
4114
+ const v = args[i + 1];
4115
+ if (!v || v.startsWith('-')) {
4116
+ throw new Error("--path requires a directory (e.g. '--path /tmp/my-claude')");
4117
+ }
4118
+ opts.path = v;
4119
+ i++;
4120
+ continue;
4121
+ }
4122
+
4123
+ if (a === '--tool') {
4124
+ const v = args[i + 1];
4125
+ if (!v || v.startsWith('-')) {
4126
+ throw new Error("--tool requires a value: claude | codex | opencode");
4127
+ }
4128
+ opts.tools.push(v);
4129
+ i++;
4130
+ continue;
4131
+ }
4132
+
4133
+ if (a === '--claude') {
4134
+ opts.tools.push('claude');
4135
+ continue;
4136
+ }
4137
+
4138
+ if (a === '--codex') {
4139
+ opts.tools.push('codex');
4140
+ continue;
4141
+ }
4142
+
4143
+ if (a === '--opencode') {
4144
+ opts.tools.push('opencode');
4145
+ continue;
4146
+ }
4147
+
4148
+ if (a === '--gemini') {
4149
+ opts.tools.push('gemini');
4150
+ continue;
4151
+ }
4152
+
4153
+ if (a === '--antigravity') {
4154
+ opts.tools.push('antigravity');
4155
+ continue;
4156
+ }
4157
+
4158
+ if (a === '--task') {
4159
+ const v = args[i + 1];
4160
+ if (!v || v.startsWith('-')) {
4161
+ throw new Error("--task requires a task id (e.g. '--task 2026-01-15-001')");
4162
+ }
4163
+ opts.task = v;
4164
+ i++;
4165
+ continue;
4166
+ }
4167
+
4168
+ if (a === '--date') {
4169
+ const v = args[i + 1];
4170
+ if (!v || v.startsWith('-')) {
4171
+ throw new Error("--date requires YYYY-MM-DD (e.g. '--date 2026-01-15')");
4172
+ }
4173
+ opts.date = v;
4174
+ i++;
4175
+ continue;
4176
+ }
4177
+
4178
+ if (a.startsWith('-')) {
4179
+ throw new Error(`Unknown option: ${a}`);
4180
+ }
4181
+
4182
+ // Positional arguments
4183
+ positionalCount++;
4184
+ if (positionalCount === 1) {
4185
+ opts.command = a;
4186
+ } else if (positionalCount === 2) {
4187
+ opts.subcommand = a;
4188
+ } else if (positionalCount === 3) {
4189
+ opts.agentName = a;
4190
+ }
4191
+ }
4192
+
4193
+ return opts;
4194
+ }
4195
+
4196
+ export async function run(args) {
4197
+ let opts;
4198
+ try {
4199
+ opts = parseArgs(args);
4200
+ } catch (err) {
4201
+ console.log(`\n ${c.red}Error:${c.reset} ${err.message}\n`);
4202
+ showHelp();
4203
+ process.exit(1);
4204
+ }
4205
+
4206
+ const scope = opts.global ? 'global' : 'project';
4207
+
4208
+ if (opts.command !== 'version') {
4209
+ checkForUpdates();
4210
+ }
4211
+
4212
+ switch (opts.command) {
4213
+ case 'init': {
4214
+ const nonInteractive = opts.global || opts.local || opts.force || opts.path || opts.tools.length > 0;
4215
+ if (nonInteractive) {
4216
+ initNonInteractive(scope, opts.force, opts.tools, opts.path);
4217
+ } else {
4218
+ if (!process.stdout.isTTY) {
4219
+ console.log(`\n ${c.red}Error:${c.reset} init requires interactive TTY, or pass flags.`);
4220
+ console.log(` Example: wtv init --claude\n`);
4221
+ process.exit(1);
4222
+ }
4223
+ await interactiveInit();
4224
+ }
4225
+ break;
4226
+ }
4227
+
4228
+ case 'update': {
4229
+ update(scope, opts.tools, opts.path);
4230
+ break;
4231
+ }
4232
+
4233
+ case 'uninstall':
4234
+ case 'remove': {
4235
+ if (opts.path && opts.tools.length === 0) {
4236
+ console.log(`\n ${c.red}Error:${c.reset} uninstall with --path requires --tool.\n`);
4237
+ process.exit(1);
4238
+ }
4239
+
4240
+ if (opts.tools.length > 0) {
4241
+ uninstall(scope, opts.tools, opts.path);
4242
+ } else if (process.stdout.isTTY) {
4243
+ await uninstallInteractive(scope);
4244
+ } else {
4245
+ console.log(`\n ${c.red}Error:${c.reset} uninstall requires --tool when not running interactively.\n`);
4246
+ process.exit(1);
4247
+ }
4248
+ break;
4249
+ }
4250
+
4251
+ case 'status': {
4252
+ status(opts.path);
4253
+ break;
4254
+ }
4255
+
4256
+ case 'vision': {
4257
+ // If subcommand is provided, it's a Habakkuk stage transition
4258
+ // Otherwise, show VISION.md status
4259
+ if (opts.subcommand) {
4260
+ habakkukVision(opts.subcommand);
4261
+ } else {
4262
+ showVisionStatus(scope);
4263
+ }
4264
+ break;
4265
+ }
4266
+
4267
+ case 'meet':
4268
+ case 'team': {
4269
+ await meetTheTeam();
4270
+ break;
4271
+ }
4272
+
4273
+ case 'log':
4274
+ case 'logs': {
4275
+ showLogs(scope, { task: opts.task, date: opts.date });
4276
+ break;
4277
+ }
4278
+
4279
+ case 'agents':
4280
+ case 'agent': {
4281
+ // Handle subcommands: list, info, add, edit, fav, remove
4282
+ const subcommand = opts.subcommand;
4283
+ const agentName = opts.agentName;
4284
+
4285
+ switch (subcommand) {
4286
+ case 'list':
4287
+ case null:
4288
+ case undefined:
4289
+ await agentsList();
4290
+ break;
4291
+ case 'info':
4292
+ if (!agentName) {
4293
+ console.log(`\n ${c.red}Error:${c.reset} Agent name required.`);
4294
+ console.log(` Usage: wtv agents info <name>\n`);
4295
+ } else {
4296
+ await agentsInfo(agentName);
4297
+ }
4298
+ break;
4299
+ case 'add':
4300
+ case 'new':
4301
+ case 'create':
4302
+ await agentsAdd(agentName, scope);
4303
+ break;
4304
+ case 'edit':
4305
+ if (!agentName) {
4306
+ console.log(`\n ${c.red}Error:${c.reset} Agent name required.`);
4307
+ console.log(` Usage: wtv agents edit <name>\n`);
4308
+ } else {
4309
+ await agentsEdit(agentName);
4310
+ }
4311
+ break;
4312
+ case 'fav':
4313
+ case 'favorite':
4314
+ case 'star':
4315
+ if (!agentName) {
4316
+ console.log(`\n ${c.red}Error:${c.reset} Agent name required.`);
4317
+ console.log(` Usage: wtv agents fav <name>\n`);
4318
+ } else {
4319
+ agentsFav(agentName, scope);
4320
+ }
4321
+ break;
4322
+ case 'remove':
4323
+ case 'rm':
4324
+ case 'delete':
4325
+ if (!agentName) {
4326
+ console.log(`\n ${c.red}Error:${c.reset} Agent name required.`);
4327
+ console.log(` Usage: wtv agents remove <name>\n`);
4328
+ } else {
4329
+ await agentsRemove(agentName);
4330
+ }
4331
+ break;
4332
+ default:
4333
+ // Assume subcommand is an agent name for quick info
4334
+ await agentsInfo(subcommand);
4335
+ }
4336
+ break;
4337
+ }
4338
+
4339
+ // ===== HABAKKUK WORKFLOW COMMANDS =====
4340
+
4341
+ case 'board': {
4342
+ const showAll = opts.all || opts.subcommand === 'all';
4343
+ habakkukBoard(showAll);
4344
+ break;
4345
+ }
4346
+
4347
+ case 'cry': {
4348
+ if (!opts.subcommand) {
4349
+ console.log(`\n ${c.red}Error:${c.reset} Description required.`);
4350
+ console.log(` Usage: wtv cry "Description of the problem or need"\n`);
4351
+ process.exit(1);
4352
+ }
4353
+ habakkukCry(opts.subcommand);
4354
+ break;
4355
+ }
4356
+
4357
+ case 'wait': {
4358
+ if (!opts.subcommand) {
4359
+ console.log(`\n ${c.red}Error:${c.reset} Item ID or slug required.`);
4360
+ console.log(` Usage: wtv wait <id|slug> [note]\n`);
4361
+ process.exit(1);
4362
+ }
4363
+ habakkukWait(opts.subcommand, opts.agentName);
4364
+ break;
4365
+ }
4366
+
4367
+ case 'run': {
4368
+ if (!opts.subcommand) {
4369
+ console.log(`\n ${c.red}Error:${c.reset} Item ID or slug required.`);
4370
+ console.log(` Usage: wtv run <id|slug>\n`);
4371
+ process.exit(1);
4372
+ }
4373
+ habakkukRun(opts.subcommand);
4374
+ break;
4375
+ }
4376
+
4377
+ case 'worship': {
4378
+ if (!opts.subcommand) {
4379
+ console.log(`\n ${c.red}Error:${c.reset} Item ID or slug required.`);
4380
+ console.log(` Usage: wtv worship <id|slug>\n`);
4381
+ process.exit(1);
4382
+ }
4383
+ await habakkukWorship(opts.subcommand);
4384
+ break;
4385
+ }
4386
+
4387
+ case 'note': {
4388
+ if (!opts.subcommand) {
4389
+ console.log(`\n ${c.red}Error:${c.reset} Item ID or slug required.`);
4390
+ console.log(` Usage: wtv note <id|slug> "Your note text"\n`);
4391
+ process.exit(1);
4392
+ }
4393
+ if (!opts.agentName) {
4394
+ console.log(`\n ${c.red}Error:${c.reset} Note text required.`);
4395
+ console.log(` Usage: wtv note <id|slug> "Your note text"\n`);
4396
+ process.exit(1);
4397
+ }
4398
+ habakkukNote(opts.subcommand, opts.agentName);
4399
+ break;
4400
+ }
4401
+
4402
+ case 'item': {
4403
+ if (!opts.subcommand) {
4404
+ console.log(`\n ${c.red}Error:${c.reset} Item ID or slug required.`);
4405
+ console.log(` Usage: wtv item <id|slug>\n`);
4406
+ process.exit(1);
4407
+ }
4408
+ habakkukItem(opts.subcommand);
4409
+ break;
4410
+ }
4411
+
4412
+ case 'stones': {
4413
+ habakkukStones();
4414
+ break;
4415
+ }
4416
+
4417
+ // ===== END HABAKKUK WORKFLOW COMMANDS =====
4418
+
4419
+ case 'version':
4420
+ case '-v':
4421
+ case '--version': {
4422
+ console.log(getVersion());
4423
+ break;
4424
+ }
4425
+
4426
+ case 'help':
4427
+ case '--help':
4428
+ case '-h': {
4429
+ showHelp();
4430
+ break;
4431
+ }
4432
+
4433
+ case null: {
4434
+ const wantsInit = opts.global || opts.local || opts.force || opts.path || opts.tools.length > 0;
4435
+
4436
+ if (wantsInit) {
4437
+ initNonInteractive(scope, opts.force, opts.tools, opts.path);
4438
+ } else {
4439
+ // NEW: Show dashboard by default
4440
+ await dashboard();
4441
+ }
4442
+
4443
+ break;
4444
+ }
4445
+
4446
+ default: {
4447
+ console.log(`\n ${c.red}Unknown command:${c.reset} ${opts.command}`);
4448
+ console.log(` Run 'wtv help' for usage.\n`);
4449
+ process.exit(1);
4450
+ }
4451
+ }
4452
+ }