baro-ai 0.23.2 → 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
@@ -7918,6 +7918,33 @@ var StorySpawnedItem = class extends ContextItem {
7918
7918
  return { type: this.type, storyId: this.storyId };
7919
7919
  }
7920
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
+ };
7921
7948
  var RunCompletedItem = class extends ContextItem {
7922
7949
  constructor(success, completedStories, failedStories, totalDurationSecs, totalAttempts, abortReason = null) {
7923
7950
  super();
@@ -9305,6 +9332,427 @@ function extractVerdictJson(text) {
9305
9332
  throw new Error(`unbalanced JSON object in critic response: ${trimmed.slice(0, 200)}`);
9306
9333
  }
9307
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
+
9308
9756
  // ../baro-orchestrator/src/participants/librarian.ts
9309
9757
  var EXPLORATION_TOOLS = /* @__PURE__ */ new Set([
9310
9758
  "Read",
@@ -9651,9 +10099,9 @@ var StoryFactory = class extends Participant {
9651
10099
  };
9652
10100
 
9653
10101
  // ../baro-orchestrator/src/participants/surgeon.ts
9654
- import { execFile as execFile3 } from "child_process";
9655
- import { promisify as promisify3 } from "util";
9656
- var execFileAsync2 = promisify3(execFile3);
10102
+ import { execFile as execFile4 } from "child_process";
10103
+ import { promisify as promisify4 } from "util";
10104
+ var execFileAsync3 = promisify4(execFile4);
9657
10105
  var SURGEON_SYSTEM_PROMPT = `You are the Surgeon \u2014 an autonomous planner that adapts a software-project
9658
10106
  DAG when stories fail. Given:
9659
10107
  1. A snapshot of the current PRD (project, story list with dependencies +
@@ -9765,7 +10213,7 @@ var Surgeon = class extends Participant {
9765
10213
  const snap = this.opts.snapshot();
9766
10214
  const prompt = buildSurgeonPrompt(snap, failure);
9767
10215
  try {
9768
- const { stdout } = await execFileAsync2(
10216
+ const { stdout } = await execFileAsync3(
9769
10217
  this.opts.claudeBin,
9770
10218
  [
9771
10219
  "--print",
@@ -9926,6 +10374,15 @@ async function orchestrate(config) {
9926
10374
  });
9927
10375
  critic.join(env);
9928
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
+ }
9929
10386
  const conductor = new Conductor({
9930
10387
  prdPath: config.prdPath,
9931
10388
  cwd: config.cwd,
@@ -10006,6 +10463,7 @@ async function orchestrate(config) {
10006
10463
  const summary = await conductor.done;
10007
10464
  if (critic) await critic.idle();
10008
10465
  if (surgeon) await surgeon.idle();
10466
+ if (finalizer) await finalizer.complete();
10009
10467
  let filesCreated = 0;
10010
10468
  let filesModified = 0;
10011
10469
  if (useGit && baseSha) {
@@ -10081,6 +10539,14 @@ var BaroEventForwarder = class extends Participant {
10081
10539
  this.handleCritique(item);
10082
10540
  return;
10083
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
+ }
10084
10550
  }
10085
10551
  handleCoordination(item) {
10086
10552
  emit({