Skip to content

Record a read run as ran and supersede a repeated proposal - #53

Merged
shipmill-romamo[bot] merged 2 commits into
mainfrom
feat/26-decision-ran-superseded
Oct 9, 2026
Merged

shipmill-romamo[bot] merged 2 commits into
mainfrom
feat/26-decision-ran-superseded

Conversation

@shipmill-romamo

Copy link
Copy Markdown
Contributor

Summary

Every operations propose saved its record as proposed, so a risk: read operation, which has already run in check mode by the time it is recorded, stayed proposed forever, and proposing the same operation again piled up open records (47 for cloudfall why --host hz1 in one disk-full incident). This implements D-1:

  • propose records a read operation with the new status ran and its run's real exit code in a new ranExitCode field (the same value as check.exitCode, which is still there)
  • when propose records an operation, every open proposed record with exactly the same operation id, targets (scope and pattern) and inputs becomes superseded, with supersededBy naming the new decision and a verdict saying so. Records in any other status (approved, executed, verified, failed, ran) are never touched
  • decisions approve refuses a ran or superseded record with PRECONDITION / decision_not_proposed (the existing refusal for a status that is not proposed), now before the person is asked to type the id; the superseded message names the newer decision
  • decisions list --status S and cloudfall why --status S (repeatable) keep only those statuses; an unknown status is a usage error (ARG_ERROR). The MCP decisions.list and why tools get the same argument
  • operation-decision.schema.json: ran and superseded are added to the status enum; ranExitCode and supersededBy are optional fields, required by if/then when the status is ran / superseded. Additive: records already on disk keep their status and load as before
  • why tells both statuses in its last sentence and gives them their own badge tone in the HTML page
  • operations propose reads the record before running check mode (to find what it supersedes), so a broken record now stops it with RECORD_INVALID before anything runs; RECORD_INVALID is added to its declared exit codes

stale is out of scope (postponed to a spec), so the issue stays open.

Test notes

  • New tests in sdk/tests/test_decision.py: a read recorded as ran with a non-zero exit code; a mutating proposal still proposed with no ranExitCode; supersede of the same call; no supersede for another target, another input value or an extra input; an approved record is never superseded; a pre-decisions: read operations stay "proposed" forever, and abandoned proposals are never superseded #26 read record (proposed) still loads and is superseded by a new read run; approve refuses ran and superseded without running anything; the issue's repro (read, then the same mutating proposal twice) lists ran, superseded, proposed, and --status filters it; an unknown --status is refused; the CLI refuses a superseded record before asking for its id
  • New tests in sdk/tests/test_why.py: the ran and superseded sentences, why --status (repeatable), an unknown status refused
  • Two existing tests approved a fresh read proposal to reach executed; a read is now ran, so they approve a read recorded the pre-decisions: read operations stay "proposed" forever, and abandoned proposals are never superseded #26 way instead, the only path left to executed (every mutating or destructive operation must declare a verify step)
  • uv run ruff check ., uv run mypy, uv run pytest (full suite), uv run cloudfall config validate --project config/examples and cloudfall-engine inventory render pass locally. The Ansible syntax-check and ansible-lint CI steps were not run locally (the session could not run them); this change touches no playbook

Part of #26

🤖 Generated with Claude Code

A risk: read operation has run by the time operations propose records it,
yet its record stayed proposed forever, and proposing the same operation
again left every older record open: one disk-full incident produced 47
records for one host. Following D-1, a read is now recorded as ran with
its run's exit code (ranExitCode), and a new proposal marks any open
proposal of the same operation, targets and inputs superseded, naming
itself in supersededBy. decisions approve refuses both before asking for
the id, and decisions list and why filter on --status (repeatable).

The schema change is additive; records already on disk keep their status.

Part of #26

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A read record written before #26 still says proposed. The next run of the
same read supersedes it with a decision recorded as ran, and approving the
old one pointed at that ran decision with "approve that one instead".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shipmill-romamo
shipmill-romamo Bot merged commit 7187b90 into main Oct 9, 2026
1 check passed
@shipmill-romamo
shipmill-romamo Bot deleted the feat/26-decision-ran-superseded branch October 9, 2026 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants