Skip to the content.

busybees is driven through GitHub. People create issues, label them, comment and merge pull requests. The factory does everything in between. This page follows an issue through it: what the factory can see, which label means what, how work is sized and ordered, and what happens when the factory needs you. What each role is, what it reads and how to configure it is on roles.md.

The flow at a glance

  1. You give the product manager input. File an issue with bees and bees:feedback (an idea, product feedback, a bug report), file a feature issue with bees and bees:feature, or write to it by mail: bees mail send --from human --to product_manager --subject "..." --body "...".
  2. The product manager owns feature issues. It makes each one detailed enough, asks you on the issue when only a person can decide something (bees:question), and breaks the feature into work items: one issue per pull-request-sized piece, each a GitHub sub-issue of the feature.
  3. The project manager triages work items. It refines each bees:triage issue, asks the product manager by mail when it needs a product decision, and moves the issue to bees:ready with a size.
  4. A developer and the reviewer ship it. A developer worker implements the issue on a branch and opens a pull request. The reviewer reviews it. Once it is approved you merge it, or the reviewer’s auto_merge does after the checks pass.
  5. QA tests the default branch after merges, files bugs and reports to the product manager.

A concrete piece of work you already understand skips the product manager: label it bees + bees:triage for the project manager, or bees + bees:ready for a developer. See Filing work.

What the factory can see

The factory reads only the issues and pull requests that match the [filter] block in bees.toml, and every configured criterion must match:

Criterion Key Default Meaning
Label filter.label bees The item carries the label (skipped with require_label = false)
Assignee filter.assignee unset The item is assigned to this login. @me is your own gh login, never the [github] account
Milestone filter.milestone unset The item is in this milestone
Creator filter.creator unset The item was opened by this login, or by the account the factory acts as

Everything outside the filter is invisible: the factory never labels or comments on it. It reads outside the filter in two places: the visibility backstop below, which lists what was created since each session to repair a missing label, and the duplicate check behind QA’s bug reports, which scores a new report against every issue in the repository, whatever labels it carries and whether it is open or closed, so that a bug a person filed by hand is not filed again (QA). A factory-error report is a third exception, though not a read: it is filed against the busybees repository, not this one (see scheduler.report_factory_errors). Adding a criterion to a running factory hides everything that does not already satisfy it. Set assignee in a repository full of unassigned bees issues and every one of them disappears in one commit. bees doctor reports that case with both counts:

34 open issues and 2 pull requests carry `bees`, 0 match your filter (label=bees AND assignee=kyle)

The label is also the prefix of every workflow label, so with label = "hive" the states are hive:triage, hive:ready and so on. bees init and bees labels sync create the whole set in the repository, and bees run and bees tick create any that are missing when they start. Existing labels keep their colour and description unless you run bees labels sync.

Everything the factory creates gets the label, and the assignee when one is configured, so it stays visible. The orchestrator backstops the roles: after every session it lists the issues and pull requests created since the session started, whoever opened them, and adds the label and assignee to anything carrying bees or a bees:* label without them. On a pull request it adds the configured milestone too. The backstop never sets a milestone on an issue: that is a person’s decision. An issue bees issue create makes inherits one from the issue it relates to, or, for a feature spawned from an agreed design, is put in the existing one that fits the design’s phasing.

Two setups cover most repositories. Alone, keep the defaults and put bees on an issue. In a shared repository where busybees handles your share of the work:

[filter]
assignee = "@me"
require_label = false

The label state machine

Every work item carries exactly one state label. The orchestrator moves most of them. The project manager moves an issue out of bees:triage, to bees:ready or bees:blocked, with its issue_set_state tool, which refuses an issue in any other state. You can move any of them. One label stacks on top of a state label instead of replacing it: bees:needs-human, which you add to hold an issue where it is (see Holding an issue by hand).

Feature issues (bees:feature) and feedback issues (bees:feedback) are not in this diagram. They never carry a state label. They belong to the product manager, and work items are what it makes from them. When the product manager is enabled, an issue with the bees label alone becomes one of them: the orchestrator labels it bees:feedback and the product manager decides what it turns into. To have it built instead, label it bees:triage or bees:ready yourself (see Filing work).

stateDiagram-v2
    [*] --> triage: product manager creates a work item (sub-issue of a feature)
    [*] --> triage: human labels an issue bees:triage (spec it first)
    [*] --> ready: human labels an issue bees:ready (build it as written)
    triage --> ready: project manager refines the issue
    triage --> blocked: project manager asks the product manager
    blocked --> triage: product manager answers (orchestrator)
    ready --> in_progress: developer worker claims it (orchestrator)
    in_progress --> blocked: developer asks the project manager (orchestrator)
    blocked --> ready: project manager answers (orchestrator)
    in_progress --> review: developer opens / updates the PR (orchestrator)
    review --> in_progress: reviewer requests changes (orchestrator)
    review --> approved: reviewer approves (orchestrator)
    approved --> ready: human reviews / comments on the PR (orchestrator)
    approved --> ready: the PR conflicts with the default branch (orchestrator)
    approved --> [*]: human merges the PR (issue closes via "Closes #N")
    approved --> [*]: reviewer auto_merge, checks green (orchestrator merges)
    approved --> in_progress: auto_merge, a check failed (developer fixes)
    approved --> needs_human: auto_merge, checks timed out / merge refused (orchestrator)
    in_progress --> needs_human: developer session failed / no PR (orchestrator)
    review --> needs_human: round limit reached / reviewer failed (orchestrator)
    needs_human --> ready: human removes bees:needs-human, adds bees:ready
    needs_human --> triage: human removes bees:needs-human, adds bees:triage

    state "bees:triage" as triage
    state "bees:ready" as ready
    state "bees:in-progress" as in_progress
    state "bees:blocked" as blocked
    state "bees:review" as review
    state "bees:approved" as approved
    state "bees:needs-human" as needs_human

The two edges out of needs_human describe an issue the factory escalated: it replaced the state label, so there is one to add back. An issue you held by hand still carries its state label underneath, and removing bees:needs-human is the whole of the undo.

