feat(board): PLAN column - where a destination is missing or untouched

A display-only table column and a `plan=` field in --row, read from the
filesystem: `-` = no file named exactly PLAN.md in the repo root; `Nd` =
whole days the plan's mtime lies behind the last commit (`0d` when as new
or newer); `?` = a plan with no commit to compare against. The content is
never read, and the legend says the column does not judge whether a
criterion can be falsified. Not a sort key, not in --plan, not in SUM.

The name is matched byte for byte through a glob plus a basename compare:
APFS is case-insensitive by default, so `[ -f PLAN.md ]` is true for a
`plan.md`. The glob needs no subprocess.

The checks were written red in a separate eval before the column existed
and pass unchanged; they now live in board-selftest section 34 and the
eval file is removed (two judges for one column would drift).
Mutation-verified: a case-blind test, sorting on the field, a plan= key
in --plan and a legend without IKKE each turn exactly one check red.

The new record field sits before `neste`, so every "last field onward"
index moved 17 -> 18, and F6's right-counted DRT read moved NF-3 -> NF-4.

board-selftest 428 -> 461 (badge 961). Suites: coord 257, board 461,
route 73, orders 116, guard 54; npm test 12/12, run after git add.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-17 16:17:49 +02:00
commit 87fb78334e
7 changed files with 327 additions and 260 deletions

View file

@ -2379,19 +2379,19 @@ check "F6: fixture ground truth - git really does fail on the broken worktree" $
F6TBL="$(CLAUDE_COORD_DIR="$F6COORD" "$BOARD" --roots "$F6ROOT" 2>/dev/null)"
# DRT is counted from the RIGHT (NF-3: DRT ALDER SISTE NESTE), never from the
# DRT is counted from the RIGHT (NF-4: DRT ALDER SISTE PLAN NESTE), never from the
# left: the KOST field is free text and its realistic spelling ("Sonnet 5/low")
# contains a space, so $7 reads FLY instead. Measured, not assumed - the first
# cut of this section passed its clean-tree control by accident that way.
printf '%s' "$F6TBL" | grep -E '^f6-clean[[:space:]]' | awk '{print $(NF-3)}' | grep -qx '0'
printf '%s' "$F6TBL" | grep -E '^f6-clean[[:space:]]' | awk '{print $(NF-4)}' | grep -qx '0'
check "F6: control - a healthy CLEAN tree still reads DRT 0" $?
printf '%s' "$F6TBL" | grep -E '^f6-dirty[[:space:]]' | awk '{print $(NF-3)}' | grep -qx '1'
printf '%s' "$F6TBL" | grep -E '^f6-dirty[[:space:]]' | awk '{print $(NF-4)}' | grep -qx '1'
check "F6: control - a healthy DIRTY tree still reads DRT 1" $?
printf '%s' "$F6TBL" | grep -E '^f6-broken[[:space:]]' | awk '{print $(NF-3)}' | grep -qx '0'
printf '%s' "$F6TBL" | grep -E '^f6-broken[[:space:]]' | awk '{print $(NF-4)}' | grep -qx '0'
[ $? -ne 0 ]
check "F6: a git failure does NOT read as DRT 0 (clean tree)" $?
printf '%s' "$F6TBL" | grep -E '^f6-broken[[:space:]]' | awk '{print $(NF-3)}' | grep -qx '?'
printf '%s' "$F6TBL" | grep -E '^f6-broken[[:space:]]' | awk '{print $(NF-4)}' | grep -qx '?'
check "F6: a git failure reads as ? (not measured), the same token coord-count uses" $?
# The load-bearing consequence. All three fixtures are status=done with no mail
@ -3455,6 +3455,218 @@ grep -q -- '--row' "$BOARD"; check "row: board.sh actually carries the --row fla
/bin/rm -rf "$ROW_ROOT" "$ROW_COORD" 2>/dev/null
# --- 34. PLAN column: a filesystem reading, display only --------------------
# Operator decision 2026-09-17. The cell answers one question: does this repo
# keep a destination where sessions actually look (a root PLAN.md), and is
# anyone touching it? `-` = no file named exactly PLAN.md in the root; `Nd` =
# whole days its mtime lies behind the last commit. It never reads the file,
# never enters a sort, a --plan block or SUM, and does not judge whether a
# criterion inside it can be falsified - the legend says so.
#
# These checks were written RED in a separate file before the column existed
# (24 of 33 failing because the column was missing, fixture ground truth
# green), kept out of this suite so main stayed green, and moved here once the
# column passed them unchanged. That file is gone: two judges for one column
# would drift. Mutation-verified against four broken variants, each turning
# exactly one check red: a case-blind `[ -f PLAN.md ]`, sorting rows on the
# plan field, a plan= key in the --plan blocks, and a legend without IKKE.
#
# Table cells are located through the HEADER (split on runs of 2+ spaces),
# never a fixed offset, so a later column move does not break this section.
PC_ROOT="$(mktemp -d)"
PC_COORD="$(mktemp -d)"
PC_NOW="$(date +%s)"
PC_DAY=86400
pc_state() {
{
echo "# STATE - $2"
echo "## NESTE - START HER"
echo "<!-- board: status=$3; blocked-on=-; next-cost=Sonnet 5/high -->"
echo "Next step for $2."
} > "$1/STATE.md"
}
# pc_commit_at <dir> <epoch> <msg>: commit everything with a fixed committer date.
pc_commit_at() {
git -C "$1" add -A >/dev/null 2>&1
GIT_AUTHOR_DATE="@$2 +0000" GIT_COMMITTER_DATE="@$2 +0000" \
git -C "$1" -c user.email=t@t -c user.name=t commit -qm "$3" >/dev/null 2>&1
}
pc_touch_at() { touch -t "$(date -r "$2" +%Y%m%d%H%M.%S)" "$1"; }
pc_last_commit() { git -C "$1" log -1 --format=%ct 2>/dev/null; }
pc_mtime() { stat -f %m "$1"; }
# pc_table_cell <table-output> <repo>: the PLAN cell of <repo>'s row, read under
# the nearest preceding header line. Empty when the header has no PLAN column.
pc_table_cell() {
printf '%s\n' "$1" | awk -v repo="$2" '
function split2(s, arr, n) { gsub(/ +/, "\t", s); sub(/^\t/, "", s); return split(s, arr, "\t") }
/^REPO / { n = split2($0, h); col = 0; for (i = 1; i <= n; i++) if (h[i] == "PLAN") col = i; next }
$1 == repo && col > 0 { split2($0, r); print r[col]; exit }'
}
pc_row_plan() { printf '%s\n' "$1" | sed -n 's/^plan=//p'; }
pc_board() { CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_ROOT" "$@" 2>/dev/null; }
# --- Fixtures ---------------------------------------------------------------
# p-none: no PLAN.md anywhere.
mkrepo "$PC_ROOT/p-none"; pc_state "$PC_ROOT/p-none" p-none planned
pc_commit_at "$PC_ROOT/p-none" $((PC_NOW - 3 * PC_DAY)) init
# p-fresh: PLAN.md newer than the last commit -> 0d. The file is EMPTY: a value
# computed from content could not produce 0d here, so this also shows that
# the column is a filesystem reading.
mkrepo "$PC_ROOT/p-fresh"; pc_state "$PC_ROOT/p-fresh" p-fresh planned
pc_commit_at "$PC_ROOT/p-fresh" $((PC_NOW - 2 * PC_DAY)) init
: > "$PC_ROOT/p-fresh/PLAN.md"
pc_touch_at "$PC_ROOT/p-fresh/PLAN.md" "$PC_NOW"
# p-stale: PLAN.md 5 days and 3 hours older than the last commit -> 5d. Its
# content looks like a value on purpose ("PLAN=99d"); it must not be read.
mkrepo "$PC_ROOT/p-stale"; pc_state "$PC_ROOT/p-stale" p-stale planned
printf '# PLAN\nPLAN=99d\n' > "$PC_ROOT/p-stale/PLAN.md"
PC_P_STALE_CT=$((PC_NOW - 3600))
pc_commit_at "$PC_ROOT/p-stale" "$PC_P_STALE_CT" init
pc_touch_at "$PC_ROOT/p-stale/PLAN.md" $((PC_P_STALE_CT - 5 * PC_DAY - 3 * 3600))
# p-docs: PLAN.md in docs/ only - the wrong place by convention.
mkrepo "$PC_ROOT/p-docs"; pc_state "$PC_ROOT/p-docs" p-docs planned
mkdir -p "$PC_ROOT/p-docs/docs"; echo "# PLAN" > "$PC_ROOT/p-docs/docs/PLAN.md"
pc_commit_at "$PC_ROOT/p-docs" $((PC_NOW - PC_DAY)) init
# p-lower: lowercase plan.md in the root. On a case-insensitive filesystem
# (APFS default) `[ -f PLAN.md ]` is TRUE here, so a board that asks the
# filesystem alone would report a plan that is not there. The name has to be
# compared byte for byte against what the directory actually stores (e.g. a
# directory listing matched exactly against PLAN.md).
mkrepo "$PC_ROOT/p-lower"; pc_state "$PC_ROOT/p-lower" p-lower planned
echo "# plan" > "$PC_ROOT/p-lower/plan.md"
pc_commit_at "$PC_ROOT/p-lower" $((PC_NOW - PC_DAY)) init
# --- Ground truth for the fixtures (these must be green today) -------------
[ -z "$(git -C "$PC_ROOT/p-stale" status --porcelain)" ]
check "plan: fixture - p-stale is committed clean" $?
PC_P_STALE_EXP="$(( ($(pc_last_commit "$PC_ROOT/p-stale") - $(pc_mtime "$PC_ROOT/p-stale/PLAN.md")) / PC_DAY ))d"
[ "$PC_P_STALE_EXP" != "0d" ]
check "plan: fixture - p-stale's expected value is computed from its timestamps and is not 0d ($PC_P_STALE_EXP)" $?
[ "$(pc_mtime "$PC_ROOT/p-fresh/PLAN.md")" -ge "$(pc_last_commit "$PC_ROOT/p-fresh")" ]
check "plan: fixture - p-fresh's PLAN.md is newer than its last commit" $?
if [ -f "$PC_ROOT/p-lower/PLAN.md" ]; then PC_LOWER_ARMED=0; else PC_LOWER_ARMED=1; fi
check "plan: fixture - the case trap is armed here - [ -f PLAN.md ] is true for plan.md (case-insensitive fs)" "$PC_LOWER_ARMED"
ls -1 "$PC_ROOT/p-lower" | grep -qx 'PLAN.md'; PC_rc=$?
[ "$PC_rc" -ne 0 ]; check "plan: fixture - a byte-exact directory listing does NOT see PLAN.md in p-lower" $?
PC_TABLE="$(pc_board)"
PC_N_REPOS="$(printf '%s\n' "$PC_TABLE" | sed -n 's/^BOARD .*(\([0-9][0-9]*\) repo).*/\1/p' | head -1)"
[ "${PC_N_REPOS:-0}" -eq 5 ]; check "plan: fixture - board scanned the 5 fixture repos (header says ${PC_N_REPOS:-nothing})" $?
# --- 1. Three different values from three repos ----------------------------
printf '%s\n' "$PC_TABLE" | grep -q '^REPO .* PLAN '
check "plan 1: the table header carries a PLAN column" $?
[ "$(pc_table_cell "$PC_TABLE" p-none)" = "-" ]; check "plan 1: no root PLAN.md -> '-'" $?
[ "$(pc_table_cell "$PC_TABLE" p-fresh)" = "0d" ]; check "plan 1: PLAN.md newer than the last commit -> '0d'" $?
[ "$(pc_table_cell "$PC_TABLE" p-stale)" = "$PC_P_STALE_EXP" ]
check "plan 1: PLAN.md older than the last commit -> '$PC_P_STALE_EXP' (from the fixture's own timestamps)" $?
PC_V3="$(pc_table_cell "$PC_TABLE" p-none) $(pc_table_cell "$PC_TABLE" p-fresh) $(pc_table_cell "$PC_TABLE" p-stale)"
[ "$(printf '%s\n' $PC_V3 | sort -u | wc -l | tr -d ' ')" -eq 3 ]
check "plan 1: the three fixtures show three DIFFERENT values ($PC_V3)" $?
# --- 2. Wrong place and wrong case are not a plan ---------------------------
[ "$(pc_table_cell "$PC_TABLE" p-docs)" = "-" ]; check "plan 2: docs/PLAN.md only -> '-'" $?
[ "$(pc_table_cell "$PC_TABLE" p-lower)" = "-" ]; check "plan 2: lowercase plan.md in the root -> '-' (byte-exact name)" $?
# --- 3. --row carries plan= with the table's value ---------------------------
for PC_r in p-none p-fresh p-stale p-docs p-lower; do
PC_RV="$(pc_row_plan "$(pc_board --row "$PC_r")")"
PC_TV="$(pc_table_cell "$PC_TABLE" "$PC_r")"
[ -n "$PC_RV" ] && [ "$PC_RV" = "$PC_TV" ]
check "plan 3: --row $PC_r has plan=${PC_RV:-<missing>} equal to the table cell (${PC_TV:-<missing>})" $?
done
[ "$(pc_board --row p-none | grep -c '^plan=')" -eq 1 ]
check "plan 3: --row emits exactly one plan= line" $?
[ "$(pc_board --row p-none | tail -1 | cut -d= -f1)" = "neste" ]
check "plan 3: neste= is still the last --row line" $?
# --- 4. Not in the sort, not in --plan blocks, not summed --------------------
# Two repos identical in every sorted property; run 1 gives s-a the stale plan
# and s-b the fresh one, run 2 swaps them. Any sort key built on PLAN would
# reverse them in one of the two runs.
PC_SROOT="$(mktemp -d)"
for PC_r in s-a s-b; do
mkrepo "$PC_SROOT/$PC_r"; pc_state "$PC_SROOT/$PC_r" "$PC_r" planned
echo "<!-- route: path=known; verification=strong; reversibility=cheap; scope=local; rationale=x -->" >> "$PC_SROOT/$PC_r/STATE.md"
: > "$PC_SROOT/$PC_r/PLAN.md"
pc_commit_at "$PC_SROOT/$PC_r" $((PC_NOW - PC_DAY)) init
pc_touch_at "$PC_SROOT/$PC_r/STATE.md" $((PC_NOW - 2 * PC_DAY))
done
pc_touch_at "$PC_SROOT/s-a/PLAN.md" $((PC_NOW - 9 * PC_DAY)); pc_touch_at "$PC_SROOT/s-b/PLAN.md" "$PC_NOW"
PC_T1="$(CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_SROOT" 2>/dev/null)"; PC_P1="$(CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_SROOT" --plan 2>/dev/null)"
pc_touch_at "$PC_SROOT/s-a/PLAN.md" "$PC_NOW"; pc_touch_at "$PC_SROOT/s-b/PLAN.md" $((PC_NOW - 9 * PC_DAY))
PC_T2="$(CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_SROOT" 2>/dev/null)"; PC_P2="$(CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_SROOT" --plan 2>/dev/null)"
PC_A1="$(pc_table_cell "$PC_T1" s-a)"; PC_A2="$(pc_table_cell "$PC_T2" s-a)"
[ -n "$PC_A1" ] && [ -n "$PC_A2" ] && [ "$PC_A1" != "$PC_A2" ]
check "plan 4: precondition - the swap really changed s-a's PLAN value (${PC_A1:-<missing>} -> ${PC_A2:-<missing>})" $?
PC_O1="$(printf '%s\n' "$PC_T1" | awk '$1 ~ /^s-/ {print $1}' | tr '\n' ' ')"
PC_O2="$(printf '%s\n' "$PC_T2" | awk '$1 ~ /^s-/ {print $1}' | tr '\n' ' ')"
[ -n "$PC_A1" ] && [ "$PC_O1" = "$PC_O2" ]; check "plan 4: table order is identical across the swap ($PC_O1| $PC_O2)" $?
PC_Q1="$(printf '%s\n' "$PC_P1" | sed -n 's/^repo=//p' | tr '\n' ' ')"
PC_Q2="$(printf '%s\n' "$PC_P2" | sed -n 's/^repo=//p' | tr '\n' ' ')"
[ -n "$PC_A1" ] && [ -n "$PC_Q1" ] && [ "$PC_Q1" = "$PC_Q2" ]; check "plan 4: --plan order is identical across the swap ($PC_Q1| $PC_Q2)" $?
if printf '%s\n' "$PC_P1" | grep -qi '^plan='; then PC_rc=1; else PC_rc=0; fi
[ -n "$PC_A1" ] && [ "$PC_rc" -eq 0 ]; check "plan 4: --plan blocks carry no plan= key" $?
PC_S1="$(printf '%s\n' "$PC_T1" | grep '^SUM:')"; PC_S2="$(printf '%s\n' "$PC_T2" | grep '^SUM:')"
if printf '%s' "$PC_S1" | grep -qi 'plan'; then PC_rc=1; else PC_rc=0; fi
[ -n "$PC_A1" ] && [ "$PC_rc" -eq 0 ] && [ "$PC_S1" = "$PC_S2" ]; check "plan 4: the SUM line neither mentions PLAN nor moves with it" $?
/bin/rm -rf "$PC_SROOT" 2>/dev/null
# --- 5. Replay: the shape C failed on ----------------------------------------
# Many commits, no root PLAN.md, the destination written in docs/*.local.md,
# and a release tag late in the window. A heuristic keyed on "activity without
# a release" went silent at the tag; this column must read '-' at every step.
PC_LROOT="$(mktemp -d)"
PC_L="$PC_LROOT/loop-repo"
mkrepo "$PC_L"; pc_state "$PC_L" loop-repo in-progress
mkdir -p "$PC_L/docs"; echo "destination lives here" > "$PC_L/docs/destination.local.md"
PC_BASE=$((PC_NOW - 60 * PC_DAY)); PC_HITS=0; PC_STEPS=0
PC_i=0
while [ "$PC_i" -lt 6 ]; do
echo "step $PC_i" > "$PC_L/work.txt"
pc_commit_at "$PC_L" $((PC_BASE + PC_i * 10 * PC_DAY)) "step $PC_i"
[ "$PC_i" -eq 4 ] && git -C "$PC_L" -c tag.gpgSign=false tag v9.9.0 >/dev/null 2>&1
PC_LT="$(CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_LROOT" 2>/dev/null)"
PC_STEPS=$((PC_STEPS + 1))
[ "$(pc_table_cell "$PC_LT" loop-repo)" = "-" ] && PC_HITS=$((PC_HITS + 1))
PC_i=$((PC_i + 1))
done
[ -n "$(git -C "$PC_L" tag -l v9.9.0)" ]; check "plan 5: fixture - the release tag exists in the replay" $?
[ "$PC_HITS" -eq "$PC_STEPS" ]; check "plan 5: '-' at every step of the replay, before and after the tag ($PC_HITS of $PC_STEPS)" $?
/bin/rm -rf "$PC_LROOT" 2>/dev/null
# --- 6. Denominator: one value per scanned repo ------------------------------
PC_WITH=0
for PC_r in p-none p-fresh p-stale p-docs p-lower; do
[ -n "$(pc_table_cell "$PC_TABLE" "$PC_r")" ] && PC_WITH=$((PC_WITH + 1))
done
# A non-empty, non-zero denominator first: with an empty scan both sides are 0
# and the comparison alone would pass on nothing.
[ "${PC_N_REPOS:-0}" -gt 0 ] && [ "$PC_WITH" -eq "$PC_N_REPOS" ]
check "plan 6: every scanned repo has a PLAN value ($PC_WITH of ${PC_N_REPOS:-?})" $?
printf '%s\n' "$PC_TABLE" | grep -q '^undersoekt: '
check "plan 6: the scan's own denominator line is still printed" $?
# --- 7. Legend -----------------------------------------------------------------
PC_LEG="$(printf '%s\n' "$PC_TABLE" | awk '/^PLAN = /{on=1} on && /^[A-Z]+ = / && !/^PLAN = /{on=0} on')"
[ -n "$PC_LEG" ]; check "plan 7: the legend has a 'PLAN = ' entry" $?
printf '%s' "$PC_LEG" | grep -q 'IKKE' && printf '%s' "$PC_LEG" | grep -qi 'felle'
check "plan 7: the legend says the column does NOT judge whether the criterion can be falsified" $?
printf '%s' "$PC_LEG" | grep -q 'touch' && printf '%s' "$PC_LEG" | grep -q 'checkout'
check "plan 7: the legend says touch/checkout reset the mtime" $?
if printf '%s' "$PC_LEG" | LC_ALL=C grep -q '[^ -~]'; then PC_rc=1; else PC_rc=0; fi
[ -n "$PC_LEG" ] && [ "$PC_rc" -eq 0 ]; check "plan 7: the legend is ASCII like the rest of the table" $?
/bin/rm -rf "$PC_ROOT" "$PC_COORD" 2>/dev/null
echo ""
echo "board-selftest: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1

View file

@ -9,6 +9,9 @@
# STATE.md board line - optional machine-readable field (see below)
# git status --porcelain - uncommitted risk
# git log -1 --format=%ct - when anything last landed (the SISTE column)
# <repo>/PLAN.md mtime - the PLAN column: existence (byte-exact name) and
# days behind the last commit. Never its content.
# DISPLAY only and not in the sort.
# ~/.claude/coord/<repo>/orders - PENDING orders (the ORDRE column).
# Rendered N:Md - the count AND the age of the
# OLDEST order, never the newest and never a mean:
@ -820,13 +823,38 @@ printf '%s\n' "$REPOS" | while IFS= read -r d; do
lastd=-1; lastcol="-"
fi
# PLAN: does this repo keep a destination where sessions actually look, and
# is anyone touching it? A FILESYSTEM reading only - PLAN.md's content is
# never read, so no wording inside it can move this cell. `-` = no file
# named exactly PLAN.md in the root; `Nd` = whole days its mtime lies behind
# the last commit (0d when it is as new or newer). The name is matched BYTE
# FOR BYTE through a glob, never by `[ -f PLAN.md ]` alone: APFS is
# case-insensitive by default, so that test is true for a `plan.md` and
# would report a plan that is not there. A glob matches the names readdir
# returns, as stored. A PLAN.md with no commit to compare against, or an
# unreadable mtime, is `?` - never `-`, which means "no plan", and never
# `0d`, which would be a measurement that did not happen.
# Display only: not in any sort key, not in --plan, not in SUM.
plancol="-"
for pl_f in "$d"/PLAN.m[d]; do
[ "${pl_f##*/}" = "PLAN.md" ] && [ -f "$pl_f" ] || continue
pl_m="$(stat -f %m "$pl_f" 2>/dev/null)"
if [ -z "$pl_m" ] || [ -z "$lastct" ]; then
plancol="?"
elif [ "$pl_m" -ge "$lastct" ]; then
plancol="0d"
else
plancol="$(( (lastct - pl_m) / 86400 ))d"
fi
done
if [ ! -f "$state" ]; then
# No plan file, so no plan age: ALDER is blank rather than quietly showing
# the commit age under a heading that means something else everywhere else
# in the table. The sort key keeps using it - order is unchanged.
printf '5|%06d|%s|-|-|%s|%s|-|%s|%s|-|%s|%s|%s|%s|(ingen STATE.md)\n' \
printf '5|%06d|%s|-|-|%s|%s|-|%s|%s|-|%s|%s|%s|%s|-|%s|(ingen STATE.md)\n' \
"$lastd" "$name" "$inbox" "$dirty" "$lastcol" "$d" \
"$orders" "$ordersage" "$claimed" "$claimedage"
"$orders" "$ordersage" "$claimed" "$claimedage" "$plancol"
continue
fi
@ -918,10 +946,11 @@ printf '%s\n' "$REPOS" | while IFS= read -r d; do
# `voy` sits between claimedage and neste, never after it: `neste` is free
# prose lifted out of a STATE.md and may contain a literal '|', so it has to
# stay the last field on the line.
# `plan` sits between voy and neste for the same reason.
voy="$(voyage_cell "$d")"
printf '%s|%06d|%s|%s|%s|%s|%s|%sd|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' \
printf '%s|%06d|%s|%s|%s|%s|%s|%sd|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' \
"$bucket" "$age" "$name" "$disp" "$cost" "$inbox" "$dirty" "$age" "$lastcol" "$d" \
"${blockedon:--}" "$orders" "$ordersage" "$claimed" "$claimedage" "$voy" "$neste"
"${blockedon:--}" "$orders" "$ordersage" "$claimed" "$claimedage" "$voy" "$plancol" "$neste"
done > "${TMPDIR:-/tmp}/board.$$"
RECORDS="${TMPDIR:-/tmp}/board.$$"
@ -1078,20 +1107,20 @@ awk -F'|' '$4 ~ /^blocked/ && $11 != "-" && $11 != "" {print $11}' "$RECORDS" \
hdr() {
printf '\n%s\n' "$1"
printf '%-32s %-34s %-14s %4s %7s %7s %7s %4s %6s %6s %s\n' \
"REPO" "STATUS" "KOST" "INN" "ORDRE" "FLY" "VOY" "DRT" "ALDER" "SISTE" "NESTE"
printf '%-32s %-34s %-14s %4s %7s %7s %7s %4s %6s %6s %5s %s\n' \
"REPO" "STATUS" "KOST" "INN" "ORDRE" "FLY" "VOY" "DRT" "ALDER" "SISTE" "PLAN" "NESTE"
}
rows() {
awk -F'|' -v b="$1" '$1==b' "$RECORDS" | sort -t'|' -k2,2n | \
while IFS='|' read -r bucket sortkey name status cost inbox dirty age last dir blockedon orders ordersage claimed claimedage voy neste; do
while IFS='|' read -r bucket sortkey name status cost inbox dirty age last dir blockedon orders ordersage claimed claimedage voy plan neste; do
# `N:age` only when there is something to date. An empty queue prints a
# bare 0, never "0:0d" - absence must not borrow the shape of a
# measurement.
o_cell="$orders"; [ "$ordersage" = "-" ] || o_cell="$orders:$ordersage"
c_cell="$claimed"; [ "$claimedage" = "-" ] || c_cell="$claimed:$claimedage"
printf '%-32s %-34s %-14s %4s %7s %7s %7s %4s %6s %6s %s\n' \
"$name" "$status" "$cost" "$inbox" "$o_cell" "$c_cell" "$voy" "$dirty" "$age" "$last" "$(trunc "$neste" "$NESTE_WIDTH")"
printf '%-32s %-34s %-14s %4s %7s %7s %7s %4s %6s %6s %5s %s\n' \
"$name" "$status" "$cost" "$inbox" "$o_cell" "$c_cell" "$voy" "$dirty" "$age" "$last" "$plan" "$(trunc "$neste" "$NESTE_WIDTH")"
done
}
@ -1281,7 +1310,7 @@ brief() {
FILENAME==OWF { ow[$1] = $2 + 0; next }
{ name = $3; owed = (name in ow) ? ow[name] : 0; if (owed > 0) print owed, $0 }
' "$OWED" "$RECORDS" | sort -t'|' -k1,1nr | \
while IFS='|' read -r owed bucket sortkey name status cost inbox dirty age last dir blockedon orders ordersage claimed claimedage voy neste; do
while IFS='|' read -r owed bucket sortkey name status cost inbox dirty age last dir blockedon orders ordersage claimed claimedage voy plan neste; do
printf ' %-32s INN %-4s %s\n' "$name" "$owed" "$status"
# Wrapped, not cut - the whole line is the point, but a 500-character one
# is unreadable in a file nobody watched being written. Locale is set for
@ -1409,7 +1438,7 @@ dispatch() {
# `neste` is the LAST record field and is taken as "field N onward". This
# index once read 14 and was never moved as fields were inserted before
# `neste`, gluing three other columns onto the prose - move it with the record.
d_neste="$(printf '%s' "$d_rec" | cut -d'|' -f17-)"
d_neste="$(printf '%s' "$d_rec" | cut -d'|' -f18-)"
[ -d "$d_dir" ] || { echo "board: the directory for $D_REPO does not exist: $d_dir" >&2; exit 2; }
# Rejected traits REFUSE. Degrading to a command without them is the worst
@ -1893,7 +1922,7 @@ plan() {
echo ""
pn=0
while IFS='|' read -r why bucket sortkey name status cost inbox dirty age last dir blockedon orders ordersage claimed claimedage voy neste; do
while IFS='|' read -r why bucket sortkey name status cost inbox dirty age last dir blockedon orders ordersage claimed claimedage voy plan neste; do
[ -n "$name" ] || continue
pn=$((pn + 1))
printf 'tab=%s\n' "$pn"
@ -1953,7 +1982,7 @@ inbox_plan() {
FILENAME==RCF {
name = $3
rc_bucket[name] = $1; rc_status[name] = $4; rc_cost[name] = $5
rc_dir[name] = $10; rc_neste[name] = $17
rc_dir[name] = $10; rc_neste[name] = $18
next
}
{
@ -2186,10 +2215,11 @@ row_one() {
r_fly="$(printf '%s' "$r_rec" | cut -d'|' -f14)"
r_flyage="$(printf '%s' "$r_rec" | cut -d'|' -f15)"
r_voy="$(printf '%s' "$r_rec" | cut -d'|' -f16)"
r_plan="$(printf '%s' "$r_rec" | cut -d'|' -f17)"
# `neste` is free prose and may contain a literal '|', which is why it is the
# last field of the record - so it is taken as "everything from 17 on", never
# as field 17.
r_neste="$(printf '%s' "$r_rec" | cut -d'|' -f17-)"
# last field of the record - so it is taken as "everything from 18 on", never
# as field 18.
r_neste="$(printf '%s' "$r_rec" | cut -d'|' -f18-)"
# The TOKEN, not the display string: the table renders a blocked repo as
# `blocked>target` because one column has to carry both, and this rendering
@ -2217,6 +2247,7 @@ row_one() {
echo "alder=$r_alder"
echo "siste=$r_siste"
echo "upushet=$r_unpushed"
echo "plan=$r_plan"
# LAST, and for the same reason it is last in the record: it is free prose
# lifted out of a STATE.md, so anything placed after it would be unreachable
# for a consumer reading from the end. Printed WHOLE - the 38-character cut
@ -2259,6 +2290,15 @@ echo " Baerer samme N:Md - alderen paa den ELDSTE claimen. Ingenting frigir
echo "VOY = Voyage-prosjekter (brief i omloep). N:Md = antall og alderen paa det"
echo " STALESTE prosjektet - dager siden NYESTE artefakt der, dvs. hvor lenge"
echo " ingenting har skjedd. Bart 0 = ingen prosjekter. Detaljer: --voyage."
# PLAN is a filesystem reading and nothing more. It shows WHERE a destination
# is missing or untouched; whether the criterion inside it can be falsified is
# a judgement no column can make, and the legend says so rather than letting
# `0d` read as "this repo has a good plan".
echo "PLAN = rot-PLAN.md: '-' = ingen fil som heter noyaktig PLAN.md. Nd = dager"
echo " planens mtime ligger bak siste commit (0d = like ny eller nyere)."
echo " '?' = plan uten commit aa sammenligne med. Bedommer IKKE om kriteriet"
echo " kan felles, og leser aldri innholdet. touch/checkout nullstiller mtime"
echo " (samme forbehold som ALDER). Kun visning: aldri sortert, aldri summert."
echo "ALDER = dager siden STATE.md endret. SISTE = dager siden siste commit."
[ "$(count 1)" -gt 0 ] && { hdr "BLOKKERT (venter paa ekstern avhengighet)"; rows 1; }

View file

@ -1,235 +0,0 @@
#!/bin/bash
# plan-column-eval.sh - the RED gate for board.sh's PLAN column, written
# before the column exists. It is NOT one of the five selftest suites and is
# deliberately not wired into `npm test`: a committed red check inside
# board-selftest.sh would turn the whole suite red on main and break the
# README number check, so "red for the right reason" could no longer be told
# apart from "red because something else broke". The session that builds the
# column moves these checks into board-selftest.sh (and updates the counts)
# once they are green.
#
# What the column is (operator decision 2026-09-17): a DISPLAY-ONLY reading of
# the filesystem, one value per scanned repo:
# - no file named exactly PLAN.md in the repo root
# Nd PLAN.md exists; N = whole days its mtime lies BEHIND the last commit
# (0d when PLAN.md is as new as, or newer than, the last commit)
# It never reads PLAN.md's content, never enters a sort, a --plan block or a
# sum, and does not judge whether a criterion can be falsified.
#
# Table cells are located by the header, never by a fixed offset: the column's
# position is the builder's choice. The locator splits header and row on runs
# of 2+ spaces, which relies on two properties the table already has - every
# cell is followed by at least two spaces, and NESTE (free prose) is last.
#
# ASCII only, bash 3.2 safe, never touches ~/repos or ~/.claude/coord.
set -u
export LC_ALL=C
DIR="$(cd "$(dirname "$0")" && pwd)"
BOARD="$DIR/board.sh"
ROOT="$(mktemp -d)"
CLAUDE_COORD_DIR="$(mktemp -d)"
export CLAUDE_COORD_DIR
cleanup() { /bin/rm -rf "$ROOT" "$CLAUDE_COORD_DIR" 2>/dev/null; }
trap cleanup EXIT
PASS=0; FAIL=0
check() { if [ "$2" -eq 0 ]; then PASS=$((PASS+1)); echo " ok - $1"; else FAIL=$((FAIL+1)); echo " FAIL - $1"; fi; }
NOW="$(date +%s)"
DAY=86400
mkrepo() { mkdir -p "$1" && git -C "$1" init -q 2>/dev/null; }
state() {
{
echo "# STATE - $2"
echo "## NESTE - START HER"
echo "<!-- board: status=$3; blocked-on=-; next-cost=Sonnet 5/high -->"
echo "Next step for $2."
} > "$1/STATE.md"
}
# commit_at <dir> <epoch> <msg>: commit everything with a fixed committer date.
commit_at() {
git -C "$1" add -A >/dev/null 2>&1
GIT_AUTHOR_DATE="@$2 +0000" GIT_COMMITTER_DATE="@$2 +0000" \
git -C "$1" -c user.email=t@t -c user.name=t commit -qm "$3" >/dev/null 2>&1
}
touch_at() { touch -t "$(date -r "$2" +%Y%m%d%H%M.%S)" "$1"; }
last_commit() { git -C "$1" log -1 --format=%ct 2>/dev/null; }
mtime() { stat -f %m "$1"; }
# table_cell <table-output> <repo>: the PLAN cell of <repo>'s row, read under
# the nearest preceding header line. Empty when the header has no PLAN column.
table_cell() {
printf '%s\n' "$1" | awk -v repo="$2" '
function split2(s, arr, n) { gsub(/ +/, "\t", s); sub(/^\t/, "", s); return split(s, arr, "\t") }
/^REPO / { n = split2($0, h); col = 0; for (i = 1; i <= n; i++) if (h[i] == "PLAN") col = i; next }
$1 == repo && col > 0 { split2($0, r); print r[col]; exit }'
}
row_plan() { printf '%s\n' "$1" | sed -n 's/^plan=//p'; }
board() { "$BOARD" --roots "$ROOT" "$@" 2>/dev/null; }
echo "plan-column-eval (root: $ROOT, mailbox: $CLAUDE_COORD_DIR)"
# --- Fixtures ---------------------------------------------------------------
# p-none: no PLAN.md anywhere.
mkrepo "$ROOT/p-none"; state "$ROOT/p-none" p-none planned
commit_at "$ROOT/p-none" $((NOW - 3 * DAY)) init
# p-fresh: PLAN.md newer than the last commit -> 0d. The file is EMPTY: a value
# computed from content could not produce 0d here, so this also shows that
# the column is a filesystem reading.
mkrepo "$ROOT/p-fresh"; state "$ROOT/p-fresh" p-fresh planned
commit_at "$ROOT/p-fresh" $((NOW - 2 * DAY)) init
: > "$ROOT/p-fresh/PLAN.md"
touch_at "$ROOT/p-fresh/PLAN.md" "$NOW"
# p-stale: PLAN.md 5 days and 3 hours older than the last commit -> 5d. Its
# content looks like a value on purpose ("PLAN=99d"); it must not be read.
mkrepo "$ROOT/p-stale"; state "$ROOT/p-stale" p-stale planned
printf '# PLAN\nPLAN=99d\n' > "$ROOT/p-stale/PLAN.md"
P_STALE_CT=$((NOW - 3600))
commit_at "$ROOT/p-stale" "$P_STALE_CT" init
touch_at "$ROOT/p-stale/PLAN.md" $((P_STALE_CT - 5 * DAY - 3 * 3600))
# p-docs: PLAN.md in docs/ only - the wrong place by convention.
mkrepo "$ROOT/p-docs"; state "$ROOT/p-docs" p-docs planned
mkdir -p "$ROOT/p-docs/docs"; echo "# PLAN" > "$ROOT/p-docs/docs/PLAN.md"
commit_at "$ROOT/p-docs" $((NOW - DAY)) init
# p-lower: lowercase plan.md in the root. On a case-insensitive filesystem
# (APFS default) `[ -f PLAN.md ]` is TRUE here, so a board that asks the
# filesystem alone would report a plan that is not there. The name has to be
# compared byte for byte against what the directory actually stores (e.g. a
# directory listing matched exactly against PLAN.md).
mkrepo "$ROOT/p-lower"; state "$ROOT/p-lower" p-lower planned
echo "# plan" > "$ROOT/p-lower/plan.md"
commit_at "$ROOT/p-lower" $((NOW - DAY)) init
# --- Ground truth for the fixtures (these must be green today) -------------
[ -z "$(git -C "$ROOT/p-stale" status --porcelain)" ]
check "fixture: p-stale is committed clean" $?
P_STALE_EXP="$(( ($(last_commit "$ROOT/p-stale") - $(mtime "$ROOT/p-stale/PLAN.md")) / DAY ))d"
[ "$P_STALE_EXP" != "0d" ]
check "fixture: p-stale's expected value is computed from its timestamps and is not 0d ($P_STALE_EXP)" $?
[ "$(mtime "$ROOT/p-fresh/PLAN.md")" -ge "$(last_commit "$ROOT/p-fresh")" ]
check "fixture: p-fresh's PLAN.md is newer than its last commit" $?
if [ -f "$ROOT/p-lower/PLAN.md" ]; then LOWER_ARMED=0; else LOWER_ARMED=1; fi
check "fixture: the case trap is armed here - [ -f PLAN.md ] is true for plan.md (case-insensitive fs)" "$LOWER_ARMED"
ls -1 "$ROOT/p-lower" | grep -qx 'PLAN.md'; rc=$?
[ "$rc" -ne 0 ]; check "fixture: a byte-exact directory listing does NOT see PLAN.md in p-lower" $?
TABLE="$(board)"
N_REPOS="$(printf '%s\n' "$TABLE" | sed -n 's/^BOARD .*(\([0-9][0-9]*\) repo).*/\1/p' | head -1)"
[ "${N_REPOS:-0}" -eq 5 ]; check "fixture: board scanned the 5 fixture repos (header says ${N_REPOS:-nothing})" $?
# --- 1. Three different values from three repos ----------------------------
printf '%s\n' "$TABLE" | grep -q '^REPO .* PLAN '
check "1: the table header carries a PLAN column" $?
[ "$(table_cell "$TABLE" p-none)" = "-" ]; check "1: no root PLAN.md -> '-'" $?
[ "$(table_cell "$TABLE" p-fresh)" = "0d" ]; check "1: PLAN.md newer than the last commit -> '0d'" $?
[ "$(table_cell "$TABLE" p-stale)" = "$P_STALE_EXP" ]
check "1: PLAN.md older than the last commit -> '$P_STALE_EXP' (from the fixture's own timestamps)" $?
V3="$(table_cell "$TABLE" p-none) $(table_cell "$TABLE" p-fresh) $(table_cell "$TABLE" p-stale)"
[ "$(printf '%s\n' $V3 | sort -u | wc -l | tr -d ' ')" -eq 3 ]
check "1: the three fixtures show three DIFFERENT values ($V3)" $?
# --- 2. Wrong place and wrong case are not a plan ---------------------------
[ "$(table_cell "$TABLE" p-docs)" = "-" ]; check "2: docs/PLAN.md only -> '-'" $?
[ "$(table_cell "$TABLE" p-lower)" = "-" ]; check "2: lowercase plan.md in the root -> '-' (byte-exact name)" $?
# --- 3. --row carries plan= with the table's value ---------------------------
for r in p-none p-fresh p-stale p-docs p-lower; do
RV="$(row_plan "$(board --row "$r")")"
TV="$(table_cell "$TABLE" "$r")"
[ -n "$RV" ] && [ "$RV" = "$TV" ]
check "3: --row $r has plan=${RV:-<missing>} equal to the table cell (${TV:-<missing>})" $?
done
[ "$(board --row p-none | grep -c '^plan=')" -eq 1 ]
check "3: --row emits exactly one plan= line" $?
[ "$(board --row p-none | tail -1 | cut -d= -f1)" = "neste" ]
check "3: neste= is still the last --row line" $?
# --- 4. Not in the sort, not in --plan blocks, not summed --------------------
# Two repos identical in every sorted property; run 1 gives s-a the stale plan
# and s-b the fresh one, run 2 swaps them. Any sort key built on PLAN would
# reverse them in one of the two runs.
SROOT="$(mktemp -d)"
for r in s-a s-b; do
mkrepo "$SROOT/$r"; state "$SROOT/$r" "$r" planned
echo "<!-- route: path=known; verification=strong; reversibility=cheap; scope=local; rationale=x -->" >> "$SROOT/$r/STATE.md"
: > "$SROOT/$r/PLAN.md"
commit_at "$SROOT/$r" $((NOW - DAY)) init
touch_at "$SROOT/$r/STATE.md" $((NOW - 2 * DAY))
done
touch_at "$SROOT/s-a/PLAN.md" $((NOW - 9 * DAY)); touch_at "$SROOT/s-b/PLAN.md" "$NOW"
T1="$("$BOARD" --roots "$SROOT" 2>/dev/null)"; P1="$("$BOARD" --roots "$SROOT" --plan 2>/dev/null)"
touch_at "$SROOT/s-a/PLAN.md" "$NOW"; touch_at "$SROOT/s-b/PLAN.md" $((NOW - 9 * DAY))
T2="$("$BOARD" --roots "$SROOT" 2>/dev/null)"; P2="$("$BOARD" --roots "$SROOT" --plan 2>/dev/null)"
A1="$(table_cell "$T1" s-a)"; A2="$(table_cell "$T2" s-a)"
[ -n "$A1" ] && [ -n "$A2" ] && [ "$A1" != "$A2" ]
check "4: precondition - the swap really changed s-a's PLAN value (${A1:-<missing>} -> ${A2:-<missing>})" $?
O1="$(printf '%s\n' "$T1" | awk '$1 ~ /^s-/ {print $1}' | tr '\n' ' ')"
O2="$(printf '%s\n' "$T2" | awk '$1 ~ /^s-/ {print $1}' | tr '\n' ' ')"
[ -n "$A1" ] && [ "$O1" = "$O2" ]; check "4: table order is identical across the swap ($O1| $O2)" $?
Q1="$(printf '%s\n' "$P1" | sed -n 's/^repo=//p' | tr '\n' ' ')"
Q2="$(printf '%s\n' "$P2" | sed -n 's/^repo=//p' | tr '\n' ' ')"
[ -n "$A1" ] && [ -n "$Q1" ] && [ "$Q1" = "$Q2" ]; check "4: --plan order is identical across the swap ($Q1| $Q2)" $?
if printf '%s\n' "$P1" | grep -qi '^plan='; then rc=1; else rc=0; fi
[ -n "$A1" ] && [ "$rc" -eq 0 ]; check "4: --plan blocks carry no plan= key" $?
S1="$(printf '%s\n' "$T1" | grep '^SUM:')"; S2="$(printf '%s\n' "$T2" | grep '^SUM:')"
if printf '%s' "$S1" | grep -qi 'plan'; then rc=1; else rc=0; fi
[ -n "$A1" ] && [ "$rc" -eq 0 ] && [ "$S1" = "$S2" ]; check "4: the SUM line neither mentions PLAN nor moves with it" $?
/bin/rm -rf "$SROOT" 2>/dev/null
# --- 5. Replay: the shape C failed on ----------------------------------------
# Many commits, no root PLAN.md, the destination written in docs/*.local.md,
# and a release tag late in the window. A heuristic keyed on "activity without
# a release" went silent at the tag; this column must read '-' at every step.
LROOT="$(mktemp -d)"
L="$LROOT/loop-repo"
mkrepo "$L"; state "$L" loop-repo in-progress
mkdir -p "$L/docs"; echo "destination lives here" > "$L/docs/destination.local.md"
BASE=$((NOW - 60 * DAY)); HITS=0; STEPS=0
i=0
while [ "$i" -lt 6 ]; do
echo "step $i" > "$L/work.txt"
commit_at "$L" $((BASE + i * 10 * DAY)) "step $i"
[ "$i" -eq 4 ] && git -C "$L" -c tag.gpgSign=false tag v9.9.0 >/dev/null 2>&1
LT="$("$BOARD" --roots "$LROOT" 2>/dev/null)"
STEPS=$((STEPS + 1))
[ "$(table_cell "$LT" loop-repo)" = "-" ] && HITS=$((HITS + 1))
i=$((i + 1))
done
[ -n "$(git -C "$L" tag -l v9.9.0)" ]; check "5: fixture - the release tag exists in the replay" $?
[ "$HITS" -eq "$STEPS" ]; check "5: '-' at every step of the replay, before and after the tag ($HITS of $STEPS)" $?
/bin/rm -rf "$LROOT" 2>/dev/null
# --- 6. Denominator: one value per scanned repo ------------------------------
WITH=0
for r in p-none p-fresh p-stale p-docs p-lower; do
[ -n "$(table_cell "$TABLE" "$r")" ] && WITH=$((WITH + 1))
done
# A non-empty, non-zero denominator first: with an empty scan both sides are 0
# and the comparison alone would pass on nothing.
[ "${N_REPOS:-0}" -gt 0 ] && [ "$WITH" -eq "$N_REPOS" ]
check "6: every scanned repo has a PLAN value ($WITH of ${N_REPOS:-?})" $?
printf '%s\n' "$TABLE" | grep -q '^undersoekt: '
check "6: the scan's own denominator line is still printed" $?
# --- 7. Legend -----------------------------------------------------------------
LEG="$(printf '%s\n' "$TABLE" | awk '/^PLAN = /{on=1} on && /^[A-Z]+ = / && !/^PLAN = /{on=0} on')"
[ -n "$LEG" ]; check "7: the legend has a 'PLAN = ' entry" $?
printf '%s' "$LEG" | grep -q 'IKKE' && printf '%s' "$LEG" | grep -qi 'felle'
check "7: the legend says the column does NOT judge whether the criterion can be falsified" $?
printf '%s' "$LEG" | grep -q 'touch' && printf '%s' "$LEG" | grep -q 'checkout'
check "7: the legend says touch/checkout reset the mtime" $?
if printf '%s' "$LEG" | LC_ALL=C grep -q '[^ -~]'; then rc=1; else rc=0; fi
[ -n "$LEG" ] && [ "$rc" -eq 0 ]; check "7: the legend is ASCII like the rest of the table" $?
echo ""
echo "plan-column-eval: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1
exit 0