wiggum-cli 0.16.0 → 0.17.1
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/bin/ralph.js +0 -0
- package/dist/agent/memory/ingest.d.ts +14 -0
- package/dist/agent/memory/ingest.js +77 -0
- package/dist/agent/memory/store.d.ts +15 -0
- package/dist/agent/memory/store.js +98 -0
- package/dist/agent/memory/types.d.ts +16 -0
- package/dist/agent/memory/types.js +14 -0
- package/dist/agent/orchestrator.d.ts +7 -0
- package/dist/agent/orchestrator.js +266 -0
- package/dist/agent/resolve-config.d.ts +26 -0
- package/dist/agent/resolve-config.js +43 -0
- package/dist/agent/tools/backlog.d.ts +27 -0
- package/dist/agent/tools/backlog.js +51 -0
- package/dist/agent/tools/dry-run.d.ts +106 -0
- package/dist/agent/tools/dry-run.js +119 -0
- package/dist/agent/tools/execution.d.ts +51 -0
- package/dist/agent/tools/execution.js +256 -0
- package/dist/agent/tools/feature-state.d.ts +43 -0
- package/dist/agent/tools/feature-state.js +184 -0
- package/dist/agent/tools/introspection.d.ts +23 -0
- package/dist/agent/tools/introspection.js +40 -0
- package/dist/agent/tools/memory.d.ts +44 -0
- package/dist/agent/tools/memory.js +99 -0
- package/dist/agent/tools/preflight.d.ts +7 -0
- package/dist/agent/tools/preflight.js +137 -0
- package/dist/agent/tools/reporting.d.ts +58 -0
- package/dist/agent/tools/reporting.js +119 -0
- package/dist/agent/tools/schemas.d.ts +2 -0
- package/dist/agent/tools/schemas.js +3 -0
- package/dist/agent/types.d.ts +45 -0
- package/dist/agent/types.js +1 -0
- package/dist/ai/conversation/conversation-manager.js +8 -0
- package/dist/ai/conversation/url-fetcher.js +27 -0
- package/dist/ai/providers.js +5 -5
- package/dist/commands/agent.d.ts +17 -0
- package/dist/commands/agent.js +114 -0
- package/dist/commands/monitor.js +50 -183
- package/dist/commands/new-auto.d.ts +15 -0
- package/dist/commands/new-auto.js +237 -0
- package/dist/commands/run.js +20 -10
- package/dist/commands/sync.d.ts +15 -0
- package/dist/commands/sync.js +68 -0
- package/dist/generator/config.d.ts +1 -41
- package/dist/generator/config.js +7 -0
- package/dist/generator/index.d.ts +2 -2
- package/dist/generator/templates.d.ts +2 -0
- package/dist/generator/templates.js +9 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +115 -4
- package/dist/repl/command-parser.d.ts +10 -0
- package/dist/repl/command-parser.js +10 -0
- package/dist/templates/prompts/PROMPT.md.tmpl +13 -10
- package/dist/templates/prompts/PROMPT_e2e.md.tmpl +13 -7
- package/dist/templates/prompts/PROMPT_feature.md.tmpl +16 -3
- package/dist/templates/prompts/PROMPT_review_auto.md.tmpl +32 -12
- package/dist/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
- package/dist/templates/prompts/PROMPT_review_merge.md.tmpl +39 -14
- package/dist/templates/prompts/PROMPT_verify.md.tmpl +5 -2
- package/dist/templates/scripts/feature-loop.sh.tmpl +441 -69
- package/dist/tui/app.d.ts +19 -2
- package/dist/tui/app.js +23 -4
- package/dist/tui/components/IssuePicker.d.ts +27 -0
- package/dist/tui/components/IssuePicker.js +73 -0
- package/dist/tui/components/RunCompletionSummary.js +6 -3
- package/dist/tui/components/StatusLine.d.ts +3 -1
- package/dist/tui/components/StatusLine.js +2 -2
- package/dist/tui/hooks/useAgentOrchestrator.d.ts +40 -0
- package/dist/tui/hooks/useAgentOrchestrator.js +491 -0
- package/dist/tui/orchestration/interview-orchestrator.d.ts +5 -1
- package/dist/tui/orchestration/interview-orchestrator.js +27 -6
- package/dist/tui/screens/AgentScreen.d.ts +24 -0
- package/dist/tui/screens/AgentScreen.js +209 -0
- package/dist/tui/screens/InitScreen.js +4 -0
- package/dist/tui/screens/InterviewScreen.d.ts +3 -1
- package/dist/tui/screens/InterviewScreen.js +146 -10
- package/dist/tui/screens/MainShell.d.ts +1 -1
- package/dist/tui/screens/MainShell.js +115 -4
- package/dist/tui/screens/RunScreen.js +72 -16
- package/dist/tui/utils/build-run-summary.d.ts +1 -1
- package/dist/tui/utils/build-run-summary.js +40 -84
- package/dist/tui/utils/clear-screen.d.ts +14 -0
- package/dist/tui/utils/clear-screen.js +16 -0
- package/dist/tui/utils/git-summary.d.ts +1 -0
- package/dist/tui/utils/git-summary.js +16 -0
- package/dist/tui/utils/loop-status.d.ts +41 -1
- package/dist/tui/utils/loop-status.js +243 -35
- package/dist/tui/utils/pr-summary.d.ts +3 -2
- package/dist/tui/utils/pr-summary.js +41 -6
- package/dist/utils/config.d.ts +8 -0
- package/dist/utils/config.js +8 -0
- package/dist/utils/github.d.ts +32 -0
- package/dist/utils/github.js +106 -0
- package/package.json +4 -1
- package/src/templates/prompts/PROMPT.md.tmpl +13 -10
- package/src/templates/prompts/PROMPT_e2e.md.tmpl +13 -7
- package/src/templates/prompts/PROMPT_feature.md.tmpl +16 -3
- package/src/templates/prompts/PROMPT_review_auto.md.tmpl +32 -12
- package/src/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
- package/src/templates/prompts/PROMPT_review_merge.md.tmpl +39 -14
- package/src/templates/prompts/PROMPT_verify.md.tmpl +5 -2
- package/src/templates/scripts/feature-loop.sh.tmpl +441 -69
|
@@ -69,6 +69,22 @@ export function getDiffStats(projectRoot, fromHash, toHash) {
|
|
|
69
69
|
* @param toHash - Ending commit hash (inclusive)
|
|
70
70
|
* @returns Array of commit entries, or null if not available
|
|
71
71
|
*/
|
|
72
|
+
export function getRecentCommits(projectRoot, count = 3) {
|
|
73
|
+
try {
|
|
74
|
+
const output = execFileSync('git', ['log', '--oneline', `-${count}`], { cwd: projectRoot, encoding: 'utf-8', timeout: 10_000 }).trim();
|
|
75
|
+
if (!output)
|
|
76
|
+
return [];
|
|
77
|
+
return output.split('\n').map((line) => {
|
|
78
|
+
const spaceIdx = line.indexOf(' ');
|
|
79
|
+
if (spaceIdx === -1)
|
|
80
|
+
return { hash: line, title: '' };
|
|
81
|
+
return { hash: line.substring(0, spaceIdx), title: line.substring(spaceIdx + 1) };
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
72
88
|
export function getCommitList(projectRoot, fromHash, toHash) {
|
|
73
89
|
try {
|
|
74
90
|
const output = execFileSync('git', ['log', '--oneline', `${fromHash}..${toHash}`], {
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Loop status helpers for the TUI run/monitor screens.
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Find the implementation plan file, checking main project and git worktrees.
|
|
6
|
+
*
|
|
7
|
+
* Shared between loop-status.ts and monitor.ts to avoid duplicating the
|
|
8
|
+
* worktree search logic.
|
|
9
|
+
*/
|
|
10
|
+
export declare function findImplementationPlan(projectRoot: string, specsRelPath: string, feature: string): string | null;
|
|
4
11
|
export interface LoopStatus {
|
|
5
12
|
running: boolean;
|
|
6
13
|
phase: string;
|
|
@@ -10,12 +17,14 @@ export interface LoopStatus {
|
|
|
10
17
|
tokensOutput: number;
|
|
11
18
|
cacheCreate: number;
|
|
12
19
|
cacheRead: number;
|
|
20
|
+
tokensUpdatedAt?: number;
|
|
13
21
|
}
|
|
14
22
|
export interface TaskCounts {
|
|
15
23
|
tasksDone: number;
|
|
16
24
|
tasksPending: number;
|
|
17
25
|
e2eDone: number;
|
|
18
26
|
e2ePending: number;
|
|
27
|
+
planExists: boolean;
|
|
19
28
|
}
|
|
20
29
|
/**
|
|
21
30
|
* Phase execution status and timing.
|
|
@@ -27,12 +36,17 @@ export interface PhaseInfo {
|
|
|
27
36
|
/** Human-readable phase label */
|
|
28
37
|
label: string;
|
|
29
38
|
/** Phase completion status */
|
|
30
|
-
status: 'success' | 'skipped' | 'failed';
|
|
39
|
+
status: 'success' | 'skipped' | 'failed' | 'started';
|
|
31
40
|
/** Duration in milliseconds, if available */
|
|
32
41
|
durationMs?: number;
|
|
33
42
|
/** Number of iterations in this phase (e.g., for implementation) */
|
|
34
43
|
iterations?: number;
|
|
35
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Phase ID to human-readable label mapping.
|
|
47
|
+
* Matches the phase IDs written by feature-loop.sh.
|
|
48
|
+
*/
|
|
49
|
+
export declare const PHASE_LABELS: Record<string, string>;
|
|
36
50
|
/**
|
|
37
51
|
* Return the conventional log file path for a feature loop.
|
|
38
52
|
*/
|
|
@@ -45,6 +59,14 @@ export declare function getLoopLogPath(feature: string): string;
|
|
|
45
59
|
* concurrent loops are rare, but callers should be aware of the limitation.
|
|
46
60
|
*/
|
|
47
61
|
export declare function detectPhase(feature: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Read the current phase from the `.phases` file written by feature-loop.sh.
|
|
64
|
+
* Returns the human-readable label of the active phase, or null if unavailable.
|
|
65
|
+
*
|
|
66
|
+
* The file format is: phase_id|status|start_timestamp|end_timestamp
|
|
67
|
+
* A line with status "started" and no end timestamp indicates the active phase.
|
|
68
|
+
*/
|
|
69
|
+
export declare function readCurrentPhase(feature: string): string | null;
|
|
48
70
|
/**
|
|
49
71
|
* Read loop status from temp files written by feature-loop.sh.
|
|
50
72
|
*
|
|
@@ -87,6 +109,10 @@ export interface ActivityEvent {
|
|
|
87
109
|
/** Inferred status based on event content */
|
|
88
110
|
status: 'success' | 'error' | 'in-progress';
|
|
89
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Returns true if a log line should be excluded from the activity feed.
|
|
114
|
+
*/
|
|
115
|
+
export declare function shouldSkipLine(line: string): boolean;
|
|
90
116
|
/**
|
|
91
117
|
* Parse the loop log file into structured activity events.
|
|
92
118
|
*
|
|
@@ -108,3 +134,17 @@ export declare function parsePhaseChanges(feature: string, lastKnownPhases?: Pha
|
|
|
108
134
|
events: ActivityEvent[];
|
|
109
135
|
currentPhases?: PhaseInfo[];
|
|
110
136
|
};
|
|
137
|
+
/**
|
|
138
|
+
* Parse phase information from the phases file written by feature-loop.sh.
|
|
139
|
+
*
|
|
140
|
+
* Format: phase_id|status|start_timestamp|end_timestamp
|
|
141
|
+
* Accepts both 3-field lines (started: phase_id|started|timestamp) and
|
|
142
|
+
* 4-field lines (completed: phase_id|status|start_ts|end_ts).
|
|
143
|
+
*
|
|
144
|
+
* The parser handles duplicate phase entries defensively (last status wins,
|
|
145
|
+
* durations aggregate), though feature-loop.sh normally writes one final
|
|
146
|
+
* line per phase.
|
|
147
|
+
*
|
|
148
|
+
* Used by build-run-summary.ts for completion summaries.
|
|
149
|
+
*/
|
|
150
|
+
export declare function parsePhases(phasesFilePath: string): PhaseInfo[];
|
|
@@ -6,6 +6,51 @@ import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { loadConfigWithDefaults } from '../../utils/config.js';
|
|
8
8
|
import { logger } from '../../utils/logger.js';
|
|
9
|
+
/**
|
|
10
|
+
* Find the implementation plan file, checking main project and git worktrees.
|
|
11
|
+
*
|
|
12
|
+
* Shared between loop-status.ts and monitor.ts to avoid duplicating the
|
|
13
|
+
* worktree search logic.
|
|
14
|
+
*/
|
|
15
|
+
export function findImplementationPlan(projectRoot, specsRelPath, feature) {
|
|
16
|
+
// 1. Check main project
|
|
17
|
+
const mainPath = join(projectRoot, specsRelPath, `${feature}-implementation-plan.md`);
|
|
18
|
+
if (existsSync(mainPath))
|
|
19
|
+
return mainPath;
|
|
20
|
+
// 2. Check git worktrees for the feature branch
|
|
21
|
+
try {
|
|
22
|
+
const output = execFileSync('git', ['worktree', 'list', '--porcelain'], {
|
|
23
|
+
cwd: projectRoot,
|
|
24
|
+
encoding: 'utf-8',
|
|
25
|
+
});
|
|
26
|
+
const worktrees = output.split('\n\n').filter(Boolean);
|
|
27
|
+
for (const wt of worktrees) {
|
|
28
|
+
const pathMatch = wt.match(/^worktree (.+)$/m);
|
|
29
|
+
const escapedFeature = feature.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
30
|
+
const branchMatch = wt.match(new RegExp(`^branch .+/feat/${escapedFeature}$`, 'm'));
|
|
31
|
+
if (pathMatch && branchMatch) {
|
|
32
|
+
const wtPlan = join(pathMatch[1], specsRelPath, `${feature}-implementation-plan.md`);
|
|
33
|
+
if (existsSync(wtPlan))
|
|
34
|
+
return wtPlan;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// git worktree list failed — ignore
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Phase ID to human-readable label mapping.
|
|
45
|
+
* Matches the phase IDs written by feature-loop.sh.
|
|
46
|
+
*/
|
|
47
|
+
export const PHASE_LABELS = {
|
|
48
|
+
planning: 'Planning',
|
|
49
|
+
implementation: 'Implementation',
|
|
50
|
+
e2e_testing: 'E2E Testing',
|
|
51
|
+
verification: 'Verification',
|
|
52
|
+
pr_review: 'PR & Review',
|
|
53
|
+
};
|
|
9
54
|
/**
|
|
10
55
|
* Track whether pgrep is available to avoid repeated failed calls.
|
|
11
56
|
* null = untested, true = available, false = unavailable
|
|
@@ -66,6 +111,48 @@ export function detectPhase(feature) {
|
|
|
66
111
|
return 'Running';
|
|
67
112
|
return 'Idle';
|
|
68
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Read the current phase from the `.phases` file written by feature-loop.sh.
|
|
116
|
+
* Returns the human-readable label of the active phase, or null if unavailable.
|
|
117
|
+
*
|
|
118
|
+
* The file format is: phase_id|status|start_timestamp|end_timestamp
|
|
119
|
+
* A line with status "started" and no end timestamp indicates the active phase.
|
|
120
|
+
*/
|
|
121
|
+
export function readCurrentPhase(feature) {
|
|
122
|
+
const phasesFile = `/tmp/ralph-loop-${feature}.phases`;
|
|
123
|
+
let content;
|
|
124
|
+
try {
|
|
125
|
+
content = readFileSync(phasesFile, 'utf-8').trim();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
if (!content)
|
|
131
|
+
return null;
|
|
132
|
+
const lines = content.split('\n');
|
|
133
|
+
let lastStartedPhase = null;
|
|
134
|
+
let lastCompletedPhase = null;
|
|
135
|
+
for (const line of lines) {
|
|
136
|
+
const parts = line.split('|');
|
|
137
|
+
if (parts.length < 2)
|
|
138
|
+
continue;
|
|
139
|
+
const [id, status] = parts;
|
|
140
|
+
if (status === 'started') {
|
|
141
|
+
lastStartedPhase = id;
|
|
142
|
+
}
|
|
143
|
+
else if (status === 'success' || status === 'failed' || status === 'skipped') {
|
|
144
|
+
lastCompletedPhase = id;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (lastStartedPhase) {
|
|
148
|
+
return PHASE_LABELS[lastStartedPhase] || lastStartedPhase;
|
|
149
|
+
}
|
|
150
|
+
if (lastCompletedPhase) {
|
|
151
|
+
const label = PHASE_LABELS[lastCompletedPhase] || lastCompletedPhase;
|
|
152
|
+
return `post-${label}`;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
69
156
|
/**
|
|
70
157
|
* Read loop status from temp files written by feature-loop.sh.
|
|
71
158
|
*
|
|
@@ -89,8 +176,14 @@ export function readLoopStatus(feature) {
|
|
|
89
176
|
try {
|
|
90
177
|
const content = readFileSync(fileToRead, 'utf-8').trim();
|
|
91
178
|
const parts = content.split('|');
|
|
92
|
-
|
|
93
|
-
|
|
179
|
+
// Require at least 2 fields (iteration|maxIterations); skip partial writes
|
|
180
|
+
if (parts.length >= 2) {
|
|
181
|
+
iteration = parseInt(parts[0] || '0', 10) || 0;
|
|
182
|
+
maxIterations = parseInt(parts[1] || '0', 10) || 0;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
logger.debug(`Status file has fewer than 2 fields, using defaults: ${content}`);
|
|
186
|
+
}
|
|
94
187
|
}
|
|
95
188
|
catch (err) {
|
|
96
189
|
logger.debug(`Failed to parse status file: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -100,28 +193,48 @@ export function readLoopStatus(feature) {
|
|
|
100
193
|
let tokensOutput = 0;
|
|
101
194
|
let cacheCreate = 0;
|
|
102
195
|
let cacheRead = 0;
|
|
196
|
+
let tokensUpdatedAt;
|
|
103
197
|
if (existsSync(tokensFile)) {
|
|
104
198
|
try {
|
|
105
199
|
const content = readFileSync(tokensFile, 'utf-8').trim();
|
|
106
200
|
const parts = content.split('|');
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
201
|
+
// Require at least 2 fields (input|output); cache fields are optional (legacy format)
|
|
202
|
+
if (parts.length >= 2) {
|
|
203
|
+
tokensInput = parseInt(parts[0] || '0', 10) || 0;
|
|
204
|
+
tokensOutput = parseInt(parts[1] || '0', 10) || 0;
|
|
205
|
+
cacheCreate = parts.length >= 3 ? (parseInt(parts[2] || '0', 10) || 0) : 0;
|
|
206
|
+
cacheRead = parts.length >= 4 ? (parseInt(parts[3] || '0', 10) || 0) : 0;
|
|
207
|
+
if (parts[4]) {
|
|
208
|
+
const epoch = parseInt(parts[4], 10);
|
|
209
|
+
if (epoch > 0)
|
|
210
|
+
tokensUpdatedAt = epoch * 1000; // convert to ms
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
logger.debug(`Tokens file has fewer than 2 fields, using defaults: ${content}`);
|
|
215
|
+
}
|
|
111
216
|
}
|
|
112
217
|
catch (err) {
|
|
113
218
|
logger.debug(`Failed to parse tokens file: ${err instanceof Error ? err.message : String(err)}`);
|
|
114
219
|
}
|
|
115
220
|
}
|
|
221
|
+
const running = isProcessRunning(`feature-loop.sh.*${feature}`);
|
|
222
|
+
// Prefer .phases file (written by feature-loop.sh on transitions) over process
|
|
223
|
+
// detection. pgrep-based detection can return stale results when old Claude
|
|
224
|
+
// sessions linger, causing the phase to appear stuck (e.g., "Planning" during
|
|
225
|
+
// Verification). The .phases file is the authoritative source of phase transitions.
|
|
226
|
+
const phaseFromFile = readCurrentPhase(feature);
|
|
227
|
+
let phase = phaseFromFile ?? detectPhase(feature);
|
|
116
228
|
return {
|
|
117
|
-
running
|
|
118
|
-
phase
|
|
229
|
+
running,
|
|
230
|
+
phase,
|
|
119
231
|
iteration,
|
|
120
232
|
maxIterations,
|
|
121
233
|
tokensInput,
|
|
122
234
|
tokensOutput,
|
|
123
235
|
cacheCreate,
|
|
124
236
|
cacheRead,
|
|
237
|
+
tokensUpdatedAt,
|
|
125
238
|
};
|
|
126
239
|
}
|
|
127
240
|
/**
|
|
@@ -143,12 +256,13 @@ export async function parseImplementationPlan(projectRoot, feature, specsDirOver
|
|
|
143
256
|
}
|
|
144
257
|
}
|
|
145
258
|
const specsDir = specsDirOverride || config?.paths.specs || '.ralph/specs';
|
|
146
|
-
const planPath =
|
|
259
|
+
const planPath = findImplementationPlan(projectRoot, specsDir, feature);
|
|
147
260
|
let tasksDone = 0;
|
|
148
261
|
let tasksPending = 0;
|
|
149
262
|
let e2eDone = 0;
|
|
150
263
|
let e2ePending = 0;
|
|
151
|
-
|
|
264
|
+
const planExists = planPath !== null;
|
|
265
|
+
if (planPath) {
|
|
152
266
|
try {
|
|
153
267
|
const content = readFileSync(planPath, 'utf-8');
|
|
154
268
|
const lines = content.split('\n');
|
|
@@ -171,12 +285,36 @@ export async function parseImplementationPlan(projectRoot, feature, specsDirOver
|
|
|
171
285
|
}
|
|
172
286
|
}
|
|
173
287
|
}
|
|
288
|
+
// Fallback: if no checkboxes found, count "#### Task N:" headers
|
|
289
|
+
if (tasksDone === 0 && tasksPending === 0 && e2eDone === 0 && e2ePending === 0) {
|
|
290
|
+
let headerTaskCount = 0;
|
|
291
|
+
for (const line of lines) {
|
|
292
|
+
if (line.match(/^#{1,4}\s+Task\s+\d+/i)) {
|
|
293
|
+
headerTaskCount++;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (headerTaskCount > 0) {
|
|
297
|
+
// Check phases file to determine completion
|
|
298
|
+
const phasesPath = `/tmp/ralph-loop-${feature}.phases`;
|
|
299
|
+
let implDone = false;
|
|
300
|
+
if (existsSync(phasesPath)) {
|
|
301
|
+
const phases = readFileSync(phasesPath, 'utf-8');
|
|
302
|
+
implDone = phases.includes('implementation|success');
|
|
303
|
+
}
|
|
304
|
+
if (implDone) {
|
|
305
|
+
tasksDone = headerTaskCount;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
tasksPending = headerTaskCount;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
174
312
|
}
|
|
175
313
|
catch (err) {
|
|
176
314
|
logger.debug(`Failed to parse implementation plan: ${err instanceof Error ? err.message : String(err)}`);
|
|
177
315
|
}
|
|
178
316
|
}
|
|
179
|
-
return { tasksDone, tasksPending, e2eDone, e2ePending };
|
|
317
|
+
return { tasksDone, tasksPending, e2eDone, e2ePending, planExists };
|
|
180
318
|
}
|
|
181
319
|
/**
|
|
182
320
|
* Get current git branch.
|
|
@@ -259,14 +397,14 @@ const SKIP_LINE_PATTERNS = [
|
|
|
259
397
|
/^Baseline commit:/,
|
|
260
398
|
/^Creating branch:/,
|
|
261
399
|
// Misc noise
|
|
262
|
-
/^\{"
|
|
400
|
+
/^\{"/, // Any line starting with JSON object (Claude result, log entries)
|
|
263
401
|
/^Pending implementation tasks: \d+$/, // raw task count (redundant with progress bar)
|
|
264
402
|
/^Ready for feedback\.?$/i, // Conversational filler
|
|
265
403
|
];
|
|
266
404
|
/**
|
|
267
405
|
* Returns true if a log line should be excluded from the activity feed.
|
|
268
406
|
*/
|
|
269
|
-
function shouldSkipLine(line) {
|
|
407
|
+
export function shouldSkipLine(line) {
|
|
270
408
|
return SKIP_LINE_PATTERNS.some((pattern) => pattern.test(line));
|
|
271
409
|
}
|
|
272
410
|
/**
|
|
@@ -279,13 +417,18 @@ function stripMarkdown(msg) {
|
|
|
279
417
|
.replace(/^\s*[-*]\s+/, '') // leading bullet points
|
|
280
418
|
.trim();
|
|
281
419
|
}
|
|
282
|
-
const SUCCESS_KEYWORDS =
|
|
283
|
-
const ERROR_KEYWORDS =
|
|
420
|
+
const SUCCESS_KEYWORDS = /\b(completed|passed|success|approved|fixed|resolved|merged|works)\b/i;
|
|
421
|
+
const ERROR_KEYWORDS = /\b(error|failed|failure)\b/i;
|
|
422
|
+
const POSITIVE_ERROR_CONTEXT = /\b(fixed|resolved|added|handled|handling|recovery|boundary|boundaries|tests?\s+passed)\b/i;
|
|
284
423
|
function inferStatus(message) {
|
|
285
424
|
if (SUCCESS_KEYWORDS.test(message))
|
|
286
425
|
return 'success';
|
|
287
|
-
if (ERROR_KEYWORDS.test(message))
|
|
426
|
+
if (ERROR_KEYWORDS.test(message)) {
|
|
427
|
+
// Avoid misclassifying positive actions that mention error-related words
|
|
428
|
+
if (POSITIVE_ERROR_CONTEXT.test(message))
|
|
429
|
+
return 'in-progress';
|
|
288
430
|
return 'error';
|
|
431
|
+
}
|
|
289
432
|
return 'in-progress';
|
|
290
433
|
}
|
|
291
434
|
/**
|
|
@@ -366,28 +509,26 @@ export function parsePhaseChanges(feature, lastKnownPhases) {
|
|
|
366
509
|
}
|
|
367
510
|
return { events: [] };
|
|
368
511
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
parsed = JSON.parse(rawContent);
|
|
372
|
-
if (!Array.isArray(parsed)) {
|
|
373
|
-
logger.debug(`parsePhaseChanges: expected array in ${phasesFile}, got ${typeof parsed}`);
|
|
374
|
-
return { events: [], currentPhases: lastKnownPhases };
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
catch (err) {
|
|
378
|
-
logger.debug(`parsePhaseChanges: invalid JSON in ${phasesFile}: ${err instanceof Error ? err.message : String(err)}`);
|
|
512
|
+
const content = rawContent.trim();
|
|
513
|
+
if (!content) {
|
|
379
514
|
return { events: [], currentPhases: lastKnownPhases };
|
|
380
515
|
}
|
|
381
|
-
//
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
516
|
+
// Parse pipe-delimited format: phase_id|status|start_timestamp|end_timestamp
|
|
517
|
+
const VALID_STATUSES = new Set(['success', 'skipped', 'failed', 'started']);
|
|
518
|
+
const phaseMap = new Map();
|
|
519
|
+
for (const line of content.split('\n')) {
|
|
520
|
+
const parts = line.split('|');
|
|
521
|
+
if (parts.length < 2)
|
|
522
|
+
continue;
|
|
523
|
+
const [id, status] = parts;
|
|
524
|
+
if (!id || !VALID_STATUSES.has(status))
|
|
525
|
+
continue;
|
|
526
|
+
const label = PHASE_LABELS[id] || id;
|
|
527
|
+
const validStatus = status;
|
|
528
|
+
// Last status wins for each phase id
|
|
529
|
+
phaseMap.set(id, { id, label, status: validStatus });
|
|
390
530
|
}
|
|
531
|
+
const currentPhases = Array.from(phaseMap.values());
|
|
391
532
|
const events = [];
|
|
392
533
|
const now = Date.now();
|
|
393
534
|
for (const current of currentPhases) {
|
|
@@ -411,3 +552,70 @@ export function parsePhaseChanges(feature, lastKnownPhases) {
|
|
|
411
552
|
}
|
|
412
553
|
return { events, currentPhases };
|
|
413
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* Parse phase information from the phases file written by feature-loop.sh.
|
|
557
|
+
*
|
|
558
|
+
* Format: phase_id|status|start_timestamp|end_timestamp
|
|
559
|
+
* Accepts both 3-field lines (started: phase_id|started|timestamp) and
|
|
560
|
+
* 4-field lines (completed: phase_id|status|start_ts|end_ts).
|
|
561
|
+
*
|
|
562
|
+
* The parser handles duplicate phase entries defensively (last status wins,
|
|
563
|
+
* durations aggregate), though feature-loop.sh normally writes one final
|
|
564
|
+
* line per phase.
|
|
565
|
+
*
|
|
566
|
+
* Used by build-run-summary.ts for completion summaries.
|
|
567
|
+
*/
|
|
568
|
+
export function parsePhases(phasesFilePath) {
|
|
569
|
+
if (!existsSync(phasesFilePath)) {
|
|
570
|
+
return [];
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
const content = readFileSync(phasesFilePath, 'utf-8').trim();
|
|
574
|
+
if (!content) {
|
|
575
|
+
return [];
|
|
576
|
+
}
|
|
577
|
+
const lines = content.split('\n');
|
|
578
|
+
const VALID_STATUSES = new Set(['success', 'skipped', 'failed', 'started']);
|
|
579
|
+
const phaseMap = new Map();
|
|
580
|
+
for (const line of lines) {
|
|
581
|
+
const parts = line.split('|');
|
|
582
|
+
if (parts.length < 2)
|
|
583
|
+
continue;
|
|
584
|
+
const [id, status] = parts;
|
|
585
|
+
if (!id || !VALID_STATUSES.has(status)) {
|
|
586
|
+
logger.warn(`Unknown phase status "${status}" for phase "${id}", treating as failed`);
|
|
587
|
+
// Still process with 'failed' status
|
|
588
|
+
}
|
|
589
|
+
const validatedStatus = VALID_STATUSES.has(status)
|
|
590
|
+
? status
|
|
591
|
+
: 'failed';
|
|
592
|
+
// Parse timestamps (may be absent for 3-field 'started' lines)
|
|
593
|
+
const startTime = parts.length >= 3 ? (parseInt(parts[2], 10) || 0) : 0;
|
|
594
|
+
const endTime = parts.length >= 4 ? (parseInt(parts[3], 10) || 0) : 0;
|
|
595
|
+
// Calculate duration (end - start) in milliseconds
|
|
596
|
+
const durationMs = endTime > 0 && startTime > 0 ? (endTime - startTime) * 1000 : undefined;
|
|
597
|
+
// Get or create phase entry
|
|
598
|
+
let phase = phaseMap.get(id);
|
|
599
|
+
if (!phase) {
|
|
600
|
+
phase = {
|
|
601
|
+
id,
|
|
602
|
+
label: PHASE_LABELS[id] || id,
|
|
603
|
+
status: validatedStatus,
|
|
604
|
+
durationMs: 0,
|
|
605
|
+
};
|
|
606
|
+
phaseMap.set(id, phase);
|
|
607
|
+
}
|
|
608
|
+
// Update status (last status wins)
|
|
609
|
+
phase.status = validatedStatus;
|
|
610
|
+
// Aggregate duration
|
|
611
|
+
if (durationMs !== undefined) {
|
|
612
|
+
phase.durationMs = (phase.durationMs || 0) + durationMs;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return Array.from(phaseMap.values());
|
|
616
|
+
}
|
|
617
|
+
catch (err) {
|
|
618
|
+
logger.warn(`Failed to parse phases file: ${err instanceof Error ? err.message : String(err)}`);
|
|
619
|
+
return [];
|
|
620
|
+
}
|
|
621
|
+
}
|
|
@@ -24,11 +24,12 @@ export interface IssueInfo {
|
|
|
24
24
|
export declare function getPrForBranch(projectRoot: string, branchName: string): PrInfo | null;
|
|
25
25
|
/**
|
|
26
26
|
* Get linked issue for a branch by parsing the PR body for closing keywords
|
|
27
|
-
* (Closes/Fixes/Resolves #N).
|
|
27
|
+
* (Closes/Fixes/Resolves #N), with fallbacks for spec file and feature name search.
|
|
28
28
|
*
|
|
29
29
|
* @param projectRoot - Root directory of the git repository
|
|
30
30
|
* @param branchName - Branch name to look up
|
|
31
31
|
* @param prInfo - Optional pre-fetched PR info to avoid redundant gh call
|
|
32
|
+
* @param featureName - Optional feature name for fallback issue detection
|
|
32
33
|
* @returns Issue info object, or null if not found or gh not available
|
|
33
34
|
*/
|
|
34
|
-
export declare function getLinkedIssue(projectRoot: string, branchName: string, prInfo?: PrInfo | null): IssueInfo | null;
|
|
35
|
+
export declare function getLinkedIssue(projectRoot: string, branchName: string, prInfo?: PrInfo | null, featureName?: string): IssueInfo | null;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* PR and Issue utilities for enhanced run summary.
|
|
3
3
|
*/
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
5
7
|
import { logger } from '../../utils/logger.js';
|
|
6
8
|
/**
|
|
7
9
|
* Get PR information for a branch.
|
|
@@ -33,16 +35,51 @@ export function getPrForBranch(projectRoot, branchName) {
|
|
|
33
35
|
title: pr.title,
|
|
34
36
|
};
|
|
35
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Extract issue number from PR body closing keywords or fallback strategies.
|
|
40
|
+
*/
|
|
41
|
+
function findIssueNumber(body, featureName, projectRoot) {
|
|
42
|
+
// Strategy 1: Closing keywords in PR body (Closes/Fixes/Resolves #N)
|
|
43
|
+
const issueMatch = body.match(/(?:closes|fixes|resolves)\s+#(\d+)/i);
|
|
44
|
+
if (issueMatch) {
|
|
45
|
+
return parseInt(issueMatch[1], 10);
|
|
46
|
+
}
|
|
47
|
+
// Strategy 2: Bare "#N" references in the body (e.g., "Closes #7" without keyword before it)
|
|
48
|
+
// Look for "# N" patterns that are likely issue references (not markdown headers)
|
|
49
|
+
const hashRefMatch = body.match(/(?:^|\s)#(\d+)\b/m);
|
|
50
|
+
if (hashRefMatch) {
|
|
51
|
+
return parseInt(hashRefMatch[1], 10);
|
|
52
|
+
}
|
|
53
|
+
// Strategy 3: Read spec file for issue reference
|
|
54
|
+
if (featureName && projectRoot) {
|
|
55
|
+
try {
|
|
56
|
+
const specPath = join(projectRoot, '.ralph', 'specs', `${featureName}.md`);
|
|
57
|
+
if (existsSync(specPath)) {
|
|
58
|
+
const specContent = readFileSync(specPath, 'utf-8');
|
|
59
|
+
// Look for "Issue: #N", "Source Issue: #N", "GitHub Issue: #N", or issue URLs
|
|
60
|
+
const specIssueMatch = specContent.match(/(?:issue|source|github)[:\s]*#(\d+)/i) || specContent.match(/github\.com\/[^/]+\/[^/]+\/issues\/(\d+)/i);
|
|
61
|
+
if (specIssueMatch) {
|
|
62
|
+
return parseInt(specIssueMatch[1], 10);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// spec read failure is non-fatal
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
36
72
|
/**
|
|
37
73
|
* Get linked issue for a branch by parsing the PR body for closing keywords
|
|
38
|
-
* (Closes/Fixes/Resolves #N).
|
|
74
|
+
* (Closes/Fixes/Resolves #N), with fallbacks for spec file and feature name search.
|
|
39
75
|
*
|
|
40
76
|
* @param projectRoot - Root directory of the git repository
|
|
41
77
|
* @param branchName - Branch name to look up
|
|
42
78
|
* @param prInfo - Optional pre-fetched PR info to avoid redundant gh call
|
|
79
|
+
* @param featureName - Optional feature name for fallback issue detection
|
|
43
80
|
* @returns Issue info object, or null if not found or gh not available
|
|
44
81
|
*/
|
|
45
|
-
export function getLinkedIssue(projectRoot, branchName, prInfo) {
|
|
82
|
+
export function getLinkedIssue(projectRoot, branchName, prInfo, featureName) {
|
|
46
83
|
try {
|
|
47
84
|
// Use provided PR info or fetch it
|
|
48
85
|
const pr = prInfo !== undefined ? prInfo : getPrForBranch(projectRoot, branchName);
|
|
@@ -57,12 +94,10 @@ export function getLinkedIssue(projectRoot, branchName, prInfo) {
|
|
|
57
94
|
}).trim();
|
|
58
95
|
const prData = JSON.parse(prBody);
|
|
59
96
|
const body = prData.body || '';
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (!issueMatch) {
|
|
97
|
+
const issueNumber = findIssueNumber(body, featureName, projectRoot);
|
|
98
|
+
if (!issueNumber) {
|
|
63
99
|
return null;
|
|
64
100
|
}
|
|
65
|
-
const issueNumber = parseInt(issueMatch[1], 10);
|
|
66
101
|
// Fetch the issue details
|
|
67
102
|
const issueOutput = execFileSync('gh', ['issue', 'view', String(issueNumber), '--json', 'number,url,state,title'], {
|
|
68
103
|
cwd: projectRoot,
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -50,6 +50,13 @@ export interface LoopConfig {
|
|
|
50
50
|
planningModel: string;
|
|
51
51
|
reviewMode: 'manual' | 'auto' | 'merge';
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Agent configuration in ralph.config.cjs
|
|
55
|
+
*/
|
|
56
|
+
export interface AgentSectionConfig {
|
|
57
|
+
defaultProvider: string;
|
|
58
|
+
defaultModel: string;
|
|
59
|
+
}
|
|
53
60
|
/**
|
|
54
61
|
* Full ralph.config.cjs structure
|
|
55
62
|
*/
|
|
@@ -59,6 +66,7 @@ export interface RalphConfig {
|
|
|
59
66
|
commands: CommandsConfig;
|
|
60
67
|
paths: PathsConfig;
|
|
61
68
|
loop: LoopConfig;
|
|
69
|
+
agent: AgentSectionConfig;
|
|
62
70
|
}
|
|
63
71
|
/**
|
|
64
72
|
* Default configuration values
|
package/dist/utils/config.js
CHANGED
|
@@ -47,6 +47,10 @@ export const DEFAULT_CONFIG = {
|
|
|
47
47
|
planningModel: 'opus',
|
|
48
48
|
reviewMode: 'manual',
|
|
49
49
|
},
|
|
50
|
+
agent: {
|
|
51
|
+
defaultProvider: '',
|
|
52
|
+
defaultModel: '',
|
|
53
|
+
},
|
|
50
54
|
};
|
|
51
55
|
/**
|
|
52
56
|
* Load ralph.config.cjs from a project directory
|
|
@@ -104,6 +108,10 @@ export async function loadConfigWithDefaults(projectRoot) {
|
|
|
104
108
|
...DEFAULT_CONFIG.loop,
|
|
105
109
|
...config.loop,
|
|
106
110
|
},
|
|
111
|
+
agent: {
|
|
112
|
+
...DEFAULT_CONFIG.agent,
|
|
113
|
+
...config?.agent,
|
|
114
|
+
},
|
|
107
115
|
};
|
|
108
116
|
}
|
|
109
117
|
/**
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare function isGhInstalled(): Promise<boolean>;
|
|
2
|
+
export declare function _resetGhCache(): void;
|
|
3
|
+
export interface GitHubIssueDetail {
|
|
4
|
+
title: string;
|
|
5
|
+
body: string;
|
|
6
|
+
labels: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function fetchGitHubIssue(owner: string, repo: string, number: number): Promise<GitHubIssueDetail | null>;
|
|
9
|
+
export interface GitHubIssueListItem {
|
|
10
|
+
number: number;
|
|
11
|
+
title: string;
|
|
12
|
+
state: 'open' | 'closed';
|
|
13
|
+
labels: string[];
|
|
14
|
+
createdAt: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ListIssuesResult {
|
|
17
|
+
issues: GitHubIssueListItem[];
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function listRepoIssues(owner: string, repo: string, search?: string, limit?: number): Promise<ListIssuesResult>;
|
|
21
|
+
export declare function detectGitHubRemote(projectRoot: string): Promise<GitHubRepo | null>;
|
|
22
|
+
export interface ParsedGitHubIssue {
|
|
23
|
+
owner: string;
|
|
24
|
+
repo: string;
|
|
25
|
+
number: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function isGitHubIssueUrl(input: string): ParsedGitHubIssue | null;
|
|
28
|
+
export interface GitHubRepo {
|
|
29
|
+
owner: string;
|
|
30
|
+
repo: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function parseGitHubRemote(remoteUrl: string): GitHubRepo | null;
|