Label Meaning Who sets it
bees:triage Needs the project manager to make it buildable Product manager (new work items), orchestrator (after an answer, when a ready issue is sized above roles.developer.max_size, or on an unlabelled issue when only the project manager is enabled), you
bees:ready Detailed enough for a developer Project manager (with a size), orchestrator (after an answer, after your feedback on an approved pull request, when an approved pull request conflicts with the default branch, or on an unlabelled issue when both managers are disabled), you
bees:in-progress A developer worker owns it and a branch exists Orchestrator
bees:blocked Waiting on an answer to a question Project manager (asking the product manager), orchestrator (the developer asking the project manager)
bees:review A pull request is open and in the review loop Orchestrator
bees:approved The reviewer approved. Waiting for you to merge, or for the checks with auto_merge Orchestrator (on the pull request too)
bees:needs-human The factory gave up on it, or you are holding it Orchestrator, you

Five labels sit outside the state machine. An issue carrying bees:feature or bees:feedback never gets a state label and is never triaged:

Label Meaning Who sets it
bees:feature A feature issue. The product manager makes it detailed enough and breaks it into work items Product manager, you
bees:feedback The product manager’s inbox: an idea, product feedback or a bug report from a person You, orchestrator (an issue with no state label and neither bees:feature nor bees:feedback, when the product manager is enabled)
bees:question The product manager is waiting for you to answer on a feature or feedback issue Product manager. The orchestrator removes it when you reply
bees:proposal A feature issue a bee wrote. It sits next to bees:feature, and you remove it to approve the feature bees issue create --feature. Only you remove it
bees:review-requested On a pull request, not an issue: one review pass from the reviewer, whoever opened the pull request. Not needed with scheduler.review_assigned_prs on. See Asking for a review of any pull request You. The orchestrator removes it as the review starts

Three more sit next to a state label rather than replacing one.

bees:planning and bees:planned are how you agree something with the product manager before anything is built. Both are yours alone: the product manager never adds or removes either. See Planning with the product manager.

Label Meaning Who sets it
bees:planning You and the product manager are still agreeing a feature or feedback issue. It discusses and breaks nothing down You
bees:planned Agreed. The product manager treats the scope as settled and breaks the issue down on its next run You

bees:priority says “build this next”. An issue keeps its state label alongside it, it survives every state change, and nothing in the factory removes it. See Priority.

bees:bug is a kind label on a work item: a bug filed by the developer, the reviewer, QA or a person. It says what the issue is, not where it goes, and the issue moves through the state machine like any other work item. Only bees:feature and bees:feedback route an issue out of the state machine, so bees + bees:bug with no state label is routed like any other unlabelled issue (feedback for the product manager when it is enabled), and bees + bees:bug + bees:triage is a work item for the project manager. You never need to add a kind label.

Sizing

Besides its state label, a work item carries at most one size label. The reviewer is told the size and adjusts its scrutiny to it, bees status breaks the ready queue down by size, and the size can pick the model a developer session runs (roles.developer.model_by_size).

Size Label Rough meaning
xs bees:size/xs One file, obvious change, no design (typo, config, trivial bug)
s bees:size/s A few files, clear approach, existing tests cover it
m bees:size/m A coherent feature slice touching several packages, needs new tests
l bees:size/l Crosses subsystems or needs a design decision. Near the limit for one pull request
xl bees:size/xl Too big for one pull request. Split it instead

Who sets it:

Size decides what gets built next

A free developer worker first picks up work already in flight. An issue in bees:in-progress or bees:review is resumed, and so is a bees:approved issue whose post-approval checks were interrupted. Then comes any bees:ready issue that already has an open pull request, one sent back for your feedback or because it conflicts with the default branch. Only then does the worker take new work from bees:ready: bees:priority issues first, then in the order scheduler.dispatch_order asks for:

dispatch_order Order
small-first (default) Smallest size first, oldest first within a size. Quick wins do not queue behind a big item
oldest Oldest first, whatever the size
large-first Largest size first, oldest first within a size

An issue without a size ranks as m, which is the label the orchestrator gives it anyway.

Two limits sit on top of that order:

Priority: “do this next”

Add bees:priority to a bees:ready issue and the next free developer takes it before the rest of the queue, whatever scheduler.dispatch_order says and however old the other issues are. The whole dispatch order is:

  1. issues being resumed (bees:in-progress, bees:review, an interrupted post-approval checks stage in bees:approved, and bees:ready issues with an open pull request), never reordered;
  2. bees:priority issues;
  3. scheduler.dispatch_order (size, or age under oldest);
  4. age.

Priority is a separate axis from size: a bees:size/xs issue does not jump a priority bees:size/l one under small-first. Between two priority issues dispatch_order decides as usual.

Set it from the GitHub UI like any other label. It is yours: it survives every state change, nothing in the factory removes it, and it stays on the issue until you take it off. Two roles may put it on an issue, each under a narrow rule. The project manager may add it to a work item that unblocks the factory itself: the default branch does not build, every pull request’s checks are red for the same reason, or the orchestrator cannot run. Its prompt rules out anything else, including reordering the queue by moving bees:ready issues back to bees:triage. The product manager only carries one: put it on a feedback issue and the work item the product manager makes from that issue gets it too, so your lever survives the hop.

Priority reorders the queue. It does not lift the limits: a priority bees:size/l issue still waits while scheduler.max_large_in_flight of them are in flight, and a priority issue above roles.developer.max_size still goes back to bees:triage to be split.

bees status counts the queued issues carrying the label on its ready row and lists their numbers under priority in --json, so you can see the lever took effect:

ready          4  (xs 1, s 2, m 1, 1 priority)

Talking to the product manager

Not everything you want to say is a buildable issue. The product manager turns product intent into work, and it listens on two kinds of issue.

Feedback issues

For a feature idea (“we should support SSO”), product feedback (“onboarding feels clunky”), or a bug report you would rather have weighed than fixed verbatim, create an issue with the bees label and the bees:feedback label. You can leave the second label off: when the product manager is enabled, an issue with bees, no state label and neither bees:feature nor bees:feedback gets bees:feedback from the orchestrator on the next poll (Filing work). Adding it yourself says what you meant. Either way the issue goes to the product manager, not to triage:

A small, well-formed ask is routed rather than weighed. When a feedback issue is really a piece of work, the product manager creates the work item directly (related: <feedback issue>, so it inherits the milestone), carries your bees:priority over if you set one, replies with the number and closes the feedback issue.

Feature issues

A feature issue (bees + bees:feature) describes a user-visible outcome: the problem, who it is for, what “done” looks like, constraints. The product manager writes most of them, from feedback, QA reports and its own roadmap, and you can file one directly. Feature issues never enter the state machine. bees status counts them in its features queue.

