Skip to content

Make Workstreams readable and advance PRs from All PRs - #12

Merged
mjsz merged 267 commits into
mainfrom
fix/workstreams-pr-advancement
Oct 6, 2026
Merged

mjsz merged 267 commits into
mainfrom
fix/workstreams-pr-advancement

Conversation

@mjsz

@mjsz mjsz commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Workstreams now uses effort cards to read exact PR and thread status, and All PRs to advance work. Cards link directly to the chosen PR; All PRs exposes the existing confirmation, merge-preview, feedback, hold, and review actions. Other open PRs can start a fresh worker conversation, including PRs whose previous worker was archived. Both lists support group selection: Advance selected previews the exact selected PRs, binds their shown heads, then starts one fresh conversation for the eligible selection. The Other heading has a select-all checkbox; mixed selections across both lists work. Existing workers do not block that explicit restart; holds, stopped efforts, and changed heads still do.

Plan Advance All starts a planning thread for every open PR except held PRs. It saves cached PR, checkout, effort, Linear, dependency, and worker facts once, supplies a compact attention index, and uses bounded Jev clustering with a rules fallback. The thread prioritizes and proposes a plan; it does not execute it. Held PR records are excluded from the saved context and Jev input.

This brings the existing Workstreams deck branch into main, including retirement of the old Pipeline/Work/Roster execution surfaces. It also fixes monorepo PR project lookup and creation of a missing effort parent, and includes the completed thread-effort read/refresh optimization. Other plugins are unchanged.

Validation: typecheck, plugin build, 1,590 passing tests, browser bundle/isolation and migration-upgrade checks, and production-component renders at desktop and phone widths, including group selection and its exact-scope confirmation. New planner and fresh-worker tests use a fake host; no real worker or GitHub write was triggered during verification.

mjsz added 30 commits September 26, 2026 22:29
mjsz and others added 29 commits October 1, 2026 09:46
5105a75 removed the Advance engine, but every writer still read its
saved jobs as a live lock. Nothing can start, settle, or cancel a job
any more, so a job a reload left queued, running, or uncertain blocked
Address, merges, inventory writes, thread messages, and effort archive
and merge on its PR and checkout permanently. thread_message told you
to cancel the job, but no cancel exists.

- Delete advance.reserved(), its status set, and the routing-facts
  schema only the lock read.
- Drop the Advance check from withPrWriter, directActionRun, agentOn,
  inventoryActions.writer, thread_message (owner and queue mode), effort
  archive, effort merge, and the merge preview's blockers.
- Keep list(): thread links, reconcileThreadIntent, and All PRs' merged
  or closed settlement still read the history.
- Replace the test that pinned the lock with ones that merge, request
  review, list for Address, and message a PR whose only owner is an
  unsettled legacy job.

No schema or migration change, and nothing writes advance_batches. The
rollback build reads the store as before and restores the lock. The live
store has 37 legacy jobs, none unsettled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Whether a PR was on Your turn, and whether Address could take it, was
decided in five places with different rules. All PRs read the row's lead
button, so a PR with an approval's note and a thread at work on it stayed
on Your turn while the same PR with a comment left. An old batch's link
switched off the at-work check for good, so All PRs offered a checkbox the
server then refused. The deck never read Dismiss, so its Address sent a PR
you had dismissed, and an old batch's Idle link led the row over the thread
now working the PR.

- Add turnOf to your-turn.ts: a pure, browser-safe rule over a PR's
  facts (feedback owed, hold, effort pile, Dismiss, a thread at work, and
  where its last Address sent it). It returns where the PR lists (turn,
  dismissed, held, in-flight, or other) and whether Address takes it, or
  why not. It never reads the row's lead button.
- yourTurn reads the PR's facts only. It no longer takes attention's
  reasons or a hold flag, and reads the approval's note through
  feedbackToAddress.
- All PRs, its badge, and its checkboxes read the row's turn. The deck
  files a PR a thread works on In flight, leads with the old batch's link
  only while the PR is on Your turn, and addresses only Your turn rows.
  Deck rows carry turn in place of yourTurn.
- The Address listing and the dispatch re-checks, before and after the
  GitHub read, go through turnOf. Dispatch now refuses a PR with no
  feedback in the listing's words: "No feedback waits on you."

A done or archived effort's PR stays on Your turn, but Address no longer
offers to take it. No store or schema change; a rollback to a993fcf reads
the same store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An Address batch thread's link to each PR lived in the board's run log, and
Sent rebuilt it by comparing a claim's start with the batch's confirm time.
The run log's rules for one action per thread broke batches: a batch of more
than one PR never read as Needs you, a failed one read as Idle with no reason,
and 200 newer runs pruned the link, so the PR lost its thread on its row and
the card.

