Architecture No. 327

Saga Transactions skill for Claude Code

Multi-service transactions that undo cleanly, without 2PC. Claude designs a distributed transaction as an ordered list of compensable, pivot and retryable steps, defaults to an orchestrated state machine with persisted state, and gives every step an idempotency key, timeout and retry bound. A bundled checker rejects specs with missing compensations or a misplaced pivot and prints the exact undo order for each failure point.

  • namesaga-transactions
  • size236 lines · ≈3,150 tokens
  • files3
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Designs distributed business transactions as sagas instead of two-phase commit - classifying each step as compensable, pivot, or retryable, defaulting to an orchestrated, persisted state machine, writing compensating actions, wiring the transactional outbox and inbox, setting per-step timeouts and a saga deadline, countering missing isolation with semantic locks, and making long-running sagas observable. Use when an operation spans several services or databases, when someone proposes 2PC or XA, when a failed checkout leaves stock reserved or a card charged, or when choosing between Temporal and a hand-rolled orchestrator.

Try asking

  • Design the checkout saga across orders, inventory and payments so a failed payment releases the stock
  • Our team wants XA transactions between two services. Propose a saga instead
  • Sagas get stuck in COMPENSATING and nobody notices. Add timeouts and monitoring

Use it when

  • One business operation writes to several services or databases
  • Failures leave reservations held, cards charged, or orders half-created
  • Choosing orchestration vs choreography, or Temporal vs a hand-rolled orchestrator
  • Long-running sagas need timeouts, deadlines and operator visibility

Skip it when

How to check the result

check_saga.py passes on the step spec, and failure-injection tests show each failing step compensates prior steps in reverse order while duplicate deliveries change state only once.

The common mistake it prevents

Treating a step timeout as a failure and compensating while the original call is still in flight, so the charge lands after the refund.

~/.claude/skills/saga-transactions/SKILL.md

--- name: saga-transactions description: Designs distributed business transactions as sagas instead of two-phase commit - classifying each step as compensable, pivot, or retryable,… license: MIT ---

Done well means: every step is classified (compensable, pivot, retryable) and ordered so the point of no return comes as late as possible; every compensable step has a tested, idempotent compensation; saga state is persisted so a crash resumes instead of forgetting; every step has a timeout and the saga has a deadline; and an operator can answer “where is order 8812 stuck and why” in one query.

Ground rules

  • No two-phase commit or XA across services. Each step is one local ACID transaction in one service’s database, and it records its outcome atomically with the message that announces it (transactional outbox).
  • A compensation is a new forward business action (RefundPayment, ReleaseStock), not a database rollback. It must be idempotent, retryable, and must never fail for a business reason. If it can fail for a business reason, the step is not compensable.
  • Steps after the pivot must be retryable until they succeed. No compensation runs after the pivot commits.
  • Every message handler in a saga is an idempotent consumer (inbox table or natural idempotence). Delivery is at-least-once; duplicates and reordering are normal.
  • Sagas give ACD without I. Other requests see intermediate state; design for it explicitly (see “Isolation countermeasures”).
  • Persist saga state before sending the next command. In-memory orchestration loses sagas on every deploy.

Workflow

- [ ] 1. Confirm a saga is needed (one database? use one transaction)
- [ ] 2. List steps with owner service, local transaction, and side effects
- [ ] 3. Classify each step: compensable, pivot, retryable; reorder
- [ ] 4. Pick coordination: orchestration (default) or choreography
- [ ] 5. Pick runtime: workflow engine or hand-rolled state machine
- [ ] 6. Design messages, idempotency keys, outbox and inbox per participant
- [ ] 7. Set step timeouts, retry policy, and saga deadline
- [ ] 8. Add isolation countermeasures (semantic locks, pessimistic ordering)
- [ ] 9. Add observability and a stuck-saga runbook
- [ ] 10. Test failure at every step, duplicates, and crash between steps

1. Confirm a saga is needed

SituationDo this instead of a saga
All writes land in one database (even across modules)One local transaction
One write plus one eventTransactional outbox alone (event-driven-architecture)
A single external call that must not run twiceIdempotency key (idempotency-retries)
Two services share a hard invariant (balance never negative across both)Move the invariant into one service; a saga cannot enforce it
Multi-step, multi-owner, can tolerate seconds-to-days of intermediate stateSaga