A feature issue a bee wrote also carries bees:proposal: it is a proposal until you approve it. The product manager writes it, refines it and asks questions on it, but nothing is broken down from it, so the factory cannot grow its own roadmap. The scheduler never presents a proposal for breakdown, and bees issue create --parent <proposal> and bees issue link refuse it, so a proposal grows no sub-issues whoever asks. Remove the bees:proposal label to approve it: the scheduler notices the label is gone and brings the feature back to the product manager, which breaks it down on its next run. A feature issue you filed never carries the label and is broken down straight away.

With scheduler.feature_proposals = false there is no gate: a feature issue a bee writes carries no bees:proposal either and is broken down straight away, and the two tools refuse nothing for the label. An issue that still carries bees:proposal from before the key was turned off stays a proposal until you remove the label; turning the key off approves nothing for you. The slop-factory config template turns it off.

For each fresh feature issue the product manager:

  1. makes sure it is detailed enough to be broken down, or asks you (below);
  2. breaks it into work items: one issue per pull-request-sized piece, created with its issue_create tool (parent: <feature>, bug: true for bugs). Each becomes a GitHub sub-issue of the feature, labelled bees + bees:triage (+ bees:bug), and inherits the feature’s milestone. GitHub tracks the feature’s progress from its sub-issues. Work items are ordered, with dependencies written as a Blocked by #N line (Dependencies). The project manager adds the implementation detail during triage. An existing issue is attached with issue_link (parent: <feature>, child: <item>), which makes the sub-issue relationship and, when the issue is in no milestone, puts it in the feature’s. Both tools refuse a feature that is still a proposal (unless scheduler.feature_proposals = false), or one you have put in planning;
  3. comments the list of work items on the feature issue, with the marker, so the feature is not presented to it again until something changes;
  4. later, closes the feature issue once all its sub-issues are closed, or when it stops making sense, saying why. The orchestrator brings a feature whose last open sub-issue has closed back to the product manager on the next pass, in a section of its own. See Features, sub-issues and milestones.

You steer a feature by commenting on it. Your comment gives the human side the last word and makes the issue fresh again.

Planning with the product manager

A feature issue is one-shot: by the time it exists, you and the product manager have never agreed on it. For anything non-trivial, plan it first.

Put bees:planning on a feature or feedback issue. While the label is there the product manager only discusses it. Every comment you leave starts a run, and it replies on the issue: the questions it needs answered, the options it sees with a recommendation, or a draft of the feature description for you to react to. It creates nothing, attaches nothing and asks nothing with bees:question. The conversation is the channel. That is enforced, not only prompted: the issue is presented in a section of its own that lists no breakdown step, and bees issue create --parent <planning issue> and bees issue link refuse it, so it grows no sub-issues whoever asks. An untouched planning issue is not work. The product manager wakes for your comment, not for the label.

End planning by swapping bees:planning for bees:planned. That label is your agreement. On its next run the product manager treats the issue as settled. It does not re-open the scope or ask for it to be confirmed again, and adds bees:question only if something new comes up that the conversation never covered. It writes what was agreed into the issue body as a short ## Decisions section, so the project manager and the developers see it without reading the thread, records the outcome in its notes, and then breaks the feature into work items, or actions and closes the feedback issue.

It does that once: a feature is presented for breakdown only while it has no sub-issues, so the ones the breakdown creates are what take it off the list. Nothing about bees:planned wakes the product manager, so the run that picks it up is the next one scheduler.product_manager_interval or a comment of yours brings round. With the default interval that is at most an hour.

One exception: when the issue is a proposal the product manager wrote, bees:proposal says you have not approved it, and it outranks bees:planned. Take it off as well, or the issue stays a proposal waiting for you.

Leave bees:planned on the issue or take it off. The factory neither reads it again nor removes it.

From an agreed design to several features

An agreed design can cover more than one coherent outcome: a design document, wherever it lives, that plans several milestones of work. Then the product manager does not refine the one issue it planned on. It spawns a feature issue per outcome, each describing that outcome the way any feature issue does, and writes the list into the agreed issue’s ## Decisions section: which features, in which milestone, in what order.

With scheduler.feature_proposals on, each new feature is a proposal like any other the product manager writes: remove bees:proposal on each to let it be broken down. The design is agreed, so that is a quick check rather than a second conversation.

Milestones. Each resulting feature goes into whichever existing open milestone best fits the design’s own phasing: nearer-term work into the nearer milestone. A feature that belongs in the agreed issue’s milestone inherits it through related. One the phasing puts in a later milestone is created with issue_create’s milestone set to that milestone’s title, as listed in the product manager’s task. That is the only time a bee names a milestone rather than inheriting one, and it only ever picks among the ones you created: the product manager never creates, edits or closes a milestone. When the phasing implies a milestone that does not exist yet, the product manager says so in a comment on the agreed issue and leaves that feature in the nearest existing milestone, until you create the milestone and move the feature.

Order. Ordering between the features uses blocked_by, the same mechanism that orders work items within one feature (Dependencies): a feature that builds on another is created with blocked_by naming the earlier feature. The scheduler holds back work items, not features, so the line on the feature records the order for the breakdown: when that feature is broken down, the work items that cannot start before the earlier feature ships carry blocked_by naming that feature, and a feature issue counts as a blocker until it closes.

Questions for you: bees:question

When only a person can decide something on a feature or feedback issue, the product manager posts the question as a comment, with the marker, adds the bees:question label, and stops working on that issue. The label is for you: it marks the issues waiting on a person. Answer in a comment. On the next poll the orchestrator sees that a person had the last word, removes bees:question, and the issue comes back to the product manager as fresh.

The ways of getting something into the factory, side by side:

You want Do this Who handles it
An idea weighed, feedback heard, a bug considered issue with bees + bees:feedback Product manager
A non-trivial idea thought through with you before anything is specced issue with bees + bees:feedback (or bees:feature) + bees:planning Product manager, in planning mode
A feature specified and broken into work items issue with bees + bees:feature Product manager
A concrete piece of work specified, then built issue with bees + bees:triage (optionally bees:bug) Project manager (triage), then a developer
A concrete piece of work built as written issue with bees + bees:ready A developer
A private note to a role, off GitHub bees mail send --from human --to product_manager --subject "..." --body "..." That role, on its next session

Filing work

Work items normally come from the product manager breaking a feature issue down, but you can file one yourself. The labels you put on the issue decide what happens next, and the default is deliberately cautious.

