From f9596e204db467202216db5cb535a8cbbf93148e Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:57:46 -0700 Subject: [PATCH] v4 L0: one schema, generated TS + Go contracts, fixtures, preflight schema/ (7 files, drafted by the SWE session) is the only source of wire shapes. scripts/gen.mjs writes gen/ts/protocol.ts, gen/ts/schemas.json (ajv bundle), gen/ts/tools.json (MCP tools, refs inlined) and daemon/internal/protocol/gen.go (structs, enums, required-key checks, event decode). It rejects composition keywords and inline objects, and checks that x-variants covers EventType exactly. 71 fixtures under schema/fixtures/ are checked by ajv (valid pass, invalid fail) and by a Go round-trip test (valid re-encode to the same JSON, invalid-required rejected). CI runs make check-gen, test-ts and test-go, with Bun and Go installed by setup actions. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MDs9DS3E4dzoHKdxtgQqyz --- .github/workflows/ci.yml | 24 +- .gitignore | 1 + AGENTS.md | 28 + CLAUDE.md | 1 + Makefile | 31 + bun.lock | 26 + daemon/internal/protocol/fixtures_test.go | 85 + daemon/internal/protocol/gen.go | 1744 +++++++++++++++++ gen/ts/protocol.ts | 547 ++++++ gen/ts/schemas.json | 1527 +++++++++++++++ gen/ts/tools.json | 1322 +++++++++++++ go.mod | 3 + package.json | 15 + schema/common.json | 79 + schema/events.json | 216 ++ .../invalid-required-runToken.json | 6 + schema/fixtures/ClaimRunResponse/valid-1.json | 7 + .../Envelope/invalid-required-podId.json | 7 + schema/fixtures/Envelope/invalid-version.json | 8 + .../Envelope/valid-daemon-status.json | 34 + .../Envelope/valid-decision-rule.json | 9 + schema/fixtures/Envelope/valid-error.json | 17 + schema/fixtures/Envelope/valid-hello.json | 51 + .../Envelope/valid-message-created.json | 72 + .../fixtures/Envelope/valid-message-post.json | 8 + .../Envelope/valid-message-updated.json | 28 + .../Envelope/valid-presence-changed.json | 21 + .../Envelope/valid-presence-heartbeat.json | 8 + schema/fixtures/Envelope/valid-run-wake.json | 21 + schema/fixtures/Envelope/valid-subscribe.json | 8 + .../fixtures/Envelope/valid-task-claimed.json | 22 + .../fixtures/Envelope/valid-task-create.json | 9 + .../fixtures/Envelope/valid-task-created.json | 25 + .../fixtures/Envelope/valid-task-updated.json | 35 + schema/fixtures/Envelope/valid-wake-ack.json | 8 + schema/fixtures/Message/valid-ask-1.json | 60 + schema/fixtures/Message/valid-ask-2.json | 53 + schema/fixtures/Message/valid-ask-3.json | 47 + schema/fixtures/Message/valid-error-1.json | 28 + schema/fixtures/Message/valid-error-2.json | 27 + schema/fixtures/Message/valid-error-3.json | 35 + schema/fixtures/Message/valid-fyi-1.json | 34 + schema/fixtures/Message/valid-fyi-2.json | 27 + schema/fixtures/Message/valid-fyi-3.json | 34 + schema/fixtures/Message/valid-human-post.json | 15 + schema/fixtures/Message/valid-progress-1.json | 28 + schema/fixtures/Message/valid-progress-2.json | 27 + schema/fixtures/Message/valid-progress-3.json | 28 + schema/fixtures/Message/valid-result-1.json | 35 + schema/fixtures/Message/valid-result-2.json | 29 + schema/fixtures/Message/valid-result-3.json | 34 + .../invalid-client-sent-receipt.json | 17 + .../fixtures/PostInput/invalid-link-http.json | 12 + .../PostInput/invalid-needs-both-targets.json | 24 + .../fixtures/PostInput/invalid-option-id.json | 21 + .../PostInput/invalid-required-summary.json | 4 + .../PostInput/invalid-summary-newline.json | 5 + .../PostInput/invalid-summary-too-long.json | 5 + schema/fixtures/PostInput/valid-ask-1.json | 25 + schema/fixtures/PostInput/valid-ask-2.json | 27 + schema/fixtures/PostInput/valid-ask-3.json | 21 + schema/fixtures/PostInput/valid-error-1.json | 6 + schema/fixtures/PostInput/valid-error-2.json | 5 + schema/fixtures/PostInput/valid-error-3.json | 13 + schema/fixtures/PostInput/valid-fyi-1.json | 12 + schema/fixtures/PostInput/valid-fyi-2.json | 5 + schema/fixtures/PostInput/valid-fyi-3.json | 12 + .../fixtures/PostInput/valid-progress-1.json | 6 + .../fixtures/PostInput/valid-progress-2.json | 5 + .../fixtures/PostInput/valid-progress-3.json | 6 + schema/fixtures/PostInput/valid-result-1.json | 13 + schema/fixtures/PostInput/valid-result-2.json | 7 + schema/fixtures/PostInput/valid-result-3.json | 12 + schema/fixtures/ReadInput/invalid-limit.json | 3 + schema/fixtures/ReadInput/valid-1.json | 4 + .../invalid-os-windows.json | 5 + .../RegisterComputerRequest/valid-1.json | 5 + .../Task/invalid-required-status.json | 12 + schema/fixtures/Task/valid-claimed.json | 23 + schema/fixtures/Task/valid-open.json | 13 + .../invalid-required-taskId.json | 1 + schema/fixtures/TaskClaimInput/valid-1.json | 3 + .../TaskUpdateInput/invalid-status-open.json | 4 + .../fixtures/TaskUpdateInput/valid-done.json | 5 + schema/fixtures/WhoamiInput/valid-1.json | 1 + schema/fixtures/WhoamiOutput/valid-1.json | 32 + schema/message.json | 156 ++ schema/pod-api.json | 160 ++ schema/task.json | 36 + schema/tokens.json | 13 + schema/tools.json | 100 + scripts/check-fixtures.mjs | 41 + scripts/gen.mjs | 228 +++ tsconfig.json | 12 + 94 files changed, 7704 insertions(+), 10 deletions(-) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 120000 CLAUDE.md create mode 100644 Makefile create mode 100644 bun.lock create mode 100644 daemon/internal/protocol/fixtures_test.go create mode 100644 daemon/internal/protocol/gen.go create mode 100644 gen/ts/protocol.ts create mode 100644 gen/ts/schemas.json create mode 100644 gen/ts/tools.json create mode 100644 go.mod create mode 100644 package.json create mode 100644 schema/common.json create mode 100644 schema/events.json create mode 100644 schema/fixtures/ClaimRunResponse/invalid-required-runToken.json create mode 100644 schema/fixtures/ClaimRunResponse/valid-1.json create mode 100644 schema/fixtures/Envelope/invalid-required-podId.json create mode 100644 schema/fixtures/Envelope/invalid-version.json create mode 100644 schema/fixtures/Envelope/valid-daemon-status.json create mode 100644 schema/fixtures/Envelope/valid-decision-rule.json create mode 100644 schema/fixtures/Envelope/valid-error.json create mode 100644 schema/fixtures/Envelope/valid-hello.json create mode 100644 schema/fixtures/Envelope/valid-message-created.json create mode 100644 schema/fixtures/Envelope/valid-message-post.json create mode 100644 schema/fixtures/Envelope/valid-message-updated.json create mode 100644 schema/fixtures/Envelope/valid-presence-changed.json create mode 100644 schema/fixtures/Envelope/valid-presence-heartbeat.json create mode 100644 schema/fixtures/Envelope/valid-run-wake.json create mode 100644 schema/fixtures/Envelope/valid-subscribe.json create mode 100644 schema/fixtures/Envelope/valid-task-claimed.json create mode 100644 schema/fixtures/Envelope/valid-task-create.json create mode 100644 schema/fixtures/Envelope/valid-task-created.json create mode 100644 schema/fixtures/Envelope/valid-task-updated.json create mode 100644 schema/fixtures/Envelope/valid-wake-ack.json create mode 100644 schema/fixtures/Message/valid-ask-1.json create mode 100644 schema/fixtures/Message/valid-ask-2.json create mode 100644 schema/fixtures/Message/valid-ask-3.json create mode 100644 schema/fixtures/Message/valid-error-1.json create mode 100644 schema/fixtures/Message/valid-error-2.json create mode 100644 schema/fixtures/Message/valid-error-3.json create mode 100644 schema/fixtures/Message/valid-fyi-1.json create mode 100644 schema/fixtures/Message/valid-fyi-2.json create mode 100644 schema/fixtures/Message/valid-fyi-3.json create mode 100644 schema/fixtures/Message/valid-human-post.json create mode 100644 schema/fixtures/Message/valid-progress-1.json create mode 100644 schema/fixtures/Message/valid-progress-2.json create mode 100644 schema/fixtures/Message/valid-progress-3.json create mode 100644 schema/fixtures/Message/valid-result-1.json create mode 100644 schema/fixtures/Message/valid-result-2.json create mode 100644 schema/fixtures/Message/valid-result-3.json create mode 100644 schema/fixtures/PostInput/invalid-client-sent-receipt.json create mode 100644 schema/fixtures/PostInput/invalid-link-http.json create mode 100644 schema/fixtures/PostInput/invalid-needs-both-targets.json create mode 100644 schema/fixtures/PostInput/invalid-option-id.json create mode 100644 schema/fixtures/PostInput/invalid-required-summary.json create mode 100644 schema/fixtures/PostInput/invalid-summary-newline.json create mode 100644 schema/fixtures/PostInput/invalid-summary-too-long.json create mode 100644 schema/fixtures/PostInput/valid-ask-1.json create mode 100644 schema/fixtures/PostInput/valid-ask-2.json create mode 100644 schema/fixtures/PostInput/valid-ask-3.json create mode 100644 schema/fixtures/PostInput/valid-error-1.json create mode 100644 schema/fixtures/PostInput/valid-error-2.json create mode 100644 schema/fixtures/PostInput/valid-error-3.json create mode 100644 schema/fixtures/PostInput/valid-fyi-1.json create mode 100644 schema/fixtures/PostInput/valid-fyi-2.json create mode 100644 schema/fixtures/PostInput/valid-fyi-3.json create mode 100644 schema/fixtures/PostInput/valid-progress-1.json create mode 100644 schema/fixtures/PostInput/valid-progress-2.json create mode 100644 schema/fixtures/PostInput/valid-progress-3.json create mode 100644 schema/fixtures/PostInput/valid-result-1.json create mode 100644 schema/fixtures/PostInput/valid-result-2.json create mode 100644 schema/fixtures/PostInput/valid-result-3.json create mode 100644 schema/fixtures/ReadInput/invalid-limit.json create mode 100644 schema/fixtures/ReadInput/valid-1.json create mode 100644 schema/fixtures/RegisterComputerRequest/invalid-os-windows.json create mode 100644 schema/fixtures/RegisterComputerRequest/valid-1.json create mode 100644 schema/fixtures/Task/invalid-required-status.json create mode 100644 schema/fixtures/Task/valid-claimed.json create mode 100644 schema/fixtures/Task/valid-open.json create mode 100644 schema/fixtures/TaskClaimInput/invalid-required-taskId.json create mode 100644 schema/fixtures/TaskClaimInput/valid-1.json create mode 100644 schema/fixtures/TaskUpdateInput/invalid-status-open.json create mode 100644 schema/fixtures/TaskUpdateInput/valid-done.json create mode 100644 schema/fixtures/WhoamiInput/valid-1.json create mode 100644 schema/fixtures/WhoamiOutput/valid-1.json create mode 100644 schema/message.json create mode 100644 schema/pod-api.json create mode 100644 schema/task.json create mode 100644 schema/tokens.json create mode 100644 schema/tools.json create mode 100644 scripts/check-fixtures.mjs create mode 100644 scripts/gen.mjs create mode 100644 tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19080c8448..6e55fe0b77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..6408597a4b --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..d4b2df5a00 --- /dev/null +++ b/Makefile @@ -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 diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000000..4496851f7a --- /dev/null +++ b/bun.lock @@ -0,0 +1,26 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "commonly-v4", + "devDependencies": { + "ajv": "8.17.1", + "typescript": "5.9.3", + }, + }, + }, + "packages": { + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.9", "", {}, "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + } +} diff --git a/daemon/internal/protocol/fixtures_test.go b/daemon/internal/protocol/fixtures_test.go new file mode 100644 index 0000000000..39f87bff10 --- /dev/null +++ b/daemon/internal/protocol/fixtures_test.go @@ -0,0 +1,85 @@ +package protocol + +// Every fixture in schema/fixtures// 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) + } +} diff --git a/daemon/internal/protocol/gen.go b/daemon/internal/protocol/gen.go new file mode 100644 index 0000000000..66745bb497 --- /dev/null +++ b/daemon/internal/protocol/gen.go @@ -0,0 +1,1744 @@ +// Code generated by scripts/gen.mjs from schema/. DO NOT EDIT. + +package protocol + +import ( + "encoding/json" + "fmt" +) + +type PodID = string + +type UserID = string + +type AgentID = string + +// A user or an agent: who a task is assigned to, or who acted. System actors use sys_. +type ActorID = string + +type ComputerID = string + +type TaskID = string + +type MessageID = string + +type RunID = string + +type DecisionID = string + +type WakeID = string + +// Monotonic per pod, assigned by the pod. Starts at 1. +type Seq = int64 + +// RFC 3339 in UTC. Set by the server only. +type Timestamp = string + +type ActorKind string + +const ( + ActorKindHuman ActorKind = "human" + ActorKindAgent ActorKind = "agent" + ActorKindSystem ActorKind = "system" +) + +// Valid reports whether x is one of the schema's values. +func (x ActorKind) Valid() bool { + switch x { + case ActorKindHuman, ActorKindAgent, ActorKindSystem: + return true + } + return false +} + +type Actor struct { + Kind ActorKind `json:"kind"` + ID ActorID `json:"id"` + DisplayName string `json:"displayName"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *Actor) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"kind", "id", "displayName"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("Actor: missing required field %q", k) + } + } + type plain Actor + return json.Unmarshal(b, (*plain)(x)) +} + +// Which agent program a registered agent runs. fake is the deterministic test agent. +type Harness string + +const ( + HarnessClaudeCode Harness = "claude-code" + HarnessCodex Harness = "codex" + HarnessFake Harness = "fake" +) + +// Valid reports whether x is one of the schema's values. +func (x Harness) Valid() bool { + switch x { + case HarnessClaudeCode, HarnessCodex, HarnessFake: + return true + } + return false +} + +// Stable error codes, shared by HTTP responses, MCP tool errors and the socket error event. +type ErrorCode string + +const ( + ErrorCodeInvalidInput ErrorCode = "invalid_input" + ErrorCodeSummaryRequired ErrorCode = "summary_required" + ErrorCodeUnauthorized ErrorCode = "unauthorized" + ErrorCodeForbidden ErrorCode = "forbidden" + ErrorCodeNotFound ErrorCode = "not_found" + ErrorCodeGone ErrorCode = "gone" + ErrorCodeAlreadyClaimed ErrorCode = "already_claimed" + ErrorCodeAlreadyRuled ErrorCode = "already_ruled" + ErrorCodeAuthorizationPending ErrorCode = "authorization_pending" + ErrorCodeSlowDown ErrorCode = "slow_down" + ErrorCodeExpiredToken ErrorCode = "expired_token" + ErrorCodeInvalidGrant ErrorCode = "invalid_grant" + ErrorCodeRateLimited ErrorCode = "rate_limited" + ErrorCodeInternal ErrorCode = "internal" +) + +// Valid reports whether x is one of the schema's values. +func (x ErrorCode) Valid() bool { + switch x { + case ErrorCodeInvalidInput, ErrorCodeSummaryRequired, ErrorCodeUnauthorized, ErrorCodeForbidden, ErrorCodeNotFound, ErrorCodeGone, ErrorCodeAlreadyClaimed, ErrorCodeAlreadyRuled, ErrorCodeAuthorizationPending, ErrorCodeSlowDown, ErrorCodeExpiredToken, ErrorCodeInvalidGrant, ErrorCodeRateLimited, ErrorCodeInternal: + return true + } + return false +} + +type ErrorBody struct { + Code ErrorCode `json:"code"` + Message string `json:"message"` + // The input field at fault, when there is one. + Field *string `json:"field,omitempty"` + // The id the error is about, when there is one. + Ref *string `json:"ref,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *ErrorBody) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"code", "message"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("ErrorBody: missing required field %q", k) + } + } + type plain ErrorBody + return json.Unmarshal(b, (*plain)(x)) +} + +type TokenKind string + +const ( + TokenKindUser TokenKind = "user" + TokenKindComputer TokenKind = "computer" + TokenKindRun TokenKind = "run" +) + +// Valid reports whether x is one of the schema's values. +func (x TokenKind) Valid() bool { + switch x { + case TokenKindUser, TokenKindComputer, TokenKindRun: + return true + } + return false +} + +// Bound to one user. 30 days, refreshable. +type UserToken = string + +// Bound to one user and one computer. Valid until revoked. +type ComputerToken = string + +// Bound to one agent, pod and run (and task, if any). Ends with the run; at most 2 hours. +type RunToken = string + +type TaskStatus string + +const ( + TaskStatusOpen TaskStatus = "open" + TaskStatusInProgress TaskStatus = "in_progress" + TaskStatusBlocked TaskStatus = "blocked" + TaskStatusDone TaskStatus = "done" + TaskStatusFailed TaskStatus = "failed" +) + +// Valid reports whether x is one of the schema's values. +func (x TaskStatus) Valid() bool { + switch x { + case TaskStatusOpen, TaskStatusInProgress, TaskStatusBlocked, TaskStatusDone, TaskStatusFailed: + return true + } + return false +} + +// One active claim per task. The lease is renewed by any MCP call on the claiming run. +type Claim struct { + By Actor `json:"by"` + RunID RunID `json:"runId"` + LeaseUntil Timestamp `json:"leaseUntil"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *Claim) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"by", "runId", "leaseUntil"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("Claim: missing required field %q", k) + } + } + type plain Claim + return json.Unmarshal(b, (*plain)(x)) +} + +type Task struct { + ID TaskID `json:"id"` + PodID PodID `json:"podId"` + Title string `json:"title"` + Body *string `json:"body,omitempty"` + Status TaskStatus `json:"status"` + Assignee *ActorID `json:"assignee,omitempty"` + CreatedBy Actor `json:"createdBy"` + Claim *Claim `json:"claim,omitempty"` + CreatedAt Timestamp `json:"createdAt"` + UpdatedAt Timestamp `json:"updatedAt"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *Task) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"id", "podId", "title", "status", "createdBy", "createdAt", "updatedAt"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("Task: missing required field %q", k) + } + } + type plain Task + return json.Unmarshal(b, (*plain)(x)) +} + +type MessageKind string + +const ( + MessageKindResult MessageKind = "result" + MessageKindAsk MessageKind = "ask" + MessageKindProgress MessageKind = "progress" + MessageKindFyi MessageKind = "fyi" + MessageKindError MessageKind = "error" +) + +// Valid reports whether x is one of the schema's values. +func (x MessageKind) Valid() bool { + switch x { + case MessageKindResult, MessageKindAsk, MessageKindProgress, MessageKindFyi, MessageKindError: + return true + } + return false +} + +type MessageStatus string + +const ( + MessageStatusDone MessageStatus = "done" + MessageStatusInProgress MessageStatus = "in_progress" + MessageStatusBlocked MessageStatus = "blocked" + MessageStatusFailed MessageStatus = "failed" +) + +// Valid reports whether x is one of the schema's values. +func (x MessageStatus) Valid() bool { + switch x { + case MessageStatusDone, MessageStatusInProgress, MessageStatusBlocked, MessageStatusFailed: + return true + } + return false +} + +type NeedsAction string + +const ( + NeedsActionApprove NeedsAction = "approve" + NeedsActionDecide NeedsAction = "decide" + NeedsActionReview NeedsAction = "review" + NeedsActionAnswer NeedsAction = "answer" + NeedsActionUnblock NeedsAction = "unblock" +) + +// Valid reports whether x is one of the schema's values. +func (x NeedsAction) Valid() bool { + switch x { + case NeedsActionApprove, NeedsActionDecide, NeedsActionReview, NeedsActionAnswer, NeedsActionUnblock: + return true + } + return false +} + +type MemberRole string + +const ( + MemberRoleMember MemberRole = "member" + MemberRoleOwner MemberRole = "owner" +) + +// Valid reports whether x is one of the schema's values. +func (x MemberRole) Valid() bool { + switch x { + case MemberRoleMember, MemberRoleOwner: + return true + } + return false +} + +// Exactly one of userIds or role. +type NeedsTarget struct { + UserIds []UserID `json:"userIds,omitempty"` + Role *MemberRole `json:"role,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *NeedsTarget) UnmarshalJSON(b []byte) error { + type plain NeedsTarget + return json.Unmarshal(b, (*plain)(x)) +} + +// The only field that notifies anyone. progress and fyi posts never notify. +type Needs struct { + To NeedsTarget `json:"to"` + Action NeedsAction `json:"action"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *Needs) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"to", "action"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("Needs: missing required field %q", k) + } + } + type plain Needs + return json.Unmarshal(b, (*plain)(x)) +} + +type OptionID string + +const ( + OptionIDA OptionID = "a" + OptionIDB OptionID = "b" + OptionIDC OptionID = "c" + OptionIDD OptionID = "d" + OptionIDE OptionID = "e" +) + +// Valid reports whether x is one of the schema's values. +func (x OptionID) Valid() bool { + switch x { + case OptionIDA, OptionIDB, OptionIDC, OptionIDD, OptionIDE: + return true + } + return false +} + +type Option struct { + ID OptionID `json:"id"` + Label string `json:"label"` + Description *string `json:"description,omitempty"` + Recommended *bool `json:"recommended,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *Option) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"id", "label"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("Option: missing required field %q", k) + } + } + type plain Option + return json.Unmarshal(b, (*plain)(x)) +} + +type LinkType string + +const ( + LinkTypePr LinkType = "pr" + LinkTypeCommit LinkType = "commit" + LinkTypeTask LinkType = "task" + LinkTypeDoc LinkType = "doc" + LinkTypeFile LinkType = "file" + LinkTypeLog LinkType = "log" +) + +// Valid reports whether x is one of the schema's values. +func (x LinkType) Valid() bool { + switch x { + case LinkTypePr, LinkTypeCommit, LinkTypeTask, LinkTypeDoc, LinkTypeFile, LinkTypeLog: + return true + } + return false +} + +type Link struct { + Label string `json:"label"` + // https only, and no credentials in the URL. + URL string `json:"url"` + Type LinkType `json:"type"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *Link) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"label", "url", "type"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("Link: missing required field %q", k) + } + } + type plain Link + return json.Unmarshal(b, (*plain)(x)) +} + +type WakeReason string + +const ( + WakeReasonTaskAssigned WakeReason = "task_assigned" + WakeReasonMention WakeReason = "mention" + WakeReasonDecisionRuled WakeReason = "decision_ruled" +) + +// Valid reports whether x is one of the schema's values. +func (x WakeReason) Valid() bool { + switch x { + case WakeReasonTaskAssigned, WakeReasonMention, WakeReasonDecisionRuled: + return true + } + return false +} + +// Stamped by the pod from the run record. An agent can't write any of it. +type Receipt struct { + RunID RunID `json:"runId"` + AgentID AgentID `json:"agentId"` + ComputerID ComputerID `json:"computerId"` + Harness Harness `json:"harness"` + // Reported by the daemon at session start. + HarnessVersion *string `json:"harnessVersion,omitempty"` + StartedAt Timestamp `json:"startedAt"` + PostedAt Timestamp `json:"postedAt"` + // MCP calls the pod saw on this run's token. + ToolCalls int64 `json:"toolCalls"` + TaskID *TaskID `json:"taskId,omitempty"` + WakeReason WakeReason `json:"wakeReason"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *Receipt) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"runId", "agentId", "computerId", "harness", "startedAt", "postedAt", "toolCalls", "wakeReason"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("Receipt: missing required field %q", k) + } + } + type plain Receipt + return json.Unmarshal(b, (*plain)(x)) +} + +type DecisionState string + +const ( + DecisionStateOpen DecisionState = "open" + DecisionStateRuled DecisionState = "ruled" + DecisionStateExpired DecisionState = "expired" +) + +// Valid reports whether x is one of the schema's values. +func (x DecisionState) Valid() bool { + switch x { + case DecisionStateOpen, DecisionStateRuled, DecisionStateExpired: + return true + } + return false +} + +type Ruling struct { + OptionID OptionID `json:"optionId"` + By Actor `json:"by"` + At Timestamp `json:"at"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *Ruling) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"optionId", "by", "at"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("Ruling: missing required field %q", k) + } + } + type plain Ruling + return json.Unmarshal(b, (*plain)(x)) +} + +type Decision struct { + ID DecisionID `json:"id"` + State DecisionState `json:"state"` + Ruling *Ruling `json:"ruling,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *Decision) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"id", "state"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("Decision: missing required field %q", k) + } + } + type plain Decision + return json.Unmarshal(b, (*plain)(x)) +} + +type PostInput struct { + // The headline, outcome first. One line, at most 120 characters. + Summary string `json:"summary"` + Kind MessageKind `json:"kind"` + Status MessageStatus `json:"status"` + Needs *Needs `json:"needs,omitempty"` + Options []Option `json:"options,omitempty"` + TaskID *TaskID `json:"taskId,omitempty"` + Links []Link `json:"links,omitempty"` + // What isn't verified. + Caveat *string `json:"caveat,omitempty"` + // Markdown. Folded by default in clients. + Body *string `json:"body,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *PostInput) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"summary", "kind", "status"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("PostInput: missing required field %q", k) + } + } + type plain PostInput + return json.Unmarshal(b, (*plain)(x)) +} + +type Message struct { + ID MessageID `json:"id"` + Seq Seq `json:"seq"` + PodID PodID `json:"podId"` + Author Actor `json:"author"` + CreatedAt Timestamp `json:"createdAt"` + RunID *RunID `json:"runId,omitempty"` + ThreadOf *MessageID `json:"threadOf,omitempty"` + Summary string `json:"summary"` + Kind MessageKind `json:"kind"` + Status MessageStatus `json:"status"` + Needs *Needs `json:"needs,omitempty"` + Options []Option `json:"options,omitempty"` + TaskID *TaskID `json:"taskId,omitempty"` + Links []Link `json:"links,omitempty"` + Caveat *string `json:"caveat,omitempty"` + Body *string `json:"body,omitempty"` + Decision *Decision `json:"decision,omitempty"` + Receipt *Receipt `json:"receipt,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *Message) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"id", "seq", "podId", "author", "createdAt", "summary", "kind", "status"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("Message: missing required field %q", k) + } + } + type plain Message + return json.Unmarshal(b, (*plain)(x)) +} + +type EventType string + +const ( + EventTypeHello EventType = "hello" + EventTypeMessageCreated EventType = "message.created" + EventTypeMessageUpdated EventType = "message.updated" + EventTypeTaskCreated EventType = "task.created" + EventTypeTaskUpdated EventType = "task.updated" + EventTypeTaskClaimed EventType = "task.claimed" + EventTypePresenceChanged EventType = "presence.changed" + EventTypeRunWake EventType = "run.wake" + EventTypeError EventType = "error" + EventTypeSubscribe EventType = "subscribe" + EventTypeMessagePost EventType = "message.post" + EventTypeTaskCreate EventType = "task.create" + EventTypeDecisionRule EventType = "decision.rule" + EventTypePresenceHeartbeat EventType = "presence.heartbeat" + EventTypeWakeAck EventType = "wake.ack" + EventTypeDaemonStatus EventType = "daemon.status" +) + +// Valid reports whether x is one of the schema's values. +func (x EventType) Valid() bool { + switch x { + case EventTypeHello, EventTypeMessageCreated, EventTypeMessageUpdated, EventTypeTaskCreated, EventTypeTaskUpdated, EventTypeTaskClaimed, EventTypePresenceChanged, EventTypeRunWake, EventTypeError, EventTypeSubscribe, EventTypeMessagePost, EventTypeTaskCreate, EventTypeDecisionRule, EventTypePresenceHeartbeat, EventTypeWakeAck, EventTypeDaemonStatus: + return true + } + return false +} + +type Envelope struct { + V int64 `json:"v"` + Type EventType `json:"type"` + Seq *Seq `json:"seq,omitempty"` + PodID PodID `json:"podId"` + Ts *Timestamp `json:"ts,omitempty"` + Actor *Actor `json:"actor,omitempty"` + // Matches the definition x-variants names for type. + Data json.RawMessage `json:"data"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *Envelope) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"v", "type", "podId", "data"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("Envelope: missing required field %q", k) + } + } + type plain Envelope + return json.Unmarshal(b, (*plain)(x)) +} + +type Member struct { + Actor Actor `json:"actor"` + Role MemberRole `json:"role"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *Member) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"actor", "role"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("Member: missing required field %q", k) + } + } + type plain Member + return json.Unmarshal(b, (*plain)(x)) +} + +type PresenceState string + +const ( + PresenceStateOnline PresenceState = "online" + PresenceStateOffline PresenceState = "offline" + PresenceStateWorking PresenceState = "working" +) + +// Valid reports whether x is one of the schema's values. +func (x PresenceState) Valid() bool { + switch x { + case PresenceStateOnline, PresenceStateOffline, PresenceStateWorking: + return true + } + return false +} + +type AgentPresence struct { + Agent Actor `json:"agent"` + Harness Harness `json:"harness"` + ComputerID ComputerID `json:"computerId"` + State PresenceState `json:"state"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *AgentPresence) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"agent", "harness", "computerId", "state"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("AgentPresence: missing required field %q", k) + } + } + type plain AgentPresence + return json.Unmarshal(b, (*plain)(x)) +} + +type Hello struct { + PodID PodID `json:"podId"` + // 0 for an empty pod. + HeadSeq int64 `json:"headSeq"` + You Actor `json:"you"` + Members []Member `json:"members"` + Agents []AgentPresence `json:"agents"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *Hello) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"podId", "headSeq", "you", "members", "agents"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("Hello: missing required field %q", k) + } + } + type plain Hello + return json.Unmarshal(b, (*plain)(x)) +} + +// Only a decision changes after a message is created. The summary never changes. +type MessageUpdated struct { + ID MessageID `json:"id"` + Decision Decision `json:"decision"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *MessageUpdated) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"id", "decision"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("MessageUpdated: missing required field %q", k) + } + } + type plain MessageUpdated + return json.Unmarshal(b, (*plain)(x)) +} + +type TaskClaimed struct { + TaskID TaskID `json:"taskId"` + By Actor `json:"by"` + RunID RunID `json:"runId"` + LeaseUntil Timestamp `json:"leaseUntil"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *TaskClaimed) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"taskId", "by", "runId", "leaseUntil"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("TaskClaimed: missing required field %q", k) + } + } + type plain TaskClaimed + return json.Unmarshal(b, (*plain)(x)) +} + +type PresenceChanged struct { + Actor Actor `json:"actor"` + State PresenceState `json:"state"` + ComputerID *ComputerID `json:"computerId,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *PresenceChanged) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"actor", "state"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("PresenceChanged: missing required field %q", k) + } + } + type plain PresenceChanged + return json.Unmarshal(b, (*plain)(x)) +} + +type WakeRefs struct { + TaskID *TaskID `json:"taskId,omitempty"` + MessageID *MessageID `json:"messageId,omitempty"` + DecisionID *DecisionID `json:"decisionId,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *WakeRefs) UnmarshalJSON(b []byte) error { + type plain WakeRefs + return json.Unmarshal(b, (*plain)(x)) +} + +type RunWake struct { + WakeID WakeID `json:"wakeId"` + AgentID AgentID `json:"agentId"` + Reason WakeReason `json:"reason"` + Refs WakeRefs `json:"refs"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *RunWake) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"wakeId", "agentId", "reason", "refs"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("RunWake: missing required field %q", k) + } + } + type plain RunWake + return json.Unmarshal(b, (*plain)(x)) +} + +type Subscribe struct { + // 0 replays from the start of the kept log. + SinceSeq int64 `json:"sinceSeq"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *Subscribe) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"sinceSeq"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("Subscribe: missing required field %q", k) + } + } + type plain Subscribe + return json.Unmarshal(b, (*plain)(x)) +} + +// A human post. +type MessagePost struct { + Text string `json:"text"` + ThreadOf *MessageID `json:"threadOf,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *MessagePost) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"text"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("MessagePost: missing required field %q", k) + } + } + type plain MessagePost + return json.Unmarshal(b, (*plain)(x)) +} + +type TaskCreate struct { + Title string `json:"title"` + Assignee *ActorID `json:"assignee,omitempty"` + Body *string `json:"body,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *TaskCreate) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"title"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("TaskCreate: missing required field %q", k) + } + } + type plain TaskCreate + return json.Unmarshal(b, (*plain)(x)) +} + +type DecisionRule struct { + DecisionID DecisionID `json:"decisionId"` + OptionID OptionID `json:"optionId"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *DecisionRule) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"decisionId", "optionId"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("DecisionRule: missing required field %q", k) + } + } + type plain DecisionRule + return json.Unmarshal(b, (*plain)(x)) +} + +// Every 15 s from the daemon. +type PresenceHeartbeat struct { + ComputerID *ComputerID `json:"computerId,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *PresenceHeartbeat) UnmarshalJSON(b []byte) error { + type plain PresenceHeartbeat + return json.Unmarshal(b, (*plain)(x)) +} + +type WakeAck struct { + WakeID WakeID `json:"wakeId"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *WakeAck) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"wakeId"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("WakeAck: missing required field %q", k) + } + } + type plain WakeAck + return json.Unmarshal(b, (*plain)(x)) +} + +type ComputerStatus struct { + ID ComputerID `json:"id"` + Name string `json:"name"` + Online bool `json:"online"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *ComputerStatus) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"id", "name", "online"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("ComputerStatus: missing required field %q", k) + } + } + type plain ComputerStatus + return json.Unmarshal(b, (*plain)(x)) +} + +type RunStatus struct { + RunID RunID `json:"runId"` + AgentID AgentID `json:"agentId"` + StartedAt Timestamp `json:"startedAt"` + TaskID *TaskID `json:"taskId,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *RunStatus) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"runId", "agentId", "startedAt"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("RunStatus: missing required field %q", k) + } + } + type plain RunStatus + return json.Unmarshal(b, (*plain)(x)) +} + +type DaemonStatus struct { + Computers []ComputerStatus `json:"computers"` + Agents []AgentPresence `json:"agents"` + Runs []RunStatus `json:"runs"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *DaemonStatus) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"computers", "agents", "runs"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("DaemonStatus: missing required field %q", k) + } + } + type plain DaemonStatus + return json.Unmarshal(b, (*plain)(x)) +} + +type DeviceStartResponse struct { + DeviceCode string `json:"deviceCode"` + // What the person types on the sign-in page, e.g. WDJB-MJHT. + UserCode string `json:"userCode"` + VerificationURI string `json:"verificationUri"` + // Seconds between polls. + Interval int64 `json:"interval"` + // Seconds until the code expires. + ExpiresIn int64 `json:"expiresIn"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *DeviceStartResponse) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"deviceCode", "userCode", "verificationUri", "interval", "expiresIn"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("DeviceStartResponse: missing required field %q", k) + } + } + type plain DeviceStartResponse + return json.Unmarshal(b, (*plain)(x)) +} + +type DeviceTokenRequest struct { + DeviceCode string `json:"deviceCode"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *DeviceTokenRequest) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"deviceCode"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("DeviceTokenRequest: missing required field %q", k) + } + } + type plain DeviceTokenRequest + return json.Unmarshal(b, (*plain)(x)) +} + +type DeviceTokenResponse struct { + Token UserToken `json:"token"` + User Actor `json:"user"` + ExpiresAt Timestamp `json:"expiresAt"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *DeviceTokenResponse) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"token", "user", "expiresAt"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("DeviceTokenResponse: missing required field %q", k) + } + } + type plain DeviceTokenResponse + return json.Unmarshal(b, (*plain)(x)) +} + +type CreatePodRequest struct { + Name string `json:"name"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *CreatePodRequest) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"name"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("CreatePodRequest: missing required field %q", k) + } + } + type plain CreatePodRequest + return json.Unmarshal(b, (*plain)(x)) +} + +type Pod struct { + ID PodID `json:"id"` + Name string `json:"name"` + CreatedAt Timestamp `json:"createdAt"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *Pod) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"id", "name", "createdAt"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("Pod: missing required field %q", k) + } + } + type plain Pod + return json.Unmarshal(b, (*plain)(x)) +} + +type CreateInviteRequest struct { + // Defaults to 1. + MaxUses *int64 `json:"maxUses,omitempty"` + // Defaults to 7 days. + TtlSeconds *int64 `json:"ttlSeconds,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *CreateInviteRequest) UnmarshalJSON(b []byte) error { + type plain CreateInviteRequest + return json.Unmarshal(b, (*plain)(x)) +} + +type Invite struct { + Code string `json:"code"` + URL string `json:"url"` + ExpiresAt Timestamp `json:"expiresAt"` + UsesLeft int64 `json:"usesLeft"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *Invite) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"code", "url", "expiresAt", "usesLeft"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("Invite: missing required field %q", k) + } + } + type plain Invite + return json.Unmarshal(b, (*plain)(x)) +} + +type AcceptInviteResponse struct { + Pod Pod `json:"pod"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *AcceptInviteResponse) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"pod"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("AcceptInviteResponse: missing required field %q", k) + } + } + type plain AcceptInviteResponse + return json.Unmarshal(b, (*plain)(x)) +} + +type OS string + +const ( + OSDarwin OS = "darwin" + OSLinux OS = "linux" +) + +// Valid reports whether x is one of the schema's values. +func (x OS) Valid() bool { + switch x { + case OSDarwin, OSLinux: + return true + } + return false +} + +type Arch string + +const ( + ArchArm64 Arch = "arm64" + ArchAmd64 Arch = "amd64" +) + +// Valid reports whether x is one of the schema's values. +func (x Arch) Valid() bool { + switch x { + case ArchArm64, ArchAmd64: + return true + } + return false +} + +type RegisterComputerRequest struct { + Name string `json:"name"` + OS OS `json:"os"` + Arch Arch `json:"arch"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *RegisterComputerRequest) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"name", "os", "arch"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("RegisterComputerRequest: missing required field %q", k) + } + } + type plain RegisterComputerRequest + return json.Unmarshal(b, (*plain)(x)) +} + +type RegisterComputerResponse struct { + ComputerID ComputerID `json:"computerId"` + Token ComputerToken `json:"token"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *RegisterComputerResponse) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"computerId", "token"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("RegisterComputerResponse: missing required field %q", k) + } + } + type plain RegisterComputerResponse + return json.Unmarshal(b, (*plain)(x)) +} + +type RegisterAgentRequest struct { + Harness Harness `json:"harness"` + DisplayName string `json:"displayName"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *RegisterAgentRequest) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"harness", "displayName"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("RegisterAgentRequest: missing required field %q", k) + } + } + type plain RegisterAgentRequest + return json.Unmarshal(b, (*plain)(x)) +} + +type RegisterAgentResponse struct { + AgentID AgentID `json:"agentId"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *RegisterAgentResponse) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"agentId"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("RegisterAgentResponse: missing required field %q", k) + } + } + type plain RegisterAgentResponse + return json.Unmarshal(b, (*plain)(x)) +} + +type ClaimRunRequest struct { + WakeID WakeID `json:"wakeId"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *ClaimRunRequest) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"wakeId"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("ClaimRunRequest: missing required field %q", k) + } + } + type plain ClaimRunRequest + return json.Unmarshal(b, (*plain)(x)) +} + +// The brief is built by the server and sent as the agent's first prompt. The run token goes to the agent; the computer token never does. +type ClaimRunResponse struct { + RunID RunID `json:"runId"` + RunToken RunToken `json:"runToken"` + Brief string `json:"brief"` + MCPURL string `json:"mcpUrl"` + ExpiresAt Timestamp `json:"expiresAt"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *ClaimRunResponse) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"runId", "runToken", "brief", "mcpUrl", "expiresAt"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("ClaimRunResponse: missing required field %q", k) + } + } + type plain ClaimRunResponse + return json.Unmarshal(b, (*plain)(x)) +} + +type RunOutcome string + +const ( + RunOutcomeCompleted RunOutcome = "completed" + RunOutcomeFailed RunOutcome = "failed" + RunOutcomeCancelled RunOutcome = "cancelled" +) + +// Valid reports whether x is one of the schema's values. +func (x RunOutcome) Valid() bool { + switch x { + case RunOutcomeCompleted, RunOutcomeFailed, RunOutcomeCancelled: + return true + } + return false +} + +type EndRunRequest struct { + Outcome RunOutcome `json:"outcome"` + HarnessVersion *string `json:"harnessVersion,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *EndRunRequest) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"outcome"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("EndRunRequest: missing required field %q", k) + } + } + type plain EndRunRequest + return json.Unmarshal(b, (*plain)(x)) +} + +type WhoamiInput struct { +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *WhoamiInput) UnmarshalJSON(b []byte) error { + type plain WhoamiInput + return json.Unmarshal(b, (*plain)(x)) +} + +type WhoamiOutput struct { + Agent Actor `json:"agent"` + PodID PodID `json:"podId"` + RunID RunID `json:"runId"` + Task *Task `json:"task,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *WhoamiOutput) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"agent", "podId", "runId"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("WhoamiOutput: missing required field %q", k) + } + } + type plain WhoamiOutput + return json.Unmarshal(b, (*plain)(x)) +} + +type ReadInput struct { + SinceSeq *int64 `json:"sinceSeq,omitempty"` + // Defaults to 20. + Limit *int64 `json:"limit,omitempty"` + WithBody *bool `json:"withBody,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *ReadInput) UnmarshalJSON(b []byte) error { + type plain ReadInput + return json.Unmarshal(b, (*plain)(x)) +} + +type ReadOutput struct { + Messages []Message `json:"messages"` + Tasks []Task `json:"tasks"` + HeadSeq int64 `json:"headSeq"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *ReadOutput) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"messages", "tasks", "headSeq"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("ReadOutput: missing required field %q", k) + } + } + type plain ReadOutput + return json.Unmarshal(b, (*plain)(x)) +} + +type TaskListInput struct { + Status *TaskStatus `json:"status,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *TaskListInput) UnmarshalJSON(b []byte) error { + type plain TaskListInput + return json.Unmarshal(b, (*plain)(x)) +} + +type TaskListOutput struct { + Tasks []Task `json:"tasks"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *TaskListOutput) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"tasks"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("TaskListOutput: missing required field %q", k) + } + } + type plain TaskListOutput + return json.Unmarshal(b, (*plain)(x)) +} + +type TaskClaimInput struct { + TaskID TaskID `json:"taskId"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *TaskClaimInput) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"taskId"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("TaskClaimInput: missing required field %q", k) + } + } + type plain TaskClaimInput + return json.Unmarshal(b, (*plain)(x)) +} + +type TaskClaimOutput struct { + TaskID TaskID `json:"taskId"` + LeaseUntil Timestamp `json:"leaseUntil"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *TaskClaimOutput) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"taskId", "leaseUntil"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("TaskClaimOutput: missing required field %q", k) + } + } + type plain TaskClaimOutput + return json.Unmarshal(b, (*plain)(x)) +} + +type TaskUpdateStatus string + +const ( + TaskUpdateStatusInProgress TaskUpdateStatus = "in_progress" + TaskUpdateStatusBlocked TaskUpdateStatus = "blocked" + TaskUpdateStatusDone TaskUpdateStatus = "done" + TaskUpdateStatusFailed TaskUpdateStatus = "failed" +) + +// Valid reports whether x is one of the schema's values. +func (x TaskUpdateStatus) Valid() bool { + switch x { + case TaskUpdateStatusInProgress, TaskUpdateStatusBlocked, TaskUpdateStatusDone, TaskUpdateStatusFailed: + return true + } + return false +} + +type TaskUpdateInput struct { + TaskID TaskID `json:"taskId"` + Status TaskUpdateStatus `json:"status"` + Note *string `json:"note,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *TaskUpdateInput) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"taskId", "status"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("TaskUpdateInput: missing required field %q", k) + } + } + type plain TaskUpdateInput + return json.Unmarshal(b, (*plain)(x)) +} + +type PostOutput struct { + MessageID MessageID `json:"messageId"` + Seq Seq `json:"seq"` + DecisionID *DecisionID `json:"decisionId,omitempty"` +} + +// UnmarshalJSON rejects input that lacks a required key. +func (x *PostOutput) UnmarshalJSON(b []byte) error { + var keys map[string]json.RawMessage + if err := json.Unmarshal(b, &keys); err != nil { + return err + } + for _, k := range []string{"messageId", "seq"} { + if _, ok := keys[k]; !ok { + return fmt.Errorf("PostOutput: missing required field %q", k) + } + } + type plain PostOutput + return json.Unmarshal(b, (*plain)(x)) +} + +// Registry returns a fresh value for each object definition, by schema name. Tests and decoders use it. +var Registry = map[string]func() any{ + "Actor": func() any { return new(Actor) }, + "ErrorBody": func() any { return new(ErrorBody) }, + "Claim": func() any { return new(Claim) }, + "Task": func() any { return new(Task) }, + "NeedsTarget": func() any { return new(NeedsTarget) }, + "Needs": func() any { return new(Needs) }, + "Option": func() any { return new(Option) }, + "Link": func() any { return new(Link) }, + "Receipt": func() any { return new(Receipt) }, + "Ruling": func() any { return new(Ruling) }, + "Decision": func() any { return new(Decision) }, + "PostInput": func() any { return new(PostInput) }, + "Message": func() any { return new(Message) }, + "Envelope": func() any { return new(Envelope) }, + "Member": func() any { return new(Member) }, + "AgentPresence": func() any { return new(AgentPresence) }, + "Hello": func() any { return new(Hello) }, + "MessageUpdated": func() any { return new(MessageUpdated) }, + "TaskClaimed": func() any { return new(TaskClaimed) }, + "PresenceChanged": func() any { return new(PresenceChanged) }, + "WakeRefs": func() any { return new(WakeRefs) }, + "RunWake": func() any { return new(RunWake) }, + "Subscribe": func() any { return new(Subscribe) }, + "MessagePost": func() any { return new(MessagePost) }, + "TaskCreate": func() any { return new(TaskCreate) }, + "DecisionRule": func() any { return new(DecisionRule) }, + "PresenceHeartbeat": func() any { return new(PresenceHeartbeat) }, + "WakeAck": func() any { return new(WakeAck) }, + "ComputerStatus": func() any { return new(ComputerStatus) }, + "RunStatus": func() any { return new(RunStatus) }, + "DaemonStatus": func() any { return new(DaemonStatus) }, + "DeviceStartResponse": func() any { return new(DeviceStartResponse) }, + "DeviceTokenRequest": func() any { return new(DeviceTokenRequest) }, + "DeviceTokenResponse": func() any { return new(DeviceTokenResponse) }, + "CreatePodRequest": func() any { return new(CreatePodRequest) }, + "Pod": func() any { return new(Pod) }, + "CreateInviteRequest": func() any { return new(CreateInviteRequest) }, + "Invite": func() any { return new(Invite) }, + "AcceptInviteResponse": func() any { return new(AcceptInviteResponse) }, + "RegisterComputerRequest": func() any { return new(RegisterComputerRequest) }, + "RegisterComputerResponse": func() any { return new(RegisterComputerResponse) }, + "RegisterAgentRequest": func() any { return new(RegisterAgentRequest) }, + "RegisterAgentResponse": func() any { return new(RegisterAgentResponse) }, + "ClaimRunRequest": func() any { return new(ClaimRunRequest) }, + "ClaimRunResponse": func() any { return new(ClaimRunResponse) }, + "EndRunRequest": func() any { return new(EndRunRequest) }, + "WhoamiInput": func() any { return new(WhoamiInput) }, + "WhoamiOutput": func() any { return new(WhoamiOutput) }, + "ReadInput": func() any { return new(ReadInput) }, + "ReadOutput": func() any { return new(ReadOutput) }, + "TaskListInput": func() any { return new(TaskListInput) }, + "TaskListOutput": func() any { return new(TaskListOutput) }, + "TaskClaimInput": func() any { return new(TaskClaimInput) }, + "TaskClaimOutput": func() any { return new(TaskClaimOutput) }, + "TaskUpdateInput": func() any { return new(TaskUpdateInput) }, + "PostOutput": func() any { return new(PostOutput) }, +} + +// EventDirection is who sends each event type: s2c, c2s or local (daemon to TUI only). +var EventDirection = map[EventType]string{ + EventTypeHello: "s2c", + EventTypeMessageCreated: "s2c", + EventTypeMessageUpdated: "s2c", + EventTypeTaskCreated: "s2c", + EventTypeTaskUpdated: "s2c", + EventTypeTaskClaimed: "s2c", + EventTypePresenceChanged: "s2c", + EventTypeRunWake: "s2c", + EventTypeError: "s2c", + EventTypeSubscribe: "c2s", + EventTypeMessagePost: "c2s", + EventTypeTaskCreate: "c2s", + EventTypeDecisionRule: "c2s", + EventTypePresenceHeartbeat: "c2s", + EventTypeWakeAck: "c2s", + EventTypeDaemonStatus: "local", +} + +// DecodeEventData decodes an envelope's data into the type x-variants names for its event type. +func DecodeEventData(t EventType, raw json.RawMessage) (any, error) { + switch t { + case EventTypeHello: + var v Hello + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("%s: %w", t, err) + } + return &v, nil + case EventTypeMessageCreated: + var v Message + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("%s: %w", t, err) + } + return &v, nil + case EventTypeMessageUpdated: + var v MessageUpdated + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("%s: %w", t, err) + } + return &v, nil + case EventTypeTaskCreated: + var v Task + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("%s: %w", t, err) + } + return &v, nil + case EventTypeTaskUpdated: + var v Task + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("%s: %w", t, err) + } + return &v, nil + case EventTypeTaskClaimed: + var v TaskClaimed + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("%s: %w", t, err) + } + return &v, nil + case EventTypePresenceChanged: + var v PresenceChanged + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("%s: %w", t, err) + } + return &v, nil + case EventTypeRunWake: + var v RunWake + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("%s: %w", t, err) + } + return &v, nil + case EventTypeError: + var v ErrorBody + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("%s: %w", t, err) + } + return &v, nil + case EventTypeSubscribe: + var v Subscribe + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("%s: %w", t, err) + } + return &v, nil + case EventTypeMessagePost: + var v MessagePost + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("%s: %w", t, err) + } + return &v, nil + case EventTypeTaskCreate: + var v TaskCreate + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("%s: %w", t, err) + } + return &v, nil + case EventTypeDecisionRule: + var v DecisionRule + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("%s: %w", t, err) + } + return &v, nil + case EventTypePresenceHeartbeat: + var v PresenceHeartbeat + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("%s: %w", t, err) + } + return &v, nil + case EventTypeWakeAck: + var v WakeAck + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("%s: %w", t, err) + } + return &v, nil + case EventTypeDaemonStatus: + var v DaemonStatus + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("%s: %w", t, err) + } + return &v, nil + } + return nil, fmt.Errorf("unknown event type %q", t) +} + +// ToolNames lists the MCP tools in schema order. +var ToolNames = []string{"commonly_whoami", "commonly_read", "commonly_task_list", "commonly_task_claim", "commonly_task_update", "commonly_post"} diff --git a/gen/ts/protocol.ts b/gen/ts/protocol.ts new file mode 100644 index 0000000000..38809f7563 --- /dev/null +++ b/gen/ts/protocol.ts @@ -0,0 +1,547 @@ +// Code generated by scripts/gen.mjs from schema/. DO NOT EDIT. +/* eslint-disable */ + +export type PodId = string; + +export type UserId = string; + +export type AgentId = string; + +/** A user or an agent: who a task is assigned to, or who acted. System actors use sys_. */ +export type ActorId = string; + +export type ComputerId = string; + +export type TaskId = string; + +export type MessageId = string; + +export type RunId = string; + +export type DecisionId = string; + +export type WakeId = string; + +/** Monotonic per pod, assigned by the pod. Starts at 1. */ +export type Seq = number; + +/** RFC 3339 in UTC. Set by the server only. */ +export type Timestamp = string; + +export type ActorKind = "human" | "agent" | "system"; +export const ActorKindValues = ["human","agent","system"] as const; + +export interface Actor { + kind: ActorKind; + id: ActorId; + displayName: string; +} + +/** Which agent program a registered agent runs. fake is the deterministic test agent. */ +export type Harness = "claude-code" | "codex" | "fake"; +export const HarnessValues = ["claude-code","codex","fake"] as const; + +/** Stable error codes, shared by HTTP responses, MCP tool errors and the socket error event. */ +export type ErrorCode = "invalid_input" | "summary_required" | "unauthorized" | "forbidden" | "not_found" | "gone" | "already_claimed" | "already_ruled" | "authorization_pending" | "slow_down" | "expired_token" | "invalid_grant" | "rate_limited" | "internal"; +export const ErrorCodeValues = ["invalid_input","summary_required","unauthorized","forbidden","not_found","gone","already_claimed","already_ruled","authorization_pending","slow_down","expired_token","invalid_grant","rate_limited","internal"] as const; + +export interface ErrorBody { + code: ErrorCode; + message: string; + /** The input field at fault, when there is one. */ + field?: string; + /** The id the error is about, when there is one. */ + ref?: string; +} + +export type TokenKind = "user" | "computer" | "run"; +export const TokenKindValues = ["user","computer","run"] as const; + +/** Bound to one user. 30 days, refreshable. */ +export type UserToken = string; + +/** Bound to one user and one computer. Valid until revoked. */ +export type ComputerToken = string; + +/** Bound to one agent, pod and run (and task, if any). Ends with the run; at most 2 hours. */ +export type RunToken = string; + +export type TaskStatus = "open" | "in_progress" | "blocked" | "done" | "failed"; +export const TaskStatusValues = ["open","in_progress","blocked","done","failed"] as const; + +/** One active claim per task. The lease is renewed by any MCP call on the claiming run. */ +export interface Claim { + by: Actor; + runId: RunId; + leaseUntil: Timestamp; +} + +export interface Task { + id: TaskId; + podId: PodId; + title: string; + body?: string; + status: TaskStatus; + assignee?: ActorId; + createdBy: Actor; + claim?: Claim; + createdAt: Timestamp; + updatedAt: Timestamp; +} + +export type MessageKind = "result" | "ask" | "progress" | "fyi" | "error"; +export const MessageKindValues = ["result","ask","progress","fyi","error"] as const; + +export type MessageStatus = "done" | "in_progress" | "blocked" | "failed"; +export const MessageStatusValues = ["done","in_progress","blocked","failed"] as const; + +export type NeedsAction = "approve" | "decide" | "review" | "answer" | "unblock"; +export const NeedsActionValues = ["approve","decide","review","answer","unblock"] as const; + +export type MemberRole = "member" | "owner"; +export const MemberRoleValues = ["member","owner"] as const; + +/** Exactly one of userIds or role. */ +export interface NeedsTarget { + userIds?: UserId[]; + role?: MemberRole; +} + +/** The only field that notifies anyone. progress and fyi posts never notify. */ +export interface Needs { + to: NeedsTarget; + action: NeedsAction; +} + +export type OptionId = "a" | "b" | "c" | "d" | "e"; +export const OptionIdValues = ["a","b","c","d","e"] as const; + +export interface Option { + id: OptionId; + label: string; + description?: string; + recommended?: boolean; +} + +export type LinkType = "pr" | "commit" | "task" | "doc" | "file" | "log"; +export const LinkTypeValues = ["pr","commit","task","doc","file","log"] as const; + +export interface Link { + label: string; + /** https only, and no credentials in the URL. */ + url: string; + type: LinkType; +} + +export type WakeReason = "task_assigned" | "mention" | "decision_ruled"; +export const WakeReasonValues = ["task_assigned","mention","decision_ruled"] as const; + +/** Stamped by the pod from the run record. An agent can't write any of it. */ +export interface Receipt { + runId: RunId; + agentId: AgentId; + computerId: ComputerId; + harness: Harness; + /** Reported by the daemon at session start. */ + harnessVersion?: string; + startedAt: Timestamp; + postedAt: Timestamp; + /** MCP calls the pod saw on this run's token. */ + toolCalls: number; + taskId?: TaskId; + wakeReason: WakeReason; +} + +export type DecisionState = "open" | "ruled" | "expired"; +export const DecisionStateValues = ["open","ruled","expired"] as const; + +export interface Ruling { + optionId: OptionId; + by: Actor; + at: Timestamp; +} + +export interface Decision { + id: DecisionId; + state: DecisionState; + ruling?: Ruling; +} + +export interface PostInput { + /** The headline, outcome first. One line, at most 120 characters. */ + summary: string; + kind: MessageKind; + status: MessageStatus; + needs?: Needs; + options?: Option[]; + taskId?: TaskId; + links?: Link[]; + /** What isn't verified. */ + caveat?: string; + /** Markdown. Folded by default in clients. */ + body?: string; +} + +export interface Message { + id: MessageId; + seq: Seq; + podId: PodId; + author: Actor; + createdAt: Timestamp; + runId?: RunId; + threadOf?: MessageId; + summary: string; + kind: MessageKind; + status: MessageStatus; + needs?: Needs; + options?: Option[]; + taskId?: TaskId; + links?: Link[]; + caveat?: string; + body?: string; + decision?: Decision; + receipt?: Receipt; +} + +export type EventType = "hello" | "message.created" | "message.updated" | "task.created" | "task.updated" | "task.claimed" | "presence.changed" | "run.wake" | "error" | "subscribe" | "message.post" | "task.create" | "decision.rule" | "presence.heartbeat" | "wake.ack" | "daemon.status"; +export const EventTypeValues = ["hello","message.created","message.updated","task.created","task.updated","task.claimed","presence.changed","run.wake","error","subscribe","message.post","task.create","decision.rule","presence.heartbeat","wake.ack","daemon.status"] as const; + +export interface Envelope { + v: 1; + type: EventType; + seq?: Seq; + podId: PodId; + ts?: Timestamp; + actor?: Actor; + /** Matches the definition x-variants names for type. */ + data: Record; +} + +export interface Member { + actor: Actor; + role: MemberRole; +} + +export type PresenceState = "online" | "offline" | "working"; +export const PresenceStateValues = ["online","offline","working"] as const; + +export interface AgentPresence { + agent: Actor; + harness: Harness; + computerId: ComputerId; + state: PresenceState; +} + +export interface Hello { + podId: PodId; + /** 0 for an empty pod. */ + headSeq: number; + you: Actor; + members: Member[]; + agents: AgentPresence[]; +} + +/** Only a decision changes after a message is created. The summary never changes. */ +export interface MessageUpdated { + id: MessageId; + decision: Decision; +} + +export interface TaskClaimed { + taskId: TaskId; + by: Actor; + runId: RunId; + leaseUntil: Timestamp; +} + +export interface PresenceChanged { + actor: Actor; + state: PresenceState; + computerId?: ComputerId; +} + +export interface WakeRefs { + taskId?: TaskId; + messageId?: MessageId; + decisionId?: DecisionId; +} + +export interface RunWake { + wakeId: WakeId; + agentId: AgentId; + reason: WakeReason; + refs: WakeRefs; +} + +export interface Subscribe { + /** 0 replays from the start of the kept log. */ + sinceSeq: number; +} + +/** A human post. */ +export interface MessagePost { + text: string; + threadOf?: MessageId; +} + +export interface TaskCreate { + title: string; + assignee?: ActorId; + body?: string; +} + +export interface DecisionRule { + decisionId: DecisionId; + optionId: OptionId; +} + +/** Every 15 s from the daemon. */ +export interface PresenceHeartbeat { + computerId?: ComputerId; +} + +export interface WakeAck { + wakeId: WakeId; +} + +export interface ComputerStatus { + id: ComputerId; + name: string; + online: boolean; +} + +export interface RunStatus { + runId: RunId; + agentId: AgentId; + startedAt: Timestamp; + taskId?: TaskId; +} + +export interface DaemonStatus { + computers: ComputerStatus[]; + agents: AgentPresence[]; + runs: RunStatus[]; +} + +export interface DeviceStartResponse { + deviceCode: string; + /** What the person types on the sign-in page, e.g. WDJB-MJHT. */ + userCode: string; + verificationUri: string; + /** Seconds between polls. */ + interval: number; + /** Seconds until the code expires. */ + expiresIn: number; +} + +export interface DeviceTokenRequest { + deviceCode: string; +} + +export interface DeviceTokenResponse { + token: UserToken; + user: Actor; + expiresAt: Timestamp; +} + +export interface CreatePodRequest { + name: string; +} + +export interface Pod { + id: PodId; + name: string; + createdAt: Timestamp; +} + +export interface CreateInviteRequest { + /** Defaults to 1. */ + maxUses?: number; + /** Defaults to 7 days. */ + ttlSeconds?: number; +} + +export interface Invite { + code: string; + url: string; + expiresAt: Timestamp; + usesLeft: number; +} + +export interface AcceptInviteResponse { + pod: Pod; +} + +export type OS = "darwin" | "linux"; +export const OSValues = ["darwin","linux"] as const; + +export type Arch = "arm64" | "amd64"; +export const ArchValues = ["arm64","amd64"] as const; + +export interface RegisterComputerRequest { + name: string; + os: OS; + arch: Arch; +} + +export interface RegisterComputerResponse { + computerId: ComputerId; + token: ComputerToken; +} + +export interface RegisterAgentRequest { + harness: Harness; + displayName: string; +} + +export interface RegisterAgentResponse { + agentId: AgentId; +} + +export interface ClaimRunRequest { + wakeId: WakeId; +} + +/** The brief is built by the server and sent as the agent's first prompt. The run token goes to the agent; the computer token never does. */ +export interface ClaimRunResponse { + runId: RunId; + runToken: RunToken; + brief: string; + mcpUrl: string; + expiresAt: Timestamp; +} + +export type RunOutcome = "completed" | "failed" | "cancelled"; +export const RunOutcomeValues = ["completed","failed","cancelled"] as const; + +export interface EndRunRequest { + outcome: RunOutcome; + harnessVersion?: string; +} + +export interface WhoamiInput { +} + +export interface WhoamiOutput { + agent: Actor; + podId: PodId; + runId: RunId; + task?: Task; +} + +export interface ReadInput { + sinceSeq?: number; + /** Defaults to 20. */ + limit?: number; + withBody?: boolean; +} + +export interface ReadOutput { + messages: Message[]; + tasks: Task[]; + headSeq: number; +} + +export interface TaskListInput { + status?: TaskStatus; +} + +export interface TaskListOutput { + tasks: Task[]; +} + +export interface TaskClaimInput { + taskId: TaskId; +} + +export interface TaskClaimOutput { + taskId: TaskId; + leaseUntil: Timestamp; +} + +export type TaskUpdateStatus = "in_progress" | "blocked" | "done" | "failed"; +export const TaskUpdateStatusValues = ["in_progress","blocked","done","failed"] as const; + +export interface TaskUpdateInput { + taskId: TaskId; + status: TaskUpdateStatus; + note?: string; +} + +export interface PostOutput { + messageId: MessageId; + seq: Seq; + decisionId?: DecisionId; +} + +/** Which data definition each event type carries (events.json x-variants). */ +export interface EventDataMap { + "hello": Hello; + "message.created": Message; + "message.updated": MessageUpdated; + "task.created": Task; + "task.updated": Task; + "task.claimed": TaskClaimed; + "presence.changed": PresenceChanged; + "run.wake": RunWake; + "error": ErrorBody; + "subscribe": Subscribe; + "message.post": MessagePost; + "task.create": TaskCreate; + "decision.rule": DecisionRule; + "presence.heartbeat": PresenceHeartbeat; + "wake.ack": WakeAck; + "daemon.status": DaemonStatus; +} + +export type EventOf = Omit & { type: T; data: EventDataMap[T] }; +export type AnyEvent = { [K in EventType]: EventOf }[EventType]; + +export const EVENT_DIRECTION = { + "hello": "s2c", + "message.created": "s2c", + "message.updated": "s2c", + "task.created": "s2c", + "task.updated": "s2c", + "task.claimed": "s2c", + "presence.changed": "s2c", + "run.wake": "s2c", + "error": "s2c", + "subscribe": "c2s", + "message.post": "c2s", + "task.create": "c2s", + "decision.rule": "c2s", + "presence.heartbeat": "c2s", + "wake.ack": "c2s", + "daemon.status": "local", +} as const; + +export const EVENT_DATA_DEF = { + "hello": "Hello", + "message.created": "Message", + "message.updated": "MessageUpdated", + "task.created": "Task", + "task.updated": "Task", + "task.claimed": "TaskClaimed", + "presence.changed": "PresenceChanged", + "run.wake": "RunWake", + "error": "ErrorBody", + "subscribe": "Subscribe", + "message.post": "MessagePost", + "task.create": "TaskCreate", + "decision.rule": "DecisionRule", + "presence.heartbeat": "PresenceHeartbeat", + "wake.ack": "WakeAck", + "daemon.status": "DaemonStatus", +} as const; + +export interface ToolIO { + commonly_whoami: { input: WhoamiInput; output: WhoamiOutput }; + commonly_read: { input: ReadInput; output: ReadOutput }; + commonly_task_list: { input: TaskListInput; output: TaskListOutput }; + commonly_task_claim: { input: TaskClaimInput; output: TaskClaimOutput }; + commonly_task_update: { input: TaskUpdateInput; output: Task }; + commonly_post: { input: PostInput; output: PostOutput }; +} +export type ToolName = keyof ToolIO; +export const TOOL_NAMES = ["commonly_whoami","commonly_read","commonly_task_list","commonly_task_claim","commonly_task_update","commonly_post"] as const; diff --git a/gen/ts/schemas.json b/gen/ts/schemas.json new file mode 100644 index 0000000000..0ee916f55f --- /dev/null +++ b/gen/ts/schemas.json @@ -0,0 +1,1527 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://commonly.me/v4/schema/bundle.json", + "$comment": "Code generated by scripts/gen.mjs from schema/. DO NOT EDIT.", + "$defs": { + "PodId": { + "type": "string", + "pattern": "^pod_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "UserId": { + "type": "string", + "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "AgentId": { + "type": "string", + "pattern": "^agt_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "ActorId": { + "description": "A user or an agent: who a task is assigned to, or who acted. System actors use sys_.", + "type": "string", + "pattern": "^(usr|agt|sys)_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "ComputerId": { + "type": "string", + "pattern": "^cmp_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "TaskId": { + "type": "string", + "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "MessageId": { + "type": "string", + "pattern": "^msg_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "RunId": { + "type": "string", + "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "DecisionId": { + "type": "string", + "pattern": "^dec_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "WakeId": { + "type": "string", + "pattern": "^wak_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "Seq": { + "description": "Monotonic per pod, assigned by the pod. Starts at 1.", + "type": "integer", + "minimum": 1 + }, + "Timestamp": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + }, + "ActorKind": { + "type": "string", + "enum": [ + "human", + "agent", + "system" + ] + }, + "Actor": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "displayName" + ], + "properties": { + "kind": { + "$ref": "#/$defs/ActorKind" + }, + "id": { + "$ref": "#/$defs/ActorId" + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "Harness": { + "description": "Which agent program a registered agent runs. fake is the deterministic test agent.", + "type": "string", + "enum": [ + "claude-code", + "codex", + "fake" + ] + }, + "ErrorCode": { + "description": "Stable error codes, shared by HTTP responses, MCP tool errors and the socket error event.", + "type": "string", + "enum": [ + "invalid_input", + "summary_required", + "unauthorized", + "forbidden", + "not_found", + "gone", + "already_claimed", + "already_ruled", + "authorization_pending", + "slow_down", + "expired_token", + "invalid_grant", + "rate_limited", + "internal" + ] + }, + "ErrorBody": { + "type": "object", + "additionalProperties": false, + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "$ref": "#/$defs/ErrorCode" + }, + "message": { + "type": "string", + "maxLength": 500 + }, + "field": { + "description": "The input field at fault, when there is one.", + "type": "string", + "maxLength": 80 + }, + "ref": { + "description": "The id the error is about, when there is one.", + "type": "string", + "maxLength": 40 + } + } + }, + "TokenKind": { + "type": "string", + "enum": [ + "user", + "computer", + "run" + ] + }, + "UserToken": { + "description": "Bound to one user. 30 days, refreshable.", + "type": "string", + "pattern": "^cmu_[0-9A-Za-z]{43}$" + }, + "ComputerToken": { + "description": "Bound to one user and one computer. Valid until revoked.", + "type": "string", + "pattern": "^cmc_[0-9A-Za-z]{43}$" + }, + "RunToken": { + "description": "Bound to one agent, pod and run (and task, if any). Ends with the run; at most 2 hours.", + "type": "string", + "pattern": "^cmr_[0-9A-Za-z]{43}$" + }, + "TaskStatus": { + "type": "string", + "enum": [ + "open", + "in_progress", + "blocked", + "done", + "failed" + ] + }, + "Claim": { + "description": "One active claim per task. The lease is renewed by any MCP call on the claiming run.", + "type": "object", + "additionalProperties": false, + "required": [ + "by", + "runId", + "leaseUntil" + ], + "properties": { + "by": { + "$ref": "#/$defs/Actor" + }, + "runId": { + "$ref": "#/$defs/RunId" + }, + "leaseUntil": { + "$ref": "#/$defs/Timestamp" + } + } + }, + "Task": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "podId", + "title", + "status", + "createdBy", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { + "$ref": "#/$defs/TaskId" + }, + "podId": { + "$ref": "#/$defs/PodId" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "body": { + "type": "string", + "maxLength": 16384 + }, + "status": { + "$ref": "#/$defs/TaskStatus" + }, + "assignee": { + "$ref": "#/$defs/ActorId" + }, + "createdBy": { + "$ref": "#/$defs/Actor" + }, + "claim": { + "$ref": "#/$defs/Claim" + }, + "createdAt": { + "$ref": "#/$defs/Timestamp" + }, + "updatedAt": { + "$ref": "#/$defs/Timestamp" + } + } + }, + "MessageKind": { + "type": "string", + "enum": [ + "result", + "ask", + "progress", + "fyi", + "error" + ] + }, + "MessageStatus": { + "type": "string", + "enum": [ + "done", + "in_progress", + "blocked", + "failed" + ] + }, + "NeedsAction": { + "type": "string", + "enum": [ + "approve", + "decide", + "review", + "answer", + "unblock" + ] + }, + "MemberRole": { + "type": "string", + "enum": [ + "member", + "owner" + ] + }, + "NeedsTarget": { + "description": "Exactly one of userIds or role.", + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "maxProperties": 1, + "properties": { + "userIds": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "items": { + "$ref": "#/$defs/UserId" + } + }, + "role": { + "$ref": "#/$defs/MemberRole" + } + } + }, + "Needs": { + "description": "The only field that notifies anyone. progress and fyi posts never notify.", + "type": "object", + "additionalProperties": false, + "required": [ + "to", + "action" + ], + "properties": { + "to": { + "$ref": "#/$defs/NeedsTarget" + }, + "action": { + "$ref": "#/$defs/NeedsAction" + } + } + }, + "OptionId": { + "type": "string", + "enum": [ + "a", + "b", + "c", + "d", + "e" + ] + }, + "Option": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "label" + ], + "properties": { + "id": { + "$ref": "#/$defs/OptionId" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "description": { + "type": "string", + "maxLength": 160 + }, + "recommended": { + "type": "boolean" + } + } + }, + "LinkType": { + "type": "string", + "enum": [ + "pr", + "commit", + "task", + "doc", + "file", + "log" + ] + }, + "Link": { + "type": "object", + "additionalProperties": false, + "required": [ + "label", + "url", + "type" + ], + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "url": { + "description": "https only, and no credentials in the URL.", + "type": "string", + "maxLength": 2048, + "pattern": "^https://[^/\\s@]+(/[^\\s]*)?$" + }, + "type": { + "$ref": "#/$defs/LinkType" + } + } + }, + "WakeReason": { + "type": "string", + "enum": [ + "task_assigned", + "mention", + "decision_ruled" + ] + }, + "Receipt": { + "description": "Stamped by the pod from the run record. An agent can't write any of it.", + "type": "object", + "additionalProperties": false, + "required": [ + "runId", + "agentId", + "computerId", + "harness", + "startedAt", + "postedAt", + "toolCalls", + "wakeReason" + ], + "properties": { + "runId": { + "$ref": "#/$defs/RunId" + }, + "agentId": { + "$ref": "#/$defs/AgentId" + }, + "computerId": { + "$ref": "#/$defs/ComputerId" + }, + "harness": { + "$ref": "#/$defs/Harness" + }, + "harnessVersion": { + "description": "Reported by the daemon at session start.", + "type": "string", + "maxLength": 64 + }, + "startedAt": { + "$ref": "#/$defs/Timestamp" + }, + "postedAt": { + "$ref": "#/$defs/Timestamp" + }, + "toolCalls": { + "description": "MCP calls the pod saw on this run's token.", + "type": "integer", + "minimum": 0 + }, + "taskId": { + "$ref": "#/$defs/TaskId" + }, + "wakeReason": { + "$ref": "#/$defs/WakeReason" + } + } + }, + "DecisionState": { + "type": "string", + "enum": [ + "open", + "ruled", + "expired" + ] + }, + "Ruling": { + "type": "object", + "additionalProperties": false, + "required": [ + "optionId", + "by", + "at" + ], + "properties": { + "optionId": { + "$ref": "#/$defs/OptionId" + }, + "by": { + "$ref": "#/$defs/Actor" + }, + "at": { + "$ref": "#/$defs/Timestamp" + } + } + }, + "Decision": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "state" + ], + "properties": { + "id": { + "$ref": "#/$defs/DecisionId" + }, + "state": { + "$ref": "#/$defs/DecisionState" + }, + "ruling": { + "$ref": "#/$defs/Ruling" + } + } + }, + "PostInput": { + "type": "object", + "additionalProperties": false, + "required": [ + "summary", + "kind", + "status" + ], + "properties": { + "summary": { + "description": "The headline, outcome first. One line, at most 120 characters.", + "type": "string", + "minLength": 1, + "maxLength": 120, + "pattern": "^[^\\r\\n]*$" + }, + "kind": { + "$ref": "#/$defs/MessageKind" + }, + "status": { + "$ref": "#/$defs/MessageStatus" + }, + "needs": { + "$ref": "#/$defs/Needs" + }, + "options": { + "type": "array", + "minItems": 2, + "maxItems": 5, + "items": { + "$ref": "#/$defs/Option" + } + }, + "taskId": { + "$ref": "#/$defs/TaskId" + }, + "links": { + "type": "array", + "maxItems": 8, + "items": { + "$ref": "#/$defs/Link" + } + }, + "caveat": { + "description": "What isn't verified.", + "type": "string", + "maxLength": 160 + }, + "body": { + "description": "Markdown. Folded by default in clients.", + "type": "string", + "maxLength": 16384 + } + } + }, + "Message": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "seq", + "podId", + "author", + "createdAt", + "summary", + "kind", + "status" + ], + "properties": { + "id": { + "$ref": "#/$defs/MessageId" + }, + "seq": { + "$ref": "#/$defs/Seq" + }, + "podId": { + "$ref": "#/$defs/PodId" + }, + "author": { + "$ref": "#/$defs/Actor" + }, + "createdAt": { + "$ref": "#/$defs/Timestamp" + }, + "runId": { + "$ref": "#/$defs/RunId" + }, + "threadOf": { + "$ref": "#/$defs/MessageId" + }, + "summary": { + "type": "string", + "minLength": 1, + "maxLength": 120, + "pattern": "^[^\\r\\n]*$" + }, + "kind": { + "$ref": "#/$defs/MessageKind" + }, + "status": { + "$ref": "#/$defs/MessageStatus" + }, + "needs": { + "$ref": "#/$defs/Needs" + }, + "options": { + "type": "array", + "minItems": 2, + "maxItems": 5, + "items": { + "$ref": "#/$defs/Option" + } + }, + "taskId": { + "$ref": "#/$defs/TaskId" + }, + "links": { + "type": "array", + "maxItems": 8, + "items": { + "$ref": "#/$defs/Link" + } + }, + "caveat": { + "type": "string", + "maxLength": 160 + }, + "body": { + "type": "string", + "maxLength": 16384 + }, + "decision": { + "$ref": "#/$defs/Decision" + }, + "receipt": { + "$ref": "#/$defs/Receipt" + } + } + }, + "EventType": { + "type": "string", + "enum": [ + "hello", + "message.created", + "message.updated", + "task.created", + "task.updated", + "task.claimed", + "presence.changed", + "run.wake", + "error", + "subscribe", + "message.post", + "task.create", + "decision.rule", + "presence.heartbeat", + "wake.ack", + "daemon.status" + ] + }, + "Envelope": { + "type": "object", + "additionalProperties": false, + "required": [ + "v", + "type", + "podId", + "data" + ], + "properties": { + "v": { + "type": "integer", + "const": 1 + }, + "type": { + "$ref": "#/$defs/EventType" + }, + "seq": { + "$ref": "#/$defs/Seq" + }, + "podId": { + "$ref": "#/$defs/PodId" + }, + "ts": { + "$ref": "#/$defs/Timestamp" + }, + "actor": { + "$ref": "#/$defs/Actor" + }, + "data": { + "description": "Matches the definition x-variants names for type.", + "type": "object" + } + } + }, + "Member": { + "type": "object", + "additionalProperties": false, + "required": [ + "actor", + "role" + ], + "properties": { + "actor": { + "$ref": "#/$defs/Actor" + }, + "role": { + "$ref": "#/$defs/MemberRole" + } + } + }, + "PresenceState": { + "type": "string", + "enum": [ + "online", + "offline", + "working" + ] + }, + "AgentPresence": { + "type": "object", + "additionalProperties": false, + "required": [ + "agent", + "harness", + "computerId", + "state" + ], + "properties": { + "agent": { + "$ref": "#/$defs/Actor" + }, + "harness": { + "$ref": "#/$defs/Harness" + }, + "computerId": { + "$ref": "#/$defs/ComputerId" + }, + "state": { + "$ref": "#/$defs/PresenceState" + } + } + }, + "Hello": { + "type": "object", + "additionalProperties": false, + "required": [ + "podId", + "headSeq", + "you", + "members", + "agents" + ], + "properties": { + "podId": { + "$ref": "#/$defs/PodId" + }, + "headSeq": { + "description": "0 for an empty pod.", + "type": "integer", + "minimum": 0 + }, + "you": { + "$ref": "#/$defs/Actor" + }, + "members": { + "type": "array", + "items": { + "$ref": "#/$defs/Member" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/$defs/AgentPresence" + } + } + } + }, + "MessageUpdated": { + "description": "Only a decision changes after a message is created. The summary never changes.", + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "decision" + ], + "properties": { + "id": { + "$ref": "#/$defs/MessageId" + }, + "decision": { + "$ref": "#/$defs/Decision" + } + } + }, + "TaskClaimed": { + "type": "object", + "additionalProperties": false, + "required": [ + "taskId", + "by", + "runId", + "leaseUntil" + ], + "properties": { + "taskId": { + "$ref": "#/$defs/TaskId" + }, + "by": { + "$ref": "#/$defs/Actor" + }, + "runId": { + "$ref": "#/$defs/RunId" + }, + "leaseUntil": { + "$ref": "#/$defs/Timestamp" + } + } + }, + "PresenceChanged": { + "type": "object", + "additionalProperties": false, + "required": [ + "actor", + "state" + ], + "properties": { + "actor": { + "$ref": "#/$defs/Actor" + }, + "state": { + "$ref": "#/$defs/PresenceState" + }, + "computerId": { + "$ref": "#/$defs/ComputerId" + } + } + }, + "WakeRefs": { + "type": "object", + "additionalProperties": false, + "properties": { + "taskId": { + "$ref": "#/$defs/TaskId" + }, + "messageId": { + "$ref": "#/$defs/MessageId" + }, + "decisionId": { + "$ref": "#/$defs/DecisionId" + } + } + }, + "RunWake": { + "type": "object", + "additionalProperties": false, + "required": [ + "wakeId", + "agentId", + "reason", + "refs" + ], + "properties": { + "wakeId": { + "$ref": "#/$defs/WakeId" + }, + "agentId": { + "$ref": "#/$defs/AgentId" + }, + "reason": { + "$ref": "#/$defs/WakeReason" + }, + "refs": { + "$ref": "#/$defs/WakeRefs" + } + } + }, + "Subscribe": { + "type": "object", + "additionalProperties": false, + "required": [ + "sinceSeq" + ], + "properties": { + "sinceSeq": { + "description": "0 replays from the start of the kept log.", + "type": "integer", + "minimum": 0 + } + } + }, + "MessagePost": { + "description": "A human post.", + "type": "object", + "additionalProperties": false, + "required": [ + "text" + ], + "properties": { + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "threadOf": { + "$ref": "#/$defs/MessageId" + } + } + }, + "TaskCreate": { + "type": "object", + "additionalProperties": false, + "required": [ + "title" + ], + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "assignee": { + "$ref": "#/$defs/ActorId" + }, + "body": { + "type": "string", + "maxLength": 16384 + } + } + }, + "DecisionRule": { + "type": "object", + "additionalProperties": false, + "required": [ + "decisionId", + "optionId" + ], + "properties": { + "decisionId": { + "$ref": "#/$defs/DecisionId" + }, + "optionId": { + "$ref": "#/$defs/OptionId" + } + } + }, + "PresenceHeartbeat": { + "description": "Every 15 s from the daemon.", + "type": "object", + "additionalProperties": false, + "properties": { + "computerId": { + "$ref": "#/$defs/ComputerId" + } + } + }, + "WakeAck": { + "type": "object", + "additionalProperties": false, + "required": [ + "wakeId" + ], + "properties": { + "wakeId": { + "$ref": "#/$defs/WakeId" + } + } + }, + "ComputerStatus": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "name", + "online" + ], + "properties": { + "id": { + "$ref": "#/$defs/ComputerId" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "online": { + "type": "boolean" + } + } + }, + "RunStatus": { + "type": "object", + "additionalProperties": false, + "required": [ + "runId", + "agentId", + "startedAt" + ], + "properties": { + "runId": { + "$ref": "#/$defs/RunId" + }, + "agentId": { + "$ref": "#/$defs/AgentId" + }, + "startedAt": { + "$ref": "#/$defs/Timestamp" + }, + "taskId": { + "$ref": "#/$defs/TaskId" + } + } + }, + "DaemonStatus": { + "type": "object", + "additionalProperties": false, + "required": [ + "computers", + "agents", + "runs" + ], + "properties": { + "computers": { + "type": "array", + "items": { + "$ref": "#/$defs/ComputerStatus" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/$defs/AgentPresence" + } + }, + "runs": { + "type": "array", + "items": { + "$ref": "#/$defs/RunStatus" + } + } + } + }, + "DeviceStartResponse": { + "type": "object", + "additionalProperties": false, + "required": [ + "deviceCode", + "userCode", + "verificationUri", + "interval", + "expiresIn" + ], + "properties": { + "deviceCode": { + "type": "string", + "minLength": 32, + "maxLength": 128 + }, + "userCode": { + "description": "What the person types on the sign-in page, e.g. WDJB-MJHT.", + "type": "string", + "pattern": "^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$" + }, + "verificationUri": { + "type": "string", + "pattern": "^https?://" + }, + "interval": { + "description": "Seconds between polls.", + "type": "integer", + "minimum": 1 + }, + "expiresIn": { + "description": "Seconds until the code expires.", + "type": "integer", + "minimum": 1 + } + } + }, + "DeviceTokenRequest": { + "type": "object", + "additionalProperties": false, + "required": [ + "deviceCode" + ], + "properties": { + "deviceCode": { + "type": "string", + "minLength": 32, + "maxLength": 128 + } + } + }, + "DeviceTokenResponse": { + "type": "object", + "additionalProperties": false, + "required": [ + "token", + "user", + "expiresAt" + ], + "properties": { + "token": { + "$ref": "#/$defs/UserToken" + }, + "user": { + "$ref": "#/$defs/Actor" + }, + "expiresAt": { + "$ref": "#/$defs/Timestamp" + } + } + }, + "CreatePodRequest": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "Pod": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "name", + "createdAt" + ], + "properties": { + "id": { + "$ref": "#/$defs/PodId" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "createdAt": { + "$ref": "#/$defs/Timestamp" + } + } + }, + "CreateInviteRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "maxUses": { + "description": "Defaults to 1.", + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + "ttlSeconds": { + "description": "Defaults to 7 days.", + "type": "integer", + "minimum": 60, + "maximum": 2592000 + } + } + }, + "Invite": { + "type": "object", + "additionalProperties": false, + "required": [ + "code", + "url", + "expiresAt", + "usesLeft" + ], + "properties": { + "code": { + "type": "string", + "pattern": "^inv_[0-9A-Za-z]{22}$" + }, + "url": { + "type": "string", + "pattern": "^https?://" + }, + "expiresAt": { + "$ref": "#/$defs/Timestamp" + }, + "usesLeft": { + "type": "integer", + "minimum": 0 + } + } + }, + "AcceptInviteResponse": { + "type": "object", + "additionalProperties": false, + "required": [ + "pod" + ], + "properties": { + "pod": { + "$ref": "#/$defs/Pod" + } + } + }, + "OS": { + "type": "string", + "enum": [ + "darwin", + "linux" + ] + }, + "Arch": { + "type": "string", + "enum": [ + "arm64", + "amd64" + ] + }, + "RegisterComputerRequest": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "os", + "arch" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "os": { + "$ref": "#/$defs/OS" + }, + "arch": { + "$ref": "#/$defs/Arch" + } + } + }, + "RegisterComputerResponse": { + "type": "object", + "additionalProperties": false, + "required": [ + "computerId", + "token" + ], + "properties": { + "computerId": { + "$ref": "#/$defs/ComputerId" + }, + "token": { + "$ref": "#/$defs/ComputerToken" + } + } + }, + "RegisterAgentRequest": { + "type": "object", + "additionalProperties": false, + "required": [ + "harness", + "displayName" + ], + "properties": { + "harness": { + "$ref": "#/$defs/Harness" + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "RegisterAgentResponse": { + "type": "object", + "additionalProperties": false, + "required": [ + "agentId" + ], + "properties": { + "agentId": { + "$ref": "#/$defs/AgentId" + } + } + }, + "ClaimRunRequest": { + "type": "object", + "additionalProperties": false, + "required": [ + "wakeId" + ], + "properties": { + "wakeId": { + "$ref": "#/$defs/WakeId" + } + } + }, + "ClaimRunResponse": { + "description": "The brief is built by the server and sent as the agent's first prompt. The run token goes to the agent; the computer token never does.", + "type": "object", + "additionalProperties": false, + "required": [ + "runId", + "runToken", + "brief", + "mcpUrl", + "expiresAt" + ], + "properties": { + "runId": { + "$ref": "#/$defs/RunId" + }, + "runToken": { + "$ref": "#/$defs/RunToken" + }, + "brief": { + "type": "string", + "minLength": 1, + "maxLength": 32768 + }, + "mcpUrl": { + "type": "string", + "pattern": "^https?://" + }, + "expiresAt": { + "$ref": "#/$defs/Timestamp" + } + } + }, + "RunOutcome": { + "type": "string", + "enum": [ + "completed", + "failed", + "cancelled" + ] + }, + "EndRunRequest": { + "type": "object", + "additionalProperties": false, + "required": [ + "outcome" + ], + "properties": { + "outcome": { + "$ref": "#/$defs/RunOutcome" + }, + "harnessVersion": { + "type": "string", + "maxLength": 64 + } + } + }, + "WhoamiInput": { + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "WhoamiOutput": { + "type": "object", + "additionalProperties": false, + "required": [ + "agent", + "podId", + "runId" + ], + "properties": { + "agent": { + "$ref": "#/$defs/Actor" + }, + "podId": { + "$ref": "#/$defs/PodId" + }, + "runId": { + "$ref": "#/$defs/RunId" + }, + "task": { + "$ref": "#/$defs/Task" + } + } + }, + "ReadInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "sinceSeq": { + "type": "integer", + "minimum": 0 + }, + "limit": { + "description": "Defaults to 20.", + "type": "integer", + "minimum": 1, + "maximum": 50 + }, + "withBody": { + "type": "boolean" + } + } + }, + "ReadOutput": { + "type": "object", + "additionalProperties": false, + "required": [ + "messages", + "tasks", + "headSeq" + ], + "properties": { + "messages": { + "type": "array", + "items": { + "$ref": "#/$defs/Message" + } + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/$defs/Task" + } + }, + "headSeq": { + "type": "integer", + "minimum": 0 + } + } + }, + "TaskListInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "status": { + "$ref": "#/$defs/TaskStatus" + } + } + }, + "TaskListOutput": { + "type": "object", + "additionalProperties": false, + "required": [ + "tasks" + ], + "properties": { + "tasks": { + "type": "array", + "items": { + "$ref": "#/$defs/Task" + } + } + } + }, + "TaskClaimInput": { + "type": "object", + "additionalProperties": false, + "required": [ + "taskId" + ], + "properties": { + "taskId": { + "$ref": "#/$defs/TaskId" + } + } + }, + "TaskClaimOutput": { + "type": "object", + "additionalProperties": false, + "required": [ + "taskId", + "leaseUntil" + ], + "properties": { + "taskId": { + "$ref": "#/$defs/TaskId" + }, + "leaseUntil": { + "$ref": "#/$defs/Timestamp" + } + } + }, + "TaskUpdateStatus": { + "type": "string", + "enum": [ + "in_progress", + "blocked", + "done", + "failed" + ] + }, + "TaskUpdateInput": { + "type": "object", + "additionalProperties": false, + "required": [ + "taskId", + "status" + ], + "properties": { + "taskId": { + "$ref": "#/$defs/TaskId" + }, + "status": { + "$ref": "#/$defs/TaskUpdateStatus" + }, + "note": { + "type": "string", + "maxLength": 500 + } + } + }, + "PostOutput": { + "type": "object", + "additionalProperties": false, + "required": [ + "messageId", + "seq" + ], + "properties": { + "messageId": { + "$ref": "#/$defs/MessageId" + }, + "seq": { + "$ref": "#/$defs/Seq" + }, + "decisionId": { + "$ref": "#/$defs/DecisionId" + } + } + } + } +} diff --git a/gen/ts/tools.json b/gen/ts/tools.json new file mode 100644 index 0000000000..10836ac846 --- /dev/null +++ b/gen/ts/tools.json @@ -0,0 +1,1322 @@ +{ + "$comment": "Code generated by scripts/gen.mjs from schema/. DO NOT EDIT.", + "tools": [ + { + "name": "commonly_whoami", + "description": "Who you are in this pod: your agent identity, the pod, your current run and the task it's for, if any.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "outputSchema": { + "type": "object", + "additionalProperties": false, + "required": [ + "agent", + "podId", + "runId" + ], + "properties": { + "agent": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "displayName" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "human", + "agent", + "system" + ] + }, + "id": { + "description": "A user or an agent: who a task is assigned to, or who acted. System actors use sys_.", + "type": "string", + "pattern": "^(usr|agt|sys)_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "podId": { + "type": "string", + "pattern": "^pod_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "runId": { + "type": "string", + "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "task": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "podId", + "title", + "status", + "createdBy", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "podId": { + "type": "string", + "pattern": "^pod_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "body": { + "type": "string", + "maxLength": 16384 + }, + "status": { + "type": "string", + "enum": [ + "open", + "in_progress", + "blocked", + "done", + "failed" + ] + }, + "assignee": { + "description": "A user or an agent: who a task is assigned to, or who acted. System actors use sys_.", + "type": "string", + "pattern": "^(usr|agt|sys)_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "createdBy": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "displayName" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "human", + "agent", + "system" + ] + }, + "id": { + "description": "A user or an agent: who a task is assigned to, or who acted. System actors use sys_.", + "type": "string", + "pattern": "^(usr|agt|sys)_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "claim": { + "description": "One active claim per task. The lease is renewed by any MCP call on the claiming run.", + "type": "object", + "additionalProperties": false, + "required": [ + "by", + "runId", + "leaseUntil" + ], + "properties": { + "by": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "displayName" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "human", + "agent", + "system" + ] + }, + "id": { + "description": "A user or an agent: who a task is assigned to, or who acted. System actors use sys_.", + "type": "string", + "pattern": "^(usr|agt|sys)_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "runId": { + "type": "string", + "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "leaseUntil": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + } + } + }, + "createdAt": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + }, + "updatedAt": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + } + } + } + } + } + }, + { + "name": "commonly_read", + "description": "Read the pod: message headlines and tasks since a sequence number. Bodies are left out unless withBody is true.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "sinceSeq": { + "type": "integer", + "minimum": 0 + }, + "limit": { + "description": "Defaults to 20.", + "type": "integer", + "minimum": 1, + "maximum": 50 + }, + "withBody": { + "type": "boolean" + } + } + }, + "outputSchema": { + "type": "object", + "additionalProperties": false, + "required": [ + "messages", + "tasks", + "headSeq" + ], + "properties": { + "messages": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "seq", + "podId", + "author", + "createdAt", + "summary", + "kind", + "status" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^msg_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "seq": { + "description": "Monotonic per pod, assigned by the pod. Starts at 1.", + "type": "integer", + "minimum": 1 + }, + "podId": { + "type": "string", + "pattern": "^pod_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "author": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "displayName" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "human", + "agent", + "system" + ] + }, + "id": { + "description": "A user or an agent: who a task is assigned to, or who acted. System actors use sys_.", + "type": "string", + "pattern": "^(usr|agt|sys)_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "createdAt": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + }, + "runId": { + "type": "string", + "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "threadOf": { + "type": "string", + "pattern": "^msg_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "summary": { + "type": "string", + "minLength": 1, + "maxLength": 120, + "pattern": "^[^\\r\\n]*$" + }, + "kind": { + "type": "string", + "enum": [ + "result", + "ask", + "progress", + "fyi", + "error" + ] + }, + "status": { + "type": "string", + "enum": [ + "done", + "in_progress", + "blocked", + "failed" + ] + }, + "needs": { + "description": "The only field that notifies anyone. progress and fyi posts never notify.", + "type": "object", + "additionalProperties": false, + "required": [ + "to", + "action" + ], + "properties": { + "to": { + "description": "Exactly one of userIds or role.", + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "maxProperties": 1, + "properties": { + "userIds": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "items": { + "type": "string", + "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$" + } + }, + "role": { + "type": "string", + "enum": [ + "member", + "owner" + ] + } + } + }, + "action": { + "type": "string", + "enum": [ + "approve", + "decide", + "review", + "answer", + "unblock" + ] + } + } + }, + "options": { + "type": "array", + "minItems": 2, + "maxItems": 5, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "label" + ], + "properties": { + "id": { + "type": "string", + "enum": [ + "a", + "b", + "c", + "d", + "e" + ] + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "description": { + "type": "string", + "maxLength": 160 + }, + "recommended": { + "type": "boolean" + } + } + } + }, + "taskId": { + "type": "string", + "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "links": { + "type": "array", + "maxItems": 8, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "label", + "url", + "type" + ], + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "url": { + "description": "https only, and no credentials in the URL.", + "type": "string", + "maxLength": 2048, + "pattern": "^https://[^/\\s@]+(/[^\\s]*)?$" + }, + "type": { + "type": "string", + "enum": [ + "pr", + "commit", + "task", + "doc", + "file", + "log" + ] + } + } + } + }, + "caveat": { + "type": "string", + "maxLength": 160 + }, + "body": { + "type": "string", + "maxLength": 16384 + }, + "decision": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "state" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^dec_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "state": { + "type": "string", + "enum": [ + "open", + "ruled", + "expired" + ] + }, + "ruling": { + "type": "object", + "additionalProperties": false, + "required": [ + "optionId", + "by", + "at" + ], + "properties": { + "optionId": { + "type": "string", + "enum": [ + "a", + "b", + "c", + "d", + "e" + ] + }, + "by": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "displayName" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "human", + "agent", + "system" + ] + }, + "id": { + "description": "A user or an agent: who a task is assigned to, or who acted. System actors use sys_.", + "type": "string", + "pattern": "^(usr|agt|sys)_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "at": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + } + } + } + } + }, + "receipt": { + "description": "Stamped by the pod from the run record. An agent can't write any of it.", + "type": "object", + "additionalProperties": false, + "required": [ + "runId", + "agentId", + "computerId", + "harness", + "startedAt", + "postedAt", + "toolCalls", + "wakeReason" + ], + "properties": { + "runId": { + "type": "string", + "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "agentId": { + "type": "string", + "pattern": "^agt_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "computerId": { + "type": "string", + "pattern": "^cmp_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "harness": { + "description": "Which agent program a registered agent runs. fake is the deterministic test agent.", + "type": "string", + "enum": [ + "claude-code", + "codex", + "fake" + ] + }, + "harnessVersion": { + "description": "Reported by the daemon at session start.", + "type": "string", + "maxLength": 64 + }, + "startedAt": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + }, + "postedAt": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + }, + "toolCalls": { + "description": "MCP calls the pod saw on this run's token.", + "type": "integer", + "minimum": 0 + }, + "taskId": { + "type": "string", + "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "wakeReason": { + "type": "string", + "enum": [ + "task_assigned", + "mention", + "decision_ruled" + ] + } + } + } + } + } + }, + "tasks": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "podId", + "title", + "status", + "createdBy", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "podId": { + "type": "string", + "pattern": "^pod_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "body": { + "type": "string", + "maxLength": 16384 + }, + "status": { + "type": "string", + "enum": [ + "open", + "in_progress", + "blocked", + "done", + "failed" + ] + }, + "assignee": { + "description": "A user or an agent: who a task is assigned to, or who acted. System actors use sys_.", + "type": "string", + "pattern": "^(usr|agt|sys)_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "createdBy": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "displayName" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "human", + "agent", + "system" + ] + }, + "id": { + "description": "A user or an agent: who a task is assigned to, or who acted. System actors use sys_.", + "type": "string", + "pattern": "^(usr|agt|sys)_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "claim": { + "description": "One active claim per task. The lease is renewed by any MCP call on the claiming run.", + "type": "object", + "additionalProperties": false, + "required": [ + "by", + "runId", + "leaseUntil" + ], + "properties": { + "by": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "displayName" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "human", + "agent", + "system" + ] + }, + "id": { + "description": "A user or an agent: who a task is assigned to, or who acted. System actors use sys_.", + "type": "string", + "pattern": "^(usr|agt|sys)_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "runId": { + "type": "string", + "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "leaseUntil": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + } + } + }, + "createdAt": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + }, + "updatedAt": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + } + } + } + }, + "headSeq": { + "type": "integer", + "minimum": 0 + } + } + } + }, + { + "name": "commonly_task_list", + "description": "List the pod's tasks, optionally filtered by status.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "status": { + "type": "string", + "enum": [ + "open", + "in_progress", + "blocked", + "done", + "failed" + ] + } + } + }, + "outputSchema": { + "type": "object", + "additionalProperties": false, + "required": [ + "tasks" + ], + "properties": { + "tasks": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "podId", + "title", + "status", + "createdBy", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "podId": { + "type": "string", + "pattern": "^pod_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "body": { + "type": "string", + "maxLength": 16384 + }, + "status": { + "type": "string", + "enum": [ + "open", + "in_progress", + "blocked", + "done", + "failed" + ] + }, + "assignee": { + "description": "A user or an agent: who a task is assigned to, or who acted. System actors use sys_.", + "type": "string", + "pattern": "^(usr|agt|sys)_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "createdBy": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "displayName" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "human", + "agent", + "system" + ] + }, + "id": { + "description": "A user or an agent: who a task is assigned to, or who acted. System actors use sys_.", + "type": "string", + "pattern": "^(usr|agt|sys)_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "claim": { + "description": "One active claim per task. The lease is renewed by any MCP call on the claiming run.", + "type": "object", + "additionalProperties": false, + "required": [ + "by", + "runId", + "leaseUntil" + ], + "properties": { + "by": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "displayName" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "human", + "agent", + "system" + ] + }, + "id": { + "description": "A user or an agent: who a task is assigned to, or who acted. System actors use sys_.", + "type": "string", + "pattern": "^(usr|agt|sys)_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "runId": { + "type": "string", + "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "leaseUntil": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + } + } + }, + "createdAt": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + }, + "updatedAt": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + } + } + } + } + } + } + }, + { + "name": "commonly_task_claim", + "description": "Claim a task before working on it. One claim per task; if someone else holds it you get already_claimed with the holder. The claim's lease renews with every tool call you make on this run.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "required": [ + "taskId" + ], + "properties": { + "taskId": { + "type": "string", + "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$" + } + } + }, + "outputSchema": { + "type": "object", + "additionalProperties": false, + "required": [ + "taskId", + "leaseUntil" + ], + "properties": { + "taskId": { + "type": "string", + "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "leaseUntil": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + } + } + } + }, + { + "name": "commonly_task_update", + "description": "Set a task's status, with an optional short note. Use done only when the task's work is finished.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "required": [ + "taskId", + "status" + ], + "properties": { + "taskId": { + "type": "string", + "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "status": { + "type": "string", + "enum": [ + "in_progress", + "blocked", + "done", + "failed" + ] + }, + "note": { + "type": "string", + "maxLength": 500 + } + } + }, + "outputSchema": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "podId", + "title", + "status", + "createdBy", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "podId": { + "type": "string", + "pattern": "^pod_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "body": { + "type": "string", + "maxLength": 16384 + }, + "status": { + "type": "string", + "enum": [ + "open", + "in_progress", + "blocked", + "done", + "failed" + ] + }, + "assignee": { + "description": "A user or an agent: who a task is assigned to, or who acted. System actors use sys_.", + "type": "string", + "pattern": "^(usr|agt|sys)_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "createdBy": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "displayName" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "human", + "agent", + "system" + ] + }, + "id": { + "description": "A user or an agent: who a task is assigned to, or who acted. System actors use sys_.", + "type": "string", + "pattern": "^(usr|agt|sys)_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "claim": { + "description": "One active claim per task. The lease is renewed by any MCP call on the claiming run.", + "type": "object", + "additionalProperties": false, + "required": [ + "by", + "runId", + "leaseUntil" + ], + "properties": { + "by": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "displayName" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "human", + "agent", + "system" + ] + }, + "id": { + "description": "A user or an agent: who a task is assigned to, or who acted. System actors use sys_.", + "type": "string", + "pattern": "^(usr|agt|sys)_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "runId": { + "type": "string", + "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "leaseUntil": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + } + } + }, + "createdAt": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + }, + "updatedAt": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + } + } + } + }, + { + "name": "commonly_post", + "description": "Post to the pod. summary is the headline people read: one line, at most 120 characters, outcome first. Use kind ask with needs and 2 to 5 options when a person must decide; mark at most one option recommended. progress and fyi posts never notify anyone. The receipt is added by the pod; don't write one.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "required": [ + "summary", + "kind", + "status" + ], + "properties": { + "summary": { + "description": "The headline, outcome first. One line, at most 120 characters.", + "type": "string", + "minLength": 1, + "maxLength": 120, + "pattern": "^[^\\r\\n]*$" + }, + "kind": { + "type": "string", + "enum": [ + "result", + "ask", + "progress", + "fyi", + "error" + ] + }, + "status": { + "type": "string", + "enum": [ + "done", + "in_progress", + "blocked", + "failed" + ] + }, + "needs": { + "description": "The only field that notifies anyone. progress and fyi posts never notify.", + "type": "object", + "additionalProperties": false, + "required": [ + "to", + "action" + ], + "properties": { + "to": { + "description": "Exactly one of userIds or role.", + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "maxProperties": 1, + "properties": { + "userIds": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "items": { + "type": "string", + "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$" + } + }, + "role": { + "type": "string", + "enum": [ + "member", + "owner" + ] + } + } + }, + "action": { + "type": "string", + "enum": [ + "approve", + "decide", + "review", + "answer", + "unblock" + ] + } + } + }, + "options": { + "type": "array", + "minItems": 2, + "maxItems": 5, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "label" + ], + "properties": { + "id": { + "type": "string", + "enum": [ + "a", + "b", + "c", + "d", + "e" + ] + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "description": { + "type": "string", + "maxLength": 160 + }, + "recommended": { + "type": "boolean" + } + } + } + }, + "taskId": { + "type": "string", + "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "links": { + "type": "array", + "maxItems": 8, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "label", + "url", + "type" + ], + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "url": { + "description": "https only, and no credentials in the URL.", + "type": "string", + "maxLength": 2048, + "pattern": "^https://[^/\\s@]+(/[^\\s]*)?$" + }, + "type": { + "type": "string", + "enum": [ + "pr", + "commit", + "task", + "doc", + "file", + "log" + ] + } + } + } + }, + "caveat": { + "description": "What isn't verified.", + "type": "string", + "maxLength": 160 + }, + "body": { + "description": "Markdown. Folded by default in clients.", + "type": "string", + "maxLength": 16384 + } + } + }, + "outputSchema": { + "type": "object", + "additionalProperties": false, + "required": [ + "messageId", + "seq" + ], + "properties": { + "messageId": { + "type": "string", + "pattern": "^msg_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "seq": { + "description": "Monotonic per pod, assigned by the pod. Starts at 1.", + "type": "integer", + "minimum": 1 + }, + "decisionId": { + "type": "string", + "pattern": "^dec_[0-9A-HJKMNP-TV-Z]{26}$" + } + } + } + } + ] +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000..dfe5db1231 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/Team-Commonly/commonly + +go 1.27 diff --git a/package.json b/package.json new file mode 100644 index 0000000000..54d183c3c2 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "commonly-v4", + "private": true, + "type": "module", + "license": "Apache-2.0", + "scripts": { + "gen": "bun scripts/gen.mjs", + "check:fixtures": "bun scripts/check-fixtures.mjs", + "check:types": "tsc -p tsconfig.json" + }, + "devDependencies": { + "ajv": "8.17.1", + "typescript": "5.9.3" + } +} diff --git a/schema/common.json b/schema/common.json new file mode 100644 index 0000000000..6c0e876e3b --- /dev/null +++ b/schema/common.json @@ -0,0 +1,79 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://commonly.me/v4/schema/common.json", + "title": "Common types", + "description": "Ids, actors, timestamps and errors shared by every other schema file. Ids are a type prefix plus a ULID.", + "$defs": { + "PodId": { "type": "string", "pattern": "^pod_[0-9A-HJKMNP-TV-Z]{26}$" }, + "UserId": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$" }, + "AgentId": { "type": "string", "pattern": "^agt_[0-9A-HJKMNP-TV-Z]{26}$" }, + "ActorId": { + "description": "A user or an agent: who a task is assigned to, or who acted. System actors use sys_.", + "type": "string", + "pattern": "^(usr|agt|sys)_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "ComputerId": { "type": "string", "pattern": "^cmp_[0-9A-HJKMNP-TV-Z]{26}$" }, + "TaskId": { "type": "string", "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$" }, + "MessageId": { "type": "string", "pattern": "^msg_[0-9A-HJKMNP-TV-Z]{26}$" }, + "RunId": { "type": "string", "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$" }, + "DecisionId": { "type": "string", "pattern": "^dec_[0-9A-HJKMNP-TV-Z]{26}$" }, + "WakeId": { "type": "string", "pattern": "^wak_[0-9A-HJKMNP-TV-Z]{26}$" }, + "Seq": { + "description": "Monotonic per pod, assigned by the pod. Starts at 1.", + "type": "integer", + "minimum": 1 + }, + "Timestamp": { + "description": "RFC 3339 in UTC. Set by the server only.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$" + }, + "ActorKind": { "type": "string", "enum": ["human", "agent", "system"] }, + "Actor": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "id", "displayName"], + "properties": { + "kind": { "$ref": "#/$defs/ActorKind" }, + "id": { "$ref": "#/$defs/ActorId" }, + "displayName": { "type": "string", "minLength": 1, "maxLength": 80 } + } + }, + "Harness": { + "description": "Which agent program a registered agent runs. fake is the deterministic test agent.", + "type": "string", + "enum": ["claude-code", "codex", "fake"] + }, + "ErrorCode": { + "description": "Stable error codes, shared by HTTP responses, MCP tool errors and the socket error event.", + "type": "string", + "enum": [ + "invalid_input", + "summary_required", + "unauthorized", + "forbidden", + "not_found", + "gone", + "already_claimed", + "already_ruled", + "authorization_pending", + "slow_down", + "expired_token", + "invalid_grant", + "rate_limited", + "internal" + ] + }, + "ErrorBody": { + "type": "object", + "additionalProperties": false, + "required": ["code", "message"], + "properties": { + "code": { "$ref": "#/$defs/ErrorCode" }, + "message": { "type": "string", "maxLength": 500 }, + "field": { "description": "The input field at fault, when there is one.", "type": "string", "maxLength": 80 }, + "ref": { "description": "The id the error is about, when there is one.", "type": "string", "maxLength": 40 } + } + } + } +} diff --git a/schema/events.json b/schema/events.json new file mode 100644 index 0000000000..bbd0e89164 --- /dev/null +++ b/schema/events.json @@ -0,0 +1,216 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://commonly.me/v4/schema/events.json", + "title": "Socket events", + "description": "Every WebSocket frame, in both directions, is an Envelope. x-variants maps each type to the definition its data must match and the direction it travels: s2c is pod to client, c2s is client to pod, local is daemon to TUI over the unix socket only. The TUI talks to the daemon with this same envelope.", + "x-rules": [ + "seq and ts are set on s2c frames only. A c2s frame that carries either is refused.", + "subscribe with sinceSeq replays every event after it, in order, with no gaps.", + "run.wake is sent only to the computer that hosts the agent. Wakes for one agent coalesce: one pending wake means one turn. An un-acked wake is re-sent on the next heartbeat after 60 s.", + "A wake row is written only for the agent a change addresses: a task assigned to it, an @mention of it, or a ruling on its own ask. Ambient messages write none." + ], + "x-variants": { + "hello": { "dir": "s2c", "data": "Hello" }, + "message.created": { "dir": "s2c", "data": "message.json#/$defs/Message" }, + "message.updated": { "dir": "s2c", "data": "MessageUpdated" }, + "task.created": { "dir": "s2c", "data": "task.json#/$defs/Task" }, + "task.updated": { "dir": "s2c", "data": "task.json#/$defs/Task" }, + "task.claimed": { "dir": "s2c", "data": "TaskClaimed" }, + "presence.changed": { "dir": "s2c", "data": "PresenceChanged" }, + "run.wake": { "dir": "s2c", "data": "RunWake" }, + "error": { "dir": "s2c", "data": "common.json#/$defs/ErrorBody" }, + "subscribe": { "dir": "c2s", "data": "Subscribe" }, + "message.post": { "dir": "c2s", "data": "MessagePost" }, + "task.create": { "dir": "c2s", "data": "TaskCreate" }, + "decision.rule": { "dir": "c2s", "data": "DecisionRule" }, + "presence.heartbeat": { "dir": "c2s", "data": "PresenceHeartbeat" }, + "wake.ack": { "dir": "c2s", "data": "WakeAck" }, + "daemon.status": { "dir": "local", "data": "DaemonStatus" } + }, + "$defs": { + "EventType": { + "type": "string", + "enum": [ + "hello", "message.created", "message.updated", "task.created", "task.updated", "task.claimed", + "presence.changed", "run.wake", "error", "subscribe", "message.post", "task.create", + "decision.rule", "presence.heartbeat", "wake.ack", "daemon.status" + ] + }, + "Envelope": { + "type": "object", + "additionalProperties": false, + "required": ["v", "type", "podId", "data"], + "properties": { + "v": { "type": "integer", "const": 1 }, + "type": { "$ref": "#/$defs/EventType" }, + "seq": { "$ref": "common.json#/$defs/Seq" }, + "podId": { "$ref": "common.json#/$defs/PodId" }, + "ts": { "$ref": "common.json#/$defs/Timestamp" }, + "actor": { "$ref": "common.json#/$defs/Actor" }, + "data": { "description": "Matches the definition x-variants names for type.", "type": "object" } + } + }, + "Member": { + "type": "object", + "additionalProperties": false, + "required": ["actor", "role"], + "properties": { + "actor": { "$ref": "common.json#/$defs/Actor" }, + "role": { "$ref": "message.json#/$defs/MemberRole" } + } + }, + "PresenceState": { "type": "string", "enum": ["online", "offline", "working"] }, + "AgentPresence": { + "type": "object", + "additionalProperties": false, + "required": ["agent", "harness", "computerId", "state"], + "properties": { + "agent": { "$ref": "common.json#/$defs/Actor" }, + "harness": { "$ref": "common.json#/$defs/Harness" }, + "computerId": { "$ref": "common.json#/$defs/ComputerId" }, + "state": { "$ref": "#/$defs/PresenceState" } + } + }, + "Hello": { + "type": "object", + "additionalProperties": false, + "required": ["podId", "headSeq", "you", "members", "agents"], + "properties": { + "podId": { "$ref": "common.json#/$defs/PodId" }, + "headSeq": { "description": "0 for an empty pod.", "type": "integer", "minimum": 0 }, + "you": { "$ref": "common.json#/$defs/Actor" }, + "members": { "type": "array", "items": { "$ref": "#/$defs/Member" } }, + "agents": { "type": "array", "items": { "$ref": "#/$defs/AgentPresence" } } + } + }, + "MessageUpdated": { + "description": "Only a decision changes after a message is created. The summary never changes.", + "type": "object", + "additionalProperties": false, + "required": ["id", "decision"], + "properties": { + "id": { "$ref": "common.json#/$defs/MessageId" }, + "decision": { "$ref": "message.json#/$defs/Decision" } + } + }, + "TaskClaimed": { + "type": "object", + "additionalProperties": false, + "required": ["taskId", "by", "runId", "leaseUntil"], + "properties": { + "taskId": { "$ref": "common.json#/$defs/TaskId" }, + "by": { "$ref": "common.json#/$defs/Actor" }, + "runId": { "$ref": "common.json#/$defs/RunId" }, + "leaseUntil": { "$ref": "common.json#/$defs/Timestamp" } + } + }, + "PresenceChanged": { + "type": "object", + "additionalProperties": false, + "required": ["actor", "state"], + "properties": { + "actor": { "$ref": "common.json#/$defs/Actor" }, + "state": { "$ref": "#/$defs/PresenceState" }, + "computerId": { "$ref": "common.json#/$defs/ComputerId" } + } + }, + "WakeRefs": { + "type": "object", + "additionalProperties": false, + "properties": { + "taskId": { "$ref": "common.json#/$defs/TaskId" }, + "messageId": { "$ref": "common.json#/$defs/MessageId" }, + "decisionId": { "$ref": "common.json#/$defs/DecisionId" } + } + }, + "RunWake": { + "type": "object", + "additionalProperties": false, + "required": ["wakeId", "agentId", "reason", "refs"], + "properties": { + "wakeId": { "$ref": "common.json#/$defs/WakeId" }, + "agentId": { "$ref": "common.json#/$defs/AgentId" }, + "reason": { "$ref": "message.json#/$defs/WakeReason" }, + "refs": { "$ref": "#/$defs/WakeRefs" } + } + }, + "Subscribe": { + "type": "object", + "additionalProperties": false, + "required": ["sinceSeq"], + "properties": { "sinceSeq": { "description": "0 replays from the start of the kept log.", "type": "integer", "minimum": 0 } } + }, + "MessagePost": { + "description": "A human post.", + "type": "object", + "additionalProperties": false, + "required": ["text"], + "properties": { + "text": { "type": "string", "minLength": 1, "maxLength": 16384 }, + "threadOf": { "$ref": "common.json#/$defs/MessageId" } + } + }, + "TaskCreate": { + "type": "object", + "additionalProperties": false, + "required": ["title"], + "properties": { + "title": { "type": "string", "minLength": 1, "maxLength": 120 }, + "assignee": { "$ref": "common.json#/$defs/ActorId" }, + "body": { "type": "string", "maxLength": 16384 } + } + }, + "DecisionRule": { + "type": "object", + "additionalProperties": false, + "required": ["decisionId", "optionId"], + "properties": { + "decisionId": { "$ref": "common.json#/$defs/DecisionId" }, + "optionId": { "$ref": "message.json#/$defs/OptionId" } + } + }, + "PresenceHeartbeat": { + "description": "Every 15 s from the daemon.", + "type": "object", + "additionalProperties": false, + "properties": { "computerId": { "$ref": "common.json#/$defs/ComputerId" } } + }, + "WakeAck": { + "type": "object", + "additionalProperties": false, + "required": ["wakeId"], + "properties": { "wakeId": { "$ref": "common.json#/$defs/WakeId" } } + }, + "ComputerStatus": { + "type": "object", + "additionalProperties": false, + "required": ["id", "name", "online"], + "properties": { + "id": { "$ref": "common.json#/$defs/ComputerId" }, + "name": { "type": "string", "minLength": 1, "maxLength": 80 }, + "online": { "type": "boolean" } + } + }, + "RunStatus": { + "type": "object", + "additionalProperties": false, + "required": ["runId", "agentId", "startedAt"], + "properties": { + "runId": { "$ref": "common.json#/$defs/RunId" }, + "agentId": { "$ref": "common.json#/$defs/AgentId" }, + "startedAt": { "$ref": "common.json#/$defs/Timestamp" }, + "taskId": { "$ref": "common.json#/$defs/TaskId" } + } + }, + "DaemonStatus": { + "type": "object", + "additionalProperties": false, + "required": ["computers", "agents", "runs"], + "properties": { + "computers": { "type": "array", "items": { "$ref": "#/$defs/ComputerStatus" } }, + "agents": { "type": "array", "items": { "$ref": "#/$defs/AgentPresence" } }, + "runs": { "type": "array", "items": { "$ref": "#/$defs/RunStatus" } } + } + } + } +} diff --git a/schema/fixtures/ClaimRunResponse/invalid-required-runToken.json b/schema/fixtures/ClaimRunResponse/invalid-required-runToken.json new file mode 100644 index 0000000000..934e15f12d --- /dev/null +++ b/schema/fixtures/ClaimRunResponse/invalid-required-runToken.json @@ -0,0 +1,6 @@ +{ + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "brief": "x", + "mcpUrl": "https://v4.example.dev/mcp", + "expiresAt": "2026-10-29T09:59:00Z" +} diff --git a/schema/fixtures/ClaimRunResponse/valid-1.json b/schema/fixtures/ClaimRunResponse/valid-1.json new file mode 100644 index 0000000000..43258cd3f9 --- /dev/null +++ b/schema/fixtures/ClaimRunResponse/valid-1.json @@ -0,0 +1,7 @@ +{ + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "runToken": "cmr_a1B2c3D4e5a1B2c3D4e5a1B2c3D4e5a1B2c3D4e5xyz", + "brief": "You were assigned TASK tsk_… Claim it, then post an ask with options.", + "mcpUrl": "https://v4.example.dev/mcp", + "expiresAt": "2026-10-29T09:59:00Z" +} diff --git a/schema/fixtures/Envelope/invalid-required-podId.json b/schema/fixtures/Envelope/invalid-required-podId.json new file mode 100644 index 0000000000..b6a351364b --- /dev/null +++ b/schema/fixtures/Envelope/invalid-required-podId.json @@ -0,0 +1,7 @@ +{ + "v": 1, + "type": "subscribe", + "data": { + "sinceSeq": 1 + } +} diff --git a/schema/fixtures/Envelope/invalid-version.json b/schema/fixtures/Envelope/invalid-version.json new file mode 100644 index 0000000000..7603bfc492 --- /dev/null +++ b/schema/fixtures/Envelope/invalid-version.json @@ -0,0 +1,8 @@ +{ + "v": 2, + "type": "subscribe", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "data": { + "sinceSeq": 1 + } +} diff --git a/schema/fixtures/Envelope/valid-daemon-status.json b/schema/fixtures/Envelope/valid-daemon-status.json new file mode 100644 index 0000000000..a4a0eb58b0 --- /dev/null +++ b/schema/fixtures/Envelope/valid-daemon-status.json @@ -0,0 +1,34 @@ +{ + "v": 1, + "type": "daemon.status", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "data": { + "computers": [ + { + "id": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "name": "sams-mac", + "online": true + } + ], + "agents": [ + { + "agent": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "harness": "claude-code", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "state": "online" + } + ], + "runs": [ + { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "startedAt": "2026-10-29T09:00:00Z", + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01" + } + ] + } +} diff --git a/schema/fixtures/Envelope/valid-decision-rule.json b/schema/fixtures/Envelope/valid-decision-rule.json new file mode 100644 index 0000000000..4a0ebfc8cf --- /dev/null +++ b/schema/fixtures/Envelope/valid-decision-rule.json @@ -0,0 +1,9 @@ +{ + "v": 1, + "type": "decision.rule", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "data": { + "decisionId": "dec_01JCZK7Q8XG4M2B9V5T3W6RH01", + "optionId": "a" + } +} diff --git a/schema/fixtures/Envelope/valid-error.json b/schema/fixtures/Envelope/valid-error.json new file mode 100644 index 0000000000..c5fb1b18e0 --- /dev/null +++ b/schema/fixtures/Envelope/valid-error.json @@ -0,0 +1,17 @@ +{ + "v": 1, + "type": "error", + "seq": 108, + "ts": "2026-10-29T09:08:00Z", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "actor": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "data": { + "code": "already_claimed", + "message": "Task is claimed by another run.", + "ref": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01" + } +} diff --git a/schema/fixtures/Envelope/valid-hello.json b/schema/fixtures/Envelope/valid-hello.json new file mode 100644 index 0000000000..5dff093dcd --- /dev/null +++ b/schema/fixtures/Envelope/valid-hello.json @@ -0,0 +1,51 @@ +{ + "v": 1, + "type": "hello", + "seq": 100, + "ts": "2026-10-29T09:00:00Z", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "actor": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "data": { + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "headSeq": 99, + "you": { + "kind": "human", + "id": "usr_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam" + }, + "members": [ + { + "actor": { + "kind": "human", + "id": "usr_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam" + }, + "role": "member" + }, + { + "actor": { + "kind": "human", + "id": "usr_01JCZK7Q8XG4M2B9V5T3W6RH02", + "displayName": "Lily" + }, + "role": "owner" + } + ], + "agents": [ + { + "agent": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "harness": "claude-code", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "state": "online" + } + ] + } +} diff --git a/schema/fixtures/Envelope/valid-message-created.json b/schema/fixtures/Envelope/valid-message-created.json new file mode 100644 index 0000000000..e18ce9e47d --- /dev/null +++ b/schema/fixtures/Envelope/valid-message-created.json @@ -0,0 +1,72 @@ +{ + "v": 1, + "type": "message.created", + "seq": 101, + "ts": "2026-10-29T09:01:00Z", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "actor": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "data": { + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH13", + "seq": 13, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "author": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "createdAt": "2026-10-29T09:01:00Z", + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "summary": "Merge the auth fix now or wait for review?", + "kind": "ask", + "status": "blocked", + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "needs": { + "to": { + "userIds": [ + "usr_01JCZK7Q8XG4M2B9V5T3W6RH01" + ] + }, + "action": "approve" + }, + "options": [ + { + "id": "a", + "label": "Merge now", + "recommended": true + }, + { + "id": "b", + "label": "Wait for review" + } + ], + "receipt": { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "harness": "claude-code", + "harnessVersion": "0.9.1", + "startedAt": "2026-10-29T09:00:00Z", + "postedAt": "2026-10-29T09:01:00Z", + "toolCalls": 2, + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "wakeReason": "task_assigned" + }, + "decision": { + "id": "dec_01JCZK7Q8XG4M2B9V5T3W6RH01", + "state": "ruled", + "ruling": { + "optionId": "a", + "by": { + "kind": "human", + "id": "usr_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam" + }, + "at": "2026-10-29T09:30:00Z" + } + } + } +} diff --git a/schema/fixtures/Envelope/valid-message-post.json b/schema/fixtures/Envelope/valid-message-post.json new file mode 100644 index 0000000000..7aaba4c30d --- /dev/null +++ b/schema/fixtures/Envelope/valid-message-post.json @@ -0,0 +1,8 @@ +{ + "v": 1, + "type": "message.post", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "data": { + "text": "Looks good to me" + } +} diff --git a/schema/fixtures/Envelope/valid-message-updated.json b/schema/fixtures/Envelope/valid-message-updated.json new file mode 100644 index 0000000000..68c3f30418 --- /dev/null +++ b/schema/fixtures/Envelope/valid-message-updated.json @@ -0,0 +1,28 @@ +{ + "v": 1, + "type": "message.updated", + "seq": 102, + "ts": "2026-10-29T09:02:00Z", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "actor": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "data": { + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH13", + "decision": { + "id": "dec_01JCZK7Q8XG4M2B9V5T3W6RH01", + "state": "ruled", + "ruling": { + "optionId": "a", + "by": { + "kind": "human", + "id": "usr_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam" + }, + "at": "2026-10-29T09:30:00Z" + } + } + } +} diff --git a/schema/fixtures/Envelope/valid-presence-changed.json b/schema/fixtures/Envelope/valid-presence-changed.json new file mode 100644 index 0000000000..91ec904db3 --- /dev/null +++ b/schema/fixtures/Envelope/valid-presence-changed.json @@ -0,0 +1,21 @@ +{ + "v": 1, + "type": "presence.changed", + "seq": 106, + "ts": "2026-10-29T09:06:00Z", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "actor": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "data": { + "actor": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "state": "working", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01" + } +} diff --git a/schema/fixtures/Envelope/valid-presence-heartbeat.json b/schema/fixtures/Envelope/valid-presence-heartbeat.json new file mode 100644 index 0000000000..861656ae8f --- /dev/null +++ b/schema/fixtures/Envelope/valid-presence-heartbeat.json @@ -0,0 +1,8 @@ +{ + "v": 1, + "type": "presence.heartbeat", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "data": { + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01" + } +} diff --git a/schema/fixtures/Envelope/valid-run-wake.json b/schema/fixtures/Envelope/valid-run-wake.json new file mode 100644 index 0000000000..41d875cd8f --- /dev/null +++ b/schema/fixtures/Envelope/valid-run-wake.json @@ -0,0 +1,21 @@ +{ + "v": 1, + "type": "run.wake", + "seq": 107, + "ts": "2026-10-29T09:07:00Z", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "actor": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "data": { + "wakeId": "wak_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "reason": "decision_ruled", + "refs": { + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "decisionId": "dec_01JCZK7Q8XG4M2B9V5T3W6RH01" + } + } +} diff --git a/schema/fixtures/Envelope/valid-subscribe.json b/schema/fixtures/Envelope/valid-subscribe.json new file mode 100644 index 0000000000..ad2cbde5ce --- /dev/null +++ b/schema/fixtures/Envelope/valid-subscribe.json @@ -0,0 +1,8 @@ +{ + "v": 1, + "type": "subscribe", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "data": { + "sinceSeq": 42 + } +} diff --git a/schema/fixtures/Envelope/valid-task-claimed.json b/schema/fixtures/Envelope/valid-task-claimed.json new file mode 100644 index 0000000000..6ee3af3d82 --- /dev/null +++ b/schema/fixtures/Envelope/valid-task-claimed.json @@ -0,0 +1,22 @@ +{ + "v": 1, + "type": "task.claimed", + "seq": 105, + "ts": "2026-10-29T09:05:00Z", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "actor": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "data": { + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "by": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "leaseUntil": "2026-10-29T09:15:00Z" + } +} diff --git a/schema/fixtures/Envelope/valid-task-create.json b/schema/fixtures/Envelope/valid-task-create.json new file mode 100644 index 0000000000..3dd0859a99 --- /dev/null +++ b/schema/fixtures/Envelope/valid-task-create.json @@ -0,0 +1,9 @@ +{ + "v": 1, + "type": "task.create", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "data": { + "title": "Fix the login redirect loop", + "assignee": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01" + } +} diff --git a/schema/fixtures/Envelope/valid-task-created.json b/schema/fixtures/Envelope/valid-task-created.json new file mode 100644 index 0000000000..271859fffc --- /dev/null +++ b/schema/fixtures/Envelope/valid-task-created.json @@ -0,0 +1,25 @@ +{ + "v": 1, + "type": "task.created", + "seq": 103, + "ts": "2026-10-29T09:03:00Z", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "actor": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "data": { + "id": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH02", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "title": "Write the README", + "status": "open", + "createdBy": { + "kind": "human", + "id": "usr_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam" + }, + "createdAt": "2026-10-29T09:00:00Z", + "updatedAt": "2026-10-29T09:00:00Z" + } +} diff --git a/schema/fixtures/Envelope/valid-task-updated.json b/schema/fixtures/Envelope/valid-task-updated.json new file mode 100644 index 0000000000..003dada8ca --- /dev/null +++ b/schema/fixtures/Envelope/valid-task-updated.json @@ -0,0 +1,35 @@ +{ + "v": 1, + "type": "task.updated", + "seq": 104, + "ts": "2026-10-29T09:04:00Z", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "actor": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "data": { + "id": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "title": "Fix the login redirect loop", + "status": "in_progress", + "assignee": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "createdBy": { + "kind": "human", + "id": "usr_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam" + }, + "claim": { + "by": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "leaseUntil": "2026-10-29T09:10:00Z" + }, + "createdAt": "2026-10-29T09:00:00Z", + "updatedAt": "2026-10-29T09:01:00Z" + } +} diff --git a/schema/fixtures/Envelope/valid-wake-ack.json b/schema/fixtures/Envelope/valid-wake-ack.json new file mode 100644 index 0000000000..ebf76123f9 --- /dev/null +++ b/schema/fixtures/Envelope/valid-wake-ack.json @@ -0,0 +1,8 @@ +{ + "v": 1, + "type": "wake.ack", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "data": { + "wakeId": "wak_01JCZK7Q8XG4M2B9V5T3W6RH01" + } +} diff --git a/schema/fixtures/Message/valid-ask-1.json b/schema/fixtures/Message/valid-ask-1.json new file mode 100644 index 0000000000..8e419495c5 --- /dev/null +++ b/schema/fixtures/Message/valid-ask-1.json @@ -0,0 +1,60 @@ +{ + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH13", + "seq": 13, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "author": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "createdAt": "2026-10-29T09:01:00Z", + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "summary": "Merge the auth fix now or wait for review?", + "kind": "ask", + "status": "blocked", + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "needs": { + "to": { + "userIds": [ + "usr_01JCZK7Q8XG4M2B9V5T3W6RH01" + ] + }, + "action": "approve" + }, + "options": [ + { + "id": "a", + "label": "Merge now", + "recommended": true + }, + { + "id": "b", + "label": "Wait for review" + } + ], + "receipt": { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "harness": "claude-code", + "harnessVersion": "0.9.1", + "startedAt": "2026-10-29T09:00:00Z", + "postedAt": "2026-10-29T09:01:00Z", + "toolCalls": 2, + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "wakeReason": "task_assigned" + }, + "decision": { + "id": "dec_01JCZK7Q8XG4M2B9V5T3W6RH01", + "state": "ruled", + "ruling": { + "optionId": "a", + "by": { + "kind": "human", + "id": "usr_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam" + }, + "at": "2026-10-29T09:30:00Z" + } + } +} diff --git a/schema/fixtures/Message/valid-ask-2.json b/schema/fixtures/Message/valid-ask-2.json new file mode 100644 index 0000000000..e9669810e7 --- /dev/null +++ b/schema/fixtures/Message/valid-ask-2.json @@ -0,0 +1,53 @@ +{ + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH14", + "seq": 14, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "author": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "createdAt": "2026-10-29T09:02:00Z", + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "summary": "Which database for the cache layer?", + "kind": "ask", + "status": "blocked", + "needs": { + "to": { + "role": "owner" + }, + "action": "decide" + }, + "options": [ + { + "id": "a", + "label": "D1", + "description": "Same platform, no new vendor." + }, + { + "id": "b", + "label": "KV" + }, + { + "id": "c", + "label": "Redis", + "description": "Fastest, adds a vendor." + } + ], + "receipt": { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "harness": "claude-code", + "harnessVersion": "0.9.1", + "startedAt": "2026-10-29T09:00:00Z", + "postedAt": "2026-10-29T09:02:00Z", + "toolCalls": 3, + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "wakeReason": "task_assigned" + }, + "decision": { + "id": "dec_01JCZK7Q8XG4M2B9V5T3W6RH11", + "state": "open" + } +} diff --git a/schema/fixtures/Message/valid-ask-3.json b/schema/fixtures/Message/valid-ask-3.json new file mode 100644 index 0000000000..7917403c92 --- /dev/null +++ b/schema/fixtures/Message/valid-ask-3.json @@ -0,0 +1,47 @@ +{ + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH15", + "seq": 15, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "author": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "createdAt": "2026-10-29T09:03:00Z", + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "summary": "Review the schema change before I regenerate", + "kind": "ask", + "status": "in_progress", + "needs": { + "to": { + "role": "member" + }, + "action": "review" + }, + "options": [ + { + "id": "a", + "label": "Looks good" + }, + { + "id": "b", + "label": "Needs changes" + } + ], + "receipt": { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "harness": "claude-code", + "harnessVersion": "0.9.1", + "startedAt": "2026-10-29T09:00:00Z", + "postedAt": "2026-10-29T09:03:00Z", + "toolCalls": 4, + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "wakeReason": "task_assigned" + }, + "decision": { + "id": "dec_01JCZK7Q8XG4M2B9V5T3W6RH12", + "state": "open" + } +} diff --git a/schema/fixtures/Message/valid-error-1.json b/schema/fixtures/Message/valid-error-1.json new file mode 100644 index 0000000000..185c0a0b95 --- /dev/null +++ b/schema/fixtures/Message/valid-error-1.json @@ -0,0 +1,28 @@ +{ + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH22", + "seq": 22, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "author": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "createdAt": "2026-10-29T09:01:00Z", + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "summary": "Build failed: missing env var DATABASE_URL", + "kind": "error", + "status": "failed", + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "receipt": { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "harness": "claude-code", + "harnessVersion": "0.9.1", + "startedAt": "2026-10-29T09:00:00Z", + "postedAt": "2026-10-29T09:01:00Z", + "toolCalls": 2, + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "wakeReason": "task_assigned" + } +} diff --git a/schema/fixtures/Message/valid-error-2.json b/schema/fixtures/Message/valid-error-2.json new file mode 100644 index 0000000000..6ab8d13b3a --- /dev/null +++ b/schema/fixtures/Message/valid-error-2.json @@ -0,0 +1,27 @@ +{ + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH23", + "seq": 23, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "author": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "createdAt": "2026-10-29T09:02:00Z", + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "summary": "Could not reach GitHub; retrying in 5 minutes", + "kind": "error", + "status": "blocked", + "receipt": { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "harness": "claude-code", + "harnessVersion": "0.9.1", + "startedAt": "2026-10-29T09:00:00Z", + "postedAt": "2026-10-29T09:02:00Z", + "toolCalls": 3, + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "wakeReason": "task_assigned" + } +} diff --git a/schema/fixtures/Message/valid-error-3.json b/schema/fixtures/Message/valid-error-3.json new file mode 100644 index 0000000000..19d0d4576a --- /dev/null +++ b/schema/fixtures/Message/valid-error-3.json @@ -0,0 +1,35 @@ +{ + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH24", + "seq": 24, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "author": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "createdAt": "2026-10-29T09:03:00Z", + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "summary": "Test suite crashed on startup", + "kind": "error", + "status": "failed", + "body": "Error: cannot find module ./gen\n at load (index.ts:3)", + "links": [ + { + "label": "CI run", + "url": "https://github.com/Team-Commonly/commonly/actions/runs/1", + "type": "log" + } + ], + "receipt": { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "harness": "claude-code", + "harnessVersion": "0.9.1", + "startedAt": "2026-10-29T09:00:00Z", + "postedAt": "2026-10-29T09:03:00Z", + "toolCalls": 4, + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "wakeReason": "task_assigned" + } +} diff --git a/schema/fixtures/Message/valid-fyi-1.json b/schema/fixtures/Message/valid-fyi-1.json new file mode 100644 index 0000000000..fb31700c55 --- /dev/null +++ b/schema/fixtures/Message/valid-fyi-1.json @@ -0,0 +1,34 @@ +{ + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH19", + "seq": 19, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "author": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "createdAt": "2026-10-29T09:01:00Z", + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "summary": "Heads up: the preview URL changed", + "kind": "fyi", + "status": "done", + "links": [ + { + "label": "preview", + "url": "https://pr-12.example.dev", + "type": "doc" + } + ], + "receipt": { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "harness": "claude-code", + "harnessVersion": "0.9.1", + "startedAt": "2026-10-29T09:00:00Z", + "postedAt": "2026-10-29T09:01:00Z", + "toolCalls": 2, + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "wakeReason": "task_assigned" + } +} diff --git a/schema/fixtures/Message/valid-fyi-2.json b/schema/fixtures/Message/valid-fyi-2.json new file mode 100644 index 0000000000..456ac01321 --- /dev/null +++ b/schema/fixtures/Message/valid-fyi-2.json @@ -0,0 +1,27 @@ +{ + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH20", + "seq": 20, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "author": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "createdAt": "2026-10-29T09:02:00Z", + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "summary": "I will be offline for an hour", + "kind": "fyi", + "status": "done", + "receipt": { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "harness": "claude-code", + "harnessVersion": "0.9.1", + "startedAt": "2026-10-29T09:00:00Z", + "postedAt": "2026-10-29T09:02:00Z", + "toolCalls": 3, + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "wakeReason": "task_assigned" + } +} diff --git a/schema/fixtures/Message/valid-fyi-3.json b/schema/fixtures/Message/valid-fyi-3.json new file mode 100644 index 0000000000..ccf2c3cd94 --- /dev/null +++ b/schema/fixtures/Message/valid-fyi-3.json @@ -0,0 +1,34 @@ +{ + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH21", + "seq": 21, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "author": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "createdAt": "2026-10-29T09:03:00Z", + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "summary": "Found an old ADR that covers this", + "kind": "fyi", + "status": "done", + "links": [ + { + "label": "ADR-004", + "url": "https://github.com/Team-Commonly/commonly/blob/main/docs/adr/ADR-004.md", + "type": "doc" + } + ], + "receipt": { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "harness": "claude-code", + "harnessVersion": "0.9.1", + "startedAt": "2026-10-29T09:00:00Z", + "postedAt": "2026-10-29T09:03:00Z", + "toolCalls": 4, + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "wakeReason": "task_assigned" + } +} diff --git a/schema/fixtures/Message/valid-human-post.json b/schema/fixtures/Message/valid-human-post.json new file mode 100644 index 0000000000..72bc91c3f4 --- /dev/null +++ b/schema/fixtures/Message/valid-human-post.json @@ -0,0 +1,15 @@ +{ + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH40", + "seq": 40, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "author": { + "kind": "human", + "id": "usr_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam" + }, + "createdAt": "2026-10-29T09:40:00Z", + "summary": "Can someone look at the flaky test?", + "kind": "fyi", + "status": "done", + "body": "Can someone look at the flaky test?\nIt failed twice today." +} diff --git a/schema/fixtures/Message/valid-progress-1.json b/schema/fixtures/Message/valid-progress-1.json new file mode 100644 index 0000000000..7534a23a55 --- /dev/null +++ b/schema/fixtures/Message/valid-progress-1.json @@ -0,0 +1,28 @@ +{ + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH16", + "seq": 16, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "author": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "createdAt": "2026-10-29T09:01:00Z", + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "summary": "Halfway: 4 of 8 routes ported", + "kind": "progress", + "status": "in_progress", + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "receipt": { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "harness": "claude-code", + "harnessVersion": "0.9.1", + "startedAt": "2026-10-29T09:00:00Z", + "postedAt": "2026-10-29T09:01:00Z", + "toolCalls": 2, + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "wakeReason": "task_assigned" + } +} diff --git a/schema/fixtures/Message/valid-progress-2.json b/schema/fixtures/Message/valid-progress-2.json new file mode 100644 index 0000000000..7ed1bb4839 --- /dev/null +++ b/schema/fixtures/Message/valid-progress-2.json @@ -0,0 +1,27 @@ +{ + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH17", + "seq": 17, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "author": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "createdAt": "2026-10-29T09:02:00Z", + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "summary": "Tests running, 2 minutes left", + "kind": "progress", + "status": "in_progress", + "receipt": { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "harness": "claude-code", + "harnessVersion": "0.9.1", + "startedAt": "2026-10-29T09:00:00Z", + "postedAt": "2026-10-29T09:02:00Z", + "toolCalls": 3, + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "wakeReason": "task_assigned" + } +} diff --git a/schema/fixtures/Message/valid-progress-3.json b/schema/fixtures/Message/valid-progress-3.json new file mode 100644 index 0000000000..4a73398fbd --- /dev/null +++ b/schema/fixtures/Message/valid-progress-3.json @@ -0,0 +1,28 @@ +{ + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH18", + "seq": 18, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "author": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "createdAt": "2026-10-29T09:03:00Z", + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "summary": "Waiting on CI before the next step", + "kind": "progress", + "status": "blocked", + "caveat": "CI has been flaky today.", + "receipt": { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "harness": "claude-code", + "harnessVersion": "0.9.1", + "startedAt": "2026-10-29T09:00:00Z", + "postedAt": "2026-10-29T09:03:00Z", + "toolCalls": 4, + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "wakeReason": "task_assigned" + } +} diff --git a/schema/fixtures/Message/valid-result-1.json b/schema/fixtures/Message/valid-result-1.json new file mode 100644 index 0000000000..c1d849d8c3 --- /dev/null +++ b/schema/fixtures/Message/valid-result-1.json @@ -0,0 +1,35 @@ +{ + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH10", + "seq": 10, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "author": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "createdAt": "2026-10-29T09:01:00Z", + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "summary": "Fixed the login redirect loop; PR is green", + "kind": "result", + "status": "done", + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "links": [ + { + "label": "PR #12", + "url": "https://github.com/Team-Commonly/commonly/pull/12", + "type": "pr" + } + ], + "receipt": { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "harness": "claude-code", + "harnessVersion": "0.9.1", + "startedAt": "2026-10-29T09:00:00Z", + "postedAt": "2026-10-29T09:01:00Z", + "toolCalls": 2, + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "wakeReason": "task_assigned" + } +} diff --git a/schema/fixtures/Message/valid-result-2.json b/schema/fixtures/Message/valid-result-2.json new file mode 100644 index 0000000000..44152f93cf --- /dev/null +++ b/schema/fixtures/Message/valid-result-2.json @@ -0,0 +1,29 @@ +{ + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH11", + "seq": 11, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "author": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "createdAt": "2026-10-29T09:02:00Z", + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "summary": "Benchmark done: p50 41 ms, p90 88 ms", + "kind": "result", + "status": "done", + "body": "| run | p50 | p90 |\n|---|---|---|\n| 1 | 41 | 88 |", + "caveat": "Measured on a laptop, not production.", + "receipt": { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "harness": "claude-code", + "harnessVersion": "0.9.1", + "startedAt": "2026-10-29T09:00:00Z", + "postedAt": "2026-10-29T09:02:00Z", + "toolCalls": 3, + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "wakeReason": "task_assigned" + } +} diff --git a/schema/fixtures/Message/valid-result-3.json b/schema/fixtures/Message/valid-result-3.json new file mode 100644 index 0000000000..c3715a2e38 --- /dev/null +++ b/schema/fixtures/Message/valid-result-3.json @@ -0,0 +1,34 @@ +{ + "id": "msg_01JCZK7Q8XG4M2B9V5T3W6RH12", + "seq": 12, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "author": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "createdAt": "2026-10-29T09:03:00Z", + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "summary": "Migrated 3 tables; no rows lost", + "kind": "result", + "status": "done", + "links": [ + { + "label": "log", + "url": "https://example.com/log/1", + "type": "log" + } + ], + "receipt": { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "harness": "claude-code", + "harnessVersion": "0.9.1", + "startedAt": "2026-10-29T09:00:00Z", + "postedAt": "2026-10-29T09:03:00Z", + "toolCalls": 4, + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "wakeReason": "task_assigned" + } +} diff --git a/schema/fixtures/PostInput/invalid-client-sent-receipt.json b/schema/fixtures/PostInput/invalid-client-sent-receipt.json new file mode 100644 index 0000000000..0d5e7539fc --- /dev/null +++ b/schema/fixtures/PostInput/invalid-client-sent-receipt.json @@ -0,0 +1,17 @@ +{ + "summary": "ok", + "kind": "result", + "status": "done", + "receipt": { + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "agentId": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "computerId": "cmp_01JCZK7Q8XG4M2B9V5T3W6RH01", + "harness": "claude-code", + "harnessVersion": "0.9.1", + "startedAt": "2026-10-29T09:00:00Z", + "postedAt": "2026-10-29T09:01:00Z", + "toolCalls": 2, + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "wakeReason": "task_assigned" + } +} diff --git a/schema/fixtures/PostInput/invalid-link-http.json b/schema/fixtures/PostInput/invalid-link-http.json new file mode 100644 index 0000000000..d46f05d588 --- /dev/null +++ b/schema/fixtures/PostInput/invalid-link-http.json @@ -0,0 +1,12 @@ +{ + "summary": "see", + "kind": "fyi", + "status": "done", + "links": [ + { + "label": "x", + "url": "http://example.com", + "type": "doc" + } + ] +} diff --git a/schema/fixtures/PostInput/invalid-needs-both-targets.json b/schema/fixtures/PostInput/invalid-needs-both-targets.json new file mode 100644 index 0000000000..27662b5eab --- /dev/null +++ b/schema/fixtures/PostInput/invalid-needs-both-targets.json @@ -0,0 +1,24 @@ +{ + "summary": "pick", + "kind": "ask", + "status": "blocked", + "needs": { + "to": { + "role": "member", + "userIds": [ + "usr_01JCZK7Q8XG4M2B9V5T3W6RH01" + ] + }, + "action": "decide" + }, + "options": [ + { + "id": "a", + "label": "A" + }, + { + "id": "b", + "label": "B" + } + ] +} diff --git a/schema/fixtures/PostInput/invalid-option-id.json b/schema/fixtures/PostInput/invalid-option-id.json new file mode 100644 index 0000000000..70fe2948f1 --- /dev/null +++ b/schema/fixtures/PostInput/invalid-option-id.json @@ -0,0 +1,21 @@ +{ + "summary": "pick", + "kind": "ask", + "status": "blocked", + "needs": { + "to": { + "role": "member" + }, + "action": "decide" + }, + "options": [ + { + "id": "f", + "label": "F" + }, + { + "id": "a", + "label": "A" + } + ] +} diff --git a/schema/fixtures/PostInput/invalid-required-summary.json b/schema/fixtures/PostInput/invalid-required-summary.json new file mode 100644 index 0000000000..05dd11ac63 --- /dev/null +++ b/schema/fixtures/PostInput/invalid-required-summary.json @@ -0,0 +1,4 @@ +{ + "kind": "result", + "status": "done" +} diff --git a/schema/fixtures/PostInput/invalid-summary-newline.json b/schema/fixtures/PostInput/invalid-summary-newline.json new file mode 100644 index 0000000000..5e435e9cb2 --- /dev/null +++ b/schema/fixtures/PostInput/invalid-summary-newline.json @@ -0,0 +1,5 @@ +{ + "summary": "two\nlines", + "kind": "result", + "status": "done" +} diff --git a/schema/fixtures/PostInput/invalid-summary-too-long.json b/schema/fixtures/PostInput/invalid-summary-too-long.json new file mode 100644 index 0000000000..49301aadab --- /dev/null +++ b/schema/fixtures/PostInput/invalid-summary-too-long.json @@ -0,0 +1,5 @@ +{ + "summary": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "kind": "result", + "status": "done" +} diff --git a/schema/fixtures/PostInput/valid-ask-1.json b/schema/fixtures/PostInput/valid-ask-1.json new file mode 100644 index 0000000000..b0d73ea3dc --- /dev/null +++ b/schema/fixtures/PostInput/valid-ask-1.json @@ -0,0 +1,25 @@ +{ + "summary": "Merge the auth fix now or wait for review?", + "kind": "ask", + "status": "blocked", + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "needs": { + "to": { + "userIds": [ + "usr_01JCZK7Q8XG4M2B9V5T3W6RH01" + ] + }, + "action": "approve" + }, + "options": [ + { + "id": "a", + "label": "Merge now", + "recommended": true + }, + { + "id": "b", + "label": "Wait for review" + } + ] +} diff --git a/schema/fixtures/PostInput/valid-ask-2.json b/schema/fixtures/PostInput/valid-ask-2.json new file mode 100644 index 0000000000..d5bd99de08 --- /dev/null +++ b/schema/fixtures/PostInput/valid-ask-2.json @@ -0,0 +1,27 @@ +{ + "summary": "Which database for the cache layer?", + "kind": "ask", + "status": "blocked", + "needs": { + "to": { + "role": "owner" + }, + "action": "decide" + }, + "options": [ + { + "id": "a", + "label": "D1", + "description": "Same platform, no new vendor." + }, + { + "id": "b", + "label": "KV" + }, + { + "id": "c", + "label": "Redis", + "description": "Fastest, adds a vendor." + } + ] +} diff --git a/schema/fixtures/PostInput/valid-ask-3.json b/schema/fixtures/PostInput/valid-ask-3.json new file mode 100644 index 0000000000..9094a76b90 --- /dev/null +++ b/schema/fixtures/PostInput/valid-ask-3.json @@ -0,0 +1,21 @@ +{ + "summary": "Review the schema change before I regenerate", + "kind": "ask", + "status": "in_progress", + "needs": { + "to": { + "role": "member" + }, + "action": "review" + }, + "options": [ + { + "id": "a", + "label": "Looks good" + }, + { + "id": "b", + "label": "Needs changes" + } + ] +} diff --git a/schema/fixtures/PostInput/valid-error-1.json b/schema/fixtures/PostInput/valid-error-1.json new file mode 100644 index 0000000000..4fbad8e6a6 --- /dev/null +++ b/schema/fixtures/PostInput/valid-error-1.json @@ -0,0 +1,6 @@ +{ + "summary": "Build failed: missing env var DATABASE_URL", + "kind": "error", + "status": "failed", + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01" +} diff --git a/schema/fixtures/PostInput/valid-error-2.json b/schema/fixtures/PostInput/valid-error-2.json new file mode 100644 index 0000000000..fa3c23c933 --- /dev/null +++ b/schema/fixtures/PostInput/valid-error-2.json @@ -0,0 +1,5 @@ +{ + "summary": "Could not reach GitHub; retrying in 5 minutes", + "kind": "error", + "status": "blocked" +} diff --git a/schema/fixtures/PostInput/valid-error-3.json b/schema/fixtures/PostInput/valid-error-3.json new file mode 100644 index 0000000000..2ef52c1c9d --- /dev/null +++ b/schema/fixtures/PostInput/valid-error-3.json @@ -0,0 +1,13 @@ +{ + "summary": "Test suite crashed on startup", + "kind": "error", + "status": "failed", + "body": "Error: cannot find module ./gen\n at load (index.ts:3)", + "links": [ + { + "label": "CI run", + "url": "https://github.com/Team-Commonly/commonly/actions/runs/1", + "type": "log" + } + ] +} diff --git a/schema/fixtures/PostInput/valid-fyi-1.json b/schema/fixtures/PostInput/valid-fyi-1.json new file mode 100644 index 0000000000..35a738cb16 --- /dev/null +++ b/schema/fixtures/PostInput/valid-fyi-1.json @@ -0,0 +1,12 @@ +{ + "summary": "Heads up: the preview URL changed", + "kind": "fyi", + "status": "done", + "links": [ + { + "label": "preview", + "url": "https://pr-12.example.dev", + "type": "doc" + } + ] +} diff --git a/schema/fixtures/PostInput/valid-fyi-2.json b/schema/fixtures/PostInput/valid-fyi-2.json new file mode 100644 index 0000000000..b677a9b4f1 --- /dev/null +++ b/schema/fixtures/PostInput/valid-fyi-2.json @@ -0,0 +1,5 @@ +{ + "summary": "I will be offline for an hour", + "kind": "fyi", + "status": "done" +} diff --git a/schema/fixtures/PostInput/valid-fyi-3.json b/schema/fixtures/PostInput/valid-fyi-3.json new file mode 100644 index 0000000000..7c2518806d --- /dev/null +++ b/schema/fixtures/PostInput/valid-fyi-3.json @@ -0,0 +1,12 @@ +{ + "summary": "Found an old ADR that covers this", + "kind": "fyi", + "status": "done", + "links": [ + { + "label": "ADR-004", + "url": "https://github.com/Team-Commonly/commonly/blob/main/docs/adr/ADR-004.md", + "type": "doc" + } + ] +} diff --git a/schema/fixtures/PostInput/valid-progress-1.json b/schema/fixtures/PostInput/valid-progress-1.json new file mode 100644 index 0000000000..d000c1ab57 --- /dev/null +++ b/schema/fixtures/PostInput/valid-progress-1.json @@ -0,0 +1,6 @@ +{ + "summary": "Halfway: 4 of 8 routes ported", + "kind": "progress", + "status": "in_progress", + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01" +} diff --git a/schema/fixtures/PostInput/valid-progress-2.json b/schema/fixtures/PostInput/valid-progress-2.json new file mode 100644 index 0000000000..7664cfe284 --- /dev/null +++ b/schema/fixtures/PostInput/valid-progress-2.json @@ -0,0 +1,5 @@ +{ + "summary": "Tests running, 2 minutes left", + "kind": "progress", + "status": "in_progress" +} diff --git a/schema/fixtures/PostInput/valid-progress-3.json b/schema/fixtures/PostInput/valid-progress-3.json new file mode 100644 index 0000000000..06f402287a --- /dev/null +++ b/schema/fixtures/PostInput/valid-progress-3.json @@ -0,0 +1,6 @@ +{ + "summary": "Waiting on CI before the next step", + "kind": "progress", + "status": "blocked", + "caveat": "CI has been flaky today." +} diff --git a/schema/fixtures/PostInput/valid-result-1.json b/schema/fixtures/PostInput/valid-result-1.json new file mode 100644 index 0000000000..f57510280c --- /dev/null +++ b/schema/fixtures/PostInput/valid-result-1.json @@ -0,0 +1,13 @@ +{ + "summary": "Fixed the login redirect loop; PR is green", + "kind": "result", + "status": "done", + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "links": [ + { + "label": "PR #12", + "url": "https://github.com/Team-Commonly/commonly/pull/12", + "type": "pr" + } + ] +} diff --git a/schema/fixtures/PostInput/valid-result-2.json b/schema/fixtures/PostInput/valid-result-2.json new file mode 100644 index 0000000000..4977bd13d5 --- /dev/null +++ b/schema/fixtures/PostInput/valid-result-2.json @@ -0,0 +1,7 @@ +{ + "summary": "Benchmark done: p50 41 ms, p90 88 ms", + "kind": "result", + "status": "done", + "body": "| run | p50 | p90 |\n|---|---|---|\n| 1 | 41 | 88 |", + "caveat": "Measured on a laptop, not production." +} diff --git a/schema/fixtures/PostInput/valid-result-3.json b/schema/fixtures/PostInput/valid-result-3.json new file mode 100644 index 0000000000..ee713aceab --- /dev/null +++ b/schema/fixtures/PostInput/valid-result-3.json @@ -0,0 +1,12 @@ +{ + "summary": "Migrated 3 tables; no rows lost", + "kind": "result", + "status": "done", + "links": [ + { + "label": "log", + "url": "https://example.com/log/1", + "type": "log" + } + ] +} diff --git a/schema/fixtures/ReadInput/invalid-limit.json b/schema/fixtures/ReadInput/invalid-limit.json new file mode 100644 index 0000000000..f156f52625 --- /dev/null +++ b/schema/fixtures/ReadInput/invalid-limit.json @@ -0,0 +1,3 @@ +{ + "limit": 51 +} diff --git a/schema/fixtures/ReadInput/valid-1.json b/schema/fixtures/ReadInput/valid-1.json new file mode 100644 index 0000000000..46812dd464 --- /dev/null +++ b/schema/fixtures/ReadInput/valid-1.json @@ -0,0 +1,4 @@ +{ + "sinceSeq": 10, + "limit": 20 +} diff --git a/schema/fixtures/RegisterComputerRequest/invalid-os-windows.json b/schema/fixtures/RegisterComputerRequest/invalid-os-windows.json new file mode 100644 index 0000000000..a5e7adde35 --- /dev/null +++ b/schema/fixtures/RegisterComputerRequest/invalid-os-windows.json @@ -0,0 +1,5 @@ +{ + "name": "pc", + "os": "windows", + "arch": "amd64" +} diff --git a/schema/fixtures/RegisterComputerRequest/valid-1.json b/schema/fixtures/RegisterComputerRequest/valid-1.json new file mode 100644 index 0000000000..58adde22e5 --- /dev/null +++ b/schema/fixtures/RegisterComputerRequest/valid-1.json @@ -0,0 +1,5 @@ +{ + "name": "sams-mac", + "os": "darwin", + "arch": "arm64" +} diff --git a/schema/fixtures/Task/invalid-required-status.json b/schema/fixtures/Task/invalid-required-status.json new file mode 100644 index 0000000000..6f248e797e --- /dev/null +++ b/schema/fixtures/Task/invalid-required-status.json @@ -0,0 +1,12 @@ +{ + "id": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "title": "x", + "createdBy": { + "kind": "human", + "id": "usr_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam" + }, + "createdAt": "2026-10-29T09:00:00Z", + "updatedAt": "2026-10-29T09:00:00Z" +} diff --git a/schema/fixtures/Task/valid-claimed.json b/schema/fixtures/Task/valid-claimed.json new file mode 100644 index 0000000000..01db2a206d --- /dev/null +++ b/schema/fixtures/Task/valid-claimed.json @@ -0,0 +1,23 @@ +{ + "id": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "title": "Fix the login redirect loop", + "status": "in_progress", + "assignee": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "createdBy": { + "kind": "human", + "id": "usr_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam" + }, + "claim": { + "by": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "leaseUntil": "2026-10-29T09:10:00Z" + }, + "createdAt": "2026-10-29T09:00:00Z", + "updatedAt": "2026-10-29T09:01:00Z" +} diff --git a/schema/fixtures/Task/valid-open.json b/schema/fixtures/Task/valid-open.json new file mode 100644 index 0000000000..f4ee06c186 --- /dev/null +++ b/schema/fixtures/Task/valid-open.json @@ -0,0 +1,13 @@ +{ + "id": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH02", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "title": "Write the README", + "status": "open", + "createdBy": { + "kind": "human", + "id": "usr_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam" + }, + "createdAt": "2026-10-29T09:00:00Z", + "updatedAt": "2026-10-29T09:00:00Z" +} diff --git a/schema/fixtures/TaskClaimInput/invalid-required-taskId.json b/schema/fixtures/TaskClaimInput/invalid-required-taskId.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/schema/fixtures/TaskClaimInput/invalid-required-taskId.json @@ -0,0 +1 @@ +{} diff --git a/schema/fixtures/TaskClaimInput/valid-1.json b/schema/fixtures/TaskClaimInput/valid-1.json new file mode 100644 index 0000000000..18e0412274 --- /dev/null +++ b/schema/fixtures/TaskClaimInput/valid-1.json @@ -0,0 +1,3 @@ +{ + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01" +} diff --git a/schema/fixtures/TaskUpdateInput/invalid-status-open.json b/schema/fixtures/TaskUpdateInput/invalid-status-open.json new file mode 100644 index 0000000000..4328fcc953 --- /dev/null +++ b/schema/fixtures/TaskUpdateInput/invalid-status-open.json @@ -0,0 +1,4 @@ +{ + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "status": "open" +} diff --git a/schema/fixtures/TaskUpdateInput/valid-done.json b/schema/fixtures/TaskUpdateInput/valid-done.json new file mode 100644 index 0000000000..7ec0fc56bd --- /dev/null +++ b/schema/fixtures/TaskUpdateInput/valid-done.json @@ -0,0 +1,5 @@ +{ + "taskId": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "status": "done", + "note": "Merged in PR #12." +} diff --git a/schema/fixtures/WhoamiInput/valid-1.json b/schema/fixtures/WhoamiInput/valid-1.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/schema/fixtures/WhoamiInput/valid-1.json @@ -0,0 +1 @@ +{} diff --git a/schema/fixtures/WhoamiOutput/valid-1.json b/schema/fixtures/WhoamiOutput/valid-1.json new file mode 100644 index 0000000000..b5abc44c1f --- /dev/null +++ b/schema/fixtures/WhoamiOutput/valid-1.json @@ -0,0 +1,32 @@ +{ + "agent": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "task": { + "id": "tsk_01JCZK7Q8XG4M2B9V5T3W6RH01", + "podId": "pod_01JCZK7Q8XG4M2B9V5T3W6RH01", + "title": "Fix the login redirect loop", + "status": "in_progress", + "assignee": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "createdBy": { + "kind": "human", + "id": "usr_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam" + }, + "claim": { + "by": { + "kind": "agent", + "id": "agt_01JCZK7Q8XG4M2B9V5T3W6RH01", + "displayName": "Sam's Claude Code" + }, + "runId": "run_01JCZK7Q8XG4M2B9V5T3W6RH01", + "leaseUntil": "2026-10-29T09:10:00Z" + }, + "createdAt": "2026-10-29T09:00:00Z", + "updatedAt": "2026-10-29T09:01:00Z" + } +} diff --git a/schema/message.json b/schema/message.json new file mode 100644 index 0000000000..3286165b61 --- /dev/null +++ b/schema/message.json @@ -0,0 +1,156 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://commonly.me/v4/schema/message.json", + "title": "Messages", + "description": "The message contract. PostInput is what an agent writes through commonly_post. Message is what the pod stores and sends: PostInput plus fields only the server sets. A post that carries a server-set field fails validation, because PostInput allows no other properties.", + "x-rules": [ + "needs is required when kind is ask, and only allowed then.", + "options is required when kind is ask, and only allowed then. At most one option is recommended, and option ids are unique.", + "A post without summary is refused with summary_required, not derived.", + "A human post (the message.post socket event) is stored as kind fyi, status done, body = text and summary = the first line of text cut to 120 characters.", + "receipt is set on every agent post and never on a human post. decision is set exactly when kind is ask.", + "A decision is ruled once. A second ruling gets already_ruled and the first stays." + ], + "$defs": { + "MessageKind": { "type": "string", "enum": ["result", "ask", "progress", "fyi", "error"] }, + "MessageStatus": { "type": "string", "enum": ["done", "in_progress", "blocked", "failed"] }, + "NeedsAction": { "type": "string", "enum": ["approve", "decide", "review", "answer", "unblock"] }, + "MemberRole": { "type": "string", "enum": ["member", "owner"] }, + "NeedsTarget": { + "description": "Exactly one of userIds or role.", + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "maxProperties": 1, + "properties": { + "userIds": { "type": "array", "minItems": 1, "maxItems": 20, "items": { "$ref": "common.json#/$defs/UserId" } }, + "role": { "$ref": "#/$defs/MemberRole" } + } + }, + "Needs": { + "description": "The only field that notifies anyone. progress and fyi posts never notify.", + "type": "object", + "additionalProperties": false, + "required": ["to", "action"], + "properties": { + "to": { "$ref": "#/$defs/NeedsTarget" }, + "action": { "$ref": "#/$defs/NeedsAction" } + } + }, + "OptionId": { "type": "string", "enum": ["a", "b", "c", "d", "e"] }, + "Option": { + "type": "object", + "additionalProperties": false, + "required": ["id", "label"], + "properties": { + "id": { "$ref": "#/$defs/OptionId" }, + "label": { "type": "string", "minLength": 1, "maxLength": 40 }, + "description": { "type": "string", "maxLength": 160 }, + "recommended": { "type": "boolean" } + } + }, + "LinkType": { "type": "string", "enum": ["pr", "commit", "task", "doc", "file", "log"] }, + "Link": { + "type": "object", + "additionalProperties": false, + "required": ["label", "url", "type"], + "properties": { + "label": { "type": "string", "minLength": 1, "maxLength": 40 }, + "url": { + "description": "https only, and no credentials in the URL.", + "type": "string", + "maxLength": 2048, + "pattern": "^https://[^/\\s@]+(/[^\\s]*)?$" + }, + "type": { "$ref": "#/$defs/LinkType" } + } + }, + "WakeReason": { "type": "string", "enum": ["task_assigned", "mention", "decision_ruled"] }, + "Receipt": { + "description": "Stamped by the pod from the run record. An agent can't write any of it.", + "type": "object", + "additionalProperties": false, + "required": ["runId", "agentId", "computerId", "harness", "startedAt", "postedAt", "toolCalls", "wakeReason"], + "properties": { + "runId": { "$ref": "common.json#/$defs/RunId" }, + "agentId": { "$ref": "common.json#/$defs/AgentId" }, + "computerId": { "$ref": "common.json#/$defs/ComputerId" }, + "harness": { "$ref": "common.json#/$defs/Harness" }, + "harnessVersion": { "description": "Reported by the daemon at session start.", "type": "string", "maxLength": 64 }, + "startedAt": { "$ref": "common.json#/$defs/Timestamp" }, + "postedAt": { "$ref": "common.json#/$defs/Timestamp" }, + "toolCalls": { "description": "MCP calls the pod saw on this run's token.", "type": "integer", "minimum": 0 }, + "taskId": { "$ref": "common.json#/$defs/TaskId" }, + "wakeReason": { "$ref": "#/$defs/WakeReason" } + } + }, + "DecisionState": { "type": "string", "enum": ["open", "ruled", "expired"] }, + "Ruling": { + "type": "object", + "additionalProperties": false, + "required": ["optionId", "by", "at"], + "properties": { + "optionId": { "$ref": "#/$defs/OptionId" }, + "by": { "$ref": "common.json#/$defs/Actor" }, + "at": { "$ref": "common.json#/$defs/Timestamp" } + } + }, + "Decision": { + "type": "object", + "additionalProperties": false, + "required": ["id", "state"], + "properties": { + "id": { "$ref": "common.json#/$defs/DecisionId" }, + "state": { "$ref": "#/$defs/DecisionState" }, + "ruling": { "$ref": "#/$defs/Ruling" } + } + }, + "PostInput": { + "type": "object", + "additionalProperties": false, + "required": ["summary", "kind", "status"], + "properties": { + "summary": { + "description": "The headline, outcome first. One line, at most 120 characters.", + "type": "string", + "minLength": 1, + "maxLength": 120, + "pattern": "^[^\\r\\n]*$" + }, + "kind": { "$ref": "#/$defs/MessageKind" }, + "status": { "$ref": "#/$defs/MessageStatus" }, + "needs": { "$ref": "#/$defs/Needs" }, + "options": { "type": "array", "minItems": 2, "maxItems": 5, "items": { "$ref": "#/$defs/Option" } }, + "taskId": { "$ref": "common.json#/$defs/TaskId" }, + "links": { "type": "array", "maxItems": 8, "items": { "$ref": "#/$defs/Link" } }, + "caveat": { "description": "What isn't verified.", "type": "string", "maxLength": 160 }, + "body": { "description": "Markdown. Folded by default in clients.", "type": "string", "maxLength": 16384 } + } + }, + "Message": { + "type": "object", + "additionalProperties": false, + "required": ["id", "seq", "podId", "author", "createdAt", "summary", "kind", "status"], + "properties": { + "id": { "$ref": "common.json#/$defs/MessageId" }, + "seq": { "$ref": "common.json#/$defs/Seq" }, + "podId": { "$ref": "common.json#/$defs/PodId" }, + "author": { "$ref": "common.json#/$defs/Actor" }, + "createdAt": { "$ref": "common.json#/$defs/Timestamp" }, + "runId": { "$ref": "common.json#/$defs/RunId" }, + "threadOf": { "$ref": "common.json#/$defs/MessageId" }, + "summary": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[^\\r\\n]*$" }, + "kind": { "$ref": "#/$defs/MessageKind" }, + "status": { "$ref": "#/$defs/MessageStatus" }, + "needs": { "$ref": "#/$defs/Needs" }, + "options": { "type": "array", "minItems": 2, "maxItems": 5, "items": { "$ref": "#/$defs/Option" } }, + "taskId": { "$ref": "common.json#/$defs/TaskId" }, + "links": { "type": "array", "maxItems": 8, "items": { "$ref": "#/$defs/Link" } }, + "caveat": { "type": "string", "maxLength": 160 }, + "body": { "type": "string", "maxLength": 16384 }, + "decision": { "$ref": "#/$defs/Decision" }, + "receipt": { "$ref": "#/$defs/Receipt" } + } + } + } +} diff --git a/schema/pod-api.json b/schema/pod-api.json new file mode 100644 index 0000000000..d52fecfb1b --- /dev/null +++ b/schema/pod-api.json @@ -0,0 +1,160 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://commonly.me/v4/schema/pod-api.json", + "title": "Worker HTTP API", + "description": "Request and response bodies for the Worker's HTTP routes. Every error response body is common.json ErrorBody. The Worker validates, authenticates, then calls the pod Durable Object; all authorization lives in the pod, never the client.", + "x-routes": [ + { "method": "POST", "path": "/v1/auth/device", "auth": "none", "response": "DeviceStartResponse" }, + { "method": "POST", "path": "/v1/auth/device/token", "auth": "none", "request": "DeviceTokenRequest", "response": "DeviceTokenResponse" }, + { "method": "POST", "path": "/v1/pods", "auth": "user", "request": "CreatePodRequest", "response": "Pod" }, + { "method": "POST", "path": "/v1/pods/:pod/invites", "auth": "user", "request": "CreateInviteRequest", "response": "Invite" }, + { "method": "POST", "path": "/v1/invites/:code/accept", "auth": "user", "response": "AcceptInviteResponse" }, + { "method": "POST", "path": "/v1/computers", "auth": "user", "request": "RegisterComputerRequest", "response": "RegisterComputerResponse" }, + { "method": "POST", "path": "/v1/pods/:pod/agents", "auth": "computer", "request": "RegisterAgentRequest", "response": "RegisterAgentResponse" }, + { "method": "GET", "path": "/v1/pods/:pod/ws", "auth": "user|computer", "upgrade": "websocket" }, + { "method": "POST", "path": "/v1/runs", "auth": "computer", "request": "ClaimRunRequest", "response": "ClaimRunResponse" }, + { "method": "POST", "path": "/v1/runs/:run/end", "auth": "computer", "request": "EndRunRequest" }, + { "method": "POST", "path": "/mcp", "auth": "run|user", "protocol": "mcp-streamable-http, stateless" } + ], + "x-rules": [ + "/__test/* routes exist only when ENV is local or preview, and are absent from the production bundle.", + "A device code polled before approval gets authorization_pending; after approval exactly one exchange succeeds and the next gets invalid_grant; an expired code gets expired_token.", + "A used or expired invite gets gone (HTTP 410). Only a member can create an invite." + ], + "$defs": { + "DeviceStartResponse": { + "type": "object", + "additionalProperties": false, + "required": ["deviceCode", "userCode", "verificationUri", "interval", "expiresIn"], + "properties": { + "deviceCode": { "type": "string", "minLength": 32, "maxLength": 128 }, + "userCode": { "description": "What the person types on the sign-in page, e.g. WDJB-MJHT.", "type": "string", "pattern": "^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$" }, + "verificationUri": { "type": "string", "pattern": "^https?://" }, + "interval": { "description": "Seconds between polls.", "type": "integer", "minimum": 1 }, + "expiresIn": { "description": "Seconds until the code expires.", "type": "integer", "minimum": 1 } + } + }, + "DeviceTokenRequest": { + "type": "object", + "additionalProperties": false, + "required": ["deviceCode"], + "properties": { "deviceCode": { "type": "string", "minLength": 32, "maxLength": 128 } } + }, + "DeviceTokenResponse": { + "type": "object", + "additionalProperties": false, + "required": ["token", "user", "expiresAt"], + "properties": { + "token": { "$ref": "tokens.json#/$defs/UserToken" }, + "user": { "$ref": "common.json#/$defs/Actor" }, + "expiresAt": { "$ref": "common.json#/$defs/Timestamp" } + } + }, + "CreatePodRequest": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 80 } } + }, + "Pod": { + "type": "object", + "additionalProperties": false, + "required": ["id", "name", "createdAt"], + "properties": { + "id": { "$ref": "common.json#/$defs/PodId" }, + "name": { "type": "string", "minLength": 1, "maxLength": 80 }, + "createdAt": { "$ref": "common.json#/$defs/Timestamp" } + } + }, + "CreateInviteRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "maxUses": { "description": "Defaults to 1.", "type": "integer", "minimum": 1, "maximum": 100 }, + "ttlSeconds": { "description": "Defaults to 7 days.", "type": "integer", "minimum": 60, "maximum": 2592000 } + } + }, + "Invite": { + "type": "object", + "additionalProperties": false, + "required": ["code", "url", "expiresAt", "usesLeft"], + "properties": { + "code": { "type": "string", "pattern": "^inv_[0-9A-Za-z]{22}$" }, + "url": { "type": "string", "pattern": "^https?://" }, + "expiresAt": { "$ref": "common.json#/$defs/Timestamp" }, + "usesLeft": { "type": "integer", "minimum": 0 } + } + }, + "AcceptInviteResponse": { + "type": "object", + "additionalProperties": false, + "required": ["pod"], + "properties": { "pod": { "$ref": "#/$defs/Pod" } } + }, + "OS": { "type": "string", "enum": ["darwin", "linux"] }, + "Arch": { "type": "string", "enum": ["arm64", "amd64"] }, + "RegisterComputerRequest": { + "type": "object", + "additionalProperties": false, + "required": ["name", "os", "arch"], + "properties": { + "name": { "type": "string", "minLength": 1, "maxLength": 80 }, + "os": { "$ref": "#/$defs/OS" }, + "arch": { "$ref": "#/$defs/Arch" } + } + }, + "RegisterComputerResponse": { + "type": "object", + "additionalProperties": false, + "required": ["computerId", "token"], + "properties": { + "computerId": { "$ref": "common.json#/$defs/ComputerId" }, + "token": { "$ref": "tokens.json#/$defs/ComputerToken" } + } + }, + "RegisterAgentRequest": { + "type": "object", + "additionalProperties": false, + "required": ["harness", "displayName"], + "properties": { + "harness": { "$ref": "common.json#/$defs/Harness" }, + "displayName": { "type": "string", "minLength": 1, "maxLength": 80 } + } + }, + "RegisterAgentResponse": { + "type": "object", + "additionalProperties": false, + "required": ["agentId"], + "properties": { "agentId": { "$ref": "common.json#/$defs/AgentId" } } + }, + "ClaimRunRequest": { + "type": "object", + "additionalProperties": false, + "required": ["wakeId"], + "properties": { "wakeId": { "$ref": "common.json#/$defs/WakeId" } } + }, + "ClaimRunResponse": { + "description": "The brief is built by the server and sent as the agent's first prompt. The run token goes to the agent; the computer token never does.", + "type": "object", + "additionalProperties": false, + "required": ["runId", "runToken", "brief", "mcpUrl", "expiresAt"], + "properties": { + "runId": { "$ref": "common.json#/$defs/RunId" }, + "runToken": { "$ref": "tokens.json#/$defs/RunToken" }, + "brief": { "type": "string", "minLength": 1, "maxLength": 32768 }, + "mcpUrl": { "type": "string", "pattern": "^https?://" }, + "expiresAt": { "$ref": "common.json#/$defs/Timestamp" } + } + }, + "RunOutcome": { "type": "string", "enum": ["completed", "failed", "cancelled"] }, + "EndRunRequest": { + "type": "object", + "additionalProperties": false, + "required": ["outcome"], + "properties": { + "outcome": { "$ref": "#/$defs/RunOutcome" }, + "harnessVersion": { "type": "string", "maxLength": 64 } + } + } + } +} diff --git a/schema/task.json b/schema/task.json new file mode 100644 index 0000000000..8c558414fd --- /dev/null +++ b/schema/task.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://commonly.me/v4/schema/task.json", + "title": "Tasks", + "$defs": { + "TaskStatus": { "type": "string", "enum": ["open", "in_progress", "blocked", "done", "failed"] }, + "Claim": { + "description": "One active claim per task. The lease is renewed by any MCP call on the claiming run.", + "type": "object", + "additionalProperties": false, + "required": ["by", "runId", "leaseUntil"], + "properties": { + "by": { "$ref": "common.json#/$defs/Actor" }, + "runId": { "$ref": "common.json#/$defs/RunId" }, + "leaseUntil": { "$ref": "common.json#/$defs/Timestamp" } + } + }, + "Task": { + "type": "object", + "additionalProperties": false, + "required": ["id", "podId", "title", "status", "createdBy", "createdAt", "updatedAt"], + "properties": { + "id": { "$ref": "common.json#/$defs/TaskId" }, + "podId": { "$ref": "common.json#/$defs/PodId" }, + "title": { "type": "string", "minLength": 1, "maxLength": 120 }, + "body": { "type": "string", "maxLength": 16384 }, + "status": { "$ref": "#/$defs/TaskStatus" }, + "assignee": { "$ref": "common.json#/$defs/ActorId" }, + "createdBy": { "$ref": "common.json#/$defs/Actor" }, + "claim": { "$ref": "#/$defs/Claim" }, + "createdAt": { "$ref": "common.json#/$defs/Timestamp" }, + "updatedAt": { "$ref": "common.json#/$defs/Timestamp" } + } + } + } +} diff --git a/schema/tokens.json b/schema/tokens.json new file mode 100644 index 0000000000..de5921d44a --- /dev/null +++ b/schema/tokens.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://commonly.me/v4/schema/tokens.json", + "title": "Tokens", + "description": "Bearer tokens. Each is a kind prefix plus 43 random base62 characters. Only the SHA-256 of a token is ever stored. A run token is the only credential an agent sees; the daemon never hands an agent its computer token.", + "x-prefixes": { "user": "cmu_", "computer": "cmc_", "run": "cmr_" }, + "$defs": { + "TokenKind": { "type": "string", "enum": ["user", "computer", "run"] }, + "UserToken": { "description": "Bound to one user. 30 days, refreshable.", "type": "string", "pattern": "^cmu_[0-9A-Za-z]{43}$" }, + "ComputerToken": { "description": "Bound to one user and one computer. Valid until revoked.", "type": "string", "pattern": "^cmc_[0-9A-Za-z]{43}$" }, + "RunToken": { "description": "Bound to one agent, pod and run (and task, if any). Ends with the run; at most 2 hours.", "type": "string", "pattern": "^cmr_[0-9A-Za-z]{43}$" } + } +} diff --git a/schema/tools.json b/schema/tools.json new file mode 100644 index 0000000000..359656e11a --- /dev/null +++ b/schema/tools.json @@ -0,0 +1,100 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://commonly.me/v4/schema/tools.json", + "title": "MCP tools", + "description": "The pod's MCP tools. The MCP server and the generated CLI both come from x-tools; the generator inlines each input and output definition into gen/ts/tools.json. The actor always comes from the bearer token; no tool takes an actor argument. Tool errors return common.json ErrorBody.", + "x-tools": [ + { "name": "commonly_whoami", "input": "WhoamiInput", "output": "WhoamiOutput", + "description": "Who you are in this pod: your agent identity, the pod, your current run and the task it's for, if any." }, + { "name": "commonly_read", "input": "ReadInput", "output": "ReadOutput", + "description": "Read the pod: message headlines and tasks since a sequence number. Bodies are left out unless withBody is true." }, + { "name": "commonly_task_list", "input": "TaskListInput", "output": "TaskListOutput", + "description": "List the pod's tasks, optionally filtered by status." }, + { "name": "commonly_task_claim", "input": "TaskClaimInput", "output": "TaskClaimOutput", + "description": "Claim a task before working on it. One claim per task; if someone else holds it you get already_claimed with the holder. The claim's lease renews with every tool call you make on this run." }, + { "name": "commonly_task_update", "input": "TaskUpdateInput", "output": "task.json#/$defs/Task", + "description": "Set a task's status, with an optional short note. Use done only when the task's work is finished." }, + { "name": "commonly_post", "input": "message.json#/$defs/PostInput", "output": "PostOutput", + "description": "Post to the pod. summary is the headline people read: one line, at most 120 characters, outcome first. Use kind ask with needs and 2 to 5 options when a person must decide; mark at most one option recommended. progress and fyi posts never notify anyone. The receipt is added by the pod; don't write one." } + ], + "$defs": { + "WhoamiInput": { "type": "object", "additionalProperties": false, "properties": {} }, + "WhoamiOutput": { + "type": "object", + "additionalProperties": false, + "required": ["agent", "podId", "runId"], + "properties": { + "agent": { "$ref": "common.json#/$defs/Actor" }, + "podId": { "$ref": "common.json#/$defs/PodId" }, + "runId": { "$ref": "common.json#/$defs/RunId" }, + "task": { "$ref": "task.json#/$defs/Task" } + } + }, + "ReadInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "sinceSeq": { "type": "integer", "minimum": 0 }, + "limit": { "description": "Defaults to 20.", "type": "integer", "minimum": 1, "maximum": 50 }, + "withBody": { "type": "boolean" } + } + }, + "ReadOutput": { + "type": "object", + "additionalProperties": false, + "required": ["messages", "tasks", "headSeq"], + "properties": { + "messages": { "type": "array", "items": { "$ref": "message.json#/$defs/Message" } }, + "tasks": { "type": "array", "items": { "$ref": "task.json#/$defs/Task" } }, + "headSeq": { "type": "integer", "minimum": 0 } + } + }, + "TaskListInput": { + "type": "object", + "additionalProperties": false, + "properties": { "status": { "$ref": "task.json#/$defs/TaskStatus" } } + }, + "TaskListOutput": { + "type": "object", + "additionalProperties": false, + "required": ["tasks"], + "properties": { "tasks": { "type": "array", "items": { "$ref": "task.json#/$defs/Task" } } } + }, + "TaskClaimInput": { + "type": "object", + "additionalProperties": false, + "required": ["taskId"], + "properties": { "taskId": { "$ref": "common.json#/$defs/TaskId" } } + }, + "TaskClaimOutput": { + "type": "object", + "additionalProperties": false, + "required": ["taskId", "leaseUntil"], + "properties": { + "taskId": { "$ref": "common.json#/$defs/TaskId" }, + "leaseUntil": { "$ref": "common.json#/$defs/Timestamp" } + } + }, + "TaskUpdateStatus": { "type": "string", "enum": ["in_progress", "blocked", "done", "failed"] }, + "TaskUpdateInput": { + "type": "object", + "additionalProperties": false, + "required": ["taskId", "status"], + "properties": { + "taskId": { "$ref": "common.json#/$defs/TaskId" }, + "status": { "$ref": "#/$defs/TaskUpdateStatus" }, + "note": { "type": "string", "maxLength": 500 } + } + }, + "PostOutput": { + "type": "object", + "additionalProperties": false, + "required": ["messageId", "seq"], + "properties": { + "messageId": { "$ref": "common.json#/$defs/MessageId" }, + "seq": { "$ref": "common.json#/$defs/Seq" }, + "decisionId": { "$ref": "common.json#/$defs/DecisionId" } + } + } + } +} diff --git a/scripts/check-fixtures.mjs b/scripts/check-fixtures.mjs new file mode 100644 index 0000000000..c64dc4605b --- /dev/null +++ b/scripts/check-fixtures.mjs @@ -0,0 +1,41 @@ +#!/usr/bin/env bun +// Validates every fixture in schema/fixtures// against the generated bundle (spec §3 L0 done-test). +// valid-*.json must pass and invalid-*.json must fail. An Envelope fixture's data is also checked +// against the definition its event type names. The Go side of the same check is +// daemon/internal/protocol/fixtures_test.go. +import fs from 'node:fs'; +import path from 'node:path'; +import Ajv2020 from 'ajv/dist/2020.js'; + +const ROOT = path.resolve(import.meta.dirname, '..'); +const bundle = JSON.parse(fs.readFileSync(path.join(ROOT, 'gen/ts/schemas.json'), 'utf8')); +const variants = JSON.parse(fs.readFileSync(path.join(ROOT, 'schema/events.json'), 'utf8'))['x-variants']; +const ajv = new Ajv2020({ allErrors: true, strict: true }); +ajv.addSchema(bundle); +const validator = (def) => ajv.getSchema(`${bundle.$id}#/$defs/${def}`) || fail(`no def ${def}`); +const defName = (ref) => ref.split('/').pop(); + +function fail(msg) { console.error(msg); process.exit(1); } + +const dir = path.join(ROOT, 'schema/fixtures'); +let checked = 0; +const problems = []; +for (const def of fs.readdirSync(dir).sort()) { + const validate = validator(def); + for (const file of fs.readdirSync(path.join(dir, def)).sort()) { + const value = JSON.parse(fs.readFileSync(path.join(dir, def, file), 'utf8')); + let ok = validate(value); + let errs = validate.errors; + if (ok && def === 'Envelope') { + const dataDef = validator(defName(variants[value.type].data)); + ok = dataDef(value.data); + errs = dataDef.errors; + } + const wantValid = file.startsWith('valid-'); + if (!wantValid && !file.startsWith('invalid-')) problems.push(`${def}/${file}: name must start with valid- or invalid-`); + else if (ok !== wantValid) problems.push(`${def}/${file}: expected ${wantValid ? 'valid' : 'invalid'}${errs ? ` — ${ajv.errorsText(errs)}` : ''}`); + checked++; + } +} +if (problems.length) fail(`check-fixtures: ${problems.length} of ${checked} wrong\n ${problems.join('\n ')}`); +console.log(`check-fixtures: ${checked} fixtures ok`); diff --git a/scripts/gen.mjs b/scripts/gen.mjs new file mode 100644 index 0000000000..d24e0ab003 --- /dev/null +++ b/scripts/gen.mjs @@ -0,0 +1,228 @@ +#!/usr/bin/env bun +// Generates every wire type from schema/*.json (build spec §2.1). Nothing else defines a wire shape. +// gen/ts/protocol.ts TS types, enum value lists, the event union, tool names +// gen/ts/schemas.json one bundled JSON Schema document for ajv in the Worker +// gen/ts/tools.json MCP tool definitions with input/output schemas fully inlined +// daemon/internal/protocol/gen.go Go structs, enums, required-key checks, event decode +// Deterministic: same schema in, same bytes out. `make gen` runs gofmt on the Go file afterwards. +import fs from 'node:fs'; +import path from 'node:path'; + +const ROOT = path.resolve(import.meta.dirname, '..'); +const SCHEMA_DIR = path.join(ROOT, 'schema'); +const FILES = ['common', 'tokens', 'task', 'message', 'events', 'pod-api', 'tools']; +const BANNED = ['oneOf', 'anyOf', 'allOf', 'not', 'if', 'then', 'else', 'patternProperties', '$dynamicRef']; +const HEADER = 'Code generated by scripts/gen.mjs from schema/. DO NOT EDIT.'; + +// ---------- load and index ---------- +const docs = Object.fromEntries(FILES.map((f) => [f, JSON.parse(fs.readFileSync(path.join(SCHEMA_DIR, `${f}.json`), 'utf8'))])); +const defs = new Map(); // name -> { file, schema } +for (const f of FILES) { + for (const [name, schema] of Object.entries(docs[f].$defs || {})) { + if (defs.has(name)) fail(`duplicate def ${name} in ${f}.json and ${defs.get(name).file}.json; def names are global`); + defs.set(name, { file: f, schema }); + } +} + +function fail(msg) { console.error(`gen: ${msg}`); process.exit(1); } + +function refName(ref, fromFile) { + const m = /^(?:([a-z-]+)\.json)?#\/\$defs\/([A-Za-z0-9]+)$/.exec(ref); + if (!m) fail(`unsupported $ref ${ref} in ${fromFile}.json`); + const name = m[2]; + const d = defs.get(name); + if (!d) fail(`$ref ${ref} in ${fromFile}.json points at no def`); + if ((m[1] || fromFile) !== d.file) fail(`$ref ${ref} in ${fromFile}.json names the wrong file (${name} lives in ${d.file}.json)`); + return name; +} + +// Reject composition keywords and inline objects anywhere below a def. +function check(node, where, depth) { + if (Array.isArray(node)) { node.forEach((n, i) => check(n, `${where}[${i}]`, depth)); return; } + if (!node || typeof node !== 'object') return; + for (const k of BANNED) if (k in node) fail(`${where} uses ${k}; the schema allows no composition (go-jsonschema can't, spec §2.1)`); + if (node.$ref) refName(node.$ref, where.split('.json')[0]); + if (depth > 0 && node.type === 'object' && node.properties) fail(`${where} is an inline object; make it a named $def`); + for (const [k, v] of Object.entries(node)) { + if (k.startsWith('x-') || k === 'enum' || k === 'const' || k === 'required') continue; + if (k === 'properties') { for (const [p, s] of Object.entries(v)) check(s, `${where}.${p}`, depth + 1); continue; } + check(v, `${where}.${k}`, depth + 1); + } +} +for (const [name, { file, schema }] of defs) check(schema, `${file}.json#${name}`, 0); + +// x-variants must cover EventType exactly. +const variants = docs.events['x-variants']; +const eventTypes = defs.get('EventType').schema.enum; +for (const t of eventTypes) if (!variants[t]) fail(`EventType ${t} has no x-variants entry`); +for (const t of Object.keys(variants)) if (!eventTypes.includes(t)) fail(`x-variants ${t} is not in EventType`); +const variantData = Object.fromEntries(Object.entries(variants).map(([t, v]) => [t, refName(v.data.includes('#') ? v.data : `#/$defs/${v.data}`, 'events')])); +const tools = docs.tools['x-tools'].map((t) => ({ + ...t, + inputDef: refName(t.input.includes('#') ? t.input : `#/$defs/${t.input}`, 'tools'), + outputDef: refName(t.output.includes('#') ? t.output : `#/$defs/${t.output}`, 'tools'), +})); + +const kindOf = (s) => (s.enum ? 'enum' : s.type); +const ordered = [...defs.entries()]; + +// ---------- TypeScript ---------- +function tsType(s, file) { + if (s.$ref) return refName(s.$ref, file); + if (s.const !== undefined) return JSON.stringify(s.const); + if (s.enum) return s.enum.map((v) => JSON.stringify(v)).join(' | '); + switch (s.type) { + case 'string': return 'string'; + case 'integer': case 'number': return 'number'; + case 'boolean': return 'boolean'; + case 'array': return `${wrapTs(tsType(s.items, file))}[]`; + case 'object': return 'Record'; + default: fail(`no TS mapping for ${JSON.stringify(s)}`); + } +} +const wrapTs = (t) => (t.includes('|') ? `(${t})` : t); +const doc = (s, indent = '') => (s.description ? `${indent}/** ${s.description.replace(/\*\//g, '*\\/')} */\n` : ''); +const tsKey = (k) => (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(k) ? k : JSON.stringify(k)); + +let ts = `// ${HEADER}\n/* eslint-disable */\n\n`; +for (const [name, { file, schema: s }] of ordered) { + ts += doc(s); + if (s.type === 'object' && s.properties) { + const req = new Set(s.required || []); + ts += `export interface ${name} {\n`; + for (const [k, p] of Object.entries(s.properties)) ts += doc(p, ' ') + ` ${tsKey(k)}${req.has(k) ? '' : '?'}: ${tsType(p, file)};\n`; + ts += '}\n'; + } else if (s.type === 'object') { + ts += `export type ${name} = Record;\n`; + } else { + ts += `export type ${name} = ${tsType(s, file)};\n`; + if (s.enum) ts += `export const ${name}Values = ${JSON.stringify(s.enum)} as const;\n`; + } + ts += '\n'; +} +ts += `/** Which data definition each event type carries (events.json x-variants). */\nexport interface EventDataMap {\n`; +for (const t of eventTypes) ts += ` ${JSON.stringify(t)}: ${variantData[t]};\n`; +ts += `}\n\nexport type EventOf = Omit & { type: T; data: EventDataMap[T] };\n`; +ts += `export type AnyEvent = { [K in EventType]: EventOf }[EventType];\n\n`; +ts += `export const EVENT_DIRECTION = {\n${eventTypes.map((t) => ` ${JSON.stringify(t)}: ${JSON.stringify(variants[t].dir)},`).join('\n')}\n} as const;\n\n`; +ts += `export const EVENT_DATA_DEF = {\n${eventTypes.map((t) => ` ${JSON.stringify(t)}: ${JSON.stringify(variantData[t])},`).join('\n')}\n} as const;\n\n`; +ts += `export interface ToolIO {\n${tools.map((t) => ` ${t.name}: { input: ${t.inputDef}; output: ${t.outputDef} };`).join('\n')}\n}\n`; +ts += `export type ToolName = keyof ToolIO;\nexport const TOOL_NAMES = ${JSON.stringify(tools.map((t) => t.name))} as const;\n`; + +// ---------- bundled JSON Schema (ajv) ---------- +function rewriteRefs(node, file) { + if (Array.isArray(node)) return node.map((n) => rewriteRefs(n, file)); + if (!node || typeof node !== 'object') return node; + const out = {}; + for (const [k, v] of Object.entries(node)) { + if (k.startsWith('x-')) continue; + out[k] = k === '$ref' ? `#/$defs/${refName(v, file)}` : rewriteRefs(v, file); + } + return out; +} +const bundle = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + $id: 'https://commonly.me/v4/schema/bundle.json', + $comment: HEADER, + $defs: Object.fromEntries(ordered.map(([name, { file, schema }]) => [name, rewriteRefs(schema, file)])), +}; + +// ---------- MCP tools, refs inlined ---------- +function inline(node, file, seen = []) { + if (Array.isArray(node)) return node.map((n) => inline(n, file, seen)); + if (!node || typeof node !== 'object') return node; + if (node.$ref) { + const name = refName(node.$ref, file); + if (seen.includes(name)) fail(`recursive $ref through ${seen.join(' -> ')} -> ${name}`); + const { file: f, schema } = defs.get(name); + const { $ref, ...rest } = node; // keep a sibling description if one is set + return { ...inline(schema, f, [...seen, name]), ...inline(rest, file, seen) }; + } + const out = {}; + for (const [k, v] of Object.entries(node)) if (!k.startsWith('x-')) out[k] = inline(v, file, seen); + return out; +} +const mcpTools = tools.map((t) => { + const inputSchema = inline({ $ref: `#/$defs/${t.inputDef}` }, defs.get(t.inputDef).file); + const outputSchema = inline({ $ref: `#/$defs/${t.outputDef}` }, defs.get(t.outputDef).file); + if (inputSchema.type !== 'object' || outputSchema.type !== 'object') fail(`tool ${t.name}: MCP input and output schemas must be objects`); + return { name: t.name, description: t.description, inputSchema, outputSchema }; +}); + +// ---------- Go ---------- +const INITIALISMS = new Set(['id', 'url', 'uri', 'mcp', 'os', 'acp', 'http', 'json', 'api', 'ui']); +const words = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1 $2').split(/[^A-Za-z0-9]+/).filter(Boolean); +const goIdent = (s) => words(s).map((w) => (INITIALISMS.has(w.toLowerCase()) ? w.toUpperCase() : w[0].toUpperCase() + w.slice(1))).join(''); +const goDef = (name) => goIdent(name); + +function goType(s, file) { + if (s.$ref) return goDef(refName(s.$ref, file)); + if (s.enum || s.const !== undefined) { + if (s.const !== undefined && Number.isInteger(s.const)) return 'int64'; + return 'string'; + } + switch (s.type) { + case 'string': return 'string'; + case 'integer': return 'int64'; + case 'number': return 'float64'; + case 'boolean': return 'bool'; + case 'array': return `[]${goType(s.items, file)}`; + case 'object': return 'json.RawMessage'; + default: fail(`no Go mapping for ${JSON.stringify(s)}`); + } +} +const goComment = (s, indent = '') => (s.description ? s.description.split('\n').map((l) => `${indent}// ${l}`).join('\n') + '\n' : ''); + +let go = `// ${HEADER}\n\npackage protocol\n\nimport (\n\t"encoding/json"\n\t"fmt"\n)\n\n`; +for (const [name, { file, schema: s }] of ordered) { + const g = goDef(name); + go += goComment(s); + if (s.type === 'object' && s.properties) { + const req = s.required || []; + go += `type ${g} struct {\n`; + for (const [k, p] of Object.entries(s.properties)) { + let t = goType(p, file); + const optional = !req.includes(k); + if (optional && !t.startsWith('[]') && t !== 'json.RawMessage') t = `*${t}`; + go += goComment(p, '\t') + `\t${goIdent(k)} ${t} \`json:"${k}${optional ? ',omitempty' : ''}"\`\n`; + } + go += '}\n\n'; + go += `// UnmarshalJSON rejects input that lacks a required key.\nfunc (x *${g}) UnmarshalJSON(b []byte) error {\n`; + if (req.length) { + go += `\tvar keys map[string]json.RawMessage\n\tif err := json.Unmarshal(b, &keys); err != nil {\n\t\treturn err\n\t}\n`; + go += `\tfor _, k := range []string{${req.map((r) => JSON.stringify(r)).join(', ')}} {\n\t\tif _, ok := keys[k]; !ok {\n\t\t\treturn fmt.Errorf("${name}: missing required field %q", k)\n\t\t}\n\t}\n`; + } + go += `\ttype plain ${g}\n\treturn json.Unmarshal(b, (*plain)(x))\n}\n\n`; + } else if (s.type === 'object') { + go += `type ${g} struct{}\n\n`; + } else if (s.enum) { + go += `type ${g} string\n\nconst (\n`; + for (const v of s.enum) go += `\t${g}${goIdent(String(v)) || 'Empty'} ${g} = ${JSON.stringify(v)}\n`; + go += ')\n\n'; + go += `// Valid reports whether x is one of the schema's values.\nfunc (x ${g}) Valid() bool {\n\tswitch x {\n\tcase ${s.enum.map((v) => `${g}${goIdent(String(v))}`).join(', ')}:\n\t\treturn true\n\t}\n\treturn false\n}\n\n`; + } else { + go += `type ${g} = ${goType(s, file)}\n\n`; + } +} +go += `// Registry returns a fresh value for each object definition, by schema name. Tests and decoders use it.\nvar Registry = map[string]func() any{\n`; +for (const [name, { schema: s }] of ordered) if (s.type === 'object') go += `\t${JSON.stringify(name)}: func() any { return new(${goDef(name)}) },\n`; +go += '}\n\n'; +go += `// EventDirection is who sends each event type: s2c, c2s or local (daemon to TUI only).\nvar EventDirection = map[EventType]string{\n`; +for (const t of eventTypes) go += `\tEventType${goIdent(t)}: ${JSON.stringify(variants[t].dir)},\n`; +go += '}\n\n'; +go += `// DecodeEventData decodes an envelope's data into the type x-variants names for its event type.\nfunc DecodeEventData(t EventType, raw json.RawMessage) (any, error) {\n\tswitch t {\n`; +for (const t of eventTypes) go += `\tcase EventType${goIdent(t)}:\n\t\tvar v ${goDef(variantData[t])}\n\t\tif err := json.Unmarshal(raw, &v); err != nil {\n\t\t\treturn nil, fmt.Errorf("%s: %w", t, err)\n\t\t}\n\t\treturn &v, nil\n`; +go += `\t}\n\treturn nil, fmt.Errorf("unknown event type %q", t)\n}\n\n`; +go += `// ToolNames lists the MCP tools in schema order.\nvar ToolNames = []string{${tools.map((t) => JSON.stringify(t.name)).join(', ')}}\n`; + +// ---------- write ---------- +const write = (rel, content) => { + const p = path.join(ROOT, rel); + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, content); +}; +write('gen/ts/protocol.ts', ts); +write('gen/ts/schemas.json', JSON.stringify(bundle, null, 2) + '\n'); +write('gen/ts/tools.json', JSON.stringify({ $comment: HEADER, tools: mcpTools }, null, 2) + '\n'); +write('daemon/internal/protocol/gen.go', go); +console.log(`gen: ${defs.size} defs, ${eventTypes.length} event types, ${tools.length} tools`); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..bf1e7ceda7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true + }, + "include": ["gen/ts/**/*.ts"] +}