> ## Documentation Index
> Fetch the complete documentation index at: https://docs.substrate.42.pe/llms.txt
> Use this file to discover all available pages before exploring further.

# Validate Your Substrate Gates Before Relying on Them

> How to prove a Substrate transition guard actually fires — using the static lint, a live probe via MCP, and dry-run check_transition.

Writing a policy and having it work are two different things. The policy DSL validates shape at author time, but a structurally valid definition can still reference the wrong group id, a field that doesn't exist, or a path that resolves to nothing — and in every one of those cases the gate loads cleanly and silently never fires. Validate every guard before you rely on it.

<Warning>
  A silently-dead gate is worse than no gate: it presents as protection that is not there.
</Warning>

## What You're Proving

A guard enforces the transition — it blocks the move when the gate field is unset or false. It does **not** verify that the underlying work actually happened; the agent self-attests. The goal is to **prove the rail fires**, not prove the work was done.

## 1. The Static Check

Run the structural lint against your boards and policies without starting a server:

```sh theme={null}
substrate validate
```

A corrupt or unparseable substrate prints an actionable error and exits non-zero — make this a CI step. The command also emits advisory warnings for logical smells, such as a guard whose `from_group` equals `to_group` (a move that can never happen). Warnings are advisory and exit 0.

<Note>
  `substrate validate` is a structural lint, not a behavioural test. It cannot tell you whether a guard fires on a real move. Always follow up with the live probe.
</Note>

## 2. The Live Probe

<Steps>
  <Step title="Create a probe task">
    Call `create_task` on the new board, placing the task in the stage **before** the gate. Leave the gate field unset or set it to `false`.
  </Step>

  <Step title="Try to cross the gate">
    Call `update_task` to move the task across the guarded transition. Expect a `transition_blocked` error naming the missing field. If you don't get one, the guard is not firing — check your `from_group` / `to_group` ids and field paths against the DSL.
  </Step>

  <Step title="Satisfy the gate and retry">
    Call `update_task` with `custom_data: { "<field>": true }`, then move again. Expect success and inspect the `policies_fired` envelope to confirm the guard appeared.
  </Step>

  <Step title="Check your responsibilities">
    For each `agent_responsibility`, write a task that matches its `when` conditions and confirm the `message` appears in `policies_fired`.
  </Step>

  <Step title="Clean up">
    Call `archive_task` on the probe task. It served its purpose.
  </Step>
</Steps>

## 3. Spot-Check Without a Throwaway Task

`check_transition` dry-runs "would moving this task to that group be allowed?" against the real guards and writes nothing:

```
check_transition({ task_id, to_group })
```

It returns:

```json theme={null}
{ "allowed": false, "from_group": "spec", "to_group": "build", "blocked_by": "gate-build" }
```

Use it to confirm a gate is live on an existing task without disrupting it.

## Testing a Human Gate

A `human_only` field creates an asymmetry that write tools cannot cross. The only way to see both halves is to exercise the full round trip:

<Steps>
  <Step title="Probe the block">
    Call `check_transition` → expect `allowed: false` with the gate field named in `blocked_by`.
  </Step>

  <Step title="Approve as the human">
    Run `substrate approve <task_id> <field>` from the command line.
  </Step>

  <Step title="Confirm the gate lifts">
    Call `check_transition` again → expect `allowed: true`.
  </Step>

  <Step title="Reset the probe">
    Run `substrate unapprove <task_id> <field>` to put the probe back to its original state.
  </Step>
</Steps>

That round trip is the only way to confirm both halves of the asymmetry that makes a human gate real.
