feat(board): rank --plan on one score, with transitive chain-root credit

blocked-on was scanned and then used for a single display string; it never
reached the ordering. A blocked chain's root could therefore rank below the
repos waiting on it - measured on the real tree, the root opened at tab 14
while the two repos it held up opened at tabs 5 and 8.

--plan now ranks on one score instead of four hard buckets, because four
groups cannot express "this repo owes one message and releases two others":

    40 x repos released transitively + 15 x inbox
    +10 in-progress / +5 planned / +2 undeclared, +3 for a Sonnet row

Chain-root credit follows blocked-on transitively to the first repo that is
not itself blocked and credits only that root. Opening a blocked repo
releases nobody - its own next step is by definition waiting. A cycle, a
blocked-on naming an unscanned repo, and a blocked repo with no target all
credit NOBODY: inventing a root there yields a plan that looks correct and
sends the operator to the wrong repo.

Debt is deliberately uncapped. A cap was proposed alongside the credit and
declined: owing a reply is the other axis from a repo's own next step, and
answering is often what unblocks a chain. The weight carries that decision -
15 per message exceeds the sum of every other bonus (13), so one message
more still outranks any combination of status and cost. At the originally
proposed 12 a tiebreak could flip the debt order, which is the cap arriving
through the back door.

blocked-on becomes a record field, inserted BEFORE neste so no existing field
index moves: neste is free prose lifted from a STATE.md and may carry a
literal '|' from a markdown table, so it stays last.

why= now names the dominant term rather than the first rule that matched, so
a block says what opening it would release instead of repeating inbox:N.

Format is otherwise unchanged for both declared consumers: tab=, repo=, dir=,
why=, status=, neste=, command=/command_missing=, paste= and the fokus_*
header all keep their names and positions.

board-selftest 118 -> 134 (chain, cycle, dangling-edge and format fixtures
written first, all failing before the change).
This commit is contained in:
Kjell Tore Guttormsen 2026-08-02 22:38:40 +02:00
commit e5e9cbe847
10 changed files with 404 additions and 51 deletions

View file

@ -54,7 +54,7 @@ marketplace plugin. Three components, one boundary:
- **Board (`scripts/board.sh`):** cross-repo attention board. Reads STATE.md
next-step blocks + board lines, `git status`, and mailbox pending counts, and
prints one line per repo. Read-only by construction: it writes to no repo, no
STATE.md and no mailbox. Pinned by `board-selftest.sh` (89 checks).
STATE.md and no mailbox. Pinned by `board-selftest.sh` (134 checks).
**It lives here because the mailbox is one of its three inputs, and it carries
the same axis distinction the mailbox does.** A pending count means *others
@ -85,15 +85,36 @@ marketplace plugin. Three components, one boundary:
**`board.sh --plan` is the THIRD rendering, and the only one that takes a
position.** It answers which repos to open a tab for today, in what order,
with which command. The position it takes is the ORDER and nothing else -
there is no cutoff, so the plan hides nothing, and every rule is a lookup over
fields the scan already read. Debt ranks first regardless of status, and that
is the rule most likely to be "fixed" into a defect: excluding `blocked` or
`done` is a claim about a repo's OWN next step, which by definition cannot be
moved, while owing a reply is the other axis entirely - answering is often
what unblocks it. Measured on the real tree at 0.16.0, two of 26 planned repos
were `done` with an unhandled inbox. Repos with no board line rank last and
are LABELLED rather than dropped, because the table already prints a MERK line
about them and a plan that omitted them silently would repeat that defect.
there is no cutoff, so the plan hides nothing, and every term is a lookup over
fields the scan already read. Since 0.19.0 it is ONE score rather than four
buckets, because four groups cannot express "this repo owes one message and
releases two others" - which is how a chain's root ended up ranked below the
repos waiting on it.
**Debt is never excluded and never capped, and that is the rule most likely to
be "fixed" into a defect.** Excluding `blocked` or `done` is a claim about a
repo's OWN next step, which by definition cannot be moved, while owing a reply
is the other axis entirely - answering is often what unblocks it. Measured on
the real tree at 0.16.0, two of 26 planned repos were `done` with an unhandled
inbox. A cap on debt was proposed with the chain credit at 0.19.0 and DECLINED
by the operator for the same reason. The weight carries that decision: 15 per
message is above the sum of every other bonus (13), so one message more still
outranks any combination of status and cost. Lowering it to 12 - the value
originally proposed - lets a tiebreak flip the debt order, which is the cap
arriving through the back door.
**Chain-root credit lands on the ROOT and nowhere else.** For every `blocked`
repo the `blocked-on` edge is followed transitively to the first repo that is
not itself blocked. Crediting a blocked repo would open a tab that cannot move;
crediting only the direct blocker leaves a two-hop chain's root uncredited,
which is the shape the real tree actually had. A cycle, a `blocked-on` naming
an unscanned repo, and a blocked repo with no target must all credit NOBODY:
inventing a root there produces a plan that looks correct and sends the
operator to the wrong repo.
Repos with no board line rank last and are LABELLED rather than dropped,
because the table already prints a MERK line about them and a plan that
omitted them silently would repeat that defect.
It renders `key=value` blocks, not prose, because it has two consumers: the
operator, and a driver repo consuming the plan. Prose would make the rendered
@ -251,7 +272,7 @@ obligations in another repo.
builtins only in hook and tests.
- TDD: no behavior change without a failing selftest check first.
`bash scripts/coord-selftest.sh` must exit 0 (183/183),
`bash scripts/board-selftest.sh` must exit 0 (89/89) and
`bash scripts/board-selftest.sh` must exit 0 (134/134) and
`bash scripts/route-selftest.sh` must exit 0 (73/73).
- English for all code, docs, and commit messages (public repo). Norwegian
trigger aliases in the skill description are deliberate.