wogiflow 1.0.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/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- package/templates/trace.md +69 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Git Worktree Isolation Module
|
|
5
|
+
*
|
|
6
|
+
* Provides safe task execution by running work in isolated git worktrees.
|
|
7
|
+
* Inspired by Auto-Claude's approach, but available for ALL modes.
|
|
8
|
+
*
|
|
9
|
+
* Benefits:
|
|
10
|
+
* - Parallel execution without conflicts
|
|
11
|
+
* - Safe rollback on failure
|
|
12
|
+
* - Clean branch history
|
|
13
|
+
* - No pollution of main working directory
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* const { createWorktree, commitAndMerge, discardWorktree } = require('./flow-worktree');
|
|
17
|
+
*
|
|
18
|
+
* const worktree = await createWorktree({ taskId: 'TASK-123', baseBranch: 'main' });
|
|
19
|
+
* // ... do work in worktree.path ...
|
|
20
|
+
* await commitAndMerge(worktree, 'feat: implement feature');
|
|
21
|
+
* // OR on failure:
|
|
22
|
+
* await discardWorktree(worktree);
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const { execFileSync, spawn } = require('child_process');
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
const { sanitizeCommitMessage } = require('./flow-security');
|
|
30
|
+
|
|
31
|
+
// ============================================================
|
|
32
|
+
// Configuration
|
|
33
|
+
// ============================================================
|
|
34
|
+
|
|
35
|
+
const WORKTREE_PREFIX = 'wogi-task-';
|
|
36
|
+
const WORKTREE_BASE_DIR = path.join(os.tmpdir(), 'wogi-worktrees');
|
|
37
|
+
|
|
38
|
+
// ============================================================
|
|
39
|
+
// Helper Functions
|
|
40
|
+
// ============================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Execute a git command safely and return the output.
|
|
44
|
+
* Uses execFileSync with array arguments to prevent shell injection.
|
|
45
|
+
*
|
|
46
|
+
* @param {string[]} args - Git arguments as an array (e.g., ['status', '--porcelain'])
|
|
47
|
+
* @param {Object} options - Execution options
|
|
48
|
+
* @returns {string|null} Command output or null on silent failure
|
|
49
|
+
*/
|
|
50
|
+
function git(args, options = {}) {
|
|
51
|
+
const { cwd = process.cwd(), silent = false } = options;
|
|
52
|
+
try {
|
|
53
|
+
const result = execFileSync('git', args, {
|
|
54
|
+
cwd,
|
|
55
|
+
encoding: 'utf-8',
|
|
56
|
+
stdio: silent ? 'pipe' : ['pipe', 'pipe', 'pipe']
|
|
57
|
+
});
|
|
58
|
+
return result.trim();
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (!silent) {
|
|
61
|
+
throw new Error(`Git command failed: git ${args.join(' ')}\n${error.stderr || error.message}`);
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if git is available and we're in a repo
|
|
69
|
+
*/
|
|
70
|
+
function isGitRepo(cwd = process.cwd()) {
|
|
71
|
+
try {
|
|
72
|
+
execFileSync('git', ['rev-parse', '--git-dir'], { cwd, stdio: 'pipe' });
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the current branch name
|
|
81
|
+
*/
|
|
82
|
+
function getCurrentBranch(cwd = process.cwd()) {
|
|
83
|
+
return git(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, silent: true }) || 'main';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get the root of the git repository
|
|
88
|
+
*/
|
|
89
|
+
function getRepoRoot(cwd = process.cwd()) {
|
|
90
|
+
return git(['rev-parse', '--show-toplevel'], { cwd, silent: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate a unique worktree branch name
|
|
95
|
+
*/
|
|
96
|
+
function generateBranchName(taskId, timestamp = Date.now()) {
|
|
97
|
+
const sanitizedTaskId = taskId
|
|
98
|
+
.replace(/[^a-zA-Z0-9-]/g, '-')
|
|
99
|
+
.toLowerCase();
|
|
100
|
+
return `${WORKTREE_PREFIX}${sanitizedTaskId}-${timestamp}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Generate the worktree path
|
|
105
|
+
*/
|
|
106
|
+
function generateWorktreePath(branchName) {
|
|
107
|
+
// Ensure base directory exists
|
|
108
|
+
if (!fs.existsSync(WORKTREE_BASE_DIR)) {
|
|
109
|
+
fs.mkdirSync(WORKTREE_BASE_DIR, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
return path.join(WORKTREE_BASE_DIR, branchName);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================
|
|
115
|
+
// Core Functions
|
|
116
|
+
// ============================================================
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Create an isolated worktree for a task
|
|
120
|
+
*
|
|
121
|
+
* @param {Object} options
|
|
122
|
+
* @param {string} options.taskId - Task identifier (e.g., 'TASK-123')
|
|
123
|
+
* @param {string} [options.baseBranch] - Branch to base work on (default: current branch)
|
|
124
|
+
* @param {string} [options.repoRoot] - Repository root (default: auto-detect)
|
|
125
|
+
* @returns {Object} Worktree info { path, branchName, baseBranch, repoRoot }
|
|
126
|
+
*/
|
|
127
|
+
async function createWorktree(options = {}) {
|
|
128
|
+
const {
|
|
129
|
+
taskId = 'unnamed-task',
|
|
130
|
+
baseBranch,
|
|
131
|
+
repoRoot: providedRoot
|
|
132
|
+
} = options;
|
|
133
|
+
|
|
134
|
+
// Validate git repo
|
|
135
|
+
const repoRoot = providedRoot || getRepoRoot();
|
|
136
|
+
if (!repoRoot) {
|
|
137
|
+
throw new Error('Not in a git repository');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Determine base branch
|
|
141
|
+
const base = baseBranch || getCurrentBranch(repoRoot);
|
|
142
|
+
|
|
143
|
+
// Generate unique branch and path
|
|
144
|
+
const branchName = generateBranchName(taskId);
|
|
145
|
+
const worktreePath = generateWorktreePath(branchName);
|
|
146
|
+
|
|
147
|
+
// Clean up if path already exists (shouldn't happen, but be safe)
|
|
148
|
+
if (fs.existsSync(worktreePath)) {
|
|
149
|
+
fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Create the worktree with a new branch
|
|
153
|
+
try {
|
|
154
|
+
git(['worktree', 'add', '-b', branchName, worktreePath, base], { cwd: repoRoot });
|
|
155
|
+
} catch (error) {
|
|
156
|
+
throw new Error(`Failed to create worktree: ${error.message}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const worktreeInfo = {
|
|
160
|
+
path: worktreePath,
|
|
161
|
+
branchName,
|
|
162
|
+
baseBranch: base,
|
|
163
|
+
repoRoot,
|
|
164
|
+
taskId,
|
|
165
|
+
createdAt: new Date().toISOString()
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Save worktree info for recovery
|
|
169
|
+
const infoPath = path.join(worktreePath, '.wogi-worktree.json');
|
|
170
|
+
fs.writeFileSync(infoPath, JSON.stringify(worktreeInfo, null, 2));
|
|
171
|
+
|
|
172
|
+
return worktreeInfo;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Commit changes in the worktree and merge back to base branch
|
|
177
|
+
*
|
|
178
|
+
* @param {Object} worktree - Worktree info from createWorktree
|
|
179
|
+
* @param {string} commitMessage - Commit message
|
|
180
|
+
* @param {Object} [options]
|
|
181
|
+
* @param {boolean} [options.push] - Push after merge (default: false)
|
|
182
|
+
* @param {boolean} [options.squash] - Squash commits on merge (default: true)
|
|
183
|
+
* @param {boolean} [options.cleanup] - Remove worktree after merge (default: true)
|
|
184
|
+
*/
|
|
185
|
+
async function commitAndMerge(worktree, commitMessage, options = {}) {
|
|
186
|
+
const {
|
|
187
|
+
push = false,
|
|
188
|
+
squash = true,
|
|
189
|
+
cleanup = true
|
|
190
|
+
} = options;
|
|
191
|
+
|
|
192
|
+
const { path: worktreePath, branchName, baseBranch, repoRoot } = worktree;
|
|
193
|
+
|
|
194
|
+
// Check for changes
|
|
195
|
+
const status = git(['status', '--porcelain'], { cwd: worktreePath, silent: true });
|
|
196
|
+
if (!status) {
|
|
197
|
+
// No changes to commit, just cleanup
|
|
198
|
+
if (cleanup) {
|
|
199
|
+
await discardWorktree(worktree);
|
|
200
|
+
}
|
|
201
|
+
return { merged: false, reason: 'no-changes' };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Sanitize commit message to prevent injection
|
|
205
|
+
const safeMessage = sanitizeCommitMessage(commitMessage);
|
|
206
|
+
|
|
207
|
+
// Stage and commit in worktree
|
|
208
|
+
git(['add', '-A'], { cwd: worktreePath });
|
|
209
|
+
git(['commit', '-m', safeMessage], { cwd: worktreePath });
|
|
210
|
+
|
|
211
|
+
// Switch to base branch in main repo
|
|
212
|
+
const originalBranch = getCurrentBranch(repoRoot);
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// Checkout base branch
|
|
216
|
+
git(['checkout', baseBranch], { cwd: repoRoot });
|
|
217
|
+
|
|
218
|
+
// Merge the worktree branch
|
|
219
|
+
if (squash) {
|
|
220
|
+
git(['merge', '--squash', branchName], { cwd: repoRoot });
|
|
221
|
+
git(['commit', '-m', safeMessage], { cwd: repoRoot });
|
|
222
|
+
} else {
|
|
223
|
+
git(['merge', branchName, '-m', safeMessage], { cwd: repoRoot });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Push if requested
|
|
227
|
+
if (push) {
|
|
228
|
+
git(['push', 'origin', baseBranch], { cwd: repoRoot });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
} catch (error) {
|
|
232
|
+
// Restore original branch on failure
|
|
233
|
+
git(['checkout', originalBranch], { cwd: repoRoot, silent: true });
|
|
234
|
+
throw new Error(`Merge failed: ${error.message}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Cleanup
|
|
238
|
+
if (cleanup) {
|
|
239
|
+
await discardWorktree(worktree, { deleteBranch: true });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { merged: true, commitMessage };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Discard a worktree without merging (rollback)
|
|
247
|
+
*
|
|
248
|
+
* @param {Object} worktree - Worktree info from createWorktree
|
|
249
|
+
* @param {Object} [options]
|
|
250
|
+
* @param {boolean} [options.deleteBranch] - Also delete the branch (default: true)
|
|
251
|
+
*/
|
|
252
|
+
async function discardWorktree(worktree, options = {}) {
|
|
253
|
+
const { deleteBranch = true } = options;
|
|
254
|
+
const { path: worktreePath, branchName, repoRoot } = worktree;
|
|
255
|
+
|
|
256
|
+
// Remove the worktree
|
|
257
|
+
try {
|
|
258
|
+
git(['worktree', 'remove', worktreePath, '--force'], { cwd: repoRoot, silent: true });
|
|
259
|
+
} catch {
|
|
260
|
+
// If git remove fails, try manual cleanup
|
|
261
|
+
if (fs.existsSync(worktreePath)) {
|
|
262
|
+
fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
263
|
+
}
|
|
264
|
+
// Prune worktree list
|
|
265
|
+
git(['worktree', 'prune'], { cwd: repoRoot, silent: true });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Delete the branch
|
|
269
|
+
if (deleteBranch) {
|
|
270
|
+
git(['branch', '-D', branchName], { cwd: repoRoot, silent: true });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { discarded: true };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* List all active wogi worktrees
|
|
278
|
+
*
|
|
279
|
+
* @param {string} [repoRoot] - Repository root
|
|
280
|
+
* @returns {Array} List of worktree info objects
|
|
281
|
+
*/
|
|
282
|
+
function listWorktrees(repoRoot = process.cwd()) {
|
|
283
|
+
const root = getRepoRoot(repoRoot) || repoRoot;
|
|
284
|
+
const output = git(['worktree', 'list', '--porcelain'], { cwd: root, silent: true });
|
|
285
|
+
if (!output) return [];
|
|
286
|
+
|
|
287
|
+
const worktrees = [];
|
|
288
|
+
const entries = output.split('\n\n').filter(Boolean);
|
|
289
|
+
|
|
290
|
+
for (const entry of entries) {
|
|
291
|
+
const lines = entry.split('\n');
|
|
292
|
+
const worktreePath = lines.find(l => l.startsWith('worktree '))?.replace('worktree ', '');
|
|
293
|
+
const branch = lines.find(l => l.startsWith('branch '))?.replace('branch refs/heads/', '');
|
|
294
|
+
|
|
295
|
+
if (worktreePath && branch && branch.startsWith(WORKTREE_PREFIX)) {
|
|
296
|
+
// Try to load saved info
|
|
297
|
+
const infoPath = path.join(worktreePath, '.wogi-worktree.json');
|
|
298
|
+
let info = { path: worktreePath, branchName: branch };
|
|
299
|
+
|
|
300
|
+
if (fs.existsSync(infoPath)) {
|
|
301
|
+
try {
|
|
302
|
+
info = JSON.parse(fs.readFileSync(infoPath, 'utf-8'));
|
|
303
|
+
} catch { /* ignore */ }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
worktrees.push(info);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return worktrees;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Cleanup all stale wogi worktrees
|
|
315
|
+
*
|
|
316
|
+
* @param {string} [repoRoot] - Repository root
|
|
317
|
+
* @param {number} [maxAgeMs] - Max age in milliseconds (default: 24 hours)
|
|
318
|
+
*/
|
|
319
|
+
async function cleanupStaleWorktrees(repoRoot = process.cwd(), maxAgeMs = 24 * 60 * 60 * 1000) {
|
|
320
|
+
const worktrees = listWorktrees(repoRoot);
|
|
321
|
+
const now = Date.now();
|
|
322
|
+
const cleaned = [];
|
|
323
|
+
|
|
324
|
+
for (const worktree of worktrees) {
|
|
325
|
+
const createdAt = worktree.createdAt ? new Date(worktree.createdAt).getTime() : 0;
|
|
326
|
+
const age = now - createdAt;
|
|
327
|
+
|
|
328
|
+
if (age > maxAgeMs || !worktree.createdAt) {
|
|
329
|
+
try {
|
|
330
|
+
await discardWorktree(worktree);
|
|
331
|
+
cleaned.push(worktree.branchName);
|
|
332
|
+
} catch { /* ignore cleanup errors */ }
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return cleaned;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Run a function in an isolated worktree context
|
|
341
|
+
*
|
|
342
|
+
* @param {Object} options - Options for createWorktree
|
|
343
|
+
* @param {Function} fn - Async function to run, receives (worktreePath, worktreeInfo)
|
|
344
|
+
* @param {Object} [fnOptions]
|
|
345
|
+
* @param {string} [fnOptions.commitMessage] - If provided, commit and merge on success
|
|
346
|
+
* @param {boolean} [fnOptions.keepOnFailure] - Keep worktree on failure for debugging
|
|
347
|
+
*/
|
|
348
|
+
async function runInWorktree(options, fn, fnOptions = {}) {
|
|
349
|
+
const { commitMessage, keepOnFailure = false } = fnOptions;
|
|
350
|
+
const worktree = await createWorktree(options);
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
const result = await fn(worktree.path, worktree);
|
|
354
|
+
|
|
355
|
+
if (commitMessage) {
|
|
356
|
+
await commitAndMerge(worktree, commitMessage);
|
|
357
|
+
} else {
|
|
358
|
+
await discardWorktree(worktree);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return { success: true, result, worktree };
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (!keepOnFailure) {
|
|
364
|
+
await discardWorktree(worktree);
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
success: false,
|
|
368
|
+
error: error.message,
|
|
369
|
+
worktree: keepOnFailure ? worktree : null
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ============================================================
|
|
375
|
+
// Exports
|
|
376
|
+
// ============================================================
|
|
377
|
+
|
|
378
|
+
module.exports = {
|
|
379
|
+
// Core functions
|
|
380
|
+
createWorktree,
|
|
381
|
+
commitAndMerge,
|
|
382
|
+
discardWorktree,
|
|
383
|
+
|
|
384
|
+
// Utilities
|
|
385
|
+
listWorktrees,
|
|
386
|
+
cleanupStaleWorktrees,
|
|
387
|
+
runInWorktree,
|
|
388
|
+
|
|
389
|
+
// Helpers
|
|
390
|
+
isGitRepo,
|
|
391
|
+
getCurrentBranch,
|
|
392
|
+
getRepoRoot,
|
|
393
|
+
|
|
394
|
+
// Constants
|
|
395
|
+
WORKTREE_PREFIX,
|
|
396
|
+
WORKTREE_BASE_DIR
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// ============================================================
|
|
400
|
+
// CLI
|
|
401
|
+
// ============================================================
|
|
402
|
+
|
|
403
|
+
if (require.main === module) {
|
|
404
|
+
const args = process.argv.slice(2);
|
|
405
|
+
const command = args[0];
|
|
406
|
+
|
|
407
|
+
async function main() {
|
|
408
|
+
switch (command) {
|
|
409
|
+
case 'list': {
|
|
410
|
+
const worktrees = listWorktrees();
|
|
411
|
+
if (worktrees.length === 0) {
|
|
412
|
+
console.log('No active wogi worktrees');
|
|
413
|
+
} else {
|
|
414
|
+
console.log('\n📁 Active Wogi Worktrees:\n');
|
|
415
|
+
for (const wt of worktrees) {
|
|
416
|
+
console.log(` ${wt.branchName}`);
|
|
417
|
+
console.log(` Path: ${wt.path}`);
|
|
418
|
+
console.log(` Task: ${wt.taskId || 'unknown'}`);
|
|
419
|
+
console.log(` Created: ${wt.createdAt || 'unknown'}`);
|
|
420
|
+
console.log('');
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
case 'cleanup': {
|
|
427
|
+
const cleaned = await cleanupStaleWorktrees();
|
|
428
|
+
if (cleaned.length === 0) {
|
|
429
|
+
console.log('No stale worktrees to clean up');
|
|
430
|
+
} else {
|
|
431
|
+
console.log(`Cleaned up ${cleaned.length} stale worktree(s):`);
|
|
432
|
+
cleaned.forEach(b => console.log(` - ${b}`));
|
|
433
|
+
}
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
case 'create': {
|
|
438
|
+
const taskId = args[1] || 'test-task';
|
|
439
|
+
const worktree = await createWorktree({ taskId });
|
|
440
|
+
console.log('\n✅ Created worktree:');
|
|
441
|
+
console.log(` Branch: ${worktree.branchName}`);
|
|
442
|
+
console.log(` Path: ${worktree.path}`);
|
|
443
|
+
console.log(` Base: ${worktree.baseBranch}`);
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
case 'discard': {
|
|
448
|
+
const branchName = args[1];
|
|
449
|
+
if (!branchName) {
|
|
450
|
+
console.error('Usage: flow-worktree.js discard <branch-name>');
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
const worktrees = listWorktrees();
|
|
454
|
+
const worktree = worktrees.find(w => w.branchName === branchName);
|
|
455
|
+
if (!worktree) {
|
|
456
|
+
console.error(`Worktree not found: ${branchName}`);
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
await discardWorktree(worktree);
|
|
460
|
+
console.log(`✅ Discarded worktree: ${branchName}`);
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
default:
|
|
465
|
+
console.log(`
|
|
466
|
+
Wogi Flow - Git Worktree Isolation
|
|
467
|
+
|
|
468
|
+
Usage:
|
|
469
|
+
node flow-worktree.js <command> [options]
|
|
470
|
+
|
|
471
|
+
Commands:
|
|
472
|
+
list List all active wogi worktrees
|
|
473
|
+
cleanup Remove stale worktrees (>24h old)
|
|
474
|
+
create [taskId] Create a new worktree for testing
|
|
475
|
+
discard <branch> Discard a specific worktree
|
|
476
|
+
|
|
477
|
+
Examples:
|
|
478
|
+
node flow-worktree.js list
|
|
479
|
+
node flow-worktree.js create TASK-123
|
|
480
|
+
node flow-worktree.js cleanup
|
|
481
|
+
`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
main().catch(err => {
|
|
486
|
+
console.error('Error:', err.message);
|
|
487
|
+
process.exit(1);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Base Adapter
|
|
5
|
+
*
|
|
6
|
+
* Base class for CLI-specific hook adapters.
|
|
7
|
+
* Each CLI (Claude Code, Gemini, Codex, etc.) extends this class.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Base adapter class
|
|
12
|
+
* Provides common functionality and defines the interface for CLI adapters.
|
|
13
|
+
*/
|
|
14
|
+
class BaseAdapter {
|
|
15
|
+
constructor(name) {
|
|
16
|
+
this.name = name;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the CLI's hook configuration path
|
|
21
|
+
* @returns {string} Path to hook config file
|
|
22
|
+
*/
|
|
23
|
+
getConfigPath() {
|
|
24
|
+
throw new Error('getConfigPath() must be implemented by subclass');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get supported hook events for this CLI
|
|
29
|
+
* @returns {string[]} Array of event names
|
|
30
|
+
*/
|
|
31
|
+
getSupportedEvents() {
|
|
32
|
+
throw new Error('getSupportedEvents() must be implemented by subclass');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Transform core result to CLI-specific format
|
|
37
|
+
* @param {string} event - Event name (e.g., 'PreToolUse')
|
|
38
|
+
* @param {Object} coreResult - Result from core module
|
|
39
|
+
* @returns {Object} CLI-specific format
|
|
40
|
+
*/
|
|
41
|
+
transformResult(event, coreResult) {
|
|
42
|
+
throw new Error('transformResult() must be implemented by subclass');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse incoming hook input from CLI
|
|
47
|
+
* @param {Object} input - Raw input from CLI
|
|
48
|
+
* @returns {Object} Normalized input
|
|
49
|
+
*/
|
|
50
|
+
parseInput(input) {
|
|
51
|
+
return input; // Default: pass through
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generate hook configuration for this CLI
|
|
56
|
+
* @param {Object} rules - Hook rules from config
|
|
57
|
+
* @returns {Object} CLI-specific hook configuration
|
|
58
|
+
*/
|
|
59
|
+
generateConfig(rules) {
|
|
60
|
+
throw new Error('generateConfig() must be implemented by subclass');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get the install command for this CLI's hooks
|
|
65
|
+
* @returns {string} Install instructions
|
|
66
|
+
*/
|
|
67
|
+
getInstallInstructions() {
|
|
68
|
+
return `Install hooks for ${this.name}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if this CLI is available/installed
|
|
73
|
+
* @returns {boolean}
|
|
74
|
+
*/
|
|
75
|
+
isAvailable() {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Standard result format from core modules
|
|
82
|
+
* All adapters expect this format from core modules.
|
|
83
|
+
*/
|
|
84
|
+
const CoreResultSchema = {
|
|
85
|
+
// Common fields
|
|
86
|
+
allowed: Boolean, // Whether action is allowed
|
|
87
|
+
blocked: Boolean, // Whether action is blocked
|
|
88
|
+
message: String, // Human-readable message
|
|
89
|
+
reason: String, // Machine-readable reason code
|
|
90
|
+
|
|
91
|
+
// Optional fields
|
|
92
|
+
warning: Boolean, // Is this a warning (vs block)?
|
|
93
|
+
task: Object, // Active task info
|
|
94
|
+
similar: Array, // Similar components found
|
|
95
|
+
results: Array, // Validation results
|
|
96
|
+
criteriaStatus: Object // Loop criteria status
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
BaseAdapter,
|
|
101
|
+
CoreResultSchema
|
|
102
|
+
};
|