Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ Cross-package release notes for relayburn. Package changelogs contain package-le

### Changed

- `relayburn-cli` / `relayburn-sdk`: `burn summary` now accepts repeatable
`--tag k=v` filters and `--group-by-tag <key>` to report cost/tokens by
generic folded enrichment tags; Claude, Codex, and OpenCode pending stamps
can now be written by external launchers.
- `relayburn-sdk` (Rust): reader hot loops in `claude.rs` and `codex.rs` now stream JSONL line-by-line via `BufReader::read_until` instead of pre-allocating a `(size - start_offset)`-byte buffer up front; only the longest single line stays resident. `memchr_newline` in the codex parser now actually uses the `memchr` crate for SIMD-accelerated newline scanning. The main `parse_claude_session` loop also drops `BufReader::lines()` in favor of `read_line` into a reused `String`. (#323)

### Removed

- `relayburn-cli`: removed the `burn run` launcher wrapper from the CLI
surface. Launchers should write attribution with `writePendingStamp()` and
ingest through `burn ingest` / SDK `ingest()`.
- Removed the old TypeScript implementation packages from the workspace. The
Rust crates now own the SDK and CLI implementation, with npm packages kept
for the Node SDK facade, MCP server, and prebuilt CLI wrappers.
Expand Down
125 changes: 35 additions & 90 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ Burn stores data under `~/.agentworkforce/burn/` by default. Set
| [`burn hotspots`](#burn-hotspots) | Find expensive files, commands, and subagents. |
| [`burn overhead`](#burn-overhead) | Attribute cached prompt cost to `CLAUDE.md`, `.claude/CLAUDE.md`, and `AGENTS.md`. |
| [`burn compare`](#burn-compare) | Compare observed model performance by activity: cost per turn, one-shot rate, and sample size. |
| [`burn run`](#burn-run) | Spawn Claude, Codex, or OpenCode with attribution tags and automatic ingest. |
| [`burn ingest`](#burn-ingest) | Import existing or live session logs without wrapping the harness. |
| [`burn mcp-server`](#burn-mcp-server) | Expose read-only cost queries to an agent through stdio MCP. |
| [`burn state`](#burn-state) | Inspect, rebuild, and prune derived ledger artifacts. |
Expand All @@ -38,6 +37,8 @@ tokens they used, and what they cost.
| `--since <range>` | Limit to a relative range like `24h`, `7d`, or `4w`, or an ISO timestamp. |
| `--project <path>` | Limit to a project path or git-canonical project key. |
| `--session <id>` | Limit to one session. |
| `--tag k=v` | Filter by folded enrichment tag. Repeatable; all tags must match. |
| `--group-by-tag <key>` | Group totals by a folded enrichment tag value. |
| `--by-provider` | Group totals by provider instead of model. |
| `--json` | Emit machine-readable output. |

Expand All @@ -46,6 +47,8 @@ tokens they used, and what they cost.
| `burn summary` | All-time cost by model. |
| `burn summary --since 24h` | Cost from the last 24 hours. |
| `burn summary --by-provider` | Cost grouped by effective provider. |
| `burn summary --tag persona=code-reviewer` | Cost for sessions stamped with that persona tag. |
| `burn summary --group-by-tag persona` | Cost grouped by persona value. |

Synthetic-routed models are recognized from `hf:*`,
`accounts/fireworks/models/*`, and `synthetic/*`.
Expand Down Expand Up @@ -123,38 +126,6 @@ testing, review, exploration, docs, and refactoring.

Run `burn summary --by-provider` to discover model IDs present in your ledger.

## `burn run`

Use `burn run` when you want Burn to launch the harness, stamp metadata, watch
for session logs, and ingest turns as the child exits. Supported harnesses:
`claude`, `codex`, and `opencode`.

| Option | What it does |
|---|---|
| `<harness>` | Required harness: `claude`, `codex`, or `opencode`. |
| `--tag k=v` | Stamp metadata onto the session. Repeatable. |
| `-- <harness args>` | Pass everything after `--` to the underlying harness. |

| Example | Result |
|---|---|
| `burn run claude --tag workflow=refactor -- --resume` | Resume Claude and stamp `workflow=refactor`. |
| `burn run codex --tag workflow=refactor` | Run Codex with workflow attribution. |
| `burn run opencode --tag agentId=ag-42 --tag tier=best` | Run OpenCode with agent and tier tags. |

Burn also reads these attribution environment variables and re-exports them to
the child process:

| Env var | Stamp key |
|---|---|
| `RELAYBURN_WORKFLOW_ID` | `workflowId` |
| `RELAYBURN_STEP_ID` | `stepId` |
| `RELAYBURN_AGENT_ID` | `agentId` |
| `RELAYBURN_PARENT_AGENT_ID` | `parentAgentId` |
| `RELAYBURN_PERSONA` | `persona` |
| `RELAYBURN_TIER` | `tier` |

Explicit `--tag k=v` values win over environment-derived tags.

## `burn ingest`

Use `burn ingest` when sessions already exist, or when another process owns the
Expand Down Expand Up @@ -223,7 +194,7 @@ content/search data in `content.sqlite`.
| `~/.agentworkforce/burn/burn.sqlite` | Events, stamps, sessions, relationships, and archive metadata. |
| `~/.agentworkforce/burn/content.sqlite` | Content blobs and the FTS5 search index. |
| `~/.agentworkforce/burn/config.json` | Content-storage and retention configuration. |
| `~/.agentworkforce/burn/pending-stamps/` | Temporary manifests used by harnesses that do not expose a session ID before spawn. |
| `~/.agentworkforce/burn/pending-stamps/` | Temporary manifests used by launchers that do not expose a session ID before spawn. |
| `RELAYBURN_HOME` | Override the whole Burn data directory. |
| `RELAYBURN_SQLITE_PATH` | Override the events database path. |
| `RELAYBURN_CONTENT_PATH` | Override the content database path. |
Expand Down Expand Up @@ -311,9 +282,12 @@ ID, and the tier. Burn accepts that context, attaches it to the session, and
makes it queryable later alongside the usage data from the session log.

The primitive is **stamping**: attach metadata to a session by ID, before or
after any turns have been recorded. For command-line use, prefer `burn run
<harness> --tag k=v`; direct Rust embedders can use `relayburn_sdk::Stamp`
and `relayburn_sdk::StampSelector` against a `LedgerHandle`.
after any turns have been recorded. Launchers that do not know the session ID
before spawn should call `@relayburn/sdk` `writePendingStamp()` before
starting the agent, then run `burn ingest` / `ingest()` to fold the tags onto
the discovered turns. Direct Rust embedders with an exact session ID can use
`relayburn_sdk::Stamp` and `relayburn_sdk::StampSelector` against a
`LedgerHandle`.

Stamp selectors:

Expand All @@ -330,33 +304,35 @@ context and decides what to attach.

### Spawner-integrated ingest

All three supported harnesses - Claude, Codex, and OpenCode - ship under one
verb:
The recommended launcher integration is the Node SDK pending-stamp primitive:

```bash
burn run <claude|codex|opencode> [--tag k=v ...] [-- <harness args>]
```

For Claude, the adapter generates a session UUID up front so metadata can be
stamped before the agent starts. It passes `--session-id` to Claude, applies
any `--tag k=v` pairs as stamps, and ingests the session into the ledger when
Claude exits. If you are building an orchestrator, the same pattern applies:
generate the UUID, stamp first, spawn with the UUID, then let burn pick up the
session log on ingest.
```js
import { writePendingStamp } from "@relayburn/sdk";

```bash
burn run claude --tag workflow=refactor --tag persona=senior-eng -- --resume abc
await writePendingStamp({
harness: "codex",
cwd: process.cwd(),
enrichment: {
persona: "code-reviewer",
personaTier: "senior",
agentworkforce: "1",
},
});
```

Codex and OpenCode do not expose Claude-style hooks or a pre-spawn session ID.
Their adapters write a pending-stamp manifest under
`$RELAYBURN_HOME/pending-stamps/` before spawning, then resolve it against the
first matching session file before the first turn is appended. They also run
burn's foreground watch loop for the child process lifetime, so long sessions
become visible incrementally instead of only after exit. Abandoned pending
manifests are cleaned up after 24 hours.
Then spawn the harness normally and let `burn ingest` or `ingest()` scan the
session stores. Claude launchers can either preallocate `--session-id` and
write an exact session stamp from Rust, or use `writePendingStamp({ harness:
"claude", ... })` when the final session ID is not available before spawn.

For passive ingest without a wrapper, run:
Codex and OpenCode do not expose a pre-spawn session ID. `writePendingStamp()`
writes a pending-stamp manifest under `$RELAYBURN_HOME/pending-stamps/` before
the launcher spawns the agent. Ingest resolves the manifest against the first
matching session file before the first turn is appended. Claude launchers can
use the same pending-stamp path when the final session ID is not available
before spawn. Abandoned pending manifests are cleaned up after 24 hours.

For passive ingest, run:

```bash
burn ingest
Expand All @@ -367,37 +343,6 @@ burn ingest --watch [--interval <ms>]
cursor and dedup path as the reporting commands. `burn ingest --watch` keeps
that scan loop running in the foreground.

### Spawner env-var contract

For orchestrators that spawn many agent sessions, threading `--tag` through
every wrapper invocation is awkward. All three adapters also read a fixed set
of `RELAYBURN_*` env vars and fold them into the stamp bag:

| Env var | Stamp key |
|---|---|
| `RELAYBURN_WORKFLOW_ID` | `workflowId` |
| `RELAYBURN_STEP_ID` | `stepId` |
| `RELAYBURN_AGENT_ID` | `agentId` |
| `RELAYBURN_PARENT_AGENT_ID` | `parentAgentId` |
| `RELAYBURN_PERSONA` | `persona` |
| `RELAYBURN_TIER` | `tier` |

`--tag k=v` flags win on key collision. The merged values are re-exported on
the child harness environment under their canonical names, so a transitive
`burn ...` invocation inside the child session inherits the same context
without the orchestrator having to re-thread it.

```bash
export RELAYBURN_WORKFLOW_ID=wf-refactor-auth
export RELAYBURN_AGENT_ID=ag-42
burn run codex
burn run opencode --tag agentId=ag-43
```

Other `RELAYBURN_*` variables (`RELAYBURN_HOME`, `RELAYBURN_SESSION_ID`,
`RELAYBURN_CONTENT_STORE`, `RELAYBURN_CONTENT_TTL_DAYS`) are burn internals and
are not treated as stamp tags.

### Hook-based ingest for orchestrators

If your code already controls the Claude Code spawn, you can install burn's
Expand Down
27 changes: 9 additions & 18 deletions crates/relayburn-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ description = "The `burn` CLI — published to crates.io. Crate name is relaybur
name = "burn"
path = "src/main.rs"

# A library target is added alongside the `burn` binary so the harness
# substrate (`HarnessAdapter` trait, registry, pending-stamp factory)
# under `src/harnesses/` can be unit-tested with `cargo test -p
# relayburn-cli` and so future integration tests under `tests/` can
# reach it without re-declaring the module tree. The binary path
# A library target is added alongside the `burn` binary so internal
# modules can be unit-tested with `cargo test -p relayburn-cli` and so
# future integration tests under `tests/` can reach them without
# re-declaring the module tree. The binary path
# (`src/main.rs`) and the library path (`src/lib.rs`) live side-by-side
# — cargo treats them as two separate compile units that share the
# same package metadata.
Expand Down Expand Up @@ -66,25 +65,17 @@ serde_json = { workspace = true, features = ["preserve_order"] }
anyhow = { workspace = true }
thiserror = { workspace = true }

# Harness substrate (#248-b): the `HarnessAdapter` trait under `harnesses/`
# uses `async fn` in trait — `async-trait` desugars to `Pin<Box<…>>` futures
# without forcing every adapter to spell that out. `phf` macros give us a
# perfect-hash registry that's evaluated at compile time, so harness lookup
# costs nothing at startup. `tokio::sync` is needed for the watch controller
# wiring exposed by `relayburn-sdk` (`WatchController` holds a `Mutex`).
# `rt` is required by Wave 2 read-path commands so they can drive
# `relayburn_sdk::ingest_all` (async) from the otherwise-sync presenter
# bodies via a current-thread runtime.
# Harness substrate unit tests still use `async-trait` and `phf`. `tokio::sync`
# is needed by MCP / ingest presenters, and `rt` is required by read-path
# commands so they can drive `relayburn_sdk::ingest_all` from otherwise-sync
# presenter bodies via a current-thread runtime.
async-trait = "0.1"
phf = { version = "0.11", features = ["macros"] }
# `process` is needed by `commands/run.rs` so the `burn run` driver can
# `await` the child via `tokio::process::Command::status()` rather than
# blocking the current-thread runtime with `std::process::Command::status()`.
# `signal` is needed for the `burn ingest --watch` SIGINT/SIGTERM trap
# (#248 D8); the watch loop blocks the foreground until a stop signal
# comes in. `rt` drives the current-thread runtime that wraps the SDK's
# async ingest verb from otherwise-sync presenter bodies.
tokio = { workspace = true, features = ["sync", "rt", "process", "signal"] }
tokio = { workspace = true, features = ["sync", "rt", "signal"] }

# `IndexMap` preserves first-seen iteration order, which matters for the
# Wave 2 read-path commands so their grouped output (`summary --by-model`,
Expand Down
38 changes: 0 additions & 38 deletions crates/relayburn-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,6 @@ pub enum Command {
/// Compare cost across two or more models on the same workload.
Compare(CompareArgs),

/// Run an agent CLI under a harness wrapper that ingests its
/// session log on exit.
Run(RunArgs),

/// Inspect or rebuild derived state under `~/.agentworkforce/burn`.
State(StateArgs),

Expand Down Expand Up @@ -182,40 +178,6 @@ pub struct McpServerArgs {
pub debug: bool,
}

/// `burn run <harness> [--tag k=v ...] [-- <harness args>]` — flags +
/// trailing argv for the harness driver. Mirrors the TS surface in
/// `packages/cli/src/commands/run.ts`.
///
/// The first positional is the harness name (`claude`, `codex`,
/// `opencode`). Everything after `--` (or any unknown flag, courtesy of
/// `trailing_var_arg`) is captured into `passthrough` and forwarded to
/// the spawned binary verbatim. `--tag k=v` may be repeated; bad shapes
/// (no `=`, empty key) are rejected at runtime by the driver with the
/// same error message as the TS sibling.
#[derive(Debug, Clone, ClapArgs)]
pub struct RunArgs {
/// Lowercase harness identifier (`claude`, `codex`, `opencode`).
/// Optional so `burn run --help` and `burn run` both succeed; the
/// driver translates a missing name to a help-or-exit-2 outcome
/// matching the TS sibling.
#[arg(value_name = "HARNESS")]
pub harness: Option<String>,

/// User-supplied stamp enrichment. Repeatable — `--tag workflow=foo
/// --tag agent=bar` produces `{"workflow":"foo","agent":"bar"}` on
/// the resulting [`relayburn_sdk::Stamp`].
#[arg(long = "tag", value_name = "K=V")]
pub tag: Vec<String>,

/// Everything after the harness name (or `--`). Forwarded to the
/// spawned binary in `SpawnPlan::args` after the adapter's own
/// transport-level args. `trailing_var_arg = true` makes clap stop
/// option parsing at the first non-flag token so `burn run claude
/// --resume` works without an explicit `--`.
#[arg(trailing_var_arg = true, allow_hyphen_values = true, value_name = "ARGS")]
pub passthrough: Vec<String>,
}

/// Per-command flag set for `burn compare`. Mirrors
/// `packages/cli/src/commands/compare.ts` so the CLI surfaces match
/// byte-for-byte; see that file for the canonical help text.
Expand Down
2 changes: 2 additions & 0 deletions crates/relayburn-cli/src/commands/ingest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,13 +359,15 @@ mod tests {
scanned_sessions: 1,
ingested_sessions: 1,
appended_turns: 1,
applied_pending_stamps: 0,
});
assert_eq!(one, "[burn] ingest: ingested 1 session (+1 turn)\n");

let many = render_ingest_line(&IngestReport {
scanned_sessions: 3,
ingested_sessions: 2,
appended_turns: 5,
applied_pending_stamps: 0,
});
assert_eq!(many, "[burn] ingest: ingested 2 sessions (+5 turns)\n");

Expand Down
2 changes: 0 additions & 2 deletions crates/relayburn-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
//! - `hotspots` — wraps `relayburn_sdk::hotspots`
//! - `overhead` — wraps `relayburn_sdk::overhead` (+ `overhead trim`)
//! - `compare` — wraps `relayburn_sdk::compare`
//! - `run` — driver around `HarnessAdapter` (added in #248-b)
//! - `state` — status / rebuild / prune / reset
//! - `ingest` — no-flag, `--watch`, `--hook claude --quiet`
//! - `mcp_server` — rmcp wrapper around the SDK query verbs
Expand All @@ -24,6 +23,5 @@ pub mod hotspots;
pub mod ingest;
pub mod mcp_server;
pub mod overhead;
pub mod run;
pub mod state;
pub mod summary;
Loading
Loading