agentleFS
TentacleOperaTentacleOpera/switchboard/.agents/skills/switchboard-orchestration/SKILL.md

switchboard-orchestration

Skill215 starsChanged 2 months agoMITView on GitHub

The complete HTTP contract for driving Switchboard from outside the webview — for a fleet coding/review agent in a Mission Control worktree, an external Mission Control driving the whole board, or a team member reading the local board. LocalApiServer is the sole writer.

The file

We split this skill into the rules it gives an agent

12 rules. About 0 are common, 5 are distinctive, and 7 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈5

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 7

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Verify Switchboard is up before anything else:

    line 36
  2. Very specific:

    If switchboard api fails with server not running, Switchboard is not running — tell the user to open the workspace in VS Code with the …

    line 41
  3. Very specific:

    Auth: All endpoints sit behind the localhost boundary.

    line 45
  4. Very specific:

    Read verbs (get* / fetch* / load*) over the generic rail return only {success:true} — their data arrives on the WS hub, which an HTTP …

    line 227
  5. Distinctive:

    When you are a head agent driving a coder terminal (dispatch a subtask, get called back, review the diff, resend a fix) — not running an …

    line 256
  6. Distinctive:

    Never clear your own terminal.

    line 309
  7. Very specific:

    Never use ptyClearAllTerminals (it clears every active terminal, you included).

    line 310
  8. Distinctive:

    Read board state via these endpoints.

    line 395
  9. Distinctive:

    When an operator asks you to monitor a fleet until completion, three concrete signals define "done".

    line 401
  10. Distinctive:

    If the API server is down you can still communicate via the filesystem (Mission Control reads these):

    line 455
  11. Very specific:

    Every row on this channel is host-written.

    line 472
  12. Very specific:

    When the operator machine is reachable over Tailscale, the board and terminals are addressable at http://100.x.y.z:7777 (the Tailscale IP, …

    line 481

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Skill: Switchboard Orchestration HTTP Surface

This is the complete HTTP contract for driving Switchboard from outside the VS Code webview — whether you are a fleet coding/review agent working inside a Mission Control worktree, an external Mission Control (Cursor / Zed / Claude Code / Antigravity) driving the whole board, or a team member (a discoverable-skill agent) reading the local board.

Switchboard's LocalApiServer runs inside the VS Code extension and is the sole writer of kanban.db. You never touch the DB directly — you call these endpoints. The board is the source of truth; the UI is just one view of it.

Endpoint inventory. The hand-written list below is a subset for the calls agents most often make. The authoritative, generated inventory is GET /catalog — it lists every verb, endpoint, and prefix the server actually serves (drift-checked in CI via catalog:check), so it can never lag behind the server the way a hand-maintained list can. When you need a call this document does not cover, hit GET /catalog first; the payload contract for any call below still lives here, since the catalog carries only {path, method, prefix} and not request shapes.

Behavior vs. invocation. This skill is the invocation authority (endpoints, verbs, payload fields). For behavior contracts — how the system behaves (cards move on coding start, completion = plan-file mtime advance, plan files are write-once-at-the-end, subtask column exclusion) — consult the switchboard-contracts protocol (switchboard api GET /protocol/switchboard-contracts). Never consult that skill for invocation; never consult this skill for behavior conventions.


1. Bootstrap

Verify Switchboard is up before anything else:

switchboard api GET /health     # -> { status: 'ok', port, roots: [...] }

If switchboard api fails with server not running, Switchboard is not running — tell the user to open the workspace in VS Code with the Switchboard extension active (or run the standalone host). Do not fall back to editing kanban.db directly.

Auth. All endpoints sit behind the localhost boundary. If a token is set in VS Code (Switchboard: Api Token), the CLI handles auth automatically.

Multi-root. /health returns roots. DB-backed endpoints accept an optional ?workspaceRoot=<root> (GET) or "workspaceRoot" body field (POST/PUT/DELETE); omit it to use the primary workspace.

Response envelope.

  • Read endpoints return { "success": true, "data": <payload> }.
  • Mutation endpoints return { "success": true, ...fields } (no data wrapper).
  • Errors return { "error": "<message>" } with an HTTP status: 400 bad input, 401 unauthorized, 404 not found, 409 conflict, 503 store/extension not ready, 500 handler error.
  • A 503 from a read endpoint also carries code and tier: { "error": "...", "code": "STORE_UNAVAILABLE", "tier": "board" | "archive" }.

