---
name: apt:verify-proof
description: "Outcome walker — drives spec.md `## User Outcomes` one at a time, captures surface-appropriate evidence, derives a two-gate verdict ({approved, rejected, needs_human}). Fails closed on file-evidence for UI surfaces."
apt-skill-version: {{APT_VERSION}}
stage: verify
intent: ship
when_to_use: "The user wants visual proof of work — screenshots, recorded video, and a browsable HTML proof report — instead of QA scores alone."
user_invocable: true
internal: false
spawns_agent: false
agent_name: null
task_context: require-existing
default_execution_mode: auto
execution_modes:
  - auto
  - step
allowed-tools: "Bash, Read, Write, Edit, Grep, Glob, Task"
argument-hint: "apt:verify-proof [--multi-model | --video]"
gates: []
config_keys:
  - verification.open_report
  - verification.report_formats
  - verification.provision_mcp
---

# Proof Verification

Adversarial visual verification with structured evidence capture. Every test requires
an action, evidence, and a verdict. No hallucinated passes.

## When to Use

- After `/apt:execute` completes — verify the implementation actually works
- When the user wants visual proof before approving a change
- When you need to generate a shareable HTML report for stakeholders
- Before creating a PR — attach proof that features work

## When Not to Use

- For quick QA scores without visual evidence — use `/apt:verify` instead
- For code review — use `/apt:review`
- If no UI/visual changes were made — `/apt:verify` is faster

## Driver Architecture

`/apt:verify-proof` is **runtime-agnostic** — the workflow refers to IDL
verbs (`driver.click`, `driver.screenshot`, `driver.navigate`, ...) and
NEVER to concrete MCP tool names (per ID-01). The driver layer at
`packages/framework/drivers/<id>/` maps verbs to transports.

### Auto-detect

The resolver auto-selects the highest-scoring driver per ID-02
(capability_coverage > compatibility > stability > priority > locality).
Preview the selection before running verify-proof:

```bash
apt-tools detect-runtime .
# → { runtime: {is_electron, is_nextjs, ...}, selected_driver_id: "..." }
```

v0.8.6 — the runtime detector now walks `apps/*/package.json` by
default and OR-merges each child's capabilities into the root. The
canonical Aperant monorepo (Electron at `apps/desktop`, Next.js at
`apps/web`) now reports `is_electron: true` AND `is_nextjs: true`
without any config override.

### Per-outcome dispatch (v0.8.6+)

When the spec ships `## User Outcomes` (the verify-proof input
contract — see CLAUDE.md), the workflow runner imports
`resolveDriversForOutcomes()` from the resolver module and picks a
driver PER OUTCOME SURFACE instead of a single per-run driver pick.

```js
import { resolveDriversForOutcomes } from '.aperant/deps/node_modules/@aperant/framework/src/cli/verify-proof/resolver.mjs'

const entries = resolveDriversForOutcomes({
  drivers, runtime,
  outcomes: parsed.outcomes,  // from parseUserOutcomes(spec)
})
// → [{outcome_id: "O1", surface: "electron", driver: {...}}, ...]
// For [electron+web] outcomes, two entries share the same outcome_id.
```

The legacy `apt-tools detect-runtime` still answers the per-run
"which driver would be picked?" question for the override preview.
For the per-outcome dispatch (the new contract), import
`resolveDriversForOutcomes` directly.

### Override

Force a specific driver chain in `.aperant/config.json`:

```json
{ "verification": { "runtimes": ["electron", "cli"] } }
```

v0.8.6 — `verification.monorepo_globs` is the new opt-out for the
monorepo walk. The walk now runs by default — adopters whose
monorepo lives at a non-canonical prefix
(`packages/apps/*`, `services/*`) override via
`verification.monorepo_globs: ["packages/apps/*"]`; set to `[]` to
disable the walk entirely (root-only behaviour, backwards compat).
The `verification.runtimes[]` override stays the final escape hatch
and is now usually unnecessary because the walk runs by default.

### Bundled drivers (6)

