Skip to content
Open
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
3 changes: 3 additions & 0 deletions .github/workflows/_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ on:
OPENAI_API_KEY:
description: "For the post-deploy codex smoke test; skipped if unset."
required: false
CODEX_HOOK_TRUST_CONFIG:
description: "Reviewed Codex hook hashes for the deployed artifact; smoke skips if unset."
required: false

permissions:
contents: write
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ jobs:
"@opencode-ai/sdk@${PEER_VERSION}"
node -e 'import(process.argv[1]).then((m) => { if (typeof m.default !== "function") process.exit(1) })' \
"$PWD/install-test/node_modules/@braintrust/trace-opencode/dist/index.mjs"
(cd install-test && node -e 'import("@braintrust/trace-opencode/tracing").then((m) => { if (typeof m.default !== "function") process.exit(1) })')

resolve-pi-compatibility:
name: Resolve Pi compatibility window
Expand Down Expand Up @@ -207,7 +208,7 @@ jobs:
tarball="$(find "$package_dir" -name 'braintrust-trace-opencode-*.tgz' -print -quit)"
npm install --prefix "$install_dir" --no-audit --no-fund \
"$tarball" @opencode-ai/plugin@latest @opencode-ai/sdk@latest
echo "OPENCODE_PLUGIN=$install_dir/node_modules/@braintrust/trace-opencode/dist/index.mjs" \
echo "OPENCODE_PLUGIN=$install_dir/node_modules/@braintrust/trace-opencode/dist/tracing.mjs" \
>> "$GITHUB_ENV"
- name: Build and install Pi extension for integration harness
shell: bash
Expand Down
27 changes: 19 additions & 8 deletions .github/workflows/smoke-codex.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ on:
required: true
OPENAI_API_KEY:
required: false
CODEX_HOOK_TRUST_CONFIG:
description: "Reviewed [hooks.state] TOML for the deployed hook definitions."
required: false
workflow_dispatch:
inputs:
dist_repo:
Expand All @@ -35,22 +38,23 @@ jobs:
guard:
runs-on: ubuntu-24.04
outputs:
has_key: ${{ steps.check.outputs.has_key }}
ready: ${{ steps.check.outputs.ready }}
steps:
- id: check
env:
KEY: ${{ secrets.OPENAI_API_KEY }}
TRUST: ${{ secrets.CODEX_HOOK_TRUST_CONFIG }}
run: |
if [ -n "$KEY" ]; then
echo "has_key=true" >> "$GITHUB_OUTPUT"
if [ -n "$KEY" ] && [ -n "$TRUST" ]; then
echo "ready=true" >> "$GITHUB_OUTPUT"
else
echo "has_key=false" >> "$GITHUB_OUTPUT"
echo "::warning::OPENAI_API_KEY not set; skipping codex smoke test."
echo "ready=false" >> "$GITHUB_OUTPUT"
echo "::warning::Codex smoke requires OPENAI_API_KEY and reviewed CODEX_HOOK_TRUST_CONFIG; skipping rather than bypassing hook trust."
fi

smoke:
needs: guard
if: needs.guard.outputs.has_key == 'true'
if: needs.guard.outputs.ready == 'true'
strategy:
fail-fast: false
matrix:
Expand All @@ -72,7 +76,7 @@ jobs:
- name: Install Codex and bt CLIs
run: |
npm install -g @openai/codex
curl -fsSL https://bt.dev/cli/install.sh | sh
curl -fsSL https://bt.dev/cli/install.sh | bash
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Require daemon-capable bt
Expand All @@ -87,6 +91,14 @@ jobs:
codex plugin marketplace add "${{ inputs.dist_repo }}"
codex plugin add "trace-codex@${MARKETPLACE}"

- name: Configure reviewed hook trust
env:
TRUST_CONFIG: ${{ secrets.CODEX_HOOK_TRUST_CONFIG }}
run: |
mkdir -p "$HOME/.codex"
printf '%s\n' "$TRUST_CONFIG" > "$HOME/.codex/config.toml"
chmod 600 "$HOME/.codex/config.toml"

