baro-ai 0.23.2 → 0.23.4

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,449 @@ 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
+ /**
9346
+ * DAG levels keyed by their ordinal as emitted in
9347
+ * LevelStartedItem. We use a Map rather than an array because
9348
+ * Conductor's ordinals are 1-based and would otherwise leave a
9349
+ * `levels[0] = undefined` hole that crashes any `for...of`
9350
+ * iteration (it walks holes too).
9351
+ */
9352
+ levels = /* @__PURE__ */ new Map();
9353
+ stories = /* @__PURE__ */ new Map();
9354
+ /**
9355
+ * Resolves once finalize() has completed (or been short-circuited).
9356
+ * Lets orchestrate.ts gate its TUI `done` event so the PR URL lands
9357
+ * in the completion screen instead of after it.
9358
+ */
9359
+ finalizePromise = null;
9360
+ constructor(opts) {
9361
+ super();
9362
+ this.opts = {
9363
+ cwd: opts.cwd,
9364
+ prdPath: opts.prdPath,
9365
+ createPr: opts.createPr ?? true,
9366
+ onLog: opts.onLog
9367
+ };
9368
+ this.baseSha = opts.baseSha ?? null;
9369
+ }
9370
+ setEnvironment(env) {
9371
+ this.envRef = env;
9372
+ }
9373
+ async onContextItem(_source, item) {
9374
+ if (item instanceof RunStartedItem) {
9375
+ this.startedAtMs = Date.now();
9376
+ if (this.baseSha == null) {
9377
+ this.baseSha = await getHeadSha(this.opts.cwd);
9378
+ }
9379
+ const prd = this.safeLoadPrd();
9380
+ this.branchName = prd?.branchName ?? null;
9381
+ return;
9382
+ }
9383
+ if (item instanceof LevelStartedItem) {
9384
+ this.levels.set(item.ordinal, [...item.storyIds]);
9385
+ for (const id of item.storyIds) {
9386
+ if (!this.stories.has(id)) {
9387
+ this.stories.set(id, {
9388
+ id,
9389
+ title: "",
9390
+ success: null,
9391
+ durationSecs: null,
9392
+ attempts: 0,
9393
+ levelOrdinal: item.ordinal
9394
+ });
9395
+ } else {
9396
+ const rec = this.stories.get(id);
9397
+ rec.levelOrdinal = item.ordinal;
9398
+ }
9399
+ }
9400
+ return;
9401
+ }
9402
+ if (item instanceof StoryResultItem) {
9403
+ const existing = this.stories.get(item.storyId) ?? {
9404
+ id: item.storyId,
9405
+ title: "",
9406
+ success: null,
9407
+ durationSecs: null,
9408
+ attempts: 0,
9409
+ levelOrdinal: null
9410
+ };
9411
+ existing.success = item.success;
9412
+ existing.durationSecs = item.durationSecs;
9413
+ existing.attempts = item.attempts;
9414
+ this.stories.set(item.storyId, existing);
9415
+ return;
9416
+ }
9417
+ if (item instanceof RunCompletedItem) {
9418
+ this.finalizePromise = this.safeFinalize(item);
9419
+ await this.finalizePromise;
9420
+ return;
9421
+ }
9422
+ }
9423
+ async safeFinalize(item) {
9424
+ try {
9425
+ await this.finalize(item);
9426
+ } catch (e) {
9427
+ const msg = e?.stack ?? String(e);
9428
+ this.log(`[finalizer] internal error, skipping PR: ${msg.split("\n")[0]}`);
9429
+ process.stderr.write(`[finalizer] crash: ${msg}
9430
+ `);
9431
+ this.emit(new PrCreatedItem(null, this.branchName ?? "", ""));
9432
+ }
9433
+ }
9434
+ /**
9435
+ * Resolves once Finalizer has finished handling RunCompletedItem
9436
+ * (PR opened, skipped, or failed). Resolves immediately if no run
9437
+ * has completed yet. Safe to call multiple times.
9438
+ */
9439
+ complete() {
9440
+ return this.finalizePromise ?? Promise.resolve();
9441
+ }
9442
+ async finalize(run) {
9443
+ if (!this.opts.createPr) return;
9444
+ if (!run.success && run.completedStories.length === 0) {
9445
+ this.log("[finalizer] run failed before producing any commits; skipping PR");
9446
+ this.emit(new PrCreatedItem(null, this.branchName ?? "", ""));
9447
+ return;
9448
+ }
9449
+ if (!await this.hasGhBinary()) {
9450
+ this.log("[finalizer] `gh` not found on PATH; skipping PR creation");
9451
+ this.emit(new PrCreatedItem(null, this.branchName ?? "", ""));
9452
+ return;
9453
+ }
9454
+ const branch = this.branchName ?? await this.detectBranch();
9455
+ if (!branch) {
9456
+ this.log("[finalizer] could not determine branch; skipping PR");
9457
+ this.emit(new PrCreatedItem(null, "", ""));
9458
+ return;
9459
+ }
9460
+ const baseBranch = await this.detectDefaultBaseBranch();
9461
+ if (!baseBranch) {
9462
+ this.log("[finalizer] could not determine base branch; skipping PR");
9463
+ this.emit(new PrCreatedItem(null, branch, ""));
9464
+ return;
9465
+ }
9466
+ if (branch === baseBranch) {
9467
+ this.log(
9468
+ `[finalizer] branch '${branch}' matches base; skipping PR (run committed straight to main?)`
9469
+ );
9470
+ this.emit(new PrCreatedItem(null, branch, baseBranch));
9471
+ return;
9472
+ }
9473
+ this.emit(new FinalizeStartedItem(branch));
9474
+ const prd = this.safeLoadPrd();
9475
+ if (prd) {
9476
+ for (const s of prd.userStories) {
9477
+ const rec = this.stories.get(s.id);
9478
+ if (rec && !rec.title) rec.title = s.title;
9479
+ }
9480
+ }
9481
+ const commits = await this.collectCommitsSinceBase();
9482
+ const orderedStories = this.orderStories();
9483
+ const { passed, failed } = this.partition(orderedStories);
9484
+ const filesStats = await this.collectFileStats();
9485
+ const totalSecs = run.totalDurationSecs;
9486
+ const title = this.buildPrTitle(prd, passed.length, orderedStories.length);
9487
+ const body = this.buildPrBody({
9488
+ prd,
9489
+ run,
9490
+ orderedStories,
9491
+ passed,
9492
+ failed,
9493
+ commits,
9494
+ filesStats,
9495
+ totalSecs,
9496
+ sequentialSecs: this.sequentialSeconds()
9497
+ });
9498
+ this.log(`[finalizer] opening PR on ${baseBranch} \u2190 ${branch}`);
9499
+ const url = await this.openPr({ title, body, baseBranch, branch });
9500
+ if (url) {
9501
+ this.log(`[finalizer] PR opened: ${url}`);
9502
+ }
9503
+ this.emit(new PrCreatedItem(url, branch, baseBranch));
9504
+ }
9505
+ // ─── Bus & env helpers ──────────────────────────────────────────
9506
+ emit(item) {
9507
+ this.envRef?.deliverContextItem(this, item);
9508
+ }
9509
+ log(line) {
9510
+ this.opts.onLog?.(line);
9511
+ }
9512
+ // ─── PRD ────────────────────────────────────────────────────────
9513
+ safeLoadPrd() {
9514
+ try {
9515
+ return loadPrd(this.opts.prdPath);
9516
+ } catch {
9517
+ return null;
9518
+ }
9519
+ }
9520
+ // ─── Story ordering & partitioning ──────────────────────────────
9521
+ /**
9522
+ * Stories returned in DAG order so the table reads top-down the same
9523
+ * way the run executed: lowest-ordinal level first. Within a level
9524
+ * we sort by id (stable) to keep the table deterministic.
9525
+ */
9526
+ orderStories() {
9527
+ const seen = /* @__PURE__ */ new Set();
9528
+ const ordered = [];
9529
+ const ordinals = [...this.levels.keys()].sort((a, b) => a - b);
9530
+ for (const ord of ordinals) {
9531
+ const ids = this.levels.get(ord);
9532
+ if (!ids) continue;
9533
+ const sorted = [...ids].sort();
9534
+ for (const id of sorted) {
9535
+ const rec = this.stories.get(id);
9536
+ if (rec && !seen.has(id)) {
9537
+ ordered.push(rec);
9538
+ seen.add(id);
9539
+ }
9540
+ }
9541
+ }
9542
+ for (const [id, rec] of this.stories.entries()) {
9543
+ if (!seen.has(id)) ordered.push(rec);
9544
+ }
9545
+ return ordered;
9546
+ }
9547
+ partition(stories) {
9548
+ const passed = [];
9549
+ const failed = [];
9550
+ for (const s of stories) {
9551
+ if (s.success === true) passed.push(s);
9552
+ else if (s.success === false) failed.push(s);
9553
+ }
9554
+ return { passed, failed };
9555
+ }
9556
+ sequentialSeconds() {
9557
+ let sum = 0;
9558
+ for (const s of this.stories.values()) {
9559
+ if (s.durationSecs && s.success !== false) sum += s.durationSecs;
9560
+ }
9561
+ return sum;
9562
+ }
9563
+ // ─── Git / commits / files ──────────────────────────────────────
9564
+ async collectCommitsSinceBase() {
9565
+ if (!this.baseSha) return [];
9566
+ try {
9567
+ const { stdout } = await execFileAsync2(
9568
+ "git",
9569
+ ["log", `${this.baseSha}..HEAD`, "--pretty=format:%H%x09%s"],
9570
+ { cwd: this.opts.cwd }
9571
+ );
9572
+ return stdout.split("\n").filter((l) => l.includes(" ")).map((l) => {
9573
+ const [sha, ...rest] = l.split(" ");
9574
+ return { sha, subject: rest.join(" ").trim() };
9575
+ });
9576
+ } catch {
9577
+ return [];
9578
+ }
9579
+ }
9580
+ async collectFileStats() {
9581
+ if (!this.baseSha) return { created: 0, modified: 0 };
9582
+ try {
9583
+ const { stdout } = await execFileAsync2(
9584
+ "git",
9585
+ ["diff", "--name-status", this.baseSha, "HEAD"],
9586
+ { cwd: this.opts.cwd }
9587
+ );
9588
+ let created = 0;
9589
+ let modified = 0;
9590
+ for (const line of stdout.split("\n")) {
9591
+ const ch = line.charAt(0);
9592
+ if (ch === "A") created++;
9593
+ else if (ch === "M" || ch === "R") modified++;
9594
+ }
9595
+ return { created, modified };
9596
+ } catch {
9597
+ return { created: 0, modified: 0 };
9598
+ }
9599
+ }
9600
+ async detectBranch() {
9601
+ try {
9602
+ const { stdout } = await execFileAsync2(
9603
+ "git",
9604
+ ["branch", "--show-current"],
9605
+ { cwd: this.opts.cwd }
9606
+ );
9607
+ return stdout.trim() || null;
9608
+ } catch {
9609
+ return null;
9610
+ }
9611
+ }
9612
+ async detectDefaultBaseBranch() {
9613
+ try {
9614
+ const { stdout } = await execFileAsync2(
9615
+ "gh",
9616
+ ["repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"],
9617
+ { cwd: this.opts.cwd }
9618
+ );
9619
+ const name = stdout.trim();
9620
+ if (name) return name;
9621
+ } catch {
9622
+ }
9623
+ try {
9624
+ const { stdout } = await execFileAsync2(
9625
+ "git",
9626
+ ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
9627
+ { cwd: this.opts.cwd }
9628
+ );
9629
+ const ref = stdout.trim();
9630
+ if (ref.startsWith("origin/")) return ref.slice("origin/".length);
9631
+ } catch {
9632
+ }
9633
+ return "main";
9634
+ }
9635
+ // ─── PR body composition ────────────────────────────────────────
9636
+ buildPrTitle(prd, passed, total) {
9637
+ const project = prd?.project ?? "baro run";
9638
+ if (passed === total) {
9639
+ return `${project} (${total} ${total === 1 ? "story" : "stories"})`;
9640
+ }
9641
+ return `${project} (${passed}/${total} stories)`;
9642
+ }
9643
+ buildPrBody(args) {
9644
+ const { prd, run, orderedStories, passed, failed, commits, filesStats } = args;
9645
+ const lines = [];
9646
+ lines.push(
9647
+ `> Opened by [baro](https://baro.rs) \u2014 Mozaik-orchestrated parallel coding agents.`
9648
+ );
9649
+ lines.push("");
9650
+ if (prd?.description) {
9651
+ lines.push("## Goal");
9652
+ lines.push("");
9653
+ lines.push(prd.description.trim());
9654
+ lines.push("");
9655
+ }
9656
+ if (this.levels.size > 0) {
9657
+ lines.push("## Plan");
9658
+ lines.push("");
9659
+ lines.push("```");
9660
+ const ordinals = [...this.levels.keys()].sort((a, b) => a - b);
9661
+ for (const ord of ordinals) {
9662
+ const ids = this.levels.get(ord) ?? [];
9663
+ lines.push(`Level ${ord} \u2500\u2500\u2500 ${ids.join(", ")}`);
9664
+ }
9665
+ lines.push("```");
9666
+ lines.push("");
9667
+ }
9668
+ lines.push("## Stories");
9669
+ lines.push("");
9670
+ lines.push("| # | Story | Status | Duration | Commit |");
9671
+ lines.push("|---|-------|--------|----------|--------|");
9672
+ for (const s of orderedStories) {
9673
+ const title = (s.title || s.id).replace(/\|/g, "\\|");
9674
+ const status = s.success === true ? "\u2713" : s.success === false ? "\u2717" : "\u2014";
9675
+ const dur = s.durationSecs != null ? formatDuration(s.durationSecs) : "\u2014";
9676
+ const commit = matchCommit(s, commits);
9677
+ lines.push(
9678
+ `| ${s.id} | ${title} | ${status} | ${dur} | ${commit ? "`" + commit.slice(0, 7) + "`" : "\u2014"} |`
9679
+ );
9680
+ }
9681
+ lines.push("");
9682
+ lines.push("## Diff stats");
9683
+ lines.push("");
9684
+ lines.push(`- **Files created**: ${filesStats.created}`);
9685
+ lines.push(`- **Files modified**: ${filesStats.modified}`);
9686
+ lines.push(`- **Total commits**: ${commits.length}`);
9687
+ lines.push("");
9688
+ lines.push("## Run summary");
9689
+ lines.push("");
9690
+ const wall = formatDuration(args.totalSecs);
9691
+ const seq = formatDuration(args.sequentialSecs);
9692
+ const speedup = args.totalSecs > 0 ? (args.sequentialSecs / args.totalSecs).toFixed(2) + "\xD7" : "\u2014";
9693
+ lines.push(`- **Wall time**: ${wall}`);
9694
+ lines.push(`- **Sequential time**: ${seq}`);
9695
+ lines.push(`- **Parallel speedup**: ${speedup}`);
9696
+ lines.push(`- **Stories passed**: ${passed.length}/${orderedStories.length}`);
9697
+ lines.push(`- **Stories failed**: ${failed.length}`);
9698
+ lines.push(`- **Total story attempts**: ${run.totalAttempts}`);
9699
+ if (run.abortReason) {
9700
+ lines.push(`- **Abort reason**: ${run.abortReason}`);
9701
+ }
9702
+ lines.push("");
9703
+ lines.push("---");
9704
+ lines.push("");
9705
+ lines.push("\u{1F916} Plan. Parallelize. Review. Ship. \u2014 opened by baro");
9706
+ return lines.join("\n");
9707
+ }
9708
+ async hasGhBinary() {
9709
+ try {
9710
+ await execFileAsync2("gh", ["--version"], { cwd: this.opts.cwd });
9711
+ return true;
9712
+ } catch {
9713
+ return false;
9714
+ }
9715
+ }
9716
+ async openPr(args) {
9717
+ try {
9718
+ const { stdout } = await execFileAsync2(
9719
+ "gh",
9720
+ [
9721
+ "pr",
9722
+ "create",
9723
+ "--base",
9724
+ args.baseBranch,
9725
+ "--head",
9726
+ args.branch,
9727
+ "--title",
9728
+ args.title,
9729
+ "--body",
9730
+ args.body
9731
+ ],
9732
+ { cwd: this.opts.cwd }
9733
+ );
9734
+ const url = stdout.trim().split("\n").pop() ?? "";
9735
+ return url || null;
9736
+ } catch (e) {
9737
+ const stderr = e?.stderr ?? "";
9738
+ const existing = stderr.match(/https:\/\/github\.com\/\S+\/pull\/\d+/)?.[0];
9739
+ if (existing) {
9740
+ this.log(`[finalizer] PR already exists: ${existing}`);
9741
+ return existing;
9742
+ }
9743
+ this.log(
9744
+ `[finalizer] gh pr create failed: ${stderr.split("\n")[0]?.trim() || e.message}`
9745
+ );
9746
+ return null;
9747
+ }
9748
+ }
9749
+ };
9750
+ function formatDuration(secs) {
9751
+ if (secs < 60) return `${Math.round(secs)}s`;
9752
+ const m = Math.floor(secs / 60);
9753
+ const s = Math.round(secs % 60);
9754
+ return `${m}:${s.toString().padStart(2, "0")}`;
9755
+ }
9756
+ function matchCommit(story, commits) {
9757
+ if (commits.length === 0) return null;
9758
+ const idPattern = new RegExp(`\\b${story.id}\\b`, "i");
9759
+ for (const c of commits) {
9760
+ if (idPattern.test(c.subject)) return c.sha;
9761
+ }
9762
+ if (!story.title) return null;
9763
+ const keywords = story.title.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 4);
9764
+ if (keywords.length === 0) return null;
9765
+ let bestSha = null;
9766
+ let bestScore = 0;
9767
+ for (const c of commits) {
9768
+ const subj = c.subject.toLowerCase();
9769
+ const hits = keywords.reduce((n, k) => subj.includes(k) ? n + 1 : n, 0);
9770
+ if (hits > bestScore) {
9771
+ bestScore = hits;
9772
+ bestSha = c.sha;
9773
+ }
9774
+ }
9775
+ return bestScore >= 2 ? bestSha : null;
9776
+ }
9777
+
9308
9778
  // ../baro-orchestrator/src/participants/librarian.ts
