Skip to content

Feature specs

The behavioral contract of OpenSwitchboard, 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

  1. 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.
  2. 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).
  3. 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.
  4. Grow and prune. Files are added when features ship and deleted when features are removed. History lives in git, not in dead prose.
  5. 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.
  6. 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] proof file::describe::it must name a test that exists — the file, then the title path. or * is a wildcard; a bare ::it continues the row's file; file:: alone means the whole file. A test title with a parameter (it.each with %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

FeatureWhat it covers
routing-and-config.mdDirectives, 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.mdTriggers (mention/DM/follow-up), ack reaction, status cards, formatting, attachments
llm-output.mdTyped 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.mdTyped run-event stream (tool calls + redacted result summaries); live in-channel status card
live-view.mdExternal 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
run-history.mdDurable 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
command-registry.mdOne 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.mdOne 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
run-loop.mdTurn/time budgets, wrap-up behavior, forced write-up, refusal/truncation handling
load-harness.mdThe 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
thread-admission.mdOne 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
run-friction.mdRun-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.mdSelf-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
execution.mdPer-thread workspaces, sandbox timeouts (exit 124), heartbeat streaming, session recovery, GitHub identity
costs.mdCosts 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
resident-repos.mdResident 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.mdThe default agent: fast model, assistant toolset — GitHub repo reads + issue writes, URL reading, no workspace; redirects code/PR/web-research asks
github-tools.mdThe github_* tools: repo reads (repos, tree, file, code search) 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)
web-tools.mdweb_fetch (SSRF-hardened URL reading, binary links as model-visible blocks) + web_search (Brave/Null seam); the research agent
public-hygiene.mdThe 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.mdCode review agent
agent-coding.mdCoding agent (ships PRs)
agent-ship.mdShip pipeline (coding → review → fix to LGTM)
reading-diff.mdReview 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.mdThe PR description as data: typed PrDescription (TL;DR, what & why, hunk-anchored Tour, decisions, risks, validation criteria + proofs) 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); a checked-in PR body is the golden
distilled-diffs.mdDistilled 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.mdMaterialize 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.mdCross-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.mdSkill 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]
mcp-tools.mdExternal 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.mdProduction 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
tracing.mdOne 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.mdThe 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.mdspecs: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
docs-site.mdThe 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.mdThe 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.mdswitchboard 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