Skip to content
Back to skills

Conformance Debug

ASecurity

Fast, targeted debugging of the Harness Conformance Lab (conformance/) — use whenever a conformance check fails (locally or in CI), a check is recorded as a declared limitation, a harness version drifted, a harness TUI check needs poking at directly, or you're about to guess at a fix instead of reproducing the real failure. Covers reading the failure's shape, checking the vendor's current docs/changelog/issues before touching code, the --sandbox and --experiment fast loops, proving the provid...

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 27, 2026
ai-agentstypescriptpythonrustgoshellbashnextjsdockertestingdebugging

Works with

  • claude code
  • terminal
  • cli
  • api
  • mcp

Security analysis

A100/100

Scanned October 7, 2026

npx -y skills add uze-sh/uze --skill conformance-debug --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Conformance Debug?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Conformance Debug
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/uze-sh-conformance-debug/badge)](https://www.skillsdirectory.com/skills/uze-sh-conformance-debug)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: conformance-debug
description: Fast, targeted debugging of the Harness Conformance Lab (conformance/) — use whenever a conformance check fails (locally or in CI), a check is recorded as a declared limitation, a harness version drifted, a harness TUI check needs poking at directly, or you're about to guess at a fix instead of reproducing the real failure. Covers reading the failure's shape, checking the vendor's current docs/changelog/issues before touching code, the --sandbox and --experiment fast loops, proving the provider speaks the real wire shape with --discovery and mitmproxy, telling a vendor limitation from a Lab defect, and reading evidence/verdict.json.
---

# Debugging the Harness Conformance Lab

The Lab runs the real vendor CLI (Claude Code, Codex, OpenCode, Antigravity)
against a synthetic provider in a disposable Docker container. What every
harness must prove lives in `conformance/contract/` (outcome terms, no
vendor named); how each harness is driven lives in
`conformance/harnesses/<vendor>/bindings.py`; what is genuinely unique to a
vendor stays in its `scenarios.py`. A full gate run
(`python3 conformance/lab.py --harness <h>`) takes 6-11 minutes and only
tells you pass/fail per check. **Never iterate against that loop.** There
are seconds-long loops built for exactly this — reach for them first.

The Lab's whole value is that a green is real. Two things make a green
false, and both have shipped for months before: a check that passes
because nothing happened (a hook that never ran, a tool call the harness
rejected before executing), and a provider that speaks a wire shape the
real API does not, so the harness silently does something else than it
would in production. Every step below exists to catch one of those.

## Step 0 — is the world still the one the Lab assumes?

Do this **before** touching code, on any failure, any ADAPTED, and any
`VERSION DRIFT` line in the run summary. The Lab pins nothing: the image
installs each vendor's channel-latest, so a run that fails "for no reason"
is usually the vendor moving.

- **Read the vendor's current docs and changelog — the ones that shipped
  with the binary you are testing, not the ones in your memory.** Several
  ship inside the CLI: Antigravity keeps its customization docs under
  `~/.gemini/antigravity-cli/builtin/skills/agy-customizations/docs/`
  (`hooks.md`, `plugins.md`, `skills.md`, `json_configs.md`, …) and prints
  release notes with `agy changelog`; Claude Code's are at
  `code.claude.com/docs` (skills, hooks, plugins); Codex and OpenCode
  publish theirs on their sites and repos. A one-line `--sandbox -- "agy
  changelog | head -80"` or a `WebFetch` of the docs page is cheaper than
  any hypothesis.
- **Search the vendor's GitHub issues** for the exact symptom (the error
  text the TUI rendered, the log line, the flag name). A known regression
  with an issue number is evidence to pin a version against; a fixed one
  tells you the next channel bump resolves it.
- **Treat every ADAPTED entry in `conformance/evidence/expected.json` as a
  question, not a fact.** Each says "this harness could not do X when we
  last looked". On every version drift, and whenever you touch the area,
  re-ask it against the current docs: the control may now exist (Claude
  gained `user-invocable: false`; the Lab kept declaring it unverifiable
  because the binding read the wrong surface). An ADAPTED that has become
  false is a false green in the making — the gate escalates a registered
  ADAPTED that starts passing precisely so you cannot miss it.
