Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
267 commits
Select commit Hold shift + click to select a range
197edeb
Keep Pipeline agent updates compact after sending
mjsz Sep 27, 2026
9e2f021
Fix empty program lifecycle in board responses
mjsz Sep 27, 2026
1ca9fa4
Add direct card controls and explicit effort moves
mjsz Sep 27, 2026
e14aecb
Document Workstreams data flow and agent hierarchy
mjsz Sep 27, 2026
602cd62
Link Pipeline stack badges to their parent cards
mjsz Sep 27, 2026
7118adb
Refine Pipeline details and mirror native thread indicators
mjsz Sep 27, 2026
9969e38
Start card agents without requiring repository worktrees
mjsz Sep 27, 2026
41437bd
Fix context workspace placement and rejected launch recovery
mjsz Sep 27, 2026
f2b8156
Share PR ownership and thread context across Pipeline controls
mjsz Sep 27, 2026
0b904c9
Require current approval feedback evidence before merge
mjsz Sep 27, 2026
8f42024
Preserve cached PRs with legacy approval follow-up fields
mjsz Sep 27, 2026
3477fcb
Scope Advance scheduling to conflicting work
mjsz Sep 27, 2026
ed91bca
Accept complete paginated review thread counts
mjsz Sep 27, 2026
f9545a8
Preserve provenance for audited feedback reconciliation
mjsz Sep 27, 2026
b6eb1a8
Route Workstreams threads through effort and repository parents
mjsz Sep 27, 2026
c315c30
Repair verified unassigned worker placement safely
mjsz Sep 27, 2026
c844c5f
Wait for parent workspace attachment before placing threads
mjsz Sep 27, 2026
9233b01
Prioritize prerequisite PRs and group visible stacks
mjsz Sep 27, 2026
3703236
Separate open PRs from branch cards in Pipeline counts
mjsz Sep 27, 2026
75cad7a
Refresh individual PRs and reconcile unchanged feedback evidence
mjsz Sep 27, 2026
402d1f1
Bind preparation direction to Advance previews
mjsz Sep 27, 2026
d28e6df
Add scoped planning conversations to Pipeline
mjsz Sep 27, 2026
a04abeb
Keep conversation controls reachable in short panes
mjsz Sep 27, 2026
2c70e55
Preserve Advance history and durable worker PR links
mjsz Sep 28, 2026
e29205d
Use Sol for Workstreams agent execution
mjsz Sep 28, 2026
d9586c7
Reconcile Advance results and continue safe queued work
mjsz Sep 28, 2026
3b78618
Show awaiting review for fresh unreviewed PRs
mjsz Sep 28, 2026
c609706
Allow stacked PR preparation and recover absent launches
mjsz Sep 28, 2026
af745a0
Add a Work view for saved requests and preparation
mjsz Sep 28, 2026
e18f0dc
Keep Work details readable in narrow panes
mjsz Sep 28, 2026
b9abde0
Preserve effort identity through archive and merge
mjsz Sep 28, 2026
77eca2b
Add effort administration with recoverable merge previews
mjsz Sep 28, 2026
18ccd95
Preserve deployed migration order for effort admin
mjsz Sep 28, 2026
9894a0d
Include recorded PR ticket references in effort membership
mjsz Sep 28, 2026
3406fcf
Keep dependency ticket references out of ownership cohorts
mjsz Sep 28, 2026
4c6456e
Pin the deployed migration prefix through effort admin sync
mjsz Sep 28, 2026
9492ae6
Replace client identifiers in fixtures and help text
mjsz Sep 28, 2026
0929efb
Treat in-progress checks with empty conclusions as pending
mjsz Sep 28, 2026
d0efbbd
Choose worker and planning models from settings
mjsz Sep 28, 2026
a58a4a2
Classify PR gates from live preparation facts
mjsz Sep 28, 2026
5a28010
Pick one current legacy attempt per PR
mjsz Sep 28, 2026
849c788
Assign permanent roster numbers and snapshots
mjsz Sep 28, 2026
6ac29b7
Serve the effort roster read model and CLI
mjsz Sep 28, 2026
7379951
Refresh one roster PR without the scan lock
mjsz Sep 28, 2026
918d3cc
Keep fixture text in the fictional bookstore domain
mjsz Sep 28, 2026
0c6b4ac
Store execution mode, persist v2 targets, and fence legacy launchers
mjsz Sep 28, 2026
6017ca6
Opt an effort into v2 with a chosen parent thread
mjsz Sep 28, 2026
f8007a6
Parse numbered effort commands
mjsz Sep 28, 2026
3880237
Add the action recipe catalog and work-order builder
mjsz Sep 28, 2026
35e3d3f
Select reuse-first execution resources
mjsz Sep 28, 2026
fe34dcc
Decide each PR's next step
mjsz Sep 28, 2026
5e9099c
Derive the outcome evidence contract
mjsz Sep 28, 2026
a1957ad
Admit numbered instructions with one acknowledgment
mjsz Sep 28, 2026
55c44c5
Record one decision per real choice and answer it inline
mjsz Sep 28, 2026
ecab597
Parse typed completion envelopes with a compatibility adapter
mjsz Sep 28, 2026
c026d06
Claim one writer and launch attempts with readback recovery
mjsz Sep 28, 2026
60bebec
Ingest worker results and SDK signals, and verify against fresh facts
mjsz Sep 28, 2026
545d3d6
Run the v2 reconciler as a background service
mjsz Sep 28, 2026
27865d7
Run lifecycle and check actions from code
mjsz Sep 28, 2026
aff1106
Repair mechanical failures within a bound
mjsz Sep 28, 2026
fed1f18
End hold reasons at the next command and refuse repeated answers
mjsz Sep 28, 2026
0ba1106
Keep a renamed thread's title when finishing an effort merge
mjsz Sep 28, 2026
0c1fb2f
Pin the plugin SDK to 0.5.9
mjsz Sep 28, 2026
4ac0496
Expose roster presentation facts
mjsz Sep 28, 2026
b47677e
Structure the acknowledgment and keep the last command
mjsz Sep 28, 2026
708a1f7
Refuse merge previews under a v2 claim
mjsz Sep 28, 2026
0a57514
Number system issues and name their recovery
mjsz Sep 28, 2026
9ec5f82
Carry decision evidence and recommendations to the roster
mjsz Sep 28, 2026
f3191de
Hold roster answers ten seconds for Undo
mjsz Sep 28, 2026
57a28c2
Tell rosters when their PRs change on GitHub
mjsz Sep 28, 2026
0521238
Share roster words from a module that imports nothing
mjsz Sep 28, 2026
e7d08e3
Guard the browser bundle against server modules
mjsz Sep 28, 2026
b164f45
List each effort's roster with its mode and parent thread
mjsz Sep 28, 2026
1656c70
Derive the roster pane's view from server state
mjsz Sep 28, 2026
c7ca5d4
Show the roster beside the effort thread
mjsz Sep 28, 2026
61310ca
Add a Roster button to effort parent thread headers
mjsz Sep 28, 2026
f3f5d98
Share effect labels with the roster pane
mjsz Sep 28, 2026
0f47cf3
Command the roster from a box pinned under its rows
mjsz Sep 28, 2026
075f139
Show an effort parent's asks and a command field in its composer banner
mjsz Sep 28, 2026
334b3e8
Answer decisions from roster cards with ten seconds to Undo
mjsz Sep 28, 2026
ba992b2
Recover system issues from the roster
mjsz Sep 28, 2026
72384ac
Merge verified candidates from one fresh preview, never on Enter
mjsz Sep 28, 2026
9b5a9ef
Keep keyboard focus on an ask you open
mjsz Sep 28, 2026
8fc3661
Show roster state on legacy cards for v2 PRs
mjsz Sep 28, 2026
8eaae0b
Prove one writer and restart recovery
mjsz Sep 28, 2026
a42e165
Pass the 22-PR acceptance case
mjsz Sep 28, 2026
d08ff40
Use a generic branch name in the rebase fixture
mjsz Sep 28, 2026
8515421
Name what each open PR waits on, who acts, and for how long
mjsz Sep 28, 2026
972711d
Date the waits that PR attention ages
mjsz Sep 28, 2026
88deaa0
Poll every open PR you author with one batched read
mjsz Sep 28, 2026
0e228b4
Show every open PR by effort in one inventory read
mjsz Sep 28, 2026
8cb91b3
Act on an inventory row with one authorized click
mjsz Sep 28, 2026
348b281
Leave out effort PRs a legacy job saw merge
mjsz Sep 28, 2026
8aaf55e
Shape the PR inventory screen from inventory_get
mjsz Sep 29, 2026
312d23d
Draw the PR inventory with its counts, groups, and rows
mjsz Sep 29, 2026
68b6817
Open Workstreams on the live PR inventory
mjsz Sep 29, 2026
3add51b
Read bb thread show exports in their nested shape
mjsz Sep 28, 2026
8ac3699
File a roster PR a legacy job saw merge as Done, then confirm it
mjsz Sep 29, 2026
75ea5a0
Rehearse the install migration on a copy with --migrate-copy
mjsz Sep 29, 2026
ec5e97c
Record your confirmation that approval feedback is handled
mjsz Sep 29, 2026
46fa750
Ask you to confirm an approval's comments and record it in one click
mjsz Sep 29, 2026
51c48a5
Confirm an approval's comments from its inventory row
mjsz Sep 29, 2026
a295fc8
Move efforts between active, held, and done piles
mjsz Sep 29, 2026
f8b6798
Mark standalone PRs as One-offs and undo a classification
mjsz Sep 29, 2026
3fd806d
Suggest an effort for each PR no effort owns
mjsz Sep 29, 2026
00a0ee7
Assign unsorted PRs to an effort or a new one from a selection
mjsz Sep 29, 2026
e19e8a3
Place new PRs by the standing rules you add, with an audit row each
mjsz Sep 29, 2026
8fae3c8
Keep each PR merge a read sees, dated by GitHub
mjsz Sep 29, 2026
fc6f1ce
Serve the effort deck: one compact card per effort, by pile
mjsz Sep 29, 2026
8650c64
Plan, confirm, and undo deck batches and Advance
mjsz Sep 29, 2026
d824114
Keep each deck view's place and settle its rows only on Mark seen
mjsz Sep 29, 2026
acfea95
Give the deck and All PRs one key registry
mjsz Sep 29, 2026
0125bc6
Shape the effort deck for the screen
mjsz Sep 29, 2026
4e2f04b
Draw the effort deck: strip, bento card, sections, and Unclassified
mjsz Sep 29, 2026
56cdd02
Open Workstreams on the effort deck
mjsz Sep 29, 2026
4160f3d
Hold a PR and use the deck's keys from All PRs
mjsz Sep 29, 2026
0b75759
Say a row that left the deck merged when a read saw it merge
mjsz Sep 29, 2026
5597ebb
Prove the deck writes only through its confirm and the merge preview
mjsz Sep 29, 2026
96bf88c
Say the panel opens on the effort deck, with All PRs one tab away
mjsz Sep 29, 2026
620a948
Refuse writes and merges on the PRs of an effort you hold
mjsz Sep 29, 2026
21c1630
Stop offering a row's Undo once its batch starts sending
mjsz Sep 29, 2026
15d8ca4
Mark deck rows whose last read failed or went stale
mjsz Sep 29, 2026
bd46d83
Read assignee, cycle, dates, and initiatives for tickets on your PRs
mjsz Sep 29, 2026
5e2d95a
Show each effort's Linear projects, cycle, people, and target date
mjsz Sep 29, 2026
ae9fcc8
Seed efforts from the Linear projects on your open PRs
mjsz Sep 29, 2026
9d31947
Add Seed from Linear to the Unclassified deck
mjsz Sep 29, 2026
fc62a5b
Suggest an effort from its Linear project, and let rules name one
mjsz Sep 29, 2026
4526e34
Show On hold on a held effort's PRs and refuse writes to a done one's
mjsz Sep 29, 2026
f64dd85
Stack the next efforts behind the deck's card
mjsz Sep 29, 2026
5d1ed00
Flip deck cards off and back onto the stack
mjsz Sep 29, 2026
8f3682e
Add a static preview of the effort deck
mjsz Sep 29, 2026
d947004
Make key badges legible on every button
mjsz Sep 29, 2026
2cfabad
Stop a ticket prefix alone from suggesting an effort
mjsz Sep 29, 2026
a5a61c4
Describe the effort deck in How this works
mjsz Sep 29, 2026
62a7d55
Read what a thread's effort chip and picker show
mjsz Sep 30, 2026
6f04385
Let Undo take back a thread's effort change
mjsz Sep 30, 2026
80b9673
Open the deck on the card a link names
mjsz Sep 30, 2026
7d5c988
Replace the thread effort modal with a popover on the effort chip
mjsz Sep 30, 2026
d445a02
Put PRs no effort owns on per-repo service cards
mjsz Sep 30, 2026
26f6c63
Promote a service card to an effort
mjsz Sep 30, 2026
1fa7cbe
Put every thread on a card, with Loose threads for the rest
mjsz Sep 30, 2026
643f433
Keep an emptied service card until Mark seen
mjsz Sep 30, 2026
60a5c22
Drop the last Unclassified wording
mjsz Sep 30, 2026
74cb76e
File held PRs under Held on every card
mjsz Sep 30, 2026
89958f9
Release held PRs through the listing confirm and Undo
mjsz Sep 30, 2026
b4b81c4
Advance one row from its line or the a key
mjsz Sep 30, 2026
c0038f3
Keep every drawn row until Mark seen, and say where one went
mjsz Sep 30, 2026
62f6df7
Show a row's Refresh at work
mjsz Sep 30, 2026
d763d6f
Forget a ghost's first-seen time once Mark seen settles it
mjsz Sep 30, 2026
3238275
Refuse a one-click confirmation that has no evidence of handling
mjsz Sep 30, 2026
c49ce13
Keep confirming review notes out of Advance and every batch
mjsz Sep 30, 2026
f732117
Ask a PR's thread to address its approval's notes
mjsz Sep 30, 2026
c4181e0
Show a PR's review notes and their evidence before any confirmation
mjsz Sep 30, 2026
0e2ad9d
Revoke your confirmation of a PR's review notes
mjsz Sep 30, 2026
a0eabe1
Say a PR ready on your confirmation is ready on your word alone
mjsz Sep 30, 2026
6c551b7
Never carry your confirmation to a new head
mjsz Sep 30, 2026
3964d74
Count only commits written after the approval as evidence
mjsz Sep 30, 2026
ed31125
Word a confirmation's evidence as the notes dialog does
mjsz Sep 30, 2026
a5e90a4
Cap the effort deck at bb's content column width
mjsz Sep 30, 2026
88e0632
Always show a row's PR number, truncating the repo instead
mjsz Sep 30, 2026
6889d0c
Move an effort's PRs to One-offs
mjsz Sep 30, 2026
9c667e5
Keep Markdown notes on each effort's card
mjsz Sep 30, 2026
0dd6b52
Show only what needs you or what's blocked from a card's header
mjsz Sep 30, 2026
ca461ca
Ask each PR's thread to fix its code work from the deck
mjsz Sep 30, 2026
b6544d8
Show PRs with feedback for you and a Back to me list
mjsz Sep 30, 2026
76ec06c
Compute Your turn from the inventory's own review reads
mjsz Sep 30, 2026
f188032
Show Your turn from Workstreams' own list, not the Reviews plugin
mjsz Sep 30, 2026
8aa3435
Count an approval as Your turn only when attention asks for its notes
mjsz Sep 30, 2026
bce924a
Ask a Your turn PR's thread to address its feedback
mjsz Sep 30, 2026
857b2c9
Offer Ask its thread only on a PR that has a thread
mjsz Sep 30, 2026
9fb3ec4
Name a reviewer's change request once in Your turn
mjsz Sep 30, 2026
718fa82
Read which review feedback still waits on your answer
mjsz Sep 30, 2026
f10f2bf
Hold a PR from ready and from merge while feedback waits on you
mjsz Sep 30, 2026
6123702
Keep a v2 roster row short of Ready while feedback waits on you
mjsz Sep 30, 2026
f864765
Keep a comment on Your turn until you reply, whatever a push or CI says
mjsz Sep 30, 2026
ee78852
Read whether a checkout's PR feedback was answered
mjsz Sep 30, 2026
2a80e17
Share one header across every Workstreams view
mjsz Sep 30, 2026
b963e21
Describe the shared header, Overview, and All PRs in the docs
mjsz Sep 30, 2026
610df9c
Drop the deck's unreachable Nothing is open state
mjsz Sep 30, 2026
fbb321f
Keep reviewer feedback until you reply on the PR, whatever links it
mjsz Sep 30, 2026
5b7bc0c
Leave linked follow-ups out of a confirmation's record
mjsz Sep 30, 2026
bbd8087
Write one batch thread's work order for Your turn feedback
mjsz Sep 30, 2026
effe07b
Plan Address batches and start their one thread at once
mjsz Sep 30, 2026
0ac021c
Start one batch thread that claims each Your turn PR it addresses
mjsz Sep 30, 2026
248e041
Select Your turn PRs and address them in one listing
mjsz Sep 30, 2026
ed68130
Describe Address selected in the docs
mjsz Sep 30, 2026
a1051f7
Keep Nudge off Your turn rows and name the re-request
mjsz Sep 30, 2026
7972379
Drop the batch prompt's lines that contradict replying to every note
mjsz Sep 30, 2026
0a9af4c
Recheck a batch thread's claims at the claim and release a gone thread's
mjsz Sep 30, 2026
0b7ae90
Add paginated compact Workstreams CLI view
mjsz Sep 30, 2026
65542b4
Start Address at once and track each sent PR on Your turn
mjsz Sep 30, 2026
c52d391
Show progress while a batch plans and sends
mjsz Oct 1, 2026
b800501
Name a batch thread by its PRs
mjsz Oct 1, 2026
7d08f94
Carry each PR's worktree and threads into its batch thread
mjsz Oct 1, 2026
78c3bc9
Count only real follow-ups on Your turn; batch every comment
mjsz Oct 1, 2026
4506382
Flag only unanswered follow-ups on Your turn
mjsz Oct 1, 2026
0891eff
Keep a person's thread on your list when a bot answers it
mjsz Oct 1, 2026
756e943
Lead with waiting feedback, not a re-request, on Your turn rows
mjsz Oct 1, 2026
28cde4c
Refresh a PR, a selection, or the whole list from GitHub
mjsz Oct 1, 2026
0b78582
Read a reviewer's turn as waiting, not Blocked, and keep sent chips
mjsz Oct 1, 2026
8a5be61
Revert "Read a reviewer's turn as waiting, not Blocked, and keep sent…
mjsz Oct 1, 2026
3154e83
Strip Your turn back to the Reviews model: one list, Dismiss, live links
mjsz Oct 1, 2026
64484ea
Hold a Dismiss on the newest word its row showed, not the click's time
mjsz Oct 1, 2026
a2eed88
Count drafts on Your turn for every clause, as Reviews does
mjsz Oct 1, 2026
dde6814
Keep an older batch thread's link when a newer Address is refused
mjsz Oct 1, 2026
b5b4758
Let a PR's own thread lead the deck row once its batch is done
mjsz Oct 1, 2026
beaf478
Say drafts count on Your turn in the skill reference
mjsz Oct 1, 2026
4a37682
Offer only Address, Refresh, and Dismiss on Your turn rows
mjsz Oct 1, 2026
8798403
Add a worktree, never a clone, for a batch PR with no checkout
mjsz Oct 1, 2026
e6539cf
Remove the Pipeline, Work, and legacy Board views
mjsz Oct 1, 2026
290ebec
Remove the Pipeline and Work server RPCs
mjsz Oct 1, 2026
e11133f
Remove the inbox model the removed RPCs left unused
mjsz Oct 1, 2026
5105a75
Replace the bulk Advance engine with a read-only history reader
mjsz Oct 1, 2026
3ea3377
Remove automatic dispatch
mjsz Oct 1, 2026
ce62a90
Remove the agent-driven Linear fetch
mjsz Oct 1, 2026
0dd4a0c
Remove the Roster view and the composer's parent banner
mjsz Oct 1, 2026
7c84a7a
Remove Effort-v2 execution
mjsz Oct 1, 2026
9477d3d
Stop pointing Ask and Fix at the removed New agent control
mjsz Oct 1, 2026
7a00f7c
Remove the nine RPCs no view calls
mjsz Oct 1, 2026
a993fcf
Remove the host reads and writes only those RPCs used
mjsz Oct 1, 2026
8112413
Stop scans from erasing review reads
mjsz Oct 1, 2026
928f9b4
Stop legacy Advance history from locking PRs
mjsz Oct 1, 2026
333fb77
Decide Your turn and Address eligibility with one rule
mjsz Oct 1, 2026
dcc7c6e
Store each batch thread's link once, and read its state from BB
mjsz Oct 1, 2026
c49742a
Hold a batch thread's PRs until BB says it finished
mjsz Oct 1, 2026
3035f73
Stop checkout scans from marking authored PRs read
mjsz Oct 1, 2026
7187086
Read a batch thread's hold one way in every writer
mjsz Oct 1, 2026
696dc93
File a PR you dismissed In flight on the deck
mjsz Oct 1, 2026
8e747fc
Peek the deck's next cards to the right, as the arrows move
mjsz Oct 1, 2026
ec3345b
Give All PRs the deck's column and row styles
mjsz Oct 1, 2026
02e5f01
Space out All PRs' effort groups and sections
mjsz Oct 1, 2026
bf6d01c
Give each PR row a second line so the title has room
mjsz Oct 1, 2026
06d491d
Name a PR's repository without its org in All PRs
mjsz Oct 1, 2026
bf647a2
Put each group's rows in a card under its title
mjsz Oct 1, 2026
447eaa5
Keep a row on Your turn while its own batch thread starts
mjsz Oct 1, 2026
d2e242b
Have a batch thread leave each PR mergeable once its feedback is handled
mjsz Oct 2, 2026
c4516c5
Open each effort card on its next moves and finish line
mjsz Oct 2, 2026
552a875
Read Linear priority, points, and dates, and keep merged tickets fresh
mjsz Oct 2, 2026
34c6704
Show where Linear and GitHub disagree, and each ticket's priority
mjsz Oct 2, 2026
38f9b9f
Move PRs between efforts from any group, and keep dismissed suggestions
mjsz Oct 2, 2026
d3a780a
Route PRs into efforts from All PRs, and offer each loose PR's sugges…
mjsz Oct 2, 2026
ad3ce99
Hold a row you acted on until Mark seen, as the batch plan does
mjsz Oct 2, 2026
f2f6323
Call a ticket open after its PRs merged only from a read since
mjsz Oct 2, 2026
a74749e
Leave canceled tickets out of the finish line's done count
mjsz Oct 2, 2026
75920cf
Count Your turn on the thread's effort chip and in the pile's order
mjsz Oct 2, 2026
ce77bab
Make Workstreams cards readable and advance PRs from All PRs
mjsz Oct 6, 2026
6f83a37
Merge remote-tracking branch 'origin/main' into fix/workstreams-pr-ad…
mjsz Oct 6, 2026
88e2655
Read thread effort from one snapshot and coalesce picker refreshes
mjsz Oct 6, 2026
762bc65
Let Other open PRs advance in fresh threads and verify clean installs
mjsz Oct 6, 2026
bfb78f6
Advance selected Other and mixed PR groups in one fresh thread
mjsz Oct 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions plugins/workstreams/.impeccable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## Design Context