An issue with the bees label and nothing else is read as feedback, not as a spec, when the product manager is enabled. On the next poll the orchestrator labels it bees:feedback and it goes to the product manager, which weighs it, decides what it becomes (a feature, a work item, or a reasoned no) and replies on it (see Feedback issues). Nothing is specced or built until someone has authorised the scope. That mirrors the rule pointing the other way: a feature issue a bee writes is only a bees:proposal until you approve it, unless scheduler.feature_proposals = false, which the slop-factory config template sets. Between the two, new scope enters the factory only through a person or through the product manager. With require_label = false the orchestrator adds bees in the same edit, so the issue is fully tagged either way.

With the product manager disabled, nothing ever reads bees:feedback, so the orchestrator routes the same unlabelled issue further down the pipeline instead: to bees:triage when the project manager is enabled, or straight to bees:ready when both managers are disabled. The first enabled role able to take the issue wins; a factory running only developers builds an unlabelled issue as it stands, with no spec and no refinement pass.

To have it built, give it a state label yourself. That is the intended fast path, not a workaround: the feedback rule applies only to an issue that carries no state label, so the one you set stands.

Nothing else is added for you on that path, so put the bees label on the issue yourself when the filter requires it. The same rule decides where a bug report lands: bees + bees:bug on its own is routed like any other unlabelled issue (feedback for the product manager when it is enabled), while bees + bees:bug + bees:triage goes straight to the project manager.

Steering: anything a person writes, in an issue, in a pull request or in mail to a role, is authoritative for every role and outranks their prompts. Commenting on an issue is the way to change direction. While the factory is working on it, your comment is delivered as mail (see Commenting on the issue). Labels are yours to move too: relabel to bees:triage to send an issue back for refinement, or remove the bees label to take it out of the factory.

Dependencies

A work item can declare what has to land first. Put a line anywhere in the body:

Blocked by #37