- **Assume nothing about the wire.** Harness behaviour that depends on the
  provider's response shape (streaming events, tool-call argument names,
  which request of a turn is the user's) changes across vendor versions.
  Claude Code accumulates a tool's input only from `input_json_delta`;
  Antigravity 1.1.24 validates a scripted call against the tool's declared
  `parametersJsonSchema` before any hook runs, and makes a side call to a
  lighter model with no tools before the user's turn. A provider that
  counts requests or invents argument names produces turns that "settle"
  with nothing having happened. See Step 3.

## Step 1 — read the shape of the failure

- **The same first check fails identically across all four harnesses**
  (e.g. `tui-reached-prompt` / `tui-started`, with a near-empty first
  `.raw` capture) → the shared **setup script** died before any harness TUI
  ever launched. Do not touch TUI-driving/wait logic — the bug is in the
  setup steps every vertical shares (`uze market add`, `uze plugin install
  ...`), or in `uze` itself.
- **Only one harness fails** → the bug is specific to that harness's
  `crates/uze-integrations/src/<vendor>/` module, its `bindings.py`, its
  `scenarios.py`, or its provider — or the vendor moved (Step 0).
- **A late-stage check fails after earlier ones pass** → the TUI/harness
  came up fine; the bug is in that specific interaction (skills, MCP,
  hooks), not in provisioning.
- **A presence check fails while its absence checks pass** (e.g.
  `hooks-*-denial-relayed` red, `hooks-*-marker-absent-*` green) → the turn
  ran with nothing intercepted. The absence checks are gated on the
  presence check for this reason; read what the harness rendered
  (`Invalid tool parameters`, `invalid arguments`, a permission prompt)
  and go to Step 3.

The setup scripts run with `set -e` and `>/dev/null 2>&1` on purpose (a
clean gate run must be quiet) — which means a setup failure is **silent**
in the normal evidence: a 0-byte or garbage-looking first `.raw` file is
the signature of "the container died during setup," not a TUI bug. Don't
debug the wait loop for a dead process.

## Step 2 — reproduce in seconds, not minutes

`lab.py` has a sandbox mode built for this (`--help` documents it in full):

```bash
# Runs one command inside the real, fully-provisioned harness container
# (fixture marketplace + synthetic provider already up), then tears down.
python3 conformance/lab.py --harness <claude|codex|opencode|antigravity> \
  --sandbox -- "<shell command>"
```

Use it to poke at the exact step you suspect, e.g.:

```bash
python3 conformance/lab.py --harness opencode --sandbox -- \
  "uze market add /work/market >&2; uze plugin install mcp-plugin@uze-lab; uze doctor"
```

This isolates "did `uze` do the right thing here" from "did the TUI render
the right thing," in seconds, with real (uncensored) stdout/stderr.

- The sandbox shell is bare: set `PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/.local/bin`
  and copy the vendor fixtures the way `scenarios.py`'s setup fragment does
  (Antigravity: `settings.json`, `jetski_state.pbtxt`, `installation_id`
  into `~/.gemini/antigravity-cli/`) before running the vendor CLI.
- `--shell` instead of `-- cmd` for an interactive rootless shell in the
  container (needs a real TTY — from a non-interactive agent session, use
  `-- cmd` instead). `--keep` leaves the provider + network alive between
  rounds.
- To test a **vendor CLI's own behavior** in isolation (does it parse this
  path/argument the way we assume? what does its validator say about this
  file?) — the sandbox drops you next to the real vendor binary. Never
  guess from docs or your own priors when a one-line call gets you the
  real exit code, stderr, or log. `agy plugin validate <dir>` counted 1
  hook where UZE meant 3, which is how the `hooks.json` wrapper bug was
  found; `agy -p "/hooks" --output-format json` lists what the harness
  actually registered; `--log-file` gives you the vendor's own log.
- **A hypothesis that needs a real turn is an experiment**, not a sandbox
  command: `conformance/experiments/<vendor>/<name>.py` gets the
  provisioned topology, starts its own provider in the mode it needs
  (`common.start_provider(cfg, "toolcall", {...})`), drives a headless or
  TUI turn, and records its own checks under the run outdir — outside the
  canonical suite, versioned in the repo as evidence. Run with
  `lab.py --harness <h> --experiment <vendor>/<name>`. Switch behaviour
  with environment variables rather than editing the file between runs.
  `experiments/antigravity/hook-tui.py` is the template: it names its
  container so the vendor's log can be read mid-session with `docker exec`.

## Step 3 — prove the provider speaks the real wire shape

