From afd74828e35e3741c4d9ec9315d392f7d594afc9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 15:10:59 +0000 Subject: [PATCH 1/3] status: delivery rounds in existing-app mode, manual-step template, no above-stage obligations in NEXT #301: in existing-app-change mode status.sh prints a ROUND line read from a PROJECT.md "## Rounds" table (last row = round in flight) and drops the agent-slot and done- stall items from WATCH and NEXT. Pipeline modes print byte-identical output. #298: templates/manual-steps.md for edits mxcli cannot round-trip (path, why, depends-on, exact clicks, Check, one Done line); a step done by patch still goes through exec.sh --patch. status.sh prints "manual steps pending N of M" when docs/manual-steps.md exists. Review of #303: NEXT drops obligations whose from-stage is above the project's current stage (final-look was sent to Stage 1-5 projects). Fixture tests/wave2/test-status-existing-app.sh: 23/23; 13 fail on the old script. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + bin/status.sh | 71 ++++++++- skills/existing-app-change.md | 59 ++++++++ templates/manual-steps.md | 40 +++++ tests/wave2/test-status-existing-app.sh | 190 ++++++++++++++++++++++++ 5 files changed, 359 insertions(+), 2 deletions(-) create mode 100644 templates/manual-steps.md create mode 100755 tests/wave2/test-status-existing-app.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 20342404..c5bd2ff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. ## Unreleased +- feat(status, existing-app-change): **existing-app-change mode reports the delivery round, not just the stage; agent-slot and done- noise is dropped in that mode only; a template for targeted manual Studio Pro steps, counted by `status.sh`** ([#301](https://github.com/MendixMau/mxcli-project-toolkit/issues/301), [#298](https://github.com/MendixMau/mxcli-project-toolkit/issues/298)). A field run delivered 11 backlog stories in 4 rounds on a 127-module app while `status.sh` still read "Stage 1 — Analysis", WATCH said "agent slots unfilled: 35 in 6 file(s)" and "done- stalled", and NEXT pointed at those. A `## Rounds` table in `PROJECT.md` (`| Round | Title | Stories | State |`, last row = round in flight) now gives a `ROUND Round N: — <state> · stories: … · D of N round(s) delivered` line in existing-app-change mode, and that mode drops the agent-slot and done- items from WATCH and NEXT. The same run needed six microflow edits mxcli could not round-trip (branches that rejoin early): new `templates/manual-steps.md` (document path, why mxcli cannot, depends-on, exact clicks with an anchor, a Check a person runs, one `- [ ] Done` line naming its evidence), and the rule that a step done by patch still goes through `exec.sh --patch`. `status.sh` prints `manual steps pending N of M` when `docs/manual-steps.md` exists. Also, from the review of #303: NEXT no longer names an obligation first owed at a later stage than the project is in (`final-look`, Stage 6, was sent to Stage 1–5 projects and hid gate-check's next-stage hint); the obligations-pending line still lists it. Apart from that NEXT fix, pipeline modes print byte-identical to before (fixture `tests/wave2/test-status-existing-app.sh`, 23 cases on real scaffolds; 13 fail on the old script) — jopterhorst - learn(testing-shape, test-result-audit): **three test-strength rules, each with a denominator: refusals probed on the server, every undeclared 4xx a failure, one clean-database final run** ([#172](https://github.com/MendixMau/mxcli-project-toolkit/issues/172)). A field comparison of two builds against one frozen spec found the toolkit build checked permission refusals only in the client (a hidden menu item), logged unexpected 400/422 responses as information, and reached 20/20 by stitching three runs. `testing-shape.md` §7 gains three report lines (`N of N restricted pairs probed server-side`, `undeclared 4xx/5xx: 0 of M requests — each fails its step`, `one clean-DB run <id>: N of N scenarios incl. timers, not stitched`); `test-result-audit.md` gains check 2e, which carries the reasons and the field evidence and asks for those numbers. Both baseline skills stay inside the 80,000-word baseline budget (79,997): the reasons live in the on-demand audit skill, and two redundant sentences in `testing-shape.md` §4's verdict paragraphs were folded (the empty-set corollary is stated in the Trace row, `harness-architecture.md` and `report-schema.md`). The backing inbox note is trimmed to its unpromoted "Keep" items. — MendixMau - learn(learned-detection-gaps, module-review): **two UI defects every model gate passes: a lazy-loaded combo box filter hides its preset, and a theme that pins row heights by position lets the next row cover an inserted widget** ([#299](https://github.com/MendixMau/mxcli-project-toolkit/issues/299)). Both passed check, mxbuild, lint and the scripted checks on a 127-module production app and were found only in the running app. Two register rows with the first oracle and the pre-build check (pluggable properties that change *when* data loads; grep the theme for fixed `height` on positional row classes before an insert), and two LOOK lines in `module-review.md` §4c with denominators (`N of N filters show their preset on first load`, `N of N widgets inserted into an existing container fully visible`). — jopterhorst - fix(app-restart, test-stack-up): **a port held by another process before `run --local` is named and reported as "instrument blocked" (exit 4), never killed** ([#262](https://github.com/MendixMau/mxcli-project-toolkit/issues/262)). Studio Pro had started its own runtime on the app/admin ports a restart needed; `mxcli run --local` then failed with "Authentication failed" (the admin API answering was Studio Pro's), the next journey ran against Studio Pro's runtime, and four more runs were NOT RUN. `app-restart.sh` now checks the app port and the admin port (record's `adminPort`, else app + 10) before stopping anything — a holder outside the recorded process tree exits 4 with its name, pid and parent — and again after the stop, where anything still holding a port is by definition not ours. `test-stack-up.sh` runs the same check wherever it would tell you to start `run --local` (`--no-docker`, no Docker daemon, `--check` with a leftover record), on the record's ports, `RUN_LOCAL_APP_PORT`/`RUN_LOCAL_ADMIN_PORT`, or mxcli's defaults 8080/8090. Holder lookup: lsof, then ss, then netstat + tasklist on Windows; unnamed holders still block when nothing of ours is running. Fixtures: `test-app-restart.sh` +2 cases (14 assertions, all fail on the old script), `test-stack-up-local.sh` T7 (4 of 6 fail on the old script). — AlvinnYeo diff --git a/bin/status.sh b/bin/status.sh index fb1b9435..3a404150 100755 --- a/bin/status.sh +++ b/bin/status.sh @@ -109,6 +109,46 @@ fi PH_TOTAL="$(mxtk_placeholder_total "$PROJECT_DIR"/.claude/agents/*.md "$PROJECT_DIR/CLAUDE.local.md")" PH_FILES="$(mxtk_placeholders "$PROJECT_DIR"/.claude/agents/*.md "$PROJECT_DIR/CLAUDE.local.md" | grep -c .)" +# --- existing-app-change: the delivery round, not the stage (#301) ---------------------------- +# A change stream on a live app is delivered as rounds of small stories, and the stage line stood +# still at "Stage 1" through four delivered rounds (field, 2026-10-13). The round is the unit of +# status in that mode: PROJECT.md "## Rounds" (skills/existing-app-change.md), one row per round, +# columns found by header name. The LAST row is the round in flight. Nothing here is read, and +# nothing printed, in the other entry modes. +ROUND_LINE=""; EAC=0 +if [ "$ENTRY" = "existing-app-change" ]; then + EAC=1 + ROUND_LINE="$(awk -F'|' ' + function trim(s) { gsub(/^[ \t*`]+|[ \t*`]+$/, "", s); return s } + function cell(k) { return (k in col) ? trim($(col[k])) : "" } + /^## +Rounds/ { sec = 1; next } + sec && /^## / { sec = 0 } + !sec || !/^\|/ { next } + !hdr { for (i = 2; i < NF; i++) col[tolower(trim($i))] = i; hdr = 1; next } + /^\| *:?-/ { next } + { n++; r = cell("round"); t = cell("title"); s = cell("state"); st = cell("stories") + if (tolower(s) ~ /^delivered/) d++ } + END { + if (!n) { print "none recorded — add a row to PROJECT.md \"## Rounds\" when a round starts (skills/existing-app-change.md)"; exit } + printf "Round %s: %s — %s%s · %d of %d round(s) delivered\n", r, (t != "" ? t : "(no title)"), (s != "" ? s : "(no state)"), (st != "" ? " · stories: " st : ""), d, n + }' "$REG")" +fi + +# --- targeted manual Studio Pro steps (#298) ------------------------------------------------- +# docs/manual-steps.md, from templates/manual-steps.md: one "- [ ] Done" line per step, ticked +# when the step is done (by hand or by patch). Pending = unticked. Fenced blocks are skipped so +# the template's worked example never counts. No file, no output — the screen is unchanged. +MANUAL="" +MS_FILE="$PROJECT_DIR/docs/manual-steps.md" +if [ -f "$MS_FILE" ]; then + MANUAL="$(awk ' + /^[ \t]*```/ { fence = !fence; next } + fence { next } + /^[ \t]*[-*] \[ \] *(\*\*)?Done/ { p++; t++ } + /^[ \t]*[-*] \[[xX]\] *(\*\*)?Done/ { t++ } + END { printf "manual steps pending %d of %d", p, t }' "$MS_FILE")" +fi + # --- doctor receipt ------------------------------------------------------------------------- DR="$PROJECT_DIR/.claude/.doctor-receipt" if [ -f "$DR" ]; then @@ -142,6 +182,23 @@ NEED_ATTN="$(printf '%s\n' "$GC" | awk '/^Needs attention/{f=1;next} /^Next up:| APPROVED_OVER="$(printf '%s\n' "$GC" | grep -m1 '^Approved over a failing gate:' | sed -E 's/^Approved over a failing gate: *//; s/ — .*$//')" N_ATTN="$(printf '%s\n' "$GC" | grep -oE '[0-9]+ need attention' | grep -oE '^[0-9]+' || echo 0)" OB_PENDING="$(printf '%s\n' "$GC" | grep -E '^Obligation ' | grep -E ' (PENDING|FAULT) ' | awk '{print $2": "$3}' | tr '\n' ',' | sed 's/,$//; s/,/, /g')" +# NEXT only names obligations the project can discharge NOW. Each row in bin/lib/obligations.tsv +# carries the stage it is first owed at (column 8); the gate-check line reports every one, so a +# Stage 1 project was told "discharge: final-look" — the Stage 6 whole-app LOOK — and never saw +# gate-check's "Next up:" hint behind it (review of #303). Above-stage obligations stay on the +# screen's "obligations pending" line; they are only kept out of NEXT. A stage line we cannot read +# keeps them all (louder, never quieter). +CUR_STAGE="$(printf '%s\n' "$STAGE_LINE" | sed -nE 's/.*Stage ([0-9P]).*/\1/p' | head -1)" +OB_NEXT="$OB_PENDING" +if [ -n "$OB_PENDING" ] && [ -n "$CUR_STAGE" ] && [ -f "$TOOLKIT_ROOT/bin/lib/obligations.tsv" ]; then + OB_NEXT="$(printf '%s\n' "$OB_PENDING" | tr ',' '\n' | sed -E 's/^ +//' | awk -v cur="$CUR_STAGE" -v tsv="$TOOLKIT_ROOT/bin/lib/obligations.tsv" ' + function rank(x) { return (x == "P") ? -1 : x + 0 } + BEGIN { FS = "\t"; while ((getline l < tsv) > 0) { if (l ~ /^#/ || l == "") continue; split(l, c, "\t"); from[c[1]] = c[8] }; FS = " " } + { id = $1; sub(/:$/, "", id) + if ((id in from) && from[id] ~ /^[0-9P]$/ && rank(from[id]) > rank(cur)) next + out = out (out == "" ? "" : ", ") $0 } + END { print out }')" +fi MOD_OPENED="$(printf '%s\n' "$GC" | grep -m1 -E '^Obligation look ' | grep -q 'no module has been opened' && echo 0 || echo '≥1')" GC_NEXT="$(printf '%s\n' "$GC" | grep -m1 '^Next up:' | sed 's/^Next up: *//')" TK_UPD="$(printf '%s\n' "$GC" | grep -m1 '^Toolkit updates:' | sed 's/^Toolkit updates: *//; s/\..*$//')" @@ -151,6 +208,13 @@ if [ -x "$PROJECT_DIR/bin/coherence-cadence.sh" ]; then COH="coherence $COH_OUT" fi +# existing-app-change: the agent-slot and done- signals are pipeline-build bookkeeping. A change +# stream delivered in rounds left them reading "agent slots unfilled: 35 in 6 file(s)" and +# "done- stalled" through four delivered rounds, and NEXT pointed at them (#301). In that mode +# only, they are dropped from WATCH and NEXT; the round line above is the progress readout. +# Every other mode prints exactly as before (tests/wave2/test-status-existing-app.sh, byte-diff). +if [ "$EAC" = 1 ]; then DONE_STALL=""; NOT_DONE_PASS=""; PH_TOTAL=0; fi + # --- NEXT: ordered lookup, earliest wins. Owner skill in the right-hand comment. ------------- next_action() { if [ -n "$APPROVED_OVER" ]; then echo "$APPROVED_OVER is approved in PROJECT.md but its gate FAILS — fix the gate, or re-open the approval with the user (bin/gate-check.sh)"; return; fi # conversion-runbook.md §2 @@ -166,7 +230,7 @@ next_action() { if [ -n "$LAST_FAIL" ]; then echo "last gate FAILED for: $LAST_FAIL— fix and re-exec (docs/BUILD-LOG.md, .mpr-snapshots/last-mxbuild-errors.json)"; return; fi # iterative-build-loop.md Gate: BUILD if [ -n "$DONE_STALL" ]; then echo "$DONE_STALL — done- needs the coverage checklist; run bin/coverage-preflight.sh to see what blocks it (iterative-build-loop.md)"; return; fi if [ -n "$NOT_DONE_PASS" ]; then echo "gate-passed but not done-: $NOT_DONE_PASS— walk the happy path + coverage checklist, then git mv to done- (iterative-build-loop.md step 13-15)"; return; fi - if [ -n "$OB_PENDING" ]; then echo "discharge: $OB_PENDING (bin/lib/obligations.tsv names the artifact each owes)"; return; fi + if [ -n "$OB_NEXT" ]; then echo "discharge: $OB_NEXT (bin/lib/obligations.tsv names the artifact each owes)"; return; fi [ -n "$GC_NEXT" ] && { echo "$GC_NEXT"; return; } echo "nothing overdue — take the next script in architecture/build-plan.md" } @@ -200,7 +264,8 @@ for _w in "${APPROVED_OVER:+APPROVED OVER A FAILING GATE: $APPROVED_OVER}" "$DON done if [ "$BRIEF" = 1 ]; then echo "WHERE $NAME · $STAGE_LINE${ENTRY:+ · $ENTRY}${ADOPTED:+ · joined at $ADOPTED}" - echo "STATE scripts $N_SCRIPTS written / $N_PASS gate-pass / $N_DONE done- · modules opened $MOD_OPENED${SKELETON:+ · skeleton $SKELETON} · UNSYNCED $UNSYNCED · open questions $OPEN_Q" + [ -n "$ROUND_LINE" ] && echo "ROUND $ROUND_LINE" + echo "STATE scripts $N_SCRIPTS written / $N_PASS gate-pass / $N_DONE done- · modules opened $MOD_OPENED${SKELETON:+ · skeleton $SKELETON} · UNSYNCED $UNSYNCED · open questions $OPEN_Q${MANUAL:+ · $MANUAL}" [ -n "$WATCH" ] && echo "WATCH $WATCH" echo "NEXT $NEXT" "$TOOLKIT_ROOT/bin/token-burn.sh" "$PROJECT_DIR" --brief 2>/dev/null @@ -208,8 +273,10 @@ if [ "$BRIEF" = 1 ]; then fi printf '\n%s — %s%s%s\n' "$NAME" "$STAGE_LINE" "${ENTRY:+ · $ENTRY}" "${ADOPTED:+ · joined at stage $ADOPTED}" printf '%s\n\n' "$TK" +[ -n "$ROUND_LINE" ] && printf 'ROUND %s\n' "$ROUND_LINE" printf 'DONE scripts: %s written, %s gate-pass, %s done- · modules opened: %s%s\n' "$N_SCRIPTS" "$N_PASS" "$N_DONE" "$MOD_OPENED" "${SKELETON:+ · skeleton proven $SKELETON}" printf 'OVERDUE %s · %s · %s · UNSYNCED markers: %s · open questions: %s\n' "$DOCTOR" "$LINT" "${COH:-coherence: cadence script not installed}" "$UNSYNCED" "$OPEN_Q" +[ -n "$MANUAL" ] && printf ' %s (docs/manual-steps.md)\n' "$MANUAL" [ -n "$OB_PENDING" ] && printf ' obligations pending: %s\n' "$OB_PENDING" [ -n "$LAST_FAIL" ] && printf ' last gate FAILED: %s\n' "$LAST_FAIL" [ -n "$WATCH" ] && printf 'WATCH %s\n' "$WATCH" diff --git a/skills/existing-app-change.md b/skills/existing-app-change.md index 9f3774d1..aca2fb93 100644 --- a/skills/existing-app-change.md +++ b/skills/existing-app-change.md @@ -199,6 +199,63 @@ end — a regression found three modules later costs the same to fix and much mo --- +## Delivering in rounds — the round is the unit of status + +A backlog of small stories (1–5 points each) is delivered in rounds of a few stories, and the stage +machine does not see rounds: on a field run (2026-10-13, 11 stories in 4 rounds on a 127-module +app) `bin/status.sh` still said **"Stage 1 — Analysis, in progress"** after the fourth delivered +round, with "agent slots unfilled: 35" and "done- stalled" in its WATCH line. Stage 0a (the map) is +still the one-time prerequisite; stages 1–4 run per round over that round's stories. + +Record each round as one row in `PROJECT.md`, under a section you add when the first round starts: + +```markdown +## Rounds + +| Round | Title | Stories | State | +|---|---|---|---| +| 1 | Export fixes | S-101, S-102, S-104 | delivered | +| 2 | Approval rework | S-110, S-111 | in test | +``` + +- **Append, never reorder.** The last row is the round in flight; `status.sh` prints it as + `ROUND Round 2: Approval rework — in test · stories: S-110, S-111 · 1 of 2 round(s) delivered`. +- **State** is one word or phrase: `planned` · `building` · `in test` · `delivered`. Only a state + starting with `delivered` counts as delivered. Update the cell when the round moves, not later. +- **Stories** names the ids from wherever the stories live (a ticket system, a `stories.json`, change + log rows in `PROJECT.md`). The per-story record — scope, touched units, decisions, test verdict, + manual steps — stays there; this table does not copy it. + +In this entry mode `status.sh` drops the agent-slot and `done-` signals from WATCH and NEXT: a +change stream delivered in rounds does not fill pipeline agent stubs or rename scripts to `done-`, +so those lines measured nothing. The round line replaces them. Other modes print as before. + +--- + +## Edits mxcli cannot make — a targeted manual-step list + +Some edits cannot go through `describe → exec` safely. Field case (2026-10-13): six microflow edits +whose decision branches rejoin early, where re-executing the described microflow would change the +flow (upstream mxcli #923) — three were done by hand in Studio Pro, three by a BSON patch. That is +neither a whole-model handoff (`handoff-to-studio-pro.md`) nor an MDL script, and it needs its own +deliverable. + +Copy `<toolkit>/templates/manual-steps.md` to `docs/manual-steps.md` and write one section per edit: +the document by full path, why mxcli cannot do it, what it depends on, the exact clicks with an +anchor (the existing activity or widget it goes next to, by caption or name), a **Check** a person can +run in the app, and one `- [ ] Done` line. List the steps in dependency order: attributes an MDL +script adds come before the hand edit that uses them. + +- **A step done by patch still goes through `./bin/exec.sh --patch <script>`.** The snapshot, the + mxbuild gate and the BUILD-LOG row are its evidence; a patch run any other way leaves none, and the + Done line cannot cite one. +- **Tick the Done line only after its Check passed**, and name the evidence on it: the commit for a + hand edit, the BUILD-LOG row for a patch. +- `status.sh` counts the file: `manual steps pending N of M` (unticked Done lines of all Done lines). + A round with pending manual steps is not `delivered`. + +--- + ## Coverage in this mode The ledger in this mode is `mxcli brain plan`, not a hand-kept `coverage-ledger.md`. Each BRD leaf @@ -237,5 +294,7 @@ it is the only entry mode where that is true. - Knowledge base — Path D, scoped, with counts recorded: modules, and per in-scope module the entity, page and microflow totals `SHOW …` returned, so a reader can see the change against the app. - BRDs for the change, each with as-is and to-be — and one line saying how many capabilities were *not* BRD'd and why. - A build plan over the change. +- When the work arrives as a backlog: a `## Rounds` table in `PROJECT.md`, one row per round, its state current. +- When an edit cannot go through mxcli: `docs/manual-steps.md` from `templates/manual-steps.md`, every step with its Check and a ticked Done line naming its evidence. - **A regression baseline that was green before the change and is green after it** — stated as N journeys over M modules, re-run at every module gate. Without this the rest is unverified. - Anything reusable learned about the app → `skills/learned-*.md`; process learnings → `process/process-learnings.md`. diff --git a/templates/manual-steps.md b/templates/manual-steps.md new file mode 100644 index 00000000..825f8c5a --- /dev/null +++ b/templates/manual-steps.md @@ -0,0 +1,40 @@ +# Manual Studio Pro steps — <change or round name> + +<!-- Copy to docs/manual-steps.md in the project. Template: templates/manual-steps.md in the toolkit; + the rules are in skills/existing-app-change.md → "Edits mxcli cannot make". + + bin/status.sh counts this file: every line that starts "- [ ] Done" is a PENDING step, every + "- [x] Done" a done one. One such line per step, nothing else in the file starts that way. + Fenced blocks (```) are not counted, which is why the worked example below is fenced. --> + +**Order matters.** Steps are listed in the order they must be done. A step that needs something an +MDL script creates (a new attribute, a new microflow) names that script under *Depends on*, and that +script is applied first. + +**A step done by patch is still an exec.** If a step is done by a Python/BSON patch instead of by +hand, it runs as `./bin/exec.sh --patch <script>` — the snapshot, the mxbuild gate and the BUILD-LOG +row are the evidence, and a patch run any other way has none. Record the BUILD-LOG date on the Done line. + +--- + +## Steps + +<!-- One section per step, copied from the example. Delete this comment and the example block + once the first real step is written. --> + +```markdown +### MS-01 — Add the "second approver" branch to ACT_Order_Approve + +- **Document:** `Orders` › `Microflows` › `Approval` › `ACT_Order_Approve` +- **Why mxcli cannot do it:** the decision's branches rejoin before the end event; DESCRIBE → exec + of this microflow re-orders the flow (upstream mxcli #923), so it is edited in place instead. +- **Depends on:** `mdlsource/031-orders-approver-attrs.mdl` (adds `Order.SecondApproverRequired`) — applied first. +- **Exact clicks:** + 1. Open the microflow. Find the activity captioned `Retrieve approval policy` (the anchor). + 2. After it, insert a Decision with caption `Above 250?` and expression `$Order/Amount > 250`. + 3. `true` → Change object `$Order`, set `SecondApproverRequired` = `true`, no commit. `false` → straight on. + 4. Join both branches into the existing merge before `Commit order`. +- **Check (a person can run this in the app):** as `Buyer`, submit an order of 300 → the order shows + "Waiting for second approver". Submit one of 200 → it does not. +- [ ] Done — by hand (commit `<sha>`) / by patch (`./bin/exec.sh --patch <script>`, BUILD-LOG row `<date>`) +``` diff --git a/tests/wave2/test-status-existing-app.sh b/tests/wave2/test-status-existing-app.sh new file mode 100755 index 00000000..594468af --- /dev/null +++ b/tests/wave2/test-status-existing-app.sh @@ -0,0 +1,190 @@ +#!/usr/bin/env bash +# test-status-existing-app.sh — bin/status.sh in existing-app-change mode (#301) and the manual +# Studio Pro step count (#298). +# +# Usage: bash tests/wave2/test-status-existing-app.sh bin/status.sh +# +# GOLDEN INPUT IS THE REAL SCAFFOLD. Every PROJECT.md here is written by the real +# bin/init-project.sh (agent stubs with their unfilled slots included); the fixture adds only an +# "Entry mode:" line, a BUILD-LOG and the sections under test. The baseline is status.sh as it was +# at BASE_REF (the commit before this change), run from the SAME toolkit copy as the subject, so the +# two outputs differ only by the script. +# +# T1 pipeline project (migration), --brief and full: byte-identical to the baseline, with the +# agent-slot and done- stall WATCH items present in both (control: the noise is real there) +# T2 pipeline project carrying a "## Rounds" section: still byte-identical (the section is +# read in existing-app-change mode only) +# T3 existing-app-change, rounds recorded: ROUND line is the LAST row, with stories and the +# delivered count; full screen carries it too +# T4 existing-app-change: no "agent slots unfilled" / "done- stalled" in WATCH, NEXT does not +# point at done-; the baseline on the same project DID print them (so the case can fail) +# T5 existing-app-change, no Rounds section: the ROUND line says none recorded and where to add it +# T6 Rounds table without a Stories column: no stories part, no crash +# T7 docs/manual-steps.md: unticked Done lines counted, ticked ones in the total, the fenced +# example ignored — on --brief STATE and on the full screen +# T8 the shipped template copied as-is counts 0 of 0 (its example is fenced) + +export MXTK_LEAKGUARD_DENYFILE="${TMPDIR:-/tmp}/mxtk-fixture-denylist.$$" +export MXTK_NO_GUIDE=1 +export MXTK_SYNC_SKIP_CLONE_CHECK=1 +set -uo pipefail + +SUT="${1:?usage: test-status-existing-app.sh /path/to/bin/status.sh}" +case "$SUT" in /*) ;; *) SUT="$PWD/$SUT" ;; esac +SRC="$(cd "$(dirname "$SUT")/.." && pwd)" +BASE_REF="${BASE_REF:-b9cf37c44884a74a357b7b14bc595ea78304a0b2}" +W="$(mktemp -d "${TMPDIR:-/tmp}/statusea.XXXXXX")" || exit 2 +trap 'rm -rf "$W"; rm -f "$MXTK_LEAKGUARD_DENYFILE"' EXIT +PASS=0; FAIL=0 +ok() { PASS=$((PASS+1)); printf ' ok %s\n' "$1"; } +bad() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | sed 's/^/ FAIL-detail: /'; } + +# One toolkit copy, not a git checkout, so the toolkit sha reads "?" for both scripts alike. +mkdir -p "$W/tk" +(cd "$SRC" && tar --exclude=.git --exclude=node_modules -cf - .) | (cd "$W/tk" && tar -xf -) +cp "$SUT" "$W/tk/bin/status.sh" +HAVE_BASE=0 +if git -C "$SRC" show "$BASE_REF:bin/status.sh" > "$W/tk/bin/status-baseline.sh" 2>/dev/null; then + chmod +x "$W/tk/bin/status-baseline.sh"; HAVE_BASE=1 +fi +NEW="$W/tk/bin/status.sh"; OLD="$W/tk/bin/status-baseline.sh" + +mkproj() { # mkproj <name> <entry mode text> + local d="$W/$1"; mkdir -p "$d/mdlsource" "$d/docs"; : > "$d/Fixture.mpr" + "$W/tk/bin/init-project.sh" "$d" >/dev/null 2>&1 + # Entry mode the way a register records it, right under the Current stage heading. + awk -v m="$2" '{print} /^## Current stage/ {getline; print; print ""; print "Entry mode: " m " CONFIRMED"}' \ + "$d/PROJECT.md" > "$d/PROJECT.md.new" && mv "$d/PROJECT.md.new" "$d/PROJECT.md" + # Three gate-passed scripts, none renamed done- — the "done- stalled" condition. + local s + # Header and row shape as project-bin/exec.sh log_build() writes them (gate column = GATE_STATE). + { echo "| when (ISO-8601 local) | script | gate | result | detail |"; echo "|---|---|---|---|---|"; } > "$d/docs/BUILD-LOG.md" + for s in 010-a 020-b 030-c; do + echo "-- $s" > "$d/mdlsource/$s.mdl" + echo "| 2026-10-13T10:00:00+0200 | \`$s.mdl\` | pass | ✅ applied | $s.mdl; lint ok |" >> "$d/docs/BUILD-LOG.md" + done + echo "$d" +} +# The doctor receipt's age ("0 min ago") can tick over between the two runs; it is the only +# wall-clock text on the screen, so it is masked for the byte comparison and nothing else is. +norm() { sed -E 's/[0-9]+ (min|h|days) ago/N \1 ago/g'; } +add_rounds() { # add_rounds <proj> (table on stdin) — appended as its own section + { printf '\n## Rounds\n\n'; cat; } >> "$1/PROJECT.md" +} + +echo "== bin/status.sh — existing-app-change rounds (#301) + manual steps (#298) ==" + +# --- T1 / T2: pipeline modes are untouched ------------------------------------------------------- +P="$(mkproj pipe "migration")" +if [ "$HAVE_BASE" = 1 ]; then + for mode in --brief ""; do + o="$("$OLD" "$P" $mode 2>&1 | norm)"; n="$("$NEW" "$P" $mode 2>&1 | norm)" + [ "$o" = "$n" ] && ok "T1 pipeline project ${mode:-full}: byte-identical to baseline" \ + || bad "T1 pipeline project ${mode:-full} output changed" "$(diff <(echo "$o") <(echo "$n"))" + done + case "$o" in *"agent slots unfilled"*"done- stalled"*|*"done- stalled"*"agent slots unfilled"*) ok "T1 control: the pipeline screen carries agent-slot + done- stall WATCH items";; + *) bad "T1 control: expected both WATCH items on the pipeline project (fixture no longer exercises them)" "$o";; esac + printf '| Round | Title | Stories | State |\n|---|---|---|---|\n| 1 | Export fixes | S-1 | in test |\n' | add_rounds "$P" + for mode in --brief ""; do + o="$("$OLD" "$P" $mode 2>&1 | norm)"; n="$("$NEW" "$P" $mode 2>&1 | norm)" + [ "$o" = "$n" ] && ok "T2 pipeline project with a Rounds section ${mode:-full}: byte-identical" \ + || bad "T2 Rounds section changed a pipeline screen (${mode:-full})" "$(diff <(echo "$o") <(echo "$n"))" + done +else + bad "T1/T2 NOT RUN: baseline $BASE_REF not in this checkout's history (fetch full history)" +fi + +# --- T3 / T4: existing-app-change with rounds ------------------------------------------------ +E="$(mkproj eac "Change an existing app")" +printf '%s\n' '| Round | Title | Stories | State |' '|---|---|---|---|' \ + '| 1 | Export fixes | S-101, S-102 | delivered |' \ + '| 2 | Approval rework | S-110, S-111 | in test |' | add_rounds "$E" +b="$("$NEW" "$E" --brief 2>&1)" +exp='ROUND Round 2: Approval rework — in test · stories: S-110, S-111 · 1 of 2 round(s) delivered' +printf '%s\n' "$b" | grep -qxF "$exp" && ok "T3 --brief ROUND line is the last row, with stories and delivered count" \ + || bad "T3 --brief ROUND line" "$b" +[ "$(printf '%s\n' "$b" | sed -n 2p | cut -c1-5)" = "ROUND" ] && ok "T3 ROUND sits right under WHERE" || bad "T3 ROUND line position" "$b" +f="$("$NEW" "$E" 2>&1)" +printf '%s\n' "$f" | grep -qF 'ROUND Round 2: Approval rework — in test' && ok "T3 full screen carries the ROUND line" || bad "T3 full screen ROUND line" "$f" +case "$b$f" in *"agent slots unfilled"*) bad "T4 agent-slot noise still printed in existing-app mode" "$b";; *) ok "T4 no agent-slot WATCH item";; esac +case "$b$f" in *"done- stalled"*|*"not done-"*) bad "T4 done- noise still printed in existing-app mode" "$b";; *) ok "T4 no done- stall in WATCH or NEXT";; esac +if [ "$HAVE_BASE" = 1 ]; then + o="$("$OLD" "$E" --brief 2>&1)" + case "$o" in *"agent slots unfilled"*) ok "T4 control: the baseline printed the agent-slot noise on this same project";; + *) bad "T4 control: baseline did not print agent-slot noise — case cannot fail" "$o";; esac + case "$o" in *ROUND*) bad "T4 control: baseline already has a ROUND line?" "$o";; *) ok "T4 control: the baseline has no ROUND line";; esac +fi + +# --- T5: no Rounds section ------------------------------------------------------------------- +E2="$(mkproj eac-none "existing-app-change")" +b="$("$NEW" "$E2" --brief 2>&1)" +printf '%s\n' "$b" | grep -q '^ROUND none recorded — add a row to PROJECT.md "## Rounds"' && ok "T5 no rounds: says none recorded and where" || bad "T5 no-rounds line" "$b" + +# --- T6: no Stories column ------------------------------------------------------------------ +E3="$(mkproj eac-nostories "Change an existing app")" +printf '%s\n' '| Round | Title | State |' '|---|---|---|' '| 1 | Login tweak | building |' | add_rounds "$E3" +b="$("$NEW" "$E3" --brief 2>&1)" +printf '%s\n' "$b" | grep -qxF 'ROUND Round 1: Login tweak — building · 0 of 1 round(s) delivered' && ok "T6 no Stories column: no stories part" || bad "T6 no-Stories-column ROUND line" "$b" + +# --- T7: manual steps count ---------------------------------------------------------------- +cat > "$E/docs/manual-steps.md" <<'EOF' +# Manual Studio Pro steps + +```markdown +- [ ] Done — example, fenced, must not count +``` + +### MS-01 — a +- [x] Done — by hand (commit abc1234) +### MS-02 — b +- [ ] Done — pending +### MS-03 — c +- [ ] **Done** — pending +Not a step: - [ ] Done mid-line +EOF +b="$("$NEW" "$E" --brief 2>&1)" +printf '%s\n' "$b" | grep '^STATE' | grep -qF '· manual steps pending 2 of 3' && ok "T7 --brief STATE: manual steps pending 2 of 3" || bad "T7 --brief manual count" "$b" +f="$("$NEW" "$E" 2>&1)" +printf '%s\n' "$f" | grep -qF 'manual steps pending 2 of 3 (docs/manual-steps.md)' && ok "T7 full screen: manual count with its file" || bad "T7 full manual count" "$f" + +# --- T8: the shipped template counts nothing ------------------------------------------------ +cp "$W/tk/templates/manual-steps.md" "$E2/docs/manual-steps.md" 2>/dev/null +b="$("$NEW" "$E2" --brief 2>&1)" +printf '%s\n' "$b" | grep -qF 'manual steps pending 0 of 0' && ok "T8 template copied as-is: 0 of 0" || bad "T8 template count" "$b" + +# --- T9: NEXT never names an obligation owed only at a later stage (review of #303) ---------- +# A scaffold always has gate-check "Needs attention" items, which win NEXT before the obligation +# rule is reached. tk2's gate-check.sh runs the REAL gate-check and drops only that block and its +# count, so the obligation lines status.sh reads are real output, not typed here. +mkdir -p "$W/tk2"; (cd "$W/tk" && tar -cf - .) | (cd "$W/tk2" && tar -xf -) +mv "$W/tk2/bin/gate-check.sh" "$W/tk2/bin/gate-check-real.sh" +cat > "$W/tk2/bin/gate-check.sh" <<'WRAP' +#!/usr/bin/env bash +"$(dirname "$0")/gate-check-real.sh" "$@" 2>/dev/null \ + | awk '/^Needs attention/{s=1;next} /^Next up:|^Approved over a failing gate:/{s=0} !s' \ + | sed -E 's/[0-9]+ need attention/0 need attention/' +WRAP +chmod +x "$W/tk2/bin/gate-check.sh" +P9="$W/ob"; mkdir -p "$P9"; : > "$P9/Fixture.mpr"; "$W/tk2/bin/init-project.sh" "$P9" >/dev/null 2>&1 +setstage() { awk -v st="$2" '/^## Current stage/{print; getline; print ""; getline; print st; next} {print}' "$1/PROJECT.md" > "$1/p.new" && mv "$1/p.new" "$1/PROJECT.md" + grep -q '^Entry mode:' "$1/PROJECT.md" || printf '\nEntry mode: migration\n' >> "$1/PROJECT.md"; } +nextof() { "$1" "$2" --brief 2>/dev/null | sed -n 's/^NEXT //p'; } +setstage "$P9" "**Stage 3 — Architecture & Design**, in progress." +grep -q '^\*\*Stage 3' "$P9/PROJECT.md" || bad "T9 setup: stage line not written" "$(sed -n '1,15p' "$P9/PROJECT.md")" +n="$(nextof "$W/tk2/bin/status.sh" "$P9")" +case "$n" in "discharge:"*) ok "T9 Stage 3: NEXT still discharges what is owed now";; *) bad "T9 Stage 3: expected a discharge NEXT" "$n";; esac +case "$n" in *final-look*|*skeleton*|*coherence*) bad "T9 Stage 3: NEXT names an obligation first owed at Stage 5/6" "$n";; *) ok "T9 Stage 3: no Stage 5/6 obligation in NEXT";; esac +case "$n" in *workflow-count*) ok "T9 Stage 3: a Stage 3 obligation stays in NEXT";; *) bad "T9 Stage 3: Stage 3 obligation dropped" "$n";; esac +f="$("$W/tk2/bin/status.sh" "$P9" 2>/dev/null)" +printf '%s\n' "$f" | grep -q 'obligations pending:.*final-look' && ok "T9 the screen still lists final-look under obligations pending" || bad "T9 obligations pending line lost final-look" "$f" +setstage "$P9" "**Stage 1 — Analysis**, in progress." +n="$(nextof "$W/tk2/bin/status.sh" "$P9")" +case "$n" in "discharge:"*) bad "T9 Stage 1: every pending obligation is above Stage 1, NEXT should fall through" "$n";; *) ok "T9 Stage 1: NEXT is not a discharge line (falls through to the later rules)";; esac +if [ "$HAVE_BASE" = 1 ]; then + cp "$OLD" "$W/tk2/bin/status-baseline.sh" + o="$(nextof "$W/tk2/bin/status-baseline.sh" "$P9")" + case "$o" in *final-look*) ok "T9 control: the baseline told a Stage 1 project to discharge final-look";; *) bad "T9 control: baseline did not name final-look — case cannot fail" "$o";; esac +fi + +echo "SCORE: $PASS/$((PASS+FAIL))" +[ "$FAIL" -eq 0 ] From 2fa68837d9ebfb881075aff56d577496de62fce3 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Tue, 13 Oct 2026 15:44:51 +0000 Subject: [PATCH 2/3] status: review fixes for #326 (final-look filter, keep done-, Rounds heading) - NEXT drops final-look only, and only when the stage line is gate-check's derived readout below its from-stage, reading the FIRST "Stage N" (mirrors #321's NOT YET DUE). Module passes owed under a lagging or hand-written line stay in NEXT. - existing-app-change drops only the "agent slots unfilled" WATCH item; done- signals and the agent-stub NEXT rule stay (Stages 5-6 run the build loop unchanged in that mode). - "## Rounds" heading anchored, so "## Rounds of review" is not read. Fixture: 27/27; 11 fail on master, T9d fails on a last-Stage mutant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 2 +- bin/status.sh | 50 +++++++++-------- skills/existing-app-change.md | 7 ++- tests/wave2/test-status-existing-app.sh | 75 ++++++++++++++++++------- 4 files changed, 87 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5bd2ff9..1f09c5a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. ## Unreleased -- feat(status, existing-app-change): **existing-app-change mode reports the delivery round, not just the stage; agent-slot and done- noise is dropped in that mode only; a template for targeted manual Studio Pro steps, counted by `status.sh`** ([#301](https://github.com/MendixMau/mxcli-project-toolkit/issues/301), [#298](https://github.com/MendixMau/mxcli-project-toolkit/issues/298)). A field run delivered 11 backlog stories in 4 rounds on a 127-module app while `status.sh` still read "Stage 1 — Analysis", WATCH said "agent slots unfilled: 35 in 6 file(s)" and "done- stalled", and NEXT pointed at those. A `## Rounds` table in `PROJECT.md` (`| Round | Title | Stories | State |`, last row = round in flight) now gives a `ROUND Round N: <title> — <state> · stories: … · D of N round(s) delivered` line in existing-app-change mode, and that mode drops the agent-slot and done- items from WATCH and NEXT. The same run needed six microflow edits mxcli could not round-trip (branches that rejoin early): new `templates/manual-steps.md` (document path, why mxcli cannot, depends-on, exact clicks with an anchor, a Check a person runs, one `- [ ] Done` line naming its evidence), and the rule that a step done by patch still goes through `exec.sh --patch`. `status.sh` prints `manual steps pending N of M` when `docs/manual-steps.md` exists. Also, from the review of #303: NEXT no longer names an obligation first owed at a later stage than the project is in (`final-look`, Stage 6, was sent to Stage 1–5 projects and hid gate-check's next-stage hint); the obligations-pending line still lists it. Apart from that NEXT fix, pipeline modes print byte-identical to before (fixture `tests/wave2/test-status-existing-app.sh`, 23 cases on real scaffolds; 13 fail on the old script) — jopterhorst +- feat(status, existing-app-change): **existing-app-change mode reports the delivery round, not just the stage; agent-slot and done- noise is dropped in that mode only; a template for targeted manual Studio Pro steps, counted by `status.sh`** ([#301](https://github.com/MendixMau/mxcli-project-toolkit/issues/301), [#298](https://github.com/MendixMau/mxcli-project-toolkit/issues/298)). A field run delivered 11 backlog stories in 4 rounds on a 127-module app while `status.sh` still read "Stage 1 — Analysis", WATCH said "agent slots unfilled: 35 in 6 file(s)" and "done- stalled", and NEXT pointed at those. A `## Rounds` table in `PROJECT.md` (`| Round | Title | Stories | State |`, last row = round in flight) now gives a `ROUND Round N: <title> — <state> · stories: … · D of N round(s) delivered` line in existing-app-change mode, and that mode drops the "agent slots unfilled" WATCH item (done- signals stay: Stages 5–6 run the build loop unchanged there). The same run needed six microflow edits mxcli could not round-trip (branches that rejoin early): new `templates/manual-steps.md` (document path, why mxcli cannot, depends-on, exact clicks with an anchor, a Check a person runs, one `- [ ] Done` line naming its evidence), and the rule that a step done by patch still goes through `exec.sh --patch`. `status.sh` prints `manual steps pending N of M` when `docs/manual-steps.md` exists. Also, from the review of #303: NEXT no longer sends a project to `final-look` (the Stage 6 whole-app LOOK) while gate-check's derived stage readout is below Stage 6 — it was sent to Stage 1–5 projects and hid gate-check's next-stage hint. `final-look` only, and only on a derived readout, the same rule as obligation-check's NOT YET DUE (#321): module passes owed under a lagging or hand-written stage line stay in NEXT; the obligations-pending line still lists `final-look`. Apart from that NEXT fix, pipeline modes print byte-identical to before (fixture `tests/wave2/test-status-existing-app.sh` on real scaffolds) — jopterhorst - learn(testing-shape, test-result-audit): **three test-strength rules, each with a denominator: refusals probed on the server, every undeclared 4xx a failure, one clean-database final run** ([#172](https://github.com/MendixMau/mxcli-project-toolkit/issues/172)). A field comparison of two builds against one frozen spec found the toolkit build checked permission refusals only in the client (a hidden menu item), logged unexpected 400/422 responses as information, and reached 20/20 by stitching three runs. `testing-shape.md` §7 gains three report lines (`N of N restricted pairs probed server-side`, `undeclared 4xx/5xx: 0 of M requests — each fails its step`, `one clean-DB run <id>: N of N scenarios incl. timers, not stitched`); `test-result-audit.md` gains check 2e, which carries the reasons and the field evidence and asks for those numbers. Both baseline skills stay inside the 80,000-word baseline budget (79,997): the reasons live in the on-demand audit skill, and two redundant sentences in `testing-shape.md` §4's verdict paragraphs were folded (the empty-set corollary is stated in the Trace row, `harness-architecture.md` and `report-schema.md`). The backing inbox note is trimmed to its unpromoted "Keep" items. — MendixMau - learn(learned-detection-gaps, module-review): **two UI defects every model gate passes: a lazy-loaded combo box filter hides its preset, and a theme that pins row heights by position lets the next row cover an inserted widget** ([#299](https://github.com/MendixMau/mxcli-project-toolkit/issues/299)). Both passed check, mxbuild, lint and the scripted checks on a 127-module production app and were found only in the running app. Two register rows with the first oracle and the pre-build check (pluggable properties that change *when* data loads; grep the theme for fixed `height` on positional row classes before an insert), and two LOOK lines in `module-review.md` §4c with denominators (`N of N filters show their preset on first load`, `N of N widgets inserted into an existing container fully visible`). — jopterhorst - fix(app-restart, test-stack-up): **a port held by another process before `run --local` is named and reported as "instrument blocked" (exit 4), never killed** ([#262](https://github.com/MendixMau/mxcli-project-toolkit/issues/262)). Studio Pro had started its own runtime on the app/admin ports a restart needed; `mxcli run --local` then failed with "Authentication failed" (the admin API answering was Studio Pro's), the next journey ran against Studio Pro's runtime, and four more runs were NOT RUN. `app-restart.sh` now checks the app port and the admin port (record's `adminPort`, else app + 10) before stopping anything — a holder outside the recorded process tree exits 4 with its name, pid and parent — and again after the stop, where anything still holding a port is by definition not ours. `test-stack-up.sh` runs the same check wherever it would tell you to start `run --local` (`--no-docker`, no Docker daemon, `--check` with a leftover record), on the record's ports, `RUN_LOCAL_APP_PORT`/`RUN_LOCAL_ADMIN_PORT`, or mxcli's defaults 8080/8090. Holder lookup: lsof, then ss, then netstat + tasklist on Windows; unnamed holders still block when nothing of ours is running. Fixtures: `test-app-restart.sh` +2 cases (14 assertions, all fail on the old script), `test-stack-up-local.sh` T7 (4 of 6 fail on the old script). — AlvinnYeo diff --git a/bin/status.sh b/bin/status.sh index 3a404150..fd11576d 100755 --- a/bin/status.sh +++ b/bin/status.sh @@ -121,7 +121,7 @@ if [ "$ENTRY" = "existing-app-change" ]; then ROUND_LINE="$(awk -F'|' ' function trim(s) { gsub(/^[ \t*`]+|[ \t*`]+$/, "", s); return s } function cell(k) { return (k in col) ? trim($(col[k])) : "" } - /^## +Rounds/ { sec = 1; next } + /^## +Rounds *$/ { sec = 1; next } sec && /^## / { sec = 0 } !sec || !/^\|/ { next } !hdr { for (i = 2; i < NF; i++) col[tolower(trim($i))] = i; hdr = 1; next } @@ -182,23 +182,25 @@ NEED_ATTN="$(printf '%s\n' "$GC" | awk '/^Needs attention/{f=1;next} /^Next up:| APPROVED_OVER="$(printf '%s\n' "$GC" | grep -m1 '^Approved over a failing gate:' | sed -E 's/^Approved over a failing gate: *//; s/ — .*$//')" N_ATTN="$(printf '%s\n' "$GC" | grep -oE '[0-9]+ need attention' | grep -oE '^[0-9]+' || echo 0)" OB_PENDING="$(printf '%s\n' "$GC" | grep -E '^Obligation ' | grep -E ' (PENDING|FAULT) ' | awk '{print $2": "$3}' | tr '\n' ',' | sed 's/,$//; s/,/, /g')" -# NEXT only names obligations the project can discharge NOW. Each row in bin/lib/obligations.tsv -# carries the stage it is first owed at (column 8); the gate-check line reports every one, so a -# Stage 1 project was told "discharge: final-look" — the Stage 6 whole-app LOOK — and never saw -# gate-check's "Next up:" hint behind it (review of #303). Above-stage obligations stay on the -# screen's "obligations pending" line; they are only kept out of NEXT. A stage line we cannot read -# keeps them all (louder, never quieter). -CUR_STAGE="$(printf '%s\n' "$STAGE_LINE" | sed -nE 's/.*Stage ([0-9P]).*/\1/p' | head -1)" +# NEXT never sends a project to final-look (the Stage 6 whole-app LOOK) before Stage 6: a Stage 1 +# project was told "discharge: final-look" and never saw gate-check's "Next up:" hint behind it +# (review of #303). final-look ONLY, and only when the stage line is gate-check's own derived +# readout — the same rule obligation-check.sh applies once it prints NOT YET DUE (#321), at which +# point final-look never reaches OB_PENDING and this is a no-op. Every other obligation keeps its +# place in NEXT whatever the stage line says: a hand-written or lagging line must not hide the +# module passes a project with an opened module owes (review of #326). The FIRST "Stage N" on the +# line is the stage; the screen's "obligations pending" line still lists final-look. OB_NEXT="$OB_PENDING" -if [ -n "$OB_PENDING" ] && [ -n "$CUR_STAGE" ] && [ -f "$TOOLKIT_ROOT/bin/lib/obligations.tsv" ]; then - OB_NEXT="$(printf '%s\n' "$OB_PENDING" | tr ',' '\n' | sed -E 's/^ +//' | awk -v cur="$CUR_STAGE" -v tsv="$TOOLKIT_ROOT/bin/lib/obligations.tsv" ' - function rank(x) { return (x == "P") ? -1 : x + 0 } - BEGIN { FS = "\t"; while ((getline l < tsv) > 0) { if (l ~ /^#/ || l == "") continue; split(l, c, "\t"); from[c[1]] = c[8] }; FS = " " } - { id = $1; sub(/:$/, "", id) - if ((id in from) && from[id] ~ /^[0-9P]$/ && rank(from[id]) > rank(cur)) next - out = out (out == "" ? "" : ", ") $0 } - END { print out }')" -fi +case "$STAGE_LINE" in *"(derived by gate-check"*) + CUR_STAGE="$(printf '%s\n' "$STAGE_LINE" | grep -oE 'Stage [0-9P]' | head -1 | sed 's/^Stage //')" + FL_FROM="$(awk -F'\t' '$1=="final-look"{print $8; exit}' "$TOOLKIT_ROOT/bin/lib/obligations.tsv" 2>/dev/null)" + case "$CUR_STAGE:$FL_FROM" in + P:[0-9]*) _behind=1 ;; + [0-9]:[0-9]*) [ "$CUR_STAGE" -lt "$FL_FROM" ] && _behind=1 || _behind=0 ;; + *) _behind=0 ;; + esac + [ "$_behind" = 1 ] && OB_NEXT="$(printf '%s\n' "$OB_PENDING" | tr ',' '\n' | sed -E 's/^ +//' | grep -v '^final-look:' | paste -s -d, - | sed 's/,/, /g')" ;; +esac MOD_OPENED="$(printf '%s\n' "$GC" | grep -m1 -E '^Obligation look ' | grep -q 'no module has been opened' && echo 0 || echo '≥1')" GC_NEXT="$(printf '%s\n' "$GC" | grep -m1 '^Next up:' | sed 's/^Next up: *//')" TK_UPD="$(printf '%s\n' "$GC" | grep -m1 '^Toolkit updates:' | sed 's/^Toolkit updates: *//; s/\..*$//')" @@ -208,12 +210,12 @@ if [ -x "$PROJECT_DIR/bin/coherence-cadence.sh" ]; then COH="coherence $COH_OUT" fi -# existing-app-change: the agent-slot and done- signals are pipeline-build bookkeeping. A change -# stream delivered in rounds left them reading "agent slots unfilled: 35 in 6 file(s)" and -# "done- stalled" through four delivered rounds, and NEXT pointed at them (#301). In that mode -# only, they are dropped from WATCH and NEXT; the round line above is the progress readout. -# Every other mode prints exactly as before (tests/wave2/test-status-existing-app.sh, byte-diff). -if [ "$EAC" = 1 ]; then DONE_STALL=""; NOT_DONE_PASS=""; PH_TOTAL=0; fi +# existing-app-change: a change stream delivered in rounds does not fill the pipeline agent stubs, +# and "agent slots unfilled: 35 in 6 file(s)" sat in WATCH through four delivered rounds (#301). +# In that mode only, that one WATCH item is dropped; the round line above is the progress readout. +# done- stays: Stage 5-6 run the build loop unchanged in this mode (existing-app-change.md), and +# the stub NEXT rule stays because it still fires only at Stage 5-7. Every other mode prints +# exactly as before (tests/wave2/test-status-existing-app.sh, byte-diff). # --- NEXT: ordered lookup, earliest wins. Owner skill in the right-hand comment. ------------- next_action() { @@ -259,7 +261,7 @@ TK="toolkit $TK_COMMIT_NOW"; [ -n "$TK_COMMIT_REG" ] && [ "$TK_COMMIT_REG" != "$ [ -n "$TK_UPD" ] && TK="$TK · updates: $TK_UPD" WATCH="" for _w in "${APPROVED_OVER:+APPROVED OVER A FAILING GATE: $APPROVED_OVER}" "$DONE_STALL" "${BP_STALE%% (*}" \ - "$( [ "${PH_TOTAL:-0}" -gt 0 ] && echo "agent slots unfilled: $PH_TOTAL in $PH_FILES file(s)" )" "$PACK_GAP"; do + "$( [ "${PH_TOTAL:-0}" -gt 0 ] && [ "$EAC" = 0 ] && echo "agent slots unfilled: $PH_TOTAL in $PH_FILES file(s)" )" "$PACK_GAP"; do [ -n "$_w" ] && WATCH="${WATCH:+$WATCH · }$_w" done if [ "$BRIEF" = 1 ]; then diff --git a/skills/existing-app-change.md b/skills/existing-app-change.md index aca2fb93..3ad624be 100644 --- a/skills/existing-app-change.md +++ b/skills/existing-app-change.md @@ -226,9 +226,10 @@ Record each round as one row in `PROJECT.md`, under a section you add when the f log rows in `PROJECT.md`). The per-story record — scope, touched units, decisions, test verdict, manual steps — stays there; this table does not copy it. -In this entry mode `status.sh` drops the agent-slot and `done-` signals from WATCH and NEXT: a -change stream delivered in rounds does not fill pipeline agent stubs or rename scripts to `done-`, -so those lines measured nothing. The round line replaces them. Other modes print as before. +In this entry mode `status.sh` drops the "agent slots unfilled" item from WATCH: a change stream +delivered in rounds does not fill the pipeline agent stubs, so that line measured nothing, and the +round line is the progress readout instead. The `done-` signals stay — Stages 5–6 run the build loop +unchanged here, `done-` rename included. Other modes print as before. --- diff --git a/tests/wave2/test-status-existing-app.sh b/tests/wave2/test-status-existing-app.sh index 594468af..423391fd 100755 --- a/tests/wave2/test-status-existing-app.sh +++ b/tests/wave2/test-status-existing-app.sh @@ -16,13 +16,18 @@ # read in existing-app-change mode only) # T3 existing-app-change, rounds recorded: ROUND line is the LAST row, with stories and the # delivered count; full screen carries it too -# T4 existing-app-change: no "agent slots unfilled" / "done- stalled" in WATCH, NEXT does not -# point at done-; the baseline on the same project DID print them (so the case can fail) +# T4 existing-app-change: no "agent slots unfilled" in WATCH, but "done- stalled" stays (the +# build loop runs unchanged at Stages 5-6); the baseline DID print the slot item # T5 existing-app-change, no Rounds section: the ROUND line says none recorded and where to add it # T6 Rounds table without a Stories column: no stories part, no crash +# T6b a "## Rounds of review" section is not the Rounds table # T7 docs/manual-steps.md: unticked Done lines counted, ticked ones in the total, the fenced # example ignored — on --brief STATE and on the full screen # T8 the shipped template copied as-is counts 0 of 0 (its example is fenced) +# T9 NEXT drops final-look ONLY, and only under gate-check's derived stage readout below its +# from-stage: (a) derived Stage P keeps every other pending pass; (b) a module opened under +# that lagging line keeps look/sweep/journeys; (c) a hand-written line filters nothing; +# (d) the FIRST "Stage N" on the line is the stage, not a later one export MXTK_LEAKGUARD_DENYFILE="${TMPDIR:-/tmp}/mxtk-fixture-denylist.$$" export MXTK_NO_GUIDE=1 @@ -107,7 +112,7 @@ printf '%s\n' "$b" | grep -qxF "$exp" && ok "T3 --brief ROUND line is the last r f="$("$NEW" "$E" 2>&1)" printf '%s\n' "$f" | grep -qF 'ROUND Round 2: Approval rework — in test' && ok "T3 full screen carries the ROUND line" || bad "T3 full screen ROUND line" "$f" case "$b$f" in *"agent slots unfilled"*) bad "T4 agent-slot noise still printed in existing-app mode" "$b";; *) ok "T4 no agent-slot WATCH item";; esac -case "$b$f" in *"done- stalled"*|*"not done-"*) bad "T4 done- noise still printed in existing-app mode" "$b";; *) ok "T4 no done- stall in WATCH or NEXT";; esac +case "$b" in *"done- stalled"*) ok "T4 done- stall still reported (Stages 5-6 run the build loop unchanged in this mode)";; *) bad "T4 done- stall dropped in existing-app mode — it must stay" "$b";; esac if [ "$HAVE_BASE" = 1 ]; then o="$("$OLD" "$E" --brief 2>&1)" case "$o" in *"agent slots unfilled"*) ok "T4 control: the baseline printed the agent-slot noise on this same project";; @@ -126,6 +131,11 @@ printf '%s\n' '| Round | Title | State |' '|---|---|---|' '| 1 | Login tweak | b b="$("$NEW" "$E3" --brief 2>&1)" printf '%s\n' "$b" | grep -qxF 'ROUND Round 1: Login tweak — building · 0 of 1 round(s) delivered' && ok "T6 no Stories column: no stories part" || bad "T6 no-Stories-column ROUND line" "$b" +# --- T6b: a "## Rounds of review" heading is a different section ------------------------------ +printf '\n## Rounds of review\n\n| Round | Title | State |\n|---|---|---|\n| 9 | Peer review | done |\n' >> "$E3/PROJECT.md" +b="$("$NEW" "$E3" --brief 2>&1)" +printf '%s\n' "$b" | grep -qxF 'ROUND Round 1: Login tweak — building · 0 of 1 round(s) delivered' && ok "T6b \"## Rounds of review\" is not read as the Rounds table" || bad "T6b a look-alike heading was read" "$b" + # --- T7: manual steps count ---------------------------------------------------------------- cat > "$E/docs/manual-steps.md" <<'EOF' # Manual Studio Pro steps @@ -152,10 +162,11 @@ cp "$W/tk/templates/manual-steps.md" "$E2/docs/manual-steps.md" 2>/dev/null b="$("$NEW" "$E2" --brief 2>&1)" printf '%s\n' "$b" | grep -qF 'manual steps pending 0 of 0' && ok "T8 template copied as-is: 0 of 0" || bad "T8 template count" "$b" -# --- T9: NEXT never names an obligation owed only at a later stage (review of #303) ---------- +# --- T9: final-look leaves NEXT only on a derived readout below Stage 6 (reviews of #303, #326) -- # A scaffold always has gate-check "Needs attention" items, which win NEXT before the obligation # rule is reached. tk2's gate-check.sh runs the REAL gate-check and drops only that block and its -# count, so the obligation lines status.sh reads are real output, not typed here. +# count, so the obligation lines status.sh reads are real output, not typed here. The stage line +# is the one the real gate-check derives and writes (a fresh scaffold derives "Stage P"). mkdir -p "$W/tk2"; (cd "$W/tk" && tar -cf - .) | (cd "$W/tk2" && tar -xf -) mv "$W/tk2/bin/gate-check.sh" "$W/tk2/bin/gate-check-real.sh" cat > "$W/tk2/bin/gate-check.sh" <<'WRAP' @@ -165,26 +176,52 @@ cat > "$W/tk2/bin/gate-check.sh" <<'WRAP' | sed -E 's/[0-9]+ need attention/0 need attention/' WRAP chmod +x "$W/tk2/bin/gate-check.sh" -P9="$W/ob"; mkdir -p "$P9"; : > "$P9/Fixture.mpr"; "$W/tk2/bin/init-project.sh" "$P9" >/dev/null 2>&1 -setstage() { awk -v st="$2" '/^## Current stage/{print; getline; print ""; getline; print st; next} {print}' "$1/PROJECT.md" > "$1/p.new" && mv "$1/p.new" "$1/PROJECT.md" - grep -q '^Entry mode:' "$1/PROJECT.md" || printf '\nEntry mode: migration\n' >> "$1/PROJECT.md"; } +[ "$HAVE_BASE" = 1 ] && cp "$OLD" "$W/tk2/bin/status-baseline.sh" nextof() { "$1" "$2" --brief 2>/dev/null | sed -n 's/^NEXT //p'; } -setstage "$P9" "**Stage 3 — Architecture & Design**, in progress." -grep -q '^\*\*Stage 3' "$P9/PROJECT.md" || bad "T9 setup: stage line not written" "$(sed -n '1,15p' "$P9/PROJECT.md")" +stage_of() { awk '/^## Current stage/{f=1;next} f && /^\*\*Stage/{print;exit}' "$1/PROJECT.md"; } +setline() { MXTK_L="$2" awk '/^## Current stage/{f=1} f && /^\*\*Stage/ && !d {print ENVIRON["MXTK_L"]; d=1; next} {print}' "$1/PROJECT.md" > "$1/p.new" && mv "$1/p.new" "$1/PROJECT.md"; } +P9="$W/ob"; mkdir -p "$P9"; : > "$P9/Fixture.mpr"; "$W/tk2/bin/init-project.sh" "$P9" >/dev/null 2>&1 +printf '\nEntry mode: migration\n' >> "$P9/PROJECT.md" +"$W/tk2/bin/gate-check-real.sh" "$P9" >/dev/null 2>&1 # writes the derived readout +DERIVED="$(stage_of "$P9")" +case "$DERIVED" in *"(derived by gate-check"*) ok "T9 setup: real gate-check wrote a derived readout";; *) bad "T9 setup: no derived readout" "$DERIVED";; esac + +# T9a derived Stage P, no module opened: final-look out of NEXT, the other pending passes stay. n="$(nextof "$W/tk2/bin/status.sh" "$P9")" -case "$n" in "discharge:"*) ok "T9 Stage 3: NEXT still discharges what is owed now";; *) bad "T9 Stage 3: expected a discharge NEXT" "$n";; esac -case "$n" in *final-look*|*skeleton*|*coherence*) bad "T9 Stage 3: NEXT names an obligation first owed at Stage 5/6" "$n";; *) ok "T9 Stage 3: no Stage 5/6 obligation in NEXT";; esac -case "$n" in *workflow-count*) ok "T9 Stage 3: a Stage 3 obligation stays in NEXT";; *) bad "T9 Stage 3: Stage 3 obligation dropped" "$n";; esac +case "$n" in "discharge:"*) ok "T9a derived Stage P: NEXT still discharges what is pending";; *) bad "T9a expected a discharge NEXT" "$n";; esac +case "$n" in *final-look*) bad "T9a derived Stage P: NEXT still names final-look" "$n";; *) ok "T9a derived Stage P: final-look not in NEXT";; esac +case "$n" in *skeleton*) ok "T9a only final-look is filtered (skeleton, also above Stage P, stays)";; *) bad "T9a filter dropped more than final-look" "$n";; esac f="$("$W/tk2/bin/status.sh" "$P9" 2>/dev/null)" -printf '%s\n' "$f" | grep -q 'obligations pending:.*final-look' && ok "T9 the screen still lists final-look under obligations pending" || bad "T9 obligations pending line lost final-look" "$f" -setstage "$P9" "**Stage 1 — Analysis**, in progress." -n="$(nextof "$W/tk2/bin/status.sh" "$P9")" -case "$n" in "discharge:"*) bad "T9 Stage 1: every pending obligation is above Stage 1, NEXT should fall through" "$n";; *) ok "T9 Stage 1: NEXT is not a discharge line (falls through to the later rules)";; esac +printf '%s\n' "$f" | grep -q 'obligations pending:.*final-look' && ok "T9a the screen still lists final-look under obligations pending" || bad "T9a obligations pending line lost final-look" if [ "$HAVE_BASE" = 1 ]; then - cp "$OLD" "$W/tk2/bin/status-baseline.sh" o="$(nextof "$W/tk2/bin/status-baseline.sh" "$P9")" - case "$o" in *final-look*) ok "T9 control: the baseline told a Stage 1 project to discharge final-look";; *) bad "T9 control: baseline did not name final-look — case cannot fail" "$o";; esac + case "$o" in *final-look*) ok "T9a control: the baseline sent a Stage P project to final-look";; *) bad "T9a control: baseline did not name final-look — case cannot fail" "$o";; esac fi +# T9b a module opened under the same lagging derived line: the module passes stay in NEXT. +mkdir -p "$P9/.claude/loop/verify/Orders" +n="$(nextof "$W/tk2/bin/status.sh" "$P9")" +case "$n" in *look*sweep*journeys*|*look*journeys*sweep*) ok "T9b module opened under a lagging line: look/sweep/journeys still in NEXT";; *) bad "T9b owed module passes hidden from NEXT" "$n";; esac +rm -rf "$P9/.claude/loop/verify" + +# T9c a hand-written (non-derived) Stage 1 line: no filtering at all. +setline "$P9" "**Stage 1 — Analysis**, in progress." +n="$(nextof "$W/tk2/bin/status.sh" "$P9")" +case "$n" in *final-look*) ok "T9c hand-written stage line: final-look stays in NEXT";; *) bad "T9c a hand-written line must not filter" "$n";; esac + +# T9d the FIRST "Stage N" on the line decides: the real derived line with its stage set to 6 and a +# later "Stage 2" in its text keeps final-look (owed from 6) in NEXT; reading the last one would +# drop it. From Stage 5 the skeleton and agent-stub rules win NEXT first, so this case records a +# proven skeleton and fills the stub slots — both are what a real Stage 6 project has. +printf '\nSkeleton proven 2026-10-13\n' >> "$P9/PROJECT.md" +for _f in "$P9"/.claude/agents/*.md "$P9/CLAUDE.local.md"; do + [ -f "$_f" ] && sed -E 's/\{\{[^}]*\}\}/filled/g' "$_f" > "$_f.new" && mv "$_f.new" "$_f" +done +setline "$P9" "$(printf '%s' "$DERIVED" | sed -E 's/Stage P — Kickoff/Stage 6 — Verify/; s/gates passed: [^(]*/gates passed: P-5, Stage 2 re-opened /')" +case "$(stage_of "$P9")" in *"Stage 6"*"Stage 2"*"(derived by gate-check"*) : ;; *) bad "T9d setup" "$(stage_of "$P9")";; esac +n="$(nextof "$W/tk2/bin/status.sh" "$P9")" +case "$n" in "discharge:"*final-look*) ok "T9d first Stage N wins: Stage 6 keeps final-look despite a later Stage 2";; + *) bad "T9d expected final-look in a discharge NEXT (first Stage N is 6)" "$n";; esac + echo "SCORE: $PASS/$((PASS+FAIL))" [ "$FAIL" -eq 0 ] From 22d6f7eaf178aaca22f6dd827404981b0453e5fa Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Tue, 13 Oct 2026 15:55:53 +0000 Subject: [PATCH 3/3] test(status): T9a holds before and after #321 NOT YET DUE When gate-check already reports final-look NOT YET DUE (#321), final-look is no longer pending: assert it is absent from the obligations-pending line and mark the baseline control not applicable. Pre-#321 assertions unchanged. 27/27 on this branch and on a scratch merge onto integration/2026-10-14. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- tests/wave2/test-status-existing-app.sh | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/wave2/test-status-existing-app.sh b/tests/wave2/test-status-existing-app.sh index 423391fd..839fca91 100755 --- a/tests/wave2/test-status-existing-app.sh +++ b/tests/wave2/test-status-existing-app.sh @@ -192,10 +192,23 @@ case "$n" in "discharge:"*) ok "T9a derived Stage P: NEXT still discharges what case "$n" in *final-look*) bad "T9a derived Stage P: NEXT still names final-look" "$n";; *) ok "T9a derived Stage P: final-look not in NEXT";; esac case "$n" in *skeleton*) ok "T9a only final-look is filtered (skeleton, also above Stage P, stays)";; *) bad "T9a filter dropped more than final-look" "$n";; esac f="$("$W/tk2/bin/status.sh" "$P9" 2>/dev/null)" -printf '%s\n' "$f" | grep -q 'obligations pending:.*final-look' && ok "T9a the screen still lists final-look under obligations pending" || bad "T9a obligations pending line lost final-look" -if [ "$HAVE_BASE" = 1 ]; then - o="$(nextof "$W/tk2/bin/status-baseline.sh" "$P9")" - case "$o" in *final-look*) ok "T9a control: the baseline sent a Stage P project to final-look";; *) bad "T9a control: baseline did not name final-look — case cannot fail" "$o";; esac +# Once obligation-check.sh itself reports final-look NOT YET DUE under an early derived readout +# (#321), final-look is no longer pending at all: then it must be ABSENT from the pending line, and +# the baseline control cannot apply (the old status.sh would not name it either). Before #321, the +# pending line keeps listing it and the baseline control shows the old NEXT sent the project there. +# Captured first: under pipefail, `gate-check | grep -q` reads as false when grep exits early. +GC9="$("$W/tk2/bin/gate-check-real.sh" --no-html "$P9" 2>/dev/null)" +if printf '%s\n' "$GC9" | grep -E '^Obligation final-look +NOT YET DUE' >/dev/null; then + printf '%s\n' "$f" | grep -q 'obligations pending:.*final-look' \ + && bad "T9a final-look is NOT YET DUE in gate-check but still listed as pending" "$(printf '%s\n' "$f" | grep 'obligations pending')" \ + || ok "T9a gate-check says final-look NOT YET DUE: absent from the obligations pending line" + ok "T9a control: not applicable — gate-check already reports final-look NOT YET DUE (#321)" +else + printf '%s\n' "$f" | grep -q 'obligations pending:.*final-look' && ok "T9a the screen still lists final-look under obligations pending" || bad "T9a obligations pending line lost final-look" + if [ "$HAVE_BASE" = 1 ]; then + o="$(nextof "$W/tk2/bin/status-baseline.sh" "$P9")" + case "$o" in *final-look*) ok "T9a control: the baseline sent a Stage P project to final-look";; *) bad "T9a control: baseline did not name final-look — case cannot fail" "$o";; esac + fi fi # T9b a module opened under the same lagging derived line: the module passes stay in NEXT.