2-3. Classify and order steps

KindDefinitionRequirement
CompensableCan be semantically undone laterHas an idempotent compensation that cannot fail for business reasons
PivotPoint of no return; once it commits the saga must completeExactly one; after it, only retryable steps
RetryableGuaranteed to succeed eventually given retriesIdempotent; no business-rule rejection possible

Ordering rules:

  1. Put steps most likely to be rejected (validation, credit check, stock reservation) first; they are cheap to compensate.
  2. Put irreversible or customer-visible effects (capture money, ship, send email, call a partner without a cancel API) at or after the pivot.
  3. Prefer reservations over final actions before the pivot: authorize instead of capture, reserve instead of decrement, hold instead of book.
  4. Turn non-compensable steps into retryable ones where possible (accept the order into a durable queue that will always be processed) rather than leaving two pivots.

Example, order checkout:

#StepServiceKindCompensation
1Create order PENDINGorderscompensableMark REJECTED
2Reserve stockinventorycompensableRelease reservation
3Authorize cardpaymentscompensableVoid authorization
4Approve order (APPROVED)orderspivot-
5Capture paymentpaymentsretryable-
6Request shipmentshippingretryable-
7Send confirmation emailnotificationsretryable-

Write the step list as JSON and validate it before writing code:

python3 scripts/check_saga.py saga.json

It fails on a missing or duplicate pivot, a compensable step after the pivot, a step before the pivot without a compensation, missing idempotency keys or timeouts, unbounded retries, and a step budget that exceeds the saga deadline; it then prints which compensations run, in order, if each step fails. Spec format is in the script docstring.

4. Coordination: default to orchestration

Orchestration (default)Choreography
ShapeOne orchestrator sends commands, receives replies, owns stateEach service reacts to the previous event and emits its own
Use when3+ steps, any timeout, branching, human approval, or need to see stateExactly 2-3 steps, one compensation, stable flow, teams want no central component
Saga stateOne row or workflow history per sagaSpread across services; reconstructed from events
Failure handlingExplicit, in one placeEvery participant must know which events trigger its compensation
RiskOrchestrator becomes a god service if it holds business rulesCyclic event dependencies; nobody can answer “where is it stuck”

Rules for the orchestrator: it decides sequence and compensation only. Business rules (pricing, stock policy) stay in participants. One orchestrator per saga type, owned by the service that owns the business outcome (orders owns checkout).

5. Runtime: engine or hand-rolled

SituationChoose
Temporal (or similar durable execution) already runs in the orgEngine; see temporal-workflows for SDK details
Waits longer than minutes, timers, human steps, many saga typesEngine (Temporal, AWS Step Functions, Azure Durable Functions, Camunda)
One or two short saga types, team owns Postgres and a broker, no new infra budgetHand-rolled state machine table + outbox
Serverless on one cloud with simple linear stepsThat cloud’s state machine service

A workflow engine replaces the orchestrator state table, timers, and retries. It does not replace idempotent participants, compensations, or the outbox in participant services. For the hand-rolled path (tables, transition function, timeout scanner, stuck-saga queries), read references/orchestrator-tables.md.

6. Messages, keys, outbox, inbox

  • Saga ID = correlation ID on every command, reply, event, and log line.
  • Idempotency key per step: <saga_id>:<step_name>; for compensations <saga_id>:<step_name>:compensate. Pass the same key to external providers (Stripe Idempotency-Key, partner APIs) so retried calls do not double-charge.
  • Participant pattern, in one local transaction:
    1. Insert into inbox (consumer, message_id) with ON CONFLICT DO NOTHING; if no row inserted, it is a duplicate: re-send the stored reply and stop.
    2. Apply the business change.
    3. Insert the reply (StockReserved / StockReservationFailed) into outbox.
  • Orchestrator pattern, in one local transaction: update saga state and version, insert the next command into its own outbox.
  • Replies carry the step name and the saga version they answer. The orchestrator ignores replies for a step that is no longer current (late reply after timeout).
  • Compensation must handle “the original never happened”: ReleaseStock for a reservation that does not exist returns success, and records a tombstone so a late ReserveStock arriving after the release is rejected.

