baro-ai 0.23.0 → 0.23.2
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/README.md +31 -7
- package/dist/cli.mjs +128 -31
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,17 @@ Give it a goal, it breaks it into stories, builds a dependency DAG, and runs the
|
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
11
|
+
> 📖 **Deep dive:** [Getting the Maximum Out of My Claude Code Subscription](https://jigjoy.ai/blog/getting-the-maximum-out-of-claude-code) — the story of why baro exists, how it pairs with Mozaik, and what it looks like in practice.
|
|
12
|
+
|
|
13
|
+
## What's new (0.22–0.23)
|
|
14
|
+
|
|
15
|
+
- **Opus as the default executor** — richer reasoning per story, still with routed Sonnet/Haiku available via `--model` or `.barorc`.
|
|
16
|
+
- **Smaller-stories planner** — the planner now biases toward narrower, more independent stories that parallelize better on the DAG.
|
|
17
|
+
- **Branch dedup** — reruns on the same goal reuse the existing `baro/<name>` branch instead of piling up duplicates.
|
|
18
|
+
- **TUI: terminal-clear on tab switch** — cleaner transitions between story logs, DAG view, and stats.
|
|
19
|
+
- **Audit log survives project resets** — JSONL event logs now live in `~/.baro/runs/` by default, so a wiped `node_modules` or a fresh clone doesn't lose history.
|
|
20
|
+
- **Always-on audit + abnormal-exit banner** — every run is recorded, and the TUI surfaces an explicit banner when the orchestrator exits unexpectedly.
|
|
21
|
+
|
|
11
22
|
## Install
|
|
12
23
|
|
|
13
24
|
```
|
|
@@ -54,7 +65,7 @@ baro --cwd ~/projects/myapp "Add REST API"
|
|
|
54
65
|
|
|
55
66
|
1. **Plan** — Claude (Opus) explores your codebase and generates a dependency graph of user stories
|
|
56
67
|
2. **Review** — You review the plan, refine with feedback, accept or quit
|
|
57
|
-
3. **Execute** — Stories run in parallel on a feature branch, each with its own Claude agent (Sonnet)
|
|
68
|
+
3. **Execute** — Stories run in parallel on a feature branch, each with its own Claude agent (Opus by default in 0.23+; Sonnet/Haiku available via `--model` or `.barorc`)
|
|
58
69
|
4. **Review Agent** — After each level, a review agent (Haiku) checks work against acceptance criteria and creates fix stories if needed
|
|
59
70
|
5. **Finalize** — Runs build verification and creates a GitHub PR with full summary
|
|
60
71
|
|
|
@@ -62,24 +73,25 @@ baro --cwd ~/projects/myapp "Add REST API"
|
|
|
62
73
|
|
|
63
74
|
- **Parallel execution** — independent stories run simultaneously, respecting dependency order
|
|
64
75
|
- **DAG engine** — topological sort with level grouping, cycle detection
|
|
65
|
-
- **Model routing** — Opus for planning
|
|
76
|
+
- **Model routing** — Opus for planning and execution (0.23+ default), Haiku for review (configurable)
|
|
66
77
|
- **Live TUI** — dashboard with story status, live agent logs, DAG view, stats
|
|
67
78
|
- **Review agent** — automated code review between levels with build detection and auto-fix
|
|
68
79
|
- **Plan refinement** — press `r` on review screen to give feedback and regenerate the plan
|
|
69
80
|
- **Build detection** — auto-detects project type (Cargo, npm, Go, Python, Make) and runs builds during review
|
|
70
81
|
- **Git coordination** — mutex-protected commits, auto-push with retry, pull --rebase, conflict detection
|
|
71
|
-
- **Branch per run** — creates `baro/<name>` branch, keeps main clean
|
|
82
|
+
- **Branch per run** — creates `baro/<name>` branch, keeps main clean, reuses existing branches on rerun (0.23+)
|
|
72
83
|
- **Dry run** — `--dry-run` generates plan and saves to `prd.json` without executing, then `--resume` to run it
|
|
73
84
|
- **Resume** — detects `prd.json` and resumes incomplete executions
|
|
74
85
|
- **PR creation** — creates GitHub PR with stories table, stats, time saved, and review summary
|
|
75
86
|
- **Configurable parallelism** — `--parallel N` to limit concurrent story execution
|
|
76
|
-
- **Story timeout** — `--timeout SECONDS` kills stuck agents (default: 10 minutes)
|
|
87
|
+
- **Story timeout** — `--timeout SECONDS` kills stuck agents (default: 10 minutes, hard timeout disabled in 0.22+)
|
|
77
88
|
- **Time saved** — shows parallel speedup vs sequential execution
|
|
78
89
|
- **System notifications** — terminal bell + OS notification (macOS/Linux/Windows) when done
|
|
79
90
|
- **Retry logic** — failed stories retry automatically (configurable per story)
|
|
80
91
|
- **Interactive settings** — configure model, parallelism, timeout, context, and planner on the welcome screen with Tab/arrow keys
|
|
81
92
|
- **Project config** — `.barorc` file in project root sets defaults (no CLI flags needed)
|
|
82
93
|
- **Session lock** — prevents multiple baro instances from running in the same directory
|
|
94
|
+
- **Audit log** — every bus event written to `~/.baro/runs/<run-id>.jsonl`
|
|
83
95
|
|
|
84
96
|
## Config file
|
|
85
97
|
|
|
@@ -207,16 +219,28 @@ Ten participants share that bus:
|
|
|
207
219
|
| `StoryAgent` | Runs one story via Claude CLI, with retries and timeout |
|
|
208
220
|
| `Librarian` | Cross-agent memory — indexes outputs of exploration tools |
|
|
209
221
|
| `Sentry` | Flags overlapping file writes across concurrent stories |
|
|
210
|
-
| `Critic` | Per-turn acceptance-criteria evaluator (
|
|
211
|
-
| `Surgeon` | Emits DAG replans when a story fails terminally (
|
|
222
|
+
| `Critic` | Per-turn acceptance-criteria evaluator (default ON, `--no-critic` to disable) |
|
|
223
|
+
| `Surgeon` | Emits DAG replans when a story fails terminally (default ON, `--no-surgeon` to disable) |
|
|
212
224
|
| `Operator` | Bridges external user commands (TUI, web UI) into bus events |
|
|
213
|
-
| `Auditor` | JSONL log of every event on the bus
|
|
225
|
+
| `Auditor` | JSONL log of every event on the bus (written to `~/.baro/runs/`) |
|
|
214
226
|
| `Cartographer` | Translates bus events into UI frames for the Rust TUI |
|
|
215
227
|
|
|
216
228
|
The bus is open. New participants — CI deployers, Slack notifiers,
|
|
217
229
|
external ticket triggers — are subscribers and emitters with no changes
|
|
218
230
|
to the orchestrator.
|
|
219
231
|
|
|
232
|
+
## Status & feedback
|
|
233
|
+
|
|
234
|
+
baro is a work in progress. I'm actively adding things, testing ideas,
|
|
235
|
+
and occasionally breaking them — if a run explodes, an [issue on
|
|
236
|
+
GitHub](https://github.com/Lotus015/baro/issues) with the run's audit
|
|
237
|
+
log from `~/.baro/runs/` is the fastest way to get it fixed.
|
|
238
|
+
|
|
239
|
+
If you like the idea and want to help shape where it goes, PRs are
|
|
240
|
+
welcome, and you can DM me on Twitter
|
|
241
|
+
[@lotus_sbc](https://twitter.com/lotus_sbc) with ideas, use cases, or
|
|
242
|
+
bug reports.
|
|
243
|
+
|
|
220
244
|
## License
|
|
221
245
|
|
|
222
246
|
MIT
|
package/dist/cli.mjs
CHANGED
|
@@ -7414,32 +7414,53 @@ async function createOrCheckoutBranch(cwd, branchName, onLog) {
|
|
|
7414
7414
|
);
|
|
7415
7415
|
}
|
|
7416
7416
|
}
|
|
7417
|
-
async function safePullRebase(cwd, onLog) {
|
|
7418
|
-
|
|
7419
|
-
onLog?.("[git] no remote, skipping pull");
|
|
7420
|
-
return;
|
|
7421
|
-
}
|
|
7422
|
-
let branch;
|
|
7423
|
-
try {
|
|
7424
|
-
branch = await getCurrentBranch(cwd);
|
|
7425
|
-
} catch {
|
|
7426
|
-
onLog?.("[git] no branch, skipping pull");
|
|
7427
|
-
return;
|
|
7428
|
-
}
|
|
7429
|
-
if (!await hasRemoteBranch(cwd, branch)) {
|
|
7430
|
-
onLog?.("[git] remote branch not found, skipping pull");
|
|
7431
|
-
return;
|
|
7432
|
-
}
|
|
7433
|
-
onLog?.("[git] pulling latest...");
|
|
7434
|
-
await execSafe("git", ["stash", "--include-untracked"], { cwd });
|
|
7417
|
+
async function safePullRebase(cwd, onLog, gate) {
|
|
7418
|
+
const release = gate ? await gate.acquire() : null;
|
|
7435
7419
|
try {
|
|
7436
|
-
await
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7420
|
+
if (!await hasRemoteOrigin(cwd)) {
|
|
7421
|
+
onLog?.("[git] no remote, skipping pull");
|
|
7422
|
+
return;
|
|
7423
|
+
}
|
|
7424
|
+
let branch;
|
|
7425
|
+
try {
|
|
7426
|
+
branch = await getCurrentBranch(cwd);
|
|
7427
|
+
} catch {
|
|
7428
|
+
onLog?.("[git] no branch, skipping pull");
|
|
7429
|
+
return;
|
|
7430
|
+
}
|
|
7431
|
+
if (!await hasRemoteBranch(cwd, branch)) {
|
|
7432
|
+
onLog?.("[git] remote branch not found, skipping pull");
|
|
7433
|
+
return;
|
|
7434
|
+
}
|
|
7435
|
+
onLog?.("[git] pulling latest...");
|
|
7436
|
+
let stashSha = null;
|
|
7437
|
+
try {
|
|
7438
|
+
const { stdout } = await exec("git", ["stash", "create"], { cwd });
|
|
7439
|
+
stashSha = stdout.trim() || null;
|
|
7440
|
+
if (stashSha) {
|
|
7441
|
+
await execSafe("git", ["reset", "--hard", "HEAD"], { cwd });
|
|
7442
|
+
}
|
|
7443
|
+
} catch {
|
|
7444
|
+
}
|
|
7445
|
+
try {
|
|
7446
|
+
await exec("git", ["pull", "--rebase", "origin", branch], { cwd });
|
|
7447
|
+
onLog?.("[git] pull ok");
|
|
7448
|
+
} catch {
|
|
7449
|
+
onLog?.("[git] pull conflict, continuing without pull");
|
|
7450
|
+
await execSafe("git", ["rebase", "--abort"], { cwd });
|
|
7451
|
+
}
|
|
7452
|
+
if (stashSha) {
|
|
7453
|
+
try {
|
|
7454
|
+
await exec("git", ["stash", "apply", stashSha], { cwd });
|
|
7455
|
+
} catch (e) {
|
|
7456
|
+
onLog?.(
|
|
7457
|
+
`[git] could not re-apply stashed edits (sha ${stashSha.slice(0, 8)}): ${e?.message ?? String(e)}`
|
|
7458
|
+
);
|
|
7459
|
+
}
|
|
7460
|
+
}
|
|
7461
|
+
} finally {
|
|
7462
|
+
release?.();
|
|
7441
7463
|
}
|
|
7442
|
-
await execSafe("git", ["stash", "pop"], { cwd });
|
|
7443
7464
|
}
|
|
7444
7465
|
async function gitPushWithRetry(gate, options) {
|
|
7445
7466
|
const release = await gate.acquire();
|
|
@@ -7451,8 +7472,8 @@ async function gitPushWithRetry(gate, options) {
|
|
|
7451
7472
|
const branch = await getCurrentBranch(options.cwd);
|
|
7452
7473
|
const max = options.maxAttempts ?? GIT_PUSH_MAX_ATTEMPTS;
|
|
7453
7474
|
let lastError = "";
|
|
7475
|
+
options.onLog?.("[git] pushing...");
|
|
7454
7476
|
for (let attempt = 1; attempt <= max; attempt++) {
|
|
7455
|
-
options.onLog?.("[git] pushing...");
|
|
7456
7477
|
try {
|
|
7457
7478
|
await exec("git", ["push", "origin", branch], { cwd: options.cwd });
|
|
7458
7479
|
options.onLog?.("[git] push ok");
|
|
@@ -7461,7 +7482,9 @@ async function gitPushWithRetry(gate, options) {
|
|
|
7461
7482
|
lastError = extractStderr(e);
|
|
7462
7483
|
}
|
|
7463
7484
|
if (attempt === max) break;
|
|
7464
|
-
options.onLog?.(
|
|
7485
|
+
options.onLog?.(
|
|
7486
|
+
`[git] push rejected (attempt ${attempt}/${max}), pulling and retrying...`
|
|
7487
|
+
);
|
|
7465
7488
|
try {
|
|
7466
7489
|
await exec("git", ["pull", "--rebase", "origin", branch], {
|
|
7467
7490
|
cwd: options.cwd
|
|
@@ -7472,7 +7495,10 @@ async function gitPushWithRetry(gate, options) {
|
|
|
7472
7495
|
throw new Error("Rebase conflict detected, push skipped");
|
|
7473
7496
|
}
|
|
7474
7497
|
}
|
|
7475
|
-
|
|
7498
|
+
const compactErr = lastError.split("\n")[0]?.trim() || lastError;
|
|
7499
|
+
options.onLog?.(
|
|
7500
|
+
`[git] push failed after ${max} attempts: ${compactErr}`
|
|
7501
|
+
);
|
|
7476
7502
|
throw new Error(`Push failed after ${max} attempts: ${lastError}`);
|
|
7477
7503
|
} finally {
|
|
7478
7504
|
release();
|
|
@@ -7921,14 +7947,26 @@ var Auditor = class extends Participant {
|
|
|
7921
7947
|
path;
|
|
7922
7948
|
skipStreamChunks;
|
|
7923
7949
|
filter;
|
|
7950
|
+
/**
|
|
7951
|
+
* Flips to true the first time a write fails (e.g. EACCES because
|
|
7952
|
+
* `~/.baro/runs/` is root-owned from a sudo install). Once disabled,
|
|
7953
|
+
* subsequent items are dropped silently — losing the audit log is
|
|
7954
|
+
* better than crashing the orchestrator on every bus event.
|
|
7955
|
+
*/
|
|
7956
|
+
disabled = false;
|
|
7924
7957
|
constructor(opts) {
|
|
7925
7958
|
super();
|
|
7926
7959
|
this.path = opts.path;
|
|
7927
7960
|
this.skipStreamChunks = opts.skipStreamChunks ?? true;
|
|
7928
7961
|
this.filter = opts.filter;
|
|
7929
|
-
|
|
7962
|
+
try {
|
|
7963
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
7964
|
+
} catch (e) {
|
|
7965
|
+
this.disable(`mkdir failed: ${e?.message ?? String(e)}`);
|
|
7966
|
+
}
|
|
7930
7967
|
}
|
|
7931
7968
|
async onContextItem(source, item) {
|
|
7969
|
+
if (this.disabled) return;
|
|
7932
7970
|
if (this.skipStreamChunks && item instanceof ClaudeStreamChunkItem) {
|
|
7933
7971
|
return;
|
|
7934
7972
|
}
|
|
@@ -7940,7 +7978,19 @@ var Auditor = class extends Participant {
|
|
|
7940
7978
|
source: this.sourceLabel(source),
|
|
7941
7979
|
item: item.toJSON()
|
|
7942
7980
|
};
|
|
7943
|
-
|
|
7981
|
+
try {
|
|
7982
|
+
appendFileSync(this.path, JSON.stringify(entry) + "\n");
|
|
7983
|
+
} catch (e) {
|
|
7984
|
+
this.disable(`append failed: ${e?.message ?? String(e)}`);
|
|
7985
|
+
}
|
|
7986
|
+
}
|
|
7987
|
+
disable(reason) {
|
|
7988
|
+
if (this.disabled) return;
|
|
7989
|
+
this.disabled = true;
|
|
7990
|
+
process.stderr.write(
|
|
7991
|
+
`[auditor] cannot write audit log at ${this.path}: ${reason} \u2014 continuing without audit
|
|
7992
|
+
`
|
|
7993
|
+
);
|
|
7944
7994
|
}
|
|
7945
7995
|
sourceLabel(source) {
|
|
7946
7996
|
const ctor = source.constructor.name;
|
|
@@ -8153,7 +8203,7 @@ function stringifyToolResultContent(content) {
|
|
|
8153
8203
|
}
|
|
8154
8204
|
|
|
8155
8205
|
// ../baro-orchestrator/src/participants/claude-cli-participant.ts
|
|
8156
|
-
var ClaudeCliParticipant = class extends Participant {
|
|
8206
|
+
var ClaudeCliParticipant = class _ClaudeCliParticipant extends Participant {
|
|
8157
8207
|
constructor(agentId, opts) {
|
|
8158
8208
|
super();
|
|
8159
8209
|
this.agentId = agentId;
|
|
@@ -8172,6 +8222,22 @@ var ClaudeCliParticipant = class extends Participant {
|
|
|
8172
8222
|
this.resolveDone = res;
|
|
8173
8223
|
});
|
|
8174
8224
|
}
|
|
8225
|
+
/**
|
|
8226
|
+
* Process-wide registry of every Claude child currently running.
|
|
8227
|
+
* Used by the orchestrator's SIGINT/SIGTERM handlers to nuke
|
|
8228
|
+
* orphaned Claude processes so a killed baro doesn't leave a swarm
|
|
8229
|
+
* of background agents burning quota.
|
|
8230
|
+
*/
|
|
8231
|
+
static active = /* @__PURE__ */ new Set();
|
|
8232
|
+
/** Send a signal to every active Claude child. Idempotent. */
|
|
8233
|
+
static killAll(signal = "SIGTERM") {
|
|
8234
|
+
for (const p of _ClaudeCliParticipant.active) {
|
|
8235
|
+
try {
|
|
8236
|
+
p.proc?.kill(signal);
|
|
8237
|
+
} catch {
|
|
8238
|
+
}
|
|
8239
|
+
}
|
|
8240
|
+
}
|
|
8175
8241
|
options;
|
|
8176
8242
|
proc = null;
|
|
8177
8243
|
buffer = "";
|
|
@@ -8221,6 +8287,7 @@ var ClaudeCliParticipant = class extends Participant {
|
|
|
8221
8287
|
return;
|
|
8222
8288
|
}
|
|
8223
8289
|
this.proc = proc;
|
|
8290
|
+
_ClaudeCliParticipant.active.add(this);
|
|
8224
8291
|
this.transition("starting");
|
|
8225
8292
|
proc.stdout.setEncoding("utf8");
|
|
8226
8293
|
proc.stderr.setEncoding("utf8");
|
|
@@ -8231,6 +8298,7 @@ var ClaudeCliParticipant = class extends Participant {
|
|
|
8231
8298
|
this.rejectReady(err);
|
|
8232
8299
|
});
|
|
8233
8300
|
proc.on("exit", (code) => {
|
|
8301
|
+
_ClaudeCliParticipant.active.delete(this);
|
|
8234
8302
|
this.exitCode = code;
|
|
8235
8303
|
const finalPhase = this.spawnError != null || code != null && code !== 0 ? "failed" : "done";
|
|
8236
8304
|
this.transition(finalPhase, code != null ? `exit code ${code}` : "no exit code");
|
|
@@ -9885,7 +9953,8 @@ async function orchestrate(config) {
|
|
|
9885
9953
|
onStoryPassed: useGit ? async (storyId) => {
|
|
9886
9954
|
await safePullRebase(
|
|
9887
9955
|
config.cwd,
|
|
9888
|
-
(line) => emitTui && emit({ type: "story_log", id: storyId, line })
|
|
9956
|
+
(line) => emitTui && emit({ type: "story_log", id: storyId, line }),
|
|
9957
|
+
gitGate
|
|
9889
9958
|
);
|
|
9890
9959
|
try {
|
|
9891
9960
|
await gitPushWithRetry(gitGate, {
|
|
@@ -10303,14 +10372,42 @@ process.on("unhandledRejection", (reason) => {
|
|
|
10303
10372
|
const stack = reason?.stack ?? String(reason);
|
|
10304
10373
|
process.stderr.write(`[cli] unhandledRejection: ${stack}
|
|
10305
10374
|
`);
|
|
10375
|
+
ClaudeCliParticipant.killAll("SIGTERM");
|
|
10306
10376
|
process.exit(1);
|
|
10307
10377
|
});
|
|
10308
10378
|
process.on("uncaughtException", (err) => {
|
|
10309
10379
|
const stack = err?.stack ?? String(err);
|
|
10310
10380
|
process.stderr.write(`[cli] uncaughtException: ${stack}
|
|
10311
10381
|
`);
|
|
10382
|
+
ClaudeCliParticipant.killAll("SIGTERM");
|
|
10312
10383
|
process.exit(1);
|
|
10313
10384
|
});
|
|
10385
|
+
var shuttingDown = false;
|
|
10386
|
+
function shutdown(signal) {
|
|
10387
|
+
if (shuttingDown) return;
|
|
10388
|
+
shuttingDown = true;
|
|
10389
|
+
process.stderr.write(`[cli] received ${signal}, killing in-flight Claude children...
|
|
10390
|
+
`);
|
|
10391
|
+
ClaudeCliParticipant.killAll("SIGTERM");
|
|
10392
|
+
setTimeout(() => {
|
|
10393
|
+
ClaudeCliParticipant.killAll("SIGKILL");
|
|
10394
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
10395
|
+
}, 1500).unref();
|
|
10396
|
+
}
|
|
10397
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
10398
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
10399
|
+
var initialPpid = process.ppid;
|
|
10400
|
+
var orphanWatchdog = setInterval(() => {
|
|
10401
|
+
if (process.ppid !== initialPpid) {
|
|
10402
|
+
process.stderr.write(
|
|
10403
|
+
`[cli] parent died (ppid ${initialPpid} \u2192 ${process.ppid}), shutting down
|
|
10404
|
+
`
|
|
10405
|
+
);
|
|
10406
|
+
clearInterval(orphanWatchdog);
|
|
10407
|
+
shutdown("SIGTERM");
|
|
10408
|
+
}
|
|
10409
|
+
}, 1e3);
|
|
10410
|
+
orphanWatchdog.unref();
|
|
10314
10411
|
main().catch((e) => {
|
|
10315
10412
|
process.stderr.write(`[cli] unhandled: ${e?.stack ?? String(e)}
|
|
10316
10413
|
`);
|