Skip to content
Closed
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
24 changes: 14 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@ jobs:
timeout-minutes: 2
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Generated contracts match the schema
run: |
if [ ! -f Makefile ] || ! grep -q '^gen:' Makefile; then
echo "::notice::no 'make gen' target yet; skipping"; exit 0
fi
make gen
git diff --exit-code
run: make check-gen
- name: Fixtures validate (ajv) and generated TS type-checks
run: make test-ts

worker:
runs-on: ubuntu-latest
Expand All @@ -50,10 +53,11 @@ jobs:
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Daemon, ACP and TUI tests
run: |
if [ ! -f go.mod ]; then echo "::notice::no go.mod yet; skipping"; exit 0; fi
go vet ./... && go test -race ./...
run: make test-go

install:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -85,7 +89,7 @@ jobs:
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
run: |
if [ ! -f worker/wrangler.toml ]; then echo "::notice::no worker/wrangler.toml yet; skipping"; exit 0; fi
if [ ! -f worker/wrangler.toml ] && [ ! -f worker/wrangler.jsonc ]; then echo "::notice::no worker/wrangler config yet; skipping"; exit 0; fi
if [ -z "$CLOUDFLARE_API_TOKEN" ]; then
echo "::warning::CLOUDFLARE_API_TOKEN secret not set yet (owner action, spec §7); preview skipped"; exit 0
fi
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
28 changes: 28 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Commonly v4

Commonly v4 is a multiplayer harness for the agents people already use: a pod where humans and their
Claude Code or Codex agents share tasks, ask for decisions and leave receipts. Backend: Cloudflare
Workers + one Durable Object per pod. First client: a Go daemon with a Bubble Tea TUI. Agents join over
ACP and use pod tools through one MCP server.

## Repo map
- `schema/` — JSON Schema, the only source of wire shapes. `schema/fixtures/` — examples every test loads.
- `gen/ts/`, `daemon/internal/protocol/gen.go` — generated from `schema/`. Never edit by hand.
- `worker/` — Worker, PodDO, MCP server, auth. `daemon/` — Go daemon, ACP, TUI. `e2e/` — the slice test.
- `scripts/` — generators and checks.

## Commands
- `make gen` — regenerate contracts after editing `schema/`.
- `make test` — fixtures (ajv + Go round trip), type checks, unit tests.
- `make preflight` — `check-gen` + `test`; CI runs the same targets.
- On the fleet Mac, Go and Bun come from `~/.commonly/toolchains/v4/bin` (the Makefile adds it). Never
install anything globally.

## Rules
- The schema is the contract: edit `schema/`, run `make gen`, commit both. No `oneOf`/`anyOf`/`allOf`;
unions are a `type` enum plus `x-variants`. Cross-field rules are `x-rules`, enforced and tested in `worker/`.
- One PR per change, base `v4`. Green in the merge queue merges. Changes under `schema/`, `worker/src/auth`,
`worker/src/mcp`, `daemon/internal/{acp,run}`, `install.sh` or `.github/workflows` also need one Opus
`GATE: PASS` review at the PR head.
- Build test-first against your lane's done-test in the build spec. Put the done-test and `E2E_MS=` in the PR body.
- A lesson goes in the repo (a test, a lint rule or a doc under `docs/`) in the same PR, never only in private memory.
1 change: 1 addition & 0 deletions CLAUDE.md
31 changes: 31 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# v4 commands (build spec §4). Seats on the fleet Mac get Go and Bun from the v4-only
# toolchain prefix; CI installs them with setup-go and setup-bun.
export PATH := $(HOME)/.commonly/toolchains/v4/bin:$(PATH)

.PHONY: gen check-gen test test-ts test-go preflight

node_modules: package.json bun.lock
bun install --frozen-lockfile
@touch node_modules

# Regenerate every contract from schema/. Never hand-edit gen/ or protocol/gen.go.
gen: node_modules
bun scripts/gen.mjs
gofmt -w daemon/internal/protocol/gen.go

# Fails if schema/ changed without `make gen`, or gen/ was edited by hand.
check-gen: gen
git diff --exit-code -- gen daemon/internal/protocol/gen.go
@test -z "$$(git ls-files --others --exclude-standard gen daemon/internal/protocol)" || (echo "untracked generated files; commit them"; exit 1)

test-ts: node_modules
bun scripts/check-fixtures.mjs
bun run check:types

test-go:
go vet ./...
go test -race ./...

test: test-ts test-go

preflight: check-gen test
26 changes: 26 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 85 additions & 0 deletions daemon/internal/protocol/fixtures_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package protocol

// Every fixture in schema/fixtures/<Def>/ round-trips through the generated Go types
// (spec §3 L0 done-test). valid-* must decode and re-encode to the same JSON;
// invalid-required-* must be rejected. Other invalid-* cases are lengths, patterns and
// enums, which only ajv checks (scripts/check-fixtures.mjs).

import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)

const fixtureDir = "../../../schema/fixtures"

func TestFixturesRoundTrip(t *testing.T) {
defs, err := os.ReadDir(fixtureDir)
if err != nil {
t.Fatal(err)
}
n := 0
for _, d := range defs {
newValue, ok := Registry[d.Name()]
if !ok {
t.Errorf("fixtures/%s: no generated type of that name", d.Name())
continue
}
files, _ := filepath.Glob(filepath.Join(fixtureDir, d.Name(), "*.json"))
for _, f := range files {
name := d.Name() + "/" + filepath.Base(f)
raw, err := os.ReadFile(f)
if err != nil {
t.Fatal(err)
}
v := newValue()
decodeErr := json.Unmarshal(raw, v)
switch base := filepath.Base(f); {
case strings.HasPrefix(base, "valid-"):
if decodeErr != nil {
t.Errorf("%s: %v", name, decodeErr)
continue
}
assertSameJSON(t, name, raw, v)
if env, ok := v.(*Envelope); ok {
data, err := DecodeEventData(env.Type, env.Data)
if err != nil {
t.Errorf("%s: data: %v", name, err)
continue
}
assertSameJSON(t, name+" data", env.Data, data)
}
case strings.HasPrefix(base, "invalid-required-"):
if decodeErr == nil {
t.Errorf("%s: decoded, want a missing-required-field error", name)
}
}
n++
}
}
if n == 0 {
t.Fatal("no fixtures found")
}
}

func assertSameJSON(t *testing.T, name string, want []byte, v any) {
t.Helper()
got, err := json.Marshal(v)
if err != nil {
t.Errorf("%s: marshal: %v", name, err)
return
}
var a, b any
if err := json.Unmarshal(want, &a); err != nil {
t.Fatalf("%s: %v", name, err)
}
if err := json.Unmarshal(got, &b); err != nil {
t.Fatalf("%s: %v", name, err)
}
if !reflect.DeepEqual(a, b) {
t.Errorf("%s: round trip changed the JSON\nwant %s\ngot %s", name, want, got)
}
}
Loading
Loading