blocked by and depends on are both recognised, case-insensitively, with an optional colon and Markdown emphasis, and several numbers separated by commas, spaces or and (Depends on: #3, #4 and #5). The phrase without a number (blocked by the missing tests) declares nothing. The issue_create tool’s blocked_by, or bees issue create --blocked-by 37, writes the line for you.

The scheduler reads the line on every poll and does not hand the issue to a developer while any of its blockers is open. Open means present in the last poll: an issue that is closed, or that the factory’s filter does not see, blocks nothing. Work items and feature issues both count as blockers.

The label does not change. The issue stays bees:ready, bees status says why it is not moving, and it becomes dispatchable on the first poll after its blocker closes:

ready          4  (xs 1, s 3, 2 waiting on deps)

Holding an issue back never costs a developer pool slot, so the rest of the queue keeps moving. Issues already in bees:in-progress or bees:review, and an interrupted post-approval checks stage in bees:approved, are resumptions and are never held back.

When the declarations form a cycle (#1 blocked by #2 blocked by #1), the scheduler ignores the dependencies of the issues in it, because nothing would ever be built otherwise, and logs a warning once per issue.

The project manager sees the open blockers of every work item in its prompt and is told to write the line rather than park a dependent item in triage. The product manager uses blocked_by when it breaks a feature down.

Development

When a developer worker claims a bees:ready issue it:

  1. labels it bees:in-progress;
  2. creates a temporary git worktree on the branch bees/issue-N (the prefix is project.branch_prefix), based on the default branch, reusing the branch when it already exists;
  3. runs a developer session that implements the issue, merges the default branch into the branch, pushes, and opens a pull request whose body contains Closes #N;
  4. labels the pull request bees, assigns it when an assignee is configured, and moves the issue to bees:review.

The developer never touches labels and never pushes to the default branch. Where the issue leaves it a choice it makes one, implements it and records it in the pull request for the reviewer to rule on. Bugs it notices outside the issue’s scope go into new bees:bug issues in triage rather than into the pull request. It merges the default branch before every push, on every round, and re-runs the tests afterwards: the default branch moves while an issue is being worked, and a pull request that has fallen behind it costs a review round. What the session is given and what it reports is on roles.md.

Best of N

With best_of_n_by_size set for a work item’s size, its first develop round runs as several developer attempts instead of one, and an assembler session picks the result that goes to review. A size with no entry, or an issue already resuming a review round or one whose branch already carries a pull request, gets the single-session round Development describes: fanning out only ever happens on a first round.

The attempts run at once, each in its own worktree and branch, bees/issue-N-attempt-i, based on whatever branch the round would otherwise start from. Each runs the same developer task as an ordinary round, on the same issue and the same mail, so all of them are recorded against the issue and spend one max_cost_per_issue budget together. Because each runs the ordinary developer task, an attempt may open its own pull request the way a single session would: seeing more than one open pull request for the same issue while a fan-out runs is expected, and they close once the fan-out cleans up.

Dispatch claims every attempt’s slot of scheduler.max_developers at once, before any of them starts, never one at a time: a worker holding its own slot while it waits for the rest could wait forever on another fan-out doing the same, so the count is capped to max_developers first, and a fan-out that cannot claim all its slots at once waits for a later pass instead. bees status shows the worker’s stage as fan-out while the attempts run.

Once every attempt has ended, the extra slots go back to the pool before anything else happens, and the attempt worktrees are removed. A branch with no commits ahead of its base is not a candidate for the result; when none of them are, every attempt branch is deleted (closing any pull request an attempt opened) and the issue goes to bees:needs-human, with what each attempt reported.

With at least one candidate, one more developer session runs: the assembler, in the worktree on the issue’s own branch, stage assembler. It is told each candidate’s branch, how many commits it carries and what its session reported, reads the candidates from their branches, and decides what the result is, one attempt as it stands or a synthesis built from several. It puts that on the issue’s own branch and opens the pull request from it itself, exactly as a single developer session does. From here the pull request goes to review and the checks like any other; nothing downstream can tell a fan-out from a single session.

When the assembler finishes, whatever it reported, every attempt branch is deleted, on the remote and in the local clone, closing any pull request an attempt opened along the way. The one exception is the account-wide session limit stopping an attempt or the assembler itself: the factory pauses and retries the fan-out later, and the branches stay for that retry instead of being deleted mid-pause.

Mixture of experts

With moe_experts_by_size naming experts for a work item’s size, its first develop round runs one developer session per expert, and an assembler session combines their work into the pull request that goes to review. A size with no entry, a size that names a single expert, an issue already resuming a review round, and one whose branch already carries a pull request, get the single-session round Development describes: fanning out only ever happens on a first round.

The experts run at once, each in its own worktree and branch, bees/issue-N-attempt-i numbered in the order the size lists them, based on whatever branch the round would otherwise start from. Each runs the ordinary developer task, on the same issue and the same mail, with the expert’s own prompt in place of the developer’s and the expert’s own model where it names one, so all of them are recorded against the issue and spend one max_cost_per_issue budget together. Because each runs the ordinary developer task, an expert may open its own pull request the way a single session would: seeing more than one open pull request for the same issue while a fan-out runs is expected, and they close once the fan-out cleans up.

Dispatch claims a slot of scheduler.max_developers for every expert at once, before any of them starts, never one at a time: the count is capped to max_developers first, so the experts past the cap do not run, and a fan-out that cannot claim all its slots at once waits for a later pass instead. bees status shows the worker’s stage as fan-out while the experts run.

Once every expert has ended, the extra slots go back to the pool before anything else happens, and the expert worktrees are removed. A branch with no commits ahead of its base is not a candidate for the result; when none of them are, every expert branch is deleted (closing any pull request an expert opened) and the issue goes to bees:needs-human, with what each session reported.

With at least one candidate, one more developer session runs: the assembler, in the worktree on the issue’s own branch, stage assembler. It is told each candidate’s branch, which expert worked it, how many commits it carries and what its session reported. It reads the candidates from their branches and builds one implementation out of them, taking each expert’s work in the part of the issue it took and keeping one reading where two of them solved the same thing in different ways. It puts that on the issue’s own branch and opens the pull request from it itself, exactly as a single developer session does, naming in the body which expert branch each part of the result came from. From here the pull request goes to review and the checks like any other; nothing downstream can tell a fan-out from a single session.

When the assembler finishes, whatever it reported, every expert branch is deleted, on the remote and in the local clone, closing any pull request an expert opened along the way. The one exception is the account-wide session limit stopping an expert or the assembler itself: the factory pauses and retries the fan-out later, and the branches stay for that retry instead of being deleted mid-pause.

Questions

Roles never talk to each other on GitHub. They use a local mailbox in the state directory, which you can read with bees mail list. The visible effect on GitHub is the bees:blocked label:

Answers travel with the issue, so it does not matter which developer worker ends up with it. To answer a question yourself, comment on the issue: while it carries bees:blocked your comment is delivered to whoever asked, and the orchestrator moves the label back to bees:ready or bees:triage for you (see Commenting on the issue). Moving the label yourself works too, and so does mail:

bees mail send --from human --to developer --issue 12 --subject "Re: which DB" --body "Use SQLite."

Review

Each developer worker runs a strictly sequential loop for its issue:

developer → reviewer → developer → reviewer → … → approved

The review is the one bees review runs (Reviewing a pull request), run on the worker’s checkout of the pull request branch: a brief of the change is distilled from its context and sized, one read-only session per angle roles.reviewer.angles gives that size looks for problems from that angle alone, and the judge merges what they found into one list, most severe first. Every session of it runs the reviewer’s agent and model, with brief_model, angle_models and judge_model replacing the model for one step each. The reviewer session then posts that list on the pull request as one comment review, every finding and nothing else, for the person who merges to read, and decides: it approves when nothing in the list needs fixing before the merge, or sends the developer one message through the mailbox with the verdict and every finding. It posts no approval or request for changes on GitHub, which refuses both from a pull request’s own author, and pushes nothing to the branch. Verifying that the change builds and passes is CI’s job: nothing in the review re-runs the repository’s test-suite. On changes requested the orchestrator moves the issue back to bees:in-progress and runs the developer again with the feedback in its prompt. The developer pushes and reports pr-updated. A review that could not run, because its brief or every one of its angles failed, is escalated with the reason.

scheduler.max_review_rounds (default 3) caps the number of reviewer passes. When the last round still requests changes the issue is escalated (below). The reviewer is told when it is on the final round.

When the reviewer approves, the orchestrator labels both the pull request and the issue bees:approved and requests a review from everyone in scheduler.notify, so the pull request shows up in their review queue. That request is best effort: GitHub refuses one from the pull request’s own author, which with a shared account is usually the configured login. With the reviewer role disabled, a pull request is approved as soon as the developer opens it.

Asking for a review of any pull request

The review loop above runs on the pull requests the factory’s developers open. To have the reviewer look at any other open pull request, your own or anyone’s, put bees:review-requested on it. The pull request needs the bees label too, or it is outside the filter and the factory never sees it; with filter.assignee set, assign it as well.

One label is one review pass. The next poll starts a reviewer session on the pull request, in a read-only checkout of its head branch, and removes the label as the session starts: it is gone whether the session approves, requests changes or fails, and adding it again asks for another pass. A request that arrives while a review of the same pull request is still running waits for the poll after it. The session takes a developer slot, so scheduler.max_developers bounds these reviews together with the developer workers, and it starts only after every ready issue that can be dispatched has been, so a request never starves the queue. A session that fails is logged, and the pull request is not tried again for five poll intervals: there is no issue to escalate. A session that reports a verdict GitHub holds no matching review for counts as one of those failures, whatever it reported.

A head branch the factory’s remote does not have, a pull request from a fork or a branch deleted since, does not stop the review: the session runs from a checkout of the default branch and reads the change with gh pr diff.

With scheduler.review_assigned_prs on, the label is not needed: every open pull request the factory can see whose head branch does not start with project.branch_prefix is reviewed on sight. The prefix is what says the factory did not write it, so the pull requests its own developers open keep going through the review loop above and are not reviewed twice. With filter.assignee set, this is every such pull request assigned to the factory; without it, every one carrying the bees label. The reviewer config template is that setting with nothing that builds enabled.

[scheduler]
review_assigned_prs = true

One head commit is one review pass. The head the reviewer looked at is remembered, so a restart does not review it again, and a push earns another pass. A draft is skipped until it is marked ready. The label keeps working alongside it and keeps its own meaning: it asks for a pass whether or not the head has already been reviewed.

The review is the same brief, angles and judge as in the loop above, and the verdict is one GitHub review on the pull request, because there is no developer to mail and no issue to label: approve when nothing the review found needs fixing before the merge, request-changes when something does, the verdict line and every finding in the body, ending with the <!-- bees:reviewer --> marker. There is no issue, so the review judges the change against the pull request’s description and the repository’s conventions, not against acceptance criteria. GitHub refuses an approval from a pull request’s own author: when the author is the login the factory acts as, the reviewer submits a comment review in place of the approval and says so in it. With no [github] table the factory acts as the account gh is signed in with, so a review of that person’s own pull request is a comment too. The review never comes back in as feedback: the orchestrator reads reviews and comments only on a pull request that closes a visible factory issue, and this one closes none. What the reviewer reads and does in this mode is under Requested reviews.

Before the review: the checks

The developer runs the repository’s own lint and test commands before it pushes, and the orchestrator reads the pull request’s checks before the first review (pre_review_checks, on by default and independent of auto_merge). Between the developer opening the pull request and the first reviewer session, the worker waits checks_wait, then polls every checks_poll_interval, for at most pre_review_checks_timeout (default 10 minutes):

The read happens once per pull request. A later review round, the developer answering the reviewer’s feedback, goes straight to the reviewer with no second read, no second wait and no checks section: the checks that were read describe a head the developer has since replaced. A restarted bees run does not read them again either. That the read happened is recorded in <state_dir>/issues/<n>.json along with the stage the worker was in.

bees status shows the worker in the pre-review checks stage while it waits. Set pre_review_checks = false to go straight from the developer to the reviewer.

Giving the developer feedback

You do not need the mailbox to steer a developer. Review the pull request on GitHub as you would a colleague’s. On every poll the orchestrator looks at each open factory pull request whose updatedAt moved since it last checked and collects the reviews, inline review comments and conversation comments written since then. It drops the ones bees wrote and empty approvals. What is left goes to the developer as one mail message from human, listing each item with its author, file and line, comment id, link and the exact gh command to reply to it. The timestamp of the last item delivered is recorded as human_seen_at in <state_dir>/issues/<n>.json. Comments on the issue itself have a clock of their own next to it, issue_human_seen_at, so reading one stream never suppresses the other.

How the orchestrator tells a bee comment from yours. Two mechanisms, and either one on its own makes a comment a bee’s:

With [github] set, the orchestrator checks the two against each other: when a session ends, a comment by the factory’s login that carries no marker is logged as a warning naming the issue or pull request, the role and the comment. It is a report, not a repair: the comment is left as it is.

The same two mechanisms decide whether a person had the last word on a feedback or feature issue, which is what wakes the product manager.

What happens next depends on the issue’s state:

Commenting on the issue

The same steering works on the issue, which is where you are more likely to write when there is no pull request yet, or when the direction is about the work rather than the diff. On every poll the orchestrator also looks at each issue in the four in-flight states bees:in-progress, bees:review, bees:approved and bees:blocked whose updatedAt moved since it last checked, and collects the issue’s own comments written since then. Bee comments are dropped by the same two mechanisms. What is left is sent as one message from human, and who it reaches depends on the state:

An issue outside those four states delivers only a comment that @-mentions the login the factory acts as ([github].login), and only that comment, not the rest of the window. The session that will next act on bees:triage, bees:ready, bees:feature or bees:feedback already renders the issue’s whole comment history in its own prompt, so mailing every comment would be a second copy of what it is about to read anyway. A mention is the one thing that rendering cannot carry: a person asking for the role now rather than on its own schedule. Who a mention reaches:

State Goes to
bees:triage, bees:ready Project manager
bees:feature, bees:feedback Product manager

With no [github] login configured, the factory shares one GitHub account with the people it works for, so there is no name of its own to mention, and none of these four states deliver anything.

The developer replies on the issue with its comment tool, and treats your comment as it treats a comment on the pull request: a direction that outranks the issue body and the reviewer.

Unlike feedback on the pull request, a comment on an issue in bees:approved does not send it back to bees:ready. “Looks good, merge it” is as likely a comment there as “change this”, and the label is your cue to merge. The message waits for the developer’s next session on that issue: the auto-merge checks stage, a comment on the pull request, or your own relabel. Comment on the pull request instead if you want the work reopened.

Until the orchestrator has recorded a time for an issue it delivers nothing. The first pass that sees the issue, in bees:triage, in bees:ready or in one of the in-flight states, records the time instead, because a fresh state directory, or the first poll after an upgrade, must not replay every comment an issue has ever received, or every mention either. Delivery starts from what is written after that. Nothing is lost: a developer session’s prompt renders the issue’s whole comment history anyway, so an earlier comment is context it already reads. What the mail adds is that the comment is fresh, that it is a person’s, and that it reaches a reviewer, unblocks a blocked issue, or reaches the project or product manager as a mention.

bees:triage and bees:ready carry a clock too, refreshed on every poll whether or not a comment on them mentions anyone, which is what makes the rest work. An issue is always seen in triage before anything can block it, so a question the project manager blocks on already has a clock, and your answer reaches it on the next poll however quickly you write it. Refreshing the clock in bees:ready too keeps an issue that waited days for a developer from handing its whole wait’s worth of comments, mentions included, to the first session that picks it up.

You can also mail a developer directly, with or without a pull request:

bees mail send --from human --to developer --issue 12 --body "Keep the CLI flag names as they are."

Roles treat what people write as authoritative. When your request conflicts with the issue or the reviewer’s feedback, the developer follows you and says so in the pull request.

Conflicts with the default branch

Every merge can leave the remaining open pull requests conflicting with the default branch, and a conflicting pull request is one you cannot merge. On every poll the orchestrator reads each open factory pull request’s merge state, which comes with the pull request list at no extra API cost, for issues in bees:review or bees:approved:

Both are backstops. The developer is told to merge the default branch itself before every push, so a pull request is rarely conflicting or behind by the time the reviewer sees it.

An issue in bees:approved goes back to bees:ready and bees:approved is removed from the pull request, exactly as for your feedback. Because it already has a pull request, a developer worker takes it ahead of any new work item. An issue in bees:review keeps its worker: the mail reaches the developer on its next round, or on the next poll once the reviewer approves. The developer’s push then goes through review again, so nothing is merged untested. An issue in bees:in-progress is skipped. The developer is on it already.

The developer is told once per head commit, recorded as conflict_notified_sha in <state_dir>/issues/<n>.json. The same conflicting head is never nagged about twice, but a push that still conflicts is reported again. GitHub computes mergeability lazily, so a pull request whose state is still unknown is left alone until the next poll.

Merging

By default nobody in the factory merges. An approved pull request waits for you, and merging it closes the issue through Closes #N.

The reviewer can be given the job instead. Set auto_merge = true under [roles.reviewer], with optional merge_method, checks_wait, checks_timeout and max_check_fix_rounds, the keys the pre-review read uses too (see configuration.md). Once the reviewer approves, the developer worker enters a checks stage:

  1. It waits checks_wait (default 1 minute), because some checks take a moment to report that they have started.
  2. It polls the pull request’s checks every checks_poll_interval (default 2 minutes) until none is pending. The required checks (gh pr checks --required) are the gate whenever the branch has any. When it has none, a repository with no branch protection, every check the pull request reports (gh pr checks) is the gate instead: gating on the checks that exist beats gating on nothing. To take a check out of the gate, mark the ones that must block a merge as required in the branch protection rules of the default branch. bees never touches those rules.
  3. All green, the orchestrator merges with gh pr merge --<merge_method> --delete-branch. The issue closes through Closes #N and QA sees the change on its next run. When nothing is reported at all, a repository with no CI, it merges too, after two consecutive empty polls, and logs that no check was reported rather than that the checks passed.
  4. A check failed: the reviewer gets a follow-up session in checks mode. It works out what failed without assuming any particular CI system, then mails the developer the main error message, its cause and how to reproduce it. The developer pushes a fix (pr-updated) and the worker goes straight back to polling the checks in the stage that found the failure, pre-review or post-approval. There is no second full review. When the reviewer decides the failure is unrelated (infrastructure, flakiness) it may re-run the check where the CI system allows and report approved, which means “wait for the checks again”.
  5. The fix loop runs at most max_check_fix_rounds times (default 2). After that, or when the checks are still pending at checks_timeout (default 30 minutes), or when GitHub refuses the merge (typically branch protection that requires a human review), the issue is escalated to bees:needs-human.

With the reviewer role disabled and auto_merge on, a developer’s pull request skips review and goes straight to the checks stage.

Stacked features

A feature broken into work items linked with Blocked by #N (Dependencies) is built independently by default: every work item’s developer worker branches from the default branch and opens a pull request against it, whatever else in the same feature is open. With scheduler.stacked_prs on, a work item whose blocker is a sub-issue of the same feature and already has an open pull request is built on top of that blocker’s branch instead: the blocked_by chain becomes a chain of branches and pull requests, each based on its predecessor’s, rather than every work item merging into the default branch on its own.

Take a feature with two work items, #2 blocked by #1:

If #1’s pull request closes unmerged instead, its branch is not retargeted: GitHub retargets a stacked pull request only when its base branch is deleted, which a merge does and a plain close does not. A worker starting on #2 after that finds no open pull request under the feature to stack on and escalates before it runs any stage, rather than building or approving against a branch nobody is going to merge. Reopening #1’s pull request, or retargeting #2’s at the default branch, and handing the issue back clears it.

Review stays pull request by pull request: the reviewer checks out #2’s branch and reads its diff with gh pr diff, which shows only #2’s own change against #1’s branch, whether or not #1 has merged yet.

Approval waits on the stack. Once #2’s own review passes, its pull request is not labelled bees:approved, and with auto_merge on it is not merged, until #1’s is: merging #2 into #1’s branch while #1 is still under review would carry unreviewed content into the default branch once #1 merges in turn. The developer worker holds its slot rather than freeing it for other work: bees status shows it in the stack-wait stage while it polls #1’s issue, at roles.reviewer.checks_poll_interval, until #1 reaches bees:approved or #1’s pull request merges. Either clears the hold, and #2 goes on to bees:approved (and, with auto_merge, the checks stage) as usual.

Merging is still your call, pull request by pull request, exactly as Merging describes: stacking changes when a pull request may reach bees:approved, not who merges it or how. Merge #1 first, so #2 is merging reviewed content into the default branch rather than into a branch still under review.

Escalation: bees:needs-human

Retries first

Not every dead session is a bad decision. Before escalating, the orchestrator classifies what went wrong:

One failure is neither: the claude session limit. It belongs to the account, not to the session, so the attempt is not repeated and the issue is not escalated. The issue keeps its state label and the whole factory stops dispatching until the limit resets. See The claude session limit.

Set scheduler.retries = 0 to escalate every failure at once. Retries apply to sessions only. Git, gh and worktree failures are not retried.

When the factory gives up

Once retries are exhausted, or the failure was behavioural, the factory hands the issue to a person:

A budget the factory hits does not always escalate: scheduler.max_cost_per_day pauses dispatch instead. Nothing is labelled, no comment is written, and the workers already running finish their loop. The scheduler starts nothing new until the rolling 24-hour spend falls under the threshold max_cost_per_day_resume_percent sets, which by default is the budget itself. bees status reports the pause.

The orchestrator sets bees:needs-human and posts a comment on the issue explaining why. It records the same reason in <state_dir>/issues/<n>.json, which is where bees run’s live view reads it to say what the factory is stuck on without asking GitHub again. An issue you label bees:needs-human by hand has no such record, and the view says so rather than inventing one. The comment mentions everyone in scheduler.notify, since a comment posted under your own account notifies nobody by itself. This is the only comment the orchestrator itself writes in this repository: the other one it writes goes to the busybees repository, on a factory-error report that duplicates an issue already there (see scheduler.report_factory_errors). Roles do comment on GitHub, but only to people: the developer replying to your feedback, the product manager replying to feedback and feature issues or asking a bees:question, always tagged <!-- bees:<role> -->. Everything between roles stays in the mailbox. The reviewer’s last feedback, if any, is there (bees mail list --issue N), and the full transcripts are under <state_dir>/sessions/.

To hand the issue back, remove bees:needs-human and add bees:ready (to retry development, reusing the branch and any pull request) or bees:triage (to have the project manager rework it).

Holding an issue by hand

bees:needs-human is not only the factory’s escalation marker. It is also how you stop the factory working on something. Add it to an issue from the GitHub issue list, without removing the state label underneath, and the issue is held where it is. Nothing dispatches it, it drops out of its own queue, and the live view’s Needs human panel lists it with no reason, since you did not escalate it and there is none to show.

Remove the label and the issue goes straight back to whatever state label it still carries, with nothing else to do. The factory never tidies the pair up, so a held issue keeps its place in the workflow for as long as you leave the label on.

While the hold is on, the issue is out of the states the orchestrator delivers your comments from, so a comment you write on it reaches nobody until you lift the hold. That is the point: nothing is working on it. Comment first, then remove the label, and the session that picks the issue up reads the whole conversation in its prompt.

QA

QA is a singleton that runs against a detached checkout of the default branch, so it only ever sees merged work. It runs when at least scheduler.qa_interval (default 30m) has passed since its last run and something matching the filter has been merged since then. That merged-PR check itself happens at most once per qa_interval. Its first run happens immediately and looks back seven days. Unread mail in the QA inbox triggers an earlier run, whatever the interval: qa_interval is a floor on the runs QA starts by itself, not on the ones a person or the product manager asks it for.

In a session QA works out from the repository’s documentation and its notes how to install dependencies, run the test-suite and exercise the product, verifies each merged pull request against its issue, explores around it, and then:

QA looks for product defects, not for critique of how the code is written. That is the reviewer’s job, on the pull request. It never starts anything that acts on the real world for it (a deploy, a job runner, a command that spends money or writes to the live project the product manages) and uses a sandbox or a dry-run flag instead.

QA stays in its lane. What it files directly is a bug report, or a small work item within the existing design. Anything that asks for new scope goes to the product manager by mail, and the product manager decides whether to drop it or turn it into a proposal for you to approve. QA never opens feature issues itself. See roles.md.

Bugs

The developer, the reviewer and QA all file bugs. They always go in as bees + bees:bug + bees:triage, in the milestone of the issue they were found on, so they flow through the project manager like any other work item. Developers and reviewers only file bugs they notice outside the scope of what they are working on. They do not fix them in passing. QA files through file_bug, which refuses a bug the repository already reports; the developer and the reviewer file with issue_create, which checks nothing, because they are reporting what they ran into in code they were already reading.

Features, sub-issues and milestones

The product manager owns the roadmap of feature issues. It runs at least every scheduler.product_manager_interval (default 1h), or sooner when it has unread mail (questions from the project manager, reports from QA), when a feature or feedback issue is fresh, when you approve a proposal, or when a feature’s work is done (below). A proposal, and an issue in planning, count as fresh only once a person has commented: until then nobody but a person can move either. It writes feature issues that describe user-visible outcomes rather than implementation, and breaks them into work items as described above, with two exceptions: a feature issue it wrote itself starts as a proposal (bees:proposal) and is only broken down once you remove that label (unless scheduler.feature_proposals = false), and an issue you put in planning (bees:planning) is only discussed until you swap that label for bees:planned. Because work items are GitHub sub-issues of their feature, progress is visible on the feature issue itself, in GitHub’s project views, and in the product manager’s prompt.

A feature whose work is done comes back to the product manager. The last open sub-issue of a feature closing is an event nobody would otherwise report: the work items are gone from the queues, and the feature would sit open until the product manager next ran for another reason. Each product manager run records which sub-issues of each feature are open. Every later pass checks those numbers against the issues the poll still finds open, which costs no GitHub call. When all of them have closed, the feature wakes the product manager and is presented in a section of its own as one yes/no decision: is the feature’s original intent complete? If it is, the product manager closes it. If it is not, it says on the issue what is still missing and creates work items for exactly that, because a finished feature is not an invitation to widen it. Only a person can be sure, so it is a decision rather than an automatic close, and it is put once: a feature the product manager deliberately leaves open is not raised again until it gains a sub-issue and that one closes too. A feature whose sub-issues all closed before the orchestrator ever recorded them is picked up on the next run for any other reason instead.

Milestones are managed by people, never by bees. No role creates, edits or closes a milestone. The product manager sees the open milestones read-only and treats them as a priority signal. What the bees do is inherit: every issue they create takes the milestone of the issue it relates to, with one exception, a feature spawned from an agreed design that the design’s phasing puts in a later existing milestone (From an agreed design to several features). A work item gets its parent feature’s milestone, a bug found while working on an issue gets that issue’s milestone (related), and a feature distilled from a feedback issue gets the feedback issue’s milestone, falling back to filter.milestone when the factory is pinned to one. Attaching an existing issue to a feature with issue_link inherits the same way, but only when that issue is in no milestone: one it already has is a person’s decision, and a bee never overwrites or clears it. So if you put a feature into a milestone, everything that grows out of it lands there too.

You shape the roadmap by creating or editing milestones and moving issues between them, editing the product manager’s notes (with the default notes.backend = "file", <state_dir>/notes/product_manager.md; see Notes files), filing feature or feedback issues, or answering the product manager’s questions.

One issue, end to end

sequenceDiagram
    actor H as Human
    participant GH as GitHub
    participant O as Orchestrator
    participant PM as Product manager
    participant PjM as Project manager
    participant Dev as Developer
    participant Rev as Reviewer
    participant QA as QA

    H->>GH: Create feature issue, label `bees` + `bees:feature`
    O->>PM: Session (fresh feature issue)
    PM->>GH: comment question, add bees:question
    H->>GH: Answer in a comment
    O->>GH: human replied → remove bees:question
    O->>PM: Session (feature fresh again)
    PM->>GH: issue_create parent=F: sub-issues, bees:triage, inherit milestone
    PM->>GH: Comment list of work items on the feature
    O->>PjM: Session with triage batch (parent feature shown)
    PjM->>GH: Edit body (scope, acceptance criteria)
    PjM-->>PM: mail: "should X support Y?"
    PjM->>GH: label bees:blocked
    O->>PM: Session with unread mail
    PM-->>PjM: mail: "yes, but only Z"
    O->>GH: answer arrived → bees:triage
    O->>PjM: Session
    PjM->>GH: bees:triage → bees:ready, with a size
    O->>GH: claim → bees:in-progress
    O->>Dev: Session on branch bees/issue-N
    Dev->>GH: push, gh pr create (Closes #N)
    Dev->>O: done: pr-opened
    O->>GH: label PR `bees`, issue → bees:review
    O->>GH: read the PR checks (pre-review)
    O->>Rev: Session on PR branch
    Rev-->>Dev: mail: review round 1 feedback
    Rev->>O: done: changes-requested
    O->>GH: issue → bees:in-progress
    O->>Dev: Session with feedback (round 2)
    Dev->>GH: push
    Dev->>O: done: pr-updated
    O->>GH: issue → bees:review
    O->>Rev: Session (round 2)
    Rev->>O: done: approved
    O->>GH: PR + issue → bees:approved
    H->>GH: Merge PR (work item closes)
    Note over O,GH: with roles.reviewer.auto_merge the orchestrator waits<br/>checks_wait, polls the PR checks and merges instead
    O->>QA: Session on default branch (merged PRs since last run)
    QA->>GH: file bees:bug issues
    QA-->>PM: mail: QA report
    O->>PM: Session (mail)
    PM->>GH: all work items closed → close feature issue