diff --git a/package-lock.json b/package-lock.json
index 617ffb8..ba8a2d7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2715,7 +2715,7 @@
},
"plugins/thread-badges": {
"name": "bb-plugin-thread-badges",
- "version": "0.1.0",
+ "version": "0.2.0",
"license": "MIT",
"dependencies": {
"@hugeicons/core-free-icons": "^4.1.3",
diff --git a/plugins/workflow-stages/PLUGIN_OVERVIEW.md b/plugins/workflow-stages/PLUGIN_OVERVIEW.md
index 994bb07..8215f4a 100644
--- a/plugins/workflow-stages/PLUGIN_OVERVIEW.md
+++ b/plugins/workflow-stages/PLUGIN_OVERVIEW.md
@@ -25,6 +25,13 @@ turn needs you. Every automatic move skips sticky stages, and "work stops" only
undoes the move "a turn starts" made, so a thread you filed by hand while it was
running stays filed.
+One automatic move ships on: a message that asks for a plan files its thread
+under Planning. Filing is otherwise the agent's job, and plan mode is the one
+moment an agent may not do it — `bb stages set` is a mutating call, and plan
+mode forbids those — so without this a `/plan` thread sits unfiled for the whole
+planning session. It reads the composer's own command mention rather than the
+message text, so a thread that merely discusses `/plan` is not filed.
+
## How it works
Ribbon owns the sidebar — rendering, drag-and-drop, manual order, and the stored
diff --git a/plugins/workflow-stages/README.md b/plugins/workflow-stages/README.md
index eb0e05a..ab5083f 100644
--- a/plugins/workflow-stages/README.md
+++ b/plugins/workflow-stages/README.md
@@ -70,6 +70,7 @@ Configured in the same editor:
| Trigger | Default |
| --- | --- |
| A thread with no placement | Inbox |
+| A message asks for a plan | Planning |
| A turn starts | off |
| Work stops | off |
| A question, an approval, or a failed turn | off |
@@ -77,6 +78,19 @@ Configured in the same editor:
"Work stops" only undoes the move "a turn starts" made, so a thread you filed by
hand while it was running stays filed. Every automatic move skips sticky stages.
+"A message asks for a plan" is the one that ships **on**, because it is the one
+trigger the agent cannot answer for itself. Filing is otherwise the agent's job:
+it reads the stage table below and runs `bb stages set`. In plan mode it may not
+— that is a mutating shell call, and plan mode is the one moment an agent is not
+allowed to make those — so a thread you started with `/plan` would sit in Inbox
+through the whole planning session and only get filed once the plan was
+approved, by which point the honest stage is Building.
+
+Detection is the composer's, not a string match: bb's plan action is a
+provider-declared slash command, and the send carries it as a structured mention
+rather than as text. So a message that merely *talks* about `/plan` is not one,
+and a provider that calls its plan action something else is still recognised.
+
The last one ships off on purpose. A stage says where the work is; whether it
needs you is a different axis, and Ribbon already draws that on the row itself.
Pointing it at a stage means a thread bounces there every time an agent asks a
diff --git a/plugins/workflow-stages/app.tsx b/plugins/workflow-stages/app.tsx
index 087aaf8..fd94b0b 100644
--- a/plugins/workflow-stages/app.tsx
+++ b/plugins/workflow-stages/app.tsx
@@ -447,6 +447,15 @@ function WorkflowSettings() {
{options(false)}
+
- A question, an approval, or a failed turn counts as needing you. Automatic moves skip - any stage marked only the user files here, and "when work stops" only undoes - the move "when a turn starts" made. + A plan is asked for when a message opens with your provider's plan command, which is + the one trigger the agent cannot answer itself. A question, an approval, or a failed + turn counts as needing you. Automatic moves skip any stage marked{" "} + only the user files here, and "when work stops" only undoes the move "when a + turn starts" made.
diff --git a/plugins/workflow-stages/server.ts b/plugins/workflow-stages/server.ts index 04c254c..2388b3e 100644 --- a/plugins/workflow-stages/server.ts +++ b/plugins/workflow-stages/server.ts @@ -9,7 +9,11 @@ // plugin — `getGroupingCatalogV1` — plus the placement calls in ribbon.ts. // Nothing has to be registered with Ribbon, and nothing here replaces bb's // sidebar. -import { defineRpcContract, type BbPluginApi } from "@get-bb/plugin-sdk"; +import { + defineRpcContract, + type BbPluginApi, + type PluginDispatchInput, +} from "@get-bb/plugin-sdk"; import { z } from "zod"; import { GLYPHS, GLYPH_NAMES, GROUPING_GLYPH } from "./icons"; import { @@ -179,6 +183,15 @@ export default async function plugin(bb: BbPluginApi) { const suffix = marks.length === 0 ? "" : ` (${marks.join(", ")})`; lines.push(`- ${stage.id} — ${stage.label}${suffix}: ${stage.description || "No rule set."}`); } + const planStage = config.planStageId === null ? null : store.get(config.planStageId); + if (planStage !== null) { + lines.push( + "", + `A message that asks for a plan files the thread under ${planStage.label} on its own,`, + "before you read this. Rule 1 is already satisfied there — leave it, and move it on", + "when the plan is agreed and you start building.", + ); + } lines.push( "", "A stage marked user-only is the user's call: never file into or out of one —", @@ -269,6 +282,90 @@ export default async function plugin(bb: BbPluginApi) { } } + // ------------------------------------------------------------- plan mode + + // Nothing on a thread says "this one is planning": no event carries it, and + // `ThreadResponse` has no field for it. The message does. bb's plan composer + // action is a provider-declared slash command, so a plan-mode send arrives + // as a text block whose leading mention is that command — which is why this + // hangs off the dispatch hook rather than off `bb.events`. + // + // The agent cannot file itself here even though the instructions ask it to: + // `bb stages set` is a mutating shell call, and plan mode is the one moment + // an agent is not allowed to make those. So the plugin does it instead. + const FALLBACK_PLAN_COMMANDS: ReadonlySet