- name: Run real traced Codex session (${{ matrix.label }})
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
Expand All @@ -108,7 +120,6 @@ jobs:
curl -fsS http://127.0.0.1:53999/version >/dev/null
codex exec \
--skip-git-repo-check \
--dangerously-bypass-hook-trust \
--sandbox read-only \
"say hi"
python3 -c 'import json, os; s=json.load(open(os.environ["MOCK_COLLECTOR_OUT"])); assert s["totalRows"] >= 1, s; print(s)'
33 changes: 22 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Braintrust coding-agent plugins — monorepo

This repo is the single source of truth for Braintrust's coding-agent plugins.
Each plugin is developed here, built, and deployed to a per-agent distribution
repository that its marketplace installs from.
Each integration is developed here and deployed through its native
marketplace or package registry.

## Layout

```text
src/plugins/<agent>/ one directory per agent (claude, codex)
src/plugins/<agent>/ one directory per agent (claude, codex, opencode, pi)
content/ the deployable plugin tree, verbatim
build.sh <dir> assemble the deployable tree into <dir>
validate.sh <dir> validate manifests and required files
Expand All @@ -18,9 +18,10 @@ Makefile build, test, and publish entry points
.github/workflows/ CI and release automation
```

Everything an agent installs lives under `src/plugins/<agent>/content/`.
Plugin hooks are thin fail-open shell launchers that invoke
`bt trace hook`; they do not contain or compile a second tracing runtime.
Everything agent-specific lives under `src/plugins/<agent>/content/`.
Claude and Codex hooks are rendered from one version-aware, fail-open shell
template and invoke `bt trace hook`; Pi and OpenCode share one generated
JavaScript daemon client. None contains a second tracing runtime.

## Adding a coding-agent integration

Expand Down Expand Up @@ -49,8 +50,10 @@ release. Marketplace manifests are not versioned.
|---|---|
| claude | `braintrustdata/braintrust-claude-plugin` |
| codex | `braintrustdata/braintrust-codex-plugin` |
| opencode | npm package `@braintrust/trace-opencode` |
| pi | npm package `@braintrust/pi-extension` |

A distribution repository is a generated artifact. Each deploy clones it,
A marketplace distribution repository is a generated artifact. Each deploy clones it,
replaces the tracked tree with a fresh build, and pushes the result.
`braintrustdata/test-coding-agent-dist` is the shared release sandbox.

Expand All @@ -65,23 +68,31 @@ Cross-repository pushes use `GH_TOKEN` or ambient Git credentials.

## Releasing

The manual `release.yml` workflow deploys a production release, records the
For Claude and Codex, the manual `release.yml` workflow deploys a production release, records the
version bump on `main`, tags it, and creates a GitHub Release. The manual
`test-release.yml` workflow exercises the same deployment against the test
repository without committing or tagging. Both call `_release.yml`.

A Codex deployment can run `smoke-codex.yml`, which installs the deployed
plugin and runs a real Codex session through the daemon when
`OPENAI_API_KEY` is available.
plugin and runs a real Codex session through the daemon when `OPENAI_API_KEY`
and a reviewed, hash-scoped `CODEX_HOOK_TRUST_CONFIG` are available. It never
globally bypasses hook trust. CI installs the exact built npm artifacts and
runs deterministic real-agent integration tests for Pi and OpenCode; their
registry release workflows remain generated by sdk-actions.

CI builds and validates both plugin packages and builds, tests, and lints the
OpenCode and Pi use their generated `release-opencode.yml` and
`release-pi.yml` npm workflows. Do not hand-edit those sdk-actions-generated
files; update them through their generator.

CI builds and validates all four integrations and builds, tests, and lints the
Rust daemon on Linux, macOS, and Windows. Concurrent runs for an obsolete
branch revision are cancelled.

## Secrets

- `PUBLISH_TOKEN` grants `contents:write` on distribution repositories.
- `OPENAI_API_KEY` enables the optional real Codex smoke test.
- `CODEX_HOOK_TRUST_CONFIG` enables only reviewed Codex hook hashes in that smoke test.

Braintrust authentication is deliberately not stored in plugin or daemon
settings. The embedding `bt` CLI owns profiles, OAuth, keychain access, API
Expand Down
22 changes: 21 additions & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
TODO Apache 2.0 (placeholder)
MIT License

Copyright (c) 2025 Braintrust Data, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ $(BUILD_RULES): build-%:
@src/plugins/$*/build.sh "$(DIST)/$*"

test: build
@for p in $(PLUGINS); do \
@set -e; for p in $(PLUGINS); do \
echo "==> validate $$p"; \
src/plugins/$$p/validate.sh "$(DIST)/$$p"; \
done
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Braintrust coding-agent plugins

A monorepo of various [Braintrust](https://braintrust.dev) coding agent plugins.
A monorepo of [Braintrust](https://braintrust.dev) coding-agent integrations.
Every integration forwards native events to the same local `bt` tracing daemon,
so authentication, routing, recovery, and trace construction stay consistent.

For further instructions, see the instructions for your desired coding agent

Expand All @@ -11,6 +13,21 @@ For further instructions, see the instructions for your desired coding agent
| OpenCode | npm: [`@braintrust/trace-opencode`](https://www.npmjs.com/package/@braintrust/trace-opencode) |
| Pi | npm: [`@braintrust/pi-extension`](https://www.npmjs.com/package/@braintrust/pi-extension) |

## Feature coverage

| Agent | Persistent setup | Managed run | Import / attach | Braintrust tools |
|---|---:|---:|---:|---:|
| Claude Code | Yes | Yes | Yes / Yes | No |
| Codex | Yes | Yes | Yes / Yes | No |
| OpenCode | Yes | Yes | No / No | Yes |
| Pi | Yes | Yes | No / No | No |

All four integrations support `bt trace enable`, `bt trace disable`, and
invocation-local `bt trace run`; `bt trace setup` remains an alias for enable.
Import and attach
are intentionally limited to agents whose native transcript stores preserve
the data needed by their daemon translators.

## Development & releasing

See [AGENTS.md](./AGENTS.md) for the repo structure, the build/deploy model, the
Expand Down
25 changes: 12 additions & 13 deletions bt-daemon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ stateful daemon that plugin **hook shims** forward events to; it owns the
event→trace state machine and sends spans to Braintrust out-of-band. See
[`docs/protocol.md`](docs/protocol.md) for the wire contract.

> **Placeholder name** — the real name is TBD. The subcommand framing
> (`serve` / `hook` / `status` / `import` / `run`) should survive a rename.

## Layout

One self-contained Cargo crate, liftable to its own repo by copying
Expand Down Expand Up @@ -123,11 +120,12 @@ create a trace for the past session. Hook-only facts absent from a native
transcript are not invented.

Add `--attach` to keep following an active Codex or Claude transcript until
Ctrl-C. `run <codex|claude> [ARGS...]` launches the selected agent with
inherited stdio and injects live Braintrust hooks for that invocation, so it
does not depend on the tracing plugin being installed or enabled. Managed runs
suppress inherited Braintrust plugin hooks to avoid logging the same session
twice; the injected hooks still use the normal daemon translator and sink.
Ctrl-C. `run <codex|claude|opencode|pi> [ARGS...]` launches the selected agent
with inherited stdio and injects live Braintrust capture for that invocation,
so it does not depend on persistent tracing being enabled. Managed runs
suppress or deduplicate inherited Braintrust capture to avoid logging the same
session twice; OpenCode uses the package's trace-only entrypoint so managed
tracing does not add its optional data tools.
Codex applies its normal hook-review flow, so the first run requires trusting
the injected Braintrust hook through `/hooks`; later runs reuse that trust while
the hook definition remains unchanged.
Expand All @@ -139,9 +137,10 @@ different profiles, organizations, projects, experiments, or parent spans.

## Status

Phases 0–5 are implemented: protocol, daemon lifecycle, Braintrust sink,
Codex and Claude translators, `bt daemon` integration, and thin hook shims for
both shipped plugins. Restart recovery replays the redacted journal with
The shared protocol, daemon lifecycle, Braintrust sink, all four production
translators, persistent setup, managed runs, and thin capture adapters are
implemented. Codex and Claude additionally support transcript import and live
attach. Restart recovery replays the redacted journal with
deterministic span ids, so resubmitted rows merge into the same spans instead
of creating duplicates. Claude lifecycle entries reference a daemon-owned
transcript mirror, so recovery does not depend on mutable external paths
Expand All @@ -156,8 +155,8 @@ journal, mirror, or conversation content — is capped or truncated; only
in-memory caches are bounded, and each is re-derivable from disk.

Windows named-pipe transport, detached spawning, lifecycle handover, and
cross-platform pipeline tests are implemented. The remaining host follow-ups
are OpenCode and pi, which are not present in this monorepo.
cross-platform pipeline tests are implemented. Pi and OpenCode use the same
long-lived JavaScript daemon client on every supported platform.

- The Braintrust sink pins `braintrust-sdk-rust` commit `d33e806`, which adds
deterministic span ids, `span_origin`/`span_attributes` passthrough, and
Expand Down
51 changes: 30 additions & 21 deletions bt-daemon/src/command_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! consistently and JSON mode never falls back to human prose.

use crate::wire::StatusResult;
use crate::AgentSpec;
use serde::Serialize;
use std::path::PathBuf;

Expand Down Expand Up @@ -61,6 +62,9 @@ pub struct SetupCommandOutput {
pub restart_required: bool,
}

/// Enable and disable report the same stable agent lifecycle shape.
pub type LifecycleCommandOutput = SetupCommandOutput;

#[derive(Debug, Clone, Serialize)]
pub struct StopCommandOutput {
pub running: bool,
Expand All @@ -72,7 +76,7 @@ pub struct StopCommandOutput {
pub enum TraceCommandOutput {
Status(StatusCommandOutput),
Enable(SetupCommandOutput),
Disable(SetupCommandOutput),
Disable(LifecycleCommandOutput),
Stop(StopCommandOutput),
}

Expand All @@ -81,36 +85,28 @@ impl TraceCommandOutput {
Self::Status(status.into())
}

pub fn setup(
source: impl Into<String>,
display_name: impl Into<String>,
settings_path: impl Into<PathBuf>,
) -> Self {
pub fn setup(spec: &AgentSpec, settings_path: impl Into<PathBuf>) -> Self {
Self::Enable(SetupCommandOutput {
source: source.into(),
display_name: display_name.into(),
source: spec.canonical_source.into(),
display_name: spec.display_name.into(),
settings_path: settings_path.into(),
restart_required: true,
})
}

pub fn stop(running: bool, stopped: bool) -> Self {
Self::Stop(StopCommandOutput { running, stopped })
}

pub fn disable(
source: impl Into<String>,
display_name: impl Into<String>,
settings_path: impl Into<PathBuf>,
) -> Self {
pub fn disable(spec: &AgentSpec, settings_path: impl Into<PathBuf>) -> Self {
Self::Disable(SetupCommandOutput {
source: source.into(),
display_name: display_name.into(),
source: spec.canonical_source.into(),
display_name: spec.display_name.into(),
settings_path: settings_path.into(),
restart_required: true,
})
}

pub fn stop(running: bool, stopped: bool) -> Self {
Self::Stop(StopCommandOutput { running, stopped })
}

pub fn render(&self, format: OutputFormat) -> anyhow::Result<String> {
match format {
OutputFormat::Json => Ok(serde_json::to_string(self)?),
Expand Down Expand Up @@ -161,8 +157,7 @@ mod tests {
#[test]
fn enable_json_contains_stable_selection_fields_without_prose() {
let output = TraceCommandOutput::setup(
"opencode",
"OpenCode",
crate::AgentId::OpenCode.spec(),
PathBuf::from("/tmp/opencode/braintrust.json"),
);
let rendered = output.render(OutputFormat::Json).unwrap();
Expand Down Expand Up @@ -206,6 +201,20 @@ mod tests {
);
}

#[test]
fn lifecycle_json_uses_canonical_source_names() {
let path = PathBuf::from("/tmp/claude/braintrust.json");
let disabled: serde_json::Value = serde_json::from_str(
&TraceCommandOutput::disable(crate::AgentId::Claude.spec(), path)
.render(OutputFormat::Json)
.unwrap(),
)
.unwrap();
assert_eq!(disabled["command"], "disable");
assert_eq!(disabled["source"], "claude-code");
assert_eq!(disabled["restart_required"], true);
}

#[test]
fn human_output_preserves_existing_messages() {
assert_eq!(
Expand Down
Loading
Loading