baro-ai 0.23.1 → 0.23.3
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/cli.mjs +598 -35
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -7414,32 +7414,53 @@ async function createOrCheckoutBranch(cwd, branchName, onLog) {
|
|
|
7414
7414
|
);
|
|
7415
7415
|
}
|
|
7416
7416
|
}
|
|
7417
|
-
async function safePullRebase(cwd, onLog) {
|
|
7418
|
-
|
|
7419
|
-
onLog?.("[git] no remote, skipping pull");
|
|
7420
|
-
return;
|
|
7421
|
-
}
|
|
7422
|
-
let branch;
|
|
7423
|
-
try {
|
|
7424
|
-
branch = await getCurrentBranch(cwd);
|
|
7425
|
-
} catch {
|
|
7426
|
-
onLog?.("[git] no branch, skipping pull");
|
|
7427
|
-
return;
|
|
7428
|
-
}
|
|
7429
|
-
if (!await hasRemoteBranch(cwd, branch)) {
|
|
7430
|
-
onLog?.("[git] remote branch not found, skipping pull");
|
|
7431
|
-
return;
|
|
7432
|
-
}
|
|
7433
|
-
onLog?.("[git] pulling latest...");
|
|
7434
|
-
await execSafe("git", ["stash", "--include-untracked"], { cwd });
|
|
7417
|
+
async function safePullRebase(cwd, onLog, gate) {
|
|
7418
|
+
const release = gate ? await gate.acquire() : null;
|
|
7435
7419
|
try {
|
|
7436
|
-
await
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7420
|
+
if (!await hasRemoteOrigin(cwd)) {
|
|
7421
|
+
onLog?.("[git] no remote, skipping pull");
|
|
7422
|
+
return;
|
|
7423
|
+
}
|
|
7424
|
+
let branch;
|
|
7425
|
+
try {
|
|
7426
|
+
branch = await getCurrentBranch(cwd);
|
|
7427
|
+
} catch {
|
|
7428
|
+
onLog?.("[git] no branch, skipping pull");
|
|
7429
|
+
return;
|
|
7430
|
+
}
|
|
7431
|
+
if (!await hasRemoteBranch(cwd, branch)) {
|
|
7432
|
+
onLog?.("[git] remote branch not found, skipping pull");
|
|
7433
|
+
return;
|
|
7434
|
+
}
|
|
7435
|
+
onLog?.("[git] pulling latest...");
|
|
7436
|
+
let stashSha = null;
|
|
7437
|
+
try {
|
|
7438
|
+
const { stdout } = await exec("git", ["stash", "create"], { cwd });
|
|
7439
|
+
stashSha = stdout.trim() || null;
|
|
7440
|
+
if (stashSha) {
|
|
7441
|
+
await execSafe("git", ["reset", "--hard", "HEAD"], { cwd });
|
|
7442
|
+
}
|
|
7443
|
+
} catch {
|
|
7444
|
+
}
|
|
7445
|
+
try {
|
|
7446
|
+
await exec("git", ["pull", "--rebase", "origin", branch], { cwd });
|
|
7447
|
+
onLog?.("[git] pull ok");
|
|
7448
|
+
} catch {
|
|
7449
|
+
onLog?.("[git] pull conflict, continuing without pull");
|
|
7450
|
+
await execSafe("git", ["rebase", "--abort"], { cwd });
|
|
7451
|
+
}
|
|
7452
|
+
if (stashSha) {
|
|
7453
|
+
try {
|
|
7454
|
+
await exec("git", ["stash", "apply", stashSha], { cwd });
|
|
7455
|
+
} catch (e) {
|
|
7456
|
+
onLog?.(
|
|
7457
|
+
`[git] could not re-apply stashed edits (sha ${stashSha.slice(0, 8)}): ${e?.message ?? String(e)}`
|
|
7458
|
+
);
|
|
7459
|
+
}
|
|
7460
|
+
}
|
|
7461
|
+
} finally {
|
|
7462
|
+
release?.();
|
|
7441
7463
|
}
|
|
7442
|
-
await execSafe("git", ["stash", "pop"], { cwd });
|
|
7443
7464
|
}
|
|
7444
7465
|
async function gitPushWithRetry(gate, options) {
|
|
7445
7466
|
const release = await gate.acquire();
|
|
@@ -7451,8 +7472,8 @@ async function gitPushWithRetry(gate, options) {
|
|
|
7451
7472
|
const branch = await getCurrentBranch(options.cwd);
|
|
7452
7473
|
const max = options.maxAttempts ?? GIT_PUSH_MAX_ATTEMPTS;
|
|
7453
7474
|
let lastError = "";
|
|
7475
|
+
options.onLog?.("[git] pushing...");
|
|
7454
7476
|
for (let attempt = 1; attempt <= max; attempt++) {
|
|
7455
|
-
options.onLog?.("[git] pushing...");
|
|
7456
7477
|
try {
|
|
7457
7478
|
await exec("git", ["push", "origin", branch], { cwd: options.cwd });
|
|
7458
7479
|
options.onLog?.("[git] push ok");
|
|
@@ -7461,7 +7482,9 @@ async function gitPushWithRetry(gate, options) {
|
|
|
7461
7482
|
lastError = extractStderr(e);
|
|
7462
7483
|
}
|
|
7463
7484
|
if (attempt === max) break;
|
|
7464
|
-
options.onLog?.(
|
|
7485
|
+
options.onLog?.(
|
|
7486
|
+
`[git] push rejected (attempt ${attempt}/${max}), pulling and retrying...`
|
|
7487
|
+
);
|
|
7465
7488
|
try {
|
|
7466
7489
|
await exec("git", ["pull", "--rebase", "origin", branch], {
|
|
7467
7490
|
cwd: options.cwd
|
|
@@ -7472,7 +7495,10 @@ async function gitPushWithRetry(gate, options) {
|
|
|
7472
7495
|
throw new Error("Rebase conflict detected, push skipped");
|
|
7473
7496
|
}
|
|
7474
7497
|
}
|
|
7475
|
-
|
|
7498
|
+
const compactErr = lastError.split("\n")[0]?.trim() || lastError;
|
|
7499
|
+
options.onLog?.(
|
|
7500
|
+
`[git] push failed after ${max} attempts: ${compactErr}`
|
|
7501
|
+
);
|
|
7476
7502
|
throw new Error(`Push failed after ${max} attempts: ${lastError}`);
|
|
7477
7503
|
} finally {
|
|
7478
7504
|
release();
|
|
@@ -7892,6 +7918,33 @@ var StorySpawnedItem = class extends ContextItem {
|
|
|
7892
7918
|
return { type: this.type, storyId: this.storyId };
|
|
7893
7919
|
}
|
|
7894
7920
|
};
|
|
7921
|
+
var FinalizeStartedItem = class extends ContextItem {
|
|
7922
|
+
constructor(branch) {
|
|
7923
|
+
super();
|
|
7924
|
+
this.branch = branch;
|
|
7925
|
+
}
|
|
7926
|
+
type = "finalize_started";
|
|
7927
|
+
toJSON() {
|
|
7928
|
+
return { type: this.type, branch: this.branch };
|
|
7929
|
+
}
|
|
7930
|
+
};
|
|
7931
|
+
var PrCreatedItem = class extends ContextItem {
|
|
7932
|
+
constructor(url, branch, baseBranch) {
|
|
7933
|
+
super();
|
|
7934
|
+
this.url = url;
|
|
7935
|
+
this.branch = branch;
|
|
7936
|
+
this.baseBranch = baseBranch;
|
|
7937
|
+
}
|
|
7938
|
+
type = "pr_created";
|
|
7939
|
+
toJSON() {
|
|
7940
|
+
return {
|
|
7941
|
+
type: this.type,
|
|
7942
|
+
url: this.url,
|
|
7943
|
+
branch: this.branch,
|
|
7944
|
+
baseBranch: this.baseBranch
|
|
7945
|
+
};
|
|
7946
|
+
}
|
|
7947
|
+
};
|
|
7895
7948
|
var RunCompletedItem = class extends ContextItem {
|
|
7896
7949
|
constructor(success, completedStories, failedStories, totalDurationSecs, totalAttempts, abortReason = null) {
|
|
7897
7950
|
super();
|
|
@@ -7921,14 +7974,26 @@ var Auditor = class extends Participant {
|
|
|
7921
7974
|
path;
|
|
7922
7975
|
skipStreamChunks;
|
|
7923
7976
|
filter;
|
|
7977
|
+
/**
|
|
7978
|
+
* Flips to true the first time a write fails (e.g. EACCES because
|
|
7979
|
+
* `~/.baro/runs/` is root-owned from a sudo install). Once disabled,
|
|
7980
|
+
* subsequent items are dropped silently — losing the audit log is
|
|
7981
|
+
* better than crashing the orchestrator on every bus event.
|
|
7982
|
+
*/
|
|
7983
|
+
disabled = false;
|
|
7924
7984
|
constructor(opts) {
|
|
7925
7985
|
super();
|
|
7926
7986
|
this.path = opts.path;
|
|
7927
7987
|
this.skipStreamChunks = opts.skipStreamChunks ?? true;
|
|
7928
7988
|
this.filter = opts.filter;
|
|
7929
|
-
|
|
7989
|
+
try {
|
|
7990
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
7991
|
+
} catch (e) {
|
|
7992
|
+
this.disable(`mkdir failed: ${e?.message ?? String(e)}`);
|
|
7993
|
+
}
|
|
7930
7994
|
}
|
|
7931
7995
|
async onContextItem(source, item) {
|
|
7996
|
+
if (this.disabled) return;
|
|
7932
7997
|
if (this.skipStreamChunks && item instanceof ClaudeStreamChunkItem) {
|
|
7933
7998
|
return;
|
|
7934
7999
|
}
|
|
@@ -7940,7 +8005,19 @@ var Auditor = class extends Participant {
|
|
|
7940
8005
|
source: this.sourceLabel(source),
|
|
7941
8006
|
item: item.toJSON()
|
|
7942
8007
|
};
|
|
7943
|
-
|
|
8008
|
+
try {
|
|
8009
|
+
appendFileSync(this.path, JSON.stringify(entry) + "\n");
|
|
8010
|
+
} catch (e) {
|
|
8011
|
+
this.disable(`append failed: ${e?.message ?? String(e)}`);
|
|
8012
|
+
}
|
|
8013
|
+
}
|
|
8014
|
+
disable(reason) {
|
|
8015
|
+
if (this.disabled) return;
|
|
8016
|
+
this.disabled = true;
|
|
8017
|
+
process.stderr.write(
|
|
8018
|
+
`[auditor] cannot write audit log at ${this.path}: ${reason} \u2014 continuing without audit
|
|
8019
|
+
`
|
|
8020
|
+
);
|
|
7944
8021
|
}
|
|
7945
8022
|
sourceLabel(source) {
|
|
7946
8023
|
const ctor = source.constructor.name;
|
|
@@ -8153,7 +8230,7 @@ function stringifyToolResultContent(content) {
|
|
|
8153
8230
|
}
|
|
8154
8231
|
|
|
8155
8232
|
// ../baro-orchestrator/src/participants/claude-cli-participant.ts
|
|
8156
|
-
var ClaudeCliParticipant = class extends Participant {
|
|
8233
|
+
var ClaudeCliParticipant = class _ClaudeCliParticipant extends Participant {
|
|
8157
8234
|
constructor(agentId, opts) {
|
|
8158
8235
|
super();
|
|
8159
8236
|
this.agentId = agentId;
|
|
@@ -8172,6 +8249,22 @@ var ClaudeCliParticipant = class extends Participant {
|
|
|
8172
8249
|
this.resolveDone = res;
|
|
8173
8250
|
});
|
|
8174
8251
|
}
|
|
8252
|
+
/**
|
|
8253
|
+
* Process-wide registry of every Claude child currently running.
|
|
8254
|
+
* Used by the orchestrator's SIGINT/SIGTERM handlers to nuke
|
|
8255
|
+
* orphaned Claude processes so a killed baro doesn't leave a swarm
|
|
8256
|
+
* of background agents burning quota.
|
|
8257
|
+
*/
|
|
8258
|
+
static active = /* @__PURE__ */ new Set();
|
|
8259
|
+
/** Send a signal to every active Claude child. Idempotent. */
|
|
8260
|
+
static killAll(signal = "SIGTERM") {
|
|
8261
|
+
for (const p of _ClaudeCliParticipant.active) {
|
|
8262
|
+
try {
|
|
8263
|
+
p.proc?.kill(signal);
|
|
8264
|
+
} catch {
|
|
8265
|
+
}
|
|
8266
|
+
}
|
|
8267
|
+
}
|
|
8175
8268
|
options;
|
|
8176
8269
|
proc = null;
|
|
8177
8270
|
buffer = "";
|
|
@@ -8221,6 +8314,7 @@ var ClaudeCliParticipant = class extends Participant {
|
|
|
8221
8314
|
return;
|
|
8222
8315
|
}
|
|
8223
8316
|
this.proc = proc;
|
|
8317
|
+
_ClaudeCliParticipant.active.add(this);
|
|
8224
8318
|
this.transition("starting");
|
|
8225
8319
|
proc.stdout.setEncoding("utf8");
|
|
8226
8320
|
proc.stderr.setEncoding("utf8");
|
|
@@ -8231,6 +8325,7 @@ var ClaudeCliParticipant = class extends Participant {
|
|
|
8231
8325
|
this.rejectReady(err);
|
|
8232
8326
|
});
|
|
8233
8327
|
proc.on("exit", (code) => {
|
|
8328
|
+
_ClaudeCliParticipant.active.delete(this);
|
|
8234
8329
|
this.exitCode = code;
|
|
8235
8330
|
const finalPhase = this.spawnError != null || code != null && code !== 0 ? "failed" : "done";
|
|
8236
8331
|
this.transition(finalPhase, code != null ? `exit code ${code}` : "no exit code");
|
|
@@ -9237,6 +9332,427 @@ function extractVerdictJson(text) {
|
|
|
9237
9332
|
throw new Error(`unbalanced JSON object in critic response: ${trimmed.slice(0, 200)}`);
|
|
9238
9333
|
}
|
|
9239
9334
|
|
|
9335
|
+
// ../baro-orchestrator/src/participants/finalizer.ts
|
|
9336
|
+
import { execFile as execFile3 } from "child_process";
|
|
9337
|
+
import { promisify as promisify3 } from "util";
|
|
9338
|
+
var execFileAsync2 = promisify3(execFile3);
|
|
9339
|
+
var Finalizer = class extends Participant {
|
|
9340
|
+
opts;
|
|
9341
|
+
envRef = null;
|
|
9342
|
+
startedAtMs = null;
|
|
9343
|
+
baseSha;
|
|
9344
|
+
branchName = null;
|
|
9345
|
+
levels = [];
|
|
9346
|
+
stories = /* @__PURE__ */ new Map();
|
|
9347
|
+
/**
|
|
9348
|
+
* Resolves once finalize() has completed (or been short-circuited).
|
|
9349
|
+
* Lets orchestrate.ts gate its TUI `done` event so the PR URL lands
|
|
9350
|
+
* in the completion screen instead of after it.
|
|
9351
|
+
*/
|
|
9352
|
+
finalizePromise = null;
|
|
9353
|
+
constructor(opts) {
|
|
9354
|
+
super();
|
|
9355
|
+
this.opts = {
|
|
9356
|
+
cwd: opts.cwd,
|
|
9357
|
+
prdPath: opts.prdPath,
|
|
9358
|
+
createPr: opts.createPr ?? true,
|
|
9359
|
+
onLog: opts.onLog
|
|
9360
|
+
};
|
|
9361
|
+
this.baseSha = opts.baseSha ?? null;
|
|
9362
|
+
}
|
|
9363
|
+
setEnvironment(env) {
|
|
9364
|
+
this.envRef = env;
|
|
9365
|
+
}
|
|
9366
|
+
async onContextItem(_source, item) {
|
|
9367
|
+
if (item instanceof RunStartedItem) {
|
|
9368
|
+
this.startedAtMs = Date.now();
|
|
9369
|
+
if (this.baseSha == null) {
|
|
9370
|
+
this.baseSha = await getHeadSha(this.opts.cwd);
|
|
9371
|
+
}
|
|
9372
|
+
const prd = this.safeLoadPrd();
|
|
9373
|
+
this.branchName = prd?.branchName ?? null;
|
|
9374
|
+
return;
|
|
9375
|
+
}
|
|
9376
|
+
if (item instanceof LevelStartedItem) {
|
|
9377
|
+
this.levels[item.ordinal] = [...item.storyIds];
|
|
9378
|
+
for (const id of item.storyIds) {
|
|
9379
|
+
if (!this.stories.has(id)) {
|
|
9380
|
+
this.stories.set(id, {
|
|
9381
|
+
id,
|
|
9382
|
+
title: "",
|
|
9383
|
+
success: null,
|
|
9384
|
+
durationSecs: null,
|
|
9385
|
+
attempts: 0,
|
|
9386
|
+
levelOrdinal: item.ordinal
|
|
9387
|
+
});
|
|
9388
|
+
} else {
|
|
9389
|
+
const rec = this.stories.get(id);
|
|
9390
|
+
rec.levelOrdinal = item.ordinal;
|
|
9391
|
+
}
|
|
9392
|
+
}
|
|
9393
|
+
return;
|
|
9394
|
+
}
|
|
9395
|
+
if (item instanceof StoryResultItem) {
|
|
9396
|
+
const existing = this.stories.get(item.storyId) ?? {
|
|
9397
|
+
id: item.storyId,
|
|
9398
|
+
title: "",
|
|
9399
|
+
success: null,
|
|
9400
|
+
durationSecs: null,
|
|
9401
|
+
attempts: 0,
|
|
9402
|
+
levelOrdinal: null
|
|
9403
|
+
};
|
|
9404
|
+
existing.success = item.success;
|
|
9405
|
+
existing.durationSecs = item.durationSecs;
|
|
9406
|
+
existing.attempts = item.attempts;
|
|
9407
|
+
this.stories.set(item.storyId, existing);
|
|
9408
|
+
return;
|
|
9409
|
+
}
|
|
9410
|
+
if (item instanceof RunCompletedItem) {
|
|
9411
|
+
this.finalizePromise = this.finalize(item);
|
|
9412
|
+
await this.finalizePromise;
|
|
9413
|
+
return;
|
|
9414
|
+
}
|
|
9415
|
+
}
|
|
9416
|
+
/**
|
|
9417
|
+
* Resolves once Finalizer has finished handling RunCompletedItem
|
|
9418
|
+
* (PR opened, skipped, or failed). Resolves immediately if no run
|
|
9419
|
+
* has completed yet. Safe to call multiple times.
|
|
9420
|
+
*/
|
|
9421
|
+
complete() {
|
|
9422
|
+
return this.finalizePromise ?? Promise.resolve();
|
|
9423
|
+
}
|
|
9424
|
+
async finalize(run) {
|
|
9425
|
+
if (!this.opts.createPr) return;
|
|
9426
|
+
if (!run.success && run.completedStories.length === 0) {
|
|
9427
|
+
this.log("[finalizer] run failed before producing any commits; skipping PR");
|
|
9428
|
+
this.emit(new PrCreatedItem(null, this.branchName ?? "", ""));
|
|
9429
|
+
return;
|
|
9430
|
+
}
|
|
9431
|
+
if (!await this.hasGhBinary()) {
|
|
9432
|
+
this.log("[finalizer] `gh` not found on PATH; skipping PR creation");
|
|
9433
|
+
this.emit(new PrCreatedItem(null, this.branchName ?? "", ""));
|
|
9434
|
+
return;
|
|
9435
|
+
}
|
|
9436
|
+
const branch = this.branchName ?? await this.detectBranch();
|
|
9437
|
+
if (!branch) {
|
|
9438
|
+
this.log("[finalizer] could not determine branch; skipping PR");
|
|
9439
|
+
this.emit(new PrCreatedItem(null, "", ""));
|
|
9440
|
+
return;
|
|
9441
|
+
}
|
|
9442
|
+
const baseBranch = await this.detectDefaultBaseBranch();
|
|
9443
|
+
if (!baseBranch) {
|
|
9444
|
+
this.log("[finalizer] could not determine base branch; skipping PR");
|
|
9445
|
+
this.emit(new PrCreatedItem(null, branch, ""));
|
|
9446
|
+
return;
|
|
9447
|
+
}
|
|
9448
|
+
if (branch === baseBranch) {
|
|
9449
|
+
this.log(
|
|
9450
|
+
`[finalizer] branch '${branch}' matches base; skipping PR (run committed straight to main?)`
|
|
9451
|
+
);
|
|
9452
|
+
this.emit(new PrCreatedItem(null, branch, baseBranch));
|
|
9453
|
+
return;
|
|
9454
|
+
}
|
|
9455
|
+
this.emit(new FinalizeStartedItem(branch));
|
|
9456
|
+
const prd = this.safeLoadPrd();
|
|
9457
|
+
if (prd) {
|
|
9458
|
+
for (const s of prd.userStories) {
|
|
9459
|
+
const rec = this.stories.get(s.id);
|
|
9460
|
+
if (rec && !rec.title) rec.title = s.title;
|
|
9461
|
+
}
|
|
9462
|
+
}
|
|
9463
|
+
const commits = await this.collectCommitsSinceBase();
|
|
9464
|
+
const orderedStories = this.orderStories();
|
|
9465
|
+
const { passed, failed } = this.partition(orderedStories);
|
|
9466
|
+
const filesStats = await this.collectFileStats();
|
|
9467
|
+
const totalSecs = run.totalDurationSecs;
|
|
9468
|
+
const title = this.buildPrTitle(prd, passed.length, orderedStories.length);
|
|
9469
|
+
const body = this.buildPrBody({
|
|
9470
|
+
prd,
|
|
9471
|
+
run,
|
|
9472
|
+
orderedStories,
|
|
9473
|
+
passed,
|
|
9474
|
+
failed,
|
|
9475
|
+
commits,
|
|
9476
|
+
filesStats,
|
|
9477
|
+
totalSecs,
|
|
9478
|
+
sequentialSecs: this.sequentialSeconds()
|
|
9479
|
+
});
|
|
9480
|
+
this.log(`[finalizer] opening PR on ${baseBranch} \u2190 ${branch}`);
|
|
9481
|
+
const url = await this.openPr({ title, body, baseBranch, branch });
|
|
9482
|
+
if (url) {
|
|
9483
|
+
this.log(`[finalizer] PR opened: ${url}`);
|
|
9484
|
+
}
|
|
9485
|
+
this.emit(new PrCreatedItem(url, branch, baseBranch));
|
|
9486
|
+
}
|
|
9487
|
+
// ─── Bus & env helpers ──────────────────────────────────────────
|
|
9488
|
+
emit(item) {
|
|
9489
|
+
this.envRef?.deliverContextItem(this, item);
|
|
9490
|
+
}
|
|
9491
|
+
log(line) {
|
|
9492
|
+
this.opts.onLog?.(line);
|
|
9493
|
+
}
|
|
9494
|
+
// ─── PRD ────────────────────────────────────────────────────────
|
|
9495
|
+
safeLoadPrd() {
|
|
9496
|
+
try {
|
|
9497
|
+
return loadPrd(this.opts.prdPath);
|
|
9498
|
+
} catch {
|
|
9499
|
+
return null;
|
|
9500
|
+
}
|
|
9501
|
+
}
|
|
9502
|
+
// ─── Story ordering & partitioning ──────────────────────────────
|
|
9503
|
+
/**
|
|
9504
|
+
* Stories returned in DAG order so the table reads top-down the same
|
|
9505
|
+
* way the run executed: level 0 first, then level 1, etc. Within a
|
|
9506
|
+
* level we sort by id (stable) to keep the table deterministic.
|
|
9507
|
+
*/
|
|
9508
|
+
orderStories() {
|
|
9509
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9510
|
+
const ordered = [];
|
|
9511
|
+
for (const lvl of this.levels) {
|
|
9512
|
+
const sorted = [...lvl].sort();
|
|
9513
|
+
for (const id of sorted) {
|
|
9514
|
+
const rec = this.stories.get(id);
|
|
9515
|
+
if (rec && !seen.has(id)) {
|
|
9516
|
+
ordered.push(rec);
|
|
9517
|
+
seen.add(id);
|
|
9518
|
+
}
|
|
9519
|
+
}
|
|
9520
|
+
}
|
|
9521
|
+
for (const [id, rec] of this.stories.entries()) {
|
|
9522
|
+
if (!seen.has(id)) ordered.push(rec);
|
|
9523
|
+
}
|
|
9524
|
+
return ordered;
|
|
9525
|
+
}
|
|
9526
|
+
partition(stories) {
|
|
9527
|
+
const passed = [];
|
|
9528
|
+
const failed = [];
|
|
9529
|
+
for (const s of stories) {
|
|
9530
|
+
if (s.success === true) passed.push(s);
|
|
9531
|
+
else if (s.success === false) failed.push(s);
|
|
9532
|
+
}
|
|
9533
|
+
return { passed, failed };
|
|
9534
|
+
}
|
|
9535
|
+
sequentialSeconds() {
|
|
9536
|
+
let sum = 0;
|
|
9537
|
+
for (const s of this.stories.values()) {
|
|
9538
|
+
if (s.durationSecs && s.success !== false) sum += s.durationSecs;
|
|
9539
|
+
}
|
|
9540
|
+
return sum;
|
|
9541
|
+
}
|
|
9542
|
+
// ─── Git / commits / files ──────────────────────────────────────
|
|
9543
|
+
async collectCommitsSinceBase() {
|
|
9544
|
+
if (!this.baseSha) return [];
|
|
9545
|
+
try {
|
|
9546
|
+
const { stdout } = await execFileAsync2(
|
|
9547
|
+
"git",
|
|
9548
|
+
["log", `${this.baseSha}..HEAD`, "--pretty=format:%H%x09%s"],
|
|
9549
|
+
{ cwd: this.opts.cwd }
|
|
9550
|
+
);
|
|
9551
|
+
return stdout.split("\n").filter((l) => l.includes(" ")).map((l) => {
|
|
9552
|
+
const [sha, ...rest] = l.split(" ");
|
|
9553
|
+
return { sha, subject: rest.join(" ").trim() };
|
|
9554
|
+
});
|
|
9555
|
+
} catch {
|
|
9556
|
+
return [];
|
|
9557
|
+
}
|
|
9558
|
+
}
|
|
9559
|
+
async collectFileStats() {
|
|
9560
|
+
if (!this.baseSha) return { created: 0, modified: 0 };
|
|
9561
|
+
try {
|
|
9562
|
+
const { stdout } = await execFileAsync2(
|
|
9563
|
+
"git",
|
|
9564
|
+
["diff", "--name-status", this.baseSha, "HEAD"],
|
|
9565
|
+
{ cwd: this.opts.cwd }
|
|
9566
|
+
);
|
|
9567
|
+
let created = 0;
|
|
9568
|
+
let modified = 0;
|
|
9569
|
+
for (const line of stdout.split("\n")) {
|
|
9570
|
+
const ch = line.charAt(0);
|
|
9571
|
+
if (ch === "A") created++;
|
|
9572
|
+
else if (ch === "M" || ch === "R") modified++;
|
|
9573
|
+
}
|
|
9574
|
+
return { created, modified };
|
|
9575
|
+
} catch {
|
|
9576
|
+
return { created: 0, modified: 0 };
|
|
9577
|
+
}
|
|
9578
|
+
}
|
|
9579
|
+
async detectBranch() {
|
|
9580
|
+
try {
|
|
9581
|
+
const { stdout } = await execFileAsync2(
|
|
9582
|
+
"git",
|
|
9583
|
+
["branch", "--show-current"],
|
|
9584
|
+
{ cwd: this.opts.cwd }
|
|
9585
|
+
);
|
|
9586
|
+
return stdout.trim() || null;
|
|
9587
|
+
} catch {
|
|
9588
|
+
return null;
|
|
9589
|
+
}
|
|
9590
|
+
}
|
|
9591
|
+
async detectDefaultBaseBranch() {
|
|
9592
|
+
try {
|
|
9593
|
+
const { stdout } = await execFileAsync2(
|
|
9594
|
+
"gh",
|
|
9595
|
+
["repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"],
|
|
9596
|
+
{ cwd: this.opts.cwd }
|
|
9597
|
+
);
|
|
9598
|
+
const name = stdout.trim();
|
|
9599
|
+
if (name) return name;
|
|
9600
|
+
} catch {
|
|
9601
|
+
}
|
|
9602
|
+
try {
|
|
9603
|
+
const { stdout } = await execFileAsync2(
|
|
9604
|
+
"git",
|
|
9605
|
+
["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
9606
|
+
{ cwd: this.opts.cwd }
|
|
9607
|
+
);
|
|
9608
|
+
const ref = stdout.trim();
|
|
9609
|
+
if (ref.startsWith("origin/")) return ref.slice("origin/".length);
|
|
9610
|
+
} catch {
|
|
9611
|
+
}
|
|
9612
|
+
return "main";
|
|
9613
|
+
}
|
|
9614
|
+
// ─── PR body composition ────────────────────────────────────────
|
|
9615
|
+
buildPrTitle(prd, passed, total) {
|
|
9616
|
+
const project = prd?.project ?? "baro run";
|
|
9617
|
+
if (passed === total) {
|
|
9618
|
+
return `${project} (${total} ${total === 1 ? "story" : "stories"})`;
|
|
9619
|
+
}
|
|
9620
|
+
return `${project} (${passed}/${total} stories)`;
|
|
9621
|
+
}
|
|
9622
|
+
buildPrBody(args) {
|
|
9623
|
+
const { prd, run, orderedStories, passed, failed, commits, filesStats } = args;
|
|
9624
|
+
const lines = [];
|
|
9625
|
+
lines.push(
|
|
9626
|
+
`> Opened by [baro](https://baro.rs) \u2014 Mozaik-orchestrated parallel coding agents.`
|
|
9627
|
+
);
|
|
9628
|
+
lines.push("");
|
|
9629
|
+
if (prd?.description) {
|
|
9630
|
+
lines.push("## Goal");
|
|
9631
|
+
lines.push("");
|
|
9632
|
+
lines.push(prd.description.trim());
|
|
9633
|
+
lines.push("");
|
|
9634
|
+
}
|
|
9635
|
+
if (this.levels.length > 0) {
|
|
9636
|
+
lines.push("## Plan");
|
|
9637
|
+
lines.push("");
|
|
9638
|
+
lines.push("```");
|
|
9639
|
+
for (let i = 0; i < this.levels.length; i++) {
|
|
9640
|
+
const ids = this.levels[i];
|
|
9641
|
+
lines.push(`Level ${i} \u2500\u2500\u2500 ${ids.join(", ")}`);
|
|
9642
|
+
}
|
|
9643
|
+
lines.push("```");
|
|
9644
|
+
lines.push("");
|
|
9645
|
+
}
|
|
9646
|
+
lines.push("## Stories");
|
|
9647
|
+
lines.push("");
|
|
9648
|
+
lines.push("| # | Story | Status | Duration | Commit |");
|
|
9649
|
+
lines.push("|---|-------|--------|----------|--------|");
|
|
9650
|
+
for (const s of orderedStories) {
|
|
9651
|
+
const title = (s.title || s.id).replace(/\|/g, "\\|");
|
|
9652
|
+
const status = s.success === true ? "\u2713" : s.success === false ? "\u2717" : "\u2014";
|
|
9653
|
+
const dur = s.durationSecs != null ? formatDuration(s.durationSecs) : "\u2014";
|
|
9654
|
+
const commit = matchCommit(s, commits);
|
|
9655
|
+
lines.push(
|
|
9656
|
+
`| ${s.id} | ${title} | ${status} | ${dur} | ${commit ? "`" + commit.slice(0, 7) + "`" : "\u2014"} |`
|
|
9657
|
+
);
|
|
9658
|
+
}
|
|
9659
|
+
lines.push("");
|
|
9660
|
+
lines.push("## Diff stats");
|
|
9661
|
+
lines.push("");
|
|
9662
|
+
lines.push(`- **Files created**: ${filesStats.created}`);
|
|
9663
|
+
lines.push(`- **Files modified**: ${filesStats.modified}`);
|
|
9664
|
+
lines.push(`- **Total commits**: ${commits.length}`);
|
|
9665
|
+
lines.push("");
|
|
9666
|
+
lines.push("## Run summary");
|
|
9667
|
+
lines.push("");
|
|
9668
|
+
const wall = formatDuration(args.totalSecs);
|
|
9669
|
+
const seq = formatDuration(args.sequentialSecs);
|
|
9670
|
+
const speedup = args.totalSecs > 0 ? (args.sequentialSecs / args.totalSecs).toFixed(2) + "\xD7" : "\u2014";
|
|
9671
|
+
lines.push(`- **Wall time**: ${wall}`);
|
|
9672
|
+
lines.push(`- **Sequential time**: ${seq}`);
|
|
9673
|
+
lines.push(`- **Parallel speedup**: ${speedup}`);
|
|
9674
|
+
lines.push(`- **Stories passed**: ${passed.length}/${orderedStories.length}`);
|
|
9675
|
+
lines.push(`- **Stories failed**: ${failed.length}`);
|
|
9676
|
+
lines.push(`- **Total story attempts**: ${run.totalAttempts}`);
|
|
9677
|
+
if (run.abortReason) {
|
|
9678
|
+
lines.push(`- **Abort reason**: ${run.abortReason}`);
|
|
9679
|
+
}
|
|
9680
|
+
lines.push("");
|
|
9681
|
+
lines.push("---");
|
|
9682
|
+
lines.push("");
|
|
9683
|
+
lines.push("\u{1F916} Plan. Parallelize. Review. Ship. \u2014 opened by baro");
|
|
9684
|
+
return lines.join("\n");
|
|
9685
|
+
}
|
|
9686
|
+
async hasGhBinary() {
|
|
9687
|
+
try {
|
|
9688
|
+
await execFileAsync2("gh", ["--version"], { cwd: this.opts.cwd });
|
|
9689
|
+
return true;
|
|
9690
|
+
} catch {
|
|
9691
|
+
return false;
|
|
9692
|
+
}
|
|
9693
|
+
}
|
|
9694
|
+
async openPr(args) {
|
|
9695
|
+
try {
|
|
9696
|
+
const { stdout } = await execFileAsync2(
|
|
9697
|
+
"gh",
|
|
9698
|
+
[
|
|
9699
|
+
"pr",
|
|
9700
|
+
"create",
|
|
9701
|
+
"--base",
|
|
9702
|
+
args.baseBranch,
|
|
9703
|
+
"--head",
|
|
9704
|
+
args.branch,
|
|
9705
|
+
"--title",
|
|
9706
|
+
args.title,
|
|
9707
|
+
"--body",
|
|
9708
|
+
args.body
|
|
9709
|
+
],
|
|
9710
|
+
{ cwd: this.opts.cwd }
|
|
9711
|
+
);
|
|
9712
|
+
const url = stdout.trim().split("\n").pop() ?? "";
|
|
9713
|
+
return url || null;
|
|
9714
|
+
} catch (e) {
|
|
9715
|
+
const stderr = e?.stderr ?? "";
|
|
9716
|
+
const existing = stderr.match(/https:\/\/github\.com\/\S+\/pull\/\d+/)?.[0];
|
|
9717
|
+
if (existing) {
|
|
9718
|
+
this.log(`[finalizer] PR already exists: ${existing}`);
|
|
9719
|
+
return existing;
|
|
9720
|
+
}
|
|
9721
|
+
this.log(
|
|
9722
|
+
`[finalizer] gh pr create failed: ${stderr.split("\n")[0]?.trim() || e.message}`
|
|
9723
|
+
);
|
|
9724
|
+
return null;
|
|
9725
|
+
}
|
|
9726
|
+
}
|
|
9727
|
+
};
|
|
9728
|
+
function formatDuration(secs) {
|
|
9729
|
+
if (secs < 60) return `${Math.round(secs)}s`;
|
|
9730
|
+
const m = Math.floor(secs / 60);
|
|
9731
|
+
const s = Math.round(secs % 60);
|
|
9732
|
+
return `${m}:${s.toString().padStart(2, "0")}`;
|
|
9733
|
+
}
|
|
9734
|
+
function matchCommit(story, commits) {
|
|
9735
|
+
if (commits.length === 0) return null;
|
|
9736
|
+
const idPattern = new RegExp(`\\b${story.id}\\b`, "i");
|
|
9737
|
+
for (const c of commits) {
|
|
9738
|
+
if (idPattern.test(c.subject)) return c.sha;
|
|
9739
|
+
}
|
|
9740
|
+
if (!story.title) return null;
|
|
9741
|
+
const keywords = story.title.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 4);
|
|
9742
|
+
if (keywords.length === 0) return null;
|
|
9743
|
+
let bestSha = null;
|
|
9744
|
+
let bestScore = 0;
|
|
9745
|
+
for (const c of commits) {
|
|
9746
|
+
const subj = c.subject.toLowerCase();
|
|
9747
|
+
const hits = keywords.reduce((n, k) => subj.includes(k) ? n + 1 : n, 0);
|
|
9748
|
+
if (hits > bestScore) {
|
|
9749
|
+
bestScore = hits;
|
|
9750
|
+
bestSha = c.sha;
|
|
9751
|
+
}
|
|
9752
|
+
}
|
|
9753
|
+
return bestScore >= 2 ? bestSha : null;
|
|
9754
|
+
}
|
|
9755
|
+
|
|
9240
9756
|
// ../baro-orchestrator/src/participants/librarian.ts
|
|
9241
9757
|
var EXPLORATION_TOOLS = /* @__PURE__ */ new Set([
|
|
9242
9758
|
"Read",
|
|
@@ -9583,9 +10099,9 @@ var StoryFactory = class extends Participant {
|
|
|
9583
10099
|
};
|
|
9584
10100
|
|
|
9585
10101
|
// ../baro-orchestrator/src/participants/surgeon.ts
|
|
9586
|
-
import { execFile as
|
|
9587
|
-
import { promisify as
|
|
9588
|
-
var
|
|
10102
|
+
import { execFile as execFile4 } from "child_process";
|
|
10103
|
+
import { promisify as promisify4 } from "util";
|
|
10104
|
+
var execFileAsync3 = promisify4(execFile4);
|
|
9589
10105
|
var SURGEON_SYSTEM_PROMPT = `You are the Surgeon \u2014 an autonomous planner that adapts a software-project
|
|
9590
10106
|
DAG when stories fail. Given:
|
|
9591
10107
|
1. A snapshot of the current PRD (project, story list with dependencies +
|
|
@@ -9697,7 +10213,7 @@ var Surgeon = class extends Participant {
|
|
|
9697
10213
|
const snap = this.opts.snapshot();
|
|
9698
10214
|
const prompt = buildSurgeonPrompt(snap, failure);
|
|
9699
10215
|
try {
|
|
9700
|
-
const { stdout } = await
|
|
10216
|
+
const { stdout } = await execFileAsync3(
|
|
9701
10217
|
this.opts.claudeBin,
|
|
9702
10218
|
[
|
|
9703
10219
|
"--print",
|
|
@@ -9858,6 +10374,15 @@ async function orchestrate(config) {
|
|
|
9858
10374
|
});
|
|
9859
10375
|
critic.join(env);
|
|
9860
10376
|
}
|
|
10377
|
+
const finalizer = useGit ? new Finalizer({
|
|
10378
|
+
cwd: config.cwd,
|
|
10379
|
+
prdPath: config.prdPath,
|
|
10380
|
+
onLog: (line) => emitTui && emit({ type: "story_log", id: "_finalizer", line })
|
|
10381
|
+
}) : null;
|
|
10382
|
+
if (finalizer) {
|
|
10383
|
+
finalizer.setEnvironment(env);
|
|
10384
|
+
finalizer.join(env);
|
|
10385
|
+
}
|
|
9861
10386
|
const conductor = new Conductor({
|
|
9862
10387
|
prdPath: config.prdPath,
|
|
9863
10388
|
cwd: config.cwd,
|
|
@@ -9885,7 +10410,8 @@ async function orchestrate(config) {
|
|
|
9885
10410
|
onStoryPassed: useGit ? async (storyId) => {
|
|
9886
10411
|
await safePullRebase(
|
|
9887
10412
|
config.cwd,
|
|
9888
|
-
(line) => emitTui && emit({ type: "story_log", id: storyId, line })
|
|
10413
|
+
(line) => emitTui && emit({ type: "story_log", id: storyId, line }),
|
|
10414
|
+
gitGate
|
|
9889
10415
|
);
|
|
9890
10416
|
try {
|
|
9891
10417
|
await gitPushWithRetry(gitGate, {
|
|
@@ -9937,6 +10463,7 @@ async function orchestrate(config) {
|
|
|
9937
10463
|
const summary = await conductor.done;
|
|
9938
10464
|
if (critic) await critic.idle();
|
|
9939
10465
|
if (surgeon) await surgeon.idle();
|
|
10466
|
+
if (finalizer) await finalizer.complete();
|
|
9940
10467
|
let filesCreated = 0;
|
|
9941
10468
|
let filesModified = 0;
|
|
9942
10469
|
if (useGit && baseSha) {
|
|
@@ -10012,6 +10539,14 @@ var BaroEventForwarder = class extends Participant {
|
|
|
10012
10539
|
this.handleCritique(item);
|
|
10013
10540
|
return;
|
|
10014
10541
|
}
|
|
10542
|
+
if (item instanceof FinalizeStartedItem) {
|
|
10543
|
+
emit({ type: "finalize_start" });
|
|
10544
|
+
return;
|
|
10545
|
+
}
|
|
10546
|
+
if (item instanceof PrCreatedItem) {
|
|
10547
|
+
emit({ type: "finalize_complete", pr_url: item.url });
|
|
10548
|
+
return;
|
|
10549
|
+
}
|
|
10015
10550
|
}
|
|
10016
10551
|
handleCoordination(item) {
|
|
10017
10552
|
emit({
|
|
@@ -10303,14 +10838,42 @@ process.on("unhandledRejection", (reason) => {
|
|
|
10303
10838
|
const stack = reason?.stack ?? String(reason);
|
|
10304
10839
|
process.stderr.write(`[cli] unhandledRejection: ${stack}
|
|
10305
10840
|
`);
|
|
10841
|
+
ClaudeCliParticipant.killAll("SIGTERM");
|
|
10306
10842
|
process.exit(1);
|
|
10307
10843
|
});
|
|
10308
10844
|
process.on("uncaughtException", (err) => {
|
|
10309
10845
|
const stack = err?.stack ?? String(err);
|
|
10310
10846
|
process.stderr.write(`[cli] uncaughtException: ${stack}
|
|
10311
10847
|
`);
|
|
10848
|
+
ClaudeCliParticipant.killAll("SIGTERM");
|
|
10312
10849
|
process.exit(1);
|
|
10313
10850
|
});
|
|
10851
|
+
var shuttingDown = false;
|
|
10852
|
+
function shutdown(signal) {
|
|
10853
|
+
if (shuttingDown) return;
|
|
10854
|
+
shuttingDown = true;
|
|
10855
|
+
process.stderr.write(`[cli] received ${signal}, killing in-flight Claude children...
|
|
10856
|
+
`);
|
|
10857
|
+
ClaudeCliParticipant.killAll("SIGTERM");
|
|
10858
|
+
setTimeout(() => {
|
|
10859
|
+
ClaudeCliParticipant.killAll("SIGKILL");
|
|
10860
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
10861
|
+
}, 1500).unref();
|
|
10862
|
+
}
|
|
10863
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
10864
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
10865
|
+
var initialPpid = process.ppid;
|
|
10866
|
+
var orphanWatchdog = setInterval(() => {
|
|
10867
|
+
if (process.ppid !== initialPpid) {
|
|
10868
|
+
process.stderr.write(
|
|
10869
|
+
`[cli] parent died (ppid ${initialPpid} \u2192 ${process.ppid}), shutting down
|
|
10870
|
+
`
|
|
10871
|
+
);
|
|
10872
|
+
clearInterval(orphanWatchdog);
|
|
10873
|
+
shutdown("SIGTERM");
|
|
10874
|
+
}
|
|
10875
|
+
}, 1e3);
|
|
10876
|
+
orphanWatchdog.unref();
|
|
10314
10877
|
main().catch((e) => {
|
|
10315
10878
|
process.stderr.write(`[cli] unhandled: ${e?.stack ?? String(e)}
|
|
10316
10879
|
`);
|