# Agents Shipgate · Long-Form Agent Reference (llms-full.txt)

> Single-fetch concatenation of the canonical agent-facing reference
> material. AI search engines and coding agents that prefer one document
> over chasing links should fetch this file. The short index is at
> [`llms.txt`](llms.txt); machine-readable triggers are at
> [`docs/triggers.json`](docs/triggers.json).
>
> Generated by `scripts/build-llms-full.py` from the source files below.
> Do not edit by hand — re-run the script to update.

## Sources (in order)

- [`AGENTS.md`](AGENTS.md)
- [`docs/agent-recipes.md`](docs/agent-recipes.md)
- [`docs/agent-contract-current.md`](docs/agent-contract-current.md)
- [`docs/checks.md`](docs/checks.md)
- [`docs/concepts.md`](docs/concepts.md)
- [`docs/autofix-policy.md`](docs/autofix-policy.md)

---


<!-- ===== source: AGENTS.md ===== -->

# Agents Shipgate · Agent Instructions

Authoritative instructions for AI coding agents (Claude Code, Codex, Cursor, Aider, Cline, Windsurf, Devin, or any other harness — see [`docs/agents/any-coding-agent.md`](docs/agents/any-coding-agent.md)) working **with** this repository or a project that uses Agents Shipgate.

> If you are a human, the README and the [wiki](https://github.com/ThreeMoonsLab/agents-shipgate/wiki) are the right places to start. This file is optimized for agent ingest: short, copy-pasteable, machine-friendly.

---

## What this project is

The deterministic merge gate for AI-generated agent capability changes. Reads `shipgate.yaml` plus tool sources (MCP exports, MCP server source (TypeScript/Go registration idioms), OpenAPI specs, OpenAI Agents SDK Python files, Anthropic Messages API tool/prompt artifacts, Google ADK Python/config files, LangChain/LangGraph Python files, CrewAI Python files, OpenAI API artifacts, Codex repo config, Codex plugin packages and marketplaces, n8n workflow JSON/stubs, Conductor OSS workflow JSON) and produces deterministic findings. Local-first and static by default — no agent execution, tool calls, LLM calls, or network access.

- **Inputs:** MCP · MCP server source · OpenAPI · OpenAI Agents SDK · Anthropic Messages API · Google ADK · LangChain/LangGraph · CrewAI · OpenAI API · Codex config · Codex plugin · n8n · Conductor OSS workflow JSON
- **Outputs:** Markdown · JSON · SARIF
- **Trust:** Static-by-default. No agent execution, tool calls, LLM calls, or network access.
- **Marketing site:** [threemoonslab.com](https://threemoonslab.com/) — human-readable companion pages. **If you are an agent working inside this repo, use the in-tree [`.well-known/agents-shipgate.json`](.well-known/agents-shipgate.json) (current `main` contract, may be ahead of the site's released copy) for schema-version and gating-signal decisions.**

---

## Naming (canonical)

Use exactly one form depending on context. Mixing them in user-visible copy is an adoption cost.

| Form | When to use |
|---|---|
| **Agents Shipgate** | Display name. Prose, headings, marketing copy, social cards, slide titles, blog posts. |
| **`agents-shipgate`** | Package, CLI binary, repo, GitHub Action, PyPI distribution name, env-var prefix (`AGENTS_SHIPGATE_*`), import path (`agents_shipgate`). Always lowercase, kebab-case. |
| **`shipgate`** | Short alias for the CLI binary only. Acceptable in shell snippets where brevity helps; never as the project name. |

Do **not** use any of: `Agent Shipgate` (singular), `Agent Shipcheck`, `agents shipgate` (display lowercase), `Agents-Shipgate` (display kebab). When in doubt: prose → `Agents Shipgate`; code → `agents-shipgate`.

The canonical tagline is:

> The deterministic merge gate for AI-generated agent capability changes.

This single sentence is the source of truth for the GitHub repo description, [README.md](README.md), the [wiki Home page](https://github.com/ThreeMoonsLab/agents-shipgate/wiki/Home), and the [marketing site](https://threemoonslab.com/) `<meta name="description">`. Keep them in sync; the website's `.well-known` discovery file is pinned to the latest released tag and refreshes at each release.

Use **Tool-Use Readiness** in Title Case when naming the product/category or
the **Tool-Use Readiness Report** artifact. Use **tool-use readiness** in
sentence case when describing the general concept.

---

## Install (canonical)

```bash
pipx install agents-shipgate
```

Alternatives if `pipx` is unavailable:

```bash
python -m pip install agents-shipgate                   # global pip
uv tool install agents-shipgate                          # via uv
python -m agents_shipgate --help                         # run from a pip install without PATH
```

The CLI binary is `agents-shipgate`. A short alias `shipgate` is also installed.

---

## Run (canonical)

Handling a capability change right now? Start with **Local control** below —
run First-time setup only when the repo has no `shipgate.yaml` yet.

**First-time setup** — in a repo that contains an agent and its tools:

```bash
agents-shipgate init --workspace . --write
agents-shipgate scan -c shipgate.yaml
```

Reports land at `agents-shipgate-reports/report.{md,json}`.

**Local control for coding agents** — before reporting an agent-capability
change complete, run the local control loop and parse stdout JSON:

```bash
shipgate check --agent codex --workspace . --format agent-boundary-json
shipgate check --agent claude-code --workspace . --format agent-boundary-json
shipgate check --agent cursor --workspace . --format agent-boundary-json
```

`--agent` identifies the caller; it never selects host coverage. Every
recognized changed Codex, Claude Code, Cursor, VS Code MCP, shared trust-root,
and GitHub workflow surface is evaluated on every run.

Read the single stdout object as `shipgate.agent_boundary_result/v1`. Switch on
`control.state`; inspect `input_coverage`, `host_coverage`, `affected_hosts`,
`policies`, `violations`, and `issues`; then follow `control.next_action`,
`control.allowed_next_commands`, and `control.human_review`. Treat `decision`
as diagnostic context, never as the operational control signal, and never
infer control from Markdown, PR comments, or prose. If
`control.state=complete`, summarize the result and finish. If
`control.state=agent_action_required`, perform only the exact coding-agent
action authorized by `control.next_action`, then rerun the command. If
`control.state=review_publishable`, a human must approve the merge — surface
the JSON result, and note that you may still commit, push, and update the pull
request so that review can happen. If `control.state=human_review_required`,
stop and surface the JSON result to a human. `control.permissions` states the
authority exactly: updating a PR is never merging it, and
`permissions.merge`/`permissions.report_complete` are false on every state but
`complete`. Conversation-level acknowledgement never changes control state;
only a new verifier artifact can clear it.

**Before editing a protected release surface** — ask the proactive static
planner first:

```bash
agents-shipgate preflight --workspace . --plan - --json
agents-shipgate preflight --changed-files changed.txt --json
agents-shipgate preflight --capability-request request.json --json
```

Switch on `control.state`. If it is `human_review_required`, stop and route the
change to a human. If it is `agent_action_required`, perform only the exact
coding-agent route in `control.next_action`. The plan form accepts `changed_files[]`,
`diff_text`, `capability_requests[]`, `host_permission_requests[]`, and
`context.{agent,task}`; prefer it whenever the agent can describe the planned
change as one JSON object. Protected surfaces include
`shipgate.yaml`, `.github/workflows/agents-shipgate.yml`,
`AGENTS.md`/`CLAUDE.md`/Cursor rules, policy packs, baselines, waivers,
suppressions, Codex hooks/config, Codex plugin manifests, `.mcp.json`,
`.app.json`, and `SKILL.md`. Preflight is a routing/projection surface only;
`release_decision.decision` remains the release gate.

Contract v32 makes instruction protection conditional on parsed structure.
For a prose edit, supply the complete proposed `diff_text` to preflight; a
path-only plan still routes to review. Only an explicit
`protected_surface_touches[].instruction_structure_unchanged: true` on that
exact path proves the supported structure did not change. Unknown/malformed
frontmatter, inline preprocessing changes, registration moves and configured
manifest/policy edits retain their review route. This does not judge prompt
safety or grant edit/merge authority. Verifier v0.17 and handoff v9 publish
`conditional_file_edits` separately from unconditional `forbidden_file_edits`;
follow current `control` as before. See the
[instruction structure boundary](docs/engineering/instruction-structure-boundary.md).

**PR / reviewer evidence** — for committed PR/CI refs, run the deterministic
verifier on the diff. Make the base ref available first because `verify` never
fetches:

```bash
agents-shipgate verify --workspace . --config shipgate.yaml \
  --base origin/main --head HEAD --ci-mode advisory --format json
```

For local uncommitted verifier work, omit `--base`/`--head` so the working tree
is scanned. Read `agents-shipgate-reports/current-control.json`
first — it names which run is current — then validate the
`verification-receipt.json` it binds, then read
`agents-shipgate-reports/agent-handoff.json` and lead
with `control.state`, then `gate.merge_verdict`
(`mergeable | human_review_required | insufficient_evidence | blocked |
unknown`), `gate.can_merge_without_human`, `next_action`,
`fix_task`, and `capability_review.top_changes[]`. Fall back to
`agents-shipgate-reports/verifier.json` only when the installed CLI contract is
older than v6. Then read
`agents-shipgate-reports/report.json.release_decision.decision`
(`blocked | review_required | insufficient_evidence | passed`), which remains
the release gate. Do not report completion unless `control.state` is
`complete`. A human-review route keeps merge and completion denied until a new
verifier artifact changes the control state; conversation-level acceptance is
not a gate
override.

Read the pointer with:

```bash
agents-shipgate agent control --workspace . --reports-dir agents-shipgate-reports
```

That returns `shipgate.agent_control/v1`, the compact control envelope: the
control state, the `permissions` vector, the next actor, the exact next action,
and the path and sha256 of every artifact `current-control.json` binds, in one
object. It is the
whole routing answer — an agent that switches on `permissions` and
`next_action` from it does not need the artifact walk above. Read
`execution` and `exit_code` as what they are: whether the tool ran, and whether
the CI gate failed. Neither is merge authority; `permissions.merge` is. Pass
`--format pointer` for the raw `current-control.json`, and use
`agents-shipgate verify --format control` to get the same envelope directly
from a run you just performed.

A zero exit means the printed answer was validated against every artifact it
binds, still describes the repository as it stands right now, and did not move
while it was read. Byte consistency is not generation consistency: one commit
is enough to make an intact artifact set describe a workspace that has moved,
so the read compares the pointer's HEAD, tree, and worktree overlay against the
live repository and refuses on drift. A non-zero exit means no control identity
is current here — you hold no authority, and a remembered result does
not substitute for one. Re-read it after any human or external-tool action,
after commit, rebase, checkout, pull, or any worktree change, after any
agents-shipgate command returns, before enforcing a cached `must_stop`, before
commit/push/PR update, before merge or release, and before declaring the task
complete. If `current_control_id` changed, discard every cached control state
and restart from the new identity. This runs in both directions: a cached stop
must not survive a newer complete run, and a cached completion must not survive
a changed workspace.

Do not bypass the verifier by suppressing findings, lowering severity,
expanding baselines or waivers, removing Shipgate CI, or weakening agent
instructions. Verify-mode `SHIP-VERIFY-*` checks make those trust-root edits
release-visible and route them to human review.
Never invent or auto-fill an action effect or action authority declaration —
including the shared `tool_sources[].authority` block, which is the same
authority claim made once for a whole source rather than once per action.
Never invent or auto-fill `agent_bindings` root, tool, or handoff declarations,
or the `tool_sources[].binding` block that makes the same closed-world claim
once for a whole published tool surface; they are reviewed claims about
deployed wiring.
Contract v14 publishes these boundaries as `action_effect`, `action_authority`,
and `agent_binding` in `do_not_auto_assert[]`; route binding and semantic next
actions to a human and rerun verification after the reviewed declaration is supplied.

There is exactly one exception, and it is narrow by construction (contract v26,
report v0.41). When `control.next_action.kind` is `confirm_declarations`, run
the command it names and nothing else. That command writes only the rows the
report itself tags `next_action.authorable_by: "coding_agent"` — rows whose
declaration the scan filled in completely from its own evidence, which today
means an effect it read directly. Everything else stays exactly as above: a row
tagged `"human"`, any authority or `agent_bindings` block, an `override`, and a
`declaration_drift` row asking a person to re-confirm an answer are never yours
to write, and you may never fill a blank the scan left or weaken a declaration
the manifest already carries. Do not reconstruct the edit by hand if the route
is absent — its absence is the answer.

`action_surface.actions[].basis` is the one field in that block that is not a
human assertion: it is a digest of the evidence the scan read for the action,
published in the row's own `declaration_template`, and it re-opens the question
as `declaration_drift` when that evidence moves. Copy it verbatim from the
report when carrying a reviewed declaration forward; it can never make an action
pass-eligible, and it does not make an effect you invented any more declared.
A `declaration_drift` row still routes to a human — it asks a person to re-read
the evidence, not a machine to restamp the digest.

To reproduce the verify-native blocked refund PR demo without writing YAML:

```bash
agents-shipgate fixture run ai_generated_refund_pr
```

To verify your install on the older static scan fixture:

```bash
agents-shipgate fixture run support_refund_agent
```

---

## First-adoption helper flow (v0.6+)

For coding agents adopting Shipgate end-to-end in one turn:

```bash
agents-shipgate detect --json
agents-shipgate init --write --ci --json
agents-shipgate scan -c shipgate.yaml --suggest-patches --format json
agents-shipgate apply-patches --from agents-shipgate-reports/report.json \
    --confidence high --apply
```

Or chain all four in one call:

```bash
agents-shipgate bootstrap --json
```

`bootstrap` runs `detect → init --write --ci → scan --suggest-patches → apply-patches --confidence high` against the current workspace, stopping on the first non-recoverable error and emitting a structured per-step summary. Use it for first-time adoption; for ongoing CI keep using the GitHub Action. Flags: `--workspace`, `--confidence`, `--no-ci`, `--no-apply`, `--json`.

- **`detect`** — read-only; classifies the workspace. `is_agent_project: false`
  is **not** on its own a reason to stop. It is false for every artifact-only
  and Codex-plugin-only workspace, which are adoptable, and it is unsafe to
  read at all when the parse was cut short. Stop only when the whole published
  stop condition holds: `is_agent_project: false` **and** `suggested_sources`
  empty **and** `codex_plugin_candidates` empty **and**
  `host_boundary_candidates` empty **and** `host_discovery_incomplete_paths`
  empty **and** `python_parse_truncated: false`. Host candidates are filenames
  only: follow `control.next_action` to `audit --host`; no manifest is needed
  and no grants have been verified. A path the census could not see through —
  a link it does not follow, a directory it could not read — can conceal nested
  host configuration, so an incomplete host census is never a terminal
  negative. It is not a failed classification either: the framework, source
  and scope answers stand beside it.
  `python_parse_truncated: true` means the
  Python parse stopped at `max_python_files`, so the negative describes the
  files that were read rather than the repository — re-run with
  `--max-python-files <workspace_signals.python_file_total>`, which is a bound
  that cannot hit the cap again. `init --write` takes the same flag and refuses
  without it while the parse is truncated, rather than declaring an agent name
  and tool surface read from part of the tree. `agent_scope` says whether one manifest can
  describe this workspace at all: `"ambiguous"` means agents live in several self-contained
  projects (`agent_project_candidates[]` lists them, and the manifest belongs
  in one of them rather than at the workspace root); `"unknown"` means
  discovery was capped before it could tell, so raise `--max-python-files` or
  name the project directly. `agent_scope_truncated: true` says the candidate
  list itself is a lower bound — the parse stopped at its cap, so any project
  in the part of the tree that was not read is missing from it. Never read
  absence from a truncated list as an answer; raise the cap first. On
  `agent_scope: "ambiguous"` with a complete parse, `next_actions[]` ranks the
  decision first (`kind: "review"`, no command) and then carries one exact
  `init --workspace <candidate> --write --json` per candidate — the list
  `init --write` publishes when it refuses the same workspace, minus the setup
  flags, which `detect` was not asked for and does not invent: if you want
  `--ci` or `--agent-instructions`, add them, or take the command from `init`'s
  own refusal, which repeats what you asked for. A candidate that already
  carries a manifest gets `doctor --config <that manifest> --json` instead —
  `init --write` there refuses a file it will not overwrite — or, when you asked
  for setup it still owes, an `init` carrying those flags. Every candidate gets
  an entry, the workspace root as a `review` rather than a command; the ten-item
  cap is on the human summary only. Choosing the project is the only work left. A truncated parse outranks that, in `detect` and in `init`
  alike: rank 1 is then the higher-cap rerun and no candidate commands are
  offered, because the list they would be built from is a lower bound.
- **`init`** — auto-detects by default. `--ci` writes
  `.github/workflows/agents-shipgate.yml`; orthogonal to `--write`. Use
  `--minimal` for the pre-v0.6 CHANGE_ME-heavy template.
  `--agent-instructions=default` renders the recommended downstream kit
  (`AGENTS.md`, `CLAUDE.md`, `.cursor/rules/agents-shipgate.mdc`,
  `.claude/commands/shipgate.md`, and `.shipgate/agent-contract.json`).
  Use `--ci` to write advisory CI. `--agent-instructions=all` means every
  supported target. A comma-separated subset can name any target:
  `agents-md,claude-md,cursor,claude-command,local-contract,codex-skill,claude-code-skill,pr-template`.
  Combined with `--write`, managed-block hosts are idempotently updated and
  full-file / skill-bundle targets use safe-update checks. The `codex-skill` and
  `claude-code-skill` targets remain explicit opt-ins and write multi-file skill
  bundles under `.agents/skills/agents-shipgate/` and
  `.claude/skills/agents-shipgate/` respectively. Strict CI and baselines remain
  opt-in human decisions; generated CI stays advisory by default.
  `--write` **refuses** a workspace whose manifest scope is unresolved —
  `manifest_status: "refused_unresolved_scope"`, exit `2`, nothing written at
  all (no manifest, no workflow, no snippets, no `.gitignore` block) — rather
  than adopting the first `Agent(name=…)` literal it parsed for a manifest that
  would cover unrelated agents. That covers both `agent_scope` values that are
  not `"single"`. Re-run with `--workspace` pointed at one of
  `auto_detected.agent_project_candidates[].path` (the emitted
  `next_actions[]` commands carry the setup flags you passed);
  `--allow-unresolved-scope` overrides when one agent surface genuinely spans
  the workspace, and `--minimal` (which adopts no detected name or tool
  surface) is never scope-gated. With `--ci`, the workflow is written at the
  repository root with a repo-relative `config:` — GitHub loads workflows from
  nowhere else — so a scoped adoption still gets a gate that runs.
- **`scan --suggest-patches`** — attaches Patch objects to every active
  finding. `Finding.patches` is absent without the flag.
- **`apply-patches`** — file-grouped, dry-run by default. Containment-
  checked against `report.manifest_dir`. v0.6 default `--confidence high`
  applies only manifest stale-removals; scope-coverage appends require
  `--confidence medium`. Trace approval/confirmation findings are
  always `ManualPatch` — never auto-applied (flipping the trace patches
  the evidence, not the agent's runtime gate).

---

## Agent mode

Every command supports JSON output for programmatic consumption:

```bash
agents-shipgate detect --workspace . --json
agents-shipgate preflight --workspace . --plan - --json
agents-shipgate init --workspace . --write --json
agents-shipgate scan -c shipgate.yaml                    # already produces report.json
agents-shipgate apply-patches --from agents-shipgate-reports/report.json --json
agents-shipgate doctor --json
agents-shipgate contract --json
agents-shipgate explain SHIP-POLICY-APPROVAL-MISSING --json
agents-shipgate list-checks --json
agents-shipgate self-check --json
agents-shipgate fixture list --json
```

Errors carry a structured `next_action` (single string, back-compat) and `next_actions` (ranked list) when agent mode is active. Agent mode auto-enables inside a known coding-agent harness (Claude Code exports `CLAUDECODE=1`, Cursor `CURSOR_TRACE_ID`); set `AGENTS_SHIPGATE_AGENT_MODE=1` to force it on elsewhere, or `=0` to force it off:

```bash
$ AGENTS_SHIPGATE_AGENT_MODE=1 agents-shipgate scan -c missing.yaml
Config error: Config file not found: missing.yaml
{"error": "config_error", "message": "...", "next_action": "agents-shipgate detect --workspace . --json", "next_actions": [{"kind": "command", "command": "agents-shipgate detect --workspace . --json", "why": "..."}, {"kind": "command", "command": "agents-shipgate init --workspace . --write", "why": "..."}]}
```

The full set of error kinds emitted in agent mode: `config_error`, `config_already_exists`, `input_parse_error`, `unknown_check_id`, `unknown_fingerprint`, `other_error`, `internal_error`, `malformed_patch`, `environment_error`. `unknown_fingerprint` is emitted by `explain-finding` when the fingerprint doesn't match any entry in the supplied report; the payload includes `suggestion` (a close-match fingerprint, when one exists) and `source_report`. `environment_error` is the one kind emitted before Agents Shipgate is running — the interpreter is unsupported, or it cannot import the package or its dependencies — so it carries the `environment` block described below instead of a `control` envelope.

The machine-readable catalog of error kinds — exit codes, typical causes, additional fields per kind, recovery hints — lives at [`docs/errors.json`](docs/errors.json). Pre-fetch it once and pattern-match the `error` field instead of re-deriving the recovery vocabulary from this prose.

`detect --json` and each `doctor --json` payload also carry `diagnostics: [...]` and `next_actions: [...]` fields. `next_action` (single string) remains the rank-1 action projected to a string; `next_actions` is the ranked list with `kind`, `command|path`, `why`, `expects`, and the structured `executable[]` / `args[]` pair. See [docs/diagnostics.md](docs/diagnostics.md) for the full catalog and schema.

### Which Shipgate answered: `environment`

Every `doctor --json` payload carries an `environment` block, and so does every `doctor` agent-mode error line — including the one where no manifest could be found and no payload is printed at all. Read it before concluding that a fix did not take or that a subcommand does not exist:

| Field | What it answers |
| ----- | --------------- |
| `interpreter` | `executable`, `version`, `minimum_supported`, `supported`. |
| `launcher` | `source` (`console_script` / `module` / `override` / `fallback`, the invocation policy above), `executable[]`, and `console_scripts[]` — each `agents-shipgate` / `shipgate` wrapper found on `PATH` with the interpreter it ultimately runs (the `exec` target when the wrapper is a `#!/bin/sh` trampoline, as `pip` writes for interpreter paths containing spaces), whether that interpreter still exists, and whether it is the running one. `null` when the wrapper names no interpreter — a compiled Windows wrapper, `#!/usr/bin/env python`, or an unrecognised handoff. |
| `import_source` | `package_path`, `root`, and `kind` (`source_checkout` / `installed` / `unknown`) — where the code that just ran came from. |
| `installed_version` / `imported_version` | What `pip` records for this interpreter, and what actually got imported. `null` installed version is normal on a source checkout. |
| `source_tree` | The enclosing Agents Shipgate checkout, if any: `root`, `version`, `launcher`, and `contains_import`. |
| `mismatches[]` | `code`, `severity` (`error` / `warning`), `detail`, and — when one exists — a runnable `command` spelled for this invocation. Empty is the normal state. |

`mismatches[]` codes: `interpreter_unsupported`, `import_outside_source_tree`, `source_tree_version_differs`, `installed_version_differs`, `console_script_interpreter_missing`, `console_script_runs_other_interpreter`. Nothing here executes an interpreter or a console script to find out — a stale wrapper is identified by reading it, because a wrapper that cannot start is exactly the one that cannot report on itself.

### One control vocabulary across the commands

`detect --json`, `init --json`, and each `doctor --json` payload carry a
`control` field holding the same `shipgate.agent_control/v1` envelope that
`verify --format control`, `check --format agent-control-json`, and
`agents-shipgate agent control` emit. Switch on `control.control_state` and
`control.permissions` for the whole adoption walk instead of learning a
different result shape per command; `control.next_action` is the one typed
rank-1 step, and `next_actions[]` beside it keeps the ranked alternatives.

When that step is a file edit, `control.next_action` is
`{"kind": "edit", "path": …, "expects": …, "command": null}` and
**`control.next_action.path` is the file to open** — exact, never normalized.
The kind is setup-only: `verify`, `check`, and `scan` never emit it, and both
schema layers reject it on those operations.

All six commands publish it; what differs is where, because a command that
publishes a control *pointer* names the envelope from that pointer rather than
carrying it on its own result:

| Command | Where the envelope is |
| ------- | --------------------- |
| `detect` | `--json` payload, `control` |
| `init` | `--json` payload, `control` |
| `doctor` | each `--json` payload, `control` |
| `check` | `--format agent-control-json` (the document *is* the envelope) |
| `verify` | `--format control` (same), or `agents-shipgate agent control` after a `--json` run |
| `scan` | `agents-shipgate agent control` after the run |

`scan`'s answer is the one that withholds a verdict: it reports
`decision: null` with a `reason` saying so. A scan pointer binds no
reconfirmable snapshot of the inputs it read, so no artifact in that directory
can show its verdict still describes the workspace. Run `verify` for one that
can.

Read `control.decision_source` before `control.decision`. Setup commands run
before a release decision exists, so they report `setup` and a verdict from
`setup_complete | setup_incomplete | setup_not_applicable`, never a release
verdict; `release_decision` means `report.json`'s
`release_decision.decision` and nothing else. The two cannot be confused: the
published schema requires a setup source to come from `detect`/`init`/`doctor`
and requires those operations to report no other source.

**Setup authorizes nothing.** Every field of `permissions` is `false` on every
setup envelope, no setup envelope binds an artifact or a `current_control_id`,
and `control_state: "complete"` is unreachable for these operations in the
schema. Running `init` successfully is not permission to commit, merge, or
report the task done — only a verifier run can grant that. When a manifest still
holds an unresolved `declared_purpose`, policy, or permission placeholder,
`control_state` is `human_review_required` and the action names the exact file,
line, and field: those are declarations a person makes, and an agent must never
supply them.

`next_action` may be `kind: "edit"` on these commands — a typed coding-agent
step with `path` and `expects` and no command. It appears only on setup output:
`verify`, `check`, and `agent control` cannot return it, and the published schema
rejects it on any other operation. `permissions.edit` is `false` beside it, which
is not a contradiction — a setup route authorizes only its own `next_action`.

`next_action` and `next_actions[0]` are **derived from the same selected route**
as `control.next_action`, so the compact envelope and the ranked list can never
send you to different work. Where the route is human-owned, that list holds
exactly one action and no command: an alternative would be a way around the
obligation.

**Agent-mode error lines from `detect`, `init`, and `doctor` carry
`control` too**
(contract v27). A setup command that could not finish publishes the same
envelope on stderr that it would have published on stdout, so one routing rule
covers both documented streams; `next_action` / `next_actions[]` are unchanged
beside it. Every such envelope reports `decision_source: "setup"`, a `decision`
from the setup vocabulary, `permissions` all false, and never
`control_state: "complete"`.

**Do not use `execution` to tell an error line from an answer** — the `error`
field does that. `execution` says whether the command reached an answer about
the workspace, so an error line carries `"failed"` when it could not (a flag
value it could not parse, discovery it could not bound, a manifest it could not
open) and `"succeeded"` with a non-zero `exit_code` when it did and the answer
is a refusal it can route past (`config_already_exists`, the unresolved-scope
`config_error`). Both authorize nothing.

The one setup line with no `control` is the shared `--workspace` refusal, which
fires before the workspace exists and therefore has no setup subject to
describe: it carries `next_action`/`next_actions` only. Error lines from
`scan`, `verify`, and `check` also carry no `control`: the first two answer
through their control pointer (`agents-shipgate agent control`), and `check`
through `--format agent-control-json`.

Every emitted command names the entry point that started the running process, so it is runnable where it was produced: a console-script run emits `agents-shipgate …`, and a `python -m agents_shipgate` run emits `<sys.executable> -m agents_shipgate …`. Set `AGENTS_SHIPGATE_CLI` to name the entry point explicitly; it wins over detection. **On `next_actions[]`, run `[*executable, *args]` (contract v23+) rather than parsing `command`** — it needs no shell and is computed from `command`, so it cannot disagree with it; it is omitted, never `null`, when the command has no faithful argv form. The operational control contracts (`control.next_action`, `allowed_next_commands`, verifier repairs) carry the string only: recover argv there with `shlex.split(command)`, which is exact on every platform because every emitted command is POSIX-rendered. Never use `shell=True`, and do not paste `command` into `cmd.exe` or PowerShell. Durable artifacts (`report.json`, `packet.*`) stay canonical so that same inputs still produce the same report. See [docs/diagnostics.md](docs/diagnostics.md#invocation-policy).

### Doctor behavior change for unresolved tool_sources

When a required `tool_sources[].path` does not resolve under the manifest directory (file missing OR resolves outside the manifest dir):

- `agents-shipgate doctor --json` exits **0** with a `SHIP-DIAG-MISSING-SOURCE-FILE` diagnostic and an `unresolved_sources: [{id, declared_path, line, reason}]` field in the payload, so an agent can route to a fix without parsing the error message. `reason` is `"missing"` or `"outside_manifest_dir"`.
- `agents-shipgate doctor` (no `--json`) prints the same `unresolved_sources` + diagnostic block in human-readable form and **exits 3**, preserving the pre-feature loud failure for interactive users.
- `agents-shipgate scan` is unchanged — it still raises `InputParseError(3)` regardless of `--json`. Once you're past doctor, missing sources are real scan failures.

### Missing vs invalid manifests

`config_error` covers two distinct shapes — agent-mode emits a different rank-1 action for each:

- **Missing**: file does not exist → `SHIP-DIAG-MISSING-MANIFEST`, rank-1 is `agents-shipgate detect --workspace <dir> --json` (then `init --write`).
- **Invalid**: file exists but the loader rejected it (invalid YAML, schema validation, unsupported version) → `SHIP-DIAG-INVALID-MANIFEST`, rank-1 is `edit <path>` with the loader error in `why`. Do **not** re-run `init` — it refuses to overwrite an existing file.

---

## When to use this tool

Use it when the user is:

- Writing or modifying an AI agent that calls tools (MCP servers, OpenAPI APIs, `@function_tool`-decorated Python, Google ADK, LangChain/LangGraph, CrewAI, n8n, Conductor OSS)
- Publishing or modifying a Codex plugin package or marketplace entry that declares skills, apps, MCP servers, or hooks
- Adding a new tool, scope, or policy to an existing agent
- Preparing an agent for production promotion
- Wiring a release gate into PR CI

Do NOT use it for:

- General code linting (ruff, eslint, etc.)
- Runtime monitoring (use observability/tracing)
- Behavior testing (use evals)
- Stopping a tool call in flight (use a gateway or guardrail)

### Should I run Shipgate on this PR?

| Trigger in this PR | Run Shipgate? |
|---|---|
| Adds/changes MCP exports, OpenAPI specs, or `tools/*openai*tools*.json` | Yes |
| Adds/changes an MCP tool registration written in TypeScript, Go or Python source (`static toolName`, `.registerTool(`, `MustTool(`, `NewTool(`, `mcp.Tool{`, `@mcp.tool`) | Yes |
| Adds/changes Codex repo config, hooks, or permission profiles | Yes |
| Adds/changes coding-agent host config, hooks, permissions, MCP servers, or workflows | Yes |
| Adds/changes Codex plugin manifests, marketplace files, `.app.json`, `.mcp.json`, or `SKILL.md` files | Yes |
| Adds/changes `@function_tool`/`@tool` decorators (LangChain, CrewAI, OpenAI Agents SDK) | Yes |
| Adds/changes a Google ADK `Agent`/`LlmAgent` `tools=[...]` list | Yes |
| Adds/changes n8n workflow JSON, credential stubs, or n8n tool inventories | Yes |
| Adds/changes Conductor OSS workflow JSON with AI/MCP tasks | Yes |
| Edits `prompts/`, `policies/`, or `permissions.scopes` in `shipgate.yaml` | Yes |
| Adds/edits `.github/workflows/agents-shipgate.yml` or related CI | Yes |
| Pure read-only doc/test changes with no manifest impact | Skip |
| Refactor with no behavior change to tools or policies | Skip (or dry-run only) |

One known gap in the Google ADK row: an edit that *modifies* a tools list on the `Agent` alias (rather than `LlmAgent`) is not matched, because a bare `Agent(..., tools=[...])` hunk with no ADK import in it cannot be distinguished from CrewAI's by diff text alone. `LlmAgent` changes and whole-file additions in either spelling are covered.

`prompts/` and `policies/` in that row match at any depth and case-insensitively: an edit under `services/foo/policies/` or `enterprise/lib/captain/Prompts/` routes exactly like a repo-root one. That is parity with the verifier, whose trust-root classification has always read those two surfaces as `**/policies/**` and `**/prompts/**` and has always tolerated the case variant a case-insensitive filesystem resolves to the canonical name. The catalog's `glob` and `none_match_glob` predicates match the same way, so a path cannot be a trust root to the verifier and a `no_match` to the router; the Tier B checks (`SHIP-VERIFY-POLICY-WEAKENED`, `SHIP-VERIFY-CI-GATE-REMOVED`, the retained non-emitting agent-instruction weakening ID, trigger-catalog drift) select their changed files the same way too, so a case variant cannot be a trust root in Tier A and invisible to the specialized check that carries the severity. `every_file_matches` is deliberately the exception and stays case-sensitive: it is the docs-only rule's own classifier, and `skip_shipgate` beats `run_shipgate`, so folding it would read `src/TEST_agent.py` — a production module on a case-sensitive filesystem — as a test file and skip a PR that adds a tool beside it. The rule is to fold the predicates that can only add evaluation, never the one that can subtract it. The three surfaces that copy this routing — the pre-commit `files:` regex, the `.cursor/rules/agents-shipgate.mdc` activation globs, and the documented copy-paste hook snippets — follow, so a nested governance edit also activates the host instructions and stages the local hook.

`shipgate.yaml` matches at any depth for the same reason. A monorepo keeps one manifest per project directory, so an edit to `services/refund/shipgate.yaml` — the file that declares that project's agent, purpose, and tool surface — routes exactly like a root-level one; a root-only rule reported it as `no_match`. A nested manifest is also an opt-in: `verify --preview` treats the changed project's own `shipgate.yaml` as the repo-already-adopted signal and routes verification to that manifest rather than to a root one governing a different boundary.

Two implicit triggers also fire even when no row above matches:

- **Repo already opted in (shipgate.yaml present in the workspace)** — run on every PR; the manifest's existence is the opt-in.
- **(Optional) Refactor or framework upgrade that may shift the extracted tool surface** — dry-run only; bumping `openai-agents`, `langchain`, `crewai`, `google-adk`, or `conductor-oss` can change static extraction even without app-code edits. The rule needs both halves of that evidence: the package token **and** a changed dependency manifest. A bare token — a README that mentions `google-adk`, a sample that imports it — is not a version bump and no longer routes as one. The manifest set is `DEPENDENCY_MANIFEST_GLOBS` (`agents_shipgate.core.dependency_manifests`), projected into `triggers.json` and pinned by the contract test; it covers Python (`pyproject.toml`, `requirements*.{txt,in}`, `constraints*.{txt,in}`, `poetry.lock`, `uv.lock`, `pdm.lock`, `pylock*.toml`, `Pipfile*`, conda), Node (`package.json`, npm/pnpm/yarn/bun locks), and the JVM (`pom.xml`, Gradle build files and version catalogs).

A machine-readable mirror of these triggers lives at [`docs/triggers.json`](docs/triggers.json). Coding agents that have not yet adopted Shipgate can fetch the file (raw URL: `https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/triggers.json`), apply the rules to a PR diff, and decide whether to propose `agents-shipgate detect`. The catalog is stable for `0.x` and pinned by the public-surface contract test against this prose table — if you change a row above, update `triggers.json` in the same commit. To evaluate a diff locally, use the first-class `trigger` subcommand:

```bash
# From a list of changed paths (and optional diff body for diff_contains rules):
agents-shipgate trigger --changed-files changed.txt --diff pr.diff --json
# Or straight from git (the ONLY mode that shells out to git):
agents-shipgate trigger --base origin/main --head HEAD --json
agents-shipgate trigger --list-rules --json
```

The command emits a stable JSON verdict: `should_run` (alias of `run_shipgate`), `force_run`, `dry_run_recommended`, `skip_reason`, `matched_rules`, `changed_files`, `diff_tokens`, plus `input_status`, `evaluation_status`, and `surface_exclusions` (catalog schema `0.4`). A *skip* verdict is withheld in two states, and `should_run`/`run_shipgate`/`skip`/`skip_reason` are `null` in both — never read `null` as `false`. `evaluation_status: not_evaluated` means the diff could not be read in full, so an unread diff is never reported as `no_match`; `next_action.kind` is `input_required`. `evaluation_status: unclassified` means the diff *was* read in full and no rule classified some or all of the changed files — a fact about the catalog, not about the PR — so the skip is withheld and `next_action` routes forward to the scan; `surface_exclusions.entries[]` names the files nobody classified. A matched `run_shipgate`/`force_run` rule also overrides the stop block, because a capability match in the diff is evidence the whole-workspace negative did not account for; read `stop_conditions_terminal` to see whether a fired stop actually decided. A *run* verdict is still published, because rule matching is monotone — evidence that already matched cannot be un-matched by the bytes that are missing — and it arrives as `evaluation_status: evaluated` with `should_run: true`. That evidence may be a rule matched on the change set or `force_run` from a manifest that is present regardless of the diff, so read `matched_rules` before attributing it. Branch on `evaluation_status`, not on `should_run` alone. The developer entry point `python -m agents_shipgate.triggers shipgate.yaml prompts/refund.md` is preserved.

**Stop conditions.** Stop and do not run `init` only when **all** of these hold:

- `agents-shipgate detect --json` returns `is_agent_project: false`, AND
- `suggested_sources` is empty (no MCP/OpenAPI hits flowing in as `mcp` or `openapi`), AND
- `codex_plugin_candidates` is empty (no Codex plugin package or marketplace hits), AND
- `host_boundary_candidates` is empty (no recognized host config paths), AND
- `host_discovery_incomplete_paths` is empty (no unresolved traversal that can hide host config), AND
- `python_parse_truncated` is `false` — every negative above is a claim about the whole workspace, and a run whose Python parse stopped at its cap read only part of one, AND (this is the raw parse bit, not `agent_scope_truncated`: that one additionally requires more than one candidate scope, so a single-scope workspace whose only agent sits past the cap leaves it false)
- no `shipgate.yaml` already exists in the workspace, AND
- the user did not explicitly request a scan.

A `detect` payload that does not carry every one of those keys leaves the block unevaluable: `trigger` reports `stop_conditions_evaluated: false` and infers no stop. Re-run `detect` with the current CLI rather than reading an absent key as `false`.

Otherwise follow `control.next_action`. Host-only repositories route to
`audit --host` without a manifest; `host_boundary_candidates[].file_type`
describes only the observed pathname, never parsed permission evidence.
Configuration paths that are directories, and unrecognized links that could
conceal nested configuration, route to inspection. `init` (including `--ci`
and agent-instruction options) and `bootstrap` hand off without writing setup
files on this host-only route. Explicit `init --minimal` retains its template
behavior. MCP/OpenAPI tool-surface and Codex plugin repositories still use
`init`; their candidates never become host grants. The trigger table above is
the authoritative go/no-go.

---

## Five common agent tasks

### Task 1 · Add the gate to an existing repo

```bash
pipx install agents-shipgate
agents-shipgate init --workspace . --write --json
# resolve the placeholders init reports, by owner (below), then:
agents-shipgate scan -c shipgate.yaml
```

`init --json` reports the placeholders two ways, and only one of them routes. **`placeholders[]` is a location list** — each entry is `path`, `current` and `line`, and carries no owner. **`control.next_action.actor` routes the turn**, not individual fields:

- `actor: "human"` — *any* human-owned value is still unresolved. `permissions.edit` is `false`: surface the whole thing and stop. Do not edit the manifest, and do not split the array.
- `actor: "coding_agent"` — every human-owned value has been supplied and only fields you own are left. `why` names the one to replace.

**Never infer ownership from absence in `why`.** That sentence is fitted to the envelope's prose budget: with seven unresolved declarations it names three paths and then `and 4 more in placeholders[]`, and the four it dropped are human-owned too. It tells you where to start, not what is yours. The rule behind the split:

- **You own** what you can read out of the repository — `agent.name`, `project.name`, the `tool_sources[]` rows. Escalating these stops a turn for work you own.
- **A person owns** every *declaration*: purpose, prohibited actions, effect, authority, binding, approval, confirmation, idempotency, safeguards, accepted debt and its owner/reason/expiry, and the manifest blocks that are declarations end to end (`action_surface`, `permissions`, `policies`, `agent_bindings`, `tool_identity`, `checks`, `baseline`, `human_ack`, `risk_overrides`, `validation`, `organization`). While one is unresolved, `init` returns `control.next_action.actor: "human"` and `permissions.edit: false`. These values must be supplied by a human, because Shipgate never invents a declaration nobody made — a purpose or authority claim you lifted out of a prompt or README is a declaration nobody made, and the engine will treat it as evidence.

Those names are examples of the rule, not the rule. `placeholders[]` is authoritative; when it disagrees with this list, it is right.

### Task 2 · Read findings programmatically

Always parse `agents-shipgate-reports/report.json`, not the markdown.

The canonical field list — `release_decision`, `capability_facts` / `declared_intentions` / `misalignments` / `release_consequence` / `suggested_scenarios`, `tool_surface_facts` / `tool_surface_diff`, and `action_surface_facts` / `action_surface_diff` — lives in [`docs/agent-contract-current.md`](docs/agent-contract-current.md#read-these-first-for-release-gating). It updates first when the contract bumps; this file links to it instead of restating the field set.

Other stable top-level fields (full history and semantics live in
[`docs/agent-contract-current.md`](docs/agent-contract-current.md); never
restate version archaeology here):

- `summary.{critical_count, high_count, medium_count, status}` (legacy,
  baseline-blind — do not gate on it)
- `findings[].{id, fingerprint, check_id, severity, tool_name, evidence, recommendation, suppressed}`
- `findings[].{autofix_safe, requires_human_review, suggested_patch_kind, docs_url, provenance_kind, blocks_release}`
- `findings[].policy_routing` (policy-pack owner/reviewer/approval routing metadata only; non-enforcing and not part of `evidence`)
- `findings[].patches[]` (only when scan ran with `--suggest-patches`)
- `baseline.{matched_count, new_count, resolved_count}` · root-reachable `tool_inventory[]` · full `tool_catalog[]` · `codex_plugin_surface`
- `action_surface_facts` / `action_surface_diff`
- `release_decision.evidence_coverage.{binding_coverage,semantic_coverage,evidence_gaps}`
- Audit envelopes: `release_decision.contribution_rules[]`, `policy_audit`,
  `privacy_audit`, `heuristics_filter` — explanatory, never a second gate

The current schema is [`docs/report-schema.v0.43.json`](docs/report-schema.v0.43.json). Emitted reports carry `report_schema_version: "0.43"`; `surface_exclusions` records every subject a stage removed from the analysed surface and whether the release decision saw it, typed predicate support prevents heuristic evidence from being upgraded by policy severity or block metadata, and verify-native reports bind the content-addressed request and decision. A `passed` result requires a complete static binding graph from its entry points plus complete, conflict-free identity, effect, authority, and applicable-policy evidence for every reachable action. Every release decision explicitly carries `static_analysis_only: true`, `runtime_behavior_verified: false`, and `static_verdict_disclaimer`; packet §1 mirrors them. Binding, semantic, and policy-applicability gaps are not Findings and cannot be suppressed or baselined. See [`docs/passed-verdict-contract.md`](docs/passed-verdict-contract.md), [`docs/verification-reproducibility.md`](docs/verification-reproducibility.md), and [`docs/agent-contract-current.md`](docs/agent-contract-current.md). v0.42 remains frozen at [`docs/report-schema.v0.42.json`](docs/report-schema.v0.42.json).

**Release gating signal**: prefer `release_decision.decision` (`"blocked" | "review_required" | "insufficient_evidence" | "passed"`) over `summary.status`. The new field is **baseline-aware** — a baseline-matched critical surfaces in `release_decision.review_items` (accepted debt), not `release_decision.blockers`. `summary.status` stays baseline-blind for v0.7 compatibility, so a baseline-matched-only critical produces both `summary.status = "release_blockers_detected"` AND `release_decision.decision = "review_required"` (intentional divergence — see [STABILITY.md](STABILITY.md#release_decisiondecision-vs-summarystatus)). `insufficient_evidence` (added v0.14) signals that the scan saw too many low-confidence tools or source-loader warnings to be trustworthy; consumers that switch on the enum must fall back to `review_required` for unknown future values.

For a step-by-step reader's primer with anti-patterns and concrete code rewrites, see [`docs/report-reading-for-agents.md`](docs/report-reading-for-agents.md).

### Task 3 · Suppress a finding with a reason

```yaml
# shipgate.yaml
checks:
  ignore:
    - check_id: SHIP-DOC-MISSING-DESCRIPTION
      tool: legacy_search
      reason: tool deprecated 2026-Q2
```

`reason` is required and non-empty; the manifest fails validation otherwise.
Suppressions apply to Findings only. They cannot accept, hide, or close a
semantic evidence gap.

### Task 4 · Save a baseline before enabling strict CI

```bash
agents-shipgate baseline save -c shipgate.yaml --out .agents-shipgate/baseline.json \
  --owner <human> --reason "<why accepted>" --expires <YYYY-MM-DD>
```

`--owner`/`--reason`/`--expires` (v0.13+) record who accepted the debt, why,
and the review-by date on newly-accepted entries. They are human-declared
values: an agent must ask the user, never invent them, and blank values are
rejected. `--apply-to-existing` fills the fields into existing entries that
lack them without overwriting previously-set values.

Then in CI:

```bash
agents-shipgate scan -c shipgate.yaml \
  --baseline .agents-shipgate/baseline.json \
  --ci-mode strict --fail-on critical,high
```

Strict mode fails CI only on **new** findings (those not in the baseline).
`agents-shipgate baseline status --json` reports accepted-debt aging
(owner, age, expiry); with `--require-owner` / `--require-expiry` /
`--max-age-days N` it exits `20` on violations (advisory exit `0` without
gate flags) — parse `violations[]` from the JSON, then route to a human:
acknowledging debt is a human decision.

### Task 5 · Explain a check or a specific finding

For static catalog metadata about a check ID (rationale, fires-when, recommendation):

```bash
agents-shipgate explain SHIP-POLICY-APPROVAL-MISSING --json
```

Returns the full `CheckMetadata` with `id`, `category`, `default_severity`, `description`, `rationale`, `fires_when`, `evidence_fields`, `recommendation`.

For a contextual explanation tied to a specific finding from a real scan (catalog metadata + the finding's evidence + a 3–5 sentence templated prose summary):

```bash
agents-shipgate explain-finding fp_<fingerprint> \
    --from agents-shipgate-reports/report.json --json
```

Returns the canonical Finding fields plus `metadata` (CheckMetadata for the check_id) and `explanation` — a deterministic prose summary suitable for direct quotation in a PR comment or chat reply. The companion prompt is [`prompts/explain-finding-to-user.md`](prompts/explain-finding-to-user.md).

---

## Agent FAQ

### Where is the manifest schema?

Use [`docs/manifest-v0.1.json`](docs/manifest-v0.1.json) for machine
validation and [`docs/manifest-v0.1.md`](docs/manifest-v0.1.md) for prose.

### Where is the report schema?

Parse `agents-shipgate-reports/report.json` and validate against
[`docs/report-schema.v0.43.json`](docs/report-schema.v0.43.json) (current).
Older reports (`report_schema_version: "0.10"`) validate against the
frozen [`docs/report-schema.v0.10.json`](docs/report-schema.v0.10.json).
Do not scrape Markdown when JSON is available.

### How do I add a new check?

Follow [`docs/architecture.md`](docs/architecture.md) and update the check
registry, tests, [`docs/checks.md`](docs/checks.md), and
[`docs/checks.json`](docs/checks.json). Check IDs must not change after
publication.

### How do I add a new framework adapter?

Start with [`docs/framework-adapter-checklist.md`](docs/framework-adapter-checklist.md).
Adapters must be static by default: no user-code import, no network access, no
agent execution.

### Where are runnable examples?

Use [`samples/README.md`](samples/README.md) for sample agents and
[`docs/examples.md`](docs/examples.md) for a narrative overview. The fastest
fixture is `agents-shipgate fixture run support_refund_agent`.

### What vocabulary should I use in user-facing copy?

Use the [canonical names](#canonical-names) table above and the website
glossary: https://threemoonslab.com/glossary/.

---

## Schemas

For the short, current statement of "which fields to read", see [`docs/agent-contract-current.md`](docs/agent-contract-current.md). It is the single file that updates first when the contract bumps; the table below lists the underlying schemas.

| What | Path | Stable |
|---|---|---|
| Manifest schema | [`docs/manifest-v0.1.json`](docs/manifest-v0.1.json) | `0.1` |
| Report schema (current) | [`docs/report-schema.v0.43.json`](docs/report-schema.v0.43.json) | `0.43` |
| Report schema (v0.38 frozen reference) | [`docs/report-schema.v0.38.json`](docs/report-schema.v0.38.json) | `0.38` |
| Report schema (v0.37 frozen reference) | [`docs/report-schema.v0.37.json`](docs/report-schema.v0.37.json) | `0.37` |
| Report schema (v0.34 frozen reference) | [`docs/report-schema.v0.34.json`](docs/report-schema.v0.34.json) | `0.34` |
| Report schema (v0.33 frozen reference) | [`docs/report-schema.v0.33.json`](docs/report-schema.v0.33.json) | `0.33` |
| Report schema (v0.32 frozen reference) | [`docs/report-schema.v0.32.json`](docs/report-schema.v0.32.json) | `0.32` |
| Report schema (v0.31 frozen reference) | [`docs/report-schema.v0.31.json`](docs/report-schema.v0.31.json) | `0.31` |
| Report schema (v0.30 frozen reference) | [`docs/report-schema.v0.30.json`](docs/report-schema.v0.30.json) | `0.30` |
| Report schema (v0.29 frozen reference) | [`docs/report-schema.v0.29.json`](docs/report-schema.v0.29.json) | `0.29` |
| Report schema (v0.28 frozen reference) | [`docs/report-schema.v0.28.json`](docs/report-schema.v0.28.json) | `0.28` |
| Report schema (v0.27 frozen reference) | [`docs/report-schema.v0.27.json`](docs/report-schema.v0.27.json) | `0.27` |
| Report schema (v0.26 frozen reference) | [`docs/report-schema.v0.26.json`](docs/report-schema.v0.26.json) | `0.26` |
| Report schema (v0.25 frozen reference) | [`docs/report-schema.v0.25.json`](docs/report-schema.v0.25.json) | `0.25` |
| Verify-run schema | [`docs/verify-run-schema.v3.json`](docs/verify-run-schema.v3.json) | `shipgate.verify_run/v3` |
| Verification plan schema | [`docs/verification-plan-schema.v1.json`](docs/verification-plan-schema.v1.json) | `shipgate.verification_plan/v1` |
| Verification unit result schema | [`docs/verification-unit-result-schema.v1.json`](docs/verification-unit-result-schema.v1.json) | `shipgate.verification_unit_result/v1` |
| Verification artifact manifest schema | [`docs/verification-artifact-manifest-schema.v1.json`](docs/verification-artifact-manifest-schema.v1.json) | `shipgate.verification_artifact_manifest/v1` |
| Verification receipt schema | [`docs/verification-receipt-schema.v1.json`](docs/verification-receipt-schema.v1.json) | `shipgate.verification_receipt/v1` |
| Agent handoff schema | [`docs/agent-handoff-schema.v5.json`](docs/agent-handoff-schema.v5.json) | `shipgate.agent_handoff/v5` |
| Agent boundary result schema | [`docs/agent-boundary-result-schema.v1.json`](docs/agent-boundary-result-schema.v1.json) | `shipgate.agent_boundary_result/v1` |
| Codex boundary result schema (deprecated frozen projection) | [`docs/codex-boundary-result-schema.v2.json`](docs/codex-boundary-result-schema.v2.json) | `shipgate.codex_boundary_result/v2` |
| Report schema (v0.24 frozen reference) | [`docs/report-schema.v0.24.json`](docs/report-schema.v0.24.json) | `0.24` |
| Report schema (v0.23 frozen reference) | [`docs/report-schema.v0.23.json`](docs/report-schema.v0.23.json) | `0.23` |
| Report schema (v0.22 frozen reference) | [`docs/report-schema.v0.22.json`](docs/report-schema.v0.22.json) | `0.22` |
| Report schema (v0.21 frozen reference) | [`docs/report-schema.v0.21.json`](docs/report-schema.v0.21.json) | `0.21` |
| Report schema (v0.20 frozen reference) | [`docs/report-schema.v0.20.json`](docs/report-schema.v0.20.json) | `0.20` |
| Report schema (v0.19 frozen reference) | [`docs/report-schema.v0.19.json`](docs/report-schema.v0.19.json) | `0.19` |
| Report schema (v0.18 frozen reference) | [`docs/report-schema.v0.18.json`](docs/report-schema.v0.18.json) | `0.18` |
| Report schema (v0.17 frozen reference) | [`docs/report-schema.v0.17.json`](docs/report-schema.v0.17.json) | `0.17` |
| Report schema (v0.16 frozen reference) | [`docs/report-schema.v0.16.json`](docs/report-schema.v0.16.json) | `0.16` |
| Report schema (v0.15 frozen reference) | [`docs/report-schema.v0.15.json`](docs/report-schema.v0.15.json) | `0.15` |
| Report schema (v0.14 frozen reference) | [`docs/report-schema.v0.14.json`](docs/report-schema.v0.14.json) | `0.14` |
| Report schema (v0.13 frozen reference) | [`docs/report-schema.v0.13.json`](docs/report-schema.v0.13.json) | `0.13` |
| Report schema (v0.12 frozen reference) | [`docs/report-schema.v0.12.json`](docs/report-schema.v0.12.json) | `0.12` |
| Report schema (v0.11 frozen reference) | [`docs/report-schema.v0.11.json`](docs/report-schema.v0.11.json) | `0.11` |
| Report schema (v0.10 frozen reference) | [`docs/report-schema.v0.10.json`](docs/report-schema.v0.10.json) | `0.10` |
| Report schema (v0.9 frozen reference) | [`docs/report-schema.v0.9.json`](docs/report-schema.v0.9.json) | `0.9` |
| Report schema (v0.8 frozen reference) | [`docs/report-schema.v0.8.json`](docs/report-schema.v0.8.json) | `0.8` |
| Report schema (v0.7 frozen reference) | [`docs/report-schema.v0.7.json`](docs/report-schema.v0.7.json) | `0.7` |
| Report schema (v0.6 frozen reference) | [`docs/report-schema.v0.6.json`](docs/report-schema.v0.6.json) | `0.6` |
| Packet schema (Release Evidence Packet, latest) | [`docs/packet-schema.v0.18.json`](docs/packet-schema.v0.18.json) | `0.18` |
| Agent result schema (current) | [`docs/agent-result-schema.v3.json`](docs/agent-result-schema.v3.json) | `agent_result_v3` |
| Verifier schema (current) | [`docs/verifier-schema.v0.17.json`](docs/verifier-schema.v0.17.json) | `0.17` |
| Agent handoff schema (current) | [`docs/agent-handoff-schema.v9.json`](docs/agent-handoff-schema.v9.json) | `shipgate.agent_handoff/v9` |
| Preflight schema (current) | [`docs/preflight-schema.v0.5.json`](docs/preflight-schema.v0.5.json) | `0.5` |
| Host-grants inventory schema | [`docs/host-grants-inventory-schema.v0.3.json`](docs/host-grants-inventory-schema.v0.3.json) | `0.3` |
| Host-grants baseline schema | [`docs/host-grants-baseline-schema.v0.3.json`](docs/host-grants-baseline-schema.v0.3.json) | `0.3` |
| Host-grants drift schema | [`docs/host-grants-drift-schema.v0.3.json`](docs/host-grants-drift-schema.v0.3.json) | `0.3` |
| Capability standard | [`docs/capability-standard.md`](docs/capability-standard.md) | `0.5` |
| Capability lock schema | [`docs/capability-lock-schema.v0.8.json`](docs/capability-lock-schema.v0.8.json) | `0.8` |
| Capability lock diff schema | [`docs/capability-lock-diff-schema.v0.9.json`](docs/capability-lock-diff-schema.v0.9.json) | `0.9` |
| Capability payload schema (frozen) | [`docs/capability-payload-schema.v1.json`](docs/capability-payload-schema.v1.json) | `shipgate.capability_payload/v1` |
| Capability delta attestation (frozen) | [`docs/capability-delta-attestation-schema.v1.json`](docs/capability-delta-attestation-schema.v1.json) | `shipgate.capability_delta_attestation/v1` |
| Governance benchmark catalog schema | [`docs/governance-benchmark-catalog-schema.v0.2.json`](docs/governance-benchmark-catalog-schema.v0.2.json) | `0.2` |
| Governance benchmark result schema | [`docs/governance-benchmark-result-schema.v0.2.json`](docs/governance-benchmark-result-schema.v0.2.json) | `0.2` |
| Check catalog | [`docs/checks.json`](docs/checks.json) | regenerated each release |
| Anti-patterns (what NOT to write) | [`samples/_anti_patterns/`](samples/_anti_patterns/) | reference |
| Minimal manifest example | [`docs/manifest-v0.1.example.minimal.yaml`](docs/manifest-v0.1.example.minimal.yaml) | reference |

For VS Code / Cursor live YAML validation, every manifest produced by `init` includes:

```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/manifest-v0.1.json
```

---

## Stable command surface

Promised to not break in `0.x` minor versions. See [STABILITY.md](STABILITY.md) for the full contract.

| Command | Stable flags |
|---|---|
| `agents-shipgate scan` | `-c`, `--out`, `--format`, `--ci-mode`, `--fail-on`, `--baseline`, `--diff-from`, `--changed-files`, `--no-plugins`, `--no-heuristics`, `--verbose`, `--packet`/`--no-packet`, `--packet-format` |
| `agents-shipgate evidence-packet` | `--from`, `--out`, `--format`, `--json` |
| `agents-shipgate init` | `--workspace`, `--write`, `--json` |
| `agents-shipgate doctor` | `-c`, `--workspace`, `--json`, `--verbose` |
| `agents-shipgate contract` | `--json` |
| `agents-shipgate explain` | `<check_id>`, `--no-plugins`, `--json` |
| `agents-shipgate explain-finding` | `<fingerprint>`, `--from`, `--no-plugins`, `--json` |
| `agents-shipgate findings` | `--from`, `--provenance-kind`, `--include-suppressed`, `--json` |
| `agents-shipgate trigger` | `--workspace`, `--changed-files`, `--diff`, `--base`, `--head`, `--manifest-present`/`--no-manifest-present`, `--user-requested`, `--list-rules`, `--json` |
| `agents-shipgate bootstrap` | `--workspace`, `--confidence`, `--no-ci`, `--no-apply`, `--json` |
| `agents-shipgate list-checks` | `--json`, `--no-plugins` |
| `agents-shipgate baseline save` | `-c`, `--out`, `--owner`, `--reason`, `--expires`, `--apply-to-existing` |
| `agents-shipgate baseline status` | `--baseline`, `--as-of`, `--require-owner`, `--require-expiry`, `--max-age-days`, `--json` (gate flags exit `20` on violations) |
| `agents-shipgate fixture` | `list`, `run`, `copy`, `verify` |
| `agents-shipgate self-check` | `--json` |
| `agents-shipgate agent handoff` | `--from`, `--report`, `--verify-run`, `--out`, `--json` |

Newer commands (stable intent, flags may still evolve):

| Command | Purpose |
|---|---|
| `shipgate audit --host` | Zero-config, read-only static inventory of coding-agent host grants with per-host coverage; deterministic repository scope by default, optional `--scope local-static`. Works without `shipgate.yaml`. |
| `agents-shipgate mcp-serve` | Local read-only stdio MCP server (`[mcp]` extra) exposing `shipgate.check`, `shipgate.preflight`, `shipgate.explain`, `shipgate.capabilities`, and `shipgate.handoff`. See [`docs/mcp-server.md`](docs/mcp-server.md). |
| `agents-shipgate org status` | Local organization governance projection over exception hygiene, policy-pack pins, host-grant drift, and registry readiness; `--json` available and governance violations exit `20`. |
| `agents-shipgate registry` | `ingest --attestation <file>` / `query` / `report --bypass` — local capability-release ledger over attestations. |
| `agents-shipgate install-hooks` | Claude Code hooks: PreToolUse trust-root boundary (`ask`/`deny`), PostToolUse trigger nudge, Stop verify. |

### Release Evidence Packet (v0.16)

`scan` emits a reviewer-shaped Release Evidence Packet alongside
`report.{md,json}` by default; outputs land at
`agents-shipgate-reports/packet.{md,json,html}` (and `packet.pdf` with the
`[pdf]` extras). The packet is derived from the report JSON, is a local
artifact only, and never gates — §1's verdict derives from
`release_decision.decision` alone, and §10 always lists what the packet did
NOT prove. Use `--no-packet` / `--packet-format` on `scan`, and
`agents-shipgate evidence-packet --from <packet.json|report.json>` to
re-render. The full packet contract (fixed sections, disclaimers,
`evidence_matrix` rules) lives in
[STABILITY.md §Release Evidence Packet](STABILITY.md#release-evidence-packet-v018)
and [`docs/agent-contract-current.md`](docs/agent-contract-current.md#read-these-for-release-review).

Exit codes (stable):

| Code | Meaning |
|---|---|
| `0` | Pass (advisory or strict-no-blockers) |
| `2` | Manifest config error |
| `3` | Input parse error (file missing, malformed, path traversal blocked, file too large) |
| `4` | Other Agents Shipgate error |
| `20` | Strict-mode gate failure |

---

## What you can't do (intentionally)

This section is the **CLI's** invariants. For the **agent's** behavioral boundary — what an agent driving Shipgate may assert in PR comments and review summaries — see [`docs/agent-autofix-boundary.md`](docs/agent-autofix-boundary.md).

- The CLI does not modify user code; it only reads.
- The CLI does not connect to MCP servers; it reads exported JSON only.
- Tool sources outside the manifest directory are rejected (path traversal containment).
- Files larger than 10 MB are rejected.
- Plugins are off by default (`AGENTS_SHIPGATE_ENABLE_PLUGINS=1` to enable; `--no-plugins` to force off).

---

## When you make changes to this repo

**Run the CLI as `./shipgate …` from the repository root** — `python shipgate …`
on Windows, which does not read a shebang. That is the one canonical command
here, for contributors and coding agents alike, and it is what every example in
`CONTRIBUTING.md` uses. Emitted commands name whichever spelling starts it, so
follow `next_actions[].executable` rather than assuming one token. `./shipgate scan -c shipgate.yaml`
is `agents-shipgate scan -c shipgate.yaml`, with three differences that matter
in a checkout: it runs *this* tree's `src/` rather than whatever copy `PATH`
resolves to, it selects a supported interpreter (`AGENTS_SHIPGATE_PYTHON`, else
the project virtualenv — the main checkout's, if this is a `git worktree`), and
it needs no installation and no `PYTHONPATH`. Recovery commands it prints name
the launcher, so they are runnable exactly as printed.

Use a bare `agents-shipgate` only to check what an *installed* build does. If a
command behaves as though your edit never happened, run
`./shipgate doctor --config shipgate.yaml --json` and read `environment`
(above): it states which interpreter ran, which package was imported, which
checkout you are standing in, and what disagrees.

The launcher stops at the repository boundary, and so should edits that spread
it. Everything written *into another repo* — the sections above, the adoption
kits, `.cursorrules`, `.claude/commands/`, `skills/`, `.agents/skills/`, the
snippets in `docs/target-repo-agent-snippets.md`, and every block
`init --write --agent-instructions=…` renders — keeps saying
`agents-shipgate`, because those run where the package is installed and there
is no launcher. It is also the same reason durable artifacts stay canonical:
those bytes are pinned by render hashes, and an absolute path from one machine
does not belong in them.

- Run `python -m ruff check .` and `python -m pytest` before committing.
- Bumping a check's behavior requires updating the test suite and any golden fixtures under `samples/*/expected/`.
- New checks must include: code in `src/agents_shipgate/checks/<category>.py` plus a `BUILTIN_CHECKS` entry in `checks/registry.py`, metadata in `docs/checks/<category>.yaml` (loaded into `CHECK_METADATA` at registry import time by `agents_shipgate.checks._metadata_loader`), a test in `tests/`, and a row in `docs/checks.md`. After editing YAML, regenerate `docs/checks.json` with `python scripts/generate_schemas.py`.
- Do not change check IDs in published versions; always add new ones.
- If you regenerate the JSON schemas, run `python scripts/generate_schemas.py` and commit every changed file under `docs/`.

---

## Reusable prompts

Prebuilt prompts for common workflows live in [`prompts/`](prompts/):

- [`decide-shipgate-relevance.md`](prompts/decide-shipgate-relevance.md) — apply [`docs/triggers.json`](docs/triggers.json) to decide whether Shipgate should run at all
- [`add-shipgate-to-repo.md`](prompts/add-shipgate-to-repo.md) — bootstrap a repo
- [`fix-top-finding.md`](prompts/fix-top-finding.md) — iterate on a single finding
- [`recommend-fixes.md`](prompts/recommend-fixes.md) — walk all active findings and surface targeted fix recommendations across the four autofix-policy classes
- [`explain-finding-to-user.md`](prompts/explain-finding-to-user.md) — translate one finding into 3–5 sentences of user-facing prose; companion to `agents-shipgate explain-finding`
- [`stabilize-strict-mode.md`](prompts/stabilize-strict-mode.md) — tune → baseline → promote
- [`triage-false-positive.md`](prompts/triage-false-positive.md) — override vs suppress decision
- [`upgrade-shipgate-version.md`](prompts/upgrade-shipgate-version.md) — bump agents-shipgate version safely (regenerate baseline if needed)

For downstream repos, use [`docs/target-repo-agent-snippets.md`](docs/target-repo-agent-snippets.md)
to copy Shipgate trigger rules into `AGENTS.md`, `CLAUDE.md`, Cursor rules,
PR templates, and advisory CI. Use
[`docs/agent-adoption-harness.md`](docs/agent-adoption-harness.md) to evaluate
whether coding agents discover and use Shipgate without being prompted by name.

### Editor / agent integrations

Per-agent install guides for dropping Shipgate into your own agent project:

- [`docs/agents/use-with-claude-code.md`](docs/agents/use-with-claude-code.md) — install the `/shipgate` slash command and `agents-shipgate` auto-discoverable skill. Source surfaces ship at [`.claude/commands/shipgate.md`](.claude/commands/shipgate.md) and [`skills/agents-shipgate/`](skills/agents-shipgate/) (named `agents-shipgate` to avoid colliding with the slash command — Claude Code lets a same-named skill preempt a command). The skill bundles the recipes in [`skills/agents-shipgate/prompts/`](skills/agents-shipgate/prompts/) and a starter advisory CI workflow at [`skills/agents-shipgate/ci-recipes/advisory-pr-comment.yml`](skills/agents-shipgate/ci-recipes/advisory-pr-comment.yml); when you change anything in [`prompts/`](prompts/) or `examples/github-actions/01-advisory-pr-comment.yml`, sync the bundled copy.
- [`docs/agents/use-with-codex.md`](docs/agents/use-with-codex.md) — install the canonical `AGENTS.md` snippet plus repo-scoped Codex skill. Source surfaces ship at [`.agents/skills/agents-shipgate/`](.agents/skills/agents-shipgate/) and are generated into downstream repos with `agents-shipgate init --write --agent-instructions=agents-md,codex-skill`. The default `all` kit does not install skill bundles. The skill is Codex-optimized: concise `SKILL.md`, on-demand references, and an advisory CI template.
- [`docs/agents/use-with-cursor.md`](docs/agents/use-with-cursor.md) — drop the canonical `.cursor/rules/agents-shipgate.mdc` auto-attach rule (from [`docs/target-repo-agent-snippets.md`](docs/target-repo-agent-snippets.md)) into your repo. The rule fires whenever a chat touches `shipgate.yaml`, an MCP/OpenAPI spec, a tool JSON, or a `.py` file.

---

## Verification

After you (the agent) complete a task involving Agents Shipgate, verify:

1. `agents-shipgate self-check --json` returns `"ready": true`.
2. `agents-shipgate contract --json` matches the installed CLI contract you expect.
3. The user's `shipgate.yaml` has no `CHANGE_ME` placeholders.
4. A scan completes with exit code 0 (advisory mode) and writes `report.json`.
5. The user's repo `.gitignore` includes `agents-shipgate-reports/` (do not commit reports).


<!-- ===== source: docs/agent-recipes.md ===== -->

# Agent recipes

Copy-pasteable workflows for AI coding agents (Claude Code, Codex, Cursor,
Aider) that need to drive `agents-shipgate` end-to-end without prompting
the user. Every command is read-only or schema-validated;
static-by-default, with audited exceptions pinned in
[`tests/test_adapter_static_only.py::ALLOWED_EXCEPTIONS`](../tests/test_adapter_static_only.py).

> If you are a human, [`quickstart.md`](quickstart.md) is the friendlier
> entry point. This page is structured for agents that consume `--json`.

---

## Recipe 0 · Verify an agent-related PR

Use this before claiming completion on a PR or local diff that changes tools,
MCP/OpenAPI surfaces, prompts, permissions, policies, release gates, or
`shipgate.yaml`.

```bash
agents-shipgate verify --preview --json
agents-shipgate preflight --workspace . --plan - --json
agents-shipgate verify --workspace . --config shipgate.yaml \
  --base origin/main --head HEAD --ci-mode advisory --format json
```

For local uncommitted work, omit `--base`/`--head`. For committed PR/CI refs,
make the base ref available first because `verify` never fetches. `--preview`
additionally wants `--head` **checked out**: it reads project markers from the
working tree, because that is the tree the `init` it recommends would write to.
Previewing some other ref establishes no project and returns
`agent_action_required` with a `fetch_base` action whose `expects` names the
missing input as a **commit id**, together with the pinned `--base`/`--head` to
re-run with — read them from `expects`, which is never truncated, because a
revision expression re-resolves against the new `HEAD`. Plain `verify` reads
`--head` from the object database and needs no checkout. Read
`agents-shipgate-reports/agent-handoff.json` first and lead with
`control.state`, `gate.merge_verdict`, `gate.can_merge_without_human`,
`next_action`, `fix_task`, and `capability_review.top_changes[]`. Fall back to
`verifier.json` only for older installed CLIs that do not report runtime
contract 14.
Then read `report.json.release_decision.decision`, which remains the only
release gate.

Before editing `shipgate.yaml`, Shipgate CI, AGENTS/CLAUDE/Cursor rules,
policy packs, baselines, waivers, suppressions, Codex hooks/config, Codex
plugin manifests, `.mcp.json`, `.app.json`, or `SKILL.md`, run
`agents-shipgate preflight --workspace . --plan - --json` with a
`PreflightPlanV1` object. Legacy `--changed-files` remains available. Switch on
`control.state`. If it is `review_publishable`, a human must approve the merge
and you may still commit, push, and update the PR; if it is
`human_review_required`, stop for a human; if it is
`agent_action_required`, perform only the exact coding-agent action in
`control.next_action` — its `command` when it names one, and otherwise the
input its `expects` names, which is the shape a `fetch_base` route carries.

Do not claim completion unless `control.state` is `complete`. Conversation-level
acknowledgement never changes control state; only a newly generated verifier
artifact can clear an obligation.

## Recipe 1 · First adoption helper

Use this when a repo doesn't yet have `shipgate.yaml` and the user wants a
scan-oriented first pass. The verifier-first path is
`verify --preview --json` →
`init --write --json` →
`verify --base origin/main --head HEAD`. The helper below remains useful when a
coding agent should also apply high-confidence manifest cleanup in the same
turn. Ongoing PR work should use Recipe 0.

```bash
agents-shipgate detect --json
agents-shipgate init --write --ci --json
agents-shipgate scan -c shipgate.yaml --suggest-patches --format json
agents-shipgate apply-patches \
    --from agents-shipgate-reports/report.json \
    --confidence high --apply
```

### Step 1 · `detect --json` (read-only)

Consume the response to decide whether to proceed. Key fields:

- Detection silently skips common fixture corpus directories such as
  `fixtures/`, `_fixtures/`, `__fixtures__/`, `testdata/`, `test_data/`,
  `test-fixtures/`, `test_fixtures/`, `golden/`, and `goldens/` when they
  are below the selected workspace. Point `--workspace` directly at a
  fixture project if you intentionally want to classify that fixture itself.
- `is_agent_project` — `true` when at least one Python framework
  scored ≥ 2.0 with a strong signal.
- `frameworks[]` — per-framework scores + evidence + candidate file
  paths.
- `agent_name_candidates[]` — ranked best-first, each
  `{value, source, role, path, rank_score, selectable, rationale[]}`.
  **Take the first entry whose `selectable` is `true`** — that is the value
  `init` writes as `agent.name`. When none is selectable the manifest keeps
  its `CHANGE_ME` placeholder rather than asserting an identity nothing
  reliably declares. Ordering is decided by, in effect:
  - `role` — `root_agent` (bound as `App(root_agent=…)`, or assigned to the
    conventional `root_agent` symbol) outranks `agent`, which outranks
    `sub_agent` (named inside another agent's `sub_agents=[…]` /
    `handoffs=[…]`). `workspace_dir` is the directory-name fallback and is
    never selectable.
  - `path` — a name declared in product code outranks one that is not the
    product's: test code, which names fixtures, and a scaffolding template
    under a `resources/templates/` directory, which names an example. This
    dominates: either one building an `App(root_agent=…)` still ranks below
    a plain agent the shipped code declares.
  - corroboration — a value the project name independently agrees with
    ranks above one only a single site declares.
  - a quality floor — values under three significant characters, and
    generic scaffolding names (`agent`, `foo`, `test`, …), are ranked last
    and marked `selectable: false`.

  One rule overrides all four: if a **project** declares an application root
  whose name cannot be resolved statically — a dynamic expression, a factory
  call, a symbol bound more than once — then nothing that project declares
  is selectable, and the `rationale[]` names the project and says why.
  Anything still ranked there is by construction not the root, so writing it
  would declare a worker as the reviewed identity. The scope is the project,
  not the repository: on a monorepo, agents in the other entries of
  `agent_project_candidates[]` stay selectable. A name two projects declare
  is rejected when either one is blocked.

  "Which project" means an entry of `agent_project_candidates[]`, not the
  nearest directory carrying a project marker. A marker directory that holds
  no agent evidence — a utilities package with its own `pyproject.toml` — is
  not a manifest scope, and a name found there belongs to the scope that
  encloses it.

  The same product/not-product split above decides *which* roots block: a
  root declared only by test code or by a scaffolding template is not the
  application a project ships, so it does not disable selection for the
  rest of the project (#398).

  `rationale[]` states which of those applied, so a ranking change is
  visible in the output rather than silently changing what the manifest
  claims. `name=` values that come from a module constant or an
  `os.environ.get("…", "…")` default in the same package are resolved
  statically (one hop, no code executed) and say so in `rationale[]`.

  All of this reads Python's binding rules or declines — a spelling is never
  taken as provenance. `Agent`/`LlmAgent`/`App` are resolved through the
  binding that reaches *the call site*: a framework constructor imported
  under an alias is recognised, and one shadowed by a local `def`/`class`,
  bound only after the call, bound conditionally, or replaced through an
  attribute (`adk.Agent = fake`) is not. Dotted spellings are held to the
  same standard — the head must prove a framework module.
  Left unresolved rather than guessed: a symbol bound more than once
  anywhere in the file; one assigned under an `if`/`try`/loop; one rebound
  in an enclosing scope (a function body executes when it is called, not
  where it is written);
  one whose import could resolve to two different in-workspace modules; an
  `os.getenv` spelling that is not a provably unshadowed stdlib import; and
  anything at all in a file carrying `from x import *` — until a later
  explicit binding re-establishes what a spelling means. Bindings that carry
  no assignment count too: `del`, `class`, `except … as`, `case`, and a
  `global`/`nonlocal` store routed to another scope all retire the agent a
  name used to hold. Scopes follow Python's own — comprehensions have their
  own, definition headers (defaults, decorators, annotations, class bases)
  are evaluated in the enclosing one, and a root declared inside a
  conditionally defined function is contingent on that branch.
- `project_name_candidates[]` — `{value, source}` only. Project names have
  no hierarchy to rank, so they carry none of the fields above. The
  `pyproject` source seeds `project.name`, never `agent.name`.
- `suggested_sources[]` — MCP/OpenAPI files matched by glob AND accepted
  by the real input adapters, so `init` never writes a `tool_sources`
  entry that `scan` rejects at parse time. These do NOT bump
  `is_agent_project` on their own.
- `excluded_sources[]` — `{type, path, reason}` for glob matches the
  input adapters reject (e.g. an `mcpServers`-style host config such as
  a Cursor plugin `mcp.json`, or a Swagger 2.0 document). Do not add
  these to `tool_sources`; the `reason` says what `scan` would fail on.
- `codex_plugin_candidates[]` — Codex plugin package or marketplace
  artifacts matched by convention. These also do NOT bump
  `is_agent_project` on their own.
- `host_boundary_candidates[]` — `{path, hosts, file_type}` from the existing
  bounded host census, including ignored settings. Filenames only; no config
  contents, grants or runtime authority have been verified. Host-only input
  routes to `audit --host` without a manifest.
- `host_discovery_incomplete_paths[]` — paths the bounded census could not
  see through: a link it does not follow (only when that link resolves to a
  directory — one pointing at a file conceals nothing and is not listed), and
  a directory it could not read or that exhausted the entry bound. Empty
  candidates beside this field do not prove absence. Follow the inspection
  route; a directory at a config filename is also an input defect requiring
  inspection. A census that stops publishes no candidates at all, and never
  refuses the rest of the classification — the framework, tool-source and
  scope answers stand, and only the product-wide negative is withheld.
- `next_actions[]` — the ranked route. On `agent_scope: "ambiguous"` rank 1 is
  the decision (`kind: "review"`, `command: null`) and every entry below it is
  one exact `init --workspace <candidate> --write --json`, with `executable`
  and `args`, in candidate order — the list `init --write` publishes when it
  refuses the same workspace, minus the setup flags: `detect` asked for no
  setup, so it promises none. Add `--ci` or `--agent-instructions` yourself if
  you want them, or take the command from `init`'s own refusal, which repeats
  the flags the run asked for. Match on the path rather than the ordering.
  Every candidate gets an entry; the workspace root is never offered, since it
  is the scope `init` refuses. A candidate that already carries a manifest gets
  `doctor --config <that manifest> --json` rather than an `init` that would
  refuse to overwrite it — unless you asked for setup it still owes: with
  `--agent-instructions` the full `init --write` is the advertised refresh and
  exits 0, and with `--ci` the command drops `--write` so the workflow is
  installed and the manifest is left alone. The workspace root, when listed, is
  a `review` entry rather than a command.

**Stop condition.** Stop and skip `init` only when ALL of:

- `is_agent_project` is `false`, AND
- `suggested_sources` is empty, AND
- `codex_plugin_candidates` is empty, AND
- `host_boundary_candidates` is empty, AND
- `host_discovery_incomplete_paths` is empty, AND
- `python_parse_truncated` is `false` — each negative above is a claim about
  the whole workspace, and a run whose Python parse stopped at its cap read
  only part of one. This is the raw parse bit, not `agent_scope_truncated`,
  which additionally requires more than one candidate scope, AND
- no `shipgate.yaml` already exists, AND
- the user did not explicitly request a scan.

A payload that does not carry every one of those keys leaves the condition
unevaluable — `trigger` reports `stop_conditions_evaluated: false` and infers
no stop. Re-run `detect` with the current CLI rather than reading an absent
key as `false`.

Otherwise proceed. MCP/OpenAPI-only tool-surface repos and Codex plugin
package repos surface as `is_agent_project: false` but should still be
onboarded — their sources will land in `tool_sources` during `init`.
Host-only repositories instead follow the emitted audit route. `init` and
`bootstrap` hand off without creating setup files; even `init --ci` cannot
create a manifest-based workflow for a repository needing only host review.
The hand-off covers every detection-driven mode — `--ci`, `--claude-code`,
`--agent-instructions`, `--local-review` — because each renders its manifest
from that classification. `init --minimal` is the exception: it never
classifies the workspace, so it still writes the manual template on request.

### Step 2 · `init --write --ci --json`

Auto-detection runs again inside `init` and writes:

- `shipgate.yaml` with `tool_sources` populated per detected framework
  candidate file.
- `.github/workflows/agents-shipgate.yml` (if `--ci` is set; refuses
  to overwrite an existing workflow file, or one where a sibling
  workflow already uses the `ThreeMoonsLab/agents-shipgate` action at
  any ref).

Key response fields:

- `manifest_status`: `"written"` | `"skipped_existing"` |
  `"refused_unresolved_scope"` | `"not_attempted"` |
  `"not_applicable_host_review"` — the last one is the host-only hand-off:
  no manifest, workflow or instruction file was written, and
  `control.next_action` names the read-only host audit instead.
- `workflow.status` (when `--ci`): `"written"` | `"skipped_existing_target"`
  | `"skipped_cross_reference"`.
- `placeholders[]` — entries the template intentionally leaves as
  `CHANGE_ME` because no high-confidence signal was available. Each has
  a `path` (YAML-pointer-ish location) and `current` value. Replace
  these before scanning. Every field the template chose without evidence is
  in here, `tool_sources[].type` included (#441).
- `tool_surface_origin`: `"detected"` | `"scaffold"` | `null`. `"scaffold"`
  means this render read no tool surface and the `tool_sources` block is a
  placeholder — `id`, `type`, and `path` are all `CHANGE_ME` and all three
  are in `placeholders[]`. `null` means this run's render reached neither disk
  nor this payload, so it describes no file you can open: `skipped_existing`
  and `refused_unresolved_scope` report it, on the same authority rule
  `placeholders` follows. `manifest_message` always states the same fact in
  prose; `control.reason` states it only where init's own reason is the
  envelope's, because on a freshly written manifest the human-owned
  `declared_purpose` declaration outranks it. Do **not** run `scan` on a
  scaffold — fill in the block first; until then there is nothing for the gate
  to read.

  In the default (auto) mode a `"scaffold"` also means full discovery found
  nothing, and `detect` on the same workspace agrees: `is_agent_project: false`
  with no suggested sources. Under `--minimal` it means only that the minimal
  template skips framework detection entirely — `detect` may well report a
  framework for the same workspace — and the summary it emits says so.
- `auto_detected.agent_name` — the value the manifest carries
  (`null` when the template fell back to `CHANGE_ME`; matches the YAML
  exactly).
- `auto_detected.agent_scope`: `"single"` | `"ambiguous"` | `"unknown"`,
  with `auto_detected.agent_project_candidates[]` naming every self-contained
  project (project-marker directory) that defines an agent. `"unknown"` means
  discovery hit its Python-file cap in a workspace with several project roots,
  so the verdict would otherwise have depended on which files were read
  first.
- `auto_detected.python_parse_truncated`: whether the Python parse stopped at
  its cap at all. Every whole-workspace negative — `is_agent_project: false`
  included — is unsafe to act on while this is `true`, and `--write` refuses,
  because the agent name and tool surface a manifest would declare were read
  from part of the tree. The recovery is mechanical and the emitted
  `next_actions[0]` carries it: the *same command you ran*, plus
  `--max-python-files <workspace_signals.python_file_total>` — a bound that
  covers every Python file and so cannot hit the cap again. From `detect` that
  is a `detect`; from `init --write` it is an `init --write`, carrying the
  setup flags the run asked for, so one step both settles the scan and
  completes the setup.
- `auto_detected.agent_scope_truncated`: whether that candidate list is an
  enumeration or a lower bound. `true` means the Python parse stopped at its
  cap in a workspace holding more than one candidate project scope, so any
  project in the part of the tree that was not read is missing from the list —
  do **not** conclude a project is absent from it. Re-run
  `detect --max-python-files <n> --json` first.
- `auto_detected.workspace_signals.project_root_count` bounds that claim: an
  uncapped, filename-only census of the directories that could be a manifest
  scope (every project-marker directory, plus the workspace root itself, which
  is a candidate whether or not it carries a marker). `init` emits the same
  block `detect` does, so the number its refusal message quotes is readable
  structurally.

`--ci` is orthogonal to `--write`: each gets its own overwrite-refusal.
Exit code is the max of per-action outcomes; manifest-error and
workflow-skip can co-occur. The workflow lands at the repository root —
GitHub loads workflows from nowhere else — named `agents-shipgate.yml` for a
root manifest and `agents-shipgate-<project>.yml` for a scoped one, because
the action takes a single `config` scalar and one shared file would leave
every project after the first ungated. Read `workflow.path`.

`refused_unresolved_scope` (exit `2`) is the one outcome where **nothing**
is written — not the manifest, not the workflow, not the agent-instruction
snippets, not the reports `.gitignore` block. It fires when agents live in
more than one project under this workspace (because one `agent.name` and one
`declared_purpose` cannot describe them all) and when discovery was capped
before it could tell. Re-run with `--workspace` pointed at one of
`agent_project_candidates[].path` rather than retrying the same command — the
emitted `next_actions[]` commands repeat whatever setup flags you passed.
`--allow-unresolved-scope` accepts a single manifest for the workspace as a
whole, and `--minimal` is never scope-gated because it adopts no detected name
or tool surface.

### Step 3 · `scan -c shipgate.yaml --suggest-patches --format json`

Writes to `agents-shipgate-reports/report.json`. Read it, walk
`findings[]` filtering on `suppressed`. Per-finding fields you can rely
on today:

- `check_id`, `title`, `severity`, `category`, `evidence`,
  `confidence`, `recommendation`.
- `patches[]` (only when `--suggest-patches` is set) — list of
  patch objects with `kind` ∈ `{set_pointer, append_pointer,
  remove_pointer, manual}`. Non-manual patches additionally carry
  `confidence` ∈ `{low, medium, high}`, `target_file`, `pointer`,
  `target_format`, `rationale`, `target_sha256`.
- `manifest_dir` (top-level on the report) — absolute path to the
  directory containing `shipgate.yaml`. `apply-patches` enforces a
  containment check against this.

When `--suggest-patches` is set, every active (unsuppressed) finding
has at least one patch. Manual-only findings (e.g. trace approval
flips, per-check policy decisions) carry a single `ManualPatch` with
`instructions` instead of a machine-applicable patch.

Optional dynamic-validation handoff:

```bash
agents-shipgate scenario suggest \
    --from agents-shipgate-reports/report.json \
    --out agents-shipgate-reports/suggested-scenarios.yaml
```

This YAML is a concrete per-finding/per-tool fan-out of
`report.json.suggested_scenarios[]`, not a separate scenario engine.
Suppressed findings are omitted; baseline-matched findings remain because
they are accepted debt, not resolved risk.

### Step 4 · `apply-patches --confidence high --apply`

Default `--confidence high` only auto-applies patches whose `confidence`
field is `"high"`. Today that's the 3 stale-manifest removals
(`SHIP-MANIFEST-STALE-{SUPPRESSION,POLICY,RISK-OVERRIDE}`). Scope
coverage appends ship at `medium` and require explicit
`--confidence medium` to apply.

`apply-patches` is dry-run by default — `--apply` is required to
mutate files. Containment-checked: any `target_file` outside
`report.manifest_dir` aborts with exit code 5 before SHA verification.

### Step 5 (optional) · Summarize for the user

When the flow completes, summarize `report.json`:

- `release_decision.decision` (`"blocked" | "review_required" | "insufficient_evidence" | "passed"`)
  — the v0.8+ release-gate signal (`insufficient_evidence` added v0.14).
  Prefer this over `summary.status`, which stays baseline-blind for
  backwards compat. Switch on the value with a `review_required`
  fallback for unknown future values.
- `release_decision.reason` (one-sentence explanation).
- Top 3 active critical/high findings with their `check_id`,
  `tool_name` (when present), and `recommendation`.
- Whether any patches were applied (count from
  `apply-patches --json` output's `files`).

Link findings back to [`docs/checks.md#<id>`](checks.md) so the user
can read full check rationale.

---

## Recipe 2 · Add Shipgate to a repo that already has tool surfaces

Same as Recipe 1, but `detect` may report `is_agent_project: false`
when the repo only ships MCP exports or OpenAPI specs. Per the soft
stop rule above, proceed anyway when `suggested_sources` is non-empty.

`init` will populate `tool_sources` from those globs. The rest of the
flow (steps 2-5) is identical.

### First-real-repo recovery rules

When the first repo scan does not produce useful tools, follow these
rules before changing code:

- If `detect --json` has MCP/OpenAPI `suggested_sources`, continue to
  `init` even when `is_agent_project` is `false`.
- If `doctor` shows zero tools, inspect `tool_sources[].path`, MCP
  `tools[]`, OpenAPI `paths`, optional source warnings, and dynamic
  ADK/MCP warnings.
- If tools are created by factories, wrappers, runtime imports, or
  dynamic ADK/MCP toolsets, provide an explicit MCP export, OpenAPI
  spec, or local tool inventory artifact.
- Resolve the placeholders `init --json` reports before scanning, and
  switch on **`control.next_action.actor`**, which routes the turn
  rather than individual fields. `placeholders[]` is a location list
  (`path`, `current`, `line`) and carries no owner. `actor: "human"`
  selects a human review or stop with `permissions.edit: false`:
  surface the required review and stop. Its `why` is fitted to a
  prose budget and may end `and N more in placeholders[]`, so absence
  from it never makes a field yours. `actor: "coding_agent"` authorizes
  only the exact `control.next_action`: read its `kind`, `path` or
  `command`, perform that step, and rerun the stated check. A blocking
  setup repair can take precedence while human-owned declarations
  remain unresolved; the actor is not proof that all remaining
  placeholders are yours. Every
  declaration — purpose, prohibited actions, effect, authority,
  binding, approval, confirmation, idempotency, safeguards, accepted
  debt — must be supplied by a human, because Shipgate never invents a
  declaration nobody made. Do not derive one from a prompt, main agent
  file or README.
- Agents Shipgate requires Python 3.12+. If the project runtime is
  older, install the CLI outside the project env with `pipx` or `uv`.
- Ensure `agents-shipgate-reports/` is listed in `.gitignore`.

---

## Recipe 3 · Re-scan after editing the manifest

When the user has already replaced `CHANGE_ME` placeholders or added
policies:

```bash
agents-shipgate scan -c shipgate.yaml --suggest-patches --format json
agents-shipgate apply-patches \
    --from agents-shipgate-reports/report.json \
    --confidence high --apply
```

`run_id` is deterministic for the same input — if the report's
`run_id` is unchanged from the previous run, nothing semantic about
the manifest+tool-surface changed.

---

## Recipe 4 · Suppress a check or finding

When a finding is a known false positive, edit `shipgate.yaml`:

```yaml
checks:
  ignore:
    - check_id: SHIP-DOC-MISSING-DESCRIPTION
      tool: support_lookup_v2  # optional; omit to suppress for ALL tools
      reason: "Tool description matches the upstream OpenAPI summary."
```

`reason` is required — empty reasons fail manifest validation. Re-run
`scan` to confirm the finding is gone (it will appear in `findings[]`
with `suppressed: true` rather than disappearing from the report).

If you suppress a check that no longer fires, the next scan emits
`SHIP-MANIFEST-STALE-SUPPRESSION` — auto-removable via
`apply-patches`.

---

## Recipe 5 · Add Shipgate to CI without changing existing workflows

```bash
agents-shipgate init --workspace . --ci  # no --write
```

Without `--write`, the manifest is printed to stdout (don't write a
new one). With `--ci`, the workflow file is still written orthogonally
unless an existing workflow already references the action — in which
case `workflow.status: "skipped_cross_reference"` and the path of the
existing workflow is reported in `cross_reference_path`.

---

## Output handling

- Always pass `--json` (where supported) and parse the result. The
  human-readable stdout is unstable; the JSON shape is the contract.
- `scan` does not have `--json`; instead pass `--format json` and read
  `agents-shipgate-reports/report.json`.
- Errors emit a structured `next_action` JSON line on stderr when
  `AGENTS_SHIPGATE_AGENT_MODE=1` is set. Surface that path to the user
  rather than scraping prose.

## Pre-flight reminder

`agents-shipgate-reports/` is a local artifact directory. Before
committing, ensure it's listed in `.gitignore`:

```gitignore
agents-shipgate-reports/
```

`init` does not touch `.gitignore` — leave that to the user or follow
up with an explicit edit.

---

## Reference

- [`docs/agent-autofix-boundary.md`](agent-autofix-boundary.md) — what
  an agent may do mechanically vs. what must defer to a human reviewer.
- [`docs/report-reading-for-agents.md`](report-reading-for-agents.md) —
  reader's primer for `agents-shipgate-reports/report.json`.
- [`docs/checks.md`](checks.md) — full check catalog with rationale
- [`docs/autofix-policy.md`](autofix-policy.md) — which findings are
  safe to apply, which need review, and how `apply-patches --confidence`
  filters them
- [`docs/minimal-real-configs.md`](minimal-real-configs.md) —
  framework-specific minimal manifests
- [`AGENTS.md`](../AGENTS.md) — top-level agent instructions, install,
  trigger table


<!-- ===== source: docs/agent-contract-current.md ===== -->

# Current Agent Contract

Runtime contract v33 adds `host_boundary_candidates[]` and
`host_discovery_incomplete_paths[]` to discovery. They describe recognized
configuration paths and traversal the bounded census could not see through,
never parsed grants. An incomplete census withholds the product-wide negative
and publishes no candidates; it does not fail the classification. A host-only
workspace follows `audit --host` without creating a manifest. `init` and
`bootstrap` hand off without setup writes; incomplete input cannot publish a
product-wide negative. Existing operational control and persisted evidence
schemas are unchanged; all setup permissions remain false.

Runtime contract v32 separates instruction prose from supported parsed permission
structure across verification, preflight, host drift and generated edit hooks.
It publishes verifier v0.17, handoff v9, preflight v0.5 and host evidence v0.3.
Raw identity still changes on prose edits; legacy evidence is never upgraded to
a new permission claim. `conditional_file_edits` is a standing routing rule with
`grants_authority: false`, separate from unconditional `forbidden_file_edits`.
See [the comparison and migration contract](engineering/instruction-structure-boundary.md).


Runtime contract v31 adds a read-only
[external review decision evaluator](human-review-decision.md) for the bounded
[`human-review-request.json`](human-review-request.md) class. It verifies current
scope, host key trust, reviewer eligibility and expiry and returns separate
evaluation evidence. It writes no artifact and grants no merge or completion
authority. The shared control union and all existing persisted schema
identifiers are unchanged; the minimum control contract stays at 21.
GitHub acquisition and persistence remain an integration obligation.

The single, current statement of what AI coding agents and CI integrations should read from Agents Shipgate output. When the contract changes, update [STABILITY.md](../STABILITY.md) first, then this file. Other agent-facing surfaces (`AGENTS.md`, `llms.txt`, `.well-known/agents-shipgate.json`, the slash command, the skill, the FAQ) link here instead of restating field lists.

For contributors changing the contract: regenerate schemas with
`python scripts/generate_schemas.py`, then run
`python scripts/regenerate_goldens.py` and its `--check` mode using the
[committed sample recipe](../CONTRIBUTING.md#sample-goldens). Review actual
artifact changes and preserve behavioral assertions; sample regeneration does
not freeze a release contract or qualify a candidate wheel.

## Current versions

Verify the installed CLI contract locally before relying on hard-coded docs:

```bash
agents-shipgate contract --json
```

### Unreleased declaration-review migration

Runtime contract v29 coordinates the public artifacts that carry changed
action declarations: report v0.43, packet v0.18, and verifier v0.16.
`minimum_control_contract_version` remains 21 because the operational control
envelope is unchanged.

Read
`release_decision.evidence_coverage.semantic_coverage.declaration_review` for
the base-vs-head reviewer projection. It includes added, removed, and
semantically modified declaration rows. A requested comparison that could not
run is distinct from an available comparison with no changes; missing or
conflicting identity and semantic evidence gaps cannot earn an
`evidence_consistent` status. Report Markdown, packets, PR comments,
annotations, and the GitHub summary consume the same bounded projection.

See the
[declaration-review migration note](../STABILITY.md#migration-note-unreleased-declaration-review).

### Earlier projection-only migration

That change moved no schema or runtime-contract version; the v0.42 report
schema gained the typed `unattested_surface` gap and optional
`EvidenceGap.policy_id`.
Completed blocked verifier runs now append the deterministically selected worst
blocker to the plain headline as well as the adoption/self-approval branches;
that same headline feeds `control.reason` and `control.next_action.why`. Runs
with no blocker do not gain the clause. A genuinely incomplete enumeration
remains `incomplete_surface`; a lower-confidence extraction without an
enumeration defect is the distinct `unattested_surface` gap, and only an explicit
`surface: enumerated` adapter fact earns positive enumeration wording. Its
remedy asks for reviewed attestation. Policy gaps retain their exact identity
in structured `policy_id`, while
policy-evidence gap prose no longer prefixes `why` with an engine-owned
`builtin-*` policy id. Public and organization-defined check ids remain stable
labels. Branch on the typed gap kind and `policy_id`, never on the explanatory
sentences. See the
[verifier-explanation migration note](../STABILITY.md#migration-note-unreleased-verifier-explanations).

Standalone trigger evaluation is unchanged. Inside a verifier artifact,
however, its generic route has already been consumed. Embedded
`trigger.next_action` therefore preserves the evaluated kind while clearing
its command and adding `authoritative: false` and
`authoritative_path: "control.next_action"`; the self-referential preview
command is not repeated. Commands on embedded `matched_rules[]` are cleared as
well. In verifier, verify-run, and preview output, follow
only `control.next_action` and `control.allowed_next_commands`. See the
[embedded-trigger migration note](../STABILITY.md#migration-note-unreleased-embedded-trigger-routing).

Runtime contract v28 publishes the capability delta as a **standalone
attestation**. `verify` writes
`agents-shipgate-reports/capability-delta-attestation.json`: an
[in-toto](https://github.com/in-toto/attestation) Statement whose
`predicateType` is
`https://threemoonslab.com/agents-shipgate/capability-delta/v1` and whose
predicate carries the frozen `shipgate.capability_payload/v1` **delta view**
unchanged. The contract now names the predicate type, both schema versions,
both schema paths, and the artifact, so a consumer can discover the format
without reading our source. Full specification:
[`docs/capability-delta-attestation.md`](capability-delta-attestation.md).

Read it when you want *what the agent can do after this change* as a portable
fact — a runtime gateway, a policy engine, a dashboard, another CI system.
Nothing about the gate moves: the attestation carries no verdict, no severity
and no release impact, and `release_decision.decision` remains the only release
decision signal.

Two things a consumer must branch on. The attestation is written **only for a
committed-tree subject** — a worktree run publishes a note on
`verifier.base_notes[]` and no file, because the bytes it evaluated are not in
any tree object. And `predicate.verification.status` is `bound` or `unbound`:
only `bound` chains to `verification-receipt.json` through `input_set_id`, and
a consumer that needs that chain checks the status rather than probing for
absent fields. Everything else is unchanged from v27.

Runtime contract v27 finishes the v24 rollout on the stream v24 left out.
**Every** agent-mode error line from `detect`, `init`, and `doctor` now carries
the same `shipgate.agent_control/v1` object those commands publish on
`--json`. Before, `doctor`'s failure routes carried one and `detect`'s and five
of `init`'s did not, so whether a caller that routes on `control` could
route at all depended on which setup command had failed and on which of its
failures — a caller cannot branch on that, and the run that most needs a route
is the one that printed no payload to carry it.

No field is removed. `next_action` (single string) and `next_actions[]` (ranked
array) are unchanged on those lines, and are derived from the same selected
route as `control.next_action`, so the three cannot send you to different work.
Three *command values* do move, and they are listed at the end of this section.

**What every setup error line guarantees**, in the published schema and not
only in the producer: `decision_source: "setup"`, a `decision` from
`setup_complete | setup_incomplete | setup_not_applicable`, every field of
`permissions` false, and `control_state` never `complete`. Setup authorizes
nothing, on either stream.

**Do not read `execution` as "this is an error line".** The `error` field says
that. `execution` answers whether the command reached an answer about the
workspace, and both values appear on error lines: `"failed"` when it could not
(an unparseable flag value, discovery that could not be bounded, a manifest it
could not open), and `"succeeded"` with a non-zero `exit_code` when it did and
the answer is a refusal it can route past — `config_already_exists` from
`init --write` declining to overwrite, and the unresolved-scope `config_error`.
The second kind still carries `permissions` all false, because it is still a
setup envelope.

**One stated exception.** The shared `--workspace` refusal (`config_error`,
exit 2, emitted by every command that takes a `--workspace`) carries
`next_action`/`next_actions[]` and no envelope. It fires because the workspace
does not exist, so there is no setup subject for `input_id` to address and no
setup facts for a state to be derived from. Treat an error line with no
`control` as "this is not a setup answer", not as "route on something else".

`scan`, `verify`, and `check` are unchanged and are deliberately not in this.
`scan` and `verify` publish a control *pointer*, so their envelope is the
promoted read `agents-shipgate agent control` — or, for `verify`, `--format
control` directly. `check` publishes no pointer at all and binds its authority
to `input_id` instead; its envelope is `--format agent-control-json`.

`init --write` over a manifest that already exists also publishes a different
route. It reported `next_action.kind: "edit"` on `shipgate.yaml` with
`expects: "The manifest reflects the desired tool sources, agent
declared_purpose, and policies"` — a postcondition the file already satisfied,
because a manifest that does *not* load is claimed by the repair route above
it. On this contract `next_action` **is** the step, so that route could not
change the answer: an envelope-only caller opened the file, found nothing to
change, re-ran, and got the identical action back. It is now the `doctor`
invocation for the manifest on disk, which reports what that manifest still
owes. The exit code (2) and the "already exists — edit it directly or remove
it" sentence are unchanged.

**Unless the request was not applied.** `init --write --control-pack <id>` over
a manifest that selects a different pack keeps a reconciliation route naming
`policies.control_pack` and the exact value. Both onward routes would otherwise
advance under the pack that is *there*, and the request would be lost with
nothing saying so — a recovery that completes with less than the caller asked
for reports success for work it did not do. The same reconciliation is
published for a scoped **candidate** whose manifest selects a different pack,
where a bare `doctor` had the same effect one directory down.

"Asked for" is read from the parser, not inferred: an explicit
`--control-pack default` over a `financial-strict` manifest **is** a request,
and it is the one that can only weaken.

**Who owns that edit is decided by the direction, not by who typed the flag.**
A governed coding agent composes its own argv, so process arguments are not
authenticated human provenance. A transition that keeps at least every
obligation the manifest has today is `agent_action_required` with a typed
`edit`; one that drops any obligation — or names a pack this build cannot
resolve, where the direction is unprovable — is `human_review_required` with no
command, naming the effects and controls it would remove. That is the same
direction `verify_policy` raises `control_pack_weakened` for, computed by the
same function.

**Two command values also move.** Every recovery `init` publishes now repeats
the whole invocation with only the invalid value corrected — `--minimal`,
`--allow-unresolved-scope`, `--agent-instructions-kit`, and a non-default
`--max-python-files` ride along, where before they were dropped; and the
`internal_error` fallback names this invocation with `--minimal` added instead
of a bare `agents-shipgate init --minimal` that ran in the process directory.
These are `command` value changes on `next_action`, `next_actions[0]`, and
`control.next_action` alike, which are one route.

Both the `AgentControl` union and `shipgate.agent_control/v1` are
byte-identical to v26 — the same envelope reaches one more place — so
`minimum_control_contract_version` stays at `21`.

Runtime contract v26 adds the **declaration continuation**: a receipt
``apply-patches`` writes beside the report it applied from
(``declaration-continuation.json``, ``shipgate.declaration_continuation/v1``),
and the one situation in which a **blocked** release decision may authorize
publication. The receipt pins the manifest by byte digest on both sides of the
write; the run additionally compares the two manifests and requires the delta
to be added ``action_surface.actions`` rows and nothing else. On that proof the
control is ``review_publishable`` — ``edit``/``commit``/``push``/``update_pr``
so the drafted proposal reaches a person, ``merge`` and ``report_complete``
still denied. Without it a blocked decision authorizes nothing, exactly as
before. `verifier.json`, `agent-handoff.json` and `verify-run.json` each carry
the resulting `declaration_continuation` boolean, which is why their schema
versions move to `0.15`, `v8` and `v5`.

Runtime contract v25 added one route to the control envelope:
`next_action.kind: "confirm_declarations"`, published by `verify` on a
**working-tree** run whose verdict is `insufficient_evidence`, with no blockers
and no policy weakening, when at least one open declaration question is one the
scan can answer from its own evidence. It is not published on a ref-bound
(`--base`/`--head`) run: `apply-patches` mutates the checkout, so the exact
rerun would re-scan the commit the edit is not in yet — the same precondition
every mechanical repair route carries.

Weakening here is `capability_review.policy_weakening_proven`, plus the
fail-closed `capability_review.policy_weakened` **unless this diff introduces
the gate it is judged by**. That exemption is a separate, narrow, proven fact —
the configured manifest is in the diff, absent at the comparison ref, the diff
removes or renames away no YAML file, and every policy surface it touches is
that manifest — so there is no prior version of this gate the change could have
loosened. It exists because `policy_weakened` stays raised whenever the
direction could not be established, and establishing it means proving no file
in the tree parses as a manifest under any name: one blob past the probe's read
bound ends that proof, which is the normal case on a real repository. Gating
the route on the flag alone made a first adoption — the run with every question
still open — the one run that could never be offered it. Nothing else moves:
the flag, the verdict, the adoption wording, and the human route are all
unchanged, and a proven weakening still refuses.

- **What it carries.** The exact `apply-patches` command that writes those
  answers, plus `questions[]` — open declaration questions, each tagged
  `authorable_by: "coding_agent" | "human"` — and the two counts
  `agent_authorable` / `human_authorable`, which are the real totals. The list
  is a prefix capped at six rows, ordered human-owned first (a drafted row is
  answered by the command whether or not it is printed; a human-owned row is
  what you have to hand a person), each half keeping the report's ranking. Read
  `release_decision.evidence_coverage.semantic_coverage.declaration_questions.open_questions[]`
  for the unabridged list.
- **What it authorizes.** Exactly the named command, plus whatever
  `permissions` says — publish-only on this route: `edit`, `commit`, `push`,
  `update_pr` true; `merge` and `report_complete` false. Writing declarations
  into `shipgate.yaml` touches the trust root, so the change still reaches the
  gate only through a human merge. The route is a proposal step, never a
  completion.
- **The content rule.** A row is `authorable_by: "coding_agent"` only when the
  scan filled every blank in its `declaration_template` — from the closed
  effect vocabulary, never weaker than any reading it observed — *and* the
  question is not one that asks a person to look again: a `declaration_drift`
  row restates a confirmed answer beside a moved pin and stays `"human"`,
  because an agent re-stamping the pin would close the very request the row is
  for. A template still carrying a `<REVIEW_REQUIRED>` blank (every authority
  block, every override) stays `"human"` too. Authorship is decided by content,
  never by who is running: an agent may propose what the evidence supports, and
  only a human may assert against it.
- **What the patch may write.** `next_action.patch` is exactly
  `declaration_template`, split into the keys that name the action and the
  fields that are written — the schema rejects any other pairing, so a row
  cannot advertise an evidence-derived tag beside a patch that writes something
  else. Its `target_path` is relative to `manifest_dir`, so the row means the
  same thing in the packet, the SARIF file, and a cached base scan.
  `apply-patches` writes only into fields the manifest leaves silent, and
  refuses — exit 5, nothing written — when a row already answers one
  differently, when two equally compatible rows name the same tool, or when the
  manifest has changed since the scan.
- **After it, in this order: rerun, then publish.** The command edits
  `shipgate.yaml`, so the moment it succeeds this control is stale —
  `agents-shipgate agent control` refuses with `workspace_changed` ("the
  working tree carries 1 uncommitted change this decision never saw"), and the
  v20 refresh rule requires that read before any commit, push, or PR update.
  The permissions printed beside the route were computed against a manifest
  that no longer exists and cannot authorize publishing what the command just
  wrote, which is why `expects` promises the write and the supersession rather
  than a commit. Re-run verification, then act on what *that* run authorizes.
  It is a fresh decision, and a declaration whose whole purpose is to make a
  risk judgeable is exactly what can move the verdict to one only a person may
  clear. If questions remain, they are the ones tagged `"human"`, and the human
  route names them rather than asking for generic review.
- **When it is withheld.** `report.json` publishes every open question with
  `authorable_by` resolved whether or not the route is published, so a control
  that offered nothing and explained nothing read as an invitation to write the
  manifest without the route. The headline — and therefore `control.reason` and
  `control.next_action.why` — now carries one sentence naming the cause, for
  example `3 declaration(s) this scan could draft are withheld: a blocker is
  open, and that decision is a person's.` It is produced by the same pass that
  withholds the route, so the cause published is the cause that acted, and only
  a comparison that actually ran may say the gate was weakened. It appears only
  where a drafting route existed to publish and something refused it; a run
  with no agent-authorable question withheld nothing and says nothing.

  **It is an explanation, not a machine field.** Like every headline context
  clause it shares the 400-byte prose budget with the verdict, the worst
  blocker, the gap-provenance clause and the reserved human-review
  requirement, and it is dropped whole — after gap provenance, which names a
  subject that left the analysed surface and outranks it — rather than
  truncated. A run whose verdict and blocker have already spent the budget
  publishes no context at all. Do not branch on its presence.

The `AgentControl` union is unchanged again, so
`minimum_control_contract_version` stays at `21`: the control holds the step as
the `repair` command it truthfully is, and the envelope publishes the richer
form — the same split `SetupEditAction` uses.

Runtime contract v24 rolls the control envelope across the setup commands.
`detect --json`, `init --json`, and every `doctor --json` payload now carry a
`control` field holding the same `shipgate.agent_control/v1` object that
`verify`, `check`, and `agent control` emit, so one vocabulary answers "what may
I do next" for the whole adoption walk. Two things keep the setup family
distinguishable from the gate:

- **Setup names its own source.** These commands run before a release decision
  exists, so they report `decision_source: "setup"` and a `decision` from the
  closed setup vocabulary `setup_complete | setup_incomplete |
  setup_not_applicable`. The published schema enforces the pairing both ways: a
  setup source can only come from `detect`/`init`/`doctor`, and those operations
  can report no other source. `decision_source: "release_decision"` still means,
  and only means, `release_decision.decision`.
- **Setup authorizes nothing.** No setup command reads a diff, so every field of
  `permissions` is `false` on every setup envelope, no setup envelope binds an
  artifact or a `current_control_id`, and `control_state: "complete"` is
  unreachable for these operations in the schema itself. Setup routes; it never
  finishes a task.

`agent control` on a `scan` generation publishes **no** release verdict. `scan`
reaches one, but its pointer records no HEAD, no worktree overlay, and no input
set, so nothing about that verdict can be reconfirmed against the workspace as it
stands: editing the manifest, a `tools.json` it references, a policy pack, or a
baseline leaves the pointer reading cleanly. What the envelope carries instead is
`reason`, stating why there is no verdict — which is what keeps it
distinguishable from an envelope produced before any engine ran, and is the
ambiguity #323 set out to remove. Run `verify` for a verdict a reader can check.

The `AgentControl` union is **unchanged**, and `minimum_control_contract_version`
stays at `21`. That union is embedded by the verifier, the handoff, preflight,
the agent result, the boundary result, and verify-run, so widening it would widen
six durable published schemas under unchanged identifiers — and five of those
artifacts record no `contract_version`, so a consumer holding a stored payload
could not use the floor to tell which shape it has.

A setup step that needs a file changed is still *typed*, though: the envelope
publishes `next_action.kind: "edit"` with `path` and `expects`, as
`SetupEditAction` — declared on the envelope, which is stdout-only, and rejected
in both layers on any non-setup operation. Routing such a step as the command
that merely *checks* the edit was tried and is wrong: an envelope-only consumer
executing it re-ran `doctor` against an unchanged file forever.

What v24 widens is `shipgate.agent_control/v1` itself, which is emitted on
stdout and never written as an artifact: there are no stored envelopes to
disambiguate, and its new operations cannot appear in anything a v21 consumer
holds.

A human-owned manifest declaration is never published as a coding-agent edit.
When `shipgate.yaml` still holds an unresolved `declared_purpose`, policy, or
permission placeholder, the setup control state is `human_review_required` and
the action names the exact file, line, and field a person must fill in. The
command-specific `next_action` / `next_actions[]` fields are unchanged, and
remain supported.

Runtime contract v23 spells every emitted command for the invocation that
produced it. A run started with `python -m agents_shipgate` now proposes
`<sys.executable> -m agents_shipgate ...` instead of a console script its
environment may not have; a console-script run is unchanged. Actions with
`kind="command"` in `next_actions[]` also carry `executable[]` and `args[]` —
**the authoritative runnable form on every platform**; run them as
`[*executable, *args]` with no shell. The pair is computed from `command` and
cannot be supplied, so the two forms cannot disagree, and it is omitted (not
`null`) whenever the command has no faithful argv form. `command` itself is a
POSIX rendering for display and POSIX shells. When the rank-1 action is a
command, the legacy `next_action` string is that command verbatim. Set `AGENTS_SHIPGATE_CLI` to
name the entry point explicitly. Durable evidence artifacts (`report.json`,
`packet.*`) stay canonical: "same inputs, same report" outranks runnability
there, and process-entry spelling is not an input.

Runtime contract v22 publishes `shipgate.agent_control/v1`, the compact control
envelope. It is a **projection of the control state, not a second decision**:
every field is copied from a producer that already published it, and the
schema's validators only assert that the copies cannot contradict each other.
One object answers the whole routing question — tool execution status, the
release or boundary decision and which engine made it, the control state, the
six-way `permissions` vector, who acts next, the exact next action, and the
content-addressed path and hash of every artifact `current-control.json`
binds — not every file a run writes, and none at all from `check`, which
publishes no pointer. It is emitted by
`agents-shipgate verify --format control`, `agents-shipgate check --format
agent-control-json`, and `agents-shipgate agent control` (now its default
output; `--format pointer` returns the raw pointer). It publishes a size *budget*,
`agent_control_budget_bytes` (6144), that representative output meets and that
is pinned by tests; it is not a hard maximum, because a long required-reviewer
list or an unusually long exact command must never be truncated to hit a size
target. Free-text fields are capped at 400 UTF-8 bytes; commands, paths, hashes,
and reviewer names never are. It is never written to disk — the artifacts it
names stay where they are.

The envelope is a **discriminated union on `control_state`**, like the
`AgentControl` union it projects, so the published JSON Schema — not only
Pydantic — rejects a contradictory payload: `execution: "failed"` beside
`control_state: "complete"`, a coding-agent route on a stopping state, a
`review_publishable` that denies publication, or merge authority outside
`complete`.

Three separations are structural, not documentary. `execution` says whether the
tool ran; `decision` says what the gate decided; `permissions` says what the
agent may do. A failed run can never authorize completion, but a *succeeded* one
carries no implication at all.

Both entry points apply one currency test. `verify --format control` reads the
pointer it just published through the generation-safe protocol, validated
against the live workspace, and withholds authority when the workspace has moved
past what the run evaluated; the route comes from the verifier bytes captured
inside that read, so a pointer is never reported beside another generation's
decision. `artifacts[].path` is relative to the directory the command was
invoked from. `exit_code` reports the CI gate signal, which is
mode-dependent — in advisory mode a `blocked` decision still exits 0 — so
`permissions.merge` is the only field that answers "may I merge". The full
`verifier.json` remains the authoritative substrate and is unchanged;
`verify --json` still emits it.

Runtime contract v20 adds `agents-shipgate-reports/current-control.json`,
the one atomic entry point that says which control identity is current. It is a
pointer, not a second decision: it binds identities and hashes the receipt,
handoff, verifier, and report already published. Every run replaces it with a
non-terminal `unavailable` marker before touching any other artifact, and
publishes the terminal pointer atomically, last. Consumers must re-read it at
every boundary in `agent_refresh_triggers` — after any human or external-tool
action, after any worktree change, after any command returns, before enforcing a
cached `must_stop`, before commit/push/PR update, before merge, and before
declaring the task complete. A control state remembered from earlier in a
conversation never outranks the pointer, in either direction: it can neither
keep blocking after a newer complete run exists, nor authorize action after the
workspace moved.

Runtime contract v21 separates publish authority from merge authority.
`control.permissions` is a required object on every state with the exact
booleans `edit`, `commit`, `push`, `update_pr`, `merge`, `report_complete`,
fixed by the state and never set independently, and the new
`control.state: "review_publishable"` means "a human must approve the merge,
and the agent may still commit, push, and update the pull request to obtain
that review". `human_review_required` keeps its exact old meaning — nothing is
authorized — and is now reserved for results Shipgate cannot vouch for: a
`block` decision, a failed run, unreadable or unbindable diff input, an
undeclared surface with no discovery route, and preflight protected-surface
touches. `merge` and `report_complete` always equal `completion_allowed`, so
human review never becomes self-approvable.

Runtime contract v19 grades the LOCAL boundary stop: a `require_review`
violation set that is entirely low/medium risk projects
`control.state: "agent_action_required"` with the exact verify command, and
the review obligation is carried in the additive
`pending_review[]` field on the agent-boundary result instead of ending the
turn. Block actions, critical risk, gate-weakening rules
(`CODEX-AGENTS-SHIPGATE-REQUIREMENT-REMOVED`), unparseable content,
incomplete input, experimental surfaces, and every gate-governing trust-root
class (manifest, policy, ci_gate, shipgate_state) keep the human route; as of
v20 an evaluated `require_review` route is `review_publishable` and a `block`
route is `human_review_required`. PR-time `release_decision` semantics are
unchanged. It retains the v18 human-authorization overlay, the v17
content-addressed verification identity,
v16 typed policy-evidence, v15 host-neutral
boundary, v14 unambiguous `AgentControl`, and v13 root-reachable binding
contracts. v18 added a signed, externally rooted human-authorization overlay for
one exact post-review coding-agent action. Agents
switch on `control.state`; `decision` remains diagnostic and
`release_decision.decision` remains the release gate. Contract v14 requires
`completion_allowed == (state == "complete")` and
`must_stop == (state == "human_review_required")`. The report, packet,
verifier, verify-run, and handoff projections all bind to the same request
and decision IDs; their current versions are listed once under *Current
schema versions* above rather than restated here, where the list went stale. The terminal receipt hashes the complete
artifact set; see [Verification Identity and Reproduction](verification-reproducibility.md).
The runtime contract also exposes the local agent command spec:
`primary_commands{}`, `commands{}`, `default_paths{}`, `artifacts{}`,
`agent_read_order[]`, `verifier_read_order[]`, `merge_verdicts[]`,
`release_decisions[]`, `do_not_auto_assert[]`, `verifier_schema_version`,
`verify_run_schema_version`, `verification_plan_schema_version`,
`verification_unit_result_schema_version`,
`verification_artifact_manifest_schema_version`,
`verification_receipt_schema_version`,
`human_authorization_request_schema_version`,
`human_authorization_schema_version`,
`human_authorization_evaluation_schema_version`,
`human_authorization_trust_policy_schema_version`,
`human_authorization_trust_policy_default_path`,
`human_authorization_schema_path`, `agent_handoff_schema_version`,
`agent_handoff_schema_path`, `agent_handoff_artifact`,
`agent_boundary_result_schema_version`, the deprecated
`codex_boundary_result_schema_version`, `attestation_schema_version`,
`registry_schema_version`, `org_evidence_bundle_schema_version`,
`host_grants_inventory_schema_version`, `host_grants_baseline_schema_version`,
`host_grants_drift_schema_version`, `trigger_catalog_schema_version`,
`agent_interface_operations[]`,
`exit_code_policy`, `mcp_tools[]`, `minimum_control_contract_version`,
`agent_control_fields[]`, and `agent_control_states[]`. The legacy
`agent_result_*` fields are retained only for older protocol readers.
`primary_commands{}` is the prominent
entry surface and contains only `shipgate check`, `agents-shipgate verify`, and
`shipgate audit --host` flows; `commands{}` is compatibility/supporting metadata
and retains local verify commands for older consumers.
The short `shipgate verify` alias remains invokable for compatibility, but it is
not the promoted PR-gate spelling in `primary_commands{}`.
Contract v11 adds `action_effect` and `action_authority` to
`do_not_auto_assert[]`. They are reviewed human claims that can close semantic
evidence gaps; an agent may route the structured next action but must never
invent or auto-fill either declaration.
Downstream repos generated with
`init --agent-instructions=default` get the minimal local copy at
`.shipgate/agent-contract.json`.

- Latest release: `v0.15.0`
- In-tree runtime: `0.16.0` — see [pyproject.toml](../pyproject.toml)
- Runtime contract: `33` (minimum control contract: `21`)
- Current report schema: `0.43` — [`docs/report-schema.v0.43.json`](report-schema.v0.43.json)
- Current packet schema: `0.18` — [`docs/packet-schema.v0.18.json`](packet-schema.v0.18.json)
- Current shared agent result schema: `agent_result_v3` — [`docs/agent-result-schema.v3.json`](agent-result-schema.v3.json)
- Current verifier schema: `0.17` — [`docs/verifier-schema.v0.17.json`](verifier-schema.v0.17.json) (`0.16` and earlier stay frozen; `0.17` adds conditional instruction-edit routing)
- Current verify-run schema: `shipgate.verify_run/v5` — [`docs/verify-run-schema.v5.json`](verify-run-schema.v5.json)
- Current verification identity schemas: [`plan v1`](verification-plan-schema.v1.json), [`unit result v1`](verification-unit-result-schema.v1.json), [`artifact manifest v1`](verification-artifact-manifest-schema.v1.json), and [`terminal receipt v1`](verification-receipt-schema.v1.json)
- Current control pointer schema: `shipgate.current_control/v1` — [`docs/current-control-schema.v1.json`](current-control-schema.v1.json)
- Current agent control envelope schema: `shipgate.agent_control/v1` — [`docs/agent-control-schema.v1.json`](agent-control-schema.v1.json)
- Current human-authorization schemas: request, signed grant, verifier evaluation, and external trust policy v1 — [`docs/human-authorization-schema.v1.json`](human-authorization-schema.v1.json)
- Current agent handoff schema: `shipgate.agent_handoff/v9` — [`docs/agent-handoff-schema.v9.json`](agent-handoff-schema.v9.json)
- Current agent boundary result schema: `shipgate.agent_boundary_result/v2` — [`docs/agent-boundary-result-schema.v2.json`](agent-boundary-result-schema.v2.json)
- Frozen deprecated Codex projection: `shipgate.codex_boundary_result/v2` — [`docs/codex-boundary-result-schema.v2.json`](codex-boundary-result-schema.v2.json)
- Current preflight schema: `0.5` — [`docs/preflight-schema.v0.5.json`](preflight-schema.v0.5.json)
- Current downstream local agent contract schema: `10`
- Current capability standard: `0.5` — [`docs/capability-standard.md`](capability-standard.md)
- Current capability lock schema: `0.8` — [`docs/capability-lock-schema.v0.8.json`](capability-lock-schema.v0.8.json)
- Current capability lock diff schema: `0.9` — [`docs/capability-lock-diff-schema.v0.9.json`](capability-lock-diff-schema.v0.9.json)
- Current capability payload schema: `shipgate.capability_payload/v1` — [`docs/capability-payload-schema.v1.json`](capability-payload-schema.v1.json), specified in [`docs/capability-payload.md`](capability-payload.md)
- Current capability delta attestation: `shipgate.capability_delta_attestation/v1`, predicate type `https://threemoonslab.com/agents-shipgate/capability-delta/v1` — [`docs/capability-delta-attestation-schema.v1.json`](capability-delta-attestation-schema.v1.json), specified in [`docs/capability-delta-attestation.md`](capability-delta-attestation.md)
- Current attestation schema: `0.5` — [`docs/attestation-schema.v0.5.json`](attestation-schema.v0.5.json)
- Current registry schema: `0.4` — [`docs/registry-schema.v0.4.json`](registry-schema.v0.4.json)
- Current org evidence bundle schema: `shipgate.org_evidence_bundle/v2` — [`docs/org-evidence-bundle-schema.v2.json`](org-evidence-bundle-schema.v2.json)
- Current host-grants inventory, baseline, and drift schemas: `0.3` — [`inventory`](host-grants-inventory-schema.v0.3.json), [`baseline`](host-grants-baseline-schema.v0.3.json), [`drift`](host-grants-drift-schema.v0.3.json)
- Current trigger catalog schema: `0.4` — [`docs/triggers.json`](triggers.json)
- Current governance benchmark catalog schema: `0.2` — [`docs/governance-benchmark-catalog-schema.v0.2.json`](governance-benchmark-catalog-schema.v0.2.json)
- Current governance benchmark result schema: `0.2` — [`docs/governance-benchmark-result-schema.v0.2.json`](governance-benchmark-result-schema.v0.2.json)
- Frozen-reference report schemas: frozen [`v0.33`](report-schema.v0.33.json), frozen [`v0.32`](report-schema.v0.32.json), frozen [`v0.31`](report-schema.v0.31.json), frozen [`v0.30`](report-schema.v0.30.json), and older versions listed in [`docs/INDEX.md`](INDEX.md#reference)
- Frozen-reference packet schemas live in [`docs/INDEX.md`](INDEX.md#reference).
- Boundary v1, verifier v0.1–v0.5, verify-run v1/v2, handoff v1–v5, and preflight
  v0.1/v0.2 remain frozen references for legacy readers.
- Frozen experimental capability lock and governance benchmark result schemas live in [`docs/INDEX.md`](INDEX.md#reference).

## Two read entry points

Both start at `agents-shipgate-reports/current-control.json` (`agents-shipgate
agent control --workspace .`), which names the run that is current. Everything
below it describes *a* run; only the pointer says *which* run. A non-zero exit
from the reader means no control identity is current here and the caller holds
no authority — not that the previous answer still stands.

**The promoted read for a coding-agent control loop is one command.**
`agents-shipgate agent control --workspace .` runs the currency protocol and
returns the `shipgate.agent_control/v1` envelope, which already carries the
route the pointer deliberately omits. An agent that routes on `permissions` and
`next_action` from that one object never needs the artifact walk below; the walk
remains the contract for consumers that want the forensic detail, for CI, and
for anything reading the artifacts directly. A run can skip the second command
entirely with `agents-shipgate verify --format control`, which emits the same
envelope for the run it just performed.

The compact reader validates **every entry in `current-control.json.artifacts`**.
It does not follow the terminal receipt into optional artifacts absent from that
map. For example, a successful read does not validate `human-review-request.json`
merely because the bound receipt references it. Changed, missing, oversized, or
symlinked receipt-only files leave the compact read unchanged; those same
conditions on pointer-bound files make it refuse.

For API consumers, `read_current_control(..., capture=...)` returns selected
bytes from the same pass that validated every pointer entry. `capture` neither
reduces validation nor adds optional files: a requested unbound key is absent
from the returned `artifacts`. Consume these captured bytes instead of reopening
their paths. A consumer that needs a receipt-only artifact must also call
`load_validated_receipt_artifacts`, compare its returned receipt with the
pointer-captured `verification_receipt`, and consume the returned closure bytes.
The full loader rejects invalid optional files. It proves a coherent artifact
snapshot, not current workspace state or permission to act; retain the currency
checks below and revalidate before consequential actions. See the
[consumer boundary and resource bounds](verification-reproducibility.md#current-control-and-receipt-closure)
for the exact distinction. Standalone renderers such as `agent handoff` do not
implicitly perform this current-control protocol.

Byte consistency is not generation consistency. A pointer whose artifacts all
still hash correctly can describe a workspace that one commit has moved past, so
the reader compares the bound `workspace_identity` against the live repository —
repository, HEAD commit, and HEAD tree — and refuses on any drift. Completion
authority is never returned without that comparison: a reader that cannot
resolve the workspace reports it as unverified rather than passing.

When the decision named a base, that base is compared too. A decision about
`base...HEAD` is a decision about that range, and advancing the base — a merge,
or a fetch moving `origin/main` — can empty the range without touching HEAD or
the working tree, leaving every HEAD-based check satisfied while the evidence
underneath has gone. The pointer therefore carries `base_ref`,
`base_commit_sha`, and `merge_base_sha`, and the reader resolves the ref live.

Uncommitted work is checked according to what the decision actually covered:

- A **worktree** decision (`snapshot_kind: "worktree_overlay"`) is re-checked
  two ways. Every path it covered must still hash to the overlay it committed
  to, and no path *outside* that set may differ from HEAD now — anything outside
  it was identical to HEAD when the decision was made, so a live change the plan
  never recorded is evidence the decision never saw. That second test is a
  subset test, not equality: `plan.inputs.changed_paths` is the union of
  `base...HEAD` and the worktree, not the uncommitted set, so requiring equality
  would refuse a clean workspace the moment the run that produced it finished.
- A **committed-tree** decision (`snapshot_kind: "committed_tree"`) stops at
  HEAD, so any uncommitted change appearing afterwards invalidates it — in both
  directions. A stale `complete` must not authorize work the decision never
  covered, and a stale `human_review_required` must not keep enforcing a
  pre-change stop. Re-running the same archived `--head` verification cannot
  clear that, so the refusal routes to a worktree verification instead.

An overlay row carries content *and* the two metadata axes Git itself tracks:
entry kind and the executable bit. Content alone is not the capability —
flipping a tool script from `100755` to `100644` changes no bytes, and swapping
a regular file for a symlink to an identical in-repo file changes no bytes
either. Full mode is deliberately not recorded: it varies with umask and would
make the identity depend on noise Git does not track.

Given a current pointer, there are two correct "read first" paths; which one
applies depends on who is reading. They are not two decisions — they are two
entry points into the same one decision engine.

- **PR / controller flow** — an autonomous coding agent deciding *continue,
  repair, or stop*. Prefer
  validate `agents-shipgate-reports/verification-receipt.json`, then read
  `agents-shipgate-reports/agent-handoff.json` for the compact
  `shipgate.agent_handoff/v9` view: lead with `control.state`, then read
  `control.next_action`, `gate.merge_verdict`, and `reproducibility.run_id` for the
  content-addressed verify identity. `verifier.json` remains the authoritative
  controller substrate and `verify-run.json` remains the detailed run
  projection; finally
  confirm `report.json.release_decision.decision` for the release gate.
  `.well-known/agents-shipgate.json` → `agent_read_order` is the
  machine-readable cross-artifact order. `verifier_read_order` remains the
  intra-`verifier.json` field order.
- **Gate / CI flow** — deciding pass/fail, or any raw `report.json` consumer.
  Read `agents-shipgate-reports/report.json` → `release_decision.decision` (the
  next section). `.well-known` → `gating_signal` names this signal.

`merge_verdict` is a deterministic projection of `release_decision.decision`, so
the two can never disagree.

### Command-scoped artifact lifecycle

Choose the read path from the command that just completed, not merely from
filenames already present in the output directory:

- After standalone `scan`, `report.json.release_decision.decision` is
  authoritative. `scan` writes report, advisory scaffold, and configured
  packet formats; it does not produce a verifier handoff or terminal receipt.
- After `verify`, validate `verification-receipt.json`, then read
  `agent-handoff.json` and the supporting verifier artifacts in the order
  above. The receipt and handoff retain the content-addressed identity of that
  exact verify run.

`current-control.json` records which of those two just happened in its
`operation` field, so the choice does not have to be inferred from filenames at
all. Only an `operation: "verify"` pointer can carry `control.state:
"complete"`, and only when it also binds a `verification_receipt` whose
`request_id` and `decision_id` are the ones the pointer records — the assembler
accepts any `--out` name under its artifacts root, so an older canonical receipt
must not be mistaken for the one a run just closed. A `scan` or `preview`
pointer is structurally incapable of authorizing completion or merge, and each
binds only the artifacts it actually wrote: a `scan --format markdown` after a
verify does not claim that verifier's `report.json`. While a run is in flight the pointer reads
`lifecycle_state: "in_progress"` with `control.state: "unavailable"`,
`must_stop: true`, so an interrupted or crashed run leaves a directory that
denies cached control rather than one that still authorizes it. Consumers built
before the pointer existed fall back through
`current_control_fallback_read_order`; the pointer's absence is evidence of an
older producer, never permission.

When standalone `scan` replaces a report set in the same output directory, it
removes the complete prior verifier route and its identity support:
`verifier.json`, `agent-handoff.json`, `pr-comment.md`, `verify-run.json`,
`verification-plan.json`, `verification-input.diff`,
`verification-base-report.json`, `verification-unit-result.json`,
`verification-artifacts.json`, `verification-receipt.json`, and
`human-authorization.json`. Their absence is intentional: an older controller
substrate, route, or receipt must never appear to authorize or describe the
newer scan. `verify` calls the same scan pipeline internally and writes a fresh,
mutually consistent verifier artifact set afterward. Supporting commands that
only need an in-memory report, including `baseline save`, scan into an isolated
temporary directory so they preserve the current report and verifier evidence.

## Primary vs supporting surfaces

Primary gates are intentionally narrow. CI gates on
`report.json.release_decision.decision`. Coding agents handling committed PRs
read `agent-handoff.json.control.state` first, with
`verifier.json.control`, `execution`, `applicability`, and `merge_verdict` as the
authoritative detailed substrate. Everything else in the
verifier/report/packet family is supporting review evidence or a convenience
projection.

Treat legacy `agent_result_v1` / `agent-result.json` compatibility surfaces,
runtime trace/evidence fields, the Release Evidence Packet, `reviewer_summary`,
`verifier_summary`, `capability_review`, non-gating capability diff
projections, and `agents-shipgate skill ...` review output as
supporting/provisional surfaces. They may be useful for routing and review, but
they do not replace the gate above and must not introduce a second verdict.

`agents-shipgate preflight --workspace . --plan - --json` remains a supporting
proactive routing surface for coding agents before edits. It accepts a single
`PreflightPlanV1` object with `changed_files[]`, optional `diff_text`,
`capability_requests[]`, `host_permission_requests[]`, and
`context.{agent,task}`. The emitted `PreflightResultV5` reports protected
surfaces, forbidden shortcut actions, required evidence for proposed high-risk
capabilities, host-grant drift when a host baseline is present, deterministic
`signals[]`, `control`, `requires_verify`, `verification_command`,
`allowed_next_commands[]`, and `plan_summary`. A concrete, resolvable diff that
only appends valid built-in `tool_sources` rows may mark that manifest touch
`requires_human_review=false` and route the coding agent to verification. This
authorizes proposal authorship only: existing rows and all other manifest
values must be unchanged, authority-bearing fields and custom adapters are
excluded, and the resulting trust-root diff still requires human review. It is
not a second gate; it must never be read as passed or mergeable. The release
gate remains `release_decision.decision`.

## Read these first for release gating

In `agents-shipgate-reports/report.json`:

- `release_decision.decision` — `"blocked"` / `"review_required"` / `"insufficient_evidence"` / `"passed"`. Baseline-aware. **This is the gating signal.** Precedence is `blocked` → `review_required` (active high/critical named concern) → `insufficient_evidence` → `review_required` (known review concern) → `passed`. Starting in v0.29, `passed` means every in-scope action has complete, conflict-free static surface, effect, and authority evidence, all applicable controls were evaluated, and no policy condition requires review. It does not prove runtime behavior or enforcement. Any required semantic dimension that is unknown, inferred-only, protocol-defaulted, partial, conflicting, invalid, or incomplete prevents `passed`, even when every other action is healthy. Existing extraction thresholds remain: low-confidence tools at least `max(1, ceil(tool_count × 0.5))` or more than three source-loader warnings also degrade evidence. `insufficient_evidence` means the scan cannot confidently gate release from the available static evidence; it does not prove the agent is unsafe. Switch on the enum with a `review_required` fallback for unknown future values.
- `release_decision.blockers[]` — items that block release on this run.
- `release_decision.review_items[]` — items the human reviewer should look at; includes baseline-matched accepted debt.
- `release_decision.{static_analysis_only,runtime_behavior_verified,static_verdict_disclaimer}` (v0.29+) — the machine-readable verdict boundary. Emitted values are `true`, `false`, and the canonical static-verdict disclaimer respectively. Packet §1 mirrors them exactly. Preserve these fields in agent summaries; `passed` must never be rewritten as runtime verification or safety proof.
- `release_decision.{blockers,review_items}[].capability_refs` (v0.24+) — stable capability IDs copied from the originating finding when a policy or policy-pack rule matched a `CapabilityFactV1`. Empty for findings that are not capability-policy matches. This is audit metadata only; `release_decision.decision` remains the gate.
- `release_decision.{blockers,review_items}[].capability_trace_refs` (v0.25+) — stable local trace-evidence IDs copied from the originating finding when an existing trace/evidence check used declared local trace artifacts. Empty when no local trace row is relevant. This is audit metadata only; `release_decision.decision` remains the gate.
- `release_decision.evidence_coverage.semantic_coverage` (v0.29+) — `{total_actions, pass_eligible_actions, gap_count, review_concern_count, reason_counts}`. A non-zero semantic `gap_count` prevents `passed`; a non-zero `review_concern_count` prevents an automatic pass and routes known review concerns to human review — unscoped/ambient authority, and (v0.36+) `acknowledged_effect_override`, a declared effect a reviewer acknowledged as weaker than the evidence inferred for it. Read `reason_counts` for which; the count is of concerns, so one action can contribute more than one. Semantic gaps are not Findings and cannot be suppressed, baselined, severity-overridden, waived by `--no-heuristics`, or satisfied by `human_ack`.
- `release_decision.evidence_coverage.semantic_coverage.declaration_questions` (v0.37+) — the same action surface counted as a questionnaire: `{total, answered, open, open_by_dimension, open_questions[]}`. A *question* is one `(action, dimension)` a reviewed `action_surface.actions` row has to answer, and only `effect` and `authority` are counted — an action whose effect the scan established by itself (an OpenAPI method, an MCP annotation) was never asked and is not in `total`, and an inventory or `agent_bindings` declaration has no per-action counterfactual to score against. `answered` is exact rather than optimistic: it counts dimensions that gap when the same action is re-resolved *without* its declaration. `total == answered + open`, and `open_by_dimension` sums to `open`. `open_questions[]` is the answer order and joins to `evidence_gaps[].subject_id`. v0.38 ranks it by the ceiling of what an answer can establish: the actions nothing has bounded first — no effect evidence, a protocol default standing in for its absence, or only a heuristic reading the scan may not act on — then the actions a reviewed declaration or policy-eligible source evidence established, strongest-acting first, with `effect` before `authority` within one action. Position is not severity: the action at the top is the one *least* is known about. Nothing here gates; it is a projection of counts the decision already made, published so a coding agent (and the generated questionnaire) can report progress instead of a gap tally.
- `release_decision.evidence_coverage.policy_gap_count` and top-level `policy_evidence_gaps[]` (v0.33+) — policy applicability that is heuristic-only, mixed, unknown, or conflicting. These rows are outside Findings and cannot be suppressed, baselined, severity-overridden, acknowledged, or removed by `--no-heuristics`; any row prevents `passed`.
- `release_decision.evidence_coverage.identity_coverage` (v0.30+) — `{total_observations, canonical_tools, bound_tools, pass_eligible_tools, ambiguous_name_count, gap_count, reason_counts}`. Provider-scoped observations remain separate unless an exact reviewed `tool_identity.bindings[]` entry joins them. Any ambiguous selector, invalid binding, or conflicting identity prevents `passed`.
- `release_decision.evidence_coverage.evidence_gaps[]` (v0.26+;
  semantic kinds added v0.29) — one structured row per measurable gap:
  `{kind, subject, source_type, source_ref, policy_id, why, next_action}`.
  `policy_id` is optional and present on policy-applicability rows; it retains
  exact machine identity even when an engine-owned id is intentionally omitted
  from adopter prose. In addition to `low_confidence_tool` and
  `source_warning`, semantic rows include `incomplete_surface` for enumeration
  failure and `unattested_surface` when lower-confidence extraction lacks
  reviewed inventory attestation without an enumeration defect. Only an exact
  adapter `surface: enumerated` fact earns positive enumeration wording. Other
  semantic kinds include `missing_effect_evidence`, `inferred_effect_only`,
  `conflicting_effect_evidence`, `missing_authority_evidence`,
  `partial_authority_evidence`, `conflicting_authority_evidence`, and
  `invalid_semantic_annotation`.

  v0.36 adds `declaration_below_inferred_evidence`: the declared effect is
  weaker than evidence this scan inferred for the same action. The declaration
  still stands as the operative effect — heuristics never drive a verdict —
  but the action is not evidence-backed-pass until a reviewer raises the
  declared effect or adds `action_surface.actions[].override` with the
  `evidence` they checked and the `reason` it does not apply. An acknowledged
  override keeps the action pass-eligible and is reported as one semantic
  review concern, so the run can never read `passed`. Each acknowledgement is
  also emitted in
  `release_decision.evidence_coverage.semantic_coverage.acknowledged_overrides[]`
  naming the action, both readings, the hint source, any source evidence that
  agrees, and the reviewer's evidence and reason. The packet's §1 and the PR
  comment render it, and policy applicability consumes it, so applying an
  override reaches review rather than trading one gap for another.

  Semantic next actions use `declare_action_effect`,
  `declare_action_authority`, `declare_tool_inventory`,
  `provide_complete_inventory`, or `resolve_semantic_conflict`, include
  accepted values and exact source/manifest pointers, and are human-routed.
  v0.37 adds `next_action.observed_readings[]` on effect rows —
  `{effect, sources[], observed}` — so the row can be answered without opening
  `action_surface_facts`. Where those readings support one conservative answer,
  `next_action.declaration_template` carries it pre-filled instead of a
  `<REVIEW_REQUIRED>` blank. The value is a proposal, never an assertion: it
  comes from the closed `ActionEffect` vocabulary, is never weaker than any
  reading, and is offered only where something was observed. A protocol
  default standing in for absent evidence, or a heuristic `read`, keeps the
  blank. The placeholders carry `auto_apply=false` and
  `requires_human_review=true`; only a reviewed manifest edit makes one
  operative.

  v0.41 adds `next_action.authorable_by` (`coding_agent` | `human`, default
  `human`) — who may write the first draft. It is `coding_agent` only where the
  scan filled every blank in `declaration_template` and the gap is not one that
  asks a person to look again. `declaration_drift` therefore stays human-owned.
  An agent-authorable row carries `suggested_patch_kind: "declare_action"` and
  a `next_action.patch` exactly matching the template, split into the action
  selector and fields written; the schema rejects any other pairing.
  `target_path` is relative to `manifest_dir`. `auto_apply` stays false and
  `requires_human_review` stays true for every row, and the patch remains
  outside `apply-patches --kinds` by default; only the
  `confirm_declarations` route proposes it.

  v0.37 also re-routes `partial_authority_evidence`: it is raised when source
  authority evidence is ambiguous or incomplete, and persists whatever the
  manifest declares. Its action is `provide_source` with no declaration
  template, not a declaration that could not close the row, and it is excluded
  from `declaration_questions` for the same reason. Work the rows in order
  instead of guessing; Agents Shipgate never auto-asserts effect or authority.
- `loaded_policy_packs[].{source,sha256,sha256_status,owner}` (v0.27+) — policy-pack distribution and ownership metadata for organization audit. `sha256_status` is `"verified"` only when the manifest pin matched; otherwise it is `"unpinned"`. This is report metadata; normal pack matching and release gating still come from deterministic rules and `release_decision.decision`.
- `findings[].support` (v0.33+) — typed predicate support with status, effective confidence, policy/block eligibility, claim IDs, evidence bases, predicate rows, and `support_hash`. Rule confidence and `block: true` are ceilings/requests; they cannot upgrade the support. Baseline matching for supported findings requires the same support hash.
- `findings[].policy_routing` (v0.28+) — optional policy-pack owner, reviewers, and approval-routing metadata. This is non-enforcing reviewer/audit metadata, not `Finding.evidence`; it does not affect fingerprints, suppressions, baselines, `blocks_release`, or `release_decision`.
- `release_decision.fail_policy.would_fail_ci` — `true`/`false`. Matches what
  the CI process will exit with. For a semantic evidence gap, strict mode emits
  the consistent tuple `decision="insufficient_evidence"`,
  `would_fail_ci=true`, `exit_code=20`; advisory mode keeps exit `0` while
  preserving the same non-pass decision.
- `release_decision.reason` — one-sentence explanation suitable for a PR comment.
- `release_decision.contribution_rules[]` (v0.17+) — deterministic per-finding audit explaining how each `report.findings` entry was classified. Exactly one row per finding (including suppressed). In v0.33, `unsupported_evidence` records a finding that cannot contribute because its typed support is not policy-eligible. Reading the contribution rule is sufficient to predict the gate outcome without re-deriving the decision logic.
- `privacy_audit` (v0.18+) — confirms the default redaction pass ran before public artifacts were written. Read `enabled`, `rules_version`, `sensitive_field_inventory_version`, `redacted_occurrence_count`, `redacted_paths[]`, and `output_surfaces[]`. `redacted_paths[]` contains structural paths and counts only, never raw values or raw hashes.
- `reviewer_summary` (v0.20+) — deterministic projection of the reviewer lens surfaces and audit envelopes; the reviewer-side parallel to `agent_summary`. Read this block first when triaging a scan for a human reviewer. Carries `verdict` (mirrors `release_decision.decision`), `headline` (≤200 chars, PR-comment-friendly), per-lens activity counts (`tool_surface_changes`, `capability_misalignments`, `action_surface_changes`, `evidence_matrix_gaps`), per-audit-envelope counts (`severity_overrides_applied`, `severity_overrides_tier_crossed`, `privacy_redactions`, `baseline_integrity_issues`), and `first_recommended_surface: ReviewerSurfacePointer | None` — a deterministic pointer naming which lens/audit to open first (`{kind, name, path, why}` where `kind` ∈ `{release_decision, lens, audit, evidence_matrix}` and `name` ∈ `{tool_surface_diff, capability_intent_diff, action_surface_diff, evidence_matrix, policy_audit, privacy_audit, baseline_integrity, release_decision}`). Same inputs always produce the same output; this block cannot disagree with the underlying lens/audit data.
- `heuristics_filter` (v0.21+) — top-level audit envelope describing the `--no-heuristics` CLI filter pass. Always present, even when the flag is unset (`enabled: False` with zero counts), so the report shape is stable. Carries `enabled: bool`, `excluded_provenance_kinds: list[str]` (`["keyword_heuristic", "regex_heuristic"]`), `filtered_finding_count: int`, and `filtered_by_kind: dict[str, int]` (per-kind breakdown). When `enabled: True`, findings whose `provenance_kind` is in the excluded list have been marked `suppressed=True` with `suppression_reason="filtered by --no-heuristics"` BEFORE the release decision was built — they remain in `findings[]` for transparency but no longer gate release. The filter never un-suppresses a finding; manifest-driven suppression reasons are preserved when they overlap with the filter. Useful for security/GRC reviewers who want declared-only findings.
- `verifier_summary` (v0.22+) — top-level **composition** for one-fetch controller consumption (the AI-coding-workflow verifier surface). It derives **no independent verdict**: `verdict` mirrors `release_decision.decision` exactly (Principle: one decision engine). Carries `by_severity: dict[str,int]` and `by_reason_code: dict[str,int]` (active-finding histograms — the complete per-code map), `capability_delta_summary: {added, removed, broadened, narrowed}` (equal by construction to the `capability_change` member-list lengths), `protected_surface_touched: bool`, `policy_weakened: bool`, `human_ack_required: bool`, `human_ack_satisfied: bool`, and `top_reason_codes: list[{reason_code, count}]` — the ranked top-five highlight (severity desc → count desc → code asc; the full set stays in `by_reason_code`). This block cannot introduce a finding-independent blocker.

In `findings[]`, v0.24 adds capability-native policy evidence for built-in
policy checks and policy packs:

- `capability_refs: list[str]` — stable `CapabilityFactV1.id` values that
  matched the rule. It is emitted as an empty list for findings that are not
  capability-policy matches.
- `capability_policy_evidence | null` — optional typed audit metadata with the
  matched capability identity, effect, authority, controls, semantic hashes,
  matched predicates, and source provenance. It is explanatory only and is not
  included in finding fingerprint inputs.
- `policy_routing | null` — optional policy-pack routing metadata with
  `owner`, `reviewers`, and `approval.{required,teams,min_approvals,enforced}`.
  `approval.enforced` is always `false`; Shipgate validates declared team names
  but does not verify external approval systems or make release decisions from
  these fields.

Deterministic match and gating `finding.evidence` keys remain stable for legacy
readers. Policy-pack routing keys that used to live in `Finding.evidence` now
live in `policy_routing`; old baseline fingerprints are still matched during
baseline comparison. Policy matching is capability-native internally, but
policy-pack behavior, suppressions, severity overrides, baselines, SARIF,
Markdown, and GitHub Action outputs remain compatible.

In `findings[]`, v0.25 adds opt-in trace/provenance references for existing
trace/evidence checks:

- `capability_trace_refs: list[str]` — stable IDs from the top-level
  `capability_runtime_evidence` block. It is emitted as an empty list for
  findings that are not linked to a local trace row.
- `provenance_kind: "runtime_trace"` — used only for findings derived from
  declared local trace artifacts. It is not filtered by `--no-heuristics`.

The top-level `capability_runtime_evidence` block is a deterministic audit
projection over local trace artifacts declared in `openai_api.trace_samples`,
`google_adk.trace_samples`, `validation.evidence.approval_traces`, and
`validation.evidence.agent_traces`. It carries summary counts, matched and
unmatched `CapabilityTraceEvidenceV1` rows, source provenance, and notes. Trace
normalization keeps only allowlisted scalar fields and discards prompts,
messages, tool arguments, tool outputs, and arbitrary payload bodies. The block
is empty when no trace inputs are declared. It is not part of capability locks,
fingerprints, baselines, run IDs, or release gating.

The remaining v0.22 verifier blocks are reviewer-facing projections / declared inputs — none gates independently (`release_decision.decision` stays the only gate). They populate with real values only under `verify` mode (a `VerificationContext` from `agents-shipgate verify` or an equivalent scan context); a plain `scan` emits their stable empty shape:

- `capability_change` (v0.22+, semantic metadata v0.23+) — the diff-derived capability delta, grouped into `{enabled, added, removed, broadened, narrowed}` member lists over `action_surface_diff` / `tool_surface_diff`. Each `CapabilityChangeMember` carries `{id, direction, subject_kind, tool, action, scope, before_scope, after_scope, before_capability_id, after_capability_id, changed_hashes, semantic_direction, semantic_changes, risk_tags, release_impact, provenance_kind, confidence, rationale, related_finding_ids}`. `broadened` = more effective capability (wider scope, escalated effect, removed control); `narrowed` = less (removed scope, added control). `semantic_direction` explains the proven capability-level movement (`added | removed | broadened | narrowed | mixed | unknown | evidence_only`), and `semantic_changes[]` gives field-level reasons when a base action snapshot is available. `enabled: false` when no base diff is available. A member with an empty `tool` and `subject_kind: "scope"` whose `scope` reads `"<agent> -> MCP binding <slot> <axis>"` is an agent's **remote binding** — the endpoint, credential reference, transport or tool filter of a remote MCP server it mounts (#538). `tool` is empty because no remote leaf was enumerated and none is invented; `before_scope` / `after_scope` carry the two sides. Only the tool-filter axis claims a direction; for the others `broadened` is the block's documented opaque-direction bucket and the `rationale` says so, because a host name or an environment variable name establishes no privilege level.
- `protected_surface_changes` (v0.22+) — list of touched release trust roots, each `{path, kind, glob, related_finding_ids}`. Derived from the active `SHIP-VERIFY-*` findings, so every row's `related_finding_ids` resolves to a real `findings[]` entry and the rollup can never disagree with the gate. A row means "a protected file was touched"; purely-semantic weakenings with no file path stay in `findings[]` and surface via `verifier_summary` flags.
- `effective_policy` (v0.22+) — normalized (not text-diff) snapshot of the release-policy surface for base-vs-head weakening comparison: `{ci_mode, fail_on[], suppressed_check_ids[], waiver_scopes[], severity_overrides{}, baseline_integrity_mode, baseline_fingerprints[], ci_gate_present}`. Every list/dict is sorted for byte-stable output; derived purely from the manifest (plus accepted-debt fingerprints). It describes the policy the repository **declares**, not the policy this invocation runs under: `--ci-mode` / `--fail-on` move `ci_mode` / `fail_on` at the top level of the report but never here, so two runs of the same tree produce the same snapshot and a base-vs-head comparison stays repository-vs-repository.
- `human_ack` (v0.22+) — declared human-acknowledgement state, `{required, satisfied, acks[], outstanding[]}`. Within the static boundary, acknowledgement is **declared evidence only — never inferred** (human authority cannot be synthesized). A trust-root weakening (`SHIP-VERIFY-POLICY-WEAKENED`, `-POLICY-BASE-ABSENT`, `-CI-GATE-REMOVED`, `-BASELINE-OR-WAIVER-EXPANDED`) makes a surface `required`; it is `satisfied` only by a matching `human_ack` entry in `shipgate.yaml` (owner + reason + affected surface, optional expiry). `required == (acks-covering-required) + outstanding`. The acknowledgement section lives in `shipgate.yaml` — itself a trust root — so a coding agent cannot add its own ack without tripping `SHIP-VERIFY-TRUST-ROOT-TOUCHED`.

New `SHIP-VERIFY-*` reason codes (v0.22+, category `verify` — suppression-immune and floor-protected; emit only under `verify` mode): `SHIP-VERIFY-POLICY-WEAKENED` (base-vs-head policy weakened), `SHIP-VERIFY-POLICY-BASE-ABSENT` (0.16+; a policy trust root changed with no base snapshot to compare against — split out of `-POLICY-WEAKENED` so a first adoption no longer reports a weakening that could not have happened; evidence `kind` is `manifest_introduced` or `base_snapshot_unavailable`, and only the former reports `policy_weakened: false`), `SHIP-VERIFY-BASELINE-OR-WAIVER-EXPANDED` (suppression/waiver/baseline broadened), `SHIP-VERIFY-CI-GATE-REMOVED` (Shipgate CI workflow deleted), `SHIP-VERIFY-AGENT-INSTRUCTIONS-WEAKENED` (deprecated compatibility ID; no findings; supported structural changes remain covered by the shared trust-root comparison), `SHIP-VERIFY-TRIGGER-CATALOG-DRIFT` (trigger catalog changed). They are ordinary `Finding`s routed through `release_decision` — never a second verdict.

The action exposes these as outputs `decision`, `blocker_count`, `review_item_count`, `ci_would_fail` (v0.8+).
For verifier-cycle PR workflows it also exposes additive outputs
`should_run`, `trigger_action`, `trigger_rule_ids`, `verifier_verdict`,
`verifier_json`, `verify_run_json`, `run_id`, `merge_verdict`,
`can_merge_without_human`, `agent_control_state`, `agent_control_reason`,
`agent_controller_must_stop`,
`agent_controller_stop_reason`, `agent_controller_completion_allowed`,
`trust_root_touched`,
`policy_weakened`, `capability_changes_added`,
`capability_changes_modified`, and `capability_changes_removed`. These are
review and routing aids only. `trust_root_touched` and `policy_weakened`
mirror `verifier_summary`; the capability counts mirror
`capability_change` (`modified` is `broadened + narrowed`). Keep using
`decision` as the release-gating output and `agent_control_state` as the
coding-agent operational output.

When the action is asked to emit organization-governance artifacts, it also
exposes `attestation_json`, `org_evidence_bundle_json`, `host_grants_json`, and
`org_status_json` as artifact paths. These are ingestion and audit surfaces for
platform teams; they never create a second verdict.

For ongoing PR workflows, prefer:

```bash
agents-shipgate verify --workspace . --config shipgate.yaml \
  --base origin/main --head HEAD --ci-mode advisory --format json
```

`verify` writes `verifier.json`, `verify-run.json`, `agent-handoff.json`, and
`pr-comment.md` alongside the head scan artifacts. `agent-handoff.json` is the
compact coding-agent projection over the verifier, verify-run, and report
artifacts; it does not gate independently. After a successful head scan it also writes the head static
capability lock to `agents-shipgate-reports/capabilities.lock.json`. When
`--base` is provided and the base scan can be materialized, verify writes
`agents-shipgate-reports/base.capabilities.lock.json`,
`agents-shipgate-reports/capability-lock-diff.json` and
`agents-shipgate-reports/capability-lock-diff.md`. The packet artifact is
intentionally `packet.json` only; use `scan` for manifest-driven packet
Markdown/HTML/PDF rendering. Read
`verifier.json.base_status` to understand whether base diff enrichment ran;
do not use it as a release verdict. The release gate is still
`report.json.release_decision.decision`. `verify` never fetches, so CI callers
must make the base ref available before invocation. Supplying `--head` makes
verify scan an isolated archive of that ref; omitting it scans the checked-out
workspace. If an explicit `--base` ref or PR diff cannot be inspected, verify
skips a head-only scan; `verifier.json.merge_verdict` is `unknown` and the
command exits 2.

`agents-shipgate verify --preview --json` is a lightweight relevance check — no
scan, no manifest required, exits 0 for every workspace it evaluates. A
`--workspace` that does not exist is not a workspace it evaluates: it is
refused as an invocation error (`config_error`, exit 2) before any directory
is created, on preview and on every other command that takes the option. It
emits a `verifier.json` with
`mode: "preview"`, `execution: "not_run"`,
`applicability: "not_evaluated"`, and
`control.state: "agent_action_required"`. `control.next_action` carries the
next recommended action: an exact
`init --workspace <workspace> --write --json`
command for unconfigured repos, or an exact `verify` command for configured
repos using the supplied workspace/config/base/head/out arguments. Use it as the
first touch before a full scan. To evaluate just the run/skip trigger, run
`agents-shipgate trigger --base origin/main --head HEAD --json`.

`agents-shipgate verify` and `verify --preview` also write
`agents-shipgate-reports/verify-run.json` whenever the output directory can be
created. It carries `schema_version: "shipgate.verify_run/v5"`, the exact
verification plan, executor, unit-result IDs, decision ID, outcome projection,
and artifact references. `request_id` is the content-addressed run identity;
the deprecated `run_id` remains for one compatibility cycle as its exact alias,
never as a separately derived identity. It has no wall-clock timestamp and is
not a second gate.

`agents-shipgate-reports/agent-handoff.json` carries
`schema_version: "shipgate.agent_handoff/v9"` and top-level sections
`gate`, `control`, `fix_task`, `blocked_by[]`,
`remediation_plan[]`, `capability_review`, `authorization`, `reproducibility`,
and `artifacts`.
`gate.decision` mirrors `release_decision.decision`; `gate.merge_verdict`
mirrors `verifier.json.merge_verdict`; and
`gate.{static_analysis_only,runtime_behavior_verified,static_verdict_disclaimer}`
mirrors the report/verifier static-only boundary. The values are locked to
`true`, `false`, and the canonical disclaimer. `control` is byte-identical to
the verifier/verify-run control object, and `can_merge_without_human` is true
only for a verified `passed` result or a completed deterministic
`not_applicable` skip. `authorization` is the byte-equivalent verifier
evaluation; the handoff cannot grant a command independently. Re-render it
from existing artifacts with:

```bash
agents-shipgate agent handoff --from agents-shipgate-reports/verifier.json --json
```

In `agents-shipgate-reports/verifier.json`, read the fields below (full
schema [`docs/verifier-schema.v0.17.json`](verifier-schema.v0.17.json)). **Lead
with `control.state`.** Every release and merge field below is a mirror or
deterministic projection of `report.json`; the authorization evaluation is an
operational overlay and cannot change those fields.
`release_decision.decision` remains the gate.

- `control` — the discriminated `complete | agent_action_required |
  review_publishable | human_review_required` operational projection. Its
  variant fixes `completion_allowed`, `must_stop`, `permissions`,
  `human_review`, and the actor/action shape; generated schemas enforce the
  variants with `oneOf`. Only a new verifier artifact can clear a pending
  control obligation.
- `execution` — `"not_run" | "succeeded" | "skipped" | "failed"`.
- `diff_status` — whether the compared change set was read at all.
  `completeness` is `"complete"` / `"partial"` / `"unavailable"`; `reason` is
  `null` only when complete, and otherwise `not_attempted`, `refs_missing`,
  `merge_base_missing` (shallow checkout — deepening restores the merge base),
  `unrelated_histories` (no common ancestor exists; no fetch can create one),
  `objects_missing`, `metadata_limit_exceeded`, `body_limit_exceeded`,
  `git_timeout`, or `git_failed`. `remediation` names
  the repair and `fetch_repairable` says whether fetching can perform it.
  **Only `"complete"` licenses reading a negative `trigger` result**; anything
  else means the diff was not read, which is never evidence that a PR is
  unrelated to agent capabilities. `null` means a pre-v0.7 artifact — unknown,
  not complete.
- `trigger` — the run/skip evaluation. Read `evaluation_status` first; two of
  its three values withhold the verdict, and in both `should_run` /
  `run_shipgate` / `skip` / `skip_reason` are `null`. Never read `null` as
  `false`. The embedded `next_action` is diagnostic only: it preserves the
  evaluated kind but carries no command, sets `authoritative: false`, and
  points at `control.next_action`, which is the route to follow. Embedded
  `matched_rules[]` carry no commands either. The standalone
  `agents-shipgate trigger --json` command retains its own actionable
  `next_action`.
  - `"not_evaluated"` — the diff could not be read. Repair the input through
    `control.next_action`; `skip_reason` is never `"no_match"` for inputs that
    were not fully read.
  - `"unclassified"` — the diff *was* read in full and no rule classified some
    or all of the changed files. That is a fact about the catalog, not about
    the PR, so the skip is withheld and `control.next_action` routes forward
    to the scan. `surface_exclusions.entries[]` lists the unclassified files.

  `stop_conditions_fired` is the raw block result; `stop_conditions_terminal`
  says whether it decided. A matched `run_shipgate`/`force_run` rule overrides
  a fired stop, because a capability match in the diff is evidence the
  whole-workspace negative did not account for. `"evaluated"` on an
  incomplete `diff_status` is not a contradiction: only *skip* verdicts are
  withheld, so a `should_run: true` reached from evidence that did not depend
  on the missing bytes is authoritative and must not be overridden. Read
  `matched_rules` to see what carried it — a `force_run` match rests on the
  manifest, not on the diff.
- `merge_verdict` — `"mergeable"` / `"human_review_required"` /
  `"insufficient_evidence"` / `"blocked"` / `"unknown"`. Deterministic projection
  of `release_decision.decision` (`passed`→`mergeable`,
  `review_required`→`human_review_required`,
  `insufficient_evidence`→`insufficient_evidence`, `blocked`→`blocked`, missing
  decision→`unknown`). It cannot disagree with the gate; switch on the enum with
  an `unknown`/`human_review_required` fallback for future values.
- `static_analysis_only`, `runtime_behavior_verified`, and
  `static_verdict_disclaimer` — locked to `true`, `false`, and the canonical
  non-runtime disclaimer. When a release decision is embedded, construction
  rejects any disagreement between these top-level values and the decision.
- `applicability` — `"not_evaluated"` / `"verified"` /
  `"not_applicable"` / `"failed"`.
  Disambiguates a `mergeable` verdict: `"verified"` means Shipgate evaluated the
  change and produced a release decision; `"not_applicable"` means the head scan
  was skipped (nothing to gate — do **not** read this as "verified safe");
  `"failed"` means the scan could not complete. Orthogonal to `merge_verdict`;
  additive and locked to `"verified"` whenever a `release_decision` is present.
- `can_merge_without_human` — `bool`.
- `decision` — mirror of `release_decision.decision` (or `null` when no scan ran).
- `headline` — single-sentence, PR-comment-friendly summary (or `null`). Every
  completed blocked run names the deterministically selected worst blocker as
  `Most severe: <title>.`, ordered by severity, check id, then title. A run
  with no blocker adds no cause clause.
- `authorization` — the
  `shipgate.human_authorization_evaluation/v1` result. Only `accepted` can
  expose a command, and that command must exactly match both
  `control.next_action.command` and the sole entry in
  `control.allowed_next_commands`. `rejected`, `not_requested`, and
  `not_applicable` carry no command authority.
- `control.human_review` and `control.next_action` are the serialized route for
  the current verifier state; when authorization is accepted, the signed
  evaluation is the provenance for the exact coding-agent next action.
- `AgentController`, `VerifierNextAction`, and `VerifierHumanReview` remain
  importable only as deprecated v0.1/v0.2 reader models. Verifier v0.6 does not
  emit or invoke the retired `build_agent_controller` projector.
- `fix_task` — `{actor, safe_to_attempt, instructions[], allowed_repairs[],
  forbidden_repairs[], forbidden_shortcuts[], verification_command, patches[]}` or `null`.
  This is the deterministic repair boundary: `actor: coding_agent` with
  `safe_to_attempt: true` means the agent may attempt only the listed mechanical
  `allowed_repairs[]` and rerun `verification_command`; `actor: human` means the
  agent must not invent action effect, action authority, approval,
  idempotency, policy, waiver, baseline, or trust-root evidence to make the
  gate pass. `forbidden_repairs[]` explicitly
  lists reward-hacking moves such as suppressing findings, lowering severity,
  expanding baselines/waivers, weakening CI or policy, adding human ack, or
  inventing action-effect/action-authority/approval/idempotency evidence.
  `patches[]` (v0.13+) carries
  `{finding_id, check_id, patch}` rows with the
  machine-applicable suggested patches for the gating findings — populated
  only when verify ran with `--suggest-patches` and the task routes to the
  coding agent; repair aids, never gate inputs.
- `trust_root_touched` — `bool`; `true` when the PR changed a release-gate trust
  root (`shipgate.yaml`, the Shipgate CI workflow, `AGENTS.md`/`CLAUDE.md`,
  policy packs, prompts, baselines, waivers, etc.). Backed by the
  `SHIP-VERIFY-TRUST-ROOT-TOUCHED` check.
- `capability_review` — reviewer-facing projection of `capability_change` with
  `{trust_root_touched, policy_weakened, policy_weakening_proven,
  capability_changes_added, capability_changes_removed,
  capability_changes_modified, top_changes[]}`. Gate on `policy_weakened` (the
  fail-closed flag, raised even when no base policy existed to compare
  against); say the policy was weakened only when `policy_weakening_proven`
  (0.16+) is also true — that one means a base-vs-head comparison actually ran.
  `top_changes[]` carries the highest-signal capability deltas with
  `{id, change_type, change_bucket, subject_kind, subject, impact, rationale,
  source_path, source_start_line, related_finding_ids}`. `impact` mirrors the
  gate (`blocks_release`, `review_required`, `insufficient_evidence`, or
  informational values) and never introduces a finding-independent blocker.
- `mode` — `"advisory"` / `"strict"` / `"skipped"` / `"preview"`.

### Trusted human authorization for one exact command

Authorization changes operational routing, never the static release verdict.
The flow is deliberately two-pass:

1. Run `agents-shipgate verify --no-plugins` and validate the resulting
   terminal receipt. Authorization requires the plan's exact effective plugin
   mode to be false; the protected executor never loads third-party plugin or
   adapter entry points.
   `agents-shipgate authorization request --receipt <receipt>
   --artifacts-root <root> --destination-ref <full-ref>
   --expected-lease-oid <oid> --out <request>` constructs the unsigned
   `shipgate.human_authorization_request/v1` from that receipt's current
   request, subject, decision, source receipt/artifact-set/engine/executor and
   tree identities, the complete ordered
   review set, and one typed Git-push operation. This command creates a
   challenge, not authority.
2. The host authenticates the human and signs the canonical request with an
   Ed25519 key kept outside coding-agent reach. Agents Shipgate supplies no
   private key and no command that signs or approves a request. The v1 trust
   policy must be stored outside the evaluated workspace and protected from
   writes by the agent. On POSIX, Agents Shipgate reads it only from the OS
   account home's fixed path
   `~/.config/agents-shipgate/human-authorization-trust-policy.json`; `HOME`
   and `XDG_CONFIG_HOME` do not redirect that lookup.
3. Rerun `agents-shipgate verify --no-plugins --authorization
   <external-grant>`. The
   verifier recomputes the current identities and validates the signature,
   principal, repository scope, TTL, request, subject, trees, decision, full
   review set, and operation before publishing any command authority.

The only v1 operation is an exact force-with-lease Git push. It binds the exact
evaluated commit, a canonical credential-free HTTPS destination whose
repository identity equals the verified repository, a full destination
`refs/heads/...` ref, and the expected remote OID. A synthetic PR merge receipt
cannot authorize pushing a different parent commit. Authorization is eligible
only when execution
succeeded and the release decision is `review_required`. An accepted grant
changes `control.state` from `human_review_required` to
`agent_action_required` for that exact command, while all release facts remain
unchanged: `release_decision.decision="review_required"`,
`merge_verdict="human_review_required"`, `can_merge_without_human=false`, and
`completion_allowed=false`. The coding agent may perform only the serialized
guarded `agents-shipgate authorization execute` command. That consumer
revalidates the current receipt, trust root, clock, repository, and commit and
isolates Git configuration and hooks before issuing the internal typed push;
the raw Git command is never operational authority. The agent must rerun
verification afterward.

The signer must authenticate the source closure: content addressing is
integrity, not provenance. It must rerun verification in a trusted worker or
verify trusted-CI attestation over the bound source receipt/artifact-set IDs.
The request exposes the evaluated base commit and merge base, and the source
commit transitively binds its full parent graph. The signer must review that
complete ancestry and reachable history rather than relying only on the final
tree diff. Execution enforces a 512 MiB graph-pack ceiling and a 120-second
process timeout; the host broker should impose tighter deployment quotas. The
compressed pack ceiling does not bound expanded-object indexing memory or CPU,
so production brokers need cgroup, container, or equivalent host resource
limits.
Execution also requires a host-protected broker with a sanitized environment,
external trust policy, interpreter, entire virtual environment and
`site-packages` tree (including startup `.pth` files), dependencies,
credentials, and separately installed Agents Shipgate distribution. Same-UID
file permissions alone are insufficient, and an
editable install rooted in the authorized workspace is ineligible. If the host
cannot enforce those boundaries, authorization remains disabled. The guarded
executor is POSIX-only in v1 and authorization remains disabled on Windows. V1 is
push-only and does not authorize the coding agent to apply reviewed protected
patches.

Malformed, untrusted, expired, not-yet-valid, incomplete-review-set,
wrong-tree, wrong-request, wrong-ref, or wrong-lease grants fail closed with
zero allowed commands. Plain JSON in the repository, a PR comment, or
conversation-level approval is not equivalent to a signed grant. This release
defines the protocol and verifier consumer; it does not claim a current Codex,
Claude Code, or other coding-host UI signing integration. Such a host adapter
must be implemented separately. A grant replayed after the remote ref advances
cannot overwrite that ref: Git enforces the signed command's explicit expected
lease OID.

`verifier.json` also carries `trigger`, `base_status`, `head_status`, `base_ref`,
`head_ref`, `changed_files`, `base_notes`, the embedded `release_decision`, and an
`artifacts` map. When present, `artifacts.capability_lock_json`,
`artifacts.base_capability_lock_json`,
`artifacts.capability_lock_diff_json`, and
`artifacts.capability_lock_diff_markdown` are review artifacts only; they do not
change the gate. The matching GitHub Action outputs are `agent_control_state`,
`agent_control_reason`, `merge_verdict`, `can_merge_without_human`, and the
compatibility mirrors `agent_controller_must_stop`,
`agent_controller_stop_reason`, `agent_controller_completion_allowed`,
`trust_root_touched`, and
`capability_changes_{added,modified,removed}` (the original `decision`,
`blocker_count`, `review_item_count`, `ci_would_fail` outputs are preserved). See
[STABILITY.md §Verify Orchestrator](../STABILITY.md#verify-orchestrator) for the
authoritative contract.

The default Action PR comment style for the verifier-cycle minor is
`capability-review`: exactly two reviewer sections, a human summary and a
fenced JSON agent instruction block. The human summary leads with
`merge_verdict`, `can_merge_without_human`, capability delta, next actor, and
artifact links, including the semantic capability-lock diff summary when a base
lock is available. The agent block carries `control` and `fix_task` for
coding-agent routing. Existing adopters that need the v1
findings-oriented comment during migration can set `pr_comment_style: findings`
for one minor release cycle.

The GitHub Action emits source-backed GitHub Actions job annotations by default
for active blockers and review items. `check_annotations: "false"` disables the
projection; `check_annotation_limit` caps the number emitted. The helper also
writes `agents-shipgate-reports/check-annotations.json` for audit/debug.

`verify` writes non-gating capability artifacts when static extraction succeeds:
`agents-shipgate-reports/capabilities.lock.json` for head, and when a base ref
is available, `base.capabilities.lock.json` plus `capability-lock-diff.json`.
These artifacts are review/integration surfaces only and cannot introduce a
second verdict.

## Read this for local boundary control

`shipgate check --agent <codex|claude-code|cursor> --workspace . --format
agent-boundary-json` is the local static multi-host boundary command. The
`--agent` value is caller identity, never a coverage selector. The command emits
exactly one stdout JSON object using
`schema_version: "shipgate.agent_boundary_result/v2"` and the schema in
[`agent-boundary-result-schema.v2.json`](agent-boundary-result-schema.v2.json).
The old `codex-boundary-json` spelling remains a deprecated `0.16.x`
compatibility projection of the same assessment.

Read `input_coverage`, `host_coverage[]`, `affected_hosts[]`, `policies[]`,
`issues[]`, `pending_review[]`, and `excluded_scopes[]` before relying on the
result. `complete`
means complete only within the declared static input scope; it is not proof of
session grants, runtime enforcement, or tool behavior. `pending_review[]` is
non-empty only alongside `agent_action_required`: those are review obligations
the graded mapping carried forward instead of stopping the turn, and an agent
must name them when summarizing the change. The detailed matrix is
[`host-boundary-support.md`](host-boundary-support.md).

Coding agents switch on `control.state`, then follow `control.next_action` and
`control.allowed_next_commands`. `decision` is diagnostic only. A pending
verification obligation produces `agent_action_required`; it can never coexist
with `completion_allowed=true`. An evaluated human route produces
`review_publishable`, `must_stop=false`, a human next action, and
`permissions` that authorize publishing but not merging. An unsafe or
unbindable one produces `human_review_required`, `must_stop=true`, and
permissions that authorize nothing.
Do not derive control from Markdown, PR comments, natural language, or a
conversation-level acknowledgement. Only a new verifier artifact can clear
the obligation. Do not confuse this local boundary result with
`agents-shipgate verify`: verify writes
`agent-handoff.json`, `verifier.json`, and `verify-run.json`, and
`report.json` remains the full CI/reviewer substrate.

## Read these for release review

`agents-shipgate contract --json` exposes `manual_review_signals[]` as the
installed CLI's stable list of report/packet fields to inspect for human review
work. `findings[].provenance_kind` is included there as a filter/review signal
only; it never changes the release decision, severity, fingerprints, baselines,
or CI exit behavior.

The runtime contract also exposes stable non-gating integration fields:
`agent_handoff_schema_version`, `agent_handoff_schema_path`,
`agent_handoff_artifact`, `agent_interface_operations[]`, `exit_code_policy`,
`mcp_tools[]`,
`capability_lock_schema_version`, `capability_lock_diff_schema_version`,
`capability_payload_schema_version`, `capability_payload_schema_path`,
`capability_delta_attestation_schema_version`,
`capability_delta_attestation_schema_path`,
`capability_delta_predicate_type`, `capability_delta_attestation_artifact`,
`capability_standard_version`,
`governance_benchmark_catalog_schema_version`,
`governance_benchmark_result_schema_version`, and
`external_integration_surfaces[]`. These advertise capability lock/diff and
benchmark artifacts for integrations and research. They do not change the gate:
`release_decision.decision` remains the only release decision signal.

The capability/intent diff fields (v0.9+), used by reviewers to spot misalignment between declared agent intent and actual tool surface:

- `capability_facts[]` — every capability surfaced from the tool inventory. In v0.29 each newly emitted fact carries `semantic_assessment`, the normalized effect/authority claims, issues, conservative effect, and pass-eligibility state consumed by the gate.
- `declared_intentions[]` — what the manifest says the agent is supposed to do.
- `misalignments[]` — where capabilities exceed (or fall short of) declared intent.
- `release_consequence` — capability-aware roll-up of the release decision.
- `suggested_scenarios[]` — dynamic-validation scenarios derived from misalignments and findings.

The Action Surface Diff fields (v0.16+), reviewer-facing PR/release delta:

- `action_surface_facts.actions[]` — deterministic snapshot of the current agent action surface: action id, operation, effect, normalized risk tags, scopes, approval policy, safeguards, evidence, hashes, and (v0.29+) the same `semantic_assessment` projected onto the corresponding capability fact. `semantic_assessment.conservative_effect`, `action.effect`, and the capability effect must agree.
- `action_surface_diff.{enabled, base, summary, added, removed, modified, notes}` — what changed vs. a base report or v0.4 baseline. Policy findings generated from this diff can set `findings[].blocks_release=true` and appear in `release_decision.blockers`.
- `findings[].blocks_release` and `release_decision.{blockers,review_items}[].blocks_release` — explicit release-policy blockers from Action Surface Diff policies and policy-pack rules with `block: true`. Advisory CI may still exit 0; strict CI exits nonzero when an active unbaselined release blocker is present.

The tool-surface diff fields (v0.10+), lower-level explanatory data:

- `tool_surface_facts.{tools, scopes, controls, policies}` — current static facts about the tool surface.
- `tool_surface_diff.{enabled, base, summary, tools, high_risk_effects, scopes, controls, metadata_changes, policy_drift, finding_deltas, notes}` — what changed vs. a base ref. Disabled diffs render as `enabled: false` with a `notes` reason.

Source provenance fields on `findings[].source` (v0.11+), additive and optional:

- `path`, `start_line`, `end_line`, `start_column`, `pointer` — manifest-relative file path, 1-based line/column, and RFC 6901 JSON pointer for the offending tool. Populated for OpenAPI, MCP, OpenAI tool artifacts, and Anthropic tool artifacts when the source is YAML. JSON inputs carry `path` and `pointer` but no line in v0.11.

Per-finding `agent_action` enum (v0.12+), deterministic projection — read this **first** when deciding what to do with a finding so you don't have to synthesize an action from `patches`/`autofix_safe`/`requires_human_review`/`suggested_patch_kind`:

- `auto_apply` — `apply-patches --confidence high` will resolve cleanly. Every patch is non-manual and high-confidence.
- `propose_patch_for_review` — at least one non-manual patch is attached and machine-applicable, but the full patch set is not auto-safe. Two shapes land here: (a) every non-manual patch is medium- or low-confidence, and (b) a high-confidence non-manual patch sits alongside one or more `ManualPatch` siblings (the non-manual is safe to apply, but the manual instructions still need a human). In both cases the agent should ask the user before `--apply` and surface any manual instructions verbatim.
- `escalate_to_human` — no machine-applicable patch. Either every patch is `ManualPatch`, or `patches` is empty/absent and the check requires human review.
- `suppress_with_reason` — reserved for future check classes that explicitly mark themselves as suppressible. Not emitted by the v0.12 deterministic projection; the schema accepts it so callers can extend.
- `informational` — no action required (suppressed finding or non-actionable advisory).

Top-level `agent_summary` block (v0.12+), one-fetch summary shaped for direct agent consumption — read this when you want the headline numbers without traversing arrays:

- `verdict` — mirrors `release_decision.decision`.
- `headline` — single-sentence verdict + counts; suitable for a PR comment lead. The headline uses `needs_human_review` (action-driven) for "require human review" wording, so a `review_required` verdict with only auto-applicable findings reads honestly as "auto-applicable; none require human input" rather than falsely claiming N findings need review.
- `blocker_count` — mirrors `len(release_decision.blockers)`.
- `review_item_count` — mirrors `len(release_decision.review_items)`; **severity-driven** (medium-and-up severity findings that aren't blockers, plus baseline-matched accepted debt). Use this when reporting release-review debt to the human reviewer.
- `auto_appliable_patches` — number of active findings with `agent_action == "auto_apply"`.
- `needs_human_review` — **action-driven**: number of active findings with `agent_action ∈ {"escalate_to_human", "propose_patch_for_review"}`. Both kinds need explicit human attention before any change applies — full escalations have no machine path, and proposed patches ship at medium/low confidence and require an explicit `--apply` after the user confirms. Use this when reasoning about what work an agent must do.
- **`review_item_count` and `needs_human_review` track different populations and can diverge.** A medium-severity stale-suppression finding lands in `release_decision.review_items` (severity rule) but its `agent_action` is `auto_apply` (high-confidence patch attached), so it's counted in `review_item_count` and `auto_appliable_patches` but **not** in `needs_human_review`.
- `first_recommended_action` — `{kind, command|null, why}`; deterministic next step. `kind: "command"` carries an actual CLI invocation; `kind: "info"` is a "surface this to the user" hint with no command. The agent_summary block is a deterministic projection — same inputs, same output, no agent-side aggregation needed.
- **Evidence-gap actionability (v0.16+).** One **selected gap** feeds every short-form surface that names one: the first `release_decision.evidence_coverage.evidence_gaps[]` row that is *addressable*, falling back to the first row when none is. `Improve evidence:` (CLI and step summary, printed only on an `insufficient_evidence` verdict) always renders the selected gap's action.

  **A row is addressable when it names a visible target or carries a publishable command.** `next_action.path` and `next_action.command` are independently nullable, and either one alone is enough:
  - *Visible target* — the path contains at least one character that actually renders. Whitespace, control characters, and Unicode Default_Ignorable code points (U+200B ZWSP, U+200E/U+202E bidi marks, U+FE0F VS16, U+034F CGJ, …) render as nothing, so a path made only of those names no surface and is not addressable.
  - *Publishable command* — the command is safe to run **exactly as authored**. A command containing any control, bidi, or invisible code point, or any whitespace other than U+0020, is suppressed entirely rather than cleaned up: deleting a zero-width character from `r␣m -rf` would author a different program. Only leading/trailing U+0020 is trimmed, which cannot change `argv[0]`.

  Two guarantees, scoped to exactly what holds:
  - **Alignment, on `insufficient_evidence` with an addressable gap.** When the verdict is `insufficient_evidence` *and* at least one gap is addressable, `release_decision.reason`, `Improve evidence:`, and `first_recommended_action.why` name the **same** gap and the same target (or, for a command-only row, the same command). The reason leads with that gap and reports the source-warning / low-confidence counts as `Context:`.
  - **No false dead end, on every verdict.** The phrase *"no machine-applicable fix is available"* is never emitted in `first_recommended_action.why` while any gap is addressable. When you do see it, no gap names a surface to open or a command to run, and the next step really is a human gathering evidence.

  Outside that first case the three surfaces answer different questions, by design — do **not** read alignment into them:
  - `insufficient_evidence` with **no** addressable gap: `reason` keeps the `Evidence coverage below threshold (…)` wording and `first_recommended_action` routes to gathering deeper sources, while `Improve evidence:` still renders the first gap's `expects`. That line is a remediation hint, not a restatement of the reason.
  - `review_required`: `reason` is severity/findings-driven and never names a gap. Whether `first_recommended_action` names one depends on which branch of the action picker fires, and **auto-apply wins on sub-threshold evidence**: when the scan carries auto-applicable patches and evidence is *not* below the `insufficient_evidence` threshold, the action is the `apply-patches` command even if an addressable gap exists (the gap is still called out in the `why`). `first_recommended_action` names the selected gap on the evidence-first branches only — evidence below the threshold, or evidence-driven review with no findings to walk. Read `evidence_coverage.evidence_gaps[]` directly if you need the gap on this verdict; do not infer it from the action.

  **Rendering never rewrites what it renders.** Values reaching these one-line surfaces are repository-derived — a gap subject is a tool name, a policy pack authors `expects`, a semantic gap's `path` embeds a tool name — and they are made line-safe without being altered otherwise. Characters that could break a line or reorder it (controls, U+2028/U+2029, bidi marks) become a visible `<U+XXXX>` escape; **nothing is deleted**. Identity-bearing invisibles survive, so `agents/👩‍💻.yaml` and identifiers carrying ZWNJ are named as they actually are. Paths and commands are additionally never whitespace-folded — `configs/foo␣␣bar.yaml` keeps both spaces, and `python -c 'print("a␣␣b")'` stays the program that was written — while prose (subjects, `why`/`expects`, loader warnings) does fold whitespace, because there a stray newline is better read as a space. An affordance is published only when it exists: a suppressed or absent command produces no `Run:` line and a `null` repair command, and accepted values with nothing visible in them are dropped.

  **Authority comes from the action's own fields, never from its `kind` or its path.** Every evidence-gap row published today is `requires_human_review: true` and `auto_apply: false` — including the `provide_source` row that regenerates a stale `--diff-from` comparison base, for which `verify` emits `fix_task.actor: "human"` and `safe_to_attempt: false`. Since v0.41 the one row that carries a machine-applicable patch says so in its own fields too: `authorable_by: "coding_agent"` with `suggested_patch_kind: "declare_action"`. That is still not a licence to apply it unasked — it is applied by the `confirm_declarations` route that proposes it, and the human merge it needs is unchanged. A `command` on such a row tells a human exactly what to run; it does not make the row agent-owned. A coding agent acts mechanically only where `fix_task.actor == "coding_agent"` and `safe_to_attempt` is true, and then only within `allowed_repairs[]`. Separately, `first_recommended_action.kind` stays `"info"` on the evidence-first actions — a statement about the summary projection, not a claim that no gap row ever carries a command, and not a promise about every `review_required` action (the auto-apply branch returns `kind: "command"`).

Codex plugin surface block (v0.13+), explanatory only — never a release-gate
input by itself:

- `codex_plugin_surface.{plugins, marketplaces, skills, apps, mcp_server_stubs, hook_stubs, mcp_inventory_files, component_path_issues, warnings}` — local static plugin package and marketplace facts.
- Only explicit MCP inventory tools from `codex_plugins.mcp_tool_inventories` appear in `tool_inventory[]`; apps, hooks, skills, and MCP server declarations stay in `codex_plugin_surface`.

Per-finding `provenance_kind` enum (v0.15+), additive classification — read this when you want to filter findings by the kind of rule that fired, independent of `confidence` (sureness):

- `static_declaration` — declared metadata: manifest, MCP export, OpenAPI schema, ADK YAML agent config, LangChain/CrewAI inventory JSON. High-trust structural facts.
- `ast_extraction` — Tool parsed from user Python source by a framework extractor (LangChain function/structured tools, CrewAI function/class tools, ADK Python toolsets). Subject to extraction errors; agents that distrust AST quality may filter these as a class.
- `keyword_heuristic` — matched a keyword list (broad-scope tokens, read-only/approval prompt terms, free-text parameter names). Higher false-positive risk than declarative facts.
- `regex_heuristic` — matched a regex (secret-like values in descriptions, prompt-injection patterns). Highest false-positive risk; pair with the recommendation before acting.
- `policy_pack` — emitted by an external policy pack rule after its predicates have authoritative typed support. Rule confidence can lower, but never raise, evidence confidence.
- `runtime_trace` — derived from declared local trace artifacts. Audit evidence only; never filtered by `--no-heuristics`.

Provenance generally follows the rule's own trigger (e.g., a rule that checks for a declared manifest field is `static_declaration` even when the underlying Tool was AST-extracted). For framework checks that fire across both AST and declarative tool sources (ADK's per-tool checks against `google_adk_function` AND `google_adk_config` tools), the label tracks the underlying tool's source. Third-party plugin checks that don't yet set the field land at `static_declaration` by default — pre-v0.15 plugins continue to validate against the v0.15 wire schema. Use `findings[].source.type` for the precise underlying tool source.

To filter operationally, use:

```bash
agents-shipgate findings --from agents-shipgate-reports/report.json \
  --provenance-kind keyword_heuristic,regex_heuristic --json
```

The command reads active findings by default; add `--include-suppressed` when a
reviewer needs suppressed entries in the same provenance summary.

For reviewer-shaped output, also read the **Release Evidence Packet** at
`agents-shipgate-reports/packet.{md,json,html}` (and `packet.pdf` when the
`[pdf]` extras are installed). The packet is a supporting/provisional reviewer
projection, not a second gate. Packet outputs are redacted by the same default
privacy layer as the report. The packet has fixed reviewer sections governed by
[`docs/packet-schema.v0.18.json`](packet-schema.v0.18.json) — see
[STABILITY.md §Release Evidence Packet](../STABILITY.md#release-evidence-packet-v018).
Packet schema `0.9` carries the report's evidence-backed semantic coverage and
gap remediation contract. Packet §1 also mirrors
`static_analysis_only=true`, `runtime_behavior_verified=false`, and
`static_verdict_disclaimer` from the report release decision. Frozen packet
schema `0.7` added capability-linked
trace summary and trace refs under `human_in_the_loop`; frozen schema `0.6`
preserved the v0.5
`action_surface_diff` section and added two independent additive extensions:

- `evidence_matrix` (PR #104) — a compact packet-only review aid
  derived from public `report.json` fields. The matrix never contributes
  to `release_decision`, CI exit behavior, severity, suppression,
  baseline matching, or `agent_summary`; its blocker and review-item
  cells are copied from `release_decision`.
- `ReleaseDecisionItem.source` and `ReleaseDecisionItem.policy_evidence_source`
  (PR #103) — packet §1 / §2 re-renders carry the same dual-source
  provenance that `Finding.source` / `Finding.policy_evidence_source`
  expose in the report.

It preserves every v0.5 field
(`human_in_the_loop.runtime_control_disclaimer`,
`human_in_the_loop.source_provenance[]`, `action_surface_diff`). The
`release_decision.verdict` label includes `INSUFFICIENT EVIDENCE` when
the report decision is insufficient evidence.

## Don't use for new gating

- `summary.status` — preserved for v0.7 callers, **baseline-blind**. A baseline-matched critical flips this to `release_blockers_detected` even though `release_decision.decision` correctly classifies it as `review_required`. New consumers should not gate on `summary.status`. See [STABILITY.md §`release_decision.decision` vs `summary.status`](../STABILITY.md#release_decisiondecision-vs-summarystatus).

## Per-finding contextual explanation (v0.12+)

For prose summaries of a single finding (PR comments, chat replies, commit messages), use:

```bash
agents-shipgate explain-finding <FINGERPRINT> \
    --from agents-shipgate-reports/report.json --json
```

The payload is the full `Finding` shape (every field on `findings[]` in `report.json`, including `source`, `patches`, `confidence`, `agent_id`, etc.) overlaid with three derived fields:

- `metadata` — full `CheckMetadata` for the check_id (rationale, fires_when, evidence_fields, docs_url, `mvp_tier`) when the check is in the catalog; null for unknown ids (third-party plugins, future checks). `mvp_tier` is display/triage metadata only and never affects gating.
- `explanation` — a deterministic 3–5 sentence prose summary suitable for direct quotation. Names the affected tool, the severity, the recommended fix, and an action-aware closing sentence keyed to `agent_action`. Same inputs always produce the same output.
- `source_report` — **absolute** path (always; relative `--from` values are resolved before serialization) to the report file the explanation was sourced from; round-trippable for caching and audit.

`explain-finding` requires `report_schema_version >= 0.12` because the action-aware explanation depends on per-finding `agent_action`. Pre-v0.12 reports are rejected with `input_parse_error` and a `next_action` pointing at the canonical scan command. The Pydantic `ReadinessReport` model is intentionally looser than this command's contract (so test fixtures can construct minimal findings); the version gate is what enforces v0.12 semantics on emitted reports.

Companion prompt: [`prompts/explain-finding-to-user.md`](../prompts/explain-finding-to-user.md). Use it when you need to translate a finding for a human who has never read the Shipgate docs. Keep `agents-shipgate explain <CHECK_ID>` for static catalog metadata (no specific finding); use `explain-finding` whenever you have a fingerprint and want the evidence-tied prose.

## Authoritative references

- [STABILITY.md](../STABILITY.md) — full alpha stability contract. Source of truth for everything above.
- [AGENTS.md](../AGENTS.md) — agent-facing instructions: install, run, single-turn flow, error semantics.
- [`docs/report-schema.v0.43.json`](report-schema.v0.43.json) — machine-validatable JSON Schema for the current report.
- [`docs/privacy.md`](privacy.md) and [`docs/report-sensitive-fields.json`](report-sensitive-fields.json) — default redaction behavior and sensitive-field inventory.
- [`docs/packet-schema.v0.18.json`](packet-schema.v0.18.json) — machine-validatable JSON Schema for the current packet.
- [`docs/checks.json`](checks.json) — check catalog, including `mvp_tier` for MVP/readiness triage.

## See also

- [`report-reading-for-agents.md`](report-reading-for-agents.md) — reader's primer that walks the JSON in the order a new consumer should read it; complements this field index.
- [`agent-autofix-boundary.md`](agent-autofix-boundary.md) — what an agent may assert mechanically vs. what must defer to a human reviewer when surfacing findings from `report.json`.


<!-- ===== source: docs/checks.md ===== -->

# Check Catalog

Agents Shipgate checks are deterministic static checks. They do not certify safety, run agents, call tools, call LLMs, or verify runtime routing.

## Severity Contract

- `critical`: strict CI exits `20` unless the finding is explicitly suppressed
  with a reason (suppression-immune checks remain active).
- `high`: requires human review but does not fail CI by default.
- `medium`: review during release hardening.
- `low` and `info`: informational.

Among Findings, only unsuppressed `critical` findings block strict mode by
default. Independently, report v0.29 semantic `insufficient_evidence` also
exits `20` in strict mode; semantic gaps are not Findings and cannot be
suppressed. Suppressed Findings remain in JSON with `suppressed: true` and are
excluded from active severity counts.

## Evidence Coverage

- `static`: all enumerated tools came from high-confidence static sources.
- `mixed`: at least one enumerated tool came from lower-confidence enrichment, such as SDK AST extraction.

Suppressions do not change evidence coverage.

## Baselines

v0.2 adds local baseline gating. `agents-shipgate baseline save` writes active,
unsuppressed findings to `.agents-shipgate/baseline.json`. A later
`agents-shipgate scan --baseline .agents-shipgate/baseline.json --ci-mode strict`
marks findings as `matched` or `new` and fails only on new findings that match
the active fail policy. Resolved baseline findings are counted in the report
baseline summary and do not fail CI.

## Checks

| Check ID | Severity | Meaning |
| --- | --- | --- |
| `SHIP-INVENTORY-NOT-ENUMERABLE` | high | No tool surface could be enumerated from the manifest inputs. |
| `SHIP-INVENTORY-WILDCARD-TOOLS` | high | A source exposes wildcard/all tools instead of an explicit allowlist. |
| `SHIP-INVENTORY-TOOL-SURFACE-TOO-LARGE` | medium | The normalized tool count exceeds the MVP review threshold. |
| `SHIP-DOC-MISSING-DESCRIPTION` | medium | A tool has no description or a description too short for reliable review. |
| `SHIP-DOC-INJECTION-RISK` | medium/high | A tool description contains instruction-override style language. High only when multiple patterns match on a write/high-risk tool. |
| `SHIP-DOC-SECRET-IN-DESCRIPTION` | medium/high | A tool description contains a secret-like token or credential value. High only when multiple patterns match on a write/high-risk tool. |
| `SHIP-SCHEMA-BROAD-FREE-TEXT` | high | A write/action-like tool accepts broad `action`, `body`, `command`, `updates`, or similar free-form input. |
| `SHIP-SCHEMA-MISSING-BOUNDS` | high | A risky numeric parameter such as `amount`, `count`, or `quantity` lacks a maximum. |
| `SHIP-SCHEMA-FREEFORM-OUTPUT` | medium | A tool returns free-form string output that may later be placed in model context. |
| `SHIP-AUTH-MISSING-SCOPE` | high | A write-like tool has no declared auth scope metadata. |
| `SHIP-AUTH-MANIFEST-BROAD-SCOPE` | high | The manifest declares broad scopes such as `*`, `admin`, or `service:*`. |
| `SHIP-AUTH-TOOL-BROAD-SCOPE` | high | A tool declares broad scopes such as `*`, `admin`, or `service:*`. |
| `SHIP-AUTH-SCOPE-COVERAGE-MISSING` | high | A tool requires scopes that are not covered by `permissions.scopes`. |
| `SHIP-SCOPE-TOOL-OUTSIDE-PURPOSE` | high | A write-capable tool contradicts a read-only declared purpose. |
| `SHIP-SCOPE-PROHIBITED-TOOL-PRESENT` | high | A tool appears to overlap with a manifest `prohibited_actions` entry. |
| `SHIP-POLICY-APPROVAL-MISSING` | critical | A high-risk tool lacks a manifest approval policy. |
| `SHIP-POLICY-CONFIRMATION-MISSING` | high | A destructive, external-write, or customer-communication tool lacks a confirmation policy. |
| `SHIP-ACTION-UNDECLARED` | high | A loaded tool lacks explicit action-surface metadata when explicit actions are required. |
| `SHIP-ACTION-POLICY-VIOLATION` | high | A user-declared action-surface policy, or a selected control pack's obligation, is not satisfied. |
| `SHIP-ACTION-FINANCIAL-WRITE-CONTROL-MISSING` | critical | A newly added financial write action lacks approval, audit, or idempotency controls. |
| `SHIP-ACTION-DESTRUCTIVE-ROLLBACK-MISSING` | critical | A newly added destructive action lacks approval or rollback controls. |
| `SHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING` | high | A newly added external communication action lacks audit evidence. |
| `SHIP-ACTION-WILDCARD-SCOPE` | critical | An action declares or expands into a wildcard/admin-like scope. |
| `SHIP-ACTION-EFFECT-ESCALATED` | critical | An action effect escalated compared with the base surface. |
| `SHIP-ACTION-EFFECT-DOWNGRADE-DECLARED` | high | An action declaration weakens the effect inferred from the loaded tool surface. |
| `SHIP-ACTION-CONTROL-DOWNGRADE` | high | An action declaration weakens an inherited approval or safeguard control. |
| `SHIP-ACTION-APPROVAL-REMOVED` | critical | An existing action approval policy was removed. |
| `SHIP-ACTION-SAFEGUARD-REMOVED` | high | An existing action safeguard was removed. |
| `SHIP-EVIDENCE-APPROVAL-TRACE-MISSING` | high | Local HITL approval trace evidence is missing or incomplete for an approval-required tool. |
| `SHIP-EVIDENCE-OVERRIDE-REASON-MISSING` | high | Local HITL override reason evidence is missing or incomplete. |
| `SHIP-EVIDENCE-HIGH-RISK-EXCLUSION-MISSING` | high | Local high-risk auto-approval exclusion evidence is missing or incomplete. |
| `SHIP-EVIDENCE-HITL-PROMOTION-CRITERIA-MISSING` | high | Local HITL promotion criteria evidence is missing or incomplete. |
| `SHIP-SIDEFX-IDEMPOTENCY-MISSING` | critical/high | A risky write tool lacks idempotency evidence. Critical only when retry behavior is known. |
| `SHIP-API-FUNCTION-SCHEMA-STRICTNESS` | high/medium | An OpenAI API function schema is missing strictness, required fields, or bounded risky fields. |
| `SHIP-API-STRUCTURED-OUTPUT-READINESS` | high/medium | An OpenAI API response format is missing or too broad for downstream decisions. |
| `SHIP-API-PROMPT-TOOL-SCOPE-MISMATCH` | high/medium | Prompt language contradicts the enabled OpenAI API tool surface or lacks approval/confirmation instructions. |
| `SHIP-API-RETRY-POLICY-MISSING` | medium | High-risk OpenAI API tools are enabled without retry policy metadata. |
| `SHIP-API-TIMEOUT-MISSING` | medium | High-risk OpenAI API tools are enabled without timeout metadata. |
| `SHIP-API-TEST-CASES-MISSING` | medium | High-risk OpenAI API tools are enabled without declared test cases. |
| `SHIP-API-TOOL-OUTPUT-SCHEMA-MISSING` | medium | A high-risk OpenAI API tool lacks success/failure output modeling. |
| `SHIP-API-RETRY-WITHOUT-IDEMPOTENCY` | high | A risky OpenAI API write tool may be retried without idempotency evidence. |
| `SHIP-API-TRACE-APPROVAL-MISSING` | medium | A trace sample shows a policy-controlled tool call without approval. |
| `SHIP-API-TRACE-CONFIRMATION-MISSING` | medium | A trace sample shows a policy-controlled tool call without confirmation. |
| `SHIP-API-OPERATIONAL-READINESS` | medium | Deprecated v0.3 compatibility alias for the v0.4 atomic OpenAI API operational readiness checks. |
| `SHIP-ADK-DYNAMIC-TOOLSET-NOT-ENUMERABLE` | high | A Google ADK toolset cannot be statically enumerated and no explicit inventory is declared. |
| `SHIP-ADK-MCP-TOOLSET-UNFILTERED` | high/medium | A Google ADK `McpToolset` has no static `tool_filter`. |
| `SHIP-ADK-FUNCTION-TOOL-METADATA-MISSING` | medium | A Google ADK function/config tool lacks static description or parameter metadata. |
| `SHIP-ADK-LONGRUNNING-CONTRACT-MISSING` | high | A Google ADK long-running tool lacks operation-id and status/progress contract evidence. |
| `SHIP-ADK-GUARDRAIL-EVIDENCE-MISSING` | high | High-risk Google ADK tools lack callback/plugin or policy guardrail evidence. |
| `SHIP-ADK-EVAL-COVERAGE-MISSING` | medium | Production-like Google ADK inputs are present without declared eval files. |
| `SHIP-LANGCHAIN-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE` | high | A LangChain/LangGraph tool surface cannot be statically enumerated and no explicit inventory is declared. |
| `SHIP-LANGCHAIN-FUNCTION-TOOL-METADATA-MISSING` | medium | A LangChain/LangGraph function tool lacks static description or parameter metadata. |
| `SHIP-CREWAI-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE` | high | A CrewAI tool surface cannot be statically enumerated and no explicit inventory is declared. |
| `SHIP-CREWAI-FUNCTION-TOOL-METADATA-MISSING` | medium | A CrewAI function/class tool lacks static description or parameter metadata. |
| `SHIP-CODEX-PLUGIN-METADATA-MISSING` | medium | A Codex plugin package has incomplete or ambiguous identity metadata. |
| `SHIP-CODEX-PLUGIN-COMPONENT-PATH-MISSING` | high | A declared Codex plugin component path is missing or outside the package/workspace. |
| `SHIP-CODEX-PLUGIN-MARKETPLACE-POLICY-MISSING` | medium | A Codex plugin marketplace entry lacks installation/authentication policy metadata. |
| `SHIP-CODEX-PLUGIN-MCP-SERVER-NOT-ENUMERABLE` | high | A Codex plugin MCP server is declared without a local enumerable tool inventory. |
| `SHIP-CODEX-PLUGIN-APP-SURFACE-NOT-ENUMERABLE` | medium | A Codex plugin connector app surface is not statically enumerable from local metadata. |
| `SHIP-CODEX-PLUGIN-SKILL-METADATA-MISSING` | medium | A Codex plugin skill lacks unique name/description frontmatter. |
| `SHIP-CODEX-BOUNDARY-CONFIG-PARSE-FAILED` | medium | Codex project configuration could not be parsed. |
| `SHIP-CODEX-BOUNDARY-UNKNOWN-PERMISSION-KEY` | medium | Codex permissions contain an unknown high-risk key. |
| `SHIP-CODEX-BOUNDARY-NETWORK-WILDCARD` | high | Codex network permissions allow a wildcard domain. |
| `SHIP-CODEX-BOUNDARY-NETWORK-EXPANDED` | high | Codex network access expanded. |
| `SHIP-CODEX-BOUNDARY-DANGER-FULL-ACCESS` | critical | Codex full-access sandbox is selected. |
| `SHIP-CODEX-BOUNDARY-MCP-AUTO-APPROVE-WRITE` | critical | Codex auto-approves a write or destructive MCP/app tool. |
| `SHIP-CODEX-BOUNDARY-MCP-AUTO-APPROVE-UNKNOWN` | high | Codex auto-approves an MCP server whose tool surface is not statically enumerable. |
| `SHIP-CODEX-BOUNDARY-APP-AUTO-APPROVE` | high | Codex app connector tool approval changed to approve. |
| `SHIP-CODEX-BOUNDARY-AGENTS-SHIPGATE-REQUIREMENT-REMOVED` | medium | Deprecated; historical ID, no current findings. |
| `SHIP-CODEX-BOUNDARY-CI-GATE-REMOVED` | critical | Shipgate GitHub Action no longer invokes the gate. |
| `SHIP-CODEX-BOUNDARY-POLICY-WEAKENED` | critical | Codex boundary policy was weakened. |
| `SHIP-CODEX-BOUNDARY-HOOK-COMMAND-CHANGED` | high | A Codex executable hook changed. |
| `SHIP-CODEX-BOUNDARY-SKILL-COMMAND-CHANGED` | medium | A Codex skill gained command-bearing instructions. |
| `SHIP-AGENT-BOUNDARY-PROTECTED-SURFACE-UNCLASSIFIED` | medium | A protected coding-agent surface lacks a safe static classification. |
| `SHIP-AGENT-BOUNDARY-EXPERIMENTAL-SURFACE-CHANGED` | high | An experimental coding-agent boundary surface changed. |
| `SHIP-AGENT-BOUNDARY-STATIC-REQUIREMENTS-CHANGED` | high | Static host requirements changed. |
| `SHIP-AGENT-BOUNDARY-INPUT-INCOMPLETE` | medium | Boundary input could not be evaluated completely. |
| `SHIP-HOST-BOUNDARY-CONFIG-PARSE-FAILED` | medium | A coding-agent host configuration file could not be parsed. |
| `SHIP-HOST-BOUNDARY-MCP-SERVER-ADDED` | high | A new MCP server was declared for the coding-agent host. |
| `SHIP-HOST-BOUNDARY-MCP-SERVER-CHANGED` | high | An existing MCP server declaration changed its command, URL, args, or env keys. |
| `SHIP-HOST-BOUNDARY-PERMISSION-WILDCARD-ALLOW` | critical | A Claude Code allow rule grants a wildcard tool surface. |
| `SHIP-HOST-BOUNDARY-PERMISSION-ALLOW-EXPANDED` | high | The Claude Code permission allowlist expanded. |
| `SHIP-HOST-BOUNDARY-PERMISSION-DENY-REMOVED` | high | A Claude Code permission deny rule was removed. |
| `SHIP-HOST-BOUNDARY-HOOK-CHANGED` | high | Claude Code hooks changed. |
| `SHIP-HOST-BOUNDARY-WORKFLOW-WRITE-ALL` | critical | A GitHub workflow grants write-all permissions. |
| `SHIP-HOST-BOUNDARY-WORKFLOW-PERMISSIONS-EXPANDED` | high | GitHub workflow permissions expanded. |
| `SHIP-HOST-BOUNDARY-PULL-REQUEST-TARGET-ADDED` | critical | A GitHub workflow gained a pull_request_target trigger. |
| `LINT-SPEC-002` | high | A skill has invalid YAML frontmatter. |
| `LINT-SPEC-003` | high | A skill is missing required `name` frontmatter. |
| `LINT-SPEC-004` | high | A skill is missing required `description` frontmatter. |
| `LINT-DESC-001` | high | A skill description is too vague to route reliably. |
| `LINT-DESC-003` | medium | A skill description is overbroad and may false-trigger. |
| `LINT-BODY-001` | medium | A skill body lacks a step-by-step procedure. |
| `LINT-BODY-003` | medium | A skill body lacks an output contract. |
| `LINT-BODY-004` | medium | A skill body lacks verification criteria. |
| `LINT-SCRIPT-001` | medium | A skill script lacks documented `--help` usage. |
| `LINT-SCRIPT-004` | medium | A stateful skill script lacks dry-run support. |
| `SEC-PI-001` | critical | A skill artifact contains instruction override language. |
| `SEC-PI-003` | high | A skill artifact tells the agent to hide behavior. |
| `SEC-SECRET-001` | critical | A skill artifact contains a hardcoded secret-like value. |
| `SEC-SECRET-003` | high | A skill artifact instructs credential or secret-file access. |
| `SEC-SCRIPT-001` | critical | Remote content is piped to a shell or interpreter. |
| `SEC-SCRIPT-002` | critical | A destructive or stateful command lacks guardrails. |
| `SEC-REMOTE-001` | medium | A skill fetches remote instruction content. |
| `SEC-REMOTE-002` | critical | Remote content is fetched and executed. |
| `SEC-TOOL-001` | high | A skill pre-approves shell or bash without justification. |
| `SEC-FLOW-004` | medium | A skill lacks data and instruction separation guidance. |
| `SEC-PROV-001` | high | A third-party skill lacks provenance metadata. |
| `SEC-MISMATCH-001` | high | A skill declares read-only behavior but bundled scripts mutate state. |
| `SHIP-N8N-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE` | high | An n8n tool surface uses runtime, unresolved, wildcard, or uninventoried custom exposure. |
| `SHIP-N8N-MCP-CLIENT-TOOLSET-UNFILTERED` | high/medium | An n8n MCP Client Tool exposes `All` or `All Except` tools without an explicit inventory. |
| `SHIP-N8N-AI-TOOL-METADATA-MISSING` | medium | An n8n AI-exposed tool lacks static description or parameter metadata. |
| `SHIP-N8N-CREDENTIAL-EVIDENCE-MISSING` | high | Production-like n8n workflows reference credentials without declared credential stubs. |
| `SHIP-N8N-EVAL-COVERAGE-MISSING` | medium | Production-like n8n workflows are present without declared eval files. |
| `SHIP-N8N-SECRET-IN-WORKFLOW-PARAMETER` | high | n8n workflow JSON contains a secret-like value; evidence is redacted. |
| `SHIP-CONDUCTOR-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE` | high | A Conductor OSS workflow uses a dynamic or unresolved tool-capability surface. |
| `SHIP-MANIFEST-STALE-SUPPRESSION` | medium | A suppression references a missing check ID or missing tool. |
| `SHIP-MANIFEST-STALE-POLICY` | medium | An approval, confirmation, or idempotency policy references a missing tool. |
| `SHIP-MANIFEST-STALE-RISK-OVERRIDE` | medium | A risk override references a missing tool. |
| `SHIP-MANIFEST-HIGH-RISK-OWNER-MISSING` | high | A high-risk production or production-like tool lacks owner metadata. |
| `SHIP-MANIFEST-UNUSED-SCOPE` | medium/high | `permissions.scopes` contains a scope unused by any loaded tool; broad unused scopes are high. |
| `SHIP-VERIFY-LOCAL-REVIEW-PROVISIONAL` | medium | Verification uses an ephemeral local-review manifest; the result is provisional and cannot carry release authority. |
| `SHIP-VERIFY-TRUST-ROOT-TOUCHED` | medium | A PR changed a release trust-root file; emitted only when a verification context (changed files) is supplied. |
| `SHIP-VERIFY-POLICY-WEAKENED` | high | Base-vs-head effective policy weakened (CI mode downgraded, fail-on loosened, a severity override lowered across a tier, or the control pack moved to one that requires less). |
| `SHIP-VERIFY-POLICY-BASE-ABSENT` | medium | A policy or manifest trust root changed and no base policy could be compared — no base report, a first adoption, or a control pack this build cannot resolve; routed to human review without a weakening claim. |
| `SHIP-VERIFY-BASELINE-OR-WAIVER-EXPANDED` | high | The PR broadens what the gate forgives — a new suppression, a widened waiver scope, or a larger accepted-debt baseline — versus the base. |
| `SHIP-VERIFY-CI-GATE-REMOVED` | critical | The PR deletes the Shipgate CI workflow from an opted-in repo, which would stop the release gate from running. |
| `SHIP-VERIFY-AGENT-INSTRUCTIONS-WEAKENED` | medium | Deprecated; retained for compatibility, with no findings emitted by current scans. |
| `SHIP-VERIFY-TRIGGER-CATALOG-DRIFT` | medium | The PR changes the trigger catalog that decides when Shipgate runs; routed to human review to rule out gate evasion. |
| `SHIP-VERIFY-CAPABILITY-SCOPE-BROADENED` | critical | The PR removes or broadens a dynamically-loaded toolkit's least-privilege configuration bound (e.g. a `stripe_agent_toolkit` allowlist), silently expanding the toolkit surface; blocks rather than degrading to insufficient_evidence. |
| `SHIP-CAP-CONFIG-BINDING-REMOVED` | high | The PR removes the config binding from a dynamic toolkit factory present on both sides of the diff, so the toolkit may fall back to its everything-enabled defaults; routed to human review instead of silent insufficient_evidence parity. |
| `SHIP-CAP-CONFIG-BINDING-CHANGED` | medium | The PR changes the config file that binds a dynamic toolkit's authority; static analysis cannot diff the effective tool list, so the config delta is routed to review. |

## Check Details

### SHIP-INVENTORY-NOT-ENUMERABLE

The scanner could not enumerate any tools from required manifest inputs. Add a local MCP JSON or OpenAPI source before relying on the report.

### SHIP-INVENTORY-WILDCARD-TOOLS

A source exposes wildcard or all-tools access. Replace it with an explicit allowlist so review can reason about the actual release surface.

### SHIP-INVENTORY-TOOL-SURFACE-TOO-LARGE

The normalized tool count exceeds the MVP review threshold. Split or reduce the surface when the report becomes too broad to review.

### SHIP-INVENTORY-LOW-CONFIDENCE-PRODUCTION-SURFACE

A production target depends on lower-confidence extraction, such as SDK AST enrichment. Declare the tools through manifest, MCP, or OpenAPI inputs.

### SHIP-DOC-MISSING-DESCRIPTION

A tool has no description or a description too short for reliable review. Add a concise capability description.

### SHIP-DOC-INJECTION-RISK

A tool description contains instruction-override-like language. Rewrite it as neutral metadata.
Purely heuristic matches default to `medium`; multiple matches on write/high-risk tools are `high`.

### SHIP-DOC-SECRET-IN-DESCRIPTION

A tool description contains a secret-like token or credential value. Remove it and rotate the exposed secret.
Purely heuristic matches default to `medium`; multiple matches on write/high-risk tools are `high`.

### SHIP-SCHEMA-BROAD-FREE-TEXT

A write/action-like tool accepts broad free-form input. Constrain the field with structured schema or enums.

### SHIP-SCHEMA-MISSING-BOUNDS

A risky numeric parameter lacks a maximum. Add a maximum or equivalent policy limit.

### SHIP-SCHEMA-FREEFORM-OUTPUT

A tool returns free-form string output that may later be placed in model context. Prefer structured output for model-consumed tool results.

### SHIP-AUTH-MISSING-SCOPE

A write or sensitive-data tool has no auth scope metadata. Declare scopes in OpenAPI, MCP, or manifest metadata.

### SHIP-AUTH-MANIFEST-BROAD-SCOPE

The manifest declares broad permission scopes such as wildcard or admin scopes. Replace them with operation-specific scopes.

### SHIP-AUTH-TOOL-BROAD-SCOPE

A tool declares broad auth scopes. Use narrower tool scopes where possible.

### SHIP-AUTH-SCOPE-COVERAGE-MISSING

A tool requires scopes that are not covered by `permissions.scopes`. Reconcile the manifest with the tool requirements.

### SHIP-SCOPE-TOOL-OUTSIDE-PURPOSE

A write-capable tool contradicts a read-only declared purpose. Remove the tool or update the declared release scope.

### SHIP-SCOPE-PROHIBITED-TOOL-PRESENT

A tool appears to overlap with a manifest `prohibited_actions` entry. Remove or narrow the tool, or revise policy/scope text.

### SHIP-SCOPE-TOOLKIT-UNBOUNDED

A recognized agent-toolkit constructor (e.g. `stripe_agent_toolkit`) is mounted with no `configuration` allowlist, so the full toolkit surface — which static extraction cannot enumerate — is granted. Routes to human review instead of passing silently. Pass an explicit `configuration` allowlist (resource:verb actions) to bound the mounted tools.

### SHIP-POLICY-APPROVAL-MISSING

A high-risk tool lacks a declared approval policy. Add an approval policy or remove the tool from the release.

### SHIP-POLICY-CONFIRMATION-MISSING

A destructive, external-write, or customer-communication tool lacks a confirmation policy. Add confirmation policy or remove the tool.

### SHIP-ACTION-UNDECLARED

`action_surface.require_explicit_actions` is true, but a loaded tool has no
matching `action_surface.actions[]` declaration. Add action metadata for the
tool or disable the explicit-action requirement.

### SHIP-ACTION-POLICY-VIOLATION

A user-declared `action_surface.policies[]` rule matched an action, and one or
more required dot-path values were absent or different. Satisfy the policy
requirements or narrow/remove the action.

This id also carries two built-in rules, distinguished by
`evidence.policy_id`. `builtin-high-impact-approval` is the approval a
production operation or code execution requires. `control-pack:<pack>:<effect>`
is an obligation the manifest's selected control pack
(`policies.control_pack`) states about an effect with no control check of its
own — a plain write, a privileged read, or identity access. Both are mandatory
current-surface controls: a `checks.ignore` entry records the exception but
does not waive the blocker.

### SHIP-ACTION-FINANCIAL-WRITE-CONTROL-MISSING

A newly added action is classified as `financial_write` and is missing
`approval.required`, `safeguards.audit_log`, or `safeguards.idempotency`.
Declare the required controls before releasing the action.

### SHIP-ACTION-DESTRUCTIVE-ROLLBACK-MISSING

A newly added destructive action is missing `approval.required` or
`safeguards.rollback`. Declare the approval and rollback controls, or remove
the destructive action from the release surface.

### SHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING

A newly added external communication action lacks `safeguards.audit_log`.
Declare audit evidence so reviewers can trace outbound side effects.

### SHIP-ACTION-WILDCARD-SCOPE

An added action declares a broad scope, or a modified action expands into a
broad scope such as wildcard/admin access. Replace it with operation-specific
scopes.

### SHIP-ACTION-EFFECT-ESCALATED

An action changed to a higher-risk effect, such as read to write or write to
destructive. Add reviewer approval for the escalation or reduce the effect.

### SHIP-ACTION-EFFECT-DOWNGRADE-DECLARED

An `action_surface.actions[]` declaration sets a lower-risk effect than
Shipgate inferred from the loaded tool metadata. Align the declared effect
with the inferred operation or remove the weaker declaration.

### SHIP-ACTION-CONTROL-DOWNGRADE

An `action_surface.actions[]` declaration sets an inherited approval or
safeguard control from `true` to `false`. Keep the inherited control enabled
or remove the weakening declaration.

### SHIP-ACTION-APPROVAL-REMOVED

The base action required approval, but the current action no longer does.
Restore `approval.required` or document a reviewed override.

### SHIP-ACTION-SAFEGUARD-REMOVED

An existing action lost a safeguard such as audit logging, idempotency,
rollback, or dry-run support. Restore the safeguard or document a reviewed
override.

### SHIP-EVIDENCE-APPROVAL-TRACE-MISSING

`validation.required_evidence.approval_trace_required` is true, but local
validation evidence does not show `approved: true` for an approval-required
tool. Add local approval trace evidence produced by runtime middleware or
change the declared review posture. Agents Shipgate reads this evidence; it
does not produce or certify it. Missing local evidence does not prove the
runtime approval control is absent.

### SHIP-EVIDENCE-OVERRIDE-REASON-MISSING

`validation.required_evidence.override_reason_required` is true, but override
logs are absent, empty, or include normalized `override`, `bypass`, or
`auto_approve` events without a non-empty `reason`. Record reviewer-visible
reasons in the local override log. Missing local evidence does not prove the
runtime override control is absent.

### SHIP-EVIDENCE-HIGH-RISK-EXCLUSION-MISSING

`validation.required_evidence.high_risk_auto_approval_exclusion_required` is
true, and a high-risk tool with declared approval policy is not listed under
`high_risk_auto_approval_exclusions`. This is separate from
`SHIP-POLICY-APPROVAL-MISSING`: it only fires after approval policy is already
declared, because it checks the local evidence that the tool is excluded from
auto-approval review posture. Missing local evidence does not prove the
runtime exclusion control is absent.

### SHIP-EVIDENCE-HITL-PROMOTION-CRITERIA-MISSING

`validation.target_review_posture` is `limited_auto_approval`, but local
promotion criteria evidence is missing or the canonical required-evidence
flags are not true in the manifest and criteria file. Finding evidence includes
`reason: file_missing` or `reason: flags_missing` so reviewers can distinguish
an absent local source from incomplete criteria. Missing local evidence does
not prove runtime controls are absent.

### SHIP-SIDEFX-IDEMPOTENCY-MISSING

A risky write tool lacks idempotency evidence. Add an idempotency key, idempotent annotation, or declared idempotency policy.

### SHIP-API-FUNCTION-SCHEMA-STRICTNESS

An OpenAI API function schema is not strict enough for reliable tool calls. The check flags missing `strict: true`, missing object parameters, `additionalProperties` not set to `false`, properties omitted from `required`, broad free-text action fields, and risky numeric fields without bounds or enums.

### SHIP-API-STRUCTURED-OUTPUT-READINESS

An OpenAI API response format is missing or under-specified. The check flags missing response schemas for high-risk API tools, broad response objects, decision/status fields without enums, missing `refusal` / `needs_review` / `error` modeling, and missing `downstream_critical_fields`.

### SHIP-API-PROMPT-TOOL-SCOPE-MISMATCH

Prompt files contradict the enabled API tool surface. The check flags prompts that say "advise only" or "read-only" while write/high-risk tools are enabled, and high-risk tools whose prompts do not mention approval and confirmation expectations.

### OpenAI API Operational Readiness Checks

v0.4 splits the former `SHIP-API-OPERATIONAL-READINESS` bundle into atomic
check IDs so suppressions, severity overrides, SARIF rules, and baselines can
target one missing contract at a time. The split checks use `model_config`,
`policy_rules`, simple test cases, and trace samples to flag missing retry
policy, missing timeouts, missing test cases, non-idempotent high-risk tools
with retry evidence, missing success/failure tool-output modeling, and trace
samples that show required approval or confirmation missing.

The old bundled check ID remains as a deprecated compatibility alias through at
least one minor release. v0.4 does not emit new findings with
`SHIP-API-OPERATIONAL-READINESS`, but existing suppressions, severity overrides,
baseline entries, `explain`, `list-checks`, and stale-suppression validation
continue to recognize it. New configs should use the specific v0.4 ID that
represents the condition.

### SHIP-API-OPERATIONAL-READINESS

Deprecated compatibility alias for the v0.3 OpenAI API operational readiness
bundle. Migrate suppressions, severity overrides, and baselines to the specific
v0.4 `SHIP-API-*` readiness checks when you touch the config.

### SHIP-API-RETRY-POLICY-MISSING

A high-risk OpenAI API tool flow runs without declared retry policy metadata.
Reviewers cannot reason about duplicate side effects when retry behavior is
unspecified. Declare `retry_policy` in `openai_api.policy_rules` or
`openai_api.model_config`.

### SHIP-API-TIMEOUT-MISSING

A high-risk OpenAI API tool flow runs without declared timeout metadata.
Without an explicit timeout, failure behavior and tool-call continuation
become ambiguous. Declare a tool-call timeout in policy rules or model
config.

### SHIP-API-TEST-CASES-MISSING

High-risk OpenAI API tools exist with no declared test cases. Tool-call flows
that approve refunds, send mail, or modify state should ship with simple test
cases as release evidence. Add cases under `openai_api.test_cases`.

### SHIP-API-TOOL-OUTPUT-SCHEMA-MISSING

A high-risk OpenAI API tool lacks declared success/failure output modeling.
Reviewers depend on `success_fields` and `failure_fields` to reason about
downstream failure handling. Declare them in policy rules.

### SHIP-API-RETRY-WITHOUT-IDEMPOTENCY

A retry policy is declared and a risky write tool lacks idempotency evidence.
Retries against non-idempotent writes can duplicate financial, destructive, or
external side effects. Either add idempotency evidence or remove the retry
policy for this tool.

### SHIP-API-TRACE-APPROVAL-MISSING

A trace sample shows a policy-controlled tool call with `approved: false` for
a tool that has approval policy evidence elsewhere in the manifest. Implement
the runtime approval gate; **do not edit the trace recording** to flip
`approved` — that patches the evidence, not the agent's behavior.

### SHIP-API-TRACE-CONFIRMATION-MISSING

A trace sample shows a policy-controlled tool call with `confirmed: false`
for a tool that has confirmation policy evidence. Implement the runtime
confirmation gate; **do not edit the trace recording** to flip `confirmed`
— same anti-pattern as the approval-missing finding above.

### SHIP-ADK-DYNAMIC-TOOLSET-NOT-ENUMERABLE

A Google ADK `OpenAPIToolset`, `McpToolset`, or dynamic tools expression could
not be enumerated statically. Provide explicit local OpenAPI, MCP, or ADK tool
inventory inputs before relying on the release report.

### SHIP-ADK-MCP-TOOLSET-UNFILTERED

An ADK `McpToolset` has no static `tool_filter`. Add a narrow filter and an
explicit inventory file so reviewers can see the intended runtime surface.

### SHIP-ADK-FUNCTION-TOOL-METADATA-MISSING

An ADK function or Agent Config tool reference lacks description or parameter
metadata. Add docstrings, type annotations, or explicit local inventory
metadata.

### SHIP-ADK-LONGRUNNING-CONTRACT-MISSING

An ADK `LongRunningFunctionTool` lacks static evidence for operation id and
status/progress fields. Google-style `name` plus `done`, `state`, `phase`,
`metadata`, or `result` fields count as contract evidence; tools may also carry
`annotations.long_running_contract: true` in explicit inventory metadata.
Document the handoff and completion contract before promotion.

### SHIP-ADK-GUARDRAIL-EVIDENCE-MISSING

High-risk ADK tools are present without static callback/plugin or manifest
policy evidence. ADK callbacks and plugins count only as static evidence of
intent; they are not proof that runtime enforcement works.

### SHIP-ADK-EVAL-COVERAGE-MISSING

Google ADK inputs target `production_like` or `production` without declared eval
files. Add eval artifacts that cover expected responses and tool-use
trajectories.

### SHIP-LANGCHAIN-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE

A LangChain/LangGraph tool list, binding, or graph node could not be enumerated
statically. Provide an explicit local inventory when tools are produced by
factories, comprehensions, loop-built lists, unresolved imports, or other
runtime-only code. This ID uses `TOOL-SURFACE` instead of ADK's `TOOLSET`
because LangChain exposes ad hoc tool lists and model/graph bindings rather
than a consistent toolset abstraction.

### SHIP-LANGCHAIN-FUNCTION-TOOL-METADATA-MISSING

A LangChain/LangGraph `@tool` function or `StructuredTool.from_function(...)`
surface lacks a static description or parameter metadata. Add docstrings,
function annotations, or same-file Pydantic `args_schema` metadata.

### SHIP-CREWAI-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE

A CrewAI agent or crew tool surface could not be enumerated statically. Provide
an explicit local inventory when tools are produced by factories,
comprehensions, loop-built lists, unresolved imports, or other runtime-only
code. This ID uses `TOOL-SURFACE` instead of ADK's `TOOLSET` because CrewAI
agents bind ad hoc tool lists rather than a consistent toolset abstraction.

### SHIP-CREWAI-FUNCTION-TOOL-METADATA-MISSING

A CrewAI `@tool` function or `BaseTool` subclass lacks a static description or
parameter metadata. Add descriptions, `_run` annotations, or same-file Pydantic
`args_schema` metadata.

### SHIP-CODEX-PLUGIN-METADATA-MISSING

A Codex plugin package has incomplete or ambiguous identity metadata. Fill
`name`, `version`, and `description`; keep the plugin name aligned with the
package root; and avoid duplicate plugin names across scanned package roots.

### SHIP-CODEX-PLUGIN-COMPONENT-PATH-MISSING

A Codex plugin component path for skills, MCP servers, apps, or hooks could not
be loaded. Paths must resolve inside both the plugin package and the manifest
directory.

### SHIP-CODEX-PLUGIN-MARKETPLACE-POLICY-MISSING

A marketplace entry lacks `policy.installation`, `policy.authentication`, or
`category`. Add those fields so coding agents can see installation and
authentication posture before adoption.

### SHIP-CODEX-PLUGIN-MCP-SERVER-NOT-ENUMERABLE

A plugin declares an MCP server in `.mcp.json`, but Agents Shipgate does not
execute MCP commands to discover tools. Provide a local MCP tools inventory via
`codex_plugins.mcp_tool_inventories`.

### SHIP-CODEX-PLUGIN-APP-SURFACE-NOT-ENUMERABLE

A plugin declares a connector app in `.app.json`. Connector-backed capabilities
are externally mediated and are review items unless a local inventory or policy
artifact documents the effective surface.

### SHIP-CODEX-PLUGIN-SKILL-METADATA-MISSING

A `skills/**/SKILL.md` file is missing parseable `name` or `description`
frontmatter, or duplicates another skill name in the same plugin. Give every
skill a unique routing name and clear description.

### SHIP-CODEX-BOUNDARY-CONFIG-PARSE-FAILED

A changed repo-local `.codex/config.toml` or `.codex/hooks.json` cannot be
parsed. Fix the malformed config or have a human review the boundary change.

### SHIP-CODEX-BOUNDARY-UNKNOWN-PERMISSION-KEY

A changed `.codex/config.toml` contains an unknown key below a permissions
profile or permissions network table. Review the key before trusting the local
Codex boundary.

### SHIP-CODEX-BOUNDARY-NETWORK-WILDCARD

A changed Codex permission profile allows a wildcard domain. Replace wildcard
network access with explicit domains or get human approval.

### SHIP-CODEX-BOUNDARY-NETWORK-EXPANDED

Codex workspace-write network access or full network mode was enabled. Have a
human approve the expanded local execution boundary.

### SHIP-CODEX-BOUNDARY-DANGER-FULL-ACCESS

Codex `sandbox_mode` or `default_permissions` selects `danger-full-access`.
Use a narrower permission profile or get explicit human approval.

### SHIP-CODEX-BOUNDARY-MCP-AUTO-APPROVE-WRITE

A changed MCP or app tool approval mode is `approve` for a write or
destructive-looking tool. Do not auto-approve write or destructive tools.

### SHIP-CODEX-BOUNDARY-MCP-AUTO-APPROVE-UNKNOWN

Codex auto-approves an MCP server whose tool surface is not statically
enumerable. Enumerate MCP tools or keep the approval mode at `prompt`.

### SHIP-CODEX-BOUNDARY-APP-AUTO-APPROVE

A Codex app connector tool approval changed to `approve`. Review connector
approval changes before local automation.

### SHIP-CODEX-BOUNDARY-AGENTS-SHIPGATE-REQUIREMENT-REMOVED

Deprecated across the unreleased minor cycle. The ID remains available for
historical reports and configuration; current runs do not judge removed or
softened prose. Complete parsed structure and unresolved-input review replace
the word heuristic. Structured host readers remain active (#545, #516).

### SHIP-CODEX-BOUNDARY-CI-GATE-REMOVED

The Shipgate GitHub Actions workflow was deleted or no longer contains a
Shipgate invocation. Restore the workflow or get human approval to remove it.

### SHIP-CODEX-BOUNDARY-POLICY-WEAKENED

The Codex boundary policy was deleted or downgraded. Restore the stricter
policy or get human approval before weakening the local boundary gate.

### SHIP-CODEX-BOUNDARY-HOOK-COMMAND-CHANGED

A changed Codex hooks source contains a command hook. Review executable hooks
before relying on them.

### SHIP-CODEX-BOUNDARY-SKILL-COMMAND-CHANGED

Deprecated across the unreleased minor cycle. The ID remains readable but
current runs emit no command claim from prose words. Supported skill frontmatter
and preprocessing declarations remain in structural comparison; unknown or
malformed structure remains a coverage limitation (#545, #516).

### SHIP-AGENT-BOUNDARY-PROTECTED-SURFACE-UNCLASSIFIED

A recognized host, instruction, policy, state, or workflow surface changed
without a specialized safe classification. The change routes to human review at
PR time because absence of a risk finding is not evidence of non-broadening
behavior. In the local `shipgate check` loop this medium row is graded: unless
the path is a gate-governing trust root (manifest, policy, CI gate, or
`.agents-shipgate/` state), the coding agent may finish its turn with the
obligation carried in `pending_review[]` rather than stopping.

### SHIP-AGENT-BOUNDARY-EXPERIMENTAL-SURFACE-CHANGED

An adapter marked experimental observed a boundary change. Experimental
coverage cannot authorize coding-agent completion without human review.

### SHIP-AGENT-BOUNDARY-STATIC-REQUIREMENTS-CHANGED

The repository's `.codex/requirements.toml` changed. This is a reviewer-owned
host trust root for approvals, sandboxing, and network constraints.

### SHIP-AGENT-BOUNDARY-INPUT-INCOMPLETE

A relevant diff or file was truncated, malformed, unsafe to read, traversing,
binary, oversized, or otherwise unresolved. Supply coherent input and rerun.

### SHIP-HOST-BOUNDARY-CONFIG-PARSE-FAILED

A changed `.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`,
`.claude/settings(.local).json`, or `.github/workflows` file cannot be parsed,
or its content cannot be resolved from the diff. Fix the malformed host config
or have a human review the change.

### SHIP-HOST-BOUNDARY-MCP-SERVER-ADDED

A changed MCP server declaration file adds a server key that did not exist
before. Have a human review the new MCP server before the agent can use it.

### SHIP-HOST-BOUNDARY-MCP-SERVER-CHANGED

An existing MCP server changed its `command`, `args`, `url`, `serverUrl`, or
`env` keys. Review the change — env var values never appear in evidence, key
names only.

### SHIP-HOST-BOUNDARY-PERMISSION-WILDCARD-ALLOW

A changed `.claude/settings.json` or `.claude/settings.local.json` adds an
allow rule that is `*`, a bare tool name, or a wildcard-shaped rule such as
`Bash(*)`. Do not allow wildcard tool permissions; scope the rule to specific
commands.

### SHIP-HOST-BOUNDARY-PERMISSION-ALLOW-EXPANDED

A changed Claude Code settings file adds a non-wildcard `permissions.allow`
entry. Have a human approve the new permission allow rule.

### SHIP-HOST-BOUNDARY-PERMISSION-DENY-REMOVED

A changed Claude Code settings file drops an entry from `permissions.deny`.
Have a human confirm the removed deny rule is no longer needed.

### SHIP-HOST-BOUNDARY-HOOK-CHANGED

A changed Claude Code settings file adds, modifies, or removes hook handlers.
Review executable hook changes before the agent relies on them.

### SHIP-HOST-BOUNDARY-WORKFLOW-WRITE-ALL

A changed `.github/workflows` file sets `permissions: write-all` at the top
level or for a job. Replace write-all with the minimal explicit permission
scopes.

### SHIP-HOST-BOUNDARY-WORKFLOW-PERMISSIONS-EXPANDED

A changed `.github/workflows` file grants an explicit write scope that the
base did not grant, at the top level or per job. Have a human approve the
expanded workflow write permission.

### SHIP-HOST-BOUNDARY-PULL-REQUEST-TARGET-ADDED

A changed `.github/workflows` file adds `pull_request_target` to its triggers.
Review it carefully — `pull_request_target` runs with secrets on fork PRs.

### LINT-BODY-001

A `SKILL.md` body lacks an ordered Procedure, Steps, or Workflow section. Add
step-by-step instructions an agent can follow.

### LINT-BODY-003

A `SKILL.md` body does not define the expected output artifact, response shape,
or completion criteria. Add an Output section.

### LINT-BODY-004

A `SKILL.md` body lacks verification or acceptance criteria. Add checks the
agent can run or cite before reporting completion.

### LINT-DESC-001

A skill description is too short or generic to route reliably. Rewrite it with
concrete trigger conditions and domain-specific nouns.

### LINT-DESC-003

A skill description is broad enough to false-trigger. Narrow it to the specific
workflow, file type, or domain where the skill applies.

### LINT-SCRIPT-001

A bundled script lacks documented `--help` usage. Document safe invocation in
`SKILL.md` and make usage discoverable without interaction.

### LINT-SCRIPT-004

A bundled script appears stateful or destructive but lacks dry-run support. Add
`--dry-run` or document a non-mutating preview path.

### LINT-SPEC-002

`SKILL.md` frontmatter is not parseable YAML mapping data. Fix the frontmatter
before relying on skill discovery.

### LINT-SPEC-003

`SKILL.md` is missing required `name` frontmatter. Add a stable routing name.

### LINT-SPEC-004

`SKILL.md` is missing required `description` frontmatter. Add a clear trigger
description that tells agents when to load the skill.

### SEC-FLOW-004

A skill combines untrusted content with outbound or secret-access behavior but
lacks instruction/data separation guidance. State that untrusted content is data
and must not supply agent instructions.

### SEC-MISMATCH-001

A skill declares read-only or review-only behavior but bundled scripts mutate
files or external state. Update the declared purpose or remove the mutation.

### SEC-PI-001

A skill or related artifact tells agents to ignore or override higher-priority
instructions. Remove the instruction-override language.

### SEC-PI-003

A skill or related artifact tells agents to hide behavior from users, reviewers,
or logs. Remove concealment instructions.

### SEC-PROV-001

A skill marked as third-party lacks `metadata.shipgate.source` provenance. Add
source, source reference, owner, and review metadata.

### SEC-REMOTE-001

A skill fetches mutable remote prompt, instruction, or skill content at runtime.
Avoid remote instruction fetch or document source trust and pinning.

### SEC-REMOTE-002

A skill sources or executes content fetched from a remote URL. Remove the
runtime remote-code execution or pin, verify, and sandbox the content.

### SEC-SCRIPT-001

A skill artifact pipes remote content to a shell or interpreter. Vendor the
script or verify pinned content instead of executing mutable remote bytes.

### SEC-SCRIPT-002

A skill artifact contains destructive or stateful command patterns without
dry-run, confirmation, or path validation. Add guardrails or remove the command.

### SEC-SECRET-001

A skill artifact contains a hardcoded secret-like value. Remove it and rotate the
exposed credential; reports redact the value by kind and location.

### SEC-SECRET-003

A skill artifact instructs agents to read broad credential files or secret-bearing
environment variables. Replace with scoped placeholder guidance.

### SEC-TOOL-001

A skill pre-approves shell or bash-like tools without reviewed-script or sandbox
justification. Remove preapproval or document the review boundary.

### SHIP-N8N-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE

An n8n workflow uses a runtime expression in a tool name, an unresolved
Call-Workflow target, wildcard MCP Server/Client exposure, or an uninventoried
community/custom tool node. Provide a local n8n/MCP inventory or replace the
dynamic exposure with a static allowlist. This is high severity in every
environment because static release evidence cannot prove the actual tool
inventory.

### SHIP-N8N-MCP-CLIENT-TOOLSET-UNFILTERED

An n8n MCP Client Tool exposes `All` or `All Except` tools without a local
inventory. Select explicit MCP tools or provide a local MCP inventory for
release review. The severity is environment-sensitive because the selector is
easy to narrow before production, while production-like use increases blast
radius.

### SHIP-N8N-AI-TOOL-METADATA-MISSING

An n8n AI-exposed tool lacks a static description or parameter metadata. Add
tool descriptions, `$fromAI()` metadata, workflow input schemas, or explicit
inventory metadata.

### SHIP-N8N-CREDENTIAL-EVIDENCE-MISSING

Production-like n8n workflows reference credentials but no local credential
stubs are declared. Declare source-control credential stubs so reviewers can
see credential types without seeing secret values.

### SHIP-N8N-EVAL-COVERAGE-MISSING

n8n workflows target `production_like` or `production` without declared eval
files. Add eval artifacts that cover expected responses and tool-use
trajectories.

### SHIP-N8N-SECRET-IN-WORKFLOW-PARAMETER

An n8n workflow parameter, node note, `pinData` entry, or `staticData` entry
contains a secret-like value. Evidence includes only the source reference,
stable pointer, and secret kind; it never includes the matched secret value or
a verifier hash for that value.

### SHIP-CONDUCTOR-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE

A Conductor OSS workflow uses a dynamic or missing MCP method/server, dynamic
LLM tool advertisement, runtime-generated task, or unresolved sub-workflow.
Make the target static and locally reviewable before release. The adapter does
not connect to Conductor, MCP servers, or model providers to fill the gap.

### SHIP-TRUST-MANIFEST-UNPROTECTED

The manifest declares `ci.mode: strict`, so this repository states the strongest
posture it can — and no CODEOWNERS file GitHub would read assigns an owner to the
manifest that decides it. Add a rule covering the manifest path and require
review on the branch it merges to, so changing what the gate enforces takes a
named human's approval.

Two halves are required, and the finding says which one is missing. A rule set
must cover the manifest **and cover itself**: one that owns `shipgate.yaml` but
not `CODEOWNERS` describes a protection one edit deep, because the same pull
request can delete the rule. `* @team` satisfies both.

It reads the file GitHub would read, and fails closed where it would not: outside
a git checkout there is no pull request for a rule to gate, a file of 3 MB or
more is ignored by GitHub entirely (with no fallback to a lower-precedence
location), and a token GitHub does not accept as an owner assigns nobody — which
matters because last-rule-wins means a narrower rule with a typo *removes* the
ownership a broader rule granted.

Guidance only: it never becomes a blocker or a review item, and it never moves a
verdict. Branch protection lives in repository settings that no file in a
checkout can read, so this check reports the CODEOWNERS half and says so rather
than inferring the rest. Silent on `ci.mode: advisory`, where the manifest is not
yet enforcing anything — `agents-shipgate doctor` names manifest protection as a
step from adoption rung 2 to rung 3.

### SHIP-MANIFEST-STALE-SUPPRESSION

A suppression references an unknown check ID or a tool that is not loaded in the
current scan. Remove stale suppressions so reviewers can trust the suppression
list as current release intent.

### SHIP-MANIFEST-STALE-POLICY

A policy entry references a tool that is not loaded. Remove or update stale
approval, confirmation, or idempotency policies so release policy matches the
actual tool surface.

### SHIP-MANIFEST-STALE-RISK-OVERRIDE

`risk_overrides.tools` references a tool that is not loaded. Remove stale
overrides or update them to the current tool names.

### SHIP-MANIFEST-HIGH-RISK-OWNER-MISSING

A high-risk tool in `production_like` or `production` has no owner metadata.
Declare an owner in the tool source or `risk_overrides.tools` so reviewers know
who is accountable for remediation.

### SHIP-MANIFEST-UNUSED-SCOPE

`permissions.scopes` includes a scope not required by any loaded tool. Remove
unused scopes or add tool metadata showing why the permission is needed. Broad
unused write/admin scopes are `high`; other unused scopes are `medium`.

### SHIP-BASELINE-INTEGRITY-MISMATCH

Baseline file integrity check failed. Emitted when the baseline JSON has been
edited outside `agents-shipgate baseline save` (hash mismatch against the
audit log), when the audit log is missing or empty for a non-empty baseline,
when the audit log is malformed, when an entry's `provenance.run_id` is not
present in the audit log, or when an entry pre-dates the v0.5 provenance
contract. In
`baseline.integrity_mode: strict` the finding carries `blocks_release=true`
and `agents-shipgate baseline verify --strict` exits with code 6.
Re-run `agents-shipgate baseline save` to refresh the baseline alongside its
audit row; investigate the diff before accepting.

### SHIP-BASELINE-ENTRY-EXPIRED

A baseline entry's reviewer-set `provenance.expires` date is past today.
Renewable consent is a deliberate choice: accepted technical debt should
need re-review on a schedule, not a silent extension. Re-review the entry
and either remove it, fix the underlying finding, or extend
`provenance.expires` with a new `reason`.

### SHIP-BASELINE-ENTRY-STALE

A baseline entry no longer corresponds to an active finding or check ID.
Two sub-kinds, both `low` severity:

- `deprecated_check_id` — entry references an alias in `LEGACY_CHECK_ID_ALIASES`.
  Update the entry to the canonical replacement check IDs (re-running
  `baseline save` does not rewrite check IDs).
- `resolved_not_pruned` — entry matched no active scan finding. Re-run
  `agents-shipgate baseline save` to drop the entry from the baseline.

### SHIP-VERIFY-LOCAL-REVIEW-PROVISIONAL

`verify` loaded a manifest Git could not prove is a committed repository input.
The reserved `.agents-shipgate-local-review.yaml` path is always
`local_review`; another untracked custom path is `uncommitted`; Git inspection
failure is `unknown`. The finding goes through the ordinary
`release_decision.decision` engine at review-required severity; it is not a
second verdict. Verifier notes, the verification plan, and handoff blockers
record the exact provenance, while the terminal receipt denies merge
authority. Adopt `shipgate.yaml` through `init --write`, verify it from the
evaluated Git tree, and follow any resulting human-review route before using
the policy as release evidence. Git presence alone does not prove human review.

### SHIP-VERIFY-TRUST-ROOT-TOUCHED

A PR changed a file that defines the release gate's trust spine — the
manifest (`shipgate.yaml`), `.agents-shipgate/` state (baselines,
waivers), `policies/`, `prompts/`, the Shipgate CI gate
(`.github/workflows/agents-shipgate.yml`), agent instructions
(`AGENTS.md`, `CLAUDE.md`, `.claude/`, `.cursor/rules/`,
`.agents/skills/`, `.codex/`), Codex plugin packages (`.codex-plugin/`),
or tool-surface declarations (`.app.json`, `.mcp.json`, `SKILL.md`).

This is Tier A trust-root protection: pure path/glob classification of
the changed files. It is the cheap half of the reward-hacking guard — a
coding agent told to make CI pass can weaken the gate instead of fixing
the readiness issue, so touching a trust root must require human review.
The finding fires only when a verification context (changed files) is
supplied (`agents-shipgate scan --changed-files ...` or, later, `verify`);
a plain `scan` emits nothing. It is one ordinary `Finding` at `medium`
severity routed through `release_decision` — never a second verdict.

### SHIP-VERIFY-POLICY-WEAKENED

Tier B trust-root protection: instead of classifying *which* files
changed, it compares the normalized effective-policy snapshot of the base
report (supplied via `--diff-from`) against the head manifest and fires
when the gate moved toward *less* review or *less* blocking — CI mode
downgraded (e.g. `strict` → `advisory`), the fail-on severity set lost a
tier, a check's severity override dropped across a tier boundary, or
`policies.control_pack` moved to a pack that requires *less* of some action
effect (`kind: control_pack_weakened`). The comparison is semantic, not a text
diff, so it is robust to reformatting.

The first three kinds answer "does the same finding still block?"; the control
pack answers "does the same action still produce the finding?", which is the
other way a gate gets weaker. A pack move is one changed line, so it is one
finding: `evidence.removed_controls` carries `{effect, controls}` for every
effect that lost something, and the sentence names a bounded prefix plus how
many it is not naming.

Where either side names a control pack this build cannot resolve, no
comparison is possible in either direction. That routes to
`SHIP-VERIFY-POLICY-BASE-ABSENT` with `kind: control_pack_unrecognized` rather
than reading as no weakening.

The claim is base-relative, so this check fires only when a base snapshot
exists to compare against. The no-base fail-safe is its own reason code,
`SHIP-VERIFY-POLICY-BASE-ABSENT`. Category `verify` (suppression-immune,
floor `high`); never a second verdict.

### SHIP-VERIFY-POLICY-BASE-ABSENT

The fail-safe for "there is no base policy to compare against". It fires in
verify mode when the PR touched a policy/manifest trust root
(`**/shipgate.yaml`, `**/policies/**`, `**/.agents-shipgate/**`, or the
configured manifest under any name) and no base effective-policy snapshot
was available — either because the base carries no Shipgate manifest at all
(a first adoption) or because no base report was obtainable (no
`--diff-from`, a pre-v0.22 base, or a base scan that produced none).

It makes no weakening claim in either direction, which is the point: on a
first adoption the base had no gate, so a weakening definitionally could not
have happened, and reporting one told every new adopter that their first PR
had loosened something. It is equally not a pass — a reward-hacker must not
be able to dodge review by breaking the base scan — so the change is routed
to a human at `medium` under this reason code instead.

Two evidence kinds separate the two conditions:

- `manifest_introduced` — git proves the base carries no manifest under any
  name and the diff removes none, so this PR adopts the gate. This is the
  one case that reports `verifier_summary.policy_weakened: false`.
- `base_snapshot_unavailable` — the comparison simply could not be made. The
  direction is unprovable, so `policy_weakened` stays raised; a
  rename-and-loosen diff cannot clear the alarm by hiding the base.

Category `verify` (suppression-immune, floor `medium`). Like the rest of the
family it requires a declared `human_ack` for the `policy` surface and is
routed through the one decision engine — never a second verdict.

### SHIP-VERIFY-BASELINE-OR-WAIVER-EXPANDED

Tier B: detects a PR that broadens what the gate forgives — a new entry in
`checks.ignore`, a widened waiver scope (e.g. one tool widened to `*`), or
a larger accepted-debt baseline — by a base-vs-head superset comparison of
the effective-policy snapshot. Suppressing or baselining a finding instead
of fixing it is a classic reward hack; this makes the expansion
release-visible. Requires a base snapshot (touching the files alone is
already covered by `SHIP-VERIFY-TRUST-ROOT-TOUCHED`). Category `verify`,
floor `high`.

### SHIP-VERIFY-CI-GATE-REMOVED

Tier B: fires when, in verify mode, a Shipgate CI workflow path
(`.github/workflows/agents-shipgate.yml`/`.yaml`) appears in the changed
files **and** that file no longer exists on disk — i.e. the PR deleted the
gate. Detectable without a base snapshot. Emitted at `critical` (floor
`high`): removing CI enforcement from an opted-in repo is the strongest
weakening signal in the family.

### SHIP-VERIFY-AGENT-INSTRUCTIONS-WEAKENED

Deprecated in the unreleased minor cycle (#516). The registered ID, severity
metadata and historical report compatibility remain for at least one minor
version cycle. Current scans emit no findings for this ID: a path change does
not establish that natural-language instructions weakened a control.

Existing structured permission, MCP, CI and hook readers remain active,
including changes beside prose in the same directory. The local word-based
instruction and skill emitters are also retired while their IDs remain readable.
The shared comparison in #545 clears only complete unchanged supported instruction
structure; unknown or malformed inputs retain explicit coverage/review routes.
`SHIP-VERIFY-TRUST-ROOT-TOUCHED` remains active for structural trust roots.
Neither deprecation nor preflight substitutes for a current verifier result.

### SHIP-VERIFY-TRIGGER-CATALOG-DRIFT

Tier B: the trigger catalog (`docs/triggers.json` or an
`.agents-shipgate` trigger config) decides *when* Shipgate runs. Editing
it can carve out paths so the gate stops firing — a gate-evasion one level
up from suppressing findings. Fires on changed-file membership in verify
mode at `medium`; the human confirms the change does not create a path
that evades the release gate.

### SHIP-VERIFY-CAPABILITY-SCOPE-BROADENED

Tier B: some agent toolkits load their tools through a runtime factory
(`*toolkit.get_tools()`) the static extractor cannot enumerate, so the only
statically-provable least-privilege signal is the *constructor's* permission
allowlist (`configuration={"actions": {...}}`). This check diffs that bound
base-vs-head and fires `critical` when the head removes it (mounting the full
toolkit surface — e.g. refund / cancel / dispute on a customer-support agent)
or adds permissions the base allowlist did not. Without it the opaque factory
would degrade the verdict to `insufficient_evidence` instead of `blocked`. A
coding agent cannot self-approve the broadening; a human must re-apply a
least-privilege configuration or explicitly approve the expanded surface. A
narrowing (bound added or tightened) emits nothing.

### SHIP-CAP-CONFIG-BINDING-REMOVED

Config-bound capability detection: a dynamic toolkit factory whose
authority-bearing constructor argument is *bound from a config read*
(`json`/`yaml`/`toml` load, `os.environ`, pydantic settings) hides its
effective tool surface from static enumeration on both sides of a verify
diff. When the same factory site appears on both sides and the head removes
the binding entirely, the toolkit may fall back to its everything-enabled
defaults — authority expands while looking like cleanup, and the generic
capability diff sees nothing. Fires `high` in verify mode and routes to
human review instead of silent `insufficient_evidence` parity. Fail-safe by
design: a head binding that is merely *unreadable* (`unknown`) never fires
this check — it degrades to a source warning; a head that moves to a
literal allowlist is the safe direction and emits nothing.

### SHIP-CAP-CONFIG-BINDING-CHANGED

Companion review item: the same factory site is config-bound on both sides
of the diff and the PR changes the bound config file itself (e.g. adding
`"refund"` to an actions list). Static analysis cannot diff the effective
tool list, so the config delta is routed to review at `medium` with the
inventory remedy. Fires only when the binding's literal config path matches
a changed file — env- or settings-bound factories never guess a match.

### SHIP-MCP-ENV-SECRET-PASSTHROUGH

Fires when a statically parsed MCP server passes through secret-like
environment variables, such as token, password, API key, or credential
names. Secret pass-through changes the credential boundary available to the
server, so Shipgate routes the change to human review without exposing raw
secret values in evidence.

### SHIP-MCP-AUTO-APPROVE-SIDE-EFFECT

Fires when an MCP tool classified as write, destructive, external,
financial, or production is configured with approval mode `approve`. This is
an explicit release blocker because auto-approved side-effecting MCP tools
can let an agent act outside the review boundary.

### SHIP-MCP-ANNOTATION-CONTRADICTION

Fires when an explicit MCP narrowing annotation conflicts with independent
side-effect evidence on the same tool: `readOnlyHint: true` beside write or
destructive evidence, or `destructiveHint: false` beside destructive evidence.
Absent annotations follow the MCP defaults and never trigger this check.
Policy-eligible protocol, provider, or scope evidence outranks a conflicting
keyword or pattern inference: for example, an OpenAPI `GET` does not become a
contradiction merely because its description mentions a later delete. Structural
side-effect evidence still triggers the check.

Only source observations independent of the published MCP annotation contribute.
Reviewed `action_surface.actions[].effect` declarations are deliberately excluded:
they are human trust-root assertions, not a second observation of the server.
Likewise, a source that publishes both `readOnlyHint: true` and
`destructiveHint: true` remains a `conflicting_effect_evidence` semantic gap; this
check does not relabel that same-source contradiction as independent evidence.

Supported structural instances route to review because MCP clients may use these
advisory hints to reduce confirmation prompts. Inferred-only instances remain
visible in `report.findings` with `support.policy_eligible: false` and
`agent_action: informational`; they also produce an evidence gap, but are excluded
from blockers, named review items, and release contribution. The check never
changes the engine's conservative permission verdict.

With a base report, `annotation_surface_changed` compares the exact complete
annotation map (including otherwise unrecognized keys and list order), while
`independent_evidence_unchanged` compares the independent semantic evidence. The
finding uses `form: delta` and fills `annotation_changes` only when reconstruction
proves the exact narrowing-hint flip against the base hash; unrelated co-changes
remain the static form. This semantic check runs in the normal
`scan`/`check`/`verify` path. It is intentionally not duplicated in the narrower
`mcp audit` host/config-diff rule table.

### SHIP-MCP-UNKNOWN-TOOL-SCHEMA

Fires for new or changed MCP capabilities whose static metadata cannot prove
the callable surface and side effect, including wildcard server exposure.
Provide an explicit local MCP inventory/schema or keep the server behind
human review. Unknown side effects route to review by default; they become
blockers only when paired with a separate blocking condition such as
auto-approved side-effecting access.

### SHIP-MCP-PERMISSION-EXPANDED

Fires when the MCP capability diff broadens scope, effect, risk tags,
controls, or accepted input schema. Narrowing and pure least-privilege
downgrades do not block by default.

### SHIP-MCP-READONLY-SERVER-ADDED

Fires as a low-severity warning when a new local documentation MCP server is
classified as read-only. It keeps benign expansion visible in review
artifacts while remaining non-blocking by default.

Risk tags are hints, not findings by themselves. Checks consume tags with confidence thresholds.

Common tags:

- `read_only`
- `write`
- `destructive`
- `external_write`
- `financial_action`
- `customer_communication`
- `sensitive_data_access`
- `infrastructure_change`
- `code_execution`

Manual `risk_overrides` in `shipgate.yaml` are treated as high-confidence evidence. Use `remove_tags` to subtract heuristic tags that are known to be wrong for a specific tool.

## Listing Checks

Use the CLI to inspect the built-in catalog:

```bash
agents-shipgate list-checks
agents-shipgate list-checks --json
agents-shipgate explain SHIP-POLICY-APPROVAL-MISSING
```

The JSON catalog includes `mvp_tier` for display and triage:

| `mvp_tier` | Meaning |
|---|---|
| `core` | Core Tool-Use Readiness MVP signal. |
| `adapter` | Framework or provider-specific readiness signal. |
| `evidence` | Validation, trace, or HITL evidence signal. |
| `lifecycle` | Baseline, diff, or action-surface evolution signal. |
| `hygiene` | Useful quality or maintenance signal, not core MVP positioning. |

`mvp_tier` never changes check execution, severity, fingerprints, baselines,
`release_decision`, or CI exit behavior.

Third-party packages can register checks through the `agents_shipgate.checks` Python entry-point group. Plugins are disabled by default because loading them imports third-party Python modules. Set `AGENTS_SHIPGATE_ENABLE_PLUGINS=1` to opt in, or pass `--no-plugins` to force them off for a scan or catalog command. Reports include `loaded_plugins` provenance for every third-party check entry point Shipgate discovered — including ones that failed validation. A plugin check should expose a callable with the same `ScanContext -> list[Finding]` shape as built-ins and attach `AGENTS_SHIPGATE_METADATA` as either a `CheckMetadata` instance or a compatible dictionary. Adapter artifacts are available through `context.framework_artifacts` or `context.artifact("openai_api", OpenAIApiArtifacts)`. Legacy `context.*_artifacts` read-only properties remain available for v0.11 plugin compatibility, raise `TypeError` on artifact type mismatch, and are scheduled for removal in v0.12.

**Plugin validation (v0.17+; six gates v0.18+).** Shipgate runs six load-time gates against every entry point — load, signature, metadata, dynamic-default-not-supported (v0.18+), ID-collision, and floor-consistency — before letting it produce findings. Metadata may use either `id` or `check_id` as the identifier key (the alias is symmetric with `Finding.check_id`); both names map to `CheckMetadata.id`. The `dynamic_default_not_supported` gate (v0.18+) rejects plugins declaring `AGENTS_SHIPGATE_METADATA.dynamic_default=True`: plugins have no path to wire into `core/dynamic_defaults.py:dynamic_check_defaults`, so a swing check would never receive a manifest-effective default and would be silently bypassable. This gate runs **before** `_coerce_metadata` so a plugin declaring `dynamic_default=True` without `floor_severity` lands here under a precise status rather than being mis-classified as `bad_floor`. Plugins that fail validation surface in `loaded_plugins[]` with a non-`valid` `validation_status` and human-readable `validation_errors`, and they do not run. At runtime, findings whose `check_id` does not match the declared plugin metadata are dropped and recorded under `loaded_plugins[].runtime_errors` — a plugin cannot smuggle findings under another check ID. Default behavior is lenient (record failures, continue scanning). Pass `--strict-plugins` to exit non-zero (code 4) when any plugin has a non-`valid` status or non-empty `runtime_errors`. See [STABILITY.md § Trust-model invariants](../STABILITY.md#trust-model-invariants) and [STABILITY.md § Severity-override floor](../STABILITY.md#severity-override-floor) (for the dynamic-default contract) for the full contracts.

## Declarative Policy Packs

v0.4 adds local YAML policy packs for organization-specific release rules.
Policy packs are static data and are safe to enable by default when declared in
`checks.policy_packs` or passed with `scan --policy-pack`. External rule IDs
must use a non-`SHIP-*` namespace such as `ORG-*`; `SHIP-*` is reserved for
built-in checks. Pack findings behave like built-ins for suppressions, severity
overrides, baselines, Markdown, JSON, and SARIF. Python plugins remain a
separate opt-in extension mechanism.

## OpenAI Agents SDK Static Extraction

SDK extraction is optional enrichment. Agents Shipgate detects Python functions decorated directly with `@function_tool`, `@function_tool(...)`, `@agents.function_tool`, `@openai_agents.function_tool`, or simple import aliases such as `from agents import function_tool as ft`, for example:

```python
@function_tool
def search_customer(customer_id: str) -> str:
    ...
```

When `tool_sources[].path` points at a directory, the extractor scans immediate
`*.py` files in sorted order; it does not recurse into nested packages. The
static extractor does not execute user code and intentionally does not detect
dynamic wrappers, factory-created tools, `Tool.from_fn()` style objects, runtime
imports, or dynamic tool lists. Declare those tools through MCP/OpenAPI inputs or
manifest metadata.

## Google ADK Static Extraction

Google ADK extraction is optional static enrichment. Agents Shipgate detects
Python `Agent` / `LlmAgent` definitions, literal function tools,
`FunctionTool`, `LongRunningFunctionTool`, `OpenAPIToolset`, `McpToolset`,
callbacks, plugins, sub-agents, and Agent Config YAML references where those
values are statically knowable.

The ADK extractor does not import user modules, run `adk`, connect to MCP
servers, fetch OpenAPI specs over the network, call tools, or call models.
Dynamic ADK toolsets produce source warnings and one ADK finding per unresolved
toolset unless explicit local MCP/OpenAPI/tool inventory inputs are provided.

## LangChain And CrewAI Static Extraction

LangChain/LangGraph and CrewAI extraction are optional static enrichment.
Agents Shipgate detects supported Python tool definitions, wrappers, agent
bindings, and local inventory files where those values are statically knowable.
CrewAI `BaseTool` class metadata may use literal strings or Pydantic-style
`Field(default="...")` assignments for `name` and `description`.

The extractors do not import user modules, import framework packages, run
agents, run graphs, run crews, connect to MCP servers, fetch specs over the
network, call tools, call models, or execute framework subprocesses. Dynamic
tool surfaces produce source warnings and framework findings unless explicit
local tool inventory inputs are provided. CrewAI prebuilt `crewai_tools.*Tool()`
references are emitted as low-confidence stubs and warnings; they do not by
themselves produce the dynamic-tools finding.

## n8n Static Extraction

n8n extraction reads only local workflow JSON exports/source-control files and
optional local stubs or evidence artifacts declared under `n8n:`. It does not
call a live n8n instance, run `n8n`, execute workflows, decrypt credentials,
connect to MCP endpoints, execute code nodes, or fetch network resources.

The adapter enumerates AI Agent tool sub-nodes, MCP Client Tool selections,
MCP Server Trigger exposed tools, Call n8n Workflow Tool entrypoints, Custom
Code Tool nodes, HTTP Request Tool nodes, and explicit inventories when those
surfaces are statically visible. Workflow triggers such as Webhook and Chat
Trigger are recorded as ingress evidence, not as tools.

Inactive workflows (`active: false`) are recorded as workflow evidence but are
not normalized as live tool or ingress surfaces; their workflow JSON is still
scanned for secret-like values. Workflow tags, error-workflow settings, and
node execution controls such as retry/continue-on-fail are preserved as
review metadata when present.

Credential names, workflow/node names, code bodies, request bodies, headers,
pinned data, static data, node notes, variable values, execution payloads, and
detected secrets are redacted or omitted from reports. Credential types and
credential IDs may be preserved as local release evidence.

## Conductor OSS Static Extraction

Conductor OSS extraction reads only local `schemaVersion: 2` workflow JSON
declared through `tool_sources[].type: conductor`. The MCP-core v1 adapter
normalizes literal `CALL_MCP_TOOL.method` call sites, records
`LIST_MCP_TOOLS`, `LLM_CHAT_COMPLETE`, `HUMAN`, and sub-workflow facts, and
recursively traverses switch/decision, loop, and fork containers.

The adapter never starts Conductor, executes expressions or inline code,
imports workers, connects to MCP/model endpoints, or treats runtime discovery
as a local inventory. Dynamic MCP bindings and unresolved sub-workflows produce
the Conductor finding above. HTTP/custom-worker/A2A/provider-native execution
is recorded as an unsupported capability and source warning in v1, so partial
coverage cannot silently produce `passed`. A `HUMAN` task is structural pause
evidence only; it does not prove reviewer identity or approval.


<!-- ===== source: docs/concepts.md ===== -->

# Concepts

The mental model behind Agents Shipgate, the deterministic merge gate for
AI-generated agent capability changes — a local-first, static Tool-Use
Readiness review.

For the product-level definition of a Tool-Use Readiness release gate, see
[`category.md`](category.md). For the agent-facing
walkthrough, see [`AGENTS.md`](../AGENTS.md).

## Tool-use readiness

**Tool-use readiness** is the static check that an agent's tool surface
is ready for promotion. It is *not* "did the tool call succeed" (a
runtime concern) or "did the model pick the right tool" (an eval
concern). It is the question a release reviewer answers at PR time:

> Given the tool surface declared in this PR, do we have explicit
> approval policies, scope coverage, idempotency evidence, and review
> readiness for every action — *before* promotion?

Tool-use readiness has seven dimensions. agents-shipgate produces
findings against each one.

| Dimension | What it asks | Evidence in the manifest |
|---|---|---|
| **Inventory** | What tools can the agent call? | A complete, named list — no wildcards, no "whatever this MCP server returns" |
| **Schema** | What inputs does each tool accept? | Strict JSON schema — `additionalProperties: false`, complete `required`, bounded numeric fields |
| **Auth** | What scopes does each tool need? | Declared per-tool or in `permissions.scopes` — narrower than the service account's actual scopes |
| **Approval** | Who reviews destructive actions before they fire? | `policies.require_approval_for_tools: [...]` for every write/destructive/financial action |
| **Side effects** | What does this tool change in the world? | Risk tags on the tool: `write`, `destructive`, `external_write`, `financial_action`, `customer_communication` |
| **Idempotency** | Can it be retried safely? | Idempotency key in the schema, documented retry policy, or explicit "do not retry" |
| **Blast radius** | If this tool fires unexpectedly, how bad is it? | Owner declared, prohibited actions enumerated, scope of resources bounded |

## Tool surface

The **tool surface** is the set of named, schemaed actions an agent can
invoke at runtime. It is declared via:

- Model Context Protocol (MCP) exports
- OpenAPI specs
- Framework-specific code (OpenAI Agents SDK Python, Google ADK, LangChain/LangGraph, CrewAI)
- API-specific artifacts (Anthropic Messages API tools.json, OpenAI
  Agents API function schemas)

The tool surface is a **release artifact** in the same sense as a
service deployment's binary or an API contract: it's a checked-in,
diff-able statement of what the agent can do, and it should be reviewed
on every PR.

## Manifest-first

agents-shipgate is **manifest-first**: the canonical claim about an
agent's surface lives in a single `shipgate.yaml` checked into the
repo. Every tool source the manifest references is reviewed at scan
time. There is one place to look for "what does this agent ship with."

This is intentional. Implicit configurations (e.g. "use whatever the
MCP registry returns") fail the inventory dimension above. The manifest
is what makes the release gate reviewable.

## Static vs dynamic

agents-shipgate is **static**. It does not run the agent, invoke the
model, call MCP servers, or make any network calls by default. Every
finding is derived from the artifact diff alone.

Static analysis covers the Tool-Use Readiness release slice. Dynamic concerns —
behavior under unusual inputs, runtime tool routing, latency,
hallucination — belong in evals, observability, and runtime guardrails.
agents-shipgate is additive to those, not a replacement.

## Where this fits in the wider stack

| Guard | When it runs | What it catches |
|---|---|---|
| Tests | CI on every PR | Code paths in the agent's *code* |
| Evals | On a schedule or per release | Model behavior on curated inputs |
| **agents-shipgate** | CI on every PR | Tool surface, scopes, policies, prompt/surface alignment |
| Runtime guardrails / gateway | At call time | Per-call policy enforcement |
| Observability | Runtime | What actually happened in production |

Each catches something the others can't. Removing any of them is a
regression.

## Related reading

- [`category.md`](category.md) — the product-level "what is an agent release gate"
- [`checks.md`](checks.md) — every check the scanner runs
- [`manifest-v0.1.md`](manifest-v0.1.md) — full manifest schema
- [`trust-model.md`](trust-model.md) — local-only guarantees and disclosure process
- [`glossary.md`](glossary.md) — category vocabulary


<!-- ===== source: docs/autofix-policy.md ===== -->

# Autofix policy

Which Agents Shipgate findings are safe to apply automatically, which
need human review, and how the per-finding metadata in `report.json`
maps to `apply-patches --confidence` flag semantics.

> **Audience.** AI coding agents driving verify-first PR checks or first-adoption helper flows
> (see [`agent-recipes.md`](agent-recipes.md)) and CI integrators
> deciding what to gate on.

---

## Semantic evidence gaps are outside the patch system

Starting with report v0.30, effect and authority evidence gaps live under
`release_decision.evidence_coverage.evidence_gaps[]`; they are not Findings and
never carry a `Patch`. Their `next_action` may show a reviewed declaration
template, accepted values, manifest path, and rerun command, but always carries
`suggested_patch_kind: "manual"`, `auto_apply: false`, and
`requires_human_review: true`.

An agent may route `declare_action_effect`, `declare_action_authority`,
`provide_complete_inventory`, or `resolve_semantic_conflict` to a human. It
must never invent or auto-fill the declaration. These two assertions are
published as `action_effect` and `action_authority` in contract v12's
`do_not_auto_assert[]`. Baselines, suppressions, severity overrides,
`--no-heuristics`, and human acknowledgement cannot close a semantic gap.

The Finding classes below apply only after this separate evidence-coverage
boundary has been checked.

---

## The four classes

Every active finding falls into one of four classes. The class is
encoded by the `autofix_safe` and `requires_human_review` fields on
each Finding, plus the `kind` and `confidence` fields on each
attached Patch.

| Class | Finding fields | Patch shape | v0.7 examples |
|---|---|---|---|
| **Safe auto-fix** | `autofix_safe: true`, `requires_human_review: false` | All patches non-manual AND high confidence | The 3 stale-manifest removals (`SHIP-MANIFEST-STALE-{SUPPRESSION,POLICY,RISK-OVERRIDE}`) when the match is unique |
| **Medium-confidence config fix** | `autofix_safe: false`, `requires_human_review: true`, `suggested_patch_kind: append_pointer/set_pointer` | Non-manual patch but at `medium` confidence | `SHIP-AUTH-SCOPE-COVERAGE-MISSING` scope appends |
| **Manual source/policy fix** | `autofix_safe: false`, `requires_human_review: true`, `suggested_patch_kind: manual` | `ManualPatch` with curated `instructions` | All other ~30 active checks (documentation, schema bounds, owner gaps, ADK/LangChain/CrewAI metadata, …) |
| **Never auto-fix** | `autofix_safe: false`, `requires_human_review: true`, `suggested_patch_kind: manual` | `ManualPatch` with explicit anti-pattern language | `SHIP-API-TRACE-{APPROVAL,CONFIRMATION}-MISSING` (flipping the trace patches the *evidence*, not the agent's runtime gate) |

Class four is a deliberate subset of class three — the distinction is
that an agent must NEVER attempt to "auto-fix" a trace finding by
editing the trace recording, even if the user asks. The
`ManualPatch.instructions` for these checks spell out the
anti-pattern in prose so even a curious operator gets the message.

---

## Catalog vs. Finding (the dual-source contract)

Two sources describe per-check remediation policy, and they answer
different questions:

| Source | Endpoint | What it answers |
|---|---|---|
| **CheckMetadata** | `agents-shipgate list-checks --json`, `agents-shipgate explain <ID> --json`, `docs/checks.json` | What an agent should *assume* when it has only the catalog and no scan output. Conservative across the board. |
| **Finding** | `agents-shipgate-reports/report.json` (per-finding) | What this *specific* instance produced. Can be more permissive than the catalog when the generator emitted clean high-confidence patches. |

**Catalog `autofix_safe` and `requires_human_review` describe the
worst-case per-check outcome.** A check whose generator USUALLY emits
a safe non-manual patch but falls back to `ManualPatch` in edge
cases (e.g. ambiguous duplicate matches in the stale-manifest
generators) keeps the safe-closed defaults at the catalog level. The
per-Finding fields tell the truth for that instance.

`suggested_patch_kind` at the catalog level is **informational** —
it documents the kind the generator *targets* when conditions are
clean, not what the report carries. An agent that sees
`suggested_patch_kind: "remove_pointer"` in `list-checks --json`
should still consult `Finding.patches` (or the per-Finding
`suggested_patch_kind`) to know whether this particular instance
actually produced one.

When in doubt, **trust the per-Finding fields over the catalog**
for any specific finding. The catalog is for static planning
("which check IDs *might* yield safe fixes"); the report is for
acting on a specific scan.

---

## Strict derivation rule

When a scan runs with `--suggest-patches`, every active finding
gets one or more attached patches and the four per-Finding fields
are derived from those patches with this rule:

```text
autofix_safe = True iff EVERY patch is non-manual AND has confidence == "high"
```

That is: a single `ManualPatch` mixed in, or a single `medium`/`low`
confidence patch mixed in, drops the entire finding to safe-closed.
The earlier "at least one safe patch wins" rule was unsafe — it
would have marked a `[high_remove, manual]` combination
auto-fixable while a ManualPatch still required review.

`suggested_patch_kind` is the kind of the **first non-manual patch**
even when ManualPatches are also present. (If ALL patches are
manual: `"manual"`. If the patches list is empty: `"none"`.)

`requires_human_review` is always the inverse of `autofix_safe`.

`docs_url` always comes from `CheckMetadata.docs_url`. Patches
don't carry per-instance documentation URLs.

### Catalog-driven escalation override

The strict derivation rule above can be **forced safe-closed** by a
per-check policy flag. When `CheckMetadata.requires_human_review_regardless_of_patch`
is `True`, `annotate_remediation` sets `autofix_safe=False` and
`requires_human_review=True` regardless of the per-patch derivation,
so `agent_action` lands at `propose_patch_for_review` (the patch is
still surfaced) instead of `auto_apply`. This catches the
approval/confirmation/idempotency, broad-scope, prohibited-action,
runtime-trace and HITL-evidence categories listed in
[`agent-autofix-boundary.md`](agent-autofix-boundary.md) §"Check-ID
mapping"; even a third-party patch generator emitting a clean
high-confidence non-manual patch on one of those check IDs cannot
auto-apply. The catalog is the contract: those check IDs always
escalate, regardless of how the patches were derived.

### Three patch states

| `Finding.patches` | Source of derived fields |
|---|---|
| `None` (scan ran without `--suggest-patches`) | CheckMetadata, with safe-closed fallback for unknown check IDs |
| `[]` (scan ran WITH `--suggest-patches` but generator emitted nothing) | Safe-closed shape, `suggested_patch_kind: "none"`. Does NOT fall back to catalog — the report carries no patches, so reporting a catalog-level kind would mislead. |
| Non-empty | Strict derivation rule above |

### Unknown check IDs (policy packs and third-party plugins)

A finding whose `check_id` isn't in the loaded catalog (a policy
pack rule, a third-party plugin emitted while plugins are disabled)
gets the safe-closed fallback when patches are absent:

```text
autofix_safe: false
requires_human_review: true
suggested_patch_kind: "manual"
docs_url: null
```

The fallback only applies when patches are absent. A high-confidence
non-manual patch from a policy pack still derives correctly.

---

## How `apply-patches --confidence` filters

`apply-patches` reads the report, filters patches by `--confidence`
and `--kinds`, and applies the survivors. Default flags:

```bash
agents-shipgate apply-patches \
    --from agents-shipgate-reports/report.json \
    --confidence high \
    --kinds set_pointer,append_pointer,remove_pointer \
    --apply
```

| Flag | Default | What it accepts |
|---|---|---|
| `--confidence` | `high` | Minimum patch confidence. Patches below this are skipped. |
| `--kinds` | `set_pointer,append_pointer,remove_pointer` | Patch kinds to include. ManualPatch is filtered out unconditionally — even with `--kinds manual`. |
| `--apply` | (off) | Without this, dry-run only. Always preview before mutating. |

So in v0.7 with the default flags:

- The 3 stale-manifest removals (when unambiguous) auto-apply.
- `SHIP-AUTH-SCOPE-COVERAGE-MISSING` scope appends are **skipped**
  (medium confidence). Pass `--confidence medium` to opt in — but
  read the appended scopes before merging, since adding scopes can
  encode policy choices.
- Trace approval/confirmation findings are **never** applied —
  ManualPatch is filtered out.
- Everything else with a ManualPatch is **never** applied.

`apply-patches` enforces a **containment check**: every patch's
`target_file` must resolve under `report.manifest_dir`. Anything
outside aborts with exit code 5 before any SHA verification.

---

## Decision tree for agents

When walking `findings[]` from a `--suggest-patches` report:

```text
for finding in active_findings:
    if finding.suggested_patch_kind == "manual":
        # Manual source/policy fix or never-auto-fix.
        # Read finding.patches[0].instructions and surface to user.
        # Do NOT attempt to auto-edit, especially for trace findings.
        surface_to_user(finding)
        continue

    if finding.suggested_patch_kind == "none":
        # Scan ran with --suggest-patches but the generator emitted
        # nothing for this finding (empty patches list — see "Three
        # patch states" above). There's nothing to apply via
        # apply-patches at any confidence level. Surface for human
        # triage instead.
        surface_to_user(finding)
        continue

    if finding.autofix_safe is True:
        # Safe to include in the next `apply-patches --confidence high`.
        plan_to_apply(finding)
        continue

    # Medium-confidence non-manual patch (e.g. scope coverage).
    # Surface as "review and run apply-patches --confidence medium"
    # but do not auto-apply on the high-confidence path.
    surface_for_medium_review(finding)
```

After running `apply-patches --apply`, re-run `scan` to confirm the
fixed findings are gone. The `run_id` will only change if the
manifest or tool surface actually changed — patches are excluded
from the hash so toggling `--suggest-patches` doesn't shift it.

---

## See also

- [`agent-autofix-boundary.md`](agent-autofix-boundary.md) — the
  *behavioral* counterpart to this *mechanical* page. What an agent may
  assert in a PR comment or review summary, beyond which patches
  `apply-patches` will run.
- [`agent-recipes.md`](agent-recipes.md) — copy-pasteable AI-agent
  workflows, including the soft-stop rule for `detect`.
- [`report-reading-for-agents.md`](report-reading-for-agents.md) —
  reader's primer for `report.json`.
- [`checks.md`](checks.md) — full check catalog with rationale.
- [`minimal-real-configs.md`](minimal-real-configs.md) — per-framework
  minimal manifests to build from.
- [`report-schema.v0.43.json`](report-schema.v0.43.json) — current JSON
  Schema for `report.json`.
- [`AGENTS.md`](../AGENTS.md) — top-level agent instructions, install,
  trigger table.
- [`STABILITY.md`](../STABILITY.md) — what won't break across `0.x`.
