baro-ai 0.51.3 → 0.51.5

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;
@@ -40040,6 +40350,150 @@ function buildDag(stories, options = {}) {
40040
40350
  return levels;
40041
40351
  }
40042
40352
 
40353
+ // ../baro-orchestrator/src/routing.ts
40354
+ var BACKENDS = ["claude", "openai", "codex", "opencode", "pi"];
40355
+ function isBackend(s2) {
40356
+ return BACKENDS.includes(s2);
40357
+ }
40358
+ var CLAUDE_TIER_RE = /^(opus|sonnet|haiku)\b/i;
40359
+ function isClaudeTierName(s2) {
40360
+ return CLAUDE_TIER_RE.test(s2.trim());
40361
+ }
40362
+ function splitBackendModel(raw) {
40363
+ const trimmed = raw.trim();
40364
+ if (!trimmed) return {};
40365
+ const idx = trimmed.indexOf(":");
40366
+ if (idx > 0) {
40367
+ const prefix = trimmed.slice(0, idx).toLowerCase();
40368
+ if (isBackend(prefix)) {
40369
+ const model = trimmed.slice(idx + 1).trim();
40370
+ return { backend: prefix, model: model.length ? model : void 0 };
40371
+ }
40372
+ }
40373
+ if (isBackend(trimmed.toLowerCase())) {
40374
+ return { backend: trimmed.toLowerCase() };
40375
+ }
40376
+ return { model: trimmed };
40377
+ }
40378
+ function looksLikeUrl(s2) {
40379
+ return /^https?:\/\//i.test(s2.trim());
40380
+ }
40381
+ function splitModelEndpoint(model) {
40382
+ const at = model.indexOf("@");
40383
+ if (at < 0) return { model };
40384
+ const ref = model.slice(at + 1).trim();
40385
+ return { model: model.slice(0, at).trim(), endpointRef: ref || void 0 };
40386
+ }
40387
+ function resolveEndpoint(ref, opts) {
40388
+ if (looksLikeUrl(ref)) {
40389
+ return { baseUrl: ref, apiKey: opts.defaultApiKey };
40390
+ }
40391
+ const ep = opts.endpoints?.[ref.toLowerCase()] ?? opts.endpoints?.[ref];
40392
+ if (!ep) {
40393
+ throw new Error(
40394
+ `unknown OpenAI endpoint "${ref}" \u2014 define it with --openai-endpoint ${ref}=<url> or use an inline https:// URL`
40395
+ );
40396
+ }
40397
+ return { baseUrl: ep.baseUrl, apiKey: ep.apiKey ?? opts.defaultApiKey };
40398
+ }
40399
+ function resolveStoryRoute(rawModel, opts) {
40400
+ const raw = (opts.override ?? rawModel ?? "").trim();
40401
+ if (!raw) return defaultRoute(opts.fallbackBackend, opts.openaiDefaultModel);
40402
+ const direct = splitBackendModel(raw);
40403
+ if (direct.backend === void 0 && opts.tierMap) {
40404
+ const mapped = opts.tierMap[raw.toLowerCase()] ?? opts.tierMap[raw];
40405
+ if (mapped) {
40406
+ return resolveStoryRoute(mapped, {
40407
+ ...opts,
40408
+ override: void 0,
40409
+ tierMap: void 0
40410
+ });
40411
+ }
40412
+ }
40413
+ if (direct.backend) {
40414
+ if (direct.model) return buildRoute(direct.backend, direct.model, opts);
40415
+ return defaultRoute(direct.backend, opts.openaiDefaultModel);
40416
+ }
40417
+ const backend = opts.fallbackBackend;
40418
+ if (backend === "claude") {
40419
+ return { backend, model: direct.model };
40420
+ }
40421
+ if (direct.model && !isClaudeTierName(direct.model)) {
40422
+ return buildRoute(backend, direct.model, opts);
40423
+ }
40424
+ return defaultRoute(backend, opts.openaiDefaultModel);
40425
+ }
40426
+ function buildRoute(backend, model, opts) {
40427
+ if (backend !== "openai") return { backend, model };
40428
+ const { model: bareModel, endpointRef } = splitModelEndpoint(model);
40429
+ if (!endpointRef) return { backend, model: bareModel };
40430
+ const ep = resolveEndpoint(endpointRef, opts);
40431
+ return { backend, model: bareModel, baseUrl: ep.baseUrl, apiKey: ep.apiKey };
40432
+ }
40433
+ function defaultRoute(backend, openaiDefaultModel) {
40434
+ if (backend === "openai") return { backend, model: openaiDefaultModel };
40435
+ return { backend };
40436
+ }
40437
+ function parseTierMap(spec) {
40438
+ const map = {};
40439
+ for (const part of spec.split(",")) {
40440
+ const seg = part.trim();
40441
+ if (!seg) continue;
40442
+ const eq = seg.indexOf("=");
40443
+ if (eq <= 0) {
40444
+ throw new Error(
40445
+ `bad --tier-map entry "${seg}" (expected tier=backend:model)`
40446
+ );
40447
+ }
40448
+ const tier = seg.slice(0, eq).trim().toLowerCase();
40449
+ const route = seg.slice(eq + 1).trim();
40450
+ if (!tier || !route) {
40451
+ throw new Error(
40452
+ `bad --tier-map entry "${seg}" (expected tier=backend:model)`
40453
+ );
40454
+ }
40455
+ const { backend } = splitBackendModel(route);
40456
+ if (!backend) {
40457
+ throw new Error(
40458
+ `--tier-map route "${route}" for tier "${tier}" must name a backend (claude: | openai: | codex: | opencode: | pi:)`
40459
+ );
40460
+ }
40461
+ map[tier] = route;
40462
+ }
40463
+ return map;
40464
+ }
40465
+ function parseEndpoints(specs, keyFor) {
40466
+ const map = {};
40467
+ for (const raw of specs) {
40468
+ const spec = raw.trim();
40469
+ if (!spec) continue;
40470
+ const eq = spec.indexOf("=");
40471
+ if (eq <= 0) {
40472
+ throw new Error(
40473
+ `bad --openai-endpoint "${spec}" (expected name=url)`
40474
+ );
40475
+ }
40476
+ const name = spec.slice(0, eq).trim().toLowerCase();
40477
+ const url = spec.slice(eq + 1).trim();
40478
+ if (!name || !url) {
40479
+ throw new Error(
40480
+ `bad --openai-endpoint "${spec}" (expected name=url)`
40481
+ );
40482
+ }
40483
+ if (!looksLikeUrl(url)) {
40484
+ throw new Error(
40485
+ `--openai-endpoint "${name}" url "${url}" must start with http:// or https://`
40486
+ );
40487
+ }
40488
+ map[name] = { baseUrl: url, apiKey: keyFor?.(name) };
40489
+ }
40490
+ return map;
40491
+ }
40492
+ function formatRoute(route) {
40493
+ const base = route.model ? `${route.backend}:${route.model}` : route.backend;
40494
+ return route.baseUrl ? `${base}@${route.baseUrl}` : base;
40495
+ }
40496
+
40043
40497
  // ../baro-orchestrator/src/participants/auditor.ts
