claudenv 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +3 -0
- package/package.json +1 -1
- package/scaffold/.claude/agents/hypothesis-tester.md +22 -0
- package/scaffold/.claude/agents/rollback-analyzer.md +22 -0
- package/src/index.js +1 -0
- package/src/loop.js +153 -9
- package/src/worktree.js +189 -0
- package/templates/rules-workflow.ejs +7 -0
- package/scaffold/global/.claude/commands/improve.md +0 -60
- package/scaffold/global/.claude/commands/setup-mcp.md +0 -115
package/bin/cli.js
CHANGED
|
@@ -132,6 +132,7 @@ program
|
|
|
132
132
|
.option('--allow-dirty', 'Allow running with uncommitted git changes')
|
|
133
133
|
.option('--rollback', 'Undo all changes from the most recent loop run')
|
|
134
134
|
.option('--unsafe', 'Remove default tool restrictions (allows rm -rf)')
|
|
135
|
+
.option('--worktree', 'Run each iteration in an isolated git worktree')
|
|
135
136
|
.option('--profile <name>', 'Autonomy profile: safe, moderate, full, ci')
|
|
136
137
|
.action(async (opts) => {
|
|
137
138
|
// --- Rollback mode ---
|
|
@@ -170,6 +171,7 @@ program
|
|
|
170
171
|
|
|
171
172
|
console.log(` Directory: ${cwd}`);
|
|
172
173
|
console.log(` Mode: ${trust ? 'full trust (--dangerously-skip-permissions)' : 'interactive'}`);
|
|
174
|
+
if (opts.worktree) console.log(` Worktree: enabled (each iteration in isolated worktree)`);
|
|
173
175
|
if (opts.iterations) console.log(` Max iterations: ${opts.iterations}`);
|
|
174
176
|
if (opts.goal) console.log(` Goal: ${opts.goal}`);
|
|
175
177
|
if (opts.model) console.log(` Model: ${opts.model}`);
|
|
@@ -187,6 +189,7 @@ program
|
|
|
187
189
|
cwd,
|
|
188
190
|
allowDirty: opts.allowDirty || false,
|
|
189
191
|
unsafe: opts.unsafe || false,
|
|
192
|
+
worktree: opts.worktree || false,
|
|
190
193
|
disallowedTools: profileDefaults.disallowedTools,
|
|
191
194
|
});
|
|
192
195
|
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hypothesis-tester
|
|
3
|
+
description: Tests implementation hypotheses in isolation. Use when exploring risky refactors, alternative approaches, or when the current approach might need rollback.
|
|
4
|
+
tools: Read, Write, Edit, Glob, Grep, Bash
|
|
5
|
+
model: sonnet
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are testing an implementation hypothesis in an isolated git worktree.
|
|
9
|
+
|
|
10
|
+
## Your workflow
|
|
11
|
+
1. Read the parent branch context (goal, constraints, current state)
|
|
12
|
+
2. Implement the hypothesis
|
|
13
|
+
3. Run tests to validate
|
|
14
|
+
4. If tests pass — commit with descriptive message, report SUCCESS
|
|
15
|
+
5. If tests fail — report FAILURE with analysis of why, do NOT commit broken code
|
|
16
|
+
|
|
17
|
+
## Output format
|
|
18
|
+
Report exactly one of:
|
|
19
|
+
- `HYPOTHESIS_SUCCESS: <summary of what worked and why>`
|
|
20
|
+
- `HYPOTHESIS_FAILURE: <summary of what failed and why>`
|
|
21
|
+
|
|
22
|
+
The parent agent will decide whether to merge your worktree branch.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rollback-analyzer
|
|
3
|
+
description: Analyzes past hypothesis branches to understand what was tried and why it failed. Use before retrying a failed approach.
|
|
4
|
+
tools: Read, Glob, Grep, Bash
|
|
5
|
+
model: haiku
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You analyze past hypothesis branches to help the parent agent avoid repeating mistakes.
|
|
9
|
+
|
|
10
|
+
## Your workflow
|
|
11
|
+
1. List branches: `git branch -l "claudenv/*"`
|
|
12
|
+
2. For each relevant branch, show: `git log --oneline <parent>..claudenv/<name>`
|
|
13
|
+
3. Read key changed files: `git diff <parent>...claudenv/<name> --stat`
|
|
14
|
+
4. Summarize: what was tried, what failed, what can be learned
|
|
15
|
+
|
|
16
|
+
## Output format
|
|
17
|
+
For each hypothesis branch:
|
|
18
|
+
- **Branch:** claudenv/<name>
|
|
19
|
+
- **Goal:** <extracted from commit messages>
|
|
20
|
+
- **Changes:** <file summary>
|
|
21
|
+
- **Result:** SUCCESS/FAILURE
|
|
22
|
+
- **Lesson:** <what to do differently>
|
package/src/index.js
CHANGED
|
@@ -7,3 +7,4 @@ export { runLoop, spawnClaude, checkClaudeCli } from './loop.js';
|
|
|
7
7
|
export { AUTONOMY_PROFILES, getProfile, listProfiles, CREDENTIAL_PATHS } from './profiles.js';
|
|
8
8
|
export { generateSettingsJson, generatePreToolUseHook, generateAuditLogHook, generateAliases, generateCIWorkflow } from './hooks-gen.js';
|
|
9
9
|
export { generateAutonomyConfig, printSecuritySummary, getFullModeWarning } from './autonomy.js';
|
|
10
|
+
export { createWorktree, removeWorktree, mergeWorktree, listHypotheses } from './worktree.js';
|
package/src/loop.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execSync, spawn } from 'node:child_process';
|
|
|
2
2
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { select } from '@inquirer/prompts';
|
|
5
|
+
import { createWorktree, removeWorktree, mergeWorktree, getCurrentBranch } from './worktree.js';
|
|
5
6
|
|
|
6
7
|
// =============================================
|
|
7
8
|
// Pre-flight: check Claude CLI
|
|
@@ -114,9 +115,42 @@ export function spawnClaude(prompt, options = {}) {
|
|
|
114
115
|
* Build the planning prompt (iteration 0).
|
|
115
116
|
*/
|
|
116
117
|
function buildPlanningPrompt(goal) {
|
|
117
|
-
|
|
118
|
+
if (goal) {
|
|
119
|
+
return `You are an expert software engineer. Your task: ${goal}
|
|
120
|
+
|
|
121
|
+
## Instructions
|
|
122
|
+
|
|
123
|
+
1. Analyze the current project state — read key files to understand what exists (README.md, CLAUDE.md, package.json, and important source files).
|
|
124
|
+
2. Create an action plan in \`.claude/improvement-plan.md\` with the following format:
|
|
125
|
+
|
|
126
|
+
\`\`\`markdown
|
|
127
|
+
# Improvement Plan
|
|
128
|
+
|
|
129
|
+
Generated by claudenv loop. Goal: ${goal}.
|
|
130
|
+
|
|
131
|
+
## Pending
|
|
132
|
+
|
|
133
|
+
### 1. [Title of step]
|
|
134
|
+
- **Impact**: High/Medium/Low — [why]
|
|
135
|
+
- **Difficulty**: High/Medium/Low
|
|
136
|
+
- **Files**: [files to create or modify]
|
|
137
|
+
|
|
138
|
+
### 2. [Title of step]
|
|
139
|
+
- **Impact**: High/Medium/Low — [why]
|
|
140
|
+
- **Difficulty**: High/Medium/Low
|
|
141
|
+
- **Files**: [files to create or modify]
|
|
142
|
+
|
|
143
|
+
## Completed
|
|
144
|
+
\`\`\`
|
|
145
|
+
|
|
146
|
+
3. Break the goal into concrete, sequential steps — each step should be implementable in a single iteration.
|
|
147
|
+
4. The plan must directly serve the stated goal — do NOT add unrelated improvements.
|
|
148
|
+
5. Include 5-10 actionable items.
|
|
149
|
+
6. Do NOT implement anything yet — only create the plan.
|
|
150
|
+
7. Commit the plan: \`git add .claude/improvement-plan.md && git commit -m "Add action plan for: ${goal}"\``;
|
|
151
|
+
}
|
|
152
|
+
|
|
118
153
|
return `You are an expert software engineer analyzing this project to create an improvement plan.
|
|
119
|
-
${goalLine}
|
|
120
154
|
|
|
121
155
|
## Instructions
|
|
122
156
|
|
|
@@ -134,7 +168,7 @@ ${goalLine}
|
|
|
134
168
|
\`\`\`markdown
|
|
135
169
|
# Improvement Plan
|
|
136
170
|
|
|
137
|
-
Generated by claudenv loop. Goal:
|
|
171
|
+
Generated by claudenv loop. Goal: General improvement.
|
|
138
172
|
|
|
139
173
|
## Pending
|
|
140
174
|
|
|
@@ -163,15 +197,21 @@ Generated by claudenv loop. Goal: ${goal || 'General improvement'}.
|
|
|
163
197
|
function buildExecutionPrompt(iteration, maxIterations, goal) {
|
|
164
198
|
const maxLine = maxIterations ? ` (iteration ${iteration} of ${maxIterations})` : ` (iteration ${iteration})`;
|
|
165
199
|
const goalLine = goal ? `\nGoal: ${goal}` : '';
|
|
166
|
-
|
|
200
|
+
const goalRules = goal
|
|
201
|
+
? `
|
|
202
|
+
- Implement REAL changes — write code, create files, build features
|
|
203
|
+
- Do NOT limit yourself to documentation or analysis — the goal requires working software`
|
|
204
|
+
: '';
|
|
205
|
+
return `You are an expert software engineer working on this project.${maxLine}
|
|
167
206
|
${goalLine}
|
|
168
207
|
|
|
169
208
|
## Instructions
|
|
170
209
|
|
|
171
210
|
1. Read \`.claude/improvement-plan.md\`
|
|
172
211
|
2. Pick the top unfinished item from the "## Pending" section
|
|
173
|
-
3. Implement it:
|
|
212
|
+
3. Implement it fully:
|
|
174
213
|
- Write the code changes
|
|
214
|
+
- Create new files as needed
|
|
175
215
|
- Add or update tests if applicable
|
|
176
216
|
- Run tests to verify nothing is broken
|
|
177
217
|
4. Update \`.claude/improvement-plan.md\`:
|
|
@@ -182,7 +222,7 @@ ${goalLine}
|
|
|
182
222
|
6. Report what you did in a brief summary
|
|
183
223
|
|
|
184
224
|
## Important rules
|
|
185
|
-
|
|
225
|
+
${goalRules}
|
|
186
226
|
- Do NOT delete files unless the deletion IS the improvement itself
|
|
187
227
|
- Do NOT make changes beyond the single item you picked
|
|
188
228
|
- If the "## Pending" section is empty or all items are done, output exactly: NO_MORE_IMPROVEMENTS
|
|
@@ -329,6 +369,12 @@ function printFinalSummary(log) {
|
|
|
329
369
|
const lastSession = log.iterations[log.iterations.length - 1].sessionId;
|
|
330
370
|
if (lastSession) console.log(` Session ID: ${lastSession} (use --resume in Claude Code)`);
|
|
331
371
|
}
|
|
372
|
+
if (log.hypotheses && log.hypotheses.length > 0) {
|
|
373
|
+
console.log('\n Hypothesis branches:');
|
|
374
|
+
for (const h of log.hypotheses) {
|
|
375
|
+
console.log(` ${h.status === 'merged' ? '+' : h.status === 'failed' ? '!' : '~'} ${h.branch} [${h.status}] (iteration ${h.iteration})`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
332
378
|
console.log(`\n To undo all changes: claudenv loop --rollback`);
|
|
333
379
|
console.log(' ═══════════════════════════════════\n');
|
|
334
380
|
}
|
|
@@ -350,6 +396,7 @@ function printFinalSummary(log) {
|
|
|
350
396
|
* @param {number} [options.budget] - Budget cap per iteration
|
|
351
397
|
* @param {string} [options.cwd] - Working directory
|
|
352
398
|
* @param {boolean} [options.allowDirty] - Allow dirty git state
|
|
399
|
+
* @param {boolean} [options.worktree] - Run each iteration in an isolated git worktree
|
|
353
400
|
*/
|
|
354
401
|
export async function runLoop(options = {}) {
|
|
355
402
|
const cwd = options.cwd || process.cwd();
|
|
@@ -391,6 +438,7 @@ export async function runLoop(options = {}) {
|
|
|
391
438
|
completedAt: null,
|
|
392
439
|
stopReason: null,
|
|
393
440
|
totalIterations: 0,
|
|
441
|
+
hypotheses: [],
|
|
394
442
|
};
|
|
395
443
|
|
|
396
444
|
// --- Shared spawn options ---
|
|
@@ -484,22 +532,103 @@ export async function runLoop(options = {}) {
|
|
|
484
532
|
// 'goal' and 'continue' both continue
|
|
485
533
|
}
|
|
486
534
|
|
|
487
|
-
|
|
535
|
+
const worktreeMode = options.worktree || false;
|
|
536
|
+
console.log(`\n Starting iteration ${i}${maxIterations < Infinity ? ` of ${maxIterations}` : ''}${worktreeMode ? ' (worktree)' : ''}...\n`);
|
|
488
537
|
|
|
489
538
|
const startedAt = new Date().toISOString();
|
|
490
539
|
const execPrompt = buildExecutionPrompt(i, maxIterations < Infinity ? maxIterations : null, options.goal);
|
|
491
540
|
|
|
541
|
+
// --- Worktree mode: run iteration in isolated worktree ---
|
|
542
|
+
let iterCwd = cwd;
|
|
543
|
+
let worktreeInfo = null;
|
|
544
|
+
if (worktreeMode) {
|
|
545
|
+
try {
|
|
546
|
+
worktreeInfo = await createWorktree(cwd, null, { goal: options.goal });
|
|
547
|
+
iterCwd = worktreeInfo.path;
|
|
548
|
+
console.log(` Worktree: ${worktreeInfo.branch} → ${worktreeInfo.path}`);
|
|
549
|
+
} catch (err) {
|
|
550
|
+
console.error(`\n Failed to create worktree: ${err.message}`);
|
|
551
|
+
log.stopReason = 'error';
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
492
556
|
let iterResult;
|
|
493
557
|
try {
|
|
494
|
-
iterResult = await spawnClaude(execPrompt, { ...spawnOpts, sessionId });
|
|
558
|
+
iterResult = await spawnClaude(execPrompt, { ...spawnOpts, cwd: iterCwd, sessionId });
|
|
495
559
|
} catch (err) {
|
|
496
560
|
console.error(`\n Iteration ${i} failed: ${err.message}`);
|
|
561
|
+
// In worktree mode, clean up the worktree on failure
|
|
562
|
+
if (worktreeInfo) {
|
|
563
|
+
try {
|
|
564
|
+
removeWorktree(cwd, worktreeInfo.name);
|
|
565
|
+
} catch { /* ignore cleanup errors */ }
|
|
566
|
+
log.hypotheses.push({
|
|
567
|
+
name: worktreeInfo.name,
|
|
568
|
+
branch: worktreeInfo.branch,
|
|
569
|
+
status: 'failed',
|
|
570
|
+
iteration: i,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
497
573
|
log.stopReason = 'error';
|
|
498
574
|
break;
|
|
499
575
|
}
|
|
500
576
|
|
|
501
577
|
sessionId = iterResult.sessionId || sessionId;
|
|
502
578
|
|
|
579
|
+
// --- Worktree mode: merge or discard ---
|
|
580
|
+
if (worktreeMode && worktreeInfo) {
|
|
581
|
+
const isSuccess = !detectConvergence(iterResult.result);
|
|
582
|
+
const hasCommits = (() => {
|
|
583
|
+
try {
|
|
584
|
+
const diff = execSync(`git log ${getCurrentBranch(cwd)}..${worktreeInfo.branch} --oneline`, {
|
|
585
|
+
cwd,
|
|
586
|
+
encoding: 'utf-8',
|
|
587
|
+
}).trim();
|
|
588
|
+
return diff !== '';
|
|
589
|
+
} catch {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
})();
|
|
593
|
+
|
|
594
|
+
if (hasCommits && isSuccess) {
|
|
595
|
+
try {
|
|
596
|
+
mergeWorktree(cwd, worktreeInfo.name);
|
|
597
|
+
console.log(` Worktree merged: ${worktreeInfo.branch}`);
|
|
598
|
+
log.hypotheses.push({
|
|
599
|
+
name: worktreeInfo.name,
|
|
600
|
+
branch: worktreeInfo.branch,
|
|
601
|
+
status: 'merged',
|
|
602
|
+
iteration: i,
|
|
603
|
+
});
|
|
604
|
+
} catch (err) {
|
|
605
|
+
console.error(` Merge failed: ${err.message}`);
|
|
606
|
+
log.hypotheses.push({
|
|
607
|
+
name: worktreeInfo.name,
|
|
608
|
+
branch: worktreeInfo.branch,
|
|
609
|
+
status: 'conflict',
|
|
610
|
+
iteration: i,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
} else {
|
|
614
|
+
// No commits or convergence — discard worktree, keep branch
|
|
615
|
+
try {
|
|
616
|
+
removeWorktree(cwd, worktreeInfo.name);
|
|
617
|
+
} catch { /* ignore */ }
|
|
618
|
+
log.hypotheses.push({
|
|
619
|
+
name: worktreeInfo.name,
|
|
620
|
+
branch: worktreeInfo.branch,
|
|
621
|
+
status: hasCommits ? 'discarded' : 'empty',
|
|
622
|
+
iteration: i,
|
|
623
|
+
});
|
|
624
|
+
if (!hasCommits) {
|
|
625
|
+
console.log(` Worktree discarded (no changes): ${worktreeInfo.branch}`);
|
|
626
|
+
} else {
|
|
627
|
+
console.log(` Worktree discarded (converged): ${worktreeInfo.branch}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
503
632
|
const iteration = {
|
|
504
633
|
number: i,
|
|
505
634
|
startedAt,
|
|
@@ -610,8 +739,23 @@ export async function rollback(options = {}) {
|
|
|
610
739
|
}
|
|
611
740
|
}
|
|
612
741
|
|
|
742
|
+
// Clean up worktrees if any (keep branches for reference)
|
|
743
|
+
if (log && log.hypotheses && log.hypotheses.length > 0) {
|
|
744
|
+
console.log(' Cleaning up worktrees...');
|
|
745
|
+
for (const h of log.hypotheses) {
|
|
746
|
+
try {
|
|
747
|
+
removeWorktree(cwd, h.name);
|
|
748
|
+
} catch { /* worktree may already be removed */ }
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
613
752
|
execSync(`git reset --hard ${tag}`, { cwd, stdio: 'inherit' });
|
|
614
753
|
execSync(`git tag -d ${tag}`, { cwd, stdio: 'inherit' });
|
|
615
754
|
|
|
616
|
-
console.log('\n Rollback complete. All loop changes have been undone
|
|
755
|
+
console.log('\n Rollback complete. All loop changes have been undone.');
|
|
756
|
+
if (log && log.hypotheses && log.hypotheses.length > 0) {
|
|
757
|
+
console.log(' Hypothesis branches preserved — use `git branch -l "claudenv/*"` to list them.\n');
|
|
758
|
+
} else {
|
|
759
|
+
console.log();
|
|
760
|
+
}
|
|
617
761
|
}
|
package/src/worktree.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { readFile, writeFile, access } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
// =============================================
|
|
6
|
+
// Constants
|
|
7
|
+
// =============================================
|
|
8
|
+
|
|
9
|
+
const WORKTREE_DIR = '.worktrees';
|
|
10
|
+
const BRANCH_PREFIX = 'claudenv';
|
|
11
|
+
|
|
12
|
+
// =============================================
|
|
13
|
+
// Path helpers
|
|
14
|
+
// =============================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the worktree directory path for a given name.
|
|
18
|
+
* @param {string} cwd - Working directory
|
|
19
|
+
* @param {string} name - Worktree name
|
|
20
|
+
* @returns {string} Absolute path to .worktrees/<name>
|
|
21
|
+
*/
|
|
22
|
+
export function getWorktreePath(cwd, name) {
|
|
23
|
+
return join(cwd, WORKTREE_DIR, name);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get the branch name for a worktree.
|
|
28
|
+
* @param {string} name - Worktree name
|
|
29
|
+
* @returns {string} Branch name claudenv/<name>
|
|
30
|
+
*/
|
|
31
|
+
export function getBranchName(name) {
|
|
32
|
+
return `${BRANCH_PREFIX}/${name}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate an auto-name based on current timestamp.
|
|
37
|
+
* @returns {string} hypothesis-YYYY-MM-DD-HHMM
|
|
38
|
+
*/
|
|
39
|
+
export function generateWorktreeName() {
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
42
|
+
return `hypothesis-${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the current branch name.
|
|
47
|
+
* @param {string} cwd - Working directory
|
|
48
|
+
* @returns {string} Current branch name
|
|
49
|
+
*/
|
|
50
|
+
export function getCurrentBranch(cwd) {
|
|
51
|
+
return execSync('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf-8' }).trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// =============================================
|
|
55
|
+
// Gitignore management
|
|
56
|
+
// =============================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Ensure .worktrees/ is in .gitignore. Idempotent.
|
|
60
|
+
* @param {string} cwd - Working directory
|
|
61
|
+
*/
|
|
62
|
+
export async function ensureGitignore(cwd) {
|
|
63
|
+
const gitignorePath = join(cwd, '.gitignore');
|
|
64
|
+
const entry = `${WORKTREE_DIR}/`;
|
|
65
|
+
|
|
66
|
+
let content = '';
|
|
67
|
+
try {
|
|
68
|
+
content = await readFile(gitignorePath, 'utf-8');
|
|
69
|
+
} catch {
|
|
70
|
+
// .gitignore doesn't exist yet
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check if already present (exact line match)
|
|
74
|
+
const lines = content.split('\n');
|
|
75
|
+
if (lines.some((line) => line.trim() === entry)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Append entry
|
|
80
|
+
const separator = content && !content.endsWith('\n') ? '\n' : '';
|
|
81
|
+
await writeFile(gitignorePath, content + separator + entry + '\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// =============================================
|
|
85
|
+
// Worktree operations
|
|
86
|
+
// =============================================
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create a new git worktree with a dedicated branch.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} cwd - Working directory (main repo)
|
|
92
|
+
* @param {string} [name] - Worktree name (auto-generated if omitted)
|
|
93
|
+
* @param {object} [options]
|
|
94
|
+
* @param {string} [options.goal] - Goal description (used in branch commit message)
|
|
95
|
+
* @returns {{ name: string, path: string, branch: string }}
|
|
96
|
+
*/
|
|
97
|
+
export async function createWorktree(cwd, name, options = {}) {
|
|
98
|
+
if (!name) {
|
|
99
|
+
name = generateWorktreeName();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const worktreePath = getWorktreePath(cwd, name);
|
|
103
|
+
const branch = getBranchName(name);
|
|
104
|
+
|
|
105
|
+
// Ensure .worktrees/ is gitignored
|
|
106
|
+
await ensureGitignore(cwd);
|
|
107
|
+
|
|
108
|
+
// Create worktree with new branch
|
|
109
|
+
execSync(`git worktree add -b "${branch}" "${worktreePath}"`, {
|
|
110
|
+
cwd,
|
|
111
|
+
stdio: 'pipe',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return { name, path: worktreePath, branch };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Remove a worktree but keep the branch for reference.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} cwd - Working directory (main repo)
|
|
121
|
+
* @param {string} name - Worktree name
|
|
122
|
+
*/
|
|
123
|
+
export function removeWorktree(cwd, name) {
|
|
124
|
+
const worktreePath = getWorktreePath(cwd, name);
|
|
125
|
+
|
|
126
|
+
execSync(`git worktree remove "${worktreePath}" --force`, {
|
|
127
|
+
cwd,
|
|
128
|
+
stdio: 'pipe',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Merge a worktree branch back into the current branch with --no-ff.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} cwd - Working directory (main repo)
|
|
136
|
+
* @param {string} name - Worktree name
|
|
137
|
+
* @param {object} [options]
|
|
138
|
+
* @param {string} [options.message] - Custom merge commit message
|
|
139
|
+
* @returns {{ merged: boolean, branch: string }}
|
|
140
|
+
* @throws {Error} On merge conflict
|
|
141
|
+
*/
|
|
142
|
+
export function mergeWorktree(cwd, name, options = {}) {
|
|
143
|
+
const branch = getBranchName(name);
|
|
144
|
+
const message = options.message || `Merge hypothesis: ${name}`;
|
|
145
|
+
|
|
146
|
+
// Remove worktree first (must be done before merge)
|
|
147
|
+
removeWorktree(cwd, name);
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
execSync(`git merge --no-ff "${branch}" -m "${message}"`, {
|
|
151
|
+
cwd,
|
|
152
|
+
stdio: 'pipe',
|
|
153
|
+
});
|
|
154
|
+
} catch (err) {
|
|
155
|
+
// Abort the merge on conflict
|
|
156
|
+
try {
|
|
157
|
+
execSync('git merge --abort', { cwd, stdio: 'pipe' });
|
|
158
|
+
} catch {
|
|
159
|
+
// merge --abort may fail if there's nothing to abort
|
|
160
|
+
}
|
|
161
|
+
throw new Error(`Merge conflict when merging ${branch}. Branch preserved for manual resolution.`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { merged: true, branch };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* List all hypothesis branches (claudenv/*).
|
|
169
|
+
*
|
|
170
|
+
* @param {string} cwd - Working directory
|
|
171
|
+
* @returns {string[]} Branch names
|
|
172
|
+
*/
|
|
173
|
+
export function listHypotheses(cwd) {
|
|
174
|
+
try {
|
|
175
|
+
const output = execSync(`git branch -l "${BRANCH_PREFIX}/*"`, {
|
|
176
|
+
cwd,
|
|
177
|
+
encoding: 'utf-8',
|
|
178
|
+
}).trim();
|
|
179
|
+
|
|
180
|
+
if (!output) return [];
|
|
181
|
+
|
|
182
|
+
return output
|
|
183
|
+
.split('\n')
|
|
184
|
+
.map((line) => line.trim().replace(/^[*+]\s*/, ''))
|
|
185
|
+
.filter(Boolean);
|
|
186
|
+
} catch {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -28,6 +28,13 @@ paths:
|
|
|
28
28
|
- Update `_state.md` when making significant architectural decisions
|
|
29
29
|
- Review `_state.md` at the start of each session to restore context
|
|
30
30
|
|
|
31
|
+
## Worktree Safety
|
|
32
|
+
- For risky changes or alternative approaches, spawn `hypothesis-tester` subagent — it runs in an isolated git worktree
|
|
33
|
+
- If a hypothesis fails, the worktree is discarded without affecting the main branch
|
|
34
|
+
- Before retrying a failed approach, spawn `rollback-analyzer` to review past hypothesis branches
|
|
35
|
+
- Past hypotheses are preserved as `claudenv/*` branches for reference
|
|
36
|
+
- Use `git branch -l "claudenv/*"` to see all hypothesis branches
|
|
37
|
+
|
|
31
38
|
## Commits and Git
|
|
32
39
|
- NEVER commit unless the user explicitly asks
|
|
33
40
|
- NEVER push to remote without explicit permission
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Analyze this project and make one high-impact improvement — fix bugs, add tests, improve code quality
|
|
3
|
-
allowed-tools: Read, Write, Glob, Grep, Bash
|
|
4
|
-
argument-hint: [area-to-improve]
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# /improve — Single Improvement Iteration
|
|
8
|
-
|
|
9
|
-
You are an expert software engineer. Your job is to analyze this project and make one high-impact improvement.
|
|
10
|
-
|
|
11
|
-
## Step 1: Read Context
|
|
12
|
-
|
|
13
|
-
Read these files if they exist:
|
|
14
|
-
- `CLAUDE.md` — project overview and conventions
|
|
15
|
-
- `_state.md` — session memory
|
|
16
|
-
- `.claude/improvement-plan.md` — existing improvement plan (if any)
|
|
17
|
-
|
|
18
|
-
## Step 2: Choose What to Improve
|
|
19
|
-
|
|
20
|
-
**If `.claude/improvement-plan.md` exists:**
|
|
21
|
-
- Read the plan and pick the top unfinished item from the "## Pending" section
|
|
22
|
-
- If `$ARGUMENTS` is provided, use it as a focus area instead of the plan
|
|
23
|
-
|
|
24
|
-
**If no plan exists:**
|
|
25
|
-
- Analyze the project: read manifest files, scan source code, check test coverage
|
|
26
|
-
- If `$ARGUMENTS` is provided, focus on that area
|
|
27
|
-
- Identify the single highest-impact improvement you can make
|
|
28
|
-
|
|
29
|
-
## Step 3: Implement the Change
|
|
30
|
-
|
|
31
|
-
- Write the code changes
|
|
32
|
-
- Add or update tests if applicable
|
|
33
|
-
- Follow existing code style and conventions
|
|
34
|
-
|
|
35
|
-
## Step 4: Verify
|
|
36
|
-
|
|
37
|
-
- Run tests (if a test command is available)
|
|
38
|
-
- Run linter (if configured)
|
|
39
|
-
- Fix any issues found
|
|
40
|
-
|
|
41
|
-
## Step 5: Update Plan
|
|
42
|
-
|
|
43
|
-
If `.claude/improvement-plan.md` exists:
|
|
44
|
-
- Move the completed item from "## Pending" to "## Completed"
|
|
45
|
-
- Add the commit hash and notes about what was done
|
|
46
|
-
- If you discovered new issues, add them to "## Pending"
|
|
47
|
-
|
|
48
|
-
## Step 6: Commit and Report
|
|
49
|
-
|
|
50
|
-
- Commit all changes with a descriptive message
|
|
51
|
-
- Report:
|
|
52
|
-
- What you changed and why
|
|
53
|
-
- What tests were added/updated
|
|
54
|
-
- What's next (remaining plan items or suggested improvements)
|
|
55
|
-
|
|
56
|
-
## Important Rules
|
|
57
|
-
|
|
58
|
-
- Do NOT delete files unless the deletion IS the improvement
|
|
59
|
-
- Make exactly ONE improvement per invocation
|
|
60
|
-
- If there's nothing left to improve, output: NO_MORE_IMPROVEMENTS
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Recommend and configure MCP servers based on project analysis
|
|
3
|
-
allowed-tools: Read, Write, Glob, Grep, Bash(curl:*), Bash(find:*), Bash(cat:*)
|
|
4
|
-
disable-model-invocation: true
|
|
5
|
-
argument-hint: [--force]
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# MCP Server Setup
|
|
9
|
-
|
|
10
|
-
You are configuring MCP servers for this project. Analyze the tech stack, search the official MCP Registry, verify trust signals, and generate `.mcp.json`.
|
|
11
|
-
|
|
12
|
-
## Step 1: Analyze the Project
|
|
13
|
-
|
|
14
|
-
Understand what this project uses:
|
|
15
|
-
|
|
16
|
-
**Read existing documentation:**
|
|
17
|
-
- Read CLAUDE.md if it exists — it contains the tech stack summary
|
|
18
|
-
- Read _state.md if it exists
|
|
19
|
-
|
|
20
|
-
**Check for existing MCP config:**
|
|
21
|
-
- Read `.mcp.json` if it exists — you will merge new entries, not overwrite
|
|
22
|
-
|
|
23
|
-
**Scan manifest files:**
|
|
24
|
-
!`find . -maxdepth 3 \( -name "package.json" -o -name "pyproject.toml" -o -name "go.mod" -o -name "Cargo.toml" -o -name "Gemfile" -o -name "composer.json" -o -name "requirements.txt" \) -not -path "*/node_modules/*" -not -path "*/.venv/*" -not -path "*/vendor/*" 2>/dev/null | head -20`
|
|
25
|
-
|
|
26
|
-
**Scan framework and tooling configs:**
|
|
27
|
-
!`find . -maxdepth 2 \( -name "tsconfig*.json" -o -name "next.config.*" -o -name "vite.config.*" -o -name "docker-compose.*" -o -name "Dockerfile" -o -name ".env*" -o -name "prisma" -o -name "drizzle.config.*" \) -not -path "*/node_modules/*" 2>/dev/null | head -20`
|
|
28
|
-
|
|
29
|
-
Read the manifest files you found. Identify:
|
|
30
|
-
- Programming languages and frameworks
|
|
31
|
-
- Databases (PostgreSQL, MongoDB, Redis, etc.)
|
|
32
|
-
- Cloud services (AWS, GCP, Azure)
|
|
33
|
-
- External APIs (Stripe, Sentry, etc.)
|
|
34
|
-
- Dev tools (Docker, GitHub Actions, etc.)
|
|
35
|
-
|
|
36
|
-
## Step 2: Search the MCP Registry
|
|
37
|
-
|
|
38
|
-
Read the MCP server reference for search and evaluation guidance:
|
|
39
|
-
@~/.claude/skills/claudenv/templates/mcp-servers.md
|
|
40
|
-
|
|
41
|
-
For each major technology in the project, search the official MCP Registry API:
|
|
42
|
-
```bash
|
|
43
|
-
curl -s 'https://registry.modelcontextprotocol.io/v0.1/servers?search=<tech>&version=latest&limit=10'
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
For each candidate server with an npm package, verify trust by checking monthly downloads:
|
|
47
|
-
```bash
|
|
48
|
-
curl -s 'https://api.npmjs.org/downloads/point/last-month/<npm-package>'
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Optionally check GitHub stars for additional trust signal:
|
|
52
|
-
```bash
|
|
53
|
-
curl -s 'https://api.github.com/repos/<owner>/<repo>'
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
**Filtering rules:**
|
|
57
|
-
- Remove servers with <100 monthly npm downloads
|
|
58
|
-
- Rank by: npm downloads (primary) + GitHub stars (secondary) + description relevance
|
|
59
|
-
- When multiple servers exist for the same technology, pick the one with the highest downloads
|
|
60
|
-
|
|
61
|
-
## Step 3: Present Recommendations
|
|
62
|
-
|
|
63
|
-
Group your recommendations into three tiers:
|
|
64
|
-
|
|
65
|
-
**Essential** — servers that directly support the project's core technologies (e.g., PostgreSQL server for a project using PostgreSQL)
|
|
66
|
-
|
|
67
|
-
**Recommended** — servers that enhance the development workflow (e.g., Context7 for library docs, GitHub for repo management)
|
|
68
|
-
|
|
69
|
-
**Optional** — servers that could be useful but aren't critical (e.g., Fetch for web content, Docker if Docker is used occasionally)
|
|
70
|
-
|
|
71
|
-
For each recommendation, explain:
|
|
72
|
-
- **What it does** and why it's relevant to THIS project
|
|
73
|
-
- **Monthly npm downloads** (trust signal)
|
|
74
|
-
- **Environment variables required** and whether they need secrets
|
|
75
|
-
|
|
76
|
-
Ask the user which servers to configure.
|
|
77
|
-
|
|
78
|
-
If `$ARGUMENTS` includes `--force`, auto-select all Essential and Recommended servers.
|
|
79
|
-
|
|
80
|
-
## Step 4: Generate .mcp.json
|
|
81
|
-
|
|
82
|
-
Build the `.mcp.json` file with the selected servers following the format in the MCP server reference.
|
|
83
|
-
|
|
84
|
-
**If `.mcp.json` already exists:**
|
|
85
|
-
- Read it and parse the existing `mcpServers` entries
|
|
86
|
-
- Merge new servers into the existing config — do NOT remove or overwrite existing entries
|
|
87
|
-
- If a server key already exists, skip it (preserve the user's existing config)
|
|
88
|
-
|
|
89
|
-
**Key rules:**
|
|
90
|
-
- Use `${ENV_VAR}` placeholders for ALL secret environment variables — NEVER literal values
|
|
91
|
-
- Non-secret env vars can use literal values when appropriate
|
|
92
|
-
- Use `npx -y <package>@latest` for stdio/npm servers
|
|
93
|
-
- Use the `type` and `url` from remotes for HTTP/SSE servers
|
|
94
|
-
|
|
95
|
-
Write the `.mcp.json` file.
|
|
96
|
-
|
|
97
|
-
## Step 5: Update CLAUDE.md
|
|
98
|
-
|
|
99
|
-
If CLAUDE.md exists, add or update an `## MCP Servers` section listing the configured servers and what they do. Keep it concise — one line per server.
|
|
100
|
-
|
|
101
|
-
If CLAUDE.md doesn't exist, skip this step and suggest running `/claudenv` first.
|
|
102
|
-
|
|
103
|
-
## Step 6: Environment Variables
|
|
104
|
-
|
|
105
|
-
List all required environment variables and how to configure them:
|
|
106
|
-
|
|
107
|
-
```
|
|
108
|
-
To configure secrets, run:
|
|
109
|
-
claude config set env.VAR_NAME "your-value"
|
|
110
|
-
|
|
111
|
-
Required environment variables:
|
|
112
|
-
VAR_NAME — Description of what this is and where to get it
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Remind the user that `.mcp.json` is safe to commit — it only contains placeholders, not actual secrets.
|