feat(board): render the day plan as a third rendering of the same scan

The table says what the state of every repo is; the briefing says who is
waiting. Neither says which repos to open a tab for today, in what order,
and with which command - so a day plan was assembled by hand every morning
from a table that deliberately takes no position.

--plan is built on exactly the argument --brief was: a lookup over data the
scan already holds, zero model calls, and route.sh already derives the
per-repo command. The order IS the position it takes, and the only one -
there is no cutoff, so nothing is hidden.

The rule most likely to be "fixed" into a defect: repos owing mail rank
first regardless of status. Excluding blocked or done is a claim about a
repo's OWN next step, which by definition cannot be moved, while owing an
answer is the other axis entirely - and answering is often what unblocks
it. Measured on the real tree, two of 26 planned repos were done with an
unhandled inbox, so the fixture tree pins done-with-debt and
blocked-with-debt rather than the general rule alone. Repos with no board
line rank last and are LABELLED, because the table already prints a note
about them and dropping them silently would repeat that defect.

key=value rather than prose, because the plan has two consumers: the
operator, and a separate repo driving a terminal from it. Prose would make
the rendered format an API no test here could hold stable for a consumer
living in another repo. dir= is absolute because a new pane inherits its
anchor's directory; command_missing= carries both no-command causes,
because a driver reading ^command= would type a blank line into a live pane.

route_cmd_for() becomes the single reader of the route-line grammar, shared
with --brief, distinguishing the two failure causes by exit code rather
than by an empty string. --brief output is unchanged.

Driving a terminal from the plan stays OUT of this repo, on the measurement
in docs/ghostty-orchestration-measurement.md rather than on taste.

board-selftest: 51 -> 86 checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011eKRzhD1RqY2MxvDEkrJAM
This commit is contained in:
Kjell Tore Guttormsen 2026-08-02 06:40:22 +02:00
commit 6d0fe14d0f
10 changed files with 485 additions and 31 deletions

View file