7. Timeouts, retries, deadline

  • Every step: a timeout on the orchestrator side (no reply in N seconds = treat as unknown, not failed). Unknown means retry the same command with the same key; only a definite business rejection triggers compensation.
  • Retry policy per step: exponential backoff with jitter, bounded attempts for compensable steps (then compensate), unbounded-but-alerting for retryable steps and compensations (they must eventually succeed; page a human after N attempts).
  • Saga deadline: a business number (“cancel if payment not authorized within 15 minutes”). When it fires before the pivot, compensate; after the pivot, alert.
  • Payment authorizations and stock reservations expire on the provider side too; keep the saga deadline shorter than the shortest reservation lifetime.

8. Isolation countermeasures

AnomalyCountermeasure
Another request acts on a half-done order (dirty read)Semantic lock: status = 'PENDING' (or *_PENDING states); other operations reject or queue while pending
Two sagas overwrite each other (lost update)Commutative updates (increment/decrement deltas, not absolute sets) or version check on write
Compensation undoes something another saga relied onPessimistic ordering: move updates other flows read into retryable steps after the pivot
Data changed between saga stepsReread and compare a version before the step commits; restart or fail the saga on mismatch

Expose pending states in the UI and API (“Payment processing”) instead of hiding them.

9. Observability

Minimum signals:

  • Count of sagas by type and state; age of the oldest non-terminal saga per state.
  • Step latency histogram per step; compensation count and rate per step.
  • Sagas past deadline; compensations retrying more than N times; outbox backlog age.
  • A trace per saga: start a span at saga creation and propagate trace context through outbox headers so the full flow appears as one trace.

Alert on: oldest COMPENSATING saga older than the deadline, any FAILED (compensation exhausted) saga, compensation rate above baseline for one step.

Runbook entries every saga type needs: how to find a saga by business ID, how to retry the current step, how to force-compensate, how to mark resolved after manual repair. Manual actions that move money or stock require explicit confirmation from the user or operator, and are logged with who and why.

Gotchas

  • Treating a timeout as failure and compensating while the original request is still in flight: the charge lands after the refund. Timeouts mean unknown; retry first.
  • Compensations that call APIs which can reject (refund window closed, coupon already used). That step was never compensable; move it after the pivot.
  • Sending an email or webhook before the pivot. You cannot unsend it.
  • Running compensations in parallel when they depend on each other; run them in reverse step order unless proven independent.
  • Saga state in the orchestrator’s memory or a cache. A redeploy strands every in-flight saga.
  • Changing the step list while sagas are in flight. Version the saga definition and let old instances finish on the old version.
  • Choreography where the compensation trigger is “absence of an event”. Absence needs a timer, and a timer needs an owner; that owner is an orchestrator.
  • Assuming the broker gives exactly-once. It does not; the inbox does.

Verification

  1. python3 scripts/check_saga.py saga.json passes and the printed compensation plan matches what the team expects for each failure point.
  2. Failure injection test per step: force step N to reject; assert steps N-1..1 are compensated in reverse order and the saga ends COMPENSATED.
  3. Duplicate delivery: deliver every command and reply twice; state changes once.
  4. Crash test: kill the orchestrator after the state update commits but before the outbox relay publishes; on restart the command is still sent.
  5. Late reply: delay a reply past the step timeout; the retry and the late reply produce one effect and the saga does not regress.
  6. Deadline: let a saga sit in a pre-pivot state past its deadline; it compensates.

If a test fails, fix the participant’s idempotency or the step classification before adding retries.

Output contract

When designing a saga, deliver:

## Saga: <name> (owner: <service>, coordination: orchestration, runtime: <engine|hand-rolled>)
Deadline: <duration> from <trigger>

| # | Step | Service | Kind | Command / reply | Compensation | Idempotency key | Timeout | Retry |
|---|---|---|---|---|---|---|---|---|

Isolation: <semantic lock states and who honors them>
Observability: <metrics, alerts, trace propagation>
Failure plan: <output of scripts/check_saga.py>
Open questions: <provider cancel windows, reservation expiry, etc.>

Bundled files

Loaded only when a step in the skill needs them. Review them here before installing.

references/orchestrator-tables.md 152 lines · 6.9 KB
# Hand-rolled saga orchestrator (Postgres)

