diff --git a/.dependency-cruiser.cjs b/.dependency-cruiser.cjs new file mode 100644 index 000000000..eeb00eb32 --- /dev/null +++ b/.dependency-cruiser.cjs @@ -0,0 +1,10 @@ +// Module boundaries for the TS side (docs/development/workflow.md). Go boundaries live in entropy.json. +module.exports = { + forbidden: [ + { name: 'no-circular', severity: 'error', from: {}, to: { circular: true } }, + { name: 'gen-is-a-leaf', severity: 'error', from: { path: '^gen/' }, to: { pathNot: '^gen/' } }, + { name: 'nothing-imports-e2e', severity: 'error', from: { pathNot: '^e2e/' }, to: { path: '^e2e/' } }, + { name: 'no-test-routes-from-prod', severity: 'error', from: { path: '^worker/src/', pathNot: '^worker/src/test/' }, to: { path: '^worker/src/test/' } } + ], + options: { doNotFollow: { path: 'node_modules' }, tsPreCompilationDeps: true } +}; diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..0bd87744e --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ +## Feature + + +## Done-test + +``` +$ +E2E_MS= STEPS=/ +``` + +## What changed and why + + +## Review +- [ ] `make preflight` green locally +- [ ] `feature_list.json`: only `passes` changed +- [ ] `progress.md` entry added +- [ ] Touches a protected path (schema/, worker/src/auth, worker/src/mcp, daemon/internal/{acp,run}, install.sh, .github/workflows)? If yes: fresh-context review done, Opus `GATE: PASS` at head requested +- [ ] Lesson captured in the repo (test, lint or docs/), or none surfaced diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19080c844..67c275167 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,10 +8,10 @@ name: CI on: pull_request: - branches: [v4] + branches: [v4, "v4-*"] merge_group: push: - branches: [v4] + branches: [v4, "v4-*"] concurrency: group: ci-${{ github.event.pull_request.number || github.ref }} @@ -24,16 +24,23 @@ permissions: jobs: gen-drift: runs-on: ubuntu-latest - timeout-minutes: 2 + timeout-minutes: 3 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: make check-gen + - name: Fixtures validate (ajv) and generated TS type-checks + run: make test-ts + - name: Entropy gate (budgets, ratchet, dead code, boundaries) 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 + git fetch --no-tags --depth=1 origin v4:refs/remotes/origin/v4 || true + make entropy worker: runs-on: ubuntu-latest @@ -50,10 +57,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 +93,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 000000000..c2658d7d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..42a957c3e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,59 @@ +# Commonly v4 + +Commonly v4 is a multiplayer harness for the agents people already use. It is a pod where humans and +their Claude Code or Codex agents share tasks, ask for decisions and leave receipts. +- Backend: Cloudflare Workers, with one Durable Object per pod. +- Local agents: a headless Go daemon runs them over ACP. +- Pod tools: agents reach them through one remote MCP server. +- Client: the main check is an API-level e2e. The web client waits on docs/design/client-options.md. + +## Start every session here +1. `./init.sh`: installs dependencies, runs the baseline and prints which features pass. + If the baseline is red, fix that before anything else. +2. Read `progress.md` (newest entry first) and `git log --oneline -15`. +3. Pick the first feature in `feature_list.json` with `passes: false` and no `blockedBy`. Work on that one + feature only. + +## The loop, one feature at a time +- **Red first.** Write the feature's e2e test from its `steps` and watch it fail for the right reason. + Then build inward with unit tests for logic (red, green, refactor). +- **Verify at the API.** The feature's `test` command passes locally. A unit test alone never marks a + feature passing. +- **Mark it.** Set `passes: true`. That is the only edit allowed to `feature_list.json`. Never delete, + reword or reorder a feature to make it pass. If one is wrong, add a new feature and say why in + `progress.md`. +- **Commit small.** One PR per feature, base `v4`, using `.github/pull_request_template.md`. +- **Log it.** Add a `progress.md` entry: what changed, what's next, and anything surprising. +- **Compact.** Compact or clear context at feature boundaries, never mid-feature. `progress.md` is the + hand-off. + +## Repo map +- `schema/`: JSON Schema, the only source of wire shapes. `schema/fixtures/` holds the examples that + every test loads. +- `gen/ts/` and `daemon/internal/protocol/gen.go`: generated by `make gen`. Never edit them by hand. +- `worker/`: the Worker, PodDO, MCP server and auth. `daemon/`: the Go daemon and ACP. +- `e2e/`: the slice tests (Go). `scripts/`: generators and checks. `docs/`: design and lessons. + +## Commands +- `make gen`: regenerate contracts after editing `schema/`. Commit both. +- `make preflight`: `check-gen`, `test` and `entropy` (budgets in `entropy.json`). CI runs the same. +- `make e2e`: boots the Worker locally and runs `go test ./e2e`. +- 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.** No `oneOf`/`anyOf`/`allOf`. A union is a `type` enum plus + `x-variants`. Cross-field rules are `x-rules`, enforced and tested in `worker/`. +- **Tokens.** An agent holds only a `cmr_` run token. It never sees `cmc_`, `cmu_` or a third-party + credential. The server stamps receipts and the actor; it never trusts them from input. +- **Push wakes only.** Nothing polls. A change that only touches unaddressed agents wakes nobody. +- **Merging.** Green in the merge queue merges. Protected paths also need a fresh-context review and + an Opus `GATE: PASS` at the PR head: `schema/`, `worker/src/auth`, `worker/src/mcp`, + `daemon/internal/{acp,run}`, `install.sh` and `.github/workflows`. +- **Stay small.** Files at most 400 lines, functions at most 60, LOC caps per area. A new dependency or + module gets a note in `docs/adr/`. Each report states lines added/removed and what it deleted. +- **Lessons.** A lesson lands in the same PR, as a test, a lint rule or a `docs/` note. Never only in + private memory. +- **Stage skills.** Plan with Pocock `tdd`, review with Pocock `code-review`, and retro with pstack + `reflect` at the end of a lane. Details: `docs/development/workflow.md`; + docs map: `docs/INDEX.md`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 000000000..47dc3e3d8 --- /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 000000000..b63d60800 --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +# 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 entropy 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 + +# Entropy gate (docs/development/workflow.md): budgets and ratchets in entropy.json, +# dead code (knip, staticcheck, deadcode), TS module boundaries (dependency-cruiser). +STATICCHECK := honnef.co/go/tools/cmd/staticcheck@v0.8.2 +DEADCODE := golang.org/x/tools/cmd/deadcode@v0.51.0 +entropy: node_modules + bun run check:entropy + bun run check:dead + bun run check:boundaries + go run $(STATICCHECK) ./... + @if [ -n "$$(go list -f '{{if eq .Name "main"}}{{.ImportPath}}{{end}}' ./...)" ]; then go run $(DEADCODE) -test ./...; fi + +preflight: check-gen test entropy diff --git a/bun.lock b/bun.lock new file mode 100644 index 000000000..c490e553b --- /dev/null +++ b/bun.lock @@ -0,0 +1,238 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "commonly-v4", + "devDependencies": { + "ajv": "8.17.1", + "dependency-cruiser": "18.6.0", + "knip": "6.41.0", + "typescript": "5.9.3", + }, + }, + }, + "packages": { + "@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || 2.0.0-beta.2", "@emnapi/runtime": "^1.7.1 || 2.0.0-beta.2" } }, "sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw=="], + + "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.153.0", "", { "os": "android", "cpu": "arm" }, "sha512-BcWu25dMwMjHpaDCEidmpAi7rGg1qZoihJeUUH9bD55pVZ0z4Wa5CQgqSTo0R41H81oozisQKW3WqvAN2m77IQ=="], + + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.153.0", "", { "os": "android", "cpu": "arm64" }, "sha512-9l5rdp2DhRqVriBHsNpu5cEf+hizZ4B58rt93HzyQEBUgZiC6qe2oFpaAAnXGCxHGnQExI1s7S2G0QhFacUZ6w=="], + + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.153.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EcpsyRXHhENDpekpG4QNaSZj2F/BAz4Gh4o89zZwNss86R8KyRSQ6sXbAj74Xyeb4z3j/yBZZ6joQZhraIWyfw=="], + + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.153.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-hlj80HUQy07/2lYp6rmxH+3u+H2ik3rjLR52CUdPApxmZCNxASDUf/+ykTzAHS/fehftmVMATKvPaWmuLHuasg=="], + + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.153.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fWRcv2pMSh1qtez46MOu8vuUJ5fiaQZPHSphbWulbL7SHBMKfRgZl2PzjXZYQJbq7NEXyXbgIuIuUm+/Rmkv8w=="], + + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.153.0", "", { "os": "linux", "cpu": "arm" }, "sha512-paZeUK1GXShl/bOJQET1/8AQxayFeuN4SIWAkgTRxE7ZQwv9uYSB2n0648smygP22N7jcI3zxAm0Z+ALNiIKxg=="], + + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.153.0", "", { "os": "linux", "cpu": "arm" }, "sha512-qFPp5NwMf4K1C0O+DCVGmcbOaYyYAq98Rvv9p4VSfb777iUwlJCqMUxf6QG9N5f5ls4TEX6LitOZHi+6GL6rYQ=="], + + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.153.0", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-oufEhZCX7saeo27Logra8cvBR1jQ/yw2zUI2hHmWw3qA/XYqEW31FOvLQBv7DWN32NjVdlcaP11WBBZTJo4k9g=="], + + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.153.0", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-zXYzuAqng85h6+LI3iRm0zeMLX1/K2z41ZAMWxVRgt0D10VvZUaV5q+TEB46KDhoKBpdexoTLC6QoB/EiP2oqQ=="], + + "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.153.0", "", { "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-J7+NLwLEzdkKM9cKyPHH/AjXXstWOs98KYgLrPPnO4N9MO8EZRBfPNnBcS3S67uMOBkRpUmCl2FhRnWdQ5CbBA=="], + + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.153.0", "", { "os": "linux", "cpu": "none", "libc": "glibc" }, "sha512-sD0K11GQTLWylln54nPMMTtgmBbQrR5It/vMpZpy/fogiw8O2AvIE+9LuWUXddrWk9zvFIHGRcSgFn4IHOfoYg=="], + + "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.153.0", "", { "os": "linux", "cpu": "none", "libc": "musl" }, "sha512-7L66uql2BhvSz51LwfExjOdn6et/ydTiQxl23VApdKzJhTc3TczA1Qt6VR/UcvlQrL2ZbytIJb447vh7p5wmUw=="], + + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.153.0", "", { "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-hXV3T7mNyrPR19rbPYkglkQJtoM/UNo/xwbZeeqdAxEg8PXgb4LXXuxPpGgI4bzRRCWwLUxdDSS58xIyC2u1TA=="], + + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.153.0", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-Da8Vqs1xdtoDYtEjmozD7wGgJeLBQkMbR0WUBECOSxodyX5DXy8ySlvJWbukyO2Q0+TM9EK+4LT6tbq0jCg4hQ=="], + + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.153.0", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-zGAPxNTIIw6X5WyTRscIFE/oDjpdNtrrl32ing6NOXdGjzRucadnxBilexLdeYQ9YxixF93GujpXyQ0jw7ddSg=="], + + "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.153.0", "", { "os": "none", "cpu": "arm64" }, "sha512-GoHzpqP5XyPIp9DHokoEdCp+K4UPl/h1neFj1uXnUB3pJYyOQ7NOhFVOttNUgCGcj2eywAvu5cRivTOSq4S10g=="], + + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.153.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-xGcy12tO9k7zFbeyQzEuilfWfQpyRq23/fLPfN3wjSWuWv58k46fQT1hsO1KFNykZuVGW9xSJE/z592TmAoSDA=="], + + "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.153.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-o1ykUjWncvXX6N5a4KebgLjtD8zRui+f1llMhSGoSC9oP2KOqapRpdwmZ3NcNG0axFouwVKmhGBjd+IhA8BksA=="], + + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.153.0", "", { "os": "win32", "cpu": "x64" }, "sha512-gxQz7G96Up136fd+2zWrP+o6FYGs9NO4Dyv55IoeJFJUDwLFHcLYY11hhA5wQKs0HoM57Q8esQ3nEVhnCdsjAQ=="], + + "@oxc-project/types": ["@oxc-project/types@0.153.0", "", {}, "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw=="], + + "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA=="], + + "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.24.2", "", { "os": "android", "cpu": "arm64" }, "sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg=="], + + "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.24.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w=="], + + "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.24.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q=="], + + "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.24.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw=="], + + "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg=="], + + "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA=="], + + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.24.2", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA=="], + + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.24.2", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw=="], + + "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.24.2", "", { "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA=="], + + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.24.2", "", { "os": "linux", "cpu": "none", "libc": "glibc" }, "sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw=="], + + "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.24.2", "", { "os": "linux", "cpu": "none", "libc": "musl" }, "sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w=="], + + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.24.2", "", { "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ=="], + + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.24.2", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg=="], + + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.24.2", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw=="], + + "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.24.2", "", { "os": "none", "cpu": "arm64" }, "sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg=="], + + "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.24.2", "", { "dependencies": { "@emnapi/core": "1.11.2", "@emnapi/runtime": "1.11.2", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q=="], + + "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.24.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg=="], + + "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="], + + "acorn": ["acorn@8.19.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "acorn-jsx-walk": ["acorn-jsx-walk@2.0.0", "", {}, "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA=="], + + "acorn-loose": ["acorn-loose@8.5.2", "", { "dependencies": { "acorn": "^8.15.0" } }, "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A=="], + + "acorn-walk": ["acorn-walk@8.3.5", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw=="], + + "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=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], + + "dependency-cruiser": ["dependency-cruiser@18.6.0", "", { "dependencies": { "acorn": "8.19.0", "acorn-jsx": "5.3.2", "acorn-jsx-walk": "2.0.0", "acorn-loose": "8.5.2", "acorn-walk": "8.3.5", "commander": "15.0.0", "enhanced-resolve": "5.26.0", "ignore": "7.0.12", "interpret": "3.1.1", "is-installed-globally": "1.0.1", "json5": "2.2.3", "picomatch": "4.0.7", "prompts": "2.4.2", "rechoir": "0.8.0", "safe-regex": "2.1.1", "semver": "7.8.5", "tsconfig-paths-webpack-plugin": "4.2.0", "watskeburt": "6.0.0" }, "bin": { "depcruise": "bin/dependency-cruiser.mjs", "depcruise-fmt": "bin/depcruise-fmt.mjs", "dependency-cruise": "bin/dependency-cruiser.mjs", "depcruise-baseline": "bin/depcruise-baseline.mjs", "dependency-cruiser": "bin/dependency-cruiser.mjs", "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs" } }, "sha512-It7NW5AmCDBCLMq7I91Nfu8VRKzrIE6S+M/FMQzALDceJ0B1PWRHo77q9YJg/HwyBBEWHVtOuTptIQki+xecOw=="], + + "enhanced-resolve": ["enhanced-resolve@5.26.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.9", "", {}, "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ=="], + + "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "formatly": ["formatly@0.11.4", "", { "dependencies": { "fd-package-json": "^2.0.0", "package-manager-detector": "^1.8.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-TeCC10lP8KCgoX+Fe5lryILIT0/Dy+nP0OSVnsuB5CwXoLuOdqxCYlPFF1inR8S/zTy1ec7TjhS2TeRKWzzImA=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-tsconfig": ["get-tsconfig@4.14.3", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA=="], + + "global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "ignore": ["ignore@7.0.12", "", {}, "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg=="], + + "ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="], + + "interpret": ["interpret@3.1.1", "", {}, "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ=="], + + "is-core-module": ["is-core-module@2.17.0", "", { "dependencies": { "hasown": "^2.0.4" } }, "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA=="], + + "is-installed-globally": ["is-installed-globally@1.0.1", "", { "dependencies": { "global-directory": "^4.0.1", "is-path-inside": "^4.0.0" } }, "sha512-li88H4QGRoNMX27jEmY8nR515oJVspAgC/WFMlhHfFV8RJ4QyGpHhXnwAe9gH5RIgKmdYRUmrOhQobcgEjIdSw=="], + + "is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], + + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "knip": ["knip@6.41.0", "", { "dependencies": { "formatly": "^0.11.4", "get-tsconfig": "4.14.3", "jiti": "^2.7.0", "oxc-parser": "^0.153.0", "oxc-resolver": "11.24.2", "picomatch": "^4.0.7", "smol-toml": "^1.9.0", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.17", "unbash": "^5.0.0", "yaml": "^2.9.1", "zod": "^4.6.5" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-DrhQlIuV6xScd5Q+WTF1znd1w41QzxFGCtAHXCajsH/ndGMotHoNYbdXbuFtzW3mIOmniHtZ7zCtvnQkUmJtQQ=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "oxc-parser": ["oxc-parser@0.153.0", "", { "dependencies": { "@oxc-project/types": "^0.153.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.153.0", "@oxc-parser/binding-android-arm64": "0.153.0", "@oxc-parser/binding-darwin-arm64": "0.153.0", "@oxc-parser/binding-darwin-x64": "0.153.0", "@oxc-parser/binding-freebsd-x64": "0.153.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.153.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.153.0", "@oxc-parser/binding-linux-arm64-gnu": "0.153.0", "@oxc-parser/binding-linux-arm64-musl": "0.153.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.153.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.153.0", "@oxc-parser/binding-linux-riscv64-musl": "0.153.0", "@oxc-parser/binding-linux-s390x-gnu": "0.153.0", "@oxc-parser/binding-linux-x64-gnu": "0.153.0", "@oxc-parser/binding-linux-x64-musl": "0.153.0", "@oxc-parser/binding-openharmony-arm64": "0.153.0", "@oxc-parser/binding-win32-arm64-msvc": "0.153.0", "@oxc-parser/binding-win32-ia32-msvc": "0.153.0", "@oxc-parser/binding-win32-x64-msvc": "0.153.0" } }, "sha512-cEnpyyV1OJRXv++SFNw0z8i+2o/cr88rVLaOYJafELDoHM5yHvCAbwGRLvxuany6SNLcbblSMUyZaGzH+quSYg=="], + + "oxc-resolver": ["oxc-resolver@11.24.2", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.24.2", "@oxc-resolver/binding-android-arm64": "11.24.2", "@oxc-resolver/binding-darwin-arm64": "11.24.2", "@oxc-resolver/binding-darwin-x64": "11.24.2", "@oxc-resolver/binding-freebsd-x64": "11.24.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-musl": "11.24.2", "@oxc-resolver/binding-openharmony-arm64": "11.24.2", "@oxc-resolver/binding-wasm32-wasi": "11.24.2", "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" } }, "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw=="], + + "package-manager-detector": ["package-manager-detector@1.9.0", "", {}, "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + + "rechoir": ["rechoir@0.8.0", "", { "dependencies": { "resolve": "^1.20.0" } }, "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ=="], + + "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve": ["resolve@1.22.13", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.17.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "safe-regex": ["safe-regex@2.1.1", "", { "dependencies": { "regexp-tree": "~0.1.1" } }, "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "smol-toml": ["smol-toml@1.9.1", "", {}, "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], + + "tsconfig-paths-webpack-plugin": ["tsconfig-paths-webpack-plugin@4.2.0", "", { "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.7.0", "tapable": "^2.2.1", "tsconfig-paths": "^4.1.2" } }, "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "unbash": ["unbash@5.1.0", "", {}, "sha512-YVGyy4XqoZ2AVgHjVdL9cchMSurlXFHWviORa8RiU/Bnol4ooW5Lpyur/5XmbgDdyCKxymmgsEQsLGBMIJ/Agg=="], + + "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], + + "watskeburt": ["watskeburt@6.0.0", "", { "bin": { "watskeburt": "dist/run-cli.js" } }, "sha512-jfiuDABaxSkC71T6oZ3vCS99roYkSHm/+As+G0Dz8taAHQb+SJBvLEm5RlsgG71XdfAj3rv7eudUBTgwcQUPlQ=="], + + "yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="], + + "zod": ["zod@4.6.7", "", {}, "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="], + } +} diff --git a/daemon/internal/protocol/fixtures_test.go b/daemon/internal/protocol/fixtures_test.go new file mode 100644 index 000000000..39f87bff1 --- /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 000000000..66745bb49 --- /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/docs/INDEX.md b/docs/INDEX.md new file mode 100644 index 000000000..a1411d7c4 --- /dev/null +++ b/docs/INDEX.md @@ -0,0 +1,11 @@ +# Docs index + +Every file under `docs/` is listed here with one line. `make entropy` doesn't check this yet. A +per-wave unit compares this list with `ls docs/**` and fixes the drift. + +| File | What it is for | +|---|---| +| `development/workflow.md` | How v4 is built: the long-running harness, stage skills, entropy gate, review and merge | +| `development/verifier.md` | The verifier protocol: what a fresh-context reviewer checks, and its verdict format | +| `adr/0001-v4-dev-tooling.md` | Dev dependencies, the entropy budgets and the slice LOC caps | +| `skills/pstack-deslop/` | Vendored pstack deslop skill (MIT), used by the verifier and the removal-only unit | diff --git a/docs/adr/0001-v4-dev-tooling.md b/docs/adr/0001-v4-dev-tooling.md new file mode 100644 index 000000000..2934de95e --- /dev/null +++ b/docs/adr/0001-v4-dev-tooling.md @@ -0,0 +1,35 @@ +# 0001 · v4 dev tooling and its dependencies + +Status: Accepted (CTO, 2026-10-29). Rule: every new dependency or module gets a note like this one. + +Each dependency below is a dev-only dependency, pinned to an exact version at least two weeks old. +None of them ships in a runtime artifact. + +| Dependency | Why | Rejected alternative | +|---|---|---| +| Bun 1.4.4 | Package manager and script runner for the TS workspace (Cofounder ruling) | npm: slower installs, no TS script runner | +| Go 1.27.3 | The daemon's language; ACP's Go SDK | — | +| ajv 8.17.1 | Strict JSON Schema 2020-12 validation of fixtures | Hand-written checks drift from the schema | +| typescript 5.9.3 | Type-checks generated contracts; its AST powers the function-size check | A regex line counter misreads nested functions | +| knip 6.41.0 | Unused files, exports and dependencies on the TS side | ts-prune (unmaintained) | +| dependency-cruiser 18.6.0 | TS module boundaries and cycles | Hand-written import greps | +| staticcheck v0.8.2 (`go run`) | Go unused code and correctness lints | golangci-lint: one more binary to pin, more config | +| deadcode, x/tools v0.51.0 (`go run`) | Unreachable Go functions from `main`; starts enforcing when a main package exists | — | + +Go tools run through `go run pkg@version`. They resolve from the module cache, with no install, which +is the rule on the fleet Mac. + +The budgets they enforce live in `entropy.json`: a file is at most 400 lines and a function at most 60; +LOC per area is capped; exceptions only shrink unless the exception cites a note here. + +The LOC budget for the slice is a deliberate choice to stay small: + +| Area | Cap | +|---|---| +| core (`schema/`, `scripts/`) | 1,500 | +| worker | 4,000 | +| daemon | 3,500 | +| e2e | 1,500 | + +That is under 11k hand-written lines. Agent-written comparables grew without bounds: herdr reached +291k lines in seven months, and Tutti 1.6M (Cofounder's figures). Raising a cap takes a note here. diff --git a/docs/development/verifier.md b/docs/development/verifier.md new file mode 100644 index 000000000..eb964eaf2 --- /dev/null +++ b/docs/development/verifier.md @@ -0,0 +1,35 @@ +# Verifier protocol + +A verifier is a fresh-context subagent. It never sees the builder's reasoning. It gets four things: +- the PR number and head SHA; +- the feature (from `feature_list.json`) or the unit's brief; +- the repo at that head; +- this file. + +It runs on a different model from the builder. + +## What it does, in order +1. **Reproduce.** Check out the head and run `./init.sh`, then the feature's `test` command. A red + result ends the review with `verifier-failed`. +2. **Acceptance.** Walk each step of the feature or brief against what the test actually asserts. + A step that no assertion covers is a finding. +3. **Review.** Apply Pocock `code-review` to the diff against base `v4`. Correctness comes first; then + the contract (schema to generated code, server-stamped fields, token scope). +4. **Deslop.** Apply pstack `deslop` (`docs/skills/pstack-deslop/SKILL.md`, base `v4`). Look for dead code, + needless abstraction, comments that restate code, and over-defensive guards. +5. **Subtract.** Answer two questions: could the diff be smaller, and does it delete anything it made + obsolete? +6. **Entropy.** Report lines added and removed (`git diff --shortstat origin/v4...HEAD`) and the LOC + lines printed by `make entropy`. + +## Verdict +The last lines of the report are always these, in this order: +``` +LEDGER +FINDINGS +GATE: PASS | GATE: FAIL +``` +- `GATE: PASS` requires zero blocking findings. +- The verifier never pushes, comments on GitHub or merges. The coordinator records the ledger row and + posts the review. +- A protected-path PR still needs lumen's Opus `GATE: PASS` at the same head. diff --git a/docs/development/workflow.md b/docs/development/workflow.md new file mode 100644 index 000000000..8c215808f --- /dev/null +++ b/docs/development/workflow.md @@ -0,0 +1,71 @@ +# How v4 is built + +v4 is built by one long-running builder session, using the harness from Anthropic's +[Effective harnesses for long-running agents](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents). + +| File | Role | +|---|---| +| `feature_list.json` | Every slice feature, written as an end-to-end test. Only `passes` may change. | +| `init.sh` | Brings up the environment, runs the baseline and prints the scoreboard. | +| `progress.md` | The hand-off between sessions and across context compactions. | +| git log | The record. Small commits, one feature per PR. | + +## Why each rule exists +- **The feature list is JSON and tests are never edited.** The article found that models rewrite + Markdown checklists. Removing a test to make it pass is the failure that hides missing + functionality. +- **One feature at a time.** Long runs fail by declaring done too early or by half-finishing several + features. Taking the top unfinished feature every time prevents both. +- **The baseline runs first.** A session that builds on a broken tree compounds the break. + `init.sh` makes the state visible before any edit. +- **Verify at the API, not with unit tests.** The article saw agents mark features done on unit + tests or a single curl. Here a feature passes only on its `test` command. For the slice, that is + the Go e2e that drives the real Worker and daemon. Browser checks join when the client lands (F18). +- **Compact at feature boundaries.** `progress.md` plus git carry the state, so a fresh context can + resume without the transcript. + +## Stage skills +| Stage | Skill | When | +|---|---|---| +| Plan and build | Pocock `tdd` | Every feature: e2e red, then unit red/green/refactor inward | +| Review | Pocock `code-review`, run in a fresh-context subagent | Protected-path PRs, before the Opus gate | +| Retro | pstack `reflect` (coordinator only; vendored in the skills repo, `pstack/reflect`) | At the end of each lane, or after a surprise | +| Entropy | pstack `deslop` (`docs/skills/pstack-deslop/`) | Every verifier pass, and the removal-only unit each wave | + +## Review and merge +- Green in the merge queue merges. +- A PR touching a protected path also needs two reviews before it merges: + - one fresh-context review: a subagent given the diff and the feature, not the builder's reasoning; + - an Opus `GATE: PASS` at the PR head (lumen). +- Protected paths: `schema/`, `worker/src/auth`, `worker/src/mcp`, `daemon/internal/{acp,run}`, + `install.sh` and `.github/workflows`. +- The press for `main` (D162) does not apply to `v4`. + +## Design-gated features +- **F18 (client) and F19 (connectors)** stay blocked until their options docs are reviewed with Sam. + The docs are `docs/design/client-options.md` and `docs/design/connector-options.md`. +- **After the review:** + 1. add the steps and test to the existing F18 or F19 entry, and clear `blockedBy`; + 2. note in `progress.md` that the review set them. + + This is the only time a feature's text changes, and only for these placeholders. + +## Entropy gate +Maintainability is a gate in the loop, not a cleanup phase. `make preflight` runs `make entropy`, and +so does CI. +- **Budgets** live in `entropy.json`: + - each file at most 400 lines and each function at most 60; + - a LOC cap per area; + - `TODO` only with a row id, like `TODO(F07)`; + - Go import boundaries; + - exceptions only shrink, unless a new one cites a `docs/adr/` note. +- **Dead code:** knip (TS), staticcheck and deadcode (Go). +- **Boundaries:** dependency-cruiser (TS). +- **Every unit report** states its lines added and removed, and answers the subtract-before-you-add + question: what did this change make obsolete, and was it deleted? +- **New dependencies:** a new dependency or module gets a short note in `docs/adr/`. +- **Each wave** includes one removal-only unit: + - `deslop` plus Pocock `codebase-design`, with no new behaviour; + - a `docs/INDEX.md` drift check; + - quarantining any flaky test, with a fix row. +- **Next, once the slice is green:** jscpd duplication and a full ratchet report. diff --git a/docs/skills/pstack-deslop/LICENSE b/docs/skills/pstack-deslop/LICENSE new file mode 100644 index 000000000..3ebb69c22 --- /dev/null +++ b/docs/skills/pstack-deslop/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2026 Lauren Tan +Copyright (c) 2026 Michael Denyer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/skills/pstack-deslop/README.md b/docs/skills/pstack-deslop/README.md new file mode 100644 index 000000000..a44e72bfc --- /dev/null +++ b/docs/skills/pstack-deslop/README.md @@ -0,0 +1,3 @@ +# pstack deslop (vendored) + +Upstream: https://github.com/michael-denyer/pstack-claude at `190fc9a009491cbcfc04e25ef5b3c2d1fcf5ca8a`, `plugins/pstack/skills/deslop`, MIT (LICENSE beside this file). Copied verbatim. On v4 the diff base is `v4`, not `main`. To update, re-copy from a new pinned commit in its own PR. diff --git a/docs/skills/pstack-deslop/SKILL.md b/docs/skills/pstack-deslop/SKILL.md new file mode 100644 index 000000000..1b3827c7f --- /dev/null +++ b/docs/skills/pstack-deslop/SKILL.md @@ -0,0 +1,22 @@ +--- +name: deslop +description: Remove AI-generated code slop and clean up code style +--- + +# Remove AI code slop + +Check the diff against main and remove AI-generated slop introduced in the branch. + +## Focus Areas + +- Extra comments that are unnecessary or inconsistent with local style +- Defensive checks or try/catch blocks that are abnormal for trusted code paths +- Casts to `any` used only to bypass type issues +- Deeply nested code that should be simplified with early returns +- Other patterns inconsistent with the file and surrounding codebase + +## Guardrails + +- Keep behavior unchanged unless fixing a clear bug. +- Prefer minimal, focused edits over broad rewrites. +- Keep the final summary concise (1-3 sentences). diff --git a/entropy.json b/entropy.json new file mode 100644 index 000000000..4e230d76a --- /dev/null +++ b/entropy.json @@ -0,0 +1,53 @@ +{ + "$comment": "Entropy budgets enforced by `make entropy` (scripts/check-entropy.mjs). Exceptions may only shrink: a new one must cite a docs/adr/ note in its reason. LOC budgets cover hand-written code only; generated files and fixtures are excluded.", + "maxFileLines": 400, + "maxFunctionLines": 60, + "generated": [ + "gen/", + "daemon/internal/protocol/gen.go", + "schema/fixtures/", + "bun.lock", + "go.sum" + ], + "markerScanSkip": [ + "scripts/check-entropy.mjs" + ], + "locBudget": { + "core": { + "paths": [ + "schema/", + "scripts/" + ], + "max": 1500 + }, + "worker": { + "paths": [ + "worker/" + ], + "max": 4000 + }, + "daemon": { + "paths": [ + "daemon/" + ], + "max": 3500 + }, + "e2e": { + "paths": [ + "e2e/" + ], + "max": 1500 + } + }, + "goImports": { + "daemon/internal/protocol": { + "allow": [ + "std" + ] + } + }, + "exceptions": { + "files": {}, + "functions": {} + } +} diff --git a/feature_list.json b/feature_list.json new file mode 100644 index 000000000..21db50ab5 --- /dev/null +++ b/feature_list.json @@ -0,0 +1,257 @@ +{ + "$comment": "The v4 slice as end-to-end tests, in priority order. A building session changes ONLY the `passes` field, and sets it true only after `test` passes locally and in CI. Never delete, reword or reorder a feature to make it pass; a feature that turns out wrong gets a new feature plus a note in progress.md. `blockedBy` names what must happen before work starts. See docs/development/workflow.md.", + "features": [ + { + "id": "F01", + "category": "contract", + "description": "One schema generates the TS and Go contracts, and every fixture validates in both", + "steps": [ + "make gen twice leaves the tree clean", + "check-fixtures passes all fixtures with ajv", + "the Go round-trip test passes and rejects every invalid-required fixture", + "a schema edit without make gen fails check-gen" + ], + "test": "make check-gen test", + "passes": true + }, + { + "id": "F02", + "category": "harness", + "description": "make e2e boots wrangler dev on a free port, waits for /healthz, runs the named e2e tests and prints E2E_MS", + "steps": [ + "make e2e starts the Worker with --persist-to a temp dir", + "GET /healthz returns 200 within 10 s", + "a failing step names itself and prints the last 20 events", + "the run ends with E2E_MS= STEPS=/" + ], + "test": "go test ./e2e -run TestF02", + "passes": false + }, + { + "id": "F03", + "category": "pod", + "description": "A test user creates a pod and receives hello over the pod WebSocket", + "steps": [ + "POST /__test/seed mints a user and returns a cmu_ token (local and preview builds only)", + "POST /v1/pods with that token creates a pod and returns its id", + "GET /v1/pods/:pod/ws with the token upgrades and the first frame is hello with headSeq 0 and the user as owner" + ], + "test": "go test ./e2e -run TestF03", + "passes": false + }, + { + "id": "F04", + "category": "pod", + "description": "Human messages fan out live, and a reconnecting client resumes from sinceSeq with no gaps", + "steps": [ + "two members subscribe; one sends message.post", + "both receive message.created with the same seq", + "the second client disconnects, three more posts happen, it reconnects with subscribe{sinceSeq}", + "it receives exactly the three missed events, in order", + "the same holds after the Durable Object hibernates" + ], + "test": "go test ./e2e -run TestF04", + "passes": false + }, + { + "id": "F05", + "category": "pod", + "description": "Invites add members; used, expired and non-member cases are refused", + "steps": [ + "the owner creates an invite and a second user accepts it", + "the second user's hello lists them as a member", + "accepting the same single-use invite again returns 410", + "a non-member creating an invite gets 403" + ], + "test": "go test ./e2e -run TestF05", + "passes": false + }, + { + "id": "F06", + "category": "board", + "description": "A member creates a task assigned to an agent and every member sees task.created", + "steps": [ + "a registered agent exists in the pod", + "a member sends task.create with the agent as assignee", + "all subscribers receive task.created with status open and the assignee set" + ], + "test": "go test ./e2e -run TestF06", + "passes": false + }, + { + "id": "F07", + "category": "wake", + "description": "Only addressed changes wake an agent, wakes coalesce, and unacked wakes are re-sent", + "steps": [ + "an ambient human message produces no run.wake", + "assigning three tasks to the agent in a row produces one pending wake, delivered once", + "a wake not acked within 60 s is re-sent on the next heartbeat (clock is test-controlled)", + "run.wake goes only to the computer that hosts the agent" + ], + "test": "go test ./e2e -run TestF07", + "passes": false + }, + { + "id": "F08", + "category": "daemon", + "description": "The daemon registers its computer and an agent, and the pod shows the agent online", + "steps": [ + "commonly daemon start --foreground registers the computer and stores cmc_ at mode 0600", + "commonly agent add fake registers an agent in the pod", + "members receive presence.changed online for the agent within 5 s", + "killing the daemon produces presence.changed offline within 45 s" + ], + "test": "go test ./e2e -run TestF08", + "passes": false + }, + { + "id": "F09", + "category": "run", + "description": "On run.wake the daemon claims a run and starts the agent with a run token and the pod MCP server", + "steps": [ + "the daemon receives run.wake and calls POST /v1/runs", + "the response carries runId, a cmr_ token, a brief and the MCP URL", + "the fake ACP agent starts with the MCP server passed in session/new", + "commonly_whoami over MCP returns the agent, pod and run", + "the agent's argv and env contain cmr_ and never cmc_" + ], + "test": "go test ./e2e -run TestF09", + "passes": false + }, + { + "id": "F10", + "category": "board", + "description": "commonly_task_claim is exclusive and its lease renews on every tool call", + "steps": [ + "two runs claim the same task concurrently; exactly one wins", + "the loser gets already_claimed naming the holder", + "members receive task.claimed with leaseUntil", + "a later tool call on the winning run moves leaseUntil forward" + ], + "test": "go test ./e2e -run TestF10", + "passes": false + }, + { + "id": "F11", + "category": "cards", + "description": "commonly_post enforces the headline contract and the pod stamps the receipt", + "steps": [ + "a post without summary gets summary_required; one over 120 chars gets invalid_input", + "an ask without options, or a non-ask with needs, gets invalid_input", + "a post carrying receipt is rejected", + "a valid ask creates a message whose receipt has the runId, harness fake, wakeReason and toolCalls equal to the MCP calls made so far", + "the ask carries an open decision" + ], + "test": "go test ./e2e -run TestF11", + "passes": false + }, + { + "id": "F12", + "category": "cards", + "description": "A human rules on an ask once, and the agent is woken with the ruling and finishes the task", + "steps": [ + "a member sends decision.rule with an option id", + "members receive message.updated with the decision ruled and frozen", + "a second ruling returns already_ruled and changes nothing", + "the agent receives run.wake with reason decision_ruled", + "the agent calls commonly_task_update done and members receive task.updated done" + ], + "test": "go test ./e2e -run TestF12", + "passes": false + }, + { + "id": "F13", + "category": "security", + "description": "Tokens are scoped: a run cannot cross pods, an ended run is dead, and test routes are absent in production", + "steps": [ + "a cmr_ for pod A posting to pod B gets forbidden", + "after POST /v1/runs/:run/end the same cmr_ gets unauthorized", + "an actor field in MCP tool arguments is ignored", + "the production build of the Worker contains no /__test handler" + ], + "test": "go test ./e2e -run TestF13", + "passes": false + }, + { + "id": "F14", + "category": "slice", + "description": "The whole slice runs end to end with the fake agent in under 60 s", + "steps": [ + "sign in, create pod, invite, join", + "start the daemon and add the fake agent", + "create a task assigned to it", + "observe run.wake, task.claimed, then an ask with a receipt", + "rule on the ask, observe the second wake and task.updated done", + "E2E_MS under 60000" + ], + "test": "go test ./e2e -run TestF14", + "passes": false + }, + { + "id": "F15", + "category": "auth", + "description": "commonly login signs in with GitHub's device flow and stores a user token safely", + "steps": [ + "against a stub GitHub, commonly login prints the user code and verification URL", + "after the stub approves, the CLI exchanges the GitHub token at POST /v1/auth/github for a cmu_", + "the token file is mode 0600 and the token is never printed", + "a revoked or invalid GitHub token gets unauthorized" + ], + "test": "go test ./e2e -run TestF15", + "passes": false + }, + { + "id": "F16", + "category": "harness-real", + "description": "A recorded real Claude Code session (claude-agent-acp) replays through the daemon and makes the slice's pod calls", + "steps": [ + "daemon/testdata/acp/claude-slice.jsonl is recorded against the pinned adapter, scrubbed", + "the replay test drives the daemon with it and the pod calls match the golden list", + "one operator walks the slice by hand with real Claude Code and the result is noted in progress.md" + ], + "test": "go test ./daemon/... -run TestReplayClaude", + "passes": false + }, + { + "id": "F17", + "category": "preview", + "description": "Every PR deploys a Workers preview and F14 passes against it", + "steps": [ + "the preview job deploys the PR to its own preview with its own DO namespace and the preview D1", + "COMMONLY_URL= go test ./e2e -run TestF14 passes" + ], + "test": "CI job e2e-preview", + "blockedBy": "CLOUDFLARE_API_TOKEN secret (owner action)", + "passes": false + }, + { + "id": "F18", + "category": "client", + "description": "A person uses the slice from the client chosen in the client options review", + "steps": ["defined after docs/design/client-options.md is reviewed with Sam"], + "test": "TBD by the review", + "blockedBy": "client options review (docs/design/client-options.md)", + "passes": false + }, + { + "id": "F19", + "category": "connector", + "description": "An agent reads Linear through a pod grant without ever holding the Linear token", + "steps": ["defined after docs/design/connector-options.md is reviewed with Sam"], + "test": "TBD by the review", + "blockedBy": "connector options review (docs/design/connector-options.md)", + "passes": false + }, + { + "id": "F20", + "category": "harness-real", + "description": "A recorded real Codex session (codex-acp) replays through the daemon and makes the slice's pod calls", + "steps": [ + "daemon/testdata/acp/codex-slice.jsonl is recorded against the pinned adapter, scrubbed", + "the replay test passes" + ], + "test": "go test ./daemon/... -run TestReplayCodex", + "passes": false + } + ] +} diff --git a/gen/ts/protocol.ts b/gen/ts/protocol.ts new file mode 100644 index 000000000..38809f756 --- /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 000000000..0ee916f55 --- /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 000000000..10836ac84 --- /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 000000000..dfe5db123 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/Team-Commonly/commonly + +go 1.27 diff --git a/init.sh b/init.sh new file mode 100755 index 000000000..814ec2144 --- /dev/null +++ b/init.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# One command: dependencies, the baseline, and the feature scoreboard. +# Every session runs this first (AGENTS.md). A red baseline is fixed before new work. +set -euo pipefail +cd "$(dirname "$0")" +export PATH="$HOME/.commonly/toolchains/v4/bin:$PATH" + +for tool in go bun; do + command -v "$tool" >/dev/null || { echo "missing $tool: see AGENTS.md (Commands)"; exit 1; } +done + +echo "== baseline: make preflight" +make preflight + +if [ -d worker ] && [ -f e2e/go.mod -o -d e2e ]; then + echo "== baseline: make e2e" + make e2e +fi + +echo "== features" +bun -e ' +const { features } = JSON.parse(await Bun.file("feature_list.json").text()); +const pass = features.filter(f => f.passes).length; +for (const f of features) { + const mark = f.passes ? "pass" : f.blockedBy ? "blocked" : "todo"; + console.log(`${f.id} ${mark.padEnd(7)} ${f.description}`); +} +const next = features.find(f => !f.passes && !f.blockedBy); +console.log(`\n${pass}/${features.length} passing. Next: ${next ? next.id + " " + next.description : "none unblocked"}`); +' diff --git a/knip.json b/knip.json new file mode 100644 index 000000000..7fc8804df --- /dev/null +++ b/knip.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + "project": [ + "scripts/**/*.mjs", + "worker/**/*.ts", + "e2e/**/*.ts" + ], + "ignoreBinaries": [ + "go" + ] +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..15e99b0c5 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "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", + "check:entropy": "bun scripts/check-entropy.mjs", + "check:dead": "knip", + "check:boundaries": "depcruise scripts gen --config .dependency-cruiser.cjs" + }, + "devDependencies": { + "ajv": "8.17.1", + "dependency-cruiser": "18.6.0", + "knip": "6.41.0", + "typescript": "5.9.3" + } +} diff --git a/progress.md b/progress.md new file mode 100644 index 000000000..2ef1baacf --- /dev/null +++ b/progress.md @@ -0,0 +1,18 @@ +# v4 progress + +Newest entry first. Each entry has four parts: what changed, the evidence (a test command and its +result, or the PR), what's next, and anything surprising. Keep each entry under 15 lines; the detail +goes in the PR. + +## 2026-10-29 · initializer (CTO session) +- **Changed:** set up the long-running harness. `feature_list.json` holds 20 slice features as e2e + tests (F01 passes). Also added `init.sh`, this file, the `AGENTS.md` loop, the PR template and + `docs/development/workflow.md`. +- **Plan:** one builder session works top-down through the list. Two design docs run alongside as + subagents (connectors, OSS-first; client options). F18 and F19 are blocked until Sam reviews those + docs. +- **Evidence:** F01 is `make check-gen test`, green on #2977 (L0). +- **Next:** F02, the `make e2e` harness (wrangler dev, /healthz, E2E_MS). +- **Surprising:** the fleet's husky pre-commit hook doesn't apply on the v4 branch. Commit with + `-c core.hooksPath=/dev/null` until v4 has hooks of its own. F17 needs a Cloudflare API token + secret, an owner action. diff --git a/schema/common.json b/schema/common.json new file mode 100644 index 000000000..6c0e876e3 --- /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 000000000..bbd0e8916 --- /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 000000000..934e15f12 --- /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 000000000..43258cd3f --- /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 000000000..b6a351364 --- /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 000000000..7603bfc49 --- /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 000000000..a4a0eb58b --- /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 000000000..4a0ebfc8c --- /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 000000000..c5fb1b18e --- /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 000000000..5dff093dc --- /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 000000000..e18ce9e47 --- /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 000000000..7aaba4c30 --- /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 000000000..68c3f3041 --- /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 000000000..91ec904db --- /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 000000000..861656ae8 --- /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 000000000..41d875cd8 --- /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 000000000..ad2cbde5c --- /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 000000000..6ee3af3d8 --- /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 000000000..3dd0859a9 --- /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 000000000..271859fff --- /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 000000000..003dada8c --- /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 000000000..ebf76123f --- /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 000000000..8e419495c --- /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 000000000..e9669810e --- /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 000000000..7917403c9 --- /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 000000000..185c0a0b9 --- /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 000000000..6ab8d13b3 --- /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 000000000..19d0d4576 --- /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 000000000..fb31700c5 --- /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 000000000..456ac0132 --- /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 000000000..ccf2c3cd9 --- /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 000000000..72bc91c3f --- /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 000000000..7534a23a5 --- /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 000000000..7ed1bb483 --- /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 000000000..4a73398fb --- /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 000000000..c1d849d8c --- /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 000000000..44152f93c --- /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 000000000..c3715a2e3 --- /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 000000000..0d5e7539f --- /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 000000000..d46f05d58 --- /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 000000000..27662b5ea --- /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 000000000..70fe2948f --- /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 000000000..05dd11ac6 --- /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 000000000..5e435e9cb --- /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 000000000..49301aada --- /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 000000000..b0d73ea3d --- /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 000000000..d5bd99de0 --- /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 000000000..9094a76b9 --- /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 000000000..4fbad8e6a --- /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 000000000..fa3c23c93 --- /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 000000000..2ef52c1c9 --- /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 000000000..35a738cb1 --- /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 000000000..b677a9b4f --- /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 000000000..7c2518806 --- /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 000000000..d000c1ab5 --- /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 000000000..7664cfe28 --- /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 000000000..06f402287 --- /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 000000000..f57510280 --- /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 000000000..4977bd13d --- /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 000000000..ee713acea --- /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 000000000..f156f5262 --- /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 000000000..46812dd46 --- /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 000000000..a5e7adde3 --- /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 000000000..58adde22e --- /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 000000000..6f248e797 --- /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 000000000..01db2a206 --- /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 000000000..f4ee06c18 --- /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 000000000..0967ef424 --- /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 000000000..18e041227 --- /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 000000000..4328fcc95 --- /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 000000000..7ec0fc56b --- /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 000000000..0967ef424 --- /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 000000000..b5abc44c1 --- /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 000000000..3286165b6 --- /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 000000000..d52fecfb1 --- /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 000000000..8c558414f --- /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 000000000..de5921d44 --- /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 000000000..359656e11 --- /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-entropy.mjs b/scripts/check-entropy.mjs new file mode 100644 index 000000000..c150dd9f3 --- /dev/null +++ b/scripts/check-entropy.mjs @@ -0,0 +1,91 @@ +// Entropy gate (docs/development/workflow.md): size budgets, TODO ids, LOC budgets, Go import +// boundaries, and the exceptions ratchet. Budgets live in entropy.json. +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import ts from 'typescript'; + +const cfg = JSON.parse(readFileSync('entropy.json', 'utf8')); +const errors = []; +const isGenerated = (f) => cfg.generated.some((g) => f === g || f.startsWith(g)); +const files = execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard'], { encoding: 'utf8' }) + .split('\n').filter((f) => /\.(ts|tsx|mjs|js|go)$/.test(f) && !isGenerated(f)); + +function tsFunctions(file, text) { + const src = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true); + const out = []; + const visit = (node) => { + if (ts.isFunctionLike(node) && 'body' in node && node.body) { + const name = node.name?.getText(src) + ?? (ts.isVariableDeclaration(node.parent) ? node.parent.name.getText(src) : ''); + const start = src.getLineAndCharacterOfPosition(node.getStart(src)).line; + const end = src.getLineAndCharacterOfPosition(node.getEnd()).line; + out.push({ name, lines: end - start + 1 }); + } + ts.forEachChild(node, visit); + }; + visit(src); + return out; +} + +// gofmt puts every top-level func's opening line and closing brace at column 0. +function goFunctions(text) { + const out = []; + let open = null; + text.split('\n').forEach((line, i) => { + const m = /^func (?:\([^)]*\) )?([A-Za-z0-9_]+)/.exec(line); + if (m && !line.endsWith('}')) open = { name: m[1], start: i }; + else if (open && line === '}') { out.push({ name: open.name, lines: i - open.start + 1 }); open = null; } + }); + return out; +} + +const loc = Object.fromEntries(Object.keys(cfg.locBudget).map((k) => [k, 0])); +for (const file of files) { + const text = readFileSync(file, 'utf8'); + const lines = text.split('\n').length; + if (lines > cfg.maxFileLines && !cfg.exceptions.files[file]) errors.push(`${file}: ${lines} lines > ${cfg.maxFileLines}`); + const fns = file.endsWith('.go') ? goFunctions(text) : tsFunctions(file, text); + for (const fn of fns) { + if (fn.lines > cfg.maxFunctionLines && !cfg.exceptions.functions[`${file}:${fn.name}`]) { + errors.push(`${file}:${fn.name}: ${fn.lines} lines > ${cfg.maxFunctionLines}`); + } + } + if (!cfg.markerScanSkip.includes(file)) text.split("\n").forEach((line, i) => { + if (/\b(TODO|FIXME|XXX)\b/.test(line) && !/\b(TODO|FIXME|XXX)\((F\d+|TASK-\d+)\)/.test(line)) { + errors.push(`${file}:${i + 1}: TODO needs a row id, e.g. TODO(F07)`); + } + }); + for (const [area, b] of Object.entries(cfg.locBudget)) if (b.paths.some((p) => file.startsWith(p))) loc[area] += lines; +} +for (const [area, b] of Object.entries(cfg.locBudget)) { + console.log(`LOC ${area.padEnd(6)} ${String(loc[area]).padStart(5)} / ${b.max}`); + if (loc[area] > b.max) errors.push(`LOC budget: ${area} ${loc[area]} > ${b.max}`); +} + +if (files.some((f) => f.endsWith('.go'))) { + const mod = execFileSync('go', ['list', '-m'], { encoding: 'utf8' }).trim(); + const pkgs = execFileSync('go', ['list', '-f', '{{.ImportPath}} {{join .Imports " "}}', './...'], { encoding: 'utf8' }); + for (const row of pkgs.trim().split('\n')) { + const [pkg, ...imports] = row.split(' ').filter(Boolean); + const rule = cfg.goImports[pkg.replace(`${mod}/`, '')]; + if (!rule) continue; + for (const imp of imports) { + const std = !imp.split('/')[0].includes('.'); + const ok = (std && rule.allow.includes('std')) || rule.allow.some((a) => imp === `${mod}/${a}`); + if (!ok) errors.push(`import boundary: ${pkg} may not import ${imp}`); + } + } +} + +// Ratchet: exceptions only shrink, unless the new one cites an ADR note. +try { + const base = JSON.parse(execFileSync('git', ['show', 'origin/v4:entropy.json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })); + for (const kind of ['files', 'functions']) { + for (const [key, reason] of Object.entries(cfg.exceptions[kind])) { + if (!(key in base.exceptions[kind]) && !reason.includes('docs/adr/')) errors.push(`new ${kind} exception ${key} must cite a docs/adr/ note`); + } + } +} catch { /* no entropy.json on origin/v4 yet */ } + +if (errors.length) { console.error(errors.join('\n')); process.exit(1); } +console.log(`entropy ok: ${files.length} files`); diff --git a/scripts/check-fixtures.mjs b/scripts/check-fixtures.mjs new file mode 100644 index 000000000..c64dc4605 --- /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 000000000..d24e0ab00 --- /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 000000000..bf1e7ceda --- /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"] +}