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 CHANGED
@@ -7414,32 +7414,53 @@ async function createOrCheckoutBranch(cwd, branchName, onLog) {
7414
7414
  );
7415
7415
  }
7416
7416
  }
7417
- async function safePullRebase(cwd, onLog) {
7418
- if (!await hasRemoteOrigin(cwd)) {
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 exec("git", ["pull", "--rebase", "origin", branch], { cwd });
7437
- onLog?.("[git] pull ok");
7438
- } catch {
7439
- onLog?.("[git] pull conflict, continuing without pull");
7440
- await execSafe("git", ["rebase", "--abort"], { cwd });
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?.("[git] push failed, pulling and retrying...");
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
- options.onLog?.(`[git] push failed after ${max} attempts`);
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
- mkdirSync(dirname(this.path), { recursive: true });
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
- appendFileSync(this.path, JSON.stringify(entry) + "\n");
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 execFile3 } from "child_process";
9587
- import { promisify as promisify3 } from "util";
9588
- var execFileAsync2 = promisify3(execFile3);
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 execFileAsync2(
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
  `);