Read this when the saga runs without a workflow engine: you need the state table, the
transition function, the timeout scanner, and the operator queries. Outbox relay
mechanics (polling vs CDC, ordering) are covered by the event-driven-architecture skill;
this file only shows how the orchestrator uses them.

## Tables

```sql
CREATE TABLE saga_instance (
  saga_id        uuid PRIMARY KEY,
  saga_type      text        NOT NULL,          -- 'checkout'
  definition_ver int         NOT NULL,          -- step list version this instance follows
  business_key   text        NOT NULL,          -- order id, for operator lookup
  state          text        NOT NULL,          -- RUNNING | COMPENSATING | COMPLETED | COMPENSATED | FAILED
  current_step   text        NOT NULL,
  attempt        int         NOT NULL DEFAULT 1,
  version        bigint      NOT NULL DEFAULT 0,  -- optimistic lock; bump on every transition
  payload        jsonb       NOT NULL,          -- ids gathered so far (reservation_id, auth_id)
  step_deadline  timestamptz NOT NULL,          -- when the current step times out
  saga_deadline  timestamptz NOT NULL,          -- business deadline
  last_error     text,
  created_at     timestamptz NOT NULL DEFAULT now(),
  updated_at     timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX saga_due ON saga_instance (step_deadline)
  WHERE state IN ('RUNNING', 'COMPENSATING');
CREATE UNIQUE INDEX saga_business ON saga_instance (saga_type, business_key);

CREATE TABLE saga_step_log (            -- append-only audit trail, one row per transition
  saga_id    uuid        NOT NULL REFERENCES saga_instance,
  seq        bigint      NOT NULL,
  step       text        NOT NULL,
  event      text        NOT NULL,     -- COMMAND_SENT | REPLY_OK | REPLY_REJECTED | TIMEOUT | COMPENSATION_SENT ...
  detail     jsonb,
  at         timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (saga_id, seq)
);
```

The unique index on `(saga_type, business_key)` makes "start saga for order 8812" itself
idempotent: a duplicate start request hits the conflict and returns the existing saga.

## Transition function

Every reply and every timeout goes through one function that runs in one transaction:
lock the row, check the reply is for the current step, compute the next state, write the
state, append to the log, insert the next command into the outbox.

```python
STEPS = ["create_order", "reserve_stock", "authorize_card", "approve_order",
         "capture_payment", "request_shipment"]
PIVOT = "approve_order"
COMPENSATION = {"create_order": "reject_order", "reserve_stock": "release_stock",
                "authorize_card": "void_authorization"}

def on_reply(tx, saga_id, step, outcome, data):
    s = tx.fetch_one("SELECT * FROM saga_instance WHERE saga_id=%s FOR UPDATE", saga_id)
    if s.state in ("COMPLETED", "COMPENSATED", "FAILED"):
        return                                   # late or duplicate reply
    if step != s.current_step:
        return                                   # reply for an older step: ignore
    i = STEPS.index(step)
    if s.state == "RUNNING" and outcome == "ok":
        payload = {**s.payload, **data}
        if i + 1 == len(STEPS):
            transition(tx, s, state="COMPLETED", step=step, payload=payload)
        else:
            nxt = STEPS[i + 1]
            transition(tx, s, state="RUNNING", step=nxt, payload=payload)
            send_command(tx, s, nxt, payload)
    elif s.state == "RUNNING" and outcome == "rejected":
        if STEPS.index(step) > STEPS.index(PIVOT):
            raise RuntimeError("retryable step rejected: page a human, do not compensate")
        start_compensation(tx, s, failed_index=i)
    elif s.state == "COMPENSATING" and outcome == "ok":
        continue_compensation(tx, s, done=step)

def start_compensation(tx, s, failed_index):
    todo = [STEPS[j] for j in range(failed_index - 1, -1, -1) if STEPS[j] in COMPENSATION]
    if not todo:
        transition(tx, s, state="COMPENSATED", step=STEPS[failed_index], payload=s.payload)
        return
    transition(tx, s, state="COMPENSATING", step=todo[0], payload={**s.payload, "undo": todo})
    send_command(tx, s, COMPENSATION[todo[0]], s.payload, compensate=True)
```

`transition` does `UPDATE ... SET state, current_step, attempt=1, version=version+1,
step_deadline=now()+timeout(step) WHERE saga_id=%s AND version=%s` and fails if no row
matched. `send_command` inserts into the orchestrator's outbox with idempotency key
`<saga_id>:<step>` or `<saga_id>:<step>:compensate`.

