relayburn-cli: burn ingest + burn mcp-server (#248 D8, closes #210)#319
relayburn-cli: burn ingest + burn mcp-server (#248 D8, closes #210)#319willwashburn merged 3 commits intomainfrom
Conversation
Wire `burn ingest` as a thin presenter over `relayburn_sdk::ingest_all` plus the SDK's `start_watch_loop` controller. Three modes mirror the TS sibling at `packages/cli/src/commands/ingest.ts`: - No flags: one full sweep, then exit. Logs `[burn] ingest: ingested N session(s) (+M turn(s))` on stderr. - `--watch [--interval MS]`: foreground poll loop. Skips empty-tick log lines (TS shape); SIGINT / SIGTERM stop drains in-flight ticks before returning. - `--hook claude [--quiet]`: stdin-driven Claude hook entrypoint. Validates the `session_id` / `transcript_path` payload shape, then drives a full sweep (cursor short-circuit means cost is bounded by new turns, not the hook payload). Failure paths log + exit 0 to avoid blocking the parent Claude tool call. Wire `burn mcp-server` as a hand-rolled JSON-RPC 2.0 stdio server, mirroring `packages/mcp/src/server.ts` rather than depending on `rmcp`. The on-wire surface (`initialize`, `ping`, `tools/list`, `tools/call`) is small enough that a tight in-tree implementation beats freezing a heavy SDK version. Single tool ships in this PR: `burn__sessionCost`, a 1:1 port of `packages/mcp/src/tools/session-cost.ts` delegating to `LedgerHandle::session_cost`. Future tools (summary, hotspots, …) land separately. Closes #210. Smoke test drops `ingest` and `mcp-server` from `UNIMPLEMENTED_SUBCOMMANDS`; `--help` for both subcommands still passes the existing help-shape assertion. Manual stdio handshake verified: `initialize` echoes the client's `protocolVersion`, `tools/list` returns `burn__sessionCost`, and `tools/call burn__sessionCost` returns the SDK payload as both stringified `text` content and a `structuredContent` mirror.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe PR implements ChangesCLI & Command Implementation
Sequence DiagramssequenceDiagram
participant CLI as CLI (burn ingest)
participant Dispatcher as Dispatcher
participant Ingest as Ingest Handler
participant Ledger as Ledger
participant SDK as SDK
participant SignalHandler as Signal Handler
participant Output as Stderr
CLI->>Dispatcher: Command::Ingest(IngestArgs)
Dispatcher->>Ingest: run(&globals, args)
alt Watch Mode
Ingest->>Ledger: open_handle()
Ledger-->>Ingest: LedgerHandle
Ingest->>SDK: start_watch_loop(ledger, interval)
loop Each tick
SDK->>SDK: scan_sessions() + ingest_all()
SDK-->>Ingest: {appended_turns, ...}
alt appended_turns > 0
Ingest->>Output: log "[burn] ingest: ingested X (+Y)"
end
end
Ingest->>SignalHandler: wait_for_signal()
SignalHandler-->>Ingest: SIGINT/SIGTERM
Ingest->>SDK: stop_controller()
Ingest->>Output: exit 0
else Hook Mode
Ingest->>Ingest: read_stdin()
Ingest->>Ingest: validate JSON {session_id, transcript_path}
Ingest->>SDK: ingest_all(ledger, session)
Ingest->>Output: log report (if appended_turns > 0)
Ingest->>Output: exit 0
else One-Shot Mode
Ingest->>Ledger: open_handle()
Ingest->>SDK: ingest_all(ledger)
Ingest->>Output: log "[burn] ingest: ingested X (+Y)"
Ingest->>Output: exit 0 or non-zero on error
end
sequenceDiagram
participant Harness as Harness Process
participant MCP as MCP Server
participant Stdin as Stdin Reader (blocking)
participant Channel as Async Channel
participant Handler as Request Handler
participant SDK as SDK (Ledger)
participant Stdout as Stdout Writer
Harness->>MCP: spawn burn mcp-server
MCP->>SDK: open_handle()
SDK-->>MCP: LedgerHandle(Arc<Mutex>)
MCP->>MCP: build current-thread Tokio runtime
par Stdin Reading
Stdin->>Channel: send line-delimited JSON-RPC frames
and Request Dispatch
Handler->>Channel: recv frame
alt Valid JSON-RPC Request
Handler->>Handler: parse & validate
alt Known Method
alt tools/call (burn__sessionCost)
Handler->>SDK: session_cost(args)
SDK-->>Handler: {result} or tool-error
Handler->>Stdout: write_response(success with payload / isError)
end
Handler->>Stdout: write_response(success)
else Unknown Method
Handler->>Stdout: write_response(JSON-RPC error -32601)
end
else Invalid JSON
Handler->>Stdout: write_response(JSON-RPC error -32700)
else Notification (no id)
Handler->>Handler: suppress response
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a1b4660e40
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if quiet && report.appended_turns == 0 { | ||
| return; | ||
| } | ||
| eprint!("{}", render_ingest_line(report)); |
There was a problem hiding this comment.
Route one-shot ingest report to stdout
burn ingest in non-watch/non-hook mode now emits its summary via stderr (log_report), but the TS source-of-truth prints that report to stdout in runIngestOnce (packages/cli/src/commands/ingest.ts, lines 121-126). This breaks parity for callers that capture stdout (e.g., shell pipelines/automation that parse the ingest summary) and makes the Rust command behave differently from the existing CLI contract in the default mode.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 3ed58fa. The one-shot path now routes its [burn] ingest: ingested N session(s) (+M turn(s)) line to stdout via a new log_report_oneshot helper that wraps print! (mirroring TS runIngestOnce at packages/cli/src/commands/ingest.ts:121-126 which uses process.stdout.write). --watch keeps its existing eprint!-driven banner + tick lines on stderr (TS sibling logs the same surface to stderr), and --hook claude keeps its breadcrumbs on stderr (matches TS hook behavior + the pending-stamp protocol).
Smoke-tested in an isolated HOME:
burn ingest > /tmp/out 2>/tmp/err→/tmp/outhas[burn] ingest: ingested 0 sessions (+0 turns),/tmp/erris empty.burn ingest --watch --interval 500(SIGINT) →foreground ingest every 500ms; Ctrl-C to stoplands on stderr, stdout empty.burn ingest --hook claudewith valid stdin → breadcrumb stays on stderr.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/relayburn-cli/src/cli.rs`:
- Around line 160-164: The --quiet flag is currently declared as a global pub
quiet field in the CLI struct (cli.rs) and is being parsed for one-shot and
--watch invocations; remove that global pub quiet and instead add a hook-scoped
flag on the hook-specific arguments (e.g., the struct or enum variant used for
the "hook" path under the ingest command—add a quiet: bool field to the
HookOptions/HookArgs or the claude hook variant). Update the code paths that
read the flag (the hook invocation handler / run_hook / ingest_hook function) to
read the new hook-scoped field and remove any usage that depended on the removed
global field so only burn ingest --hook [--quiet] affects hook logging.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 1d445569-3529-46f5-a125-b5ae31fa158f
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (7)
CHANGELOG.mdcrates/relayburn-cli/Cargo.tomlcrates/relayburn-cli/src/cli.rscrates/relayburn-cli/src/commands/ingest.rscrates/relayburn-cli/src/commands/mcp_server.rscrates/relayburn-cli/src/main.rscrates/relayburn-cli/tests/smoke.rs
# Conflicts: # CHANGELOG.md
…et hook scope) - Merge origin/main (D6 codex landed); CHANGELOG additive, registry/mod auto-merged with codex slot from main. - Codex P2: route burn ingest one-shot summary to stdout (matches TS runIngestOnce at packages/cli/src/commands/ingest.ts:121-126); --watch banner + --hook breadcrumbs stay on stderr. - CodeRabbit Minor: scope --quiet to --hook via clap requires = "hook" (mirrors the --reingest requires = "force" pattern from #313). Standalone --quiet and --watch --quiet are now rejected at parse time.
Absorbs #319 (Wave 2 D8 — burn ingest + burn mcp-server) on top of D5's prior merge with origin/main. Resolutions: - crates/relayburn-cli/src/cli.rs — auto-merged: keep all eight typed Command variants (D5's Run + D8's Ingest/McpServer + the four already typed by D1-D4 + State from D4). - crates/relayburn-cli/src/main.rs — auto-merged: dispatch arms for all eight subcommands route to typed handlers; no more not_yet_implemented fallbacks anywhere. - crates/relayburn-cli/Cargo.toml — union the tokio feature flags from both branches: D5 needed `process` (tokio::process::Command::status for the run driver), D8 needed `signal` (SIGINT/SIGTERM trap in --watch). Final feature set: ["sync", "rt", "process", "signal"]. - crates/relayburn-cli/tests/smoke.rs — every Wave 2 subcommand is now wired, so UNIMPLEMENTED_SUBCOMMANDS becomes [] and the each_stub_exits_one_with_not_yet_implemented_message iteration helper becomes a no-op pass over an empty slice (constant kept so a future scaffold has somewhere to land). Also pivoted json_mode_emits_error_envelope_on_unimplemented to use `compare`'s no-models error path (still routes through report_error so the JSON envelope contract is exercised); renamed it json_mode_emits_error_envelope_on_argument_failure. - crates/relayburn-cli/src/commands/mod.rs — `not_yet_implemented` is no longer called by any subcommand; added `#[allow(dead_code)]` so the placeholder helper survives without warning. - CHANGELOG.md — additive: keep both D5 and D8 bullets under [Unreleased]. - Cargo.lock — refreshed via `git checkout origin/main -- Cargo.lock && cargo build --workspace`. Tokio's `signal` feature pulled in signal-hook-registry / mio extensions cleanly. Verified: cargo build --workspace clean; cargo test --workspace green (610 SDK + 9 sdk-node + 10 cli smoke + 5 cli golden); BURN_GOLDEN=1 cargo test --test golden -p relayburn-cli passes byte-for-byte.
Summary
burn ingest(no-flag scan,--watchpoll loop,--hook claude --quiet) as a thin presenter overrelayburn_sdk::ingest_all+start_watch_loop. TS source-of-truth:packages/cli/src/commands/ingest.ts.burn mcp-serveras a hand-rolled JSON-RPC 2.0 stdio server exposingburn__sessionCost. Mirrorspackages/mcp/src/server.ts+packages/mcp/src/tools/session-cost.ts1:1; closes mcp: refactor@relayburn/mcpas a thin wrapper overburn <verb> --jsonso the MCP surface tracks the CLI automatically #210 (the standalone-MCP-server question).ingestandmcp-serverfromUNIMPLEMENTED_SUBCOMMANDSin the CLI smoke test. (runstays — D5 owns its removal.)Notes
rmcp. The on-wire surface is tiny (initialize/ping/tools/list/tools/call), the TS sibling already hand-rolls the same shape, and freezing a heavy SDK version buys nothing for the read-only surface this command exposes today. If/when we need richer transports, this module is localized enough to swap in one place.burn__sessionCostonly.burn__summary/burn__hotspotsare tracked as follow-ups so the scope of D8 stays tight.--hook claudemode validates the payload shape and (today) drives a fullingest_allsweep — the SDK does not yet expose a single-transcript verb. The cursor short-circuit makes this no slower than the TS hook in practice; a single-transcript SDK verb is a clean follow-up if needed.signaltotokiofeatures inrelayburn-cli/Cargo.tomlso the--watchmode can trap SIGINT / SIGTERM and drain in-flight ticks before exiting.Test plan
cargo build --workspaceclean.cargo test --workspacegreen (8 CLI smoke tests, 610 SDK unit tests, doc-tests).burn ingest --helplists--watch,--interval,--hook,--quiet.burn ingest(no flags, isolatedHOME) prints[burn] ingest: ingested 0 sessions (+0 turns)and exits 0.burn ingest --watch --interval 500 --quietaccepts SIGINT, exits 0.burn ingest --watch --hook clauderejects with exit 2 + the canonical message.burn ingest --hook codexrejects with exit 2 (unsupported harness).burn ingest --hook claudewith valid stdin payload exits 0; empty stdin emits thenothing to dobreadcrumb and exits 0.burn mcp-server --helplists--session-id,--debug,--ledger-path.initialize+tools/list+tools/call burn__sessionCost+pingover stdin returns the expected JSON-RPC frames;tools/callwith nosessionIdand no--session-idreturns the descriptive"no session id provided and server was not registered with one"note.