40044
40498
  import { appendFileSync, mkdirSync } from "fs";
40045
40499
  import { dirname } from "path";
@@ -40159,8 +40613,8 @@ var Auditor = class extends BaseObserver {
40159
40613
  };
40160
40614
 
40161
40615
  // ../baro-orchestrator/src/participants/conductor.ts
40162
- import { existsSync, readFileSync as readFileSync2 } from "fs";
40163
- import { join } from "path";
40616
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
40617
+ import { join as join2 } from "path";
40164
40618
 
40165
40619
  // ../baro-orchestrator/src/prd.ts
40166
40620
  import { readFileSync, writeFileSync } from "fs";
@@ -40345,6 +40799,14 @@ var Conductor = class extends BaseObserver {
40345
40799
  /** Resolved when the run terminates, exposed for callers that need it. */
40346
40800
  done;
40347
40801
  resolveDone;
40802
+ /**
40803
+ * Serializes event handling. Mozaik delivers events without awaiting the
40804
+ * handler, so two StoryResults settling together could interleave — one
40805
+ * spawning the next DAG level while another's `await onStoryPassed`
40806
+ * (worktree merge-back, #50) is still in flight, leaving the dependent
40807
+ * level without the merged work. Chaining handlers keeps them sequential.
40808
+ */
40809
+ handleChain = Promise.resolve();
40348
40810
  constructor(opts) {
40349
40811
  super();
40350
40812
  this.opts = {
@@ -40381,7 +40843,13 @@ var Conductor = class extends BaseObserver {
40381
40843
  async onExternalEvent(_source, event) {
40382
40844
  await this.handle(event);
40383
40845
  }
40384
- async handle(event) {
40846
+ handle(event) {
40847
+ const run = this.handleChain.then(() => this.handleEvent(event));
40848
+ this.handleChain = run.catch(() => {
40849
+ });
40850
+ return run;
40851
+ }
40852
+ async handleEvent(event) {
40385
40853
  if (RunStartRequest.is(event)) {
40386
40854
  await this.handleRunStart();
40387
40855
  return;
@@ -40521,6 +40989,20 @@ var Conductor = class extends BaseObserver {
40521
40989
  } else {
40522
40990
  this.currentLevel.failed.push(item.storyId);
40523
40991
  this.addGlobalFailed(item.storyId);
40992
+ if (this.opts.onStoryFailed) {
40993
+ try {
40994
+ await this.opts.onStoryFailed(item.storyId);
40995
+ } catch (e2) {
40996
+ this.emit(
40997
+ ConductorState.create({
40998
+ phase: "running_level",
40999
+ detail: `onStoryFailed hook for ${item.storyId} failed: ${e2?.message ?? String(e2)}`,
41000
+ currentLevel: this.currentLevel.ordinal,
41001
+ totalLevels: this.currentLevel.totalLevelsHint
41002
+ })
41003
+ );
41004
+ }
41005
+ }
40524
41006
  }
40525
41007
  await this.fillSpawnSlots();
40526
41008
  if (this.currentLevel.pending.size === 0) {
@@ -40790,9 +41272,9 @@ ${prompt}`;
40790
41272
  return true;
40791
41273
  }
40792
41274
  resolvePrompt(story) {
40793
- const candidatePath = this.opts.promptTemplatePath ?? join(this.opts.cwd, "prompt.md");
41275
+ const candidatePath = this.opts.promptTemplatePath ?? join2(this.opts.cwd, "prompt.md");
40794
41276
  let prompt;
40795
- if (existsSync(candidatePath)) {
41277
+ if (existsSync2(candidatePath)) {
40796
41278
  const tpl = readFileSyncSafe(candidatePath);
40797
41279
  prompt = tpl ? applyTemplate(tpl, story) : buildDefaultStoryPrompt(story);
40798
41280
  } else {
@@ -40871,9 +41353,9 @@ function applyTemplate(tpl, story) {
40871
41353
  }
40872
41354
 
40873
41355
  // ../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);
41356
+ import { execFile as execFile3 } from "child_process";
41357
+ import { promisify as promisify4 } from "util";
41358
+ var execFileAsync = promisify4(execFile3);
40877
41359
  var VERDICT_SYSTEM_PROMPT = `You are a strict acceptance-criteria evaluator. You will receive:
40878
41360
  1. A list of acceptance criteria that must ALL be satisfied.
40879
41361
  2. The output text produced by an agent.
@@ -42009,9 +42491,9 @@ ${userPrompt}`;
42009
42491
  };
42010
42492
 
42011
42493
  // ../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);
42494
+ import { execFile as execFile4 } from "child_process";
42495
+ import { promisify as promisify5 } from "util";
42496
+ var execFileAsync2 = promisify5(execFile4);
42015
42497
  var Finalizer = class extends BaseObserver {
42016
42498
  opts;
42017
42499
  envRef = null;
@@ -42576,9 +43058,16 @@ var ProgressForwarder = class extends BaseObserver {
42576
43058
  };
42577
43059
 
42578
43060
  // ../baro-orchestrator/src/participants/forwarders/story-lifecycle.ts
43061
+ var CREATE_TOOLS = /* @__PURE__ */ new Set(["Write", "write_file"]);
43062
+ var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "NotebookEdit", "edit_file"]);
42579
43063
  var StoryLifecycleForwarder = class extends BaseObserver {
42580
43064
  startedStories = /* @__PURE__ */ new Set();
42581
43065
  retryCounts = /* @__PURE__ */ new Map();
43066
+ // storyId → (path → first-touch kind). Distinct paths per story, so a
43067
+ // file touched many times counts once, classified by its first write.
43068
+ // Reset on each retry so story_complete reflects only the winning
43069
+ // attempt's touches, not files a failed attempt wrote and abandoned.
43070
+ filesByStory = /* @__PURE__ */ new Map();
42582
43071
  async onExternalEvent(_source, event) {
42583
43072
  if (AgentState.is(event)) {
42584
43073
  this.handleAgentState(event.data);
@@ -42589,6 +43078,27 @@ var StoryLifecycleForwarder = class extends BaseObserver {
42589
43078
  return;
42590
43079
  }
42591
43080
  }
43081
+ // Same signal the Sentry uses: a write/edit tool call from a story
43082
+ // agent. We attribute the touched path to that agent's story and
43083
+ // remember its first-touch kind so story_complete can report real
43084
+ // per-story file counts instead of a hardcoded 0.
43085
+ async onExternalFunctionCall(source, item) {
43086
+ const isCreate = CREATE_TOOLS.has(item.name);
43087
+ const isEdit = EDIT_TOOLS.has(item.name);
43088
+ if (!isCreate && !isEdit) return;
43089
+ const agentId = source.agentId;
43090
+ if (typeof agentId !== "string") return;
43091
+ const path6 = extractPath(item);
43092
+ if (!path6) return;
43093
+ let paths = this.filesByStory.get(agentId);
43094
+ if (!paths) {
43095
+ paths = /* @__PURE__ */ new Map();
43096
+ this.filesByStory.set(agentId, paths);
43097
+ }
43098
+ if (!paths.has(path6)) {
43099
+ paths.set(path6, isCreate ? "created" : "modified");
43100
+ }
43101
+ }
42592
43102
  handleAgentState(item) {
42593
43103
  if (item.phase === "running" && !this.startedStories.has(item.agentId)) {
42594
43104
  this.startedStories.add(item.agentId);
@@ -42597,19 +43107,31 @@ var StoryLifecycleForwarder = class extends BaseObserver {
42597
43107
  if (item.phase === "waiting" && item.detail?.includes("retrying")) {
42598
43108
  const count = (this.retryCounts.get(item.agentId) ?? 0) + 1;
42599
43109
  this.retryCounts.set(item.agentId, count);
43110
+ this.filesByStory.delete(item.agentId);
42600
43111
  emit({ type: "story_retry", id: item.agentId, attempt: count });
42601
43112
  }
42602
43113
  }
42603
43114
  handleStoryResult(item) {
42604
43115
  if (item.success) {
43116
+ const paths = this.filesByStory.get(item.storyId);
43117
+ let created = 0;
43118
+ let modified = 0;
43119
+ if (paths) {
43120
+ for (const kind of paths.values()) {
43121
+ if (kind === "created") created++;
43122
+ else modified++;
43123
+ }
43124
+ }
43125
+ this.filesByStory.delete(item.storyId);
42605
43126
  emit({
42606
43127
  type: "story_complete",
42607
43128
  id: item.storyId,
42608
43129
  duration_secs: item.durationSecs,
42609
- files_created: 0,
42610
- files_modified: 0
43130
+ files_created: created,
43131
+ files_modified: modified
42611
43132
  });
42612
43133
  } else {
43134
+ this.filesByStory.delete(item.storyId);
42613
43135
  emit({
42614
43136
  type: "story_error",
42615
43137
  id: item.storyId,
@@ -42620,6 +43142,19 @@ var StoryLifecycleForwarder = class extends BaseObserver {
42620
43142
  }
42621
43143
  }
42622
43144
  };
43145
+ function extractPath(item) {
43146
+ let args;
43147
+ try {
43148
+ args = JSON.parse(item.args);
43149
+ } catch {
43150
+ return null;
43151
+ }
43152
+ for (const key of ["file_path", "path", "notebook_path"]) {
43153
+ const v = args[key];
43154
+ if (typeof v === "string") return v;
43155
+ }
43156
+ return null;
43157
+ }
42623
43158
 
42624
43159
  // ../baro-orchestrator/src/participants/forwarders/token-usage.ts
42625
43160
  var TokenUsageForwarder = class extends BaseObserver {
@@ -42923,10 +43458,10 @@ function tokenizeHints(prompt) {
42923
43458
 
42924
43459
  // ../baro-orchestrator/src/participants/memory-librarian.ts
42925
43460
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync3 } from "fs";
42926
- import { join as join3 } from "path";
43461
+ import { join as join4 } from "path";
42927
43462
  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`);
43463
+ var LOG_DIR = join4(process.env.HOME || "/tmp", ".baro", "runs");
43464
+ var LOG_FILE = join4(LOG_DIR, `memory-${Date.now()}.log`);
42930
43465
  try {
42931
43466
  mkdirSync3(LOG_DIR, { recursive: true });
42932
43467
  } catch {
@@ -43256,7 +43791,7 @@ var Sentry = class extends BaseObserver {
43256
43791
  if (!WRITE_TOOLS.has(item.name)) return;
43257
43792
  const agentId = source.agentId;
43258
43793
  if (typeof agentId !== "string") return;
43259
- const path6 = extractPath(item);
43794
+ const path6 = extractPath2(item);
43260
43795
  if (!path6) return;
43261
43796
  const touch = {
43262
43797
  agentId,
@@ -43291,7 +43826,7 @@ var Sentry = class extends BaseObserver {
43291
43826
  }
43292
43827
  }
43293
43828
  };
43294
- function extractPath(item) {
43829
+ function extractPath2(item) {
43295
43830
  let args;
43296
43831
  try {
43297
43832
  args = JSON.parse(item.args);
@@ -46676,150 +47211,6 @@ function raceWithTimeout4(p, ms, label) {
46676
47211
  ]);
46677
47212
  }
46678
47213
 
46679
- // ../baro-orchestrator/src/routing.ts
46680
- var BACKENDS = ["claude", "openai", "codex", "opencode", "pi"];
46681
- function isBackend(s2) {
46682
- return BACKENDS.includes(s2);
46683
- }
46684
- var CLAUDE_TIER_RE = /^(opus|sonnet|haiku)\b/i;
46685
- function isClaudeTierName(s2) {
46686
- return CLAUDE_TIER_RE.test(s2.trim());
46687
- }
46688
- function splitBackendModel(raw) {
46689
- const trimmed = raw.trim();
46690
- if (!trimmed) return {};
46691
- const idx = trimmed.indexOf(":");
46692
- if (idx > 0) {
46693
- const prefix = trimmed.slice(0, idx).toLowerCase();
46694
- if (isBackend(prefix)) {
46695
- const model = trimmed.slice(idx + 1).trim();
46696
- return { backend: prefix, model: model.length ? model : void 0 };
46697
- }
46698
- }
46699
- if (isBackend(trimmed.toLowerCase())) {
46700
- return { backend: trimmed.toLowerCase() };
46701
- }
46702
- return { model: trimmed };
46703
- }
46704
- function looksLikeUrl(s2) {
46705
- return /^https?:\/\//i.test(s2.trim());
46706
- }
46707
- function splitModelEndpoint(model) {
46708
- const at = model.indexOf("@");
46709
- if (at < 0) return { model };
46710
- const ref = model.slice(at + 1).trim();
46711
- return { model: model.slice(0, at).trim(), endpointRef: ref || void 0 };
46712
- }
46713
- function resolveEndpoint(ref, opts) {
46714
- if (looksLikeUrl(ref)) {
46715
- return { baseUrl: ref, apiKey: opts.defaultApiKey };
46716
- }
46717
- const ep = opts.endpoints?.[ref.toLowerCase()] ?? opts.endpoints?.[ref];
46718
- if (!ep) {
46719
- throw new Error(
46720
- `unknown OpenAI endpoint "${ref}" \u2014 define it with --openai-endpoint ${ref}=<url> or use an inline https:// URL`
46721
- );
46722
- }
46723
- return { baseUrl: ep.baseUrl, apiKey: ep.apiKey ?? opts.defaultApiKey };
46724
- }
46725
- function resolveStoryRoute(rawModel, opts) {
46726
- const raw = (opts.override ?? rawModel ?? "").trim();
46727
- if (!raw) return defaultRoute(opts.fallbackBackend, opts.openaiDefaultModel);
46728
- const direct = splitBackendModel(raw);
46729
- if (direct.backend === void 0 && opts.tierMap) {
46730
- const mapped = opts.tierMap[raw.toLowerCase()] ?? opts.tierMap[raw];
46731
- if (mapped) {
46732
- return resolveStoryRoute(mapped, {
46733
- ...opts,
46734
- override: void 0,
46735
- tierMap: void 0
46736
- });
46737
- }
46738
- }
46739
- if (direct.backend) {
46740
- if (direct.model) return buildRoute(direct.backend, direct.model, opts);
46741
- return defaultRoute(direct.backend, opts.openaiDefaultModel);
46742
- }
46743
- const backend = opts.fallbackBackend;
46744
- if (backend === "claude") {
46745
- return { backend, model: direct.model };
46746
- }
46747
- if (direct.model && !isClaudeTierName(direct.model)) {
46748
- return buildRoute(backend, direct.model, opts);
46749
- }
46750
- return defaultRoute(backend, opts.openaiDefaultModel);
46751
- }
46752
- function buildRoute(backend, model, opts) {
46753
- if (backend !== "openai") return { backend, model };
46754
- const { model: bareModel, endpointRef } = splitModelEndpoint(model);
46755
- if (!endpointRef) return { backend, model: bareModel };
46756
- const ep = resolveEndpoint(endpointRef, opts);
46757
- return { backend, model: bareModel, baseUrl: ep.baseUrl, apiKey: ep.apiKey };
46758
- }
46759
- function defaultRoute(backend, openaiDefaultModel) {
46760
- if (backend === "openai") return { backend, model: openaiDefaultModel };
46761
- return { backend };
46762
- }
46763
- function parseTierMap(spec) {
46764
- const map = {};
46765
- for (const part of spec.split(",")) {
46766
- const seg = part.trim();
46767
- if (!seg) continue;
46768
- const eq = seg.indexOf("=");
46769
- if (eq <= 0) {
46770
- throw new Error(
46771
- `bad --tier-map entry "${seg}" (expected tier=backend:model)`
46772
- );
46773
- }
46774
- const tier = seg.slice(0, eq).trim().toLowerCase();
46775
- const route = seg.slice(eq + 1).trim();
46776
- if (!tier || !route) {
46777
- throw new Error(
46778
- `bad --tier-map entry "${seg}" (expected tier=backend:model)`
46779
- );
46780
- }
46781
- const { backend } = splitBackendModel(route);
46782
- if (!backend) {
46783
- throw new Error(
46784
- `--tier-map route "${route}" for tier "${tier}" must name a backend (claude: | openai: | codex: | opencode: | pi:)`
46785
- );
46786
- }
46787
- map[tier] = route;
46788
- }
46789
- return map;
46790
- }
46791
- function parseEndpoints(specs, keyFor) {
46792
- const map = {};
46793
- for (const raw of specs) {
46794
- const spec = raw.trim();
46795
- if (!spec) continue;
46796
- const eq = spec.indexOf("=");
46797
- if (eq <= 0) {
46798
- throw new Error(
46799
- `bad --openai-endpoint "${spec}" (expected name=url)`
46800
- );
46801
- }
46802
- const name = spec.slice(0, eq).trim().toLowerCase();
46803
- const url = spec.slice(eq + 1).trim();
46804
- if (!name || !url) {
46805
- throw new Error(
46806
- `bad --openai-endpoint "${spec}" (expected name=url)`
46807
- );
46808
- }
46809
- if (!looksLikeUrl(url)) {
46810
- throw new Error(
46811
- `--openai-endpoint "${name}" url "${url}" must start with http:// or https://`
46812
- );
46813
- }
46814
- map[name] = { baseUrl: url, apiKey: keyFor?.(name) };
46815
- }
46816
- return map;
46817
- }
46818
- function formatRoute(route) {
46819
- const base = route.model ? `${route.backend}:${route.model}` : route.backend;
46820
- return route.baseUrl ? `${base}@${route.baseUrl}` : base;
46821
- }
46822
-
46823
47214
  // ../baro-orchestrator/src/participants/story-factory.ts
46824
47215
  var StoryFactory = class extends BaseObserver {
46825
47216
  constructor(opts) {
@@ -46831,6 +47222,8 @@ var StoryFactory = class extends BaseObserver {
46831
47222
  // narrows to vanilla AgenticEnvironment.
46832
47223
  envRef = null;
46833
47224
  active = /* @__PURE__ */ new Map();
47225
+ /** Story ids whose spawn is in progress (closes the await-create window). */
47226
+ spawning = /* @__PURE__ */ new Set();
46834
47227
  setEnvironment(env) {
46835
47228
  this.envRef = env;
46836
47229
  }
@@ -46849,7 +47242,16 @@ var StoryFactory = class extends BaseObserver {
46849
47242
  }
46850
47243
  async spawn(req) {
46851
47244
  if (!this.envRef) return;
46852
- if (this.active.has(req.storyId)) return;
47245
+ if (this.active.has(req.storyId) || this.spawning.has(req.storyId)) return;
47246
+ this.spawning.add(req.storyId);
47247
+ try {
47248
+ await this.buildAndLaunch(req);
47249
+ } finally {
47250
+ this.spawning.delete(req.storyId);
47251
+ }
47252
+ }
47253
+ async buildAndLaunch(req) {
47254
+ if (!this.envRef) return;
46853
47255
  const route = resolveStoryRoute(req.model, {
46854
47256
  tierMap: this.opts.tierMap,
46855
47257
  fallbackBackend: this.opts.llm ?? "claude",
@@ -46861,24 +47263,25 @@ var StoryFactory = class extends BaseObserver {
46861
47263
  process.stderr.write(
46862
47264
  `[story-factory] ${req.storyId} \u2192 ${formatRoute(route)}` + (req.model ? ` (model="${req.model}")` : "") + "\n"
46863
47265
  );
47266
+ const storyCwd = this.opts.worktrees ? await this.opts.worktrees.create(req.storyId) ?? this.opts.cwd : this.opts.cwd;
46864
47267
  const agent = route.backend === "pi" ? new PiStoryAgent({
46865
47268
  id: req.storyId,
46866
47269
  prompt: req.prompt,
46867
- cwd: this.opts.cwd,
47270
+ cwd: storyCwd,
46868
47271
  model: route.model,
46869
47272
  retries: req.retries,
46870
47273
  timeoutSecs: req.timeoutSecs
46871
47274
  }) : route.backend === "opencode" ? new OpenCodeStoryAgent({
46872
47275
  id: req.storyId,
46873
47276
  prompt: req.prompt,
46874
- cwd: this.opts.cwd,
47277
+ cwd: storyCwd,
46875
47278
  model: route.model,
46876
47279
  retries: req.retries,
46877
47280
  timeoutSecs: req.timeoutSecs
46878
47281
  }) : route.backend === "codex" ? new CodexStoryAgent({
46879
47282
  id: req.storyId,
46880
47283
  prompt: req.prompt,
46881
- cwd: this.opts.cwd,
47284
+ cwd: storyCwd,
46882
47285
  // undefined → let Codex pick its account default.
46883
47286
  model: route.model,
46884
47287
  retries: req.retries,
@@ -46887,7 +47290,7 @@ var StoryFactory = class extends BaseObserver {
46887
47290
  {
46888
47291
  id: req.storyId,
46889
47292
  prompt: req.prompt,
46890
- cwd: this.opts.cwd,
47293
+ cwd: storyCwd,
46891
47294
  model: route.model,
46892
47295
  retries: req.retries,
46893
47296
  timeoutSecs: req.timeoutSecs
@@ -46900,7 +47303,7 @@ var StoryFactory = class extends BaseObserver {
46900
47303
  ) : new StoryAgent({
46901
47304
  id: req.storyId,
46902
47305
  prompt: req.prompt,
46903
- cwd: this.opts.cwd,
47306
+ cwd: storyCwd,
46904
47307
  // undefined → StoryAgent applies its own default.
46905
47308
  model: route.model,
46906
47309
  effort: this.opts.effort,
@@ -46918,9 +47321,9 @@ var StoryFactory = class extends BaseObserver {
46918
47321
  };
46919
47322
 
46920
47323
  // ../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);
47324
+ import { execFile as execFile5 } from "child_process";
47325
+ import { promisify as promisify6 } from "util";
47326
+ var execFileAsync3 = promisify6(execFile5);
46924
47327
  var SURGEON_SYSTEM_PROMPT = `You are the Surgeon \u2014 an autonomous planner that adapts a software-project
46925
47328
  DAG when stories fail. Given:
46926
47329
  1. A snapshot of the current PRD (project, story list with dependencies +
@@ -46997,7 +47400,8 @@ var Surgeon = class extends BaseObserver {
46997
47400
  maxReplans: opts.maxReplans ?? 10,
46998
47401
  claudeBin: opts.claudeBin ?? "claude",
46999
47402
  timeoutMs: opts.timeoutMs ?? 9e4,
47000
- snapshot: opts.snapshot
47403
+ snapshot: opts.snapshot,
47404
+ resolveRoute: opts.resolveRoute
47001
47405
  };
47002
47406
  }
47003
47407
  /** Resolves once every in-flight LLM evaluation has completed. */
@@ -47036,7 +47440,7 @@ var Surgeon = class extends BaseObserver {
47036
47440
  */
47037
47441
  async evaluateWithLlm(failure) {
47038
47442
  const snap = this.opts.snapshot();
47039
- const prompt = buildSurgeonPrompt(snap, failure);
47443
+ const prompt = buildSurgeonPrompt(snap, failure, this.opts.resolveRoute);
47040
47444
  try {
47041
47445
  const { stdout } = await execFileAsync3(
47042
47446
  this.opts.claudeBin,
@@ -47086,11 +47490,12 @@ var Surgeon = class extends BaseObserver {
47086
47490
  }
47087
47491
  }
47088
47492
  };
47089
- function buildSurgeonPrompt(snap, failure) {
47493
+ function buildSurgeonPrompt(snap, failure, resolveRoute) {
47090
47494
  const storyLines = snap.stories.map(
47091
47495
  (s2) => ` - ${s2.id} ${s2.passes ? "[passed]" : "[pending]"} ${s2.model ? `<tier:${s2.model}> ` : ""}"${s2.title}" deps=${JSON.stringify(s2.dependsOn)}`
47092
47496
  ).join("\n");
47093
47497
  const failureStory = snap.stories.find((s2) => s2.id === failure.storyId);
47498
+ const ranOn = resolveRoute ? resolveRoute(failureStory?.model) : null;
47094
47499
  return [
47095
47500
  `# Project: ${snap.project}`,
47096
47501
  `Description: ${snap.description}`,
@@ -47103,6 +47508,9 @@ function buildSurgeonPrompt(snap, failure) {
47103
47508
  `Title: ${failureStory?.title ?? "(unknown)"}`,
47104
47509
  `Description: ${failureStory?.description ?? "(unknown)"}`,
47105
47510
  `Tier that just failed: ${failureStory?.model ?? "(default)"}`,
47511
+ ...ranOn ? [
47512
+ `Model that actually ran: ${ranOn} (an override replaced the planner tier above; refer to THIS model in your reason, not the tier)`
47513
+ ] : [],
47106
47514
  `Attempts: ${failure.attempts}`,
47107
47515
  `Error: ${failure.error ?? "(no reason captured)"}`,
47108
47516
  "",
@@ -47153,7 +47561,8 @@ var SurgeonCodex = class extends BaseObserver {
47153
47561
  maxReplans: opts.maxReplans ?? 10,
47154
47562
  codexBin: opts.codexBin ?? "codex",
47155
47563
  timeoutMs: opts.timeoutMs ?? 3e5,
47156
- snapshot: opts.snapshot
47564
+ snapshot: opts.snapshot,
47565
+ resolveRoute: opts.resolveRoute
47157
47566
  };
47158
47567
  }
47159
47568
  async idle() {
@@ -47177,7 +47586,7 @@ var SurgeonCodex = class extends BaseObserver {
47177
47586
  }
47178
47587
  async evaluateWithLlm(failure) {
47179
47588
  const snap = this.opts.snapshot();
47180
- const userPrompt = buildSurgeonPrompt(snap, failure);
47589
+ const userPrompt = buildSurgeonPrompt(snap, failure, this.opts.resolveRoute);
47181
47590
  const prompt = `${SURGEON_SYSTEM_PROMPT}
47182
47591
 
47183
47592
  ${userPrompt}`;
@@ -47249,7 +47658,8 @@ var SurgeonOpenAI = class extends BaseObserver {
47249
47658
  this.opts = {
47250
47659
  maxReplans: opts.maxReplans ?? 10,
47251
47660
  model: opts.model ?? "gpt-5.5",
47252
- snapshot: opts.snapshot
47661
+ snapshot: opts.snapshot,
47662
+ resolveRoute: opts.resolveRoute
47253
47663
  };
47254
47664
  this.model = pickModel3(this.opts.model);
47255
47665
  }
@@ -47281,7 +47691,7 @@ var SurgeonOpenAI = class extends BaseObserver {
47281
47691
  */
47282
47692
  async evaluate(failure) {
47283
47693
  const snap = this.opts.snapshot();
47284
- const userPrompt = buildSurgeonPrompt(snap, failure);
47694
+ const userPrompt = buildSurgeonPrompt(snap, failure, this.opts.resolveRoute);
47285
47695
  const context = ModelContext.create("surgeon").addContextItem(SystemMessageItem.create(SURGEON_SYSTEM_PROMPT)).addContextItem(UserMessageItem.create(userPrompt));
47286
47696
  try {
47287
47697
  const round = await runInferenceRound(context, this.model);
@@ -47338,7 +47748,8 @@ var SurgeonOpenCode = class extends BaseObserver {
47338
47748
  maxReplans: opts.maxReplans ?? 10,
47339
47749
  opencodeBin: opts.opencodeBin ?? "opencode",
47340
47750
  timeoutMs: opts.timeoutMs ?? 3e5,
47341
- snapshot: opts.snapshot
47751
+ snapshot: opts.snapshot,
47752
+ resolveRoute: opts.resolveRoute
47342
47753
  };
47343
47754
  }
47344
47755
  async idle() {
@@ -47362,7 +47773,7 @@ var SurgeonOpenCode = class extends BaseObserver {
47362
47773
  }
47363
47774
  async evaluateWithLlm(failure) {
47364
47775
  const snap = this.opts.snapshot();
47365
- const userPrompt = buildSurgeonPrompt(snap, failure);
47776
+ const userPrompt = buildSurgeonPrompt(snap, failure, this.opts.resolveRoute);
47366
47777
  const prompt = `${SURGEON_SYSTEM_PROMPT}
47367
47778
 
47368
47779
  ${userPrompt}`;
@@ -47417,7 +47828,8 @@ var SurgeonPi = class extends BaseObserver {
47417
47828
  maxReplans: opts.maxReplans ?? 10,
47418
47829
  piBin: opts.piBin ?? "pi",
47419
47830
  timeoutMs: opts.timeoutMs ?? 3e5,
47420
- snapshot: opts.snapshot
47831
+ snapshot: opts.snapshot,
47832
+ resolveRoute: opts.resolveRoute
47421
47833
  };
47422
47834
  }
47423
47835
  async idle() {
@@ -47441,7 +47853,7 @@ var SurgeonPi = class extends BaseObserver {
47441
47853
  }
47442
47854
  async evaluateWithLlm(failure) {
47443
47855
  const snap = this.opts.snapshot();
47444
- const userPrompt = buildSurgeonPrompt(snap, failure);
47856
+ const userPrompt = buildSurgeonPrompt(snap, failure, this.opts.resolveRoute);
47445
47857
  const prompt = `${SURGEON_SYSTEM_PROMPT}
47446
47858
 
47447
47859
  ${userPrompt}`;
@@ -47574,11 +47986,19 @@ async function orchestrate(config) {
47574
47986
  const useGit = config.withGit ?? await isInsideGitRepo(config.cwd);
47575
47987
  const gitGate = new GitGate();
47576
47988
  let baseSha = null;
47989
+ const runId = `run-${Date.now()}-${process.pid}`;
47990
+ const worktreesEnabled = config.withWorktrees ?? !("BARO_NO_WORKTREES" in process.env);
47991
+ const worktrees = useGit && worktreesEnabled ? new WorktreeManager(config.cwd, gitGate, runId, {
47992
+ linkDepDirs: config.worktreeLinkDepDirs ?? true,
47993
+ onLog: (line) => emitTui && emit({ type: "story_log", id: "_git", line })
47994
+ }) : null;
47995
+ const storyPushes = [];
47996
+ let worktreePushNeeded = false;
47577
47997
  const useLibrarian = config.withLibrarian ?? true;
47578
47998
  const useSentry = config.withSentry ?? true;
47579
47999
  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;
48000
+ const sessionsDir = join6(process.env.HOME || "/tmp", ".baro", "sessions");
48001
+ const memorySessionPath = useMemory ? join6(sessionsDir, runId, "memory") : void 0;
47582
48002
  if (useMemory) {
47583
48003
  try {
47584
48004
  const { pruneOldSessions: pruneOldSessions2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports));
@@ -47610,32 +48030,53 @@ async function orchestrate(config) {
47610
48030
  }))
47611
48031
  };
47612
48032
  };
48033
+ const storyRouting = {
48034
+ fallbackBackend: storyLlm,
48035
+ openaiDefaultModel: config.storyModel ?? "gpt-5.5",
48036
+ override: config.storyModel,
48037
+ tierMap: config.tierMap,
48038
+ endpoints: config.openaiEndpoints,
48039
+ defaultApiKey: process.env.OPENAI_API_KEY
48040
+ };
48041
+ const routingOverridden = storyLlm !== "claude" || !!config.storyModel || !!config.tierMap;
48042
+ const resolveRoute = routingOverridden ? (model) => {
48043
+ try {
48044
+ return formatRoute(resolveStoryRoute(model, storyRouting));
48045
+ } catch {
48046
+ return null;
48047
+ }
48048
+ } : void 0;
47613
48049
  if (surgeonLlm === "openai") {
47614
48050
  surgeon = new SurgeonOpenAI({
47615
48051
  snapshot,
48052
+ resolveRoute,
47616
48053
  model: config.surgeonModel ?? "gpt-5.5"
47617
48054
  });
47618
48055
  } else if (surgeonLlm === "codex") {
47619
48056
  surgeon = new SurgeonCodex({
47620
48057
  snapshot,
48058
+ resolveRoute,
47621
48059
  useLlm: config.surgeonUseLlm ?? true,
47622
48060
  model: config.surgeonModel
47623
48061
  });
47624
48062
  } else if (surgeonLlm === "opencode") {
47625
48063
  surgeon = new SurgeonOpenCode({
47626
48064
  snapshot,
48065
+ resolveRoute,
47627
48066
  useLlm: config.surgeonUseLlm ?? true,
47628
48067
  model: config.surgeonModel
47629
48068
  });
47630
48069
  } else if (surgeonLlm === "pi") {
47631
48070
  surgeon = new SurgeonPi({
47632
48071
  snapshot,
48072
+ resolveRoute,
47633
48073
  useLlm: config.surgeonUseLlm ?? true,
47634
48074
  model: config.surgeonModel
47635
48075
  });
47636
48076
  } else {
47637
48077
  surgeon = new Surgeon({
47638
48078
  snapshot,
48079
+ resolveRoute,
47639
48080
  useLlm: config.surgeonUseLlm ?? false,
47640
48081
  model: config.surgeonModel ?? "opus"
47641
48082
  });
@@ -47702,6 +48143,7 @@ async function orchestrate(config) {
47702
48143
  (line) => emitTui && emit({ type: "story_log", id: "_git", line })
47703
48144
  );
47704
48145
  }
48146
+ await worktrees?.cleanupStaleOnStart();
47705
48147
  } : void 0,
47706
48148
  onBeforeStoryLaunch: librarian ? (storyId, story) => {
47707
48149
  const hints = [
@@ -47711,40 +48153,50 @@ async function orchestrate(config) {
47711
48153
  return librarian.gatherContext(storyId, hints);
47712
48154
  } : void 0,
47713
48155
  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
- });
48156
+ const log2 = (line) => emitTui && emit({ type: "story_log", id: storyId, line });
48157
+ if (worktrees) {
48158
+ let merged = false;
48159
+ try {
48160
+ merged = await worktrees.mergeBack(storyId);
48161
+ } catch (e2) {
48162
+ log2(`[git] merge-back failed; worktree preserved for recovery: ${e2?.message ?? String(e2)}`);
48163
+ if (emitTui) {
48164
+ emit({ type: "push_status", id: storyId, success: false, error: e2?.message ?? String(e2) });
48165
+ }
48166
+ return;
47731
48167
  }
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
- });
48168
+ if (merged) {
48169
+ await worktrees.cleanup(storyId);
48170
+ worktreePushNeeded = true;
48171
+ if (emitTui) {
48172
+ emit({ type: "push_status", id: storyId, success: true, error: null });
48173
+ }
48174
+ return;
47740
48175
  }
47741
48176
  }
47742
- } : void 0
48177
+ await safePullRebase(config.cwd, log2, gitGate);
48178
+ storyPushes.push(
48179
+ (async () => {
48180
+ try {
48181
+ await gitPushWithRetry(gitGate, { cwd: config.cwd, onLog: log2 });
48182
+ if (emitTui) {
48183
+ emit({ type: "push_status", id: storyId, success: true, error: null });
48184
+ }
48185
+ } catch (e2) {
48186
+ if (emitTui) {
48187
+ emit({ type: "push_status", id: storyId, success: false, error: e2?.message ?? String(e2) });
48188
+ }
48189
+ }
48190
+ })()
48191
+ );
48192
+ } : void 0,
48193
+ onStoryFailed: worktrees ? (storyId) => worktrees.cleanup(storyId) : void 0
47743
48194
  });
47744
48195
  conductor.setEnvironment(env);
47745
48196
  conductor.join(env);
47746
48197
  const storyFactory = new StoryFactory({
47747
48198
  cwd: config.cwd,
48199
+ worktrees: worktrees ?? void 0,
47748
48200
  llm: storyLlm,
47749
48201
  openaiModel: config.storyModel ?? "gpt-5.5",
47750
48202
  storyModelOverride: config.storyModel,
@@ -47776,6 +48228,17 @@ async function orchestrate(config) {
47776
48228
  RunStartRequest.create({ reason: "orchestrate" })
47777
48229
  );
47778
48230
  const summary = await conductor.done;
48231
+ await Promise.allSettled(storyPushes);
48232
+ if (worktreePushNeeded) {
48233
+ const log2 = (line) => emitTui && emit({ type: "story_log", id: "_git", line });
48234
+ try {
48235
+ await gitPushWithRetry(gitGate, { cwd: config.cwd, onLog: log2 });
48236
+ if (emitTui) emit({ type: "push_status", id: "_git", success: true, error: null });
48237
+ } catch (e2) {
48238
+ if (emitTui) emit({ type: "push_status", id: "_git", success: false, error: e2?.message ?? String(e2) });
48239
+ }
48240
+ }
48241
+ await worktrees?.cleanupAll();
47779
48242
  if (critic) await critic.idle();
47780
48243
  if (surgeon) await surgeon.idle();
47781
48244
  if (finalizer) await finalizer.complete();
@@ -48054,7 +48517,7 @@ async function main() {
48054
48517
  }
48055
48518
  const cwd = resolve3(args.cwd);
48056
48519
  const prdPath = resolve3(cwd, args.prd);
48057
- if (!existsSync5(prdPath)) {
48520
+ if (!existsSync6(prdPath)) {
48058
48521
  process.stderr.write(`[cli] PRD not found: ${prdPath}
48059
48522
  `);
48060
48523
  process.exit(2);