## Timeout scanner

Run every few seconds from one or more workers. `SKIP LOCKED` lets several workers share
the load without double-processing a saga.

```sql
BEGIN;
SELECT saga_id, current_step, attempt, state
FROM saga_instance
WHERE state IN ('RUNNING', 'COMPENSATING') AND step_deadline < now()
ORDER BY step_deadline
LIMIT 50
FOR UPDATE SKIP LOCKED;
-- for each row, in application code:
--   if saga_deadline passed and step is before the pivot  -> start_compensation
--   elif attempt < max_attempts(step) or step is retryable/compensation
--        -> re-send the SAME command (same idempotency key), attempt+1, new step_deadline
--   else (pre-pivot step exhausted)                        -> start_compensation
--   compensation exhausted alert_after                     -> state stays COMPENSATING, alert
COMMIT;
```

Never mark a timed-out step as failed without re-sending first: the participant may have
done the work and the reply may be delayed.

## Operator queries

```sql
-- Where is order 8812?
SELECT s.state, s.current_step, s.attempt, s.last_error, s.updated_at
FROM saga_instance s WHERE saga_type='checkout' AND business_key='8812';
SELECT seq, step, event, detail, at FROM saga_step_log
WHERE saga_id = (SELECT saga_id FROM saga_instance WHERE business_key='8812') ORDER BY seq;

-- Oldest stuck saga per state (alert input)
SELECT saga_type, state, current_step, min(updated_at) AS oldest, count(*)
FROM saga_instance WHERE state IN ('RUNNING','COMPENSATING')
GROUP BY 1,2,3 ORDER BY oldest;

-- Compensation rate per step over the last day
SELECT step, count(*) FILTER (WHERE event='REPLY_REJECTED') AS rejected, count(*) AS replies
FROM saga_step_log WHERE at > now() - interval '1 day' GROUP BY step;
```

Manual repair (force-compensate, skip a step, mark resolved) is a transition like any
other: go through the transition function, write a log row with the operator name and
reason, and get explicit confirmation before running it against production.

## Versioning the definition

- Store `definition_ver` on each instance. Deploy the new step list as a new version;
  keep the old transition table until no instance with the old version is non-terminal.
- Removing or reordering steps in place breaks in-flight sagas whose `current_step` no
  longer exists or now means something else.

## Retention

Archive terminal sagas and their logs after the dispute window for the business (for
payments, often months). Keep the archive queryable by business key.
scripts/check_saga.py 141 lines · 5.9 KB
#!/usr/bin/env python3
"""Validate a saga step definition and print its compensation plan.

Usage:
    python3 check_saga.py saga.json

Spec format (JSON):
{
  "name": "checkout",
  "deadline_s": 900,
  "steps": [
    {"name": "create_order", "service": "orders", "kind": "compensable",
     "compensation": "reject_order", "idempotency_key": "{saga_id}:create_order",
     "timeout_s": 5, "retry": {"max_attempts": 3}},
    {"name": "approve_order", "service": "orders", "kind": "pivot",
     "idempotency_key": "{saga_id}:approve_order", "timeout_s": 5,
     "retry": {"max_attempts": 3}},
    {"name": "capture_payment", "service": "payments", "kind": "retryable",
     "idempotency_key": "{saga_id}:capture", "timeout_s": 10,
     "retry": {"max_attempts": null, "alert_after": 10}}
  ]
}

kind is one of: compensable, pivot, retryable.
retry.max_attempts: integer, or null for "retry until success" (allowed only for
retryable steps, and then retry.alert_after is required).

Exit codes: 0 = no errors, 1 = design errors found, 2 = unreadable input.
"""
import json
import sys

KINDS = {"compensable", "pivot", "retryable"}