- **browser** (priority=100, ga) — Next.js / vanilla web frontends.
- **electron** (priority=90, ga) — Electron desktop apps.
- **tauri** (priority=85, ga) — Tauri desktop apps.
- **mobile** (priority=80, ga) — iOS + Android (Expo / React Native / native) via `mobile-mcp`. App build/launch is owned by `apt-tools mobile prepare`, not the driver.
- **cli** (priority=60, ga) — headless CI, output-shape assertions.
- **api** (priority=50, ga) — HTTP / JSON-API regression tests.

> **Precondition.** When `--parallel` is requested, the sandbox gate at apt-verify-proof/SKILL.md Step 2c runs BEFORE this step. An unverified sandbox exits with `{status:"error", code:"E_SANDBOX_UNVERIFIED", verdict:"needs_human", reason:"sandbox_unverified"}` without loading any MCP driver tools or entering the outcome-walker loop. See `docs/verify-proof-sandbox-patterns.md` for setup.

### Deferred-tool discovery (Claude Code / OpenCode hosts) — A5 fix

On Claude Code and OpenCode hosts, MCP driver tools listed under
`drivers/<surface>/manifest.json :: transport.mcp_server_id` are
**deferred** — they live in the host's MCP server registry but their
schemas are NOT loaded by default. Before driving any UI-surface
outcome the workflow runner MUST load them via `ToolSearch(query='select:...')`.

**Worked example.** For the bundled `browser` driver (manifest field
`transport.mcp_server_id = "puppeteer"` → `mcp__puppeteer__*` tools):

```
ToolSearch(query='select:mcp__puppeteer__browser_navigate,mcp__puppeteer__browser_click,mcp__puppeteer__browser_screenshot,mcp__puppeteer__browser_type')
```

For the bundled `electron` driver (manifest field
`transport.mcp_server_id = "electron"` → `mcp__electron__*` tools):

```
ToolSearch(query='select:mcp__electron__app_launch,mcp__electron__app_click,mcp__electron__app_screenshot,mcp__electron__app_type')
```

For the bundled `mobile` driver (manifest field
`transport.mcp_server_id = "mobile-mcp"` → `mcp__mobile-mcp__*` tools):

```
ToolSearch(query='select:mcp__mobile-mcp__mobile_click_on_screen_at_coordinates,mcp__mobile-mcp__mobile_swipe_on_screen,mcp__mobile-mcp__mobile_save_screenshot,mcp__mobile-mcp__mobile_type_keys')
```

After the call, list the now-loaded tool names back to confirm they
appear in the active tool surface. If any required driver tool fails to
load, emit `result: "needs_human_with_transport"` on each affected
outcome — do NOT fall back to file-evidence. The legacy
"commit hash as proof" pattern is rejected by the proof_gate.

The manifest field is `transport.mcp_server_id` (and `transport.skill_id`
where applicable). The names `mcp_transport.tool_prefix` and
`transport.tool_prefix` are NOT real manifest fields — do not search
for them. This block is no-op on hosts whose entire MCP surface is
loaded eagerly (some Codex builds); `ToolSearch` is idempotent there.

### Install-time MCP provisioning (verification.provision_mcp)

`apt-tools init` (and `/apt:update`, which routes through the same path)
auto-provisions the MCP transport a driver needs into each SELECTED host
CLI's config, so verify-proof works at the prompt with no manual MCP
setup. It detects the project runtime and, per host:

- **claude** → JSON-merges the server into the project-root `.mcp.json`
  (`mcpServers.<id>`).
- **codex** → upserts a `[mcp_servers.<id>]` table into the project-root
  `.codex/config.toml`.

