Skip to content

Design: Waits that cannot silently fail

Every wait in this estate is hand-written at the moment it is needed, and a wait that cannot conclude is indistinguishable from one that is merely being patient. This design makes waiting a tool with a contract — typed predicates that remove the hand-composed shell string from the common path, a terminal marker on every path so silence is never ambiguous, and a helper that refuses to arm on a predicate it cannot evaluate. It widens Epic #1802 from its original CI-only framing to cover any wait, agent or pipeline.

The failure this exists to prevent — three instances, one shape

Section titled “The failure this exists to prevent — three instances, one shape”

A watch that fires spuriously is obvious. A watch that can never fire looks exactly like patience, so it is discovered only by the thing it was supposed to catch.

DateInstanceWhy it could never conclude
2026-08-04Six-hour stall on a handoff scriptpgrep -f <script> matched the monitor’s own command line, so liveness was always true
2026-08-04Monitor never fired on a green pipelineThe marker path was reconstructed from a 7-char SHA; the real file used 8
2026-08-18Fifteen-minute stall on a recovered probeShell quoting sent service%3D%5C%22…%5C%22; Prometheus returned HTTP 400 forever

ADR-0200’s original case — a CI gate reading an asynchronously-ingested result once, immediately, and misreading queue lag as systemic loss — is the same class seen from the pipeline side. All four are a wait whose question was wrong.

ADO board sweep, WIQL over System.Title and System.Description, across six mechanism-class vocabularies — pipewait, waiting, wait, Monitor, watcher, polling.

CandidateVerdict
#1802 Async Verification Gate standardThis Epic. To Do, ungrilled, no children, created 2026-07-18. Its description already framed the decision — “no standard governs ‘verify async work by polling to a deadline with early-exit, only fail after the window’. Decide whether to write the standard + ADR, or close as not-worth-standardising.” Widened here rather than duplicated.
#2488 Agent system-access helpersPartial overlap, deliberately not merged. Doing, tagged grilled, approved 2026-08-18. Covers the helper-by-default ruling and lists Azure DevOps under partial coverage (pipewait/snag only). Its scope is systems with a credentialed control plane; waiting is orchestration, so waitfor is a consumer of pitlab_access, not a member of it.
#1988 pipewait: cross-repo resource-triggered runsA narrow pipewait trigger feature. Does not cover waiting generally.
#1831 pipewait hardeningDone. Historical hardening of the ADO waiter only.
#2506, #2503, #463, #2339Incidental matches on the words “wait”/“monitor”. No coverage.

Docs corpus sweep, two phrasings over designs/, adr/, standards/:

  • ADR-0200 — the single-instance precedent (poll Dependency-Track to a 240 s deadline, early-exit, proven both ways). Not generalised.
  • ADR-0340 / the guard-must-observe-what-it-guards lesson — the same class stated for guards rather than waits.
  • Token-Optimization Standard — already publishes the discipline: “immediately after arming, confirm the file or condition the watch names actually exists.” This is the key negative finding: the doctrine existed and did not prevent the 2026-08-18 failure, which is the whole argument for a mechanism over more words.
  • No async-verification, wait or equivalent standard file exists.
