Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 12 additions & 9 deletions plugins/stewards/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ which writes the status record:
|---|---|
| phase, summary | Where the entity is, in a sentence. |
| next action, actor | What happens next, and who does it: `user` (the server's owner has to act), `steward` (the steward itself, including its own next step such as re-checking on the next deployment wake) or `other` (someone else). The actor is a party, not a pronoun: BB-95 misread `me` as the steward and filed its own re-check as needing the user. |
| blocked on | Who or what outside the thread it waits for. |
| waiting on | The party or artifact the next action waits on: checks, a review, a deployment, another team. It never marks the effort blocked; a hold, an open `stuck` escalation or a needs item does. The old name `blockedOn` is accepted as an alias for one release (BB-109). |
| requeue after | When to wake it if nothing else does. Defaults to the playbook's `requeue-default`. |
| conditions | Open escalations, `stuck`. |
| resources | Active workers, open pull requests, child stewards. |
Expand Down Expand Up @@ -720,7 +720,7 @@ escalations; the plugin-filed gate items (`approve-spec`, `approve-pr`,
changes while one of its escalations is with you, the project steward is
woken with the change, and resolves the escalation or leaves it open.
The steward's own signed description edit does not count.
- **Waits nobody hears.** A status whose `blockedOn` names the project
- **Waits nobody hears.** A status whose `waitingOn` names the project
steward or you, with no open escalation, is flagged in `bb stewards list`
and the project steward's wakes, so the nightly digest shows the drift.
- **Backstop:** a status record that has not advanced within the playbook's
Expand Down Expand Up @@ -1312,9 +1312,11 @@ The rules that hold:
- **Dedupe.** A need is keyed by (binding, kind, url) while open: a steward
that re-reports updates the wording rather than adding a row. The store
enforces it with a partial unique index.
- **`blockedOn` stays for third parties.** A `steward_report` whose `blockedOn`
names the user, the owner or a human with no open needs item is refused and
told to file one, so the ask reaches the page instead of only the prose.
- **`waitingOn` stays for third parties.** A `steward_report` whose
`waitingOn` names the user, the owner or a human with no open needs item is
refused and told to file one, so the ask reaches the page instead of only
the prose. The wait is a plain line on the card, not a block: blocked is
only a hold, an open `stuck` escalation or a needs item (BB-109).

## 19. Dependency stewards

Expand Down Expand Up @@ -1619,7 +1621,8 @@ without the thread actions menu when the thread is no longer in the live view.

The glyph is one of four shapes, in precedence order `you › blocked › running ›
quiet`: a solid yellow dot when it waits on you, an empty red ring when
blocked (`blockedOn`, a hold or `stuck`), a blue ring that pulses slowly while
blocked (a hold or an open `stuck` escalation, BB-109 — the free-text
`waitingOn` never blocks), a blue ring that pulses slowly while
a worker or reviewer runs, and an empty grey ring when quiet. The pulse is CSS
only and stops under `prefers-reduced-motion`. One-off threads use the same four glyphs — yellow
when unread, blue animated while their agent runs, grey otherwise — except a
Expand All @@ -1630,8 +1633,8 @@ project steward footer row is the grey ring. Steward rows carry no
thread-briefs ring at all: the single off switch leaves every steward thread
with no brief, so this list's own glyph is the only one they draw.

**The status phrase (BB-76).** First match wins: `needs you`; `blocked on
<blockedOn>`; `held behind <holdBehind keys>` when `holdBehind` is non-empty,
**The status phrase (BB-76, BB-109).** First match wins: `needs you`;
`held behind <holdBehind keys>` when `holdBehind` is non-empty,
else `on hold`; `stuck` for an open stuck escalation; `N worker(s) running`
counting only workers whose worker-table status is `running` (a busy steward or
reviewer with no running worker falls through, rather than reading `0 workers
Expand All @@ -1649,7 +1652,7 @@ and `phrase` as pure functions and sorts by the section rules; `sidebar.test.ts`
covers every rule and the precedence.

**Sections.** Blocked holds the needs-you (yellow) and blocked (red) efforts,
whether stewarded or `ops`; a needs-you, blockedOn, held or stuck `ops`
whether stewarded or `ops`; a needs-you, held or stuck `ops`
binding leaves Monitors for Blocked. In flight holds the rest of the stewarded,
non-ops efforts, running before quiet, then by recency. Pinned rows stay first within their section.
Recently ended holds the ended bindings, newest first, each built by
Expand Down
11 changes: 6 additions & 5 deletions plugins/stewards/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ bb. [DESIGN.md](DESIGN.md) is the design; this is how it is built and run.
third round. When the entity changes while one of its
escalations is with you, the project steward is woken with the change too,
since you may have answered on the ticket rather than in its thread. A
status blocked on the project steward or the user with no open
status waiting on the project steward or the user with no open
escalation or needs item is flagged in `bb stewards list` and the digest.
- **"Needs you" items** are the structured things only the user can do. The
plugin files the gate items (`approve-spec`, `approve-pr`, `merge`) from
Expand All @@ -276,8 +276,8 @@ bb. [DESIGN.md](DESIGN.md) is the design; this is how it is built and run.
labels (two to six): the page shows one button per choice, a note field
and **Other…** instead of a free-text **Answer**, and a choice is saved as
guidance. `bb stewards done <id> --choice <label>` records the same from a
thread. A `blockedOn` naming the user, the owner or a human is refused
unless an open item says so. The sidebar's **Needs you** block and count
thread. A `waitingOn` naming the user, the owner or a human is refused
unless an open item says so; it never marks the effort blocked (BB-109). The sidebar's **Needs you** block and count
badge show the same items, so a question reaches you where you already look
(BB-46).
- **One escalation chain (BB-30).** Every hop is one `escalations` row: a
Expand Down Expand Up @@ -423,9 +423,10 @@ switchable from where you are.
**The row and the glyphs (BB-76).** An effort row is two lines: the glyph, the
entity key in bold and the title on line one; the kind (`ticket`, `pull
request`, `review`, `ops`), the entity's phase when it has one, and the status
phrase on line two (first match wins: `needs you`, `blocked on …`, a hold,
phrase on line two (first match wins: `needs you`, a hold,
`stuck`, `N worker(s) running`, the deployment state, then the report's
`nextAction` verbatim). A row that needs you adds its prompt and one-click actions
`nextAction` verbatim). A free-text `waitingOn` is a plain line on the effort
card, never a block or a phrase prefix (BB-109). A row that needs you adds its prompt and one-click actions
under line two, in the same row. The glyph is one of four, in precedence order
`you › blocked › running › quiet`: a solid yellow dot when it waits on you, an
empty red ring when blocked, a blue ring that pulses slowly while a worker or
Expand Down
17 changes: 10 additions & 7 deletions plugins/stewards/app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ const sidebarBinding = (over: Record<string, unknown>) => ({
phase: null,
hold: null,
holdBehind: [],
blockedOn: null,
waitingOn: null,
needsUser: false,
stuck: false,
status: null,
Expand Down Expand Up @@ -574,7 +574,7 @@ const boardSteward = (over: Record<string, unknown> = {}) => ({
summary: null,
nextAction: null,
actor: null,
blockedOn: null,
waitingOn: null,
priority: null,
hold: null,
holdBehind: [],
Expand Down Expand Up @@ -640,7 +640,7 @@ describe("the Stewards page's project view and detail views (BB-85)", () => {
const slot = await renderBoardWith({
stewards: [
boardSteward({ key: "ENG-1", title: "Fix login", needsUser: true }),
boardSteward({ key: "ENG-2", title: "Held up", blockedOn: "bb-gate#88" }),
boardSteward({ key: "ENG-2", title: "Held up", hold: "a freeze" }),
boardSteward({ key: "acme/web", title: "Ops watch", kind: "ops" }),
],
questions: [question({ id: "qst_project", key: null, question: "Which release?" })],
Expand Down Expand Up @@ -741,13 +741,16 @@ describe("the Stewards page's project view and detail views (BB-85)", () => {
slot.lifecycle.unmount();
});

it("shows the blocked, hold and stuck lines in the status", async () => {
it("shows the waiting, hold and stuck lines in the status", async () => {
const slot = await renderBoardWith({
subPath: "p1/ENG-1",
stewards: [boardSteward({ key: "ENG-1", title: "Fix login", blockedOn: "bb-gate#88", hold: "a freeze", holdBehind: ["ENG-9"], stuck: true, summary: "x", nextAction: "y" })],
stewards: [boardSteward({ key: "ENG-1", title: "Fix login", waitingOn: "bb-gate#88", hold: "a freeze", holdBehind: ["ENG-9"], stuck: true, summary: "x", nextAction: "y" })],
stuck: [{ key: "ENG-1", url: null, threadId: "thr_s", why: "waiting on a decision", since: 1 }],
});
await waitFor(() => expect(slot.getByText("Blocked on bb-gate#88")).toBeTruthy());
// The wait is a plain muted line, not a destructive one (BB-109).
const waiting = await waitFor(() => slot.getByText("Waiting on bb-gate#88"));
expect(waiting.className).toContain("text-muted-foreground");
expect(waiting.className).not.toContain("text-destructive");
expect(slot.getByText(/On hold: a freeze \(behind ENG-9\)/u)).toBeTruthy();
expect(slot.getByText(/Stuck: waiting on a decision/u)).toBeTruthy();
slot.lifecycle.unmount();
Expand Down Expand Up @@ -785,7 +788,7 @@ describe("the Stewards page's project view and detail views (BB-85)", () => {
const bindings = [
sidebarBinding({ key: "ENG-1", threadId: "thr_1", needsUser: true, phase: "In Review" }),
sidebarBinding({ key: "ENG-2", threadId: "thr_2", workers: [{ threadId: "thr_w", title: "w", status: "running" }] }),
sidebarBinding({ key: "ENG-3", threadId: "thr_3", blockedOn: "bb-gate#88" }),
sidebarBinding({ key: "ENG-3", threadId: "thr_3", waitingOn: "bb-gate#88" }),
sidebarBinding({ key: "ENG-4", threadId: "thr_4", deployment: "deployed" }),
];
const captured = await loadApp();
Expand Down
6 changes: 2 additions & 4 deletions plugins/stewards/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,6 @@ function overviewEffortStatus(s: Steward): EffortStatus {
busy: false,
runningWorkers: s.workers,
needsUser: s.needsUser,
blockedOn: s.blockedOn,
hold: s.hold,
holdBehind: s.holdBehind,
stuck: s.stuck,
Expand Down Expand Up @@ -465,7 +464,7 @@ function StewardRow({ steward: s, now }: { steward: Steward; now: number }) {
{s.summary} <span className="text-muted-foreground">Next: {s.nextAction}{s.actor === null ? "" : ` (${ACTOR_LABEL[s.actor] ?? s.actor})`}</span>
</p>
)}
{s.blockedOn !== null ? <p className="text-muted-foreground">Blocked on {s.blockedOn}</p> : null}
{s.waitingOn !== null ? <p className="text-muted-foreground">Waiting on {s.waitingOn}</p> : null}
{s.hold !== null ? <p className="text-attention">On hold: {s.hold}</p> : null}
{s.escalations.map((x, i) => (
<p key={i} className={x.route === "me" ? "text-destructive" : "text-muted-foreground"}>
Expand Down Expand Up @@ -641,7 +640,6 @@ function boardEffortStatus(s: BoardSteward): EffortStatus {
busy: false,
runningWorkers: s.workers.length,
needsUser: s.needsUser,
blockedOn: s.blockedOn,
hold: s.hold,
holdBehind: s.holdBehind,
stuck: s.stuck,
Expand Down Expand Up @@ -844,7 +842,7 @@ function EffortDetail({ board: b, effortKey, refetch }: { board: Board; effortKe
Next: {s.nextAction ?? "—"}
{s.actor === null ? "" : ` (${ACTOR_LABEL[s.actor] ?? s.actor})`}
</p>
{s.blockedOn !== null ? <p className="text-destructive">Blocked on {s.blockedOn}</p> : null}
{s.waitingOn !== null ? <p className="text-muted-foreground">Waiting on {s.waitingOn}</p> : null}
{s.hold !== null ? <p className="text-attention">On hold: {s.hold}{s.holdBehind.length > 0 ? ` (behind ${s.holdBehind.join(", ")})` : ""}</p> : null}
{s.stuck ? <p className="text-destructive">Stuck{stuck === null ? "" : `: ${stuck.why}`}</p> : null}
<p className="text-xs text-muted-foreground">
Expand Down
8 changes: 4 additions & 4 deletions plugins/stewards/reconcile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,10 @@ describe("a worker in flight between its turns (BB-11)", () => {
});

describe("waits nobody hears", () => {
const status = (blockedOn: string | null) => ({ phase: "Todo", summary: "s", nextAction: "n", actor: "other" as const, blockedOn, requeueAfterMs: null, conditions: [], resources: { workers: [], pullRequests: [], children: [] }, at: 1 });
const status = (waitingOn: string | null) => ({ phase: "Todo", summary: "s", nextAction: "n", actor: "other" as const, waitingOn, requeueAfterMs: null, conditions: [], resources: { workers: [], pullRequests: [], children: [] }, at: 1 });
const open = [{ id: "esc_1", bindingId: "b", kind: "decision" as const, question: "q", context: null, meanwhile: "m", route: "project" as const, recommendation: null, choices: [], status: "open" as const, resolution: null, createdAt: 1, resolvedAt: null, deliveredAt: null, projectId: null, origin: { threadId: null, bindingId: null, chain: [] }, resolutionKind: null, url: null, data: {}, answeredBy: null, forwardedFrom: null, headSha: null, isQuestion: false, citedLessons: [], correctionsCounted: false, rationaleWanted: null, rationaleAsked: false, rationaleGiven: false, clusterKey: null }];

it("flags a status blocked on the project steward or the user with no escalation open", () => {
it("flags a status waiting on the project steward or the user with no escalation open", () => {
expect(unheardWait(status("project steward's approval of the new spec"), [])).toMatch(/project steward/u);
expect(unheardWait(status("the user's answer on scope"), [])).toMatch(/the user/u);
expect(unheardWait(status("project steward's approval"), open)).toBeNull();
Expand Down Expand Up @@ -270,7 +270,7 @@ describe("approval words", () => {
});

it("flags a report that asks for an approval already open, and only that", () => {
const status = (summary: string, nextAction: string) => ({ phase: "Todo", summary, nextAction, actor: "user" as const, blockedOn: null, requeueAfterMs: null, conditions: [], resources: { workers: [], pullRequests: [], children: [] }, at: 1 });
const status = (summary: string, nextAction: string) => ({ phase: "Todo", summary, nextAction, actor: "user" as const, waitingOn: null, requeueAfterMs: null, conditions: [], resources: { workers: [], pullRequests: [], children: [] }, at: 1 });
const open = [{ kind: "approval" } as never];
expect(asksForOpenApproval(status("Spec posted.", "Ask the user to approve the spec"), open)).toMatch(/already open/u);
// Waiting on the request, or on the project steward, is not asking again.
Expand Down Expand Up @@ -585,7 +585,7 @@ describe("the pull request gate (build-tickets)", () => {
});
});

describe("a blockedOn naming the user", () => {
describe("a waitingOn naming the user", () => {
it("is refused without an item and allowed with one", () => {
expect(namesUserAsBlocker("the user's answer on scope")).toBe(true);
expect(namesUserAsBlocker("the owner's approval")).toBe(true);
Expand Down
10 changes: 5 additions & 5 deletions plugins/stewards/reconcile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,9 +632,9 @@ export function prGate(entity: EntitySnapshot, opts: PrGateOptions = {}): PrGate
}

/**
* A `blockedOn` that names the user, the owner or a human. A status record
* A `waitingOn` that names the user, the owner or a human. A status record
* naming one of them without an open `needs` item is refused: the item is what
* puts the ask on the Stewards page.
* puts the ask on the Stewards page (BB-109 keeps the refusal for `waitingOn`).
*/
export function namesUserAsBlocker(text: string): boolean {
return /(?:^|[^\p{L}\p{N}])(?:user|owner|human)(?:[^\p{L}\p{N}]|$)/iu.test(text);
Expand Down Expand Up @@ -776,9 +776,9 @@ export const approvalWanted = (gate: GateState): boolean => !gate.none && gate.c
* none of its escalations is open: a wait nobody above it hears about.
*/
export function unheardWait(status: Binding["status"], open: readonly Escalation[], needs: readonly Pick<Need, "kind">[] = []): string | null {
if (status === null || status.blockedOn === null || open.length > 0 || needs.length > 0) return null;
if (/project steward/iu.test(status.blockedOn)) return "waits on the project steward, with no open escalation";
if (/\buser\b/iu.test(status.blockedOn)) return "waits on the user, with no open escalation";
if (status === null || status.waitingOn === null || open.length > 0 || needs.length > 0) return null;
if (/project steward/iu.test(status.waitingOn)) return "waits on the project steward, with no open escalation";
if (/\buser\b/iu.test(status.waitingOn)) return "waits on the user, with no open escalation";
return null;
}

Expand Down
Loading
Loading