fleetproof 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {fleetproof-0.3.0/src/fleetproof.egg-info → fleetproof-0.4.0}/PKG-INFO +200 -35
  2. {fleetproof-0.3.0 → fleetproof-0.4.0}/README.md +199 -34
  3. {fleetproof-0.3.0 → fleetproof-0.4.0}/pyproject.toml +1 -1
  4. {fleetproof-0.3.0 → fleetproof-0.4.0}/src/fleetproof/__init__.py +1 -1
  5. {fleetproof-0.3.0 → fleetproof-0.4.0}/src/fleetproof/checker.py +166 -27
  6. {fleetproof-0.3.0 → fleetproof-0.4.0}/src/fleetproof/checks.py +146 -5
  7. {fleetproof-0.3.0 → fleetproof-0.4.0}/src/fleetproof/cli.py +320 -11
  8. fleetproof-0.4.0/src/fleetproof/hookgate.py +1051 -0
  9. {fleetproof-0.3.0 → fleetproof-0.4.0}/src/fleetproof/ledger.py +456 -21
  10. {fleetproof-0.3.0 → fleetproof-0.4.0}/src/fleetproof/report.py +31 -5
  11. {fleetproof-0.3.0 → fleetproof-0.4.0}/src/fleetproof/runlog.py +11 -3
  12. {fleetproof-0.3.0 → fleetproof-0.4.0}/src/fleetproof/telemetry.py +29 -7
  13. {fleetproof-0.3.0 → fleetproof-0.4.0}/src/fleetproof/telemetry_export.py +14 -4
  14. {fleetproof-0.3.0 → fleetproof-0.4.0/src/fleetproof.egg-info}/PKG-INFO +200 -35
  15. fleetproof-0.4.0/tests/test_checker.py +467 -0
  16. fleetproof-0.4.0/tests/test_checks.py +280 -0
  17. fleetproof-0.4.0/tests/test_cli.py +677 -0
  18. fleetproof-0.4.0/tests/test_hookgate.py +1651 -0
  19. {fleetproof-0.3.0 → fleetproof-0.4.0}/tests/test_ledger.py +142 -0
  20. {fleetproof-0.3.0 → fleetproof-0.4.0}/tests/test_report.py +11 -0
  21. {fleetproof-0.3.0 → fleetproof-0.4.0}/tests/test_runlog.py +33 -0
  22. {fleetproof-0.3.0 → fleetproof-0.4.0}/tests/test_telemetry.py +36 -0
  23. {fleetproof-0.3.0 → fleetproof-0.4.0}/tests/test_telemetry_export.py +22 -0
  24. fleetproof-0.3.0/src/fleetproof/hookgate.py +0 -624
  25. fleetproof-0.3.0/tests/test_checker.py +0 -244
  26. fleetproof-0.3.0/tests/test_checks.py +0 -107
  27. fleetproof-0.3.0/tests/test_cli.py +0 -348
  28. fleetproof-0.3.0/tests/test_hookgate.py +0 -633
  29. {fleetproof-0.3.0 → fleetproof-0.4.0}/LICENSE +0 -0
  30. {fleetproof-0.3.0 → fleetproof-0.4.0}/setup.cfg +0 -0
  31. {fleetproof-0.3.0 → fleetproof-0.4.0}/src/fleetproof/__main__.py +0 -0
  32. {fleetproof-0.3.0 → fleetproof-0.4.0}/src/fleetproof/config.py +0 -0
  33. {fleetproof-0.3.0 → fleetproof-0.4.0}/src/fleetproof.egg-info/SOURCES.txt +0 -0
  34. {fleetproof-0.3.0 → fleetproof-0.4.0}/src/fleetproof.egg-info/dependency_links.txt +0 -0
  35. {fleetproof-0.3.0 → fleetproof-0.4.0}/src/fleetproof.egg-info/entry_points.txt +0 -0
  36. {fleetproof-0.3.0 → fleetproof-0.4.0}/src/fleetproof.egg-info/requires.txt +0 -0
  37. {fleetproof-0.3.0 → fleetproof-0.4.0}/src/fleetproof.egg-info/top_level.txt +0 -0
  38. {fleetproof-0.3.0 → fleetproof-0.4.0}/tests/test_adversarial_regressions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleetproof
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Independent, out-of-band verification for agent fleets.
5
5
  Author: Gathrazio
6
6
  License: MIT
