rafcode 2.4.0 → 2.4.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/settings.local.json +3 -1
- package/CLAUDE.md +3 -1
- package/RAF/ahwidh-quick-fix-gremlin/decisions.md +37 -0
- package/RAF/ahwidh-quick-fix-gremlin/input.md +35 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/01-fix-name-generation-prompt.md +33 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/02-fix-amend-commit-scope.md +43 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/03-fix-diverged-main-branch-sync.md +32 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/04-wire-rate-limit-to-do-command.md +61 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/05-add-config-get-set-flags.md +125 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/06-sync-worktree-branch-before-execution.md +96 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/07-update-frontmatter-format.md +107 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/08-remove-plan-token-report.md +76 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/01-fix-name-generation-prompt.md +52 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/02-fix-amend-commit-scope.md +48 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/03-fix-diverged-main-branch-sync.md +49 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/04-wire-rate-limit-to-do-command.md +78 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/05-add-config-get-set-flags.md +101 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/06-sync-worktree-branch-before-execution.md +92 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/07-update-frontmatter-format.md +105 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/08-remove-plan-token-report.md +50 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +209 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/do.d.ts.map +1 -1
- package/dist/commands/do.js +36 -5
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +2 -55
- package/dist/commands/plan.js.map +1 -1
- package/dist/core/claude-runner.d.ts +0 -6
- package/dist/core/claude-runner.d.ts.map +1 -1
- package/dist/core/claude-runner.js +1 -5
- package/dist/core/claude-runner.js.map +1 -1
- package/dist/core/worktree.d.ts +12 -0
- package/dist/core/worktree.d.ts.map +1 -1
- package/dist/core/worktree.js +33 -1
- package/dist/core/worktree.js.map +1 -1
- package/dist/prompts/amend.d.ts.map +1 -1
- package/dist/prompts/amend.js +3 -1
- package/dist/prompts/amend.js.map +1 -1
- package/dist/prompts/planning.d.ts.map +1 -1
- package/dist/prompts/planning.js +3 -1
- package/dist/prompts/planning.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +13 -3
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +40 -10
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/name-generator.d.ts.map +1 -1
- package/dist/utils/name-generator.js +7 -16
- package/dist/utils/name-generator.js.map +1 -1
- package/dist/utils/terminal-symbols.js +2 -2
- package/dist/utils/terminal-symbols.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/config.ts +242 -0
- package/src/commands/do.ts +37 -4
- package/src/commands/plan.ts +2 -65
- package/src/core/claude-runner.ts +1 -12
- package/src/core/worktree.ts +37 -1
- package/src/prompts/amend.ts +3 -1
- package/src/prompts/planning.ts +3 -1
- package/src/utils/frontmatter.ts +41 -11
- package/src/utils/name-generator.ts +7 -16
- package/src/utils/terminal-symbols.ts +2 -2
- package/tests/unit/commit-planning-artifacts-worktree.test.ts +6 -14
- package/tests/unit/commit-planning-artifacts.test.ts +4 -12
- package/tests/unit/config-command.test.ts +170 -0
- package/tests/unit/frontmatter.test.ts +95 -1
- package/tests/unit/name-generator.test.ts +1 -1
- package/tests/unit/post-execution-picker.test.ts +1 -0
- package/tests/unit/worktree.test.ts +68 -1
- package/src/utils/session-parser.ts +0 -161
- package/tests/unit/session-parser.test.ts +0 -301
package/src/commands/plan.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import * as crypto from 'node:crypto';
|
|
4
3
|
import { Command } from 'commander';
|
|
5
4
|
import { ProjectManager } from '../core/project-manager.js';
|
|
6
5
|
import { ClaudeRunner } from '../core/claude-runner.js';
|
|
@@ -16,10 +15,7 @@ import {
|
|
|
16
15
|
resolveModelOption,
|
|
17
16
|
} from '../utils/validation.js';
|
|
18
17
|
import { logger } from '../utils/logger.js';
|
|
19
|
-
import { getWorktreeDefault, getModel, getModelShortName,
|
|
20
|
-
import { TokenTracker } from '../utils/token-tracker.js';
|
|
21
|
-
import { parseSessionById } from '../utils/session-parser.js';
|
|
22
|
-
import { formatTokenTotalSummary, TokenSummaryOptions } from '../utils/terminal-symbols.js';
|
|
18
|
+
import { getWorktreeDefault, getModel, getModelShortName, getSyncMainBranch } from '../utils/config.js';
|
|
23
19
|
import { generateProjectNames } from '../utils/name-generator.js';
|
|
24
20
|
import { pickProjectName } from '../ui/name-picker.js';
|
|
25
21
|
import {
|
|
@@ -289,25 +285,17 @@ async function runPlanCommand(projectName?: string, model?: string, autoMode: bo
|
|
|
289
285
|
worktreeMode,
|
|
290
286
|
});
|
|
291
287
|
|
|
292
|
-
// Generate session ID for token tracking
|
|
293
|
-
const sessionId = crypto.randomUUID();
|
|
294
|
-
const sessionCwd = worktreePath ?? process.cwd();
|
|
295
|
-
|
|
296
288
|
try {
|
|
297
289
|
const exitCode = await claudeRunner.runInteractive(systemPrompt, userMessage, {
|
|
298
290
|
dangerouslySkipPermissions: autoMode,
|
|
299
291
|
// Run Claude session in the worktree root if in worktree mode
|
|
300
292
|
cwd: worktreePath ?? undefined,
|
|
301
|
-
sessionId,
|
|
302
293
|
});
|
|
303
294
|
|
|
304
295
|
if (exitCode !== 0) {
|
|
305
296
|
logger.warn(`Claude exited with code ${exitCode}`);
|
|
306
297
|
}
|
|
307
298
|
|
|
308
|
-
// Parse session file and display token usage summary
|
|
309
|
-
displayPlanSessionTokenSummary(sessionId, sessionCwd);
|
|
310
|
-
|
|
311
299
|
// Check for created plan files
|
|
312
300
|
const plansDir = getPlansDir(projectPath);
|
|
313
301
|
const planFiles = fs.existsSync(plansDir)
|
|
@@ -604,25 +592,17 @@ async function runAmendCommand(identifier: string, model?: string, autoMode: boo
|
|
|
604
592
|
worktreeMode,
|
|
605
593
|
});
|
|
606
594
|
|
|
607
|
-
// Generate session ID for token tracking
|
|
608
|
-
const sessionId = crypto.randomUUID();
|
|
609
|
-
const sessionCwd = worktreePath ?? process.cwd();
|
|
610
|
-
|
|
611
595
|
try {
|
|
612
596
|
const exitCode = await claudeRunner.runInteractive(systemPrompt, userMessage, {
|
|
613
597
|
dangerouslySkipPermissions: autoMode,
|
|
614
598
|
// Run Claude session in the worktree root if in worktree mode
|
|
615
599
|
cwd: worktreePath ?? undefined,
|
|
616
|
-
sessionId,
|
|
617
600
|
});
|
|
618
601
|
|
|
619
602
|
if (exitCode !== 0) {
|
|
620
603
|
logger.warn(`Claude exited with code ${exitCode}`);
|
|
621
604
|
}
|
|
622
605
|
|
|
623
|
-
// Parse session file and display token usage summary
|
|
624
|
-
displayPlanSessionTokenSummary(sessionId, sessionCwd);
|
|
625
|
-
|
|
626
606
|
// Check for new plan files
|
|
627
607
|
const allPlanFiles = fs.existsSync(plansDir)
|
|
628
608
|
? fs.readdirSync(plansDir).filter((f) => f.endsWith('.md')).sort()
|
|
@@ -647,11 +627,9 @@ async function runAmendCommand(identifier: string, model?: string, autoMode: boo
|
|
|
647
627
|
logger.info(` - plans/${planFile}`);
|
|
648
628
|
}
|
|
649
629
|
|
|
650
|
-
// Commit planning artifacts (input.md, decisions.md
|
|
651
|
-
const newPlanPaths = newPlanFiles.map(f => path.join(plansDir, f));
|
|
630
|
+
// Commit planning artifacts (input.md, decisions.md only — plan files committed during execution)
|
|
652
631
|
await commitPlanningArtifacts(projectPath, {
|
|
653
632
|
cwd: worktreePath ?? undefined,
|
|
654
|
-
additionalFiles: newPlanPaths,
|
|
655
633
|
isAmend: true,
|
|
656
634
|
});
|
|
657
635
|
|
|
@@ -698,44 +676,3 @@ ${taskList}
|
|
|
698
676
|
# Describe what you want to add below:
|
|
699
677
|
`;
|
|
700
678
|
}
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* Display token usage summary for a plan/amend session.
|
|
704
|
-
* Parses the Claude session file and displays formatted usage data.
|
|
705
|
-
*/
|
|
706
|
-
function displayPlanSessionTokenSummary(sessionId: string, cwd: string): void {
|
|
707
|
-
const result = parseSessionById(sessionId, cwd);
|
|
708
|
-
|
|
709
|
-
if (!result.success) {
|
|
710
|
-
// Session file not found or couldn't be parsed - just log debug and continue
|
|
711
|
-
logger.debug(`Could not parse session file: ${result.error}`);
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// Check if there's any usage data
|
|
716
|
-
const totalTokens = result.usage.inputTokens + result.usage.outputTokens;
|
|
717
|
-
if (totalTokens === 0) {
|
|
718
|
-
logger.debug('No token usage data found in session file');
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// Create tracker and add the session as a single "task"
|
|
723
|
-
const pricingConfig = getPricingConfig();
|
|
724
|
-
const tracker = new TokenTracker(pricingConfig);
|
|
725
|
-
const entry = tracker.addTask('plan', [result.usage]);
|
|
726
|
-
|
|
727
|
-
// Get display options
|
|
728
|
-
const displayConfig = getDisplayConfig();
|
|
729
|
-
const options: TokenSummaryOptions = {
|
|
730
|
-
showCacheTokens: displayConfig.showCacheTokens,
|
|
731
|
-
showRateLimitEstimate: displayConfig.showRateLimitEstimate,
|
|
732
|
-
rateLimitPercentage: displayConfig.showRateLimitEstimate
|
|
733
|
-
? tracker.calculateRateLimitPercentage(entry.cost.totalCost)
|
|
734
|
-
: undefined,
|
|
735
|
-
};
|
|
736
|
-
|
|
737
|
-
// Display the summary
|
|
738
|
-
logger.newline();
|
|
739
|
-
const summary = formatTokenTotalSummary(result.usage, entry.cost, options);
|
|
740
|
-
console.log(summary);
|
|
741
|
-
}
|
|
@@ -31,12 +31,6 @@ export interface ClaudeRunnerOptions {
|
|
|
31
31
|
* Claude will still ask planning interview questions.
|
|
32
32
|
*/
|
|
33
33
|
dangerouslySkipPermissions?: boolean;
|
|
34
|
-
/**
|
|
35
|
-
* Session ID for Claude CLI. When provided, passed as --session-id to enable
|
|
36
|
-
* locating the session file after the session ends for token tracking.
|
|
37
|
-
* Only used in interactive mode (runInteractive).
|
|
38
|
-
*/
|
|
39
|
-
sessionId?: string;
|
|
40
34
|
/**
|
|
41
35
|
* Path to the outcome file. When provided, enables completion detection:
|
|
42
36
|
* - Monitors stdout for completion markers (<promise>COMPLETE/FAILED</promise>)
|
|
@@ -286,7 +280,7 @@ export class ClaudeRunner {
|
|
|
286
280
|
userMessage: string,
|
|
287
281
|
options: ClaudeRunnerOptions = {}
|
|
288
282
|
): Promise<number> {
|
|
289
|
-
const { cwd = process.cwd(), dangerouslySkipPermissions = false
|
|
283
|
+
const { cwd = process.cwd(), dangerouslySkipPermissions = false } = options;
|
|
290
284
|
|
|
291
285
|
return new Promise((resolve) => {
|
|
292
286
|
const args = ['--model', this.model];
|
|
@@ -296,11 +290,6 @@ export class ClaudeRunner {
|
|
|
296
290
|
args.push('--dangerously-skip-permissions');
|
|
297
291
|
}
|
|
298
292
|
|
|
299
|
-
// Add --session-id if provided (for token tracking)
|
|
300
|
-
if (sessionId) {
|
|
301
|
-
args.push('--session-id', sessionId);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
293
|
// System instructions via --append-system-prompt
|
|
305
294
|
args.push('--append-system-prompt', systemPrompt);
|
|
306
295
|
|
package/src/core/worktree.ts
CHANGED
|
@@ -442,6 +442,11 @@ export interface SyncMainBranchResult {
|
|
|
442
442
|
error?: string;
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
export interface RebaseResult {
|
|
446
|
+
success: boolean;
|
|
447
|
+
error?: string;
|
|
448
|
+
}
|
|
449
|
+
|
|
445
450
|
/**
|
|
446
451
|
* Detect the main branch name from the remote.
|
|
447
452
|
* Uses refs/remotes/origin/HEAD, falling back to main/master.
|
|
@@ -531,7 +536,7 @@ export function pullMainBranch(cwd?: string): SyncMainBranchResult {
|
|
|
531
536
|
});
|
|
532
537
|
logger.debug(`Fetched origin/${mainBranch} (local ${mainBranch} diverged, not updated)`);
|
|
533
538
|
return {
|
|
534
|
-
success:
|
|
539
|
+
success: false,
|
|
535
540
|
mainBranch,
|
|
536
541
|
hadChanges: false,
|
|
537
542
|
error: `Local ${mainBranch} has diverged from origin, not updated`,
|
|
@@ -662,3 +667,34 @@ export function pushMainBranch(cwd?: string): SyncMainBranchResult {
|
|
|
662
667
|
};
|
|
663
668
|
}
|
|
664
669
|
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Rebase the current branch onto the main branch.
|
|
673
|
+
* If the rebase fails with conflicts, aborts the rebase and returns failure.
|
|
674
|
+
*
|
|
675
|
+
* @param mainBranch - The main branch name to rebase onto (e.g., 'main' or 'master')
|
|
676
|
+
* @param cwd - The directory to run git commands in (defaults to current directory)
|
|
677
|
+
*/
|
|
678
|
+
export function rebaseOntoMain(mainBranch: string, cwd: string): RebaseResult {
|
|
679
|
+
try {
|
|
680
|
+
execSync(`git rebase ${mainBranch}`, {
|
|
681
|
+
encoding: 'utf-8',
|
|
682
|
+
stdio: 'pipe',
|
|
683
|
+
cwd,
|
|
684
|
+
});
|
|
685
|
+
return { success: true };
|
|
686
|
+
} catch (error) {
|
|
687
|
+
// Abort the failed rebase to restore clean state
|
|
688
|
+
try {
|
|
689
|
+
execSync('git rebase --abort', {
|
|
690
|
+
encoding: 'utf-8',
|
|
691
|
+
stdio: 'pipe',
|
|
692
|
+
cwd,
|
|
693
|
+
});
|
|
694
|
+
} catch {
|
|
695
|
+
// Ignore abort errors
|
|
696
|
+
}
|
|
697
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
698
|
+
return { success: false, error: msg };
|
|
699
|
+
}
|
|
700
|
+
}
|
package/src/prompts/amend.ts
CHANGED
|
@@ -135,9 +135,10 @@ After interviewing the user about all NEW tasks, create plan files starting from
|
|
|
135
135
|
- ${projectPath}/plans/${encodeTaskId(nextTaskNumber + 1)}-task-name.md
|
|
136
136
|
- etc.
|
|
137
137
|
|
|
138
|
-
Each plan file MUST have Obsidian-style frontmatter at the top, before the \`# Task:\` heading. The frontmatter format
|
|
138
|
+
Each plan file MUST have Obsidian-style frontmatter at the top, before the \`# Task:\` heading. The frontmatter uses standard YAML format with opening and closing \`---\` delimiters:
|
|
139
139
|
|
|
140
140
|
\`\`\`markdown
|
|
141
|
+
---
|
|
141
142
|
effort: medium
|
|
142
143
|
---
|
|
143
144
|
# Task: [Task Name]
|
|
@@ -186,6 +187,7 @@ The \`effort\` field is REQUIRED in every plan file. It indicates task complexit
|
|
|
186
187
|
|
|
187
188
|
Optionally, you can add an explicit \`model\` field to override the effort-based model selection:
|
|
188
189
|
\`\`\`markdown
|
|
190
|
+
---
|
|
189
191
|
effort: medium
|
|
190
192
|
model: opus
|
|
191
193
|
---
|
package/src/prompts/planning.ts
CHANGED
|
@@ -80,9 +80,10 @@ After interviewing the user about all tasks, create plan files in the plans fold
|
|
|
80
80
|
- ${projectPath}/plans/02-task-name.md
|
|
81
81
|
- etc.
|
|
82
82
|
|
|
83
|
-
Each plan file MUST have Obsidian-style frontmatter at the top, before the \`# Task:\` heading. The frontmatter format
|
|
83
|
+
Each plan file MUST have Obsidian-style frontmatter at the top, before the \`# Task:\` heading. The frontmatter uses standard YAML format with opening and closing \`---\` delimiters:
|
|
84
84
|
|
|
85
85
|
\`\`\`markdown
|
|
86
|
+
---
|
|
86
87
|
effort: medium
|
|
87
88
|
---
|
|
88
89
|
# Task: [Task Name]
|
|
@@ -128,6 +129,7 @@ The \`effort\` field is REQUIRED in every plan file. It indicates task complexit
|
|
|
128
129
|
|
|
129
130
|
Optionally, you can add an explicit \`model\` field to override the effort-based model selection:
|
|
130
131
|
\`\`\`markdown
|
|
132
|
+
---
|
|
131
133
|
effort: medium
|
|
132
134
|
model: opus
|
|
133
135
|
---
|
package/src/utils/frontmatter.ts
CHANGED
|
@@ -26,10 +26,20 @@ export interface FrontmatterParseResult {
|
|
|
26
26
|
/**
|
|
27
27
|
* Parse Obsidian-style frontmatter from plan file content.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* Supports two formats:
|
|
30
|
+
* 1. Standard format (preferred): `---` delimiter at the top and bottom
|
|
31
|
+
* 2. Legacy format (backward compatibility): properties followed by closing `---` only
|
|
31
32
|
*
|
|
32
|
-
*
|
|
33
|
+
* Standard format example:
|
|
34
|
+
* ```
|
|
35
|
+
* ---
|
|
36
|
+
* effort: medium
|
|
37
|
+
* model: sonnet
|
|
38
|
+
* ---
|
|
39
|
+
* # Task: ...
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* Legacy format example:
|
|
33
43
|
* ```
|
|
34
44
|
* effort: medium
|
|
35
45
|
* model: sonnet
|
|
@@ -50,15 +60,35 @@ export function parsePlanFrontmatter(content: string): FrontmatterParseResult {
|
|
|
50
60
|
warnings: [],
|
|
51
61
|
};
|
|
52
62
|
|
|
53
|
-
|
|
54
|
-
const delimiterIndex = content.indexOf('---');
|
|
55
|
-
if (delimiterIndex === -1) {
|
|
56
|
-
// No delimiter found - no frontmatter
|
|
57
|
-
return result;
|
|
58
|
-
}
|
|
63
|
+
const trimmedContent = content.trimStart();
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
let frontmatterSection: string;
|
|
66
|
+
|
|
67
|
+
if (trimmedContent.startsWith('---')) {
|
|
68
|
+
// Standard format: ---\nkey: value\n---
|
|
69
|
+
const afterOpener = trimmedContent.substring(3);
|
|
70
|
+
// Skip the rest of the opener line (handles "---\n" or "--- \n")
|
|
71
|
+
const openerEnd = afterOpener.indexOf('\n');
|
|
72
|
+
if (openerEnd === -1) {
|
|
73
|
+
// No newline after opening delimiter - no valid frontmatter
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
const rest = afterOpener.substring(openerEnd + 1);
|
|
77
|
+
const closerIndex = rest.indexOf('---');
|
|
78
|
+
if (closerIndex === -1) {
|
|
79
|
+
// No closing delimiter - no valid frontmatter
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
frontmatterSection = rest.substring(0, closerIndex);
|
|
83
|
+
} else {
|
|
84
|
+
// Legacy format: key: value\n---
|
|
85
|
+
const delimiterIndex = content.indexOf('---');
|
|
86
|
+
if (delimiterIndex === -1) {
|
|
87
|
+
// No delimiter found - no frontmatter
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
frontmatterSection = content.substring(0, delimiterIndex);
|
|
91
|
+
}
|
|
62
92
|
|
|
63
93
|
// Parse key: value lines
|
|
64
94
|
const lines = frontmatterSection.split('\n');
|
|
@@ -3,7 +3,9 @@ import { logger } from './logger.js';
|
|
|
3
3
|
import { sanitizeProjectName } from './validation.js';
|
|
4
4
|
import { getModel } from './config.js';
|
|
5
5
|
|
|
6
|
-
const NAME_GENERATION_PROMPT = `
|
|
6
|
+
const NAME_GENERATION_PROMPT = `Output ONLY the kebab-case name. No introduction, no explanation, no quotes.
|
|
7
|
+
|
|
8
|
+
Generate a short, punchy, creative project name (1-3 words, kebab-case).
|
|
7
9
|
|
|
8
10
|
Be creative! Use metaphors, analogies, or evocative words that capture the SPIRIT of the project.
|
|
9
11
|
Don't literally describe what it does - make it memorable and fun.
|
|
@@ -15,26 +17,15 @@ Good examples:
|
|
|
15
17
|
- Refactoring → 'spring-cleaning', 'phoenix', 'makeover'
|
|
16
18
|
- New feature → 'moonshot', 'secret-sauce', 'magic-wand'
|
|
17
19
|
|
|
18
|
-
Output ONLY the kebab-case name. No quotes, no explanation.
|
|
19
|
-
|
|
20
20
|
Project description:`;
|
|
21
21
|
|
|
22
|
-
const MULTI_NAME_GENERATION_PROMPT = `
|
|
23
|
-
|
|
24
|
-
IMPORTANT: Each name should use a DIFFERENT naming style:
|
|
25
|
-
1. **Metaphorical** - Use a metaphor or analogy (e.g., 'phoenix', 'lighthouse', 'compass')
|
|
26
|
-
2. **Fun/Playful** - Make it fun or quirky (e.g., 'turbo-boost', 'magic-beans', 'ninja-move')
|
|
27
|
-
3. **Action-oriented** - Focus on what it does with flair (e.g., 'bug-squasher', 'speed-demon', 'data-whisperer')
|
|
28
|
-
4. **Abstract** - Use abstract/poetic concepts (e.g., 'horizon', 'cascade', 'catalyst')
|
|
29
|
-
5. **Cultural reference** - Reference pop culture, mythology, or literature (e.g., 'atlas', 'merlin', 'gandalf')
|
|
22
|
+
const MULTI_NAME_GENERATION_PROMPT = `Output EXACTLY 5 project names, one per line. Do NOT include any introduction, explanation, preamble, numbering, or quotes.
|
|
30
23
|
|
|
31
24
|
Rules:
|
|
32
|
-
- Each name
|
|
33
|
-
-
|
|
25
|
+
- Each name: 1-3 words, kebab-case, lowercase with hyphens only
|
|
26
|
+
- Use varied styles: metaphorical, playful, action-oriented, abstract, cultural reference
|
|
34
27
|
- Make them memorable and evocative
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
Output format: ONLY output 5 names, one per line, no numbers, no explanations, no quotes.
|
|
28
|
+
- For projects with many unrelated tasks, prefer abstract/metaphorical names
|
|
38
29
|
|
|
39
30
|
Project description:`;
|
|
40
31
|
|
|
@@ -177,7 +177,7 @@ function formatTokenLine(
|
|
|
177
177
|
indent: string = ' ',
|
|
178
178
|
options: TokenSummaryOptions = {}
|
|
179
179
|
): string {
|
|
180
|
-
const { showCacheTokens = true, showRateLimitEstimate =
|
|
180
|
+
const { showCacheTokens = true, showRateLimitEstimate = true, rateLimitPercentage } = options;
|
|
181
181
|
const parts: string[] = [];
|
|
182
182
|
const tokenPart = `${formatNumber(usage.inputTokens)} in / ${formatNumber(usage.outputTokens)} out`;
|
|
183
183
|
parts.push(prefix ? `${prefix}: ${tokenPart}` : `Tokens: ${tokenPart}`);
|
|
@@ -255,7 +255,7 @@ export function formatTokenTotalSummary(
|
|
|
255
255
|
cost: CostBreakdown,
|
|
256
256
|
options: TokenSummaryOptions = {}
|
|
257
257
|
): string {
|
|
258
|
-
const { showCacheTokens = true, showRateLimitEstimate =
|
|
258
|
+
const { showCacheTokens = true, showRateLimitEstimate = true, rateLimitPercentage } = options;
|
|
259
259
|
const lines: string[] = [];
|
|
260
260
|
const divider = '── Token Usage Summary ──────────────────';
|
|
261
261
|
lines.push(divider);
|
|
@@ -162,13 +162,9 @@ describe('commitPlanningArtifacts - worktree integration', () => {
|
|
|
162
162
|
'# Task: New Task'
|
|
163
163
|
);
|
|
164
164
|
|
|
165
|
-
// Call commitPlanningArtifacts
|
|
166
|
-
const additionalFiles = [
|
|
167
|
-
path.join(wtProjectPath, 'plans', '02-new-task.md'),
|
|
168
|
-
];
|
|
165
|
+
// Call commitPlanningArtifacts (plan files not included in amend commit)
|
|
169
166
|
await commitPlanningArtifacts(wtProjectPath, {
|
|
170
167
|
cwd: worktreePath,
|
|
171
|
-
additionalFiles,
|
|
172
168
|
isAmend: true,
|
|
173
169
|
});
|
|
174
170
|
|
|
@@ -176,11 +172,11 @@ describe('commitPlanningArtifacts - worktree integration', () => {
|
|
|
176
172
|
const lastMsg = getLastCommitMessage(worktreePath);
|
|
177
173
|
expect(lastMsg).toMatch(/RAF\[aatest\] Amend: my-project/);
|
|
178
174
|
|
|
179
|
-
// Verify
|
|
175
|
+
// Verify only input.md and decisions.md are in the commit (not plan files)
|
|
180
176
|
const committedFiles = getLastCommitFiles(worktreePath);
|
|
181
177
|
expect(committedFiles).toContain(`RAF/${projectFolder}/input.md`);
|
|
182
178
|
expect(committedFiles).toContain(`RAF/${projectFolder}/decisions.md`);
|
|
183
|
-
expect(committedFiles).toContain(`RAF/${projectFolder}/plans/02-new-task.md`);
|
|
179
|
+
expect(committedFiles).not.toContain(`RAF/${projectFolder}/plans/02-new-task.md`);
|
|
184
180
|
});
|
|
185
181
|
|
|
186
182
|
it('should commit after worktree recreation from branch', async () => {
|
|
@@ -234,13 +230,9 @@ describe('commitPlanningArtifacts - worktree integration', () => {
|
|
|
234
230
|
'# Task: New Task'
|
|
235
231
|
);
|
|
236
232
|
|
|
237
|
-
// Call commitPlanningArtifacts
|
|
238
|
-
const additionalFiles = [
|
|
239
|
-
path.join(recreatedProjectPath, 'plans', '02-new-task.md'),
|
|
240
|
-
];
|
|
233
|
+
// Call commitPlanningArtifacts (plan files not included in amend commit)
|
|
241
234
|
await commitPlanningArtifacts(recreatedProjectPath, {
|
|
242
235
|
cwd: recreatedWtPath,
|
|
243
|
-
additionalFiles,
|
|
244
236
|
isAmend: true,
|
|
245
237
|
});
|
|
246
238
|
|
|
@@ -248,11 +240,11 @@ describe('commitPlanningArtifacts - worktree integration', () => {
|
|
|
248
240
|
const lastMsg = getLastCommitMessage(recreatedWtPath);
|
|
249
241
|
expect(lastMsg).toMatch(/RAF\[aatest\] Amend: my-project/);
|
|
250
242
|
|
|
251
|
-
// Verify
|
|
243
|
+
// Verify only input.md and decisions.md are in the commit (not plan files)
|
|
252
244
|
const committedFiles = getLastCommitFiles(recreatedWtPath);
|
|
253
245
|
expect(committedFiles).toContain(`RAF/${projectFolder}/input.md`);
|
|
254
246
|
expect(committedFiles).toContain(`RAF/${projectFolder}/decisions.md`);
|
|
255
|
-
expect(committedFiles).toContain(`RAF/${projectFolder}/plans/02-new-task.md`);
|
|
247
|
+
expect(committedFiles).not.toContain(`RAF/${projectFolder}/plans/02-new-task.md`);
|
|
256
248
|
});
|
|
257
249
|
|
|
258
250
|
it('should work when only some files have changed', async () => {
|
|
@@ -252,7 +252,7 @@ describe('commitPlanningArtifacts', () => {
|
|
|
252
252
|
);
|
|
253
253
|
});
|
|
254
254
|
|
|
255
|
-
it('should stage
|
|
255
|
+
it('should not stage plan files in amend mode', async () => {
|
|
256
256
|
mockExecSync.mockImplementation((cmd: unknown) => {
|
|
257
257
|
const cmdStr = cmd as string;
|
|
258
258
|
if (cmdStr.includes('rev-parse')) {
|
|
@@ -262,32 +262,24 @@ describe('commitPlanningArtifacts', () => {
|
|
|
262
262
|
return '';
|
|
263
263
|
}
|
|
264
264
|
if (cmdStr.includes('git diff --cached')) {
|
|
265
|
-
return 'RAF/aaaaar-decision-vault/input.md\
|
|
265
|
+
return 'RAF/aaaaar-decision-vault/input.md\n';
|
|
266
266
|
}
|
|
267
267
|
return '';
|
|
268
268
|
});
|
|
269
269
|
|
|
270
|
-
const additionalFiles = [
|
|
271
|
-
'/Users/test/RAF/aaaaar-decision-vault/plans/04-new-task.md',
|
|
272
|
-
'/Users/test/RAF/aaaaar-decision-vault/plans/05-another-task.md',
|
|
273
|
-
];
|
|
274
|
-
|
|
275
270
|
await commitPlanningArtifacts('/Users/test/RAF/aaaaar-decision-vault', {
|
|
276
|
-
additionalFiles,
|
|
277
271
|
isAmend: true,
|
|
278
272
|
});
|
|
279
273
|
|
|
280
|
-
// Verify git add called for
|
|
274
|
+
// Verify git add called for only 2 files (input, decisions)
|
|
281
275
|
const addCalls = mockExecSync.mock.calls.filter(
|
|
282
276
|
(call) => (call[0] as string).includes('git add')
|
|
283
277
|
);
|
|
284
|
-
expect(addCalls.length).toBe(
|
|
278
|
+
expect(addCalls.length).toBe(2);
|
|
285
279
|
|
|
286
280
|
const addCmds = addCalls.map((c) => c[0] as string);
|
|
287
281
|
expect(addCmds.some((cmd) => cmd.includes('input.md'))).toBe(true);
|
|
288
282
|
expect(addCmds.some((cmd) => cmd.includes('decisions.md'))).toBe(true);
|
|
289
|
-
expect(addCmds.some((cmd) => cmd.includes('04-new-task.md'))).toBe(true);
|
|
290
|
-
expect(addCmds.some((cmd) => cmd.includes('05-another-task.md'))).toBe(true);
|
|
291
283
|
});
|
|
292
284
|
|
|
293
285
|
it('should pass cwd to isGitRepo for worktree support', async () => {
|
|
@@ -50,6 +50,18 @@ describe('Config Command', () => {
|
|
|
50
50
|
expect(resetOption).toBeDefined();
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
it('should have a --get option', () => {
|
|
54
|
+
const cmd = createConfigCommand();
|
|
55
|
+
const getOption = cmd.options.find((o) => o.long === '--get');
|
|
56
|
+
expect(getOption).toBeDefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should have a --set option', () => {
|
|
60
|
+
const cmd = createConfigCommand();
|
|
61
|
+
const setOption = cmd.options.find((o) => o.long === '--set');
|
|
62
|
+
expect(setOption).toBeDefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
53
65
|
it('should register in a parent program', () => {
|
|
54
66
|
const program = new Command();
|
|
55
67
|
program.addCommand(createConfigCommand());
|
|
@@ -239,4 +251,162 @@ describe('Config Command', () => {
|
|
|
239
251
|
expect(config2.timeout).toBe(120);
|
|
240
252
|
});
|
|
241
253
|
});
|
|
254
|
+
|
|
255
|
+
describe('--get flag', () => {
|
|
256
|
+
it('should return full config when no key is provided', () => {
|
|
257
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
258
|
+
fs.writeFileSync(configPath, JSON.stringify({ timeout: 120 }, null, 2));
|
|
259
|
+
|
|
260
|
+
const config = resolveConfig(configPath);
|
|
261
|
+
expect(config.timeout).toBe(120);
|
|
262
|
+
expect(config.models.execute).toBe(DEFAULT_CONFIG.models.execute);
|
|
263
|
+
|
|
264
|
+
// Verify full config has all expected top-level keys
|
|
265
|
+
expect(config).toHaveProperty('models');
|
|
266
|
+
expect(config).toHaveProperty('effortMapping');
|
|
267
|
+
expect(config).toHaveProperty('timeout');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should return specific value for dot-notation key', () => {
|
|
271
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
272
|
+
fs.writeFileSync(configPath, JSON.stringify({ models: { plan: 'sonnet' } }, null, 2));
|
|
273
|
+
|
|
274
|
+
const config = resolveConfig(configPath);
|
|
275
|
+
expect(config.models.plan).toBe('sonnet');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should handle nested keys', () => {
|
|
279
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
280
|
+
fs.writeFileSync(configPath, JSON.stringify({ display: { showRateLimitEstimate: false } }, null, 2));
|
|
281
|
+
|
|
282
|
+
const config = resolveConfig(configPath);
|
|
283
|
+
expect(config.display.showRateLimitEstimate).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should handle deeply nested pricing keys', () => {
|
|
287
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
288
|
+
fs.writeFileSync(configPath, JSON.stringify({ pricing: { opus: { inputPerMTok: 20 } } }, null, 2));
|
|
289
|
+
|
|
290
|
+
const config = resolveConfig(configPath);
|
|
291
|
+
expect(config.pricing.opus.inputPerMTok).toBe(20);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe('--set flag', () => {
|
|
296
|
+
it('should set a string value', () => {
|
|
297
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
298
|
+
|
|
299
|
+
// Start with empty config
|
|
300
|
+
expect(fs.existsSync(configPath)).toBe(false);
|
|
301
|
+
|
|
302
|
+
// Simulate setting models.plan to sonnet
|
|
303
|
+
const userConfig: Record<string, unknown> = {};
|
|
304
|
+
const keys = 'models.plan'.split('.');
|
|
305
|
+
let current: Record<string, unknown> = userConfig;
|
|
306
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
307
|
+
const key = keys[i]!;
|
|
308
|
+
current[key] = {};
|
|
309
|
+
current = current[key] as Record<string, unknown>;
|
|
310
|
+
}
|
|
311
|
+
current[keys[keys.length - 1]!] = 'sonnet';
|
|
312
|
+
|
|
313
|
+
fs.writeFileSync(configPath, JSON.stringify(userConfig, null, 2));
|
|
314
|
+
|
|
315
|
+
const config = resolveConfig(configPath);
|
|
316
|
+
expect(config.models.plan).toBe('sonnet');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should set a number value', () => {
|
|
320
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
321
|
+
|
|
322
|
+
const userConfig = { timeout: 120 };
|
|
323
|
+
fs.writeFileSync(configPath, JSON.stringify(userConfig, null, 2));
|
|
324
|
+
|
|
325
|
+
const config = resolveConfig(configPath);
|
|
326
|
+
expect(config.timeout).toBe(120);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should set a boolean value', () => {
|
|
330
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
331
|
+
|
|
332
|
+
const userConfig = { autoCommit: false };
|
|
333
|
+
fs.writeFileSync(configPath, JSON.stringify(userConfig, null, 2));
|
|
334
|
+
|
|
335
|
+
const config = resolveConfig(configPath);
|
|
336
|
+
expect(config.autoCommit).toBe(false);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should remove key when value matches default', () => {
|
|
340
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
341
|
+
|
|
342
|
+
// Set a non-default value first
|
|
343
|
+
fs.writeFileSync(configPath, JSON.stringify({ models: { plan: 'sonnet' } }, null, 2));
|
|
344
|
+
let config = resolveConfig(configPath);
|
|
345
|
+
expect(config.models.plan).toBe('sonnet');
|
|
346
|
+
|
|
347
|
+
// Now set back to default (opus)
|
|
348
|
+
fs.writeFileSync(configPath, JSON.stringify({}, null, 2));
|
|
349
|
+
config = resolveConfig(configPath);
|
|
350
|
+
expect(config.models.plan).toBe(DEFAULT_CONFIG.models.plan);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should remove empty parent objects after key removal', () => {
|
|
354
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
355
|
+
|
|
356
|
+
// Start with a models override
|
|
357
|
+
const userConfig = { models: { plan: 'sonnet' } };
|
|
358
|
+
fs.writeFileSync(configPath, JSON.stringify(userConfig, null, 2));
|
|
359
|
+
|
|
360
|
+
// Remove the override (simulating setting to default)
|
|
361
|
+
fs.writeFileSync(configPath, JSON.stringify({}, null, 2));
|
|
362
|
+
|
|
363
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
364
|
+
const parsed = JSON.parse(content);
|
|
365
|
+
|
|
366
|
+
// Should be empty object
|
|
367
|
+
expect(Object.keys(parsed).length).toBe(0);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should validate config after modification', () => {
|
|
371
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
372
|
+
|
|
373
|
+
// Valid config
|
|
374
|
+
const validConfig = { models: { execute: 'sonnet' } };
|
|
375
|
+
fs.writeFileSync(configPath, JSON.stringify(validConfig, null, 2));
|
|
376
|
+
expect(() => validateConfig(validConfig)).not.toThrow();
|
|
377
|
+
|
|
378
|
+
// Invalid config
|
|
379
|
+
const invalidConfig = { models: { execute: 'invalid-model' } };
|
|
380
|
+
fs.writeFileSync(configPath, JSON.stringify(invalidConfig, null, 2));
|
|
381
|
+
expect(() => validateConfig(invalidConfig)).toThrow(ConfigValidationError);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should delete config file when it becomes empty', () => {
|
|
385
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
386
|
+
|
|
387
|
+
// Create a config file
|
|
388
|
+
fs.writeFileSync(configPath, JSON.stringify({ timeout: 120 }, null, 2));
|
|
389
|
+
expect(fs.existsSync(configPath)).toBe(true);
|
|
390
|
+
|
|
391
|
+
// Simulate removing all keys (setting everything to defaults)
|
|
392
|
+
fs.writeFileSync(configPath, JSON.stringify({}, null, 2));
|
|
393
|
+
|
|
394
|
+
// Check if file still exists (in the actual implementation, empty configs are deleted)
|
|
395
|
+
// For this test, we just verify the file operations work
|
|
396
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
397
|
+
expect(JSON.parse(content)).toEqual({});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should handle nested value updates', () => {
|
|
401
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
402
|
+
|
|
403
|
+
// Set a nested value
|
|
404
|
+
const userConfig = { display: { showRateLimitEstimate: false } };
|
|
405
|
+
fs.writeFileSync(configPath, JSON.stringify(userConfig, null, 2));
|
|
406
|
+
|
|
407
|
+
const config = resolveConfig(configPath);
|
|
408
|
+
expect(config.display.showRateLimitEstimate).toBe(false);
|
|
409
|
+
expect(config.display.showCacheTokens).toBe(DEFAULT_CONFIG.display.showCacheTokens);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
242
412
|
});
|