A

Autonomous Agents Knowledge Module

Autonomous Agents Fleet Operations — Knowledge Module

Autonomous Agents knowledge module — UI selectors, data model, and page states documenting Fleet Operations.

Available free v1.39.0 Browser
$ sidebutton install agents
Download ZIP

Fleet Operations

Operational workflows for autonomous agent fleets: repo management, environment updates, QA validation, SE implementation, PM epic analysis and breakdown, SD coverage improvement and file discovery, experiment scoring, public data collection, and session handoff — resuming a stopped job step's session on another agent.

Evidence Publishing

Workflows that produce evidence — screenshots, logs, mocks, diagrams, datasets, reports — save each file and publish it during the session via the publish_artifact tool (an agent-local sidebutton-server tool; SCRUM-1606). The tool uploads the file, attaches it to the job's ticket — a tracker issue or a portal issue (see Portal Tickets below) — and returns a download_url: a tokenized, no-login, revocable link. The agent cites that link inline in its single resolution comment, so one comment carries both the outcome and its evidence — no separate "📎" attachment comment is posted for agent-initiated publishes.

  • Fallback: if publish_artifact is unavailable (e.g. an older sidebutton build without the tool), save each file under ~/workspace/artifacts/ — the Stop hook uploads leftovers after the session for post-run collection. The convention degrades gracefully; agents never block on the tool.
  • By role: QA workflows (agent_qa_validate / _new_ticket / _regression / _followup / _pass) publish evidence unconditionally, as does agent_docs_verify (build output, walked-page screenshots, scan output). SE workflows (agent_se_rca / _rca_fix / _work) publish only when they produced a mock, diagram, screenshot, or report — their primary deliverable is a PR, which is also why agent_docs_write publishes nothing: its screenshots are page assets committed to the PR, not evidence. agent_pm_research may publish charts/datasets/reports alongside its cited sources.
  • Exception — app_edit_session: it publishes nothing, and its boot turn captures no screenshot at all. The portal keys the session's connected state on the boot job completing (the ready report) plus its liveness probe — never on an artifact upload (SCRUM-1965), so no evidence file is load-bearing here and the session posts no ticket comment. Evidence a later turn produces drops in the workspace's own <entry_path>/artifacts/ for the post-session lane, which globs exactly ONE directory — the first of <cwd>/artifacts, ~/workspace/artifacts, ~/artifacts that exists. Since v3 (KAN-64) the session's cwd is the workspace root and the workflow creates that folder before Claude starts, so the first candidate is the workspace's folder whatever the workspace is called; ~/artifacts stays shadowed wherever ~/workspace/artifacts exists. Never inside a checkout: those trees are pushed every turn.
  • Verdict-gate safety: the pipeline string-matches the resolution comment for verdict tokens and forbidden tokens (pass/fail/merged/blocked/conflict). A download_url is inert to that gate — the share token is random hex and cannot spell a verdict word — so citing links never disturbs verdict parsing. Keep the QA PASS/FAIL token intact and last, keep evidence filenames free of verdict words, and note that agent_experiment_score (which forbids those tokens outright) does not publish and is excluded from this convention.

Design Links (Figma)

A job whose ticket, hint or goal carries a figma.com link — a frame, a prototype, a board or a deck — loads skill://figma.com/_skill.md and then skill://figma.com/frame-to-code/_skill.md before it reads the design, and reads it through the workspace's figma MCP server (get_figma_data, download_figma_images), never in the browser: the agent VM carries no Figma session, so a browser only reaches Figma's sign-in wall. The portal adds that server to .mcp.json, and delivers the FIGMA_OAUTH_TOKEN it runs on into ~/.agent-env, only while the account's Figma connection is connected and healthy (FI-3 / DEV-119). When the figma tools are missing, the resolution comment says the design could not be read, rather than presenting work built from the ticket's text alone as if it matched the frame.

Portal Tickets

Since KAN-82 the portal keeps issues of its own — the Tasks pool without a tracker — and a job dispatched on one carries the issue's API URL as {{ticket_url}}: <portal origin>/api/issues/<KEY>, e.g. https://sidebutton.com/api/issues/DW-12 (D12). A /api/issues/ URL is the whole signal that the ticket is a portal issue. Everything the prompt asks of a ticket — read it with attachments and all comments, post ONE comment as the last action — is done over HTTPS with curl: there is no MCP tool and no browser page for it (D10), and the gate reads the result exactly as it reads a Jira comment.

Identity. Two headers on every call: Authorization: Bearer $SIDEBUTTON_AGENT_TOKEN and X-Agent-Name: $SIDEBUTTON_AGENT_NAME — both already in ~/.agent-env, the same pair the collect callback and publish_artifact send. The token decides who you are; the name must equal the token's agent (a missing or different name is a 400). The token reaches your owner's account and the account of the job you are working; any other key is a flat 404. SIDEBUTTON_API_URL — the portal's public origin, delivered beside the token (KAN-83); PORTAL_URL is its older twin and the fallback on a VM that predates it — is needed only to build a URL from a bare key ($SIDEBUTTON_API_URL/api/issues/<KEY>); ticket_url is absolute, so a dispatched job never needs it.