@@ -28,6 +28,16 @@ records what your agents do to a durable run log, and — critically — grades
28
28
  work with a checker that runs in a separate process from the agent that did
29
29
  it. An agent may author the checks; it never executes and grades its own work.
30
30
 
31
+ > **Install and enable the plugin *before* the session starts.** Hooks
32
+ > register at session start, so a mid-session install is a **silently inert
33
+ > gate**: nothing fires, nothing blocks, and every "your stop will be gated"
34
+ > belief is false — the exact failure mode this tool exists to prevent, in the
35
+ > tool itself. Preflight every gated session: run one trivial tool call, then
36
+ > confirm a `claude-tool-*` run record with a non-null session id appeared
37
+ > under `.fleetproof/runs/`. No record means not live. The CLI backstops this:
38
+ > `fleetproof fleet`, `check`, and `report` each warn when the current session
39
+ > has no hook-produced record.
40
+
31
41
  ## Why this exists
32
42
 
33
43
  The failure mode this targets is the false "done": an agent reports a task
@@ -75,6 +85,7 @@ No language model sits in the grading path. Grading is comparison.
75
85
  | `fleetproof fleet` | The dispatch board: every dispatch, its state, its tier, and whether anything graded it. |
76
86
  | `fleetproof report` | One self-contained HTML file: per run, claimed-done vs. independently-verified. |
77
87
  | `fleetproof telemetry` | v0.3: per-dispatch outcome records, a local reliability summary, and a strict-allowlist export. |
88
+ | `fleetproof arm` / `disarm`, `dispatch park`, `fleet --orphans` | v0.4: the fleet-operating surface — phase arming, deliberate termination, and the orphan-stop view. |
78
89
 
79
90
  Runtime dependencies: none (Python standard library only). A tool whose job is
80
91
  being trustworthy should add as little dependency and supply-chain surface as it can.
@@ -146,7 +157,9 @@ Inference reads the shape of the run tree: no parent means `bridge`, a parent th
146
157
  tops its own chain means `lane`, anything deeper means `leaf`. `coordinator` is
147
158
  never inferred — it is a role you assign, not a shape that shows up in a run tree.
148
159
  On the board a declared tier carries a trailing `!`, so you can tell a decision
149
- from a guess.
160
+ from a guess — and a tier that is neither declared nor inferred but *defaulted*
161
+ (a captured spawn no intent matched) renders as `lane?`, because a guess
162
+ wearing a declared tier's clothes is how a leaf gets graded as a lane.
150
163
 
151
164
  ### The two new hooks
152
165
 
@@ -158,13 +171,30 @@ SubagentStop is the gate. Each step is its own reason to refuse the stop:
158
171
  1. Report-before-idle. No final message means no report: an agent that went idle
159
172
  saying nothing has not reported, and writing that down as `reported` would
160
173
  launder silence into a claim. Blocked without transitioning.
161
- 2. Pin drift. Every dispatch pins the SHA-256 of the check spec that was in force
162
- when the work was ordered. If the spec changed mid-flight, the stop is blocked
163
- rather than graded against a spec the agent could have edited itself.
164
- 3. That agent's tier of the spec. A blocking failure records `contradicted` and
165
- blocks; a pass records `verified` and closes the dispatch. If the tier selects
166
- no checks at all, no verdict is recorded — an absent grade must never read as a
167
- passing one.
174
+ 2. Pin drift. Every dispatch pins the SHA-256 of the check spec that was in
175
+ force when the work was ordered — and, as of 0.4.0, a tree hash over the
176
+ check scripts under `.fleetproof/checks/`, because the graders are part of
177
+ the spec and a rewritten grader is drift the same as a rewritten spec. If
178
+ either changed mid-flight, the stop is blocked rather than graded against a
179
+ spec the agent could have edited itself, and the block names which one
180
+ drifted. The one exception: if the agent's tier selects nothing runnable,
181
+ there is nothing to mis-grade — the drift is noted and the stop allowed,
182
+ ungraded.
183
+ 3. That agent's tier of the spec, unioned with any checks declared on the
184
+ dispatch's own manifest (`{"id", "cmd"}` entries — blocking, expect-exit0).
185
+ A blocking failure records `contradicted` and blocks; a pass records
186
+ `verified` and closes the dispatch. If nothing is runnable — the tier selects
187
+ no repo checks and the manifest declares none — no verdict is recorded: an
188
+ absent grade must never read as a passing one.
189
+
190
+ Two guardrails around the gate. A SubagentStop with no dispatch to join —
191
+ harness-internal helper agents emit these every session — is recorded as an
192
+ **orphan**: a sighting, never a graded dispatch, because a ledger that
193
+ manufactures dispatches out of unpaired stops manufactures verdicts too. And a
194
+ dispatch that keeps failing the same gate does not loop forever: after three
195
+ contradicted stops it is terminated as `abandoned` — never `verified` — and
196
+ the dispatcher is told to park it, fix the spec or tier, or re-dispatch (see
197
+ *Operating a fleet* below).
168
198
 
