feat(board)!: rank --plan on five ordered groups, planned above in-progress

Replaces the weighted score shipped in 0.19.0 with five lookups: chain-root
credit, unhandled inbox, planned, in-progress, undeclared status. Within a
group: that group's own quantity, then a Sonnet next-cost, then oldest plan.

The score's objection is accepted, not forgotten, and is written into board.sh
and CLAUDE.md so a later session reads it as decided rather than as an unfixed
defect: a group order cannot express "owes one message AND releases two others"
as one quantity. What the score could not do was hold still for the format's
second consumer - re-tuning one weight against another silently reorders a
parser in another repo, and no test here can catch that.

planned now ranks above in-progress, inverted by the same decision: converting a
decision into motion is the slow step; live work is already moving.

Debt stays uncapped and never excluded. One group below chain-root credit is not
the cap declined at 0.19.0 - the debtor keeps its tab, its most-owed-first
position, and its why=inbox:N. Pinned by a discriminating fixture the score
would fail: a root releasing one repo outranks a repo owing four.

board-selftest 134 -> 138. Suite 183 + 138 + 73 = 394.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6ULuFCPMNYAPNN3pAjsXQ
This commit is contained in:
Kjell Tore Guttormsen 2026-08-03 06:55:21 +02:00
commit d0a5ffe515
10 changed files with 287 additions and 118 deletions

View file

