Create a starter manifest:
agents-shipgate init --workspace . --writeThen inspect the sources before running checks:
agents-shipgate doctor --config shipgate.yamlEvery command that takes --workspace refuses a path that is not there, with
config_error and exit 2, before it creates anything. Two causes, both common
on a first run:
- The clone has not happened yet. Preview is the first command in the
adoption path, so it is easy to run one step early. Clone first, then point
--workspaceat the checkout. - The relative path resolved against a different directory. The message names the directory it resolved against for exactly this case.
--workspace is not a directory: <path> is a different mistake: the path
exists but is a file. Point the option at the checkout root, not at a file
inside it (shipgate.yaml goes to --config).
| Manifest state | Message | Recovery |
|---|---|---|
| Not on disk | Config file not found: <path> in <cwd>. |
init --workspace . --write — the next_action bootstraps rather than edits. |
| Present but empty | Config file is empty: <path>. |
Edit the file in place; do not re-run init, which refuses to overwrite. |
| Present, valid YAML, not a mapping | Config file must contain a YAML object: <path> |
Edit the file in place. |
If you see "must contain a YAML object" for a file you never created, that is a bug — the three states are asserted to stay distinguishable.
Read the start of the refusal: it says why the workspace could not be read, and
next_actions[0] follows from that.
Static worktree collection refuses executable Git filters (filter.lfs.clean),… refuses Git filter attributes … (assets/logo.bin),… refuses repository-local diff.* configuration … (diff.git-crypt.textconv)or… non-empty or aliased .git/info/attributes …. The repository's own Git configuration runs programs, or rewrites what Git reports, while the working tree is read, and Shipgate does not read a working tree under it. Git LFS and git-crypt repositories are configured this way on purpose. Noverifyrun changes the answer, with or without--head: the refresh reads the working tree for every pointer, soverifyandagent controlcannot give a current answer in this repository while the configuration is in place. The next action is areviewstep for a human, not a rerun. Do not remove LFS or git-crypt configuration to get past it.agents-shipgate diffstill shows what the change does to the agent's host grants, read-only.The reports directory … holds …: see One Repo, Several Agent Projects for the output-directory rule.… could not be read (body_limit_exceeded),metadata_limit_exceededorgit_timeout: the uncommitted change is larger than Shipgate reads, or Git did not finish in time. Commit or shrink the change (keep generated output out of the working tree), then re-run theverifycommand the next action names.Workspace is not inside a git checkout: …:.gitis missing or Git refuses the checkout. Every pointer that binds a Git identity refuses until the checkout reads again; averify --previewrun outside Git is unaffected.
Use this before reading the full manifest schema.
| Symptom | Diagnostic ID | Fix |
|---|---|---|
detect --json returns is_agent_project: false, but suggested_sources has MCP or OpenAPI files |
SHIP-DIAG-MCP-OPENAPI-ARTIFACT-ONLY |
Continue to init --workspace . --write; artifact-only repos are valid Shipgate targets. |
detect --json returns is_agent_project: false, but codex_plugin_candidates is non-empty |
SHIP-DIAG-CODEX-PLUGIN-PACKAGE-DETECTED |
Continue to init --workspace . --write; Codex plugin repos are valid static Shipgate targets. |
host_boundary_candidates is non-empty while framework and tool-source lists are empty |
Host-only discovery (contract 33) | Follow control.next_action to audit --host; no manifest is needed. Config directories require inspection. |
host_discovery_incomplete_paths is non-empty |
Host traversal the census could not see through | Inspect the named paths — resolve an unfollowed directory link, make an unreadable directory readable, or select the actual project directory. Empty candidates do not prove absence; the rest of the classification is unaffected. |
doctor shows zero tools |
SHIP-DIAG-ZERO-TOOLS |
Check tool_sources[].path, MCP tools[], OpenAPI paths, optional source parse warnings, and dynamic toolset warnings. |
| SDK/framework extractor finds no tools | SHIP-DIAG-DYNAMIC-TOOLSETS-ONLY |
Add an explicit MCP export, OpenAPI spec, or local tool inventory instead of relying on dynamic code discovery. |
shipgate.yaml still has CHANGE_ME |
SHIP-DIAG-CHANGE-ME-PLACEHOLDERS |
placeholders[] gives locations only. control.next_action.actor routes the turn: "human" means surface all of them and stop, and its why may end and N more in placeholders[], so absence from it grants nothing. agent.name is repository reading a coding agent may do; agent.declared_purpose must be supplied by a human — Shipgate never invents a declaration nobody made, so do not derive it from a prompt, main agent file or README. |
Required tool_sources[].path does not resolve |
SHIP-DIAG-MISSING-SOURCE-FILE |
doctor --json reports unresolved_sources: [...] and a diagnostic with kind="edit", path="shipgate.yaml:<line>". Fix the path. (scan still exits 3 on the same condition — fix it before scanning.) |
init --write exits 2 with manifest_status: "refused_unresolved_scope" |
— | The manifest scope is unresolved: agents in more than one self-contained project, or discovery capped before it could tell. Re-run with --workspace pointed at one of auto_detected.agent_project_candidates[].path. See One repo, several agent projects. |
| Install fails in an older project environment | — | Agents Shipgate requires Python 3.12+. Install with pipx or uv using a Python 3.12+ interpreter. |
| A fix or an upgrade behaves as though it never happened; a documented subcommand looks "missing" | — | Another copy is answering. doctor --json reports environment.mismatches[] with the interpreter, the imported package, and a runnable recovery command. See diagnostics.md. |
agents-shipgate exits with ModuleNotFoundError or "bad interpreter" before printing anything |
— | The console script outlived the environment that backed it, so nothing in Shipgate can report it. Run python -m agents_shipgate doctor --json with a working interpreter: it names the stale wrapper as console_script_interpreter_missing. Then reinstall or remove that wrapper. |
| Reports show up as untracked files | — | For durable adoption, add agents-shipgate-reports/ to .gitignore. For a temporary external-repository assessment, rerun setup with init --local-review; it uses .git/info/exclude and reports its cleanup paths without editing tracked files. |
When run under AGENTS_SHIPGATE_AGENT_MODE=1, errors carry a next_actions: [...] array alongside the legacy next_action: str. The full catalog and schema is in diagnostics.md.
A shipgate.yaml describes one agent surface: one agent.name, one
declared_purpose, one set of tool_sources. In a repository that holds
several self-contained agent projects — the python/agents/<name>/ layout of
a samples monorepo, an apps/* or packages/* workspace — a manifest written
at the repository root declares all of them at once, and the alignment layer
that compares what an agent says it does against what it can do has nothing
left to compare.
So Shipgate scopes rather than guesses:
verify --previewderives the--workspaceit recommends from the changed paths — committed and uncommitted, because the command it emits runs against the working tree. Each is attributed to the nearest directory at or above it that carries a project marker (pyproject.toml,package.json,go.mod,Cargo.toml,pom.xml, …), and the recommendation is that project when exactly one is claimed. Run its command verbatim. Changes spanning two projects, or sitting at the repository root, keep recommending the root as before. Documentation and tests cannot outvote code: where two projects are claimed, one claimed only by documentation or test paths drops out — the trigger catalog's docs-only rule decides which those are — so a README edited beside a sibling project does not send the answer back to the root. When the changed project already carries its ownshipgate.yaml, preview routes you toverifythere instead of to setup.- An unconfigured project routes to durable
init --writeby default. For a temporary external assessment, explicitly preview with--config .agents-shipgate-local-review.yaml; that route creates.agents-shipgate-local-review.yaml, privately excludes it and the report directory through.git/info/exclude, and leaves the worktree clean. Its verifier findingSHIP-VERIFY-LOCAL-REVIEW-PROVISIONALmakes the one release decision review-required, so a structurally valid local manifest cannot look like durable release policy. Runinit --local-review --undo --jsonto remove the local manifest and exclusion block while preserving reports. - Preview reads project markers from the working tree, because that is the
tree the
initit recommends would write to. Soverify --preview --head <ref>claims no scope whenever<ref>is not the commit the worktree has checked out — whatever that worktree happens to hold, since the two trees can disagree and only one of them is the treeinitwould run against. (On the reported pull request they disagreed completely: the changed directory existed only on the PR branch.) That is not a verdict about the change: the control route isagent_action_requiredwith afetch_baseaction whoseexpectsnames what is missing: a commit id, not the ref you passed. Check that commit out, then re-run the preview with the pinned--base/--headthewhyspells out. The pinning is load-bearing —--head HEAD~1names one commit before the checkout and its parent after, so a route spelled with the expression would walk history backwards.agent controlrefuses the old pointer once you check out, so a refresh-driven caller re-runs preview rather than repeating the checkout. Plainverifyis unaffected — it reads--headfrom the object database, not the worktree. init --writerefuses a workspace whose agents live in more than one project:manifest_status: "refused_unresolved_scope", exit2, and nothing written — no manifest, no CI workflow, no agent-instruction snippets, no.gitignoreblock.auto_detected.agent_project_candidates[]lists each project and the agent names inside it; pick the one your change belongs to and re-run with--workspace <that path>.--allow-unresolved-scopewrites the single root manifest anyway. Use it only when one agent surface genuinely spans those directories.- Reports follow the workspace:
verify --workspace apps/awritesapps/a/agents-shipgate-reports/, which is what the managed.gitignoreblock in that project covers, and two projects in one repository never overwrite each other's results. An explicit--outis your path: absolute as given, or relative to the directory you run the command in, not the repository root (#818). Run from anywhere else,verifyprints anote:on stderr naming the directory it now writes to and the one1.0.0wrote to; pass that absolute path to keep the old location. The artifact pathsverify --format jsonprints open from the directory you ran it in. verifyexits2withVerifier --out <dir> holds tracked repository files(orheld committed repository files that the change being verified removes,holds untracked files that Git does not ignore and that are not Shipgate artifacts,lies inside a trust root (…) that Git does not ignore, orholds trust-root files named like Shipgate artifacts) when the output directory is inside the repository and holds anything besides uncommitted Shipgate artifacts outside any trust root. The run leaves that directory out of the change it decides on, so--out docsor--out .claude/commandswould hide those files from the decision. Omit--out, or name a directory that is gitignored or outside the repository; do not gitignore, untrack or move that directory's files to get past it.agent controlrefuses a pointer already sitting in such a directory asworkspace_unverifiable, and its recovery is the producing run's command without--out.- The same refusal for the default directory reads
The default verifier output directory agents-shipgate-reports holds …(orVerifier --out agents-shipgate-reports holds …when--outnames it, as the Action does). Omitting--outwould publish back into it, so it is never the advice there. Move stray files that are not Shipgate artifacts out, or gitignoreagents-shipgate-reports/asinitdoes. Committed reports need a change that runsgit rm -r --cached agents-shipgate-reportsand gitignores the directory; a worktreeverifyof that change still finds the removal, so verify it with--outnaming a directory that is gitignored or outside the repository until it merges. The Action's--headrun passes. Reports thatskill lint,skill securityandskill reviewwrite there are Shipgate artifacts and are not refused. The Claude Code Stop hook runsverifyinto the default directory and reports this exit2as advisory context without blocking the Stop, so resolve the refusal rather than relying on the hook. - With
--ci, each manifest gets its own workflow —agents-shipgate.ymlfor a repository-root manifest,agents-shipgate-<project>.ymlfor a scoped one. The action takes a singleconfig, so one shared file would gate whichever project initialized first and leave the rest ungated. Readworkflow.pathrather than assuming the name. agent_scope: "unknown"is the same refusal for a different reason: discovery stopped at its Python-file cap in a workspace with several project roots, so a "single project" answer would just be whichever files were read first. Raisedetect --max-python-files, or name the project directly.agent_scope_truncated: truesays the candidate list is a lower bound, not an enumeration: the Python parse stopped at its cap, so a project in the part of the tree that was never read is missing from it. If the project you are changing is not listed, that is the first thing to rule out — re-run withdetect --max-python-files <n>before concluding it is not an agent project.workspace_signals.project_root_countis the uncapped count the list is measured against.agents-shipgate detect --jsonanswers the same question without writing anything: readagent_scope,agent_scope_truncated, andagent_project_candidates[]. On"ambiguous"with a complete parse itsnext_actions[]carries the same shapeinit's refusal does — the decision first (kind: "review", no command, because naming one candidate would make the pick Shipgate declines to make), then one exactinit --workspace <candidate> --write --jsonper candidate, each withexecutable/args. Match on the path of the project you are changing and run that entry; the workspace root is never among the commands, since that is the scopeinitrefuses — it gets areviewentry of its own instead, because--allow-unresolved-scopeadopts the whole workspace rather than that one agent. Every candidate gets an entry — there is no display cap on the routing — and one that already carries a manifest getsdoctor --config <that manifest> --json, becauseinit --writethere would refuse a file it will not overwrite. A truncated parse outranks all of it: rank 1 is then the higher-capdetectbelow, and no candidate commands are offered, because the list they would be built from is a lower bound.python_parse_truncated: trueis the wider version of the same warning: the Python parse stopped at its cap, so any whole-workspace negative — most of allis_agent_project: false— describes the files that were read rather than the repository. Nothing publishes a terminal route while it is true; the emittednext_actions[0]isdetect --max-python-files <workspace_signals.python_file_total> --json, which covers every Python file and so cannot land back here.
One manifest per project directory is the supported monorepo shape; each is
verified with verify --workspace <project> --config shipgate.yaml from
anywhere in the repository.
Common causes:
tool_sources[].pathpoints at the wrong file.- The MCP export does not contain a
toolsarray. - The OpenAPI document has no
pathsobject. - The source is marked
optional: trueand failed to parse. - A Google ADK source uses dynamic toolsets without explicit MCP/OpenAPI/tool inventory inputs.
Run with verbose logs:
AGENTS_SHIPGATE_LOG_FORMAT=json agents-shipgate doctor --config shipgate.yaml --verboseThe OpenAI Agents SDK extractor is AST-only. It recognizes direct function_tool decorators and simple import aliases such as:
from agents import function_tool as ft
@ft
def lookup_customer(customer_id: str) -> str:
...It intentionally does not execute imports, factories, dynamic wrappers, Tool.from_fn() calls, or dynamic tool lists. Declare those tools through MCP/OpenAPI inputs or manifest metadata.
Agents Shipgate never runs ADK or connects to MCP servers. For ADK McpToolset
or dynamic OpenAPIToolset usage, provide explicit local review inputs:
tool_sources:
- id: adk
type: google_adk
path: agent.py
- id: support_openapi
type: openapi
path: specs/support.openapi.yaml
google_adk:
tool_inventories:
- path: inventories/adk-mcp-tools.json
source_id: adksource_id names the tool source the inventory completes. Omit it and the file
becomes an independent source: entries sharing a name with an already-extracted
tool are added beside it, which grows the catalog and leaves the
incomplete_surface gap that asked for the inventory open. See
Tool Inventories.
Static tool_filter values reduce ADK MCP risk, but they do not enumerate the
tool schemas by themselves. Add an inventory when reviewers need full schema
evidence.
Static OpenAPI spec resolution covers simple literal-path idioms such as
Path("spec.yaml").read_text() and open("spec.yaml").read(). Module-relative
patterns such as Path(__file__).parent / "spec.yaml" are treated as dynamic
by the static extractor. Declare those specs under tool_sources or provide a
local inventory
artifact when you want them resolved by the scanner.
Suppress it with a reason:
checks:
ignore:
- check_id: SHIP-SCHEMA-BROAD-FREE-TEXT
tool: support.search_kb
reason: "Search query intentionally accepts free text."Suppressed findings remain in JSON with suppressed: true and are excluded from active severity counts.
Use risk_overrides to add or remove tags:
risk_overrides:
tools:
refund_status_lookup:
tags: ["read_only"]
remove_tags: ["financial_action"]
reason: "This endpoint only reads refund status."