9309
9779
  var EXPLORATION_TOOLS = /* @__PURE__ */ new Set([
9310
9780
  "Read",
@@ -9651,9 +10121,9 @@ var StoryFactory = class extends Participant {
9651
10121
  };
9652
10122
 
9653
10123
  // ../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);
10124
+ import { execFile as execFile4 } from "child_process";
10125
+ import { promisify as promisify4 } from "util";
10126
+ var execFileAsync3 = promisify4(execFile4);
9657
10127
  var SURGEON_SYSTEM_PROMPT = `You are the Surgeon \u2014 an autonomous planner that adapts a software-project
9658
10128
  DAG when stories fail. Given:
9659
10129
  1. A snapshot of the current PRD (project, story list with dependencies +
@@ -9765,7 +10235,7 @@ var Surgeon = class extends Participant {
9765
10235
  const snap = this.opts.snapshot();
9766
10236
  const prompt = buildSurgeonPrompt(snap, failure);
9767
10237
  try {
9768
- const { stdout } = await execFileAsync2(
10238
+ const { stdout } = await execFileAsync3(
9769
10239
  this.opts.claudeBin,
9770
10240
  [
9771
10241
  "--print",
@@ -9926,6 +10396,15 @@ async function orchestrate(config) {
9926
10396
  });
9927
10397
  critic.join(env);
9928
10398
  }
10399
+ const finalizer = useGit ? new Finalizer({
10400
+ cwd: config.cwd,
10401
+ prdPath: config.prdPath,
10402
+ onLog: (line) => emitTui && emit({ type: "story_log", id: "_finalizer", line })
10403
+ }) : null;
10404
+ if (finalizer) {
10405
+ finalizer.setEnvironment(env);
10406
+ finalizer.join(env);
10407
+ }
9929
10408
  const conductor = new Conductor({
9930
10409
  prdPath: config.prdPath,
9931
10410
  cwd: config.cwd,
@@ -10006,6 +10485,7 @@ async function orchestrate(config) {
10006
10485
  const summary = await conductor.done;
10007
10486
  if (critic) await critic.idle();
10008
10487
  if (surgeon) await surgeon.idle();
10488
+ if (finalizer) await finalizer.complete();
10009
10489
  let filesCreated = 0;
10010
10490
  let filesModified = 0;
10011
10491
  if (useGit && baseSha) {
@@ -10081,6 +10561,14 @@ var BaroEventForwarder = class extends Participant {
10081
10561
  this.handleCritique(item);
10082
10562
  return;
10083
10563
  }
10564
+ if (item instanceof FinalizeStartedItem) {
10565
+ emit({ type: "finalize_start" });
10566
+ return;
10567
+ }
10568
+ if (item instanceof PrCreatedItem) {
10569
+ emit({ type: "finalize_complete", pr_url: item.url });
10570
+ return;
10571
+ }
10084
10572
  }
10085
10573
  handleCoordination(item) {
10086
10574
  emit({