rafcode 2.5.0 → 2.5.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/CLAUDE.md +1 -1
- package/RAF/ahwzmc-echo-forge/decisions.md +15 -0
- package/RAF/ahwzmc-echo-forge/input.md +4 -0
- package/RAF/ahwzmc-echo-forge/outcomes/01-change-low-effort-default-to-sonnet.md +57 -0
- package/RAF/ahwzmc-echo-forge/outcomes/02-add-no-worktree-flag.md +79 -0
- package/RAF/ahwzmc-echo-forge/outcomes/03-update-readme.md +75 -0
- package/RAF/ahwzmc-echo-forge/plans/01-change-low-effort-default-to-sonnet.md +57 -0
- package/RAF/ahwzmc-echo-forge/plans/02-add-no-worktree-flag.md +51 -0
- package/RAF/ahwzmc-echo-forge/plans/03-update-readme.md +48 -0
- package/RAF/aifqwf-fix-amend-commit-again/decisions.md +7 -0
- package/RAF/aifqwf-fix-amend-commit-again/input.md +2 -0
- package/RAF/aifqwf-fix-amend-commit-again/outcomes/01-update-effort-mapping-defaults.md +35 -0
- package/RAF/aifqwf-fix-amend-commit-again/outcomes/02-fix-amend-worktree-commit.md +50 -0
- package/RAF/aifqwf-fix-amend-commit-again/plans/01-update-effort-mapping-defaults.md +37 -0
- package/RAF/aifqwf-fix-amend-commit-again/plans/02-fix-amend-worktree-commit.md +55 -0
- package/README.md +26 -12
- package/dist/commands/do.d.ts.map +1 -1
- package/dist/commands/do.js +1 -0
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +10 -3
- package/dist/commands/plan.js.map +1 -1
- package/dist/core/git.d.ts.map +1 -1
- package/dist/core/git.js +20 -4
- package/dist/core/git.js.map +1 -1
- package/dist/types/config.js +2 -2
- package/dist/types/config.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/do.ts +1 -0
- package/src/commands/plan.ts +10 -3
- package/src/core/git.ts +23 -4
- package/src/prompts/config-docs.md +7 -7
- package/src/types/config.ts +2 -2
- package/tests/unit/commit-planning-artifacts-worktree.test.ts +113 -0
- package/tests/unit/commit-planning-artifacts.test.ts +1 -1
- package/tests/unit/config-command.test.ts +2 -2
- package/tests/unit/config.test.ts +14 -14
- package/tests/unit/worktree-flag-override.test.ts +186 -0
package/src/commands/plan.ts
CHANGED
|
@@ -73,6 +73,7 @@ export function createPlanCommand(): Command {
|
|
|
73
73
|
.option('--sonnet', 'Use Sonnet model (shorthand for --model sonnet)')
|
|
74
74
|
.option('-y, --auto', "Skip Claude's permission prompts for file operations")
|
|
75
75
|
.option('-w, --worktree', 'Create a git worktree for isolated planning')
|
|
76
|
+
.option('--no-worktree', 'Disable worktree mode (overrides config)')
|
|
76
77
|
.option('-r, --resume <identifier>', 'Resume a planning session for an existing project')
|
|
77
78
|
.action(async (projectName: string | undefined, options: PlanCommandOptions) => {
|
|
78
79
|
// Validate and resolve model option
|
|
@@ -318,8 +319,12 @@ async function runPlanCommand(projectName?: string, model?: string, autoMode: bo
|
|
|
318
319
|
logger.info(` - plans/${planFile}`);
|
|
319
320
|
}
|
|
320
321
|
|
|
321
|
-
// Commit planning artifacts (input.md
|
|
322
|
-
|
|
322
|
+
// Commit planning artifacts (input.md, decisions.md, and plan files)
|
|
323
|
+
const planAbsolutePaths = planFiles.map((f) => path.join(plansDir, f));
|
|
324
|
+
await commitPlanningArtifacts(projectPath, {
|
|
325
|
+
cwd: worktreePath ?? undefined,
|
|
326
|
+
additionalFiles: planAbsolutePaths,
|
|
327
|
+
});
|
|
323
328
|
|
|
324
329
|
logger.newline();
|
|
325
330
|
if (worktreeMode) {
|
|
@@ -632,10 +637,12 @@ async function runAmendCommand(identifier: string, model?: string, autoMode: boo
|
|
|
632
637
|
logger.info(` - plans/${planFile}`);
|
|
633
638
|
}
|
|
634
639
|
|
|
635
|
-
// Commit planning artifacts (input.md, decisions.md
|
|
640
|
+
// Commit planning artifacts (input.md, decisions.md, and new plan files)
|
|
641
|
+
const newPlanAbsolutePaths = newPlanFiles.map((f) => path.join(plansDir, f));
|
|
636
642
|
await commitPlanningArtifacts(projectPath, {
|
|
637
643
|
cwd: worktreePath ?? undefined,
|
|
638
644
|
isAmend: true,
|
|
645
|
+
additionalFiles: newPlanAbsolutePaths,
|
|
639
646
|
});
|
|
640
647
|
|
|
641
648
|
logger.newline();
|
package/src/core/git.ts
CHANGED
|
@@ -227,6 +227,9 @@ export function isFileCommittedInHead(filePath: string): boolean {
|
|
|
227
227
|
*/
|
|
228
228
|
export async function commitPlanningArtifacts(projectPath: string, options?: { cwd?: string; additionalFiles?: string[]; isAmend?: boolean }): Promise<void> {
|
|
229
229
|
const execCwd = options?.cwd;
|
|
230
|
+
const execOpts = execCwd ? { cwd: execCwd } : {};
|
|
231
|
+
|
|
232
|
+
logger.debug(`commitPlanningArtifacts: projectPath=${projectPath}, cwd=${execCwd ?? 'process.cwd()'}, isAmend=${options?.isAmend ?? false}`);
|
|
230
233
|
|
|
231
234
|
// Check if we're in a git repository
|
|
232
235
|
if (!isGitRepo(execCwd)) {
|
|
@@ -268,6 +271,20 @@ export async function commitPlanningArtifacts(projectPath: string, options?: { c
|
|
|
268
271
|
? absoluteFiles.map(f => path.relative(execCwd, f))
|
|
269
272
|
: absoluteFiles;
|
|
270
273
|
|
|
274
|
+
logger.debug(`commitPlanningArtifacts: staging files: ${filesToStage.join(', ')}`);
|
|
275
|
+
|
|
276
|
+
// Check git status before staging to understand the current state
|
|
277
|
+
try {
|
|
278
|
+
const preStatus = execSync('git status --porcelain', {
|
|
279
|
+
encoding: 'utf-8',
|
|
280
|
+
stdio: 'pipe',
|
|
281
|
+
...execOpts,
|
|
282
|
+
}).trim();
|
|
283
|
+
logger.debug(`commitPlanningArtifacts: pre-stage git status:\n${preStatus || '(clean)'}`);
|
|
284
|
+
} catch {
|
|
285
|
+
logger.debug('commitPlanningArtifacts: could not get pre-stage git status');
|
|
286
|
+
}
|
|
287
|
+
|
|
271
288
|
// Stage each file individually so one missing file doesn't block the others
|
|
272
289
|
let stagedCount = 0;
|
|
273
290
|
for (const file of filesToStage) {
|
|
@@ -275,7 +292,7 @@ export async function commitPlanningArtifacts(projectPath: string, options?: { c
|
|
|
275
292
|
execSync(`git add -- "${file}"`, {
|
|
276
293
|
encoding: 'utf-8',
|
|
277
294
|
stdio: 'pipe',
|
|
278
|
-
...
|
|
295
|
+
...execOpts,
|
|
279
296
|
});
|
|
280
297
|
stagedCount++;
|
|
281
298
|
} catch (error) {
|
|
@@ -294,19 +311,21 @@ export async function commitPlanningArtifacts(projectPath: string, options?: { c
|
|
|
294
311
|
const stagedStatus = execSync('git diff --cached --name-only', {
|
|
295
312
|
encoding: 'utf-8',
|
|
296
313
|
stdio: 'pipe',
|
|
297
|
-
...
|
|
314
|
+
...execOpts,
|
|
298
315
|
}).trim();
|
|
299
316
|
|
|
300
317
|
if (!stagedStatus) {
|
|
301
|
-
logger.debug('No changes to planning artifacts to commit');
|
|
318
|
+
logger.debug('No changes to planning artifacts to commit (git add succeeded but nothing changed in index)');
|
|
302
319
|
return;
|
|
303
320
|
}
|
|
304
321
|
|
|
322
|
+
logger.debug(`commitPlanningArtifacts: staged files: ${stagedStatus}`);
|
|
323
|
+
|
|
305
324
|
// Commit the staged files
|
|
306
325
|
execSync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, {
|
|
307
326
|
encoding: 'utf-8',
|
|
308
327
|
stdio: 'pipe',
|
|
309
|
-
...
|
|
328
|
+
...execOpts,
|
|
310
329
|
});
|
|
311
330
|
|
|
312
331
|
logger.debug(`Committed planning artifacts: ${commitMessage}`);
|
|
@@ -49,8 +49,8 @@ Maps task complexity labels (in plan frontmatter) to Claude models. When a plan
|
|
|
49
49
|
|
|
50
50
|
| Key | Default | Description |
|
|
51
51
|
|-----|---------|-------------|
|
|
52
|
-
| `effortMapping.low` | `"
|
|
53
|
-
| `effortMapping.medium` | `"
|
|
52
|
+
| `effortMapping.low` | `"sonnet"` | Model for low-complexity tasks |
|
|
53
|
+
| `effortMapping.medium` | `"opus"` | Model for medium-complexity tasks |
|
|
54
54
|
| `effortMapping.high` | `"opus"` | Model for high-complexity tasks |
|
|
55
55
|
|
|
56
56
|
Values must be a short alias (`"sonnet"`, `"haiku"`, `"opus"`) or a full model ID.
|
|
@@ -61,11 +61,11 @@ Example:
|
|
|
61
61
|
```json
|
|
62
62
|
{
|
|
63
63
|
"models": { "execute": "sonnet" },
|
|
64
|
-
"effortMapping": { "low": "
|
|
64
|
+
"effortMapping": { "low": "sonnet", "medium": "opus", "high": "opus" }
|
|
65
65
|
}
|
|
66
66
|
```
|
|
67
|
-
- Task with `effort: low` →
|
|
68
|
-
- Task with `effort: medium` → sonnet (
|
|
67
|
+
- Task with `effort: low` → sonnet (at ceiling)
|
|
68
|
+
- Task with `effort: medium` → sonnet (capped to ceiling, not opus)
|
|
69
69
|
- Task with `effort: high` → sonnet (capped to ceiling, not opus)
|
|
70
70
|
|
|
71
71
|
### `timeout` — Task Timeout
|
|
@@ -217,8 +217,8 @@ Uses Sonnet for planning and caps task execution at Sonnet (tasks with `effort:
|
|
|
217
217
|
"config": "sonnet"
|
|
218
218
|
},
|
|
219
219
|
"effortMapping": {
|
|
220
|
-
"low": "
|
|
221
|
-
"medium": "
|
|
220
|
+
"low": "sonnet",
|
|
221
|
+
"medium": "opus",
|
|
222
222
|
"high": "opus"
|
|
223
223
|
},
|
|
224
224
|
"timeout": 60,
|
package/src/types/config.ts
CHANGED
|
@@ -179,6 +179,50 @@ describe('commitPlanningArtifacts - worktree integration', () => {
|
|
|
179
179
|
expect(committedFiles).not.toContain(`RAF/${projectFolder}/plans/02-new-task.md`);
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
+
it('should commit amend artifacts with plan files as additionalFiles in worktree', async () => {
|
|
183
|
+
const projectFolder = 'aatest-my-project';
|
|
184
|
+
|
|
185
|
+
// Create initial project and commit
|
|
186
|
+
createInitialProject(repoDir, projectFolder);
|
|
187
|
+
execSync('git add -A', { cwd: repoDir, stdio: 'pipe' });
|
|
188
|
+
execSync('git commit -m "Initial plan"', { cwd: repoDir, stdio: 'pipe' });
|
|
189
|
+
|
|
190
|
+
// Create worktree
|
|
191
|
+
worktreePath = createWorktreeForProject(repoDir, projectFolder);
|
|
192
|
+
const wtProjectPath = path.join(worktreePath, 'RAF', projectFolder);
|
|
193
|
+
|
|
194
|
+
// Simulate amend: update files and create new plan
|
|
195
|
+
fs.writeFileSync(
|
|
196
|
+
path.join(wtProjectPath, 'input.md'),
|
|
197
|
+
'original input\n\n---\n\nnew task description'
|
|
198
|
+
);
|
|
199
|
+
fs.writeFileSync(
|
|
200
|
+
path.join(wtProjectPath, 'decisions.md'),
|
|
201
|
+
'# Decisions\n\n## Q1?\nA1\n\n## Q2?\nA2'
|
|
202
|
+
);
|
|
203
|
+
fs.writeFileSync(
|
|
204
|
+
path.join(wtProjectPath, 'plans', '02-new-task.md'),
|
|
205
|
+
'# Task: New Task'
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Call commitPlanningArtifacts with plan files as additionalFiles (as plan.ts now does)
|
|
209
|
+
await commitPlanningArtifacts(wtProjectPath, {
|
|
210
|
+
cwd: worktreePath,
|
|
211
|
+
isAmend: true,
|
|
212
|
+
additionalFiles: [path.join(wtProjectPath, 'plans', '02-new-task.md')],
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Verify commit was made with Amend prefix
|
|
216
|
+
const lastMsg = getLastCommitMessage(worktreePath);
|
|
217
|
+
expect(lastMsg).toMatch(/RAF\[aatest\] Amend: my-project/);
|
|
218
|
+
|
|
219
|
+
// Verify all files are in the commit (input, decisions, AND plan files)
|
|
220
|
+
const committedFiles = getLastCommitFiles(worktreePath);
|
|
221
|
+
expect(committedFiles).toContain(`RAF/${projectFolder}/input.md`);
|
|
222
|
+
expect(committedFiles).toContain(`RAF/${projectFolder}/decisions.md`);
|
|
223
|
+
expect(committedFiles).toContain(`RAF/${projectFolder}/plans/02-new-task.md`);
|
|
224
|
+
});
|
|
225
|
+
|
|
182
226
|
it('should commit after worktree recreation from branch', async () => {
|
|
183
227
|
const projectFolder = 'aatest-my-project';
|
|
184
228
|
|
|
@@ -247,6 +291,75 @@ describe('commitPlanningArtifacts - worktree integration', () => {
|
|
|
247
291
|
expect(committedFiles).not.toContain(`RAF/${projectFolder}/plans/02-new-task.md`);
|
|
248
292
|
});
|
|
249
293
|
|
|
294
|
+
it('should commit all artifacts after worktree recreation with additionalFiles', async () => {
|
|
295
|
+
const projectFolder = 'aatest-my-project';
|
|
296
|
+
|
|
297
|
+
// Create initial project, commit, and create initial worktree
|
|
298
|
+
createInitialProject(repoDir, projectFolder);
|
|
299
|
+
execSync('git add -A', { cwd: repoDir, stdio: 'pipe' });
|
|
300
|
+
execSync('git commit -m "Initial plan"', { cwd: repoDir, stdio: 'pipe' });
|
|
301
|
+
|
|
302
|
+
// Create worktree
|
|
303
|
+
const initialWtPath = createWorktreeForProject(repoDir, projectFolder);
|
|
304
|
+
const initialWtProjectPath = path.join(initialWtPath, 'RAF', projectFolder);
|
|
305
|
+
|
|
306
|
+
// Commit something on the worktree branch (simulating initial plan commit + task execution)
|
|
307
|
+
fs.writeFileSync(
|
|
308
|
+
path.join(initialWtProjectPath, 'decisions.md'),
|
|
309
|
+
'# Decisions\n\n## Q1?\nA1\n\n## Q2?\nA2'
|
|
310
|
+
);
|
|
311
|
+
execSync('git add -A', { cwd: initialWtPath, stdio: 'pipe' });
|
|
312
|
+
execSync('git commit -m "Plan commit on branch"', { cwd: initialWtPath, stdio: 'pipe' });
|
|
313
|
+
|
|
314
|
+
// Remove the worktree (simulating cleanup after execution)
|
|
315
|
+
execSync(`git worktree remove "${initialWtPath}" --force`, {
|
|
316
|
+
cwd: repoDir,
|
|
317
|
+
stdio: 'pipe',
|
|
318
|
+
});
|
|
319
|
+
worktreePaths.splice(worktreePaths.indexOf(initialWtPath), 1);
|
|
320
|
+
|
|
321
|
+
// Recreate worktree from existing branch (simulating amend --worktree)
|
|
322
|
+
const recreatedWtPath = path.join(makeTempDir('raf-wt-recreated-'), projectFolder);
|
|
323
|
+
execSync(`git worktree add "${recreatedWtPath}" "${projectFolder}"`, {
|
|
324
|
+
cwd: repoDir,
|
|
325
|
+
stdio: 'pipe',
|
|
326
|
+
});
|
|
327
|
+
worktreePaths.push(recreatedWtPath);
|
|
328
|
+
|
|
329
|
+
const recreatedProjectPath = path.join(recreatedWtPath, 'RAF', projectFolder);
|
|
330
|
+
|
|
331
|
+
// Simulate amend: update input.md, update decisions.md, create new plan
|
|
332
|
+
fs.writeFileSync(
|
|
333
|
+
path.join(recreatedProjectPath, 'input.md'),
|
|
334
|
+
'original input\n\n---\n\namend task description'
|
|
335
|
+
);
|
|
336
|
+
fs.writeFileSync(
|
|
337
|
+
path.join(recreatedProjectPath, 'decisions.md'),
|
|
338
|
+
'# Decisions\n\n## Q1?\nA1\n\n## Q2?\nA2\n\n## Q3?\nA3'
|
|
339
|
+
);
|
|
340
|
+
fs.writeFileSync(
|
|
341
|
+
path.join(recreatedProjectPath, 'plans', '02-new-task.md'),
|
|
342
|
+
'# Task: New Task'
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// Call commitPlanningArtifacts with plan files as additionalFiles (as plan.ts now does)
|
|
346
|
+
await commitPlanningArtifacts(recreatedProjectPath, {
|
|
347
|
+
cwd: recreatedWtPath,
|
|
348
|
+
isAmend: true,
|
|
349
|
+
additionalFiles: [path.join(recreatedProjectPath, 'plans', '02-new-task.md')],
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Verify commit was made
|
|
353
|
+
const lastMsg = getLastCommitMessage(recreatedWtPath);
|
|
354
|
+
expect(lastMsg).toMatch(/RAF\[aatest\] Amend: my-project/);
|
|
355
|
+
|
|
356
|
+
// Verify all files are in the commit (including plan files)
|
|
357
|
+
const committedFiles = getLastCommitFiles(recreatedWtPath);
|
|
358
|
+
expect(committedFiles).toContain(`RAF/${projectFolder}/input.md`);
|
|
359
|
+
expect(committedFiles).toContain(`RAF/${projectFolder}/decisions.md`);
|
|
360
|
+
expect(committedFiles).toContain(`RAF/${projectFolder}/plans/02-new-task.md`);
|
|
361
|
+
});
|
|
362
|
+
|
|
250
363
|
it('should work when only some files have changed', async () => {
|
|
251
364
|
const projectFolder = 'aatest-my-project';
|
|
252
365
|
|
|
@@ -130,7 +130,7 @@ describe('commitPlanningArtifacts', () => {
|
|
|
130
130
|
|
|
131
131
|
// Should log debug message and not throw
|
|
132
132
|
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
133
|
-
'No changes to planning artifacts to commit'
|
|
133
|
+
'No changes to planning artifacts to commit (git add succeeded but nothing changed in index)'
|
|
134
134
|
);
|
|
135
135
|
expect(mockLogger.warn).not.toHaveBeenCalled();
|
|
136
136
|
});
|
|
@@ -77,7 +77,7 @@ describe('Config Command', () => {
|
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
it('should accept valid config with effortMapping override', () => {
|
|
80
|
-
const config = { effortMapping: { low: '
|
|
80
|
+
const config = { effortMapping: { low: 'sonnet', medium: 'opus' } };
|
|
81
81
|
expect(() => validateConfig(config)).not.toThrow();
|
|
82
82
|
});
|
|
83
83
|
|
|
@@ -204,7 +204,7 @@ describe('Config Command', () => {
|
|
|
204
204
|
// These are the values that runConfigSession uses when config loading fails
|
|
205
205
|
expect(DEFAULT_CONFIG.models.config).toBe('sonnet');
|
|
206
206
|
// effortMapping defaults used for per-task model resolution
|
|
207
|
-
expect(DEFAULT_CONFIG.effortMapping.medium).toBe('
|
|
207
|
+
expect(DEFAULT_CONFIG.effortMapping.medium).toBe('opus');
|
|
208
208
|
});
|
|
209
209
|
|
|
210
210
|
it('should be able to read raw file contents even when config is invalid JSON', () => {
|
|
@@ -91,7 +91,7 @@ describe('Config', () => {
|
|
|
91
91
|
it('should accept a full valid config', () => {
|
|
92
92
|
const config = {
|
|
93
93
|
models: { plan: 'opus', execute: 'haiku' },
|
|
94
|
-
effortMapping: { low: '
|
|
94
|
+
effortMapping: { low: 'sonnet', medium: 'opus', high: 'opus' },
|
|
95
95
|
timeout: 30,
|
|
96
96
|
maxRetries: 5,
|
|
97
97
|
autoCommit: false,
|
|
@@ -274,7 +274,7 @@ describe('Config', () => {
|
|
|
274
274
|
|
|
275
275
|
const config = resolveConfig(configPath);
|
|
276
276
|
expect(config.effortMapping.medium).toBe('opus');
|
|
277
|
-
expect(config.effortMapping.low).toBe('
|
|
277
|
+
expect(config.effortMapping.low).toBe('sonnet'); // default preserved
|
|
278
278
|
expect(config.effortMapping.high).toBe('opus'); // default preserved
|
|
279
279
|
});
|
|
280
280
|
|
|
@@ -368,7 +368,7 @@ describe('Config', () => {
|
|
|
368
368
|
fs.writeFileSync(configPath, JSON.stringify({ effortMapping: { high: 'sonnet' } }));
|
|
369
369
|
const config = resolveConfig(configPath);
|
|
370
370
|
expect(config.effortMapping.high).toBe('sonnet');
|
|
371
|
-
expect(config.effortMapping.low).toBe('
|
|
371
|
+
expect(config.effortMapping.low).toBe('sonnet'); // default preserved
|
|
372
372
|
});
|
|
373
373
|
|
|
374
374
|
it('getCommitFormat returns correct format', () => {
|
|
@@ -401,8 +401,8 @@ describe('Config', () => {
|
|
|
401
401
|
});
|
|
402
402
|
|
|
403
403
|
it('should have all effortMapping levels defined', () => {
|
|
404
|
-
expect(DEFAULT_CONFIG.effortMapping.low).toBe('
|
|
405
|
-
expect(DEFAULT_CONFIG.effortMapping.medium).toBe('
|
|
404
|
+
expect(DEFAULT_CONFIG.effortMapping.low).toBe('sonnet');
|
|
405
|
+
expect(DEFAULT_CONFIG.effortMapping.medium).toBe('opus');
|
|
406
406
|
expect(DEFAULT_CONFIG.effortMapping.high).toBe('opus');
|
|
407
407
|
});
|
|
408
408
|
|
|
@@ -483,9 +483,9 @@ describe('Config', () => {
|
|
|
483
483
|
expect(DEFAULT_CONFIG.models.prGeneration).toBe('sonnet');
|
|
484
484
|
});
|
|
485
485
|
|
|
486
|
-
it('should default effortMapping to
|
|
487
|
-
expect(DEFAULT_CONFIG.effortMapping.low).toBe('
|
|
488
|
-
expect(DEFAULT_CONFIG.effortMapping.medium).toBe('
|
|
486
|
+
it('should default effortMapping to sonnet/opus/opus', () => {
|
|
487
|
+
expect(DEFAULT_CONFIG.effortMapping.low).toBe('sonnet');
|
|
488
|
+
expect(DEFAULT_CONFIG.effortMapping.medium).toBe('opus');
|
|
489
489
|
expect(DEFAULT_CONFIG.effortMapping.high).toBe('opus');
|
|
490
490
|
});
|
|
491
491
|
|
|
@@ -596,8 +596,8 @@ describe('Config', () => {
|
|
|
596
596
|
const config = resolveConfig(configPath);
|
|
597
597
|
expect(config.effortMapping.high).toBe('sonnet');
|
|
598
598
|
// Others should remain at defaults
|
|
599
|
-
expect(config.effortMapping.low).toBe('
|
|
600
|
-
expect(config.effortMapping.medium).toBe('
|
|
599
|
+
expect(config.effortMapping.low).toBe('sonnet');
|
|
600
|
+
expect(config.effortMapping.medium).toBe('opus');
|
|
601
601
|
});
|
|
602
602
|
|
|
603
603
|
it('should use custom commit format when configured', () => {
|
|
@@ -711,8 +711,8 @@ describe('Config', () => {
|
|
|
711
711
|
const configPath = path.join(tempDir, 'default.json');
|
|
712
712
|
// Use default config
|
|
713
713
|
const config = resolveConfig(path.join(tempDir, 'nonexistent.json'));
|
|
714
|
-
expect(config.effortMapping.low).toBe('
|
|
715
|
-
expect(config.effortMapping.medium).toBe('
|
|
714
|
+
expect(config.effortMapping.low).toBe('sonnet');
|
|
715
|
+
expect(config.effortMapping.medium).toBe('opus');
|
|
716
716
|
expect(config.effortMapping.high).toBe('opus');
|
|
717
717
|
});
|
|
718
718
|
});
|
|
@@ -721,8 +721,8 @@ describe('Config', () => {
|
|
|
721
721
|
it('should accept valid effortMapping config', () => {
|
|
722
722
|
expect(() => validateConfig({
|
|
723
723
|
effortMapping: {
|
|
724
|
-
low: '
|
|
725
|
-
medium: '
|
|
724
|
+
low: 'sonnet',
|
|
725
|
+
medium: 'opus',
|
|
726
726
|
high: 'opus',
|
|
727
727
|
},
|
|
728
728
|
})).not.toThrow();
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { createPlanCommand } from '../../src/commands/plan.js';
|
|
6
|
+
import { createDoCommand } from '../../src/commands/do.js';
|
|
7
|
+
import { getWorktreeDefault, resetConfigCache, saveConfig } from '../../src/utils/config.js';
|
|
8
|
+
|
|
9
|
+
describe('Worktree Flag Override', () => {
|
|
10
|
+
let tempDir: string;
|
|
11
|
+
let originalHome: string | undefined;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'raf-worktree-flag-test-'));
|
|
15
|
+
originalHome = process.env.HOME;
|
|
16
|
+
process.env.HOME = tempDir;
|
|
17
|
+
resetConfigCache();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
process.env.HOME = originalHome;
|
|
22
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
23
|
+
resetConfigCache();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('Commander.js --no-worktree flag parsing', () => {
|
|
27
|
+
it('should parse --worktree as true', () => {
|
|
28
|
+
const planCommand = createPlanCommand();
|
|
29
|
+
// Use parseOptions instead of parse to avoid running the action
|
|
30
|
+
planCommand.parseOptions(['--worktree']);
|
|
31
|
+
const opts = planCommand.opts();
|
|
32
|
+
expect(opts.worktree).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should parse --no-worktree as false', () => {
|
|
36
|
+
const planCommand = createPlanCommand();
|
|
37
|
+
planCommand.parseOptions(['--no-worktree']);
|
|
38
|
+
const opts = planCommand.opts();
|
|
39
|
+
expect(opts.worktree).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should parse omitted flag as undefined', () => {
|
|
43
|
+
const planCommand = createPlanCommand();
|
|
44
|
+
planCommand.parseOptions([]);
|
|
45
|
+
const opts = planCommand.opts();
|
|
46
|
+
expect(opts.worktree).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should parse --worktree for do command as true', () => {
|
|
50
|
+
const doCommand = createDoCommand();
|
|
51
|
+
doCommand.parseOptions(['--worktree']);
|
|
52
|
+
const opts = doCommand.opts();
|
|
53
|
+
expect(opts.worktree).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should parse --no-worktree for do command as false', () => {
|
|
57
|
+
const doCommand = createDoCommand();
|
|
58
|
+
doCommand.parseOptions(['--no-worktree']);
|
|
59
|
+
const opts = doCommand.opts();
|
|
60
|
+
expect(opts.worktree).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should parse omitted flag for do command as undefined', () => {
|
|
64
|
+
const doCommand = createDoCommand();
|
|
65
|
+
doCommand.parseOptions([]);
|
|
66
|
+
const opts = doCommand.opts();
|
|
67
|
+
expect(opts.worktree).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('Config resolution with --no-worktree flag', () => {
|
|
72
|
+
it('should resolve to true when --worktree flag is passed (regardless of config)', () => {
|
|
73
|
+
// Simulate: options.worktree = true (from --worktree flag)
|
|
74
|
+
const options = { worktree: true };
|
|
75
|
+
// With nullish coalescing, explicit true takes precedence
|
|
76
|
+
const resolved = options.worktree ?? getWorktreeDefault();
|
|
77
|
+
expect(resolved).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should resolve to false when --no-worktree flag is passed (regardless of config)', () => {
|
|
81
|
+
// Simulate: options.worktree = false (from --no-worktree flag)
|
|
82
|
+
const options = { worktree: false };
|
|
83
|
+
// With nullish coalescing, explicit false takes precedence
|
|
84
|
+
const resolved = options.worktree ?? getWorktreeDefault();
|
|
85
|
+
expect(resolved).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should resolve to config default when no flag is passed', () => {
|
|
89
|
+
// Simulate: options.worktree = undefined (no flag passed)
|
|
90
|
+
const options = { worktree: undefined };
|
|
91
|
+
// With nullish coalescing, undefined falls back to getWorktreeDefault()
|
|
92
|
+
const resolved = options.worktree ?? getWorktreeDefault();
|
|
93
|
+
// We can't assert a specific value here since it depends on the user's actual config
|
|
94
|
+
expect(typeof resolved).toBe('boolean');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('Tri-state behavior verification', () => {
|
|
99
|
+
it('should correctly handle all three states (true/false/undefined) in plan command', () => {
|
|
100
|
+
// State 1: --worktree (explicit true)
|
|
101
|
+
const planCmd1 = createPlanCommand();
|
|
102
|
+
planCmd1.parseOptions(['--worktree']);
|
|
103
|
+
const opts1 = planCmd1.opts();
|
|
104
|
+
expect(opts1.worktree).toBe(true);
|
|
105
|
+
const resolved1 = opts1.worktree ?? getWorktreeDefault();
|
|
106
|
+
expect(resolved1).toBe(true);
|
|
107
|
+
|
|
108
|
+
// State 2: --no-worktree (explicit false)
|
|
109
|
+
const planCmd2 = createPlanCommand();
|
|
110
|
+
planCmd2.parseOptions(['--no-worktree']);
|
|
111
|
+
const opts2 = planCmd2.opts();
|
|
112
|
+
expect(opts2.worktree).toBe(false);
|
|
113
|
+
const resolved2 = opts2.worktree ?? getWorktreeDefault();
|
|
114
|
+
expect(resolved2).toBe(false);
|
|
115
|
+
|
|
116
|
+
// State 3: omitted (undefined, falls back to config)
|
|
117
|
+
const planCmd3 = createPlanCommand();
|
|
118
|
+
planCmd3.parseOptions([]);
|
|
119
|
+
const opts3 = planCmd3.opts();
|
|
120
|
+
expect(opts3.worktree).toBeUndefined();
|
|
121
|
+
const resolved3 = opts3.worktree ?? getWorktreeDefault();
|
|
122
|
+
// Should be a boolean (actual value depends on config)
|
|
123
|
+
expect(typeof resolved3).toBe('boolean');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should correctly handle all three states (true/false/undefined) in do command', () => {
|
|
127
|
+
// State 1: --worktree (explicit true)
|
|
128
|
+
const doCmd1 = createDoCommand();
|
|
129
|
+
doCmd1.parseOptions(['--worktree']);
|
|
130
|
+
const opts1 = doCmd1.opts();
|
|
131
|
+
expect(opts1.worktree).toBe(true);
|
|
132
|
+
const resolved1 = opts1.worktree ?? getWorktreeDefault();
|
|
133
|
+
expect(resolved1).toBe(true);
|
|
134
|
+
|
|
135
|
+
// State 2: --no-worktree (explicit false)
|
|
136
|
+
const doCmd2 = createDoCommand();
|
|
137
|
+
doCmd2.parseOptions(['--no-worktree']);
|
|
138
|
+
const opts2 = doCmd2.opts();
|
|
139
|
+
expect(opts2.worktree).toBe(false);
|
|
140
|
+
const resolved2 = opts2.worktree ?? getWorktreeDefault();
|
|
141
|
+
expect(resolved2).toBe(false);
|
|
142
|
+
|
|
143
|
+
// State 3: omitted (undefined, falls back to config)
|
|
144
|
+
const doCmd3 = createDoCommand();
|
|
145
|
+
doCmd3.parseOptions([]);
|
|
146
|
+
const opts3 = doCmd3.opts();
|
|
147
|
+
expect(opts3.worktree).toBeUndefined();
|
|
148
|
+
const resolved3 = opts3.worktree ?? getWorktreeDefault();
|
|
149
|
+
// Should be a boolean (actual value depends on config)
|
|
150
|
+
expect(typeof resolved3).toBe('boolean');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('Override semantics', () => {
|
|
155
|
+
it('--no-worktree should override config default (explicit false takes precedence)', () => {
|
|
156
|
+
const planCommand = createPlanCommand();
|
|
157
|
+
planCommand.parseOptions(['--no-worktree']);
|
|
158
|
+
const opts = planCommand.opts();
|
|
159
|
+
const resolved = opts.worktree ?? getWorktreeDefault();
|
|
160
|
+
|
|
161
|
+
expect(opts.worktree).toBe(false); // Flag sets explicit false
|
|
162
|
+
expect(resolved).toBe(false); // Final result is false (flag takes precedence)
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('--worktree should override config default (explicit true takes precedence)', () => {
|
|
166
|
+
const doCommand = createDoCommand();
|
|
167
|
+
doCommand.parseOptions(['--worktree']);
|
|
168
|
+
const opts = doCommand.opts();
|
|
169
|
+
const resolved = opts.worktree ?? getWorktreeDefault();
|
|
170
|
+
|
|
171
|
+
expect(opts.worktree).toBe(true); // Flag sets explicit true
|
|
172
|
+
expect(resolved).toBe(true); // Final result is true (flag takes precedence)
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('omitting flag should fall back to config default', () => {
|
|
176
|
+
const planCommand = createPlanCommand();
|
|
177
|
+
planCommand.parseOptions([]);
|
|
178
|
+
const opts = planCommand.opts();
|
|
179
|
+
const resolved = opts.worktree ?? getWorktreeDefault();
|
|
180
|
+
|
|
181
|
+
expect(opts.worktree).toBeUndefined(); // Flag not set
|
|
182
|
+
expect(typeof resolved).toBe('boolean'); // Falls back to config (which is a boolean)
|
|
183
|
+
expect(resolved).toBe(getWorktreeDefault()); // Final result matches config
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|