Use when launching any long or silent work that could hang — your own probe/script/deploy/fetch-loop/browser-automation, a sub-agent, or a workflow — or when the operator says "the background task hangs", "the agent sits idle", "it never resumes", "is it still running?", or a turn ended waiting on something that never came back. Delivers FR-27's never-launch-bare protocol: bounded/watchdog wrapper, in-turn completion, or BLOCKED-AT return; how to diagnose a suspected hang by activity not 0-by...
Scanned 9/13/2026
Install to Claude Code
npx -y skills add fusebase-dev/fusebase-flow --skill liveness-discipline --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Liveness Discipline?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/fusebase-dev-liveness-discipline)More formats (shields.io, HTML) on the badges page.
---
name: liveness-discipline
description: Use when launching any long or silent work that could hang — your own probe/script/deploy/fetch-loop/browser-automation, a sub-agent, or a workflow — or when the operator says "the background task hangs", "the agent sits idle", "it never resumes", "is it still running?", or a turn ended waiting on something that never came back. Delivers FR-27's never-launch-bare protocol: bounded/watchdog wrapper, in-turn completion, or BLOCKED-AT return; how to diagnose a suspected hang by activity not 0-byte existence; how to recover. Do NOT use to justify a blocking gate or a hook that claims to verify the protocol (a hang is undetectable by construction), and do NOT use for delegated sub-agent turn-completion alone (task-delegation owns that slice — cross-linked here).
source_inspiration: conceptual-only
license_status: clean-room-original
fusebase_flow_version: "3.28"
risk_level: low
invocation: automatic
expected_outputs:
- every backgrounded/long/silent launch carries at least one liveness guarantee (bounded wrapper + incremental logging, in-turn completion, or BLOCKED-AT) before it runs
- a suspected hang is diagnosed by activity/mtime/last-progress, not by 0-byte existence, then recovered (stop -> robust re-run -> clean residue)
related_workflows:
- greenlight-implement.md
- greenlight-deploy.md
- smoke-verification.md
hook_dependencies:
- none
---
# Liveness Discipline (FR-27)
## The failure (why this rule exists)
A long/silent command (probe, test script, deploy, fetch loop, browser automation), a sub-agent, or a harness-auto-backgrounded workflow that **HANGS** emits **no completion event**. The host therefore never re-invokes the agent, which idles indefinitely while appearing "done" — until a human asks "is it still running?". The hang→no-event→idle failure is **invisible by construction**: there is no elapsed-time/idle event in the hook schema, and a call that never returns can never fire a post-call hook. So this rule is **not** enforced by a gate or a verification hook — a Stop-time "watchdog: applied" signal would be attestation theatre. Enforcement = **safe-default tooling** (`hooks/local/lib/bounded-run.sh`) + **present-by-construction delivery** (this skill in context at launch time).
## Guardrail (the rule's floor)
**Quality/safety floor unchanged.** Never launch bare — but a liveness guarantee is the floor, not a license to kill a slow-but-progressing job. A bounded deadline is sized for the work, never so tight it trains premature kills (the FR-26 "budget gate trains truncation" lesson, inverse). Diagnose before you kill.
## Protocol — never launch bare → attach ≥1 liveness guarantee
Before launching ANY long/silent work, attach at least one of these. In preference order:
| # | Guarantee | When | How |
|---|---|---|---|
| 1 | **Complete in-turn** | the work fits the current turn | run foreground with a hard timeout; do not background it at all |
| 2 | **Bounded wrapper + incremental logging** | the work is genuinely long and you must background it | `source hooks/local/lib/bounded-run.sh` + one `bounded_run` call (below); the wrapper emits a terminal timeout line so the job reaches completion-or-death, never a silent idle |
| 3 | **`BLOCKED-AT-<gate>` + record-then-read pointer** | the remaining wait is UNBOUNDED (human approval gate, external event with no ETA) | return the explicit `BLOCKED-AT-<gate>` verdict + what "cleared" looks like + a pointer to where reality is recorded; the orchestrator re-dispatches when the gate clears (`flow-skills/task-delegation` Blocked-return rule) |
Mandatory regardless of which guarantee you pick:
- **Internal watchdog from the FIRST version** — put the deadline in at authoring time, not after it hangs once. Retrofitting a timeout after the first idle is too late.
- **Per-call timeouts** — every individual fetch/RPC/subprocess inside a loop carries its own timeout, not just the outer wrapper.
- **Always flush partial results** — write durable facts AS THEY OCCUR (skeleton first, rows as earned) so a kill at the deadline still leaves evidence (`flow-skills/task-delegation` progress ledger, `flow-skills/smoke-testing` record-then-read).
- **Incremental progress logging to stderr** — a job that prints "still working: step 3/8" cannot masquerade as a hang, and `bounded_run` does this for you.
## Compact bounded-run example (D5 — the copy-paste skeleton)
`bounded_run <deadline-secs> <progress-label> -- <cmd> [args...]` sources the shared core from `run-with-timeout.sh` (so the `ffhc_*` health-check API is untouched), bounds the MONITORED process to the deadline, logs a heartbeat to stderr every interval, and ALWAYS emits a terminal line — either the command's completion or a `bounded-run: TIMEOUT` line — so the job reaches completion-or-death instead of a silent idle.
```bash
#!/usr/bin/env bash
set -uo pipefail
source "$(git rev-parse --show-toplevel)/hooks/local/lib/bounded-run.sh"
# Bound a slow re-verify probe to 120s; heartbeat to stderr; terminal line guaranteed.
bounded_run 120 "re-verify probe" -- ./scripts/reverify.sh --target "$URL"
rc=$?
# rc 124 = deadline elapsed; rc 137 = ignored SIGTERM, SIGKILLed after the -k grace.
# On timeout the wrapper already printed the terminal "bounded-run: TIMEOUT" line —
# surface it, flush partial results, then diagnose (do not blind-retry).
```
**Honest scope (D7 — do not overstate).** `bounded_run` bounds the **monitored** process only. It does **NOT**:
- kill a grandchild you `&`-detached or `Start-Process`'d out from under it — **so don't detach under the wrapper**; keep the work in the monitored process tree;
- interrupt an uninterruptible OS wait;
- *prove* the host re-invokes you — it ensures the process reaches completion/death so a host that surfaces background completion has an event to deliver, and so a 0-byte silent idle becomes a visible timeout line.
Therefore: **put a deadline INSIDE long scripts too** (an event-loop-blocked `setTimeout` won't fire; an inner fetch needs its own timeout), don't `&`-detach under the wrapper, and always flush partial results.
## Diagnose a suspected hang (activity, not existence)
A slow-but-progressing job is NOT a hang. Distinguish before you kill:
- Check **activity**: last log line, output file **mtime**, byte growth, last progress marker — NOT mere "the file exists" (a 0-byte file proves nothing) and NOT "the process is listed".
- **Slow but progressing** (mtime advancing, new log lines, byte count climbing) → let it run; widen the deadline if the work legitimately needs it.
- **Stalled** (no activity past the expected interval, no heartbeat, deadline elapsed) → confirmed hang; recover.
## Recover from a confirmed hang
1. **Stop** the stalled process (the wrapper's timeout line means it already terminated; otherwise terminate it).
2. **Re-run the robust version** — the same launch with a deadline + incremental logging attached (never blind-retry the bare launch; that's the two-strike anti-pattern, FR-26).
3. **Clean residue** — partial files, locks, half-written state from the killed run.
4. **Retry only transient rate-limits** with bounded, labeled backoff; a structural hang is not a rate-limit and re-attempting it unchanged is forbidden.
## Zero-trust sub-agent liveness (mandatory — WS8)
A dispatched sub-agent (or Codex) is a long/silent launch under someone else's control — the same hang→no-event→idle failure applies, plus you cannot see its process. **Never trust or passively wait on its completion ping.** The completion notification can be missing, late, or fire on a transcript that is 0 bytes. Apply the zero-trust protocol:
| Step | Action |
|---|---|
| **Poll, don't wait** | Proactively check the sub-agent's liveness on a FIXED cadence (~every 60–90s) via GIT PROGRESS (new commits / advancing SHA), process activity, or output-file byte growth — NOT the 0-byte transcript file's mere existence (existence proves nothing; the diagnose-by-activity rule above applies). **A missing completion ping is evidence of neither completion nor death** — it is no information at all; only observed progress (or its absence past the expected interval) decides which. |
| **Re-dispatch a transient stall** | A transient provider rate-limit / server error / no-start is a DISPATCH failure, never the slice's verdict — **never record it as `FAILED`, `BLOCKED`, or "the delegate couldn't do it"**; the work was never attempted. Recipe: re-dispatch with a minimal **"try again"**, inside the **bounded delegate-retry envelope** below (3 attempts / 5 min / labeled backoff), and only then judge the work. A structural hang is not a rate-limit — do not blind-retry it unchanged. **Provider-limit death ≠ lost session (proven 2026-07-07):** a delegated sub-agent that dies mid-run on a provider rate/session limit usually keeps its context — `SendMessage` the "try again" to the SAME agent id and it resumes from where it stopped. Do NOT spawn a fresh agent (loses context, redoes work) and do NOT re-send the full brief (the resumed agent still has it). |
| **Verify before trusting** | Before you trust ANY sub-agent's output, verify the FINAL git state yourself: clean linear history, the expected commits landed, 0 mirror drift (`mirror-skills.sh --check`), gate evidence present. A completion ping is a claim, not proof. |
| **Sync for simple work; background+poll for long runs** | Prefer synchronous sub-agent runs for short autonomous work — a backgrounded sub-agent can't autonomously self-resume if it yields mid-task. For a LONG autonomous run where you want progress visibility, backgrounding IS viable when paired with active polling (this table's poll rule) + `SendMessage`-resume on a provider-limit death (row above) — proven 2026-07-07 to continue the same session. Either way, never passively background-and-wait: the run must produce progress, resume on death, or return `BLOCKED-AT-<gate>` (task-delegation turn-completion). |
This is the sub-agent application of the FR-27 floor; `flow-skills/task-delegation` owns the turn-completion / `BLOCKED-AT` return shape (cross-linked below, not duplicated).
### Bounded delegate-retry envelope (canonical — the ONE exception)
The single sanctioned exception to don't-poll-while-running / record-then-read (`token-economy` TE-08) and to the two-strike rule (TE-05). It covers ONLY a delegate that never STARTED — transient provider rate-limit, server error, no-start. A structural hang, a started-then-wrong result, and a failing approach are all outside it and get zero retries under this envelope.
| Bound | Value (binding — one number, not a range) |
|---|---|
| Max attempts | **3** re-dispatches ("try again" to the SAME agent id), counted from the first no-start |
| Max wall-time | **5 minutes** total across all attempts, whichever limit is hit first |
| Backoff | labeled and stated in chat each time: ~60s → ~90s → ~120s — e.g. "retry 2/3 after ~90s; no progress observed since `sha:abc1234`" |
| Progress read | exactly ONE cheap progress check per interval (advancing SHA / new commit / output-file byte growth). Never tail output, never a second read inside one interval |
| Exhaustion (attempts OR wall-time) | **STOP retrying.** Read the `task-delegation` progress ledger → re-brief a SUCCESSOR with verify-from-records. No successor possible → return `BLOCKED-AT-delegate-no-start` + what cleared looks like + the ledger path. Never `FAILED-<reason>` and never `BLOCKED-AT-<the-slice's-gate>` — the slice was never attempted |
**When delegate-progress polling overrides record-then-read:** record-then-read presumes a durable surface that outlives the run. A delegate that never started writes nothing, so no record exists to read afterwards — the interval progress read is the only available evidence, and it is capped at one read per interval. The moment the delegate IS producing progress the override ends: judge the work from its commits and artifacts, never by watching it run.
Re-dispatches inside this envelope are **not** two-strike attempts: the approach was never executed. A retry after the delegate has started IS a strike.
## Cross-links (reuse, do not duplicate)
| Slice | Canonical home | Relationship |
|---|---|---|
| Delegated sub-agent turn-completion + `BLOCKED-AT` | `flow-skills/task-delegation/SKILL.md` (turn-completion / Blocked-return) | FR-27 is the GENERAL rule (covers the agent's own work too); task-delegation keeps the sub-agent slice — cross-linked, not narrowed (D6) |
| Record-then-read / observability-gap finding | `flow-skills/smoke-testing/SKILL.md` § Verification cost discipline | the prescribed remedy for "how do I know it finished?" — read durable evidence once after the run; no durable surface = an observability-gap finding, not agent-side polling |
| Don't poll while it runs | `flow-skills/token-economy/SKILL.md` (record-then-read row) | agent-side watching costs tokens linear with wall-clock; bound + log + read records instead. Its ONE exception is § Bounded delegate-retry envelope above (canonical here; TE-05/TE-08 and `task-delegation` point at it) |
## Anti-patterns
- Launching a background probe/deploy/fetch-loop with no timeout, then ending the turn "running in background — I'll resume when it completes" (you won't — the host never re-invokes a hung call).
- Adding a hook or Stop-time signal that claims to VERIFY this protocol or detect a hang — structurally impossible; it would be attestation theatre (the inert-lever anti-pattern).
- A blocking gate on background launches — the failure is semantic (real hang vs legitimate long-live run); a gate trains premature kills (intelligence damage).
- `&`-detaching under `bounded_run` and assuming the deadline still bounds the detached grandchild (it does not).
- Diagnosing a hang by "the output file exists" (0-byte existence proves nothing) instead of by activity/mtime/last-progress.
- Blind-retrying the same bare launch after it hung (two-strike, FR-26) instead of re-running the robust, bounded, logged version.
- Reading a delegate's transient provider-limit death as a task verdict (`FAILED` / "it couldn't") — the work was never attempted; re-dispatch "try again", retrying only inside § Bounded delegate-retry envelope above (max 3 attempts / 5 min), then judge.
- Treating a missing completion ping as either completion or death — it is no information; poll for progress instead.
- Re-pasting this protocol into every handoff — it lives here; handoffs carry the promoted Hard-invariant clause + a pointer (FR-23/FR-24).
## Clean-room note
Original Fusebase Flow content. Designed after reviewing public AI coding workflow patterns; no third-party code, prompts, skill files, or hook scripts are copied. The bounded-run tooling reuses Fusebase Flow's own `run-with-timeout.sh` core (not a third-party watchdog). See `docs/source-map.md`.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!