Skip to content

fix: accept the empty-state sentinel row in rotation and learnings gates - #10

Merged
ahrazzle merged 2 commits into
mainfrom
fix/empty-state-sentinel-gates
Oct 9, 2026
Merged

ahrazzle merged 2 commits into
mainfrom
fix/empty-state-sentinel-gates

Conversation

@abu-aska

@abu-aska abu-aska commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #9 (the gate half — not the bootstrap-script half).

Problem

A freshly bootstrapped ops record is all sentinel and no rows. check-inflight.py
has always skipped a row whose first cell contains empty state. The rotation and
learnings gates did not, so a brand-new install's very first preflight came back
red for the crime of having done nothing yet:

FAIL: row '(empty state)': previous worker '-' not on the roster
FAIL: row '(empty state)': next worker '-' not on the roster
FAIL: row '(empty state)': verified provider/model receipt empty or not a provider/model pair: '-'
FAIL: row '(empty state)': learning id must be exactly one id shaped lrn-<n>
FAIL: row '(empty state)': field 'incident' empty or placeholder: '-'
... (9 violations across the two gates)

Since the control-plane skill makes the rotation and learnings gates mandatory
during preflight of every dispatch, and section D says "a gate failure blocks
advancement", a fresh install cannot complete a compliant dispatch.

Fix

Skip the sentinel row in both gates, exactly mirroring the one-line skip already
in check-inflight.py. The skip is deliberately narrow — only a first cell
containing empty state is exempted. Every subsequent row is validated exactly
as before, which is what test_red_sentinel_does_not_mask_a_real_violation
guards.

Three of the three record gates now agree on what an empty record is.

Changes

  • scripts/protean-ops/check-rotation.py — sentinel skip in check_rotation_rows
  • scripts/protean-ops/check-learnings.py — sentinel skip in check_rows
  • tests/fixtures/valid/ROTATION-STATE-empty.md — green fixture
  • tests/fixtures/valid/LEARNINGS-empty.md — green fixture
  • tests/fixtures/valid/ROTATION-STATE-empty-then-offroster.md — sentinel
    followed by a genuinely off-roster row; proves the skip does not mask a live
    violation
  • tests/test_gates.py — 6 new cases (green sentinel, shipped fixture,
    and the red "not masked" case for each of the two gates)
  • .github/workflows/verify.yml — run both empty fixtures in the gate step, so
    the empty-record path is exercised on every push rather than only when a
    populated fixture happens to exist
  • CHANGELOG.md, .gitignore (__pycache__/)

Verification

  • New tests fail before the fix, pass after it — confirmed by stashing the two
    gate files, re-running the two sentinel tests (FAILED (failures=2) with the
    nine diagnostics above), then popping the stash.
  • Full suite: python3 -m unittest discover -s tests → 157 tests, OK.
  • Both new CI gate commands exit 0 from the repo root:
    check-rotation.py records/examples/valid/ROTATION-STATE-empty.md --roster records/examples/valid/ROSTER.txt
    and check-learnings.py records/examples/valid/LEARNINGS-empty.md.
  • gates/protean-ops/check-internal-names.py . → clean, 73 files, 0 findings.
  • Environment: Linux x86_64 (Omarchy), Python 3.14.7.

Scope note

This fixes only the gate behaviour. It does not add the bootstrap step that
materialises live records in the first place, and it does not resolve the
underlying design question about whether an empty record should be a sentinel
row or a header-only table — both are still open on #9 and deliberately left
there for the maintainer. This PR makes the existing documented sentinel
convention work consistently; it does not pick a new one.

Reviewers: per repo convention the author does not audit their own work — this
needs an independent QA verdict before merge.

A freshly bootstrapped record is all sentinel and no rows. check-inflight.py
has always skipped a row whose first cell contains "empty state"; check-rotation.py
and check-learnings.py did not, so a new install's first preflight was red for
the crime of having done nothing yet:

  FAIL: row '(empty state)': previous worker '-' not on the roster
  FAIL: row '(empty state)': verified provider/model receipt empty or not a provider/model pair: '-'
  FAIL: row '(empty state)': learning id must be exactly one id shaped lrn-<n>
  FAIL: row '(empty state)': field 'incident' empty or placeholder: '-'
  ... (9 violations)

Skip the sentinel in both gates, matching the inflight convention. The skip is
narrow: only a first cell containing "empty state" is exempted, and every later
row is validated exactly as before.

Ships green fixtures tests/fixtures/valid/ROTATION-STATE-empty.md and
LEARNINGS-empty.md. ROTATION-STATE-empty-then-offroster.md carries a real
off-roster row immediately after the sentinel, proving the skip does not mask a
live violation. Both empty fixtures are wired into verify.yml so the empty-record
path is exercised on every push.

Refs #9
The verify.yml gate step added in the previous commit references
records/examples/valid/, but the fixtures were only added under
tests/fixtures/valid/. CI failed with:

  FAIL: rotation record not found: records/examples/valid/ROTATION-STATE-empty.md
  FAIL: learnings record not found: records/examples/valid/LEARNINGS-empty.md

records/examples/valid/ is also the directory the descriptor's declared gate
commands and the README gate table use, so that is the right home for these.
The tests/fixtures/valid/ copies stay: those are what the unit tests load.
@ahrazzle
ahrazzle merged commit f2b7ce5 into main Oct 9, 2026
2 checks passed
@ahrazzle
ahrazzle deleted the fix/empty-state-sentinel-gates branch October 9, 2026 09:48
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.

Record templates are never instantiated: all live record gates FAIL on a fresh install

2 participants