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

@ -20,7 +20,7 @@ description: >-
user names no repo and no tool — choosing *between* repos is this skill. Not for
"where were we" inside the current repo: that is this repo's own STATE.md,
already injected at session start.
version: "0.18.0"
version: "0.19.0"
---
# board — which repo deserves the next session
@ -139,10 +139,29 @@ and it has its own rendering:
This is the one case where a list *is* the answer and the no-dumping rule does not
apply — the user asked for the day, and a day has more than one repo in it. Pass
the plan through with a short framing line; do not re-rank it, re-order it, or trim
it. The order is the engine's position and it is deterministic: debt first
(most-owed first, whatever the status), then `in-progress`, then `planned`, then
repos with no board line. Substituting your own judgement for that order makes the
plan unreproducible and costs the property that makes it trustworthy.
it. The order is the engine's position and it is deterministic — one score, not
four groups:
40 x repos released transitively (chain-root credit)
15 x unhandled inbox messages
+10 in-progress / +5 planned / +2 no declared status
+3 when next-cost names a Sonnet row
**Chain-root credit is the term worth understanding before you explain an order
to the operator.** For every `blocked` repo the engine follows `blocked-on` to
the ROOT of the chain and credits only the root, transitively. Opening a blocked
repo releases nobody — its next step is by definition waiting; opening the root
releases everything behind it. A cycle or a `blocked-on` naming an unscanned repo
credits nobody. This is engine rule 1 above, now computed rather than eyeballed.
Debt is **uncapped** by decision: owing a reply is the other axis from a repo's
own next step, and answering is often what unblocks a chain. At 15 per message a
repo owing one more still outranks any combination of status and cost bonuses.
`why=` names the **dominant** term, so a block can read `why=unblocks:2` even
though the repo also owes mail. Read it as "what opening this releases", not as
the only reason it qualified. Substituting your own judgement for the order makes
the plan unreproducible and costs the property that makes it trustworthy.
Two things to say out loud when you hand it over:

View file

@ -15,7 +15,7 @@ description: >-
covers retiring a broadcast that has become wrong or obsolete: "retract that
broadcast", "that announcement is outdated, pull it", "trekk tilbake kringkastingen",
"den broadcasten er utdatert".
version: "0.18.0"
version: "0.19.0"
---
# coord-send — natural-language front door for inter-repo messages

View file

@ -14,7 +14,7 @@ description: >-
the operator names no model and no tool — choosing the model for the next
session IS this skill. Not for choosing which REPO gets the next session:
that is the `board` skill.
version: "0.18.0"
version: "0.19.0"
---
# route — what the next session should run with