#1802 governance half#2488 helper halfA wait that cannot concludelooks exactly like patienceLayer 0already covered?Partial overlapWiden #1802 · no new EpicStandard, tool,or both?Both the helper ISthe declared mechanismPredicate form?Typed system read ·file/marker· --shell with dry-runpipewait?Sibling. Untouched.Shared terminal-markercontractHome?ansible/scripts/usr/local/binlauncher + poller · PD6 cat1Watchingthe watcher?CI --self-test proven red+ scheduled synthetic +dead-manDeliver via /go#2529 build atomic#2530 PRR held backfrom /go
#1802 governance half#2488 helper halfA wait that cannot concludelooks exactly like patienceLayer 0already covered?Partial overlapWiden #1802 · no new EpicStandard, tool,or both?Both the helper ISthe declared mechanismPredicate form?Typed system read ·file/marker· --shell with dry-runpipewait?Sibling. Untouched.Shared terminal-markercontractHome?ansible/scripts/usr/local/binlauncher + poller · PD6 cat1Watchingthe watcher?CI --self-test proven red+ scheduled synthetic +dead-manDeliver via /go#2529 build atomic#2530 PRR held backfrom /go
DecisionChoiceRationaleRejected
Where it belongsWiden #1802Its stated question is this decision; ungrilled, no children, open since 18 JulA new Epic (duplicates approved work); folding into #2488 (stretches its credentialed-access scope)
DeliverableStandard and helperThe doctrine already existed in the Token-Optimization Standard and did not prevent the failureStandard only (is today’s state with more words); helper only (leaves ADR-0200’s CI half ungoverned)
PredicateTyped, with --shell fallbackRemoves the hand-composed shell string from the common path, so the quoting failure is impossible rather than detectedTyped-only (sends container/queue/git waits back to hand-rolling); shell-only (detects rather than eliminates)
pipewaitSibling, untouched149 runs on 2026-08-18, all with terminal markers, zero malfunctions. Two ADRs of domain correctness (0205, 0367)Absorbing it (re-implements SHA discovery, fan-out, approval detection for a tidier diagram); wrapping it (indirection for no correctness gain, and six soul.md citations would need rewording)
Homeansible/scripts//usr/local/binPD6 category 1; launcher + poller mirrors pipewait’s own shapeInside pitlab-access (blurs the access contract #2488 just defined); its own repo (disproportionate)
EnforcementClass machine, helper is the mechanismStructural at the point of use, not inspected afterwardsreview (auto-demotes to advisory without a checkpoint, and a post-session check cannot catch a mid-session strand)
AssuranceSelf-test and scheduled syntheticThe typed path can break out-of-band — an expiring credential, a changed pitlab_access read contract — which is what earns the second layer under the meta-standardSelf-test only (blind to out-of-band); the observability ratchet alone (governs observability, not function)
Delivery/go, PRR held backA sub-agent that just built the helper is the least independent reviewer available/go closing its own PRR (self-certification, in a design about watches that cannot self-certify)
RequirementHow this design satisfies it
Pillar 1 — MonitoringZabbix item + trigger on the scheduled synthetic’s freshness dead-man.
Pillar 2 — ObservabilityPoller logs to stdout → journald → Loki. No dashboard is owed: this is a CLI helper, not a service with an RED profile.
Pillar 3 — AlertingThe dead-man alerts when the synthetic stops concluding. Severity warning — a broken waiter degrades autonomy, it does not break production.
Pillar 4 — SBOM/vulnStdlib-only Python plus pitlab_access, which carries its own coverage under #2488. No new image, no new dependency surface.
Pillar 5 — Docs as codeThe standard, its ADR, and a runbook section land in the same change as the code.
Config as codeScript in ansible/scripts/, declared in control01.yml, deployed by playbook. Schedule declared in schedule_config.yml.
Software update & patch lifecycleHouse pattern (1) host operational script — it has no independent version. It updates when the repo does, deployed by the sync_control01 pipeline on push. No pinned upstream, so no bump trigger and no staleness signal are owed; its dependency pitlab_access is versioned and pinned under #2488, which owns that refresh path. Staleness of function rather than version is covered by the scheduled synthetic.
SecurityNo new credentials — the typed path borrows pitlab_access’s existing resolution. No network exposure. No secret ever in argv.
NotificationsDead-man routes via the existing Alertmanager path. Nothing new emails.
TaggingLoki stream labels per the taxonomy; Zabbix item and trigger tags per RULE 10.
Minimal decomposition2 issues, 1 /go run, PRR held back for a human.
DORA / SREDeployment frequency and lead time come from the existing sync_control01 pipeline data. Change failure rate is the self-test’s red rate. MTTD is the metric this design actually moves: the failure class it removes is one where time-to-detect was 15 minutes in the best documented case and 6 hours in the worst, because the signal was silence.
ControlFrameworkEvidenceResidual risk
The helper refuses an unevaluable predicateNIST CSF 2.0 Protect · internal Standard-Enforcement Standard (class machine)--self-test proven red then green in ansible-ciA wait armed without the helper is still unguarded — accepted; detecting it would require parsing session transcripts
Scheduled synthetic + freshness dead-manCPS 234 control testing · NIST CSF DetectDead-man proven to fire when the synthetic is skippedDetects that the helper stopped concluding, not that it concluded wrongly
Terminal marker on every pathNIST CSF Detect · ADR-0205’s existing patternMarker asserted present on success, failure and timeout
Runbook + cold-read walkthroughISO 27001 A.5.37 (documented operating procedures)PRR checklist item: walked cold by a session with no context

Control testing is the live obligation here; CPS 230 tolerance-for-disruption and material-service-provider clauses do not apply — this introduces no third party and supports no critical operation directly.

  • Service record. Not a service: agent tooling, like pipewait. No NetBox asset, no criticality tier. It owes a runbook section alongside the existing operations/waiting-for-ado-pipeline-runs page, which is where a reader already goes for waiting.
  • Reliability contract. The SLI is does an armed wait reach a terminal state. The dead-man is the alert; there is no error budget ceremony for a CLI helper and inventing one would be theatre.
  • Test strategy. Per sub-issue: the self-test proven red before trusted green (ADR-0141’s house pattern). No testplan.yaml is owed — the Service Test Plan Standard governs services with a golden path, and this has no runtime surface; the scheduled synthetic is its executable acceptance contract. Any future change to the helper runs the self-test first as a regression baseline and updates it in the same change.
  • Day-2 BAU & toil. None recurring. The synthetic is automated and declared in schedule_config.yml. The handover does not increase manual load — it reduces it, by removing the per-wait composition step.
  • Backup & restore. Nothing stateful. Recovery is a redeploy from Git.
  • Standing owner. ansible-ci runs the self-test on every push; the script-observability ratchet inventories it; the schedule reconciler tracks the synthetic; the dead-man pages if it stops. No new human owner is created.
  • Exit. Retiring it means deleting the script, its manifest entry and its schedule row, and demoting the standard’s clause to advisory. No third party, so no provider-failure fallback applies.
  1. #2529 — waitfor helper, the standard, and its assurance, landed atomically. Everything in one delivery. Atomic because the standards meta-gate resolves declared mechanisms against the live repos, and a cross-repo declaration cycle reds the shared pipelines for every concurrent session. Begins by enumerating pitlab_access’s read verb names, which are unverified.
  2. #2530 — Production Readiness Review. Held back from /go deliberately. Evidence-checked by Pit. Two-week early-life soak watching for false refusals — a valid predicate wrongly rejected — since that is the failure mode that would drive usage back to hand-rolled loops.
  • Frequency was never measured. The case rests on three documented failures, not a census of how often waits are hand-rolled. If the true rate is low, this is over-built.
  • pitlab_access read verb names are UNVERIFIED. Both probes on 2026-08-18 were refused for wrong verb names. Child 1 must enumerate them first.
  • No schedule slot has been checked for the synthetic.
  • Layer-3 compliance was worked lightly — control testing is mapped; no full ISO 27001 Annex A traversal was done for what is an internal CLI helper.

Filled by wrapup Check O at Epic close.