The four calls — TICKET_URL is {{ticket_url}} verbatim:

# read — ONE markdown document: header, ## Description, ## Attachments (n), ## Comments (n, oldest first)
curl -sS -H "Authorization: Bearer $SIDEBUTTON_AGENT_TOKEN" -H "X-Agent-Name: $SIDEBUTTON_AGENT_NAME" -H "Accept: text/markdown" "$TICKET_URL"
# comment — comment.json is {"body": "<markdown>"} → 201 {"id": <n>, "url": "<TICKET_URL>/comments/<n>", "comment": {…}}
curl -sS -X POST -H "Authorization: Bearer $SIDEBUTTON_AGENT_TOKEN" -H "X-Agent-Name: $SIDEBUTTON_AGENT_NAME" -H "Content-Type: application/json" --data-binary @comment.json "$TICKET_URL/comments"
# edit — the same {"body": …} at the comment's own url → 200 {"comment": {…}}
curl -sS -X PATCH -H "Authorization: Bearer $SIDEBUTTON_AGENT_TOKEN" -H "X-Agent-Name: $SIDEBUTTON_AGENT_NAME" -H "Content-Type: application/json" --data-binary @comment.json "$TICKET_URL/comments/<id>"
# delete — soft-delete, idempotent → 200 {"ok": true, "id": <n>, "deleted_at": "…"}
curl -sS -X DELETE -H "Authorization: Bearer $SIDEBUTTON_AGENT_TOKEN" -H "X-Agent-Name: $SIDEBUTTON_AGENT_NAME" "$TICKET_URL/comments/<id>"
  • Read. Accept: text/markdown (or ?format=md) returns the whole ticket as one markdown text: # <KEY> — <title>, then type · status · workspace · blocked-by · URL, ## Description verbatim, ## Attachments (n) as - `file.png` (image/png, 12.1 KB) — <url> · share: <share_url> — uploaded <when> by … and ## Comments (n, oldest first) as ### Comment <id> — agent `<name>` · <created_at>[ · edited <when>][ · step <n>] followed by the body verbatim. Without the header the same GET returns the JSON document — attachments[].url / .share_url, comments[].id / .url / .run_step_id — when you would rather parse than read. Attachments are not inlined: fetch each from the URL the document lists, with the same two headers ($TICKET_URL/attachments/<id>, -o <file>; ?download=1 forces an attachment disposition). The share: twin (…/api/artifacts/shared/<token>) serves the same bytes to anyone holding the token, no headers at all — that is the link a comment cites.
  • Comment. Write the body to a file and build the JSON from it, so newlines and quotes survive: jq -n --rawfile body comment.md '{body: $body}' > comment.json. The body is markdown (≤ 64 KB) — rendered as markdown on the task, stored verbatim in the document: no Jira wiki markup, no ADF, no [^file.png] / !file.png! inline-attachment references. Cite evidence by URL: the download_url that publish_artifact returns (it attaches to a portal issue exactly as to a tracker ticket), or the share: link of a file you uploaded yourself (POST "$TICKET_URL/attachments?filename=<name>" with the raw bytes and the same two headers → 201 {"id", "filename", "mime", "size", "url", "download_url"}; 25 MB per file, 50 per issue). The 201 of the comment carries its id and url — keep them, the edit and delete calls take the id.
  • Step stamp. An agent comment is stamped with the job step it was posted from (run_step_id, the · step <n> of the rendering): your currently running step by default, or the step of the Claude session named by an explicit ?session_id=<id> on the POST ("$TICKET_URL/comments?session_id=<id>" — the id is .session_id of ~/.sidebutton/job-context.json, the same source publish_artifact reads; a session that is not yours falls back to your active step). The gate never reads a comment stamped for ANOTHER job (a previous run's report on the same issue), never an operator's reply, and never a comment an earlier step of this run already used as its verdict — so none of them can be mistaken for yours; an unstamped agent comment inside the step's window still counts. A dispatched job needs nothing beyond the default; pass session_id only when job-context.json names a session and you want the stamp explicit.

The report rules — the same contract as on Jira, stated for the API:

  • ONE report comment, posted by you, as the last action, the verdict token alone on its last line. The gate takes your LATEST comment on the issue created after the step started, as it does on Jira — no comment → the conservative pause with the re-check window; a later comment does not add to an earlier one, it becomes the verdict in its place.
  • Fix by editing, never by posting again. A PATCH keeps the comment inside the gate window — the gate re-reads the edited body on its re-check, so a corrected verdict line routes. Only your own comments are editable (403 otherwise), and an already-deleted one is 404.
  • Delete withdraws. A DELETE soft-deletes: the comment leaves the document and the gate never reads it again — the step then has no verdict until you post a new report.
  • The thread is the ticket's decision record, as on a tracker: read the operator's comments (user … in the rendering) and never edit or delete anyone else's — the API refuses it anyway. Do not PATCH the issue's status: transitions belong to the run and the operator.
  • Every other rule in this file holds unchanged — the verdict vocabulary below, evidence filenames free of verdict words, the agent_ops_validate_resolution comment shape, agent_experiment_score's single JSON comment.

Gate-Verdict Vocabulary (metadata.verdicts)

A workflow that ends in a machine-matchable outcome declares it in its YAML — metadata.verdicts: [TOKEN, …] (UPPER_SNAKE). The portal syncs the declaration with the workflow and uses it as the playbook gate vocabulary (PLAYBOOKS.md §5): a playbook step running the workflow can content-route on those tokens, matched from the step's single resolution comment. Undeclared ⇒ the step is presence-gated (any comment advances; text never inspected).

WorkflowDeclared verdicts
agent_qa_new_ticketBUG_CONFIRMED · NO_BUG
agent_qa_validatePASS · FAIL
agent_qa_followupPASS · FAIL
agent_qa_regressionPASS · FAIL
agent_qa_passPASS (pass complete — every finding posted on the epic) · FAIL (pass incomplete — pauses) · HANDOFF (session handoff, bare line only) · BLOCKED
agent_qa_consolidateISSUES_FILED · NEEDS_APPROVAL (proposal posted — pauses for a person) · NOT_READY (a QA child still open — pauses) · BLOCKED
agent_se_workPR_OPEN · WORK_CONTINUE (step continuation — NOT-DONE handoff, token-only) · BLOCKED
agent_se_rca_fixPR_OPEN · BLOCKED
agent_se_review_mergeMERGED · CONFLICT · CI_FAIL
agent_se_reviewREADY_TO_MERGE · NEEDS_CHANGES
agent_se_followupFEEDBACK_APPLIED · BLOCKED
agent_docs_writePAGES_READY · STALE_SKILL · NEEDS_DECISION
agent_docs_verifyPASS · FAIL · BLOCKED
agent_pm_goal_analysisREADY_TO_PLAN · NEEDS_DECISIONS · NO_CHANGE
agent_pm_breakdownISSUES_CREATED · ISSUES_RECONCILED
agent_ops_validate_resolutionVALIDATED · INCOMPLETE · UNVERIFIABLE
agent_pm_landing_scopeGATES_ALIGNED · NEEDS_INPUTS
agent_pm_landing_callsNEEDS_DECISIONS · NO_CHANGE
agent_pm_wireframeREQUIREMENTS_READY · NEEDS_INPUT
agent_se_design_checkDEPS_CLEAR · BLOCKERS_FOUND
agent_design_assembleMOCK_READY · NEEDS_INPUT
agent_docs_scopeGATES_ALIGNED · NEEDS_INPUTS
agent_docs_decisionsNEEDS_DECISIONS · NO_CHANGE
agent_docs_postdeployLIVE_OK · DEPLOY_PENDING · DRIFT_FOUND
app_edit_sessionSESSION_READY · BOOT_FAILED

The five landing/design workflows serve the landing-website pack, which documents their contracts, roles and gates — the YAMLs live here because this is the one directory the default-pack sync registers workflows from. Three are ticket-anchored playbook steps (wireframe → design check → assemble); agent_pm_landing_scope and agent_pm_landing_calls anchor on {{goal_url}} instead of ticket_url — no issue exists at the goal's scope/decide phases. agent_docs_decisions is goal-anchored for the same reason: it runs the decide phase of a docs-site goal (the docs question class — visibility · locale · canonical section naming · retire/redirect per stray page · canonical demo tenant), and it is the first goal-anchored workflow whose deliverable also includes a PR — the DOCS-INSTRUCTION instance on the site repo, authored on a greenfield site and validated deltas-only on a site that already carries one.

agent_docs_scope is the third goal-anchored workflow and the first goal-anchored one with role: sd (agent_sd_coverage already carries that role, ticket-anchored): the scope phase of the docs-site goal type, one goal per target site. It sweeps the account's skill packs for freshness and publishes the type's five docs gates, together with its verdict, through the goal gates endpoint below. The sd role playbook is ticket- and discovery-shaped and says nothing about goal phases, so the prompt carries that framing itself — publish a result, create no issues.

Its five gate ids are a cross-repo contract with the docs-site type registry, frozen here so both sides key on the same tokens (blocking names the phase in the chain scope → decide → plan → build → launch):

Gate idSettlesArmsBlocking
source-allowlistwhich packs, and which modules inside them, feed this siteuser-input · workspacedecide
staleness-ledgerper source: manifest vs index version, evidence type, verdict, required action before useuser-input · workspaceplan
confidence-floorthe minimum module confidence a page may be written from, and the modules under ituser-input · workspace · platform-floor (0.70)plan
sitemap-draftthe page inventory derived from the module inventory, with its declared coverage gapsuser-input · workspaceplan
deploy-targetwhere the site publishes, and who controls each hop of the deploy chainuser-input · workspace · platform-floor (a SideButton-hosted docs subdomain)launch

agent_docs_write and agent_docs_verify are the two per-page steps of the docs-page playbook (write → verify → agent_se_review_merge, with the verify FAIL routed back to write). They split deliberately along the source they trust: write derives the page from the content packs the ticket maps and refuses to write at all when those are stale, below the confidence floor, or unreconciled between index.json and the pack manifest; verify judges the result against the audit pack's per-module _roles/qa.md and the live product, never re-reading the content packs — checking a page against the same modules it was generated from confirms nothing. Both prompts carry a reporting-discipline clause, and neither is optional. Write's three tokens are fresh, so they are matched token-only and cannot collide with prose — but token-only matching is not the whole risk: matchVerdict scans the WHOLE comment and returns the first hit in gate order (retry → loopback → pass → stop → pause), so write's PAGES_READY (pass) outranks STALE_SKILL/NEEDS_DECISION (pause) wherever it appears. One stray mention in the body advances a run that wrote nothing — the unsafe direction — which is why write names none of its tokens outside the last line. Verify reuses the curated PASS/FAIL/BLOCKED, which additionally keep their case-insensitive prose tiers, so it reports every count neutrally ("dead links: 0"); there FAIL sits in loopback ahead of pass, so a stray hit costs a wasted loop rather than a false advance. agent_docs_write is also the one workflow here declaring role: writer — and writer is not a builtin role, so it needs registering before a docs-page step is pickable. Declaring it here is necessary but not sufficient: role auto-registration (registerPackRoles) runs only on an ACCOUNT pack sync, over that account's own registry clone; the global catalog sync that ingests this repo discards the roles it collects. So this pack's roles array and metadata.role get the workflow registered with the right role string, but they never mint the account registry row that the role pickers and auto-dispatch read. That row comes from a builtin seed (how design was enabled, ahead of the landing pack shipping it) or from an account pack carrying _roles/writer.md — one of the two has to happen, and it is not this pack's to do.

agent_docs_postdeploy serves the docs-site goal type (the Documentation Agent) and is the one workflow here declaring both anchors: it takes goal_url and ticket_url, each defaulted to "", and exactly one is set per dispatch. The goal type's Launch phase runs it goal-anchored over every route the goal changed; the docs-page playbook's optional last step runs it ticket-anchored over the one page that ticket delivered. Both dispatches ask the identical question, so a second id would have been a copy — and the type's ship lane pins a fixed set of agent_docs_* ids. Its DEPLOY_PENDING is a hold, not a failure: on a target whose publish hop is batched and release-driven (source → OSS mirror → Pages, for our own docs site), it is the correct pre-sync answer and the phase waits for the sync rather than failing.

app_edit_session is the one id here without the agent_ prefix, and deliberately so: executePipeline writes the exact string into ~/.sidebutton/job-context.json (.workflow_id), the portal floor entry keys on it, and SP-D's per-turn Stop-hook branch (SCRUM-1937) will read it from there — the id is a frozen cross-repo contract. It is also the only workflow that is not ticket-anchored — it takes a target (the workspace, or one checkout inside it), not a ticket_url, and its verdicts describe the boot turn (SESSION_READY once the agent has oriented on the target and posted the ready report — since v3 (KAN-64) the boot starts no dev server and touches no network, so the report itself is the readiness — and BOOT_FAILED only when the target folder is missing). Like every other workflow here its terminal.open cwd is {{entry_path}} (v3; v2 opened in the app folder): the target is told to the agent in the prompt, never cd'd into, so the workspace's own setup — .claude/skills, settings, shared/ — stays in play. Its later chat turns emit neither token: they are conversation on a live session, not gated step results — including the on-request dev-server turn, whose contract lives in the dev-session module.

agent_qa_pass is the quality lane's multi-session job and the only QA workflow whose vocabulary carries a continuation token. The four older QA ids are single-session by contract: none reads the ticket's comments for a checkpoint, none files defects as tracker issues, and agent_qa_validate / agent_qa_followup still ask for the PASS/FAIL words inside their results table — already a wasted loop wherever a cell reads FAIL above a genuine Verdict: PASS, and a sharper hazard the moment a playbook routes a third token, since matchVerdict scans the WHOLE comment and returns the first hit in gate order (retry → loopback → pass → stop → pause), so a stray FAIL — the QA loop-back verdict — outranks a genuine PASS on the last line. Changing them would alter the final step of every delivery seed on every account, so the QA-only lane got its own id instead. HANDOFF is its session-continuation token: fresh, matched as a bare line only (the prompt also uses the word as the name of its handoff shape), and routed through gate.retry — the portal's CONTINUATION_WORKFLOWS map names this job with it, so the QA Pass seed re-dispatches the same pass to the next session, up to the step's max_attempts; an account playbook that chains a closing step may route it through gate.loopback instead. Either way the next session resumes from the handoff block's remaining-checklist instead of re-walking the area, and waiting on runs the session started is done in-session, never by handing off. Bare-line matching is what makes the re-dispatch safe — retry and loop-back verdicts are match-tried ahead of pass verdicts, so a token with a prose tier would re-run a step that had finished. The verdicts mean this: PASS says the pass is COMPLETE — every scenario walked, every defect filed as a Bug — so a filed defect never lowers it; FAIL says the pass itself is incomplete or its results cannot be trusted, and the QA Pass seed routes it to pause (a person resumes), never to Finish. A session that finds a verdict line already on the task is the closing one: it re-checks the acceptance criteria and the recorded defects once, completes what was left incomplete, and posts the final verdict. Since 1.32.0 it files no tracker issues: every defect, observation, stale pack or missing fixture profile is a numbered finding (KEY-Fn) posted in one block on the ticket's parent epic before the report — the gate reads only the ticket's latest comment, so the block is invisible to routing — and agent_qa_consolidate, the epic-level close-out run once every QA child is done, builds the ledger from those blocks, merges what shares a root cause or a surface, posts a proposal and pauses (NEEDS_APPROVAL), then files the Bugs and the SD tasks on the operator's re-run (ISSUES_FILED; NOT_READY when a QA child is still open; a hint of auto-file skips the pause). Its four tokens are pack-declared, so they match their explicit form only, and the QA Consolidation seed routes all four. The pass's report shape is neutral for the same reason agent_docs_verify's is: cells read ok / defect (Fn) / n/a, and the verdict token appears exactly once, on the last line.

agent_sd_files is the second discovery workflow here carrying role: sd (agent_docs_scope and agent_docs_decisions carry it too, as goal-anchored docs-site phases — four in all), and the first scoped to a file set rather than to repos — a handbook, a price list, a policy folder, a synced Drive folder. It is a separate id rather than a fourth target mode on agent_sd_coverage because every mode there opens at README-first target resolution, and a file set has no README to resolve: the prompt starts at the set it was given and runs the sd playbook's File Discovery Harness over it — the extracted text as the snapshot analog, a heading path plus page anchor as the selector analog, and confidence capped with its reason named for anything it could not fully read, the same honesty rule a mobile app without an emulator gets. What it emits is ordinary modules carrying numeric confidence frontmatter, so recomputeSkillDomainsFromSource counts them into skill_domains / skill_modules and the Skills pages render them with no new display code. It is an enrichment beside a file pipeline, never a stage inside one: a portal that accepts a file extracts, chunks, summarises and indexes it in process and answers from it on the next question with nothing dispatched, so no upload may ever wait on a run of this job (the-assistant PLAN-kadmo-chat-component.md §3.2, §4; KAN-229).

Free-form workflows (agent_se_rca, agent_se_plan, agent_pm_research, agent_sentry_triage, agent_sd_coverage, agent_sd_files, agent_pull_repos, agent_self_update, agent_wipe_workspace) deliberately declare nothing. So does agent_continue_session, for the opposite reason: it has no outcome of its own — the portal gates it with the vocabulary of the workflow it continues (Session Handoff, below). agent_experiment_score must NEVER declare a vocabulary — its comment is parsed as JSON and its prompt forbids verdict-looking tokens outright. The reserved engine-synthesized verdicts (COMMENT_POSTED, JOB_COMPLETED, JOB_FAILED, JOB_CANCELLED) are refused at sync and must not be declared.

Session Handoff (agent_continue_session)

A job step whose Claude Code session stopped — its window closed, the agent went offline or restarted, a provider quota ended it, or an operator wants it elsewhere — can be resumed instead of re-run: the portal's "Resume session on…" (the-assistant POST /api/jobs/:id/continue, DEV-53 · SH-3) closes the source step, takes a bundle of its session, and dispatches agent_continue_session as a new job on the chosen agent (the same one or another, under the same agent app or a different one — the transcript is provider-neutral). That job resumes the transcript as a fork under its own session id, so it carries the whole context — the findings, the decisions, the tool outputs, the sub-agent reports — rather than a 600-character prose handoff. Plan: the-assistant docs/plans/PLAN-job-continue-on-another-agent.md.

Continuable — a step that has a session_id and a transcript the portal can serve: the source agent's live export (preferred), else the last checkpoint or final upload it holds. Not continuable — a step that died before a session existed ("Workflow not found", a missing cwd); the maintenance workflows in NO_CONTINUE_WORKFLOWS (agent_pull_repos, agent_self_update, agent_wipe_workspace — short and idempotent, re-run them); an app_edit_session whose boot turn completed (a chat session that ended is reconnected, not resumed); a step already continued (unless that continuation died without a transcript of its own).

Lineage. The continuation job carries continued_from_job_id / continued_from_step_index / continuation_reason, the source step continued_by_job_id; both are stamped when an agent claims the row. In a playbook run the continuation is an attempt of the SAME step — it is re-attached to the run step, counted against the same max_attempts as a prose continuation (a continuation event with mode: "resume"), and the gate reads only comments created after its new started_at. A continuation is never re-sent or cloned; resuming it again is its retry.

Verdicts and role. The YAML declares role: ops and no verdicts: the enqueue passes the source row's role, and the gate uses the vocabulary of source_workflow_id (a continuation of a continuation keeps the original workflow). The continuation prompt tells the model to keep the original prompt's execution rules, verdict vocabulary and report format — the source workflow's final comment is still the deliverable.

The bundle (GET /api/jobs/<source_job_id>/session-bundle?step=<n>, served only to the agent holding the continuation — any other token gets a 404) is a tar.gz with manifest.json (v1: source, transcript with path/bytes/last_entry_at/stale and on a snapshot sha256, companions, workspace[] with repo/branch/head/pushed/delta_patch/delta_omitted), the main transcript, its subagents/ and tool-results/, .claude/file-history/<sid>/, and one workspace/<repo>.patch per dirty repo (git diff HEAD plus untracked source files, ≤ 5 MiB, else delta_omitted). Paths are relative to $HOME and scoped to the source session id. It never contains ~/.agent-env*, ~/.sidebutton/job-context.json or the session-heads/branches files — identity and credentials come from the target's own dispatch. Ceilings: 64 MiB of gzip, 512 MiB unpacked. A snapshot (the source agent was unreachable) holds the main transcript only, under .claude/projects/sbjob-<sid>/, with transcript.stale: true.

What the workflow does, every step printed as a handoff: line in the job terminal:

  1. Validates the params it needs before the launch (digits, a UUID), reads the entry path from its cwd, and sources ~/.agent-env plus the app env like every ops workflow.
  2. Fetches the bundle with SIDEBUTTON_AGENT_TOKEN + X-Agent-Name.
  3. Verifies it before anything is extracted: the 64 MiB ceiling; the bundle digest when the portal sends one (X-Session-Bundle-SHA256); gzip -t, the trailer's CRC32 and length — the one checksum every bundle carries, which the tar reader never reaches; then one python3 pass over the archive — regular files only (no links, devices, .. or absolute paths), every path inside the session's allow-list (the exporter's own isAllowedBundlePath), no duplicates, a v1 manifest naming this session, the transcript's bytes and (on a snapshot) sha256, and each companion's file count and bytes. Any miss prints one clear line and launches nothing; the window stays open 5 minutes (SB_HANDOFF_FAIL_HOLD_SEC) so the reason can be read. A restore that fails part-way (a full disk) removes the files it had placed, so a retry never mistakes a half-written copy for this agent's own.
  4. Stages the files under a fresh ~/.sidebutton/handoff/<new session id>.XXXXXX/stage/ of its own — the new id read off its tmux session (sbjob-<id>), job-context.json only as the fallback, since one file per agent may already name a neighbour's dispatch — (week-old handoff dirs are pruned; after the restore only the manifest and the patches stay) and restores .claude/** into $HOME, keeping what this agent already holds except a shorter copy of the transcript. When this agent still has a fuller copy of the session's transcript than the bundle (a same-agent continuation from a snapshot), it resumes from that one and says so.
  5. Restores each checkout under the entry path — a repository's own top level, never a directory inside another repo. Back on the agent the session ran on with the checkout still at its branch and head and the delta still in it (the patch reverse-applies), the tree is left exactly as it is (it already holds the session's work, including what an export leaves out). Otherwise: git fetch (no credential prompt, a timeout); what to restore to is settled next — the recorded head when the clone has it, else origin/<branch>, and with neither the tree is left exactly as it is; then git stash push -u when the tree is dirty (named in the prompt: the session's own work on the same agent, another job's elsewhere), a local branch tip the reset would orphan kept at refs/sb-handoff/<sid>/<branch>@<tip>, git checkout -B <branch> <head> + reset --hard, the upstream set to origin/<branch>, then git apply --3way of the delta, unstaged afterwards. A conflicted apply names its conflicted files; a refused one says nothing of it is in the tree; a repo the export could not diff (error), files it left out (untracked_skipped) and a repo with no commit yet are named too. A head not in the clone falls back to origin/<branch> — told apart as never pushed, rewritten, or a failed fetch; a missing repo never aborts — every outcome becomes part of the prompt, and no restore is ever claimed for a repo this agent did not restore. A repo it did not restore (an SE job's linked worktree <entry>/.worktrees/<ticket> is missing on any other agent) is named with the path of its kept patch, so the session can recreate the checkout and apply it.
  6. Launches claude --dangerously-skip-permissions --resume <restored transcript> --fork-session "<continuation prompt>"; the daemon injects --session-id <new uuid>, so every hook reports the continuation as its own step.

The continuation prompt (plan §4.6, under 2000 characters — the opening line and the closing rules with the verdict vocabulary and the ticket always whole, the restore/stale middle clipped next, the hint last) is assembled from the placeholders the portal resolved at enqueue — source_agent, moved_at, reason, branch, head, delta_note, stale_note, hint — with the agent's own restore result winning over the portal's delta_note, and the manifest filling a placeholder the portal left empty. On the stale path branch/head/delta_note are empty and stale_note carries the instruction.

Editing rule for this YAML. The daemon injects --session-id at the FIRST literal claude in the step body after interpolation, so nothing above the launch line may contain it — comments included (~/.claude and would match) — and every free-text param (hint, the placeholders) is read in a heredoc BELOW the launch token. scripts/test-ops-continue-session.mjs pins this, together with the AC3/AC4 cases: fixture bundles (export + snapshot) from a stub portal, a stub claude, and tampered bundles that must abort.

CLAUDE_CODE_PROJECT_DIR_NAME is exported as sbjob-<new session id> per the plan, but Claude Code 2.1.283 honours it only when CLAUDE_CONFIG_DIR is set — and setting that (even to ~/.claude) moves the CLI's global config from ~/.claude.json to ~/.claude/.claude.json, losing onboarding, folder trust and user MCP servers. So CLAUDE_CONFIG_DIR stays unset and the fork lands under its cwd slug; nothing depends on the directory name (hooks read transcript_path from their input, the export finds <sid>.jsonl under any slug).

Where the transcript comes from when the source is gone. agent-runners' sb-checkpoint-transcript.sh uploads the running transcript every 5 minutes (checkpoint=1), so a session killed before its Stop is at most five minutes stale when resumed from the portal's copy.

Blocked sessions are not dead ones. A session held by a claude.ai usage limit or a provider error (plan §4.12) fires StopFailure, not Stop; agent-runners reports it as a kind=blocked needs-input row (with auto_continue when Claude Code will continue by itself at the reset) and picks "Wait here, then continue automatically" when the usage-limit menu is on screen. It waits in place and is never moved automatically — the operator cancels it or resumes it elsewhere with this workflow when the wait is not acceptable.

Publishing to a Goal ({{goal_url}} workflows)

A goal-anchored job does not publish through its prose result — the portal renders a goal's gates and calls only from the rows these endpoints write, and nothing parses the job's text. A workflow that anchors on {{goal_url}} must therefore spell the channel out in its prompt; a call described in prose alone never reaches the goal page and never locks.

agent_docs_scope and agent_docs_decisions name this channel today. agent_pm_landing_scope and agent_pm_landing_calls still say "publish the gates/calls as this job's result" and issue no POST — read them as the pattern to repair, never as the one to copy. Migrating that pair is its own change; until it lands, do not infer from their prompts that prose is a working channel.

Identity is the session_id in ~/.sidebutton/job-context.json (job_id is the fallback); auth is Authorization: Bearer $SIDEBUTTON_AGENT_TOKEN against $PORTAL_URL, both already in ~/.agent-env. A publish must prove one of those two — a call carrying only ?goal=<slug> is refused 403 ("a token is not a job"), so there the slug is an assertion, not an identity: passed alongside it only cross-checks, and one that disagrees with the job's own goal is a 404. The two read routes are the ones that do accept a bare ?goal=<slug>, since they authorise nothing against a job. Either publish lands only while its job is still its phase's live job — a superseded run gets 409, so a stale session cannot overwrite the run that replaced it.

CallPurpose
GET /api/agents/goal/read?session_id=…goal, current phase, every phase's state, aligned gates, decisions already on record, intake files[] (names only) and links[]
GET /api/agents/goal/file?name=…&session_id=…the bytes of one intake file
POST /api/agents/goal/gates?session_id=…the scope phase's gates + its verdict
POST /api/agents/goal/decisions?session_id=…the decide phase's calls + its verdict

gates takes {"verdict":…,"gates":[{"id","content","arm"}]} — arm is the rung the value resolved on (user-input, workspace or platform-floor, or null when none applies), body ≤ 256 KB, verdict ≤ 64 chars. It is all or nothing: the type's whole gate set publishes in one delivery, exactly once each, and a partial, duplicated or unknown set is a 422 naming expected / missing / unknown / duplicates that writes nothing. So a delta-aware rework re-derives selectively but publishes completely — "publish only what changed" makes every rework run fail and record nothing. A gate the user edited comes back in kept rather than being replaced — the dispatch hint marks those "(yours — keep)", and the route enforces it even when the edit landed after dispatch — while the ones the job wrote come back in published. On a scope phase GATES_ALIGNED auto-chains the decide job, but only on decide's first advance: once that phase has any history a rework republish chains nothing, and reconcile stays the operator's ⟳ door. That outcome rides the 200 as chained, which may report dispatched: false — a publish that landed behind a chain that did not.

decisions takes {"verdict":…,"decisions":[{"id","question","options","recommended","why"}]} — id and question non-empty and ids unique (duplicates are refused), body ≤ 256 KB, verdict ≤ 64 chars. Ids are the idempotency key, so they must be stable across runs. The same POST records the phase verdict, and the response reconciles three ways: published are the calls now open carrying your recommendation; kept are the ones already answered — left exactly as they stand, whether you re-stated them or omitted them; dropped are the ids you omitted that nobody had answered, and only those are deleted. An answered call you omit is not retractable this way — it comes back as kept. So re-publish the whole set every run.

answered_by is what separates the two kinds of answer, and the distinction is load-bearing. A human's answer freezes the row: re-stating it is reported kept and rewrites nothing. An auto answer is only the decide-lock having applied your own previous recommendation, so it is not settled — re-stating it publishes over the row and re-opens it, which is exactly what a ⟳ Rework run is for. Treat an auto row as an open call, not as the user's input. Every call needs a non-empty recommended unless it is already answered: decide-lock refuses a call with neither and the phase stalls.

Validation Verdict Comment Contract

agent_ops_validate_resolution is the one workflow whose comment shape — not just its verdict token — is consumed by the portal. Two portal surfaces read it, so the prompt states the order explicitly rather than leaving the judge to invent one (it previously did, and the portal parser was written against the shapes it happened to produce).

ElementOrderConsumed by
Validation verdict: <TOKEN>1st linematchValidationVerdict — the token must appear nowhere else
Evidence lines (outcome → PR/commit/file)2ndOperator read; the peek-rail validation panel renders the comment verbatim
Notes: — reported, non-verdict-bearing items3rdOperator read only
Gaps: heading + numbered list, last4thextractValidationGap — sliced heading-to-end into the re-run steering hint

Three constraints follow, and all three are load-bearing:

  • The gap list must come last and nothing may follow it. The slice runs from the heading to the end of the comment, so any closing paragraph ("Net: …", "Bottom line: …") is carried into every re-run's prompt.
  • Notes: must precede Gaps:, for the same reason — a Notes section after the heading would steer re-runs by exactly the items that are defined not to be gaps. No other line may begin with the word "gap" either: the heading match takes the FIRST line that opens with gap/gaps, and with no heading at all the fallback keeps the comment's tail.
  • The comment carries no date. The verdict's timestamp is stamped portal-side when the sweep reads the comment (task_evaluations.created_at) and displayed as the flag's age in the Tasks "needs you" band, where it is also the oldest-first ordering key. A self-stated date is redundant there and can disagree with the stamp.

The verdict judges delivery, not tracker hygiene. Unfiled follow-ups, stale description text on a re-scoped ticket, operator-accepted deferrals and in-thread findings owned by other tickets go under Notes: — they are not gaps. The portal's own gap routing already assumes this: deriveGapClass biases every deps-resolved flag to verify (confirmation-only) precisely because over-calling a flag triggers wasteful re-implementation runs.

Experiment Scoring

agent_experiment_score is the A/B experiment judge: after a forked playbook step finishes both branches, the engine dispatches it with ticket_url, branch_a_ref, branch_b_ref (each ref is a PR URL, or an instruction pointing at the branch's [exp A]/[exp B] ticket comment). It is read-only except for its single output — ONE ticket comment holding a short comparison summary plus one fenced json block with per-branch scores (coverage/correctness/quality/overall, 0–100), a recommended winner ("A"/"B"), and a rationale. The engine parses that comment into the experiment ledger; the operator makes the pick. Quality only — cost/turns/model come from job telemetry, and the judge's own model/effort are pinned engine-side for cross-experiment comparability.

Collection Workflow

agent_collect_source collects structured data from any public URL. It's a two-step workflow:

  1. Claude loads platform skill packs, navigates to the source, extracts items, saves JSON to /tmp/collect_result.json
  2. curl POSTs the file to the callback_url with agent auth headers, then cleans up

Output Contract

{
  "collect_result": {
    "source_url": "https://...",
    "platform": "truthsocial|x|whitehouse|reddit|...",
    "captured_at": "ISO8601",
    "items": [
      {
        "external_id": "post-123",
        "event_type": "post|article|executive_order|statement",
        "published_at": "ISO8601",
        "title": "...",
        "content": "Full text...",
        "url": "https://...",
        "engagement": {"likes": 100, "replies": 20}
      }
    ],
    "item_count": 5
  }
}

Auth

The curl step sends:

  • Authorization: Bearer $SIDEBUTTON_AGENT_TOKEN — standard agent token from ~/.agent-env
  • X-Agent-Name: $SIDEBUTTON_AGENT_NAME — agent identifier for tracing

The receiving API validates the token and records the agent name.