The synthetic provider is the Lab's model of the vendor's API. If it drifts
from what the real API sends, the harness is not being tested — a
different program is. Whenever a turn "settles" without the expected
effect, or a harness rejects a scripted call, read the wire:

```bash
python3 conformance/lab.py --harness <h> --discovery      # any mode: sandbox, experiment, vertical
```

`--discovery` appends every raw request the harness sent to the provider
to `<outdir>/raw-requests.log`, across every per-phase provider restart.
Read the request that carries the tool result: Antigravity's
`functionResponse.output` said *"missing properties 'Cwd',
'WaitMsBeforeAsync', 'CommandLine', 'toolSummary', 'toolAction' —
additional properties 'command' not allowed"*, which named the fix. Read
the request's `tools`/`functionDeclarations` (or Anthropic `tools`) for
the schema the harness declared — script calls in **that** shape, never
one you remember. Read which request is the user's turn (roles, declared
tools, `<USER_REQUEST>`) and make the provider answer by content, the way
a model would, never by request count.

For the shape of the **response** side — what the real API streams, which
the Lab cannot capture offline — use the observation tooling in
`conformance/discovery/` (mitmproxy addons that sanitize at capture time,
`run_agy_obs.sh` for a real authenticated session through the proxy). It
is observation-only and its raw captures never enter the repository; what
it teaches becomes the provider's contract, written down in the provider's
module docstring with the vendor version it was observed on (see
`harnesses/claude/provider.py`: streaming event sequence, `input_json_delta`,
no `data: [DONE]` for Gemini).

The checklist for any provider change:

1. The request the harness sends after a tool call carries the tool's
   *real* result (proof marker, stdout marker, denial reason) — not an
   error string. Grep `raw-requests.log` for it.
2. The provider answers the user's turn, not the harness's side calls
   (title generation, summaries, a lighter model with no tools).
3. Streaming events match what the vendor's SDK accumulates.
4. A structural summary (`struct.json`) records *presence* of markers per
   request; aggregate with `common.observed_markers` (a union), never
   last-write-wins.

## Step 4 — vendor limitation, or Lab defect?

Only declare a limitation when the vendor itself cannot do it **in a way
you have measured**. A declaration goes through `declare` (or
`contract/declared.py`'s `presence`/`absence`), whose `holds` is the
measurement of this run, never a constant: the lint refuses a literal.
Register it in `evidence/expected.json` with its reason, its measurement
and the versions it was observed on; `*` is refused. The gate then reads
each run's measurement: a registered one that still holds passes, one that
holds on a version not listed passes as a repin and the run writes
`expected.next.json`, one whose measurement found the control escalates,
and one whose measurement never ran is unproven. The tools for the
measurement:

- **A control probe in the vendor's own format at the vendor's own path,
  with no UZE in the loop.** If a vendor-format deny hook at
  `~/.gemini/config/hooks.json` is loaded, listed by `/hooks`, and never
  executes — not even a `touch` per event — then no UZE delivery can be
  observed there, and the vertical must measure that gate at run time
  (`hooks > vendor` in the Antigravity vertical) rather than assume it in
  a spec. A live precondition is the only declaration that can expire on
  its own: the gate escalates the day the vendor opens it.
- **The same probe for a rendering or a prompt.** Whether a harness shows
  a hook's denial as a decision was settled by a hand-written hook in the
  user's own `settings.json` (`experiments/claude/deny-render`): Claude
  2.1.290 showed the documented JSON decision as "hook error" too, so the
  check became a registered declaration instead of a UZE fix. The image
  has no Python or jq; an experiment edits vendor JSON with `sed`.
- **Pin the previous image.** `UZE_LAB_IMAGE=<image id> python3
  conformance/lab.py ...` runs the same scenario against the harness
  version that last passed. Same result on both → it was never the
  vendor's regression (and probably never worked: check the old green for
  vacuity). Different → a regression, pin the version in the registry and
  file/reference the vendor issue.
- **Read the vendor's log and binary before concluding "impossible".**
  `--log-file` shows what loaded and what surfaced; `strings` on the binary
  (copy it out with `docker cp`) shows the flags and proto fields that
  gate a feature (`CustomizationConfig.enable_json_hooks` was found this
  way). A gate fed by a server-side feature provider the offline Lab never
  receives is a real limitation; a gate that is a documented setting is a
  fixture to set.

## Step 5 — after any Rust change, rebuild the Lab image before trusting a run

The image bakes a release build of `uze` plus the real vendor CLIs. A stale
image silently tests your *old* code:

