Skip to main content
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.
A silently-dead gate is worse than no gate: it presents as protection that is not there.

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:
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.
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.

2. The Live Probe

1

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.
2

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.
3

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.
4

Check your responsibilities

For each agent_responsibility, write a task that matches its when conditions and confirm the message appears in policies_fired.
5

Clean up

Call archive_task on the probe task. It served its purpose.

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:
It returns:
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:
1

Probe the block

Call check_transition → expect allowed: false with the gate field named in blocked_by.
2

Approve as the human

Run substrate approve <task_id> <field> from the command line.
3

Confirm the gate lifts

Call check_transition again → expect allowed: true.
4

Reset the probe

Run substrate unapprove <task_id> <field> to put the probe back to its original state.
That round trip is the only way to confirm both halves of the asymmetry that makes a human gate real.