Feature specs
The behavioral contract of Switchboard, versioned with the code. Every file in this directory states what a feature is supposed to do, why, and how to prove it still does — so at any git SHA, the validation criteria describe exactly the code at that SHA, and an agent (or human) can verify behavior for any moment in history.
Specs, not ledgers. A feature file holds behavior, criteria, and the kind of proof each criterion has. It never holds the proof itself: no receipts, dates, run links, incident narratives, or "validated on …" notes. Those are tracking: they change on every validation of a running installation, and a spec that carried them would conflict with every parallel change. Receipts are not in this repository at all — see rule 6.
The rules
- Same-PR updates. A PR that changes behavior updates the matching feature file in that PR — new criteria for new behavior, edits for changed behavior, deletions for removed behavior. A feature file that describes code that no longer exists is a bug.
- TDD from the spec. New behavior starts as validation criteria here, then failing tests, then implementation to green. Unit tests are the preferred proof; each criterion names its test (
file::test name). - Agent-runnable validation for the rest. Criteria that genuinely can't be unit-tested (live Slack flows, sandbox infrastructure, deploy-gated behavior) carry explicit
[agent]instructions an agent can execute — the exact commands/messages to run and the expected observable result. The instructions are timeless: they say how to prove it, never when it was last proven. - Grow and prune. Files are added when features ship and deleted when features are removed. History lives in git, not in dead prose.
- Link, don't duplicate. Feature files own behavioral expectations; the technical how lives in README.md (architecture, diagrams) and AGENTS.md (invariants, map). Each feature file links to its code and docs.
- Receipts are not in the repository. A spec says how a criterion is proven, never that it was: the result of running an
[agent]procedure against an installation (criterion number, date, deployed commit, evidence) belongs to whoever operates that installation — in their own tracker, append-only, never in a PR. The work behind a[gap], incident write-ups and comparison campaigns are tracker items too, not spec text. A PR that adds a receipt line to a feature file is a review finding.
Criterion labels
[unit]— proven by a named test in the suite (npm test).[agent]— proven by following the written validation instructions against a live deployment.[gap]— known-untested; a criterion we hold but haven't yet encoded. Gaps are work items, not decoration: a row is closed by a test or an[agent]procedure, or deleted when the criterion is dropped.
Nothing else goes on a criterion: no dates, no receipt links, no "observed live" prose. Proof status is read from the test suite and the operator's own records, not from the spec.
Bindings are checked
npm run specs:check (part of npm run verify) reads every spec and fails when a binding no longer holds:
- A
[unit]prooffile::describe::itmust name a test that exists — the file, then the title path.…or*is a wildcard; a bare::itcontinues the row'sfile;file::alone means the whole file. A test title with a parameter (it.eachwith%s, a template literal) matches at the parameter. A bare file name (worker.test.ts) is allowed when the spec's Tests header, or the repository, has exactly one. - Every path in the Code and Tests headers must exist. Those headers are what maps a changed file back to the spec that covers it.
Rename a test and the build is red until the spec changes with it. Adopting the check on a tree with stale references: npm run specs:check -- --update-baseline writes them to specs-check.baseline.json, and from then on the check fails on any reference not in that list and on any listed reference that has since been fixed, so the file only shrinks until it is deleted. This tree has no baseline: every reference binds. A bare truncation of a title (title for a test called title and more) is not a binding; npm run specs:check -- --fix rewrites such a reference to title… where that is the one match in the file and leaves anything ambiguous for a person.
Index
| Feature | What it covers |
|---|---|
| Browser Home | Complete authorized durable work discovery and the replacement browser presentation |
| Organization orchestration | Durable source-attributed admission, private streams and reconciliation contracts |
| routing-and-config.md | Directives, config layers, thread stickiness, permission gates, config commands, config awareness, custom instructions, durable runtime overrides (the OverridesBacking seam → the state Worker's ConfigDO in prod) |
| slack-channel.md | Triggers (mention/DM/follow-up), ack reaction, status cards, formatting, attachments |
| slack-context-tools.md | Bounded, requester-scoped Slack thread, channel, permalink and attached-file reads for the main agent |
| llm-output.md | Typed LLM output contract: per-datatype request/response modules (OutputType seam), markdown canonicalization at the answer boundary, raw+canonical in the run record, deterministic retry loop |
| run-visibility.md | Typed run-event stream (tool calls + redacted result summaries); live in-channel status card |
| live-view.md | External run page: per-run capability token + SSE stream while a run is live (in-memory registry), the same page served tokenless from run history once it has finished; Access-gated /runs index with an active-only default and ?all=1; the /runs "Scheduled" panel — schedule registry, next fire, last firing + run link |
| session-log.md | The session log: one Durable Object per thread and agent holding every run's transcript rows in order, each run a range of it (session on the ledger row and the record, broken after a detach), kept when a run finishes and dropped by the retention sweep when enabled once its last kept run is gone and no run is live on the thread; the resume read by range, the tail read within a byte budget, the per-session byte policy that replaces the oldest tool results with a marker, pi's compaction entry as a row rendered into a rebuilt session, and the external-content FTS5 index kept in step |
| run-history.md | Durable run records and the live-run ledger (lease + fencing token, the transcript the model saw stored whole per run, step records before tools run, reclaim across bot generations): every finished run kept for a retention window instead of evicted after 60 s — the node-free record contract + retention, the RunStore seam (in-memory / file / RunHistoryDO on the state Worker), the runHistory config, the post-reply write path, the RunsService read merge behind runs.* and the run page, friction ledger served from the store |
| run-metrics.md | One flat metrics point per finished run into a Workers Analytics Engine dataset: the point's positional contract (POINT_COLUMNS, pointOf), the emission rule (written after the store's commit, only when the row turned final), the provisional-over-final guard, the optional RUN_METRICS binding and the byte-identical off state |
| command-registry.md | One channel-agnostic command registry: every operator command registered once as a typed TS function (help, config.*, runs.*, friction.*, repo.*, memory.*, schedule.list, deploy.*, env.bootstrap), generic HTTP /api/<group>.<verb> / MCP <group>_<verb> / CLI / chat adapters with no per-command code, one action (<group>:read/write/exec) per command authorized against the adapter-resolved caller, auth→parse→handle→map, audit line, untrusted-content wrapper, one shared text renderer; no command starts a run |
| authorization.md | One authorization model: typed Actor (user / service / schedule / agent, namespaced ids, onBehalfOf intersection) resolved once per surface, actions = today's <group>:read|write|exec + agent:run / repo:use / memory:write, typed resources, a policy table of data rows over a CLOSED condition vocabulary (has-grant, member-of, is-self, owner-of, all-channels), authorize as the one decision (deny → not_found on point reads) and predicateFor pushed into stores for lists (predicate ⇔ authorize differential), channel visibility as membership on every surface (ChannelDirectory, unknown = not a member, runs stamped with channelVisibility), reflection write gating (private/DM never → org), the grants + restrict config blocks |
| load-harness.md | The npm run load harness: peak-concurrency baseline from the run store, N synthetic threads against a resident or the cold fleet, end-to-end runs through a bot on a scripted model, the Slack card path simulated over the real coalescer against the published limits, /healthz.process, the resident purge-bindings op; the pi driver's coding tasks and its review suite (merged pull requests of the checkout's repository reviewed under the read allowlist, verdicts recorded and posted nowhere) |
| thread-admission.md | One live run per thread: a follow-up during a run is steered into it at the next step or refused with a pointer, never a rival run; unconsumed follow-ups become a fresh turn |
| private-worker-io.md | Stable internal worker thread identity and durable private replies, progress and history without posting to Slack; the main agent's report handling and steering remain an explicit gap |
| delegated-outcomes.md | Typed Ship outcomes retained through settlement and private progress reads, with validated PR binding, replay protection and explicit gaps for provider results and automatic return |
| run-friction.md | Run-friction analyzer: pure deterministic diagnosis of delay causes from the run-event stream (slow/failed tools, retries, setup/install, wrap-up, budget hits, infra failures); at/infra/run_note stream extensions; read-only GET /runs/:id/friction + the CLI-only friction analyze |
| self-improvement.md | Self-improvement proposals: friction ledger of every run's diagnosis; pure cross-run clustering/ranking of recurring patterns (+ long_run cost-spike proxy); friction report / friction propose registry commands (chat, HTTP, MCP, CLI) that file labeled, marker-deduped GitHub issues with evidence + suggested fix — proposals only, human-gated |
| coding-checks.md | Task-directed baseline and verification commands, factual execution receipts and historical replay |
| first-coding-test.md | Legacy explicit startup-smoke policy, durable receipts and unknown-outcome holds |
| execution.md | Per-thread workspaces, sandbox timeouts (exit 124), heartbeat streaming, session recovery, GitHub identity |
| cold-publication.md | Runner-owned publication of a precreated Ship branch and cold checkpoint recovery |
| installer-repair.md | Default-off trusted Bot install policy and exact-head receipt shape; effectful repair remains held |
| costs.md | Costs dash: Access-gated GET /costs + JSON twin — per-day spend for a named group (Workers' DOs + container apps + Anthropic workspace) priced live from Cloudflare's billing datasets and the Anthropic Admin cost report |
| delivery.md | Delivery indicators: GET /delivery + JSON twin and delivery report on every surface — issue-to-merge time, first-pass CI, review rounds, the review agent's findings and the share resolved with no human edit, per week and per unit, computed from GitHub's own record of the merged pull requests and the run history, nothing written |
| resident-repos.md | Resident repo environments: auth scopes, atomic cap, lifecycle engine, thread data plane, bot-side selection, repo onboard/offboard/rebuild/list chat commands (fail-closed gate, --dry-run plans) |
| agent-general.md | The default agent: fast model, assistant toolset — GitHub repo reads + issue writes, URL reading, no workspace; redirects code/PR/web-research asks |
| github-tools.md | The github_* tools: repo reads (repos, tree, file, code search), Actions run triage (a run, its jobs, a job's log) and issue read/write over the App credential from the bot process, per-repo write gate, toolset enablement (assistant for general, reads for research/review) |
| depot-ci.md | Coding-only, repo-bound Depot CI workflow/log inspection and failed-only retry; organization token held at the Worker edge, never the bot or coding container |
| web-tools.md | web_fetch (SSRF-hardened URL reading, binary links as model-visible blocks) + web_search (Brave/Null seam); the research agent |
| public-hygiene.md | The public tree carries no company, person, tracker, plan-id, platform-id or incident-date imprint; a ratchet (hygiene:check) counts every hit per file and class and only shrinks, with lines allowed by name |
| agent-review.md | Code review agent |
| agent-coding.md | Coding agent (ships PRs) |
| pr-images.md | Explicit PNG publication for inline PR display, thread delivery, private-bucket isolation, retention and revocation |
| agent-ship.md | Ship pipeline (coding → review → fix to LGTM); the child contract and handoff; the plan runner's state machine over the coordinator's step returns; every request handed to the runner (a task, a plan, a resume at review) |
| main-agent-actions.md | Main agent status, steer and stop for one durable linked Ship unit |
| main-agent-start.md | Main agent starts one private Ship worker from a sourced fix request |
| pull-requests.md | Rebase ownership between a live pipeline runner and the operator's pulls rebase sweep |
| orchestration-plane.md | The orchestration plane's table: every live and recently ended run, every ship unit and every tracked pull request with its owner and health, as plane show on every surface and the Access-gated /plane panel; read-only over the stores that exist |
| agent-explore.md | The long, read-only investigation: a cold sandbox with the checkout on a read credential, two hours, web search, a claim table with commands and numbers, setsid -f for a job past the command cap, never a pull request; the first repo-cold preset |
| agent-conductor.md | A run that starts other runs — by agent:conductor or the router's compound form, whose brief names the parts: spawn_run through spawnChild() → dispatch() as the requesting user, each child in a thread of its own at depth 1; read children have a fan-out cap and inherit the parent's remaining clock, while exact-PR Ship units take their own lease, parentRunId on its record; read children start from the parent's text turns then their prompt (seed: parent); a typed linked-PR Ship batch may start one exact-PR write child per target without that seed, while other write children are refused before a thread opens; send_to_run steers a live child through the inbox a thread reply takes; await_runs waits for the children's ends within the parent's budget and hands each back as data, an interrupted child reported and never restarted; list_runs / get_run_status under the requester's runs:read predicate; the one toolset that holds a run tool |
| reading-diff.md | Review runs carry the change as a reviewer reads it: a review_artifact event with the full git diff or meat.dev's abridged reading diff — a config/env switch (git|meat|off), meat falling back to git, produced concurrently with the review — and the PR's description as data (TL;DR, the Tour's steps with their anchors), the object the coding run submitted for that head or the body parsed back |
| pr-description.md | The PR description as data: typed PrDescription — a capped map for the reader (TL;DR, why, at most seven linked pointers, feedback wanted, risk, verified) with decisions, validation criteria + proofs and agent notes collapsed under it — with one renderer per surface: GitHub markdown today (anchors rendered at the head sha, so a repush is a re-render) and its inverse parser (a body read back into the object, the previous Tour shape included); a checked-in PR body is the golden |
| distilled-diffs.md | Distilled diff digest: shapes the coding PR description and orients the review agent, which reads the code and never runs tests — CI does |
| agent-env-bootstrap.md | Materialize a downstream service's UAT env vars into the agent's execution environment from the operator's secrets vault via a read-only, UAT-vault-scoped service account: JSONC manifest, UAT-only allowlist, dry-run default, chmod-600 env file, buildAgentEnv integration hook |
| memory.md | Cross-session self-learning memory: MemoryStore seam, read path (keyword+recency scorer, hard budget, org-scoped advisory context block) and write path (post-reply async reflection on memory.model, validated + secret-redacted candidates, dedup/supersede on write, shutdown-drain tracked). Flag-gated OFF by default (byte-identical to memory-off). Durable store: the Memory Worker (deploy/cloudflare-memory/, one SQLite Durable Object per scope, FTS5) behind WorkerMemoryStore, sharing one engine with the in-process store. User scope: org + the requesting user's own records, audience-routed on write, isolated by construction. Repo + channel scopes: repo:owner/name / channel:slack:C… shared by everyone who runs there, audience-routed on write. Human controls: memory list [me|org|repo|channel] [--limit] [words] / memory forget <id> (own scope free, shared scopes admin-gated, other users unreachable; soft-delete). Per-scope cap: memory.maxRecordsPerScope (default 500), LRU soft-eviction on write in both stores. No open [gap] |
| skills.md | Skill loading: SkillStore seam (bundled + in-memory), seeded addyosmani lifecycle skills, list_skills/use_skill read-only tools, per-agent scoping, progressive-disclosure prompt block (names+descriptions in-prompt, bodies on demand). Durable user-uploaded store (PR2) is [gap] |
| account-link-consent.md | Disabled, loopback-only browser consent over verified Access/Slack proofs; protected POSTs, one-time callback, atomic receipt replay and isolated theme fixtures — no live linking |
| settings-page.md | The settings page: GET /settings as an adapter over the command registry — MCPs (mcp list as a table with add/connect/remove), Channels (the index of configured channels, one channel's scope as a form over `config show |
| web-chat.md | The home page: /threads and /threads/<conversation> as a chat over the browser channel adapter (record 0043) — POST /threads/<id>/send into dispatch() as the session's person, a conversation is the runs of one web: thread, the rail the viewer's threads across channels, an assistant turn is the run on the run page's own model over its stream, a hand-back fills the composer, a message during a live run steers it, one send/steer/stop control, the empty state's mark and chips derived from the viewer's runs, and the motion rules a test checks |
| mcp-tools.md | External MCP servers as agent tools: McpClient seam (Streamable-HTTP client + in-memory), the bridge (mcp__<server>__<tool>, untrusted-wrapped descriptions/results, sideEffectFree only under readOnlyHint, per-run call cap, one mcp.<server>.<tool> span per call), per-run discovery with an in-process cache and mcp_unavailable notes, the MCP prompt block; SSRF guard shared with web_fetch. Servers are Scope.mcpServers in three tiers (org defaults / channel / user, union with org winning a clash; only org may reach coding/review), written by mcp list|add|connect|show|remove on every surface; sealed credentials + one-time tickets beside the overrides on the ConfigDO; the Access-gated identity-bound connect page (verify-then-seal); the config-awareness MCP line. OAuth 2.1, ship-round tools, key rotation are [gap] |
| release-and-deploy.md | Production follows the release PR: release-please tags and publishes, CI runs deploy all --affected on the release commit — only the Workers whose inputs changed since the commit each one serves (import closure of its worker.ts, its dir, its production dependencies, the bot's image sources; tests/docs/CI inert; an unclassified path → the whole fleet, loudly); the release PR carries the plan as a sticky comment, every PR's CI shows what its diff would deploy; manual deploys are the same workflow dispatched from main |
| harness.md | The harness contract: the six clauses — credential, gate, relay, record, conversation, survival — any process that runs a model loop for a run is held to; the Harness object the run loop is handed and calls, never a word compared; the row's facts as a union keyed on the harness's name, read by the harness that wrote them and refused by every other, with the relaunch count the loop keeps; pi as the one implementation; the container seam any harness's process is driven through (the program, its arguments, its stdout filter, its layout and a port as the start's inputs; an HTTP request into the container over loopback); the roster and the configuration word harness.<preset> — pi by default, the row's word winning on a resume — and the opencode block; run_meta.harness; the model proxy's two dialects as the credential clause's roster; OpenCode as the second implementation |
| harness-pi.md | The first implementation of the harness contract, and the harness every preset runs on unless its configuration word says otherwise: pi 0.85.1 in every image; pi started in the run's container as the thread's user with the run bearer as its only key (ExecOptions.env), or as a child of the bot over loopback for a preset without a workspace, driven over its RPC protocol through the HarnessContainer seam; every pi event bridged onto the run's own stream in the vocabulary the native loop wrote; the extension relaying our tools to the bot and asking its gate before every call; the transcript mirrored onto the ledger and the two ways back after a restart; the budgets, the wrap-up and the forced write-up; the two post-turns on the run's own session; pi's model library inside the bot as the provider layer for the two calls made outside a run — the router's and reflection's; the harness: key's default and the source scan that keeps the native loop deleted and the roster in the wiring |
| model-proxy.md | The per-run model-credential proxy: POST /v1/messages and POST /v1/chat/completions on the bot, a run bearer minted as the executor is provisioned and revoked at the run's end, the request pinned to the preset's model and caps, the turn guard enforced as a typed run event, every proxied call one model.turn span with the runner's attrs, the real key never leaving the bot; the operator's probe bearer under deploy:write |
| tracing.md | One measurement primitive (a span) for every unit of work from message receipt to the first reply, the partition that turns spans into a seven-term timeline that always sums to the window, the clock ratchet, the one duration formatter, and the span log |
| capabilities.md | The product adapts to what is on: one Capabilities value computed once at startup from config + env (execution, residents, memory, run history, ledger, MCP, costs, schedules, GitHub, ingress, dashboard auth, docs) and read by every surface; a command's enabledWhen hides it everywhere instead of answering unavailable; three installations as real configurations (minimal, local-full, cloud-full) with every surface snapshotted under each, and the conformance suite's capability axis |
| specs-coverage.md | specs:coverage: a change's paths mapped to the specs whose Code/Tests headers cover them (a header path covers its subtree), the touched-spec list the review agent reads, and the changed source paths no spec claims — warn-only until --require joins the gate; --test-guard: a test removed or narrowed only with a change to a spec that covers it, in two classes (removed: fails, check: is for the reviewer) |
| docs-site.md | The docs/ tree compiled and hosted: one markdown source that renders on GitHub and builds to a searchable static site (relative .md links rewritten, README.md as each directory's index, plans/ unpublished, dead links fail the build, mermaid on both surfaces); the mechanical reference tables generated from the command registry into marked regions with docs:check as the drift gate; the public /docs redirect and the dashboard header's docs link; the assets-only docs Worker — the project's website, its name and host from project.json, deployed by the project's CI on docs changes and unable to disturb a run; never a Worker an installation runs |
| packaging.md | The npm package the release publishes (@coreplane/switchboard, the CLI bundled with the files it reads under dist/assets/ — and the bot, which start runs from any directory with no Docker): one resolver finds those files in a checkout, the image and the package; the smoke test installs the tarball and runs it; one release-please component moves the version; publish-npm publishes through trusted publishing, no token and no provenance (npm signs from GitHub-hosted runners alone); the curl front door checks Node and hands off to npx; what stays clone-only and why |
| init.md | switchboard init, the one-command installer (setup.init, CLI only): .env at mode 600 and config.yaml derived from the checked-in examples with the values given, deploy/profile.json + the Worker configs with --cloudflare, flags first and prompts only on a terminal, refusals as registry codes (conflict without --force, every incoherent flag at once), the config proven through the real loader and the capabilities it computes, the next commands verbatim, --dry-run with secrets masked; the image's entrypoint runs the CLI when given arguments |