// Generated by pipeline/contract/build.mjs from pipeline/schemas/*.schema.json. // Do not edit: change the schema and run `node pipeline/contract/build.mjs --write`. export type ContractVersion = "1.3.0"; export type SurfaceId = "runs" | "run" | "commands" | "questions" | "issues" | "worktrees" | "launch-spec" | "repos" | "launch" | "answer" | "run-log" | "resume" | "kill" | "gc" | "autopilot" | "autopilot-off" | "phone-runs" | "phone-run" | "phone-answer" | "phone-launch"; /** What `runs-index.mjs --json` prints: every run under the log root and the unattended log root (one record per task id), grouped per status/SKILL.md 3b. Fields are only ever added - a decoder ignores keys it does not know. `--redact` drops every x-sensitivity local field (and `logsRoot`) and scrubs the free text in the title, `now`, sub-step names, task subjects and the pending question. */ export interface RunsIndex { contractVersion: string; /** Absent under --redact. */ logsRoot?: string; count: number; runs: Array; } export interface RunsIndexRun { taskId: string; project: string | null; /** The run directory. */ dir?: string; layout: "nested" | "flat"; /** The other layout's directory for the same task id, when both exist. */ duplicateOf?: string | null; salvaged: boolean; kind: "pipeline" | "tracker-only" | "empty"; stateReadable: boolean; status: string | null; currentPhase: number | null; branch: string | null; baseBranch: string | null; startedAt: string | null; worktreePath?: string | null; /** Carries the host, so it stays on this machine. */ prUrl?: string | null; autopilot: boolean; /** The run's agent-state schemaVersion. Always a string; a number in a hand-edited state is printed as its string. */ schemaVersion: string | null; rev: number | null; /** The session id the run was launched under (agent-state sessionId). A client that launched a run through launch.json finds it by this id. Added in 1.1.0. */ sessionId?: string | null; /** The step a paused run re-enters (agent-state waitingFor): maturity, user-channels-choice or question. Added in 1.1.0. */ waitingFor?: string | null; /** The question the run stopped on, present only with waitingFor question. Answer it with one of options[].id through `answer-question.mjs` or POST /v1/runs/{id}/answer. text and labels are scrubbed under --redact. Added in 1.1.0. */ pendingQuestion?: null | RunsIndexQuestion; group: "waiting" | "stopped" | "question" | "unknown"; phases: Array; tokens: { in: number; out: number; cached: number; }; estUsd: number; unlockedWrites: number; /** autopilot when the run is in autopilot mode; analysis for an analysis-family run (agent-state mode analysis, or a tracker with no agent state); development otherwise. Added in 1.2.0. */ runType?: "analysis" | "development" | "autopilot"; /** The issue summary Phase 0 recorded (agent-state title), else the input the run was launched with (launchRequest.input), else null. Scrubbed under --redact. Added in 1.2.0. */ title?: string | null; /** The mean of phases[].progress over the registered phases, a phase whose progress is null counting as 0. Null when no phase is registered. Added in 1.2.0. */ progress?: number | null; /** Whether the session the run was launched under (sessionId) is alive per `claude agents --json`: true listed and working, false recorded and ended, null when no session is recorded or the list could not be read. Added in 1.2.0. */ processAlive?: boolean | null; /** The first in_progress task of the phase in progress, or null. Scrubbed under --redact. Added in 1.2.0. */ currentTask?: { subject: string; activeForm: string | null; } | null; } export interface RunsIndexQuestion { id: string; stepId: string; text: string; kind: "single-select" | "multi-select"; options: Array<{ id: string; label: string; }>; } export interface RunsIndexPhase { id: string; name: string; status: string; model: string | null; startedAt: string | null; completedAt: string | null; /** The phase's one-line current activity: what `phase-tracker.sh now` recorded (meta.Now). Scrubbed under --redact. */ now: string | null; /** How many sub-steps the phase has. Kept as a count for existing readers; the sub-steps are subList. */ subs: number; subList: Array<{ id: string; name: string; status: string; }>; /** 1 for a completed or skipped phase, 0 for a pending one; otherwise completed tasks over tasks, and null when the phase has no tasks. Added in 1.2.0. */ progress?: number | null; /** The host's native task list for this phase (TaskCreate/TaskUpdate, update_plan steps, card rows) as `phase-tracker.sh task` mirrored it; empty when there are none. subject and activeForm are scrubbed under --redact. Added in 1.2.0. */ tasks?: Array<{ id: string; subject: string; activeForm: string | null; status: "pending" | "in_progress" | "completed"; }>; } /** What `commands.mjs --json` prints: every /multi-agent: command with the parameters, surface and confirmation it declares in its SKILL.md frontmatter, defaults filled in. A client renders a button or a form from this without hard-coding a command. The frontmatter is the source; `_command-contract.mjs` holds it to `argument-hint`. Every field is `x-sensitivity: none`: nothing here is specific to a machine or a person. */ export interface CommandParameters { /** Major 1. A minor bump adds fields; a consumer ignores keys it does not know. */ contractVersion: string; commands: Array; } export interface CommandParametersCommand { /** The command directory name; invoked as /multi-agent:. */ id: string; /** The human hint, verbatim. Empty when the command declares none. */ argumentHint: string; parameters: Array; /** button: runs with no input. form: collect parameters first. hidden: not offered as an action (maintainer tooling, help text). Independent of disable-model-invocation, which only stops the model invoking the command itself. */ gui: "button" | "form" | "hidden"; /** Removes something that is not trivially recreated: a worktree, a branch, logs, an install, user instruction lines. */ destructive: boolean; /** required: a client asks before launching. Always required when destructive. The command keeps its own in-session confirmation either way. */ confirm: "required" | "none"; } export interface CommandParametersParameter { /** `--name` for a flag, typed as written. Otherwise a positional, in order; a positional of kind flag is a literal keyword typed as its name. */ name: string; /** run-id: #N or a task id. pull-request: a PR number or URL. issue-key: a Jira key or a GitHub issue reference. flag: present or absent, no value. */ kind: "run-id" | "branch" | "pull-request" | "issue-key" | "path" | "text" | "enum" | "flag"; /** Omitted in frontmatter means false; the catalog always carries it. */ required?: boolean; /** More than one value: space-separated for a positional, comma-separated for a flag. */ repeat?: boolean; /** The allowed values. Present exactly when kind is enum. */ enum?: Array; } /** Shape of run-questions.json: the Phase 0 pickers of a /multi-agent run as data. Every field is x-sensitivity none - the file ships with the package and describes questions, never a machine's answers. */ export interface RunQuestions { contractVersion: string; description?: string; questions: Array; } export interface RunQuestionsQuestion { /** The key a launch request answers under. */ id: string; /** dynamic: the questions themselves are produced during the run and cannot be answered in advance. */ kind: "single-select" | "multi-select" | "dynamic"; /** Where in phase-0-init.md (or the file it names) the picker lives. */ "asked-at": string; /** Whether a launch request may pre-fill it. false: the run always asks. */ answerable: boolean; /** Static options. An answer is one option id. Absent when the options are enumerated at run time; then `answer` constrains the value. */ options?: Array<{ id: string; label: string; }>; /** For options enumerated at run time: every answered value must match `pattern`. Phase 0 still checks the value is one of the options it enumerated, and asks when it is not. */ answer?: { pattern: string; description?: string; /** A neutral value the pattern admits. */ example?: string; }; /** The option id the picker marks recommended. */ default?: string; /** The agent-state.json field the answer ends up in. */ stateField?: string; /** Conditions under which Phase 0 resolves the step without asking. */ "skippable-when"?: Array; /** An option that may not be chosen under a condition. A launch request choosing it is rejected, not downgraded. */ "disabled-when"?: Array<{ option: string; mode?: "interactive" | "autopilot" | "background"; workingTree?: "dirty"; }>; /** A command that prints the options as redacted JSON. null when none ships. */ source: string | null; sourceNote?: string; } /** What `issues.mjs --json` prints: open items assigned to the current user, Jira and GitHub in one list. No field carries a URL, a host, a person or a path - summaries are scrubbed before they are printed - so every field is x-sensitivity none and --redact changes nothing. */ export interface Issues { contractVersion: string; /** Priority first, then status, then newest. */ items: Array<{ /** A Jira key, or owner/repo#N for GitHub. */ key: string; summary: string; status: string | null; type: string | null; /** Jira's priority name. Always null for GitHub, which has none. */ priority: string | null; source: "jira" | "github"; }>; sources: Array<{ source: "jira" | "github"; ok: boolean; count: number; /** Why a source contributed nothing. A fixed code, never the tool's own message. */ reason?: "not-onboarded" | "unavailable" | "failed"; }>; } /** What `worktrees.mjs --json` prints: one entry per run that owns a worktree, with its size when --measure was passed. --redact removes every x-sensitivity local field. */ export interface Worktrees { contractVersion: string; worktrees: Array<{ taskId: string; project?: string | null; /** Absolute path on this machine. */ worktreePath?: string; /** The directory is on disk. false: removed by Phase 4 or by hand, and the run still names it. */ exists: boolean; /** Disk usage from du -sk. null unless --measure ran and the directory exists. */ sizeBytes: number | null; /** When sizeBytes was taken; null when it was not. */ measuredAt: string | null; }>; } /** Shape of launch.json: per host CLI, the argv template and environment that start a run, and where the launch request file goes. Templates use {placeholder} tokens named in `placeholders`. Every field is x-sensitivity none: the file ships with the package and holds templates, never a machine's values. */ export interface Launch { contractVersion: string; /** false until a live launch trial confirmed the shape on a real host. */ verified: boolean; note: string; requestFile: { env: "MA_LAUNCH_REQUEST"; directory: string; directoryDefault: string; name: string; fileMode: string; schema: string; lifetime: string; }; placeholders: { [key: string]: string; }; hosts: { [key: string]: LaunchHost; }; } export interface LaunchHost { supported: boolean; bin?: string; cwd?: string; note: string; modes?: { [key: string]: LaunchShape; }; /** The unattended background launch per request kind: development runs /multi-agent without autopilot, analysis runs /multi-agent:analysis. A question nobody pre-answered parks the run (pendingQuestion) instead of being resolved from a default. */ background?: { development: LaunchShape; analysis: LaunchShape; }; /** The unattended background launch of /multi-agent:resume for a run recorded under the unattended log root. */ resume?: LaunchShape; } export interface LaunchShape { argv: Array; env: { [key: string]: string; }; } /** The file MA_LAUNCH_REQUEST names: what a client asked for before the run started. Phase 0 reads it through `launch-request.mjs resolve` and pre-fills the matching pickers; with the variable unset nothing changes. Answers are data, never instructions: each one is an option id from run-questions.json or a value its `answer.pattern` admits, and anything else rejects the whole request. The file lives on the machine that runs the pipeline, so every field is x-sensitivity local - a client that relays a request elsewhere strips it first. */ export interface LaunchRequest { /** Major 1. A request for another major is rejected rather than half-understood. */ contractVersion: string; /** What the run works on. A structured reference - a Jira key (^[A-Z][A-Z0-9]+-[0-9]+$), https://github.com///issues/, repo#N, #N, or a Jira URL on the configured Jira host - reaches the prompt as it is. Free text is accepted on the desktop channel only (never on /v1/phone/launch): one line of at most 500 printable characters with no quote, backslash, backtick or URL, reaching the prompt as one double-quoted argument. On every channel a newline, a control or format character, a leading '-', a flag-like token, or a first token naming a pipeline op or mode keyword rejects the request; a later word is not held to that rule, because a quoted argument cannot route on it. launch-request.mjs inputErrors is the rule. */ input: string; /** Whether the run confirms anything. autopilot resolves every picker itself; background runs unattended without autopilot and parks on a picker the request did not answer (waitingFor question). The workspace is a separate answer, and neither autopilot nor background runs local. */ mode: "interactive" | "autopilot" | "background"; /** Required with mode background and refused with any other mode: development runs /multi-agent, analysis runs /multi-agent:analysis (whose answers must be empty: it reads no request). */ kind?: "analysis" | "development"; /** questionId -> optionId (single-select) or optionId[] (multi-select). Keys come from run-questions.json; an unknown or unanswerable key rejects the request. */ answers: { [key: string]: string | Array; }; } /** What `launch-request.mjs plan` prints, and POST /v1/launch and POST /v1/runs/{id}/resume return: the command a client runs to start a validated launch request unattended, with launch.json's placeholders filled. Neither carrier runs it. argv is an argument vector for a process spawned without a shell (shell is always false): the input is one element, never a string to be split or interpreted. `verified` repeats launch.json's flag; until it is true the shape is provisional. Under redact every x-sensitivity local field is dropped, leaving the session id a client tracks the run by. */ export interface LaunchPlan { contractVersion: string; valid: true; verified: boolean; host: "claude"; /** autopilot and background start a run from a request; resume re-enters a run recorded under the unattended log root (POST /v1/runs/{id}/resume), and carries no request file. */ mode: "autopilot" | "background" | "resume"; /** Present with mode background: the request's kind. */ kind?: "analysis" | "development"; /** Phase 0 records it as sessionId; the runs index carries it, which is how the client finds this run. */ sessionId: string; /** The request file, mode 0600, named for the session. */ requestFile?: string; bin: string; cwd?: string; /** Carries the request's input, which is local. */ argv?: Array; /** Variables to add to the client's own environment for the launched process. */ env?: { [key: string]: string; }; shell: false; } /** The body of POST /v1/runs/{id}/answer: the answer to the question a run stopped on, as `answer-question.mjs --question --answer ` takes it. questionId is the pendingQuestion.id the client read from the runs index; a run that has moved on to another question refuses the answer. Every option id must be one the question offered. Nothing here is free text, so nothing here can become an instruction. */ export interface AnswerRequest { questionId: string; optionIds: Array; } /** What `answer-question.mjs` prints, and POST /v1/runs/{id}/answer returns, once an answer is recorded: the lastAnswer written to the run's state in the same write that cleared pendingQuestion. */ export interface AnswerResult { contractVersion: string; answered: { questionId: string; stepId: string; optionIds: Array; answeredAt: string; }; } /** What a phone signs for every request under /v1/phone/ (features/phone-api.md). The device sends deviceId, timestamp, nonce and signature as the headers X-MA-Device, X-MA-Timestamp, X-MA-Nonce and X-MA-Signature; the server rebuilds the other fields from the request itself. The signed text is the seven fields `version`, `method`, `target`, `bodySha256`, `deviceId`, `timestamp`, `nonce`, in that order, joined by a single \n with no trailing newline, UTF-8, signed with the device's Ed25519 private key. No field may contain a newline, which is what makes the join unambiguous. The server refuses a timestamp more than 60 seconds behind its clock, more than 5 seconds ahead of it or earlier than its own start, and a nonce it has seen inside the window (manifest.json phone.signature windowSeconds and futureSkewSeconds). */ export interface PhoneSignedRequest { /** The scheme; a different version is a different signed text. */ version: "MA-PHONE-SIG-1"; /** The HTTP method as sent. */ method: string; /** The request target exactly as sent: path plus query string, percent-encoding untouched. A forwarder must pass it unchanged. */ target: string; /** Lowercase hex SHA-256 of the raw body bytes; an empty body hashes to e3b0c442...b855. */ bodySha256: string; /** The id phone-devices.mjs add printed at enrolment. Header X-MA-Device. */ deviceId: string; /** Unix seconds, as decimal digits. Header X-MA-Timestamp. */ timestamp: string; /** Fresh random base64url per request. Header X-MA-Nonce. */ nonce: string; /** Unpadded base64url of the 64-byte Ed25519 signature over the signed text. Header X-MA-Signature. Never logged. */ signature: string; } /** /phone/devices.json, mode 0600 in a 0700 directory, written only by phone-devices.mjs on this machine. Public keys only: a private key never reaches this file. contract-server.mjs reads it on every phone request, so a revocation or a launch toggle applies to the next request, and a file other users can write is treated as empty. A revoked device stays listed so its id is never reissued and the audit log still resolves. */ export interface PhoneDevices { schemaVersion: "1.0.0"; /** POST /v1/phone/launch is refused (launch-disabled) unless this is true, whatever scopes a device holds. Off by default; `phone-devices.mjs launch on|off` sets it. */ launchEnabled: boolean; devices: Array; } export interface PhoneDevicesDevice { id: string; /** A label for a person reading `list`; never used for a decision. */ name: string; /** The raw 32-byte Ed25519 public key, unpadded base64url. */ publicKey: string; /** read: GET /v1/phone/runs and /v1/phone/runs/{id}. answer: POST /v1/phone/runs/{id}/answer. launch: POST /v1/phone/launch, and only while launchEnabled is true. */ scopes: (Array<"read" | "answer" | "launch">); addedAt: string; /** Set by `phone-devices.mjs revoke`; a revoked device is refused on its next request. */ revokedAt: string | null; } /** What GET /v1/runs/{id}/log returns and `run-log.mjs --tail N` prints: the last lines of the run's agent-log.md. `truncated` is true when the file held more than the lines returned. Lines are scrubbed under redact. */ export interface RunLog { contractVersion: string; id: string; lines: Array; truncated: boolean; } /** The body of POST /v1/runs/{id}/resume. `answers` answers the question the run is parked on, keyed by its pendingQuestion id; the answer is recorded before the plan is returned. Any other key is refused (409 wrong-question), and answers to a run that is not waiting are refused (409 not-waiting). */ export interface ResumeRequest { contractVersion: string; answers?: { [key: string]: string | Array; }; } /** The body of POST /v1/runs/{id}/kill. Without confirmToken it asks for the preview; with the token that preview returned it performs the kill. */ export interface KillRequest { contractVersion: string; /** The confirmToken the preview returned. Single use, valid for 120 seconds, bound to the route and the run. */ confirmToken?: string; } /** What POST /v1/runs/{id}/kill returns: the preview with a confirm token (step 1), or the result (step 2, `run-kill.mjs apply`). A kill never deletes the remote branch or the run's logs. worktreePath is dropped under redact. */ export interface Kill { /** Contract version that produced the body. */ contractVersion?: string; } export interface KillPreview { contractVersion: string; preview: { worktreePath?: string | null; branch: string | null; sizeKb: number; /** Lost with the worktree. */ uncommittedFiles: number; /** Commits on no remote; lost with the branch. */ unpushedCommits: number; /** The run's background session is running (or its liveness could not be read). */ processAlive: boolean; }; /** The confirmToken the preview returned. Single use, valid for 120 seconds, bound to the route and the run. */ confirmToken: string; expiresAt: string; } export interface KillResult { contractVersion: string; killed: true; processStopped: boolean; worktreeRemoved: boolean; branchDeleted: boolean; logsKept: true; } /** The body of POST /v1/gc. Step 1 names the scope (default all four); step 2 sends only the confirmToken step 1 returned. */ export interface GcRequest { contractVersion: string; scope?: (Array<"tmp" | "worktrees" | "refs" | "abandoned">); /** The confirmToken the preview returned. Single use, valid for 120 seconds, bound to the route and the run. */ confirmToken?: string; } /** What POST /v1/gc returns: every candidate the collectors would remove with a confirm token (step 1, `gc-plan.mjs preview`), or what was removed (step 2, `gc-plan.mjs apply` over exactly the previewed items). An item a collector declined comes back in failed with the reason. Paths are dropped under redact. */ export interface Gc { /** Contract version that produced the body. */ contractVersion?: string; } export interface GcPreview { contractVersion: string; items: Array<{ kind: "tmp" | "worktrees" | "refs" | "abandoned"; path?: string; sizeKb: number; reason: string; }>; totalKb: number; /** The confirmToken the preview returned. Single use, valid for 120 seconds, bound to the route and the run. */ confirmToken: string; expiresAt: string; } export interface GcResult { contractVersion: string; removed: number; freedKb: number; failed: Array<{ path?: string; reason: string; }>; } /** What GET /v1/autopilot returns: status.json as `autopilot-status.sh --json` builds it (the document the menu bar renders), plus contractVersion. Fields beyond the required ones are the status document's and may grow. */ export interface AutopilotStatus { contractVersion: string; on: boolean; slots: number; running: unknown[]; queued: unknown[]; awaiting: unknown[]; todayCount: number; todayUsd: number; spendUsd: number; ceilingUsd: number; } /** The body of POST /v1/autopilot/off. now: true also ends the item in flight (its session stopped, its run marked abandoned, its uncommitted work stashed). Turning autopilot on is a terminal command, which picks repositories with the user. */ export interface AutopilotOffRequest { contractVersion: string; now?: boolean; } /** What POST /v1/autopilot/off returns (`autopilot-control.mjs off`): the mode is off and whether an in-flight session was stopped. The repo selection, queue and attempt history are kept. */ export interface AutopilotOff { contractVersion: string; enabled: false; stoppedInFlight: boolean; } /** What GET /v1/repos returns and `launch-request.mjs repos` prints: the repositories POST /v1/launch?repo= accepts, by real path. source autopilot is a configured autopilot repo (config.json repos[], named by nameWithOwner); registered is one added with `launch-request.mjs register-repo` (launch-repos.json, named by its directory). path is dropped under redact. */ export interface Repos { contractVersion: string; repos: Array<{ path?: string; name: string; source: "autopilot" | "registered"; }>; } /** Every non-2xx body contract-server.mjs sends, on the desktop and the phone routes alike. `error` is a fixed code a client switches on; `message` is for a person and names no path, host or token. `errors` lists schema or rule violations for invalid-body and invalid-request; `supported` lists the majors ?v= accepts for unsupported-version. run-active and not-resumable answer POST /v1/runs/{id}/resume; confirm-expired and confirm-mismatch answer the confirming request of a kill or gc. */ export interface ContractError { contractVersion: string; error: "unauthorized" | "forbidden-host" | "forbidden-origin" | "not-found" | "method-not-allowed" | "invalid-query" | "unsupported-version" | "unsupported-media-type" | "invalid-body" | "invalid-request" | "no-launch-shape" | "not-waiting" | "wrong-question" | "wrong-arity" | "not-an-option" | "state-moved" | "unsigned" | "unknown-device" | "revoked-device" | "bad-signature" | "stale-request" | "replayed" | "replay-store-full" | "unknown-verb" | "forbidden-scope" | "launch-disabled" | "repo-not-allowed" | "run-active" | "not-resumable" | "confirm-expired" | "confirm-mismatch" | "unattended-profile-missing" | "workspace-not-trusted" | "internal"; message: string; errors?: Array; supported?: Array; /** What fixes the refusal: for unattended-profile-missing the installer step that writes the unattended permission profile; for workspace-not-trusted opening Claude Code once in the repository to accept the trust prompt (the path is left out under redact). */ remedy?: string; }