feat(hosted): a plan carries its rollout beside its document (rollout 1/15)
CH
5d ago+10056+10056+01234567890123456789012345678901234567890123456789-81-81-01234567890123456789
Rollout 1 of 15: a plan's work cut into stacked pull requests. This PR adds the rollout as structured data beside the plan, and nothing writes to it yet.
The rollout is not a field and never part of the document's text. It sits beside the plan's text. Since #2256, every plan is one document.
What changes
- Wire (
packages/hosted/src/plan-wire.ts):rolloutItemSchema{ title, scope, base: "main" | <earlier position>, acceptance: NullOr(text) }, plusrolloutSchema(at mostROLLOUT_BOUNDS.MAX_PULL_REQUESTS= 20 items). A base at or past the item's own position is refused, so a rollout can never contain a cycle.- Also new:
ROLLOUT_BASE, and theRolloutItemandRollouttypes. planSchema.rolloutis required on the plan.planRevisionAnswerSchema.revisiongainsrolloutandpreviousRollout.
- Stacks (
plan-wire.ts):rolloutStacksOf(rollout): readonly RolloutStack[], whereRolloutStack = { bottom, positions, base: "main" | position }. It cuts the rollout into chains, one GitHub stack each, because a stack is linear. Every PR on main starts a chain on main. At each PR, its lowest-numbered child continues the chain and every other child starts a new chain based on that PR. Chains are sorted by bottom position. The stack keeper's brief (PR 7) and the Stack tab will use it. - Store:
0096_plan_rollout.sql(0088–0095 were taken on main) addsplan.rolloutandplan_revision.rollout, bothjsonb not null default '[]'.- The store answers
plan.rollout. - Every revision records the rollout as it stood. A restore puts the revision's rollout back with its content, and counts a changed rollout as one change.
- A save also accepts a rollout, ready for PR 2.
- The store answers
- Drawing it, never storing it:
rolloutSection(rollout)inplan-template.tsdraws## Rolloutas a numbered list:**Title** · base: mainor· base: #2, the scope under it, andDone when: …where set. An empty rollout draws_No rollout yet_.planMarkdown(Copy, and a coding agent'splan_snapshot) is the plan's name as a title over its text, then this section. A code fence left open, whether at the end of the text or in a PR's scope, is closed before what follows it, so neither the rollout nor a "Done when" line is drawn as code.- The window draws it read-only after the document. The History tab shows a version's rollout against the one before it (not in the single-section drill-in).
- The planning model's standing context carries
[rollout]as JSON once the rollout has items. ROLLOUT_PURPOSEandROLLOUT_BARare added to the model's section list.
- Fix found on the way:
compose-planning.ts'ssummaryOfnow stripsrollout. Without that, pinning, archiving, or editing a plan would emit a list row that the planning view's schema refuses. tests/inbox-migration.test.ts(from main): it migrates to 0087 and wrote its plan through the store. The store's write now names the rollout column, so the test writes a raw plan row naming its own columns, as it already does for its agent row.- Fixture and docs: the fixture plan carries a three-PR rollout.
docs/PLANNING.mdandREADME.mddescribe the rollout. - Goldens (
LUKE_UPDATE_FIXTURES=1):- New:
plan-wire-rolloutItemSchemaandplan-wire-rolloutSchema. - Updated with the new
rolloutkeys:plan-wire-planSchema,planAnswerSchema,planRevisionAnswerSchema, andplan-edit-planEditAnswerSchema. - The notetaker's output schema and
PLAN_FIELDare unchanged.
- New:
Tests
plan-markdown.test.ts:- The rollout is drawn after the plan's text as numbered pull requests.
- An empty rollout ends every copy with "No rollout yet".
- A self, later, or over-the-limit base is refused.
- A fence left open at the end of the text or in a scope is closed before the rollout or the "Done when" line.
plan-wire.test.ts:rolloutStacksOfon a chain, a sibling pair, two PRs on main, a 15-PR tree, and an empty rollout, each with expected values written by hand.plan-copy-act.test.ts: the copy act accepts the longest possible rollout.plan-revisions.test.ts:- A plan returns its rollout beside its text, never inside it.
- A revision keeps the rollout and its previous one, and a restore brings it back.
tests/plan-rollout-migration.test.ts(new, since main deletedplan-template-migration.test.ts): a plan and revision written before 0096 read back with an empty rollout and unchanged text, and running 0096 again over a database that already has the column keeps every rollout. The migration usesADD COLUMN IF NOT EXISTS, because this branch's preview database already had the column from when the migration was numbered 0088/0090.hosted-planning.test.ts: the standing context carries the rollout once it has items, and not before.plan-body.test.ts: the rollout is drawn last and read-only, even with an edit door.plan-history-model.test.ts: a version marks the pull request it changed.compose-planning.test.ts: five tests catch thesummaryOfbug../scripts/check.shexits 0, andpnpm --filter @luke/web test:storepasses on a real Postgres 16 (68 files, 731 tests).
Mac
CI could not verify this. It adds a section to the window's plan view and version history (CSS in planning.css and desktop.css) and changes the fixture plan. This was built on a Linux VM, so ./scripts/verify.sh has not run and its PNG is still owed (LUKE-159 style) before this merges.
🤖 Generated with Claude Code