@ -1,6 +1,6 @@
{
"name": "repo-mailbox",
"version": "0.19.0",
"version": "0.20.0",
"description": "Local mailbox for coordination between Claude Code sessions in different repositories. Directed messages and broadcasts as plain Markdown files on your own disk, injected as context at session start. Local, private, no network.",
"author": {
"name": "Kjell Tore Guttormsen"

View file

@ -5,6 +5,53 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.20.0] - 2026-08-03
### Changed
- **`--plan` ranks on FIVE ORDERED GROUPS, replacing the weighted score shipped
one version earlier.** The groups are chain-root credit, unhandled inbox,
`planned`, `in-progress`, and undeclared status — each a lookup over a field
the scan already read, so a selftest pins an outcome rather than a
coefficient. Within a group: the quantity that group is about, then a Sonnet
`next-cost`, then oldest plan first.
The objection the score answered is **accepted, not forgotten**, and is
recorded in `board.sh` and `CLAUDE.md` so it is read as decided rather than as
an unfixed defect: a group order genuinely cannot express "owes one message
**and** releases two others" as a single quantity. What the score could not do
was hold still for the format's second consumer — re-tuning 40 against 15
silently reorders a parser living in another repo, and no test here can catch
that. Operator decision, 2026-08-03.
- **`planned` now ranks ABOVE `in-progress`**, inverted from every earlier
version by the same decision. A decided but unstarted next step is the step
that converts a decision into motion; live work is already moving. Flipping it
back is a policy change, not a sort fix.
- **`why=` names the group that admitted the repo**, not the dominant scoring
term. The emitted values are unchanged (`unblocks:N`, `inbox:N`, `planned`,
`in-progress`, `uavklart`), so neither consumer sees a new token — but the
chain-root group is tested first, so `why=unblocks:N` now wins for **any**
root, not only where its weight happened to dominate.
- **The Sonnet `next-cost` tiebreak introduced with the score is carried
forward deliberately**, demoted to the last key below group and magnitude. It
is a lookup, and removing a shipped property nobody asked to remove would be
the larger surprise. It can now only separate repos that are otherwise equal.
### Notes
- **Debt is still neither capped nor excluded.** Being one group below
chain-root credit is not the cap that was proposed and declined at 0.19.0: a
debtor keeps its tab, its most-owed-first position among the other debtors,
and its `why=inbox:N`. Selftest section 12 pins both halves on a
discriminating fixture — a root releasing **one** repo outranks a repo owing
**four** (which the score would have ordered the other way), and that debtor
keeps everything it had.
- `board-selftest.sh` is 134 → **138 checks**. Suite total 183 + 138 + 73 =
**394**.
## [0.19.0] - 2026-08-02
### Added

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` (134 checks).
STATE.md and no mailbox. Pinned by `board-selftest.sh` (138 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
@ -86,10 +86,24 @@ marketplace plugin. Three components, one boundary:
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 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.
fields the scan already read. Since 0.20.0 it is FIVE ORDERED GROUPS -
chain-root, debt, planned, in-progress, undeclared - ranked within a group by
that group's own quantity, then a Sonnet next-cost, then oldest plan first.
**0.19.0 shipped a weighted score here and 0.20.0 replaced it, and the
objection the score answered is ACCEPTED, not forgotten.** A group order
genuinely cannot express "this repo owes one message and releases two others"
as one quantity; a score could, and that was its point. What a score could not
do was hold still for the second consumer - re-tuning 40 against 15 silently
reorders a parser living in another repo, and no test in THIS repo can catch
that. The operator weighed both and chose the lookup (2026-08-03). Write that
down every time this paragraph is edited: a later session that reads the
objection as an unfixed defect will "restore" the score, and the round trip is
the loop this file exists to stop.
**`planned` ranks ABOVE `in-progress`, inverted at 0.20.0 by operator
decision.** Turning a decision into motion is the slow step; live work is
already moving. Flipping it back is a policy change, not a sort fix.
**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
@ -97,11 +111,12 @@ marketplace plugin. Three components, one boundary:
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.
by the operator. Sitting one group below chain-root credit is NOT that cap:
the debtor keeps its tab, its most-owed-first position among the other
debtors, and its `why=inbox:N`. A change that DROPPED a debtor from the plan
would be the declined cap wearing the group order as a disguise, and selftest
section 12 pins both halves - the root outranking four owed messages, and the
debtor keeping everything it had.
**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
@ -272,7 +287,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 (134/134) and
`bash scripts/board-selftest.sh` must exit 0 (138/138) 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.

View file

@ -8,7 +8,7 @@
*AI-generated: all code produced by Claude Code through dialog-driven development.*
![Version](https://img.shields.io/badge/version-0.19.0-blue)
![Version](https://img.shields.io/badge/version-0.20.0-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Hooks](https://img.shields.io/badge/hooks-1-green)
![Skills](https://img.shields.io/badge/skills-3-orange)
@ -151,7 +151,7 @@ Note that raising the inbox's priority (Rule 7) deliberately does **not** widen
## Development
bash scripts/coord-selftest.sh # 183 checks against a throwaway mailbox
bash scripts/board-selftest.sh # 134 checks against a throwaway repo tree
bash scripts/board-selftest.sh # 138 checks against a throwaway repo tree
bash scripts/route-selftest.sh # 73 checks, incl. the route->board round trip
npm test # all three selftests via node --test

View file

@ -1,6 +1,6 @@
{
"name": "repo-mailbox",
"version": "0.19.0",
"version": "0.20.0",
"private": true,
"type": "module",
"engines": {

View file

@ -591,10 +591,18 @@ 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.
# Debt before live work: repo-h is in-progress and owes nothing. Stated as
# group 2 over group 4, NOT as "before any that owes none" - since 0.20.0 the
# chain-root group sits above debt, so a repo owing nothing CAN outrank one that
# does. Section 12 pins that half on the fixture that shows it cleanly.
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" $?
check "a repo owing mail is planned before live work that owes and releases nothing" $?
# Group 2 over group 3: repo-g is planned and owes nothing.
n_g="$(printf '%s\n' "$PLAN" | grep -n '^repo=repo-g$' | cut -d: -f1)"
[ -n "$n_g" ] && [ "$n_ow" -lt "$n_g" ]
check "a repo owing mail is planned before a repo that merely has a plan" $?
# 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.
@ -602,17 +610,28 @@ check "every repo owing mail is planned before any that owes none" $?
check "a repo qualifying under two rules is planned exactly once" $?
# repo-a owes 3 messages AND is the blocker plug-x and repo-blocked-owes both
# name. why= reports the DOMINANT term, so it says what a session there would
# release rather than what it owes - the whole reason this field was changed.
# Before chain-root credit existed this read `inbox:3`, and that was the line
# eleven blocks repeated verbatim.
# name. Under the group form why= names the GROUP that put the repo in the plan,
# and the chain-root group is the first one tested - so it reports what a session
# there would release rather than what it owes. Between 0.19.0 and 0.20.0 the
# same assertion held for a different reason (the dominant term of a score);
# the reason is written down because the check passes either way and only the
# reason says which form is running.
printf '%s\n' "$PLAN" | grep -A3 '^repo=repo-a$' | grep -q '^why=unblocks:2$'
check "why= names the dominant reason, releasing over owing when it dominates" $?
check "why= names the group that planned the repo, releasing over owing" $?
# 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" $?
# planned outranks in-progress among the repos that neither owe nor release.
# INVERTED at 0.20.0 by operator decision: a decided-but-unstarted next step is
# the step that converts a decision into motion, and live work is already moving.
# This is a policy call, not a preference - it is recorded here and in board.sh
# so a later session cannot read it as a sort that drifted.
[ -n "$n_g" ] && [ "$n_g" -lt "$n_h" ]
check "planned work is planned before live in-progress work (operator inversion)" $?
# Group 4 over group 5: repo-e declares an out-of-set status token and lands in
# the undeclared group, which is planned LAST and never dropped.
n_e="$(printf '%s\n' "$PLAN" | grep -n '^repo=repo-e$' | cut -d: -f1)"
[ -n "$n_e" ] && [ "$n_h" -lt "$n_e" ]
check "a repo with no declared status is planned after live work, not dropped" $?
# 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.
@ -896,9 +915,9 @@ mkrepo "$ROOT/chain-root"
{
echo "# STATE - chain-root"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
# Deliberately `planned`, not `in-progress`: under the old four-bucket order
# that puts the root BELOW every live repo, so the ranking check below fails
# for the right reason instead of passing on fixture order.
# Deliberately `planned`, not `in-progress`: the root must earn its position
# from the chain-root group alone, never from the status group, so the ranking
# checks below fail for the right reason instead of passing on fixture order.
echo "<!-- board: status=planned; blocked-on=-; next-cost=Opus 5/high -->"
printf 'Roten %s ingen gjeld, men holder to repo oppe.\n' "$EMDASH"
} > "$ROOT/chain-root/STATE.md"
@ -948,6 +967,43 @@ mkrepo "$ROOT/dangle"
echo "Venter paa noe som ikke finnes."
} > "$ROOT/dangle/STATE.md"
# THE DISCRIMINATING PAIR for the 0.20.0 group form. Every other ordering check
# in this section passes under BOTH the 0.19.0 score and the group form, because
# chain-root releases two repos and 40*2 beats any debt in this tree - it would
# have gone green while the operator's decision quietly failed to ship.
#
# These two are built so the forms DISAGREE. solo-root releases exactly one repo
# and owes nothing (score: 40 + 5 planned = 45). heavy-debtor owes four (score:
# 60 + 10 in-progress = 70). The score ranks the debtor first; the group form
# ranks the root first, because releasing is group 1 and debt is group 2.
mkrepo "$ROOT/solo-root"
{
echo "# STATE - solo-root"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=planned; blocked-on=-; next-cost=Opus 5/high -->"
printf 'Losner ett repo, skylder ingenting.\n'
} > "$ROOT/solo-root/STATE.md"
mkrepo "$ROOT/solo-leaf"
{
echo "# STATE - solo-leaf"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=blocked; blocked-on=solo-root; next-cost=Opus 5/high -->"
printf 'Venter p%s solo-root.\n' "$EMDASH"
} > "$ROOT/solo-leaf/STATE.md"
mkrepo "$ROOT/heavy-debtor"
{
echo "# STATE - heavy-debtor"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=in-progress; blocked-on=-; next-cost=Opus 5/high -->"
printf 'Skylder fire svar, losner ingen.\n'
} > "$ROOT/heavy-debtor/STATE.md"
mkdir -p "$CLAUDE_COORD_DIR/heavy-debtor/inbox"
for n in 1 2 3 4; do
echo "msg" > "$CLAUDE_COORD_DIR/heavy-debtor/inbox/2026-msg$n-from-v.md"
done
P2="$("$BOARD" --roots "$ROOT" --plan 2>/dev/null)"; rc=$?
check "--plan still exits 0 with chain, cycle and dangling fixtures present" "$rc"
@ -973,6 +1029,22 @@ n2_h="$(printf '%s\n' "$P2" | grep -n '^repo=repo-h$' | cut -d: -f1)"
[ -n "$n2_cr" ] && [ -n "$n2_h" ] && [ "$n2_cr" -lt "$n2_h" ]
check "a chain root ranks above live work that releases nothing" $?
# THE decisive check for the 0.20.0 group form - the discriminating pair built
# above. solo-root releases ONE and owes nothing; heavy-debtor owes FOUR. This is
# the only ordering assertion in the file that the 0.19.0 score would fail, so it
# is the one that proves which form is actually running. Debt is still neither
# capped nor excluded here - it is one group lower, not down-weighted.
n2_sr="$(printf '%s\n' "$P2" | grep -n '^repo=solo-root$' | cut -d: -f1)"
n2_hd="$(printf '%s\n' "$P2" | grep -n '^repo=heavy-debtor$' | cut -d: -f1)"
[ -n "$n2_sr" ] && [ -n "$n2_hd" ] && [ "$n2_sr" -lt "$n2_hd" ]
check "releasing ONE repo outranks owing FOUR (group form, not the score)" $?
# The other half of the same rule: the debtor is still IN the plan, and still
# says debt is why. A group form that ranked debt lower by dropping it would be
# the excluded-debt defect wearing the operator's decision as a disguise.
printf '%s\n' "$P2" | grep -A3 '^repo=heavy-debtor$' | grep -q '^why=inbox:4$'
check "a repo outranked by a chain root still keeps its tab and its why=" $?
# A cycle terminates and credits nobody. Both halves matter: hanging is the
# failure everyone imagines, but silently inventing a root is the one that would
# ship - it looks like a working plan.
@ -989,9 +1061,9 @@ check "a dangling blocked-on target is never planned into existence" $?
printf '%s' "$P2" | grep -q '^repo=dangle$'; [ $? -ne 0 ]
check "the repo carrying a dangling blocked-on is itself still excluded" $?
# The format is the API. Adding a scoring rule must not move, rename or drop a
# single field - the driver parses these keys positionally-independently, but it
# parses THESE keys.
# The format is the API. Changing the ranking form must not move, rename or drop
# a single field - the driver parses these keys positionally-independently, but
# it parses THESE keys.
n2_tab="$(printf '%s\n' "$P2" | grep -c '^tab=')"
n2_rep="$(printf '%s\n' "$P2" | grep -c '^repo=')"
n2_dir="$(printf '%s\n' "$P2" | grep -c '^dir=')"
@ -1003,10 +1075,10 @@ n2_cms="$(printf '%s\n' "$P2" | grep -c '^command_missing=')"
[ "$n2_tab" -gt 0 ] && [ "$n2_rep" -eq "$n2_tab" ] && [ "$n2_dir" -eq "$n2_tab" ] \
&& [ "$n2_why" -eq "$n2_tab" ] && [ "$n2_sta" -eq "$n2_tab" ] \
&& [ "$n2_nes" -eq "$n2_tab" ] && [ $(( n2_cmd + n2_cms )) -eq "$n2_tab" ]
check "scored plan still carries every documented field exactly once per block" $?
check "ranked plan still carries every documented field exactly once per block" $?
printf '%s' "$P2" | grep -q '^command=$'; [ $? -ne 0 ]
check "scored plan never emits an empty command= line" $?
check "ranked plan never emits an empty command= line" $?
# Debt still puts a repo in the plan and still names itself when it is the
# dominant reason. Rejecting the proposed cap on debt was a deliberate call:
@ -1016,12 +1088,12 @@ check "debt still names itself in why= when it is the dominant reason" $?
# --brief is a second RENDERING of the same scan and must survive the change.
"$BOARD" --roots "$ROOT" --brief >/dev/null 2>&1
check "--brief still exits 0 after the scoring change" $?
check "--brief still exits 0 after the ranking change" $?
# The plan must still print the scan whole. There is no cutoff by design, so a
# consumer's own tab cap is the only thing that ever hides a repo.
[ "$n2_tab" -ge "$n_tab" ]
check "the scored plan hides nothing the bucketed plan showed" $?
check "the grouped plan hides nothing an earlier ordering showed" $?
echo ""
echo "board-selftest: $PASS passed, $FAIL failed"

View file

@ -70,13 +70,25 @@
# 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 and there is no cutoff, so nothing is hidden. It is
# ONE score, not four buckets, because four groups cannot express "this repo
# owes one message and releases two others":
# FIVE ORDERED GROUPS, each a lookup over a field the scan already read:
#
# 40 x repos released transitively (chain-root credit, see below)
# 15 x unhandled inbox messages
# +10 in-progress / +5 planned / +2 undeclared status
# +3 when next-cost names a Sonnet row (cheap to move under quota pressure)
# 1. chain-root credit - repos released transitively (see below)
# 2. unhandled inbox messages, most-owed first, WHATEVER the status
# 3. planned
# 4. in-progress
# 5. undeclared status ('?' / MALFORMED) - last, and labelled
#
# Within a group: the quantity that group is about (released, then owed), then a
# Sonnet next-cost (cheap to move under quota pressure), then oldest plan first.
#
# 0.19.0 shipped a weighted score here instead, and 0.20.0 replaced it. The score
# expressed one thing this cannot - "owes one message AND releases two others" as
# a single quantity - and the operator accepted losing that. What a score could
# not do was hold still for the second consumer: re-tuning 40 against 15 here
# would silently reorder a parser living in another repo, and no test in THIS one
# could catch it. Groups 3 and 4 are inverted from every version before 0.20.0
# (planned above in-progress), also by operator decision: a decided but unstarted
# step is what converts a decision into motion; live work is already moving.
#
# CHAIN-ROOT CREDIT is the term that needed blocked-on to stop being a display
# string. For every blocked repo the edge is followed transitively to the ROOT
@ -87,15 +99,18 @@
# target all credit NOBODY: inventing a root there would look like a working
# plan while sending the operator to the wrong repo.
#
# Debt is deliberately UNCAPPED. A cap was proposed with the credit and declined
# by the operator: owing a reply is the other axis from a repo's own next step,
# and answering is often what unblocks a chain. 15 per message is above the sum
# of every other bonus (13), so one message more still outranks any combination
# of status and cost - the bonuses separate repos that owe the SAME.
# Debt is deliberately UNCAPPED and never excluded. A cap was proposed with the
# credit and declined by the operator: owing a reply is the other axis from a
# repo's own next step, and answering is often what unblocks a chain. Being one
# group below chain-root credit is NOT a cap - the debtor keeps its tab, its
# most-owed-first position among the other debtors, and its `why=inbox:N`. A
# change that dropped a debtor from the plan would be the declined cap wearing
# this decision as a disguise, and section 12 of the selftest pins that half.
#
# why= names the DOMINANT term rather than the first rule that matched, because
# with a score there is no single rule that put a repo in the plan - and a why=
# that always read `inbox:N` was the same sentence on every block.
# why= names the GROUP that put the repo in the plan. Under the score it named
# the dominant TERM, because a score has no single rule that admitted a repo;
# under groups the admitting rule is the group, so the two coincide wherever the
# chain-root group applies and the field is unchanged for both consumers.
# Read-only like the rest: --plan writes nothing, in the repo or the mailbox.
#
# --focus "<prose>" narrows --plan to the repos whose STATE.md DECLARES a
@ -592,17 +607,25 @@ focus_heldback() {
# 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
# there is no cutoff, so nothing is hidden. Five groups, all deterministic over
# fields the scan already read:
# 1. INN > 0, most-owed first, WHATEVER the status. A message owed is an
# 1. chain-root credit - repos this one releases transitively, most first.
# Above debt because it is the only move that frees sessions other than
# your own; a blocked repo is never credited, since its own next step is
# by definition waiting.
# 2. 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
# 3. planned - a decided but unstarted next step.
# 4. in-progress - live work, oldest plan first.
# 5. '?' 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.
# Groups 3 and 4 are in that order by operator decision at 0.20.0, inverted from
# every earlier version: turning a decision into motion is the slow step, and
# live work is already moving. Deciding it the other way is a policy change, not
# a sort fix.
# 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.
# Two lines for two consumers, and they are not redundant. A driver cd's the
@ -626,25 +649,43 @@ plan_cmd() {
}
plan() {
# ONE score, not four hard buckets. Four groups could rank a repo by what it
# owes or by whether its own work is live, but could never express "this repo
# owes one message and releases two others" - the case the four-bucket order
# got exactly backwards, ranking a chain's root below the repos waiting on it.
# FIVE ORDERED GROUPS, not a weighted score. Each group is a lookup over a
# field the scan already read, so two readers can disagree about the order but
# never about how it was produced - and a selftest can pin the outcome rather
# than a coefficient.
#
# Every term is a lookup over a field the scan already read, so the order stays
# reproducible and no term needs a model call. The weights are judgement and
# are meant to be re-tuned against measurement, which is why they sit here
# alone rather than spread through four sort invocations.
# THE COST THIS FORM ACCEPTS, recorded because it is real and was argued at
# length before it was chosen: a group order cannot express "this repo owes one
# message AND releases two others" as a single quantity. The 0.19.0 score could,
# and that was its point. What it could not do was stay stable for a consumer in
# another repo - re-tuning 40 against 15 here silently reorders `morning`,
# `--brief` and `brief-nightly`, and no test in THIS repo can hold a weighted
# ranking still for a parser in that one. The operator weighed both and chose
# the lookup (2026-08-03). This paragraph exists so a later session reads the
# objection as ANSWERED rather than as an unfixed defect and "restores" a score.
#
# Debt is deliberately NOT capped. A cap was proposed and declined by the
# operator: owing a reply is the OTHER axis from a repo's own next step, and
# answering is often precisely what unblocks a chain. A big mailbox therefore
# still outranks a small one, all else equal.
# Group 1 is chain-root credit, and it sits ABOVE debt: opening the root of a
# blocked chain is the only move that releases sessions other than your own.
# Group 3 is `planned` ABOVE group 4 `in-progress` - inverted from every prior
# version by operator decision the same day. The reason is that a decided but
# unstarted next step is the step that turns a decision into motion, while live
# work is already moving. It is a policy call, not a sort that drifted.
#
# The score is prefixed as field 1 and stripped again after sorting; `why`
# then becomes field 1 exactly as the four-bucket version left it, so the read
# loop below and both consumers see an unchanged block. Prefixing rather than
# appending is deliberate - `neste` is the last field and is free prose, so
# Debt is neither capped nor excluded - it is group 2, ranked most-owed-first,
# WHATEVER the status. A cap was proposed and declined; being outranked by a
# chain root is not a cap, because the debtor keeps its tab, its position among
# the other debtors, and its `why=inbox:N`.
#
# `cheap` carries the one property 0.19.0 added that no one asked to remove:
# under quota pressure a Sonnet row moves for less. It is the LAST tiebreak,
# below the group's own magnitude and above age, so it separates repos that are
# otherwise equal and can never lift one past another that owes or releases
# more.
#
# The three sort keys are prefixed as fields 1-3 and stripped again afterwards;
# `why` then becomes field 1 exactly as every previous version left it, so the
# read loop below and both consumers see an unchanged block. 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.
pf="${TMPDIR:-/tmp}/board-plan.$$"
: > "$pf"
@ -658,43 +699,31 @@ plan() {
blockedp = (status ~ /^blocked/)
u = (!blockedp && (name in ub)) ? ub[name] : 0
# 15 per message, not the 12 the order proposed. The weights were
# explicitly delegated, and 12 has a measurable defect: the status and
# cost bonuses sum to 13, so they could outrank a repo owing one message
# MORE than another. That is the debt-ordering rule weakening through a
# tiebreak, which is the one thing the declined cap would also have done.
# At 15 a single message outweighs every non-unblock bonus combined, so
# within the debt group the order is still most-owed-first, and the
# bonuses do what they are for: separating repos that owe the same.
score = 40 * u + 15 * inbox
if (status == "in-progress") score += 10
else if (status == "planned") score += 5
else if (status == "?" || status ~ /^MALFORMED:/) score += 2
# Cheap-to-move first under quota pressure, and only ever a tiebreak.
if (cost ~ /[Ss]onnet/) score += 3
# Membership is unchanged except for one addition: a repo that releases
# someone is planned even when it owes nothing and its own status would
# otherwise exclude it. Everything else keeps the old rule, so done,
# deferred, blocked and no-STATE repos with no debt stay out.
# Membership is unchanged: a repo that releases someone is planned even
# when it owes nothing and its own status would otherwise exclude it, and
# done, deferred, blocked and no-STATE repos with no debt stay out.
keep = (inbox > 0) || (u > 0) || (status == "in-progress") || \
(status == "planned") || (status == "?") || (status ~ /^MALFORMED:/)
if (!keep) next
# why= names the DOMINANT term, not the first rule that happened to match:
# with a score there is no single rule that "put the repo in the plan",
# and a why= that always said inbox: would be the uninformative line this
# field was changed to fix.
if (u > 0 && 40 * u >= 15 * inbox) why = "unblocks:" u
else if (inbox > 0) why = "inbox:" inbox
else if (status == "in-progress" || status == "planned") why = status
else why = "uavklart"
# The group IS the reason, so why= is the group and needs no separate rule.
# `mag` orders within a group by the quantity that group is about - released
# in group 1, messages owed in group 2 - and is 0 where the group has no
# quantity, leaving those to the tiebreaks below.
if (u > 0) { grp = 1; mag = u; why = "unblocks:" u }
else if (inbox > 0) { grp = 2; mag = inbox; why = "inbox:" inbox }
else if (status == "planned") { grp = 3; mag = 0; why = "planned" }
else if (status == "in-progress") { grp = 4; mag = 0; why = "in-progress" }
else { grp = 5; mag = 0; why = "uavklart" }
print score, why, $0
# Cheap-to-move first under quota pressure. Sorted ascending, so 0 = cheap.
cheap = (cost ~ /[Ss]onnet/) ? 0 : 1
print grp, mag, cheap, why, $0
}
' "$UNBLOCKS" "$RECORDS" \
| sort -t'|' -k1,1nr -k4,4n \
| cut -d'|' -f2- >> "$pf"
| sort -t'|' -k1,1n -k2,2nr -k3,3n -k6,6n \
| cut -d'|' -f4- >> "$pf"
# The cutoff, and its disclosure, computed together - they are one feature.
FOCUS_SLUGS=""
@ -718,7 +747,7 @@ plan() {
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: score = 40*repo losnet (kjede-rot) + 15*innboks + status + kost."
echo "# Rekkefolge: 5 grupper - kjede-rot, innboks, planned, in-progress, 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.

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.19.0"
version: "0.20.0"
---
# board — which repo deserves the next session
@ -139,13 +139,18 @@ 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 — one score, not
four groups:
it. The order is the engine's position and it is deterministic — five ordered
groups, each a lookup, no weights:
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
1. chain-root credit (repos released transitively, most first)
2. unhandled inbox (most owed first, whatever the status)
3. planned
4. in-progress
5. no declared status (last, and labelled)
Within a group: that group's own quantity, then a Sonnet `next-cost`, then oldest
plan first. `planned` sits above `in-progress` by operator decision at 0.20.0 —
turning a decision into motion is the slow step; live work is already moving.
**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
@ -154,13 +159,14 @@ repo releases nobody — its next step is by definition waiting; opening the roo
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.
Debt is **uncapped and never excluded** by decision: owing a reply is the other
axis from a repo's own next step, and answering is often what unblocks a chain.
Sitting one group below chain-root credit is not a cap — a debtor keeps its tab,
its most-owed-first position among the other debtors, and its `why=inbox:N`.
`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
`why=` names the **group** that put the repo in the plan, 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.19.0"
version: "0.20.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.19.0"
version: "0.20.0"
---
# route — what the next session should run with