Three read outcomes — branch on all three. A board read has exactly three answers, and two of them used to be the same value:

OutcomeShapeMeaning
Rows200 {success:true, data:...}The store answered. An empty data: [] genuinely means nothing matches.
No such record404 {error}The card does not exist — in the board window or the archive. Both were searched.
Store unavailable503 {error, code:"STORE_UNAVAILABLE", tier}The store did not answer. You know nothing about the board's contents.

STORE_UNAVAILABLE is not an empty board. An orchestrator that reads it as one makes confident decisions about a board it cannot see — it will dispatch nothing, report a fleet as idle, or mark work missing. On STORE_UNAVAILABLE: retry once after a few seconds; if it repeats, stop and report, naming the tier that failed. Never poll it in a loop, and never fold it into "no cards found".


2. Read endpoints (GET)

EndpointReturns
GET /health{ status, port, roots } — liveness + workspace roots
GET /kanban/boardFull board: every active plan record for the workspace
GET /kanban/plans?column=<col>Plans filtered to one column
GET /kanban/plans?featureId=<id>Subtasks of a feature
GET /kanban/plan?planId=<id>One plan record, its full file content (.data.content), and which store answered (.data.source: 'board' | 'archive')
GET /kanban/columns{ builtIn: [...defs], custom: [{id,label,labelSource,enabled,displayModeOf?,legacyAliasOf?}], displayOnly: [{label,aliasOf}] } — every column includes enabled (boolean) and enabledSource (`'config''default''structural''unknown'). displayModeOf/legacyAliasOf mark a column that is NOT an independent peer (BACKLOGis a view ofCREATED; CODEDis a legacy alias ofLEAD CODED). Filter destinations to enabled !== false`.
GET /kanban/featuresAll features (isFeature rows)
GET /kanban/dependencies[?planId=<id>]The durable dependency graph. With planId: that plan's dependencies (its depends_on_plan_id list), its mapFingerprint, and its analysisFileSet. Without: every edge in the workspace. Read-only — a controller reads this to arrange missions
GET /kanban/sendable?column=PLAN%20REVIEWEDThe batch that can go now: { sendablePlanIds, stalePlanIds }. Dependency-ready plans in the column, greedily selected so no two share a file (computed by the same resolver the board filter uses). Read-only — no mission, no staging, no card move. stalePlanIds names cards whose plan file changed since analysis
GET /worktree/listAll worktree rows (path, branch, subtask_plan_id, feature_id, tier, status, base_branch)
GET /mission-control/session-logMission Control's session file — .switchboard/mission-control/session.md when it exists, falling back to the legacy .switchboard/mission-control/session-log.md (markdown string, '' when neither exists)
switchboard api GET /kanban/board
switchboard api GET "/kanban/plans?column=PLAN%20REVIEWED"
switchboard api GET "/kanban/plan?planId=a1b2c3d4"      # includes .data.content (the .md file)
switchboard api GET /kanban/features
switchboard api GET /worktree/list

⚠ planId is NOT a valid query filter on GET /kanban/plans. The handler (_handleGetPlans, LocalApiServer.ts:6483) reads only column and featureId from the query string — a planId param is silently ignored and the full workspace array is returned. An agent consuming res.data[0] then inspects whichever card happens to be first in the DB (often from a different column), generating false reports. For a single plan, use GET /kanban/plan?planId=<id> (returns one plan plus its file content at .data.content). Client-side filtering (res.data.find(p => p.planId === id)) works but fetches the whole board — prefer the dedicated endpoint.

Column filter requires the storage column id, not the board label. GET /kanban/plans?column=Planned returns an empty array — the stored id is PLAN REVIEWED, URL-encoded as PLAN%20REVIEWED. See the column-translation table in query-kanban/SKILL.md or call GET /kanban/columns for the live {id, label} mapping.

Record lookups span the archive; collection reads stay windowed. The board keeps a window of recent work; completed cards that age past it move to a separate archive store. The two kinds of read treat that boundary differently, on purpose:

  • GET /kanban/plan?planId= spans both stores and labels the result with .data.source ('board' or 'archive'). A card is returned exactly once even while the archive sweep is moving it. So a 404 here is a real absence in both stores — never "it got old". The storage window is a fact about where a card is kept, not a fact about the card.
  • GET /kanban/board, GET /kanban/plans and GET /kanban/features are windowed and deliberately exclude dormant cards — that is the human board's view, and spanning them would flood it. To ask about a specific old card, use the record lookup.

.data.source is where the record was found; a GET does not promote a dormant card back to the board.

Plan records include: planId, sessionId, topic, planFile, kanbanColumn, status, complexity, tags, project, isFeature, featureId, worktreeId, worktreeStatus, dispatchedAt (null = not currently working), recommendedRole.

recommendedRole (lead | coder | intern) is the seat the board would route this plan to — resolved through the operator's kanban.routingMapConfig and the pair-mode intern bypass, the same rule POST /kanban/dispatch uses for "auto". Dispatch a subtask to a seat of that role on your team; if your team has no such seat, dispatch to a coder and say why. Absent when the plan's complexity is unknown — treat absence as coder. Do not re-derive it by reading the plan file's Recommendation: line: nothing in the system parses that line, and a remapped board or an active pair mode would make it wrong.


3. Plan lifecycle (POST / PUT / DELETE)

EndpointBodyPurpose
POST /kanban/plans{ title, slug?, complexity?, tags?, project?, description?, body?, workspaceRoot? }Create a plan (writes .switchboard/plans/<slug>.md, imports it, returns assigned planId). workspaceRoot must match a server root from GET /health (roots), else 400.
DELETE /kanban/plans?planId=<id>[&deleteFile=true]—Delete the DB row; deleteFile=true also unlinks the .md
PUT /kanban/plans/project{ planId, project, workspaceRoot? }Set a plan's project
PUT /kanban/plans/complexity{ planId, complexity, workspaceRoot? }Set a plan's complexity ("1"–"10" or "Unknown")
PUT /kanban/plans/priority{ planId, starred, workspaceRoot? }Set a plan's priority star (starred: true/false/1/0/"true"/"false"). Starred cards sort first in every consumer. Idempotent. Accepts sessionId as an alias for planId. Returns { success, planId, starred }.
POST /kanban/plans/import{ workspaceRoot? }Rescan .switchboard/plans/*.md and upsert. workspaceRoot must match a server root from GET /health (roots), else 400.
# Create — returns { success:true, planId, planFile, slug }
switchboard api POST /kanban/plans '{
  "title": "Add rate limiting to the API",
  "complexity": "5",
  "tags": "backend, api",
  "body": "Add token-bucket rate limiting to the public endpoints."
}'

switchboard api PUT /kanban/plans/project '{"planId":"a1b2c3d4","project":"Platform"}'

switchboard api PUT /kanban/plans/priority '{"planId":"a1b2c3d4","starred":true}'

switchboard api DELETE "/kanban/plans?planId=a1b2c3d4"                    # DB row only
switchboard api DELETE "/kanban/plans?planId=a1b2c3d4&deleteFile=true"    # also remove the file

workspaceRoot validation: POST /kanban/plans and POST /kanban/plans/import require workspaceRoot (if provided) to match a known registered workspace root as reported by GET /health → roots. Any unregistered root (or a differently-cased / symlinked spelling that does not match) is refused with 400.

delete_plan gotcha: without deleteFile=true, the .md file stays on disk and the plan re-appears on the next import_plans (or a webview reset). Use deleteFile=true for a permanent delete. create_plan refuses to overwrite an existing file (409) and rejects path-traversal slugs (400).


4. Board mutations (POST)

EndpointBodyPurpose
POST /kanban/dispatch{ plan: planId or plan-file path, targetColumn?, workspaceRoot?, from? }ONE-call advance-and-dispatch. targetColumn omitted/"auto" → routed by plan complexity through the board's own rule (default bands 1–4 → INTERN CODED, 5–6 → CODER CODED, 7+/unknown → LEAD CODED; custom routing maps + pair-mode bypass honored; decision returned in routing). Canonicalizes explicit columns, persists the move, fires the column's role prompt (CLI-triggers setting does not gate API dispatches), verifies vs DB. Honest response (moved, dispatched, dispatchedAt); 4xx/409 when it can't work (no role on column, no live terminal agent). Prefer this over move + raw triggerAction (whose exact webview field names and hollow {success:true} acks hide no-ops). from — your own terminal name. Supply it and a role dispatch (e.g. CODE REVIEWED → reviewer) is routed to the member of YOUR team rather than the first matching terminal on the board. The response echoes teamRouting naming the decision, including when it fell back
POST /kanban/queue/next{ from, workspaceRoot? }The coding lead's pull door onto the same dispatch machinery POST /kanban/dispatch is. Pops the next staged card (STAGING column only; subtasks excluded via empty featureId), dispatches it through performKanbanDispatch with targetTerminalOverride: from (complexity routing picks the column, the requesting head is the terminal — the lead asked, the lead receives, and it delegates subtasks itself), and returns what it dispatched. The head is the expected caller — call this after you have posted POST /kanban/task/complete for the card your team holds. The board never refuses a dispatch: a seat still holding a card does NOT produce a 409 — ownership is advisory display metadata, and the dispatch itself resets the owner stamp and completed_at. Two response shapes: 200 { success:true, dispatched:{...} } (a card was popped and dispatched — work it) and 200 { success:true, dispatched:null, reason:'queue empty' } (no eligible staged cards remain — report and stop). The pop is serialized per-process so two heads asking at once never receive the same card. A failed dispatch (no live terminal, card dragged out) is passed through and the card stays staged. from is the head's own terminal name; an unresolvable from is a 400. The route is a thin wrapper over the in-process dispatchNextFromQueue method — the schedule timer and the handoff call the method directly so the serialization chain is the single critical section
POST /kanban/move{ planId or sessionId, targetColumn, workspaceRoot? }Move a card (feature cascade + tracker sync inherited). Column IDs are canonical uppercase (LEAD CODED), never state-file slugs (lead-coded) — both endpoints canonicalize and 400 on unknown columns. workspaceRoot omitted ⇒ resolved by identity across all registered roots (GET /health → roots); supplied ⇒ that root or a not-found error. Response echoes resolvedWorkspaceRoot and rootResolution (explicit/default/searched/path). Unavailable (503) on the standalone host
POST /kanban/feature{ name, planIds: [...], description?, workspaceRoot? }Create a feature from plan IDs
POST /kanban/feature/assign{ featurePlanId, planIds: [...], workspaceRoot? }Assign plans to a feature
POST /kanban/feature/remove{ subtaskPlanId, workspaceRoot? }Detach a subtask from its feature
POST /kanban/feature/delete{ featurePlanId, deleteSubtasks?, workspaceRoot? }Delete a feature
POST /kanban/feature/split{ featurePlanId, keptPlanIds: [...], firstFeatureName, secondFeatureName, workspaceRoot? }Split a feature in two
POST /kanban/task/complete`{ from, planId?ordinal?, outcome?, note?, workspaceRoot? }`Assert a plan is FINISHED. Writes completed_at on the row; outcome/note are recorded when supplied and are never required — the post itself is the completion signal. Idempotent within a dispatch: a repeat while completed_at is newer than the last dispatch stamp returns the existing timestamp. Dispatch resets completed_at, so a post after a re-dispatch counts as fresh work. The ONLY fact that marks work done; nothing else infers doneness. planId and ordinal are mutually exclusive; when both are absent the server resolves the poster's acceptance candidates — ordinal indexes the held feature's subtask order (the number in the feature file's Subtasks list), and a bare post resolves exactly one incomplete candidate or 400s naming them
POST /worktree/feature{ featureId, featureTopic?, repoName?, workspaceRoot? }Create the per-feature worktree (branch + tree + worktrees row + best-effort terminal seating). The ONE implementation, shared with the webview's per-feature create button. featureTopic names the branch and is resolved from the board when omitted. A feature that already has an active worktree returns 200 { success:false, error:"Feature already has worktree: <branch>" } — not a 4xx, so a loop over several features reports that one and continues. Never writes feature_worktree_mode
POST /worktree/cleanup{ worktreeId or branch, workspaceRoot? }Mark a worktree merged and clean it up (kind-aware)
POST /mission-control/adopt{ workspaceRoot?, terminalName? }The caller IS Mission Control. Records the seat and returns { mode, prompt, seat, liveDelivery, note? } — the same pre-flight prompt /mission-control/start would have injected into a terminal it created, handed back so you run it in your own session. Seats no terminal. Does not arm — arming is POST /mission-control/confirm. Pass terminalName from $SWITCHBOARD_TERMINAL when set; omitted or unmatched → liveDelivery: false and turn-end notices arrive in the reports inbox instead
POST /mission-control/start{ workspaceRoot? }Two paths, decided by whether a lead/coder agent is configured. Terminal mode: create a pty terminal named Mission Control, boot the lead/coder CLI into it, and deliver the pre-flight interview (the agent adopts the seat itself via POST /mission-control/adopt — the server does NOT seat). Clipboard mode (no agent configured, or pty unavailable): NO terminal is created; returns { mode:'clipboard', prompt } with the /switchboard launcher text for the caller to run. Does not arm in either path — the terminal-mode message says Mission Control is seated and awaiting confirmation; arming is POST /mission-control/confirm. Double-click protection: a second call while an Mission Control terminal is already live redelivers the persona prompt to it instead of spawning a second terminal
POST /mission-control/confirm{ workspaceRoot? }Arm a Mission Control session after the pre-flight. Verifies .switchboard/mission-control/session.md exists, then arms Mission Control switch (sets missionControlArmed; the schedule switch is independent and may also be on). Returns { success, sessionFile } or { success:false, error } when session.md is absent. The only path that arms
POST /mission-control/handoff{ headTerminal, stagedCount?, firstCardPlanId?, summary, workspaceRoot? }Hand off Mission Control to a coding lead and exit (default pre-flight exit). Refuses with 409 if no live coding head, if queue is empty, if already armed, or if already handed off. Writes summary to session log, closes Mission Control seat, and leaves automation state untouched
POST /mission-control/stop—Disarm Mission Control and archive session.md to sessions/session-<ISO>.md
# Column vocabulary: CREATED | PLAN REVIEWED | STAGING | LEAD CODED | CODER CODED | INTERN CODED
#                    | CODE REVIEWED | COMPLETED   (see GET /kanban/columns)
switchboard api POST /kanban/move '{"planId":"a1b2c3d4","targetColumn":"CODE REVIEWED"}'

switchboard api POST /kanban/feature '{"name":"Auth Refactor","planIds":["id1","id2"],"description":"Group the auth work."}'

4a. Verb-rail traps (read before you call POST /<panel>/verb/<name>)

The generic verb rail (POST /<panel>/verb/<name>) routes webview messages through the same handler a UI click takes. Two traps hide on this surface, and they share one failure mode: the route answers {success:true} and nothing happens. An agent cannot detect the no-op from the response, so it reports success and moves on — the most expensive class of bug on this surface.

Trap 1 — read verbs return only {success:true}

Read verbs (get* / fetch* / load*) over the generic rail return only {success:true} — their data arrives on the WS hub, which an HTTP client cannot see. The call looks successful and returns nothing.

Use the dedicated GET endpoints instead: GET /kanban/board, GET /kanban/plans, GET /kanban/plan?planId=<id>. They return the data in the response body (wrapped in .data).

Trap 2 — exact webview field names

Raw verbs expect the exact webview message field names. The two that bite:

  • triggerAction takes { sessionId, targetColumn } — not planId, not column.
  • promptOnDrop takes { sessionIds, sourceColumn, targetColumn } — not planId, not column.

Wrong names (planId, column) make the arm silently no-op while the route layer still answers {success:true} — the call LOOKS successful and nothing happened.

Prefer the first-class endpoints (POST /kanban/dispatch, POST /kanban/move) — they validate, canonicalize columns, verify against the DB, and return honest errors. Use raw verbs only when no endpoint exists, and verify the effect afterwards (GET /kanban/plan?planId=<id> → check dispatchedAt and kanbanColumn).


4b. Prompt delivery (POST /terminals/verb/*) — attended coder driving

When you are a head agent driving a coder terminal (dispatch a subtask, get called back, review the diff, resend a fix) — not running an unattended column sweep — use the prompt-delivery verb pair. The behavioral rules are inlined in the enriched drive prefix built by _buildDrivePrefix in KanbanProvider.ts; the endpoints are:

EndpointBodyPurpose
POST /terminals/verb/ptySendPrompt{ name, data, clearBeforePrompt, origin?, dispatch?, kind?, machineOrigin? }Deliver a prompt to a named terminal. Pass clearBeforePrompt: false explicitly — the omitted-field default has moved once already; if it moves back, every send wipes the coder's conversation. Both hosts apply standing orders (the callback contract) on a dispatch payload only — see the kinds below; a send that declares no kind and carries no dispatch object is read as a message and gets none of the three appends. Payload kinds:<br>• kind: "dispatch" — seat starting work; receives standing orders, seat directive block, and dispatch directives.<br>• kind: "message" (or legacy alias machineOrigin: true, to be retired once coder standing-order texts are reissued) — questions, instructions, relayed messages, and notifications; suppresses all three appends.<br>• kind: "orders-refresh" — reserved for after-clear delivery path (rejected if sent).<br>Omitted kind defaults to dispatch if dispatch object is supplied or if prompt text contains dispatch identity (PLANS TO PROCESS:), and defaults to message otherwise. Dispatching a subtask? Pass dispatch: { planFile|planId, role } — the host then registers the dispatch before delivering and attaches the protocol directives the board attaches (plan-file completion report + Mission Control reports directive), echoing attributed and directivesAttached. Without it the coder is never told to write a completion report, so a finished subtask reports nothing the board can see. Fails closed: attributed: 0 → success: false, nothing delivered. Never send dispatch on a plain message or on a report back to your head — it would make the recipient write a plan file and fire a false completed. The dispatch-protocol directives (COMPLETION REPORT, ORCHESTRATOR REPORT) are appended automatically for coder/intern/lead recipients — do not paste your own. Messaging or dispatching from your own seat? Pass origin: "<your own terminal name>" — the roster barrier clears the rest of the team when a new feature enters it, and origin is how it knows not to clear you. Without it a lead can wipe its own context with its own dispatch; the busy check only saves a seat that happens to be emitting output at that instant. Omit it only when you are relaying a human's board action.
POST /terminals/verb/ptyListTerminals{}Enumerate live terminals: { terminals: [...] } — one array, every live terminal. Copy friendlyName verbatim.
POST /terminals/clear{ from, name? | team? | seats? }Canonical first-class clear endpoint. Clears a single seat (name), team roster (team), or explicit set (seats). Enforces server-side invariants: from is required and never cleared; team scope preserves the lead and defers mid-turn seats; redelivers standing orders; rolls log session boundary. Returns { success: true, cleared: [...], deferred: [...], skipped: [{ name, reason }] }.

(Note: POST /terminals/verb/ptyClearTerminal remains as a low-level host verb used internally, but POST /terminals/clear is the canonical agent-facing endpoint.)

ptySendPrompt delivery evidence & response fields

  • success: true: Bytes were written to the pty and the submit CR was sent. It is not an echo, not a round trip, and not a claim that the CLI parsed anything.
  • bytesWritten: UTF-8 byte length of text written to the terminal, including host-appended directive, seat, and standing-orders blocks. It is expected to exceed the length of your data field. A larger number is not evidence of corruption.
  • promptSeq: That seat's delivery ordinal (post-increment promptCount). Re-reading promptCount on ptyListTerminals and seeing it advance past your promptSeq means a later send landed, not that yours did — your own evidence is the promptSeq on your own response.
  • lastDataAt: The seat's output heartbeat from ptyListTerminals. It proves the CLI emitted something; it never proves the CLI received something.

Clear a coder the moment you stand it down, not on the way back in — the precondition is completion received and next work assigned elsewhere, and never clear your own terminal (see the inlined "Clear a terminal only when at rest" rule in the drive prefix).

Terminal Reset & Clear Protocol — why /clear via ptySendPrompt fails

Agents frequently try to clear a terminal by sending /clear through ptySendPrompt. This does not work, pollutes the prompt, and is rejected.

ptySendPrompt wraps data in bracketed-paste escape sequences (\x1b[200~ ... \x1b[201~). A CLI receiving bracketed-paste /clear treats it as literal text input, not a slash command. Inside the block it is absorbed as literal text and silently prefixes or pollutes the payload with appended orders. Sending bare slash commands like /clear via ptySendPrompt is rejected by the server with an error directing callers to POST /terminals/clear.

The canonical mechanism is POST /terminals/clear:

# Clear a single seat
switchboard api POST /terminals/clear '{"name":"<terminal friendlyName>","from":"<your terminal name>"}'

# Clear team terminals
switchboard api POST /terminals/clear '{"team":"<head terminal friendlyName or teamId>","from":"<your terminal name>"}'

Rules:

  • from is required on all calls to prevent accidental caller context wipe.
  • Clear a coder the moment you stand it down, not on the way back in. A clear issued at rest is what resets a coder you always send with clearBeforePrompt: false, and it lands long before the next dispatch instead of racing it.
  • Never clear your own terminal.
  • Never use ptyClearAllTerminals (it clears every active terminal, you included).

5. Comms (POST)

  • POST /comment — { provider, id, body }: reply to a Linear/Notion/ClickUp remote-control card.
  • POST /phone-a-friend — { planFile, originRole? }: notify a second-pass terminal at batch end.

6. Integration proxies (POST)

Passthrough to the tracker APIs using Switchboard's stored tokens (you never see the tokens):

  • POST /api/clickup, POST /api/linear — raw API proxy.
  • GET /resolve/<clickup|linear>/name/<name>, GET /metadata/<clickup|linear>, /task/*, POST /doc/clickup, POST /diagram/generate — see the clickup_api / linear_api skills.

7. Workflow A — fleet coding/review agent inside a worktree

You were dispatched into a worktree to code or review one plan. You report back to your head agent via ptySendPrompt (installed as a standing order on every team member). You do not have a chat channel to Mission Control; you use HTTP for board reads.

# 1. Find your plan (its planId is in your dispatch prompt) and read its full spec.
switchboard api GET "/kanban/plan?planId=$PLAN_ID" --json | jq -r '.result.data.content'

# 2. Do the work in this worktree. Commit as you go (Mission Control verifies via git, not chat).

# 3. Report back to your head agent when done (the standing order installed on dispatch).
#    The head agent or the extension's turn-end notifier will signal Mission Control.

You do not move your own card or merge — Mission Control does that after verifying your git state.


8. Workflow B — external Mission Control driving the board

You are an external agent acting as Mission Control. Mirror the in-VS-Code persona (switchboard-mission-control): coding + code-review only; planner-stage questions escalate to the human.

# 1. Read the whole board on each wake.
switchboard api GET /kanban/board --json | jq '.result.data'

# 2. Group loose plans into features (the external equivalent of group-into-features).
switchboard api POST /kanban/feature '{"name":"Checkout v2","planIds":["id1","id2","id3"]}'

# 3. For each feature, read its subtasks + message the team leads (or dispatch to your own agents).
switchboard api GET "/kanban/plans?featureId=$FEATURE_ID" --json | jq '.result.data'
switchboard api POST /terminals/verb/ptySendPrompt "{
  \"name\":\"<lead terminal friendlyName>\",
  \"data\":\"You are leading the <feature name> feature. Your PLAN REVIEWED subtasks are: <list>. Implement each, commit, and report back when done.\",
  \"origin\":\"<your own terminal friendlyName>\",
  \"clearBeforePrompt\":false,
  \"kind\":\"dispatch\",
  \"dispatch\":{\"planId\":\"<featurePlanId>\",\"role\":\"lead\"}}"

# 4. On each wake: VERIFY VIA GIT, not self-report. A subtask is "coded" only when its worktree
#    branch is ahead of base with committed work. Use the base_branch from GET /worktree/list.
switchboard api GET /worktree/list --json | jq -r '.result.data[] | [.path, .base_branch] | @tsv' |
while IFS=$'\t' read -r wt base; do
  echo "$wt: $(git -C "$wt" rev-list --count "${base:-main}"..HEAD 2>/dev/null) commits ahead"
done

# 5. Advance verified cards; escalate planner-stage questions to the human.
switchboard api POST /kanban/move "{\"planId\":\"$PLAN_ID\",\"targetColumn\":\"CODE REVIEWED\"}"

# 6. When all of a feature's subtasks are CODE REVIEWED, merge the feature's single shared
#    worktree branch into main (per-feature model: one worktree per feature, so one merge —
#    git -C <main checkout> merge <feature branch>), then clean up the worktree.
switchboard api POST /worktree/cleanup "{\"branch\":\"$BRANCH\"}"

9. The truth rule

Trust git and board state, never an agent's self-reported "done":

  • Coded = worktree branch ahead of base with committed work (git -C <wt> rev-list --count <base>..HEAD > 0) and a clean tree (git -C <wt> status --porcelain).
  • Reviewed = the review stage genuinely passed and the card sits in CODE REVIEWED.
  • Read board state via these endpoints; read ground truth via your own git commands.

9a. Fleet Completion Detection Recipe

When an operator asks you to monitor a fleet until completion, three concrete signals define "done". Check them in order; the first non-null/stopping signal wins.

  1. Database signal — completedAt. completed_at is non-null on the plan record (written by POST /kanban/task/complete via setCompletedAt). Read it via GET /kanban/plan?planId=<id> → .data.completedAt. NULL means the team is still working — do not report completion.
    • The completed_at write is idempotent — a repeat POST /kanban/task/complete for the same planId returns the existing timestamp without re-writing. When polling, a stable timestamp is not a new event; do not re-report.
    • outcome is optional — a post with no outcome still completes the card. The post itself is the completion signal; prose does not gate it.
  2. Queue signal — POST /kanban/queue/next. Returns 200 { success: true, dispatched: null, reason: "queue empty" } when no eligible staged cards remain — report and stop. (The response is wrapped in the standard success envelope; an agent parsing for a bare { dispatched, reason } object will not find it under res.data and may misread the response.)
    • There is no in-flight refusal: a card your team still holds does not block the pop. A re-dispatched card resets its completed_at and owner stamp; if the work is already done the dispatched agent reads the plan and says so — that is the intended cheap path, not an error.
    • To leave a resume hint for the next dispatch, append a checkpoint event (switchboard verb checkpoint '{"planId":"<id>","text":"…"}'). The next dispatch surfaces it in the prompt as advisory resume context; it never gates dispatch.
  3. Git signal — Switchboard-Plan trailer. A commit on the integration branch (main or the feature's shared worktree branch) carrying the git trailer Switchboard-Plan: <planId> (one line per planId in a batch dispatch), preceded by Switchboard-Stage: <stage>. The trailer block requires a blank line before it (git only parses trailers in the message's final paragraph). Verify with:
    git log --format='%(trailers:key=Switchboard-Plan,valueonly)'

All three are ground truth; an agent's self-reported "done" is not. Trust the database timestamp, the queue response, and the git trailer — never a chat claim.


10. Failure modes

  • SWITCHBOARD_NOT_RUNNING — port file missing or /health fails → tell the user to start the extension. Never edit kanban.db directly.
  • 404 — plan/feature/worktree not found (bad id).
  • 400 — invalid input (bad column, empty body, path-traversal slug); the message names the problem.
  • 409 — create_plan slug already exists.
  • 503 with code: "STORE_UNAVAILABLE" — the board or archive store did not answer (tier says which). This is NOT an empty board and NOT a missing card. Retry once after a few seconds, then stop and report — do not loop on a store that is down, and do not act as though the board were empty.
  • 503 without that code — extension not ready yet → retry after a short delay.

11. File-based fallback (no HTTP)

If the API server is down you can still communicate via the filesystem (Mission Control reads these):

  • Session file: .switchboard/mission-control/session.md — the current session file (Rules + append-only Log); read it to see Mission Control's decisions. The legacy .switchboard/mission-control/session-log.md is still honoured as a fallback by GET /mission-control/session-log on installs that have one.
  • Progress: .switchboard/mission-control/progress.json — Mission Control's per-plan stall state.

Reports channel — switchboard reports

A report is a host-recorded turn-end event stored in the plan_events table (event_type turn_end), not a file. The host writes a row on every turn-end (finished, blocked, stalled), joined to the card's current kanban column. This is not an HTTP surface — there is no GET /mission-control/reports endpoint. Read reports via the CLI:

switchboard reports [--kind blocked|finished] [--limit N] [--json]
  • --kind blocked filters to blocked turn-ends (a seat went quiet or a feature stalled).
  • --kind finished filters to finished turn-ends (a seat completed its turn).
  • The output includes each card's current kanban column, so you can tell whether a formerly blocked card is still blocked.
  • A row whose card was deleted or archived still appears, with [no card] — the record survives its card.
  • Rows are pruned by the retention service; the accumulation that 1900+ files became cannot recur.
  • Every row on this channel is host-written; the row carries no from field. Agent-authored reports are a separate path (team reports via GET /teams/<id>/reports), not this channel.
  • The old file-based channel (.switchboard/mission-control/reports/) is retired. Existing files remain as archival evidence; no code reads or writes them.

Notes

  • localhost only (127.0.0.1) — never a public interface.
  • Reads wrap payloads in .data; mutations return { success, ...fields }.
  • This surface is documented for external tools; the in-VS-Code Mission Control persona is switchboard-mission-control.

Remote & Mobile Network Addressing (Tailscale)

When the operator machine is reachable over Tailscale, the board and terminals are addressable at http://100.x.y.z:7777 (the Tailscale IP, port from the live server). SSH access to the host requires key-based authentication when password auth is hardened — the operator must have a registered SSH key, not a password.

Tailscale IS the network boundary. The API server must never be exposed on a public interface. Tailscale's encrypted mesh is the controlled boundary that replaces exposing a port to the open internet; it is not a hole in the localhost boundary, it is the boundary itself.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.