vibe-fabric 0.1.0

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 (271) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -0
  3. package/dist/cli/commands/claude.d.ts +19 -0
  4. package/dist/cli/commands/claude.d.ts.map +1 -0
  5. package/dist/cli/commands/claude.js +107 -0
  6. package/dist/cli/commands/claude.js.map +1 -0
  7. package/dist/cli/commands/coverage.d.ts +37 -0
  8. package/dist/cli/commands/coverage.d.ts.map +1 -0
  9. package/dist/cli/commands/coverage.js +374 -0
  10. package/dist/cli/commands/coverage.js.map +1 -0
  11. package/dist/cli/commands/doctor.d.ts +30 -0
  12. package/dist/cli/commands/doctor.d.ts.map +1 -0
  13. package/dist/cli/commands/doctor.js +187 -0
  14. package/dist/cli/commands/doctor.js.map +1 -0
  15. package/dist/cli/commands/gaps.d.ts +52 -0
  16. package/dist/cli/commands/gaps.d.ts.map +1 -0
  17. package/dist/cli/commands/gaps.js +487 -0
  18. package/dist/cli/commands/gaps.js.map +1 -0
  19. package/dist/cli/commands/help.d.ts +7 -0
  20. package/dist/cli/commands/help.d.ts.map +1 -0
  21. package/dist/cli/commands/help.js +51 -0
  22. package/dist/cli/commands/help.js.map +1 -0
  23. package/dist/cli/commands/init.d.ts +39 -0
  24. package/dist/cli/commands/init.d.ts.map +1 -0
  25. package/dist/cli/commands/init.js +246 -0
  26. package/dist/cli/commands/init.js.map +1 -0
  27. package/dist/cli/commands/prd.d.ts +30 -0
  28. package/dist/cli/commands/prd.d.ts.map +1 -0
  29. package/dist/cli/commands/prd.js +179 -0
  30. package/dist/cli/commands/prd.js.map +1 -0
  31. package/dist/cli/commands/repo/add.d.ts +36 -0
  32. package/dist/cli/commands/repo/add.d.ts.map +1 -0
  33. package/dist/cli/commands/repo/add.js +303 -0
  34. package/dist/cli/commands/repo/add.js.map +1 -0
  35. package/dist/cli/commands/scope.d.ts +36 -0
  36. package/dist/cli/commands/scope.d.ts.map +1 -0
  37. package/dist/cli/commands/scope.js +312 -0
  38. package/dist/cli/commands/scope.js.map +1 -0
  39. package/dist/cli/commands/send.d.ts +43 -0
  40. package/dist/cli/commands/send.d.ts.map +1 -0
  41. package/dist/cli/commands/send.js +469 -0
  42. package/dist/cli/commands/send.js.map +1 -0
  43. package/dist/cli/commands/status.d.ts +32 -0
  44. package/dist/cli/commands/status.d.ts.map +1 -0
  45. package/dist/cli/commands/status.js +422 -0
  46. package/dist/cli/commands/status.js.map +1 -0
  47. package/dist/cli/commands/sync.d.ts +37 -0
  48. package/dist/cli/commands/sync.d.ts.map +1 -0
  49. package/dist/cli/commands/sync.js +299 -0
  50. package/dist/cli/commands/sync.js.map +1 -0
  51. package/dist/cli/commands/version.d.ts +7 -0
  52. package/dist/cli/commands/version.d.ts.map +1 -0
  53. package/dist/cli/commands/version.js +45 -0
  54. package/dist/cli/commands/version.js.map +1 -0
  55. package/dist/cli/index.d.ts +3 -0
  56. package/dist/cli/index.d.ts.map +1 -0
  57. package/dist/cli/index.js +65 -0
  58. package/dist/cli/index.js.map +1 -0
  59. package/dist/cli/ui/components/ActiveScopes.d.ts +13 -0
  60. package/dist/cli/ui/components/ActiveScopes.d.ts.map +1 -0
  61. package/dist/cli/ui/components/ActiveScopes.js +25 -0
  62. package/dist/cli/ui/components/ActiveScopes.js.map +1 -0
  63. package/dist/cli/ui/components/EmptyState.d.ts +24 -0
  64. package/dist/cli/ui/components/EmptyState.d.ts.map +1 -0
  65. package/dist/cli/ui/components/EmptyState.js +13 -0
  66. package/dist/cli/ui/components/EmptyState.js.map +1 -0
  67. package/dist/cli/ui/components/Header.d.ts +11 -0
  68. package/dist/cli/ui/components/Header.d.ts.map +1 -0
  69. package/dist/cli/ui/components/Header.js +32 -0
  70. package/dist/cli/ui/components/Header.js.map +1 -0
  71. package/dist/cli/ui/components/PrdCoverage.d.ts +12 -0
  72. package/dist/cli/ui/components/PrdCoverage.d.ts.map +1 -0
  73. package/dist/cli/ui/components/PrdCoverage.js +15 -0
  74. package/dist/cli/ui/components/PrdCoverage.js.map +1 -0
  75. package/dist/cli/ui/components/RecentActivity.d.ts +12 -0
  76. package/dist/cli/ui/components/RecentActivity.d.ts.map +1 -0
  77. package/dist/cli/ui/components/RecentActivity.js +40 -0
  78. package/dist/cli/ui/components/RecentActivity.js.map +1 -0
  79. package/dist/cli/ui/components/RepoList.d.ts +12 -0
  80. package/dist/cli/ui/components/RepoList.d.ts.map +1 -0
  81. package/dist/cli/ui/components/RepoList.js +24 -0
  82. package/dist/cli/ui/components/RepoList.js.map +1 -0
  83. package/dist/cli/ui/components/Shortcuts.d.ts +7 -0
  84. package/dist/cli/ui/components/Shortcuts.d.ts.map +1 -0
  85. package/dist/cli/ui/components/Shortcuts.js +7 -0
  86. package/dist/cli/ui/components/Shortcuts.js.map +1 -0
  87. package/dist/cli/ui/components/index.d.ts +12 -0
  88. package/dist/cli/ui/components/index.d.ts.map +1 -0
  89. package/dist/cli/ui/components/index.js +12 -0
  90. package/dist/cli/ui/components/index.js.map +1 -0
  91. package/dist/cli/ui/dashboard.d.ts +9 -0
  92. package/dist/cli/ui/dashboard.d.ts.map +1 -0
  93. package/dist/cli/ui/dashboard.js +102 -0
  94. package/dist/cli/ui/dashboard.js.map +1 -0
  95. package/dist/core/commands.d.ts +32 -0
  96. package/dist/core/commands.d.ts.map +1 -0
  97. package/dist/core/commands.js +361 -0
  98. package/dist/core/commands.js.map +1 -0
  99. package/dist/core/config.d.ts +18 -0
  100. package/dist/core/config.d.ts.map +1 -0
  101. package/dist/core/config.js +78 -0
  102. package/dist/core/config.js.map +1 -0
  103. package/dist/core/coverage.d.ts +32 -0
  104. package/dist/core/coverage.d.ts.map +1 -0
  105. package/dist/core/coverage.js +286 -0
  106. package/dist/core/coverage.js.map +1 -0
  107. package/dist/core/dashboard/data.d.ts +73 -0
  108. package/dist/core/dashboard/data.d.ts.map +1 -0
  109. package/dist/core/dashboard/data.js +250 -0
  110. package/dist/core/dashboard/data.js.map +1 -0
  111. package/dist/core/dependencies.d.ts +39 -0
  112. package/dist/core/dependencies.d.ts.map +1 -0
  113. package/dist/core/dependencies.js +160 -0
  114. package/dist/core/dependencies.js.map +1 -0
  115. package/dist/core/doctor/auth.d.ts +22 -0
  116. package/dist/core/doctor/auth.d.ts.map +1 -0
  117. package/dist/core/doctor/auth.js +147 -0
  118. package/dist/core/doctor/auth.js.map +1 -0
  119. package/dist/core/doctor/config.d.ts +26 -0
  120. package/dist/core/doctor/config.d.ts.map +1 -0
  121. package/dist/core/doctor/config.js +172 -0
  122. package/dist/core/doctor/config.js.map +1 -0
  123. package/dist/core/doctor/environment.d.ts +26 -0
  124. package/dist/core/doctor/environment.d.ts.map +1 -0
  125. package/dist/core/doctor/environment.js +145 -0
  126. package/dist/core/doctor/environment.js.map +1 -0
  127. package/dist/core/doctor/index.d.ts +44 -0
  128. package/dist/core/doctor/index.d.ts.map +1 -0
  129. package/dist/core/doctor/index.js +134 -0
  130. package/dist/core/doctor/index.js.map +1 -0
  131. package/dist/core/doctor/repos.d.ts +22 -0
  132. package/dist/core/doctor/repos.d.ts.map +1 -0
  133. package/dist/core/doctor/repos.js +262 -0
  134. package/dist/core/doctor/repos.js.map +1 -0
  135. package/dist/core/doctor/sync.d.ts +18 -0
  136. package/dist/core/doctor/sync.d.ts.map +1 -0
  137. package/dist/core/doctor/sync.js +146 -0
  138. package/dist/core/doctor/sync.js.map +1 -0
  139. package/dist/core/gaps.d.ts +70 -0
  140. package/dist/core/gaps.d.ts.map +1 -0
  141. package/dist/core/gaps.js +448 -0
  142. package/dist/core/gaps.js.map +1 -0
  143. package/dist/core/github.d.ts +38 -0
  144. package/dist/core/github.d.ts.map +1 -0
  145. package/dist/core/github.js +102 -0
  146. package/dist/core/github.js.map +1 -0
  147. package/dist/core/prd/analyzer.d.ts +44 -0
  148. package/dist/core/prd/analyzer.d.ts.map +1 -0
  149. package/dist/core/prd/analyzer.js +259 -0
  150. package/dist/core/prd/analyzer.js.map +1 -0
  151. package/dist/core/prd/check.d.ts +17 -0
  152. package/dist/core/prd/check.d.ts.map +1 -0
  153. package/dist/core/prd/check.js +154 -0
  154. package/dist/core/prd/check.js.map +1 -0
  155. package/dist/core/prd/index.d.ts +6 -0
  156. package/dist/core/prd/index.d.ts.map +1 -0
  157. package/dist/core/prd/index.js +6 -0
  158. package/dist/core/prd/index.js.map +1 -0
  159. package/dist/core/project.d.ts +13 -0
  160. package/dist/core/project.d.ts.map +1 -0
  161. package/dist/core/project.js +810 -0
  162. package/dist/core/project.js.map +1 -0
  163. package/dist/core/prompts.d.ts +52 -0
  164. package/dist/core/prompts.d.ts.map +1 -0
  165. package/dist/core/prompts.js +266 -0
  166. package/dist/core/prompts.js.map +1 -0
  167. package/dist/core/repo/framework.d.ts +38 -0
  168. package/dist/core/repo/framework.d.ts.map +1 -0
  169. package/dist/core/repo/framework.js +142 -0
  170. package/dist/core/repo/framework.js.map +1 -0
  171. package/dist/core/repo/index.d.ts +6 -0
  172. package/dist/core/repo/index.d.ts.map +1 -0
  173. package/dist/core/repo/index.js +6 -0
  174. package/dist/core/repo/index.js.map +1 -0
  175. package/dist/core/repo/templates/claude-agents.d.ts +6 -0
  176. package/dist/core/repo/templates/claude-agents.d.ts.map +1 -0
  177. package/dist/core/repo/templates/claude-agents.js +173 -0
  178. package/dist/core/repo/templates/claude-agents.js.map +1 -0
  179. package/dist/core/repo/templates/claude-commands.d.ts +6 -0
  180. package/dist/core/repo/templates/claude-commands.d.ts.map +1 -0
  181. package/dist/core/repo/templates/claude-commands.js +278 -0
  182. package/dist/core/repo/templates/claude-commands.js.map +1 -0
  183. package/dist/core/repo/templates/claude-prompts.d.ts +6 -0
  184. package/dist/core/repo/templates/claude-prompts.d.ts.map +1 -0
  185. package/dist/core/repo/templates/claude-prompts.js +258 -0
  186. package/dist/core/repo/templates/claude-prompts.js.map +1 -0
  187. package/dist/core/repo/templates/claude-scripts.d.ts +6 -0
  188. package/dist/core/repo/templates/claude-scripts.d.ts.map +1 -0
  189. package/dist/core/repo/templates/claude-scripts.js +212 -0
  190. package/dist/core/repo/templates/claude-scripts.js.map +1 -0
  191. package/dist/core/repo/templates/index.d.ts +22 -0
  192. package/dist/core/repo/templates/index.d.ts.map +1 -0
  193. package/dist/core/repo/templates/index.js +121 -0
  194. package/dist/core/repo/templates/index.js.map +1 -0
  195. package/dist/core/repo/templates/vibe-readme.d.ts +6 -0
  196. package/dist/core/repo/templates/vibe-readme.d.ts.map +1 -0
  197. package/dist/core/repo/templates/vibe-readme.js +204 -0
  198. package/dist/core/repo/templates/vibe-readme.js.map +1 -0
  199. package/dist/core/repo/templates/vibe-scripts.d.ts +6 -0
  200. package/dist/core/repo/templates/vibe-scripts.d.ts.map +1 -0
  201. package/dist/core/repo/templates/vibe-scripts.js +308 -0
  202. package/dist/core/repo/templates/vibe-scripts.js.map +1 -0
  203. package/dist/core/repo/validation.d.ts +46 -0
  204. package/dist/core/repo/validation.d.ts.map +1 -0
  205. package/dist/core/repo/validation.js +154 -0
  206. package/dist/core/repo/validation.js.map +1 -0
  207. package/dist/core/runner.d.ts +38 -0
  208. package/dist/core/runner.d.ts.map +1 -0
  209. package/dist/core/runner.js +124 -0
  210. package/dist/core/runner.js.map +1 -0
  211. package/dist/core/send.d.ts +83 -0
  212. package/dist/core/send.d.ts.map +1 -0
  213. package/dist/core/send.js +565 -0
  214. package/dist/core/send.js.map +1 -0
  215. package/dist/core/status.d.ts +76 -0
  216. package/dist/core/status.d.ts.map +1 -0
  217. package/dist/core/status.js +430 -0
  218. package/dist/core/status.js.map +1 -0
  219. package/dist/core/sync/aggregator.d.ts +22 -0
  220. package/dist/core/sync/aggregator.d.ts.map +1 -0
  221. package/dist/core/sync/aggregator.js +278 -0
  222. package/dist/core/sync/aggregator.js.map +1 -0
  223. package/dist/core/sync/completion.d.ts +37 -0
  224. package/dist/core/sync/completion.d.ts.map +1 -0
  225. package/dist/core/sync/completion.js +264 -0
  226. package/dist/core/sync/completion.js.map +1 -0
  227. package/dist/core/sync/index.d.ts +51 -0
  228. package/dist/core/sync/index.d.ts.map +1 -0
  229. package/dist/core/sync/index.js +200 -0
  230. package/dist/core/sync/index.js.map +1 -0
  231. package/dist/core/sync/scanner.d.ts +39 -0
  232. package/dist/core/sync/scanner.d.ts.map +1 -0
  233. package/dist/core/sync/scanner.js +364 -0
  234. package/dist/core/sync/scanner.js.map +1 -0
  235. package/dist/types/config.d.ts +157 -0
  236. package/dist/types/config.d.ts.map +1 -0
  237. package/dist/types/config.js +58 -0
  238. package/dist/types/config.js.map +1 -0
  239. package/dist/types/coverage.d.ts +100 -0
  240. package/dist/types/coverage.d.ts.map +1 -0
  241. package/dist/types/coverage.js +8 -0
  242. package/dist/types/coverage.js.map +1 -0
  243. package/dist/types/doctor.d.ts +68 -0
  244. package/dist/types/doctor.d.ts.map +1 -0
  245. package/dist/types/doctor.js +5 -0
  246. package/dist/types/doctor.js.map +1 -0
  247. package/dist/types/gaps.d.ts +129 -0
  248. package/dist/types/gaps.d.ts.map +1 -0
  249. package/dist/types/gaps.js +8 -0
  250. package/dist/types/gaps.js.map +1 -0
  251. package/dist/types/prompts.d.ts +99 -0
  252. package/dist/types/prompts.d.ts.map +1 -0
  253. package/dist/types/prompts.js +5 -0
  254. package/dist/types/prompts.js.map +1 -0
  255. package/dist/types/runner.d.ts +156 -0
  256. package/dist/types/runner.d.ts.map +1 -0
  257. package/dist/types/runner.js +41 -0
  258. package/dist/types/runner.js.map +1 -0
  259. package/dist/types/send.d.ts +157 -0
  260. package/dist/types/send.d.ts.map +1 -0
  261. package/dist/types/send.js +18 -0
  262. package/dist/types/send.js.map +1 -0
  263. package/dist/types/status.d.ts +150 -0
  264. package/dist/types/status.d.ts.map +1 -0
  265. package/dist/types/status.js +15 -0
  266. package/dist/types/status.js.map +1 -0
  267. package/dist/types/sync.d.ts +259 -0
  268. package/dist/types/sync.d.ts.map +1 -0
  269. package/dist/types/sync.js +38 -0
  270. package/dist/types/sync.js.map +1 -0
  271. package/package.json +72 -0
