bmalph 2.4.0 → 2.5.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/README.md +50 -16
- package/dist/cli.js +6 -0
- package/dist/commands/doctor.d.ts +0 -11
- package/dist/commands/doctor.js +0 -49
- package/dist/commands/init.js +4 -2
- package/dist/commands/status.js +14 -1
- package/dist/commands/watch.d.ts +6 -0
- package/dist/commands/watch.js +19 -0
- package/dist/installer.d.ts +0 -6
- package/dist/installer.js +0 -10
- package/dist/transition/artifacts.d.ts +0 -2
- package/dist/transition/artifacts.js +0 -27
- package/dist/transition/index.d.ts +1 -1
- package/dist/transition/index.js +1 -1
- package/dist/utils/state.d.ts +0 -2
- package/dist/utils/validate.js +1 -0
- package/dist/watch/dashboard.d.ts +4 -0
- package/dist/watch/dashboard.js +60 -0
- package/dist/watch/file-watcher.d.ts +9 -0
- package/dist/watch/file-watcher.js +27 -0
- package/dist/watch/renderer.d.ts +16 -0
- package/dist/watch/renderer.js +241 -0
- package/dist/watch/state-reader.d.ts +9 -0
- package/dist/watch/state-reader.js +190 -0
- package/dist/watch/types.d.ts +55 -0
- package/dist/watch/types.js +1 -0
- package/package.json +9 -4
- package/ralph/lib/circuit_breaker.sh +86 -59
- package/ralph/lib/enable_core.sh +3 -6
- package/ralph/lib/response_analyzer.sh +5 -29
- package/ralph/lib/task_sources.sh +45 -11
- package/ralph/lib/wizard_utils.sh +9 -0
- package/ralph/ralph_import.sh +7 -2
- package/ralph/ralph_loop.sh +29 -34
- package/ralph/ralph_monitor.sh +4 -0
- package/slash-commands/bmalph-watch.md +20 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
const BOX_CHARS = {
|
|
3
|
+
topLeft: "\u250C",
|
|
4
|
+
topRight: "\u2510",
|
|
5
|
+
bottomLeft: "\u2514",
|
|
6
|
+
bottomRight: "\u2518",
|
|
7
|
+
horizontal: "\u2500",
|
|
8
|
+
vertical: "\u2502",
|
|
9
|
+
headerLeft: "\u2554",
|
|
10
|
+
headerRight: "\u2557",
|
|
11
|
+
headerBottom: "\u255A",
|
|
12
|
+
headerBottomRight: "\u255D",
|
|
13
|
+
headerHoriz: "\u2550",
|
|
14
|
+
headerVert: "\u2551",
|
|
15
|
+
};
|
|
16
|
+
const PROGRESS_FILLED = "\u2588";
|
|
17
|
+
const PROGRESS_EMPTY = "\u2591";
|
|
18
|
+
// eslint-disable-next-line no-control-regex
|
|
19
|
+
const ANSI_PATTERN = /\x1B\[\d+m/g;
|
|
20
|
+
function stripAnsi(str) {
|
|
21
|
+
return str.replace(ANSI_PATTERN, "");
|
|
22
|
+
}
|
|
23
|
+
export function padRight(str, len) {
|
|
24
|
+
const visualLen = stripAnsi(str).length;
|
|
25
|
+
if (visualLen >= len) {
|
|
26
|
+
return str;
|
|
27
|
+
}
|
|
28
|
+
return str + " ".repeat(len - visualLen);
|
|
29
|
+
}
|
|
30
|
+
export function progressBar(completed, total, width) {
|
|
31
|
+
if (total <= 0) {
|
|
32
|
+
return PROGRESS_EMPTY.repeat(width);
|
|
33
|
+
}
|
|
34
|
+
const ratio = Math.min(completed / total, 1);
|
|
35
|
+
const filled = Math.round(ratio * width);
|
|
36
|
+
return chalk.green(PROGRESS_FILLED.repeat(filled)) + PROGRESS_EMPTY.repeat(width - filled);
|
|
37
|
+
}
|
|
38
|
+
export function formatSessionAge(createdAt) {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
const start = new Date(createdAt).getTime();
|
|
41
|
+
const diffSeconds = Math.max(0, Math.floor((now - start) / 1000));
|
|
42
|
+
const hours = Math.floor(diffSeconds / 3600);
|
|
43
|
+
const minutes = Math.floor((diffSeconds % 3600) / 60);
|
|
44
|
+
const seconds = diffSeconds % 60;
|
|
45
|
+
if (hours > 0) {
|
|
46
|
+
return `${String(hours)}h ${String(minutes)}m`;
|
|
47
|
+
}
|
|
48
|
+
return `${String(minutes)}m ${String(seconds)}s`;
|
|
49
|
+
}
|
|
50
|
+
export function formatStatus(status) {
|
|
51
|
+
switch (status) {
|
|
52
|
+
case "running":
|
|
53
|
+
return chalk.yellow(status);
|
|
54
|
+
case "completed":
|
|
55
|
+
case "success":
|
|
56
|
+
return chalk.green(status);
|
|
57
|
+
case "halted":
|
|
58
|
+
case "stopped":
|
|
59
|
+
case "blocked":
|
|
60
|
+
return chalk.red(status);
|
|
61
|
+
default:
|
|
62
|
+
return status;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function formatCBState(state) {
|
|
66
|
+
switch (state) {
|
|
67
|
+
case "CLOSED":
|
|
68
|
+
return chalk.green(state);
|
|
69
|
+
case "HALF_OPEN":
|
|
70
|
+
return chalk.yellow(state);
|
|
71
|
+
case "OPEN":
|
|
72
|
+
return chalk.red(state);
|
|
73
|
+
default:
|
|
74
|
+
return state;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function formatTime(date) {
|
|
78
|
+
const h = String(date.getUTCHours()).padStart(2, "0");
|
|
79
|
+
const m = String(date.getUTCMinutes()).padStart(2, "0");
|
|
80
|
+
const s = String(date.getUTCSeconds()).padStart(2, "0");
|
|
81
|
+
return `${h}:${m}:${s}`;
|
|
82
|
+
}
|
|
83
|
+
function extractTime(timestamp) {
|
|
84
|
+
const timePart = timestamp.split(" ")[1];
|
|
85
|
+
return timePart ?? formatTime(new Date(timestamp));
|
|
86
|
+
}
|
|
87
|
+
export function box(title, lines, cols) {
|
|
88
|
+
const innerWidth = cols - 2;
|
|
89
|
+
const titleStr = title ? `\u2500 ${title} ` : "";
|
|
90
|
+
const topBorder = BOX_CHARS.topLeft +
|
|
91
|
+
titleStr +
|
|
92
|
+
BOX_CHARS.horizontal.repeat(Math.max(0, innerWidth - titleStr.length)) +
|
|
93
|
+
BOX_CHARS.topRight;
|
|
94
|
+
const bottomBorder = BOX_CHARS.bottomLeft + BOX_CHARS.horizontal.repeat(innerWidth) + BOX_CHARS.bottomRight;
|
|
95
|
+
const contentLines = lines.map((line) => BOX_CHARS.vertical + " " + padRight(line, innerWidth - 1) + BOX_CHARS.vertical);
|
|
96
|
+
return [topBorder, ...contentLines, bottomBorder].join("\n");
|
|
97
|
+
}
|
|
98
|
+
export function renderHeader(cols) {
|
|
99
|
+
const innerWidth = cols - 2;
|
|
100
|
+
const title = "RALPH MONITOR";
|
|
101
|
+
const padding = Math.max(0, Math.floor((innerWidth - title.length) / 2));
|
|
102
|
+
const centeredTitle = " ".repeat(padding) + title + " ".repeat(innerWidth - padding - title.length);
|
|
103
|
+
const topBorder = BOX_CHARS.headerLeft + BOX_CHARS.headerHoriz.repeat(innerWidth) + BOX_CHARS.headerRight;
|
|
104
|
+
const titleLine = BOX_CHARS.headerVert + chalk.bold(centeredTitle) + BOX_CHARS.headerVert;
|
|
105
|
+
const bottomBorder = BOX_CHARS.headerBottom + BOX_CHARS.headerHoriz.repeat(innerWidth) + BOX_CHARS.headerBottomRight;
|
|
106
|
+
return [topBorder, titleLine, bottomBorder].join("\n");
|
|
107
|
+
}
|
|
108
|
+
export function renderLoopPanel(loop, execution, session, cols) {
|
|
109
|
+
if (loop === null) {
|
|
110
|
+
return box("Loop Status", ["Status: waiting for data"], cols);
|
|
111
|
+
}
|
|
112
|
+
const apiPercent = loop.maxCallsPerHour > 0
|
|
113
|
+
? Math.round((loop.callsMadeThisHour / loop.maxCallsPerHour) * 100)
|
|
114
|
+
: 0;
|
|
115
|
+
const loopStr = `Loop: #${String(loop.loopCount)}`;
|
|
116
|
+
const statusStr = `Status: ${formatStatus(loop.status)}`;
|
|
117
|
+
const apiStr = `API: ${String(loop.callsMadeThisHour)}/${String(loop.maxCallsPerHour)} (${String(apiPercent)}%)`;
|
|
118
|
+
const line1 = `${padRight(loopStr, 17)}${padRight(statusStr, 21)}${apiStr}`;
|
|
119
|
+
const actionLabel = execution !== null ? execution.status : loop.lastAction;
|
|
120
|
+
const actionStr = `Action: ${actionLabel}`;
|
|
121
|
+
const sessionStr = session !== null ? `Session: ${formatSessionAge(session.createdAt)}` : "";
|
|
122
|
+
const innerWidth = cols - 4;
|
|
123
|
+
const sessionPad = Math.max(0, innerWidth - actionStr.length - sessionStr.length);
|
|
124
|
+
const line2 = `${actionStr}${" ".repeat(sessionPad)}${sessionStr}`;
|
|
125
|
+
return box("Loop Status", [line1, line2], cols);
|
|
126
|
+
}
|
|
127
|
+
export function renderCircuitBreakerPanel(cb, cols) {
|
|
128
|
+
const halfCols = Math.floor(cols / 2) - 1;
|
|
129
|
+
if (cb === null) {
|
|
130
|
+
return box("Circuit Breaker", ["N/A"], halfCols);
|
|
131
|
+
}
|
|
132
|
+
const lines = [
|
|
133
|
+
`State: ${formatCBState(cb.state)}`,
|
|
134
|
+
`No-progress: ${String(cb.consecutiveNoProgress)}`,
|
|
135
|
+
`Opens: ${String(cb.totalOpens)}`,
|
|
136
|
+
];
|
|
137
|
+
if (cb.state === "OPEN" && cb.reason) {
|
|
138
|
+
lines.push(`Reason: ${cb.reason}`);
|
|
139
|
+
}
|
|
140
|
+
return box("Circuit Breaker", lines, halfCols);
|
|
141
|
+
}
|
|
142
|
+
export function renderStoriesPanel(stories, cols) {
|
|
143
|
+
const halfCols = Math.floor(cols / 2) - 1;
|
|
144
|
+
if (stories === null) {
|
|
145
|
+
return box("Stories", ["N/A"], halfCols);
|
|
146
|
+
}
|
|
147
|
+
const total = stories.total;
|
|
148
|
+
const completed = stories.completed;
|
|
149
|
+
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
150
|
+
const bar = progressBar(completed, total, 20);
|
|
151
|
+
const lines = [
|
|
152
|
+
`Progress: ${String(completed)}/${String(total)} (${String(percent)}%)`,
|
|
153
|
+
`[${bar}]`,
|
|
154
|
+
];
|
|
155
|
+
if (stories.nextStory !== null) {
|
|
156
|
+
lines.push(`Next: ${stories.nextStory}`);
|
|
157
|
+
}
|
|
158
|
+
return box("Stories", lines, halfCols);
|
|
159
|
+
}
|
|
160
|
+
export function renderSideBySide(left, right, cols) {
|
|
161
|
+
const halfWidth = Math.floor(cols / 2) - 1;
|
|
162
|
+
const leftLines = left.split("\n");
|
|
163
|
+
const rightLines = right.split("\n");
|
|
164
|
+
const maxLines = Math.max(leftLines.length, rightLines.length);
|
|
165
|
+
const result = [];
|
|
166
|
+
for (let i = 0; i < maxLines; i++) {
|
|
167
|
+
const l = leftLines[i] ?? "";
|
|
168
|
+
const r = rightLines[i] ?? "";
|
|
169
|
+
result.push(padRight(l, halfWidth) + " " + r);
|
|
170
|
+
}
|
|
171
|
+
return result.join("\n");
|
|
172
|
+
}
|
|
173
|
+
export function renderAnalysisPanel(analysis, cols) {
|
|
174
|
+
if (analysis === null) {
|
|
175
|
+
return box("Last Analysis", ["N/A"], cols);
|
|
176
|
+
}
|
|
177
|
+
const yesNo = (v) => (v ? "yes" : "no");
|
|
178
|
+
const line1 = [
|
|
179
|
+
`Files: ${String(analysis.filesModified)}`,
|
|
180
|
+
`Confidence: ${String(analysis.confidenceScore)}%`,
|
|
181
|
+
`Test-only: ${yesNo(analysis.isTestOnly)}`,
|
|
182
|
+
`Stuck: ${yesNo(analysis.isStuck)}`,
|
|
183
|
+
].join(" ");
|
|
184
|
+
const line2 = [
|
|
185
|
+
`Exit signal: ${yesNo(analysis.exitSignal)}`,
|
|
186
|
+
`Permission denials: ${String(analysis.permissionDenialCount)}`,
|
|
187
|
+
].join(" ");
|
|
188
|
+
return box("Last Analysis", [line1, line2], cols);
|
|
189
|
+
}
|
|
190
|
+
export function renderLogsPanel(logs, cols) {
|
|
191
|
+
if (logs.length === 0) {
|
|
192
|
+
return box("Recent Activity", [chalk.dim("No activity yet")], cols);
|
|
193
|
+
}
|
|
194
|
+
const innerWidth = cols - 4;
|
|
195
|
+
const lines = logs.map((entry) => {
|
|
196
|
+
const time = extractTime(entry.timestamp);
|
|
197
|
+
const level = padRight(entry.level, 7);
|
|
198
|
+
const prefix = `[${time}] ${level}`;
|
|
199
|
+
const maxMsg = Math.max(0, innerWidth - prefix.length - 1);
|
|
200
|
+
const msg = entry.message.length > maxMsg ? entry.message.slice(0, maxMsg) : entry.message;
|
|
201
|
+
return `${chalk.dim(`[${time}]`)} ${level} ${msg}`;
|
|
202
|
+
});
|
|
203
|
+
return box("Recent Activity", lines, cols);
|
|
204
|
+
}
|
|
205
|
+
export function renderFooter(lastUpdated, cols) {
|
|
206
|
+
const left = chalk.dim("q quit");
|
|
207
|
+
const right = `Updated: ${formatTime(lastUpdated)}`;
|
|
208
|
+
const gap = Math.max(1, cols - "q quit".length - right.length);
|
|
209
|
+
return ` ${left}${" ".repeat(gap - 1)}${chalk.dim(right)}`;
|
|
210
|
+
}
|
|
211
|
+
function hasAnyData(state) {
|
|
212
|
+
return (state.loop !== null ||
|
|
213
|
+
state.circuitBreaker !== null ||
|
|
214
|
+
state.stories !== null ||
|
|
215
|
+
state.analysis !== null ||
|
|
216
|
+
state.execution !== null ||
|
|
217
|
+
state.session !== null ||
|
|
218
|
+
state.recentLogs.length > 0);
|
|
219
|
+
}
|
|
220
|
+
export function renderDashboard(state, cols) {
|
|
221
|
+
const width = cols ?? process.stdout.columns ?? 80;
|
|
222
|
+
if (!hasAnyData(state)) {
|
|
223
|
+
const lines = [];
|
|
224
|
+
lines.push(renderHeader(width));
|
|
225
|
+
lines.push("");
|
|
226
|
+
lines.push(chalk.dim(padRight(" Waiting for Ralph to start...", width)));
|
|
227
|
+
lines.push("");
|
|
228
|
+
lines.push(renderFooter(state.lastUpdated, width));
|
|
229
|
+
return lines.join("\n");
|
|
230
|
+
}
|
|
231
|
+
const sections = [];
|
|
232
|
+
sections.push(renderHeader(width));
|
|
233
|
+
sections.push(renderLoopPanel(state.loop, state.execution, state.session, width));
|
|
234
|
+
const leftPanel = renderCircuitBreakerPanel(state.circuitBreaker, width);
|
|
235
|
+
const rightPanel = renderStoriesPanel(state.stories, width);
|
|
236
|
+
sections.push(renderSideBySide(leftPanel, rightPanel, width));
|
|
237
|
+
sections.push(renderAnalysisPanel(state.analysis, width));
|
|
238
|
+
sections.push(renderLogsPanel(state.recentLogs, width));
|
|
239
|
+
sections.push(renderFooter(state.lastUpdated, width));
|
|
240
|
+
return sections.join("\n");
|
|
241
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DashboardState, LoopInfo, CircuitBreakerInfo, StoryProgress, AnalysisInfo, ExecutionProgress, SessionInfo, LogEntry } from "./types.js";
|
|
2
|
+
export declare function readDashboardState(projectDir: string): Promise<DashboardState>;
|
|
3
|
+
export declare function readLoopInfo(projectDir: string): Promise<LoopInfo | null>;
|
|
4
|
+
export declare function readCircuitBreakerInfo(projectDir: string): Promise<CircuitBreakerInfo | null>;
|
|
5
|
+
export declare function readStoryProgress(projectDir: string): Promise<StoryProgress | null>;
|
|
6
|
+
export declare function readAnalysisInfo(projectDir: string): Promise<AnalysisInfo | null>;
|
|
7
|
+
export declare function readExecutionProgress(projectDir: string): Promise<ExecutionProgress | null>;
|
|
8
|
+
export declare function readSessionInfo(projectDir: string): Promise<SessionInfo | null>;
|
|
9
|
+
export declare function readRecentLogs(projectDir: string, maxLines?: number): Promise<LogEntry[]>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { open, readFile } from "fs/promises";
|
|
3
|
+
import { readJsonFile } from "../utils/json.js";
|
|
4
|
+
import { RALPH_DIR } from "../utils/constants.js";
|
|
5
|
+
import { parseFixPlan } from "../transition/fix-plan.js";
|
|
6
|
+
import { validateCircuitBreakerState, validateRalphSession, normalizeRalphStatus, } from "../utils/validate.js";
|
|
7
|
+
const LOG_LINE_PATTERN = /^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] \[(\w+)\] (.+)$/;
|
|
8
|
+
const DEFAULT_MAX_LOG_LINES = 8;
|
|
9
|
+
const TAIL_BYTES = 4096;
|
|
10
|
+
export async function readDashboardState(projectDir) {
|
|
11
|
+
const [loop, circuitBreaker, stories, analysis, execution, session, recentLogs] = await Promise.all([
|
|
12
|
+
readLoopInfo(projectDir),
|
|
13
|
+
readCircuitBreakerInfo(projectDir),
|
|
14
|
+
readStoryProgress(projectDir),
|
|
15
|
+
readAnalysisInfo(projectDir),
|
|
16
|
+
readExecutionProgress(projectDir),
|
|
17
|
+
readSessionInfo(projectDir),
|
|
18
|
+
readRecentLogs(projectDir),
|
|
19
|
+
]);
|
|
20
|
+
const ralphCompleted = loop !== null && loop.status === "completed";
|
|
21
|
+
return {
|
|
22
|
+
loop,
|
|
23
|
+
circuitBreaker,
|
|
24
|
+
stories,
|
|
25
|
+
analysis,
|
|
26
|
+
execution,
|
|
27
|
+
session,
|
|
28
|
+
recentLogs,
|
|
29
|
+
ralphCompleted,
|
|
30
|
+
lastUpdated: new Date(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export async function readLoopInfo(projectDir) {
|
|
34
|
+
try {
|
|
35
|
+
const data = await readJsonFile(join(projectDir, RALPH_DIR, "status.json"));
|
|
36
|
+
if (data === null)
|
|
37
|
+
return null;
|
|
38
|
+
const normalized = normalizeRalphStatus(data);
|
|
39
|
+
const lastAction = typeof data.last_action === "string" ? data.last_action : "";
|
|
40
|
+
const callsMadeThisHour = typeof data.calls_made_this_hour === "number" ? data.calls_made_this_hour : 0;
|
|
41
|
+
const maxCallsPerHour = typeof data.max_calls_per_hour === "number" ? data.max_calls_per_hour : 0;
|
|
42
|
+
return {
|
|
43
|
+
loopCount: normalized.loopCount,
|
|
44
|
+
status: normalized.status,
|
|
45
|
+
lastAction,
|
|
46
|
+
callsMadeThisHour,
|
|
47
|
+
maxCallsPerHour,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export async function readCircuitBreakerInfo(projectDir) {
|
|
55
|
+
try {
|
|
56
|
+
const data = await readJsonFile(join(projectDir, RALPH_DIR, ".circuit_breaker_state"));
|
|
57
|
+
if (data === null)
|
|
58
|
+
return null;
|
|
59
|
+
const validated = validateCircuitBreakerState(data);
|
|
60
|
+
const totalOpens = typeof data.total_opens === "number" ? data.total_opens : 0;
|
|
61
|
+
return {
|
|
62
|
+
state: validated.state,
|
|
63
|
+
consecutiveNoProgress: validated.consecutive_no_progress,
|
|
64
|
+
totalOpens,
|
|
65
|
+
reason: validated.reason,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function readStoryProgress(projectDir) {
|
|
73
|
+
let content;
|
|
74
|
+
try {
|
|
75
|
+
content = await readFile(join(projectDir, RALPH_DIR, "@fix_plan.md"), "utf-8");
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const items = parseFixPlan(content);
|
|
81
|
+
const completed = items.filter((item) => item.completed).length;
|
|
82
|
+
const total = items.length;
|
|
83
|
+
const nextItem = items.find((item) => !item.completed);
|
|
84
|
+
const nextStory = nextItem ? `Story ${nextItem.id}: ${nextItem.title ?? ""}`.trim() : null;
|
|
85
|
+
return { completed, total, nextStory };
|
|
86
|
+
}
|
|
87
|
+
export async function readAnalysisInfo(projectDir) {
|
|
88
|
+
try {
|
|
89
|
+
const data = await readJsonFile(join(projectDir, RALPH_DIR, ".response_analysis"));
|
|
90
|
+
if (data === null)
|
|
91
|
+
return null;
|
|
92
|
+
const analysis = data.analysis;
|
|
93
|
+
if (typeof analysis !== "object" || analysis === null)
|
|
94
|
+
return null;
|
|
95
|
+
const a = analysis;
|
|
96
|
+
const filesModified = typeof a.files_modified === "number" ? a.files_modified : 0;
|
|
97
|
+
const confidenceScore = typeof a.confidence_score === "number" ? a.confidence_score : 0;
|
|
98
|
+
const isTestOnly = typeof a.is_test_only === "boolean" ? a.is_test_only : false;
|
|
99
|
+
const isStuck = typeof a.is_stuck === "boolean" ? a.is_stuck : false;
|
|
100
|
+
const exitSignal = typeof a.exit_signal === "boolean" ? a.exit_signal : false;
|
|
101
|
+
const hasPermissionDenials = typeof a.has_permission_denials === "boolean" ? a.has_permission_denials : false;
|
|
102
|
+
const permissionDenialCount = typeof a.permission_denial_count === "number" ? a.permission_denial_count : 0;
|
|
103
|
+
return {
|
|
104
|
+
filesModified,
|
|
105
|
+
confidenceScore,
|
|
106
|
+
isTestOnly,
|
|
107
|
+
isStuck,
|
|
108
|
+
exitSignal,
|
|
109
|
+
hasPermissionDenials,
|
|
110
|
+
permissionDenialCount,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export async function readExecutionProgress(projectDir) {
|
|
118
|
+
try {
|
|
119
|
+
const data = await readJsonFile(join(projectDir, RALPH_DIR, "progress.json"));
|
|
120
|
+
if (data === null)
|
|
121
|
+
return null;
|
|
122
|
+
const status = typeof data.status === "string" ? data.status : "";
|
|
123
|
+
if (status !== "executing")
|
|
124
|
+
return null;
|
|
125
|
+
const elapsedSeconds = typeof data.elapsed_seconds === "number" ? data.elapsed_seconds : 0;
|
|
126
|
+
return { status, elapsedSeconds };
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export async function readSessionInfo(projectDir) {
|
|
133
|
+
try {
|
|
134
|
+
const data = await readJsonFile(join(projectDir, RALPH_DIR, ".ralph_session"));
|
|
135
|
+
if (data === null)
|
|
136
|
+
return null;
|
|
137
|
+
const validated = validateRalphSession(data);
|
|
138
|
+
return {
|
|
139
|
+
createdAt: validated.created_at,
|
|
140
|
+
lastUsed: validated.last_used,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export async function readRecentLogs(projectDir, maxLines = DEFAULT_MAX_LOG_LINES) {
|
|
148
|
+
const logPath = join(projectDir, RALPH_DIR, "logs", "ralph.log");
|
|
149
|
+
let content;
|
|
150
|
+
try {
|
|
151
|
+
const fh = await open(logPath, "r");
|
|
152
|
+
try {
|
|
153
|
+
const stats = await fh.stat();
|
|
154
|
+
if (stats.size === 0) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
if (stats.size <= TAIL_BYTES) {
|
|
158
|
+
content = await fh.readFile("utf-8");
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const position = stats.size - TAIL_BYTES;
|
|
162
|
+
const buf = Buffer.alloc(TAIL_BYTES);
|
|
163
|
+
const { bytesRead } = await fh.read(buf, 0, TAIL_BYTES, position);
|
|
164
|
+
const raw = buf.toString("utf-8", 0, bytesRead);
|
|
165
|
+
const newlineIdx = raw.indexOf("\n");
|
|
166
|
+
content = newlineIdx >= 0 ? raw.slice(newlineIdx + 1) : raw;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
await fh.close();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
const lines = content.split(/\r?\n/).filter((line) => line.length > 0);
|
|
177
|
+
const tail = lines.slice(-maxLines);
|
|
178
|
+
const entries = [];
|
|
179
|
+
for (const line of tail) {
|
|
180
|
+
const match = LOG_LINE_PATTERN.exec(line);
|
|
181
|
+
if (match) {
|
|
182
|
+
entries.push({
|
|
183
|
+
timestamp: match[1],
|
|
184
|
+
level: match[2],
|
|
185
|
+
message: match[3],
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return entries;
|
|
190
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface LoopInfo {
|
|
2
|
+
loopCount: number;
|
|
3
|
+
status: string;
|
|
4
|
+
lastAction: string;
|
|
5
|
+
callsMadeThisHour: number;
|
|
6
|
+
maxCallsPerHour: number;
|
|
7
|
+
}
|
|
8
|
+
export interface CircuitBreakerInfo {
|
|
9
|
+
state: "CLOSED" | "HALF_OPEN" | "OPEN";
|
|
10
|
+
consecutiveNoProgress: number;
|
|
11
|
+
totalOpens: number;
|
|
12
|
+
reason?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface StoryProgress {
|
|
15
|
+
completed: number;
|
|
16
|
+
total: number;
|
|
17
|
+
nextStory: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface AnalysisInfo {
|
|
20
|
+
filesModified: number;
|
|
21
|
+
confidenceScore: number;
|
|
22
|
+
isTestOnly: boolean;
|
|
23
|
+
isStuck: boolean;
|
|
24
|
+
exitSignal: boolean;
|
|
25
|
+
hasPermissionDenials: boolean;
|
|
26
|
+
permissionDenialCount: number;
|
|
27
|
+
}
|
|
28
|
+
export interface ExecutionProgress {
|
|
29
|
+
status: "executing" | "idle";
|
|
30
|
+
elapsedSeconds: number;
|
|
31
|
+
}
|
|
32
|
+
export interface SessionInfo {
|
|
33
|
+
createdAt: string;
|
|
34
|
+
lastUsed?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface LogEntry {
|
|
37
|
+
timestamp: string;
|
|
38
|
+
level: string;
|
|
39
|
+
message: string;
|
|
40
|
+
}
|
|
41
|
+
export interface DashboardState {
|
|
42
|
+
loop: LoopInfo | null;
|
|
43
|
+
circuitBreaker: CircuitBreakerInfo | null;
|
|
44
|
+
stories: StoryProgress | null;
|
|
45
|
+
analysis: AnalysisInfo | null;
|
|
46
|
+
execution: ExecutionProgress | null;
|
|
47
|
+
session: SessionInfo | null;
|
|
48
|
+
recentLogs: LogEntry[];
|
|
49
|
+
ralphCompleted: boolean;
|
|
50
|
+
lastUpdated: Date;
|
|
51
|
+
}
|
|
52
|
+
export interface WatchOptions {
|
|
53
|
+
interval: number;
|
|
54
|
+
projectDir: string;
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bmalph",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Unified AI Development Framework - BMAD phases with Ralph execution loop
|
|
3
|
+
"version": "2.5.0",
|
|
4
|
+
"description": "Unified AI Development Framework - BMAD phases with Ralph execution loop",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"bmalph": "./bin/bmalph.js"
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"test:watch": "vitest",
|
|
14
14
|
"test:e2e": "vitest run --config vitest.config.e2e.ts",
|
|
15
15
|
"test:coverage": "vitest run --coverage",
|
|
16
|
-
"test:
|
|
16
|
+
"test:bash": "bash -c 'command -v bats &>/dev/null && bats tests/bash/*.bats tests/bash/drivers/*.bats || echo \"[skip] bats not installed\"'",
|
|
17
|
+
"test:all": "npm run test && npm run test:e2e && npm run test:bash",
|
|
17
18
|
"check": "npm run lint && npm run build && npm test",
|
|
18
19
|
"dev": "tsc --watch",
|
|
19
20
|
"lint": "eslint src tests",
|
|
@@ -31,7 +32,11 @@
|
|
|
31
32
|
"ai",
|
|
32
33
|
"development",
|
|
33
34
|
"framework",
|
|
34
|
-
"agents"
|
|
35
|
+
"agents",
|
|
36
|
+
"bmad",
|
|
37
|
+
"ralph",
|
|
38
|
+
"autonomous",
|
|
39
|
+
"coding-assistant"
|
|
35
40
|
],
|
|
36
41
|
"author": "Lars Cowe",
|
|
37
42
|
"license": "MIT",
|