Electron and Tauri projects get the matching MCP server; web frontends
get the browser driver's declared `agent-browser` skill copied into the
host skill dir (`.claude/skills/` / `.agents/skills/`) — its `agent-browser`
CLI is a documented one-time prereq, the same class as the
`electron-mcp-server` / `tauri-plugin-mcp-server` npm packages (see each
driver's README). All writes are **TOFU/non-clobbering**: a pre-existing
server id, config table, or skill file is never overwritten. Opt out with
`verification.provision_mcp: false` (default `true`) — then init/update
writes no host MCP config and no skill.

### Driver authors

Third-party driver authors should run `apt-tools driver doctor .` before
publishing to validate manifest schema + IDL conformance:

```bash
apt-tools driver doctor .
# → { drivers: [{driverId, manifest_valid, idl_conformant, ...}], summary: {...} }
```

IDL types, error taxonomy, and conformance kit live inside the framework
itself under `packages/framework/src/driver-sdk/` — see that directory's
public-api-lock test for the surface contract.

## Step 1 — First-run sandbox-setup hook (interactive `--parallel` only)

When the user invokes `/apt:verify-proof --parallel` on a project where
`verification.sandbox` is ABSENT from `.aperant/config.json` AND
`host.capabilities.structured_prompts === true` AND
`process.stdout.isTTY === true`, prompt:

```
AskUserQuestion([
  {
    question: "Parallel /apt:verify-proof is not configured. Run setup now?",
    header: "Sandbox Setup",
    multiSelect: false,
    options: [
      { label: "Yes — run sandbox setup", description: "Walk recipe-diff with you, then apply-recipe --confirm --accept-all (after you've reviewed each change), then verify-sandbox before continuing" },
      { label: "Cancel", description: "Exit; re-invoke without --parallel for single-instance verify-proof" }
    ]
  }
])
```

On `Yes`, dispatch `/apt:verify-proof setup` (the setup-mode flow) before
returning to the walker. On `Cancel`, exit with the same envelope the
Step 2c gate would produce so the user sees the structured remediation.

Non-interactive surfaces (`structured_prompts:false` OR
`stdout.isTTY:false`) print the one-line stderr tip and proceed:

```
[apt] tip: run `/apt:verify-proof setup` to enable parallel verification
```

then exit at the Step 2c gate. Never block CI / fan-out children on
this prompt (ID-08).

## Step 2c — Sandbox gate (precondition for `--parallel`)

When `--parallel` is requested AND `.aperant/config.json :: verification.sandbox`
is present, this gate runs BEFORE Step 2.5 (Deferred-tool-load) and BEFORE
entering the Outcome Walker loop. It is a **precondition gate**, NOT a
per-outcome transport-failure gate.

**Dispatch on the state file** at
`.aperant/state/verify-proof-sandbox-verified.json`:

| condition | action |
|-----------|--------|
| `--parallel` requested + state file shows `verified:true` (verdict:`approved`) | **proceed** to Step 2.5; the parallel surface is now safe (each instance has its own SQLite path + userData dir + singleton-lock opt-out). |
| `--parallel` requested + state file missing OR `verified:false` | **exit BEFORE Step 2.5** with the JSON envelope below; do NOT load deferred MCP tools, do NOT initialize the outcome walker, do NOT write verification.json. |
| `--parallel` NOT requested | today's behavior unchanged — this gate is a no-op (Fast Path Guarantee ID-07). |

**Fail-loud envelope.** sandbox not verified — run `/apt:verify-proof setup` first:

```json
{
  "status": "error",
  "code": "E_SANDBOX_UNVERIFIED",
  "verdict": "needs_human",
  "reason": "sandbox_unverified",
  "remediation": "/apt:verify-proof setup"
}
```

The single-line stderr message is: `sandbox not verified — run \`/apt:verify-proof setup\` first`.

**v2 vocabulary alignment.** The gate's failure envelope uses verify-proof
v2's existing `verdict:"needs_human"` vocabulary (ID-09) so reviewers,
downstream tools, and the e2e see one shape across both the per-run
envelope and per-outcome verification.json. We deliberately do NOT emit
per-outcome `needs_human_with_transport` rows for this case — sandbox
unverified is a precondition failure for the whole walker, not a
per-outcome transport failure for individual outcomes. Only the two
v2-shipped verdicts (`approved`, `needs_human`) appear in this gate's
envelope — the deprecated middle-band verdict v2 removed is not
referenced anywhere in this block (TD-07 grep guard locks this).

**Anti-queue invariant.** This gate fails loud rather than silently
serializing parallel invocations through a cooperative queue. The
`install/install-semaphore.mjs` queuing primitive is explicitly forbidden
in this code path (ID-01); a regression test grep-guards the invariant.

See `docs/verify-proof-sandbox-patterns.md` for the worked
electron-libsql adopter flow + the manual-integration template for
unrecognized runtimes.

## Execution — Outcome Walker (v0.9.0)

Execute the verify-proof workflow end-to-end using IDL verbs (the driver
maps verbs to its transport — MCP server, local command, HTTP). The
workflow is an **outcome walker**: iterate `spec.md ## User Outcomes`
parent bullets one at a time, drive the stated bullet AND each inferred
child, capture surface-appropriate evidence, fix-loop on red, advance on
green.

<execution_context>
@packages/framework/workflows/verify-proof.md
@packages/framework/context/installed/verification.md
@packages/framework/templates/proof-verification.md
</execution_context>

<process>
Follow the verify-proof workflow from @packages/framework/workflows/verify-proof.md.

**Critical:** Read the verification context (@packages/framework/context/installed/verification.md) FIRST
to adopt the adversarial QA mindset. You are not a helpful assistant during verification —
you are trying to break things and prove they work.

Respect ALL workflow gates:
1. **Pre-check gate** — all subtasks completed.
2. **User Outcomes gate** — spec.md MUST carry `## User Outcomes` with `body_kind ∈ {outcomes, empty-with-note}`. NO fallback to feature-registry-derived tests.
3. **Deferred-tool-load gate** — on Claude Code / OpenCode hosts, MCP driver tools under `drivers/<surface>/manifest.json :: transport.mcp_server_id` MUST be loaded via `ToolSearch(query='select:...')` BEFORE driving any UI-surface outcome.
4. **Automation gate** — `automated_checks.{tests,typecheck,lint}.status ∈ {pass, fail}` (closed set, no `"enforced_at_commit"` dodge).
5. **Outcome-walker gate** — every parent outcome AND every inferred child has its own test entry with evidence appropriate to its surface OR `result: "needs_human_with_transport"`.
6. **Proof gate** — UI-surface outcomes carrying `evidence: "file-evidence: ..."` are rejected as `needs_human_with_transport`. File-evidence is NEVER sufficient for any surface.
7. **Verdict gate** — verdict is exactly one of `{approved, rejected, needs_human}`. The legacy `approved_with_notes` middle band is DELETED.
8. **Design-lint gate (C11)** — for every UI-touching task, run `apt-tools design-lint` against the implementation's generated HTML + `{task_dir}/mockups/mockup_final.html` (when present). Violations contribute to the proof-gate evidence story but are NOT blocking on their own.

Use the proof-verification template (@packages/framework/templates/proof-verification.md)
for the verification.json output schema.

**Design-lint invocation** (when the task touched UI):

```bash
node packages/framework/bin/apt-tools.mjs design-lint \
  --design-file DESIGN.md \
  {task_dir}/mockups/mockup_final.html \
  <implementation-rendered-html-files>
```

Include the command's full JSON output verbatim in the report's `## Design-Lint` section. When violations exist, include each rule's `why` + `rewrite` so the reviewer sees the fix path without following a link.
</process>

## References

For multi-model verification details:
- `references/multi-model-verification.md` — Gemini, Codex, Claude verdicts and quorum
- `references/multi-model-setup.md` — API keys, model presets, benchmarks
- `references/proof-capture.md` — Evidence capture best practices

## Config

Settings live in the MERGED Aperant config (shared `.aperant/config.json` + per-device `.aperant/config.local.json`; local wins). Run `/apt:setup` to configure. Read via `apt-tools config read .` or the framework's `loadMergedProjectConfig()` — NEVER raw `JSON.parse(readFileSync('.aperant/config.json'))`, which misses per-device overrides.

- `verification.evidence_capture` — shared. capture screenshots + terminal output (default: true)
- `verification.open_report` — **per-device (config.local.json)** per AUDIT-001. open HTML in browser when done (default: true)
- `multi_model.enabled` — shared. run multi-model verification (default: false)
- `multi_model.cross_verification.tools` — **per-device (config.local.json)** per AUDIT-001. which CLIs to cross-verify with.
- `multi_model.gemini.model` — shared. Gemini preset: default/thorough/fast
