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.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/cli/commands/claude.d.ts +19 -0
- package/dist/cli/commands/claude.d.ts.map +1 -0
- package/dist/cli/commands/claude.js +107 -0
- package/dist/cli/commands/claude.js.map +1 -0
- package/dist/cli/commands/coverage.d.ts +37 -0
- package/dist/cli/commands/coverage.d.ts.map +1 -0
- package/dist/cli/commands/coverage.js +374 -0
- package/dist/cli/commands/coverage.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +30 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +187 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/gaps.d.ts +52 -0
- package/dist/cli/commands/gaps.d.ts.map +1 -0
- package/dist/cli/commands/gaps.js +487 -0
- package/dist/cli/commands/gaps.js.map +1 -0
- package/dist/cli/commands/help.d.ts +7 -0
- package/dist/cli/commands/help.d.ts.map +1 -0
- package/dist/cli/commands/help.js +51 -0
- package/dist/cli/commands/help.js.map +1 -0
- package/dist/cli/commands/init.d.ts +39 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +246 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/prd.d.ts +30 -0
- package/dist/cli/commands/prd.d.ts.map +1 -0
- package/dist/cli/commands/prd.js +179 -0
- package/dist/cli/commands/prd.js.map +1 -0
- package/dist/cli/commands/repo/add.d.ts +36 -0
- package/dist/cli/commands/repo/add.d.ts.map +1 -0
- package/dist/cli/commands/repo/add.js +303 -0
- package/dist/cli/commands/repo/add.js.map +1 -0
- package/dist/cli/commands/scope.d.ts +36 -0
- package/dist/cli/commands/scope.d.ts.map +1 -0
- package/dist/cli/commands/scope.js +312 -0
- package/dist/cli/commands/scope.js.map +1 -0
- package/dist/cli/commands/send.d.ts +43 -0
- package/dist/cli/commands/send.d.ts.map +1 -0
- package/dist/cli/commands/send.js +469 -0
- package/dist/cli/commands/send.js.map +1 -0
- package/dist/cli/commands/status.d.ts +32 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +422 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +37 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +299 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/commands/version.d.ts +7 -0
- package/dist/cli/commands/version.d.ts.map +1 -0
- package/dist/cli/commands/version.js +45 -0
- package/dist/cli/commands/version.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +65 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/ui/components/ActiveScopes.d.ts +13 -0
- package/dist/cli/ui/components/ActiveScopes.d.ts.map +1 -0
- package/dist/cli/ui/components/ActiveScopes.js +25 -0
- package/dist/cli/ui/components/ActiveScopes.js.map +1 -0
- package/dist/cli/ui/components/EmptyState.d.ts +24 -0
- package/dist/cli/ui/components/EmptyState.d.ts.map +1 -0
- package/dist/cli/ui/components/EmptyState.js +13 -0
- package/dist/cli/ui/components/EmptyState.js.map +1 -0
- package/dist/cli/ui/components/Header.d.ts +11 -0
- package/dist/cli/ui/components/Header.d.ts.map +1 -0
- package/dist/cli/ui/components/Header.js +32 -0
- package/dist/cli/ui/components/Header.js.map +1 -0
- package/dist/cli/ui/components/PrdCoverage.d.ts +12 -0
- package/dist/cli/ui/components/PrdCoverage.d.ts.map +1 -0
- package/dist/cli/ui/components/PrdCoverage.js +15 -0
- package/dist/cli/ui/components/PrdCoverage.js.map +1 -0
- package/dist/cli/ui/components/RecentActivity.d.ts +12 -0
- package/dist/cli/ui/components/RecentActivity.d.ts.map +1 -0
- package/dist/cli/ui/components/RecentActivity.js +40 -0
- package/dist/cli/ui/components/RecentActivity.js.map +1 -0
- package/dist/cli/ui/components/RepoList.d.ts +12 -0
- package/dist/cli/ui/components/RepoList.d.ts.map +1 -0
- package/dist/cli/ui/components/RepoList.js +24 -0
- package/dist/cli/ui/components/RepoList.js.map +1 -0
- package/dist/cli/ui/components/Shortcuts.d.ts +7 -0
- package/dist/cli/ui/components/Shortcuts.d.ts.map +1 -0
- package/dist/cli/ui/components/Shortcuts.js +7 -0
- package/dist/cli/ui/components/Shortcuts.js.map +1 -0
- package/dist/cli/ui/components/index.d.ts +12 -0
- package/dist/cli/ui/components/index.d.ts.map +1 -0
- package/dist/cli/ui/components/index.js +12 -0
- package/dist/cli/ui/components/index.js.map +1 -0
- package/dist/cli/ui/dashboard.d.ts +9 -0
- package/dist/cli/ui/dashboard.d.ts.map +1 -0
- package/dist/cli/ui/dashboard.js +102 -0
- package/dist/cli/ui/dashboard.js.map +1 -0
- package/dist/core/commands.d.ts +32 -0
- package/dist/core/commands.d.ts.map +1 -0
- package/dist/core/commands.js +361 -0
- package/dist/core/commands.js.map +1 -0
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +78 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/coverage.d.ts +32 -0
- package/dist/core/coverage.d.ts.map +1 -0
- package/dist/core/coverage.js +286 -0
- package/dist/core/coverage.js.map +1 -0
- package/dist/core/dashboard/data.d.ts +73 -0
- package/dist/core/dashboard/data.d.ts.map +1 -0
- package/dist/core/dashboard/data.js +250 -0
- package/dist/core/dashboard/data.js.map +1 -0
- package/dist/core/dependencies.d.ts +39 -0
- package/dist/core/dependencies.d.ts.map +1 -0
- package/dist/core/dependencies.js +160 -0
- package/dist/core/dependencies.js.map +1 -0
- package/dist/core/doctor/auth.d.ts +22 -0
- package/dist/core/doctor/auth.d.ts.map +1 -0
- package/dist/core/doctor/auth.js +147 -0
- package/dist/core/doctor/auth.js.map +1 -0
- package/dist/core/doctor/config.d.ts +26 -0
- package/dist/core/doctor/config.d.ts.map +1 -0
- package/dist/core/doctor/config.js +172 -0
- package/dist/core/doctor/config.js.map +1 -0
- package/dist/core/doctor/environment.d.ts +26 -0
- package/dist/core/doctor/environment.d.ts.map +1 -0
- package/dist/core/doctor/environment.js +145 -0
- package/dist/core/doctor/environment.js.map +1 -0
- package/dist/core/doctor/index.d.ts +44 -0
- package/dist/core/doctor/index.d.ts.map +1 -0
- package/dist/core/doctor/index.js +134 -0
- package/dist/core/doctor/index.js.map +1 -0
- package/dist/core/doctor/repos.d.ts +22 -0
- package/dist/core/doctor/repos.d.ts.map +1 -0
- package/dist/core/doctor/repos.js +262 -0
- package/dist/core/doctor/repos.js.map +1 -0
- package/dist/core/doctor/sync.d.ts +18 -0
- package/dist/core/doctor/sync.d.ts.map +1 -0
- package/dist/core/doctor/sync.js +146 -0
- package/dist/core/doctor/sync.js.map +1 -0
- package/dist/core/gaps.d.ts +70 -0
- package/dist/core/gaps.d.ts.map +1 -0
- package/dist/core/gaps.js +448 -0
- package/dist/core/gaps.js.map +1 -0
- package/dist/core/github.d.ts +38 -0
- package/dist/core/github.d.ts.map +1 -0
- package/dist/core/github.js +102 -0
- package/dist/core/github.js.map +1 -0
- package/dist/core/prd/analyzer.d.ts +44 -0
- package/dist/core/prd/analyzer.d.ts.map +1 -0
- package/dist/core/prd/analyzer.js +259 -0
- package/dist/core/prd/analyzer.js.map +1 -0
- package/dist/core/prd/check.d.ts +17 -0
- package/dist/core/prd/check.d.ts.map +1 -0
- package/dist/core/prd/check.js +154 -0
- package/dist/core/prd/check.js.map +1 -0
- package/dist/core/prd/index.d.ts +6 -0
- package/dist/core/prd/index.d.ts.map +1 -0
- package/dist/core/prd/index.js +6 -0
- package/dist/core/prd/index.js.map +1 -0
- package/dist/core/project.d.ts +13 -0
- package/dist/core/project.d.ts.map +1 -0
- package/dist/core/project.js +810 -0
- package/dist/core/project.js.map +1 -0
- package/dist/core/prompts.d.ts +52 -0
- package/dist/core/prompts.d.ts.map +1 -0
- package/dist/core/prompts.js +266 -0
- package/dist/core/prompts.js.map +1 -0
- package/dist/core/repo/framework.d.ts +38 -0
- package/dist/core/repo/framework.d.ts.map +1 -0
- package/dist/core/repo/framework.js +142 -0
- package/dist/core/repo/framework.js.map +1 -0
- package/dist/core/repo/index.d.ts +6 -0
- package/dist/core/repo/index.d.ts.map +1 -0
- package/dist/core/repo/index.js +6 -0
- package/dist/core/repo/index.js.map +1 -0
- package/dist/core/repo/templates/claude-agents.d.ts +6 -0
- package/dist/core/repo/templates/claude-agents.d.ts.map +1 -0
- package/dist/core/repo/templates/claude-agents.js +173 -0
- package/dist/core/repo/templates/claude-agents.js.map +1 -0
- package/dist/core/repo/templates/claude-commands.d.ts +6 -0
- package/dist/core/repo/templates/claude-commands.d.ts.map +1 -0
- package/dist/core/repo/templates/claude-commands.js +278 -0
- package/dist/core/repo/templates/claude-commands.js.map +1 -0
- package/dist/core/repo/templates/claude-prompts.d.ts +6 -0
- package/dist/core/repo/templates/claude-prompts.d.ts.map +1 -0
- package/dist/core/repo/templates/claude-prompts.js +258 -0
- package/dist/core/repo/templates/claude-prompts.js.map +1 -0
- package/dist/core/repo/templates/claude-scripts.d.ts +6 -0
- package/dist/core/repo/templates/claude-scripts.d.ts.map +1 -0
- package/dist/core/repo/templates/claude-scripts.js +212 -0
- package/dist/core/repo/templates/claude-scripts.js.map +1 -0
- package/dist/core/repo/templates/index.d.ts +22 -0
- package/dist/core/repo/templates/index.d.ts.map +1 -0
- package/dist/core/repo/templates/index.js +121 -0
- package/dist/core/repo/templates/index.js.map +1 -0
- package/dist/core/repo/templates/vibe-readme.d.ts +6 -0
- package/dist/core/repo/templates/vibe-readme.d.ts.map +1 -0
- package/dist/core/repo/templates/vibe-readme.js +204 -0
- package/dist/core/repo/templates/vibe-readme.js.map +1 -0
- package/dist/core/repo/templates/vibe-scripts.d.ts +6 -0
- package/dist/core/repo/templates/vibe-scripts.d.ts.map +1 -0
- package/dist/core/repo/templates/vibe-scripts.js +308 -0
- package/dist/core/repo/templates/vibe-scripts.js.map +1 -0
- package/dist/core/repo/validation.d.ts +46 -0
- package/dist/core/repo/validation.d.ts.map +1 -0
- package/dist/core/repo/validation.js +154 -0
- package/dist/core/repo/validation.js.map +1 -0
- package/dist/core/runner.d.ts +38 -0
- package/dist/core/runner.d.ts.map +1 -0
- package/dist/core/runner.js +124 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/core/send.d.ts +83 -0
- package/dist/core/send.d.ts.map +1 -0
- package/dist/core/send.js +565 -0
- package/dist/core/send.js.map +1 -0
- package/dist/core/status.d.ts +76 -0
- package/dist/core/status.d.ts.map +1 -0
- package/dist/core/status.js +430 -0
- package/dist/core/status.js.map +1 -0
- package/dist/core/sync/aggregator.d.ts +22 -0
- package/dist/core/sync/aggregator.d.ts.map +1 -0
- package/dist/core/sync/aggregator.js +278 -0
- package/dist/core/sync/aggregator.js.map +1 -0
- package/dist/core/sync/completion.d.ts +37 -0
- package/dist/core/sync/completion.d.ts.map +1 -0
- package/dist/core/sync/completion.js +264 -0
- package/dist/core/sync/completion.js.map +1 -0
- package/dist/core/sync/index.d.ts +51 -0
- package/dist/core/sync/index.d.ts.map +1 -0
- package/dist/core/sync/index.js +200 -0
- package/dist/core/sync/index.js.map +1 -0
- package/dist/core/sync/scanner.d.ts +39 -0
- package/dist/core/sync/scanner.d.ts.map +1 -0
- package/dist/core/sync/scanner.js +364 -0
- package/dist/core/sync/scanner.js.map +1 -0
- package/dist/types/config.d.ts +157 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +58 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/coverage.d.ts +100 -0
- package/dist/types/coverage.d.ts.map +1 -0
- package/dist/types/coverage.js +8 -0
- package/dist/types/coverage.js.map +1 -0
- package/dist/types/doctor.d.ts +68 -0
- package/dist/types/doctor.d.ts.map +1 -0
- package/dist/types/doctor.js +5 -0
- package/dist/types/doctor.js.map +1 -0
- package/dist/types/gaps.d.ts +129 -0
- package/dist/types/gaps.d.ts.map +1 -0
- package/dist/types/gaps.js +8 -0
- package/dist/types/gaps.js.map +1 -0
- package/dist/types/prompts.d.ts +99 -0
- package/dist/types/prompts.d.ts.map +1 -0
- package/dist/types/prompts.js +5 -0
- package/dist/types/prompts.js.map +1 -0
- package/dist/types/runner.d.ts +156 -0
- package/dist/types/runner.d.ts.map +1 -0
- package/dist/types/runner.js +41 -0
- package/dist/types/runner.js.map +1 -0
- package/dist/types/send.d.ts +157 -0
- package/dist/types/send.d.ts.map +1 -0
- package/dist/types/send.js +18 -0
- package/dist/types/send.js.map +1 -0
- package/dist/types/status.d.ts +150 -0
- package/dist/types/status.d.ts.map +1 -0
- package/dist/types/status.js +15 -0
- package/dist/types/status.js.map +1 -0
- package/dist/types/sync.d.ts +259 -0
- package/dist/types/sync.d.ts.map +1 -0
- package/dist/types/sync.js +38 -0
- package/dist/types/sync.js.map +1 -0
- 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
|