MCP tools¶
relaybay serves MCP over Streamable HTTP at http://127.0.0.1:8765/mcp. Every request needs
Authorization: Bearer <token> (relaybay token prints it). Results are short markdown meant to be
read by the model; failures (unknown task, version conflict, invalid stage) come back as tool errors
with a message that says what to do.
Server instructions¶
The server's MCP instructions (sent once when a session connects) are a short working contract:
read project_snapshot on start (its first line names the rules); pick or create a task and pass
session (your local_ id with session_title if you know it, plus hook_session if the
session start gave you one; if not, don't rebind — the coordinator binds you); update next_step, waiting_on and progress on every stage change and
report; start a report with the configured first line, states and sizes (taken from the
effective global rules, so they match rules_get); set waiting_on to who
decides when the work needs a decision; use task_patch for fields your tool schema lacks; treat a
Warning (rules) line as something to fix; when a hook hands over owner comments, handle them like
a message from the user and call comment_ack (or answer with comment_add); before touching a
server, cluster or database, read environments_get — the level there says what is allowed
unasked, live is read-only unless the owner set otherwise — never paste secrets into relaybay and
propose changes with environment_propose; before moving a task to review or test, write what the
owner should check with review_steps_set and attach screenshots with attach_file (the owner's
decisions arrive as owner comments); start with a plan — plan_set from PLAN.md or your own 2-5
steps (S tasks: at least a todo) — and mark steps in_progress / done with plan_step_update
(done needs one line as planned or deviation: why). The details live in the rules below.
Conventions:
- Task reference — the key (
acme-shop-12) or the numeric id. - Stages —
plan,dev,review,test,live,done. Any transition is allowed; the event log keeps the history.reviewis where the owner accepts the work step by step (see Review below). session— optional on writes: who is calling. Pass the Claude desktop app session id (local_<uuid>) if the session knows it, together withsession_title(its display name); otherwise a human-readable name. relaybay creates the session on first use and records it as the actor of the event. It is for display only; the token is the only authorization.hook_session— optional on every write tool: your own Claude Code session id as relaybay's hooks report it, never another session's. An id another live session linked first is not moved (the response ends withWarning: hook_session … belongs to another session (…); not relinked.), andtask_bind_sessionnever links one (a coordinator binds other sessions; pass the id on your own writes). With hooks set up (relaybay setup), the session start adds "Your relaybay hook session:. Pass it as hook_sessionin relaybay writes." to the agent's context. Passed together withsession, it links that hook id to the calling session exactly, so the board's live status (working / waiting / idle / ended) follows the right card. A row the hooks had created for the id by guess hands over its status and is removed. Without an exact link relaybay guesses from the working directory (see Session status from hooks).- Session aliases — if you don't know your
local_id, don't rebind a task: the coordinator binds you; pass your session name only assessionon writes. Whentask_bind_sessiongets a plain name for a task bound to a desktop session, the desktop binding (and its "Open in desktop" link) is kept and the name is recorded as that session's alias (eventsession.alias_added). From then on the name resolves to the desktop session: writes are recorded as it,task_listandtask_unbind_sessionaccept the name. Binding by anotherlocal_id still rebinds; a plain name on a task without a desktop session binds as before. Aliases are global: one name maps to one session across all projects, and later binds by that name move it to the desktop session bound to that task. Because an alias would hijack a plain session of the same name, none is created when such a session exists; the call then changes nothing and answers "already bound to(local_…); pass your local id". - Claude desktop app session links — for a desktop session id, relaybay builds a link
(
claude://claude.ai/epitaxy/<id>by default, seedesktop_link_templatein the README) that the board shows as "Open in desktop" and thattask_getprints. Plain names have no link. - Clearing a field — in
task_update, an omitted field stays unchanged;"-"or an empty string clears it (models cannot always send an empty string, so"-"is the reliable form). Intask_create,"-"leaves a field empty; inreport_add,progress="-"clears the summary. - Description and progress — two markdown fields per task.
descriptionsays what to do and why (1–5 sentences).progressis a running summary of everything done so far, not the last step: the session rewrites it as it goes, ideally with every report (report_add(progress=…)). The board shows both in full in the task dialog (not on the card). - Next step and waiting on — keep them current:
task_set_stageandreport_addtake optionalnext_step(what happens next) andwaiting_on(who acts next), saved in the same transaction as the stage change or the report; omitted, they stay;"-"or""clears them. A next step written before the task's last stage change is stale: the card and the task dialog show it muted with a "stale" marker,task_getandproject_snapshotappend(stale). Writing it again, even with the same text, confirms it as current (atask.updatedwith a version bump). On the board, saving the edit form confirms it only when the next-step text itself was changed. - External tracker —
tracker_ref(issue key, e.g.ENG-12, up to 100 characters),tracker_name(any tracker, free text),tracker_url(optional,http://orhttps://only; anything else is refused) andtracker_logged(bool: the work log is entered there). The card shows "" as a badge whenever a key is set, a link with a URL, with ✓ when logged. With the server setting require_tracker_log = true, a done task that is not logged shows "not logged" (also intask_get/ snapshots) and is never archived. - Tracker history — every issue key a task has had is kept: setting a different
tracker_refappends an entry (earlier ones stay, clearing the key keeps them too), and markingtracker_loggedstamps "logged" / "unlogged " on the current entry (logging again restarts its mark). task_getlists the entries newest first; snapshots and the card show only the current issue. - Automatic unlog — a logged task that gets new work loses the mark in the same transaction,
with an event
task.tracker_unlogged(payloadreason):report_add, a changedprogress, or a move back out ofliveordoneto an earlier stage. Metadata edits (title, branch, waiting on, next step, other tracker fields) keep it; passingtracker_loggedin the same write wins. A newtracker_refstarts as not logged unlesstracker_loggedis passed in the same write (a plaintask.updated; the previous issue's entry keeps its "logged" date). Mark it again once the log covers the new work. - Done window — the board's
donecolumn shows tasks closed in the lastdone_window_days(default 7); older ones are archived on the board only (withrequire_tracker_log, not-logged done tasks stay).project_snapshotandtask_listare unaffected:include_donereturns all done tasks.
Reading¶
| Tool | Arguments | Returns |
|---|---|---|
project_list |
— | Table of projects with task counts per stage. |
project_snapshot |
project, include_done? |
Open tasks grouped by stage (waiting on, next step, branch, session name and desktop id, version, the start of the description and progress, last report) and recent decisions. |
task_list |
project?, stage?, waiting_on?, session? (id or name), include_done? |
One line per task. Done tasks are left out unless stage or include_done is given. |
task_get |
task, reports? (default 5) |
Ends with "Editable fields: …". All fields (a stale next step ends with (stale)), description and progress in full, current version, the bound session (name and desktop id, link), the tracker issue and work-log mark, session binding history, tracker history, latest reports with their bodies, decisions. |
events_since |
since_id? (default 0), project?, limit? (default 50, max 500) |
Events oldest first and the since_id for the next call. |
comments_list |
task, unread_only? |
The task's comments oldest first: owner comments with sent / delivered … to <session> / read marks, agents' replies (unread by the owner until the owner marks them read). |
environments_get |
project |
The project's environments (see Environments and access below): the preset and guard mode, per environment kind, base URL, agents' level ((default) when it comes from the default preset, (policy: <layer>) from another policy layer) and per-capability overrides, sign-in flag and login-state path, access entries (references only; test secrets by name only unless the policy exposes them), notes, last change. |
policy_get |
project?, environment? |
The effective policy (see Policy below) of the global scope, a project or one of its environments: the layer chain, then one line per value with the layer that set it, e.g. - levels.live.default = "read" (preset:default), and any ignored broken layer. |
plan_get |
task |
The plan (see Plan below): status (accepted by … <time> or draft), 2/4 done, 1 deviation(s), each step as 1. [done] Title — done <time> · as planned · outcome · report:12 with its "what and why", then your Claude Code todo as a separate list. Without steps: how to write one, and the todo if any. |
review_get |
task |
The checklist (1. [accepted] Sign in — accepted by owner …) with each step's what / how / why, its screenshots (screenshot #7 (image/png, 41 KB): caption), the owner's comments on the step, then screenshots of no step. |
rules_get |
project? |
The working rules as markdown: hash, scope, mode, report format, stage requirements, description template, guidance. Without project, the global rules; with it, the project's merged rules. |
Writing¶
| Tool | Arguments | Effect |
|---|---|---|
project_upsert |
slug, name?, repo_path?, description?, session?, session_title?, hook_session? |
Creates the project, or updates the given fields. Slugs: lowercase letters, digits, dashes. |
task_create |
project, title, stage? (default plan), waiting_on?, next_step?, branch?, doc_path?, description?, progress?, tracker_ref?, tracker_name?, tracker_url?, tracker_logged? (default false), session?, session_title?, hook_session? |
Creates a task with the next key of the project (acme-shop-1, acme-shop-2, …). With session, the task is bound to it. |
task_update |
task, title?, waiting_on?, next_step?, branch?, doc_path?, stage_note?, description?, progress?, tracker_ref?, tracker_name?, tracker_url?, tracker_logged?, size? (S, M, L), expected_version?, session?, session_title?, hook_session? |
Patches fields; "-" or "" clears a text field. With expected_version, fails instead of overwriting when the task changed meanwhile. |
task_patch |
task, fields (JSON object), expected_version?, session?, session_title?, hook_session? |
Changes any editable task fields with the same validation as task_update / task_set_stage (a stage value moves the task, auto-unlog applies). Unknown names fail with the list of allowed ones; "-", "" or null clears an optional text field. The allowed fields: the end of task_get or relaybay://schema/task. |
task_set_stage |
task, stage, note?, next_step?, waiting_on?, session?, session_title?, hook_session? |
Moves the task; note is shown next to the stage. Moving to done sets the closed time. next_step and waiting_on are saved in the same transaction (task.stage_changed + task.updated, one version bump); "-" clears them. |
task_bind_session |
task, session, session_title?, hook_session? (ignored: never linked here, a note says so) |
Shows the session in the task dialog as the one working on the task (a desktop session also gets an "open in desktop" icon on the card). The previous binding is closed and kept in the task's history (event task.session_bound). A plain name on a task bound to a desktop session keeps that binding and becomes its alias (see Session aliases). |
task_unbind_session |
task, session? |
Removes the bound session from the task; the binding is closed and kept in the history (event task.session_unbound). With session (desktop id or name), fails without changes if another session is bound. Nothing bound: says so, changes nothing. |
report_add |
task, body, progress?, next_step?, waiting_on?, session?, session_title?, hook_session? |
Appends a markdown report. Its first line is the report summary; a first line state · size · gist also sets the state (inspected, changed, verified, committed, pushed, blocked) and size (S, M, L); that size also becomes the task's size when it has none. progress replaces the task's progress summary in the same transaction (task.updated + report.added); omitted, the summary stays; "-" clears it. next_step and waiting_on work the same way and go into the same task.updated. |
decision_add |
project, text, task?, decided_by?, source?, supersedes?, session?, session_title?, hook_session? |
Records a decision of the project (scope task with task); it appears in the project snapshot and decision_list. source: report:12, comment:7, event:301 or message:<id>, optionally followed by display text, or display text alone; a named report, comment or event must exist in the project. supersedes: the id of a current decision of the same project this one replaces (see Memory and documents). |
comment_ack |
comment_id? or task?, session?, session_title?, hook_session? |
Marks owner comments read: one by id, or every unread one of the task (event comment.read each). The board shows them as read and the Stop hook stops reminding. Neither argument, or an id that is not an owner comment: a tool error. |
comment_add |
task, body (markdown, up to 4000 characters), in_reply_to?, session?, session_title?, hook_session? |
An agent's reply on the task's thread (author: the calling session; comment.added). It marks the owner comment it answers read, or without in_reply_to every unread owner comment of the task. The owner sees unread replies in the Inbox. |
environment_propose |
project, change (object), reason, session?, session_title?, hook_session? |
Records a proposal for the owner and changes nothing (environment.proposed); it shows in the owner's Inbox and on the Stands & access page, where the owner accepts or rejects it. change: environment (name) plus any of kind, base_url, notes, sign_in_allowed, login_state_path, level (none / read / ask / full / default), overrides ({"ssh": "ask"}; capabilities sign_in, ssh, kubectl, db, deploy), access ([{"kind": "ssh", "fields": {"alias": "acme-live"}}] to add). A new environment needs kind. Secret values are refused (a tool error naming the field); test secrets are never accepted from agents. |
policy_propose |
scope (global, <project> or <project>/<environment>), patch (object), reason, session?, session_title?, hook_session? |
Records a policy proposal and changes nothing (policy.proposed); it shows in the owner's Inbox and in Settings → Policy. The patch is merged into the layer's JSON (objects key by key, other values replace, null removes a key) and the result must validate against the policy schema; a tool error names the path otherwise. Accepting applies it (policy.updated, policy.proposal_accepted), with a confirmation when it widens agents' access on live. |
| review_steps_set | task, steps ([{"title", "details?"}], up to 50), session?, session_title?, hook_session? | Replaces the checklist (see Review). Step N keeps its status and screenshots while its title and details stay the same; a changed step is open again; steps past the new end are removed (their screenshots stay on the task). |
| review_step_update | task, step (number), title?, details?, status? (only open), session?, session_title?, hook_session? | Changes one step's text (which reopens it) or reopens it after a fix. Accepting and rejecting is the owner's, on the board. |
| attach_file | task, path? or content_base64?, filename?, step? (number), caption? (markdown), session?, session_title?, hook_session? | Copies a PNG, JPEG, WebP or GIF (checked by content; SVG and others refused) to the task, optionally to a step. path is absolute and inside the project's repo_path, the temp folder or attachment_roots. Limits attachments_max_mb per file and attachments_task_max_mb per task. The same image on the same task is stored once (the answer says so). How to produce the file: Screenshots for review. |
| attachment_caption | attachment_id, caption ("-" clears), session?, session_title?, hook_session? | Changes a screenshot's caption. |
| plan_set | task, steps ([{"title", "details?"}], up to 30), reason?, session?, session_title?, hook_session? | Replaces the plan (see Plan). Step N keeps its status and checkpoint while its title stays the same; a renamed step is pending again; steps past the new end are removed; [] removes the plan. A new plan is a draft; changing the titles or their order makes an accepted plan a draft again (the answer says so). Every write is kept in the plan's history with reason. |
| plan_step_update | task, step (number), status (pending, in_progress, done, blocked), outcome? (one line), link? (done only: a report of the task such as 12, or a PR URL), conformance? (required with done: as planned or deviation: <why>), session?, session_title?, hook_session? | Moves one step. Done is a checkpoint with its time; leaving done clears it. Never turns an accepted plan back into a draft. |
| plan_accept | task, session?, session_title?, hook_session? | The coordinator accepts the plan (the owner uses the board). Already accepted: says so, changes nothing. A task without plan steps, or a call from the session bound to the task (an agent does not accept its own plan): a tool error. |
Writes that change a task increment its version. Every change appends an event and updates open
boards immediately.
A stage move (task_set_stage, task_patch / task_update with a stage, the board form) is
checked against the project's rules. In warn mode the write succeeds and the response gets one
line per unmet requirement, e.g. Warning (rules): moving to test usually needs a report since the
last stage change.; the task.stage_changed event carries the same lines as warnings. In
strict mode the write fails with Rules: moving to test requires … Nothing changed.
Review¶
The owner accepts a task step by step in the task dialog's Review tab. What to check by eye (the Owner check) belongs in the checklist, not only in the report:
review_steps_set(task="acme-shop-12", steps=[
{"title": "Sign in with 2FA", "details": "**What:** the code screen. **How:** sign in with the
demo account. **Why:** the new step must not skip the code."},
{"title": "Cart badge", "details": "Add two products; the badge shows 2."}])
attach_file(task="acme-shop-12", path="/tmp/acme-shop-2fa.png", step=1, caption="Code screen")
task_set_stage(task="acme-shop-12", stage="review", waiting_on="owner")
Moving a task to review or test with an empty checklist adds a Hint: line to the answer.
Each decision on the board reaches the bound session as an owner comment (like any other, see
Owner comments through hooks), linked to its step: Step 1 'Sign in with 2FA' accepted —
continue with step 2, Step 2 'Cart badge' rejected: the badge shows 1, or a plain comment On
step 2 'Cart badge': …. Acknowledge it with comment_ack; after a fix, reopen the step with
review_step_update(status="open") or change its text. The card shows "Review 1/2"; tasks in
review or test with a checklist are listed in the owner's Inbox (Review queue).
Plan¶
Every task should be worked by a plan the owner can see. Write it before you start coding, from PLAN.md or as your own 2-5 steps, and keep it current:
plan_set(task="acme-shop-12", steps=[
{"title": "Address table and model", "details": "Store addresses per account."},
{"title": "Address book page", "details": "Shoppers add, edit and remove addresses."},
{"title": "Picker at checkout", "details": "Pick a saved address instead of typing it."}],
reason="from PLAN.md")
plan_step_update(task="acme-shop-12", step=1, status="in_progress")
plan_step_update(task="acme-shop-12", step=1, status="done", outcome="Migration and model",
link="12", conformance="as planned")
plan_step_update(task="acme-shop-12", step=2, status="done",
conformance="deviation: editing waits for the shared form component")
The plan is a draft until the owner accepts it in the task's Plan tab or the coordinator calls
plan_accept. Renaming or reordering steps of an accepted plan makes it a draft again; marking
steps does not. The card shows Plan 1/3 · now: Address book page, deviations highlighted.
Work by plan (rule). By default a move to dev needs an accepted plan unless the task's
size is S ({kind = "plan_accepted", unless_size = ["S"]}); a task without a size needs one.
In warn mode the move gets a Warning (rules) line, in strict mode it is refused. Moving a
task with no plan and no todo to dev also adds Hint: … write down what you are going to do
(todo or 2-3 steps with plan_set).
Your todo is the second layer. Claude Code's own todo list (TaskCreate / TaskUpdate, or
TodoWrite in older builds) is read from the PostToolUse hook of your session and shown under
the current plan step ("Now doing: …"), never merged into the plan. An S task without plan steps
shows the todo as "Agent's plan, not accepted". The todo tools are off by default (Claude Code
2.1.309); turn them on in Claude Code's settings (relaybay setup does not write this):
{ "env": { "CLAUDE_CODE_ENABLE_TODO_TOOLS": "1" } }
Reminders. When you linked your hook session (hook_session), the hooks remind you of the
current step ("Plan of acme-shop-12 (accepted): step 2/3 'Address book page' — … Now doing: …") on
session start, after a todo change (only when the text changed) and every plan_reminder_every
tool calls (default 30).
Session status from hooks¶
Not an MCP tool: Claude Code's hooks call POST /hooks/claude-code (same Bearer token as /mcp)
with the hook's input JSON. relaybay setup writes the hook entries (see
connect-claude-code.md).
| Hook event | Session status | Activity shown |
|---|---|---|
SessionStart |
working | started (<source>); the answer adds the agent's hook_session id to its context, plus "You have N unread owner comment(s)…" when there are some |
UserPromptSubmit |
working | prompt (the prompt text is not stored) |
PreToolUse, PostToolUse |
working | the tool name (the tool input is not stored) |
Notification |
waiting | the notification message |
Stop |
idle | turn finished |
SessionEnd |
ended | the session is unbound from its tasks (history kept, task.session_unbound with reason: "session_end"), except after /clear or /resume and for desktop sessions |
Other events are ignored (204); an event with nothing to say also gets an empty 204. Activity of a working session is written at most once per 5 s per
session (the latest one wins); status changes are written at once. Each change is a
session.status_changed event per task the session is bound to.
A hook session is matched to a relaybay session in this order: on SessionStart, the desktop
session named by CLAUDE_CODE_HOST_SESSION_ID (set by the Claude desktop app; after /clear or a
restart the new Claude Code id moves to it by itself; a claude -p started from the window's tools
runs unattended and never takes the window's link, nor an id another live session's agent
linked); the exact link from a write with hook_session; otherwise, on first sight, the one desktop session bound to a task in the project
(not ended) whose repo_path contains the hook's cwd. When more than one could match (or the folder belongs
to several projects, or the bound desktop sessions are linked to other live hook sessions), nothing
is guessed: the hook session stays unlinked and is listed on the board under Integrations until the
agent passes its hook_session. With no desktop session in the project (or no project), it becomes
a plain session named after the folder.
Owner comments through hooks¶
Comments the owner writes on a task (task dialog or task page) reach the agent of the session bound
to the task, but only when the agent linked its hook session itself (hook_session on a write); a
link guessed from the folder, an unlinked or ended session never gets comment text.
UserPromptSubmit,PreToolUse,PostToolUse: the answer is{"hookSpecificOutput": {"hookEventName": …, "additionalContext": …}}with the undelivered owner comments of the session's tasks, one line each —Owner comment on acme-shop-12 (#7): …— and how to answer (comment_add/comment_ack). They count as delivered (comment.delivered) and are not sent again. At most about 8,000 characters per answer (below Claude Code's 10,000 limit); the rest follow with the next events.Stop: while delivered owner comments are not acknowledged (or new ones arrived), the answer is{"decision": "block", "reason": …}with the comments, so the agent handles them before it ends the turn. At most twice per turn per session and twice per comment in all; never whenstop_hook_activeis set before relaybay blocked in this turn (another hook is already continuing the turn); the per-turn counter resets on the nextUserPromptSubmitand whenever a Stop is allowed. Claude Code's own cap (8 continuations) still applies.- A rebind to another session hands the delivered-but-unread comments to the new session.
- An idle session sees a comment with its next prompt. The coordinator sees
comment.addedinevents_sinceand can forward it; with the opt-in wake-up hook (relaybay setup --wake, below) the comment wakes the idle session itself.
The command hook prints the server's answer only when it is a 200 with a JSON object, and still
exits 0 within 0.3 s per network step; http hooks get the same JSON body.
Wake-up hook (opt-in). relaybay setup --apply --wake adds a separate command hook
to Stop with "asyncRewake": true and a 1800 s timeout:
<python> -I -m relaybay.hook_client --url …/hooks/claude-code/wait --token-file <path> --wait.
Claude Code runs it in the background after every turn; it long-polls POST /hooks/claude-code/wait
for up to 25 minutes. When an owner comment arrives for the idle, exactly linked session, the server
marks it delivered and answers {"wake": text}; the hook prints the text to stderr and exits 2,
which wakes the session with the text as a system reminder. In every other case (nothing arrived,
relaybay down, the session started working, a newer wait for the same session) it exits 0 silently.
Checked live with Claude Code 2.1.309; --no-wake removes only this hook, a plain --apply keeps
it.
Fallback risk: a Claude Code build that ignored asyncRewake would run this hook in the
foreground and hold the end of every turn for up to 30 minutes, so --wake on a Claude Code older
than 2.1.309 (or of unknown version) is refused unless --force-wake is given too. If turns end only after a long wait, run relaybay setup --apply --no-wake.
Environments and access¶
Each project can list its environments (local, test, live) on the board's Stands & access
page: base URL, how to reach each one (ssh alias, kube context, database host, GitOps repository,
log and monitoring links) and what agents may do there. relaybay stores references only: a key
file path (~/.ssh/acme_live), an environment variable name ($DB_PASSWORD) or a vault path
(vault://kv/acme/db), never a password, key or token; writes with something that looks like one
are refused.
- Levels —
none(do not touch),read(look only),ask(ask the owner before each action),full(act as needed), per environment and optionally per capability (sign_in,ssh,kubectl,db,deploy). Where an environment sets none, the policy's level for its kind applies (default preset: local and testfull, liveread). Only the owner changes them on the board; agents callenvironment_proposeorpolicy_propose. - Guard — the
PreToolUsehook checksBashcommands and the input of MCP tools for the environments' names (whole tokens; a single word such asapponly as an ssh,--context,-hor URL host) and answers by level and the policy's guard mode (off/log/warn/block, defaultwarn;logonly journals):deny(with the reason shown to you) fornone, or forreadwhen the command looks like a change (kubectl apply / delete / rollout restart …, helm upgrade, ssh withsystemctl restart/rm…, SQLinsert/update/delete/drop…,git push, and the policy's own patterns);ask(the user confirms) forask; a warning inadditionalContextinstead ofdenyinwarnmode. It is a safety net against mistakes, not a sandbox: it knows only the names listed on the board. Details and the full rule table: SPEC §7. - Journal — every guarded tool call that touches a live environment is recorded as a
live.actionevent (tool, a short redacted command, decision) on your bound task; the owner sees them under Activity → Actions on live and in the task dialog.
Policy¶
What agents may do, what the guard counts as a change and what counts as a secret come from a
policy: a built-in preset (default, strict, trusted), the global policy.json, and project
and environment layers, a later one winning. policy_get shows the effective values and which
layer set each; policy_propose asks the owner for a change. Test secrets (harmless test values
on local and test stands, only when the policy allows them) are never sent to you unless the
policy sets expose_test_secrets_to_agents. Keys and presets: policy reference.
Memory and documents¶
Project documents (spec, status, brief, runbooks, ADRs), decisions, memory notes and blocks live in
the database; the board shows them on Docs & rules (/p/<project>/docs). Every write that
holds a secret value (a key, a token, password: …) is refused with the detector's name, never
the value. Who may read what (coordinator everything, a manager its project, a session its task)
is not enforced yet: every client with the token sees everything.
| Tool | Arguments | Returns or does |
|---|---|---|
doc_list |
project |
The project's documents: name, title, kind, version, last change, resource URI. |
doc_get |
project, doc, section?, version? |
The markdown (or one section: its heading and everything up to the next heading of the same or a higher level), after a header line with the current version to pass as base_version, and the section anchors. Anchors come from # headings: ## Gift cards → gift-cards, a repeat → gift-cards-2. |
doc_update |
project, doc, body, reason, base_version, section?, title?, kind?, session?, session_title?, hook_session? |
Writes a new version (document.updated). base_version 0 creates the document (title: title, else its first # heading, else the name; kind default doc). With section, body replaces that section: starting with a heading it replaces the heading too, otherwise only the text under it. A base_version that is not the current version saves nothing and fails with the current version number and the current text (or section) to merge. The same text again writes no version. |
doc_history |
project, doc, from_version?, to_version? |
The versions newest first (author, time, section, reason, size, base version); with both versions also their unified diff. |
decision_list |
project, task?, query?, include_superseded?, limit? (default 20) |
Decisions newest first (with query, best matches first), with source and superseded by #n. Current ones only unless include_superseded. |
memory_add |
scope (global / role / project), kind (user / feedback / project / reference), title, body, project?, role?, tags?, source?, review_at?, op?, target?, base_version?, force_new?, session?, session_title?, hook_session? |
Without op: looks for current notes of the same scope sharing a word with the title; if there are any, writes nothing and lists them (id, title, first line, title similarity). Call again with op = add, update (rewrites target; needs base_version = the candidate's v<n>, a newer version is a conflict), supersede (a new note replaces target, which stays in history) or noop. With no candidates it adds directly. A strong match is a candidate whose stemmed title words overlap the new title's by at least 0.6 (shared / all, stop words left out); op=add next to one needs force_new=true. memory.added / memory.updated. |
memory_update |
id, base_version, title?, body?, tags?, review_at? ("-" clears tags and the date), session?, session_title?, hook_session? |
Changes a note (memory.updated); a stale base_version fails with the current note to merge. A superseded note cannot be changed. |
memory_get |
id |
The note in full (scope, kind, version, author, time axes, source, tags, review date, read count); reading it counts (last_read_at, read_count). |
memory_search |
query, scope?, project?, kinds?, limit? (default 10) |
Hits over notes, decisions, documents (current versions), reports and comments: kind, id, project and task, title, and the line that holds most query words (line 14: …). kinds picks among note, decision, doc, report, comment; scope narrows to notes of that scope. With project: its items plus global and role notes. Superseded notes and decisions are left out. |
block_get |
label?, project? |
A block (label, version, characters used and limit, owner-only mark, text), or the list of blocks of the project or the global scope. |
block_set |
label, body, base_version, project?, description?, char_limit?, session?, session_title?, hook_session? |
Writes a block (block.updated); base_version 0 creates it (limit 2000 characters unless char_limit). Over the limit it fails with the count; a stale base_version fails with the current text. Owner-only blocks are read-only here: the owner edits them on the board. |
Search uses SQLite FTS5 when the SQLite build has it: words match by prefix and by their Snowball
stem (English, or Russian for Cyrillic words), diacritics are ignored, a title hit ranks highest.
User text is turned into quoted words, so ", (, AND, * never cause a syntax error. Without
FTS5 the same search runs as LIKE substring matches of each word or its stem (case-insensitive
for ASCII letters only).
Resources¶
relaybay://project/{slug} — the same markdown as project_snapshot, so a session can attach a
project snapshot as context.
relaybay://project/{slug}/environments — the project's environments map (same as
environments_get); references only, never secrets.
relaybay://rules — the global working rules (same as rules_get()).
relaybay://rules/{project} — a project's working rules, merged (same as
rules_get(project=…)).
relaybay://doc/{project}/{doc} — a project document's current markdown (same as doc_get).
relaybay://schema/task — the editable task fields as a markdown table (field, type, description,
how to clear it), generated from the same list the server validates writes against.
Working rules¶
One structured document with a fixed set of rule kinds:
| Key | Meaning | Default |
|---|---|---|
report_format |
first_line (the pattern shown to agents), states, sizes (subsets of the built-in states and sizes) |
state · size · gist, all states, S/M/L |
stage_requirements |
per target stage, a list of requirements: {kind = "report_since_stage_change"} (a report added since the task's last stage change), {kind = "field_present", field = "<task field>"} or {kind = "plan_accepted", unless_size = ["S"]} (an accepted plan unless the task's size is listed; see Plan) |
dev: an accepted plan unless size S; test: a report since the last stage change; done: progress |
description_template |
markdown template for new task descriptions (shown in the rules) | none |
guidance |
free-text markdown shown to agents | one short paragraph |
Layers, merged in this order:
- built-in defaults;
- the global
[rules]table inconfig.toml(see the README); - a project override: a row in the table
project_rules(project_id,body= the same document as JSON,updated_at). There is no write tool yet; insert the row with any SQLite client, e.g.INSERT INTO project_rules (project_id, body, updated_at) SELECT id, '{"stage_requirements": {"test": []}}', datetime('now') FROM project WHERE slug = 'acme-shop';
Each layer may be partial. report_format merges per key, stage_requirements per stage (a stage's
list replaces the earlier list; [] drops the stage's requirements), description_template
replaces; the config guidance replaces the built-in text, a project's guidance is appended after
the global one. Every layer is validated (unknown keys, stages, rule kinds or fields are errors
naming the key). An error in config.toml stops the server at startup. A broken project override
(invalid JSON or rules) never stops the server or the project: it is logged once as a warning,
the project uses the global rules, and rules_get / relaybay://rules/{project} and the first
line of project_snapshot add "project override ignored:
The rules hash is the first 8 hex digits of the SHA-256 of the merged document plus rules_mode
(canonical JSON), so it changes exactly when the rules an agent reads change, including a switch
between warn and strict. project_snapshot starts with
Rules <hash>: accepted plan before dev, report before test, progress before done (read
relaybay://rules/<project>).
rules_mode (config, default warn) decides what an unmet stage requirement does: warn lets the
write through with a warning, strict refuses it.
Schema changes and long-running sessions¶
An MCP client keeps the parameter schema of a tool it has already loaded. A Claude Code session
that was running before relaybay was upgraded can reconnect after a server restart and see new
tools, while an existing tool (such as task_update) keeps its old parameter list, so a new field
looks unavailable. Two ways out:
- Use
task_patch(task, fields={...})for fields your client does not know yet:fieldsis a plain JSON object, validated on the server with the same rules astask_updateandtask_set_stage. The allowed names are listed at the end oftask_get("Editable fields: …") and in the resourcerelaybay://schema/task(name, type, description, how to clear). - Refresh the schemas: reconnect the server in the client (
/mcpin the Claude Code CLI) or start a new session.
Example¶
project_upsert(slug="acme-shop", name="Acme Shop")
task_create(project="acme-shop", title="Checkout page", branch="feat/checkout",
description="Let customers pay by card without leaving the shop.",
session="local_8f14e45f-ceea-467f-a0b6-1f1a2b3c4d5e", session_title="Checkout")
→ Created **acme-shop-1** “Checkout page” · stage plan · version 1.
task_set_stage(task="acme-shop-1", stage="dev", next_step="Card form", waiting_on="-",
session="local_8f14e45f-ceea-467f-a0b6-1f1a2b3c4d5e")
report_add(task="acme-shop-1", body="verified · M · checkout works\n\nTests pass.",
progress="- Cart with totals\n- Card form with validation\n- Tests",
next_step="Review the PR", waiting_on="reviewer",
session="local_8f14e45f-ceea-467f-a0b6-1f1a2b3c4d5e")
→ Report #1 added to **acme-shop-1** (verified · M): checkout works
task_unbind_session(task="acme-shop-1", session="local_8f14e45f-ceea-467f-a0b6-1f1a2b3c4d5e")
→ Unbound Checkout (local_8f14e45f-ceea-467f-a0b6-1f1a2b3c4d5e) from **acme-shop-1** “Checkout page” · stage dev · version 4.