Skip to content

tabularius: publish board projection - #2001

Merged
4444J99 merged 1068 commits into
mainfrom
tabularius/board-projection
Aug 16, 2026
Merged

tabularius: publish board projection#2001
4444J99 merged 1068 commits into
mainfrom
tabularius/board-projection

Conversation

@4444J99

@4444J99 4444J99 commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Restores the publication→main merge that has been dead since 2026-07-26 (last one: #1569).

What this carries

tasks.yaml only — 5762 insertions / 760 deletions, 732 commits of keeper-published
projection. Concretely: +107 tasks, +177 dispatch receipts (all agents), and the budget
track advances from 2026-07-26 to 2026-07-27+.

Why it was stuck

preserve_board_projection was retired to PreserveResult(skipped=True, reason="remote-keeper-owns-projection") — correctly, since a local process must never commit or
push the canonical board. But that function was also the only thing that opened this PR.
BOARD_PUBLICATION_TITLE has been a dead constant ever since: nothing replaced the
PR-opening half. The keeper kept publishing to tabularius/board-projection (latest commit
today 11:57) and nothing carried it to main.

Why it matters — this is the #1995 root cause

The live checkout's tasks.yaml is byte-identical to origin/main, so with the branch unmerged
the local projection froze at track.date: 2026-07-26. Downstream:

Per ci.yml's board fast lane, a board-projection PR implicates no code, so the matrix runs
post-merge for check-main-green's exact-head evidence rather than on this PR's critical path.

Refs #1995

Copilot AI lite review requested due to automatic review settings August 7, 2026 16:16
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review any files in this pull request.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Multi-agent review roll call
@codex review

(CodeRabbit and Claude review automatically. Reviewers: post substantive findings only. Authors/agents: address every thread, push fixes to this branch, reply and resolve, then re-request review.)

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • tasks.yaml
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7402f55a-6cf7-4919-a791-37f09d981fba

📥 Commits

Reviewing files that changed from the base of the PR and between ea67111 and ebb0fb0.

📒 Files selected for processing (1)
  • tasks.yaml

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

4444J99 added a commit that referenced this pull request Aug 7, 2026
…atch receipt

Every serial dispatch receipt has 409'd at broker registration since 2026-07-19:

    ConductError: conduct broker rejected request (409):
      {"detail": "session_id is already registered to another identity"}

`register()` binds agent/surface/session_id from the authenticated principal and then rejects
any re-registration whose WHOLE identity object differs. Post-binding those three are forced
equal, so the only fields that can still differ are `provider_identity` and `native_run_id` —
both client-declared. That check is therefore structurally incapable of catching an authority
mismatch (`session_principals`, two lines later, is what guards authority); it can only ever
fire on cosmetic provider drift. Since every relay call site passes a FIXED session_id literal,
a literal is claimable exactly once and the loser is refused forever.

This is the #1408 relay freeze, recurring on the fields the #1408 fix did not normalize —
`_bind_conductor_identity` carries the comment describing exactly this failure mode. Reproduced
against the real broker with ONE principal throughout:

    #1 provider_identity='limen-cli'          : OK
    #2 same again (idempotent re-register)    : OK
    #3 provider_identity='limen-cli-2'        : CONFLICT
    #4 'limen-cli-2' again (permanent?)       : CONFLICT
    #5 native_run_id set, provider unchanged  : CONFLICT
    #6 original identity still works          : OK

The freeze is per-literal, which is why the symptom looked so specific: writers using their own
literals (`harvest`, `dispatch-async/reserve`, `heal-board/lifecycle-repair`) kept landing
receipts all month — the published projection holds 177 August receipts — while the one path
that records a *dispatch* was silently dead. `logs/throughput-governor.jsonl` then read
`dispatched: 0` on every pass and pinned the jules lane in `bootstrap` at 25/day, making the
requested 100/day structurally unreachable (#1995).

Fix: a refused literal falls back once to an id keyed on exactly the identity fields the keeper
compares but does not normalize.

A FALLBACK, NOT ALWAYS-KEYED — and the first cut of this fix got that wrong. The relay session id
is not internal plumbing: the keeper stamps it into `session_id` on every projection event, so it
is part of the recorded receipt that harvest and 14 tests read. Keying it unconditionally renames
an observable identifier estate-wide to fix a condition that only arises once a literal is already
poisoned (14 tests failed on exactly that). So the healthy path keeps the stable literal and only a
refused one falls back — which also makes the fallback legible: a keyed `session_id` in a
dispatch_log entry IS the signal that its literal is frozen.

Classified on the STATUS, never the prose — ConductError's own documented contract, since three
keepers word this refusal three ways. Both 409 shapes (frozen identity, bound principal) mean one
thing here: that literal is not mine to use. Registering a different id cannot touch the session
the keeper is protecting, and every authority check still applies to whatever this client does
register. A non-conflict failure is re-raised untouched.

Verified live against the poisoned keeper: a real `limen dispatch --agent jules --limit 1 --live`
now gets PAST registration and reaches `submit()`, where it stops on a different and legitimate
409 — `task ORG-artist-organ-face-0705 exact revision moved` — because the local projection has
been frozen at `track.date: 2026-07-26` since the board-publication PR rung was retired (#2001
carries that backlog, #2014 the drift that blocks it). Stacked defects; this is one of four.

4 regression tests: the healthy path keeps the literal; a frozen literal falls back and still
relays; a non-conflict failure is never retried; and the real ConductBroker is driven through the
freeze and then through the keyed id. cli/tests/test_tabularius.py: 34 passed.

Refs #1995
@4444J99

4444J99 commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

HOLD is correct, and the blocker now has an owner.

validate fails on a real data defect in the published projection, not on this PR:

tasks.yaml has 12 needs-human task(s) still available to dispatch
  ASK-quicken-login, ASK-quicken-credential, ASK-quicken-d2l, ASK-quicken-delete,
  ASK-quicken-send, ASK-quicken-escalate-* (7)

All 12 are needs_human on main and regressed to open on the keeper's published projection
while keeping their needs-human label — login / credential / delete / send atoms made
dispatchable. The gate is right to refuse that, so this PR must not be merged until the canonical
board is repaired.

The repair could not happen because every self-heal organ reads the local projection, where
those tasks are already needs_human. heal-board.py has the exact reconcile for this and is
wired into the heartbeat — it found nothing wrong, because it was looking at a stale copy. And that
same drift held main red here, blocking the publication merge that would have refreshed the copy:
the drift was protecting itself from the repair.

#2014 opens that loop — heal-board.py --canonical runs the same repairs against the keeper's
published projection, submitted through the authenticated relay with that board as the explicit
compare-and-swap base. Dry-run against the live estate reports exactly these 12 and nothing else.

So the sequence is: #2014 merges → the beat runs the canonical reconcile (it is a loop-body edit,
so the daemon needs a kickstart) → the keeper republishes with the 12 back at needs_human
validate goes green here → this merges and the local projection is finally current.

Leaving this open rather than closing it: the backlog it carries is real (+107 tasks, +177 receipts)
and its head advances as the keeper publishes. Owner of the merge is the beat's merge rung once
validate clears.

4444J99 added a commit that referenced this pull request Aug 7, 2026
… froze for 12 days (#2016)

THE MISSING HALF OF A RETIREMENT. `preserve_board_projection` was retired to
`PreserveResult(skipped=True, reason="remote-keeper-owns-projection")` — correctly, since a local
process must never commit or push the canonical board. But that function was ALSO the only thing
that opened the publication PR, and nothing replaced it:

    $ grep -rn "BOARD_PUBLICATION_TITLE" --include='*.py' cli/src scripts cli/tests
    cli/src/limen/tabularius.py:82:BOARD_PUBLICATION_TITLE = "tabularius: publish board projection"

A dead constant. The keeper kept publishing to `tabularius/board-projection` and nothing carried it
to `main`. Last publication merged: 2026-07-26 (#1569) — exactly the `track.date` the board was
still frozen at 12 days later.

This was NOT a sync failure. The live checkout's `tasks.yaml` was byte-identical to
`origin/main` (md5 25aeba82…), fully converged; the publication branch simply sat +107 tasks and
+177 dispatch receipts ahead (md5 57c1bca1…). A current checkout with a dead rung.

Everything downstream reads the frozen copy:

  - `dispatch` SELECTS from it — ORG-artist-organ-face-0705 was `open` locally and `failed_blocked`
    canonically, so a jules session was launched for a task the keeper considers blocked;
  - every receipt ticket computes its compare-and-swap precondition from it, so the keeper answers
    `409 task <id> exact revision moved`;
  - `lane_throughput_window` counts 0 dispatches in it, which pinned the jules lane in `bootstrap`
    at 25/day and made the requested 100/day structurally unreachable (#1995).

Fix: a beat rung that opens or reports the publication PR. It NEVER merges and NEVER pushes —
`merge-policy.sh` decides and the beat's merge rung owns it. Idempotent: an already-open PR is a
no-op, and so is a projection branch whose commits change no board content (gated on the tasks.yaml
tree, not the commit count, because the branch periodically reconciles main into itself). Fail-open
on every unreadable condition; this rung must never take a beat down.

WHY SHELL. Opening a PR is an outward action, and check-effectors Class C exists because a
PreToolUse(Bash) hook can never see subprocess.run(["gh", ...]) inside a Python module. The `gh`
call belongs on the Bash rail where that guard can reach it — the same reason session-plan.py prints
its `gh` command instead of making it.

Verified against the live estate: correctly detects the hand-opened #2001 and no-ops.

    publish-board-pr: PR #2001 already open for tabularius/board-projection -> main
      (owner: the beat's merge rung)

check-params, check-effectors and check-runner-coverage all clean; the three new parameters are
declared in the panel.

NOTE: this is a loop-body edit, so the running daemon needs a kickstart to load it.

Refs #1995
4444J99 added a commit that referenced this pull request Aug 7, 2026
…atch receipt (#2005)

Every serial dispatch receipt has 409'd at broker registration since 2026-07-19:

    ConductError: conduct broker rejected request (409):
      {"detail": "session_id is already registered to another identity"}

`register()` binds agent/surface/session_id from the authenticated principal and then rejects
any re-registration whose WHOLE identity object differs. Post-binding those three are forced
equal, so the only fields that can still differ are `provider_identity` and `native_run_id` —
both client-declared. That check is therefore structurally incapable of catching an authority
mismatch (`session_principals`, two lines later, is what guards authority); it can only ever
fire on cosmetic provider drift. Since every relay call site passes a FIXED session_id literal,
a literal is claimable exactly once and the loser is refused forever.

This is the #1408 relay freeze, recurring on the fields the #1408 fix did not normalize —
`_bind_conductor_identity` carries the comment describing exactly this failure mode. Reproduced
against the real broker with ONE principal throughout:

    #1 provider_identity='limen-cli'          : OK
    #2 same again (idempotent re-register)    : OK
    #3 provider_identity='limen-cli-2'        : CONFLICT
    #4 'limen-cli-2' again (permanent?)       : CONFLICT
    #5 native_run_id set, provider unchanged  : CONFLICT
    #6 original identity still works          : OK

The freeze is per-literal, which is why the symptom looked so specific: writers using their own
literals (`harvest`, `dispatch-async/reserve`, `heal-board/lifecycle-repair`) kept landing
receipts all month — the published projection holds 177 August receipts — while the one path
that records a *dispatch* was silently dead. `logs/throughput-governor.jsonl` then read
`dispatched: 0` on every pass and pinned the jules lane in `bootstrap` at 25/day, making the
requested 100/day structurally unreachable (#1995).

Fix: a refused literal falls back once to an id keyed on exactly the identity fields the keeper
compares but does not normalize.

A FALLBACK, NOT ALWAYS-KEYED — and the first cut of this fix got that wrong. The relay session id
is not internal plumbing: the keeper stamps it into `session_id` on every projection event, so it
is part of the recorded receipt that harvest and 14 tests read. Keying it unconditionally renames
an observable identifier estate-wide to fix a condition that only arises once a literal is already
poisoned (14 tests failed on exactly that). So the healthy path keeps the stable literal and only a
refused one falls back — which also makes the fallback legible: a keyed `session_id` in a
dispatch_log entry IS the signal that its literal is frozen.

Classified on the STATUS, never the prose — ConductError's own documented contract, since three
keepers word this refusal three ways. Both 409 shapes (frozen identity, bound principal) mean one
thing here: that literal is not mine to use. Registering a different id cannot touch the session
the keeper is protecting, and every authority check still applies to whatever this client does
register. A non-conflict failure is re-raised untouched.

Verified live against the poisoned keeper: a real `limen dispatch --agent jules --limit 1 --live`
now gets PAST registration and reaches `submit()`, where it stops on a different and legitimate
409 — `task ORG-artist-organ-face-0705 exact revision moved` — because the local projection has
been frozen at `track.date: 2026-07-26` since the board-publication PR rung was retired (#2001
carries that backlog, #2014 the drift that blocks it). Stacked defects; this is one of four.

4 regression tests: the healthy path keeps the literal; a frozen literal falls back and still
relays; a non-conflict failure is never retried; and the real ConductBroker is driven through the
freeze and then through the keyed id. cli/tests/test_tabularius.py: 34 passed.

Refs #1995
@4444J99

4444J99 commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Blocker inventory corrected — this PR is held by two independent gates, not one. My earlier comment named only the first.

gate why it fails owner
validate 12 needs-human-labelled ASK-quicken-* atoms regressed to open on the keeper (login / credential / d2l / delete / send + 7 escalations) #2014heal-board --canonical, rebased and in flight
pr-gatecheck-board-partition 8 new partner-lane findings: named third parties on a board bound for a public head #1780 — the broker scrub. Full analysis filed there.

The second one compounds: the baseline pins REV-organvm-mirror-mirror-revenue-readiness-0623-0630, a daily-minted dated task id baselined one day at a time and abandoned at 06-30. Every day since mints rows that cannot be in it, so this gate is red-by-construction on any board publication and gains one finding per partner lane per day. Publication stalling makes publication harder — a second self-reinforcing loop, independent of the needs-human drift.

Related, shipped in #2019: --update was re-pinning to all current findings despite the shrink-only rule stated in the docstring, the baseline header, and a test name. Since --check prints run --update to drop it for the 15 cleared entries, the obvious move to clear the noise here would have silently baselined all 8 new disclosures. That path is now refused.

Both gates are correct to refuse. I have not tried to route around either. validate clears when #2014 lands and the beat reconciles; check-board-partition needs a disclosure decision that is not an agent’s to make.

4444J99 added a commit that referenced this pull request Aug 7, 2026
…silently (#2019)

`check-board-partition.py` states its ratchet three times — module docstring
("the baseline may only shrink"), the baseline file header ("this list may only
SHRINK"), and a test literally named `test_the_baseline_only_shrinks`. The code
enforced it zero times: `--update` called `_write_baseline(current)`, re-pinning
to whatever is on the board right now, additions included.

The gap is not theoretical, it is a loaded trap in the gate's own output. When a
finding clears, `--check` prints `note baseline entry no longer reproduces (run
--update to drop it)`. Measured 2026-08-07 on PR #2001 (the board publication
backlog): 15 entries had cleared and were advertising exactly that instruction
while 8 NEW partner-lane findings were failing in the same run. Following the
gate's own advice to clear the noise would have silently accepted all 8 — new
named-third-party disclosures on a PUBLIC head, landed by an agent tidying up,
with the diff reading as a routine re-pin.

Why it went unenforced: `--update` is this predicate's only write, and its target
was not redirectable. Overriding LIMEN_ROOT to point it elsewhere also relocates
the partner-lane registries, so `findings()` raises PartitionRegistryError before
any baseline logic runs. So the one surface that could break the invariant was
the one surface no test could reach, and `test_the_baseline_only_shrinks` asserts
at `--check`, which never writes. An untestable write is how a stated rule
survives with no code behind it.

- `--update` is shrink-only: drops cleared entries, refuses to add, exit 1,
  naming each would-add line. A refused run leaves the file byte-identical — a
  partial write is the same leak, just quieter.
- Growth stays possible for a genuinely accepted disclosure, but only behind an
  explicit `--accept-new-disclosures`, because that is a disclosure decision and
  it should have to be spelled out in the command someone ran.
- BASELINE is now redirectable via LIMEN_BOARD_PARTITION_BASELINE (declared in
  the parameter panel), independently of LIMEN_ROOT — which is what finally makes
  the write testable.
- 5 tests covering the write surface: refuses growth, still shrinks bare, grows
  only when accepted out loud, a refused growth does not quietly bank the shrink,
  and an unset env var still resolves to the tracked file.

Verified live against the real canonical board (6.1MB, extracted from
origin/tabularius/board-projection): refuses all 8, exit 1, baseline untouched,
and it names the 15-drop temptation instead of acting on it. The pre-fix
`--update` was deliberately NOT run — it has no redirect and would have rewritten
the tracked baseline, which is itself the proof of the gap. A/B is source-level:
zero fix markers in `origin/main`'s copy, whose update path is a bare
`_write_baseline(current)` → `return 0`.

check-params OK (949 declared), ruff clean, 52 partition tests pass.

Refs #1780, #2001
4444J99 added a commit that referenced this pull request Aug 8, 2026
…lt (#2070)

`check-board-partition` has held PR #2001 red since ~16:16 UTC on 8 fresh findings naming private
partner lanes. The gate's docstring names "the broker scrub" as those rows' owner four times. It
cannot be built as described, and the reason is structural rather than neglect:

  - dispatch.py:1846 hard-requires task.repo ("remote lane needs GitHub owner/repo")
  - dispatch.py reads tasks.yaml directly (644, 654, 701, 754)
  - tasks.yaml is tracked and organvm/limen is PUBLIC

One file must carry `repo` (or dispatch cannot route) AND not carry partner attribution (or it is
a public disclosure). No redaction placed in that file satisfies both, so a scrub that redacts
partner rows silently starves the work-supply for exactly the lanes it protects — invisible,
because the board still looks full.

This also invalidates the obvious fix, which was this plan's own first draft: teaching
generate-revenue-backlog.py and constellation/seed-tasks.py to redact before submission breaks
dispatch. Recorded here so it is not re-proposed.

The 425-line baseline is therefore not a backlog awaiting a scrub — it IS the estate's de-facto
answer, accept-and-pin, 200 `row` entries deep. Applied to a single public board, IF-ATOM-HOMING
("counts, ids and generalizations cross into the public tree; a statement never does") has one
satisfiable reading: a private board projection. That is an architecture build, not a patch.

Both remaining paths are disclosure decisions, so neither was taken unilaterally. Until one is
chosen PR #2001 stays red, which is the gate working, not a new failure.

Issue: #2069


Claude-Session: https://claude.ai/code/session_01RzELciaZt1hwavDvA4ey4G

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
4444J99 added a commit that referenced this pull request Aug 15, 2026
… + file L-FRICTION-FEDERATION-SOURCE (#2454)

The board-partition lever claimed the three red self-repair rungs as its
designed consequence; live diagnosis proved them independent defects (fixed
in #2447/#2448/#2449/#2453 + worker deploy) — the lever's live consequence
narrows to publication PR #2001 and the local private-custody cutover.
Also files the friction-federation data-source decision surfaced by the
notification-wall triage: the cloud routine's store has had no feed for 73+
days, and every fix arm is a disclosure or fleet-contract decision.


Claude-Session: https://claude.ai/code/session_011nj7dCkmwjt6Jqopop5ZHx

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
4444J99 added a commit that referenced this pull request Aug 15, 2026
…e + hydrate it every beat (#2461)

The partition's worker half shipped in #2299: canonical state lives in the
authenticated Durable Object and the published tasks.yaml is a counts-only
aggregate. The local half was missing — nothing hydrated private custody and
nothing knew the aggregate is not a work board. Landing publication PR #2001
in that state would have handed every local consumer a board with ZERO tasks,
which reads exactly like 'there is no work'.

- private_board.py becomes the single authority on which board local code
  OPERATES on. operational_board_path() derives custody from the public file's
  SHAPE (a cheap 4KB header probe, never a 5.8MB parse): explicit
  LIMEN_PRIVATE_TASKS wins, an aggregate public projection resolves to
  $LIMEN_PRIVATE_ROOT/board/canonical.yaml, and a full board is still its own
  custody. Missing custody under an aggregate raises PrivateCustodyUnavailable
  naming the fix — loud beats empty.
- cli.resolve_tasks_path() derives through it, so LIMEN_TASKS pointing at the
  post-cutover public file resolves by shape rather than by name.
-  prints what local operation resolves to (exit 3 =
  aggregate with no custody), so the beat and every operator share one answer.
- scripts/hydrate-private-board.sh + the hydrate-private-board rung refresh
  custody from the keeper BEFORE any board reader runs (stale custody produces
  stale CAS preconditions — the 2026-08-15 'exact revision moved' wedge).
  Self-arming off the public shape: a pre-cutover no-op with no flag to flip.
  Fail-open on keeper outages with custody present, fail-loud without it.

Verified live: the rung no-ops pre-cutover, and --force hydrates 3151 tasks
from the keeper. test_private_board 8/8 (incl. the aggregate-without-custody
must-raise property); private_board + board_partition + dispatch 340/340;
check-params OK (994 declared, no new hardcodes).


Claude-Session: https://claude.ai/code/session_011nj7dCkmwjt6Jqopop5ZHx

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
4444J99 added a commit that referenced this pull request Aug 15, 2026
… is lawful (#2469)

* feat(board): derive private custody from the public projection's shape + hydrate it every beat

The partition's worker half shipped in #2299: canonical state lives in the
authenticated Durable Object and the published tasks.yaml is a counts-only
aggregate. The local half was missing — nothing hydrated private custody and
nothing knew the aggregate is not a work board. Landing publication PR #2001
in that state would have handed every local consumer a board with ZERO tasks,
which reads exactly like 'there is no work'.

- private_board.py becomes the single authority on which board local code
  OPERATES on. operational_board_path() derives custody from the public file's
  SHAPE (a cheap 4KB header probe, never a 5.8MB parse): explicit
  LIMEN_PRIVATE_TASKS wins, an aggregate public projection resolves to
  $LIMEN_PRIVATE_ROOT/board/canonical.yaml, and a full board is still its own
  custody. Missing custody under an aggregate raises PrivateCustodyUnavailable
  naming the fix — loud beats empty.
- cli.resolve_tasks_path() derives through it, so LIMEN_TASKS pointing at the
  post-cutover public file resolves by shape rather than by name.
-  prints what local operation resolves to (exit 3 =
  aggregate with no custody), so the beat and every operator share one answer.
- scripts/hydrate-private-board.sh + the hydrate-private-board rung refresh
  custody from the keeper BEFORE any board reader runs (stale custody produces
  stale CAS preconditions — the 2026-08-15 'exact revision moved' wedge).
  Self-arming off the public shape: a pre-cutover no-op with no flag to flip.
  Fail-open on keeper outages with custody present, fail-loud without it.

Verified live: the rung no-ops pre-cutover, and --force hydrates 3151 tasks
from the keeper. test_private_board 8/8 (incl. the aggregate-without-custody
must-raise property); private_board + board_partition + dispatch 340/340;
check-params OK (994 declared, no new hardcodes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nj7dCkmwjt6Jqopop5ZHx

* feat(board): make the board gates shape-aware so the public aggregate is lawful

tasks.yaml has two lawful shapes — the pre-cutover full board and the keeper's
counts-only public aggregate — and two organs misread the second in opposite,
equally damaging ways:

- validate.yaml carried the board contract as an INLINE heredoc that assumed
  the full-board shape, so the aggregate failed with 'Missing version'. That is
  the single gate that has held publication PR #2001 red. The contract moves
  into scripts/validate-task-board.py (the named predicate, runnable locally,
  testable) which now branches on shape. The aggregate arm is deliberately
  STRICTER than the full-board arm: zero task rows AND no task-material field
  anywhere in the document, because the partition exists precisely so partner
  attribution never reaches a public surface. --require-shape pins an expected
  shape for callers that know which side of the cutover they are on.

- heal-board.py reads zero task rows as a COLLAPSE (total <= floor) and
  restores the board from git HEAD. Post-cutover that is an organ undoing the
  architecture it lives in, every beat, forever. A lawful aggregate is now
  recognized before the collapse arm; lifecycle repair runs against private
  custody (repair_lifecycle takes the board it repairs), and missing custody is
  loud rather than a restore.

Verified against the REAL published aggregate (origin/tabularius/board-projection):
the exact document that fails 'Missing version' today now validates
(total=3156, zero rows, no task material). Full board still validates
unchanged (3111 tasks). New suite 6/6; check-board-partition clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nj7dCkmwjt6Jqopop5ZHx

* fix(board): run status semantics before the schema contract

CI caught a real regression: cli/tests/test_validate_task_board.py feeds
deliberately minimal fixtures (a two-row board proving a duplicate id, no
version or budget), and running the newly-absorbed contract check first
answered every one of them with 'missing version' — hiding the defect each
test actually exercises. Order changes which failure surfaces, never whether
the contract is enforced: a real board missing its version still fails, just
after its statuses are judged.

test_validate_task_board + test_board_aggregate_shape 8/8; the live full board
and the real published aggregate both still validate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nj7dCkmwjt6Jqopop5ZHx

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
4444J99 added a commit that referenced this pull request Aug 15, 2026
…RTITION-DECISION) (#2480)

The operator directed Option A on 2026-08-15 and the build landed in four merged
PRs. B was never taken, so no partner-lane attribution was published — the fork
is closed except for one mechanical step, and the lever should say so rather
than keep presenting a choice that has been made.

Built: private custody DERIVED from the public projection's shape plus a
hydrate-private-board beat rung (#2461); validate and heal-board made shape-aware
so the counts-only aggregate is lawful rather than 'Missing version' / 'collapsed'
(#2469); all 24 board readers routed through the custody resolver behind a
shrink-only predicate, with PrivateCustodyUnavailable inheriting BaseException so
a reader's broad except cannot turn missing custody into a silent 'zero tasks'
(#2478).

Remaining: merge publication PR #2001, which needs one fresh keeper republish to
re-run the now-fixed validate gate — blocked by the keeper's Durable Object write
exhaustion (#2054), not by any decision of the operator's. The lever
stays open only so that last step keeps a visible owner; close it when #2001
merges.


Claude-Session: https://claude.ai/code/session_011nj7dCkmwjt6Jqopop5ZHx

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@4444J99
4444J99 merged commit c0768c9 into main Aug 16, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lifecycle:delivery Deliverable work eligible for reviewed merge drain

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants