diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20342404..1f09c5a8 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:
— · 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 : 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..fd11576d 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,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 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"
+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/\..*$//')"
@@ -151,6 +210,13 @@ if [ -x "$PROJECT_DIR/bin/coherence-cadence.sh" ]; then
COH="coherence $COH_OUT"
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() {
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 +232,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"
}
@@ -195,12 +261,13 @@ 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
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 +275,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..3ad624be 100644
--- a/skills/existing-app-change.md
+++ b/skills/existing-app-change.md
@@ -199,6 +199,64 @@ 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 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.
+
+---
+
+## 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 `/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