### Users

Engineers coordinate pull requests, checkouts, and agent work across repositories from a dense control center. They scan for blockers, inspect readiness, move to linked threads, and take the next action without losing their place.

### Brand Personality

Calm, precise, and capable, with a little warmth. The interface should make complex work feel legible and under control.

### Aesthetic Direction

Use the host application's typography, theme, icons, and color tokens. Draw on the clarity of a focused PR inbox and the compact structure of issue triage. Keep the interface sophisticated and dense without crowding it.

### Design Principles

- Put the next useful action and its context first.
- Group related facts with spacing and alignment instead of nested cards.
- Reserve color for real status and keep secondary navigation quiet.
- Preserve keyboard access, clear labels, and readable layouts at narrow widths.
9 changes: 5 additions & 4 deletions plugins/workstreams/PLUGIN_OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ See work across repositories by ticket and by the action it needs next.

## Explore and act

- **Efforts:** Flip through one card per effort, each listing its open pull
requests by the move each needs. Every GitHub write lists each pull request
in a confirm first, and merges run only from a fresh merge preview.
- **All PRs:** See every open pull request you author or an effort names, with
the ones waiting on you first.
- **Map:** Explore ticket clusters within named efforts, programs, and domains.
Levels collapse when they add no useful grouping. Switch between theme and
risk, filter by status or code surface, and open linked agent threads.
- **Board:** Group checkouts by Action or Effort while keeping urgent work first
within each group. Find CI fixes, review responses, merges, and reviewer
nudges. Confirm direct GitHub actions, or review a prompt before an agent
starts work in a dedicated thread.
- **CLI:** Run `bb workstreams list [--json]` to read the board, `bb workstreams
refresh` to rescan, and `bb workstreams group <TICKET> <name>` to set an effort
name.
Expand Down
267 changes: 166 additions & 101 deletions plugins/workstreams/README.md

