baro-ai 0.51.3 → 0.51.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
@@ -13750,8 +13750,8 @@ var require_jwa = __commonJS({
13750
13750
  }
13751
13751
  function typeError(template) {
13752
13752
  var args = [].slice.call(arguments, 1);
13753
- var errMsg = util.format.bind(util, template).apply(null, args);
13754
- return new TypeError(errMsg);
13753
+ var errMsg2 = util.format.bind(util, template).apply(null, args);
13754
+ return new TypeError(errMsg2);
13755
13755
  }
13756
13756
  function bufferOrString(obj) {
13757
13757
  return Buffer4.isBuffer(obj) || typeof obj === "string";
@@ -22243,9 +22243,9 @@ var require_websocket_server = __commonJS({
22243
22243
 
22244
22244
  // ../baro-memory/dist/vectra-store.js
22245
22245
  import { LocalIndex } from "vectra";
22246
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2, renameSync, rmSync, readdirSync, statSync as statSync2, lstatSync } from "fs";
22247
- import { join as join2 } from "path";
22248
- import { tmpdir, homedir } from "os";
22246
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync3, renameSync, rmSync as rmSync2, readdirSync, statSync as statSync2, lstatSync } from "fs";
22247
+ import { join as join3 } from "path";
22248
+ import { tmpdir as tmpdir2, homedir } from "os";
22249
22249
  async function createMemoryStore(config) {
22250
22250
  const cfg = { ...DEFAULTS, ...config };
22251
22251
  if (cfg.defaultMinSimilarity < 0 || cfg.defaultMinSimilarity > 1) {
@@ -22257,23 +22257,23 @@ async function createMemoryStore(config) {
22257
22257
  if (cfg.disabled) {
22258
22258
  return new NoOpMemoryStore();
22259
22259
  }
22260
- const sessionPath = cfg.sessionPath || join2(tmpdir(), `baro-memory-${process.pid}-${Date.now()}`);
22260
+ const sessionPath = cfg.sessionPath || join3(tmpdir2(), `baro-memory-${process.pid}-${Date.now()}`);
22261
22261
  validateSessionPath(sessionPath);
22262
22262
  mkdirSync2(sessionPath, { recursive: true });
22263
- const indexPath = join2(sessionPath, "index");
22263
+ const indexPath = join3(sessionPath, "index");
22264
22264
  mkdirSync2(indexPath, { recursive: true });
22265
22265
  const index = new LocalIndex(indexPath);
22266
22266
  if (!await index.isIndexCreated()) {
22267
22267
  await index.createIndex({ version: 1 });
22268
22268
  }
22269
22269
  const transformers = await import("@xenova/transformers");
22270
- transformers.env.cacheDir = process.env.TRANSFORMERS_CACHE || join2(homedir(), ".baro", "models");
22270
+ transformers.env.cacheDir = process.env.TRANSFORMERS_CACHE || join3(homedir(), ".baro", "models");
22271
22271
  const { pipeline: pipeline2 } = transformers;
22272
22272
  const extractor = await pipeline2("feature-extraction", cfg.embeddingModel);
22273
22273
  return new VectraMemoryStore(index, extractor, sessionPath, cfg);
22274
22274
  }
22275
22275
  function validateSessionPath(sessionPath) {
22276
- const resolved = join2(sessionPath);
22276
+ const resolved = join3(sessionPath);
22277
22277
  if (resolved.includes("..")) {
22278
22278
  throw new Error(`Invalid session path (contains ..): ${resolved}`);
22279
22279
  }
@@ -22284,26 +22284,26 @@ function validateSessionPath(sessionPath) {
22284
22284
  }
22285
22285
  }
22286
22286
  const normalizedPath = resolved.toLowerCase();
22287
- const isSafe = ALLOWED_SESSION_PARENTS.some((p) => normalizedPath.includes(p)) || normalizedPath.startsWith(tmpdir().toLowerCase());
22287
+ const isSafe = ALLOWED_SESSION_PARENTS.some((p) => normalizedPath.includes(p)) || normalizedPath.startsWith(tmpdir2().toLowerCase());
22288
22288
  if (!isSafe) {
22289
22289
  throw new Error(`Invalid session path (must be under ~/.baro, tmpdir, or contain 'baro-memory'): ${resolved}`);
22290
22290
  }
22291
22291
  }
22292
22292
  function pruneOldSessions(sessionsDir) {
22293
22293
  try {
22294
- if (!existsSync2(sessionsDir))
22294
+ if (!existsSync3(sessionsDir))
22295
22295
  return;
22296
22296
  const now = Date.now();
22297
22297
  for (const entry of readdirSync(sessionsDir)) {
22298
22298
  if (!entry.startsWith("run-"))
22299
22299
  continue;
22300
- const entryPath = join2(sessionsDir, entry);
22300
+ const entryPath = join3(sessionsDir, entry);
22301
22301
  try {
22302
22302
  const stat2 = lstatSync(entryPath);
22303
22303
  if (stat2.isSymbolicLink())
22304
22304
  continue;
22305
22305
  if (stat2.isDirectory() && now - stat2.mtimeMs > SESSION_TTL_MS) {
22306
- rmSync(entryPath, { recursive: true, force: true });
22306
+ rmSync2(entryPath, { recursive: true, force: true });
22307
22307
  }
22308
22308
  } catch {
22309
22309
  }
@@ -22354,10 +22354,10 @@ var init_vectra_store = __esm({
22354
22354
  this.index = index;
22355
22355
  this.extractor = extractor;
22356
22356
  this.sessionPath = sessionPath;
22357
- this.indexPath = join2(sessionPath, "index");
22358
- this.indexFilePath = join2(this.indexPath, "index.json");
22359
- this.cachePath = join2(sessionPath, "cache.json");
22360
- this.lockPath = join2(sessionPath, "cache.lock");
22357
+ this.indexPath = join3(sessionPath, "index");
22358
+ this.indexFilePath = join3(this.indexPath, "index.json");
22359
+ this.cachePath = join3(sessionPath, "cache.json");
22360
+ this.lockPath = join3(sessionPath, "cache.lock");
22361
22361
  this.config = config;
22362
22362
  }
22363
22363
  /**
@@ -22381,7 +22381,7 @@ var init_vectra_store = __esm({
22381
22381
  */
22382
22382
  refreshIndexIfChanged() {
22383
22383
  try {
22384
- if (!existsSync2(this.indexFilePath))
22384
+ if (!existsSync3(this.indexFilePath))
22385
22385
  return;
22386
22386
  const mtimeMs = statSync2(this.indexFilePath).mtimeMs;
22387
22387
  if (mtimeMs === this.lastIndexMtimeMs)
@@ -22556,7 +22556,7 @@ ${result.content}
22556
22556
  }
22557
22557
  async close() {
22558
22558
  try {
22559
- rmSync(this.lockPath, { force: true });
22559
+ rmSync2(this.lockPath, { force: true });
22560
22560
  } catch {
22561
22561
  }
22562
22562
  }
@@ -22605,7 +22605,7 @@ ${result.content}
22605
22605
  */
22606
22606
  loadCache() {
22607
22607
  try {
22608
- if (existsSync2(this.cachePath)) {
22608
+ if (existsSync3(this.cachePath)) {
22609
22609
  const raw = readFileSync3(this.cachePath, "utf-8");
22610
22610
  if (raw.trim()) {
22611
22611
  return JSON.parse(raw);
@@ -22633,8 +22633,8 @@ ${result.content}
22633
22633
  }
22634
22634
  } finally {
22635
22635
  try {
22636
- if (existsSync2(this.lockPath))
22637
- rmSync(this.lockPath);
22636
+ if (existsSync3(this.lockPath))
22637
+ rmSync2(this.lockPath);
22638
22638
  } catch {
22639
22639
  }
22640
22640
  }
@@ -22692,12 +22692,12 @@ var init_dist2 = __esm({
22692
22692
  });
22693
22693
 
22694
22694
  // ../baro-orchestrator/scripts/cli.ts
22695
- import { existsSync as existsSync5 } from "fs";
22695
+ import { existsSync as existsSync6 } from "fs";
22696
22696
  import { resolve as resolve3 } from "path";
22697
22697
 
22698
22698
  // ../baro-orchestrator/src/orchestrate.ts
22699
22699
  import { mkdirSync as mkdirSync5 } from "fs";
22700
- import { dirname as dirname3, join as join5 } from "path";
22700
+ import { dirname as dirname3, join as join6 } from "path";
22701
22701
 
22702
22702
  // ../../node_modules/openai/internal/tslib.mjs
22703
22703
  function __classPrivateFieldSet(receiver, state, value, kind, f3) {
@@ -39880,7 +39880,7 @@ async function safePullRebase(cwd, onLog, gate) {
39880
39880
  } catch {
39881
39881
  }
39882
39882
  try {
39883
- await exec("git", ["pull", "--rebase", "origin", branch], { cwd });
39883
+ await exec("git", ["pull", "--rebase=merges", "origin", branch], { cwd });
39884
39884
  onLog?.("[git] pull ok");
39885
39885
  } catch {
39886
39886
  onLog?.("[git] pull conflict, continuing without pull");
@@ -39923,7 +39923,7 @@ async function gitPushWithRetry(gate, options) {
39923
39923
  `[git] push rejected (attempt ${attempt}/${max}), pulling and retrying...`
39924
39924
  );
39925
39925
  try {
39926
- await exec("git", ["pull", "--rebase", "origin", branch], {
39926
+ await exec("git", ["pull", "--rebase=merges", "origin", branch], {
39927
39927
  cwd: options.cwd
39928
39928
  });
39929
39929
  } catch {
@@ -39991,6 +39991,316 @@ function extractStderr(e2) {
39991
39991
  return e2 instanceof Error ? e2.message : String(e2);
39992
39992
  }
39993
39993
 
39994
+ // ../baro-orchestrator/src/worktree.ts
39995
+ import { execFile as execFile2 } from "child_process";
39996
+ import { existsSync, rmSync, symlinkSync } from "fs";
39997
+ import { tmpdir } from "os";
39998
+ import { join } from "path";
39999
+ import { promisify as promisify3 } from "util";
40000
+ var exec2 = promisify3(execFile2);
40001
+ var LINKED_DEP_DIRS = ["node_modules", ".venv", "vendor"];
40002
+ var WorktreeManager = class {
40003
+ constructor(repoRoot, gate, runId, opts = {}) {
40004
+ this.repoRoot = repoRoot;
40005
+ this.gate = gate;
40006
+ this.runId = runId;
40007
+ this.baseDir = join(tmpdir(), "baro-worktrees", runId);
40008
+ this.linkDepDirs = opts.linkDepDirs ?? true;
40009
+ this.log = opts.onLog ?? ((line) => process.stderr.write(`[worktree] ${line}
40010
+ `));
40011
+ }
40012
+ paths = /* @__PURE__ */ new Map();
40013
+ /** Stories whose merge-back failed: their branch is kept for recovery. */
40014
+ preserved = /* @__PURE__ */ new Set();
40015
+ baseDir;
40016
+ linkDepDirs;
40017
+ log;
40018
+ branchOf(storyId) {
40019
+ return `baro-wt/${this.runId}/${sanitize(storyId)}`;
40020
+ }
40021
+ pathOf(storyId) {
40022
+ return join(this.baseDir, sanitize(storyId));
40023
+ }
40024
+ /**
40025
+ * Create an isolated worktree for a story, branched off the current
40026
+ * run-branch HEAD. Returns the worktree path, or null on any failure so
40027
+ * the caller can fall back to the shared repo cwd (preserving the old
40028
+ * behavior rather than failing the story).
40029
+ */
40030
+ async create(storyId) {
40031
+ const release = await this.gate.acquire();
40032
+ try {
40033
+ const branch = this.branchOf(storyId);
40034
+ const path6 = this.pathOf(storyId);
40035
+ await this.removeWorktreeQuiet(path6);
40036
+ await this.deleteBranchQuiet(branch);
40037
+ await exec2(
40038
+ "git",
40039
+ ["worktree", "add", "-b", branch, path6, "HEAD"],
40040
+ { cwd: this.repoRoot }
40041
+ );
40042
+ this.paths.set(storyId, path6);
40043
+ if (this.linkDepDirs) this.symlinkDepDirs(path6);
40044
+ this.log(`created ${branch} at ${path6}`);
40045
+ return path6;
40046
+ } catch (e2) {
40047
+ this.log(
40048
+ `could not create worktree for ${storyId} (${errMsg(e2)}); falling back to shared tree`
40049
+ );
40050
+ return null;
40051
+ } finally {
40052
+ release();
40053
+ }
40054
+ }
40055
+ /**
40056
+ * Merge a passed story's branch onto the run branch. Returns true if a
40057
+ * worktree merge happened, false if the story had no worktree (create()
40058
+ * fell back to the shared tree). On any merge failure, retries once with
40059
+ * `-X theirs` (the merging story wins). Throws — leaving the run branch
40060
+ * clean (`merge --abort`) — only when even that can't resolve it; the
40061
+ * caller must then preserve the branch rather than discard the work.
40062
+ */
40063
+ async mergeBack(storyId) {
40064
+ const path6 = this.paths.get(storyId);
40065
+ if (!path6) return false;
40066
+ const branch = this.branchOf(storyId);
40067
+ const release = await this.gate.acquire();
40068
+ try {
40069
+ await this.autoCommitLeftovers(storyId, path6);
40070
+ const msg = `baro: merge story ${storyId}`;
40071
+ try {
40072
+ await exec2("git", ["merge", "--no-ff", "-m", msg, branch], {
40073
+ cwd: this.repoRoot
40074
+ });
40075
+ return true;
40076
+ } catch {
40077
+ const conflicts = await this.conflictedPaths();
40078
+ await this.abortMerge(storyId);
40079
+ this.log(
40080
+ `WARNING: story ${storyId} conflicts with already-merged work` + (conflicts.length ? ` on [${conflicts.join(", ")}]` : "") + `; auto-resolving with -X theirs (this story wins)`
40081
+ );
40082
+ try {
40083
+ await exec2(
40084
+ "git",
40085
+ ["merge", "--no-ff", "-X", "theirs", "-m", msg, branch],
40086
+ { cwd: this.repoRoot }
40087
+ );
40088
+ return true;
40089
+ } catch (e2) {
40090
+ await this.abortMerge(storyId);
40091
+ this.preserved.add(storyId);
40092
+ throw new Error(
40093
+ `could not merge story ${storyId} even with -X theirs: ${errMsg(e2)}`
40094
+ );
40095
+ }
40096
+ }
40097
+ } finally {
40098
+ release();
40099
+ }
40100
+ }
40101
+ /**
40102
+ * Abort an in-progress merge, logging if the abort itself fails. A
40103
+ * lingering MERGE_HEAD (e.g. a held index.lock) would otherwise make the
40104
+ * NEXT story's merge fail and be misdiagnosed against the wrong story.
40105
+ */
40106
+ async abortMerge(storyId) {
40107
+ try {
40108
+ await exec2("git", ["merge", "--abort"], { cwd: this.repoRoot });
40109
+ } catch (e2) {
40110
+ this.log(
40111
+ `WARNING: 'git merge --abort' failed after story ${storyId} (${errMsg(e2)}); run branch may have a lingering MERGE_HEAD`
40112
+ );
40113
+ }
40114
+ }
40115
+ /** Remove a story's worktree + branch (after merge-back, or on failure). */
40116
+ async cleanup(storyId) {
40117
+ const path6 = this.paths.get(storyId);
40118
+ const branch = this.branchOf(storyId);
40119
+ const release = await this.gate.acquire();
40120
+ try {
40121
+ if (path6) {
40122
+ await this.removeWorktreeQuiet(path6);
40123
+ this.paths.delete(storyId);
40124
+ }
40125
+ await this.deleteBranchQuiet(branch);
40126
+ } finally {
40127
+ release();
40128
+ }
40129
+ }
40130
+ /**
40131
+ * Remove every worktree this manager created, plus its temp dir. Branches
40132
+ * are deleted too — EXCEPT those marked preserved (an unresolvable
40133
+ * merge-back): their worktree dir is freed but the branch ref is kept so
40134
+ * the commits stay recoverable after the run.
40135
+ */
40136
+ async cleanupAll() {
40137
+ const release = await this.gate.acquire();
40138
+ try {
40139
+ for (const [storyId, path6] of this.paths) {
40140
+ await this.removeWorktreeQuiet(path6);
40141
+ if (this.preserved.has(storyId)) {
40142
+ this.log(
40143
+ `kept branch ${this.branchOf(storyId)} for recovery (merge-back failed); inspect with: git log ${this.branchOf(storyId)}`
40144
+ );
40145
+ } else {
40146
+ await this.deleteBranchQuiet(this.branchOf(storyId));
40147
+ }
40148
+ }
40149
+ this.paths.clear();
40150
+ await execQuiet("git", ["worktree", "prune"], this.repoRoot);
40151
+ rmSyncQuiet(this.baseDir);
40152
+ } finally {
40153
+ release();
40154
+ }
40155
+ }
40156
+ /**
40157
+ * Reclaim worktree admin entries whose dirs are already gone
40158
+ * (`git worktree prune`) and delete any branches under THIS run id
40159
+ * (defensive re-entrancy guard — ids are unique per process, so this
40160
+ * does not touch a concurrent run's worktrees).
40161
+ */
40162
+ async cleanupStaleOnStart() {
40163
+ const release = await this.gate.acquire();
40164
+ try {
40165
+ await execQuiet("git", ["worktree", "prune"], this.repoRoot);
40166
+ const prefix = `baro-wt/${this.runId}/`;
40167
+ let branches = [];
40168
+ try {
40169
+ const { stdout } = await exec2(
40170
+ "git",
40171
+ ["branch", "--list", `${prefix}*`, "--format=%(refname:short)"],
40172
+ { cwd: this.repoRoot }
40173
+ );
40174
+ branches = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
40175
+ } catch {
40176
+ }
40177
+ for (const branch of branches) {
40178
+ await this.deleteBranchQuiet(branch);
40179
+ }
40180
+ } finally {
40181
+ release();
40182
+ }
40183
+ }
40184
+ // ── internals ────────────────────────────────────────────────────
40185
+ symlinkDepDirs(worktreePath) {
40186
+ for (const dir of LINKED_DEP_DIRS) {
40187
+ const src = join(this.repoRoot, dir);
40188
+ const dest = join(worktreePath, dir);
40189
+ if (!existsSync(src) || existsSync(dest)) continue;
40190
+ try {
40191
+ symlinkSync(src, dest, "dir");
40192
+ } catch (e2) {
40193
+ this.log(`could not symlink ${dir} into worktree (${errMsg(e2)})`);
40194
+ }
40195
+ }
40196
+ }
40197
+ /**
40198
+ * Commit work the agent edited but didn't commit, so it isn't lost on
40199
+ * cleanup (passing is signalled by the agent, not by a commit existing).
40200
+ * Never commits the symlinked dep dirs, and surfaces a warning rather than
40201
+ * silently dropping work if any git step fails.
40202
+ */
40203
+ async autoCommitLeftovers(storyId, worktreePath) {
40204
+ let dirty = false;
40205
+ try {
40206
+ const { stdout } = await exec2("git", ["status", "--porcelain"], {
40207
+ cwd: worktreePath
40208
+ });
40209
+ dirty = stdout.trim().length > 0;
40210
+ } catch {
40211
+ return;
40212
+ }
40213
+ if (!dirty) return;
40214
+ try {
40215
+ await exec2("git", ["add", "-A"], { cwd: worktreePath });
40216
+ } catch (e2) {
40217
+ this.log(
40218
+ `WARNING: failed to stage story ${storyId}'s leftover work (${errMsg(e2)}); it will not be included in the merge`
40219
+ );
40220
+ return;
40221
+ }
40222
+ await execQuiet("git", ["reset", "-q", "--", ...LINKED_DEP_DIRS], worktreePath);
40223
+ let staged = [];
40224
+ let diffFailed = false;
40225
+ try {
40226
+ const { stdout } = await exec2("git", ["diff", "--cached", "--name-only"], {
40227
+ cwd: worktreePath
40228
+ });
40229
+ staged = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
40230
+ } catch (e2) {
40231
+ diffFailed = true;
40232
+ this.log(
40233
+ `WARNING: could not inspect staged changes for story ${storyId} (${errMsg(e2)}); committing whatever is staged`
40234
+ );
40235
+ }
40236
+ const depStaged = staged.filter(
40237
+ (p) => LINKED_DEP_DIRS.some((d) => p === d || p.startsWith(`${d}/`))
40238
+ );
40239
+ if (depStaged.length > 0) {
40240
+ this.log(
40241
+ `WARNING: could not keep dep dirs [${depStaged.join(", ")}] out of story ${storyId}'s auto-commit; skipping it to avoid committing symlinks`
40242
+ );
40243
+ await execQuiet("git", ["reset", "-q"], worktreePath);
40244
+ return;
40245
+ }
40246
+ if (!diffFailed && staged.length === 0) return;
40247
+ this.log(
40248
+ `WARNING: story ${storyId} passed with uncommitted changes; auto-committing ${diffFailed ? "" : `${staged.length} path(s) `}before merge`
40249
+ );
40250
+ try {
40251
+ await exec2(
40252
+ "git",
40253
+ ["commit", "-m", `baro: auto-commit uncommitted work for story ${storyId}`],
40254
+ { cwd: worktreePath }
40255
+ );
40256
+ } catch (e2) {
40257
+ this.log(
40258
+ `WARNING: failed to auto-commit story ${storyId}'s leftover work (${errMsg(e2)}); it will not be included in the merge`
40259
+ );
40260
+ }
40261
+ }
40262
+ async conflictedPaths() {
40263
+ try {
40264
+ const { stdout } = await exec2(
40265
+ "git",
40266
+ ["diff", "--name-only", "--diff-filter=U"],
40267
+ { cwd: this.repoRoot }
40268
+ );
40269
+ return stdout.split("\n").map((l) => l.trim()).filter(Boolean);
40270
+ } catch {
40271
+ return [];
40272
+ }
40273
+ }
40274
+ async removeWorktreeQuiet(path6) {
40275
+ await execQuiet("git", ["worktree", "remove", "--force", path6], this.repoRoot);
40276
+ if (existsSync(path6)) {
40277
+ rmSyncQuiet(path6);
40278
+ await execQuiet("git", ["worktree", "prune"], this.repoRoot);
40279
+ }
40280
+ }
40281
+ async deleteBranchQuiet(branch) {
40282
+ await execQuiet("git", ["branch", "-D", branch], this.repoRoot);
40283
+ }
40284
+ };
40285
+ function sanitize(storyId) {
40286
+ return storyId.replace(/[^A-Za-z0-9._-]/g, "_");
40287
+ }
40288
+ async function execQuiet(cmd, args, cwd) {
40289
+ try {
40290
+ await exec2(cmd, args, { cwd });
40291
+ } catch {
40292
+ }
40293
+ }
40294
+ function rmSyncQuiet(path6) {
40295
+ try {
40296
+ rmSync(path6, { recursive: true, force: true });
40297
+ } catch {
40298
+ }
40299
+ }
40300
+ function errMsg(e2) {
40301
+ return e2?.message ?? String(e2);
40302
+ }
40303
+
39994
40304
  // ../baro-orchestrator/src/dag.ts
39995
40305
  function buildDag(stories, options = {}) {
39996
40306
  const onlyIncomplete = options.onlyIncomplete ?? false;
@@ -40159,8 +40469,8 @@ var Auditor = class extends BaseObserver {
40159
40469
  };
40160
40470
 
40161
40471
  // ../baro-orchestrator/src/participants/conductor.ts
40162
- import { existsSync, readFileSync as readFileSync2 } from "fs";
40163
- import { join } from "path";
40472
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
40473
+ import { join as join2 } from "path";
40164
40474
 
40165
40475
  // ../baro-orchestrator/src/prd.ts
40166
40476
  import { readFileSync, writeFileSync } from "fs";
@@ -40345,6 +40655,14 @@ var Conductor = class extends BaseObserver {
40345
40655
  /** Resolved when the run terminates, exposed for callers that need it. */
40346
40656
  done;
40347
40657
  resolveDone;
40658
+ /**
40659
+ * Serializes event handling. Mozaik delivers events without awaiting the
40660
+ * handler, so two StoryResults settling together could interleave — one
40661
+ * spawning the next DAG level while another's `await onStoryPassed`
40662
+ * (worktree merge-back, #50) is still in flight, leaving the dependent
40663
+ * level without the merged work. Chaining handlers keeps them sequential.
40664
+ */
40665
+ handleChain = Promise.resolve();
40348
40666
  constructor(opts) {
40349
40667
  super();
40350
40668
  this.opts = {
@@ -40381,7 +40699,13 @@ var Conductor = class extends BaseObserver {
40381
40699
  async onExternalEvent(_source, event) {
40382
40700
  await this.handle(event);
40383
40701
  }
40384
- async handle(event) {
40702
+ handle(event) {
40703
+ const run = this.handleChain.then(() => this.handleEvent(event));
40704
+ this.handleChain = run.catch(() => {
40705
+ });
40706
+ return run;
40707
+ }
40708
+ async handleEvent(event) {
40385
40709
  if (RunStartRequest.is(event)) {
40386
40710
  await this.handleRunStart();
40387
40711
  return;
@@ -40521,6 +40845,20 @@ var Conductor = class extends BaseObserver {
40521
40845
  } else {
40522
40846
  this.currentLevel.failed.push(item.storyId);
40523
40847
  this.addGlobalFailed(item.storyId);
40848
+ if (this.opts.onStoryFailed) {
40849
+ try {
40850
+ await this.opts.onStoryFailed(item.storyId);
40851
+ } catch (e2) {
40852
+ this.emit(
40853
+ ConductorState.create({
40854
+ phase: "running_level",
40855
+ detail: `onStoryFailed hook for ${item.storyId} failed: ${e2?.message ?? String(e2)}`,
40856
+ currentLevel: this.currentLevel.ordinal,
40857
+ totalLevels: this.currentLevel.totalLevelsHint
40858
+ })
40859
+ );
40860
+ }
40861
+ }
40524
40862
  }
40525
40863
  await this.fillSpawnSlots();
40526
40864
  if (this.currentLevel.pending.size === 0) {
@@ -40790,9 +41128,9 @@ ${prompt}`;
40790
41128
  return true;
40791
41129
  }
40792
41130
  resolvePrompt(story) {
40793
- const candidatePath = this.opts.promptTemplatePath ?? join(this.opts.cwd, "prompt.md");
41131
+ const candidatePath = this.opts.promptTemplatePath ?? join2(this.opts.cwd, "prompt.md");
40794
41132
  let prompt;
40795
- if (existsSync(candidatePath)) {
41133
+ if (existsSync2(candidatePath)) {
40796
41134
  const tpl = readFileSyncSafe(candidatePath);
40797
41135
  prompt = tpl ? applyTemplate(tpl, story) : buildDefaultStoryPrompt(story);
40798
41136
  } else {
@@ -40871,9 +41209,9 @@ function applyTemplate(tpl, story) {
40871
41209
  }
40872
41210
 
40873
41211
  // ../baro-orchestrator/src/participants/critic.ts
40874
- import { execFile as execFile2 } from "child_process";
40875
- import { promisify as promisify3 } from "util";
40876
- var execFileAsync = promisify3(execFile2);
41212
+ import { execFile as execFile3 } from "child_process";
41213
+ import { promisify as promisify4 } from "util";
41214
+ var execFileAsync = promisify4(execFile3);
40877
41215
  var VERDICT_SYSTEM_PROMPT = `You are a strict acceptance-criteria evaluator. You will receive:
40878
41216
  1. A list of acceptance criteria that must ALL be satisfied.
40879
41217
  2. The output text produced by an agent.
@@ -42009,9 +42347,9 @@ ${userPrompt}`;
42009
42347
  };
42010
42348
 
42011
42349
  // ../baro-orchestrator/src/participants/finalizer.ts
42012
- import { execFile as execFile3 } from "child_process";
42013
- import { promisify as promisify4 } from "util";
42014
- var execFileAsync2 = promisify4(execFile3);
42350
+ import { execFile as execFile4 } from "child_process";
42351
+ import { promisify as promisify5 } from "util";
42352
+ var execFileAsync2 = promisify5(execFile4);
42015
42353
  var Finalizer = class extends BaseObserver {
42016
42354
  opts;
42017
42355
  envRef = null;
@@ -42923,10 +43261,10 @@ function tokenizeHints(prompt) {
42923
43261
 
42924
43262
  // ../baro-orchestrator/src/participants/memory-librarian.ts
42925
43263
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync3 } from "fs";
42926
- import { join as join3 } from "path";
43264
+ import { join as join4 } from "path";
42927
43265
  var DEBUG = process.env.BARO_DEBUG?.includes("memory") ?? false;
42928
- var LOG_DIR = join3(process.env.HOME || "/tmp", ".baro", "runs");
42929
- var LOG_FILE = join3(LOG_DIR, `memory-${Date.now()}.log`);
43266
+ var LOG_DIR = join4(process.env.HOME || "/tmp", ".baro", "runs");
43267
+ var LOG_FILE = join4(LOG_DIR, `memory-${Date.now()}.log`);
42930
43268
  try {
42931
43269
  mkdirSync3(LOG_DIR, { recursive: true });
42932
43270
  } catch {
@@ -46831,6 +47169,8 @@ var StoryFactory = class extends BaseObserver {
46831
47169
  // narrows to vanilla AgenticEnvironment.
46832
47170
  envRef = null;
46833
47171
  active = /* @__PURE__ */ new Map();
47172
+ /** Story ids whose spawn is in progress (closes the await-create window). */
47173
+ spawning = /* @__PURE__ */ new Set();
46834
47174
  setEnvironment(env) {
46835
47175
  this.envRef = env;
46836
47176
  }
@@ -46849,7 +47189,16 @@ var StoryFactory = class extends BaseObserver {
46849
47189
  }
46850
47190
  async spawn(req) {
46851
47191
  if (!this.envRef) return;
46852
- if (this.active.has(req.storyId)) return;
47192
+ if (this.active.has(req.storyId) || this.spawning.has(req.storyId)) return;
47193
+ this.spawning.add(req.storyId);
47194
+ try {
47195
+ await this.buildAndLaunch(req);
47196
+ } finally {
47197
+ this.spawning.delete(req.storyId);
47198
+ }
47199
+ }
47200
+ async buildAndLaunch(req) {
47201
+ if (!this.envRef) return;
46853
47202
  const route = resolveStoryRoute(req.model, {
46854
47203
  tierMap: this.opts.tierMap,
46855
47204
  fallbackBackend: this.opts.llm ?? "claude",
@@ -46861,24 +47210,25 @@ var StoryFactory = class extends BaseObserver {
46861
47210
  process.stderr.write(
46862
47211
  `[story-factory] ${req.storyId} \u2192 ${formatRoute(route)}` + (req.model ? ` (model="${req.model}")` : "") + "\n"
46863
47212
  );
47213
+ const storyCwd = this.opts.worktrees ? await this.opts.worktrees.create(req.storyId) ?? this.opts.cwd : this.opts.cwd;
46864
47214
  const agent = route.backend === "pi" ? new PiStoryAgent({
46865
47215
  id: req.storyId,
46866
47216
  prompt: req.prompt,
46867
- cwd: this.opts.cwd,
47217
+ cwd: storyCwd,
46868
47218
  model: route.model,
46869
47219
  retries: req.retries,
46870
47220
  timeoutSecs: req.timeoutSecs
46871
47221
  }) : route.backend === "opencode" ? new OpenCodeStoryAgent({
46872
47222
  id: req.storyId,
46873
47223
  prompt: req.prompt,
46874
- cwd: this.opts.cwd,
47224
+ cwd: storyCwd,
46875
47225
  model: route.model,
46876
47226
  retries: req.retries,
46877
47227
  timeoutSecs: req.timeoutSecs
46878
47228
  }) : route.backend === "codex" ? new CodexStoryAgent({
46879
47229
  id: req.storyId,
46880
47230
  prompt: req.prompt,
46881
- cwd: this.opts.cwd,
47231
+ cwd: storyCwd,
46882
47232
  // undefined → let Codex pick its account default.
46883
47233
  model: route.model,
46884
47234
  retries: req.retries,
@@ -46887,7 +47237,7 @@ var StoryFactory = class extends BaseObserver {
46887
47237
  {
46888
47238
  id: req.storyId,
46889
47239
  prompt: req.prompt,
46890
- cwd: this.opts.cwd,
47240
+ cwd: storyCwd,
46891
47241
  model: route.model,
46892
47242
  retries: req.retries,
46893
47243
  timeoutSecs: req.timeoutSecs
@@ -46900,7 +47250,7 @@ var StoryFactory = class extends BaseObserver {
46900
47250
  ) : new StoryAgent({
46901
47251
  id: req.storyId,
46902
47252
  prompt: req.prompt,
46903
- cwd: this.opts.cwd,
47253
+ cwd: storyCwd,
46904
47254
  // undefined → StoryAgent applies its own default.
46905
47255
  model: route.model,
46906
47256
  effort: this.opts.effort,
@@ -46918,9 +47268,9 @@ var StoryFactory = class extends BaseObserver {
46918
47268
  };
46919
47269
 
46920
47270
  // ../baro-orchestrator/src/participants/surgeon.ts
46921
- import { execFile as execFile4 } from "child_process";
46922
- import { promisify as promisify5 } from "util";
46923
- var execFileAsync3 = promisify5(execFile4);
47271
+ import { execFile as execFile5 } from "child_process";
47272
+ import { promisify as promisify6 } from "util";
47273
+ var execFileAsync3 = promisify6(execFile5);
46924
47274
  var SURGEON_SYSTEM_PROMPT = `You are the Surgeon \u2014 an autonomous planner that adapts a software-project
46925
47275
  DAG when stories fail. Given:
46926
47276
  1. A snapshot of the current PRD (project, story list with dependencies +
@@ -47574,11 +47924,19 @@ async function orchestrate(config) {
47574
47924
  const useGit = config.withGit ?? await isInsideGitRepo(config.cwd);
47575
47925
  const gitGate = new GitGate();
47576
47926
  let baseSha = null;
47927
+ const runId = `run-${Date.now()}-${process.pid}`;
47928
+ const worktreesEnabled = config.withWorktrees ?? !("BARO_NO_WORKTREES" in process.env);
47929
+ const worktrees = useGit && worktreesEnabled ? new WorktreeManager(config.cwd, gitGate, runId, {
47930
+ linkDepDirs: config.worktreeLinkDepDirs ?? true,
47931
+ onLog: (line) => emitTui && emit({ type: "story_log", id: "_git", line })
47932
+ }) : null;
47933
+ const storyPushes = [];
47934
+ let worktreePushNeeded = false;
47577
47935
  const useLibrarian = config.withLibrarian ?? true;
47578
47936
  const useSentry = config.withSentry ?? true;
47579
47937
  const useMemory = config.withMemory ?? true;
47580
- const sessionsDir = join5(process.env.HOME || "/tmp", ".baro", "sessions");
47581
- const memorySessionPath = useMemory ? join5(sessionsDir, `run-${Date.now()}-${process.pid}`, "memory") : void 0;
47938
+ const sessionsDir = join6(process.env.HOME || "/tmp", ".baro", "sessions");
47939
+ const memorySessionPath = useMemory ? join6(sessionsDir, runId, "memory") : void 0;
47582
47940
  if (useMemory) {
47583
47941
  try {
47584
47942
  const { pruneOldSessions: pruneOldSessions2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports));
@@ -47702,6 +48060,7 @@ async function orchestrate(config) {
47702
48060
  (line) => emitTui && emit({ type: "story_log", id: "_git", line })
47703
48061
  );
47704
48062
  }
48063
+ await worktrees?.cleanupStaleOnStart();
47705
48064
  } : void 0,
47706
48065
  onBeforeStoryLaunch: librarian ? (storyId, story) => {
47707
48066
  const hints = [
@@ -47711,40 +48070,50 @@ async function orchestrate(config) {
47711
48070
  return librarian.gatherContext(storyId, hints);
47712
48071
  } : void 0,
47713
48072
  onStoryPassed: useGit ? async (storyId) => {
47714
- await safePullRebase(
47715
- config.cwd,
47716
- (line) => emitTui && emit({ type: "story_log", id: storyId, line }),
47717
- gitGate
47718
- );
47719
- try {
47720
- await gitPushWithRetry(gitGate, {
47721
- cwd: config.cwd,
47722
- onLog: (line) => emitTui && emit({ type: "story_log", id: storyId, line })
47723
- });
47724
- if (emitTui) {
47725
- emit({
47726
- type: "push_status",
47727
- id: storyId,
47728
- success: true,
47729
- error: null
47730
- });
48073
+ const log2 = (line) => emitTui && emit({ type: "story_log", id: storyId, line });
48074
+ if (worktrees) {
48075
+ let merged = false;
48076
+ try {
48077
+ merged = await worktrees.mergeBack(storyId);
48078
+ } catch (e2) {
48079
+ log2(`[git] merge-back failed; worktree preserved for recovery: ${e2?.message ?? String(e2)}`);
48080
+ if (emitTui) {
48081
+ emit({ type: "push_status", id: storyId, success: false, error: e2?.message ?? String(e2) });
48082
+ }
48083
+ return;
47731
48084
  }
47732
- } catch (e2) {
47733
- if (emitTui) {
47734
- emit({
47735
- type: "push_status",
47736
- id: storyId,
47737
- success: false,
47738
- error: e2?.message ?? String(e2)
47739
- });
48085
+ if (merged) {
48086
+ await worktrees.cleanup(storyId);
48087
+ worktreePushNeeded = true;
48088
+ if (emitTui) {
48089
+ emit({ type: "push_status", id: storyId, success: true, error: null });
48090
+ }
48091
+ return;
47740
48092
  }
47741
48093
  }
47742
- } : void 0
48094
+ await safePullRebase(config.cwd, log2, gitGate);
48095
+ storyPushes.push(
48096
+ (async () => {
48097
+ try {
48098
+ await gitPushWithRetry(gitGate, { cwd: config.cwd, onLog: log2 });
48099
+ if (emitTui) {
48100
+ emit({ type: "push_status", id: storyId, success: true, error: null });
48101
+ }
48102
+ } catch (e2) {
48103
+ if (emitTui) {
48104
+ emit({ type: "push_status", id: storyId, success: false, error: e2?.message ?? String(e2) });
48105
+ }
48106
+ }
48107
+ })()
48108
+ );
48109
+ } : void 0,
48110
+ onStoryFailed: worktrees ? (storyId) => worktrees.cleanup(storyId) : void 0
47743
48111
  });
47744
48112
  conductor.setEnvironment(env);
47745
48113
  conductor.join(env);
47746
48114
  const storyFactory = new StoryFactory({
47747
48115
  cwd: config.cwd,
48116
+ worktrees: worktrees ?? void 0,
47748
48117
  llm: storyLlm,
47749
48118
  openaiModel: config.storyModel ?? "gpt-5.5",
47750
48119
  storyModelOverride: config.storyModel,
@@ -47776,6 +48145,17 @@ async function orchestrate(config) {
47776
48145
  RunStartRequest.create({ reason: "orchestrate" })
47777
48146
  );
47778
48147
  const summary = await conductor.done;
48148
+ await Promise.allSettled(storyPushes);
48149
+ if (worktreePushNeeded) {
48150
+ const log2 = (line) => emitTui && emit({ type: "story_log", id: "_git", line });
48151
+ try {
48152
+ await gitPushWithRetry(gitGate, { cwd: config.cwd, onLog: log2 });
48153
+ if (emitTui) emit({ type: "push_status", id: "_git", success: true, error: null });
48154
+ } catch (e2) {
48155
+ if (emitTui) emit({ type: "push_status", id: "_git", success: false, error: e2?.message ?? String(e2) });
48156
+ }
48157
+ }
48158
+ await worktrees?.cleanupAll();
47779
48159
  if (critic) await critic.idle();
47780
48160
  if (surgeon) await surgeon.idle();
47781
48161
  if (finalizer) await finalizer.complete();
@@ -48054,7 +48434,7 @@ async function main() {
48054
48434
  }
48055
48435
  const cwd = resolve3(args.cwd);
48056
48436
  const prdPath = resolve3(cwd, args.prd);
48057
- if (!existsSync5(prdPath)) {
48437
+ if (!existsSync6(prdPath)) {
48058
48438
  process.stderr.write(`[cli] PRD not found: ${prdPath}
48059
48439
  `);
48060
48440
  process.exit(2);