@@ -0,0 +1,810 @@
1
+ import { mkdir, writeFile } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import path from 'path';
4
+ import { saveConfig } from './config.js';
5
+ import { generatePlanningHubCommands, ensureCustomCommandsDir } from './commands.js';
6
+ /**
7
+ * Planning hub directory structure
8
+ */
9
+ const DIRECTORY_STRUCTURE = [
10
+ 'docs/prd/vision',
11
+ 'docs/prd/modules',
12
+ 'docs/scopes/drafts',
13
+ 'docs/scopes/ready',
14
+ 'docs/scopes/sent',
15
+ 'docs/scopes/completed',
16
+ 'docs/sync-cache',
17
+ '.claude/commands',
18
+ '.claude/scripts',
19
+ '.claude/prompts',
20
+ ];
21
+ /**
22
+ * Create a new planning hub directory structure
23
+ */
24
+ export async function createPlanningHub(projectPath, projectName, projectType) {
25
+ // Create root directory if it doesn't exist
26
+ if (!existsSync(projectPath)) {
27
+ await mkdir(projectPath, { recursive: true });
28
+ }
29
+ // Create all subdirectories
30
+ for (const dir of DIRECTORY_STRUCTURE) {
31
+ const dirPath = path.join(projectPath, dir);
32
+ await mkdir(dirPath, { recursive: true });
33
+ }
34
+ // Create initial config
35
+ const config = {
36
+ project: {
37
+ name: projectName,
38
+ created: new Date().toISOString(),
39
+ type: projectType,
40
+ },
41
+ repos: [],
42
+ };
43
+ await saveConfig(projectPath, config);
44
+ // Create initial files
45
+ await createInitialFiles(projectPath, projectName);
46
+ // Generate Claude commands for planning hub
47
+ await generatePlanningHubCommands(projectPath);
48
+ await ensureCustomCommandsDir(projectPath);
49
+ // Copy vibe-runner.py to .claude/scripts/
50
+ await createRunnerScript(projectPath);
51
+ }
52
+ /**
53
+ * Create initial files for the planning hub
54
+ */
55
+ async function createInitialFiles(projectPath, projectName) {
56
+ // CLAUDE.md - Project context for AI
57
+ const claudeMd = `# ${projectName}
58
+
59
+ > Project context for Claude Code
60
+
61
+ ## Overview
62
+
63
+ This is a vibe-fabric planning hub for coordinating development across repositories.
64
+
65
+ ## Structure
66
+
67
+ - \`docs/prd/\` - Product Requirements Documents
68
+ - \`docs/scopes/\` - Scope briefs for development
69
+ - \`docs/sync-cache/\` - Synced data from repositories
70
+ - \`.claude/\` - Claude Code commands and scripts
71
+
72
+ ## Getting Started
73
+
74
+ 1. Add repositories: \`vibe repo add\`
75
+ 2. Create PRD: \`vibe prd\`
76
+ 3. Create scopes: \`vibe scope\`
77
+ 4. Send to repos: \`vibe send <scope-id>\`
78
+ 5. Sync progress: \`vibe sync\`
79
+
80
+ ## Commands
81
+
82
+ Run \`vibe --help\` for available commands.
83
+ `;
84
+ // .gitignore for the planning hub
85
+ const gitignore = `# Environment
86
+ .env
87
+ .env.local
88
+
89
+ # Claude runner artifacts
90
+ .claude/vibe-state.json
91
+ .claude/outputs/
92
+ .claude/temp-prompt.md
93
+
94
+ # Sync cache (large, can be regenerated)
95
+ docs/sync-cache/*/maps/
96
+
97
+ # OS files
98
+ .DS_Store
99
+ Thumbs.db
100
+ `;
101
+ // PRD index
102
+ const prdIndex = `# ${projectName} - Product Requirements
103
+
104
+ > Living PRD managed by vibe-fabric
105
+
106
+ ## Status
107
+
108
+ | Metric | Value |
109
+ |--------|-------|
110
+ | Modules | 0 |
111
+ | Requirements | 0 |
112
+ | Coverage | 0% |
113
+
114
+ ## Modules
115
+
116
+ _No modules yet. Run \`vibe prd\` to create requirements._
117
+
118
+ ---
119
+
120
+ *Last updated: ${new Date().toISOString().split('T')[0]}*
121
+ `;
122
+ // Write files
123
+ await writeFile(path.join(projectPath, 'CLAUDE.md'), claudeMd, 'utf-8');
124
+ await writeFile(path.join(projectPath, '.gitignore'), gitignore, 'utf-8');
125
+ await writeFile(path.join(projectPath, 'docs/prd/_index.md'), prdIndex, 'utf-8');
126
+ }
127
+ /**
128
+ * Check if a path is inside an existing vibe-fabric project
129
+ */
130
+ export function isInsideVibeProject(dirPath) {
131
+ let currentPath = dirPath;
132
+ while (currentPath !== path.dirname(currentPath)) {
133
+ if (existsSync(path.join(currentPath, 'vibe-fabric.toml'))) {
134
+ return true;
135
+ }
136
+ currentPath = path.dirname(currentPath);
137
+ }
138
+ return false;
139
+ }
140
+ /**
141
+ * Get the root of the current vibe-fabric project
142
+ */
143
+ export function findProjectRoot(startPath) {
144
+ let currentPath = startPath;
145
+ while (currentPath !== path.dirname(currentPath)) {
146
+ if (existsSync(path.join(currentPath, 'vibe-fabric.toml'))) {
147
+ return currentPath;
148
+ }
149
+ currentPath = path.dirname(currentPath);
150
+ }
151
+ return null;
152
+ }
153
+ /**
154
+ * Create the vibe-runner.py script in .claude/scripts/
155
+ */
156
+ async function createRunnerScript(projectPath) {
157
+ const runnerScript = `#!/usr/bin/env python3
158
+ # /// script
159
+ # requires-python = ">=3.11"
160
+ # dependencies = [
161
+ # "rich>=13.0.0",
162
+ # "pydantic>=2.0.0",
163
+ # ]
164
+ # ///
165
+ """
166
+ Unified vibe-fabric runner for AI operations.
167
+
168
+ This script orchestrates multi-session Claude interactions using the
169
+ Assess → Break → Execute → Approve pattern.
170
+
171
+ Usage:
172
+ uv run vibe-runner.py --operation prd --project /path/to/project
173
+ uv run vibe-runner.py --operation prd --project /path/to/project --loop
174
+ uv run vibe-runner.py --resume
175
+ uv run vibe-runner.py --cancel
176
+ """
177
+
178
+ import argparse
179
+ import json
180
+ import os
181
+ import shutil
182
+ import signal
183
+ import subprocess
184
+ import sys
185
+ from datetime import datetime, timezone
186
+ from pathlib import Path
187
+ from typing import Any, Optional
188
+
189
+ from pydantic import BaseModel, Field
190
+ from rich.console import Console
191
+ from rich.panel import Panel
192
+ from rich.progress import Progress, SpinnerColumn, TextColumn
193
+ from rich.table import Table
194
+
195
+ console = Console()
196
+
197
+ # Constants
198
+ STATE_FILE = ".claude/vibe-state.json"
199
+ OUTPUT_DIR = ".claude/outputs"
200
+ PROMPTS_DIR = ".claude/prompts"
201
+ TEMP_PROMPT_FILE = ".claude/temp-prompt.md"
202
+
203
+
204
+ class Task(BaseModel):
205
+ """Represents a single task in the operation."""
206
+
207
+ id: int
208
+ name: str
209
+ description: str
210
+ prompt: str
211
+ interactive: bool = False
212
+ context: dict[str, Any] = Field(default_factory=dict)
213
+ status: str = "pending" # pending, in_progress, complete, failed
214
+
215
+
216
+ class OperationState(BaseModel):
217
+ """Persistent state for an operation."""
218
+
219
+ operation: str
220
+ project_path: str
221
+ phase: str = "assess" # assess, execute, approve, complete
222
+ total_tasks: int = 0
223
+ current_task: int = 0
224
+ tasks: list[Task] = Field(default_factory=list)
225
+ outputs: list[str] = Field(default_factory=list)
226
+ discovery_output: dict[str, Any] = Field(default_factory=dict)
227
+ started_at: str = ""
228
+ updated_at: str = ""
229
+
230
+
231
+ def get_timestamp() -> str:
232
+ """Get current UTC timestamp in ISO format."""
233
+ return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
234
+
235
+
236
+ def save_state(state: OperationState) -> None:
237
+ """Save operation state to file."""
238
+ state.updated_at = get_timestamp()
239
+ state_path = Path(state.project_path) / STATE_FILE
240
+ state_path.parent.mkdir(parents=True, exist_ok=True)
241
+ state_path.write_text(state.model_dump_json(indent=2))
242
+
243
+
244
+ def load_state(project_path: str) -> Optional[OperationState]:
245
+ """Load operation state from file."""
246
+ state_path = Path(project_path) / STATE_FILE
247
+ try:
248
+ content = state_path.read_text()
249
+ return OperationState.model_validate_json(content)
250
+ except FileNotFoundError:
251
+ return None
252
+ except Exception as e:
253
+ console.print(f"[red]Error loading state: {e}[/red]")
254
+ return None
255
+
256
+
257
+ def delete_state(project_path: str) -> None:
258
+ """Delete state file."""
259
+ state_path = Path(project_path) / STATE_FILE
260
+ state_path.unlink(missing_ok=True)
261
+
262
+
263
+ def clear_claude_cache() -> None:
264
+ """Clear Claude cache for fresh context.
265
+
266
+ This ensures each task starts with a clean context.
267
+ The cache location varies by platform.
268
+ """
269
+ # Common Claude cache locations
270
+ cache_dirs = [
271
+ Path.home() / ".claude" / "cache",
272
+ Path.home() / ".cache" / "claude",
273
+ ]
274
+
275
+ for cache_dir in cache_dirs:
276
+ if cache_dir.exists():
277
+ try:
278
+ shutil.rmtree(cache_dir)
279
+ except Exception:
280
+ pass # Ignore errors - cache clearing is best effort
281
+
282
+
283
+ def parse_prompt_variables(template: str) -> list[dict[str, Any]]:
284
+ """Parse the Variables table from a prompt template.
285
+
286
+ Returns list of dicts with keys: name, description, required
287
+ """
288
+ import re
289
+
290
+ variables = []
291
+
292
+ # Find the Variables section table
293
+ variables_match = re.search(
294
+ r'## Variables\\s*\\n\\n\\|[^\\n]+\\|\\s*\\n\\|[-|\\s]+\\|\\s*\\n((?:\\|[^\\n]+\\|\\s*\\n?)+)',
295
+ template
296
+ )
297
+ if not variables_match:
298
+ return variables
299
+
300
+ table_rows = variables_match.group(1).strip().split('\\n')
301
+
302
+ for row in table_rows:
303
+ # Parse table row: | {VAR} | Description | Required |
304
+ cells = [c.strip() for c in row.split('|') if c.strip()]
305
+ if len(cells) >= 2:
306
+ var_match = re.search(r'\\{([A-Z][A-Z0-9_]*)\\}', cells[0])
307
+ if var_match:
308
+ variables.append({
309
+ 'name': var_match.group(1),
310
+ 'description': cells[1],
311
+ 'required': cells[2].lower() == 'yes' if len(cells) > 2 else True
312
+ })
313
+
314
+ return variables
315
+
316
+
317
+ def extract_template_variables(template: str) -> set[str]:
318
+ """Extract all variable references from a template."""
319
+ import re
320
+ return set(re.findall(r'\\{([A-Z][A-Z0-9_]*)\\}', template))
321
+
322
+
323
+ def validate_variables(template: str, variables: dict[str, str]) -> tuple[bool, list[str]]:
324
+ """Validate that all required variables are provided.
325
+
326
+ Returns (valid, list of error messages).
327
+ """
328
+ errors = []
329
+
330
+ # Parse declared variables
331
+ declared_vars = parse_prompt_variables(template)
332
+ provided_keys = set(variables.keys())
333
+
334
+ # Check required variables
335
+ for declared in declared_vars:
336
+ if declared['required'] and declared['name'] not in provided_keys:
337
+ errors.append(f"Missing required variable: {{{declared['name']}}}")
338
+
339
+ return len(errors) == 0, errors
340
+
341
+
342
+ def check_unsubstituted_variables(content: str) -> list[str]:
343
+ """Check for any remaining unsubstituted variables in content."""
344
+ return list(extract_template_variables(content))
345
+
346
+
347
+ def fill_prompt_template(template_path: Path, variables: dict[str, str]) -> str:
348
+ """Fill a prompt template with variables.
349
+
350
+ Raises ValueError if required variables are missing or unsubstituted.
351
+ """
352
+ if not template_path.exists():
353
+ raise FileNotFoundError(f"Prompt template not found: {template_path}")
354
+
355
+ template = template_path.read_text()
356
+
357
+ # Validate required variables before substitution
358
+ valid, errors = validate_variables(template, variables)
359
+ if not valid:
360
+ error_msg = "Variable validation failed:\\n " + "\\n ".join(errors)
361
+ raise ValueError(error_msg)
362
+
363
+ # Replace {VARIABLE} patterns
364
+ result = template
365
+ for key, value in variables.items():
366
+ result = result.replace(f"{{{key}}}", str(value))
367
+
368
+ # Check for unsubstituted variables (fail fast)
369
+ remaining = check_unsubstituted_variables(result)
370
+ if remaining:
371
+ raise ValueError(f"Unsubstituted variables in template: {remaining}")
372
+
373
+ return result
374
+
375
+
376
+ def spawn_claude_session(
377
+ project_path: str,
378
+ prompt_content: str,
379
+ interactive: bool = True,
380
+ ) -> bool:
381
+ """Spawn a Claude Code session with the given prompt.
382
+
383
+ Returns True if session completed successfully.
384
+ """
385
+ # Write prompt to temp file
386
+ temp_prompt = Path(project_path) / TEMP_PROMPT_FILE
387
+ temp_prompt.parent.mkdir(parents=True, exist_ok=True)
388
+ temp_prompt.write_text(prompt_content)
389
+
390
+ try:
391
+ # Spawn Claude with the prompt
392
+ # Using --print for non-interactive mode, regular for interactive
393
+ cmd = ["claude"]
394
+ if not interactive:
395
+ cmd.extend(["--print"])
396
+ cmd.extend(["--prompt", str(temp_prompt)])
397
+
398
+ result = subprocess.run(
399
+ cmd,
400
+ cwd=project_path,
401
+ check=False,
402
+ )
403
+
404
+ return result.returncode == 0
405
+
406
+ except FileNotFoundError:
407
+ console.print(
408
+ "[red]Error: Claude Code not found. Please install Claude Code first.[/red]"
409
+ )
410
+ console.print(
411
+ "[dim]Visit: https://claude.ai/code[/dim]"
412
+ )
413
+ return False
414
+
415
+ except Exception as e:
416
+ console.print(f"[red]Error spawning Claude: {e}[/red]")
417
+ return False
418
+
419
+ finally:
420
+ # Clean up temp prompt file
421
+ temp_prompt.unlink(missing_ok=True)
422
+
423
+
424
+ def run_assessment(state: OperationState) -> bool:
425
+ """Run the assessment/discovery phase.
426
+
427
+ This phase:
428
+ 1. Analyzes current state (PRD, maps, scopes)
429
+ 2. Identifies issues and opportunities
430
+ 3. Asks user about their intent
431
+ 4. Creates a task list
432
+
433
+ Returns True if successful.
434
+ """
435
+ console.print()
436
+ console.print(
437
+ Panel(
438
+ "[bold blue]Discovery Phase[/bold blue]\\n"
439
+ "Analyzing PRD state and determining tasks...",
440
+ title="Phase 1/3",
441
+ border_style="blue",
442
+ )
443
+ )
444
+ console.print()
445
+
446
+ prompt_path = (
447
+ Path(state.project_path) / PROMPTS_DIR / state.operation / "assess.md"
448
+ )
449
+ output_file = Path(state.project_path) / OUTPUT_DIR / f"{state.operation}-assess.json"
450
+ output_file.parent.mkdir(parents=True, exist_ok=True)
451
+
452
+ # Fill template
453
+ try:
454
+ prompt_content = fill_prompt_template(
455
+ prompt_path,
456
+ {
457
+ "PROJECT_PATH": state.project_path,
458
+ "OUTPUT_FILE": str(output_file),
459
+ },
460
+ )
461
+ except FileNotFoundError:
462
+ console.print(f"[red]Prompt template not found: {prompt_path}[/red]")
463
+ console.print(
464
+ "[dim]Run 'vibe init' to set up prompt templates.[/dim]"
465
+ )
466
+ return False
467
+
468
+ # Clear cache and run Claude
469
+ clear_claude_cache()
470
+ success = spawn_claude_session(state.project_path, prompt_content, interactive=True)
471
+
472
+ if not success:
473
+ console.print("[red]Discovery phase failed.[/red]")
474
+ return False
475
+
476
+ # Load tasks from assessment output
477
+ if output_file.exists():
478
+ try:
479
+ result = json.loads(output_file.read_text())
480
+ state.tasks = [Task(**t) for t in result.get("tasks", [])]
481
+ state.total_tasks = len(state.tasks)
482
+ state.discovery_output = result
483
+ console.print(
484
+ f"[green]Created {state.total_tasks} task(s) to execute.[/green]"
485
+ )
486
+ except Exception as e:
487
+ console.print(f"[yellow]Warning: Could not parse assessment output: {e}[/yellow]")
488
+ # Continue even if parsing fails - Claude may have handled it differently
489
+
490
+ state.phase = "execute"
491
+ save_state(state)
492
+ return True
493
+
494
+
495
+ def run_task(state: OperationState, task: Task) -> bool:
496
+ """Execute a single task.
497
+
498
+ Returns True if successful.
499
+ """
500
+ console.print()
501
+ console.print(
502
+ f"[bold]Task {task.id}/{state.total_tasks}:[/bold] {task.description}"
503
+ )
504
+
505
+ if task.interactive:
506
+ console.print("[dim]This task requires your input.[/dim]")
507
+ else:
508
+ console.print("[dim]Running silently...[/dim]")
509
+
510
+ prompt_path = (
511
+ Path(state.project_path) / PROMPTS_DIR / state.operation / f"{task.prompt}.md"
512
+ )
513
+ output_file = (
514
+ Path(state.project_path) / OUTPUT_DIR / f"{state.operation}-task-{task.id:02d}.json"
515
+ )
516
+
517
+ # Build variables for template
518
+ variables = {
519
+ "PROJECT_PATH": state.project_path,
520
+ "TASK_NAME": task.name,
521
+ "TASK_DESCRIPTION": task.description,
522
+ "OUTPUT_FILE": str(output_file),
523
+ "PREVIOUS_OUTPUTS": json.dumps(state.outputs),
524
+ }
525
+ variables.update({k.upper(): str(v) for k, v in task.context.items()})
526
+
527
+ try:
528
+ prompt_content = fill_prompt_template(prompt_path, variables)
529
+ except FileNotFoundError:
530
+ console.print(f"[red]Prompt template not found: {prompt_path}[/red]")
531
+ task.status = "failed"
532
+ save_state(state)
533
+ return False
534
+
535
+ # Clear cache and run Claude
536
+ clear_claude_cache()
537
+ task.status = "in_progress"
538
+ save_state(state)
539
+
540
+ success = spawn_claude_session(
541
+ state.project_path, prompt_content, interactive=task.interactive
542
+ )
543
+
544
+ if success:
545
+ task.status = "complete"
546
+ if output_file.exists():
547
+ state.outputs.append(str(output_file))
548
+ state.current_task = task.id
549
+ console.print(f"[green]✓ Task {task.id} complete[/green]")
550
+ else:
551
+ task.status = "failed"
552
+ console.print(f"[red]✗ Task {task.id} failed[/red]")
553
+
554
+ save_state(state)
555
+ return success
556
+
557
+
558
+ def run_approval(state: OperationState) -> bool:
559
+ """Run the approval/synthesis phase.
560
+
561
+ This phase:
562
+ 1. Loads all task outputs
563
+ 2. Presents summary of changes
564
+ 3. Gets user approval
565
+ 4. Applies changes to PRD
566
+
567
+ Returns True if successful.
568
+ """
569
+ console.print()
570
+ console.print(
571
+ Panel(
572
+ "[bold green]Approval Phase[/bold green]\\n"
573
+ "Review and approve the changes...",
574
+ title="Phase 3/3",
575
+ border_style="green",
576
+ )
577
+ )
578
+ console.print()
579
+
580
+ prompt_path = (
581
+ Path(state.project_path) / PROMPTS_DIR / state.operation / "synthesize.md"
582
+ )
583
+
584
+ # Fill template
585
+ try:
586
+ prompt_content = fill_prompt_template(
587
+ prompt_path,
588
+ {
589
+ "PROJECT_PATH": state.project_path,
590
+ "TASK_OUTPUTS": json.dumps(state.outputs),
591
+ "TOTAL_TASKS": str(state.total_tasks),
592
+ },
593
+ )
594
+ except FileNotFoundError:
595
+ console.print(f"[red]Prompt template not found: {prompt_path}[/red]")
596
+ return False
597
+
598
+ # Clear cache and run Claude
599
+ clear_claude_cache()
600
+ success = spawn_claude_session(state.project_path, prompt_content, interactive=True)
601
+
602
+ if success:
603
+ state.phase = "complete"
604
+ save_state(state)
605
+ console.print("[green]Changes applied successfully.[/green]")
606
+ else:
607
+ console.print("[red]Approval phase failed.[/red]")
608
+
609
+ return success
610
+
611
+
612
+ def show_status(state: OperationState) -> None:
613
+ """Show current operation status."""
614
+ table = Table(title="Operation Status")
615
+ table.add_column("Field", style="cyan")
616
+ table.add_column("Value")
617
+
618
+ table.add_row("Operation", state.operation)
619
+ table.add_row("Phase", state.phase)
620
+ table.add_row("Tasks", f"{state.current_task}/{state.total_tasks}")
621
+ table.add_row("Started", state.started_at)
622
+ table.add_row("Updated", state.updated_at)
623
+
624
+ console.print(table)
625
+
626
+
627
+ def main() -> int:
628
+ """Main entry point."""
629
+ parser = argparse.ArgumentParser(
630
+ description="Vibe-fabric AI runner",
631
+ formatter_class=argparse.RawDescriptionHelpFormatter,
632
+ )
633
+ parser.add_argument(
634
+ "--operation",
635
+ choices=["prd", "scope", "gaps", "analyze"],
636
+ help="Operation to run",
637
+ )
638
+ parser.add_argument(
639
+ "--project",
640
+ help="Path to project directory",
641
+ )
642
+ parser.add_argument(
643
+ "--resume",
644
+ action="store_true",
645
+ help="Resume interrupted operation",
646
+ )
647
+ parser.add_argument(
648
+ "--loop",
649
+ action="store_true",
650
+ help="Run all phases automatically",
651
+ )
652
+ parser.add_argument(
653
+ "--cancel",
654
+ action="store_true",
655
+ help="Cancel current operation",
656
+ )
657
+ parser.add_argument(
658
+ "--status",
659
+ action="store_true",
660
+ help="Show current operation status",
661
+ )
662
+ args = parser.parse_args()
663
+
664
+ # Determine project path
665
+ project_path = args.project or os.getcwd()
666
+
667
+ # Handle graceful shutdown
668
+ state: Optional[OperationState] = None
669
+
670
+ def handle_signal(sig: int, frame: Any) -> None:
671
+ console.print("\\n[yellow]Interrupted. State saved for resume.[/yellow]")
672
+ if state:
673
+ save_state(state)
674
+ sys.exit(130)
675
+
676
+ signal.signal(signal.SIGINT, handle_signal)
677
+ signal.signal(signal.SIGTERM, handle_signal)
678
+
679
+ # Handle cancel
680
+ if args.cancel:
681
+ delete_state(project_path)
682
+ console.print("[green]Operation cancelled. State cleared.[/green]")
683
+ return 0
684
+
685
+ # Handle status
686
+ if args.status:
687
+ state = load_state(project_path)
688
+ if state:
689
+ show_status(state)
690
+ else:
691
+ console.print("[yellow]No operation in progress.[/yellow]")
692
+ return 0
693
+
694
+ # Load or create state
695
+ if args.resume:
696
+ state = load_state(project_path)
697
+ if not state:
698
+ console.print("[red]Error: No saved state to resume.[/red]")
699
+ console.print("[dim]Start a new operation with --operation[/dim]")
700
+ return 1
701
+ console.print(f"[green]Resuming {state.operation} operation from {state.phase} phase...[/green]")
702
+ else:
703
+ if not args.operation:
704
+ parser.error("--operation is required unless using --resume, --cancel, or --status")
705
+
706
+ # Check for existing operation
707
+ existing = load_state(project_path)
708
+ if existing and existing.phase != "complete":
709
+ console.print(
710
+ f"[yellow]Operation '{existing.operation}' is already in progress.[/yellow]"
711
+ )
712
+ console.print()
713
+ console.print("Options:")
714
+ console.print(" --resume Continue the existing operation")
715
+ console.print(" --cancel Cancel and start fresh")
716
+ return 1
717
+
718
+ state = OperationState(
719
+ operation=args.operation,
720
+ project_path=project_path,
721
+ phase="assess",
722
+ started_at=get_timestamp(),
723
+ updated_at=get_timestamp(),
724
+ )
725
+ save_state(state)
726
+
727
+ # Ensure output directory exists
728
+ output_dir = Path(project_path) / OUTPUT_DIR
729
+ output_dir.mkdir(parents=True, exist_ok=True)
730
+
731
+ # Run phases
732
+ try:
733
+ # Assessment phase
734
+ if state.phase == "assess":
735
+ success = run_assessment(state)
736
+ if not success:
737
+ return 1
738
+ if not args.loop:
739
+ console.print()
740
+ console.print("[dim]Run with --loop to continue automatically,[/dim]")
741
+ console.print("[dim]or run again with --resume to continue.[/dim]")
742
+ return 0
743
+
744
+ # Execution phase
745
+ if state.phase == "execute":
746
+ console.print()
747
+ console.print(
748
+ Panel(
749
+ "[bold yellow]Execution Phase[/bold yellow]\\n"
750
+ f"Running {state.total_tasks} task(s)...",
751
+ title="Phase 2/3",
752
+ border_style="yellow",
753
+ )
754
+ )
755
+
756
+ for task in state.tasks:
757
+ if task.status in ("complete", "failed"):
758
+ continue # Skip completed or failed tasks
759
+
760
+ success = run_task(state, task)
761
+ if not success and not args.loop:
762
+ console.print(
763
+ "[dim]Task failed. Fix issues and run with --resume.[/dim]"
764
+ )
765
+ return 1
766
+
767
+ if not args.loop:
768
+ console.print()
769
+ console.print("[dim]Run again with --resume to continue.[/dim]")
770
+ return 0
771
+
772
+ state.phase = "approve"
773
+ save_state(state)
774
+
775
+ # Approval phase
776
+ if state.phase == "approve":
777
+ success = run_approval(state)
778
+ if not success:
779
+ return 1
780
+
781
+ # Complete
782
+ if state.phase == "complete":
783
+ delete_state(project_path)
784
+ console.print()
785
+ console.print(
786
+ Panel(
787
+ "[bold green]Operation Complete![/bold green]\\n"
788
+ "All phases finished successfully.",
789
+ border_style="green",
790
+ )
791
+ )
792
+ return 0
793
+
794
+ except Exception as e:
795
+ console.print(f"[red]Error: {e}[/red]")
796
+ if state:
797
+ save_state(state)
798
+ console.print("[dim]State saved. Run with --resume to retry.[/dim]")
799
+ return 1
800
+
801
+ return 0
802
+
803
+
804
+ if __name__ == "__main__":
805
+ sys.exit(main())
806
+ `;
807
+ const scriptPath = path.join(projectPath, '.claude', 'scripts', 'vibe-runner.py');
808
+ await writeFile(scriptPath, runnerScript, 'utf-8');
809
+ }
810
+ //# sourceMappingURL=project.js.map