- Add pr_threads (pr_url, thread_id, batch_id, linked_at), an appended
  migration. Dispatch writes a row per PR when the thread's start returns,
  and spawn metadata names the batch. Recovery after a reload writes the
  link when it binds a claim to the thread BB made. Each load copies links
  an earlier build left only in the run log.
- Sent joins the PR's newest Address item with its newest link and BB's
  status for that thread. A thread asking you something reads Needs you,
  whatever the batch's size, and a failed thread reads "Failed: <why>". A
  refusal keeps the older batch's thread linked. No time comparison
  decides which batch a thread belongs to.
- A batch thread holds its PRs while BB lists it starting, queued, at work,
  or asking you, or, not listed yet, within two minutes of its link and
  after the last thread list began. A claim whose start hasn't returned
  still holds its PR. Agent and thread starts, messages, and Address read
  that one rule; the run log's open claims no longer count once bound.
- Work-context links read pr_threads too, so the card and the row's
  executor keep the batch thread after pruning.

Dispatch still writes, binds, and signals ADDRESS_RUN rows for one release.
A rollback to a993fcf reads them as before and ignores pr_threads and the
extra metadata key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dcc7c6e read a batch thread's hold from BB's word alone, and took two
silences for a finish that a993fcf never trusted. After a reload, until the
first thread list answered, a thread linked over two minutes ago read as
gone. A new thread BB listed idle before its first turn read as done. Either
way Address listed its PRs again and started a second batch thread on them.

- Before any thread list answers after a load, a batch thread is starting:
  it holds its PRs as its open claim did. A list that never answers holds
  them until one does.
- Under two minutes from its link, a thread BB lists idle or failed is
  starting unless its own idle or failed event said so: the grace
  reconcileRuns gives a claim. The event map that kept why a thread failed
  now keeps that its turn ended.

No store change. A rollback to a993fcf reads the store as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8112413 stopped a checkout scan from rewriting an authored PR, but the scan
still stamped the PR read: it set checked_at to now and cleared a failed
read's time and reason, over facts it didn't write. The row then read
"checked just now" instead of "read failed", and a Refresh that waited out
the scan took that stamp for its own read and skipped GitHub.

- observe() marks read only the open PRs the inventory doesn't keep. An
  authored PR's read stays the inventory's poll and Refresh's. Merged and
  closed PRs still leave the inventory as before.

No schema change. A rollback to a993fcf reads the store as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dcc7c6e made BB's word decide whether a batch thread holds a PR, but kept
the run log's copy of its claims deciding it in four places. Effort archive,
the merge preview, and the merge itself counted only running claims, so a
batch thread resumed after its claims ended didn't stop them: the effort
archived, and the preview showed no blocker. A message to a PR's own thread
counted claims BB's word had ended, and refused what Address let through.

- Archive, the merge preview, and the merge wait for each PR addressHeld
  holds, or its checkout, as they wait for an open run. The preview names
  the PR.
- None of them, nor thread_message's owners, read an Address claim from
  the run log. A claim whose start hasn't returned still holds through
  addressHeld.
- Name the run log's remaining Address readers: the badge, Map rows, thread
  links, and the read of its PRs once claims end.

No store change. A rollback to a993fcf reads the store as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
333fb77 made the deck's Address skip a PR you dismissed from Your turn, but
the deck still filed it by the button its row leads with: under Work,
counted in Needs you. All PRs lists it apart, under Dismissed, so the deck
kept asking for a move you had declined.

- place() files a row turnOf lists as dismissed In flight, as it does a
  row a thread is at work on. It leaves Needs you, the card's next steps,
  and Fix and Ask, until the head moves or a person says more.

No store change. A rollback to a993fcf reads the store as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The card now leads with up to three moves, ranked by one rule (someone
waits on you, one step from merged, your blockers, a reviewer holding a
PR 4+ days), each over All PRs' own rows. Chores fold into one Advance
line. A finish line shows tickets done, the project target or cycle end,
and an ETA at the last 14 days' merge pace; p opens five answers. Notes,
Threads, Linear, Held, and All PRs become footer toggles. Strip chips
count Your turn only.

Removed: the bento tiles, the nine sections and their buttons, need-you
counts, four of Advance's five entry points, the deck's batch bar, and
ghost, lands-here, and moved-to rows. Accept moves to ⇧A so p can open
progress; Revoke confirmation moves to ⌘K.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
detailQuery now asks for priority, priorityLabel, estimate, createdAt,
startedAt, completedAt, and canceledAt. The fields are optional on
LinearDetail, so a row cached before them still reads; it refills when
its 12-hour cache runs out rather than all at once. linear_detail.detail
is JSON, so there is no migration.

