codeharness 0.25.5 → 0.25.6
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/index.js
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
validateDockerfile,
|
|
52
52
|
warn,
|
|
53
53
|
writeState
|
|
54
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-6RL5EK57.js";
|
|
55
55
|
|
|
56
56
|
// src/index.ts
|
|
57
57
|
import { Command } from "commander";
|
|
@@ -169,600 +169,254 @@ function registerBridgeCommand(program) {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
// src/commands/run.ts
|
|
172
|
-
import { existsSync as
|
|
173
|
-
import { join as
|
|
172
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
173
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
174
174
|
import { StringDecoder as StringDecoder2 } from "string_decoder";
|
|
175
175
|
|
|
176
|
-
// src/
|
|
177
|
-
import {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
176
|
+
// src/modules/sprint/state.ts
|
|
177
|
+
import { readFileSync as readFileSync2, writeFileSync, renameSync, existsSync as existsSync3, unlinkSync } from "fs";
|
|
178
|
+
import { join as join2, dirname } from "path";
|
|
179
|
+
|
|
180
|
+
// src/modules/sprint/migration.ts
|
|
181
|
+
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
182
|
+
import { join } from "path";
|
|
183
|
+
var OLD_FILES = {
|
|
184
|
+
storyRetries: "ralph/.story_retries",
|
|
185
|
+
flaggedStories: "ralph/.flagged_stories",
|
|
186
|
+
ralphStatus: "ralph/status.json",
|
|
187
|
+
sprintStatusYaml: "_bmad-output/implementation-artifacts/sprint-status.yaml",
|
|
188
|
+
sessionIssues: "_bmad-output/implementation-artifacts/.session-issues.md"
|
|
188
189
|
};
|
|
189
|
-
function
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
function storyStatusToBeadsStatus(storyStatus) {
|
|
193
|
-
return STORY_TO_BEADS_STATUS[storyStatus] ?? null;
|
|
194
|
-
}
|
|
195
|
-
function storyKeyFromPath(filePath) {
|
|
196
|
-
const base = filePath.split("/").pop() ?? filePath;
|
|
197
|
-
return base.replace(/\.md$/, "");
|
|
190
|
+
function resolve(relative3) {
|
|
191
|
+
return join(process.cwd(), relative3);
|
|
198
192
|
}
|
|
199
|
-
function
|
|
200
|
-
const
|
|
201
|
-
if (!
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (!trimmed.endsWith(".md")) {
|
|
193
|
+
function readIfExists(relative3) {
|
|
194
|
+
const p = resolve(relative3);
|
|
195
|
+
if (!existsSync2(p)) return null;
|
|
196
|
+
try {
|
|
197
|
+
return readFileSync(p, "utf-8");
|
|
198
|
+
} catch {
|
|
206
199
|
return null;
|
|
207
200
|
}
|
|
208
|
-
return trimmed;
|
|
209
201
|
}
|
|
210
|
-
function
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
return match[1].trim();
|
|
202
|
+
function emptyStory() {
|
|
203
|
+
return {
|
|
204
|
+
status: "backlog",
|
|
205
|
+
attempts: 0,
|
|
206
|
+
lastAttempt: null,
|
|
207
|
+
lastError: null,
|
|
208
|
+
proofPath: null,
|
|
209
|
+
acResults: null
|
|
210
|
+
};
|
|
220
211
|
}
|
|
221
|
-
function
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
} else {
|
|
233
|
-
lines.unshift(`Status: ${newStatus}`, "");
|
|
234
|
-
}
|
|
235
|
-
writeFileSync(filePath, lines.join("\n"), "utf-8");
|
|
212
|
+
function upsertStory(stories, key, patch) {
|
|
213
|
+
stories[key] = { ...stories[key] ?? emptyStory(), ...patch };
|
|
214
|
+
}
|
|
215
|
+
function parseStoryRetries(content, stories) {
|
|
216
|
+
for (const line of content.split("\n")) {
|
|
217
|
+
const trimmed = line.trim();
|
|
218
|
+
if (!trimmed) continue;
|
|
219
|
+
const parts = trimmed.split(/\s+/);
|
|
220
|
+
if (parts.length < 2) continue;
|
|
221
|
+
const count = parseInt(parts[1], 10);
|
|
222
|
+
if (!isNaN(count)) upsertStory(stories, parts[0], { attempts: count });
|
|
236
223
|
}
|
|
237
224
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (!existsSync3(filePath)) {
|
|
248
|
-
return {};
|
|
225
|
+
function parseStoryRetriesRecord(content) {
|
|
226
|
+
const result = {};
|
|
227
|
+
for (const line of content.split("\n")) {
|
|
228
|
+
const trimmed = line.trim();
|
|
229
|
+
if (!trimmed) continue;
|
|
230
|
+
const parts = trimmed.split(/\s+/);
|
|
231
|
+
if (parts.length < 2) continue;
|
|
232
|
+
const count = parseInt(parts[1], 10);
|
|
233
|
+
if (!isNaN(count) && count >= 0) result[parts[0]] = count;
|
|
249
234
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
if (
|
|
258
|
-
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
function parseFlaggedStoriesList(content) {
|
|
238
|
+
const seen = /* @__PURE__ */ new Set();
|
|
239
|
+
const result = [];
|
|
240
|
+
for (const line of content.split("\n")) {
|
|
241
|
+
const trimmed = line.trim();
|
|
242
|
+
if (trimmed !== "" && !seen.has(trimmed)) {
|
|
243
|
+
seen.add(trimmed);
|
|
244
|
+
result.push(trimmed);
|
|
259
245
|
}
|
|
260
|
-
return devStatus;
|
|
261
|
-
} catch {
|
|
262
|
-
return {};
|
|
263
246
|
}
|
|
247
|
+
return result;
|
|
264
248
|
}
|
|
265
|
-
function
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
warn(`sprint-status.yaml not found at ${filePath}, skipping update`);
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
const content = readFileSync2(filePath, "utf-8");
|
|
273
|
-
const keyPattern = new RegExp(`^(\\s*${escapeRegExp(storyKey)}:\\s*)\\S+(.*)$`, "m");
|
|
274
|
-
if (!keyPattern.test(content)) {
|
|
275
|
-
return;
|
|
249
|
+
function parseFlaggedStories(content, stories) {
|
|
250
|
+
for (const line of content.split("\n")) {
|
|
251
|
+
const key = line.trim();
|
|
252
|
+
if (key) upsertStory(stories, key, { status: "blocked" });
|
|
276
253
|
}
|
|
277
|
-
const updated = content.replace(keyPattern, `$1${newStatus}$2`);
|
|
278
|
-
writeFileSync2(filePath, updated, "utf-8");
|
|
279
254
|
}
|
|
280
|
-
function
|
|
281
|
-
|
|
255
|
+
function mapYamlStatus(value) {
|
|
256
|
+
const mapping = {
|
|
257
|
+
done: "done",
|
|
258
|
+
backlog: "backlog",
|
|
259
|
+
verifying: "verifying",
|
|
260
|
+
"in-progress": "in-progress",
|
|
261
|
+
"ready-for-dev": "ready",
|
|
262
|
+
blocked: "blocked",
|
|
263
|
+
failed: "failed",
|
|
264
|
+
review: "review",
|
|
265
|
+
ready: "ready"
|
|
266
|
+
};
|
|
267
|
+
return mapping[value.trim().toLowerCase()] ?? null;
|
|
282
268
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
storyKey: "",
|
|
294
|
-
beadsId,
|
|
295
|
-
previousStatus: "",
|
|
296
|
-
newStatus: "",
|
|
297
|
-
synced: false,
|
|
298
|
-
error: `Beads issue not found: ${beadsId}`
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
const storyFilePath = resolveStoryFilePath(issue);
|
|
302
|
-
if (!storyFilePath) {
|
|
303
|
-
return {
|
|
304
|
-
storyKey: "",
|
|
305
|
-
beadsId,
|
|
306
|
-
previousStatus: issue.status,
|
|
307
|
-
newStatus: "",
|
|
308
|
-
synced: false,
|
|
309
|
-
error: `No story file path in beads issue description: ${beadsId}`
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
const storyKey = storyKeyFromPath(storyFilePath);
|
|
313
|
-
const fullPath = join2(root, storyFilePath);
|
|
314
|
-
const currentStoryStatus = readStoryFileStatus(fullPath);
|
|
315
|
-
if (currentStoryStatus === null) {
|
|
316
|
-
return {
|
|
317
|
-
storyKey,
|
|
318
|
-
beadsId,
|
|
319
|
-
previousStatus: issue.status,
|
|
320
|
-
newStatus: "",
|
|
321
|
-
synced: false,
|
|
322
|
-
error: `Story file not found or has no Status line: ${storyFilePath}`
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
const targetStoryStatus = beadsStatusToStoryStatus(issue.status);
|
|
326
|
-
if (!targetStoryStatus) {
|
|
327
|
-
return {
|
|
328
|
-
storyKey,
|
|
329
|
-
beadsId,
|
|
330
|
-
previousStatus: currentStoryStatus,
|
|
331
|
-
newStatus: "",
|
|
332
|
-
synced: false,
|
|
333
|
-
error: `Unknown beads status: ${issue.status}`
|
|
334
|
-
};
|
|
269
|
+
function parseSprintStatusYaml(content, stories) {
|
|
270
|
+
for (const line of content.split("\n")) {
|
|
271
|
+
const trimmed = line.trim();
|
|
272
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
273
|
+
const match = trimmed.match(/^([a-zA-Z0-9_-]+):\s*(.+)$/);
|
|
274
|
+
if (!match) continue;
|
|
275
|
+
const key = match[1];
|
|
276
|
+
if (key === "development_status" || key.startsWith("epic-")) continue;
|
|
277
|
+
const status = mapYamlStatus(match[2]);
|
|
278
|
+
if (status) upsertStory(stories, key, { status });
|
|
335
279
|
}
|
|
336
|
-
|
|
280
|
+
}
|
|
281
|
+
function parseRalphStatus(content) {
|
|
282
|
+
try {
|
|
283
|
+
const data = JSON.parse(content);
|
|
337
284
|
return {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
285
|
+
active: data.status === "running",
|
|
286
|
+
startedAt: null,
|
|
287
|
+
iteration: data.loop_count ?? 0,
|
|
288
|
+
cost: 0,
|
|
289
|
+
completed: [],
|
|
290
|
+
failed: [],
|
|
291
|
+
currentStory: null,
|
|
292
|
+
currentPhase: null,
|
|
293
|
+
lastAction: null,
|
|
294
|
+
acProgress: null
|
|
343
295
|
};
|
|
296
|
+
} catch {
|
|
297
|
+
return null;
|
|
344
298
|
}
|
|
345
|
-
updateStoryFileStatus(fullPath, targetStoryStatus);
|
|
346
|
-
updateSprintStatus(storyKey, targetStoryStatus, root);
|
|
347
|
-
return {
|
|
348
|
-
storyKey,
|
|
349
|
-
beadsId,
|
|
350
|
-
previousStatus: currentStoryStatus,
|
|
351
|
-
newStatus: targetStoryStatus,
|
|
352
|
-
synced: true
|
|
353
|
-
};
|
|
354
299
|
}
|
|
355
|
-
function
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const fullPath = join2(root, storyFilePath);
|
|
359
|
-
const currentStoryStatus = readStoryFileStatus(fullPath);
|
|
360
|
-
if (currentStoryStatus === null) {
|
|
300
|
+
function parseRalphStatusToSession(content) {
|
|
301
|
+
try {
|
|
302
|
+
const data = JSON.parse(content);
|
|
361
303
|
return {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
synced: false,
|
|
367
|
-
error: `Story file not found or has no Status line: ${storyFilePath}`
|
|
304
|
+
active: data.status === "running",
|
|
305
|
+
startedAt: null,
|
|
306
|
+
iteration: data.loop_count ?? 0,
|
|
307
|
+
elapsedSeconds: data.elapsed_seconds ?? 0
|
|
368
308
|
};
|
|
309
|
+
} catch {
|
|
310
|
+
return null;
|
|
369
311
|
}
|
|
370
|
-
const issues = beadsFns.listIssues();
|
|
371
|
-
const issue = issues.find((i) => {
|
|
372
|
-
const path = resolveStoryFilePath(i);
|
|
373
|
-
if (!path) return false;
|
|
374
|
-
return storyKeyFromPath(path) === storyKey;
|
|
375
|
-
});
|
|
376
|
-
if (!issue) {
|
|
377
|
-
return {
|
|
378
|
-
storyKey,
|
|
379
|
-
beadsId: "",
|
|
380
|
-
previousStatus: currentStoryStatus,
|
|
381
|
-
newStatus: "",
|
|
382
|
-
synced: false,
|
|
383
|
-
error: `No beads issue found for story: ${storyKey}`
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
const targetBeadsStatus = storyStatusToBeadsStatus(currentStoryStatus);
|
|
387
|
-
if (!targetBeadsStatus) {
|
|
388
|
-
return {
|
|
389
|
-
storyKey,
|
|
390
|
-
beadsId: issue.id,
|
|
391
|
-
previousStatus: currentStoryStatus,
|
|
392
|
-
newStatus: "",
|
|
393
|
-
synced: false,
|
|
394
|
-
error: `Unknown story status: ${currentStoryStatus}`
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
if (issue.status === targetBeadsStatus) {
|
|
398
|
-
return {
|
|
399
|
-
storyKey,
|
|
400
|
-
beadsId: issue.id,
|
|
401
|
-
previousStatus: currentStoryStatus,
|
|
402
|
-
newStatus: currentStoryStatus,
|
|
403
|
-
synced: false
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
if (targetBeadsStatus === "closed") {
|
|
407
|
-
beadsFns.closeIssue(issue.id);
|
|
408
|
-
} else {
|
|
409
|
-
beadsFns.updateIssue(issue.id, { status: targetBeadsStatus });
|
|
410
|
-
}
|
|
411
|
-
updateSprintStatus(storyKey, currentStoryStatus, root);
|
|
412
|
-
return {
|
|
413
|
-
storyKey,
|
|
414
|
-
beadsId: issue.id,
|
|
415
|
-
previousStatus: issue.status,
|
|
416
|
-
newStatus: targetBeadsStatus,
|
|
417
|
-
synced: true
|
|
418
|
-
};
|
|
419
312
|
}
|
|
420
|
-
function
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
previousStatus: issue.status,
|
|
441
|
-
newStatus: "closed",
|
|
442
|
-
synced: false,
|
|
443
|
-
error: `No story file path in beads issue description: ${beadsId}`
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
const storyKey = storyKeyFromPath(storyFilePath);
|
|
447
|
-
const fullPath = join2(root, storyFilePath);
|
|
448
|
-
const previousStatus = readStoryFileStatus(fullPath);
|
|
449
|
-
if (previousStatus === null) {
|
|
450
|
-
if (!existsSync4(fullPath)) {
|
|
451
|
-
return {
|
|
452
|
-
storyKey,
|
|
453
|
-
beadsId,
|
|
454
|
-
previousStatus: "",
|
|
455
|
-
newStatus: "closed",
|
|
456
|
-
synced: false,
|
|
457
|
-
error: `Story file not found: ${storyFilePath}`
|
|
458
|
-
};
|
|
313
|
+
function parseSessionIssues(content) {
|
|
314
|
+
const items = [];
|
|
315
|
+
let currentStory = "";
|
|
316
|
+
let itemId = 0;
|
|
317
|
+
for (const line of content.split("\n")) {
|
|
318
|
+
const headerMatch = line.match(/^###\s+([a-zA-Z0-9_-]+)\s*[—-]/);
|
|
319
|
+
if (headerMatch) {
|
|
320
|
+
currentStory = headerMatch[1];
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const bulletMatch = line.match(/^-\s+(.+)/);
|
|
324
|
+
if (bulletMatch && currentStory) {
|
|
325
|
+
itemId++;
|
|
326
|
+
items.push({
|
|
327
|
+
id: `migrated-${itemId}`,
|
|
328
|
+
story: currentStory,
|
|
329
|
+
description: bulletMatch[1],
|
|
330
|
+
source: "retro",
|
|
331
|
+
resolved: false
|
|
332
|
+
});
|
|
459
333
|
}
|
|
460
334
|
}
|
|
461
|
-
|
|
462
|
-
|
|
335
|
+
return items;
|
|
336
|
+
}
|
|
337
|
+
function migrateV1ToV2(v1) {
|
|
338
|
+
const defaults = defaultState();
|
|
339
|
+
const retriesContent = readIfExists(OLD_FILES.storyRetries);
|
|
340
|
+
const retries = retriesContent ? parseStoryRetriesRecord(retriesContent) : {};
|
|
341
|
+
const flaggedContent = readIfExists(OLD_FILES.flaggedStories);
|
|
342
|
+
const flagged = flaggedContent ? parseFlaggedStoriesList(flaggedContent) : [];
|
|
343
|
+
const statusContent = readIfExists(OLD_FILES.ralphStatus);
|
|
344
|
+
const session = statusContent ? parseRalphStatusToSession(statusContent) ?? defaults.session : defaults.session;
|
|
463
345
|
return {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
346
|
+
version: 2,
|
|
347
|
+
sprint: v1.sprint,
|
|
348
|
+
stories: v1.stories,
|
|
349
|
+
retries,
|
|
350
|
+
flagged,
|
|
351
|
+
epics: {},
|
|
352
|
+
session,
|
|
353
|
+
observability: defaults.observability,
|
|
354
|
+
run: {
|
|
355
|
+
...defaults.run,
|
|
356
|
+
...v1.run
|
|
357
|
+
},
|
|
358
|
+
actionItems: v1.actionItems
|
|
469
359
|
};
|
|
470
360
|
}
|
|
471
|
-
function
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
let issues;
|
|
361
|
+
function migrateFromOldFormat() {
|
|
362
|
+
const hasAnyOldFile = Object.values(OLD_FILES).some((rel) => existsSync2(resolve(rel)));
|
|
363
|
+
if (!hasAnyOldFile) return fail2("No old format files found for migration");
|
|
475
364
|
try {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
const storyKey = storyKeyFromPath(storyFilePath);
|
|
495
|
-
try {
|
|
496
|
-
if (direction === "beads-to-files" || direction === "bidirectional") {
|
|
497
|
-
const result = syncBeadsToStoryFile(issue.id, { listIssues: cachedListIssues }, root);
|
|
498
|
-
results.push(result);
|
|
499
|
-
} else if (direction === "files-to-beads") {
|
|
500
|
-
const result = syncStoryFileToBeads(storyKey, {
|
|
501
|
-
listIssues: cachedListIssues,
|
|
502
|
-
updateIssue: beadsFns.updateIssue,
|
|
503
|
-
closeIssue: beadsFns.closeIssue
|
|
504
|
-
}, root);
|
|
505
|
-
results.push(result);
|
|
506
|
-
}
|
|
507
|
-
} catch (err) {
|
|
508
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
509
|
-
results.push({
|
|
510
|
-
storyKey,
|
|
511
|
-
beadsId: issue.id,
|
|
512
|
-
previousStatus: "",
|
|
513
|
-
newStatus: "",
|
|
514
|
-
synced: false,
|
|
515
|
-
error: message
|
|
516
|
-
});
|
|
365
|
+
const stories = {};
|
|
366
|
+
let run = defaultState().run;
|
|
367
|
+
let actionItems = [];
|
|
368
|
+
const yamlContent = readIfExists(OLD_FILES.sprintStatusYaml);
|
|
369
|
+
if (yamlContent) parseSprintStatusYaml(yamlContent, stories);
|
|
370
|
+
const retriesContent = readIfExists(OLD_FILES.storyRetries);
|
|
371
|
+
if (retriesContent) parseStoryRetries(retriesContent, stories);
|
|
372
|
+
const flaggedContent = readIfExists(OLD_FILES.flaggedStories);
|
|
373
|
+
if (flaggedContent) parseFlaggedStories(flaggedContent, stories);
|
|
374
|
+
const statusContent = readIfExists(OLD_FILES.ralphStatus);
|
|
375
|
+
let session = defaultState().session;
|
|
376
|
+
if (statusContent) {
|
|
377
|
+
const parsed = parseRalphStatus(statusContent);
|
|
378
|
+
if (parsed) run = parsed;
|
|
379
|
+
const parsedSession = parseRalphStatusToSession(statusContent);
|
|
380
|
+
if (parsedSession) session = parsedSession;
|
|
517
381
|
}
|
|
382
|
+
const issuesContent = readIfExists(OLD_FILES.sessionIssues);
|
|
383
|
+
if (issuesContent) actionItems = parseSessionIssues(issuesContent);
|
|
384
|
+
const sprint = computeSprintCounts(stories);
|
|
385
|
+
const retries = retriesContent ? parseStoryRetriesRecord(retriesContent) : {};
|
|
386
|
+
const flagged = flaggedContent ? parseFlaggedStoriesList(flaggedContent) : [];
|
|
387
|
+
const migrated = {
|
|
388
|
+
version: 2,
|
|
389
|
+
sprint,
|
|
390
|
+
stories,
|
|
391
|
+
retries,
|
|
392
|
+
flagged,
|
|
393
|
+
epics: {},
|
|
394
|
+
session,
|
|
395
|
+
observability: defaultState().observability,
|
|
396
|
+
run,
|
|
397
|
+
actionItems
|
|
398
|
+
};
|
|
399
|
+
const writeResult = writeStateAtomic(migrated);
|
|
400
|
+
if (!writeResult.success) return fail2(writeResult.error);
|
|
401
|
+
return ok2(migrated);
|
|
402
|
+
} catch (err) {
|
|
403
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
404
|
+
return fail2(`Migration failed: ${msg}`);
|
|
518
405
|
}
|
|
519
|
-
return results;
|
|
520
406
|
}
|
|
521
407
|
|
|
522
408
|
// src/modules/sprint/state.ts
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
// src/modules/sprint/migration.ts
|
|
527
|
-
import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
|
|
528
|
-
import { join as join3 } from "path";
|
|
529
|
-
var OLD_FILES = {
|
|
530
|
-
storyRetries: "ralph/.story_retries",
|
|
531
|
-
flaggedStories: "ralph/.flagged_stories",
|
|
532
|
-
ralphStatus: "ralph/status.json",
|
|
533
|
-
sprintStatusYaml: "_bmad-output/implementation-artifacts/sprint-status.yaml",
|
|
534
|
-
sessionIssues: "_bmad-output/implementation-artifacts/.session-issues.md"
|
|
535
|
-
};
|
|
536
|
-
function resolve(relative3) {
|
|
537
|
-
return join3(process.cwd(), relative3);
|
|
409
|
+
function projectRoot() {
|
|
410
|
+
return process.cwd();
|
|
538
411
|
}
|
|
539
|
-
function
|
|
540
|
-
|
|
541
|
-
if (!existsSync5(p)) return null;
|
|
542
|
-
try {
|
|
543
|
-
return readFileSync3(p, "utf-8");
|
|
544
|
-
} catch {
|
|
545
|
-
return null;
|
|
546
|
-
}
|
|
412
|
+
function statePath() {
|
|
413
|
+
return join2(projectRoot(), "sprint-state.json");
|
|
547
414
|
}
|
|
548
|
-
function
|
|
549
|
-
return
|
|
550
|
-
status: "backlog",
|
|
551
|
-
attempts: 0,
|
|
552
|
-
lastAttempt: null,
|
|
553
|
-
lastError: null,
|
|
554
|
-
proofPath: null,
|
|
555
|
-
acResults: null
|
|
556
|
-
};
|
|
415
|
+
function tmpPath() {
|
|
416
|
+
return join2(projectRoot(), ".sprint-state.json.tmp");
|
|
557
417
|
}
|
|
558
|
-
function
|
|
559
|
-
|
|
560
|
-
}
|
|
561
|
-
function parseStoryRetries(content, stories) {
|
|
562
|
-
for (const line of content.split("\n")) {
|
|
563
|
-
const trimmed = line.trim();
|
|
564
|
-
if (!trimmed) continue;
|
|
565
|
-
const parts = trimmed.split(/\s+/);
|
|
566
|
-
if (parts.length < 2) continue;
|
|
567
|
-
const count = parseInt(parts[1], 10);
|
|
568
|
-
if (!isNaN(count)) upsertStory(stories, parts[0], { attempts: count });
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
function parseStoryRetriesRecord(content) {
|
|
572
|
-
const result = {};
|
|
573
|
-
for (const line of content.split("\n")) {
|
|
574
|
-
const trimmed = line.trim();
|
|
575
|
-
if (!trimmed) continue;
|
|
576
|
-
const parts = trimmed.split(/\s+/);
|
|
577
|
-
if (parts.length < 2) continue;
|
|
578
|
-
const count = parseInt(parts[1], 10);
|
|
579
|
-
if (!isNaN(count) && count >= 0) result[parts[0]] = count;
|
|
580
|
-
}
|
|
581
|
-
return result;
|
|
582
|
-
}
|
|
583
|
-
function parseFlaggedStoriesList(content) {
|
|
584
|
-
const seen = /* @__PURE__ */ new Set();
|
|
585
|
-
const result = [];
|
|
586
|
-
for (const line of content.split("\n")) {
|
|
587
|
-
const trimmed = line.trim();
|
|
588
|
-
if (trimmed !== "" && !seen.has(trimmed)) {
|
|
589
|
-
seen.add(trimmed);
|
|
590
|
-
result.push(trimmed);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
return result;
|
|
594
|
-
}
|
|
595
|
-
function parseFlaggedStories(content, stories) {
|
|
596
|
-
for (const line of content.split("\n")) {
|
|
597
|
-
const key = line.trim();
|
|
598
|
-
if (key) upsertStory(stories, key, { status: "blocked" });
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
function mapYamlStatus(value) {
|
|
602
|
-
const mapping = {
|
|
603
|
-
done: "done",
|
|
604
|
-
backlog: "backlog",
|
|
605
|
-
verifying: "verifying",
|
|
606
|
-
"in-progress": "in-progress",
|
|
607
|
-
"ready-for-dev": "ready",
|
|
608
|
-
blocked: "blocked",
|
|
609
|
-
failed: "failed",
|
|
610
|
-
review: "review",
|
|
611
|
-
ready: "ready"
|
|
612
|
-
};
|
|
613
|
-
return mapping[value.trim().toLowerCase()] ?? null;
|
|
614
|
-
}
|
|
615
|
-
function parseSprintStatusYaml(content, stories) {
|
|
616
|
-
for (const line of content.split("\n")) {
|
|
617
|
-
const trimmed = line.trim();
|
|
618
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
619
|
-
const match = trimmed.match(/^([a-zA-Z0-9_-]+):\s*(.+)$/);
|
|
620
|
-
if (!match) continue;
|
|
621
|
-
const key = match[1];
|
|
622
|
-
if (key === "development_status" || key.startsWith("epic-")) continue;
|
|
623
|
-
const status = mapYamlStatus(match[2]);
|
|
624
|
-
if (status) upsertStory(stories, key, { status });
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
function parseRalphStatus(content) {
|
|
628
|
-
try {
|
|
629
|
-
const data = JSON.parse(content);
|
|
630
|
-
return {
|
|
631
|
-
active: data.status === "running",
|
|
632
|
-
startedAt: null,
|
|
633
|
-
iteration: data.loop_count ?? 0,
|
|
634
|
-
cost: 0,
|
|
635
|
-
completed: [],
|
|
636
|
-
failed: [],
|
|
637
|
-
currentStory: null,
|
|
638
|
-
currentPhase: null,
|
|
639
|
-
lastAction: null,
|
|
640
|
-
acProgress: null
|
|
641
|
-
};
|
|
642
|
-
} catch {
|
|
643
|
-
return null;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
function parseRalphStatusToSession(content) {
|
|
647
|
-
try {
|
|
648
|
-
const data = JSON.parse(content);
|
|
649
|
-
return {
|
|
650
|
-
active: data.status === "running",
|
|
651
|
-
startedAt: null,
|
|
652
|
-
iteration: data.loop_count ?? 0,
|
|
653
|
-
elapsedSeconds: data.elapsed_seconds ?? 0
|
|
654
|
-
};
|
|
655
|
-
} catch {
|
|
656
|
-
return null;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
function parseSessionIssues(content) {
|
|
660
|
-
const items = [];
|
|
661
|
-
let currentStory = "";
|
|
662
|
-
let itemId = 0;
|
|
663
|
-
for (const line of content.split("\n")) {
|
|
664
|
-
const headerMatch = line.match(/^###\s+([a-zA-Z0-9_-]+)\s*[—-]/);
|
|
665
|
-
if (headerMatch) {
|
|
666
|
-
currentStory = headerMatch[1];
|
|
667
|
-
continue;
|
|
668
|
-
}
|
|
669
|
-
const bulletMatch = line.match(/^-\s+(.+)/);
|
|
670
|
-
if (bulletMatch && currentStory) {
|
|
671
|
-
itemId++;
|
|
672
|
-
items.push({
|
|
673
|
-
id: `migrated-${itemId}`,
|
|
674
|
-
story: currentStory,
|
|
675
|
-
description: bulletMatch[1],
|
|
676
|
-
source: "retro",
|
|
677
|
-
resolved: false
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
return items;
|
|
682
|
-
}
|
|
683
|
-
function migrateV1ToV2(v1) {
|
|
684
|
-
const defaults = defaultState();
|
|
685
|
-
const retriesContent = readIfExists(OLD_FILES.storyRetries);
|
|
686
|
-
const retries = retriesContent ? parseStoryRetriesRecord(retriesContent) : {};
|
|
687
|
-
const flaggedContent = readIfExists(OLD_FILES.flaggedStories);
|
|
688
|
-
const flagged = flaggedContent ? parseFlaggedStoriesList(flaggedContent) : [];
|
|
689
|
-
const statusContent = readIfExists(OLD_FILES.ralphStatus);
|
|
690
|
-
const session = statusContent ? parseRalphStatusToSession(statusContent) ?? defaults.session : defaults.session;
|
|
691
|
-
return {
|
|
692
|
-
version: 2,
|
|
693
|
-
sprint: v1.sprint,
|
|
694
|
-
stories: v1.stories,
|
|
695
|
-
retries,
|
|
696
|
-
flagged,
|
|
697
|
-
epics: {},
|
|
698
|
-
session,
|
|
699
|
-
observability: defaults.observability,
|
|
700
|
-
run: {
|
|
701
|
-
...defaults.run,
|
|
702
|
-
...v1.run
|
|
703
|
-
},
|
|
704
|
-
actionItems: v1.actionItems
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
function migrateFromOldFormat() {
|
|
708
|
-
const hasAnyOldFile = Object.values(OLD_FILES).some((rel) => existsSync5(resolve(rel)));
|
|
709
|
-
if (!hasAnyOldFile) return fail2("No old format files found for migration");
|
|
710
|
-
try {
|
|
711
|
-
const stories = {};
|
|
712
|
-
let run = defaultState().run;
|
|
713
|
-
let actionItems = [];
|
|
714
|
-
const yamlContent = readIfExists(OLD_FILES.sprintStatusYaml);
|
|
715
|
-
if (yamlContent) parseSprintStatusYaml(yamlContent, stories);
|
|
716
|
-
const retriesContent = readIfExists(OLD_FILES.storyRetries);
|
|
717
|
-
if (retriesContent) parseStoryRetries(retriesContent, stories);
|
|
718
|
-
const flaggedContent = readIfExists(OLD_FILES.flaggedStories);
|
|
719
|
-
if (flaggedContent) parseFlaggedStories(flaggedContent, stories);
|
|
720
|
-
const statusContent = readIfExists(OLD_FILES.ralphStatus);
|
|
721
|
-
let session = defaultState().session;
|
|
722
|
-
if (statusContent) {
|
|
723
|
-
const parsed = parseRalphStatus(statusContent);
|
|
724
|
-
if (parsed) run = parsed;
|
|
725
|
-
const parsedSession = parseRalphStatusToSession(statusContent);
|
|
726
|
-
if (parsedSession) session = parsedSession;
|
|
727
|
-
}
|
|
728
|
-
const issuesContent = readIfExists(OLD_FILES.sessionIssues);
|
|
729
|
-
if (issuesContent) actionItems = parseSessionIssues(issuesContent);
|
|
730
|
-
const sprint = computeSprintCounts(stories);
|
|
731
|
-
const retries = retriesContent ? parseStoryRetriesRecord(retriesContent) : {};
|
|
732
|
-
const flagged = flaggedContent ? parseFlaggedStoriesList(flaggedContent) : [];
|
|
733
|
-
const migrated = {
|
|
734
|
-
version: 2,
|
|
735
|
-
sprint,
|
|
736
|
-
stories,
|
|
737
|
-
retries,
|
|
738
|
-
flagged,
|
|
739
|
-
epics: {},
|
|
740
|
-
session,
|
|
741
|
-
observability: defaultState().observability,
|
|
742
|
-
run,
|
|
743
|
-
actionItems
|
|
744
|
-
};
|
|
745
|
-
const writeResult = writeStateAtomic(migrated);
|
|
746
|
-
if (!writeResult.success) return fail2(writeResult.error);
|
|
747
|
-
return ok2(migrated);
|
|
748
|
-
} catch (err) {
|
|
749
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
750
|
-
return fail2(`Migration failed: ${msg}`);
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
// src/modules/sprint/state.ts
|
|
755
|
-
function projectRoot() {
|
|
756
|
-
return process.cwd();
|
|
757
|
-
}
|
|
758
|
-
function statePath() {
|
|
759
|
-
return join4(projectRoot(), "sprint-state.json");
|
|
760
|
-
}
|
|
761
|
-
function tmpPath() {
|
|
762
|
-
return join4(projectRoot(), ".sprint-state.json.tmp");
|
|
763
|
-
}
|
|
764
|
-
function sprintStatusYamlPath() {
|
|
765
|
-
return join4(projectRoot(), "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
|
|
418
|
+
function sprintStatusYamlPath() {
|
|
419
|
+
return join2(projectRoot(), "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
|
|
766
420
|
}
|
|
767
421
|
function defaultState() {
|
|
768
422
|
return {
|
|
@@ -884,9 +538,9 @@ function writeSprintStatusYaml(state) {
|
|
|
884
538
|
try {
|
|
885
539
|
const yamlPath = sprintStatusYamlPath();
|
|
886
540
|
const dir = dirname(yamlPath);
|
|
887
|
-
if (!
|
|
541
|
+
if (!existsSync3(dir)) return;
|
|
888
542
|
const content = generateSprintStatusYaml(state);
|
|
889
|
-
|
|
543
|
+
writeFileSync(yamlPath, content, "utf-8");
|
|
890
544
|
} catch {
|
|
891
545
|
}
|
|
892
546
|
}
|
|
@@ -895,7 +549,7 @@ function writeStateAtomic(state) {
|
|
|
895
549
|
const data = JSON.stringify(state, null, 2) + "\n";
|
|
896
550
|
const tmp = tmpPath();
|
|
897
551
|
const final = statePath();
|
|
898
|
-
|
|
552
|
+
writeFileSync(tmp, data, "utf-8");
|
|
899
553
|
renameSync(tmp, final);
|
|
900
554
|
writeSprintStatusYaml(state);
|
|
901
555
|
return ok2(void 0);
|
|
@@ -906,9 +560,9 @@ function writeStateAtomic(state) {
|
|
|
906
560
|
}
|
|
907
561
|
function getSprintState() {
|
|
908
562
|
const fp = statePath();
|
|
909
|
-
if (
|
|
563
|
+
if (existsSync3(fp)) {
|
|
910
564
|
try {
|
|
911
|
-
const raw =
|
|
565
|
+
const raw = readFileSync2(fp, "utf-8");
|
|
912
566
|
const parsed = JSON.parse(raw);
|
|
913
567
|
const version = parsed.version;
|
|
914
568
|
if (version === 2) {
|
|
@@ -1048,10 +702,10 @@ function reconcileState() {
|
|
|
1048
702
|
const state = JSON.parse(JSON.stringify(stateResult.data));
|
|
1049
703
|
const corrections = [];
|
|
1050
704
|
let changed = false;
|
|
1051
|
-
const retriesPath =
|
|
1052
|
-
if (
|
|
705
|
+
const retriesPath = join2(projectRoot(), "ralph", ".story_retries");
|
|
706
|
+
if (existsSync3(retriesPath)) {
|
|
1053
707
|
try {
|
|
1054
|
-
const content =
|
|
708
|
+
const content = readFileSync2(retriesPath, "utf-8");
|
|
1055
709
|
const fileRetries = parseStoryRetriesRecord(content);
|
|
1056
710
|
const mergedRetries = { ...state.retries };
|
|
1057
711
|
for (const [key, count] of Object.entries(fileRetries)) {
|
|
@@ -1071,10 +725,10 @@ function reconcileState() {
|
|
|
1071
725
|
corrections.push("removed malformed .story_retries file");
|
|
1072
726
|
}
|
|
1073
727
|
}
|
|
1074
|
-
const flaggedPath =
|
|
1075
|
-
if (
|
|
728
|
+
const flaggedPath = join2(projectRoot(), "ralph", ".flagged_stories");
|
|
729
|
+
if (existsSync3(flaggedPath)) {
|
|
1076
730
|
try {
|
|
1077
|
-
const content =
|
|
731
|
+
const content = readFileSync2(flaggedPath, "utf-8");
|
|
1078
732
|
const fileKeys = parseFlaggedStoriesList(content);
|
|
1079
733
|
const existing = new Set(state.flagged);
|
|
1080
734
|
const merged = [...state.flagged];
|
|
@@ -1361,9 +1015,9 @@ function generateReport(state, now) {
|
|
|
1361
1015
|
}
|
|
1362
1016
|
|
|
1363
1017
|
// src/modules/sprint/timeout.ts
|
|
1364
|
-
import { readFileSync as
|
|
1018
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync, readdirSync } from "fs";
|
|
1365
1019
|
import { execSync } from "child_process";
|
|
1366
|
-
import { join as
|
|
1020
|
+
import { join as join3 } from "path";
|
|
1367
1021
|
var GIT_TIMEOUT_MS = 5e3;
|
|
1368
1022
|
var DEFAULT_MAX_LINES = 100;
|
|
1369
1023
|
function captureGitDiff() {
|
|
@@ -1392,14 +1046,14 @@ function captureGitDiff() {
|
|
|
1392
1046
|
}
|
|
1393
1047
|
function captureStateDelta(beforePath, afterPath) {
|
|
1394
1048
|
try {
|
|
1395
|
-
if (!
|
|
1049
|
+
if (!existsSync4(beforePath)) {
|
|
1396
1050
|
return fail2(`State snapshot not found: ${beforePath}`);
|
|
1397
1051
|
}
|
|
1398
|
-
if (!
|
|
1052
|
+
if (!existsSync4(afterPath)) {
|
|
1399
1053
|
return fail2(`Current state file not found: ${afterPath}`);
|
|
1400
1054
|
}
|
|
1401
|
-
const beforeRaw =
|
|
1402
|
-
const afterRaw =
|
|
1055
|
+
const beforeRaw = readFileSync3(beforePath, "utf-8");
|
|
1056
|
+
const afterRaw = readFileSync3(afterPath, "utf-8");
|
|
1403
1057
|
const before = JSON.parse(beforeRaw);
|
|
1404
1058
|
const after = JSON.parse(afterRaw);
|
|
1405
1059
|
const beforeStories = before.stories ?? {};
|
|
@@ -1424,10 +1078,10 @@ function captureStateDelta(beforePath, afterPath) {
|
|
|
1424
1078
|
}
|
|
1425
1079
|
function capturePartialStderr(outputFile, maxLines = DEFAULT_MAX_LINES) {
|
|
1426
1080
|
try {
|
|
1427
|
-
if (!
|
|
1081
|
+
if (!existsSync4(outputFile)) {
|
|
1428
1082
|
return fail2(`Output file not found: ${outputFile}`);
|
|
1429
1083
|
}
|
|
1430
|
-
const content =
|
|
1084
|
+
const content = readFileSync3(outputFile, "utf-8");
|
|
1431
1085
|
const lines = content.split("\n");
|
|
1432
1086
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
1433
1087
|
lines.pop();
|
|
@@ -1475,7 +1129,7 @@ function captureTimeoutReport(opts) {
|
|
|
1475
1129
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1476
1130
|
const gitResult = captureGitDiff();
|
|
1477
1131
|
const gitDiff = gitResult.success ? gitResult.data : `(unavailable: ${gitResult.error})`;
|
|
1478
|
-
const statePath2 =
|
|
1132
|
+
const statePath2 = join3(process.cwd(), "sprint-state.json");
|
|
1479
1133
|
const deltaResult = captureStateDelta(opts.stateSnapshotPath, statePath2);
|
|
1480
1134
|
const stateDelta = deltaResult.success ? deltaResult.data : `(unavailable: ${deltaResult.error})`;
|
|
1481
1135
|
const stderrResult = capturePartialStderr(opts.outputFile);
|
|
@@ -1489,15 +1143,15 @@ function captureTimeoutReport(opts) {
|
|
|
1489
1143
|
partialStderr,
|
|
1490
1144
|
timestamp
|
|
1491
1145
|
};
|
|
1492
|
-
const reportDir =
|
|
1146
|
+
const reportDir = join3(process.cwd(), "ralph", "logs");
|
|
1493
1147
|
const safeStoryKey = opts.storyKey.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
1494
1148
|
const reportFileName = `timeout-report-${opts.iteration}-${safeStoryKey}.md`;
|
|
1495
|
-
const reportPath =
|
|
1496
|
-
if (!
|
|
1149
|
+
const reportPath = join3(reportDir, reportFileName);
|
|
1150
|
+
if (!existsSync4(reportDir)) {
|
|
1497
1151
|
mkdirSync(reportDir, { recursive: true });
|
|
1498
1152
|
}
|
|
1499
1153
|
const reportContent = formatReport(capture);
|
|
1500
|
-
|
|
1154
|
+
writeFileSync2(reportPath, reportContent, "utf-8");
|
|
1501
1155
|
return ok2({
|
|
1502
1156
|
filePath: reportPath,
|
|
1503
1157
|
capture
|
|
@@ -1509,8 +1163,8 @@ function captureTimeoutReport(opts) {
|
|
|
1509
1163
|
}
|
|
1510
1164
|
function findLatestTimeoutReport(storyKey) {
|
|
1511
1165
|
try {
|
|
1512
|
-
const reportDir =
|
|
1513
|
-
if (!
|
|
1166
|
+
const reportDir = join3(process.cwd(), "ralph", "logs");
|
|
1167
|
+
if (!existsSync4(reportDir)) {
|
|
1514
1168
|
return ok2(null);
|
|
1515
1169
|
}
|
|
1516
1170
|
const safeStoryKey = storyKey.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
@@ -1532,8 +1186,8 @@ function findLatestTimeoutReport(storyKey) {
|
|
|
1532
1186
|
}
|
|
1533
1187
|
matches.sort((a, b) => b.iteration - a.iteration);
|
|
1534
1188
|
const latest = matches[0];
|
|
1535
|
-
const reportPath =
|
|
1536
|
-
const content =
|
|
1189
|
+
const reportPath = join3(reportDir, latest.fileName);
|
|
1190
|
+
const content = readFileSync3(reportPath, "utf-8");
|
|
1537
1191
|
let durationMinutes = 0;
|
|
1538
1192
|
let filesChanged = 0;
|
|
1539
1193
|
const durationMatch = content.match(/\*\*Duration:\*\*\s*(\d+)\s*minutes/);
|
|
@@ -1562,12 +1216,12 @@ function findLatestTimeoutReport(storyKey) {
|
|
|
1562
1216
|
}
|
|
1563
1217
|
|
|
1564
1218
|
// src/modules/sprint/feedback.ts
|
|
1565
|
-
import { readFileSync as
|
|
1566
|
-
import { existsSync as
|
|
1567
|
-
import { join as
|
|
1219
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
1220
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1221
|
+
import { join as join4 } from "path";
|
|
1568
1222
|
|
|
1569
1223
|
// src/modules/sprint/validator.ts
|
|
1570
|
-
import { readFileSync as
|
|
1224
|
+
import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
|
|
1571
1225
|
var VALID_STATUSES = /* @__PURE__ */ new Set([
|
|
1572
1226
|
"backlog",
|
|
1573
1227
|
"ready",
|
|
@@ -1598,10 +1252,10 @@ function parseSprintStatusKeys(content) {
|
|
|
1598
1252
|
}
|
|
1599
1253
|
function parseStateFile(statePath2) {
|
|
1600
1254
|
try {
|
|
1601
|
-
if (!
|
|
1255
|
+
if (!existsSync6(statePath2)) {
|
|
1602
1256
|
return fail2(`State file not found: ${statePath2}`);
|
|
1603
1257
|
}
|
|
1604
|
-
const raw =
|
|
1258
|
+
const raw = readFileSync5(statePath2, "utf-8");
|
|
1605
1259
|
const parsed = JSON.parse(raw);
|
|
1606
1260
|
return ok2(parsed);
|
|
1607
1261
|
} catch (err) {
|
|
@@ -1616,10 +1270,10 @@ function validateStateConsistency(statePath2, sprintStatusPath) {
|
|
|
1616
1270
|
return fail2(stateResult.error);
|
|
1617
1271
|
}
|
|
1618
1272
|
const state = stateResult.data;
|
|
1619
|
-
if (!
|
|
1273
|
+
if (!existsSync6(sprintStatusPath)) {
|
|
1620
1274
|
return fail2(`Sprint status file not found: ${sprintStatusPath}`);
|
|
1621
1275
|
}
|
|
1622
|
-
const statusContent =
|
|
1276
|
+
const statusContent = readFileSync5(sprintStatusPath, "utf-8");
|
|
1623
1277
|
const keysResult = parseSprintStatusKeys(statusContent);
|
|
1624
1278
|
if (!keysResult.success) {
|
|
1625
1279
|
return fail2(keysResult.error);
|
|
@@ -1770,12 +1424,7 @@ function readSprintStatusFromState() {
|
|
|
1770
1424
|
const stateResult = getSprintState();
|
|
1771
1425
|
if (!stateResult.success) return {};
|
|
1772
1426
|
const statuses = getStoryStatusesFromState(stateResult.data);
|
|
1773
|
-
|
|
1774
|
-
try {
|
|
1775
|
-
return readSprintStatus(process.cwd());
|
|
1776
|
-
} catch {
|
|
1777
|
-
return {};
|
|
1778
|
-
}
|
|
1427
|
+
return statuses;
|
|
1779
1428
|
}
|
|
1780
1429
|
function shouldDeferPhase2(phase, remainingMinutes) {
|
|
1781
1430
|
return shouldDeferPhase(phase, remainingMinutes);
|
|
@@ -2276,8 +1925,8 @@ function parseResultEvent(parsed) {
|
|
|
2276
1925
|
|
|
2277
1926
|
// src/lib/agents/ralph.ts
|
|
2278
1927
|
import { spawn } from "child_process";
|
|
2279
|
-
import { existsSync as
|
|
2280
|
-
import { join as
|
|
1928
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1929
|
+
import { join as join5 } from "path";
|
|
2281
1930
|
var ANSI_ESCAPE = /\x1b\[[0-9;]*m/g;
|
|
2282
1931
|
var TIMESTAMP_PREFIX = /^\[[\d-]+\s[\d:]+\]\s*/;
|
|
2283
1932
|
var SUCCESS_STORY = /\[SUCCESS\]\s+Story\s+([\w-]+):\s+DONE(.*)/;
|
|
@@ -2365,17 +2014,17 @@ function buildSpawnArgs(opts) {
|
|
|
2365
2014
|
return args;
|
|
2366
2015
|
}
|
|
2367
2016
|
function resolveRalphPath() {
|
|
2368
|
-
return
|
|
2017
|
+
return join5(getPackageRoot(), "ralph", "ralph.sh");
|
|
2369
2018
|
}
|
|
2370
2019
|
var RalphDriver = class {
|
|
2371
2020
|
name = "ralph";
|
|
2372
2021
|
config;
|
|
2373
2022
|
constructor(config) {
|
|
2374
|
-
this.config = config ?? { pluginDir:
|
|
2023
|
+
this.config = config ?? { pluginDir: join5(process.cwd(), ".claude") };
|
|
2375
2024
|
}
|
|
2376
2025
|
spawn(opts) {
|
|
2377
2026
|
const ralphPath = resolveRalphPath();
|
|
2378
|
-
if (!
|
|
2027
|
+
if (!existsSync7(ralphPath)) {
|
|
2379
2028
|
throw new Error(`Ralph loop not found at ${ralphPath} \u2014 reinstall codeharness`);
|
|
2380
2029
|
}
|
|
2381
2030
|
const args = buildSpawnArgs({
|
|
@@ -2517,7 +2166,7 @@ function getDriver(name, config) {
|
|
|
2517
2166
|
|
|
2518
2167
|
// src/commands/run.ts
|
|
2519
2168
|
function resolvePluginDir() {
|
|
2520
|
-
return
|
|
2169
|
+
return join6(process.cwd(), ".claude");
|
|
2521
2170
|
}
|
|
2522
2171
|
function handleAgentEvent(event, rendererHandle, state) {
|
|
2523
2172
|
switch (event.type) {
|
|
@@ -2571,7 +2220,7 @@ function registerRunCommand(program) {
|
|
|
2571
2220
|
const isJson = !!globalOpts.json;
|
|
2572
2221
|
const outputOpts = { json: isJson };
|
|
2573
2222
|
const pluginDir = resolvePluginDir();
|
|
2574
|
-
if (!
|
|
2223
|
+
if (!existsSync8(pluginDir)) {
|
|
2575
2224
|
fail("Plugin directory not found \u2014 run codeharness init first", outputOpts);
|
|
2576
2225
|
process.exitCode = 1;
|
|
2577
2226
|
return;
|
|
@@ -2596,7 +2245,7 @@ function registerRunCommand(program) {
|
|
|
2596
2245
|
info(`[WARN] Container cleanup failed: ${cleanup.error}`, outputOpts);
|
|
2597
2246
|
}
|
|
2598
2247
|
const projectDir = process.cwd();
|
|
2599
|
-
const sprintStatusPath =
|
|
2248
|
+
const sprintStatusPath = join6(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
|
|
2600
2249
|
const statuses = readSprintStatusFromState();
|
|
2601
2250
|
const counts = countStories(statuses);
|
|
2602
2251
|
if (counts.total === 0) {
|
|
@@ -2615,7 +2264,7 @@ function registerRunCommand(program) {
|
|
|
2615
2264
|
process.exitCode = 1;
|
|
2616
2265
|
return;
|
|
2617
2266
|
}
|
|
2618
|
-
const promptFile =
|
|
2267
|
+
const promptFile = join6(projectDir, "ralph", ".harness-prompt.md");
|
|
2619
2268
|
let flaggedStories;
|
|
2620
2269
|
const flaggedState = getSprintState2();
|
|
2621
2270
|
if (flaggedState.success && flaggedState.data.flagged?.length > 0) {
|
|
@@ -2628,7 +2277,7 @@ function registerRunCommand(program) {
|
|
|
2628
2277
|
});
|
|
2629
2278
|
try {
|
|
2630
2279
|
mkdirSync2(dirname3(promptFile), { recursive: true });
|
|
2631
|
-
|
|
2280
|
+
writeFileSync4(promptFile, promptContent, "utf-8");
|
|
2632
2281
|
} catch (err) {
|
|
2633
2282
|
const message = err instanceof Error ? err.message : String(err);
|
|
2634
2283
|
fail(`Failed to write prompt file: ${message}`, outputOpts);
|
|
@@ -2733,12 +2382,12 @@ function registerRunCommand(program) {
|
|
|
2733
2382
|
});
|
|
2734
2383
|
});
|
|
2735
2384
|
if (isJson) {
|
|
2736
|
-
const statusFile =
|
|
2385
|
+
const statusFile = join6(projectDir, driver.getStatusFile());
|
|
2737
2386
|
let statusData = null;
|
|
2738
2387
|
let exitReason = "status_file_missing";
|
|
2739
|
-
if (
|
|
2388
|
+
if (existsSync8(statusFile)) {
|
|
2740
2389
|
try {
|
|
2741
|
-
statusData = JSON.parse(
|
|
2390
|
+
statusData = JSON.parse(readFileSync6(statusFile, "utf-8"));
|
|
2742
2391
|
exitReason = "";
|
|
2743
2392
|
} catch {
|
|
2744
2393
|
exitReason = "status_file_unreadable";
|
|
@@ -2775,7 +2424,7 @@ import { join as join20 } from "path";
|
|
|
2775
2424
|
import { readFileSync as readFileSync18 } from "fs";
|
|
2776
2425
|
|
|
2777
2426
|
// src/modules/verify/proof.ts
|
|
2778
|
-
import { existsSync as
|
|
2427
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
2779
2428
|
function classifyEvidenceCommands(proofContent) {
|
|
2780
2429
|
const results = [];
|
|
2781
2430
|
const codeBlockPattern = /```(?:bash|shell)\n([\s\S]*?)```/g;
|
|
@@ -2861,10 +2510,10 @@ function validateProofQuality(proofPath) {
|
|
|
2861
2510
|
otherCount: 0,
|
|
2862
2511
|
blackBoxPass: false
|
|
2863
2512
|
};
|
|
2864
|
-
if (!
|
|
2513
|
+
if (!existsSync9(proofPath)) {
|
|
2865
2514
|
return emptyResult;
|
|
2866
2515
|
}
|
|
2867
|
-
const content =
|
|
2516
|
+
const content = readFileSync7(proofPath, "utf-8");
|
|
2868
2517
|
const bbTierMatch = /\*\*Tier:\*\*\s*(unit-testable|black-box)/i.exec(content);
|
|
2869
2518
|
const bbIsUnitTestable = bbTierMatch ? bbTierMatch[1].toLowerCase() === "unit-testable" : false;
|
|
2870
2519
|
const bbEnforcement = bbIsUnitTestable ? { blackBoxPass: true, grepSrcCount: 0, dockerExecCount: 0, observabilityCount: 0, otherCount: 0, grepRatio: 0, acsMissingDockerExec: [] } : checkBlackBoxEnforcement(content);
|
|
@@ -3012,7 +2661,7 @@ import { join as join13 } from "path";
|
|
|
3012
2661
|
|
|
3013
2662
|
// src/lib/doc-health/types.ts
|
|
3014
2663
|
import { readdirSync as readdirSync2, statSync } from "fs";
|
|
3015
|
-
import { join as
|
|
2664
|
+
import { join as join7 } from "path";
|
|
3016
2665
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
|
|
3017
2666
|
function getExtension(filename) {
|
|
3018
2667
|
const dot = filename.lastIndexOf(".");
|
|
@@ -3033,7 +2682,7 @@ function getNewestSourceMtime(dir) {
|
|
|
3033
2682
|
const dirName = current.split("/").pop() ?? "";
|
|
3034
2683
|
if (dirName === "node_modules" || dirName === ".git") return;
|
|
3035
2684
|
for (const entry of entries) {
|
|
3036
|
-
const fullPath =
|
|
2685
|
+
const fullPath = join7(current, entry);
|
|
3037
2686
|
let stat;
|
|
3038
2687
|
try {
|
|
3039
2688
|
stat = statSync(fullPath);
|
|
@@ -3060,22 +2709,22 @@ function getNewestSourceMtime(dir) {
|
|
|
3060
2709
|
|
|
3061
2710
|
// src/lib/doc-health/scanner.ts
|
|
3062
2711
|
import {
|
|
3063
|
-
existsSync as
|
|
3064
|
-
readFileSync as
|
|
2712
|
+
existsSync as existsSync11,
|
|
2713
|
+
readFileSync as readFileSync9,
|
|
3065
2714
|
readdirSync as readdirSync4,
|
|
3066
2715
|
statSync as statSync3
|
|
3067
2716
|
} from "fs";
|
|
3068
|
-
import { join as
|
|
2717
|
+
import { join as join9, relative as relative2 } from "path";
|
|
3069
2718
|
|
|
3070
2719
|
// src/lib/doc-health/staleness.ts
|
|
3071
2720
|
import { execSync as execSync2 } from "child_process";
|
|
3072
2721
|
import {
|
|
3073
|
-
existsSync as
|
|
3074
|
-
readFileSync as
|
|
2722
|
+
existsSync as existsSync10,
|
|
2723
|
+
readFileSync as readFileSync8,
|
|
3075
2724
|
readdirSync as readdirSync3,
|
|
3076
2725
|
statSync as statSync2
|
|
3077
2726
|
} from "fs";
|
|
3078
|
-
import { join as
|
|
2727
|
+
import { join as join8, relative } from "path";
|
|
3079
2728
|
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
|
|
3080
2729
|
var DO_NOT_EDIT_HEADER = "<!-- DO NOT EDIT MANUALLY";
|
|
3081
2730
|
function getSourceFilesInModule(modulePath) {
|
|
@@ -3090,7 +2739,7 @@ function getSourceFilesInModule(modulePath) {
|
|
|
3090
2739
|
const dirName = current.split("/").pop() ?? "";
|
|
3091
2740
|
if (dirName === "node_modules" || dirName === ".git" || dirName === "__tests__" || dirName === "dist" || dirName === "coverage" || dirName.startsWith(".") && current !== modulePath) return;
|
|
3092
2741
|
for (const entry of entries) {
|
|
3093
|
-
const fullPath =
|
|
2742
|
+
const fullPath = join8(current, entry);
|
|
3094
2743
|
let stat;
|
|
3095
2744
|
try {
|
|
3096
2745
|
stat = statSync2(fullPath);
|
|
@@ -3111,8 +2760,8 @@ function getSourceFilesInModule(modulePath) {
|
|
|
3111
2760
|
return files;
|
|
3112
2761
|
}
|
|
3113
2762
|
function getMentionedFilesInAgentsMd(agentsPath) {
|
|
3114
|
-
if (!
|
|
3115
|
-
const content =
|
|
2763
|
+
if (!existsSync10(agentsPath)) return [];
|
|
2764
|
+
const content = readFileSync8(agentsPath, "utf-8");
|
|
3116
2765
|
const mentioned = /* @__PURE__ */ new Set();
|
|
3117
2766
|
const filenamePattern = /[\w./-]*[\w-]+\.(?:ts|js|py)\b/g;
|
|
3118
2767
|
let match;
|
|
@@ -3136,12 +2785,12 @@ function checkAgentsMdCompleteness(agentsPath, modulePath) {
|
|
|
3136
2785
|
}
|
|
3137
2786
|
function checkAgentsMdForModule(modulePath, dir) {
|
|
3138
2787
|
const root = dir ?? process.cwd();
|
|
3139
|
-
const fullModulePath =
|
|
3140
|
-
let agentsPath =
|
|
3141
|
-
if (!
|
|
3142
|
-
agentsPath =
|
|
2788
|
+
const fullModulePath = join8(root, modulePath);
|
|
2789
|
+
let agentsPath = join8(fullModulePath, "AGENTS.md");
|
|
2790
|
+
if (!existsSync10(agentsPath)) {
|
|
2791
|
+
agentsPath = join8(root, "AGENTS.md");
|
|
3143
2792
|
}
|
|
3144
|
-
if (!
|
|
2793
|
+
if (!existsSync10(agentsPath)) {
|
|
3145
2794
|
return {
|
|
3146
2795
|
path: relative(root, agentsPath),
|
|
3147
2796
|
grade: "missing",
|
|
@@ -3172,9 +2821,9 @@ function checkAgentsMdForModule(modulePath, dir) {
|
|
|
3172
2821
|
};
|
|
3173
2822
|
}
|
|
3174
2823
|
function checkDoNotEditHeaders(docPath) {
|
|
3175
|
-
if (!
|
|
2824
|
+
if (!existsSync10(docPath)) return false;
|
|
3176
2825
|
try {
|
|
3177
|
-
const content =
|
|
2826
|
+
const content = readFileSync8(docPath, "utf-8");
|
|
3178
2827
|
if (content.length === 0) return false;
|
|
3179
2828
|
return content.trimStart().startsWith(DO_NOT_EDIT_HEADER);
|
|
3180
2829
|
} catch {
|
|
@@ -3204,15 +2853,15 @@ function checkStoryDocFreshness(storyId, dir) {
|
|
|
3204
2853
|
for (const mod of modulesToCheck) {
|
|
3205
2854
|
const result = checkAgentsMdForModule(mod, root);
|
|
3206
2855
|
documents.push(result);
|
|
3207
|
-
const moduleAgentsPath =
|
|
3208
|
-
const actualAgentsPath =
|
|
3209
|
-
if (
|
|
2856
|
+
const moduleAgentsPath = join8(root, mod, "AGENTS.md");
|
|
2857
|
+
const actualAgentsPath = existsSync10(moduleAgentsPath) ? moduleAgentsPath : join8(root, "AGENTS.md");
|
|
2858
|
+
if (existsSync10(actualAgentsPath)) {
|
|
3210
2859
|
checkAgentsMdLineCountInternal(actualAgentsPath, result.path, documents);
|
|
3211
2860
|
}
|
|
3212
2861
|
}
|
|
3213
2862
|
if (modulesToCheck.length === 0) {
|
|
3214
|
-
const rootAgentsPath =
|
|
3215
|
-
if (
|
|
2863
|
+
const rootAgentsPath = join8(root, "AGENTS.md");
|
|
2864
|
+
if (existsSync10(rootAgentsPath)) {
|
|
3216
2865
|
documents.push({
|
|
3217
2866
|
path: "AGENTS.md",
|
|
3218
2867
|
grade: "fresh",
|
|
@@ -3250,7 +2899,7 @@ function getRecentlyChangedFiles(dir) {
|
|
|
3250
2899
|
}
|
|
3251
2900
|
function checkAgentsMdLineCountInternal(filePath, docPath, documents) {
|
|
3252
2901
|
try {
|
|
3253
|
-
const content =
|
|
2902
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
3254
2903
|
const lineCount = content.split("\n").length;
|
|
3255
2904
|
if (lineCount > 100) {
|
|
3256
2905
|
documents.push({
|
|
@@ -3286,7 +2935,7 @@ function findModules(dir, threshold) {
|
|
|
3286
2935
|
let sourceCount = 0;
|
|
3287
2936
|
const subdirs = [];
|
|
3288
2937
|
for (const entry of entries) {
|
|
3289
|
-
const fullPath =
|
|
2938
|
+
const fullPath = join9(current, entry);
|
|
3290
2939
|
let stat;
|
|
3291
2940
|
try {
|
|
3292
2941
|
stat = statSync3(fullPath);
|
|
@@ -3320,17 +2969,17 @@ function scanDocHealth(dir) {
|
|
|
3320
2969
|
const root = dir ?? process.cwd();
|
|
3321
2970
|
const documents = [];
|
|
3322
2971
|
const modules = findModules(root);
|
|
3323
|
-
const rootAgentsPath =
|
|
3324
|
-
if (
|
|
2972
|
+
const rootAgentsPath = join9(root, "AGENTS.md");
|
|
2973
|
+
if (existsSync11(rootAgentsPath)) {
|
|
3325
2974
|
if (modules.length > 0) {
|
|
3326
2975
|
const docMtime = statSync3(rootAgentsPath).mtime;
|
|
3327
2976
|
let allMissing = [];
|
|
3328
2977
|
let staleModule = "";
|
|
3329
2978
|
let newestCode = null;
|
|
3330
2979
|
for (const mod of modules) {
|
|
3331
|
-
const fullModPath =
|
|
3332
|
-
const modAgentsPath =
|
|
3333
|
-
if (
|
|
2980
|
+
const fullModPath = join9(root, mod);
|
|
2981
|
+
const modAgentsPath = join9(fullModPath, "AGENTS.md");
|
|
2982
|
+
if (existsSync11(modAgentsPath)) continue;
|
|
3334
2983
|
const { missing } = checkAgentsMdCompleteness(rootAgentsPath, fullModPath);
|
|
3335
2984
|
if (missing.length > 0 && staleModule === "") {
|
|
3336
2985
|
staleModule = mod;
|
|
@@ -3378,8 +3027,8 @@ function scanDocHealth(dir) {
|
|
|
3378
3027
|
});
|
|
3379
3028
|
}
|
|
3380
3029
|
for (const mod of modules) {
|
|
3381
|
-
const modAgentsPath =
|
|
3382
|
-
if (
|
|
3030
|
+
const modAgentsPath = join9(root, mod, "AGENTS.md");
|
|
3031
|
+
if (existsSync11(modAgentsPath)) {
|
|
3383
3032
|
const result = checkAgentsMdForModule(mod, root);
|
|
3384
3033
|
if (result.path !== "AGENTS.md") {
|
|
3385
3034
|
documents.push(result);
|
|
@@ -3387,9 +3036,9 @@ function scanDocHealth(dir) {
|
|
|
3387
3036
|
}
|
|
3388
3037
|
}
|
|
3389
3038
|
}
|
|
3390
|
-
const indexPath =
|
|
3391
|
-
if (
|
|
3392
|
-
const content =
|
|
3039
|
+
const indexPath = join9(root, "docs", "index.md");
|
|
3040
|
+
if (existsSync11(indexPath)) {
|
|
3041
|
+
const content = readFileSync9(indexPath, "utf-8");
|
|
3393
3042
|
const hasAbsolutePaths = /https?:\/\/|file:\/\//i.test(content);
|
|
3394
3043
|
documents.push({
|
|
3395
3044
|
path: "docs/index.md",
|
|
@@ -3399,11 +3048,11 @@ function scanDocHealth(dir) {
|
|
|
3399
3048
|
reason: hasAbsolutePaths ? "Contains absolute URLs (may violate NFR25)" : "Uses relative paths"
|
|
3400
3049
|
});
|
|
3401
3050
|
}
|
|
3402
|
-
const activeDir =
|
|
3403
|
-
if (
|
|
3051
|
+
const activeDir = join9(root, "docs", "exec-plans", "active");
|
|
3052
|
+
if (existsSync11(activeDir)) {
|
|
3404
3053
|
const files = readdirSync4(activeDir).filter((f) => f.endsWith(".md"));
|
|
3405
3054
|
for (const file of files) {
|
|
3406
|
-
const filePath =
|
|
3055
|
+
const filePath = join9(activeDir, file);
|
|
3407
3056
|
documents.push({
|
|
3408
3057
|
path: `docs/exec-plans/active/${file}`,
|
|
3409
3058
|
grade: "fresh",
|
|
@@ -3414,11 +3063,11 @@ function scanDocHealth(dir) {
|
|
|
3414
3063
|
}
|
|
3415
3064
|
}
|
|
3416
3065
|
for (const subdir of ["quality", "generated"]) {
|
|
3417
|
-
const dirPath =
|
|
3418
|
-
if (!
|
|
3066
|
+
const dirPath = join9(root, "docs", subdir);
|
|
3067
|
+
if (!existsSync11(dirPath)) continue;
|
|
3419
3068
|
const files = readdirSync4(dirPath).filter((f) => !f.startsWith("."));
|
|
3420
3069
|
for (const file of files) {
|
|
3421
|
-
const filePath =
|
|
3070
|
+
const filePath = join9(dirPath, file);
|
|
3422
3071
|
let stat;
|
|
3423
3072
|
try {
|
|
3424
3073
|
stat = statSync3(filePath);
|
|
@@ -3451,7 +3100,7 @@ function scanDocHealth(dir) {
|
|
|
3451
3100
|
}
|
|
3452
3101
|
function checkAgentsMdLineCount(filePath, docPath, documents) {
|
|
3453
3102
|
try {
|
|
3454
|
-
const content =
|
|
3103
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
3455
3104
|
const lineCount = content.split("\n").length;
|
|
3456
3105
|
if (lineCount > 100) {
|
|
3457
3106
|
documents.push({
|
|
@@ -3468,13 +3117,13 @@ function checkAgentsMdLineCount(filePath, docPath, documents) {
|
|
|
3468
3117
|
|
|
3469
3118
|
// src/lib/doc-health/report.ts
|
|
3470
3119
|
import {
|
|
3471
|
-
existsSync as
|
|
3120
|
+
existsSync as existsSync12,
|
|
3472
3121
|
mkdirSync as mkdirSync3,
|
|
3473
|
-
readFileSync as
|
|
3122
|
+
readFileSync as readFileSync10,
|
|
3474
3123
|
unlinkSync as unlinkSync2,
|
|
3475
|
-
writeFileSync as
|
|
3124
|
+
writeFileSync as writeFileSync5
|
|
3476
3125
|
} from "fs";
|
|
3477
|
-
import { join as
|
|
3126
|
+
import { join as join10 } from "path";
|
|
3478
3127
|
function printDocHealthOutput(report) {
|
|
3479
3128
|
for (const doc of report.documents) {
|
|
3480
3129
|
switch (doc.grade) {
|
|
@@ -3495,11 +3144,11 @@ function printDocHealthOutput(report) {
|
|
|
3495
3144
|
}
|
|
3496
3145
|
function completeExecPlan(storyId, dir) {
|
|
3497
3146
|
const root = dir ?? process.cwd();
|
|
3498
|
-
const activePath =
|
|
3499
|
-
if (!
|
|
3147
|
+
const activePath = join10(root, "docs", "exec-plans", "active", `${storyId}.md`);
|
|
3148
|
+
if (!existsSync12(activePath)) {
|
|
3500
3149
|
return null;
|
|
3501
3150
|
}
|
|
3502
|
-
let content =
|
|
3151
|
+
let content = readFileSync10(activePath, "utf-8");
|
|
3503
3152
|
content = content.replace(/^Status:\s*active$/m, "Status: completed");
|
|
3504
3153
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3505
3154
|
content = content.replace(
|
|
@@ -3507,10 +3156,10 @@ function completeExecPlan(storyId, dir) {
|
|
|
3507
3156
|
`$1
|
|
3508
3157
|
Completed: ${timestamp}`
|
|
3509
3158
|
);
|
|
3510
|
-
const completedDir =
|
|
3159
|
+
const completedDir = join10(root, "docs", "exec-plans", "completed");
|
|
3511
3160
|
mkdirSync3(completedDir, { recursive: true });
|
|
3512
|
-
const completedPath =
|
|
3513
|
-
|
|
3161
|
+
const completedPath = join10(completedDir, `${storyId}.md`);
|
|
3162
|
+
writeFileSync5(completedPath, content, "utf-8");
|
|
3514
3163
|
try {
|
|
3515
3164
|
unlinkSync2(activePath);
|
|
3516
3165
|
} catch {
|
|
@@ -3518,6 +3167,331 @@ Completed: ${timestamp}`
|
|
|
3518
3167
|
return completedPath;
|
|
3519
3168
|
}
|
|
3520
3169
|
|
|
3170
|
+
// src/lib/sync/story-files.ts
|
|
3171
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
|
|
3172
|
+
var BEADS_TO_STORY_STATUS = {
|
|
3173
|
+
open: "in-progress",
|
|
3174
|
+
closed: "done"
|
|
3175
|
+
};
|
|
3176
|
+
var STORY_TO_BEADS_STATUS = {
|
|
3177
|
+
backlog: "open",
|
|
3178
|
+
"ready-for-dev": "open",
|
|
3179
|
+
"in-progress": "open",
|
|
3180
|
+
review: "open",
|
|
3181
|
+
done: "closed"
|
|
3182
|
+
};
|
|
3183
|
+
function beadsStatusToStoryStatus(beadsStatus) {
|
|
3184
|
+
return BEADS_TO_STORY_STATUS[beadsStatus] ?? null;
|
|
3185
|
+
}
|
|
3186
|
+
function storyStatusToBeadsStatus(storyStatus) {
|
|
3187
|
+
return STORY_TO_BEADS_STATUS[storyStatus] ?? null;
|
|
3188
|
+
}
|
|
3189
|
+
function storyKeyFromPath(filePath) {
|
|
3190
|
+
const base = filePath.split("/").pop() ?? filePath;
|
|
3191
|
+
return base.replace(/\.md$/, "");
|
|
3192
|
+
}
|
|
3193
|
+
function resolveStoryFilePath(beadsIssue) {
|
|
3194
|
+
const desc = beadsIssue.description;
|
|
3195
|
+
if (!desc || !desc.trim()) {
|
|
3196
|
+
return null;
|
|
3197
|
+
}
|
|
3198
|
+
const trimmed = desc.trim();
|
|
3199
|
+
if (!trimmed.endsWith(".md")) {
|
|
3200
|
+
return null;
|
|
3201
|
+
}
|
|
3202
|
+
return trimmed;
|
|
3203
|
+
}
|
|
3204
|
+
function readStoryFileStatus(filePath) {
|
|
3205
|
+
if (!existsSync13(filePath)) {
|
|
3206
|
+
return null;
|
|
3207
|
+
}
|
|
3208
|
+
const content = readFileSync11(filePath, "utf-8");
|
|
3209
|
+
const match = content.match(/^#{0,2}\s*Status:\s*(.+)$/m);
|
|
3210
|
+
if (!match) {
|
|
3211
|
+
return null;
|
|
3212
|
+
}
|
|
3213
|
+
return match[1].trim();
|
|
3214
|
+
}
|
|
3215
|
+
function updateStoryFileStatus(filePath, newStatus) {
|
|
3216
|
+
const content = readFileSync11(filePath, "utf-8");
|
|
3217
|
+
const statusRegex = /^(#{0,2}\s*)Status:\s*.+$/m;
|
|
3218
|
+
if (statusRegex.test(content)) {
|
|
3219
|
+
const updated = content.replace(statusRegex, `$1Status: ${newStatus}`);
|
|
3220
|
+
writeFileSync6(filePath, updated, "utf-8");
|
|
3221
|
+
} else {
|
|
3222
|
+
const lines = content.split("\n");
|
|
3223
|
+
const titleIndex = lines.findIndex((l) => l.startsWith("# "));
|
|
3224
|
+
if (titleIndex !== -1) {
|
|
3225
|
+
lines.splice(titleIndex + 1, 0, "", `Status: ${newStatus}`);
|
|
3226
|
+
} else {
|
|
3227
|
+
lines.unshift(`Status: ${newStatus}`, "");
|
|
3228
|
+
}
|
|
3229
|
+
writeFileSync6(filePath, lines.join("\n"), "utf-8");
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
// src/lib/sync/sprint-yaml.ts
|
|
3234
|
+
import { existsSync as existsSync14, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
|
|
3235
|
+
import { join as join11 } from "path";
|
|
3236
|
+
import { parse } from "yaml";
|
|
3237
|
+
var SPRINT_STATUS_PATH = "_bmad-output/implementation-artifacts/sprint-status.yaml";
|
|
3238
|
+
function updateSprintStatus(storyKey, newStatus, dir) {
|
|
3239
|
+
const root = dir ?? process.cwd();
|
|
3240
|
+
const filePath = join11(root, SPRINT_STATUS_PATH);
|
|
3241
|
+
if (!existsSync14(filePath)) {
|
|
3242
|
+
warn(`sprint-status.yaml not found at ${filePath}, skipping update`);
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3245
|
+
const content = readFileSync12(filePath, "utf-8");
|
|
3246
|
+
const keyPattern = new RegExp(`^(\\s*${escapeRegExp(storyKey)}:\\s*)\\S+(.*)$`, "m");
|
|
3247
|
+
if (!keyPattern.test(content)) {
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
3250
|
+
const updated = content.replace(keyPattern, `$1${newStatus}$2`);
|
|
3251
|
+
writeFileSync7(filePath, updated, "utf-8");
|
|
3252
|
+
}
|
|
3253
|
+
function escapeRegExp(s) {
|
|
3254
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
// src/lib/sync/beads.ts
|
|
3258
|
+
import { existsSync as existsSync15 } from "fs";
|
|
3259
|
+
import { join as join12 } from "path";
|
|
3260
|
+
function syncBeadsToStoryFile(beadsId, beadsFns, dir) {
|
|
3261
|
+
const root = dir ?? process.cwd();
|
|
3262
|
+
const issues = beadsFns.listIssues();
|
|
3263
|
+
const issue = issues.find((i) => i.id === beadsId);
|
|
3264
|
+
if (!issue) {
|
|
3265
|
+
return {
|
|
3266
|
+
storyKey: "",
|
|
3267
|
+
beadsId,
|
|
3268
|
+
previousStatus: "",
|
|
3269
|
+
newStatus: "",
|
|
3270
|
+
synced: false,
|
|
3271
|
+
error: `Beads issue not found: ${beadsId}`
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
const storyFilePath = resolveStoryFilePath(issue);
|
|
3275
|
+
if (!storyFilePath) {
|
|
3276
|
+
return {
|
|
3277
|
+
storyKey: "",
|
|
3278
|
+
beadsId,
|
|
3279
|
+
previousStatus: issue.status,
|
|
3280
|
+
newStatus: "",
|
|
3281
|
+
synced: false,
|
|
3282
|
+
error: `No story file path in beads issue description: ${beadsId}`
|
|
3283
|
+
};
|
|
3284
|
+
}
|
|
3285
|
+
const storyKey = storyKeyFromPath(storyFilePath);
|
|
3286
|
+
const fullPath = join12(root, storyFilePath);
|
|
3287
|
+
const currentStoryStatus = readStoryFileStatus(fullPath);
|
|
3288
|
+
if (currentStoryStatus === null) {
|
|
3289
|
+
return {
|
|
3290
|
+
storyKey,
|
|
3291
|
+
beadsId,
|
|
3292
|
+
previousStatus: issue.status,
|
|
3293
|
+
newStatus: "",
|
|
3294
|
+
synced: false,
|
|
3295
|
+
error: `Story file not found or has no Status line: ${storyFilePath}`
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3298
|
+
const targetStoryStatus = beadsStatusToStoryStatus(issue.status);
|
|
3299
|
+
if (!targetStoryStatus) {
|
|
3300
|
+
return {
|
|
3301
|
+
storyKey,
|
|
3302
|
+
beadsId,
|
|
3303
|
+
previousStatus: currentStoryStatus,
|
|
3304
|
+
newStatus: "",
|
|
3305
|
+
synced: false,
|
|
3306
|
+
error: `Unknown beads status: ${issue.status}`
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
3309
|
+
if (currentStoryStatus === targetStoryStatus) {
|
|
3310
|
+
return {
|
|
3311
|
+
storyKey,
|
|
3312
|
+
beadsId,
|
|
3313
|
+
previousStatus: currentStoryStatus,
|
|
3314
|
+
newStatus: currentStoryStatus,
|
|
3315
|
+
synced: false
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
updateStoryFileStatus(fullPath, targetStoryStatus);
|
|
3319
|
+
updateSprintStatus(storyKey, targetStoryStatus, root);
|
|
3320
|
+
return {
|
|
3321
|
+
storyKey,
|
|
3322
|
+
beadsId,
|
|
3323
|
+
previousStatus: currentStoryStatus,
|
|
3324
|
+
newStatus: targetStoryStatus,
|
|
3325
|
+
synced: true
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
function syncStoryFileToBeads(storyKey, beadsFns, dir) {
|
|
3329
|
+
const root = dir ?? process.cwd();
|
|
3330
|
+
const storyFilePath = `_bmad-output/implementation-artifacts/${storyKey}.md`;
|
|
3331
|
+
const fullPath = join12(root, storyFilePath);
|
|
3332
|
+
const currentStoryStatus = readStoryFileStatus(fullPath);
|
|
3333
|
+
if (currentStoryStatus === null) {
|
|
3334
|
+
return {
|
|
3335
|
+
storyKey,
|
|
3336
|
+
beadsId: "",
|
|
3337
|
+
previousStatus: "",
|
|
3338
|
+
newStatus: "",
|
|
3339
|
+
synced: false,
|
|
3340
|
+
error: `Story file not found or has no Status line: ${storyFilePath}`
|
|
3341
|
+
};
|
|
3342
|
+
}
|
|
3343
|
+
const issues = beadsFns.listIssues();
|
|
3344
|
+
const issue = issues.find((i) => {
|
|
3345
|
+
const path = resolveStoryFilePath(i);
|
|
3346
|
+
if (!path) return false;
|
|
3347
|
+
return storyKeyFromPath(path) === storyKey;
|
|
3348
|
+
});
|
|
3349
|
+
if (!issue) {
|
|
3350
|
+
return {
|
|
3351
|
+
storyKey,
|
|
3352
|
+
beadsId: "",
|
|
3353
|
+
previousStatus: currentStoryStatus,
|
|
3354
|
+
newStatus: "",
|
|
3355
|
+
synced: false,
|
|
3356
|
+
error: `No beads issue found for story: ${storyKey}`
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
const targetBeadsStatus = storyStatusToBeadsStatus(currentStoryStatus);
|
|
3360
|
+
if (!targetBeadsStatus) {
|
|
3361
|
+
return {
|
|
3362
|
+
storyKey,
|
|
3363
|
+
beadsId: issue.id,
|
|
3364
|
+
previousStatus: currentStoryStatus,
|
|
3365
|
+
newStatus: "",
|
|
3366
|
+
synced: false,
|
|
3367
|
+
error: `Unknown story status: ${currentStoryStatus}`
|
|
3368
|
+
};
|
|
3369
|
+
}
|
|
3370
|
+
if (issue.status === targetBeadsStatus) {
|
|
3371
|
+
return {
|
|
3372
|
+
storyKey,
|
|
3373
|
+
beadsId: issue.id,
|
|
3374
|
+
previousStatus: currentStoryStatus,
|
|
3375
|
+
newStatus: currentStoryStatus,
|
|
3376
|
+
synced: false
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3379
|
+
if (targetBeadsStatus === "closed") {
|
|
3380
|
+
beadsFns.closeIssue(issue.id);
|
|
3381
|
+
} else {
|
|
3382
|
+
beadsFns.updateIssue(issue.id, { status: targetBeadsStatus });
|
|
3383
|
+
}
|
|
3384
|
+
updateSprintStatus(storyKey, currentStoryStatus, root);
|
|
3385
|
+
return {
|
|
3386
|
+
storyKey,
|
|
3387
|
+
beadsId: issue.id,
|
|
3388
|
+
previousStatus: issue.status,
|
|
3389
|
+
newStatus: targetBeadsStatus,
|
|
3390
|
+
synced: true
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
function syncClose(beadsId, beadsFns, dir) {
|
|
3394
|
+
const root = dir ?? process.cwd();
|
|
3395
|
+
const issues = beadsFns.listIssues();
|
|
3396
|
+
const issue = issues.find((i) => i.id === beadsId);
|
|
3397
|
+
beadsFns.closeIssue(beadsId);
|
|
3398
|
+
if (!issue) {
|
|
3399
|
+
return {
|
|
3400
|
+
storyKey: "",
|
|
3401
|
+
beadsId,
|
|
3402
|
+
previousStatus: "",
|
|
3403
|
+
newStatus: "closed",
|
|
3404
|
+
synced: false,
|
|
3405
|
+
error: `Beads issue not found: ${beadsId}`
|
|
3406
|
+
};
|
|
3407
|
+
}
|
|
3408
|
+
const storyFilePath = resolveStoryFilePath(issue);
|
|
3409
|
+
if (!storyFilePath) {
|
|
3410
|
+
return {
|
|
3411
|
+
storyKey: "",
|
|
3412
|
+
beadsId,
|
|
3413
|
+
previousStatus: issue.status,
|
|
3414
|
+
newStatus: "closed",
|
|
3415
|
+
synced: false,
|
|
3416
|
+
error: `No story file path in beads issue description: ${beadsId}`
|
|
3417
|
+
};
|
|
3418
|
+
}
|
|
3419
|
+
const storyKey = storyKeyFromPath(storyFilePath);
|
|
3420
|
+
const fullPath = join12(root, storyFilePath);
|
|
3421
|
+
const previousStatus = readStoryFileStatus(fullPath);
|
|
3422
|
+
if (previousStatus === null) {
|
|
3423
|
+
if (!existsSync15(fullPath)) {
|
|
3424
|
+
return {
|
|
3425
|
+
storyKey,
|
|
3426
|
+
beadsId,
|
|
3427
|
+
previousStatus: "",
|
|
3428
|
+
newStatus: "closed",
|
|
3429
|
+
synced: false,
|
|
3430
|
+
error: `Story file not found: ${storyFilePath}`
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
updateStoryFileStatus(fullPath, "done");
|
|
3435
|
+
updateSprintStatus(storyKey, "done", root);
|
|
3436
|
+
return {
|
|
3437
|
+
storyKey,
|
|
3438
|
+
beadsId,
|
|
3439
|
+
previousStatus: previousStatus ?? "",
|
|
3440
|
+
newStatus: "done",
|
|
3441
|
+
synced: true
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
function syncAll(direction, beadsFns, dir) {
|
|
3445
|
+
const root = dir ?? process.cwd();
|
|
3446
|
+
const results = [];
|
|
3447
|
+
let issues;
|
|
3448
|
+
try {
|
|
3449
|
+
issues = beadsFns.listIssues();
|
|
3450
|
+
} catch (err) {
|
|
3451
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3452
|
+
return [{
|
|
3453
|
+
storyKey: "",
|
|
3454
|
+
beadsId: "",
|
|
3455
|
+
previousStatus: "",
|
|
3456
|
+
newStatus: "",
|
|
3457
|
+
synced: false,
|
|
3458
|
+
error: `Failed to list beads issues: ${message}`
|
|
3459
|
+
}];
|
|
3460
|
+
}
|
|
3461
|
+
const cachedListIssues = () => issues;
|
|
3462
|
+
for (const issue of issues) {
|
|
3463
|
+
const storyFilePath = resolveStoryFilePath(issue);
|
|
3464
|
+
if (!storyFilePath) {
|
|
3465
|
+
continue;
|
|
3466
|
+
}
|
|
3467
|
+
const storyKey = storyKeyFromPath(storyFilePath);
|
|
3468
|
+
try {
|
|
3469
|
+
if (direction === "beads-to-files" || direction === "bidirectional") {
|
|
3470
|
+
const result = syncBeadsToStoryFile(issue.id, { listIssues: cachedListIssues }, root);
|
|
3471
|
+
results.push(result);
|
|
3472
|
+
} else if (direction === "files-to-beads") {
|
|
3473
|
+
const result = syncStoryFileToBeads(storyKey, {
|
|
3474
|
+
listIssues: cachedListIssues,
|
|
3475
|
+
updateIssue: beadsFns.updateIssue,
|
|
3476
|
+
closeIssue: beadsFns.closeIssue
|
|
3477
|
+
}, root);
|
|
3478
|
+
results.push(result);
|
|
3479
|
+
}
|
|
3480
|
+
} catch (err) {
|
|
3481
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3482
|
+
results.push({
|
|
3483
|
+
storyKey,
|
|
3484
|
+
beadsId: issue.id,
|
|
3485
|
+
previousStatus: "",
|
|
3486
|
+
newStatus: "",
|
|
3487
|
+
synced: false,
|
|
3488
|
+
error: message
|
|
3489
|
+
});
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
return results;
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3521
3495
|
// src/templates/showboat-template.ts
|
|
3522
3496
|
function verificationSummaryBlock(criteria) {
|
|
3523
3497
|
const total = criteria.length;
|
|
@@ -7558,7 +7532,7 @@ function registerTeardownCommand(program) {
|
|
|
7558
7532
|
} else if (otlpMode === "remote-routed") {
|
|
7559
7533
|
if (!options.keepDocker) {
|
|
7560
7534
|
try {
|
|
7561
|
-
const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-
|
|
7535
|
+
const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-F4U5KY6M.js");
|
|
7562
7536
|
stopCollectorOnly2();
|
|
7563
7537
|
result.docker.stopped = true;
|
|
7564
7538
|
if (!isJson) {
|
|
@@ -7590,7 +7564,7 @@ function registerTeardownCommand(program) {
|
|
|
7590
7564
|
info("Shared stack: kept running (other projects may use it)");
|
|
7591
7565
|
}
|
|
7592
7566
|
} else if (isLegacyStack) {
|
|
7593
|
-
const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-
|
|
7567
|
+
const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-F4U5KY6M.js");
|
|
7594
7568
|
let stackRunning = false;
|
|
7595
7569
|
try {
|
|
7596
7570
|
stackRunning = isStackRunning2(composeFile);
|
|
@@ -9489,7 +9463,7 @@ function registerAuditCommand(program) {
|
|
|
9489
9463
|
}
|
|
9490
9464
|
|
|
9491
9465
|
// src/index.ts
|
|
9492
|
-
var VERSION = true ? "0.25.
|
|
9466
|
+
var VERSION = true ? "0.25.6" : "0.0.0-dev";
|
|
9493
9467
|
function createProgram() {
|
|
9494
9468
|
const program = new Command();
|
|
9495
9469
|
program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
|