rafcode 2.1.0 → 2.2.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 +4 -1
- package/CLAUDE.md +59 -11
- package/RAF/ahslfe-config-wizard/decisions.md +34 -0
- package/RAF/ahslfe-config-wizard/input.md +1 -0
- package/RAF/ahslfe-config-wizard/outcomes/01-define-config-schema.md +38 -0
- package/RAF/ahslfe-config-wizard/outcomes/02-refactor-codebase-to-use-config.md +67 -0
- package/RAF/ahslfe-config-wizard/outcomes/03-create-config-documentation.md +37 -0
- package/RAF/ahslfe-config-wizard/outcomes/04-implement-raf-config-command.md +47 -0
- package/RAF/ahslfe-config-wizard/outcomes/05-update-claude-md.md +26 -0
- package/RAF/ahslfe-config-wizard/plans/01-define-config-schema.md +73 -0
- package/RAF/ahslfe-config-wizard/plans/02-refactor-codebase-to-use-config.md +74 -0
- package/RAF/ahslfe-config-wizard/plans/03-create-config-documentation.md +57 -0
- package/RAF/ahslfe-config-wizard/plans/04-implement-raf-config-command.md +66 -0
- package/RAF/ahslfe-config-wizard/plans/05-update-claude-md.md +60 -0
- package/RAF/ahstvo-token-tracker/decisions.md +44 -0
- package/RAF/ahstvo-token-tracker/input.md +3 -0
- package/RAF/ahstvo-token-tracker/outcomes/01-full-model-id-support.md +43 -0
- package/RAF/ahstvo-token-tracker/outcomes/02-name-generation-no-session.md +33 -0
- package/RAF/ahstvo-token-tracker/outcomes/03-unify-stream-json-execution.md +48 -0
- package/RAF/ahstvo-token-tracker/outcomes/04-token-tracking-cost-calculation.md +53 -0
- package/RAF/ahstvo-token-tracker/outcomes/05-token-cost-console-reporting.md +57 -0
- package/RAF/ahstvo-token-tracker/outcomes/06-runtime-verbose-toggle.md +53 -0
- package/RAF/ahstvo-token-tracker/outcomes/07-readme-config-docs.md +36 -0
- package/RAF/ahstvo-token-tracker/plans/01-full-model-id-support.md +35 -0
- package/RAF/ahstvo-token-tracker/plans/02-name-generation-no-session.md +36 -0
- package/RAF/ahstvo-token-tracker/plans/03-unify-stream-json-execution.md +44 -0
- package/RAF/ahstvo-token-tracker/plans/04-token-tracking-cost-calculation.md +56 -0
- package/RAF/ahstvo-token-tracker/plans/05-token-cost-console-reporting.md +55 -0
- package/RAF/ahstvo-token-tracker/plans/06-runtime-verbose-toggle.md +48 -0
- package/RAF/ahstvo-token-tracker/plans/07-readme-config-docs.md +44 -0
- package/README.md +34 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +173 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/do.d.ts.map +1 -1
- package/dist/commands/do.js +50 -28
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +3 -2
- package/dist/commands/plan.js.map +1 -1
- package/dist/core/claude-runner.d.ts +17 -13
- package/dist/core/claude-runner.d.ts.map +1 -1
- package/dist/core/claude-runner.js +42 -257
- 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 +6 -3
- package/dist/core/failure-analyzer.js.map +1 -1
- package/dist/core/git.d.ts.map +1 -1
- package/dist/core/git.js +10 -3
- package/dist/core/git.js.map +1 -1
- package/dist/core/pull-request.d.ts +1 -1
- package/dist/core/pull-request.d.ts.map +1 -1
- package/dist/core/pull-request.js +7 -4
- package/dist/core/pull-request.js.map +1 -1
- package/dist/core/shutdown-handler.d.ts.map +1 -1
- package/dist/core/shutdown-handler.js +0 -4
- package/dist/core/shutdown-handler.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/parsers/stream-renderer.d.ts +16 -4
- package/dist/parsers/stream-renderer.d.ts.map +1 -1
- package/dist/parsers/stream-renderer.js +35 -5
- package/dist/parsers/stream-renderer.js.map +1 -1
- package/dist/prompts/execution.d.ts.map +1 -1
- package/dist/prompts/execution.js +11 -1
- package/dist/prompts/execution.js.map +1 -1
- package/dist/types/config.d.ts +95 -5
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +63 -3
- package/dist/types/config.js.map +1 -1
- package/dist/utils/config.d.ts +59 -7
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +276 -21
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/name-generator.d.ts +3 -7
- package/dist/utils/name-generator.d.ts.map +1 -1
- package/dist/utils/name-generator.js +75 -61
- package/dist/utils/name-generator.js.map +1 -1
- package/dist/utils/terminal-symbols.d.ts +21 -0
- package/dist/utils/terminal-symbols.d.ts.map +1 -1
- package/dist/utils/terminal-symbols.js +62 -0
- package/dist/utils/terminal-symbols.js.map +1 -1
- package/dist/utils/token-tracker.d.ts +45 -0
- package/dist/utils/token-tracker.d.ts.map +1 -0
- package/dist/utils/token-tracker.js +107 -0
- package/dist/utils/token-tracker.js.map +1 -0
- package/dist/utils/validation.d.ts +5 -5
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +10 -6
- package/dist/utils/validation.js.map +1 -1
- package/dist/utils/verbose-toggle.d.ts +33 -0
- package/dist/utils/verbose-toggle.d.ts.map +1 -0
- package/dist/utils/verbose-toggle.js +94 -0
- package/dist/utils/verbose-toggle.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/config.ts +204 -0
- package/src/commands/do.ts +59 -27
- package/src/commands/plan.ts +3 -2
- package/src/core/claude-runner.ts +58 -311
- package/src/core/failure-analyzer.ts +6 -3
- package/src/core/git.ts +10 -3
- package/src/core/pull-request.ts +7 -4
- package/src/core/shutdown-handler.ts +0 -5
- package/src/index.ts +2 -0
- package/src/parsers/stream-renderer.ts +55 -8
- package/src/prompts/config-docs.md +331 -0
- package/src/prompts/execution.ts +13 -1
- package/src/types/config.ts +156 -8
- package/src/utils/config.ts +335 -21
- package/src/utils/name-generator.ts +84 -71
- package/src/utils/terminal-symbols.ts +68 -0
- package/src/utils/token-tracker.ts +135 -0
- package/src/utils/validation.ts +15 -10
- package/src/utils/verbose-toggle.ts +103 -0
- package/tests/unit/claude-runner.test.ts +216 -403
- package/tests/unit/config-command.test.ts +163 -0
- package/tests/unit/config.test.ts +608 -30
- package/tests/unit/name-generator.test.ts +99 -75
- package/tests/unit/pull-request.test.ts +2 -0
- package/tests/unit/stream-renderer.test.ts +83 -30
- package/tests/unit/terminal-symbols.test.ts +157 -0
- package/tests/unit/token-tracker.test.ts +352 -0
- package/tests/unit/verbose-toggle.test.ts +204 -0
- package/RAF/ahrtxf-session-sentinel/decisions.md +0 -19
- package/RAF/ahrtxf-session-sentinel/input.md +0 -1
- package/RAF/ahrtxf-session-sentinel/outcomes/01-capture-session-id.md +0 -37
- package/RAF/ahrtxf-session-sentinel/outcomes/02-resume-flag.md +0 -45
- package/RAF/ahrtxf-session-sentinel/plans/01-capture-session-id.md +0 -41
- package/RAF/ahrtxf-session-sentinel/plans/02-resume-flag.md +0 -51
|
@@ -4,11 +4,14 @@ import type { IDisposable } from 'node-pty';
|
|
|
4
4
|
import { execSync, spawn } from 'node:child_process';
|
|
5
5
|
import { logger } from '../utils/logger.js';
|
|
6
6
|
import { renderStreamEvent } from '../parsers/stream-renderer.js';
|
|
7
|
+
import type { UsageData } from '../types/config.js';
|
|
7
8
|
import { getHeadCommitHash, getHeadCommitMessage, isFileCommittedInHead } from './git.js';
|
|
9
|
+
import { getClaudeCommand, getModel } from '../utils/config.js';
|
|
8
10
|
|
|
9
11
|
function getClaudePath(): string {
|
|
12
|
+
const cmd = getClaudeCommand();
|
|
10
13
|
try {
|
|
11
|
-
return execSync(
|
|
14
|
+
return execSync(`which ${cmd}`, { encoding: 'utf-8' }).trim();
|
|
12
15
|
} catch {
|
|
13
16
|
throw new Error('Claude CLI not found. Please ensure it is installed and in your PATH.');
|
|
14
17
|
}
|
|
@@ -56,6 +59,12 @@ export interface ClaudeRunnerOptions {
|
|
|
56
59
|
* Only applied in non-interactive modes (run, runVerbose).
|
|
57
60
|
*/
|
|
58
61
|
effortLevel?: 'low' | 'medium' | 'high';
|
|
62
|
+
/**
|
|
63
|
+
* Dynamic verbose display callback. When provided, called for each stream event
|
|
64
|
+
* to determine whether to write display output to stdout. Overrides the static
|
|
65
|
+
* verbose parameter in _runStreamJson. Used by the runtime verbose toggle.
|
|
66
|
+
*/
|
|
67
|
+
verboseCheck?: () => boolean;
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
export interface ClaudeRunnerConfig {
|
|
@@ -71,7 +80,8 @@ export interface RunResult {
|
|
|
71
80
|
exitCode: number;
|
|
72
81
|
timedOut: boolean;
|
|
73
82
|
contextOverflow: boolean;
|
|
74
|
-
|
|
83
|
+
/** Token usage data from the stream-json result event. */
|
|
84
|
+
usageData?: UsageData;
|
|
75
85
|
}
|
|
76
86
|
|
|
77
87
|
const CONTEXT_OVERFLOW_PATTERNS = [
|
|
@@ -259,18 +269,9 @@ export class ClaudeRunner {
|
|
|
259
269
|
private activeProcess: pty.IPty | null = null;
|
|
260
270
|
private killed = false;
|
|
261
271
|
private model: string;
|
|
262
|
-
private _sessionId: string | undefined;
|
|
263
272
|
|
|
264
273
|
constructor(config: ClaudeRunnerConfig = {}) {
|
|
265
|
-
this.model = config.model ?? '
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Get the session ID captured from the most recent Claude session.
|
|
270
|
-
* Available after the system.init event is received during run() or runVerbose().
|
|
271
|
-
*/
|
|
272
|
-
get sessionId(): string | undefined {
|
|
273
|
-
return this._sessionId;
|
|
274
|
+
this.model = config.model ?? getModel('execute');
|
|
274
275
|
}
|
|
275
276
|
|
|
276
277
|
/**
|
|
@@ -372,8 +373,8 @@ export class ClaudeRunner {
|
|
|
372
373
|
|
|
373
374
|
/**
|
|
374
375
|
* Run Claude non-interactively and collect output.
|
|
375
|
-
*
|
|
376
|
-
*
|
|
376
|
+
* Uses stream-json format internally to capture token usage data.
|
|
377
|
+
* Tool display is suppressed (non-verbose mode).
|
|
377
378
|
*
|
|
378
379
|
* TIMEOUT BEHAVIOR:
|
|
379
380
|
* - The timeout is applied per individual call to this method
|
|
@@ -383,147 +384,7 @@ export class ClaudeRunner {
|
|
|
383
384
|
* - Default timeout is 60 minutes if not specified
|
|
384
385
|
*/
|
|
385
386
|
async run(prompt: string, options: ClaudeRunnerOptions = {}): Promise<RunResult> {
|
|
386
|
-
|
|
387
|
-
// Ensure timeout is a positive number, fallback to 60 minutes
|
|
388
|
-
const validatedTimeout = Number(timeout) > 0 ? Number(timeout) : 60;
|
|
389
|
-
const timeoutMs = validatedTimeout * 60 * 1000;
|
|
390
|
-
|
|
391
|
-
return new Promise((resolve) => {
|
|
392
|
-
let output = '';
|
|
393
|
-
let stderr = '';
|
|
394
|
-
let timedOut = false;
|
|
395
|
-
let contextOverflow = false;
|
|
396
|
-
let sessionId: string | undefined;
|
|
397
|
-
|
|
398
|
-
const claudePath = getClaudePath();
|
|
399
|
-
|
|
400
|
-
logger.debug(`Starting Claude execution session with model: ${this.model}`);
|
|
401
|
-
logger.debug(`Claude path: ${claudePath}`);
|
|
402
|
-
|
|
403
|
-
// Build env, optionally injecting effort level
|
|
404
|
-
const env = effortLevel
|
|
405
|
-
? { ...process.env, CLAUDE_CODE_EFFORT_LEVEL: effortLevel }
|
|
406
|
-
: process.env;
|
|
407
|
-
|
|
408
|
-
// Use --output-format stream-json to get structured events including session_id.
|
|
409
|
-
// Unlike runVerbose(), output is parsed silently without writing to stdout.
|
|
410
|
-
// --dangerously-skip-permissions bypasses interactive prompts
|
|
411
|
-
// -p enables print mode (non-interactive)
|
|
412
|
-
const proc = spawn(claudePath, [
|
|
413
|
-
'--dangerously-skip-permissions',
|
|
414
|
-
'--model',
|
|
415
|
-
this.model,
|
|
416
|
-
'--append-system-prompt',
|
|
417
|
-
prompt,
|
|
418
|
-
'--output-format',
|
|
419
|
-
'stream-json',
|
|
420
|
-
'--verbose',
|
|
421
|
-
'-p',
|
|
422
|
-
'Execute the task as described in the system prompt.',
|
|
423
|
-
], {
|
|
424
|
-
cwd,
|
|
425
|
-
env,
|
|
426
|
-
stdio: ['ignore', 'pipe', 'pipe'], // no stdin needed
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
// Track this process
|
|
430
|
-
this.activeProcess = proc as any;
|
|
431
|
-
|
|
432
|
-
// Set up timeout
|
|
433
|
-
const timeoutHandle = setTimeout(() => {
|
|
434
|
-
timedOut = true;
|
|
435
|
-
logger.warn('Claude session timed out');
|
|
436
|
-
if (sessionId) {
|
|
437
|
-
logger.info(`Session ID: ${sessionId}`);
|
|
438
|
-
}
|
|
439
|
-
proc.kill('SIGTERM');
|
|
440
|
-
}, timeoutMs);
|
|
441
|
-
|
|
442
|
-
// Set up completion detection (stdout marker + outcome file polling)
|
|
443
|
-
const completionDetector = createCompletionDetector(
|
|
444
|
-
() => proc.kill('SIGTERM'),
|
|
445
|
-
outcomeFilePath,
|
|
446
|
-
commitContext,
|
|
447
|
-
);
|
|
448
|
-
|
|
449
|
-
// Buffer for incomplete NDJSON lines (data chunks may split across line boundaries)
|
|
450
|
-
let lineBuffer = '';
|
|
451
|
-
|
|
452
|
-
// Collect stdout - parse NDJSON silently (no display output)
|
|
453
|
-
proc.stdout.on('data', (data) => {
|
|
454
|
-
lineBuffer += data.toString();
|
|
455
|
-
|
|
456
|
-
// Process complete lines from the NDJSON stream
|
|
457
|
-
let newlineIndex: number;
|
|
458
|
-
while ((newlineIndex = lineBuffer.indexOf('\n')) !== -1) {
|
|
459
|
-
const line = lineBuffer.substring(0, newlineIndex);
|
|
460
|
-
lineBuffer = lineBuffer.substring(newlineIndex + 1);
|
|
461
|
-
|
|
462
|
-
const result = renderStreamEvent(line);
|
|
463
|
-
|
|
464
|
-
if (result.sessionId && !sessionId) {
|
|
465
|
-
sessionId = result.sessionId;
|
|
466
|
-
this._sessionId = sessionId;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (result.textContent) {
|
|
470
|
-
output += result.textContent;
|
|
471
|
-
|
|
472
|
-
// Check for completion marker to start grace period
|
|
473
|
-
completionDetector.checkOutput(output);
|
|
474
|
-
|
|
475
|
-
// Check for context overflow
|
|
476
|
-
for (const pattern of CONTEXT_OVERFLOW_PATTERNS) {
|
|
477
|
-
if (pattern.test(result.textContent)) {
|
|
478
|
-
contextOverflow = true;
|
|
479
|
-
logger.warn('Context overflow detected');
|
|
480
|
-
if (sessionId) {
|
|
481
|
-
logger.info(`Session ID: ${sessionId}`);
|
|
482
|
-
}
|
|
483
|
-
proc.kill('SIGTERM');
|
|
484
|
-
break;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Silently discard display output (unlike runVerbose)
|
|
490
|
-
}
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
// Collect stderr
|
|
494
|
-
proc.stderr.on('data', (data) => {
|
|
495
|
-
stderr += data.toString();
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
proc.on('close', (exitCode) => {
|
|
499
|
-
// Process any remaining data in the line buffer
|
|
500
|
-
if (lineBuffer.trim()) {
|
|
501
|
-
const result = renderStreamEvent(lineBuffer);
|
|
502
|
-
if (result.sessionId && !sessionId) {
|
|
503
|
-
sessionId = result.sessionId;
|
|
504
|
-
}
|
|
505
|
-
if (result.textContent) {
|
|
506
|
-
output += result.textContent;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
clearTimeout(timeoutHandle);
|
|
511
|
-
completionDetector.cleanup();
|
|
512
|
-
this.activeProcess = null;
|
|
513
|
-
|
|
514
|
-
if (stderr) {
|
|
515
|
-
logger.debug(`Claude stderr: ${stderr}`);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
resolve({
|
|
519
|
-
output,
|
|
520
|
-
exitCode: exitCode ?? (this.killed ? 130 : 1),
|
|
521
|
-
timedOut,
|
|
522
|
-
contextOverflow,
|
|
523
|
-
sessionId,
|
|
524
|
-
});
|
|
525
|
-
});
|
|
526
|
-
});
|
|
387
|
+
return this._runStreamJson(prompt, options, false);
|
|
527
388
|
}
|
|
528
389
|
|
|
529
390
|
/**
|
|
@@ -539,21 +400,40 @@ export class ClaudeRunner {
|
|
|
539
400
|
* - Default timeout is 60 minutes if not specified
|
|
540
401
|
*/
|
|
541
402
|
async runVerbose(prompt: string, options: ClaudeRunnerOptions = {}): Promise<RunResult> {
|
|
542
|
-
|
|
403
|
+
return this._runStreamJson(prompt, options, true);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Internal unified execution method using stream-json format.
|
|
408
|
+
* Both run() and runVerbose() delegate to this method.
|
|
409
|
+
*
|
|
410
|
+
* @param verbose - When true, tool descriptions and text are printed to stdout.
|
|
411
|
+
* When false, display output is suppressed but usage data is still captured.
|
|
412
|
+
*/
|
|
413
|
+
private async _runStreamJson(
|
|
414
|
+
prompt: string,
|
|
415
|
+
options: ClaudeRunnerOptions,
|
|
416
|
+
verbose: boolean,
|
|
417
|
+
): Promise<RunResult> {
|
|
418
|
+
const { timeout = 60, cwd = process.cwd(), outcomeFilePath, commitContext, effortLevel, verboseCheck } = options;
|
|
543
419
|
// Ensure timeout is a positive number, fallback to 60 minutes
|
|
544
420
|
const validatedTimeout = Number(timeout) > 0 ? Number(timeout) : 60;
|
|
545
421
|
const timeoutMs = validatedTimeout * 60 * 1000;
|
|
546
422
|
|
|
423
|
+
// When verboseCheck callback is provided, use it to dynamically determine display.
|
|
424
|
+
// Otherwise, fall back to the static verbose parameter.
|
|
425
|
+
const shouldDisplay = verboseCheck ?? (() => verbose);
|
|
426
|
+
|
|
547
427
|
return new Promise((resolve) => {
|
|
548
428
|
let output = '';
|
|
549
429
|
let stderr = '';
|
|
550
430
|
let timedOut = false;
|
|
551
431
|
let contextOverflow = false;
|
|
552
|
-
let
|
|
432
|
+
let usageData: import('../types/config.js').UsageData | undefined;
|
|
553
433
|
|
|
554
434
|
const claudePath = getClaudePath();
|
|
555
435
|
|
|
556
|
-
logger.debug(`Starting Claude execution session (
|
|
436
|
+
logger.debug(`Starting Claude execution session (stream-json, verbose=${verbose}) with model: ${this.model}`);
|
|
557
437
|
logger.debug(`Prompt length: ${prompt.length}, timeout: ${timeoutMs}ms, cwd: ${cwd}`);
|
|
558
438
|
logger.debug(`Claude path: ${claudePath}`);
|
|
559
439
|
|
|
@@ -564,7 +444,7 @@ export class ClaudeRunner {
|
|
|
564
444
|
|
|
565
445
|
logger.debug('Spawning process...');
|
|
566
446
|
// Use --output-format stream-json --verbose to get real-time streaming events
|
|
567
|
-
// including tool calls, file operations, and
|
|
447
|
+
// including tool calls, file operations, and token usage in the result event.
|
|
568
448
|
// --dangerously-skip-permissions bypasses interactive prompts
|
|
569
449
|
// -p enables print mode (non-interactive)
|
|
570
450
|
const proc = spawn(claudePath, [
|
|
@@ -592,9 +472,6 @@ export class ClaudeRunner {
|
|
|
592
472
|
const timeoutHandle = setTimeout(() => {
|
|
593
473
|
timedOut = true;
|
|
594
474
|
logger.warn('Claude session timed out');
|
|
595
|
-
if (sessionId) {
|
|
596
|
-
logger.info(`Session ID: ${sessionId}`);
|
|
597
|
-
}
|
|
598
475
|
proc.kill('SIGTERM');
|
|
599
476
|
}, timeoutMs);
|
|
600
477
|
|
|
@@ -623,35 +500,32 @@ export class ClaudeRunner {
|
|
|
623
500
|
const line = lineBuffer.substring(0, newlineIndex);
|
|
624
501
|
lineBuffer = lineBuffer.substring(newlineIndex + 1);
|
|
625
502
|
|
|
626
|
-
const
|
|
503
|
+
const rendered = renderStreamEvent(line);
|
|
627
504
|
|
|
628
|
-
if (
|
|
629
|
-
|
|
630
|
-
this._sessionId = sessionId;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
if (result.textContent) {
|
|
634
|
-
output += result.textContent;
|
|
505
|
+
if (rendered.textContent) {
|
|
506
|
+
output += rendered.textContent;
|
|
635
507
|
|
|
636
508
|
// Check for completion marker to start grace period
|
|
637
509
|
completionDetector.checkOutput(output);
|
|
638
510
|
|
|
639
511
|
// Check for context overflow
|
|
640
512
|
for (const pattern of CONTEXT_OVERFLOW_PATTERNS) {
|
|
641
|
-
if (pattern.test(
|
|
513
|
+
if (pattern.test(rendered.textContent)) {
|
|
642
514
|
contextOverflow = true;
|
|
643
515
|
logger.warn('Context overflow detected');
|
|
644
|
-
if (sessionId) {
|
|
645
|
-
logger.info(`Session ID: ${sessionId}`);
|
|
646
|
-
}
|
|
647
516
|
proc.kill('SIGTERM');
|
|
648
517
|
break;
|
|
649
518
|
}
|
|
650
519
|
}
|
|
651
520
|
}
|
|
652
521
|
|
|
653
|
-
|
|
654
|
-
|
|
522
|
+
// Capture usage data from result events
|
|
523
|
+
if (rendered.usageData) {
|
|
524
|
+
usageData = rendered.usageData;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (shouldDisplay() && rendered.display) {
|
|
528
|
+
process.stdout.write(rendered.display);
|
|
655
529
|
}
|
|
656
530
|
}
|
|
657
531
|
});
|
|
@@ -664,15 +538,15 @@ export class ClaudeRunner {
|
|
|
664
538
|
proc.on('close', (exitCode) => {
|
|
665
539
|
// Process any remaining data in the line buffer
|
|
666
540
|
if (lineBuffer.trim()) {
|
|
667
|
-
const
|
|
668
|
-
if (
|
|
669
|
-
|
|
541
|
+
const rendered = renderStreamEvent(lineBuffer);
|
|
542
|
+
if (rendered.textContent) {
|
|
543
|
+
output += rendered.textContent;
|
|
670
544
|
}
|
|
671
|
-
if (
|
|
672
|
-
|
|
545
|
+
if (rendered.usageData) {
|
|
546
|
+
usageData = rendered.usageData;
|
|
673
547
|
}
|
|
674
|
-
if (
|
|
675
|
-
process.stdout.write(
|
|
548
|
+
if (shouldDisplay() && rendered.display) {
|
|
549
|
+
process.stdout.write(rendered.display);
|
|
676
550
|
}
|
|
677
551
|
}
|
|
678
552
|
|
|
@@ -690,134 +564,7 @@ export class ClaudeRunner {
|
|
|
690
564
|
exitCode: exitCode ?? (this.killed ? 130 : 1),
|
|
691
565
|
timedOut,
|
|
692
566
|
contextOverflow,
|
|
693
|
-
|
|
694
|
-
});
|
|
695
|
-
});
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
/**
|
|
700
|
-
* Resume an interrupted Claude session by session ID.
|
|
701
|
-
* Spawns Claude with --resume flag only — no prompt, model, or system prompt flags.
|
|
702
|
-
* Uses stream-json output format for session ID extraction and completion detection.
|
|
703
|
-
*/
|
|
704
|
-
async runResume(sessionId: string, options: ClaudeRunnerOptions = {}): Promise<RunResult> {
|
|
705
|
-
const { timeout = 60, cwd = process.cwd(), outcomeFilePath, commitContext, effortLevel } = options;
|
|
706
|
-
const validatedTimeout = Number(timeout) > 0 ? Number(timeout) : 60;
|
|
707
|
-
const timeoutMs = validatedTimeout * 60 * 1000;
|
|
708
|
-
|
|
709
|
-
return new Promise((resolve) => {
|
|
710
|
-
let output = '';
|
|
711
|
-
let stderr = '';
|
|
712
|
-
let timedOut = false;
|
|
713
|
-
let contextOverflow = false;
|
|
714
|
-
let newSessionId: string | undefined;
|
|
715
|
-
|
|
716
|
-
const claudePath = getClaudePath();
|
|
717
|
-
|
|
718
|
-
logger.debug(`Resuming Claude session: ${sessionId}`);
|
|
719
|
-
|
|
720
|
-
const env = effortLevel
|
|
721
|
-
? { ...process.env, CLAUDE_CODE_EFFORT_LEVEL: effortLevel }
|
|
722
|
-
: process.env;
|
|
723
|
-
|
|
724
|
-
// Resume with minimal flags: --resume, --dangerously-skip-permissions, --output-format stream-json --verbose
|
|
725
|
-
// Do NOT pass --model, --append-system-prompt, or -p — Claude restores these from the session
|
|
726
|
-
const proc = spawn(claudePath, [
|
|
727
|
-
'--resume',
|
|
728
|
-
sessionId,
|
|
729
|
-
'--dangerously-skip-permissions',
|
|
730
|
-
'--output-format',
|
|
731
|
-
'stream-json',
|
|
732
|
-
'--verbose',
|
|
733
|
-
], {
|
|
734
|
-
cwd,
|
|
735
|
-
env,
|
|
736
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
this.activeProcess = proc as any;
|
|
740
|
-
|
|
741
|
-
const timeoutHandle = setTimeout(() => {
|
|
742
|
-
timedOut = true;
|
|
743
|
-
logger.warn('Resumed Claude session timed out');
|
|
744
|
-
if (newSessionId) {
|
|
745
|
-
logger.info(`Session ID: ${newSessionId}`);
|
|
746
|
-
}
|
|
747
|
-
proc.kill('SIGTERM');
|
|
748
|
-
}, timeoutMs);
|
|
749
|
-
|
|
750
|
-
const completionDetector = createCompletionDetector(
|
|
751
|
-
() => proc.kill('SIGTERM'),
|
|
752
|
-
outcomeFilePath,
|
|
753
|
-
commitContext,
|
|
754
|
-
);
|
|
755
|
-
|
|
756
|
-
let lineBuffer = '';
|
|
757
|
-
|
|
758
|
-
proc.stdout.on('data', (data) => {
|
|
759
|
-
lineBuffer += data.toString();
|
|
760
|
-
|
|
761
|
-
let newlineIndex: number;
|
|
762
|
-
while ((newlineIndex = lineBuffer.indexOf('\n')) !== -1) {
|
|
763
|
-
const line = lineBuffer.substring(0, newlineIndex);
|
|
764
|
-
lineBuffer = lineBuffer.substring(newlineIndex + 1);
|
|
765
|
-
|
|
766
|
-
const result = renderStreamEvent(line);
|
|
767
|
-
|
|
768
|
-
if (result.sessionId && !newSessionId) {
|
|
769
|
-
newSessionId = result.sessionId;
|
|
770
|
-
this._sessionId = newSessionId;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
if (result.textContent) {
|
|
774
|
-
output += result.textContent;
|
|
775
|
-
completionDetector.checkOutput(output);
|
|
776
|
-
|
|
777
|
-
for (const pattern of CONTEXT_OVERFLOW_PATTERNS) {
|
|
778
|
-
if (pattern.test(result.textContent)) {
|
|
779
|
-
contextOverflow = true;
|
|
780
|
-
logger.warn('Context overflow detected');
|
|
781
|
-
if (newSessionId) {
|
|
782
|
-
logger.info(`Session ID: ${newSessionId}`);
|
|
783
|
-
}
|
|
784
|
-
proc.kill('SIGTERM');
|
|
785
|
-
break;
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
proc.stderr.on('data', (data) => {
|
|
793
|
-
stderr += data.toString();
|
|
794
|
-
});
|
|
795
|
-
|
|
796
|
-
proc.on('close', (exitCode) => {
|
|
797
|
-
if (lineBuffer.trim()) {
|
|
798
|
-
const result = renderStreamEvent(lineBuffer);
|
|
799
|
-
if (result.sessionId && !newSessionId) {
|
|
800
|
-
newSessionId = result.sessionId;
|
|
801
|
-
}
|
|
802
|
-
if (result.textContent) {
|
|
803
|
-
output += result.textContent;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
clearTimeout(timeoutHandle);
|
|
808
|
-
completionDetector.cleanup();
|
|
809
|
-
this.activeProcess = null;
|
|
810
|
-
|
|
811
|
-
if (stderr) {
|
|
812
|
-
logger.debug(`Claude stderr: ${stderr}`);
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
resolve({
|
|
816
|
-
output,
|
|
817
|
-
exitCode: exitCode ?? (this.killed ? 130 : 1),
|
|
818
|
-
timedOut,
|
|
819
|
-
contextOverflow,
|
|
820
|
-
sessionId: newSessionId,
|
|
567
|
+
usageData,
|
|
821
568
|
});
|
|
822
569
|
});
|
|
823
570
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
+
import { getModel, getClaudeCommand } from '../utils/config.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Failure types that can be detected programmatically without using the API.
|
|
@@ -212,8 +213,9 @@ function extractRelevantOutput(output: string, maxLines: number): string {
|
|
|
212
213
|
* Get the path to Claude CLI.
|
|
213
214
|
*/
|
|
214
215
|
function getClaudePath(): string {
|
|
216
|
+
const cmd = getClaudeCommand();
|
|
215
217
|
try {
|
|
216
|
-
return execSync(
|
|
218
|
+
return execSync(`which ${cmd}`, { encoding: 'utf-8' }).trim();
|
|
217
219
|
} catch {
|
|
218
220
|
throw new Error('Claude CLI not found. Please ensure it is installed and in your PATH.');
|
|
219
221
|
}
|
|
@@ -306,9 +308,10 @@ Respond with ONLY a markdown report in this exact format:
|
|
|
306
308
|
|
|
307
309
|
const claudePath = getClaudePath();
|
|
308
310
|
|
|
309
|
-
// Use
|
|
311
|
+
// Use configured model for failure analysis
|
|
312
|
+
const failureModel = getModel('failureAnalysis');
|
|
310
313
|
const proc = spawn(claudePath, [
|
|
311
|
-
'--model',
|
|
314
|
+
'--model', failureModel,
|
|
312
315
|
'--dangerously-skip-permissions',
|
|
313
316
|
'-p',
|
|
314
317
|
prompt,
|
package/src/core/git.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { execSync } from 'node:child_process';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { logger } from '../utils/logger.js';
|
|
4
4
|
import { extractProjectNumber, extractProjectName } from '../utils/paths.js';
|
|
5
|
+
import { getCommitFormat, getCommitPrefix, renderCommitMessage } from '../utils/config.js';
|
|
5
6
|
|
|
6
7
|
export interface GitStatus {
|
|
7
8
|
isRepo: boolean;
|
|
@@ -246,9 +247,15 @@ export async function commitPlanningArtifacts(projectPath: string, options?: { c
|
|
|
246
247
|
const inputFile = path.join(projectPath, 'input.md');
|
|
247
248
|
const decisionsFile = path.join(projectPath, 'decisions.md');
|
|
248
249
|
|
|
249
|
-
// Build commit message
|
|
250
|
-
const
|
|
251
|
-
const
|
|
250
|
+
// Build commit message from config template
|
|
251
|
+
const formatType = options?.isAmend ? 'amend' as const : 'plan' as const;
|
|
252
|
+
const template = getCommitFormat(formatType);
|
|
253
|
+
const commitPrefix = getCommitPrefix();
|
|
254
|
+
const commitMessage = renderCommitMessage(template, {
|
|
255
|
+
prefix: commitPrefix,
|
|
256
|
+
projectId: projectNumber,
|
|
257
|
+
projectName,
|
|
258
|
+
});
|
|
252
259
|
|
|
253
260
|
// Build list of files to stage (absolute paths)
|
|
254
261
|
const absoluteFiles = [inputFile, decisionsFile, ...(options?.additionalFiles ?? [])];
|
package/src/core/pull-request.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import { logger } from '../utils/logger.js';
|
|
6
|
+
import { getModel, getClaudeCommand } from '../utils/config.js';
|
|
6
7
|
import { extractProjectName, getInputPath, getDecisionsPath, getOutcomesDir, TASK_ID_PATTERN } from '../utils/paths.js';
|
|
7
8
|
|
|
8
9
|
export interface PrCreateResult {
|
|
@@ -222,7 +223,7 @@ export function readProjectContext(projectPath: string): {
|
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
/**
|
|
225
|
-
* Generate a PR body by calling Claude
|
|
226
|
+
* Generate a PR body by calling Claude to summarize project context.
|
|
226
227
|
* Falls back to a simple body if Claude is unavailable.
|
|
227
228
|
*/
|
|
228
229
|
export async function generatePrBody(
|
|
@@ -348,12 +349,13 @@ export function filterClaudeOutput(output: string): string {
|
|
|
348
349
|
}
|
|
349
350
|
|
|
350
351
|
/**
|
|
351
|
-
* Call Claude
|
|
352
|
+
* Call Claude to generate a PR body.
|
|
352
353
|
*/
|
|
353
354
|
async function callClaudeForPrBody(prompt: string, timeoutMs: number): Promise<string> {
|
|
355
|
+
const cmd = getClaudeCommand();
|
|
354
356
|
let claudePath: string;
|
|
355
357
|
try {
|
|
356
|
-
claudePath = execSync(
|
|
358
|
+
claudePath = execSync(`which ${cmd}`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
357
359
|
} catch {
|
|
358
360
|
throw new Error('Claude CLI not found');
|
|
359
361
|
}
|
|
@@ -362,8 +364,9 @@ async function callClaudeForPrBody(prompt: string, timeoutMs: number): Promise<s
|
|
|
362
364
|
let output = '';
|
|
363
365
|
let stderr = '';
|
|
364
366
|
|
|
367
|
+
const prModel = getModel('prGeneration');
|
|
365
368
|
const proc = spawn(claudePath, [
|
|
366
|
-
'--model',
|
|
369
|
+
'--model', prModel,
|
|
367
370
|
'--dangerously-skip-permissions',
|
|
368
371
|
'-p',
|
|
369
372
|
prompt,
|
|
@@ -54,11 +54,6 @@ class ShutdownHandler {
|
|
|
54
54
|
this.isShuttingDown = true;
|
|
55
55
|
logger.info(`\nReceived ${signal}, shutting down gracefully...`);
|
|
56
56
|
|
|
57
|
-
// Print session ID if available (for resumption/debugging)
|
|
58
|
-
if (this.claudeRunner?.sessionId) {
|
|
59
|
-
logger.info(`Session ID: ${this.claudeRunner.sessionId}`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
57
|
// Kill Claude process if running
|
|
63
58
|
if (this.claudeRunner?.isRunning()) {
|
|
64
59
|
logger.debug('Killing Claude process...');
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { createPlanCommand } from './commands/plan.js';
|
|
|
5
5
|
import { createDoCommand } from './commands/do.js';
|
|
6
6
|
import { createStatusCommand } from './commands/status.js';
|
|
7
7
|
import { createMigrateCommand } from './commands/migrate.js';
|
|
8
|
+
import { createConfigCommand } from './commands/config.js';
|
|
8
9
|
import { getVersion } from './utils/version.js';
|
|
9
10
|
|
|
10
11
|
const program = new Command();
|
|
@@ -18,5 +19,6 @@ program.addCommand(createPlanCommand());
|
|
|
18
19
|
program.addCommand(createDoCommand());
|
|
19
20
|
program.addCommand(createStatusCommand());
|
|
20
21
|
program.addCommand(createMigrateCommand());
|
|
22
|
+
program.addCommand(createConfigCommand());
|
|
21
23
|
|
|
22
24
|
program.parse();
|