Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 statusRepresentative codeClient action
400invalid_requestCorrect the indicated field
401authentication_requiredShow public setup help or reconnect with configured credentials
403operation_not_permittedShow the missing operation permission; do not re-enroll an already authenticated client
404record_not_foundReconcile the authenticated project’s binding/reference
409claim_conflict, revision_conflict, lease_expired, policy_changedFollow the supplied current-state and next-action links
413payload_too_largeReduce/split the report or use an external artifact link
422requirements_unsatisfiedAddress explicit blockers or validation details
429rate_limitedHonor Retry-After; a delayed retry never extends an expired lease
503temporarily_unavailableBack 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_at is 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 (integer 0–8760, default 24) keeps sessions whose last activity falls inside the window; 0 lists 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 are 400.

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/queue lists 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 with authority_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 and targets, 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 carries reverted: {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 (r equal to t0) whose candidate commits intersect a reverted landing, and revert_task_id: the subject’s own task id when it is a revert task in mechanical mode (null otherwise), so the integrator reports a reproduced check failure of that candidate not-mechanical rather than revising it. The response adds reverts, the revert tasks awaiting a mechanical candidate (see Reverts below): {id, task_id, result_id, r, title, priority, repository_url, target_branch, target}, where id is the revert task id and target is {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 answer integration_owned_by_agents.

  • POST /api/v1/projects/{project_id}/integrator/results pins one integration result r (with its tree, the target tip t0 and tree it was computed on, the landing range and the roster read from t0) per submission and t0. Sending the same result again returns the stored one; a different r for the same pair is result_conflict, and a c that is not the submission’s candidate is candidate_changed. A new no-op result (r equal to t0) whose c or 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 with candidate_reverted_in_history (details.reverted_results and the shared details.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/receipts records one GitHub Actions check run observed on r (head_sha must equal r, otherwise receipt_head_mismatch). A run attempt is recorded once; a different outcome for it is receipt_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-progress with {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 still queued or in_progress (at most 100; completed runs are receipts). head_sha must equal r (progress_head_mismatch) and every check_name must be in the result’s roster (progress_check_not_required). The snapshot and its time feed the processing object that task lists and details show for a waiting_integration task (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-authority with {result_id} authorizes pushing r. It refuses checks_not_passed (details list pending and failed roster checks) unless every roster check has a deciding run with the roster’s workflow blob that concluded success, skipped or neutral (as a GitHub required check passes); protected_check_missing when the roster drops a check in the project’s workflow policy; stacked_on_unapproved when the landing range carries commits of another task’s submission that is neither approved nor integrated; observation_required while another result of the submission holds authority; integration_target_held while another integration holds the target; privilege_gate_unresolved while a privilege_gate report on the result is open; and privilege_gate_denied once a human resolved one with deny (both name the report as details.report_id). A result with no privilege_gate report, or whose report carries the decision allow, 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/observations with {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, or already_contained when r equals t0; published_after_reopen if a human reopened the submission meanwhile); a published revert whose reason is defect also proposes its re-land task; equal_t0 (not_published) and moved (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 is observation_required), and r is observed published only while its result holds authority (otherwise authority_not_issued). Never infer publication from a push exit code.

  • POST /api/v1/projects/{project_id}/integrator/revise with {submission_id, reason_code, evidence, result_id?, moved_by_result_id?} sends a candidate back to its author; only conflict, check_failed and reverted_in_history are accepted, never while push authority is outstanding (observation_required). reverted_in_history sends back a candidate whose no-op result results refuses with candidate_reverted_in_history; its evidence names 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, otherwise not_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’s reverted, and when results refuses with candidate_reverted_in_history, citing details.commits. It counts toward the limits below like any integrator revise and never serializes. A conflict revise may name in moved_by_result_id the 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 a published observation, and otherwise ignores it. The response carries serialized_after (a task id, or null), park_reason (or null) and, under revise, the resolved landing_task_id. A subject parks for agents (the unmet revise_limit_reached precondition; 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 a conflict citing 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, and serialized_after names the landing task, so the subject stays claimable. Otherwise that revise applies and parks the subject, and park_reason says why: the reason is not conflict (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 reports serialized_cap. A revise of a subject that is already parked is refused with details.gate: "revise_limit_reached" and details.park_reason (subject_parked, or serialized_cap). A check_failed revise must name in result_id a result of that submission on which one roster check, under the roster’s workflow blob, is reproduced: its deciding run concluded failure or timed_out, and at least two of its attempts did. Otherwise it is refused with check_failure_not_reproduced (details give each roster identity’s deciding conclusion and failure count under checks), or not found when the result is not the submission’s.

  • POST /api/v1/projects/{project_id}/integrator/reports with {kind, dedupe_key, task_id?, submission_id?, result_id?, details} records a finding for the digest. kind is privilege_gate, flaky, fix_target, unreviewed_landing, target_rewritten or ruleset_missing; a privilege_gate report must name its result_id. The first report per kind and dedupe_key wins: sending it again returns the stored report, whatever its details, including any resolution. The service sets requires_human for privilege_gate, target_rewritten and ruleset_missing, and for any report whose details.blocks_subject is true (the integrator sets it on fix_target reports and on flaky reports that leave a subject blocked: no_result and rerun_refused), so a subject never waits unseen. A reverted_in_history revise the service refuses with not_reverted_in_history is a fix_target report with verdict not_reverted_in_history, and the integrator skips the subject. The integrator raises privilege_gate, failing closed, when any path that differs between the target tip and the result lies under .github/, is the .github entry itself, .gitmodules or a CODEOWNERS file, or is in the local-action scope read from the target tip (paths compared ignoring letter case). That scope holds the directories named by uses: ./<dir> in files under .github/ and in the referenced actions’ definitions, the paths a referenced action’s main, pre, post and local image name, 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. A uses value 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 is unreadable. The report lists each gated path with reason codes that hint at what changed (for example adds_secrets, removes_permissions, triggers_changed, changed_ci_definition or changed_local_action); the codes do not decide the gate. It pushes that result only once the report shows allowed: true. It raises ruleset_missing (once per freeze episode, which ends when the rules are back) and target_rewritten when 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 is target_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 tip t0 of that result up to its r) 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 (a Claude-Session trailer, or a Co-authored-by trailer naming Claude or Codex or the address [email protected] or [email protected]), it raises unreviewed_landing, keyed by target, from and to, with details {repository_url, target_branch, from, to, integrator_results, flagged, flagged_count, unflagged_count}; flagged lists flagged commits as {sha, subject, trailers}, cut to fit the details size limit (flagged_count is 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 on r as GitHub does: success, skipped and neutral pass, failure and timed_out fail, and any other conclusion (such as cancelled) 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 the actions: write permission) and posts a receipt for every attempt. A pass after a failure is a flaky report 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 a flaky report with verdict no_result or rerun_refused and skips the subject; flaky reports 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 with check_failed only when the same check’s latest attempt with a result on the target tip passed and started after the check’s first failure on r, 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 raises fix_target with verdict target_failing or target_unverified (one per target, tip, verdict and check) whose details carry target_result (run_id, run_attempt, conclusion, started_at, completed_at, stale) and skips the subject until the tip moves or the check passes there. A check_failed revise 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 revert not-mechanical with reason check_failed instead of revising it.

  • While a subject is in integration and an open requires_human report blocks it, the task’s workflow view (workflow in tasks show, the workflow route, orientation and the console task page) carries integration_blocked, else null: {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; a fix_target report with blocks_subject also blocks every subject of the same repository and target branch, because the target is what needs fixing (target_revision is the failing tip, check_name the failing check). next_actions then holds action (fix the target, see the report, resolve it) instead of the claim guidance. blockers and work_status are unchanged. The field is null again once a human resolves the report or the subject integrates.

  • GET /api/v1/projects/{project_id}/integrator/reports lists a project’s reports, newest first, for any authenticated reader. open=true keeps only unresolved ones; limit is 1 to 1000 (default 200); before=<report_id> continues after that report, and each page’s next_before is 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 and check_name, repository_url and target_branch keep only reports whose details name exactly that check, repository or target branch; every filter combines with the others, with open and with paging. An unknown kind, or a check_name or target_branch over 512 bytes, a repository_url over 2,048 bytes or a task_id over 100 bytes, is a 400 bad_request. An unknown before is not_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, is not_found. The native integrator-reports list and show commands and the MCP tools coordinator_integrator_reports_list and coordinator_integrator_report_get read these two routes; none of them resolves a report.

  • POST /api/v1/projects/{project_id}/integrator/reports/{report_id}/resolve with {note, decision?} is human-only (other callers get the integrator_report_resolution gate). A privilege_gate report needs decision allow (its result may be pushed; the report then shows allowed: true) or deny; other kinds take no decision. A resolved report is report_already_resolved.

  • GET /api/v1/projects/{project_id}/next?role=integrator returns the queue head as an integrate action (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}/reverts with {result_id, reason, evidence?, note?, priority?} creates a revert on an integrator-owned project. reason is defect, author_withdraw, audit_rejection or human; priority defaults to 1. result_id must name a result of this project (otherwise not found) with a published observation whose task is done (otherwise result_not_published). A second revert of a result whose revert task is still planned or open is revert_exists (details.revert_task_id); reverting a revert’s own result is allowed. A human’s revert needs no review and records a revert.escaped_defect_canary event. An agent’s revert needs a reason other than human (otherwise the revert_without_evidence human gate) and non-empty evidence, text or JSON (otherwise revert_evidence_required); its candidate needs the project’s reviews and at least an agent review even when review_mode is none. 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 mode is mechanical only the integrator produces the candidate: claiming or unblocking the task is refused with revert_awaits_integrator, it is skipped by next-eligible claims, and a revise of its candidate blocks it again and returns it to the queue’s reverts. 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, or service for 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 (or agent_rule_editing for cancel). A canceled or archived revert leaves the integrator’s queue and takes no candidate (revert_not_open), and the original’s reverted_by no longer names it; restoring an archived revert is refused with revert_exists while 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 with revert_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’s landing_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 checkpoint contributor_session_ids) and, because a cancel or archive cannot assert an independence level, the distinct_launch check (workflow::shares_launch: no launch shared with a contributor, failing closed beyond the 64-hop walk cap); distinct_host and distinct_vendor are 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’s reverts. The integrator computes the candidate itself on the target’s current tip X, in a disposable worktree: git revert -m 1 R when R is its merge into t0, otherwise the landed range t0..R reverted as one step. It commits with a pinned identity and date and the message Revert "<candidate subject>" with the trailers Agent-Coordinator-Revert-Task and Agent-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 builtin union, still apply to the revert (they are reviewed repository content). A clean revert is pushed create-only to refs/agent-coordinator/candidates/reverts/<task_id>/<X> and recorded with the candidate route below, t0 being X. A revert that conflicts on X, whose R is not in X’s history, or that would change nothing on X is reported not-mechanical with reason conflict, 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 as revert_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}/candidate with {t0, candidate_commit, candidate_tree, mechanical: true, candidate_ref?} records that candidate as the revert’s code submission, created by the integrator with the attestation mechanical and based on t0. 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 same t0 again 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 that t0 is revert_candidate_conflict. Other refusals: revert_not_mechanical, revert_not_open, revert_claimed, revert_candidate_exists (a candidate is already in review or integration) and workflow_policy_required.
  • POST /api/v1/projects/{project_id}/integrator/reverts/{task_id}/not-mechanical with {t0, reason, evidence} (reason is conflict or check_failed) reports that the revert cannot be computed mechanically. The service supersedes any candidate still in review or integration (refused with observation_required while 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 same t0 and reason again replay; anything else is revert_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 rejection records the review (activity_id, reviewer_id, summary, rejected_at), it leaves the queue and the original’s reverted_by no 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 defect is observed published, 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’s reland_task_id names 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-rows with {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}: role is impl or rev, usd is dollars or null when the launch was unpriced (stored as exact millionths), canary marks a row of the host’s canary project, and task_id, session_id and attempt_id are the host’s claims, not checked against the service’s records. Unknown fields are refused.
  • Every telemetry route answers 404 project_not_found with details.project_id for 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-results with {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}: outcome is ok, failed or timeout, and served_by is the list of harnesses the host’s ledger shows for the task, or null.
  • Both writes are idempotent twice over. The usual Idempotency-Key replays the saved reply. Independently, row_id (the host’s stable identity of one ledger row; the supervisor and e2e-canary.py use 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 one telemetry.cost_reported or telemetry.canary_reported event (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 is 401. An unknown project is not_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 has impl and rev figures {launches, usd, unpriced_launches, estimated_launches, input_tokens, cached_input_tokens, output_tokens}, total_usd and last_at, plus task_id and the task’s title (null for an unknown task) or day (YYYY-MM-DD). totals sums every matching row; truncated says limit (1 to 1000, default 100) cut groups off. Rows of a canary project and of budget-exempt (canary) tasks are left out unless include_canary=true, as the digest leaves them out.
  • GET /canaries?host=&harness=&outcome=&since_ms=&limit=&before= returns the canary history, newest run first (limit 1 to 1000, default 100; before is a result id and an unknown one is not_found; next_before continues), and latest, the newest result of every host and harness the project has seen.
  • POST /caps with {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 (403 for a human, read-only or integrator credential; unknown fields are refused) and emits one telemetry.caps_reported event. agentc-supervisor reports 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, and parked_subjects, subjects an integrator revise parked); parked (count and items: the open tasks the revise limit holds from agents now, with agent_revises_24h and counted_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); and caps, the latest report of each host. Stalls have no recorded start, so they are not part of human_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; an end_ms after the service’s clock is refused with 400 window_not_closed, and a missing, reversed or too wide window with 400 invalid_request. Every section except current_state is computed only from the window and from records stamped no later than end_ms, so the same query returns the same figures whenever it is asked (a retained audit fixture reproduces); it never reads the clock. The rolling GET /autonomy read is unchanged. The reply (schema: "autonomy-window-v1") keeps its sources apart:
    • value: tasks that reached done in the window (workflow_subjects.updated_at where the phase is done, plus task.completed_externally events for tasks not already counted), as completed (count, by_origin human, agent and service, completed_externally, by_day); human_origin (count, per_day over the exact window width); and latency (task created_at to 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 of integrator_direct_push (integrator observations published or published_after_reopen), agent_integration_published, already_contained_only and no_publication_evidence has count, ratio (null with an empty denominator) and its evidence. status is partial while any completed task has no publication evidence. Push time is unavailable: 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 after end_ms is 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_reporter and unclassified, an agent credential with no recorded class, such as an event from before classes were recorded), and by_kind with each kind’s events and distinct records. interventions.events is the raw count of interactive and human-session events, not a count of distinct interventions. classification.status is partial while unclassified events exist; audit_log gives 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 after end_ms count as running. started_in_window counts them, items lists up to 100 (truncated says more exist), carried_in counts those that started earlier and had no submitting attempt before the window (a task canceled meanwhile is still counted), and attempts_without_end_time counts attempts whose end was not recorded. episodes.parks lists 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. components lists the sources (interactive and human events, human-required reports, parked subjects, stall episodes started). status is complete only when every source was available; otherwise it is partial, count is null, lower_bound is what the available sources recorded, outcome is unknown and unavailable_sources names each missing source (the audit log not reaching the window start, unclassified events, attempts without an end time). A complete zero reads outcome: "none_recorded".
    • current_state: the only query-time section. as_of is the clock, and stalled_tasks and parked are 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.

OperationEndpointRequired content
Inspect current workGET /attempts/{id}Returns current authority and historical outcome
RenewPOST /attempts/{id}/renewGeneration and health observation; no invented progress
Register checkoutPOST /attempts/{id}/checkoutWorkstation, resolved worktree identity, branch, base revision, local path, clean/dirty state
CheckpointPOST /attempts/{id}/checkpointsGeneration, summary, current action, next step, blockers, revision/job references
Reserve resourcesPOST /attempts/{id}/reservationsComplete requested resource/unit set; grant all or none
Delegate reportingPOST /attempts/{id}/reportersCurrent generation, permitted attempt/job IDs, bounded reporting deadline, client-generated reporter-proof verifier
SubmitPOST /attempts/{id}/submitGeneration, expected task/policy revisions, outcome evidence, handoff, lessons
RelinquishPOST /attempts/{id}/releaseGeneration, final checkpoint, ready/blocked disposition and reason
Resolve recoveryPOST /attempts/{id}/recovery-resolutionGeneration, inspected evidence, disposition and outstanding holds
Add late informationPOST /attempts/{id}/late-notesHistorical 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

SurfaceOperations and behavior
TasksCreate, get/list, revision-checked edit/admit, dependencies, cancel, supersede, human-only complete-external for work delivered outside the coordinator; no unrestricted status write
JobsRegister before local launch; append authorized observations; report producer terminal status; inspect without relaunching
ArtifactsExplicit bounded upload, authenticated metadata/download, authorized deletion with retained tombstone; external link registration
KnowledgeCreate/search/get, revision-checked correction and supersession, usefulness feedback, explicit rule adoption under current delegated permission
DecisionsOpen a scoped question, inspect pending answers, record an answer under the required actor type, preserve the authorization it conveys
Project policyGet current/history, revision-checked update under human or delegated agent authority
EventsCursor-based authenticated history, filter by project/task, no credential values
ImportsUpload/inspect source, preview mappings/conflicts, then explicitly apply a versioned preview
ExportsGenerate 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.