@ -428,6 +428,199 @@ CLAUDE_BRIEF_FILE="$OUTFILE" BOARD_ROOTS="$ROOT/does-not-exist" \
grep -q 'FORRIGE BRIEFING' "$OUTFILE"
check "empty render never overwrites the previous briefing" $?
# --- 10. Day-plan rendering (--plan) ---------------------------------------
# A THIRD rendering of the same scan, on exactly the argument --brief was built
# on. The table answers "what is the state of every repo"; the briefing answers
# "who is waiting on me"; the plan answers "which repos do I open a tab for
# today, in what order, with which command". It has TWO consumers - the operator
# pasting commands, and a separate driver repo consuming the plan - which is why
# it renders key=value blocks rather than prose: prose would make the FORMAT an
# API that no test in this repo could hold stable.
#
# repo-blocked-owes: blocked AND owing mail. The exclusion of `blocked` is about
# a repo's OWN next step, which by definition cannot be moved; answering a
# message is a different axis, and is often precisely what unblocks it. Without
# this fixture the debt-first rule and the status exclusion never meet, and
# whichever one was written second would silently win.
mkrepo "$ROOT/repo-blocked-owes"
{
echo "# STATE - repo-blocked-owes"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=blocked; blocked-on=repo-a; next-cost=Opus 5/high -->"
printf 'Venter p%s repo-a, men skylder to svar.\n' "$EMDASH"
} > "$ROOT/repo-blocked-owes/STATE.md"
mkdir -p "$CLAUDE_COORD_DIR/repo-blocked-owes/inbox"
for n in 1 2; do
echo "msg" > "$CLAUDE_COORD_DIR/repo-blocked-owes/inbox/2026-msg$n-from-w.md"
done
# repo-done-owes: done AND owing mail. Same rule as repo-blocked-owes, but this
# is the shape that actually occurs - measured on the real tree at 0.16.0, two
# of the 26 planned repos were `done` with an unhandled inbox, and none were
# blocked-with-debt. A repo whose own work is finished can still owe an answer.
mkrepo "$ROOT/repo-done-owes"
{
echo "# STATE - repo-done-owes"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=done; blocked-on=-; next-cost=Sonnet 5/high -->"
printf 'Arbeidet er ferdig %s men innboksen er ikke gjort opp.\n' "$EMDASH"
} > "$ROOT/repo-done-owes/STATE.md"
mkdir -p "$CLAUDE_COORD_DIR/repo-done-owes/inbox"
echo "msg" > "$CLAUDE_COORD_DIR/repo-done-owes/inbox/2026-msg1-from-w.md"
PLAN="$("$BOARD" --roots "$ROOT" --plan 2>/dev/null)"; rc=$?
check "--plan exits 0" "$rc"
printf '%s' "$PLAN" | grep -q '^# PLAN '
check "plan carries a header naming what it is" $?
# Machine-readable by construction: the driver repo reads these keys.
printf '%s' "$PLAN" | grep -q '^tab=1$'
check "plan numbers tabs starting at 1" $?
# Debt outranks everything, most-owed first. repo-a owes 3, repo-blocked-owes 2,
# repo-owes and repo-typo 1 each.
[ "$(printf '%s\n' "$PLAN" | grep -A1 '^tab=1$' | grep '^repo=' | cut -d= -f2)" = "repo-a" ]
check "the repo owing the most messages is tab 1" $?
n_a="$(printf '%s\n' "$PLAN" | grep -n '^repo=repo-a$' | cut -d: -f1)"
n_bo="$(printf '%s\n' "$PLAN" | grep -n '^repo=repo-blocked-owes$' | cut -d: -f1)"
n_ow="$(printf '%s\n' "$PLAN" | grep -n '^repo=repo-owes$' | cut -d: -f1)"
[ -n "$n_a" ] && [ -n "$n_bo" ] && [ -n "$n_ow" ] \
&& [ "$n_a" -lt "$n_bo" ] && [ "$n_bo" -lt "$n_ow" ]
check "debt group is ordered by message count, descending" $?
# The status exclusion is about a repo's own next step, never about its debt.
printf '%s' "$PLAN" | grep -q '^repo=repo-blocked-owes$'
check "a BLOCKED repo that owes mail is still planned (debt is the other axis)" $?
printf '%s\n' "$PLAN" | grep -A4 '^repo=repo-blocked-owes$' | grep -q '^why=inbox:2$'
check "a blocked repo in the plan says debt, not status, is why it is there" $?
# Debt before live work: repo-h is in-progress and owes nothing.
n_h="$(printf '%s\n' "$PLAN" | grep -n '^repo=repo-h$' | cut -d: -f1)"
[ -n "$n_h" ] && [ "$n_ow" -lt "$n_h" ]
check "every repo owing mail is planned before any that owes none" $?
# repo-a is BOTH in-progress and owing. It must appear exactly once - a repo
# listed twice is two tabs for one repo, which is the plan failing at its job.
[ "$(printf '%s\n' "$PLAN" | grep -c '^repo=repo-a$')" -eq 1 ]
check "a repo qualifying under two rules is planned exactly once" $?
printf '%s\n' "$PLAN" | grep -A3 '^repo=repo-a$' | grep -q '^why=inbox:3$'
check "why= names the rule that put the repo in the plan" $?
# in-progress outranks planned among the repos owing nothing.
n_g="$(printf '%s\n' "$PLAN" | grep -n '^repo=repo-g$' | cut -d: -f1)"
[ -n "$n_g" ] && [ "$n_h" -lt "$n_g" ]
check "live work (in-progress) is planned before merely planned work" $?
# Excluded classes. A tab for a done or deferred repo is noise; a tab for a
# blocked repo that owes nothing cannot be moved at all.
printf '%s' "$PLAN" | grep -q '^repo=repo-wt$'; [ $? -ne 0 ]
check "plan excludes a done repo that owes nothing" $?
# ...but the exclusion is conditional on owing nothing, and the header says so.
# Read the other way, the plan's own header would call these entries a bug.
printf '%s' "$PLAN" | grep -q '^repo=repo-done-owes$'
check "a DONE repo that owes mail is still planned" $?
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" $?
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 ]
check "plan excludes a blocked repo that owes nothing" $?
printf '%s' "$PLAN" | grep -q '^repo=repo-c$'; [ $? -ne 0 ]
check "plan excludes a repo with no STATE.md and no debt" $?
# A repo with no board line has no declared next step, but dropping it SILENTLY
# is what the table's own MERK line exists to prevent. It is planned last and
# labelled, never omitted.
printf '%s' "$PLAN" | grep -q '^repo=repo-b$'
check "a repo lacking a board line is planned, not silently dropped" $?
printf '%s\n' "$PLAN" | grep -A3 '^repo=repo-b$' | grep -q '^why=uavklart$'
check "a repo with unknown status is labelled uavklart, not guessed into a rule" $?
n_b="$(printf '%s\n' "$PLAN" | grep -n '^repo=repo-b$' | cut -d: -f1)"
[ -n "$n_b" ] && [ "$n_g" -lt "$n_b" ]
check "uavklart repos rank below every repo that declared a status" $?
# The command comes from route.sh, same single copy of the rubric the briefing
# uses - advisor flag included, since that is a property of the ROW.
printf '%s' "$PLAN" | grep -q '^command=claude --model sonnet --effort high --advisor opus$'
check "plan derives the exact startup command from the repo's route line" $?
# Both no-command causes must degrade to a marker. A bare `command=` would be
# the shape of a runnable command carrying nothing - worse than none, because a
# driver reading `^command=` would type an empty line into a live pane.
printf '%s' "$PLAN" | grep -q '^command=$'; [ $? -ne 0 ]
check "plan never emits an empty command= line" $?
printf '%s\n' "$PLAN" | grep -A6 '^repo=repo-typo$' | grep -q '^command_missing='
check "unparseable route line degrades to command_missing=, not to a guess" $?
printf '%s\n' "$PLAN" | grep -A6 '^repo=repo-a$' | grep -q '^command_missing='
check "repo owing mail but lacking a route line is marked, never guessed at" $?
# Same argument as the briefing: the 38-char cut is the TABLE column's property.
printf '%s' "$PLAN" | grep -q '^neste=.*check-versions'
check "plan prints the full NESTE line, not the 38-char table excerpt" $?
# The driver cds into this, so a relative or missing path lands a session in the
# wrong repo - constraint 2 of the Ghostty measurement, in field form.
pdir="$(printf '%s\n' "$PLAN" | grep -m1 '^dir=' | cut -d= -f2-)"
[ -n "$pdir" ] && [ -d "$pdir" ] && [ "${pdir#/}" != "$pdir" ]
check "dir= is an absolute path that exists" $?
# Every block must be complete: a driver that reads a block missing dir= or
# command= has no safe default, and guessing one types into a live pane.
n_tab="$(printf '%s\n' "$PLAN" | grep -c '^tab=')"
n_rep="$(printf '%s\n' "$PLAN" | grep -c '^repo=')"
n_dir="$(printf '%s\n' "$PLAN" | grep -c '^dir=')"
n_why="$(printf '%s\n' "$PLAN" | grep -c '^why=')"
n_sta="$(printf '%s\n' "$PLAN" | grep -c '^status=')"
n_nes="$(printf '%s\n' "$PLAN" | grep -c '^neste=')"
n_cmd="$(printf '%s\n' "$PLAN" | grep -c '^command=')"
n_cms="$(printf '%s\n' "$PLAN" | grep -c '^command_missing=')"
[ "$n_tab" -gt 0 ] && [ "$n_rep" -eq "$n_tab" ] && [ "$n_dir" -eq "$n_tab" ] \
&& [ "$n_why" -eq "$n_tab" ] && [ "$n_sta" -eq "$n_tab" ] \
&& [ "$n_nes" -eq "$n_tab" ] && [ $(( n_cmd + n_cms )) -eq "$n_tab" ]
check "every planned block carries every field exactly once" $?
# Numbering must be dense: a driver opening tab 4 of 3 is a real failure mode.
seq_ok=0
printf '%s\n' "$PLAN" | grep '^tab=' | cut -d= -f2 > "$ROOT/tabs.txt"
# An empty tab list satisfies "every number is its index" vacuously, which is
# the check passing by finding nothing rather than by reading the numbering.
[ -s "$ROOT/tabs.txt" ] || seq_ok=1
i=0; while read -r t; do i=$((i+1)); [ "$t" = "$i" ] || seq_ok=1; done < "$ROOT/tabs.txt"
check "tab numbers are dense and consecutive" "$seq_ok"
# Read-only, same as every other rendering. Reading IS delivering in this
# engine, so a plan that consumed a backlog would be the core defect.
p4="$(snap "$CLAUDE_COORD_DIR")"
"$BOARD" --roots "$ROOT" --plan >/dev/null 2>&1
[ "$p4" = "$(snap "$CLAUDE_COORD_DIR")" ]
check "plan writes nothing to the mailbox (reading is delivering)" $?
# The scan and the mailbox are two populations. An orphan mailbox has no repo to
# cd into, so it cannot BE a tab - but dropping it makes the plan claim a
# completeness it knows it lacks. It is reported as commentary.
printf '%s' "$PLAN" | grep -q 'orphan-surface'
check "plan surfaces a pending mailbox with no repo in the scan tree" $?
printf '%s' "$PLAN" | grep -q '^repo=orphan-surface$'; [ $? -ne 0 ]
check "orphan mailbox is commentary, never a tab block (there is no dir to cd to)" $?
# Comment lines are '#'-prefixed so a consumer can drop them with one rule.
printf '%s\n' "$PLAN" | grep -vE '^#|^$|^[a-z_]+=' | grep -q .; [ $? -ne 0 ]
check "every plan line is a comment, a blank, or a key=value pair" $?
printf '%s' "$PLAN" | iconv -f UTF-8 -t UTF-8 >/dev/null 2>&1
check "plan output is valid UTF-8" $?
"$BOARD" --help 2>/dev/null | grep -q '\-\-plan'
check "--help documents --plan" $?
echo ""
echo "board-selftest: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1

