Skip to content

fix(gates): a mid-stage decision is not the stage approval (#477) - #483

Merged
MendixMau merged 3 commits into
masterfrom
fix/gate-stage-approval
Oct 20, 2026
Merged

MendixMau merged 3 commits into
masterfrom
fix/gate-stage-approval

Conversation

@MendixMau

@MendixMau MendixMau commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Fixes #477

Direct lane (changes to skills/, bug-logs/, bin/, project-bin/, project-tests/, pipelines)

What changed and why (one paragraph):
The #207 check prefixes a FAIL with APPROVED OVER A FAILING GATE when the register says the stage was approved. It asked has_confirmed_decision "$stage", which accepts any CONFIRMED Decisions row whose Stage cell is N. So a stage still being worked on raised the flag the moment the user answered any question in it. The issue's field case was | 5 | Sub-agents run on Sonnet | CONFIRMED | user in chat | on a mid-build Stage 5. status.sh then printed WATCH APPROVED OVER A FAILING GATE: Stage 5 and a NEXT telling the lead to re-open an approval nobody gave, which buried the real gap: the PENDING LOOK/sweep/journeys/coherence obligations. This PR adds stage_exit_recorded N, used only by that flag. It counts a stage as approved on either of two pieces of evidence, both in CONFIRMED rows of the Decisions table, with the same table scoping and the same field-anchored CONFIRMED (dated statuses allowed) as has_confirmed_decision:

  1. An explicit exit row for Stage N, in one of four wordings:
    • Stage N approved / Stage N exit… / Stage N gate: signed off / … closed|complete|done|passed. Separators may be spaces, :, -, or an em or en dash, e.g. Stage 4 — approved with conditions. The dashes are written as an awk alternation, not inside a bracket class, so POSIX awk handles the bytes; checked under gawk, mawk and original-awk.
    • Approved: Stage N ….
    • Gate approved|signed off|passed|closed, with gate as the first word.
    • <artifact> approved|signed off, where the artifact comes from a fixed list: build plan, plan, blueprint, architecture, design (system), wireframes, scope, triage, BRDs, requirements. Domain decisions such as Requests over 5k must be approved, Status values: draft, submitted, approved or Approval gate closed after 30 days therefore never match, and neither does Stage 40 approved.
    • No checkpoint writes such a row today. In practice rule 2 does the real work. Having the CAC checkpoint write an exit row is the follow-up (see "Left open").
  2. The project moved past N. Stage N has CONFIRMED rows of its own and there is a CONFIRMED row for a later stage up to 6. This is how the [bug] A stage approval can be recorded over a failing gate; nothing compares the two (exec.sh/status.sh stay silent) #207 field case read: a Stage-4 approval, then 47 scripts built on it. It is also the "register's position is past N" reading the issue suggests; bin/lib/artifact-check.sh already derives the register's rank from CONFIRMED rows. Stage 7 is excluded, and so is any later row reading N/A / skip / no cutover. checkpoint-cutover.md and existing-app-change.md tell projects to write No legacy system, no cutover early, and that is not evidence Stage 5 was signed off. A forward-filed decision never "approves" a stage that has no rows of its own.

has_confirmed_decision and the Stage 3/4 gate checks are unchanged: there, any CONFIRMED row is the evidence that the stage's interview was held. Every verdict and exit code is unchanged. The only wording change is the flag's note, Stage N is approved in PROJECT.md instead of is CONFIRMED in; the summary line status.sh greps (^Approved over a failing gate:) is untouched.

The documented rule is ambiguous, so this picks the safest reading. conversion-runbook.md §2 closes a gate on "its ✋ decisions CONFIRMED and gate-check.sh run with its output pasted". §1 step 5 writes every gate decision to PROJECT.md as CONFIRMED. The register format init-project.sh writes is | Stage | Decision | Status | Notes |. Nowhere is there a dedicated stage-exit row, and no CAC checkpoint writes one. So "Stage N CONFIRMED" can mean "a ✋ decision of Stage N was confirmed" (the Stage 3/4 gates' reading, and #207's) or "Stage N was signed off" (what the flag's wording claims). I read it as sign-off evidence: an explicit exit row or a later stage's decisions. That kills the #477 false positive and keeps the #207 true positive. A project that skips ahead without ever writing an exit row is still flagged, through rule 2. What it gives up: a mid-stage gate decision intended as the approval is not flagged until the project moves on. Residual: a stage that has its own mid-stage rows and a forward-filed decision for a later stage (≤ 6) is still flagged. Only an exit row written by the checkpoint can tell those apart. The FAIL verdict and exit code stay visible either way.

Field evidence (rule 4):
Reported by MendixMau on a requirements-driven field project (Mendix 11.13.0, toolkit ecb918c, 2026-10-20): the flag sat over a FAIL produced by two mid-stage decisions (sub-agent model, test depth). The register row in the fixture is quoted verbatim from the issue. Reproduced on master with that row over a failing Stage 4 (Stage 4 stands in for 5, because its FAIL needs only a plan without closing rows and no harness obligations): the note and the summary both read APPROVED OVER A FAILING GATE — Stage 4 is CONFIRMED. With the fix, no flag; an explicit exit row or a later-stage decision still flags. The author session could not use any client project, so a re-run of gate-check.sh . 5 and status.sh . --brief on the reporting project is owed by the maintainer.

Impact

Row Answer
Process: human steps or gates added/removed; does anything new block; new stage artifacts none. Nothing new blocks. One advisory flag fires less often (only on sign-off evidence).
Dependencies: shared files touched; order vs other open PRs; new tools/platform assumptions; new artifact reads + their producer bin/gate-check.sh only (one new function, one call site). It reads PROJECT.md's Decisions table as before. Producers: §1 step 5 / the CAC checkpoints (decision rows), and the next stage's own decisions (rule 2). No mandatory step writes an explicit exit row today. Rule 2 needs none, and that is why it is there. status.sh is unchanged and reads the same summary line. No ordering constraint against #469/#470/#471 beyond a trivial CHANGELOG.md line merge.
Context cost: always-loaded words per role, base → head none: no routed or always-loaded file touched (bin/render-routing.sh --check clean, same numbers).
Behaviour: A/B for agent-read text n/a: scripts only.
Runtime One more awk pass over PROJECT.md, only for a stage that FAILs. Negligible.
  • No client data anywhere in the diff (whole-word grep of added lines for the denylist terms, /Users/ and /home/user: 0 hits; LEAKGUARD_ALLOW_NO_DENYLIST=1 bin/check-no-client-data.sh clean)
  • Size cap: 3 files, about 85 lines in gate-check.sh (mostly the comment); the rest is the fixture and CHANGELOG
  • Test tier reached: T1. tests/wave2/test-bug03-gates.sh bin/gate-check.sh gives 47/47. T17 has 16 cases:
    • must flag (in both the note and the summary): Stage 4 approved, a dated Stage 4 gate: signed off, Build plan approved, Stage 4 — approved with conditions, Approved: Stage 4 build plan, and the stage's own rows plus a later-stage row;
    • must not flag: the issue's verbatim mid-stage row; four domain wordings (Purchase requests above 5k need approval, Status values: draft, submitted, approved, Requests over 5k must be approved, Approval gate closed after 30 days); Stage 40 approved; an ASSUMED exit row or another stage's row; an Open-questions row; and, over a failing Stage 5 with no rows of its own, | 7 | No legacy system, no cutover | CONFIRMED | and | 6 | Test depth: journeys per module | CONFIRMED |.
    • On master's gate-check.sh (copied under bin/ so TOOLKIT_DIR resolves the same) T17 gives 7 FAIL. On the first head of this PR (e8db9ef) it gave 8 FAIL: the review's two Stage-5 false positives, the dash and Approved: wordings, and the domain/Stage 40 matches. The gate fixtures that grep CONFIRMED all pass: test-bug02-register.sh 8/0, test-closeout.sh 34/0, test-stage-header.sh 13/0, test-stage-p.sh 14/0, test-wireframe-count.sh 5/0, test-wrong-verdicts.sh 20/0, test-status-existing-app.sh bin/status.sh 27/27. bin/check-scripts.sh, bin/check-portability.sh, bin/render-routing.sh --check and bin/check-pr-discipline.sh origin/master are clean.
  • For a changed instrument: golden input is the issue's verbatim register row. Layouts: the register is resolved by the existing $REGISTER logic, unchanged. Platforms: POSIX awk only (split, tolower, dynamic regex), no gawk extensions (check-portability.sh clean).
  • For a new skill: n/a
  • CHANGELOG.md line appended, crediting MendixMau
  • New bug entries: n/a

Left open: the issue's other option, a dedicated Stage N approved row written by the CAC checkpoint, would make rule 1 the normal path. That changes skills/checkpoints/ and the runbook, which are agent-read text that needs its own A/B per the authoring rules, so it is not in this PR.

https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw


Generated by Claude Code

The approved-over-a-failing-gate flag (#207) read any CONFIRMED row of
stage N as the approval, so a stage still in progress was flagged the
moment any question in it was answered. New stage_exit_recorded counts
an explicit exit row ("Stage N approved", "Build plan approved") or a
CONFIRMED decision for a later stage. Stage 3/4 gate checks unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
Rule 2 now needs the stage's own CONFIRMED rows plus a later stage up to
6, and skips N/A / skip / no-cutover rows, so an early Stage-7 "no
cutover" row or a forward-filed Stage-6 decision no longer flags a
mid-build Stage 5. Rule 1 accepts em/en dashes and "Approved: Stage N",
and its short form is limited to an artifact-noun allowlist, so domain
decisions ("Requests over 5k must be approved") and "Stage 40 approved"
no longer match. T17 grows to 16 cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
@MendixMau
MendixMau merged commit efd4e9a into master Oct 20, 2026
2 checks passed
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.

[bug] gate-check: any CONFIRMED row for the current stage counts as the stage approval, so an in-progress stage reports "APPROVED OVER A FAILING GATE"

2 participants