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
109 changes: 108 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,90 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 11.21.0
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: src/plugins/opencode/content/pnpm-lock.yaml
- name: Reject Bun tooling in OpenCode and the shared client
shell: bash
run: |
if git grep -n -E 'bun:test|bun run|bun install|packageManager.*bun' -- \
src/plugins/opencode/content src/runtime/js-daemon-client; then
echo "OpenCode or the shared daemon client still depends on Bun" >&2
exit 1
fi
if find src/plugins/opencode -name 'bun.lock*' -print -quit | grep -q .; then
echo "OpenCode still contains a Bun lockfile" >&2
exit 1
fi
- name: Build and validate plugins
run: make test

js-daemon-client:
name: JavaScript daemon client (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- name: Test RPC transport contracts
run: node --test src/runtime/js-daemon-client/tests/client.test.ts
- name: Verify generated client preparation
run: |
node scripts/prepare-js-daemon-client.mjs opencode
node scripts/prepare-js-daemon-client.mjs opencode --check

opencode-compatibility:
name: OpenCode package (${{ matrix.peer-version }})
strategy:
fail-fast: false
matrix:
peer-version: ["1.2.25", "latest"]
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 11.21.0
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: src/plugins/opencode/content/pnpm-lock.yaml
- name: Build and validate package
run: make validate-opencode
- name: Install package with OpenCode peers
env:
PEER_VERSION: ${{ matrix.peer-version }}
run: |
set -euo pipefail
package_dir="$(mktemp -d)"
(cd dist/opencode && pnpm pack --pack-destination "$package_dir" >/dev/null)
tarball="$(find "$package_dir" -name 'braintrust-trace-opencode-*.tgz' -print -quit)"
mkdir install-test
npm install --prefix install-test --no-audit --no-fund \
"$tarball" \
"@opencode-ai/plugin@${PEER_VERSION}" \
"@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"

daemon:
name: Daemon (${{ matrix.os }})
strategy:
Expand All @@ -48,11 +129,36 @@ jobs:
rustup default stable
rustup component add clippy rustfmt
- name: Install latest coding agents
run: npm install --prefix "${{ runner.temp }}/coding-agents" --no-save --no-package-lock --no-audit --no-fund --cache "${{ runner.temp }}/npm-cache" @openai/codex@latest @anthropic-ai/claude-code@latest
run: npm install --prefix "${{ runner.temp }}/coding-agents" --no-save --no-package-lock --no-audit --no-fund --cache "${{ runner.temp }}/npm-cache" @openai/codex@latest @anthropic-ai/claude-code@latest opencode-ai@latest
- name: Set up pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 11.21.0
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: src/plugins/opencode/content/pnpm-lock.yaml
- name: Build and install OpenCode plugin for integration harness
shell: bash
run: |
set -euo pipefail
make build-opencode
package_dir="$RUNNER_TEMP/opencode-package"
install_dir="$RUNNER_TEMP/opencode-plugin-install"
mkdir -p "$package_dir" "$install_dir"
(cd dist/opencode && pnpm pack --pack-destination "$package_dir" >/dev/null)
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" \
>> "$GITHUB_ENV"
- name: Report coding-agent versions
run: |
npm exec --prefix "${{ runner.temp }}/coding-agents" -- codex --version
npm exec --prefix "${{ runner.temp }}/coding-agents" -- claude --version
npm exec --prefix "${{ runner.temp }}/coding-agents" -- opencode --version
- name: Check formatting
if: runner.os == 'Linux'
run: cargo fmt --manifest-path bt-daemon/Cargo.toml -- --check
Expand All @@ -66,6 +172,7 @@ jobs:
BT_AGENT_INGEST_MODE: mock
CODEX_BIN: ${{ runner.temp }}/coding-agents/node_modules/.bin/codex${{ matrix.agent_suffix }}
CLAUDE_BIN: ${{ runner.temp }}/coding-agents/node_modules/.bin/claude${{ matrix.agent_suffix }}
OPENCODE_BIN: ${{ runner.temp }}/coding-agents/node_modules/.bin/opencode${{ matrix.agent_suffix }}
run: cargo test --manifest-path bt-daemon/Cargo.toml --all-features --locked --test agent_integration -- --ignored --nocapture --test-threads=1
- name: Lint daemon
run: cargo clippy --manifest-path bt-daemon/Cargo.toml --all-targets --all-features --locked -- -D warnings
141 changes: 141 additions & 0 deletions .github/workflows/release-opencode.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# GENERATED by sdk-actions `bin/workflow` — update with: bin/workflow update <this file>
# sdk-actions: {"template":"release/js/turnkey","ref":"71731d4a726df3e3bf42e50c2da4a75842b038dd","version":"1.0.0","params":{"access":"public","allowed_channels":"latest,rc,next,beta","dry_run_environment":"npm-opencode-dry-run","emoji":":javascript:","node_version":"src/plugins/opencode/content/.tool-versions","package_name":"@braintrust/trace-opencode","provenance":"true","publish_environment":"npm-opencode","slack_channel_var":"SLACK_SDK_RELEASE_CHANNEL","slack_token_secret":"SLACK_BOT_TOKEN","workflow_name":"Release OpenCode","working_directory":"src/plugins/opencode/content"}}

name: Release OpenCode

# Releases @braintrust/trace-opencode to npm via braintrustdata/sdk-actions. Dispatch after merging
# the commit that bumps package.json's "version"; the release runs against that commit's SHA.
on:
workflow_dispatch:
inputs:
release_type:
description: "Release type (stable → GitHub release; prerelease → none)"
type: choice
default: stable
options: [stable, prerelease]
channel:
description: "npm dist-tag / channel"
type: choice
default: latest
options: [latest, rc, next, beta]
sha:
description: "Commit SHA (of the version bump) to release"
required: true
type: string
prev_release:
description: "Release-notes anchor: a tag or SHA (empty → the previous tag)"
type: string
required: false
dry_run:
description: "Dry run: build without tagging or publishing"
type: boolean
default: false

jobs:
# FACT-FIND — derive version/tag/notes, check the registry, and emit the package manifest.
# contents: write is for the releases/generate-notes API.
configure:
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: write
outputs:
version: ${{ steps.configure.outputs.version }}
release_tag: ${{ steps.configure.outputs.release_tag }}
prev_release: ${{ steps.configure.outputs.prev_release }}
branch: ${{ steps.configure.outputs.branch }}
on_release_branch: ${{ steps.configure.outputs.on_release_branch }}
commit_message: ${{ steps.configure.outputs.commit_message }}
channel: ${{ steps.configure.outputs.channel }}
github_release: ${{ steps.configure.outputs.github_release }}
already_published: ${{ steps.configure.outputs.already_published }}
notes: ${{ steps.configure.outputs.notes }}
package: ${{ steps.configure.outputs.package }}
steps:
- name: Configure release
id: configure
uses: braintrustdata/sdk-actions/actions/release/lang/js/configure@71731d4a726df3e3bf42e50c2da4a75842b038dd
with:
sha: ${{ inputs.sha }}
working_directory: src/plugins/opencode/content
tag_format: 'v{version}-opencode'
release_type: ${{ inputs.release_type }}
npm_channel: ${{ inputs.channel }}
npm_package_name: '@braintrust/trace-opencode'
package_label: '@braintrust/trace-opencode'
emoji: ':javascript:'
prev_release: ${{ inputs.prev_release }}

validate:
needs: [configure]
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Validate release
uses: braintrustdata/sdk-actions/actions/release/lang/js/validate@71731d4a726df3e3bf42e50c2da4a75842b038dd
with:
sha: ${{ inputs.sha }}
dry_run: ${{ inputs.dry_run }}
working_directory: src/plugins/opencode/content
node_version: src/plugins/opencode/content/.tool-versions
release_tag: ${{ needs.configure.outputs.release_tag }}
channel: ${{ needs.configure.outputs.channel }}
allowed_channels: 'latest,rc,next,beta'
on_release_branch: ${{ needs.configure.outputs.on_release_branch }}
notes: ${{ needs.configure.outputs.notes }}
already_published: ${{ needs.configure.outputs.already_published }}
enforce_release_branch: 'true'

request-approval:
needs: [configure, validate]
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions: {}
steps:
- name: Request release approval
uses: braintrustdata/sdk-actions/actions/release/request-approval@71731d4a726df3e3bf42e50c2da4a75842b038dd
with:
packages: '{"packages":[${{ needs.configure.outputs.package }}]}'
sha: ${{ inputs.sha }}
branch: ${{ needs.configure.outputs.branch }}
on_release_branch: ${{ needs.configure.outputs.on_release_branch }}
commit_message: ${{ needs.configure.outputs.commit_message }}
dry_run: ${{ inputs.dry_run }}
slack_token: ${{ secrets.SLACK_BOT_TOKEN }}
slack_channel: ${{ vars.SLACK_SDK_RELEASE_CHANNEL }}

build-and-ship:
needs: [configure, validate, request-approval]
runs-on: ubuntu-24.04
timeout-minutes: 15
environment: ${{ inputs.dry_run && 'npm-opencode-dry-run' || 'npm-opencode' }}
permissions:
contents: write
id-token: write # OIDC trusted publishing + provenance
attestations: write # signed SBOM attestation
steps:
- name: Build and ship
uses: braintrustdata/sdk-actions/actions/release/lang/js/build-and-ship@71731d4a726df3e3bf42e50c2da4a75842b038dd
with:
sha: ${{ inputs.sha }}
working_directory: src/plugins/opencode/content
node_version: src/plugins/opencode/content/.tool-versions
dry_run: ${{ inputs.dry_run }}
release_tag: ${{ needs.configure.outputs.release_tag }}
already_published: ${{ needs.configure.outputs.already_published }}
github_release: ${{ needs.configure.outputs.github_release }}
version: ${{ needs.configure.outputs.version }}
access: 'public'
provenance: 'true'
channel: ${{ needs.configure.outputs.channel }}
package_name: '@braintrust/trace-opencode'
label: '@braintrust/trace-opencode'
notes: ${{ needs.configure.outputs.notes }}
prev_release: ${{ needs.configure.outputs.prev_release }}
branch: ${{ needs.configure.outputs.branch }}
on_release_branch: ${{ needs.configure.outputs.on_release_branch }}
slack_token: ${{ secrets.SLACK_BOT_TOKEN }}
slack_channel: ${{ vars.SLACK_SDK_RELEASE_CHANNEL }}
emoji: ':javascript:'
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ DIST := dist
# name would shadow it in recipe subshells.
BUILD_RULES := $(addprefix build-,$(PLUGINS))
PUBLISH_RULES := $(addprefix publish-,$(PLUGINS))
VALIDATE_RULES := $(addprefix validate-,$(PLUGINS))

.PHONY: build test publish clean $(BUILD_RULES) $(PUBLISH_RULES)
.PHONY: build test publish clean $(BUILD_RULES) $(VALIDATE_RULES) $(PUBLISH_RULES)

build: $(BUILD_RULES)

Expand All @@ -38,6 +39,10 @@ test: build
src/plugins/$$p/validate.sh "$(DIST)/$$p"; \
done

$(VALIDATE_RULES): validate-%: build-%
@echo "==> validate $*"
@src/plugins/$*/validate.sh "$(DIST)/$*"

# Deploy every plugin named in the PUBLISH_TARGETS env var map. Fails if unset.
publish:
@scripts/publish.sh
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ For further instructions, see the instructions for your desired coding agent
|-------------|-------------------------|
| Claude Code | [braintrustdata/braintrust-claude-plugin](https://github.com/braintrustdata/braintrust-claude-plugin) |
| Codex | [braintrustdata/braintrust-codex-plugin](https://github.com/braintrustdata/braintrust-codex-plugin) |
| OpenCode | npm: [`@braintrust/trace-opencode`](https://www.npmjs.com/package/@braintrust/trace-opencode) |

## Development & releasing

Expand Down
9 changes: 8 additions & 1 deletion bt-daemon/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ impl Session {
pub fn spawn(
session_id: String,
source: String,
plugin_version: Option<String>,
journal: JournalWriter,
replay: Vec<Envelope>,
translators: Arc<Registry>,
Expand All @@ -57,6 +58,7 @@ impl Session {
let actor = SessionActor {
session_id: session_id.clone(),
source: source.clone(),
plugin_version,
translators,
sink_factory,
counters: counters.clone(),
Expand Down Expand Up @@ -164,6 +166,7 @@ async fn hydrate_transcript_snapshot(env: &mut Envelope) {
struct SessionActor {
session_id: String,
source: String,
plugin_version: Option<String>,
translators: Arc<Registry>,
sink_factory: Arc<dyn SinkFactory>,
counters: Arc<Counters>,
Expand All @@ -175,7 +178,11 @@ struct SessionActor {
impl SessionActor {
async fn run(self, mut rx: mpsc::UnboundedReceiver<SessionMsg>) {
let mut translator = self.translators.create(&self.source, &self.session_id);
let mut sink = match self.sink_factory.create(&self.session_id, &self.source) {
let mut sink = match self.sink_factory.create(
&self.session_id,
&self.source,
self.plugin_version.as_deref(),
) {
Ok(s) => s,
Err(e) => {
self.set_error(format!("sink init failed: {e}"));
Expand Down
1 change: 1 addition & 0 deletions bt-daemon/src/journal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ pub fn envelope_from_redacted(r: RedactedEnvelope) -> Envelope {
Envelope {
source: r.source,
source_version: r.source_version,
plugin_version: r.plugin_version,
session_id: r.session_id,
event: r.event,
ts_ms: r.ts_ms,
Expand Down
7 changes: 6 additions & 1 deletion bt-daemon/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ pub async fn run_hook(
let env = Envelope {
source: args.source.clone(),
source_version: args.source_version.clone(),
plugin_version: None,
session_id,
event,
ts_ms: now_ms(),
Expand Down Expand Up @@ -612,7 +613,11 @@ impl ImportProcessor {
Some(live) => live,
None => {
let translator = self.opts.translators.create(&env.source, &sid);
let sink = self.opts.sink_factory.create(&sid, &env.source)?;
let sink = self.opts.sink_factory.create(
&sid,
&env.source,
env.plugin_version.as_deref(),
)?;
self.sessions.insert(
sid.clone(),
ImportLive {
Expand Down
9 changes: 9 additions & 0 deletions bt-daemon/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ impl Daemon {
let session = Session::spawn(
env.session_id.clone(),
env.source.clone(),
env.plugin_version.clone(),
journal,
replay,
self.translators.clone(),
Expand Down Expand Up @@ -333,6 +334,14 @@ pub async fn run(args: ServeArgs, opts: ServeOptions) -> anyhow::Result<()> {

async fn accept_loop(daemon: Arc<Daemon>, mut listener: Listener) -> anyhow::Result<()> {
loop {
// `Notify::notify_waiters` does not retain a permit. If accepting a
// connection wins the select at the same time shutdown is requested,
// check the sticky flag before waiting again so the notification
// cannot be lost.
if daemon.shutting_down.load(Ordering::SeqCst) {
tracing::info!("shutdown requested");
return Ok(());
}
tokio::select! {
_ = daemon.shutdown.notified() => {
tracing::info!("shutdown requested");
Expand Down
Loading
Loading