169
199
  The Stop hook keeps its v0.1 job and adds a ledger sweep. A dispatch that reported
170
200
  and then never terminated blocks the bridge from stopping: something started
@@ -176,14 +206,27 @@ it is reported and not blocked on.
176
206
 
177
207
  ```
178
208
  fleetproof dispatch new --prompt "..." # --prompt-file for a real, long one
209
+ fleetproof dispatch new --agent-name worker \
210
+ --prompt-file p.md # joinable: the next SubagentStop of
211
+ # that spawn name adopts this record
179
212
  fleetproof dispatch report <run-id> --report report.json
180
213
  fleetproof dispatch close <run-id>
214
+ fleetproof dispatch park <run-id> --reason "..." # terminate on purpose, reason kept
215
+
216
+ fleetproof dispatch intent --agent recon --prompt-file p.md \
217
+ [--manifest m.json] [--tier lane] [--role tester]
218
+ # declare the NEXT spawn of an agent
219
+ # type; the start capture consumes it
181
220
 
182
221
  fleetproof fleet # the board, newest first
183
222
  fleetproof fleet --open # only what has not terminated
223
+ fleetproof fleet --orphans # unpaired stops: sightings, not dispatches
184
224
  fleetproof fleet --format json # full records, for an agent to read
185
225
 
186
226
  fleetproof check --tier lane # grade one rung of the spec
227
+
228
+ fleetproof arm # bridge Stop gate blocks (the default)
229
+ fleetproof disarm --note "why" # bridge gate advisory; note required
187
230
  ```
188
231
 
189
232
  The board never prints a bare dash where a verdict goes. A dispatch nothing graded
@@ -194,19 +237,31 @@ transition trail, and which process wrote each transition.
194
237
 
195
238
  ### What the ledger does not do yet
196
239
 
197
- - The spawn prompt is in neither subagent hook payload. A captured dispatch records
198
- a placeholder saying so, rather than inventing a prompt it never saw. To get the
199
- real prompt on the record, dispatch through `fleetproof dispatch new`.
200
- - Captured subagents are recorded as `lane` tier. Real nesting depth is not visible
201
- from the payload — there is no parent-agent field — so declaring one beats
202
- inferring it wrongly. What that costs in practice is a pilot question.
240
+ - The spawn prompt is in neither subagent hook payload, so an undeclared spawn
241
+ still records a placeholder saying so, rather than inventing a prompt it never
242
+ saw. To get the real prompt (and manifest, and tier) onto a captured dispatch,
243
+ write an intent sidecar first: `fleetproof dispatch intent --agent <type>
244
+ --prompt-file <file>` writes `.fleetproof/intents/<agent_type>.json`, and the
245
+ next SubagentStart of that agent type consumes it — one intent, one spawn.
246
+ - Captured subagents without a declared intent tier are recorded at `lane`
247
+ tier, marked `defaulted` — `lane?` on the board — and the miss is loud: the
248
+ start capture writes a stderr line naming the spawn that matched nothing and
249
+ listing the sidecars that were present. Real nesting depth is not visible
250
+ from the payload — there is no parent-agent field — so declaring a tier
251
+ beats inferring it wrongly.
203
252
  - An agent killed mid-turn can reach SubagentStop with an empty final message,
204
253
  which report-before-idle blocks. Blocking the stop of an agent that is already
205
254
  gone is not useful; how often it happens is under observation.
206
255
  - There is no correlation field between the Task call that spawned an agent and