Large diffs are not rendered by default.

315 changes: 47 additions & 268 deletions plugins/workstreams/actions.test.ts

Large diffs are not rendered by default.

303 changes: 20 additions & 283 deletions plugins/workstreams/actions.ts

Large diffs are not rendered by default.

1,048 changes: 1,048 additions & 0 deletions plugins/workstreams/address-batch-server.test.ts

Large diffs are not rendered by default.

36 changes: 0 additions & 36 deletions plugins/workstreams/advance-contract.ts

This file was deleted.

170 changes: 45 additions & 125 deletions plugins/workstreams/advance-host.test.ts
Original file line number Diff line number Diff line change
@@ -1,135 +1,55 @@
import { describe, expect, it } from "vitest";
import { advanceChecks, readAdvancePr } from "./advance-host.js";
import { advanceInspectionSchema } from "./advance-contract.js";
import { readEqualHeadTrees } from "./advance-host.js";
import type { GhRunner } from "./ghactions.js";

const head = "a".repeat(40);
const base = "b".repeat(40);
const url = "https://github.com/example/widget/pull/42";
const view = {
url, number: 42, title: "Fix account lookup", state: "OPEN", isDraft: false, isCrossRepository: false,
headRefName: "fix-account", baseRefName: "main", headRefOid: head, baseRefOid: base,
reviewDecision: "APPROVED", mergeStateStatus: "CLEAN", mergeable: "MERGEABLE",
latestReviews: [], statusCheckRollup: [{ status: "COMPLETED", conclusion: "SUCCESS" }],
};

