feat(hosted): a plan carries its rollout beside its document (rollout 1/15)

CH
5d ago
+10056-81

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) }, plus rolloutSchema (at most ROLLOUT_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 the RolloutItem and Rollout types.
    • planSchema.rollout is required on the plan.
    • planRevisionAnswerSchema.revision gains rollout and previousRollout.
  • Stacks (plan-wire.ts): rolloutStacksOf(rollout): readonly RolloutStack[], where RolloutStack = { 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) adds plan.rollout and plan_revision.rollout, both jsonb 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.
  • Drawing it, never storing it:
    • rolloutSection(rollout) in plan-template.ts draws ## Rollout as a numbered list: **Title** · base: main or · base: #2, the scope under it, and Done when: … where set. An empty rollout draws _No rollout yet_.
    • planMarkdown (Copy, and a coding agent's plan_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_PURPOSE and ROLLOUT_BAR are added to the model's section list.
  • Fix found on the way: compose-planning.ts's summaryOf now strips rollout. 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.md and README.md describe the rollout.
  • Goldens (LUKE_UPDATE_FIXTURES=1):
    • New: plan-wire-rolloutItemSchema and plan-wire-rolloutSchema.
    • Updated with the new rollout keys: plan-wire-planSchema, planAnswerSchema, planRevisionAnswerSchema, and plan-edit-planEditAnswerSchema.
    • The notetaker's output schema and PLAN_FIELD are unchanged.

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: rolloutStacksOf on 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 deleted plan-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 uses ADD 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 the summaryOf bug.
  • ./scripts/check.sh exits 0, and pnpm --filter @luke/web test:store passes 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


Open workspace in Conductor