def main(argv):
    if len(argv) != 2:
        print("usage: python3 check_saga.py saga.json")
        return 2
    try:
        with open(argv[1], encoding="utf-8") as fh:
            spec = json.load(fh)
    except (OSError, json.JSONDecodeError) as exc:
        print(f"ERROR: cannot read {argv[1]}: {exc}")
        return 2

    errors, warnings = [], []
    steps = spec.get("steps")
    if not isinstance(steps, list) or not steps:
        print("ERROR: spec needs a non-empty 'steps' list")
        return 2

    deadline = spec.get("deadline_s")
    if not isinstance(deadline, (int, float)) or deadline <= 0:
        errors.append("saga has no positive 'deadline_s' (business deadline for completion)")

    names, comp_names = set(), set()
    pivot_idx = [i for i, s in enumerate(steps) if s.get("kind") == "pivot"]
    if len(pivot_idx) > 1:
        errors.append(f"{len(pivot_idx)} pivot steps; a saga has exactly one point of no return")
    if not pivot_idx:
        warnings.append("no pivot step: every step is treated as compensable; confirm nothing is irreversible")
    pivot = pivot_idx[0] if pivot_idx else len(steps)

    budget = 0.0
    for i, s in enumerate(steps):
        n = s.get("name") or f"<step {i + 1}>"
        kind = s.get("kind")
        if n in names:
            errors.append(f"{n}: duplicate step name")
        names.add(n)
        if kind not in KINDS:
            errors.append(f"{n}: kind must be one of {sorted(KINDS)}, got {kind!r}")
            continue
        if not s.get("idempotency_key"):
            errors.append(f"{n}: missing idempotency_key (retries and duplicate deliveries will repeat the effect)")
        t = s.get("timeout_s")
        if not isinstance(t, (int, float)) or t <= 0:
            errors.append(f"{n}: missing positive timeout_s")
            t = 0
        retry = s.get("retry") or {}
        attempts = retry.get("max_attempts", "missing")
        if attempts == "missing":
            errors.append(f"{n}: retry.max_attempts not set (use an integer, or null for retry-until-success on retryable steps)")
            attempts = 1
        elif attempts is None:
            if kind != "retryable":
                errors.append(f"{n}: unbounded retries on a {kind} step; bound it so the saga can compensate")
            elif not retry.get("alert_after"):
                errors.append(f"{n}: unbounded retries need retry.alert_after so a human is paged")
            attempts = retry.get("alert_after") or 1
        elif not isinstance(attempts, int) or attempts < 1:
            errors.append(f"{n}: retry.max_attempts must be a positive integer or null")
            attempts = 1
        if i < pivot:
            budget += t * attempts

        if i < pivot:
            if kind == "retryable":
                warnings.append(f"{n}: retryable step before the pivot; fine only if it has no effect that needs undoing")
            elif kind == "compensable" and not s.get("compensation"):
                errors.append(f"{n}: compensable step without a 'compensation'")
        elif i > pivot:
            if kind == "compensable":
                errors.append(f"{n}: compensable step after the pivot; after the point of no return steps must be retryable")
        comp = s.get("compensation")
        if comp:
            if i >= pivot:
                errors.append(f"{n}: has a compensation but sits at/after the pivot; it would never run")
            if comp in comp_names or comp in {x.get("name") for x in steps}:
                errors.append(f"{n}: compensation name {comp!r} collides with another step or compensation")
            comp_names.add(comp)

    if isinstance(deadline, (int, float)) and deadline > 0 and budget > deadline:
        errors.append(
            f"pre-pivot worst case {budget:.0f}s (timeout x attempts) exceeds deadline_s {deadline}s; "
            "shorten timeouts or attempts, or the deadline fires mid-retry"
        )

    print(f"Saga: {spec.get('name', '<unnamed>')}  steps={len(steps)}  pivot="
          f"{steps[pivot]['name'] if pivot < len(steps) else 'none'}")
    for w in warnings:
        print(f"WARN:  {w}")
    for e in errors:
        print(f"ERROR: {e}")

    print("\nFailure plan (compensations run in this order):")
    for i, s in enumerate(steps):
        n = s.get("name") or f"<step {i + 1}>"
        if i < pivot or (i == pivot and pivot < len(steps)):
            undo = [steps[j].get("compensation") for j in range(i - 1, -1, -1) if steps[j].get("compensation")]
            print(f"  {n} fails -> " + (", ".join(undo) if undo else "nothing to compensate; mark saga failed"))
        else:
            print(f"  {n} fails -> retry until success; alert, never compensate")

    print(f"\nResult: {'FAIL' if errors else 'PASS'} ({len(errors)} errors, {len(warnings)} warnings)")
    return 1 if errors else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc