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

@ -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; }