The scan's sync also covers live efforts' own tickets and the tickets of
PRs merged in the last 14 days, so a ticket's state no longer freezes
when its last PR merges. Each ticket still waits out its 12-hour cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A card's moves end with Reconcile when Linear and GitHub disagree: one
line per mismatch, with its tickets and one button. "2 Done in Linear ·
2 PRs open" shows those PRs under it; "1 open with every PR merged"
opens the ticket in Linear. It writes nothing and has no key, so the
hint bar skips it. A held effort shows none.

Each row on a card carries its ticket on a chip, with Linear's priority
as a glyph, red only for Urgent. The p expand adds a Left line, such as
"1 Urgent · 1 High · 6 of 9 pts left", when Linear gives priorities or
points.

deck_get reads each merged PR's tickets, so a ticket still open after
its PRs merged in the last 14 days counts. The preview fixture gains
priorities, points, and both mismatches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
classify_move puts PRs All PRs lists into an effort, One-offs, or a new
effort by name, out of whichever effort has each now, or none. It is one
action through the assignment store's move, with an audit row per PR
naming where it was, so classify_undo puts each back and removes a new
effort again. A PR that isn't open now, a done or archived effort, or a
taken name refuses it, and a destination it would have made is removed.

classify_get also lists the efforts on the active pile, One-offs aside,
and the suggestions you dismissed, which classify_dismiss keeps in KV as
one key per PR naming the effort. No migration: the rows use existing
sources and the from_effort_id column, so the last build still undoes
them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion

Every open row in All PRs selects now, not only Your turn's. Move to
effort…, or e, opens a small type-to-filter picker over the selection
bar: the active efforts, One-offs, and a new effort by the typed name.
Enter moves the selection through classify_move at once, with Undo, and
the rows regroup under their new effort. e with nothing selected moves
the focused row.

A row under No effort shows the classifier's pick for it as a quiet
"→ Effort?" chip, with its signals on hover. A click or ⇧A accepts it
through classify_assign, as a service card's Accept does, with Undo; ×
hides that effort's suggestion for the PR, and Undo shows it again. A
No effort group with several offers Accept all suggestions (N). Only an
effort pick at medium or better shows: a low or tied pick, a new
effort, and One-offs show nothing.

Address stays Your turn's: with any other row selected, the bar's
Address is off and says "Address takes Your turn rows only.", and b
says why. e and ⇧A are the registry's existing Move and Accept keys,
so the hint bar, ⌘K, and ? list them in All PRs too; y is Mark ready.

The deck's card rows keep their checkboxes on Your turn alone. The
inventory preview gains routing frames at 1100 and 420 px, with the
picker open and a new effort typed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The card freed a row as soon as its write was sent, but deck_batch_plan
still skips a sent write until you mark the row seen after it. So a card
could read Advance 2 or Fix 1… and open a listing that skipped a row as
"A write on it is waiting or just ran." for up to a day.

The card now reads the plan's own rule (counted): a write holds its row
from its Undo window until you mark the row seen after it lands. A
thread's fix leaves GitHub as it was, so only your look says the read
is past it. Mark seen offers to settle a write that landed. A test pins
the card's chores and Fix move to what the plan takes, for each state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reconcile's "N open with every PR merged" read each ticket's cached
Linear state, which the sync refetched only once its 12-hour cache ran
out. A ticket read while its PR was open kept that state after the
merge, so nearly every merge showed a false mismatch for up to 12 hours.

Linear moves a merged PR's tickets itself, soon after the merge. So the
sync now reads a ticket again after its newest merge, on each scan until
a read lands 10 minutes past it, and Reconcile counts a ticket only from
such a read. linear.readAt gives the deck each ticket's read time. The
newest merge naming a ticket in any effort counts. No migration: the
cache already kept fetched_at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"N of M done" counted canceled tickets in M but never in N, so an
effort whose tickets were all done or canceled could never read all
done: Done, Done, Canceled read "2 of 3 done" beside "0 open tickets".
M is now the tickets Linear didn't cancel. With every ticket canceled,
the line counts merged PRs, as with no Linear data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The composer's effort chip and its popover still showed the old amber
"N need you", chores included, so an effort read 0 on its strip chip
and, say, 6 amber above the composer. The deck also ordered a session's
first strip by that count.

deck_get's cards now carry yourTurn: the rows All PRs lists on Your
turn, as the strip chip counts them. The chip, its popover's choices,
and the active pile's order read it, so amber means a person waits on
you everywhere. The thread effort contract's needsYou becomes yourTurn,
and the chip says "5 your turn" as the strip chip does. needsYou stays
on deck_get for agents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vancement

# Conflicts:
#	plugins/workstreams/ghactions.test.ts
#	plugins/workstreams/howto.tsx
#	plugins/workstreams/inventory-server.test.ts
#	plugins/workstreams/outcomes.test.ts
#	plugins/workstreams/workstream-attention.test.ts
#	plugins/workstreams/workstreams-inbox.test.ts
@mjsz
mjsz merged commit 7a8d754 into main Oct 6, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant