Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m

## Unreleased

- fix(build-plan-status): **every State the build-plan skill lists (built, partial, not built, blocked) and every spelling the phase card already counts (complete, verified, in progress, not needed and the like) is now read the same way by the plan header and the phase badge, so card, header and badge give one count; blocked counts as not built and is named in the header, genuinely unreadable values are still reported as unreadable; the later-stage placeholder FAIL is relabelled PENDING only while that stage is pending in PROJECT.md, with tests that a real FAIL stays FAIL and that FAIL stays when the current stage is unknown** — MendixMau
- fix(release-review): **the v1.0 release review: the multilingual skill loses its em and en dashes (wording only, content unchanged; its routing row keeps the trigger separator the agent stubs cut on), `doctor.sh` warns when a hook copy in `~/.claude/hooks` calls a toolkit script that no longer exists (it names the script and says to re-run `bin/install-claude-hooks.sh --full`; a warning, never a failure), and the `.gitignore` line for `.claude/progress-report-sessions.txt` now also reaches a project checked out as a git worktree (`.git` is a file there) in `doctor.sh` and `sync-project.sh`; `check-ignore --no-index` stops the line being appended again when the file is already tracked, and a `git rm --cached` hint is printed.** Tests: new `test-doctor-stale-hooks.sh`; worktree and tracked-file cases added to `test-progress-report-gitignore.sh`. CHANGELOG tidy (a stray blank line, one credit). — MendixMau
- fix(progress-report): **round 6 of the review: the session cache is machine-local and tied to its project, the first cached run is not slower than a cold one, and the build ledger decides the Stage 4 to 5 switch.** (1) `.claude/progress-report-sessions.txt` holds absolute session-log paths, so `init-project.sh`, `doctor.sh` and `sync-project.sh` now put `/.claude/progress-report-sessions.txt` in the project's `.gitignore`, the way they do for the doctor receipt and `toolkit.env`. (2) The cache header records the project's real path and the attribution rules (25% of tool calls, 20 calls); a copied or moved project, or a changed threshold, reads it as cold, so a copy never lists the original's sessions. (3) Files skipped by the 300-file cap are recorded as `no`, so a cached run never reads more files than a cold one. (4) With rows in `metrics-ledger.tsv` (else BUILD-LOG) the switch comes from the ledger; an earlier exec.sh call or build skill in a session log is ignored, the timed sign-off rule stays, and the footer says how the switch is chosen. Tests: F15 to F17 in `test-progress-report-field-fixes.sh`, new `test-progress-report-gitignore.sh` — MendixMau
- fix(progress-report): **polish on a fresh and an updated demo project: a "done <date>" plan row counts as built in the card, the plan header and the phase badge alike; a placeholder gate failure on a stage after the current one reads PENDING, not FAIL; the no-session-log notice is one calm line with the regenerate hint inside "How this was measured"; the test-moments table says unit tests run for the flows chosen at CAC-5, not "when switched on".** — field review of the #434 report
Expand Down
40 changes: 28 additions & 12 deletions project-bin/build-plan-status.sh
Original file line number Diff line number Diff line change
Expand Up @@ -300,18 +300,27 @@ def column_map(headers):
m[key] = i
return m if "number" in m and "state" in m else None

# Same prefixes as classify_state in progress-report.py, so the phase card, the plan header and
# the phase badge give one count. brd-to-build-plan.md lists `built` / `partial` / `not built` /
# `blocked`; the rest are the spellings the card already counts. `blocked` counts as not built.
# A "not needed" style row is out of the count and out of the rollup. None = cannot be read.
def classify(state):
s = state.lower()
if s.startswith("not built"): return "not built"
if s.startswith(("built", "done")): return "built"
s = state.lower().replace("**", "").strip()
if s.startswith("pending a person"): return "pending a person"
if s.startswith(("blocked",)): return "blocked"
if s.startswith(("not built", "not done", "not started", "planned", "to do", "todo", "open", "pending", "failed")): return "not built"
if s.startswith(("not needed", "n/a", "dropped", "skipped", "waived", "deferred")): return "skipped"
if s.startswith(("built", "done", "complete", "verified", "proven", "passed")): return "built"
if s.startswith(("partial", "in progress", "started", "wip")): return "partial"
return None

def rollup(steps):
kinds = [classify(s["state"]) for s in steps]
if not kinds or None in kinds: return "unknown"
if None in kinds: return "unknown"
kinds = [k for k in kinds if k != "skipped"]
if not kinds: return "unknown"
if all(k == "built" for k in kinds): return "built"
if all(k == "not built" for k in kinds): return "not built"
if all(k in ("not built", "blocked") for k in kinds): return "not built"
if all(k in ("built", "pending a person") for k in kinds): return "pending a person"
return "in progress"

Expand Down Expand Up @@ -556,19 +565,23 @@ def on_disk(pat):
if any(fnmatch.fnmatchcase(f, pat) for f in files): return "written"
return "missing"

def classify(state): # the parser's rollup reading, per row
s = (state or "").lower()
if s.startswith("not built"): return "not built"
if s.startswith(("built", "done")): return "built"
def classify(state): # the parser's rollup reading, per row (keep in step with classify in parse_plan)
s = (state or "").lower().replace("**", "").strip()
if s.startswith("pending a person"): return "pending a person"
if s.startswith("blocked"): return "blocked"
if s.startswith(("not built", "not done", "not started", "planned", "to do", "todo", "open", "pending", "failed")): return "not built"
if s.startswith(("not needed", "n/a", "dropped", "skipped", "waived", "deferred")): return "skipped"
if s.startswith(("built", "done", "complete", "verified", "proven", "passed")): return "built"
if s.startswith(("partial", "in progress", "started", "wip")): return "partial"
return None

CLS = {"built": "st-built", "not built": "st-todo", "pending a person": "st-person",
"partial": "st-progress", "blocked": "st-todo", "skipped": "st-todo",
"in progress": "st-progress", "unknown": "st-unknown", None: "st-unknown"}

phases = d.get("phases", [])
all_steps = [s for p in phases for s in p["steps"]]
tally = {"built": 0, "not built": 0, "pending a person": 0, None: 0}
tally = {"built": 0, "not built": 0, "pending a person": 0, "partial": 0, "blocked": 0, "skipped": 0, None: 0}
for s in all_steps:
tally[classify(s["state"])] += 1
named_total = named_done = drift_rows = 0
Expand Down Expand Up @@ -607,9 +620,12 @@ for p in phases:
plural = lambda n, w: "%d %s%s" % (n, w, "" if n == 1 else "s")
print("<h2>The plan</h2>")
print("<p class=lede>From <code>architecture/build-plan.md</code>: %s, %s: "
"<b class=st-built>%d built</b>, <b class=st-todo>%d not built</b>, <b class=st-person>%d pending a person</b>%s.</p>" % (
"<b class=st-built>%d built</b>, <b class=st-progress>%d partial</b>, <b class=st-todo>%d not built</b>%s, <b class=st-person>%d pending a person</b>%s%s.</p>" % (
plural(len(phases), "phase"), plural(len(all_steps), "row"),
tally["built"], tally["not built"], tally["pending a person"],
tally["built"], tally["partial"], tally["not built"] + tally["blocked"],
(" (%d of them blocked)" % tally["blocked"]) if tally["blocked"] else "",
tally["pending a person"],
(", %d not needed" % tally["skipped"]) if tally["skipped"] else "",
(", <b class=st-unknown>%d with a State cell this page cannot read</b>" % tally[None]) if tally[None] else ""))
if named_total:
print("<p class=note>Scripts named in the plan: %d, of which %d are renamed <code>done-</code> under <code>mdlsource/</code>.%s</p>" % (
Expand Down
2 changes: 1 addition & 1 deletion project-bin/progress-report.py
Original file line number Diff line number Diff line change
Expand Up @@ -1475,7 +1475,7 @@ def money(c): return "n/a" if unp else "$%.0f" % c
elif row:
# a placeholder FAIL on a stage after the current one is just "not started yet": show it as pending, not as a failure
rs = row["status"]
if rs == "FAIL" and "placeholder" in row["detail"].lower() and info["current"] in STAGES and STAGES.index(s) > STAGES.index(info["current"]): rs = "PENDING"
if rs == "FAIL" and states[s] == "pending" and "placeholder" in row["detail"].lower() and info["current"] in STAGES and STAGES.index(s) > STAGES.index(info["current"]): rs = "PENDING"
w("<div class=sub title='%s'>Gate-check page: <b>%s</b> %s</div>" % (esc(row["detail"]), esc(rs), esc(row["detail"][:110] + ("..." if len(row["detail"]) > 110 else ""))))
files = list(arts[s]["files"].items()); prim = [(f, i) for f, i in files if link_kind(f) == "html"]; rest = [(f, i) for f, i in files if link_kind(f) != "html"]
def chip(f, cls=""):
Expand Down
76 changes: 75 additions & 1 deletion tests/wave2/test-progress-report-polish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
# nothing is reported as a State cell the page cannot read. "not built" is still checked first.
# P2 a gate-check placeholder failure on a stage AFTER the current one reads PENDING, not FAIL; the same failure
# on the current stage stays FAIL.
# P2b only a placeholder FAIL is relabelled: a real FAIL on a later stage stays FAIL, and every FAIL stays FAIL
# when the current stage cannot be read from PROJECT.md.
# P5 every State the build-plan skill lists (built / partial / not built / blocked) and the spellings the card
# counts (complete, verified, in progress, ...) are read the same way by the card, the plan header and the
# phase badge; a genuinely unreadable value is still reported as unreadable.
# P3 no session log: one calm line (not the orange box), the regenerate hint inside "How this was measured";
# with --no-log the page never claims it looked in folders or logs.
#
Expand Down Expand Up @@ -53,7 +58,7 @@ CLAUDE_CONFIG_DIR="$EMPTY" bash "$SCRIPT" "$P1" --html >/dev/null 2>&1
R="$P1/architecture/progress-report.html"
[ -f "$R" ] && ok "report written" || bad "report written"
has "$R" "2 of 3 rows built" "Stage 5 card counts the done row as built (2 of 3)"
has "$R" "<b class=st-built>2 built</b>, <b class=st-todo>1 not built</b>" "plan header agrees: 2 built, 1 not built"
has "$R" "<b class=st-built>2 built</b>, <b class=st-progress>0 partial</b>, <b class=st-todo>1 not built</b>" "plan header agrees: 2 built, 1 not built"
hasnt "$R" "cannot read" "no State cell is called unreadable"
has "$R" "1 of 2 rows built" "Phase 2 shows 1 of 2 built"
hasnt "$R" 'st-unknown">unknown' "no phase is labelled unknown"
Expand All @@ -78,6 +83,75 @@ assert line("0") == "PENDING", line("0")
assert line("P") == "FAIL", line("P")
PY

echo "P2b only placeholder FAILs relabel; unknown current stage keeps FAIL"
P2B="$WORK/p2b"; mkproj "$P2B" "**Stage P - Kickoff**, in progress - gates passed: none"
cat > "$P2B/index.html" <<'HTML'
<table>
<tr><td>P</td><td>Kickoff</td><td>FAIL</td><td>'Confirmed by:' still holds the shipped placeholder: "[user] on [date]"</td></tr>
<tr><td>0</td><td>Triage</td><td>FAIL</td><td>decision table has no rows</td></tr>
<tr><td>1</td><td>Intake</td><td>FAIL</td><td>'Confirmed by:' still holds the shipped placeholder: "[user] on [date]"</td></tr>
</table>
HTML
CLAUDE_CONFIG_DIR="$EMPTY" bash "$SCRIPT" "$P2B" --html >/dev/null 2>&1
stage_line() { # report stage -> status word shown on the gate-check line
"$PY" - "$1" "$2" <<'PY'
import re, sys
h = open(sys.argv[1], encoding="utf-8").read()
i = h.index("id='stage-%s'" % sys.argv[2]); j = h.find("class='card stage", i + 10)
print(re.search(r"Gate-check page: <b>(\w+)</b>", h[i:j if j > 0 else len(h)]).group(1))
PY
}
R2B="$P2B/architecture/progress-report.html"
[ "$(stage_line "$R2B" 0)" = FAIL ] && ok "non-placeholder FAIL on a later stage stays FAIL" || bad "non-placeholder FAIL on a later stage stays FAIL"
[ "$(stage_line "$R2B" 1)" = PENDING ] && ok "placeholder FAIL on a later stage still reads PENDING" || bad "placeholder FAIL on a later stage still reads PENDING"
P2C="$WORK/p2c"; mkdir -p "$P2C/architecture"; printf '# PROJECT.md\n\nno stage line here\n' > "$P2C/PROJECT.md"
cp "$P2/index.html" "$P2C/index.html"
CLAUDE_CONFIG_DIR="$EMPTY" bash "$SCRIPT" "$P2C" --html >/dev/null 2>&1
R2C="$P2C/architecture/progress-report.html"
[ "$(stage_line "$R2C" 0)" = FAIL ] && ok "unknown current stage: placeholder FAIL stays FAIL" || bad "unknown current stage: placeholder FAIL stays FAIL"

echo "P5 every listed State is read the same by card, plan header and phase badge"
P5="$WORK/p5"; mkproj "$P5" "**Stage 5 - Build**, in progress - gates passed: P, 0, 1, 2, 3, 4"
cat > "$P5/architecture/build-plan.md" <<'MD'
# Build plan

### Phase 1 - Alpha

| # | Kind | Step | Produces | Depends on | State |
|---|---|---|---|---|---|
| 1 | BUILD | one | 01-a.mdl | - | built |
| 2 | BUILD | two | 02-a.mdl | 1 | complete |
| 3 | BUILD | three | 03-a.mdl | 2 | verified |

### Phase 2 - Beta

| # | Kind | Step | Produces | Depends on | State |
|---|---|---|---|---|---|
| 4 | BUILD | four | 04-b.mdl | - | partial |
| 5 | BUILD | five | 05-b.mdl | 4 | blocked |
| 6 | BUILD | six | 06-b.mdl | 4 | not built |

### Phase 3 - Gamma

| # | Kind | Step | Produces | Depends on | State |
|---|---|---|---|---|---|
| 7 | BUILD | seven | 07-c.mdl | - | blocked (waiting on an answer) |
| 8 | BUILD | eight | 08-c.mdl | - | not built |
MD
CLAUDE_CONFIG_DIR="$EMPTY" bash "$SCRIPT" "$P5" --html >/dev/null 2>&1
R5="$P5/architecture/progress-report.html"
has "$R5" "3 of 8 rows built, 1 partial" "card: 3 of 8 built, 1 partial"
has "$R5" "<b class=st-built>3 built</b>, <b class=st-progress>1 partial</b>, <b class=st-todo>4 not built</b> (2 of them blocked)" "plan header: 3 built, 1 partial, 4 not built of which 2 blocked"
hasnt "$R5" "cannot read" "no listed State is called unreadable"
hasnt "$R5" 'st-unknown">unknown' "no phase is labelled unknown"
has "$R5" 'st-built">built</span>' "Phase 1 badge: built"
has "$R5" 'st-progress">in progress</span>' "Phase 2 badge: in progress"
has "$R5" 'st-todo">not built</span>' "Phase 3 badge: not built (blocked counts as not built)"
has "$R5" "3 of 3 rows built" "Phase 1 shows 3 of 3 built"
sed 's/| not built |$/| whenever it suits |/' "$P5/architecture/build-plan.md" > "$P5/architecture/bp2.md" && mv "$P5/architecture/bp2.md" "$P5/architecture/build-plan.md"
CLAUDE_CONFIG_DIR="$EMPTY" bash "$SCRIPT" "$P5" --html >/dev/null 2>&1
has "$R5" "2 with a State cell this page cannot read" "a genuinely unreadable State is still reported as unreadable"

echo "P3 no-log line is calm and honest"
has "$R" "class=nolog" "one calm line"
hasnt "$R" "class='card note-card'" "no big banner box"
Expand Down
Loading