Proposed HTTP and CLI contract
Status: design, not an implemented API. Names and examples below are the proposed first-release interface. Implementation will publish matching OpenAPI schemas, CLI help, and service-delivered agent instructions from shared contract types.
Conventions
Use /api/v1 for the JSON API over HTTPS. IDs are opaque strings. Project IDs
are explicit in project operations; there is no global current project. Dates
are UTC RFC 3339 values; revision and ownership-generation fields are integers.
Pagination uses opaque cursors with a proposed default of 50 and maximum of 200
items. Responses include request_id and server_time.
server_time is the UTC wall-clock observation when the response is emitted,
for correlation and display. It can be behind protected coordinator time during
a clock incident; never calculate lease authority as expires_at - server_time.
Use server-computed lease_remaining_ms and renew_after_seconds, subtract local
monotonic request elapsed time and a safety margin, and honor explicit authority
validity flags. See clock safety.
Authenticate agents with Authorization: Bearer <agent-token>. Browser sessions
use the cookie/CSRF contract in onboarding-contract.md.
Never put authentication or session secrets in query strings. Resolve
authentication before disclosing whether a requested project/record exists.
Every retryable mutation requires an Idempotency-Key generated and persisted
before the first request. The key is scoped to principal, method, and operation
path. Its stored request fingerprint includes relevant input, expected revisions,
session identity, and a verifier for any submitted secret, never raw secrets.
Reusing the key with different input is a conflict. Receipt access still checks
current authentication and authorization.
Propose retaining full mutation receipts for at least 30 days, plus compact
principal/operation/key/fingerprint tombstones after that. A retry after receipt
expiry returns idempotency_receipt_expired and reconciliation instructions;
it cannot execute the old operation as new. A principal’s tombstones can be
removed when the principal is permanently retired and all credentials revoked.
Use these HTTP/error families:
| HTTP status | Representative code | Client action |
|---|---|---|
| 400 | invalid_request | Correct the indicated field |
| 401 | authentication_required | Show public setup help or reconnect with configured credentials |
| 403 | operation_not_permitted | Show the missing operation permission; do not re-enroll an already authenticated client |
| 404 | record_not_found | Reconcile the authenticated project’s binding/reference |
| 409 | claim_conflict, revision_conflict, lease_expired, policy_changed | Follow the supplied current-state and next-action links |
| 413 | payload_too_large | Reduce/split the report or use an external artifact link |
| 422 | requirements_unsatisfied | Address explicit blockers or validation details |
| 429 | rate_limited | Honor Retry-After; a delayed retry never extends an expired lease |
| 503 | temporarily_unavailable | Back off, retain the original mutation key, and observe the existing lease deadline |
An error body contains error.code, a concise error.message, structured
error.details, error.next_actions, and retryable. Never recommend blind
retries for expired authority, denied permission, or missing verification.
Public help, credentials, and sessions
GET /api/v1/info returns product/API/instruction versions and public setup help.
GET /api/v1/help/authentication returns human-readable enrollment steps and
non-secret configuration examples. Neither returns private project information.
An unauthenticated private request returns the same useful help reference:
{
"request_id": "request-example",
"server_time": "2026-09-09T18:00:00Z",
"error": {
"code": "authentication_required",
"message": "Configure this workstation's agent credential, then reconnect.",
"details": {"help_path": "/api/v1/help/authentication"},
"next_actions": [{"action": "show_operator_setup_help"}],
"retryable": false
}
}
Browser sign-in/out and password-change endpoints operate on local human
accounts. Admin endpoints manage principals and issue/revoke tokens. First-admin
creation and account recovery use a host-local command, never public enrollment.
All issued agent tokens have agent identity; an agent token cannot record a human
review. A project may opt into allow_subagent_reviews through its human-managed
policy. Omission in a policy update preserves the current setting; the default is
false and delegated agent rule editing cannot change it. Session registration
accepts optional subagent: {project_id, name, parent_session_id}. The server
returns subagent_identity_id and the registration fields on session reads.
Names map to durable identities within a project and principal; parent identity
is immutable, and a new identity requires an active same-principal parent.
An owning attempt can register delegated helpers using checkpoint
contributor_session_ids. Contributor exclusion is enforced at review claim and
decision, across sessions of the same subagent identity. A claim or decision
asserting review_independence distinct_launch is also refused
(reviewer_shares_launch) when the reviewer shares a launch (parent-session
root) with a contributor of the subject or its landing range, or when either
launch is nested beyond the 64-hop walk cap. The preconditions preview of a
review activity adds distinct_launch_eligible and
distinct_launch_unmet_preconditions for an agent caller, so a reviewer can
check the assertion before claiming. This is a trusted harness
policy; shared credentials cannot attest independent reasoning. See the
CLI examples.
An agent token cannot record a human approval even when a human operator created it.
Credential issuance displays a newly generated token once. Its mutation receipt
retains the issued credential ID and metadata, never a replayable token value.
If the issuing response is lost, replay returns the original issuance identity
with secret_unavailable and instructions to revoke that unused credential and
issue a replacement. It cannot silently create a second token or claim the old
secret can be recovered from its verifier.
For a harness session, the client generates and saves a session ID and a random
session proof before POST /api/v1/sessions. Send the proof in
X-Coordinator-Session-Proof; store only its verifier on the service. Return the
session ID and state without echoing the proof. Identical retries can recover the
session without a stored replayable secret. The session records its principal,
issuing credential, workstation, harness label/version, and declared capabilities.
Subsequent ownership operations send X-Coordinator-Session and the proof header
in addition to the agent token. A session belongs to its issuing credential;
revocation invalidates its authority. GET /api/v1/sessions/{id} reconciles it;
POST /api/v1/sessions/{id}/close closes it explicitly. Compaction and ordinary
turn completion do not close it or create another session automatically.
Host storage maintenance also closes idle sessions, by default after 7 days
without recorded activity in any project (see the
retention contract). A session that
holds an active attempt (even one whose lease lapsed), a live job reporter, a held
reservation, an unreconciled job or an open subagent session is never closed this
way. Each closure records an agent_session_closed event (no project) whose data
names reason: "idle", the session’s last activity and the idle period.
A closed session, however it was closed, can never be reopened. GET /api/v1/sessions/{id} still answers it with closed_at set; registering its ID
again returns 409 session_closed; every other request that authenticates with
it is refused as 401 authentication_required. The client registers a fresh
session identity and proof. agent-coordinator connect (and its mcp-client
launcher) does this automatically for a closed saved session; a standalone MCP
adapter reports session_state: "closed" and its host provisions a new session.
Connected sessions per project
GET /api/v1/projects/{project_id}/sessions?active_within_hours=24 lists the
open agent sessions bound to a project so an owner can see who is still working
and quiesce them before a test, preflight or cutover. Any authenticated
principal may read it, including read-only agent credentials and human browser
sessions; an unknown project is 404. It is derived from existing records:
- A session is bound to the project when it acknowledged the project’s instructions or holds any attempt (work, recovery, review or integration) in it. Only sessions that are not closed, whose credential is neither revoked nor expired, and whose principal is enabled are listed.
last_activity_atis the newest of the session’s registration, its instruction acknowledgment for this project, and, for its attempts in this project, their claim, heartbeat, progress and end times and their checkpoints. Reads never count as activity.active_within_hours(integer0–8760, default24) keeps sessions whose last activity falls inside the window;0lists every open bound session. A session that holds an active attempt in the project is always listed, even when that attempt’s lease has lapsed. Other values are400.
Items are ordered by last_activity_at (newest first), then session ID, and
capped at 500 with truncated: true when more match. Session proofs and token
verifiers are never returned.
{"project_id": "…", "active_within_hours": 24, "generated_at": "…",
"truncated": false,
"items": [{"session_id": "…",
"principal": {"id": "…", "name": "builder", "kind": "agent"},
"harness": "agent-coordinator-cli", "workstation_id": "…",
"capabilities": ["git"], "subagent": null, "credential_id": "…",
"started_at": "…", "last_activity_at": "…",
"held_attempts": [{"attempt_id": "…", "task_id": "…", "task_title": "…",
"generation": 1, "mode": "work", "activity_kind": "agent_review",
"subject_task_id": "…", "expires_at": "…", "last_heartbeat_at": "…",
"lease_expired": false}]}]}
subagent is {"name", "parent_session_id"} for a subagent session.
activity_kind and subject_task_id name the workflow activity (review or
integration) a held attempt claims, and are null for ordinary task work.
held_attempts lists every attempt still in state active, including one
whose lease lapsed before it was expired or recovered: lease_expired is
true when expires_at is not after the service time. The session may still
be running and the task needs recovery, so it matters when quiescing. The CLI
exposes the list as agent-coordinator sessions list.
The CLI implements this as agent-coordinator connect, returning orientation
and current ownership. It reads repository binding and protected local session
state. First connection creates a session; resume reconciles the saved one. The
underlying HTTP workflow remains fully documented for clients without the CLI.
Orientation and claiming
GET /api/v1/projects lists all active projects for every authenticated principal;
?archived=true lists only archived projects instead. POST /api/v1/projects/{project_id}/archive and .../restore (body {"reason": "..."},
human operators only) hide a project from the default list and bring it back; an
archived project reports archived_at, keeps every record, and refuses claims, new
tasks and policy changes with project_archived (409). Archiving is refused with
project_active (409) while a task is claimed or a review or integration is in
flight, and archiving or restoring in the wrong state returns project_archived or
project_not_archived.
GET /api/v1/projects/{project_id}/orientation returns current rules, policy and
instruction versions, session work, recovery candidates, blockers/decisions,
relevant lessons, candidate tasks, and next actions. Required rules may require
pagination; instructions_complete: false prevents new work until acknowledged.
Workflow subjects include service-known precondition blockers and hints for
candidate merge preflight and reopen or agent revise.
POST /api/v1/sessions/{id}/instruction-acknowledgments records the project,
policy/instruction revisions, and required section IDs the client has received
and read. Claims require the current complete acknowledgment. This establishes
protocol acknowledgment, not proof that a model understood the prose.
GET /api/v1/projects/{project_id}/preconditions/{task_or_activity_id} is a
read-only inspection of current unmet claim/review/integration preconditions.
Task detail and orientation workflow subjects surface the same service-known
blockers. Results are observations and can become stale immediately; guarded
claim and mutation endpoints remain authoritative. Code integration also returns
a local merge-preflight hint: the service cannot inspect workstation Git or
determine whether an immutable candidate conflicts with the current remote target.
A candidate whose submission is superseded or whose task’s judged fields changed
must be reopened by an operator or revised by an agent (reason_code
requirements_changed). Refusals and preconditions that only a human can clear keep
operation_not_permitted and add details.required_actor: "human" and a
details.gate name, or required_actor: "human" on the precondition.
Task dependencies. GET /api/v1/projects/{project_id}/tasks/{task_id} keeps
depends_on, the sorted array of prerequisite task IDs, unchanged. It also returns
prerequisites, one {id, title, work_status, lifecycle} object per prerequisite in
the same order, and dependents, the same object for each task that lists this task as
a prerequisite, ordered by task ID. dependents holds at most 50 entries;
dependents_truncated is true when more exist. Both lists are direct relations only,
never a transitive walk, and omit deleted tasks.
Task kind corrections. PATCH /api/v1/projects/{project_id}/tasks/{task_id}
accepts optional kind (code or general) with the existing TaskEdit
definition and expected_revision. Omission preserves the kind and old saved
request fingerprints. The same unowned open/planned/blocked eligibility,
delegated editing rights, and agent contributor restrictions apply. A kind
change is refused with task_kind_workflow_protected (409) while a submission
or its review/integration activities exist for the current attempt generation,
even if reopened for revision. Objectives remain General and revert tasks remain Code; changing these fixed
contracts returns task_kind_fixed (409). The accepted transaction increments the task
revision, saves its definition and actor, and records old_kind, new_kind,
and actor_id in the task.edited event. Correct a misclassified task through
this edit path instead of canceling it and creating a replacement.
Last progress. Every task view, including each item of
GET /api/v1/projects/{project_id}/tasks, carries last_progress_at: the RFC 3339
created_at of the newest checkpoint saved for any attempt of the task, or null
when the task has no checkpoint. It is read-only, does not change the task revision, and
lets a client sort a list without requesting each task’s detail.
Human-delivered tasks. Every task view carries human_delivered (default false).
POST /api/v1/projects/{project_id}/tasks accepts human_delivered: true to create a
task a human delivers, and POST …/tasks/{task_id}/human-delivery with
{"expected_revision", "human_delivered", "reason"} marks or unmarks an existing
open or planned task. The mark does not change the task’s revision, its judged
fields or a submission under review. A human may set and clear it; an interactive
agent session may only set it; a supervised or read-only credential may do neither
(403 operation_not_permitted, details.gate human_delivery). Nothing infers the
mark from a title, kind or origin, and existing tasks keep false. For a supervised or
read-only (shadow) credential, a marked task has the unmet precondition
human_delivery_required (required_actor: "human"), so GET …/next never offers it
and counts it in human_queue, including when a review returns it to ready with
changes_requested (revision_needed). A supervisor’s explicit claim is refused with
403 human_delivery_required (details.required_actor human, details.gate
human_delivery), and a next-eligible claim skips the task. A human or an
interactive session claims it as usual, and a human completes it in the manager
(complete-external, which a CLI agent session cannot call).
Task origin and admission. POST /api/v1/projects/{project_id}/tasks accepts an optional admission_class
(revert, fix_target, deflake or refusal_fix; anything else is a 400).
Every task view carries origin (human, agent or service), admission_class
(or null) and held_by_budget (true while a budget-held task is still planned).
An agent-created task without a class is admitted only while fewer than N
agent-originated tasks were admitted in the current ISO week (UTC, Monday
through Sunday) in the task’s own project (the budget is per project);
otherwise it is created planned. The
creation response then adds admission: origin, admission_class, held,
weekly_budget (limit, admitted_this_week, week_start) and, when held,
reason. Objectives are tasks and follow the same rule. Human-created tasks,
tasks with a class and tasks created planned are never held and do not use
the budget. Service-created reverts (revert) and the follow-up of a revise
that lost to a landed push (fix_target) set their class; a re-land is
created planned without one. An agent’s task-definition edit cannot release a
held task (details.gate admission_budget); a human’s edit with
planned: false can. N defaults to 14 and is set with
--agent-task-weekly-budget or COORDINATOR_AGENT_TASK_WEEKLY_BUDGET (0
through 10000).
A human or the service may set any class. An agent may set revert,
fix_target or deflake only with admission_evidence that the service
verifies against its own records of the project: {"result_id"} for a landed
integration result (revert) or one with a failed check receipt (fix_target,
optionally with check_name), or {"report_id"} for a fix_target (fix_target)
or flaky (deflake) integrator report. refusal_fix has no such record. An
agent the operator names with --fix-class-principals
(COORDINATOR_FIX_CLASS_PRINCIPALS, comma-separated; empty by default) may set
any fix class without evidence. For any other agent, a class without matching
evidence gets a 403 operation_not_permitted and nothing is created; the same
task without a class is budgeted. Unknown evidence fields are a 400.
The end-to-end canary is exempt. admission_class also accepts canary, which
only an agent principal named in --canary-principals (or
COORDINATOR_CANARY_PRINCIPALS, comma-separated; empty by default) may use; any
other creator, human or agent, gets a 403 and nothing is created. A canary
task is always admitted, is not counted in admitted_this_week and is never
held; task views and the admission object report admission_class: "canary".
A canary principal’s tasks without the class are budgeted like any agent’s.
The digest leaves canary tasks out of every figure below: they are never
stalled tasks, never ready work for the stalled-queue rule, never task progress,
and integrator reports about them are not listed among the human interventions.
GET /api/v1/projects/{project_id}/digest?hours=N (default 24, at most 336)
is the attention-budget summary: proceeded_decisions (reversible decisions the
service answered with their recommendation in the window; each lists
affected_task_ids and affected_tasks, the same tasks as task_id and title),
pending_reversible_decisions (with proceeds_at), and hri, the
human-required interventions: count, stalled_tasks, lapsed_activities,
stalled_queue, parked_tasks and items. Two stall rules exist, and each stall item names the rule that fired in
rule. repeated_attempt_failures (code stalled_task, counted in
stalled_tasks): a task is stalled when it is open and its last three attempts
all ended (released, blocked, expired or canceled) without a submission.
no_progress (code stalled_queue, counted in stalled_queue): the project has
ready work (an open, unarchived, unblocked task with its dependencies done, no
live attempt, and not waiting on review or integration) and no task progress,
meaning a claim, checkpoint, submission, review decision or integration
authorization or result, for more than N hours. The idle clock starts at the
later of the last progress and the time the oldest ready task became ready, so
an empty queue, or one that just filled, does not count. One continuing stall is
one item, with ready_tasks, idle_hours, threshold_hours,
last_progress_at and stalled_since; it clears on the next progress. N
defaults to 6 hours and is set with --stall-hours or COORDINATOR_STALL_HOURS
(1 through 336). --quiet-hours START-END or COORDINATOR_QUIET_HOURS, in UTC
hours such as 22-07, removes those hours from the idle clock; they are unset
by default. A workflow activity whose attempt lease lapsed while the activity is still active is a
human-required item with code activity_lease_lapsed and rule lease_lapsed (counted in
lapsed_activities): activity_id, activity_kind, activity_task_id, task_id (the
subject), title, lease_expired_at, target_held and publication_intent. The activity
reads recovery_required everywhere it is shown; its integration hold and publication intent
are kept for guarded reconciliation, never released or discarded by the lapse. A task the revise limit parks is a human-required item too, so that
a task agents may no longer claim or revise is not left waiting unseen. Every open,
non-canary task with REVISE_LIMIT (3) agent revises not serialized after a landing,
or SERIALIZED_REVISE_CAP (6) agent revises of any kind, in the last 24 hours is
listed with code revise_limit_reached (counted in parked_tasks), required_actor: "human", task_id, title, park_reason (subject_parked or serialized_cap),
revise_count (the revises that count toward the limit that parks it),
counted_revises_24h, agent_revises_24h, parked_at (when the revise that
reached the limit was made), oldest_revise_leaves_window_at (when the oldest
counted revise leaves the 24-hour window) and unparks_at (when the task is
released if no further revise is made). The item is computed on demand by the same
rule that refuses agent claims and revises, so it disappears once the task is no
longer parked. Open human-required integrator reports are the other items; each
carries task_title, the title of the task its report names (null when it names
none). Wherever the digest text mentions a task, the title comes first and the id
follows in parentheses. The digest
also carries held_agent_tasks (the project’s agent-created tasks the weekly
admission budget held as planned, oldest first: task_id, title, priority,
held_at) and agent_task_weekly_budget (the project’s own limit, admitted_this_week and
week_start), and
last_read_at, when it was last read (null if never).
Reads are recorded in two ways only. A human opening the digest in the dashboard
sends POST /api/v1/projects/{project_id}/digest/read ({}, an idempotent
mutation; agent credentials get operation_not_permitted), and GET ...digest
itself never counts as a read. GET ...digest?ack_link=true adds ack_link:
url and expires_at of a signed acknowledgement link, valid for 7 days, but
only for a human or for the project’s designated digest sender; any other
agent credential gets the digest with ack_link: null, because a read silences
the digest-neglect page and agents must not be able to do that. The digest also
reports digest_sender, the agent principal id designated for the project
(null by default). A human administrator designates or clears it with
POST /api/v1/projects/{project_id}/digest/sender ({"principal_id": "<agent principal id>"} or {"principal_id": null}, an idempotent mutation; the target
must be an enabled agent principal and agents get forbidden), and the host
operator with agent-coordinator-server designate-digest-sender (see the
deployment runbook). The
link’s token is <expiry ms>.<minter principal id>.<HMAC-SHA256 of purpose, project, expiry and minter> under a key the service generates; it is a bearer
capability for one effect and no credential. A read recorded through it keeps
the minting principal (digest_reads.minted_by), and the link stops working
when its minter is no longer a human or the designated sender (it is refused
as digest_link_invalid). GET .../digest/ack?token= answers a page with an “I read this”
button (so a link preview or mail scanner records nothing), and
POST .../digest/ack with the form field token records the read. Both need no
other credential, and both refuse a token that is malformed or signed for
another project (digest_link_invalid, 400) or past its expiry
(digest_link_expired, 410). POST /api/v1/projects/{project_id}/tasks/{task_id}/paths
with {"paths": [...]} replaces the repository-relative paths (files or
directories, at most 50) a task touches. The integrator records the files of
out-of-band commits without an agent trailer with
POST /api/v1/projects/{project_id}/integrator/human-ships
({"commit", "files"}, integrator credentials only), and next (implementer)
skips a task whose declared paths equal or contain, or sit inside, a file
recorded in the last 24 hours, counting it under skipped.path_overlap. Claims
by explicit task are not affected.
GET /api/v1/projects/{project_id}/next?role=implementer|reviewer is a read-only
answer to “what should a launch of this role do now”. It walks the same
candidates and preconditions a claim would check and returns at most one
action (claim_task, recover_task or claim_review) with an exact call
template (method, path and body) and the equivalent CLI command. Recoverable
work comes before new work; reviews are ordered by subject priority and must be
independent of the caller. caller_steps lists preconditions the caller clears
itself (acknowledging current instructions); human_queue counts inspected
candidates that only a human can unblock, and skipped counts every blocking
precondition code seen. For every role, human_queue_items lists the project’s open integrator
reports that need a human, each with required_actor: "human", the report kind
as gate, the report and its resolve call. It also lists, after the reports, the project’s
open non-canary tasks the revise limit parks, as the digest’s revise_limit_reached
items below. With no action,
retry_after_seconds suggests a poll interval. Read-access credentials may call it, so a shadow supervisor can log
what it would launch without holding write authority.
GET /api/v1/projects/{project_id}/state-wait accepts target_kind=task|activity|job,
target_id, an after_state_token returned by task detail/precondition inspection,
activity detail/precondition inspection, or individual job detail, and optional timeout_seconds from
1 through 30 (default 15). It polls at 500 ms intervals, releases its database
connection between reads, and returns immediately when the state token changes.
On timeout it returns the latest snapshot with timed_out: true. This is an
opt-in long poll for one task work_status, workflow activity, or job state; it
does not hold a transaction, reserve resources, or renew ownership. Expected
detection latency is at most about 500 ms plus request/database scheduling.
Tokens from task and activity details include caller-visible eligibility and
unmet-precondition state, so an instruction acknowledgment can wake a task wait
without changing work_status. Job tokens use persisted job state and exclude
derived observation age and freshness, so those display values do not wake a
wait by themselves.
POST /api/v1/projects/{project_id}/claims accepts exactly one task ID or a
next-eligible selector. For an explicit task, include its expected revision.
For next-eligible work, supply permitted kinds, capabilities, and the policy/
instruction versions read. The server applies the same eligibility checks to
both forms. mode is work or recovery.
{
"task_id": "task-example",
"expected_task_revision": 4,
"mode": "work",
"policy_revision": 3,
"instruction_version": "1"
}
A successful claim returns the task, attempt ID, ownership generation, server lease deadline, remaining lease duration sampled before sending the response, renewal recommendation, and worktree/resource preparation steps. The client subtracts elapsed monotonic request time and a safety margin from the returned remaining duration. It does not assume its wall clock matches the service.
A specific competing claim returns 409 with current status and alternatives.
Next-eligible selection with no available work returns 200 with claim: null,
structured queue reasons, and a suggested next check. An empty queue is normal.
Retrying a successful claim retrieves its original receipt and current authority
status; it cannot return a different task as a substitute.
CLI equivalents are tasks list, claim --task <id> --revision <n>, and
claim --next, with explicit project binding and JSON output available.
Deterministic integrator (P4, additive)
A project’s human-managed policy carries integration_owner: agent (default;
agents integrate through the workflow activities above) or integrator. Only a
human may change it; omitting it on PATCH …/policy keeps the current value.
The integrator authenticates with an agent credential of class integrator
(see the operator access contract). That class may call only the routes below,
and only that class may call them, except that any reader may list integrator
reports and only a human resolves one.
-
GET /api/v1/projects/{project_id}/integrator/queuelists approved code subjects in integration whose submission is current and whose judged task fields still match the pinned digest, by subject priority and then time in integration. Each item carries the candidate revision, tree and ref, the reviewed base, the pinned task digest and any results already recorded for it (each withauthority_expires_at, set while its push authority is outstanding, so the integrator can observe that result before pinning a newer one); the response adds the current required-check roster andtargets, the distinct{repository_url, target_branch}pairs the project integrates into: the project’s configured repository and branch first, then any other pair a listed item’s submission pinned. The integrator watches every target each cycle, with or without queued items. Each item also carriesreverted:{revert_task_id, result_id, repository_url, target_branch, landing_range}for every result of the project on the item’s repository and target branch that a revert task (not canceled) targets, so the integrator can refuse a no-op (requal tot0) whose candidate commits intersect a reverted landing, andrevert_task_id: the subject’s own task id when it is a revert task inmechanicalmode (null otherwise), so the integrator reports a reproduced check failure of that candidatenot-mechanicalrather than revising it. The response addsreverts, the revert tasks awaiting a mechanical candidate (see Reverts below):{id, task_id, result_id, r, title, priority, repository_url, target_branch, target}, whereidis the revert task id andtargetis{submission_id, result_id, original_task_id, r, t0, c, landing_range, reason, evidence}. Each call records the integrator heartbeat. Projects still owned by agents answerintegration_owned_by_agents. -
POST /api/v1/projects/{project_id}/integrator/resultspins one integration resultr(with its tree, the target tipt0and tree it was computed on, the landing range and the roster read fromt0) per submission andt0. Sending the same result again returns the stored one; a differentrfor the same pair isresult_conflict, and acthat is not the submission’s candidate iscandidate_changed. A new no-op result (requal tot0) whosecor landing range shares a commit with the landing range, on the same repository and target branch, of a result a revert task (not canceled) targets is refused withcandidate_reverted_in_history(details.reverted_resultsand the shareddetails.commits): re-land candidates must be new commits. Results that are not no-ops are unaffected. The roster is{"required_checks":[{"identity","check_name","workflow_path","workflow_blob"}]}. -
POST /api/v1/projects/{project_id}/integrator/receiptsrecords one GitHub Actions check run observed onr(head_shamust equalr, otherwisereceipt_head_mismatch). A run attempt is recorded once; a different outcome for it isreceipt_conflict. The response names the deciding run for that check and workflow blob: the latest attempt of the latest run. -
POST /api/v1/projects/{project_id}/integrator/check-progresswith{result_id, head_sha, runs:[{check_name, status, run_id?, run_attempt?, started_at?}]}replaces the result’s snapshot of required check runs that are stillqueuedorin_progress(at most 100; completed runs are receipts).head_shamust equalr(progress_head_mismatch) and everycheck_namemust be in the result’s roster (progress_check_not_required). The snapshot and its time feed theprocessingobject that task lists and details show for awaiting_integrationtask (see the workflow specification); it grants no authority, owner or lease, and the integrator posts it best-effort on each poll. -
POST /api/v1/projects/{project_id}/integrator/push-authoritywith{result_id}authorizes pushingr. It refuseschecks_not_passed(details listpendingandfailedroster checks) unless every roster check has a deciding run with the roster’s workflow blob that concludedsuccess,skippedorneutral(as a GitHub required check passes);protected_check_missingwhen the roster drops a check in the project’s workflow policy;stacked_on_unapprovedwhen the landing range carries commits of another task’s submission that is neither approved nor integrated;observation_requiredwhile another result of the submission holds authority;integration_target_heldwhile another integration holds the target;privilege_gate_unresolvedwhile aprivilege_gatereport on the result is open; andprivilege_gate_deniedonce a human resolved one withdeny(both name the report asdetails.report_id). A result with noprivilege_gatereport, or whose report carries the decisionallow, passes. If an approving reviewer contributed to another task whose commits the landing range carries, the approval is voided, a replacement review is queued (that reviewer cannot claim it) and the response is{"granted":false,"refusal":"approver_is_contributor"}. Otherwise it takes the submission’s integration hold and returns{granted:true, r, t0, deciding_runs, roster_ids, protected_ids, expires_at}; authority stays outstanding until the next observation. -
POST /api/v1/projects/{project_id}/integrator/observationswith{result_id, tip, ancestry, evidence}records what the integrator saw at the target; the service cannot reach the Git host, so it trusts this attestation.contained(r is in the tip) completes the subject and its integration and readies dependents (published, oralready_containedwhenrequalst0;published_after_reopenif a human reopened the submission meanwhile); a published revert whose reason isdefectalso proposes its re-land task;equal_t0(not_published) andmoved(target_moved, compute a new result on the new tip) end the authority and release the hold. Only the result holding the submission’s push authority can be observed while it is outstanding (another result isobservation_required), andris observedpublishedonly while its result holds authority (otherwiseauthority_not_issued). Never infer publication from a push exit code. -
POST /api/v1/projects/{project_id}/integrator/revisewith{submission_id, reason_code, evidence, result_id?, moved_by_result_id?}sends a candidate back to its author; onlyconflict,check_failedandreverted_in_historyare accepted, never while push authority is outstanding (observation_required).reverted_in_historysends back a candidate whose no-op resultresultsrefuses withcandidate_reverted_in_history; itsevidencenames the shared commits. The service verifies it: the submission’s candidate commit, or the landing range of a result recorded for it, must share a commit with the landing range, on the same repository and target branch, of a result a revert task (not canceled) targets, otherwisenot_reverted_in_history. The integrator sends it instead of pinning a no-op result whose candidate commit or landing range meets a landing listed in the item’sreverted, and whenresultsrefuses withcandidate_reverted_in_history, citingdetails.commits. It counts toward the limits below like any integrator revise and never serializes. Aconflictrevise may name inmoved_by_result_idthe result whose landing moved the target: the newest result the integrator published on that target since the candidate’s reviewed base. The service resolves it to that result’s task only when it is a result of this project with apublishedobservation, and otherwise ignores it. The response carriesserialized_after(a task id, or null),park_reason(or null) and, underrevise, the resolvedlanding_task_id. A subject parks for agents (the unmetrevise_limit_reachedprecondition; agent claims are refused with that code) once it has, in the last 24 hours, three agent revises not serialized after a landing, or six agent revises of any kind. A revise that leaves the subject below the first limit only records the landing. The revise that would reach it is serialized when it is aconflictciting a landing task that no serialized revise of the subject cited in the last 24 hours: the subject gains a dependency on the landing task (already satisfied when that task is done), the revise applies without counting toward the limit, andserialized_afternames the landing task, so the subject stays claimable. Otherwise that revise applies and parks the subject, andpark_reasonsays why: the reason is notconflict(not_a_conflict), the landing is missing or unresolved (landing_unknown), is the subject’s own task (landing_is_subject), is cited by a serialized revise in the window (landing_repeated, a persistent cycle), or would close a dependency cycle (dependency_cycle). A revise that reaches the cap of six revises reportsserialized_cap. A revise of a subject that is already parked is refused withdetails.gate: "revise_limit_reached"anddetails.park_reason(subject_parked, orserialized_cap). Acheck_failedrevise must name inresult_ida result of that submission on which one roster check, under the roster’s workflow blob, is reproduced: its deciding run concludedfailureortimed_out, and at least two of its attempts did. Otherwise it is refused withcheck_failure_not_reproduced(details give each roster identity’s deciding conclusion and failure count underchecks), or not found when the result is not the submission’s. -
POST /api/v1/projects/{project_id}/integrator/reportswith{kind, dedupe_key, task_id?, submission_id?, result_id?, details}records a finding for the digest.kindisprivilege_gate,flaky,fix_target,unreviewed_landing,target_rewrittenorruleset_missing; aprivilege_gatereport must name itsresult_id. The first report per kind anddedupe_keywins: sending it again returns the stored report, whatever itsdetails, including any resolution. The service setsrequires_humanforprivilege_gate,target_rewrittenandruleset_missing, and for any report whosedetails.blocks_subjectistrue(the integrator sets it onfix_targetreports and onflakyreports that leave a subject blocked:no_resultandrerun_refused), so a subject never waits unseen. Areverted_in_historyrevise the service refuses withnot_reverted_in_historyis afix_targetreport withverdictnot_reverted_in_history, and the integrator skips the subject. The integrator raisesprivilege_gate, failing closed, when any path that differs between the target tip and the result lies under.github/, is the.githubentry itself,.gitmodulesor aCODEOWNERSfile, or is in the local-action scope read from the target tip (paths compared ignoring letter case). That scope holds the directories named byuses: ./<dir>in files under.github/and in the referenced actions’ definitions, the paths a referenced action’smain,pre,postand localimagename, and the targets of symbolic links under.github/or a scope directory. Reading the scope from the target tip is safe because every change the result makes under.github/is gated anyway. Ausesvalue the integrator cannot read as a plain path (an alias, an escape, an expression) makes the whole repository the scope (unparsed_local_action_reference), and a gated path that cannot be read isunreadable. The report lists each gated path with reason codes that hint at what changed (for exampleadds_secrets,removes_permissions,triggers_changed,changed_ci_definitionorchanged_local_action); the codes do not decide the gate. It pushes that result only once the report showsallowed: true. It raisesruleset_missing(once per freeze episode, which ends when the rules are back) andtarget_rewrittenwhen it freezes a target. Its tip monitor compares each target’s tip with the tip it recorded last: a tip that does not descend from it istarget_rewritten; a forward move the integrator did not make itself is an out-of-band landing. A result’s own landing range (the commits from the tipt0of that result up to itsr) counts as the integrator’s when the service granted this integrator push authority for that result; every other commit of the move is out-of-band. If any of those carries an agent trailer (aClaude-Sessiontrailer, or aCo-authored-bytrailer naming Claude or Codex or the address[email protected]or[email protected]), it raisesunreviewed_landing, keyed by target,fromandto, withdetails{repository_url, target_branch, from, to, integrator_results, flagged, flagged_count, unflagged_count};flaggedlists flagged commits as{sha, subject, trailers}, cut to fit the details size limit (flagged_countis the total). The report does not block anything; it feeds post-hoc review through the digest. Commits without an agent trailer are the user’s own work and are not reported. Trailer detection is best effort and not a security control: a session can strip its trailers, and commits of unknown provenance are not detected. When a move has flagged commits, its new tip becomes the recorded tip only once the report is stored or refused (409/403); until then the target’s items wait (tip_move_unsettled), so nothing is integrated on that tip, and the next cycle reports the whole move from the recorded tip. Flake attribution counts a check’s attempts onras GitHub does:success,skippedandneutralpass,failureandtimed_outfail, and any other conclusion (such ascancelled) has no result; the latest attempt decides. When it failed for the first time or had no result, the integrator reruns that workflow run’s failed jobs (never a run whose jobs all passed; the GitHub App needs theactions: writepermission) and posts a receipt for every attempt. A pass after a failure is aflakyreport and the result proceeds. A check that two reruns of its run leave undecided (its latest attempt has no result, or is its only failure), or a rerun request GitHub refuses, is aflakyreport withverdictno_resultorrerun_refusedand skips the subject;flakyreports are keyed by result, check and verdict (a refused rerun by result and run). A failure is reproduced once the deciding attempt and at least one earlier attempt failed; it revises the author withcheck_failedonly when the same check’s latest attempt with a result on the target tip passed and started after the check’s first failure onr, and waits while the tip’s run is still going. A pass that started earlier may predate a change outside the commit (an advisory database, a toolchain or runner image), so it does not attribute: the integrator reruns that one job on the tip (POST …/actions/jobs/{job_id}/rerun, at most twice per result; a refusal that only means the run is going waits) and waits for the new attempt. When the latest attempt failed, or the tip has no result for the check (or its only pass is still stale after the reruns or the rerun is refused), the integrator raisesfix_targetwithverdicttarget_failingortarget_unverified(one per target, tip, verdict and check) whose details carrytarget_result(run_id,run_attempt,conclusion,started_at,completed_at,stale) and skips the subject until the tip moves or the check passes there. Acheck_failedrevise names the tip’s run, attempt and completion time it relied on. A mechanical revert’s candidate has no author: when its failure is reproduced and passes on the tip, the integrator reports the revertnot-mechanicalwith reasoncheck_failedinstead of revising it. -
While a subject is in integration and an open
requires_humanreport blocks it, the task’s workflow view (workflowintasks show, the workflow route, orientation and the console task page) carriesintegration_blocked, elsenull:{report_id, kind, verdict, target_revision, result_revision, check_name, repository_url, target_branch, created_at, action}, from the newest such report. A report blocks the subject when it names the current submission, one of its results, or only the task; afix_targetreport withblocks_subjectalso blocks every subject of the same repository and target branch, because the target is what needs fixing (target_revisionis the failing tip,check_namethe failing check).next_actionsthen holdsaction(fix the target, see the report, resolve it) instead of the claim guidance.blockersandwork_statusare unchanged. The field isnullagain once a human resolves the report or the subject integrates. -
GET /api/v1/projects/{project_id}/integrator/reportslists a project’s reports, newest first, for any authenticated reader.open=truekeeps only unresolved ones;limitis 1 to 1000 (default 200);before=<report_id>continues after that report, and each page’snext_beforeis the cursor of the following page (null on the last).task_id=<task_id>keeps only reports about that task.kind=<kind>keeps only reports of that kind andcheck_name,repository_urlandtarget_branchkeep only reports whose details name exactly that check, repository or target branch; every filter combines with the others, withopenand with paging. An unknownkind, or acheck_nameortarget_branchover 512 bytes, arepository_urlover 2,048 bytes or atask_idover 100 bytes, is a 400bad_request. An unknownbeforeisnot_found. -
GET /api/v1/projects/{project_id}/integrator/reports/{report_id}returns one report in the shape of a list item, for any authenticated reader. An unknown ID, or a report of another project, isnot_found. The nativeintegrator-reports listandshowcommands and the MCP toolscoordinator_integrator_reports_listandcoordinator_integrator_report_getread these two routes; none of them resolves a report. -
POST /api/v1/projects/{project_id}/integrator/reports/{report_id}/resolvewith{note, decision?}is human-only (other callers get theintegrator_report_resolutiongate). Aprivilege_gatereport needsdecisionallow(its result may be pushed; the report then showsallowed: true) ordeny; other kinds take no decision. A resolved report isreport_already_resolved. -
GET /api/v1/projects/{project_id}/next?role=integratorreturns the queue head as anintegrateaction (integrator credentials only).
On integrator-owned projects the agent and human integration routes (claiming or
authorizing an integration activity, publication intents, integration results,
finalize, both publication reconciliations) refuse with
integration_owned_by_integrator (details.required_actor is integrator). An
agent revise that arrives while push authority is outstanding is recorded and
answered with revise_deferred: true: it applies if the push does not land, and
becomes a follow-up task if it does; for author_withdraw the follow-up is a
revert task (priority 0, reason author_withdraw, the withdraw request as its
evidence, review required), or the open revert of that result when one exists.
Only a published observation creates that revert; after already_contained
nothing new landed, and the follow-up is an ordinary task. When a human reopen
superseded the submission meanwhile, the observation resolves the deferred
revise as moot (revise.resolution is moot): it is neither applied nor
followed up, whether or not the push landed.
A human reopen always applies immediately. Until a project is switched,
nothing in the agent integration path changes.
Reverts
A revert task undoes one published integration result R. It is an ordinary
code task whose task view carries revert: {result_id, submission_id, original_task_id, r, reason, evidence, review_required, mode, not_mechanical, rejection, reland_task_id, created_by, created_at, candidates} (null on other
tasks). candidates lists the integrator’s attested candidates {t0, submission_id, candidate_commit, candidate_tree, attestation, attested_by, attested_at}, so reviewers see what they judge. Every task
view also carries reverted_by, the id of the newest revert of that task
that is not canceled (or null); the original task stays done. No admission
limit applies to reverts.
POST /api/v1/projects/{project_id}/revertswith{result_id, reason, evidence?, note?, priority?}creates a revert on an integrator-owned project.reasonisdefect,author_withdraw,audit_rejectionorhuman;prioritydefaults to 1.result_idmust name a result of this project (otherwise not found) with apublishedobservation whose task isdone(otherwiseresult_not_published). A second revert of a result whose revert task is still planned or open isrevert_exists(details.revert_task_id); reverting a revert’s own result is allowed. A human’s revert needs no review and records arevert.escaped_defect_canaryevent. An agent’s revert needs a reason other thanhuman(otherwise therevert_without_evidencehuman gate) and non-emptyevidence, text or JSON (otherwiserevert_evidence_required); its candidate needs the project’s reviews and at least an agent review even whenreview_modeisnone. That review judges the decision to revert and its evidence, not the inverse diff. The creator (for an automatic revert, the author whose withdraw lost to the push) and every contributor of the reverted task are recorded as contributors to the revert, so none of them can review it (reviewer_not_independent).- While
modeismechanicalonly the integrator produces the candidate: claiming or unblocking the task is refused withrevert_awaits_integrator, it is skipped by next-eligible claims, and a revise of its candidate blocks it again and returns it to the queue’sreverts. Apart from the integrator’s candidate or not-mechanical report, its only exit is canceling the task. A revert task follows the task lifecycle rules by its own origin (its creator’s, orservicefor an automatic revert): an agent session may cancel, archive, restore or delete an agent- or service-origin revert under the same lifecycle guards as any other task, while a human-origin revert needs the human (oragent_rule_editingfor cancel). A canceled or archived revert leaves the integrator’s queue and takes no candidate (revert_not_open), and the original’sreverted_byno longer names it; restoring an archived revert is refused withrevert_existswhile another open revert of the result exists. An agent session may cancel or archive a revert only when it is independent of the reverted change; otherwise the call is refused withrevert_author_conflict(409) and an independent agent or a human must do it. The reverted change is the reverted task and every task of a submission in the revert’s landing range: a submission whose candidate or own landing range shares a commit with the result’slanding_range_json, plus the contributor tasks the result recorded. The actor must pass the same contributor checks as a reviewer of that work (workflow::contributed: no recorded contribution by principal, session or shared subagent identity, including checkpointcontributor_session_ids) and, because a cancel or archive cannot assert an independence level, thedistinct_launchcheck (workflow::shares_launch: no launch shared with a contributor, failing closed beyond the 64-hop walk cap);distinct_hostanddistinct_vendorare not enforced by the service. Humans are unaffected. Restore and delete are not covered: restoring an archived revert returns it to the integrator’s queue and deleting a canceled one only hides a revert that is already retired, so neither lets the author suppress the revert. The revert task stays blocked while it waits for its candidate and is listed under the queue’sreverts. The integrator computes the candidate itself on the target’s current tip X, in a disposable worktree:git revert -m 1 Rwhen R is its merge intot0, otherwise the landed ranget0..Rreverted as one step. It commits with a pinned identity and date and the messageRevert "<candidate subject>"with the trailersAgent-Coordinator-Revert-TaskandAgent-Coordinator-Reverted-Result, and without the host’s global or system Git configuration, so the same X and R always give the same commit. A known limit: the target’s own.gitattributes, including merge drivers such as the builtinunion, still apply to the revert (they are reviewed repository content). A clean revert is pushed create-only torefs/agent-coordinator/candidates/reverts/<task_id>/<X>and recorded with the candidate route below,t0being X. A revert that conflicts on X, whose R is not in X’s history, or that would change nothing on X is reportednot-mechanicalwith reasonconflict, its evidence naming the conflicting paths, or saying that R is not in the target history or is already undone; the integrator never proposes an empty candidate. A refusal of either post (such asrevert_candidate_conflict) skips the revert for that cycle; the next cycle computes it again. The integrator works reverts before ordinary items, one revert or item per cycle, and skips those of a held target. POST /api/v1/projects/{project_id}/integrator/reverts/{task_id}/candidatewith{t0, candidate_commit, candidate_tree, mechanical: true, candidate_ref?}records that candidate as the revert’s code submission, created by the integrator with the attestationmechanicaland based ont0. The submission enters review (an agent’s revert) or integration (a human’s) and then follows the integrator path above. Sending the same commit and tree for the samet0again returns the same submission (candidate_submission_id) while that submission is still the revert’s candidate in review or integration; once a revise or reopen sent that candidate back, the same commit and tree are recorded as a new submission. A different candidate for thatt0isrevert_candidate_conflict. Other refusals:revert_not_mechanical,revert_not_open,revert_claimed,revert_candidate_exists(a candidate is already in review or integration) andworkflow_policy_required.POST /api/v1/projects/{project_id}/integrator/reverts/{task_id}/not-mechanicalwith{t0, reason, evidence}(reasonisconflictorcheck_failed) reports that the revert cannot be computed mechanically. The service supersedes any candidate still in review or integration (refused withobservation_requiredwhile its push authority is outstanding) and turns the revert into ordinary implementation work: undo the original’s behaviour while keeping the changes integrated after it, with review required. The samet0andreasonagain replay; anything else isrevert_not_mechanical. A capped cascade of further reverts is future work.- A review that requests changes on a mechanical revert’s candidate rejects
the decision to revert: the revert task is canceled (a new task
revision, recorded in its history), its
rejectionrecords the review (activity_id,reviewer_id,summary,rejected_at), it leaves the queue and the original’sreverted_byno longer names it. A human may create a new revert. A revert converted to implementation work is revised like any code change instead. - When a revert whose reason is
defectis observedpublished, the service creates a planned (not yet admitted) re-land task seeded with the original’s description, its candidate reference and its acceptance criteria, plus the revert evidence as one more criterion; the revert’sreland_task_idnames it.
Supervisor telemetry (cost and canary)
Supervisor hosts report launch cost and end-to-end canary results so the
console can show autonomy health per project. Each host’s own files
(costs.jsonl, e2e-canary.jsonl) stay the durable source; the service stores
only what hosts report, per project, attributed to the reporting principal and
host. All paths are under /api/v1/projects/{project_id}/telemetry.
POST /cost-rowswith{host, rows}(1 to 200 rows) records launch cost rows. A row is{row_id, role, harness, model, input_tokens, cached_input_tokens, output_tokens, usd, usd_estimated, canary, task_id, session_id, attempt_id, occurred_at_ms}:roleisimplorrev,usdis dollars or null when the launch was unpriced (stored as exact millionths),canarymarks a row of the host’s canary project, andtask_id,session_idandattempt_idare the host’s claims, not checked against the service’s records. Unknown fields are refused.- Every telemetry route answers
404 project_not_foundwithdetails.project_idfor a project the service does not have, so a host can tell a missing project (rows are quarantined, not retried) from a coordinator without the route (a 404 with another code, retried). POST /canary-resultswith{host, rows}records end-to-end canary runs. A row is{row_id, harness, outcome, duration_seconds, detail, task_id, served_by, occurred_at_ms}:outcomeisok,failedortimeout, andserved_byis the list of harnesses the host’s ledger shows for the task, or null.- Both writes are idempotent twice over. The usual
Idempotency-Keyreplays the saved reply. Independently,row_id(the host’s stable identity of one ledger row; the supervisor ande2e-canary.pyuse the SHA-256 of the ledger line) is unique per project and host, so a retry under a new key, a batch of other composition or a re-imported ledger stores each row once; the first report of a row wins. The reply is{project_id, host, kind, accepted, duplicates}. The write, its receipt and onetelemetry.cost_reportedortelemetry.canary_reportedevent (host and counts only) commit together. - Authorization: reporting needs an agent credential with write access, so a
human session, a read-only credential (shadow and reviewer principals) and the
integrator credential are refused with
403; a missing credential is401. An unknown project isnot_found. Reads accept any authenticated reader. GET /cost?group_by=task|day&host=&since_ms=&until_ms=&include_canary=&limit=returns the project’s launch cost per task (default) or per UTC day, most recent group first. Each item hasimplandrevfigures{launches, usd, unpriced_launches, estimated_launches, input_tokens, cached_input_tokens, output_tokens},total_usdandlast_at, plustask_idand the task’stitle(null for an unknown task) orday(YYYY-MM-DD).totalssums every matching row;truncatedsayslimit(1 to 1000, default 100) cut groups off. Rows of a canary project and of budget-exempt (canary) tasks are left out unlessinclude_canary=true, as the digest leaves them out.GET /canaries?host=&harness=&outcome=&since_ms=&limit=&before=returns the canary history, newest run first (limit1 to 1000, default 100;beforeis a resultidand an unknown one isnot_found;next_beforecontinues), andlatest, the newest result of every host and harness the project has seen.POST /capswith{host, impl_daily_usd, rev_daily_usd}records the daily spend caps (dollars per 24 hours; 0 means no cap, null means not reported) the host enforces per role, replacing that host’s earlier report. It follows the write rules above (403for a human, read-only or integrator credential; unknown fields are refused) and emits onetelemetry.caps_reportedevent.agentc-supervisorreports its[health]caps for each project it serves.GET /autonomy?days=N(default 14, at most 90) returns the figures the console’s Autonomy view shows besides cost and canaries, from the service’s own records, leaving out canary (budget-exempt) tasks as the digest does:finished(count,by_day: code or general tasks whose workflow reached done);human_required(count,by_day,per_finished_task, null when nothing finished, and its two sources,integrator_reports, human-required reports opened in the window, andparked_subjects, subjects an integrator revise parked);parked(countanditems: the open tasks the revise limit holds from agents now, withagent_revises_24handcounted_revises_24h);reviews(submissions_reviewed,approved_first_pass, a task’s first submission with no change requested;revised, a submission with a requested change;disagreements, a submission one reviewer approved and another sent back;decisions); andcaps, the latest report of each host. Stalls have no recorded start, so they are not part ofhuman_required; the digest reports the current ones. Reads accept any authenticated reader.GET /autonomy/window?start_ms=&end_ms=returns exact fixed-window autonomy metrics for the interval[start_ms, end_ms]in milliseconds since the epoch. Both parameters are required and both bounds are included: a record stamped exactly at either bound counts. The window is at most 92 days wide and must be closed; anend_msafter the service’s clock is refused with400 window_not_closed, and a missing, reversed or too wide window with400 invalid_request. Every section exceptcurrent_stateis computed only from the window and from records stamped no later thanend_ms, so the same query returns the same figures whenever it is asked (a retained audit fixture reproduces); it never reads the clock. The rollingGET /autonomyread is unchanged. The reply (schema: "autonomy-window-v1") keeps its sources apart:value: tasks that reached done in the window (workflow_subjects.updated_atwhere the phase is done, plustask.completed_externallyevents for tasks not already counted), ascompleted(count,by_originhuman, agent and service,completed_externally,by_day);human_origin(count,per_dayover the exact window width); andlatency(taskcreated_atto completion;count,median_seconds,p90_seconds,max_seconds, nearest rank) for all tasks and for human-origin ones.direct_push: coverage over an explicit denominator, the non-canary code tasks completed in the window (general tasks and external completions are reported separately as not applicable). Each ofintegrator_direct_push(integrator observationspublishedorpublished_after_reopen),agent_integration_published,already_contained_onlyandno_publication_evidencehascount,ratio(null with an empty denominator) and itsevidence.statusispartialwhile any completed task has no publication evidence. Push time isunavailable: the service records when the integrator or an agent reported the observation, and git author or commit timestamps are neither stored nor used as push times. Evidence recorded afterend_msis ignored.attention_events: the project’s audit events in the window by the class of the credential behind each (by_class:interactive,human_session,supervised,integrator,job_reporterandunclassified, an agent credential with no recorded class, such as an event from before classes were recorded), andby_kindwith each kind’s events and distinct records.interventions.eventsis the raw count of interactive and human-session events, not a count of distinct interventions.classification.statusispartialwhileunclassifiedevents exist;audit_loggives the earliest event and whether it precedes the window start.integrator_signals: human-required integrator reports opened in the window.episodes.stalls: stall episodes reconstructed from attempt history, three or more consecutive attempts of a task that ended without a submission, one episode however many nightly digest readings saw it. An episode starts when the third attempt ends and ends when an attempt submits; attempts that ended afterend_mscount as running.started_in_windowcounts them,itemslists up to 100 (truncatedsays more exist),carried_incounts those that started earlier and had no submitting attempt before the window (a task canceled meanwhile is still counted), andattempts_without_end_timecounts attempts whose end was not recorded.episodes.parkslists subjects an integrator revise parked, by revise time.reserved_exclusions: canary (budget-exempt) tasks are left out of every figure above and counted here by section (completions,attention_events,human_required_reports,parked_subjects,stall_episodes). Events are attributed to a canary task when their record is the task, or a submission or attempt of it.hri: the human-required total as recorded attention, never a bare success label.componentslists the sources (interactive and human events, human-required reports, parked subjects, stall episodes started).statusiscompleteonly when every source was available; otherwise it ispartial,countis null,lower_boundis what the available sources recorded,outcomeisunknownandunavailable_sourcesnames each missing source (the audit log not reaching the window start, unclassified events, attempts without an end time). A complete zero readsoutcome: "none_recorded".current_state: the only query-time section.as_ofis the clock, andstalled_tasksandparkedare the digest’s current views of tasks that stalled or are parked now. Do not compare it with the windowed sections.
Attempt operations
All paths below are under /api/v1/projects/{project_id}. Attempts have their
own IDs and ownership generation; ownership-dependent input must name the
expected generation. Authorized historical reads/late notes do not require a
live ownership grant. Human review uses browser authentication and CSRF with a
human-owned review attempt; it does not require an agent-token session proof.
| Operation | Endpoint | Required content |
|---|---|---|
| Inspect current work | GET /attempts/{id} | Returns current authority and historical outcome |
| Renew | POST /attempts/{id}/renew | Generation and health observation; no invented progress |
| Register checkout | POST /attempts/{id}/checkout | Workstation, resolved worktree identity, branch, base revision, local path, clean/dirty state |
| Checkpoint | POST /attempts/{id}/checkpoints | Generation, summary, current action, next step, blockers, revision/job references |
| Reserve resources | POST /attempts/{id}/reservations | Complete requested resource/unit set; grant all or none |
| Delegate reporting | POST /attempts/{id}/reporters | Current generation, permitted attempt/job IDs, bounded reporting deadline, client-generated reporter-proof verifier |
| Submit | POST /attempts/{id}/submit | Generation, expected task/policy revisions, outcome evidence, handoff, lessons |
| Relinquish | POST /attempts/{id}/release | Generation, final checkpoint, ready/blocked disposition and reason |
| Resolve recovery | POST /attempts/{id}/recovery-resolution | Generation, inspected evidence, disposition and outstanding holds |
| Add late information | POST /attempts/{id}/late-notes | Historical attribution and text/evidence; grants no current authority |
Checkpointing does not implicitly renew ownership. The CLI may explicitly perform both operations and must report each outcome. This avoids a retry of an old checkpoint appearing to grant a fresh lease.
A project policy may set max_attempt_seconds (0, the default, is no limit;
otherwise 600–604800, changed only by a human). No claim, workflow activity
claim, renewal or job-reporter renewal sets an attempt’s lease past its
creation plus that limit, and once the lease already ends at
the limit a renewal is refused with 409 attempt_duration_exceeded: record a
checkpoint and release with a handoff. Continuation claims that keep the same owner across attempts
are not implemented yet.
A project policy may also set allow_relayed_human_answers (default false,
changed only by a human). When true, an agent session may add a relay object
(prompt, response) to POST .../decisions/{id}/answer for a
required_actor=human decision; the answer is recorded as relayed, as described
in the knowledge contract. Without it the call is refused with
403 operation_not_permitted.
Submitting code includes a candidate repository/base/commit/tree identity and check evidence. Review submission names the immutable reviewed submission and decision. Integration submission names the target before/after revisions, publication observation, and verification of the resulting tree. Shared domain guards reject content that is inappropriate for the task kind or obsolete input.
Jobs, artifacts, knowledge, and decisions
| Surface | Operations and behavior |
|---|---|
| Tasks | Create, get/list, revision-checked edit/admit, dependencies, cancel, supersede, human-only complete-external for work delivered outside the coordinator; no unrestricted status write |
| Jobs | Register before local launch; append authorized observations; report producer terminal status; inspect without relaunching |
| Artifacts | Explicit bounded upload, authenticated metadata/download, authorized deletion with retained tombstone; external link registration |
| Knowledge | Create/search/get, revision-checked correction and supersession, usefulness feedback, explicit rule adoption under current delegated permission |
| Decisions | Open a scoped question, inspect pending answers, record an answer under the required actor type, preserve the authorization it conveys |
| Project policy | Get current/history, revision-checked update under human or delegated agent authority |
| Events | Cursor-based authenticated history, filter by project/task, no credential values |
| Imports | Upload/inspect source, preview mappings/conflicts, then explicitly apply a versioned preview |
| Exports | Generate a snapshot at an identified record/event revision with provenance and generated-file markers |
Job observations have a separate authorized reporter identity and monotonically increasing observation sequence. They do not require a still-active implementation lease, but do require current reporter authorization. Late running observations cannot overwrite a terminal result. Corrections to erroneous terminal reports are explicit attributed amendments, not silent history replacement.
Task submission may reference only finalized artifacts. Knowledge updates made independently use revision checks; new lessons included in a task submission are stored atomically with that submission. Import apply must detect service changes since its preview rather than overwriting them.
Resource reservation trail
GET /api/v1/resources/{resource_id}/reservations (any authenticated actor) lists
every reservation that includes the resource, across all projects, newest first
(created_at descending, then id descending). Query: limit (1–200, default 50),
cursor (the opaque next_cursor of the previous page), and state=held to keep
only reservations still holding units (stored state held, which includes derived
recovery_required). Any other state value, a malformed cursor or an out-of-range
limit is a 400; an unknown resource is a 404. Each item has the project reservation
shape (id, project_id, attempt_id, task_id, generation, derived state,
stored_state, items, created_at, release and resolution reasons, times and
evidence) plus project_name and task_title, so a client can open the task in its
own project. The response is {items, next_cursor}; next_cursor is null on the last
page. It carries no credentials or session proofs. The existing
reservation_items(resource_id) index serves the lookup, so no migration is needed.
The dashboard’s Resources view uses it for the History dialog: one page per open
and per Load more, never on a timer.
CLI behavior and contract checks
Provide JSON input through --input <file> or standard input and JSON output
through --json. Native PowerShell and Linux-shell examples use the same payload
files. Keep API tokens/session proofs in the protected client configuration or
explicit process environment, not positional command-line arguments or output.
Use exit 0 for success, 2 for invalid input, 3 for authentication/setup required, 4 for denied operations, 5 for state/ownership conflicts, 6 for unsatisfied requirements, and 7 for temporary transport/service failures. JSON includes the stable API error code and remedy. The CLI never generates a new mutation key merely because a response was lost.
Reporter credentials are subordinate to the issuing principal/agent credential. They permit only the named observations and, when explicitly delegated, bounded lease renewal. They cannot create sessions or extend their own scope/deadline. Revoking the parent credential revokes reporters. Attempt expiry stops delegated renewal; appropriately authorized job observations may continue without reactivating the expired attempt. Use a separate bearer credential namespace so reporter credentials cannot be mistaken for full agent API tokens.
Acceptance includes a complete workflow performed once through the CLI and once through direct HTTP: connect, read instructions, claim, register checkout, checkpoint/renew, report a job, submit, review, integrate, retrieve lessons. Exercise lost responses, two-session ownership isolation, revocation, expiry, stale policy, no-ready-work, and a reviewer who contributed to the candidate. Every advertised next action and CLI command must match shipped schemas/help.