fix(board): plan/brief use actual debt, not raw pending mail

board.sh --plan group 2 and --brief conflated every unhandled inbox
message with an obligation to reply, including ones the sender
declared reply-expected: no (a notice, not a request). Reported by
morning-driver (2026-08-11), independently reproduced against the live
mailbox on 2026-08-13: 27 of 72 pending messages were notices. Both
paths now join against coord-count.sh's owed column instead, so a
done/deferred/blocked repo whose only mail is FYI no longer gets a
plan tab, and --brief no longer counts a notice as an obligation. The
table's raw INN column is unchanged by design.

While extending that join with a second lookup file, found and fixed
a more severe, independent defect: the existing $UNBLOCKS/$RECORDS
join used the NR==FNR awk idiom, which silently empties the entire
plan whenever the first file is empty -- i.e. whenever the repo tree
has zero blocked repos, a common, ordinary state, not an edge case.
Verified against the shipped 0.21.0 script. Fixed by matching on
FILENAME instead of cumulative line counts, for both lookup files.

Also repoints the README's governance link at repo-standard's
canonical GOVERNANCE.md (was pointing at the marketplace's copy),
per org-ops D11.

board-selftest.sh: 142 -> 150 checks. Version 0.21.0 -> 0.22.0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGWMPskXBsTjMrrQ2GofFx
This commit is contained in:
Kjell Tore Guttormsen 2026-08-13 20:54:55 +02:00
commit 03e712a423
10 changed files with 263 additions and 27 deletions

View file

@ -611,6 +611,67 @@ mkrepo "$ROOT/repo-done-owes"
mkdir -p "$CLAUDE_COORD_DIR/repo-done-owes/inbox"
echo "msg" > "$CLAUDE_COORD_DIR/repo-done-owes/inbox/2026-msg1-from-w.md"
# repo-done-fyi: done, with pending mail, but every message declares
# reply-expected: no (a notice, not a request). Every OTHER fixture message in
# this suite is written with no frontmatter at all, which owes_reply() in
# coord-count.sh treats as owing by default - so pending and owed have been
# equal in every prior fixture, and a raw-pending-as-debt bug is invisible to
# this suite without a genuine FYI-only mailbox. Measured on the live mailbox
# 2026-08-11 by morning-driver: 14 of 22 unhandled messages were notices.
mkrepo "$ROOT/repo-done-fyi"
{
echo "# STATE - repo-done-fyi"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=done; blocked-on=-; next-cost=Sonnet 5/high -->"
printf 'Ferdig %s to varsler ligger i innboksen, ingen skylder svar.\n' "$EMDASH"
} > "$ROOT/repo-done-fyi/STATE.md"
mkdir -p "$CLAUDE_COORD_DIR/repo-done-fyi/inbox"
for n in 1 2; do
{
echo "---"
echo "from: w"
echo "to: repo-done-fyi"
echo "reply-expected: no"
echo "---"
echo "fyi only"
} > "$CLAUDE_COORD_DIR/repo-done-fyi/inbox/2026-msg$n-from-w.md"
done
# repo-blocked-mixed: blocked, 3 pending messages but only 2 owe a reply - the
# third declares reply-expected: no. repo-blocked-owes (above) cannot prove
# ranking uses OWED rather than raw pending, because every one of its messages
# happens to owe; this fixture separates the two counts (3 pending, 2 owed).
mkrepo "$ROOT/repo-blocked-mixed"
{
echo "# STATE - repo-blocked-mixed"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=blocked; blocked-on=-; next-cost=Opus 5/high -->"
printf 'Blokkert %s tre meldinger, bare to skylder svar.\n' "$EMDASH"
} > "$ROOT/repo-blocked-mixed/STATE.md"
mkdir -p "$CLAUDE_COORD_DIR/repo-blocked-mixed/inbox"
echo "msg" > "$CLAUDE_COORD_DIR/repo-blocked-mixed/inbox/2026-msg1-from-w.md"
echo "msg" > "$CLAUDE_COORD_DIR/repo-blocked-mixed/inbox/2026-msg2-from-w.md"
{
echo "---"
echo "from: w"
echo "to: repo-blocked-mixed"
echo "reply-expected: no"
echo "---"
echo "fyi only"
} > "$CLAUDE_COORD_DIR/repo-blocked-mixed/inbox/2026-msg3-from-w.md"
# A fresh --brief, taken after repo-done-fyi and repo-blocked-mixed exist:
# BRIEF (above) predates both fixtures, so it cannot exercise them.
BRIEF3="$("$BOARD" --roots "$ROOT" --brief 2>/dev/null)"
printf '%s' "$BRIEF3" | grep -q 'repo-done-fyi'; [ $? -ne 0 ]
check "brief excludes a repo whose pending mail is all FYI (no debt)" $?
printf '%s' "$BRIEF3" | grep -q 'repo-blocked-mixed'
check "brief includes a repo with mixed mail (it has 2 owed)" $?
printf '%s' "$BRIEF3" | grep -qE 'repo-blocked-mixed[[:space:]]+INN[[:space:]]+2([[:space:]]|$)'
check "brief's INN count is owed messages, not raw pending (3 pending, 2 owed)" $?
PLAN="$("$BOARD" --roots "$ROOT" --plan 2>/dev/null)"; rc=$?
check "--plan exits 0" "$rc"
@ -695,6 +756,22 @@ printf '%s\n' "$PLAN" | grep -A4 '^repo=repo-done-owes$' | grep -q '^status=done
check "a done repo in the plan still reports its real status" $?
printf '%s\n' "$PLAN" | grep -m1 '^# Utelatt' | grep -q 'skylder svar'
check "the header states the exclusions are conditional on owing nothing" $?
# The header above already says "skylder svar" (owes a reply) - a done repo
# whose pending mail is 100% notices does not owe a reply, so it is excluded
# on the SAME rule as repo-wt, not a new one. Pending (2) must not stand in
# for owed (0).
printf '%s' "$PLAN" | grep -q '^repo=repo-done-fyi$'; [ $? -ne 0 ]
check "plan excludes a done repo whose pending mail is all FYI (owed 0, pending 2)" $?
# blocked-with-debt (repo-blocked-owes, above) stays planned on its debt; this
# fixture proves the COUNT that seats and ranks it is the owed count, not the
# raw pending count - 3 pending, 2 owed.
printf '%s' "$PLAN" | grep -q '^repo=repo-blocked-mixed$'
check "a blocked repo with mixed mail is still planned on its owed messages" $?
printf '%s\n' "$PLAN" | grep -A4 '^repo=repo-blocked-mixed$' | grep -q '^why=inbox:2$'
check "why=inbox:N counts owed messages, not raw pending (3 pending, 2 owed)" $?
printf '%s' "$PLAN" | grep -q '^repo=repo-d$'; [ $? -ne 0 ]
check "plan excludes a deferred repo" $?
printf '%s' "$PLAN" | grep -q '^repo=plug-x$'; [ $? -ne 0 ]
@ -1144,6 +1221,35 @@ check "--brief still exits 0 after the ranking change" $?
[ "$n2_tab" -ge "$n_tab" ]
check "the grouped plan hides nothing an earlier ordering showed" $?
# --- 13. Zero blocked repos does not empty the plan (NR==FNR regression) ---
# Isolated root, not the shared fixture tree above: that tree has carried a
# blocked repo (repo-blocked-owes, plug-x, ...) since the chain-root feature
# shipped, which is exactly the state that kept this bug invisible to every
# check above it. $UNBLOCKS is empty only when NO repo in the scan is blocked
# - an ordinary tree state, reproduced here with a root that has none at all.
# Verified against the shipped 0.21.0 script before this fix existed: this
# exact fixture produced "0 tabber" - solo-repo, with an unhandled inbox
# message and nothing blocking anything, vanished from its own plan.
NOBLOCK_ROOT="$(mktemp -d)"
NOBLOCK_COORD="$(mktemp -d)"
mkrepo "$NOBLOCK_ROOT/solo-repo"
{
echo "# STATE - solo-repo"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=in-progress; blocked-on=-; next-cost=Sonnet 5/high -->"
echo "Ordinary work, nothing blocked anywhere in this tree."
} > "$NOBLOCK_ROOT/solo-repo/STATE.md"
mkdir -p "$NOBLOCK_COORD/solo-repo/inbox"
echo "msg" > "$NOBLOCK_COORD/solo-repo/inbox/2026-msg1-from-w.md"
NOBLOCK_PLAN="$(CLAUDE_COORD_DIR="$NOBLOCK_COORD" "$BOARD" --roots "$NOBLOCK_ROOT" --plan 2>/dev/null)"
printf '%s' "$NOBLOCK_PLAN" | grep -q '^repo=solo-repo$'
check "zero blocked repos in the tree does not empty the plan (NR==FNR regression)" $?
printf '%s\n' "$NOBLOCK_PLAN" | grep -A3 '^repo=solo-repo$' | grep -q '^why=inbox:1$'
check "the surviving repo still ranks correctly with an empty chain-root lookup" $?
/bin/rm -rf "$NOBLOCK_ROOT" "$NOBLOCK_COORD" 2>/dev/null
echo ""
echo "board-selftest: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1