wiggum-cli 0.13.2 → 0.14.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/dist/templates/scripts/feature-loop.sh.tmpl +81 -4
- package/dist/tui/app.js +2 -1
- package/dist/tui/components/ChatInput.js +27 -9
- package/dist/tui/components/RunCompletionSummary.d.ts +3 -9
- package/dist/tui/components/RunCompletionSummary.js +59 -14
- package/dist/tui/components/SummaryBox.d.ts +59 -0
- package/dist/tui/components/SummaryBox.js +97 -0
- package/dist/tui/screens/MainShell.js +23 -2
- package/dist/tui/screens/RunScreen.d.ts +116 -1
- package/dist/tui/screens/RunScreen.js +33 -5
- package/dist/tui/utils/build-run-summary.d.ts +24 -0
- package/dist/tui/utils/build-run-summary.js +241 -0
- package/dist/tui/utils/git-summary.d.ts +24 -0
- package/dist/tui/utils/git-summary.js +63 -0
- package/dist/tui/utils/input-utils.d.ts +20 -0
- package/dist/tui/utils/input-utils.js +27 -0
- package/dist/tui/utils/pr-summary.d.ts +34 -0
- package/dist/tui/utils/pr-summary.js +84 -0
- package/dist/utils/summary-file.d.ts +25 -0
- package/dist/utils/summary-file.js +37 -0
- package/package.json +1 -1
- package/src/templates/scripts/feature-loop.sh.tmpl +81 -4
|
@@ -13,8 +13,98 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import React from 'react';
|
|
15
15
|
import type { SessionState } from '../../repl/session-state.js';
|
|
16
|
+
/**
|
|
17
|
+
* Phase execution status and timing
|
|
18
|
+
*/
|
|
19
|
+
export interface PhaseInfo {
|
|
20
|
+
/** Unique phase identifier (e.g., 'planning', 'implementation') */
|
|
21
|
+
id: string;
|
|
22
|
+
/** Human-readable phase label */
|
|
23
|
+
label: string;
|
|
24
|
+
/** Phase completion status */
|
|
25
|
+
status: 'success' | 'skipped' | 'failed';
|
|
26
|
+
/** Duration in milliseconds, if available */
|
|
27
|
+
durationMs?: number;
|
|
28
|
+
/** Number of iterations in this phase (e.g., for implementation) */
|
|
29
|
+
iterations?: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Iteration breakdown across different contexts
|
|
33
|
+
*/
|
|
34
|
+
export interface IterationBreakdown {
|
|
35
|
+
/** Total iterations across all runs */
|
|
36
|
+
total: number;
|
|
37
|
+
/** Iterations during implementation phase */
|
|
38
|
+
implementation?: number;
|
|
39
|
+
/** Iterations during resume operations */
|
|
40
|
+
resumes?: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* File change statistics from git diff
|
|
44
|
+
*/
|
|
45
|
+
export interface FileChangeStat {
|
|
46
|
+
/** Relative path from project root */
|
|
47
|
+
path: string;
|
|
48
|
+
/** Lines added */
|
|
49
|
+
added: number;
|
|
50
|
+
/** Lines removed */
|
|
51
|
+
removed: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Changes summary with git diff stats
|
|
55
|
+
*/
|
|
56
|
+
export interface ChangesSummary {
|
|
57
|
+
/** Total number of files changed */
|
|
58
|
+
totalFilesChanged?: number;
|
|
59
|
+
/** Per-file diff statistics */
|
|
60
|
+
files?: FileChangeStat[];
|
|
61
|
+
/** Whether git diff information was available */
|
|
62
|
+
available: boolean;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Commit information from git
|
|
66
|
+
*/
|
|
67
|
+
export interface CommitsSummary {
|
|
68
|
+
/** Starting commit hash (short) */
|
|
69
|
+
fromHash?: string;
|
|
70
|
+
/** Ending commit hash (short) */
|
|
71
|
+
toHash?: string;
|
|
72
|
+
/** Merge type if applicable */
|
|
73
|
+
mergeType?: 'squash' | 'normal' | 'none';
|
|
74
|
+
/** Whether git commit information was available */
|
|
75
|
+
available: boolean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Pull request metadata
|
|
79
|
+
*/
|
|
80
|
+
export interface PrSummary {
|
|
81
|
+
/** PR number if created */
|
|
82
|
+
number?: number;
|
|
83
|
+
/** PR URL if created */
|
|
84
|
+
url?: string;
|
|
85
|
+
/** Whether PR information was available to query */
|
|
86
|
+
available: boolean;
|
|
87
|
+
/** Whether a PR was created as part of this loop */
|
|
88
|
+
created: boolean;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Issue metadata
|
|
92
|
+
*/
|
|
93
|
+
export interface IssueSummary {
|
|
94
|
+
/** Issue number if linked */
|
|
95
|
+
number?: number;
|
|
96
|
+
/** Issue URL if available */
|
|
97
|
+
url?: string;
|
|
98
|
+
/** Issue status (e.g., 'Closed') */
|
|
99
|
+
status?: string;
|
|
100
|
+
/** Whether issue information was available to query */
|
|
101
|
+
available: boolean;
|
|
102
|
+
/** Whether an issue was linked/closed as part of this loop */
|
|
103
|
+
linked: boolean;
|
|
104
|
+
}
|
|
16
105
|
export interface RunSummary {
|
|
17
106
|
feature: string;
|
|
107
|
+
/** Legacy field: total iterations (deprecated, use iterationBreakdown.total) */
|
|
18
108
|
iterations: number;
|
|
19
109
|
maxIterations: number;
|
|
20
110
|
tasksDone: number;
|
|
@@ -27,6 +117,29 @@ export interface RunSummary {
|
|
|
27
117
|
branch?: string;
|
|
28
118
|
logPath?: string;
|
|
29
119
|
errorTail?: string;
|
|
120
|
+
/** Loop start timestamp (ISO 8601 or epoch ms) */
|
|
121
|
+
startedAt?: string | number;
|
|
122
|
+
/** Loop end timestamp (ISO 8601 or epoch ms) */
|
|
123
|
+
endedAt?: string | number;
|
|
124
|
+
/** Total duration across all runs/resumes in milliseconds */
|
|
125
|
+
totalDurationMs?: number;
|
|
126
|
+
/** Detailed iteration breakdown */
|
|
127
|
+
iterationBreakdown?: IterationBreakdown;
|
|
128
|
+
/** Task completion counts */
|
|
129
|
+
tasks?: {
|
|
130
|
+
completed: number | null;
|
|
131
|
+
total: number | null;
|
|
132
|
+
};
|
|
133
|
+
/** Phase execution details */
|
|
134
|
+
phases?: PhaseInfo[];
|
|
135
|
+
/** Git diff changes summary */
|
|
136
|
+
changes?: ChangesSummary;
|
|
137
|
+
/** Git commit information */
|
|
138
|
+
commits?: CommitsSummary;
|
|
139
|
+
/** Pull request metadata */
|
|
140
|
+
pr?: PrSummary;
|
|
141
|
+
/** Issue metadata */
|
|
142
|
+
issue?: IssueSummary;
|
|
30
143
|
}
|
|
31
144
|
export interface RunScreenProps {
|
|
32
145
|
/** Pre-built header element from App */
|
|
@@ -36,9 +149,11 @@ export interface RunScreenProps {
|
|
|
36
149
|
sessionState: SessionState;
|
|
37
150
|
/** Monitor-only mode: don't spawn, just poll status */
|
|
38
151
|
monitorOnly?: boolean;
|
|
152
|
+
/** Override review mode from CLI flags (takes precedence over config) */
|
|
153
|
+
reviewMode?: 'manual' | 'auto';
|
|
39
154
|
onComplete: (summary: RunSummary) => void;
|
|
40
155
|
/** Called when user presses Esc to background the run */
|
|
41
156
|
onBackground?: (featureName: string) => void;
|
|
42
157
|
onCancel: () => void;
|
|
43
158
|
}
|
|
44
|
-
export declare function RunScreen({ header, featureName, projectRoot, sessionState, monitorOnly, onComplete, onBackground, onCancel, }: RunScreenProps): React.ReactElement;
|
|
159
|
+
export declare function RunScreen({ header, featureName, projectRoot, sessionState, monitorOnly, reviewMode: reviewModeProp, onComplete, onBackground, onCancel, }: RunScreenProps): React.ReactElement;
|
|
@@ -22,6 +22,8 @@ import { AppShell } from '../components/AppShell.js';
|
|
|
22
22
|
import { RunCompletionSummary } from '../components/RunCompletionSummary.js';
|
|
23
23
|
import { colors, theme } from '../theme.js';
|
|
24
24
|
import { readLoopStatus, parseImplementationPlan, getGitBranch, formatNumber, getLoopLogPath, } from '../utils/loop-status.js';
|
|
25
|
+
import { buildEnhancedRunSummary } from '../utils/build-run-summary.js';
|
|
26
|
+
import { writeRunSummaryFile } from '../../utils/summary-file.js';
|
|
25
27
|
import { loadConfigWithDefaults } from '../../utils/config.js';
|
|
26
28
|
import { logger } from '../../utils/logger.js';
|
|
27
29
|
const POLL_INTERVAL_MS = 2500;
|
|
@@ -80,7 +82,7 @@ function readLogTail(logPath, maxLines) {
|
|
|
80
82
|
return `[Unable to read log: ${err instanceof Error ? err.message : String(err)}]`;
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
|
-
export function RunScreen({ header, featureName, projectRoot, sessionState, monitorOnly = false, onComplete, onBackground, onCancel, }) {
|
|
85
|
+
export function RunScreen({ header, featureName, projectRoot, sessionState, monitorOnly = false, reviewMode: reviewModeProp, onComplete, onBackground, onCancel, }) {
|
|
84
86
|
const [status, setStatus] = useState(() => {
|
|
85
87
|
try {
|
|
86
88
|
return readLoopStatus(featureName);
|
|
@@ -159,7 +161,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
159
161
|
const tasksDone = nextTasks.tasksDone + nextTasks.e2eDone;
|
|
160
162
|
const tasksTotal = tasksDone + nextTasks.tasksPending + nextTasks.e2ePending;
|
|
161
163
|
const errorTail = exitCode !== 0 ? readLogTail(logPath, ERROR_TAIL_LINES) || undefined : undefined;
|
|
162
|
-
|
|
164
|
+
const basicSummary = {
|
|
163
165
|
feature: featureName,
|
|
164
166
|
iterations: nextStatus.iteration,
|
|
165
167
|
maxIterations: nextStatus.maxIterations,
|
|
@@ -172,6 +174,19 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
172
174
|
branch: getGitBranch(projectRoot),
|
|
173
175
|
logPath,
|
|
174
176
|
errorTail,
|
|
177
|
+
};
|
|
178
|
+
let enhancedSummary;
|
|
179
|
+
try {
|
|
180
|
+
enhancedSummary = buildEnhancedRunSummary(basicSummary, projectRoot, featureName);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
logger.error(`Failed to build enhanced summary for ${featureName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
184
|
+
enhancedSummary = basicSummary;
|
|
185
|
+
}
|
|
186
|
+
setCompletionSummary(enhancedSummary);
|
|
187
|
+
// Persist summary to JSON file (non-blocking)
|
|
188
|
+
writeRunSummaryFile(featureName, enhancedSummary).catch((err) => {
|
|
189
|
+
logger.error(`Failed to persist summary file for ${featureName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
175
190
|
});
|
|
176
191
|
}
|
|
177
192
|
}, [featureName, projectRoot, monitorOnly]);
|
|
@@ -272,7 +287,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
272
287
|
setIsStarting(false);
|
|
273
288
|
return;
|
|
274
289
|
}
|
|
275
|
-
const reviewMode = config.loop.reviewMode ?? 'manual';
|
|
290
|
+
const reviewMode = reviewModeProp ?? config.loop.reviewMode ?? 'manual';
|
|
276
291
|
if (reviewMode !== 'manual' && reviewMode !== 'auto') {
|
|
277
292
|
setError(`Invalid reviewMode '${reviewMode}'. Allowed values are 'manual' or 'auto'.`);
|
|
278
293
|
setIsStarting(false);
|
|
@@ -348,7 +363,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
348
363
|
const tasksTotal = tasksDone + latestTasks.tasksPending + latestTasks.e2ePending;
|
|
349
364
|
const exitCode = typeof code === 'number' ? code : 1;
|
|
350
365
|
const errorTail = exitCode === 0 ? undefined : readLogTail(logPath, ERROR_TAIL_LINES) || undefined;
|
|
351
|
-
const
|
|
366
|
+
const basicSummary = {
|
|
352
367
|
feature: featureName,
|
|
353
368
|
iterations: latestStatus.iteration,
|
|
354
369
|
maxIterations: latestStatus.maxIterations || config.loop.maxIterations,
|
|
@@ -361,8 +376,21 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
361
376
|
logPath,
|
|
362
377
|
errorTail,
|
|
363
378
|
};
|
|
379
|
+
// Build enhanced summary with phases, git stats, PR/issue metadata
|
|
380
|
+
let enhancedSummary;
|
|
381
|
+
try {
|
|
382
|
+
enhancedSummary = buildEnhancedRunSummary(basicSummary, projectRoot, featureName);
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
logger.error(`Failed to build enhanced summary for ${featureName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
386
|
+
enhancedSummary = basicSummary;
|
|
387
|
+
}
|
|
364
388
|
// Show completion summary inline
|
|
365
|
-
setCompletionSummary(
|
|
389
|
+
setCompletionSummary(enhancedSummary);
|
|
390
|
+
// Persist summary to JSON file (non-blocking)
|
|
391
|
+
writeRunSummaryFile(featureName, enhancedSummary).catch((err) => {
|
|
392
|
+
logger.error(`Failed to persist summary file for ${featureName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
393
|
+
});
|
|
366
394
|
});
|
|
367
395
|
}
|
|
368
396
|
catch (spawnErr) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build enhanced run summary from loop state and external data sources.
|
|
3
|
+
*/
|
|
4
|
+
import type { RunSummary } from '../screens/RunScreen.js';
|
|
5
|
+
/**
|
|
6
|
+
* Build enhanced run summary from loop completion state.
|
|
7
|
+
*
|
|
8
|
+
* This function aggregates data from:
|
|
9
|
+
* - Basic loop state (iterations, tasks, tokens, exit code)
|
|
10
|
+
* - Phase timing file (/tmp/ralph-loop-<feature>.phases)
|
|
11
|
+
* - Baseline commit file (/tmp/ralph-loop-<feature>.baseline)
|
|
12
|
+
* - Git diff stats (via git-summary utilities)
|
|
13
|
+
* - PR/issue metadata (via pr-summary utilities)
|
|
14
|
+
*
|
|
15
|
+
* @param basicSummary - The minimal RunSummary constructed by RunScreen
|
|
16
|
+
* @param projectRoot - Root directory of the project
|
|
17
|
+
* @param feature - Feature name
|
|
18
|
+
* @returns Enhanced RunSummary with all available metadata
|
|
19
|
+
*
|
|
20
|
+
* Note: This function performs synchronous I/O and subprocess execution,
|
|
21
|
+
* which blocks the event loop. Callers should wrap in try-catch to handle
|
|
22
|
+
* failures gracefully.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildEnhancedRunSummary(basicSummary: RunSummary, projectRoot: string, feature: string): RunSummary;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build enhanced run summary from loop state and external data sources.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { logger } from '../../utils/logger.js';
|
|
6
|
+
import { getCurrentCommitHash, getDiffStats } from './git-summary.js';
|
|
7
|
+
import { getPrForBranch, getLinkedIssue } from './pr-summary.js';
|
|
8
|
+
/**
|
|
9
|
+
* Phase ID to human-readable label mapping
|
|
10
|
+
*/
|
|
11
|
+
const PHASE_LABELS = {
|
|
12
|
+
planning: 'Planning',
|
|
13
|
+
implementation: 'Implementation',
|
|
14
|
+
e2e_testing: 'E2E Testing',
|
|
15
|
+
verification: 'Verification',
|
|
16
|
+
pr_review: 'PR & Review',
|
|
17
|
+
};
|
|
18
|
+
const VALID_PHASE_STATUSES = new Set(['success', 'skipped', 'failed']);
|
|
19
|
+
/**
|
|
20
|
+
* Parse phase information from the phases file written by feature-loop.sh.
|
|
21
|
+
*
|
|
22
|
+
* Format: phase_id|status|start_timestamp|end_timestamp
|
|
23
|
+
* The parser handles duplicate phase entries defensively (last status wins,
|
|
24
|
+
* durations aggregate), though feature-loop.sh normally writes one final
|
|
25
|
+
* line per phase.
|
|
26
|
+
*
|
|
27
|
+
* @param phasesFilePath - Path to the phases file
|
|
28
|
+
* @returns Array of phase info objects
|
|
29
|
+
*/
|
|
30
|
+
function parsePhases(phasesFilePath) {
|
|
31
|
+
if (!existsSync(phasesFilePath)) {
|
|
32
|
+
logger.debug(`Phases file not found: ${phasesFilePath}`);
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const content = readFileSync(phasesFilePath, 'utf-8').trim();
|
|
37
|
+
if (!content) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const lines = content.split('\n');
|
|
41
|
+
const phaseMap = new Map();
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
const parts = line.split('|');
|
|
44
|
+
if (parts.length < 4) {
|
|
45
|
+
logger.warn(`Skipping malformed phase line: ${line}`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const [id, status, startStr, endStr] = parts;
|
|
49
|
+
// Validate status
|
|
50
|
+
if (!VALID_PHASE_STATUSES.has(status)) {
|
|
51
|
+
logger.warn(`Unknown phase status "${status}" for phase "${id}", treating as failed`);
|
|
52
|
+
}
|
|
53
|
+
const validatedStatus = VALID_PHASE_STATUSES.has(status)
|
|
54
|
+
? status
|
|
55
|
+
: 'failed';
|
|
56
|
+
// Parse timestamps
|
|
57
|
+
const startTime = parseInt(startStr, 10) || 0;
|
|
58
|
+
const endTime = parseInt(endStr, 10) || 0;
|
|
59
|
+
// Calculate duration (end - start) in milliseconds
|
|
60
|
+
const durationMs = endTime > 0 && startTime > 0 ? (endTime - startTime) * 1000 : undefined;
|
|
61
|
+
// Get or create phase entry
|
|
62
|
+
let phase = phaseMap.get(id);
|
|
63
|
+
if (!phase) {
|
|
64
|
+
phase = {
|
|
65
|
+
id,
|
|
66
|
+
label: PHASE_LABELS[id] || id,
|
|
67
|
+
status: validatedStatus,
|
|
68
|
+
durationMs: 0,
|
|
69
|
+
};
|
|
70
|
+
phaseMap.set(id, phase);
|
|
71
|
+
}
|
|
72
|
+
// Update status (last status wins)
|
|
73
|
+
phase.status = validatedStatus;
|
|
74
|
+
// Aggregate duration
|
|
75
|
+
if (durationMs !== undefined) {
|
|
76
|
+
phase.durationMs = (phase.durationMs || 0) + durationMs;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return Array.from(phaseMap.values());
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
logger.warn(`Failed to parse phases file: ${err instanceof Error ? err.message : String(err)}`);
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Read baseline commit hash from the baseline file.
|
|
88
|
+
*
|
|
89
|
+
* @param baselineFilePath - Path to the baseline file
|
|
90
|
+
* @returns Short commit hash, or null if not available
|
|
91
|
+
*/
|
|
92
|
+
function readBaselineCommit(baselineFilePath) {
|
|
93
|
+
if (!existsSync(baselineFilePath)) {
|
|
94
|
+
logger.debug(`Baseline file not found: ${baselineFilePath}`);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const content = readFileSync(baselineFilePath, 'utf-8').trim();
|
|
99
|
+
// Validate content looks like a hex commit hash
|
|
100
|
+
if (!/^[0-9a-f]{7,40}$/i.test(content)) {
|
|
101
|
+
logger.warn(`Baseline file ${baselineFilePath} contains invalid content: "${content.substring(0, 20)}"`);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return content.substring(0, 7) || null;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
logger.warn(`Failed to read baseline file: ${err instanceof Error ? err.message : String(err)}`);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build enhanced run summary from loop completion state.
|
|
113
|
+
*
|
|
114
|
+
* This function aggregates data from:
|
|
115
|
+
* - Basic loop state (iterations, tasks, tokens, exit code)
|
|
116
|
+
* - Phase timing file (/tmp/ralph-loop-<feature>.phases)
|
|
117
|
+
* - Baseline commit file (/tmp/ralph-loop-<feature>.baseline)
|
|
118
|
+
* - Git diff stats (via git-summary utilities)
|
|
119
|
+
* - PR/issue metadata (via pr-summary utilities)
|
|
120
|
+
*
|
|
121
|
+
* @param basicSummary - The minimal RunSummary constructed by RunScreen
|
|
122
|
+
* @param projectRoot - Root directory of the project
|
|
123
|
+
* @param feature - Feature name
|
|
124
|
+
* @returns Enhanced RunSummary with all available metadata
|
|
125
|
+
*
|
|
126
|
+
* Note: This function performs synchronous I/O and subprocess execution,
|
|
127
|
+
* which blocks the event loop. Callers should wrap in try-catch to handle
|
|
128
|
+
* failures gracefully.
|
|
129
|
+
*/
|
|
130
|
+
export function buildEnhancedRunSummary(basicSummary, projectRoot, feature) {
|
|
131
|
+
const phasesFilePath = `/tmp/ralph-loop-${feature}.phases`;
|
|
132
|
+
const baselineFilePath = `/tmp/ralph-loop-${feature}.baseline`;
|
|
133
|
+
// Parse phases and set implementation iterations from actual loop count
|
|
134
|
+
const phases = parsePhases(phasesFilePath);
|
|
135
|
+
const implPhase = phases.find((p) => p.id === 'implementation');
|
|
136
|
+
if (implPhase) {
|
|
137
|
+
implPhase.iterations = basicSummary.iterations;
|
|
138
|
+
}
|
|
139
|
+
// Calculate total duration from phases
|
|
140
|
+
let totalDurationMs;
|
|
141
|
+
if (phases.length > 0) {
|
|
142
|
+
const totalMs = phases.reduce((sum, phase) => sum + (phase.durationMs || 0), 0);
|
|
143
|
+
totalDurationMs = totalMs > 0 ? totalMs : undefined;
|
|
144
|
+
}
|
|
145
|
+
// Build iteration breakdown
|
|
146
|
+
const iterationBreakdown = {
|
|
147
|
+
total: basicSummary.iterations,
|
|
148
|
+
implementation: phases.find((p) => p.id === 'implementation')?.iterations,
|
|
149
|
+
// TODO: Detect resumes from multiple phase entries (future enhancement)
|
|
150
|
+
};
|
|
151
|
+
// Build task summary
|
|
152
|
+
const tasks = {
|
|
153
|
+
completed: basicSummary.tasksDone,
|
|
154
|
+
total: basicSummary.tasksTotal,
|
|
155
|
+
};
|
|
156
|
+
// Git changes and commits
|
|
157
|
+
const baselineCommit = readBaselineCommit(baselineFilePath);
|
|
158
|
+
const currentCommit = getCurrentCommitHash(projectRoot);
|
|
159
|
+
let changes = { available: false };
|
|
160
|
+
let commits = { available: false };
|
|
161
|
+
if (baselineCommit && currentCommit) {
|
|
162
|
+
commits = {
|
|
163
|
+
fromHash: baselineCommit,
|
|
164
|
+
toHash: currentCommit,
|
|
165
|
+
mergeType: 'none', // TODO: Detect merge type from git history
|
|
166
|
+
available: true,
|
|
167
|
+
};
|
|
168
|
+
// Get diff stats
|
|
169
|
+
const diffStats = getDiffStats(projectRoot, baselineCommit, currentCommit);
|
|
170
|
+
if (diffStats !== null) {
|
|
171
|
+
changes = {
|
|
172
|
+
totalFilesChanged: diffStats.length,
|
|
173
|
+
files: diffStats.map((stat) => ({
|
|
174
|
+
path: stat.path,
|
|
175
|
+
added: stat.added,
|
|
176
|
+
removed: stat.removed,
|
|
177
|
+
})),
|
|
178
|
+
available: true,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
changes = { available: true }; // Git is available but diff failed
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else if (currentCommit) {
|
|
186
|
+
// Only current commit available (no baseline)
|
|
187
|
+
commits = {
|
|
188
|
+
toHash: currentCommit,
|
|
189
|
+
available: true,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// PR and issue metadata
|
|
193
|
+
let pr = { available: false, created: false };
|
|
194
|
+
let issue = { available: false, linked: false };
|
|
195
|
+
if (basicSummary.branch) {
|
|
196
|
+
try {
|
|
197
|
+
const prInfo = getPrForBranch(projectRoot, basicSummary.branch);
|
|
198
|
+
if (prInfo) {
|
|
199
|
+
pr = {
|
|
200
|
+
number: prInfo.number,
|
|
201
|
+
url: prInfo.url,
|
|
202
|
+
available: true,
|
|
203
|
+
created: true,
|
|
204
|
+
};
|
|
205
|
+
// Try to get linked issue, passing prInfo to avoid redundant gh call
|
|
206
|
+
const issueInfo = getLinkedIssue(projectRoot, basicSummary.branch, prInfo);
|
|
207
|
+
if (issueInfo) {
|
|
208
|
+
issue = {
|
|
209
|
+
number: issueInfo.number,
|
|
210
|
+
url: issueInfo.url,
|
|
211
|
+
status: issueInfo.state,
|
|
212
|
+
available: true,
|
|
213
|
+
linked: true,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
issue = { available: true, linked: false };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
pr = { available: true, created: false };
|
|
222
|
+
issue = { available: true, linked: false };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
logger.warn(`gh CLI query failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
227
|
+
// pr and issue remain { available: false } from defaults above
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
...basicSummary,
|
|
232
|
+
totalDurationMs,
|
|
233
|
+
iterationBreakdown,
|
|
234
|
+
tasks,
|
|
235
|
+
phases,
|
|
236
|
+
changes,
|
|
237
|
+
commits,
|
|
238
|
+
pr,
|
|
239
|
+
issue,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git utilities for enhanced run summary.
|
|
3
|
+
*/
|
|
4
|
+
export interface FileDiffStat {
|
|
5
|
+
path: string;
|
|
6
|
+
added: number;
|
|
7
|
+
removed: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Get the current commit hash (HEAD).
|
|
11
|
+
*
|
|
12
|
+
* @param projectRoot - Root directory of the git repository
|
|
13
|
+
* @returns Short commit hash, or null if not available
|
|
14
|
+
*/
|
|
15
|
+
export declare function getCurrentCommitHash(projectRoot: string): string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Get diff stats between two commits.
|
|
18
|
+
*
|
|
19
|
+
* @param projectRoot - Root directory of the git repository
|
|
20
|
+
* @param fromHash - Starting commit hash
|
|
21
|
+
* @param toHash - Ending commit hash
|
|
22
|
+
* @returns Array of file diff stats, or null if not available
|
|
23
|
+
*/
|
|
24
|
+
export declare function getDiffStats(projectRoot: string, fromHash: string, toHash: string): FileDiffStat[] | null;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git utilities for enhanced run summary.
|
|
3
|
+
*/
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { logger } from '../../utils/logger.js';
|
|
6
|
+
/**
|
|
7
|
+
* Get the current commit hash (HEAD).
|
|
8
|
+
*
|
|
9
|
+
* @param projectRoot - Root directory of the git repository
|
|
10
|
+
* @returns Short commit hash, or null if not available
|
|
11
|
+
*/
|
|
12
|
+
export function getCurrentCommitHash(projectRoot) {
|
|
13
|
+
try {
|
|
14
|
+
const hash = execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
|
|
15
|
+
cwd: projectRoot,
|
|
16
|
+
encoding: 'utf-8',
|
|
17
|
+
timeout: 10_000,
|
|
18
|
+
}).trim();
|
|
19
|
+
return hash || null;
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
logger.warn(`getCurrentCommitHash failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get diff stats between two commits.
|
|
28
|
+
*
|
|
29
|
+
* @param projectRoot - Root directory of the git repository
|
|
30
|
+
* @param fromHash - Starting commit hash
|
|
31
|
+
* @param toHash - Ending commit hash
|
|
32
|
+
* @returns Array of file diff stats, or null if not available
|
|
33
|
+
*/
|
|
34
|
+
export function getDiffStats(projectRoot, fromHash, toHash) {
|
|
35
|
+
try {
|
|
36
|
+
const output = execFileSync('git', ['diff', '--numstat', `${fromHash}..${toHash}`], {
|
|
37
|
+
cwd: projectRoot,
|
|
38
|
+
encoding: 'utf-8',
|
|
39
|
+
timeout: 10_000,
|
|
40
|
+
}).trim();
|
|
41
|
+
if (!output) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const stats = [];
|
|
45
|
+
const lines = output.split('\n');
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
// Format: <added>\t<removed>\t<path>
|
|
48
|
+
const parts = line.split('\t');
|
|
49
|
+
if (parts.length !== 3)
|
|
50
|
+
continue;
|
|
51
|
+
const [addedStr, removedStr, path] = parts;
|
|
52
|
+
// Binary files show '-' for added/removed
|
|
53
|
+
const added = addedStr === '-' ? 0 : parseInt(addedStr, 10) || 0;
|
|
54
|
+
const removed = removedStr === '-' ? 0 : parseInt(removedStr, 10) || 0;
|
|
55
|
+
stats.push({ path, added, removed });
|
|
56
|
+
}
|
|
57
|
+
return stats;
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
logger.warn(`getDiffStats failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -104,6 +104,26 @@ export declare function deleteCharAfter(value: string, cursorIndex: number): Cur
|
|
|
104
104
|
* moveCursorByWordLeft("test", 0) // => 0 (no-op at start)
|
|
105
105
|
* ```
|
|
106
106
|
*/
|
|
107
|
+
/**
|
|
108
|
+
* Deletes the word before the cursor (Ctrl+W behavior)
|
|
109
|
+
*
|
|
110
|
+
* Skips trailing whitespace, then deletes the preceding word.
|
|
111
|
+
* Uses moveCursorByWordLeft to find the word boundary.
|
|
112
|
+
*
|
|
113
|
+
* @param value - Current input value
|
|
114
|
+
* @param cursorIndex - Current cursor position
|
|
115
|
+
* @returns New value and cursor index after deletion
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* deleteWordBefore("hello world", 11)
|
|
120
|
+
* // => { newValue: "hello ", newCursorIndex: 6 }
|
|
121
|
+
*
|
|
122
|
+
* deleteWordBefore("hello world", 0)
|
|
123
|
+
* // => { newValue: "hello world", newCursorIndex: 0 } (no-op at start)
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export declare function deleteWordBefore(value: string, cursorIndex: number): CursorManipulationResult;
|
|
107
127
|
export declare function moveCursorByWordLeft(value: string, cursorIndex: number): number;
|
|
108
128
|
/**
|
|
109
129
|
* Moves cursor to the end of the next word (word-right navigation)
|
|
@@ -134,6 +134,33 @@ export function deleteCharAfter(value, cursorIndex) {
|
|
|
134
134
|
* moveCursorByWordLeft("test", 0) // => 0 (no-op at start)
|
|
135
135
|
* ```
|
|
136
136
|
*/
|
|
137
|
+
/**
|
|
138
|
+
* Deletes the word before the cursor (Ctrl+W behavior)
|
|
139
|
+
*
|
|
140
|
+
* Skips trailing whitespace, then deletes the preceding word.
|
|
141
|
+
* Uses moveCursorByWordLeft to find the word boundary.
|
|
142
|
+
*
|
|
143
|
+
* @param value - Current input value
|
|
144
|
+
* @param cursorIndex - Current cursor position
|
|
145
|
+
* @returns New value and cursor index after deletion
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* deleteWordBefore("hello world", 11)
|
|
150
|
+
* // => { newValue: "hello ", newCursorIndex: 6 }
|
|
151
|
+
*
|
|
152
|
+
* deleteWordBefore("hello world", 0)
|
|
153
|
+
* // => { newValue: "hello world", newCursorIndex: 0 } (no-op at start)
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export function deleteWordBefore(value, cursorIndex) {
|
|
157
|
+
if (cursorIndex <= 0) {
|
|
158
|
+
return { newValue: value, newCursorIndex: 0 };
|
|
159
|
+
}
|
|
160
|
+
const newCursorIndex = moveCursorByWordLeft(value, cursorIndex);
|
|
161
|
+
const newValue = value.slice(0, newCursorIndex) + value.slice(cursorIndex);
|
|
162
|
+
return { newValue, newCursorIndex };
|
|
163
|
+
}
|
|
137
164
|
export function moveCursorByWordLeft(value, cursorIndex) {
|
|
138
165
|
let idx = cursorIndex;
|
|
139
166
|
// Skip trailing whitespace
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR and Issue utilities for enhanced run summary.
|
|
3
|
+
*/
|
|
4
|
+
export interface PrInfo {
|
|
5
|
+
number: number;
|
|
6
|
+
url: string;
|
|
7
|
+
state: string;
|
|
8
|
+
title: string;
|
|
9
|
+
}
|
|
10
|
+
export interface IssueInfo {
|
|
11
|
+
number: number;
|
|
12
|
+
url: string;
|
|
13
|
+
state: string;
|
|
14
|
+
title: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Get PR information for a branch.
|
|
18
|
+
*
|
|
19
|
+
* @param projectRoot - Root directory of the git repository
|
|
20
|
+
* @param branchName - Branch name to look up
|
|
21
|
+
* @returns PR info object, or null if no PR exists for this branch
|
|
22
|
+
* @throws When gh CLI is unavailable or the command fails
|
|
23
|
+
*/
|
|
24
|
+
export declare function getPrForBranch(projectRoot: string, branchName: string): PrInfo | null;
|
|
25
|
+
/**
|
|
26
|
+
* Get linked issue for a branch by parsing the PR body for closing keywords
|
|
27
|
+
* (Closes/Fixes/Resolves #N).
|
|
28
|
+
*
|
|
29
|
+
* @param projectRoot - Root directory of the git repository
|
|
30
|
+
* @param branchName - Branch name to look up
|
|
31
|
+
* @param prInfo - Optional pre-fetched PR info to avoid redundant gh call
|
|
32
|
+
* @returns Issue info object, or null if not found or gh not available
|
|
33
|
+
*/
|
|
34
|
+
export declare function getLinkedIssue(projectRoot: string, branchName: string, prInfo?: PrInfo | null): IssueInfo | null;
|