Skip to content

Commit f33b663

Browse files
authored
feat: LLM observability via OTLP (Langfuse and any collector) (#27)
* feat(traces): extend Span with gen_ai fields, table footer, export metadata * feat(traces): resolve OTLP settings from env and config.json Add optional squad.OTLPConfig and traces.ResolveSettings with env-wins precedence. Empty endpoint is unset (no autoexport). Default protocol http/protobuf; capture defaults false. Unknown protocol errors. * feat(traces): add OTel HTTP+gRPC factory and span export * feat(traces): wire run and watch execute to parent+child OTLP Map OpenCode Prompt Info into RunResult, share traces.Write for JSONL plus optional push, skip double-record from watch via SkipRecord, and exit 2 on a bad OTLP protocol before work starts. * docs(traces): document optional OTLP push and traces UX File spans.jsonl is default; endpoint env enables live push. Both http/protobuf and grpc; capture default off. Aspire stays wont-port consumer. CONTRIBUTING notes live-traces.ps1. * feat(traces): add live-traces.ps1 and local Langfuse v4 compose * fix(traces): require this-run Langfuse parent+child in poll window * fix(traces): pin dummy/live scripts to stock OpenCode models * fix(traces): isolate unit tests from ambient OTLP and label Write errors * fix(traces): replace deprecated attribute.Value.Emit in tests * chore: add Taskfile for local CI and Langfuse/live * chore: add Taskfile release, bump, tidy, cover, and clean
1 parent 1dcb938 commit f33b663

29 files changed

Lines changed: 2188 additions & 80 deletions

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ squad-export.json
3333
.env
3434
.env.*
3535
!.env.example
36+
# Local-only Langfuse init keys (not production); needed for live-traces.ps1
37+
!scripts/langfuse/.env
3638
*.pem
3739
*.key
3840

‎AGENTS.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ go test ./...
157157
go build -o squad-oc.exe ./cmd/squad-oc
158158
```
159159

160+
Prefer Task when it is on PATH (`task --list`): `task test`, `task build`, `task ci`, `task release` (snapshot only), `task bump TAG=vX.Y.Z`, `task langfuse:up` / `task langfuse:down`, `task live:e2e` / `task live:traces`. Real publish is `task release:tag TAG=vX.Y.Z` on main then `task release:push`. Do not edit `internal/version/version.go`. Do not use this git clone as the live serve cwd.
161+
160162
Live OpenCode HTTP checks need a running `opencode serve` and use the `live` build tag.
161163

162164
## Architecture

‎CONTRIBUTING.md‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,36 @@ go test ./...
1919
go build -o squad-oc ./cmd/squad-oc
2020
```
2121

22-
Requires **Go 1.26.6+**. Optional live check (dummy project only, never this clone as the serve cwd unless you mean to dogfood):
22+
Or, with [Task](https://taskfile.dev) (`go install github.com/go-task/task/v3/cmd/task@latest`):
23+
24+
```bash
25+
task # list
26+
task test # go test ./...
27+
task build # go build -o squad-oc[.exe]
28+
task ci # local CI gate (fmt/vet/lint/race/build/vuln/actionlint)
29+
task tools # install lint/vuln/actionlint/goreleaser
30+
task release:check # validate .goreleaser.yaml
31+
task release # GoReleaser snapshot into dist/ (no tag, no publish)
32+
task bump TAG=vX.Y.Z # pin Homebrew/Scoop/winget from dist/ (no PR)
33+
task release:tag TAG=vX.Y.Z # annotated tag on main only (no push)
34+
task release:push TAG=vX.Y.Z # push tag; GitHub release workflow publishes
35+
task langfuse:up
36+
task langfuse:down
37+
task live:e2e
38+
task live:traces
39+
```
40+
41+
Do not edit `internal/version/version.go` to bump. Do not tag a feature branch.
42+
43+
Requires **Go 1.26.6+**. Optional live checks (dummy project only, never this clone as the serve cwd unless you mean to dogfood):
2344

2445
```powershell
2546
./scripts/live-e2e.ps1
47+
./scripts/live-traces.ps1
48+
# same as: task live:e2e / task live:traces
2649
```
2750

28-
That talks to `opencode serve` on `127.0.0.1:4096`. The TUI (`opencode`) is not the API.
51+
`live-e2e.ps1` talks to `opencode serve` on `127.0.0.1:4096` (doctor + PONG). `live-traces.ps1` exercises JSONL spans and optional local Langfuse OTLP ingest. The TUI (`opencode`) is not the API. CI stays `go test ./...` without live env. `task ci` is the local mirror of `.github/workflows/ci.yml`.
2952

3053
## Branch and PR
3154

‎README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ OpenCode creates `.opencode/package.json` (`@opencode-ai/plugin`) and runs an in
9494
| `pack <path\|git-url>` | One-shot pull of extra agents/skills |
9595
| `link <team-dir|git-url>` / `link --sync` / `link --off` | Share one team directory across several repos (git URL clones into `~/.squad-oc/links/`) |
9696
| `update-check [--json] [--refresh]` | Prints `up to date` or `update available` vs GitHub latest tag |
97-
| `traces [--last N] [--json] [--export file]` | Local `run` / `watch` spans; `--export` writes OTLP JSON |
97+
| `traces [--last N] [--json] [--export file]` | Local `run` / `watch` spans (`.squad/traces/spans.jsonl`); `--export` writes OTLP JSON; optional live push via `OTEL_EXPORTER_OTLP_*` |
9898
| `mcp apply` / `list` / `init` | Merge org `.squad/mcp-config.json` into `opencode.json` |
9999
| `marketplace add` / `list` / `remove` / `browse` / `install` | Register a skills pack and copy a plugin into `.opencode/skills/` |
100100
| `plugin install <name>@<marketplace>` / `list` / `uninstall <name>` | Named skill install; uninstall removes only `.opencode/skills/<name>/` |
@@ -133,7 +133,7 @@ squad-oc (Go)
133133

134134
`upgrade --self` downloads the latest GitHub Release for this OS/arch and replaces the running binary. On Windows, if the exe is locked, it writes `squad-oc.exe.new` beside it (`replaced on next start`).
135135

136-
`traces` lists local spans from `run` and `watch --execute`. `--export file` writes OTLP JSON any collector can ingest.
136+
`traces` lists local spans from `run` and `watch --execute`. Default storage is `.squad/traces/spans.jsonl`. `--export file` writes OTLP JSON any collector can ingest. Set `OTEL_EXPORTER_OTLP_ENDPOINT` (or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`) to push live during `run` / `watch --execute`. Protocols: `http/protobuf` (default) and `grpc` via `OTEL_EXPORTER_OTLP_PROTOCOL`. Prompt/completion bodies always land in local JSONL; OTel message attributes only when `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` is on (default off). Langfuse local example: endpoint `http://127.0.0.1:3000/api/public/otel`, Basic Auth in `OTEL_EXPORTER_OTLP_HEADERS`, plus header `x-langfuse-ingestion-version=4`. Aspire Dashboard remains a valid OTLP **consumer**, not a shipped `squad-oc` command.
137137

138138
### Share extra agents (`upstream` / `pack`)
139139

@@ -218,7 +218,7 @@ Original-Squad / Copilot-host pieces we are not building:
218218

219219
- GitHub Copilot CLI / Copilot SDK
220220
- Interactive Ink/`squad` shell (use the OpenCode TUI)
221-
- Aspire / .NET dashboard (traces are local OTLP JSON, not Aspire)
221+
- Aspire / .NET dashboard (won’t ship; point any OTLP consumer at optional push or `traces --export`)
222222

223223
## Develop
224224

‎Taskfile.yml‎

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# https://taskfile.dev — local CI + Langfuse/live helpers.
2+
# Install: go install github.com/go-task/task/v3/cmd/task@latest
3+
version: "3"
4+
5+
vars:
6+
BINARY: '{{if eq OS "windows"}}squad-oc.exe{{else}}squad-oc{{end}}'
7+
COMPOSE: scripts/langfuse/docker-compose.yml
8+
PWSH: '{{if eq OS "windows"}}powershell{{else}}pwsh{{end}}'
9+
10+
tasks:
11+
default:
12+
desc: List tasks
13+
silent: true
14+
cmds:
15+
- task --list
16+
17+
tools:
18+
desc: Install golangci-lint, govulncheck, actionlint, goreleaser (versions match CI)
19+
cmds:
20+
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
21+
- go install golang.org/x/vuln/cmd/govulncheck@v1.7.0
22+
- go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.12
23+
- go install github.com/goreleaser/goreleaser/v2@v2.17.1
24+
25+
tidy:
26+
desc: go mod tidy
27+
cmds:
28+
- go mod tidy
29+
30+
cover:
31+
desc: Unit tests with coverage profile
32+
cmds:
33+
- go test -coverprofile=coverage.out ./...
34+
- go tool cover -func=coverage.out
35+
36+
test:
37+
desc: Unit tests (no race, no live OpenCode)
38+
cmds:
39+
- go test ./...
40+
41+
test:race:
42+
desc: Unit tests with -race (needs a C toolchain on Windows)
43+
cmds:
44+
- go test -race -count=1 ./...
45+
46+
vet:
47+
desc: go vet
48+
cmds:
49+
- go vet ./...
50+
51+
lint:
52+
desc: golangci-lint (same timeout as CI)
53+
cmds:
54+
- golangci-lint run --timeout=4m
55+
56+
fmt:
57+
desc: Apply gofmt to all .go files
58+
cmds:
59+
- gofmt -w .
60+
61+
fmt:check:
62+
desc: Fail if gofmt would change files (Linux/macOS; CI). Skipped on Windows (CRLF false positive).
63+
cmds:
64+
- task: fmt:check:windows
65+
- task: fmt:check:unix
66+
67+
fmt:check:windows:
68+
internal: true
69+
platforms: [windows]
70+
cmds:
71+
- cmd /c echo skip gofmt -l on Windows CI still checks
72+
73+
fmt:check:unix:
74+
internal: true
75+
platforms: [linux, darwin]
76+
cmds:
77+
- |
78+
files=$(gofmt -l .)
79+
if [ -n "$files" ]; then
80+
echo unformatted:
81+
echo "$files"
82+
exit 1
83+
fi
84+
85+
build:
86+
desc: Build squad-oc
87+
cmds:
88+
- go build -o {{.BINARY}} ./cmd/squad-oc
89+
90+
install:
91+
desc: go install ./cmd/squad-oc onto GOPATH/bin
92+
cmds:
93+
- go install ./cmd/squad-oc
94+
95+
clean:
96+
desc: Remove local binary, dist/, and coverage.out
97+
cmds:
98+
- '{{if eq OS "windows"}}cmd /c if exist {{.BINARY}} del /q {{.BINARY}} & if exist dist rmdir /s /q dist & if exist coverage.out del /q coverage.out{{else}}rm -rf {{.BINARY}} dist coverage.out{{end}}'
99+
100+
vuln:
101+
desc: govulncheck
102+
cmds:
103+
- govulncheck ./...
104+
105+
actionlint:
106+
desc: Lint GitHub workflow YAML
107+
cmds:
108+
- actionlint
109+
110+
ci:
111+
desc: Local CI gate matching .github/workflows/ci.yml
112+
deps: [fmt:check, vet, lint, test:race, build, vuln, actionlint]
113+
114+
langfuse:up:
115+
desc: Start local Langfuse v4 on 127.0.0.1:3000
116+
cmds:
117+
- docker compose -f {{.COMPOSE}} up -d
118+
119+
langfuse:down:
120+
desc: Stop local Langfuse (keeps volumes)
121+
cmds:
122+
- docker compose -f {{.COMPOSE}} down
123+
124+
langfuse:down:volumes:
125+
desc: Stop local Langfuse and remove squad-oc-langfuse-* volumes
126+
cmds:
127+
- docker compose -f {{.COMPOSE}} down -v
128+
129+
live:e2e:
130+
desc: Dummy PONG check (D:\xAI\squad-oc-dummy only)
131+
cmds:
132+
- "{{.PWSH}} -File scripts/live-e2e.ps1"
133+
134+
live:traces:
135+
desc: Dummy traces + optional local Langfuse OTLP
136+
cmds:
137+
- "{{.PWSH}} -File scripts/live-traces.ps1"
138+
139+
live:traces:skip-langfuse:
140+
desc: Dummy traces only (no Langfuse)
141+
cmds:
142+
- "{{.PWSH}} -File scripts/live-traces.ps1 -SkipLangfuse"
143+
144+
live:models:
145+
desc: Dummy per-agent model pin check
146+
cmds:
147+
- "{{.PWSH}} -File scripts/live-models.ps1"
148+
149+
release:check:
150+
desc: Validate .goreleaser.yaml
151+
cmds:
152+
- goreleaser check
153+
154+
release:
155+
desc: Local GoReleaser snapshot into dist/ (no tag, no GitHub publish)
156+
cmds:
157+
- goreleaser release --snapshot --clean
158+
159+
release:tag:
160+
desc: Annotated tag TAG=vX.Y.Z on main HEAD only (does not push)
161+
requires:
162+
vars: [TAG]
163+
cmds:
164+
- task: release:tag:windows
165+
- task: release:tag:unix
166+
167+
release:tag:windows:
168+
internal: true
169+
platforms: [windows]
170+
cmds:
171+
- powershell -NoProfile -File scripts/release-tag.ps1 -Tag {{.TAG}}
172+
173+
release:tag:unix:
174+
internal: true
175+
platforms: [linux, darwin]
176+
cmds:
177+
- bash scripts/release-tag.sh {{.TAG}}
178+
179+
release:push:
180+
desc: Push existing tag TAG=vX.Y.Z to origin (starts GitHub release workflow)
181+
requires:
182+
vars: [TAG]
183+
cmds:
184+
- git push origin {{.TAG}}
185+
186+
bump:
187+
desc: Pin Homebrew/Scoop/winget from dist/ (TAG=vX.Y.Z). Does not open a PR.
188+
requires:
189+
vars: [TAG]
190+
cmds:
191+
- bash -c "TAG='{{.TAG}}' REPO='{{.REPO | default "xeaser/squad-opencode"}}' DIST='{{.DIST | default "dist"}}' scripts/copy-packaging-from-dist.sh"

‎docs/get-started.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Inside OpenCode:
5353
3. Paste your API key
5454
4. Quit when done
5555

56-
Optional: pin a seat to an OpenCode id (`provider/model-id`) with `squad-oc cast --model squad xai/grok-3` or `cast --add NAME --model anthropic/claude-sonnet-4-5`. Empty cells inherit the Squad model, then the session default. This does not write `opencode.json`.
56+
Optional: pin a seat to an OpenCode id (`provider/model-id`) with `squad-oc cast --model squad opencode/big-pickle` or `cast --add NAME --model opencode/hy3-free`. Use an id from `opencode models` on this machine — `xai/…` and `anthropic/…` only work after `/connect` for that provider. Empty cells inherit the Squad model, then the session default. This does not write `opencode.json`.
5757

5858
---
5959

@@ -186,7 +186,7 @@ The script builds `squad-oc.exe`, inits `D:\xAI\squad-oc-dummy` if needed, start
186186
- `squad-oc watch --health` prints the last Ralph snapshot
187187
- `squad-oc brief` prints the morning listing (PRs, tickets, last done, next). Soft if `gh` is missing.
188188
- `squad-oc watch --execute --overnight-start 18:00 --overnight-end 08:00`
189-
- `squad-oc traces` lists local run/watch spans (`--export` writes OTLP JSON)
189+
- `squad-oc traces` lists local run/watch spans (default: `.squad/traces/spans.jsonl`; `--export` writes OTLP JSON). File is the default; set `OTEL_EXPORTER_OTLP_ENDPOINT` (or traces-specific) to push during `run` / `watch --execute`. Protocols: `http/protobuf` (default) and `grpc` via `OTEL_EXPORTER_OTLP_PROTOCOL`. Bodies always in local JSONL; OTel message content only if `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` is on (default off). Langfuse local: endpoint `http://127.0.0.1:3000/api/public/otel`, Basic Auth in `OTEL_EXPORTER_OTLP_HEADERS`, header `x-langfuse-ingestion-version=4`. Aspire standalone can consume OTLP; it is not a shipped command.
190190
- `squad-oc run -p "…"` starts `opencode serve` on `127.0.0.1:4096` if nothing is there; a custom `--url` never auto-starts
191191
- `squad-oc pack <path|git-url>` or `squad-oc upstream add <name> <path|git-url>` to pull extra agents/skills (see README)
192192
- `squad-oc mcp init` / `apply` / `list` — org `.squad/mcp-config.json` into `opencode.json` (workshop §8)

‎docs/use-cases.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Matches README **Non-goals** (plus the memory MCP the roadmap refuses):
6565
|------------------------------|---------|
6666
| GitHub Copilot CLI / Copilot SDK | Different host. OpenCode TUI + `opencode serve`. |
6767
| Interactive Ink / `squad` shell | Use the OpenCode TUI. |
68-
| Aspire / .NET dashboard | `squad-oc traces` is local OTLP JSON, not Aspire. |
68+
| Aspire / .NET dashboard | Won’t ship a dashboard. Local JSONL + optional OTLP push; Aspire standalone can still consume OTLP. |
6969
| `squad_state` memory MCP | OpenCode + `.squad/` files remain memory. |
7070

7171
Also not product features (workshop may mention them as **example** org MCP servers after P1): WorkIQ, Outlook COM, Teams Adaptive Cards.
@@ -101,7 +101,7 @@ Each original-Squad ease row is a **squad-oc command** you already ran, a **late
101101
| Snapshot team files | `export` / `import` |
102102
| Move team out of the worktree | `externalize` / `internalize` |
103103
| Context / PII hygiene | `nap` / `scrub-emails` |
104-
| Local spans | `squad-oc traces` |
104+
| Local spans / observability | `squad-oc traces` (`.squad/traces/spans.jsonl`) + optional OTLP push via `OTEL_EXPORTER_OTLP_ENDPOINT` |
105105
| Drop-in `mcp-config.json` | `squad-oc mcp init` / `apply` / `list` |
106106
| Marketplace browse + install | `squad-oc marketplace browse` / `install` |
107107
| Office / themed names | `squad-oc init --theme office` (native `@michael`) or later `cast --theme office` (mention map; `@lead` gone) |

‎go.mod‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,35 @@ module github.com/xeaser/squad-opencode
22

33
go 1.26.6
44

5-
require github.com/sst/opencode-sdk-go v0.19.2
5+
require (
6+
github.com/sst/opencode-sdk-go v0.19.2
7+
go.opentelemetry.io/otel v1.46.0
8+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0
9+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0
10+
go.opentelemetry.io/otel/sdk v1.46.0
11+
go.opentelemetry.io/otel/trace v1.46.0
12+
)
613

714
require (
15+
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
16+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
17+
github.com/go-logr/logr v1.4.4 // indirect
18+
github.com/go-logr/stdr v1.2.2 // indirect
19+
github.com/google/uuid v1.6.0 // indirect
20+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect
821
github.com/tidwall/gjson v1.14.4 // indirect
922
github.com/tidwall/match v1.1.1 // indirect
1023
github.com/tidwall/pretty v1.2.1 // indirect
1124
github.com/tidwall/sjson v1.2.5 // indirect
25+
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
26+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect
27+
go.opentelemetry.io/otel/metric v1.46.0 // indirect
28+
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
29+
golang.org/x/net v0.58.0 // indirect
30+
golang.org/x/sys v0.47.0 // indirect
31+
golang.org/x/text v0.41.0 // indirect
32+
google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect
33+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect
34+
google.golang.org/grpc v1.83.1 // indirect
35+
google.golang.org/protobuf v1.36.12 // indirect
1236
)

0 commit comments

Comments
 (0)