```bash
docker build -f conformance/Dockerfile -t conformance-harness:latest .
```

Rebuilds are fast when only `crates/`/`src/` changed (cached apt/provision
layers); expect ~30-40s from a clean base, ~5-10s incremental. Python under
`conformance/` (providers, scenarios, bindings, contract) runs on the host —
no rebuild needed. The fixture marketplace is baked into the image, so a
fixture added since the last build fails `materialize_marketplace` before
the harness starts; rebuild, or run with
`UZE_MARKETPLACE_MOUNT=$PWD/conformance/_fixtures/marketplace`.

## The measured vocabulary

What a harness calls its tools is measured, never remembered:

- `capture.declared_tools` records every tool each request declares to the
  model (`declared-tools.json`); a scripted call naming a tool the request
  did not declare is refused and recorded (`undeclared-calls.json`).
- The `hook-rows` fixture's census group (no matcher) records the tool
  name and input every hook receives (`hooked-tools.json`); each alias has
  its own row group, and `RowsFixtureTest` fails when one is missing.
- `harnesses/<h>/vocabulary.json` is the Lab's own expectation, written
  independently of UZE's `ToolBinding` tables; `vocabulary-*` checks hold
  both against the capture, and Rust's `hooks/measured_tests.rs` holds the
  tables against the committed snapshot (`evidence/tools/<h>.json`).
- `--record-vocabulary` rewrites the snapshot from the run; the diff is the
  review of what the vendor changed. An `optional` tool (offered only on
  opt-in) is judged by the session that scripted it, never by the run's
  union: an agent that lists a tool in its own definition is offered it.

## Prompts and markers

- Every prompt a person meets is answered on screen through the binding
  (`approval_prompts`, `prepare`), the way a person answers it. A flag,
  a seeded fixture answer or a bypass needs a `# decision: <id>` naming an
  entry under "Prompts the Lab answers" in `conformance/DECISIONS.md`; the
  lint fails otherwise.
- A marker is a nonce no harness writes on its own; the lint refuses a
  common word and a marker inside a scripted call's arguments.

## Reading evidence

Each run's `outdir` (default `/tmp/harness-conformance/<harness>/run<N>/`,
or `$AGY_OUTDIR`) holds:

- `NN_<checkpoint>.raw` / `<phase>.raw` — plain-text screen snapshot at
  that checkpoint. Empty/near-empty on an early one is the setup-died
  signature (Step 1). The screen helper returns only the bytes that
  arrived since the last read; a snapshot is what one wait consumed.
- `<phase>.typescript` + `.timing` — full terminal recording (`script(1)`
  format); replay with `make lab-replay`, or strip ANSI and read the tail
  to see what the harness rendered right before a turn stalled (a
  permission prompt, `Invalid tool parameters`, a survey).
- `<phase>_struct.json` — the provider's structural summary per request
  (path, roles, declared tools, marker presence). Which model/path each
  request hit tells you which request was the user's turn.
- `raw-requests.log` — with `--discovery`, the verbatim requests.
- `verdict.json` — structured result per check, including `gate.adjudication`
  (`asserted`, `known_declared`, `repin`, `unregistered`, `wildcard`,
  `unproven`, `escalated`) and the run manifest
  (harness/uze/image/fixture versions — check `version_drift` first if
  nothing else changed).

## Don't

- Don't iterate against the full gate run (`lab.py --harness <h>` with no
  `--sandbox`/`--experiment`) while narrowing down a cause — that's the
  ~10-minute loop, reserved for confirming the fix at the end.
- Don't assume vendor CLI or API behaviour from memory, or from docs older
  than the binary under test, when a one-line sandbox call, the vendor's
  shipped docs, its changelog, or a `--discovery` capture gets you the
  real answer.
- Don't treat "all 4 harnesses broke on the same push" as 4 separate bugs —
  check the shared setup path first (Step 1).
- Don't hard-code a declaration in a scenario. It is either a binding's
  `unsupported` reason (a harness limitation, reviewable) measured through
  `contract/declared.py`, or a live precondition that measures the vendor
  each run; a registry entry must exist for it, pinned to the observed
  versions.
- Don't answer a prompt for the person with a flag or a seeded file to get
  a phase green: that proves the capability on a machine nobody has.
- Don't let a check pass on a turn where nothing happened. Gate every
  absence on a presence; demand the tool's own stdout, not merely "a tool
  result"; read the wire when in doubt.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.