View file

@ -65,7 +65,15 @@
# brief-nightly.sh, which renders to a temp file and renames it into place, and
# refuses to overwrite a good briefing with an empty render.
#
# Usage: board.sh [--roots <dir>[,<dir>...]] [--plain] [--brief]
# --plan is a THIRD rendering of that same scan, and the only one that takes a
# position: it answers which repos to open a tab for today and in what order.
# It prints key=value blocks, not prose, because it has two consumers - the
# operator pasting commands, and a separate repo driving a terminal from it.
# Ordering is deterministic (debt, then in-progress, then planned, then repos
# with no declared status) and there is no cutoff, so nothing is hidden.
# Read-only like the rest: --plan writes nothing, in the repo or the mailbox.
#
# Usage: board.sh [--roots <dir>[,<dir>...]] [--plain] [--brief|--plan]
# Env: CLAUDE_COORD_DIR overrides the mailbox root.
# BOARD_ROOTS overrides the default scan roots.
# ASCII only, bash 3.2 safe.
@ -76,6 +84,7 @@ COORD="${CLAUDE_COORD_DIR:-$HOME/.claude/coord}"
ROOTS="${BOARD_ROOTS:-$HOME/repos}"
NESTE_WIDTH=38
BRIEF=0
PLAN=0
# Sibling calculator, invoked rather than reimplemented: the rubric that turns
# four traits into a model has exactly one copy, and it is route.sh's row
@ -88,7 +97,9 @@ while [ $# -gt 0 ]; do
# bash 3.2: `shift 2` past the end of $# is a no-op -> would loop forever.
--roots) [ $# -ge 2 ] || { echo "board: --roots requires a value" >&2; exit 2; }
ROOTS="$2"; shift 2 ;;
--brief) BRIEF=1; shift ;;
# Three renderings of one scan, so exactly one may be selected: last wins.
--brief) BRIEF=1; PLAN=0; shift ;;
--plan) PLAN=1; BRIEF=0; shift ;;
--plain) shift ;;
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "board: unknown argument: $1 (ignored)" >&2; shift ;;
@ -261,24 +272,34 @@ rows() {
# next-cost printed and is told where the command comes from - a guessed
# command would read as authoritative while being a guess, which is worse than
# no command at all.
# Shared by --brief and --plan, because there is one route line grammar and it
# gets one reader. Two no-command cases, and callers must keep them apart:
# exit 1 - no route line at all
# exit 0, empty out - a route line route.sh rejects (a typo'd trait value)
# Neither may become a guessed command, and neither may become a bare command
# marker: a driver reading `command=` would type an empty line into a live pane.
route_cmd_for() {
rc_line="$(grep -m1 '^<!-- route:' "$1/STATE.md" 2>/dev/null)"
[ -n "$rc_line" ] || return 1
rc_p="$(printf '%s' "$rc_line" | sed -n 's/.*path=\([a-z-]*\).*/\1/p')"
rc_v="$(printf '%s' "$rc_line" | sed -n 's/.*verification=\([a-z-]*\).*/\1/p')"
rc_r="$(printf '%s' "$rc_line" | sed -n 's/.*reversibility=\([a-z-]*\).*/\1/p')"
rc_s="$(printf '%s' "$rc_line" | sed -n 's/.*scope=\([a-z-]*\).*/\1/p')"
bash "$ROUTE" --path "$rc_p" --verification "$rc_v" \
--reversibility "$rc_r" --scope "$rc_s" --rationale brief 2>/dev/null \
| sed -n 's/^command=//p'
return 0
}
brief_cmd() {
bc_state="$1/STATE.md"
bc_line="$(grep -m1 '^<!-- route:' "$bc_state" 2>/dev/null)"
if [ -z "$bc_line" ]; then
printf 'KOST: %s (ingen route-linje - kjor route-skillen i det repoet)' "$2"
return
fi
bc_p="$(printf '%s' "$bc_line" | sed -n 's/.*path=\([a-z-]*\).*/\1/p')"
bc_v="$(printf '%s' "$bc_line" | sed -n 's/.*verification=\([a-z-]*\).*/\1/p')"
bc_r="$(printf '%s' "$bc_line" | sed -n 's/.*reversibility=\([a-z-]*\).*/\1/p')"
bc_s="$(printf '%s' "$bc_line" | sed -n 's/.*scope=\([a-z-]*\).*/\1/p')"
bc_cmd="$(bash "$ROUTE" --path "$bc_p" --verification "$bc_v" \
--reversibility "$bc_r" --scope "$bc_s" --rationale brief 2>/dev/null \
| sed -n 's/^command=//p')"
if [ -n "$bc_cmd" ]; then
printf '$ %s' "$bc_cmd"
if bc_cmd="$(route_cmd_for "$1")"; then
if [ -n "$bc_cmd" ]; then
printf '$ %s' "$bc_cmd"
else
printf 'KOST: %s (route-linjen kunne ikke tolkes)' "$2"
fi
else
printf 'KOST: %s (route-linjen kunne ikke tolkes)' "$2"
printf 'KOST: %s (ingen route-linje - kjor route-skillen i det repoet)' "$2"
fi
}
@ -361,6 +382,110 @@ if [ "$BRIEF" -eq 1 ]; then
exit 0
fi
# --- Day-plan rendering (--plan) -------------------------------------------
# A THIRD rendering of the same scan, built on exactly the argument --brief was:
# it is a lookup over data the scan already holds, it costs zero model calls,
# and route.sh already derives the per-repo command. The table says what the
# state of every repo is; the briefing says who is waiting; the plan says which
# repos to open a tab for today, in what order, with which command.
#
# key=value blocks, not prose, because the plan has TWO consumers: the operator
# pasting commands, and a separate repo driving a terminal from it. Prose would
# make the rendered FORMAT an API, and no test in this repo could hold it stable
# for a consumer living in another one. Comment lines all start with '#', so a
# consumer drops them with one rule.
#
# ORDER IS THE POSITION THIS RENDERING TAKES, and it is the only one it takes -
# there is no cutoff, so nothing is hidden. Four rules, all deterministic over
# fields the scan already read:
# 1. INN > 0, most-owed first, WHATEVER the status. A message owed is an
# obligation to another session. Excluding `blocked` is about a repo's own
# next step, which by definition cannot be moved; answering is a different
# axis, and is frequently what unblocks it.
# 2. in-progress - live work, oldest plan first.
# 3. planned.
# 4. '?' and MALFORMED - no declared status. Planned LAST, and labelled. The
# table already prints a MERK line about repos with no board line; a plan
# that dropped them silently would repeat exactly that defect.
# Excluded: done, deferred, blocked-without-debt, and repos with no STATE.md and
# no debt - a tab that cannot be moved is not a plan entry.
plan_cmd() {
if pc_cmd="$(route_cmd_for "$1")"; then
if [ -n "$pc_cmd" ]; then
printf 'command=%s\n' "$pc_cmd"
else
printf 'command_missing=route-linjen kunne ikke tolkes (kost: %s)\n' "$2"
fi
else
printf 'command_missing=ingen route-linje - kjor route-skillen der (kost: %s)\n' "$2"
fi
}
plan() {
# Groups are appended in rank order to one file, then numbered in a single
# loop: `while ... < file` keeps the counter in THIS shell, where a pipe into
# while would run it in a subshell and reset every tab number to 1.
#
# Each group prefixes its `why` as a new FIRST field, so the sort keys shift
# by one: inbox 6->7, sortkey (ALDER) 2->3. Prefixing rather than appending is
# deliberate - `neste` is the last field and is free prose, so anything added
# after it could be swallowed by a stray separator in a STATE.md.
pf="${TMPDIR:-/tmp}/board-plan.$$"
: > "$pf"
awk -F'|' -v OFS='|' '$6+0 > 0 {print "inbox:" $6, $0}' "$RECORDS" \
| sort -t'|' -k7,7nr -k3,3n >> "$pf"
awk -F'|' -v OFS='|' '$6+0 == 0 && $4 == "in-progress" {print "in-progress", $0}' "$RECORDS" \
| sort -t'|' -k3,3n >> "$pf"
awk -F'|' -v OFS='|' '$6+0 == 0 && $4 == "planned" {print "planned", $0}' "$RECORDS" \
| sort -t'|' -k3,3n >> "$pf"
awk -F'|' -v OFS='|' '$6+0 == 0 && ($4 == "?" || $4 ~ /^MALFORMED:/) {print "uavklart", $0}' "$RECORDS" \
| sort -t'|' -k3,3n >> "$pf"
echo "# PLAN $(date '+%Y-%m-%d %H:%M') - en blokk per tab, i den rekkefolgen"
echo "# Kilder: STATE.md (NESTE + route-linje), git, coord-innboks. 0 modellkall."
echo "# Rekkefolge: innboksgjeld (INN desc), sa in-progress, sa planned, sa uavklart."
# "uten gjeld" governs the WHOLE list, not just the token nearest to it: a
# done or deferred repo that owes mail IS planned, and the real tree has two.
# Read the other way this line calls its own tab 4 a bug.
echo "# Utelatt naar repoet ikke skylder svar: done, deferred, blocked, uten STATE.md."
echo ""
pn=0
while IFS='|' read -r why bucket sortkey name status cost inbox dirty age last dir neste; do
[ -n "$name" ] || continue
pn=$((pn + 1))
printf 'tab=%s\n' "$pn"
printf 'repo=%s\n' "$name"
# Absolute, and the driver must cd into it explicitly: a new terminal pane
# inherits its anchor's working directory, so a plan that omitted this would
# look right and point at the wrong repo.
printf 'dir=%s\n' "$dir"
printf 'why=%s\n' "$why"
printf 'status=%s\n' "$status"
printf 'neste=%s\n' "$neste"
plan_cmd "$dir" "$cost"
echo ""
done < "$pf"
/bin/rm -f "$pf" 2>/dev/null
printf '# %s tabber.\n' "$pn"
# An orphan mailbox has no directory to cd into, so it cannot BE a tab - but
# omitting it lets the plan claim a completeness it knows it lacks. Reported
# as commentary, reusing the briefing's single copy of that cross-check.
po="$(brief_orphans)"
if [ -n "$po" ]; then
printf '%s\n' "$po" | sed -e 's/^/# /' -e 's/^# *$/#/'
fi
echo "# MERK: INN teller hva ANDRE venter paa fra deg. Hva et repo venter PAA"
echo "# staar kun i dets egen board-linje (blocked-on)."
}
if [ "$PLAN" -eq 1 ]; then
plan
exit 0
fi
count() { awk -F'|' -v b="$1" '$1==b' "$RECORDS" | wc -l | tr -d ' '; }
echo "BOARD - tverr-repo oppmerksomhetstavle ($(awk 'END{print NR}' "$RECORDS") repo)"