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.
@@ -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
- setCompletionSummary({
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 summary = {
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(summary);
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;