function fixture(options: { view?: Record<string, unknown>; review?: Record<string, unknown>; finalRefs?: Record<string, unknown>; bases?: unknown; views?: Record<string, unknown>[] } = {}) {
let views = 0;
const calls: string[][] = [];
const run: GhRunner = async (args) => {
calls.push([...args]);
let value: unknown;
if (args[0] === "api") value = { data: { repository: { pullRequest: {
headRefOid: head, baseRefOid: base, baseRefName: "main", baseRef: { name: "main", target: { oid: base } },
reviews: { pageInfo: { hasPreviousPage: false }, nodes: [] },
reviewThreads: { pageInfo: { hasNextPage: false }, nodes: [] },
...(args.some((arg) => arg.includes("reviewThreads")) ? options.review : options.finalRefs),
} } } };
else if (args[1] === "list") value = options.bases ?? [];
else { value = { ...view, ...options.view, ...options.views?.[views] }; views++; }
return { ok: true, stdout: JSON.stringify(value) };
};
return { run, calls };
}

describe("bulk advance verification", () => {
it.each(["MERGED", "CLOSED"] as const)("recognizes %s without review reads or surviving branch refs", async (state) => {
const fake = fixture({ view: { state, headRefName: null, headRefOid: null, baseRefName: null, latestReviews: null, statusCheckRollup: null } });
const result = await readAdvancePr(fake.run, url);
expect(result).toMatchObject({ ok: true, facts: { state, readiness: state.toLowerCase(), needsPreparation: false, baseOid: "", headOid: "" } });
expect(advanceInspectionSchema.safeParse(result).success).toBe(true);
expect(fake.calls).toHaveLength(1);
});
it("requires commit identities in open inspection contracts", async () => {
const result = await readAdvancePr(fixture().run, url);
if (!result.ok) throw new Error(result.error);
expect(advanceInspectionSchema.safeParse({ ...result, facts: { ...result.facts, baseOid: "" } }).success).toBe(false);
});
it("still validates the identity of completed PR responses", async () => {
expect(await readAdvancePr(fixture({ view: { state: "MERGED", number: 43 } }).run, url)).toMatchObject({ ok: false, error: "GitHub returned a different pull request." });
});

it("calls a PR ready only after approval, feedback, checks, stack, and commit identities agree", async () => {
const result = await readAdvancePr(fixture().run, url);
expect(result).toMatchObject({ ok: true, facts: { readiness: "ready", headOid: head, baseOid: base, needsPreparation: false } });
});

it("keeps approved comments as attention even after the branch is mergeable", async () => {
const result = await readAdvancePr(fixture({ review: { reviewThreads: { pageInfo: { hasNextPage: false }, nodes: [{ isResolved: false }] } } }).run, url);
expect(result).toMatchObject({ ok: true, facts: { readiness: "needs-attention", unresolvedThreads: 1 } });
});

it("prepares a blocked approved PR without claiming its comments have been addressed", async () => {
const result = await readAdvancePr(fixture({ view: { mergeStateStatus: "DIRTY", mergeable: "CONFLICTING" }, review: { reviewThreads: { pageInfo: { hasNextPage: false }, nodes: [{ isResolved: false }] } } }).run, url);
expect(result).toMatchObject({ ok: true, facts: { needsPreparation: true, readiness: "needs-attention", unresolvedThreads: 1 } });
});

it("does not turn approved-with-note into ready when follow-up is unconfirmed", async () => {
const result = await readAdvancePr(fixture({ view: { latestReviews: [{ state: "APPROVED", body: "Fix the fallback" }] } }).run, url);
expect(result).toMatchObject({ ok: true, facts: { approvalNotePending: true, readiness: "needs-attention" } });
});

it("leaves empty pending conclusions waiting, and distinguishes lost approval", async () => {
expect(await readAdvancePr(fixture({ view: { statusCheckRollup: [{ status: "IN_PROGRESS", conclusion: "" }] } }).run, url))
.toMatchObject({ ok: true, facts: { readiness: "waiting-checks", checks: "pending" } });
expect(await readAdvancePr(fixture({ view: { reviewDecision: "REVIEW_REQUIRED" } }).run, url))
.toMatchObject({ ok: true, facts: { readiness: "waiting-review" } });
});

it("refuses readiness when approval history or check data is incomplete", async () => {
for (const options of [
{ review: { reviews: { pageInfo: { hasPreviousPage: true }, nodes: [] } } },
{ view: { statusCheckRollup: [{}] } },
]) expect(await readAdvancePr(fixture(options).run, url)).toMatchObject({ ok: true, facts: { readiness: "needs-attention" } });
expect(await readAdvancePr(fixture({ review: { reviewThreads: { pageInfo: { hasNextPage: true }, nodes: [] } } }).run, url)).toMatchObject({ ok: false });
});

it("reports a live open base PR as a dependency even when all checks pass", async () => {
expect(await readAdvancePr(fixture({ bases: [{ number: 41, headRefName: "main" }] }).run, url))
.toMatchObject({ ok: true, facts: { basePrNumber: 41, readiness: "needs-attention" } });
});

it("retries a head change and accepts only the coherent second attempt", async () => {
const fake = fixture({ views: [{ headRefOid: "c".repeat(40) }] });
expect(await readAdvancePr(fake.run, url)).toMatchObject({ ok: true, facts: { headOid: head } });
expect(fake.calls.filter((args) => args[1] === "view")).toHaveLength(4);
});

it("fails boundedly if review facts describe an old head or base", async () => {
for (const review of [{ headRefOid: "c".repeat(40) }, { baseRef: { name: "main", target: { oid: "c".repeat(40) } } }]) {
const fake = fixture({ review });
expect(await readAdvancePr(fake.run, url)).toMatchObject({ ok: false, error: expect.stringContaining("changed during verification") });
expect(fake.calls.filter((args) => args[1] === "view")).toHaveLength(6);
describe("equal head trees", () => {
it("reuses identical commit trees only while the same open PR still has the expected head", async () => {
const old = "c".repeat(40), fresh = "d".repeat(40), tree = "e".repeat(40);
const calls: string[][] = [];
const run: GhRunner = async (args) => {
calls.push([...args]);
const sha = args.at(-1)!.split("/").at(-1)!;
return { ok: true, stdout: JSON.stringify(args[0] === "api" ? { sha, tree: { sha: tree } } :
{ url, state: "OPEN", headRefOid: fresh }) };
};
expect(await readEqualHeadTrees(run, url, old, fresh)).toEqual({ ok: true, priorTreeOid: tree, currentTreeOid: tree });
expect(calls.filter((args) => args[0] === "api")).toHaveLength(2);
expect(await readEqualHeadTrees(run, url, old, fresh)).toMatchObject({ ok: true });
expect(calls.filter((args) => args[0] === "api")).toHaveLength(2);
expect(calls.filter((args) => args[1] === "view")).toHaveLength(2);
});

it("fails closed on a changed tree, incomplete commit read, or a head race", async () => {
for (const [reason, oldDigit, freshDigit] of [["tree", "f", "1"], ["missing", "7", "8"], ["head", "9", "a"]] as const) {
const old = oldDigit.repeat(40), fresh = freshDigit.repeat(40);
const calls: string[][] = [];
const run: GhRunner = async (args) => {
calls.push([...args]);
if (args[0] !== "api") return { ok: true, stdout: JSON.stringify({ url, state: "OPEN", headRefOid: reason === "head" ? old : fresh }) };
const sha = args.at(-1)!.split("/").at(-1)!;
return { ok: true, stdout: JSON.stringify(reason === "missing" ? {} : { sha,
tree: { sha: sha === old ? "2".repeat(40) : reason === "tree" ? "3".repeat(40) : "2".repeat(40) } }) };
};
expect(await readEqualHeadTrees(run, url, old, fresh)).toEqual({ ok: false });
expect(calls.filter((args) => args[0] === "api")).toHaveLength(2);
expect(calls.filter((args) => args[1] === "view")).toHaveLength(reason === "head" ? 1 : 0);
}
});

it("allows read-only verification of an already ready fork", async () => {
expect(await readAdvancePr(fixture({ view: { isCrossRepository: true } }).run, url)).toMatchObject({ ok: true, facts: { readiness: "ready" } });
});

it("rejects partial API results and ambiguous base branches", async () => {
expect(await readAdvancePr(fixture({ finalRefs: { baseRef: null } }).run, url)).toMatchObject({ ok: false });
expect(await readAdvancePr(fixture({ bases: [{ number: 40, headRefName: "main" }, { number: 41, headRefName: "main" }] }).run, url)).toMatchObject({ ok: false });
});

it("uses the current base ref when the PR's historical base snapshot is stale", async () => {
const stale = "c".repeat(40);
expect(await readAdvancePr(fixture({ view: { baseRefOid: stale }, review: { baseRefOid: stale } }).run, url))
.toMatchObject({ ok: true, facts: { baseOid: base, readiness: "ready" } });
});

it("refuses readiness when the actual base branch moves during verification", async () => {
const fake = fixture({ finalRefs: { baseRef: { name: "main", target: { oid: "c".repeat(40) } } } });
expect(await readAdvancePr(fake.run, url)).toMatchObject({ ok: false, error: expect.stringContaining("changed during verification") });
expect(fake.calls.filter((args) => args[1] === "view")).toHaveLength(6);
});
});

describe("advance checks", () => {
it("fails closed for unknown states while permitting explicit skipped and neutral checks", () => {
expect(advanceChecks([{ status: "COMPLETED", conclusion: "NEUTRAL" }, { status: "COMPLETED", conclusion: "SKIPPED" }])).toBe("passed");
expect(advanceChecks([{ status: "NEW_STATE", conclusion: "SUCCESS" }])).toBe("unknown");
expect(advanceChecks([{ state: "FAILURE" }])).toBe("failed");
expect(advanceChecks([{ state: "PENDING" }])).toBe("pending");
it("uses an enterprise host flag without putting the hostname in the REST repository path", async () => {
const old = "4".repeat(40), fresh = "5".repeat(40), enterprise = "https://github.example.test/acme/widget/pull/42";
const calls: string[][] = [];
const run: GhRunner = async (args) => { calls.push([...args]);
const sha = args.at(-1)!.split("/").at(-1)!;
return { ok: true, stdout: JSON.stringify(args[0] === "api" ? { sha, tree: { sha: "6".repeat(40) } } :
{ url: enterprise, state: "OPEN", headRefOid: fresh }) };
};
expect(await readEqualHeadTrees(run, enterprise, old, fresh)).toMatchObject({ ok: true });
expect(calls.filter((args) => args[0] === "api")).toEqual([
["api", "--hostname", "github.example.test", `repos/acme/widget/git/commits/${old}`],
["api", "--hostname", "github.example.test", `repos/acme/widget/git/commits/${fresh}`],
]);
});
});
Loading
Loading