207
- that agent's stop, so `agent_id` plus `session_id` is the entire join key. That
208
- is also why the lookup is scoped to non-terminal dispatches: an agent id can be
209
- reused once a dispatch is closed out.
256
+ that agent's stop, so `agent_id` plus `session_id` is the primary join key.
257
+ A dispatch recorded from the CLI with `--agent-name` has no agent id yet; the
258
+ first SubagentStop in the same session whose spawn name matches it uniquely
259
+ adopts it (the adoption is written into the transitions). An ambiguous name
260
+ match — two open dispatches, same name — joins nothing: the stop lands as an
261
+ orphan with a stderr note naming the ambiguity, because guessing which
262
+ dispatch to grade is worse than grading neither. The lookup is scoped to
263
+ non-terminal dispatches: an agent id can be reused once a dispatch is closed
264
+ out.
210
265
 
211
266
  ## Verification telemetry (v0.3)
212
267
 
@@ -267,7 +322,95 @@ operator-attested. The checker writes verdicts from a separate process, and v0.3
267
322
  chains a rolling hash over the records as they're written (`anchor` gives you a
268
323
  head you can sign or store elsewhere) — but a local ledger on a shared filesystem
269
324
  is not tamper-proof against everything that can write to it, and FleetProof will
270
- not pretend otherwise.
325
+ not pretend otherwise. The dispatch-intent sidecar (0.3.1) sits in the same
326
+ trust class as `checks.json` itself: operator-attested, exactly as forgeable as
327
+ the rest of `.fleetproof/`, no worse — anything that can write the repo can
328
+ write an intent, and the manifest checks it carries run as shell commands at
329
+ the stop gate the same way repo checks do.
330
+
331
+ ## Operating a fleet (v0.4)
332
+
333
+ v0.4 is the field-hardening release: everything in it was shaped by running a
334
+ multi-lane fleet under the gate in a field deployment on Windows. The
335
+ mechanics are FleetProof's; the discipline below is yours. It is what keeps a
336
+ verification gate from decaying into either wallpaper or a wedge.
337
+
338
+ **Declare intent before every spawn, and verify the capture.** Intent sidecars
339
+ are keyed by the *exact spawn name* — the `agent_type` the harness reports —
340
+ not by what you think of the role as. If the names in your head and your spawn
341
+ calls drift apart, key the sidecar to the role instead with
342
+ `dispatch intent --role`; matching is exact string equality on filename OR
343
+ role, never a prefix guess. Then look at `fleetproof fleet` right after each
344
+ spawn: the row should show the real prompt and a `tier!`. A `lane?` means no
345
+ intent matched — the dispatch is running with a placeholder prompt at a
346
+ defaulted tier, and the capture you thought you declared didn't happen.
347
+
348
+ **Never dispatch an agent against a blocking check its seat cannot satisfy.**
349
+ A lane gated on something only the bridge or the operator can do will fail,
350
+ retry, and fail again — the gate is working; the dispatch was wrong. Declare
351
+ `"owner"` on such checks (`leaf|lane|coordinator|bridge|operator`): at any
352
+ other tier the check still runs and renders, but as advisory — it blocks only
353
+ at the seat that can actually satisfy it. `operator`-owned checks are advisory
354
+ at every agent tier; they keep a criterion visible without wedging anyone, and
355
+ the operator closes them out of band.
356
+
357
+ **Expensive checks belong where they run once.** The gate runs a tier's checks
358
+ at every graded stop on that tier. A slow suite gated on every leaf stop taxes
359
+ the whole fleet; tier it to the rung where it runs once, and demote what
360
+ doesn't need to block to `block: false`.
361
+
362
+ **Edit specs between phases, not under pinned dispatches.** Every dispatch
363
+ pins the spec and the check-script tree; an edit while agents are in flight
364
+ drift-blocks each of them at stop. Propose spec edits as text mid-phase, land
365
+ them at the phase boundary, then dispatch. And if the temptation is to flip
366
+ checks to `block: false` because a phase can't pass them yet, that flip is
367
+ itself a spec edit — use arming instead.
368
+
369
+ **Arming is the phase switch; disarming is an operator action.**
370
+ `fleetproof disarm --note "why"` sets the bridge Stop gate to advisory:
371
+ checks still run and render their failures, but the bridge can end its turn —
372
+ for build phases whose release checks can only pass at publish.
373
+ `fleetproof arm` turns blocking back on, and the armed state is the default —
374
+ no file, a corrupt file, or an unknown value all read as armed. The note is
375
+ required on disarm by design, and the file records who set it and when; the
376
+ board echoes a disarmed gate on every render. A bridge disarming its own gate
377
+ is therefore visible and attributed, not silent: anything that can write the
378
+ repo can flip the switch, but it cannot flip it quietly. The ledger sweep
379
+ still blocks on stalled dispatches regardless of arming, and subagent grading
380
+ is never affected by it.
381
+
382
+ **The graders are part of the spec.** The tree hash pins every file under
383
+ `.fleetproof/checks/` alongside `checks.json` itself. A check whose grading
384
+ logic lives in a script is only as trustworthy as that script's bytes, so a
385
+ rewritten grader now trips the same drift block as a rewritten spec.
386
+
387
+ **Orphans are sightings, never verdicts.** Harness-internal helper agents emit
388
+ SubagentStops that never had a start; a parked or abandoned dispatch's agent
389
+ may stop again after its dispatch is terminal. These land in the orphan list
390
+ (`fleet --orphans`), not on the board, and are never graded — a stop that
391
+ joins no dispatch must not be allowed to manufacture one, because once lane
392
+ checks are green a manufactured dispatch grades itself `verified`.
393
+
394
+ **A wedge must terminate, not loop.** The escalation ladder: a blocking
395
+ failure blocks; the third contradicted stop on the same dispatch terminates it
396
+ as `abandoned` instead — the agent is released, the board shows `abandoned!`
397
+ (never `verified`), and the dispatcher is told to park, fix, or re-dispatch.
398
+ For a dispatch you can see is wedged before the ladder runs out — or one whose
399
+ check its seat can't satisfy — `fleetproof dispatch park <run-id> --reason`
400
+ closes it on purpose with the reason kept. A parked dispatch is terminal; its
401
+ agent, if it stops again, lands as an orphan.
402
+
403
+ **Treat `.fleetproof/runs/` as secret-class, and keep secrets off command
404
+ lines.** Check stdout and stderr are persisted into the run log — that is the
405
+ point, and the risk. Builtin redactions (connection-string keys, SAS-style
406
+ signatures, private-key blocks, JWTs, bearer tokens) plus per-check `redact`
407
+ patterns run before anything is written, and the env snapshot is
408
+ denylist-filtered — but redaction is a backstop, not a permission slip. The
409
+ PostToolUse hook records `tool_input` *verbatim by design*: redacting
410
+ arbitrary code before recording it would be false comfort about what your
411
+ agents ran, so a secret typed on a command line is a secret in the run log,
412
+ full stop. Never type one. And add `.fleetproof/runs/` to `.gitignore` — it
413
+ is evidence, not source.
271
414
 
272
415
  ## Install (each line is one command in Claude Code)
273
416
 
@@ -325,6 +468,17 @@ you (or an agent, under your review) write *before* work is graded, kept in the
325
468
  repo where it is versioned and diffable. JSON, not YAML, on purpose: there is no
326
469
  third-party parser in the runtime.
327
470
 
471
+ Beyond `{ "id", "run", "expect", "block" }`, a check can declare a `tier`
472
+ (which rung grades it — see the ledger section), an `owner` (which rung can
473
+ *satisfy* it — see *Operating a fleet*), and `redact` patterns applied to its
474
+ captured output before anything is persisted. `run` may also be a JSON array
475
+ of argv strings, executed with `shell=False` — no shell at all, which on
476
+ Windows means no cmd.exe quoting hazards; the string form keeps shell
477
+ semantics for compatibility. Manifest checks (`{ "id", "cmd" }`) accept the
478
+ same array form. Checks execute from the resolved project root, not from
479
+ wherever the hook's shell happened to be `cd`'d, and every rendered verdict
480
+ prints the cwd it ran from.
481
+
328
482
  An agent is welcome to propose or edit checks. The guarantee FleetProof makes is
329
483
  narrower and firmer than "the agent verified its work": it is that *something
330
484
  other than the agent* ran the checks and recorded the result.
@@ -361,36 +515,42 @@ choose runs outside the agent's process*.
361
515
 
362
516
  Guidance that keeps the gate honest: keep blocking checks fast and
363
517
  deterministic (flaky checks flap the gate; slow ones tax every stop) and demote
364
- heavy suites to `block: false`. For fleets doing varied tasks in one repo,
518
+ heavy suites to `block: false`. A spec whose selected checks are *all*
519
+ advisory renders its verdict as `ADVISORY - 0 blocking; ...`, never the bare
520
+ word PASS — green with nothing blocking is a fact worth labelling. For fleets doing varied tasks in one repo,
365
521
  the working convention is to have the agent author task-specific checks at task
366
522
  start — authoring is a feature — and let the spec-drift flag make any later
367
523
  revision loud. Per-task check scoping as a first-class mechanism is on the
368
524
  roadmap.
369
525
 
370
- ## Scope and limitations (v0.1)
526
+ ## Scope and limitations
371
527
 
372
- This is an early release. Honest boundaries:
528
+ Honest boundaries:
373
529
 
374
530
  - **`python` must be on `PATH`.** The plugin hooks invoke the checker via
375
531
  `python "${CLAUDE_PLUGIN_ROOT}/scripts/..."`. Environments where the interpreter
376
532
  is only reachable as `python3`, or not on `PATH`, need a shim — any wrapper on
377
533
  `PATH` named `python` works, e.g. `sudo ln -s $(which python3) /usr/local/bin/python`.
378
- - **Checks are shell commands.** Their portability is your responsibility — a
379
- check that shells out to `grep` won't behave identically on every OS. And
380
- because a repo's `checks.json` runs shell commands on the Stop event, treat a
534
+ - **Checks are commands you chose.** Their portability is your responsibility —
535
+ a check that shells out to `grep` won't behave identically on every OS
536
+ (the argv form of `run` sidesteps the shell-quoting half of that problem).
537
+ And because a repo's `checks.json` runs commands on the Stop event, treat a
381
538
  cloned repo's `checks.json` with the same trust you give its `Makefile` or
382
539
  pre-commit config: it is executable code. FleetProof adds no network surface of
383
540
  its own, and first-run consent-per-spec-hash is on the roadmap.
384
541
  - **The gate is only as good as the checks.** FleetProof enforces that an
385
542
  independent process runs your checks; it cannot know whether your checks capture
386
543
  what "done" really means. Weak checks give false confidence.
387
- - **An agent can weaken its own checks.** Authoring `checks.json` is a feature, so
388
- nothing stops an agent from editing it mid-session to slip the gate. FleetProof
389
- does not (yet) forbid spec edits; instead it records the SHA-256 of the spec on
390
- every verdict and flags mid-session *spec drift* loudly — in the Stop-gate
391
- output, the CLI, and the report — when a verdict's spec hash differs from the
392
- session's first. Drift does not by itself block a passing verdict in v0.1; spec
393
- pinning and consent-on-change are on the roadmap.
544
+ - **An agent can weaken its own checks — but not quietly.** Authoring
545
+ `checks.json` is a feature, so nothing forbids an agent editing it
546
+ mid-session to slip the gate. What stands in the way: every dispatch pins
547
+ the spec hash *and* the tree hash over the check scripts under
548
+ `.fleetproof/checks/`, and a pinned agent whose spec or graders drifted is
549
+ blocked at its stop rather than graded against the edited version. At the
550
+ session level, drift from the session's first spec is flagged loudly — in
551
+ the Stop-gate output, the CLI, and the report. What remains open: an agent
552
+ that edits the spec *before* any dispatch pins it is drift-invisible;
553
+ consent-on-change is on the roadmap.
394
554
  - **The local ledger is forgeable by design.** An agent with write access to the
395
555
  repo can, in principle, forge or tamper with records in its own `.fleetproof/`
396
556
  — verdicts, transitions, reports. The gates make honest mistakes loud and
@@ -403,9 +563,14 @@ This is an early release. Honest boundaries:
403
563
  rather than blocking every task. But once a spec has graded work this session
404
564
  — or a dispatch has pinned one — a missing, emptied, or unreadable spec
405
565
  *blocks*: a promised gate cannot be switched off by deleting its spec.
406
- - **Blocking can repeat.** If a blocking check keeps failing, the Stop hook keeps
407
- blocking. That is intended (don't stop on a false done), but it means a check
408
- that can never pass needs operator intervention.
566
+ - **Blocking can repeat — but not unboundedly.** If a blocking check keeps
567
+ failing, the gate keeps blocking: don't stop on a false done. Two escape
568
+ hatches keep that from becoming a wedge. A subagent's dispatch is terminated
569
+ as `abandoned` after three contradicted stops — the agent is released, the
570
+ work is marked unverified, and the dispatcher is told. And the bridge's Stop
571
+ gate can be set to advisory with `fleetproof disarm --note` when a phase
572
+ legitimately cannot pass its checks yet — an operator decision, recorded and
573
+ echoed on the board.
409
574
 
410
575
  ## FleetProof for teams (coming — waitlist open)
411
576