rafcode 2.2.0 → 2.4.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 +19 -4
- package/RAF/ahtahs-token-reaper/decisions.md +37 -0
- package/RAF/ahtahs-token-reaper/input.md +20 -0
- package/RAF/ahtahs-token-reaper/outcomes/01-extend-token-tracker-data-model.md +42 -0
- package/RAF/ahtahs-token-reaper/outcomes/02-accumulate-usage-in-retry-loop.md +31 -0
- package/RAF/ahtahs-token-reaper/outcomes/03-per-attempt-display-formatting.md +60 -0
- package/RAF/ahtahs-token-reaper/outcomes/04-add-model-name-to-claude-call-logs.md +57 -0
- package/RAF/ahtahs-token-reaper/outcomes/05-handle-invalid-config-in-raf-config.md +46 -0
- package/RAF/ahtahs-token-reaper/outcomes/06-fix-verbose-toggle-timer-display.md +38 -0
- package/RAF/ahtahs-token-reaper/plans/01-extend-token-tracker-data-model.md +36 -0
- package/RAF/ahtahs-token-reaper/plans/02-accumulate-usage-in-retry-loop.md +36 -0
- package/RAF/ahtahs-token-reaper/plans/03-per-attempt-display-formatting.md +43 -0
- package/RAF/ahtahs-token-reaper/plans/04-add-model-name-to-claude-call-logs.md +38 -0
- package/RAF/ahtahs-token-reaper/plans/05-handle-invalid-config-in-raf-config.md +36 -0
- package/RAF/ahtahs-token-reaper/plans/06-fix-verbose-toggle-timer-display.md +40 -0
- package/RAF/ahvrih-rate-forge/decisions.md +70 -0
- package/RAF/ahvrih-rate-forge/input.md +44 -0
- package/RAF/ahvrih-rate-forge/outcomes/01-remove-claude-command-config.md +58 -0
- package/RAF/ahvrih-rate-forge/outcomes/02-fix-mixed-attempt-cost.md +46 -0
- package/RAF/ahvrih-rate-forge/outcomes/03-rate-limit-estimation.md +82 -0
- package/RAF/ahvrih-rate-forge/outcomes/04-show-version-in-do-logs.md +45 -0
- package/RAF/ahvrih-rate-forge/outcomes/05-sync-main-before-worktree.md +96 -0
- package/RAF/ahvrih-rate-forge/outcomes/06-sync-readme-with-codebase.md +45 -0
- package/RAF/ahvrih-rate-forge/outcomes/07-no-session-persistence.md +26 -0
- package/RAF/ahvrih-rate-forge/outcomes/08-plan-execution-metadata.md +130 -0
- package/RAF/ahvrih-rate-forge/plans/01-remove-claude-command-config.md +36 -0
- package/RAF/ahvrih-rate-forge/plans/02-fix-mixed-attempt-cost.md +33 -0
- package/RAF/ahvrih-rate-forge/plans/03-rate-limit-estimation.md +82 -0
- package/RAF/ahvrih-rate-forge/plans/04-show-version-in-do-logs.md +32 -0
- package/RAF/ahvrih-rate-forge/plans/05-sync-main-before-worktree.md +40 -0
- package/RAF/ahvrih-rate-forge/plans/06-sync-readme-with-codebase.md +61 -0
- package/RAF/ahvrih-rate-forge/plans/07-no-session-persistence.md +28 -0
- package/RAF/ahvrih-rate-forge/plans/08-plan-execution-metadata.md +123 -0
- package/README.md +27 -7
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +24 -7
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/do.d.ts.map +1 -1
- package/dist/commands/do.js +122 -27
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +79 -3
- package/dist/commands/plan.js.map +1 -1
- package/dist/core/claude-runner.d.ts +6 -6
- package/dist/core/claude-runner.d.ts.map +1 -1
- package/dist/core/claude-runner.js +9 -10
- package/dist/core/claude-runner.js.map +1 -1
- package/dist/core/failure-analyzer.d.ts.map +1 -1
- package/dist/core/failure-analyzer.js +3 -3
- package/dist/core/failure-analyzer.js.map +1 -1
- package/dist/core/pull-request.d.ts.map +1 -1
- package/dist/core/pull-request.js +5 -3
- package/dist/core/pull-request.js.map +1 -1
- package/dist/core/state-derivation.d.ts +5 -0
- package/dist/core/state-derivation.d.ts.map +1 -1
- package/dist/core/state-derivation.js +14 -4
- package/dist/core/state-derivation.js.map +1 -1
- package/dist/core/worktree.d.ts +32 -0
- package/dist/core/worktree.d.ts.map +1 -1
- package/dist/core/worktree.js +215 -0
- package/dist/core/worktree.js.map +1 -1
- package/dist/prompts/amend.d.ts.map +1 -1
- package/dist/prompts/amend.js +26 -11
- package/dist/prompts/amend.js.map +1 -1
- package/dist/prompts/planning.d.ts.map +1 -1
- package/dist/prompts/planning.js +26 -11
- package/dist/prompts/planning.js.map +1 -1
- package/dist/types/config.d.ts +30 -13
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +14 -10
- package/dist/types/config.js.map +1 -1
- package/dist/utils/config.d.ts +53 -4
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +197 -30
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +43 -0
- package/dist/utils/frontmatter.d.ts.map +1 -0
- package/dist/utils/frontmatter.js +85 -0
- package/dist/utils/frontmatter.js.map +1 -0
- package/dist/utils/name-generator.d.ts.map +1 -1
- package/dist/utils/name-generator.js +2 -3
- package/dist/utils/name-generator.js.map +1 -1
- package/dist/utils/session-parser.d.ts +44 -0
- package/dist/utils/session-parser.d.ts.map +1 -0
- package/dist/utils/session-parser.js +122 -0
- package/dist/utils/session-parser.js.map +1 -0
- package/dist/utils/terminal-symbols.d.ts +28 -5
- package/dist/utils/terminal-symbols.d.ts.map +1 -1
- package/dist/utils/terminal-symbols.js +77 -18
- package/dist/utils/terminal-symbols.js.map +1 -1
- package/dist/utils/token-tracker.d.ts +31 -1
- package/dist/utils/token-tracker.d.ts.map +1 -1
- package/dist/utils/token-tracker.js +94 -4
- package/dist/utils/token-tracker.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/config.ts +26 -7
- package/src/commands/do.ts +157 -29
- package/src/commands/plan.ts +89 -2
- package/src/core/claude-runner.ts +16 -17
- package/src/core/failure-analyzer.ts +3 -3
- package/src/core/pull-request.ts +5 -3
- package/src/core/state-derivation.ts +20 -4
- package/src/core/worktree.ts +230 -0
- package/src/prompts/amend.ts +26 -11
- package/src/prompts/config-docs.md +91 -29
- package/src/prompts/planning.ts +26 -11
- package/src/types/config.ts +46 -21
- package/src/utils/config.ts +222 -33
- package/src/utils/frontmatter.ts +110 -0
- package/src/utils/name-generator.ts +2 -3
- package/src/utils/session-parser.ts +161 -0
- package/src/utils/terminal-symbols.ts +105 -18
- package/src/utils/token-tracker.ts +109 -4
- package/tests/unit/claude-runner-interactive.test.ts +8 -6
- package/tests/unit/claude-runner.test.ts +5 -66
- package/tests/unit/config-command.test.ts +84 -5
- package/tests/unit/config.test.ts +292 -45
- package/tests/unit/frontmatter.test.ts +182 -0
- package/tests/unit/post-execution-picker.test.ts +5 -0
- package/tests/unit/session-parser.test.ts +301 -0
- package/tests/unit/terminal-symbols.test.ts +263 -33
- package/tests/unit/timer-verbose-integration.test.ts +170 -0
- package/tests/unit/token-tracker.test.ts +653 -17
- package/tests/unit/validation.test.ts +6 -4
- package/tests/unit/worktree.test.ts +242 -0
package/src/commands/config.ts
CHANGED
|
@@ -9,10 +9,12 @@ import { logger } from '../utils/logger.js';
|
|
|
9
9
|
import {
|
|
10
10
|
getConfigPath,
|
|
11
11
|
getModel,
|
|
12
|
-
|
|
12
|
+
getModelShortName,
|
|
13
13
|
validateConfig,
|
|
14
14
|
ConfigValidationError,
|
|
15
|
+
resetConfigCache,
|
|
15
16
|
} from '../utils/config.js';
|
|
17
|
+
import { DEFAULT_CONFIG } from '../types/config.js';
|
|
16
18
|
|
|
17
19
|
interface ConfigCommandOptions {
|
|
18
20
|
reset?: boolean;
|
|
@@ -153,11 +155,28 @@ async function handleReset(): Promise<void> {
|
|
|
153
155
|
|
|
154
156
|
async function runConfigSession(initialPrompt?: string): Promise<void> {
|
|
155
157
|
const configPath = getConfigPath();
|
|
156
|
-
const model = getModel('config');
|
|
157
|
-
const effort = getEffort('config');
|
|
158
158
|
|
|
159
|
-
//
|
|
160
|
-
|
|
159
|
+
// Try to load config, but fall back to defaults if it's broken
|
|
160
|
+
// This allows raf config to be used to fix a broken config file
|
|
161
|
+
let model: string;
|
|
162
|
+
let configError: Error | null = null;
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
model = getModel('config');
|
|
166
|
+
} catch (error) {
|
|
167
|
+
// Config file has errors - fall back to defaults so the session can launch
|
|
168
|
+
configError = error instanceof Error ? error : new Error(String(error));
|
|
169
|
+
model = DEFAULT_CONFIG.models.config;
|
|
170
|
+
// Clear the cached config so subsequent calls don't use the broken cache
|
|
171
|
+
resetConfigCache();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Warn user if config has errors, before starting the session
|
|
175
|
+
if (configError) {
|
|
176
|
+
logger.warn(`Config file has errors, using defaults: ${configError.message}`);
|
|
177
|
+
logger.warn('Fix the config in this session or run `raf config --reset` to start fresh.');
|
|
178
|
+
logger.newline();
|
|
179
|
+
}
|
|
161
180
|
|
|
162
181
|
// Load config docs
|
|
163
182
|
let configDocs: string;
|
|
@@ -181,8 +200,8 @@ async function runConfigSession(initialPrompt?: string): Promise<void> {
|
|
|
181
200
|
shutdownHandler.init();
|
|
182
201
|
shutdownHandler.registerClaudeRunner(claudeRunner);
|
|
183
202
|
|
|
184
|
-
|
|
185
|
-
logger.info(`
|
|
203
|
+
const configModel = getModelShortName(model);
|
|
204
|
+
logger.info(`Starting config session with ${configModel}...`);
|
|
186
205
|
logger.newline();
|
|
187
206
|
|
|
188
207
|
try {
|
package/src/commands/do.ts
CHANGED
|
@@ -13,7 +13,9 @@ import { getRafDir, extractProjectNumber, extractProjectName, extractTaskNameFro
|
|
|
13
13
|
import { pickPendingProject, getPendingProjects, getPendingWorktreeProjects } from '../ui/project-picker.js';
|
|
14
14
|
import type { PendingProjectInfo } from '../ui/project-picker.js';
|
|
15
15
|
import { logger } from '../utils/logger.js';
|
|
16
|
-
import { getConfig,
|
|
16
|
+
import { getConfig, getWorktreeDefault, getModel, getModelShortName, resolveFullModelId, getSyncMainBranch, resolveEffortToModel, applyModelCeiling } from '../utils/config.js';
|
|
17
|
+
import type { PlanFrontmatter } from '../utils/frontmatter.js';
|
|
18
|
+
import { getVersion } from '../utils/version.js';
|
|
17
19
|
import { createTaskTimer, formatElapsedTime } from '../utils/timer.js';
|
|
18
20
|
import { createStatusLine } from '../utils/status-line.js';
|
|
19
21
|
import {
|
|
@@ -49,6 +51,8 @@ import {
|
|
|
49
51
|
mergeWorktreeBranch,
|
|
50
52
|
removeWorktree,
|
|
51
53
|
resolveWorktreeProjectByIdentifier,
|
|
54
|
+
pushMainBranch,
|
|
55
|
+
pullMainBranch,
|
|
52
56
|
} from '../core/worktree.js';
|
|
53
57
|
import { createPullRequest, prPreflight } from '../core/pull-request.js';
|
|
54
58
|
import type { DoCommandOptions } from '../types/config.js';
|
|
@@ -61,6 +65,74 @@ import type { DoCommandOptions } from '../types/config.js';
|
|
|
61
65
|
*/
|
|
62
66
|
export type PostExecutionAction = 'merge' | 'pr' | 'leave';
|
|
63
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Result of resolving a task's model from frontmatter.
|
|
70
|
+
*/
|
|
71
|
+
interface TaskModelResolution {
|
|
72
|
+
/** The resolved model (after ceiling is applied). */
|
|
73
|
+
model: string;
|
|
74
|
+
/** Whether a warning should be logged about missing frontmatter. */
|
|
75
|
+
missingFrontmatter: boolean;
|
|
76
|
+
/** Frontmatter parsing warnings to log. */
|
|
77
|
+
warnings: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the execution model for a task from its frontmatter metadata.
|
|
82
|
+
*
|
|
83
|
+
* Resolution order:
|
|
84
|
+
* 1. Explicit `model` in frontmatter (subject to ceiling)
|
|
85
|
+
* 2. `effort` in frontmatter resolved via effortMapping (subject to ceiling)
|
|
86
|
+
* 3. Fallback to models.execute (the ceiling, with a warning)
|
|
87
|
+
*
|
|
88
|
+
* @param frontmatter - Parsed frontmatter from the plan file
|
|
89
|
+
* @param frontmatterWarnings - Warnings from frontmatter parsing
|
|
90
|
+
* @param ceilingModel - The ceiling model (usually models.execute from config)
|
|
91
|
+
* @param isRetry - Whether this is a retry attempt (escalates to ceiling)
|
|
92
|
+
*/
|
|
93
|
+
function resolveTaskModel(
|
|
94
|
+
frontmatter: PlanFrontmatter | undefined,
|
|
95
|
+
frontmatterWarnings: string[] | undefined,
|
|
96
|
+
ceilingModel: string,
|
|
97
|
+
isRetry: boolean,
|
|
98
|
+
): TaskModelResolution {
|
|
99
|
+
const warnings = frontmatterWarnings ? [...frontmatterWarnings] : [];
|
|
100
|
+
|
|
101
|
+
// Retry escalation: always use the ceiling model on retry
|
|
102
|
+
if (isRetry) {
|
|
103
|
+
return { model: ceilingModel, missingFrontmatter: false, warnings };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// No frontmatter - fallback to ceiling with warning
|
|
107
|
+
if (!frontmatter) {
|
|
108
|
+
return {
|
|
109
|
+
model: ceilingModel,
|
|
110
|
+
missingFrontmatter: true,
|
|
111
|
+
warnings,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Explicit model in frontmatter - apply ceiling
|
|
116
|
+
if (frontmatter.model) {
|
|
117
|
+
const model = applyModelCeiling(frontmatter.model, ceilingModel);
|
|
118
|
+
return { model, missingFrontmatter: false, warnings };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Effort-based resolution - apply ceiling
|
|
122
|
+
if (frontmatter.effort) {
|
|
123
|
+
const mappedModel = resolveEffortToModel(frontmatter.effort);
|
|
124
|
+
const model = applyModelCeiling(mappedModel, ceilingModel);
|
|
125
|
+
return { model, missingFrontmatter: false, warnings };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Frontmatter present but no effort or model - fallback to ceiling with warning
|
|
129
|
+
return {
|
|
130
|
+
model: ceilingModel,
|
|
131
|
+
missingFrontmatter: true,
|
|
132
|
+
warnings,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
64
136
|
/**
|
|
65
137
|
* Format failure history for console output.
|
|
66
138
|
* Shows attempts that failed before eventual success or final failure.
|
|
@@ -166,6 +238,18 @@ async function runDoCommand(projectIdentifierArg: string | undefined, options: D
|
|
|
166
238
|
// Record original branch before any worktree operations
|
|
167
239
|
originalBranch = getCurrentBranch() ?? undefined;
|
|
168
240
|
|
|
241
|
+
// Sync main branch before worktree operations (if enabled)
|
|
242
|
+
if (getSyncMainBranch()) {
|
|
243
|
+
const syncResult = pullMainBranch();
|
|
244
|
+
if (syncResult.success) {
|
|
245
|
+
if (syncResult.hadChanges) {
|
|
246
|
+
logger.info(`Synced ${syncResult.mainBranch} from remote`);
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
logger.warn(`Could not sync main branch: ${syncResult.error}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
169
253
|
if (!projectIdentifier) {
|
|
170
254
|
// Auto-discovery flow
|
|
171
255
|
const selected = await discoverAndPickWorktreeProject(repoBasename, rafDir, rafRelativePath);
|
|
@@ -394,7 +478,6 @@ async function runDoCommand(projectIdentifierArg: string | undefined, options: D
|
|
|
394
478
|
force,
|
|
395
479
|
maxRetries,
|
|
396
480
|
autoCommit,
|
|
397
|
-
showModel: true,
|
|
398
481
|
model,
|
|
399
482
|
worktreeCwd: worktreeRoot,
|
|
400
483
|
}
|
|
@@ -500,6 +583,19 @@ async function executePostAction(
|
|
|
500
583
|
|
|
501
584
|
case 'pr': {
|
|
502
585
|
logger.newline();
|
|
586
|
+
|
|
587
|
+
// Push main branch to remote before PR creation (if enabled)
|
|
588
|
+
if (getSyncMainBranch()) {
|
|
589
|
+
const syncResult = pushMainBranch();
|
|
590
|
+
if (syncResult.success) {
|
|
591
|
+
if (syncResult.hadChanges) {
|
|
592
|
+
logger.info(`Pushed ${syncResult.mainBranch} to remote`);
|
|
593
|
+
}
|
|
594
|
+
} else {
|
|
595
|
+
logger.warn(`Could not push main branch: ${syncResult.error}`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
503
599
|
logger.info(`Creating PR for branch "${worktreeBranch}"...`);
|
|
504
600
|
|
|
505
601
|
const prResult = await createPullRequest(worktreeBranch, projectPath, { cwd: worktreeRoot });
|
|
@@ -658,7 +754,6 @@ interface SingleProjectOptions {
|
|
|
658
754
|
force: boolean;
|
|
659
755
|
maxRetries: number;
|
|
660
756
|
autoCommit: boolean;
|
|
661
|
-
showModel: boolean;
|
|
662
757
|
model: string;
|
|
663
758
|
/** Worktree root directory. When set, Claude runs with cwd in the worktree. */
|
|
664
759
|
worktreeCwd?: string;
|
|
@@ -669,7 +764,7 @@ async function executeSingleProject(
|
|
|
669
764
|
projectName: string,
|
|
670
765
|
options: SingleProjectOptions
|
|
671
766
|
): Promise<ProjectExecutionResult> {
|
|
672
|
-
const { timeout, verbose, debug, force, maxRetries, autoCommit,
|
|
767
|
+
const { timeout, verbose, debug, force, maxRetries, autoCommit, model, worktreeCwd } = options;
|
|
673
768
|
|
|
674
769
|
if (!validatePlansExist(projectPath)) {
|
|
675
770
|
return {
|
|
@@ -709,11 +804,12 @@ async function executeSingleProject(
|
|
|
709
804
|
: state.tasks.filter((t) => t.status !== 'completed').map((t) => t.id)
|
|
710
805
|
);
|
|
711
806
|
|
|
712
|
-
// Set up shutdown handler
|
|
713
|
-
const claudeRunner = new ClaudeRunner({ model });
|
|
807
|
+
// Set up shutdown handler - we'll register runners dynamically per-task
|
|
714
808
|
const projectManager = new ProjectManager();
|
|
715
809
|
shutdownHandler.init();
|
|
716
|
-
|
|
810
|
+
|
|
811
|
+
// The ceiling model for all tasks (can be overridden per-task, subject to this ceiling)
|
|
812
|
+
const ceilingModel = model;
|
|
717
813
|
|
|
718
814
|
// Initialize token tracker for usage reporting
|
|
719
815
|
const tokenTracker = new TokenTracker();
|
|
@@ -725,15 +821,13 @@ async function executeSingleProject(
|
|
|
725
821
|
// Start project timer
|
|
726
822
|
const projectStartTime = Date.now();
|
|
727
823
|
|
|
824
|
+
// Resolve and display version + ceiling model info (before any tasks run)
|
|
825
|
+
const fullCeilingModelId = resolveFullModelId(ceilingModel);
|
|
826
|
+
logger.dim(`RAF v${getVersion()} | Ceiling: ${fullCeilingModelId}`);
|
|
827
|
+
|
|
728
828
|
if (verbose) {
|
|
729
829
|
logger.info(`Executing project: ${projectName}`);
|
|
730
830
|
logger.info(`Tasks: ${state.tasks.length}, Task timeout: ${timeout} minutes`);
|
|
731
|
-
|
|
732
|
-
// Log Claude model name
|
|
733
|
-
if (showModel && model) {
|
|
734
|
-
logger.info(`Using model: ${model}`);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
831
|
logger.newline();
|
|
738
832
|
} else {
|
|
739
833
|
// Minimal mode: show project header
|
|
@@ -905,28 +999,63 @@ async function executeSingleProject(
|
|
|
905
999
|
let attempts = 0;
|
|
906
1000
|
let lastOutput = '';
|
|
907
1001
|
let failureReason = '';
|
|
908
|
-
|
|
1002
|
+
// Collect usage data from all attempts (for accurate token tracking across retries)
|
|
1003
|
+
const attemptUsageData: import('../types/config.js').UsageData[] = [];
|
|
909
1004
|
// Track failure history for each attempt (attempt number -> reason)
|
|
910
1005
|
const failureHistory: Array<{ attempt: number; reason: string }> = [];
|
|
911
1006
|
|
|
912
1007
|
// Set up timer for elapsed time tracking
|
|
913
1008
|
const statusLine = createStatusLine();
|
|
914
1009
|
const timer = createTaskTimer(verbose ? undefined : (elapsed) => {
|
|
1010
|
+
// When verbose is toggled ON at runtime, clear the status line and skip updates
|
|
1011
|
+
if (verboseToggle.isVerbose) {
|
|
1012
|
+
statusLine.clear();
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
915
1015
|
// Show running status with task name and timer (updates in place)
|
|
916
1016
|
statusLine.update(formatTaskProgress(taskNumber, totalTasks, 'running', displayName, elapsed, taskId));
|
|
917
1017
|
});
|
|
918
1018
|
timer.start();
|
|
919
1019
|
|
|
1020
|
+
// Log frontmatter warnings once before the retry loop
|
|
1021
|
+
if (task.frontmatterWarnings && task.frontmatterWarnings.length > 0) {
|
|
1022
|
+
for (const warning of task.frontmatterWarnings) {
|
|
1023
|
+
logger.warn(` Frontmatter warning: ${warning}`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
920
1027
|
while (!success && attempts < maxRetries) {
|
|
921
1028
|
attempts++;
|
|
1029
|
+
const isRetry = attempts > 1;
|
|
1030
|
+
|
|
1031
|
+
// Resolve the model for this attempt (escalates to ceiling on retry)
|
|
1032
|
+
const modelResolution = resolveTaskModel(
|
|
1033
|
+
task.frontmatter,
|
|
1034
|
+
undefined, // warnings already logged above
|
|
1035
|
+
ceilingModel,
|
|
1036
|
+
isRetry,
|
|
1037
|
+
);
|
|
1038
|
+
|
|
1039
|
+
// Log missing frontmatter warning on first attempt only
|
|
1040
|
+
if (!isRetry && modelResolution.missingFrontmatter) {
|
|
1041
|
+
logger.warn(` No effort frontmatter found — using ceiling model`);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Create a runner for this attempt's model
|
|
1045
|
+
const taskRunner = new ClaudeRunner({ model: modelResolution.model });
|
|
1046
|
+
shutdownHandler.registerClaudeRunner(taskRunner);
|
|
922
1047
|
|
|
923
|
-
if (verbose &&
|
|
924
|
-
|
|
1048
|
+
if (verbose && isRetry) {
|
|
1049
|
+
const retryModel = resolveFullModelId(modelResolution.model);
|
|
1050
|
+
logger.info(` Retry ${attempts}/${maxRetries} for task ${taskLabel} (model: ${retryModel})...`);
|
|
1051
|
+
} else if (verbose && !isRetry) {
|
|
1052
|
+
const taskModel = resolveFullModelId(modelResolution.model);
|
|
1053
|
+
logger.info(` Model: ${taskModel}`);
|
|
925
1054
|
}
|
|
926
1055
|
|
|
927
1056
|
// Build execution prompt (inside loop to include retry context on retries)
|
|
928
1057
|
// Check if previous outcome file exists for retry context
|
|
929
|
-
const previousOutcomeFileForRetry =
|
|
1058
|
+
const previousOutcomeFileForRetry = isRetry && fs.existsSync(outcomeFilePath)
|
|
930
1059
|
? outcomeFilePath
|
|
931
1060
|
: undefined;
|
|
932
1061
|
|
|
@@ -955,22 +1084,20 @@ async function executeSingleProject(
|
|
|
955
1084
|
} : undefined;
|
|
956
1085
|
|
|
957
1086
|
// Run Claude (use worktree root as cwd if in worktree mode)
|
|
958
|
-
const executeEffort = getEffort('execute');
|
|
959
1087
|
const runnerOptions = {
|
|
960
1088
|
timeout,
|
|
961
1089
|
outcomeFilePath,
|
|
962
1090
|
commitContext,
|
|
963
1091
|
cwd: worktreeCwd,
|
|
964
|
-
effortLevel: executeEffort,
|
|
965
1092
|
verboseCheck: () => verboseToggle.isVerbose,
|
|
966
1093
|
};
|
|
967
1094
|
const result = verbose
|
|
968
|
-
? await
|
|
969
|
-
: await
|
|
1095
|
+
? await taskRunner.runVerbose(prompt, runnerOptions)
|
|
1096
|
+
: await taskRunner.run(prompt, runnerOptions);
|
|
970
1097
|
|
|
971
1098
|
lastOutput = result.output;
|
|
972
1099
|
if (result.usageData) {
|
|
973
|
-
|
|
1100
|
+
attemptUsageData.push(result.usageData);
|
|
974
1101
|
}
|
|
975
1102
|
|
|
976
1103
|
// Parse result
|
|
@@ -1088,9 +1215,9 @@ Task completed. No detailed report provided.
|
|
|
1088
1215
|
}
|
|
1089
1216
|
|
|
1090
1217
|
// Track and display token usage for this task
|
|
1091
|
-
if (
|
|
1092
|
-
const entry = tokenTracker.addTask(task.id,
|
|
1093
|
-
logger.dim(formatTaskTokenSummary(entry
|
|
1218
|
+
if (attemptUsageData.length > 0) {
|
|
1219
|
+
const entry = tokenTracker.addTask(task.id, attemptUsageData);
|
|
1220
|
+
logger.dim(formatTaskTokenSummary(entry, (u) => tokenTracker.calculateCost(u)));
|
|
1094
1221
|
}
|
|
1095
1222
|
|
|
1096
1223
|
completedInSession.add(task.id);
|
|
@@ -1108,16 +1235,17 @@ Task completed. No detailed report provided.
|
|
|
1108
1235
|
|
|
1109
1236
|
if (verbose) {
|
|
1110
1237
|
logger.error(` Task ${taskLabel} failed: ${failureReason} (${elapsedFormatted})`);
|
|
1111
|
-
|
|
1238
|
+
const analysisModel = getModelShortName(getModel('failureAnalysis'));
|
|
1239
|
+
logger.info(` Analyzing failure with ${analysisModel}...`);
|
|
1112
1240
|
} else {
|
|
1113
1241
|
// Minimal mode: show failed task line
|
|
1114
1242
|
logger.info(formatTaskProgress(taskNumber, totalTasks, 'failed', displayName, elapsedMs, task.id));
|
|
1115
1243
|
}
|
|
1116
1244
|
|
|
1117
1245
|
// Track token usage even for failed tasks (partial data still useful for totals)
|
|
1118
|
-
if (
|
|
1119
|
-
const entry = tokenTracker.addTask(task.id,
|
|
1120
|
-
logger.dim(formatTaskTokenSummary(entry
|
|
1246
|
+
if (attemptUsageData.length > 0) {
|
|
1247
|
+
const entry = tokenTracker.addTask(task.id, attemptUsageData);
|
|
1248
|
+
logger.dim(formatTaskTokenSummary(entry, (u) => tokenTracker.calculateCost(u)));
|
|
1121
1249
|
}
|
|
1122
1250
|
|
|
1123
1251
|
// Analyze failure and generate structured report
|
package/src/commands/plan.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import * as crypto from 'node:crypto';
|
|
3
4
|
import { Command } from 'commander';
|
|
4
5
|
import { ProjectManager } from '../core/project-manager.js';
|
|
5
6
|
import { ClaudeRunner } from '../core/claude-runner.js';
|
|
@@ -15,7 +16,10 @@ import {
|
|
|
15
16
|
resolveModelOption,
|
|
16
17
|
} from '../utils/validation.js';
|
|
17
18
|
import { logger } from '../utils/logger.js';
|
|
18
|
-
import { getWorktreeDefault } from '../utils/config.js';
|
|
19
|
+
import { getWorktreeDefault, getModel, getModelShortName, getDisplayConfig, getPricingConfig, getSyncMainBranch } from '../utils/config.js';
|
|
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';
|
|
19
23
|
import { generateProjectNames } from '../utils/name-generator.js';
|
|
20
24
|
import { pickProjectName } from '../ui/name-picker.js';
|
|
21
25
|
import {
|
|
@@ -48,6 +52,7 @@ import {
|
|
|
48
52
|
validateWorktree,
|
|
49
53
|
removeWorktree,
|
|
50
54
|
computeWorktreeBaseDir,
|
|
55
|
+
pullMainBranch,
|
|
51
56
|
} from '../core/worktree.js';
|
|
52
57
|
|
|
53
58
|
interface PlanCommandOptions {
|
|
@@ -155,7 +160,8 @@ async function runPlanCommand(projectName?: string, model?: string, autoMode: bo
|
|
|
155
160
|
// Get or generate project name
|
|
156
161
|
let finalProjectName = projectName;
|
|
157
162
|
if (!finalProjectName) {
|
|
158
|
-
|
|
163
|
+
const nameModel = getModelShortName(getModel('nameGeneration'));
|
|
164
|
+
logger.info(`Generating project name suggestions with ${nameModel}...`);
|
|
159
165
|
const suggestedNames = await generateProjectNames(cleanInput);
|
|
160
166
|
logger.newline();
|
|
161
167
|
|
|
@@ -184,6 +190,18 @@ async function runPlanCommand(projectName?: string, model?: string, autoMode: bo
|
|
|
184
190
|
const repoRoot = getRepoRoot()!;
|
|
185
191
|
const rafDir = getRafDir();
|
|
186
192
|
|
|
193
|
+
// Sync main branch before creating worktree (if enabled)
|
|
194
|
+
if (getSyncMainBranch()) {
|
|
195
|
+
const syncResult = pullMainBranch();
|
|
196
|
+
if (syncResult.success) {
|
|
197
|
+
if (syncResult.hadChanges) {
|
|
198
|
+
logger.info(`Synced ${syncResult.mainBranch} from remote`);
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
logger.warn(`Could not sync main branch: ${syncResult.error}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
187
205
|
// Compute project number from main repo's RAF directory
|
|
188
206
|
const projectNumber = getNextProjectNumber(rafDir);
|
|
189
207
|
const sanitizedName = sanitizeProjectName(finalProjectName);
|
|
@@ -271,17 +289,25 @@ async function runPlanCommand(projectName?: string, model?: string, autoMode: bo
|
|
|
271
289
|
worktreeMode,
|
|
272
290
|
});
|
|
273
291
|
|
|
292
|
+
// Generate session ID for token tracking
|
|
293
|
+
const sessionId = crypto.randomUUID();
|
|
294
|
+
const sessionCwd = worktreePath ?? process.cwd();
|
|
295
|
+
|
|
274
296
|
try {
|
|
275
297
|
const exitCode = await claudeRunner.runInteractive(systemPrompt, userMessage, {
|
|
276
298
|
dangerouslySkipPermissions: autoMode,
|
|
277
299
|
// Run Claude session in the worktree root if in worktree mode
|
|
278
300
|
cwd: worktreePath ?? undefined,
|
|
301
|
+
sessionId,
|
|
279
302
|
});
|
|
280
303
|
|
|
281
304
|
if (exitCode !== 0) {
|
|
282
305
|
logger.warn(`Claude exited with code ${exitCode}`);
|
|
283
306
|
}
|
|
284
307
|
|
|
308
|
+
// Parse session file and display token usage summary
|
|
309
|
+
displayPlanSessionTokenSummary(sessionId, sessionCwd);
|
|
310
|
+
|
|
285
311
|
// Check for created plan files
|
|
286
312
|
const plansDir = getPlansDir(projectPath);
|
|
287
313
|
const planFiles = fs.existsSync(plansDir)
|
|
@@ -411,6 +437,18 @@ async function runAmendCommand(identifier: string, model?: string, autoMode: boo
|
|
|
411
437
|
logger.info(`Recreated worktree from branch: ${folderName}`);
|
|
412
438
|
} else {
|
|
413
439
|
// No branch — create fresh worktree and copy project files
|
|
440
|
+
// Sync main branch before creating worktree (if enabled)
|
|
441
|
+
if (getSyncMainBranch()) {
|
|
442
|
+
const syncResult = pullMainBranch();
|
|
443
|
+
if (syncResult.success) {
|
|
444
|
+
if (syncResult.hadChanges) {
|
|
445
|
+
logger.info(`Synced ${syncResult.mainBranch} from remote`);
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
logger.warn(`Could not sync main branch: ${syncResult.error}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
414
452
|
const result = createWorktree(repoBasename, folderName);
|
|
415
453
|
if (!result.success) {
|
|
416
454
|
logger.error(`Failed to create worktree: ${result.error}`);
|
|
@@ -566,17 +604,25 @@ async function runAmendCommand(identifier: string, model?: string, autoMode: boo
|
|
|
566
604
|
worktreeMode,
|
|
567
605
|
});
|
|
568
606
|
|
|
607
|
+
// Generate session ID for token tracking
|
|
608
|
+
const sessionId = crypto.randomUUID();
|
|
609
|
+
const sessionCwd = worktreePath ?? process.cwd();
|
|
610
|
+
|
|
569
611
|
try {
|
|
570
612
|
const exitCode = await claudeRunner.runInteractive(systemPrompt, userMessage, {
|
|
571
613
|
dangerouslySkipPermissions: autoMode,
|
|
572
614
|
// Run Claude session in the worktree root if in worktree mode
|
|
573
615
|
cwd: worktreePath ?? undefined,
|
|
616
|
+
sessionId,
|
|
574
617
|
});
|
|
575
618
|
|
|
576
619
|
if (exitCode !== 0) {
|
|
577
620
|
logger.warn(`Claude exited with code ${exitCode}`);
|
|
578
621
|
}
|
|
579
622
|
|
|
623
|
+
// Parse session file and display token usage summary
|
|
624
|
+
displayPlanSessionTokenSummary(sessionId, sessionCwd);
|
|
625
|
+
|
|
580
626
|
// Check for new plan files
|
|
581
627
|
const allPlanFiles = fs.existsSync(plansDir)
|
|
582
628
|
? fs.readdirSync(plansDir).filter((f) => f.endsWith('.md')).sort()
|
|
@@ -652,3 +698,44 @@ ${taskList}
|
|
|
652
698
|
# Describe what you want to add below:
|
|
653
699
|
`;
|
|
654
700
|
}
|
|
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
|
+
}
|
|
@@ -6,12 +6,11 @@ import { logger } from '../utils/logger.js';
|
|
|
6
6
|
import { renderStreamEvent } from '../parsers/stream-renderer.js';
|
|
7
7
|
import type { UsageData } from '../types/config.js';
|
|
8
8
|
import { getHeadCommitHash, getHeadCommitMessage, isFileCommittedInHead } from './git.js';
|
|
9
|
-
import {
|
|
9
|
+
import { getModel } from '../utils/config.js';
|
|
10
10
|
|
|
11
11
|
function getClaudePath(): string {
|
|
12
|
-
const cmd = getClaudeCommand();
|
|
13
12
|
try {
|
|
14
|
-
return execSync(
|
|
13
|
+
return execSync('which claude', { encoding: 'utf-8' }).trim();
|
|
15
14
|
} catch {
|
|
16
15
|
throw new Error('Claude CLI not found. Please ensure it is installed and in your PATH.');
|
|
17
16
|
}
|
|
@@ -32,6 +31,12 @@ export interface ClaudeRunnerOptions {
|
|
|
32
31
|
* Claude will still ask planning interview questions.
|
|
33
32
|
*/
|
|
34
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;
|
|
35
40
|
/**
|
|
36
41
|
* Path to the outcome file. When provided, enables completion detection:
|
|
37
42
|
* - Monitors stdout for completion markers (<promise>COMPLETE/FAILED</promise>)
|
|
@@ -53,12 +58,6 @@ export interface ClaudeRunnerOptions {
|
|
|
53
58
|
/** Path to the outcome file that should be committed. */
|
|
54
59
|
outcomeFilePath: string;
|
|
55
60
|
};
|
|
56
|
-
/**
|
|
57
|
-
* Claude Code reasoning effort level.
|
|
58
|
-
* Sets CLAUDE_CODE_EFFORT_LEVEL env var for the spawned process.
|
|
59
|
-
* Only applied in non-interactive modes (run, runVerbose).
|
|
60
|
-
*/
|
|
61
|
-
effortLevel?: 'low' | 'medium' | 'high';
|
|
62
61
|
/**
|
|
63
62
|
* Dynamic verbose display callback. When provided, called for each stream event
|
|
64
63
|
* to determine whether to write display output to stdout. Overrides the static
|
|
@@ -287,7 +286,7 @@ export class ClaudeRunner {
|
|
|
287
286
|
userMessage: string,
|
|
288
287
|
options: ClaudeRunnerOptions = {}
|
|
289
288
|
): Promise<number> {
|
|
290
|
-
const { cwd = process.cwd(), dangerouslySkipPermissions = false } = options;
|
|
289
|
+
const { cwd = process.cwd(), dangerouslySkipPermissions = false, sessionId } = options;
|
|
291
290
|
|
|
292
291
|
return new Promise((resolve) => {
|
|
293
292
|
const args = ['--model', this.model];
|
|
@@ -297,6 +296,11 @@ export class ClaudeRunner {
|
|
|
297
296
|
args.push('--dangerously-skip-permissions');
|
|
298
297
|
}
|
|
299
298
|
|
|
299
|
+
// Add --session-id if provided (for token tracking)
|
|
300
|
+
if (sessionId) {
|
|
301
|
+
args.push('--session-id', sessionId);
|
|
302
|
+
}
|
|
303
|
+
|
|
300
304
|
// System instructions via --append-system-prompt
|
|
301
305
|
args.push('--append-system-prompt', systemPrompt);
|
|
302
306
|
|
|
@@ -415,7 +419,7 @@ export class ClaudeRunner {
|
|
|
415
419
|
options: ClaudeRunnerOptions,
|
|
416
420
|
verbose: boolean,
|
|
417
421
|
): Promise<RunResult> {
|
|
418
|
-
const { timeout = 60, cwd = process.cwd(), outcomeFilePath, commitContext,
|
|
422
|
+
const { timeout = 60, cwd = process.cwd(), outcomeFilePath, commitContext, verboseCheck } = options;
|
|
419
423
|
// Ensure timeout is a positive number, fallback to 60 minutes
|
|
420
424
|
const validatedTimeout = Number(timeout) > 0 ? Number(timeout) : 60;
|
|
421
425
|
const timeoutMs = validatedTimeout * 60 * 1000;
|
|
@@ -437,11 +441,6 @@ export class ClaudeRunner {
|
|
|
437
441
|
logger.debug(`Prompt length: ${prompt.length}, timeout: ${timeoutMs}ms, cwd: ${cwd}`);
|
|
438
442
|
logger.debug(`Claude path: ${claudePath}`);
|
|
439
443
|
|
|
440
|
-
// Build env, optionally injecting effort level
|
|
441
|
-
const env = effortLevel
|
|
442
|
-
? { ...process.env, CLAUDE_CODE_EFFORT_LEVEL: effortLevel }
|
|
443
|
-
: process.env;
|
|
444
|
-
|
|
445
444
|
logger.debug('Spawning process...');
|
|
446
445
|
// Use --output-format stream-json --verbose to get real-time streaming events
|
|
447
446
|
// including tool calls, file operations, and token usage in the result event.
|
|
@@ -460,7 +459,7 @@ export class ClaudeRunner {
|
|
|
460
459
|
'Execute the task as described in the system prompt.',
|
|
461
460
|
], {
|
|
462
461
|
cwd,
|
|
463
|
-
env,
|
|
462
|
+
env: process.env,
|
|
464
463
|
stdio: ['ignore', 'pipe', 'pipe'], // no stdin needed
|
|
465
464
|
});
|
|
466
465
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
-
import { getModel
|
|
3
|
+
import { getModel } from '../utils/config.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Failure types that can be detected programmatically without using the API.
|
|
@@ -213,9 +213,8 @@ function extractRelevantOutput(output: string, maxLines: number): string {
|
|
|
213
213
|
* Get the path to Claude CLI.
|
|
214
214
|
*/
|
|
215
215
|
function getClaudePath(): string {
|
|
216
|
-
const cmd = getClaudeCommand();
|
|
217
216
|
try {
|
|
218
|
-
return execSync(
|
|
217
|
+
return execSync('which claude', { encoding: 'utf-8' }).trim();
|
|
219
218
|
} catch {
|
|
220
219
|
throw new Error('Claude CLI not found. Please ensure it is installed and in your PATH.');
|
|
221
220
|
}
|
|
@@ -312,6 +311,7 @@ Respond with ONLY a markdown report in this exact format:
|
|
|
312
311
|
const failureModel = getModel('failureAnalysis');
|
|
313
312
|
const proc = spawn(claudePath, [
|
|
314
313
|
'--model', failureModel,
|
|
314
|
+
'--no-session-persistence',
|
|
315
315
|
'--dangerously-skip-permissions',
|
|
316
316
|
'-p',
|
|
317
317
|
prompt,
|