Repository files navigation

bb-plugin-dispatch

Turn a one-liner into a scoped thread in the right project.

bb can spawn a thread, but only once you have already decided both the project and the full prompt. This automates that intake step — the part bb has no surface for. (The Workflows plugin is the execution layer and presupposes both: it must run from an existing project thread and takes a written script.)

bb dispatch "fix the flaky auth test"# preview — nothing spawns
bb dispatch "fix the flaky auth test" --go # spawn
bb dispatch "..." --project proj_abc123 --raw --go # skip both intake steps

Or the Dispatch entry in the sidebar, which does the same thing and drops you into the new thread.

Screenshots

dispatch

Dispatch: turn a one-liner into a scoped thread in the right project.

How it works

  1. Classify — asks a model which of your registered bb projects the request belongs to, and reports its own confidence.
  2. Expand — runs the request through the prompt enhancer (see below).
  3. Spawnsdk.threads.spawn() into the chosen project, which inherits that project's remembered provider/model defaults.

The intake lane

Plugins cannot reach bb's internal inference (PluginHosts exposes only port and tunnel methods), so intake calls a model the only way a plugin can: it spawns a hidden bb thread, waits, reads the output, and archives it. That thread runs on the intake lane — one saved set of selections covering project, provider, model, reasoning level, permission mode, and environment.

Pick it with bb's own new-thread composer, in Settings → Plugins → Dispatch. Submitting saves the selections; the prompt you type is discarded and no thread is started.

Why a composer and not settings fields:

  • pi alone publishes 373 models, so a select descriptor would be worse than free text, and the composer is the only host-owned picker the SDK exports.
  • The composer marks every selection caller-explicit in executionInputSources, and the whole request is stored and spread back into threads.spawn verbatim. That is load-bearing: the server drops a requested providerId/model that arrives with no provenance and re-derives it from the project's stored defaults. A hand-built spawn can therefore run intake on a lane you never chose, silently and at someone else's cost.

The lane lives in plugin kv rather than in settings, because a plugin can read its settings but not write them — the same reason the enhancer template lives there. The plugin declares no settings at all; the composer is the whole configuration surface.

Headless equivalent, for a machine with no UI:

bb dispatch lane # show current
bb dispatch lane --provider pi --model litellm/bulk-primary --project proj_abc123

Intake threads are archived, not deleted — bb refuses destructive actions without an interactive confirmation, so deleting them fails silently and leaks hidden threads.

One path, not two

An earlier version could also POST to an OpenAI-compatible proxy instead of spawning a thread. It was faster, and it could force JSON through response_format where an agent can only be asked. It was still removed: it only ran on a machine with LiteLLM in front of it, so it was a second code path most installs could never take, and it cost four settings descriptors to configure.

The thread path's weaknesses are therefore accepted rather than routed around. An agent has tools and a personality, so the prompts forbid both and parsing is tolerant of narration. Intake is a spawn plus a turn, not one request.

If you do run LiteLLM, none of that is a loss — pi exposes its aliases to bb's model selector (litellm/bulk-primary and friends), so the lane reaches exactly the same proxy without the plugin knowing it exists.

The CLI form synthesises the provenance and a default host environment, since a shell has no picker to resolve them from. It marks only providerId and model explicit; reasoning and permission mode fall through to the usual defaults.

Limitations

It can only route to registered bb projects — but it can tell you what is missing. bb dispatch projects lists repos bb has discovered on this host that are not registered, ranked with last activity and whether an agent has already worked there:

bb dispatch projects # list the gap
bb dispatch projects --register relay,agenda-sync
bb dispatch projects --register all

This is the discovery half of routing fidelity, and it matters more than the classifier. Two requests that misrouted at 0.4 and 0.85 confidence both went to the correct project at 1.0 once the repos they referred to were registered — the classifier had been picking the nearest available project because the right one did not exist.

Nothing here reaches GitHub for repos you have not cloned; those are not dispatchable anyway, since bb needs a local checkout.

Unregistered work is invisible. The classifier picks from sdk.projects.list(). Work that lives in a directory bb does not know about is invisible to it, and the classifier will confidently pick the nearest registered project instead. If dispatch keeps choosing the wrong target, check whether the right one is even a project:

bb project list --include-personal
bb project create --name <name> --root <path> --machine <machine>

Classification accuracy is bounded by context. It sees only project name and path. Observed confidence on real requests has been 0.3–0.4 — honest, but thin. Preview-by-default exists because of this. If it stays low with a complete project list, feed each project's CLAUDE.md/AGENTS.md summary into the classifier prompt.

Intake degrades, it does not guess. Each failure is reported as a note: line and dispatch falls back to what bb does natively — the raw text, to a project you name:

FailureEffect
No intake lane set, or its thread errorsno classification, no expansion — pass --project
Enhancer command fails, or template lacks $ARGUMENTSno expansion; request used as written
Classifier returns an unknown project idignored; pass --project

Threads run in the project checkout, not an isolated worktree.environment: host + workspace: unmanaged with a null path. Right default for "go fix this"; if you want every dispatch branch-isolated, change the spawn call.

The CLI cannot prompt. A plugin command returns stdout and has no interactive channel, so the preview/--go two-step is the confirmation.

Settings

There are none — bb plugin config dispatch reports "This plugin declares no settings." Both things that need configuring outgrew what a settings descriptor can express, so each lives where it can be edited properly:

WhatWhereHeadless
Intake laneSettings → Plugins → Dispatch (bb's composer)bb dispatch lane
Enhancer templateThe Dispatch panelbb dispatch enhancer
Dispatch historyThe Dispatch panel (below the composer)

Reload after changing either: bb plugin reload dispatch.

Prompt enhancer

Two sources, chosen from a dropdown in the panel:

  • Text (default) — a template edited in a textarea. Ships with a working default, so a fresh install needs no vault and no external tooling.
  • Command — a shell command whose stdout is the template. Use this to keep the prompt where it already lives instead of copying it: obsidian read path="Prompts/Enhancer.md". Fenced output (``` or ~~~) is unwrapped automatically.

Set it headlessly (the panel editor is the other way):

bb dispatch enhancer # show current
bb dispatch enhancer --command 'obsidian read path="Prompts/Enhancer.md"'
bb dispatch enhancer --source text # back to the bundled template

Setting it resolves the template immediately and fails loudly if the command does not produce one, rather than silently degrading at the next dispatch.

Either way the template must contain $ARGUMENTS, which is replaced with the request. Without it, expansion is skipped rather than sending a malformed prompt.

Command mode runs through a shell. That is the same trust level as the rest of the plugin — full-trust code on your own machine — but it is not sandboxed.

Reload after changing settings: bb plugin reload dispatch.

Developing

Installed from a path, so the backend loads server.ts directly — edit and bb plugin reload dispatch. Frontend changes need bb plugin build . first, or run bb plugin dev to watch.

Alias the SDK's experimental_ components before using them in JSX. A lowercase-leading tag resolves to an intrinsic HTML element, never to a component in scope, so <experimental_NewThreadComposer /> renders a literal <experimental_newthreadcomposer> custom element with every prop stringified onto it. It type-checks, logs nothing, and shows an empty gap where the component should be. Assign it to a capitalized name first.

About

Turn a one-liner into a scoped thread in the right bb project: classify, expand, spawn.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

bb-plugin-dispatch

Turn a one-liner into a scoped thread in the right project.

bb can spawn a thread, but only once you have already decided both the project and the full prompt. This automates that intake step — the part bb has no surface for. (The Workflows plugin is the execution layer and presupposes both: it must run from an existing project thread and takes a written script.)

bb dispatch "fix the flaky auth test"# preview — nothing spawns
bb dispatch "fix the flaky auth test" --go # spawn
bb dispatch "..." --project proj_abc123 --raw --go # skip both intake steps

Or the Dispatch entry in the sidebar, which does the same thing and drops you into the new thread.

Screenshots

dispatch

Dispatch: turn a one-liner into a scoped thread in the right project.

How it works

  1. Classify — asks a model which of your registered bb projects the request belongs to, and reports its own confidence.
  2. Expand — runs the request through the prompt enhancer (see below).
  3. Spawnsdk.threads.spawn() into the chosen project, which inherits that project's remembered provider/model defaults.

The intake lane

Plugins cannot reach bb's internal inference (PluginHosts exposes only port and tunnel methods), so intake calls a model the only way a plugin can: it spawns a hidden bb thread, waits, reads the output, and archives it. That thread runs on the intake lane — one saved set of selections covering project, provider, model, reasoning level, permission mode, and environment.

Pick it with bb's own new-thread composer, in Settings → Plugins → Dispatch. Submitting saves the selections; the prompt you type is discarded and no thread is started.

Why a composer and not settings fields:

  • pi alone publishes 373 models, so a select descriptor would be worse than free text, and the composer is the only host-owned picker the SDK exports.
  • The composer marks every selection caller-explicit in executionInputSources, and the whole request is stored and spread back into threads.spawn verbatim. That is load-bearing: the server drops a requested providerId/model that arrives with no provenance and re-derives it from the project's stored defaults. A hand-built spawn can therefore run intake on a lane you never chose, silently and at someone else's cost.

The lane lives in plugin kv rather than in settings, because a plugin can read its settings but not write them — the same reason the enhancer template lives there. The plugin declares no settings at all; the composer is the whole configuration surface.

Headless equivalent, for a machine with no UI:

bb dispatch lane # show current
bb dispatch lane --provider pi --model litellm/bulk-primary --project proj_abc123

Intake threads are archived, not deleted — bb refuses destructive actions without an interactive confirmation, so deleting them fails silently and leaks hidden threads.

One path, not two

An earlier version could also POST to an OpenAI-compatible proxy instead of spawning a thread. It was faster, and it could force JSON through response_format where an agent can only be asked. It was still removed: it only ran on a machine with LiteLLM in front of it, so it was a second code path most installs could never take, and it cost four settings descriptors to configure.

The thread path's weaknesses are therefore accepted rather than routed around. An agent has tools and a personality, so the prompts forbid both and parsing is tolerant of narration. Intake is a spawn plus a turn, not one request.

If you do run LiteLLM, none of that is a loss — pi exposes its aliases to bb's model selector (litellm/bulk-primary and friends), so the lane reaches exactly the same proxy without the plugin knowing it exists.

The CLI form synthesises the provenance and a default host environment, since a shell has no picker to resolve them from. It marks only providerId and model explicit; reasoning and permission mode fall through to the usual defaults.

Limitations

It can only route to registered bb projects — but it can tell you what is missing. bb dispatch projects lists repos bb has discovered on this host that are not registered, ranked with last activity and whether an agent has already worked there:

bb dispatch projects # list the gap
bb dispatch projects --register relay,agenda-sync
bb dispatch projects --register all

This is the discovery half of routing fidelity, and it matters more than the classifier. Two requests that misrouted at 0.4 and 0.85 confidence both went to the correct project at 1.0 once the repos they referred to were registered — the classifier had been picking the nearest available project because the right one did not exist.

Nothing here reaches GitHub for repos you have not cloned; those are not dispatchable anyway, since bb needs a local checkout.

Unregistered work is invisible. The classifier picks from sdk.projects.list(). Work that lives in a directory bb does not know about is invisible to it, and the classifier will confidently pick the nearest registered project instead. If dispatch keeps choosing the wrong target, check whether the right one is even a project:

bb project list --include-personal
bb project create --name <name> --root <path> --machine <machine>

Classification accuracy is bounded by context. It sees only project name and path. Observed confidence on real requests has been 0.3–0.4 — honest, but thin. Preview-by-default exists because of this. If it stays low with a complete project list, feed each project's CLAUDE.md/AGENTS.md summary into the classifier prompt.

Intake degrades, it does not guess. Each failure is reported as a note: line and dispatch falls back to what bb does natively — the raw text, to a project you name:

FailureEffect
No intake lane set, or its thread errorsno classification, no expansion — pass --project
Enhancer command fails, or template lacks $ARGUMENTSno expansion; request used as written
Classifier returns an unknown project idignored; pass --project

Threads run in the project checkout, not an isolated worktree.environment: host + workspace: unmanaged with a null path. Right default for "go fix this"; if you want every dispatch branch-isolated, change the spawn call.

The CLI cannot prompt. A plugin command returns stdout and has no interactive channel, so the preview/--go two-step is the confirmation.

Settings

There are none — bb plugin config dispatch reports "This plugin declares no settings." Both things that need configuring outgrew what a settings descriptor can express, so each lives where it can be edited properly:

WhatWhereHeadless
Intake laneSettings → Plugins → Dispatch (bb's composer)bb dispatch lane
Enhancer templateThe Dispatch panelbb dispatch enhancer
Dispatch historyThe Dispatch panel (below the composer)

Reload after changing either: bb plugin reload dispatch.

Prompt enhancer

Two sources, chosen from a dropdown in the panel:

  • Text (default) — a template edited in a textarea. Ships with a working default, so a fresh install needs no vault and no external tooling.
  • Command — a shell command whose stdout is the template. Use this to keep the prompt where it already lives instead of copying it: obsidian read path="Prompts/Enhancer.md". Fenced output (``` or ~~~) is unwrapped automatically.

Set it headlessly (the panel editor is the other way):

bb dispatch enhancer # show current
bb dispatch enhancer --command 'obsidian read path="Prompts/Enhancer.md"'
bb dispatch enhancer --source text # back to the bundled template

Setting it resolves the template immediately and fails loudly if the command does not produce one, rather than silently degrading at the next dispatch.

Either way the template must contain $ARGUMENTS, which is replaced with the request. Without it, expansion is skipped rather than sending a malformed prompt.

Command mode runs through a shell. That is the same trust level as the rest of the plugin — full-trust code on your own machine — but it is not sandboxed.

Reload after changing settings: bb plugin reload dispatch.

Developing

Installed from a path, so the backend loads server.ts directly — edit and bb plugin reload dispatch. Frontend changes need bb plugin build . first, or run bb plugin dev to watch.

Alias the SDK's experimental_ components before using them in JSX. A lowercase-leading tag resolves to an intrinsic HTML element, never to a component in scope, so <experimental_NewThreadComposer /> renders a literal <experimental_newthreadcomposer> custom element with every prop stringified onto it. It type-checks, logs nothing, and shows an empty gap where the component should be. Assign it to a capitalized name first.

About

Turn a one-liner into a scoped thread in the right bb project: classify, expand, spawn.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

bb-plugin-dispatch

Turn a one-liner into a scoped thread in the right project.

bb can spawn a thread, but only once you have already decided both the project and the full prompt. This automates that intake step — the part bb has no surface for. (The Workflows plugin is the execution layer and presupposes both: it must run from an existing project thread and takes a written script.)

bb dispatch "fix the flaky auth test"# preview — nothing spawns
bb dispatch "fix the flaky auth test" --go # spawn
bb dispatch "..." --project proj_abc123 --raw --go # skip both intake steps

Or the Dispatch entry in the sidebar, which does the same thing and drops you into the new thread.

Screenshots

dispatch

Dispatch: turn a one-liner into a scoped thread in the right project.

How it works

  1. Classify — asks a model which of your registered bb projects the request belongs to, and reports its own confidence.
  2. Expand — runs the request through the prompt enhancer (see below).
  3. Spawnsdk.threads.spawn() into the chosen project, which inherits that project's remembered provider/model defaults.

The intake lane

Plugins cannot reach bb's internal inference (PluginHosts exposes only port and tunnel methods), so intake calls a model the only way a plugin can: it spawns a hidden bb thread, waits, reads the output, and archives it. That thread runs on the intake lane — one saved set of selections covering project, provider, model, reasoning level, permission mode, and environment.

Pick it with bb's own new-thread composer, in Settings → Plugins → Dispatch. Submitting saves the selections; the prompt you type is discarded and no thread is started.

Why a composer and not settings fields:

  • pi alone publishes 373 models, so a select descriptor would be worse than free text, and the composer is the only host-owned picker the SDK exports.
  • The composer marks every selection caller-explicit in executionInputSources, and the whole request is stored and spread back into threads.spawn verbatim. That is load-bearing: the server drops a requested providerId/model that arrives with no provenance and re-derives it from the project's stored defaults. A hand-built spawn can therefore run intake on a lane you never chose, silently and at someone else's cost.

The lane lives in plugin kv rather than in settings, because a plugin can read its settings but not write them — the same reason the enhancer template lives there. The plugin declares no settings at all; the composer is the whole configuration surface.

Headless equivalent, for a machine with no UI:

bb dispatch lane # show current
bb dispatch lane --provider pi --model litellm/bulk-primary --project proj_abc123

Intake threads are archived, not deleted — bb refuses destructive actions without an interactive confirmation, so deleting them fails silently and leaks hidden threads.

One path, not two

An earlier version could also POST to an OpenAI-compatible proxy instead of spawning a thread. It was faster, and it could force JSON through response_format where an agent can only be asked. It was still removed: it only ran on a machine with LiteLLM in front of it, so it was a second code path most installs could never take, and it cost four settings descriptors to configure.

The thread path's weaknesses are therefore accepted rather than routed around. An agent has tools and a personality, so the prompts forbid both and parsing is tolerant of narration. Intake is a spawn plus a turn, not one request.

If you do run LiteLLM, none of that is a loss — pi exposes its aliases to bb's model selector (litellm/bulk-primary and friends), so the lane reaches exactly the same proxy without the plugin knowing it exists.

The CLI form synthesises the provenance and a default host environment, since a shell has no picker to resolve them from. It marks only providerId and model explicit; reasoning and permission mode fall through to the usual defaults.

Limitations

It can only route to registered bb projects — but it can tell you what is missing. bb dispatch projects lists repos bb has discovered on this host that are not registered, ranked with last activity and whether an agent has already worked there:

bb dispatch projects # list the gap
bb dispatch projects --register relay,agenda-sync
bb dispatch projects --register all

This is the discovery half of routing fidelity, and it matters more than the classifier. Two requests that misrouted at 0.4 and 0.85 confidence both went to the correct project at 1.0 once the repos they referred to were registered — the classifier had been picking the nearest available project because the right one did not exist.

Nothing here reaches GitHub for repos you have not cloned; those are not dispatchable anyway, since bb needs a local checkout.

Unregistered work is invisible. The classifier picks from sdk.projects.list(). Work that lives in a directory bb does not know about is invisible to it, and the classifier will confidently pick the nearest registered project instead. If dispatch keeps choosing the wrong target, check whether the right one is even a project:

bb project list --include-personal
bb project create --name <name> --root <path> --machine <machine>

Classification accuracy is bounded by context. It sees only project name and path. Observed confidence on real requests has been 0.3–0.4 — honest, but thin. Preview-by-default exists because of this. If it stays low with a complete project list, feed each project's CLAUDE.md/AGENTS.md summary into the classifier prompt.

Intake degrades, it does not guess. Each failure is reported as a note: line and dispatch falls back to what bb does natively — the raw text, to a project you name:

FailureEffect
No intake lane set, or its thread errorsno classification, no expansion — pass --project
Enhancer command fails, or template lacks $ARGUMENTSno expansion; request used as written
Classifier returns an unknown project idignored; pass --project

Threads run in the project checkout, not an isolated worktree.environment: host + workspace: unmanaged with a null path. Right default for "go fix this"; if you want every dispatch branch-isolated, change the spawn call.

The CLI cannot prompt. A plugin command returns stdout and has no interactive channel, so the preview/--go two-step is the confirmation.

Settings

There are none — bb plugin config dispatch reports "This plugin declares no settings." Both things that need configuring outgrew what a settings descriptor can express, so each lives where it can be edited properly:

WhatWhereHeadless
Intake laneSettings → Plugins → Dispatch (bb's composer)bb dispatch lane
Enhancer templateThe Dispatch panelbb dispatch enhancer
Dispatch historyThe Dispatch panel (below the composer)

Reload after changing either: bb plugin reload dispatch.

Prompt enhancer

Two sources, chosen from a dropdown in the panel:

  • Text (default) — a template edited in a textarea. Ships with a working default, so a fresh install needs no vault and no external tooling.
  • Command — a shell command whose stdout is the template. Use this to keep the prompt where it already lives instead of copying it: obsidian read path="Prompts/Enhancer.md". Fenced output (``` or ~~~) is unwrapped automatically.

Set it headlessly (the panel editor is the other way):

bb dispatch enhancer # show current
bb dispatch enhancer --command 'obsidian read path="Prompts/Enhancer.md"'
bb dispatch enhancer --source text # back to the bundled template

Setting it resolves the template immediately and fails loudly if the command does not produce one, rather than silently degrading at the next dispatch.

Either way the template must contain $ARGUMENTS, which is replaced with the request. Without it, expansion is skipped rather than sending a malformed prompt.

Command mode runs through a shell. That is the same trust level as the rest of the plugin — full-trust code on your own machine — but it is not sandboxed.

Reload after changing settings: bb plugin reload dispatch.

Developing

Installed from a path, so the backend loads server.ts directly — edit and bb plugin reload dispatch. Frontend changes need bb plugin build . first, or run bb plugin dev to watch.

Alias the SDK's experimental_ components before using them in JSX. A lowercase-leading tag resolves to an intrinsic HTML element, never to a component in scope, so <experimental_NewThreadComposer /> renders a literal <experimental_newthreadcomposer> custom element with every prop stringified onto it. It type-checks, logs nothing, and shows an empty gap where the component should be. Assign it to a capitalized name first.

About

Turn a one-liner into a scoped thread in the right bb project: classify, expand, spawn.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

bb-plugin-dispatch

Turn a one-liner into a scoped thread in the right project.

bb can spawn a thread, but only once you have already decided both the project and the full prompt. This automates that intake step — the part bb has no surface for. (The Workflows plugin is the execution layer and presupposes both: it must run from an existing project thread and takes a written script.)

bb dispatch "fix the flaky auth test"# preview — nothing spawns
bb dispatch "fix the flaky auth test" --go # spawn
bb dispatch "..." --project proj_abc123 --raw --go # skip both intake steps

Or the Dispatch entry in the sidebar, which does the same thing and drops you into the new thread.

Screenshots

dispatch

Dispatch: turn a one-liner into a scoped thread in the right project.

How it works

  1. Classify — asks a model which of your registered bb projects the request belongs to, and reports its own confidence.
  2. Expand — runs the request through the prompt enhancer (see below).
  3. Spawnsdk.threads.spawn() into the chosen project, which inherits that project's remembered provider/model defaults.

The intake lane

Plugins cannot reach bb's internal inference (PluginHosts exposes only port and tunnel methods), so intake calls a model the only way a plugin can: it spawns a hidden bb thread, waits, reads the output, and archives it. That thread runs on the intake lane — one saved set of selections covering project, provider, model, reasoning level, permission mode, and environment.

Pick it with bb's own new-thread composer, in Settings → Plugins → Dispatch. Submitting saves the selections; the prompt you type is discarded and no thread is started.

Why a composer and not settings fields:

  • pi alone publishes 373 models, so a select descriptor would be worse than free text, and the composer is the only host-owned picker the SDK exports.
  • The composer marks every selection caller-explicit in executionInputSources, and the whole request is stored and spread back into threads.spawn verbatim. That is load-bearing: the server drops a requested providerId/model that arrives with no provenance and re-derives it from the project's stored defaults. A hand-built spawn can therefore run intake on a lane you never chose, silently and at someone else's cost.

The lane lives in plugin kv rather than in settings, because a plugin can read its settings but not write them — the same reason the enhancer template lives there. The plugin declares no settings at all; the composer is the whole configuration surface.

Headless equivalent, for a machine with no UI:

bb dispatch lane # show current
bb dispatch lane --provider pi --model litellm/bulk-primary --project proj_abc123

Intake threads are archived, not deleted — bb refuses destructive actions without an interactive confirmation, so deleting them fails silently and leaks hidden threads.

One path, not two

An earlier version could also POST to an OpenAI-compatible proxy instead of spawning a thread. It was faster, and it could force JSON through response_format where an agent can only be asked. It was still removed: it only ran on a machine with LiteLLM in front of it, so it was a second code path most installs could never take, and it cost four settings descriptors to configure.

The thread path's weaknesses are therefore accepted rather than routed around. An agent has tools and a personality, so the prompts forbid both and parsing is tolerant of narration. Intake is a spawn plus a turn, not one request.

If you do run LiteLLM, none of that is a loss — pi exposes its aliases to bb's model selector (litellm/bulk-primary and friends), so the lane reaches exactly the same proxy without the plugin knowing it exists.

The CLI form synthesises the provenance and a default host environment, since a shell has no picker to resolve them from. It marks only providerId and model explicit; reasoning and permission mode fall through to the usual defaults.

Limitations

It can only route to registered bb projects — but it can tell you what is missing. bb dispatch projects lists repos bb has discovered on this host that are not registered, ranked with last activity and whether an agent has already worked there:

bb dispatch projects # list the gap
bb dispatch projects --register relay,agenda-sync
bb dispatch projects --register all

This is the discovery half of routing fidelity, and it matters more than the classifier. Two requests that misrouted at 0.4 and 0.85 confidence both went to the correct project at 1.0 once the repos they referred to were registered — the classifier had been picking the nearest available project because the right one did not exist.

Nothing here reaches GitHub for repos you have not cloned; those are not dispatchable anyway, since bb needs a local checkout.

Unregistered work is invisible. The classifier picks from sdk.projects.list(). Work that lives in a directory bb does not know about is invisible to it, and the classifier will confidently pick the nearest registered project instead. If dispatch keeps choosing the wrong target, check whether the right one is even a project:

bb project list --include-personal
bb project create --name <name> --root <path> --machine <machine>

Classification accuracy is bounded by context. It sees only project name and path. Observed confidence on real requests has been 0.3–0.4 — honest, but thin. Preview-by-default exists because of this. If it stays low with a complete project list, feed each project's CLAUDE.md/AGENTS.md summary into the classifier prompt.

Intake degrades, it does not guess. Each failure is reported as a note: line and dispatch falls back to what bb does natively — the raw text, to a project you name:

FailureEffect
No intake lane set, or its thread errorsno classification, no expansion — pass --project
Enhancer command fails, or template lacks $ARGUMENTSno expansion; request used as written
Classifier returns an unknown project idignored; pass --project

Threads run in the project checkout, not an isolated worktree.environment: host + workspace: unmanaged with a null path. Right default for "go fix this"; if you want every dispatch branch-isolated, change the spawn call.

The CLI cannot prompt. A plugin command returns stdout and has no interactive channel, so the preview/--go two-step is the confirmation.

Settings

There are none — bb plugin config dispatch reports "This plugin declares no settings." Both things that need configuring outgrew what a settings descriptor can express, so each lives where it can be edited properly:

WhatWhereHeadless
Intake laneSettings → Plugins → Dispatch (bb's composer)bb dispatch lane
Enhancer templateThe Dispatch panelbb dispatch enhancer
Dispatch historyThe Dispatch panel (below the composer)

Reload after changing either: bb plugin reload dispatch.

Prompt enhancer

Two sources, chosen from a dropdown in the panel:

  • Text (default) — a template edited in a textarea. Ships with a working default, so a fresh install needs no vault and no external tooling.
  • Command — a shell command whose stdout is the template. Use this to keep the prompt where it already lives instead of copying it: obsidian read path="Prompts/Enhancer.md". Fenced output (``` or ~~~) is unwrapped automatically.

Set it headlessly (the panel editor is the other way):

bb dispatch enhancer # show current
bb dispatch enhancer --command 'obsidian read path="Prompts/Enhancer.md"'
bb dispatch enhancer --source text # back to the bundled template

Setting it resolves the template immediately and fails loudly if the command does not produce one, rather than silently degrading at the next dispatch.

Either way the template must contain $ARGUMENTS, which is replaced with the request. Without it, expansion is skipped rather than sending a malformed prompt.

Command mode runs through a shell. That is the same trust level as the rest of the plugin — full-trust code on your own machine — but it is not sandboxed.

Reload after changing settings: bb plugin reload dispatch.

Developing

Installed from a path, so the backend loads server.ts directly — edit and bb plugin reload dispatch. Frontend changes need bb plugin build . first, or run bb plugin dev to watch.

Alias the SDK's experimental_ components before using them in JSX. A lowercase-leading tag resolves to an intrinsic HTML element, never to a component in scope, so <experimental_NewThreadComposer /> renders a literal <experimental_newthreadcomposer> custom element with every prop stringified onto it. It type-checks, logs nothing, and shows an empty gap where the component should be. Assign it to a capitalized name first.

About

Turn a one-liner into a scoped thread in the right bb project: classify, expand, spawn.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

bb-plugin-dispatch

Turn a one-liner into a scoped thread in the right project.

bb can spawn a thread, but only once you have already decided both the project and the full prompt. This automates that intake step — the part bb has no surface for. (The Workflows plugin is the execution layer and presupposes both: it must run from an existing project thread and takes a written script.)

bb dispatch "fix the flaky auth test"# preview — nothing spawns
bb dispatch "fix the flaky auth test" --go # spawn
bb dispatch "..." --project proj_abc123 --raw --go # skip both intake steps

Or the Dispatch entry in the sidebar, which does the same thing and drops you into the new thread.

Screenshots

dispatch

Dispatch: turn a one-liner into a scoped thread in the right project.

How it works

  1. Classify — asks a model which of your registered bb projects the request belongs to, and reports its own confidence.
  2. Expand — runs the request through the prompt enhancer (see below).
  3. Spawnsdk.threads.spawn() into the chosen project, which inherits that project's remembered provider/model defaults.

The intake lane

Plugins cannot reach bb's internal inference (PluginHosts exposes only port and tunnel methods), so intake calls a model the only way a plugin can: it spawns a hidden bb thread, waits, reads the output, and archives it. That thread runs on the intake lane — one saved set of selections covering project, provider, model, reasoning level, permission mode, and environment.

Pick it with bb's own new-thread composer, in Settings → Plugins → Dispatch. Submitting saves the selections; the prompt you type is discarded and no thread is started.

Why a composer and not settings fields:

  • pi alone publishes 373 models, so a select descriptor would be worse than free text, and the composer is the only host-owned picker the SDK exports.
  • The composer marks every selection caller-explicit in executionInputSources, and the whole request is stored and spread back into threads.spawn verbatim. That is load-bearing: the server drops a requested providerId/model that arrives with no provenance and re-derives it from the project's stored defaults. A hand-built spawn can therefore run intake on a lane you never chose, silently and at someone else's cost.

The lane lives in plugin kv rather than in settings, because a plugin can read its settings but not write them — the same reason the enhancer template lives there. The plugin declares no settings at all; the composer is the whole configuration surface.

Headless equivalent, for a machine with no UI:

bb dispatch lane # show current
bb dispatch lane --provider pi --model litellm/bulk-primary --project proj_abc123

Intake threads are archived, not deleted — bb refuses destructive actions without an interactive confirmation, so deleting them fails silently and leaks hidden threads.

One path, not two

An earlier version could also POST to an OpenAI-compatible proxy instead of spawning a thread. It was faster, and it could force JSON through response_format where an agent can only be asked. It was still removed: it only ran on a machine with LiteLLM in front of it, so it was a second code path most installs could never take, and it cost four settings descriptors to configure.

The thread path's weaknesses are therefore accepted rather than routed around. An agent has tools and a personality, so the prompts forbid both and parsing is tolerant of narration. Intake is a spawn plus a turn, not one request.

If you do run LiteLLM, none of that is a loss — pi exposes its aliases to bb's model selector (litellm/bulk-primary and friends), so the lane reaches exactly the same proxy without the plugin knowing it exists.

The CLI form synthesises the provenance and a default host environment, since a shell has no picker to resolve them from. It marks only providerId and model explicit; reasoning and permission mode fall through to the usual defaults.

Limitations

It can only route to registered bb projects — but it can tell you what is missing. bb dispatch projects lists repos bb has discovered on this host that are not registered, ranked with last activity and whether an agent has already worked there:

bb dispatch projects # list the gap
bb dispatch projects --register relay,agenda-sync
bb dispatch projects --register all

This is the discovery half of routing fidelity, and it matters more than the classifier. Two requests that misrouted at 0.4 and 0.85 confidence both went to the correct project at 1.0 once the repos they referred to were registered — the classifier had been picking the nearest available project because the right one did not exist.

Nothing here reaches GitHub for repos you have not cloned; those are not dispatchable anyway, since bb needs a local checkout.

Unregistered work is invisible. The classifier picks from sdk.projects.list(). Work that lives in a directory bb does not know about is invisible to it, and the classifier will confidently pick the nearest registered project instead. If dispatch keeps choosing the wrong target, check whether the right one is even a project:

bb project list --include-personal
bb project create --name <name> --root <path> --machine <machine>

Classification accuracy is bounded by context. It sees only project name and path. Observed confidence on real requests has been 0.3–0.4 — honest, but thin. Preview-by-default exists because of this. If it stays low with a complete project list, feed each project's CLAUDE.md/AGENTS.md summary into the classifier prompt.

Intake degrades, it does not guess. Each failure is reported as a note: line and dispatch falls back to what bb does natively — the raw text, to a project you name:

FailureEffect
No intake lane set, or its thread errorsno classification, no expansion — pass --project
Enhancer command fails, or template lacks $ARGUMENTSno expansion; request used as written
Classifier returns an unknown project idignored; pass --project

Threads run in the project checkout, not an isolated worktree.environment: host + workspace: unmanaged with a null path. Right default for "go fix this"; if you want every dispatch branch-isolated, change the spawn call.

The CLI cannot prompt. A plugin command returns stdout and has no interactive channel, so the preview/--go two-step is the confirmation.

Settings

There are none — bb plugin config dispatch reports "This plugin declares no settings." Both things that need configuring outgrew what a settings descriptor can express, so each lives where it can be edited properly:

WhatWhereHeadless
Intake laneSettings → Plugins → Dispatch (bb's composer)bb dispatch lane
Enhancer templateThe Dispatch panelbb dispatch enhancer
Dispatch historyThe Dispatch panel (below the composer)

Reload after changing either: bb plugin reload dispatch.

Prompt enhancer

Two sources, chosen from a dropdown in the panel:

  • Text (default) — a template edited in a textarea. Ships with a working default, so a fresh install needs no vault and no external tooling.
  • Command — a shell command whose stdout is the template. Use this to keep the prompt where it already lives instead of copying it: obsidian read path="Prompts/Enhancer.md". Fenced output (``` or ~~~) is unwrapped automatically.

Set it headlessly (the panel editor is the other way):

bb dispatch enhancer # show current
bb dispatch enhancer --command 'obsidian read path="Prompts/Enhancer.md"'
bb dispatch enhancer --source text # back to the bundled template

Setting it resolves the template immediately and fails loudly if the command does not produce one, rather than silently degrading at the next dispatch.

Either way the template must contain $ARGUMENTS, which is replaced with the request. Without it, expansion is skipped rather than sending a malformed prompt.

Command mode runs through a shell. That is the same trust level as the rest of the plugin — full-trust code on your own machine — but it is not sandboxed.

Reload after changing settings: bb plugin reload dispatch.

Developing

Installed from a path, so the backend loads server.ts directly — edit and bb plugin reload dispatch. Frontend changes need bb plugin build . first, or run bb plugin dev to watch.

Alias the SDK's experimental_ components before using them in JSX. A lowercase-leading tag resolves to an intrinsic HTML element, never to a component in scope, so <experimental_NewThreadComposer /> renders a literal <experimental_newthreadcomposer> custom element with every prop stringified onto it. It type-checks, logs nothing, and shows an empty gap where the component should be. Assign it to a capitalized name first.

About

Turn a one-liner into a scoped thread in the right bb project: classify, expand, spawn.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

bb-plugin-dispatch

Turn a one-liner into a scoped thread in the right project.

bb can spawn a thread, but only once you have already decided both the project and the full prompt. This automates that intake step — the part bb has no surface for. (The Workflows plugin is the execution layer and presupposes both: it must run from an existing project thread and takes a written script.)

bb dispatch "fix the flaky auth test"# preview — nothing spawns
bb dispatch "fix the flaky auth test" --go # spawn
bb dispatch "..." --project proj_abc123 --raw --go # skip both intake steps

Or the Dispatch entry in the sidebar, which does the same thing and drops you into the new thread.

Screenshots

dispatch

Dispatch: turn a one-liner into a scoped thread in the right project.

How it works

  1. Classify — asks a model which of your registered bb projects the request belongs to, and reports its own confidence.
  2. Expand — runs the request through the prompt enhancer (see below).
  3. Spawnsdk.threads.spawn() into the chosen project, which inherits that project's remembered provider/model defaults.

The intake lane

Plugins cannot reach bb's internal inference (PluginHosts exposes only port and tunnel methods), so intake calls a model the only way a plugin can: it spawns a hidden bb thread, waits, reads the output, and archives it. That thread runs on the intake lane — one saved set of selections covering project, provider, model, reasoning level, permission mode, and environment.

Pick it with bb's own new-thread composer, in Settings → Plugins → Dispatch. Submitting saves the selections; the prompt you type is discarded and no thread is started.

Why a composer and not settings fields:

  • pi alone publishes 373 models, so a select descriptor would be worse than free text, and the composer is the only host-owned picker the SDK exports.
  • The composer marks every selection caller-explicit in executionInputSources, and the whole request is stored and spread back into threads.spawn verbatim. That is load-bearing: the server drops a requested providerId/model that arrives with no provenance and re-derives it from the project's stored defaults. A hand-built spawn can therefore run intake on a lane you never chose, silently and at someone else's cost.

The lane lives in plugin kv rather than in settings, because a plugin can read its settings but not write them — the same reason the enhancer template lives there. The plugin declares no settings at all; the composer is the whole configuration surface.

Headless equivalent, for a machine with no UI:

bb dispatch lane # show current
bb dispatch lane --provider pi --model litellm/bulk-primary --project proj_abc123

Intake threads are archived, not deleted — bb refuses destructive actions without an interactive confirmation, so deleting them fails silently and leaks hidden threads.

One path, not two

An earlier version could also POST to an OpenAI-compatible proxy instead of spawning a thread. It was faster, and it could force JSON through response_format where an agent can only be asked. It was still removed: it only ran on a machine with LiteLLM in front of it, so it was a second code path most installs could never take, and it cost four settings descriptors to configure.

The thread path's weaknesses are therefore accepted rather than routed around. An agent has tools and a personality, so the prompts forbid both and parsing is tolerant of narration. Intake is a spawn plus a turn, not one request.

If you do run LiteLLM, none of that is a loss — pi exposes its aliases to bb's model selector (litellm/bulk-primary and friends), so the lane reaches exactly the same proxy without the plugin knowing it exists.

The CLI form synthesises the provenance and a default host environment, since a shell has no picker to resolve them from. It marks only providerId and model explicit; reasoning and permission mode fall through to the usual defaults.

Limitations

It can only route to registered bb projects — but it can tell you what is missing. bb dispatch projects lists repos bb has discovered on this host that are not registered, ranked with last activity and whether an agent has already worked there:

bb dispatch projects # list the gap
bb dispatch projects --register relay,agenda-sync
bb dispatch projects --register all

This is the discovery half of routing fidelity, and it matters more than the classifier. Two requests that misrouted at 0.4 and 0.85 confidence both went to the correct project at 1.0 once the repos they referred to were registered — the classifier had been picking the nearest available project because the right one did not exist.

Nothing here reaches GitHub for repos you have not cloned; those are not dispatchable anyway, since bb needs a local checkout.

Unregistered work is invisible. The classifier picks from sdk.projects.list(). Work that lives in a directory bb does not know about is invisible to it, and the classifier will confidently pick the nearest registered project instead. If dispatch keeps choosing the wrong target, check whether the right one is even a project:

bb project list --include-personal
bb project create --name <name> --root <path> --machine <machine>

Classification accuracy is bounded by context. It sees only project name and path. Observed confidence on real requests has been 0.3–0.4 — honest, but thin. Preview-by-default exists because of this. If it stays low with a complete project list, feed each project's CLAUDE.md/AGENTS.md summary into the classifier prompt.

Intake degrades, it does not guess. Each failure is reported as a note: line and dispatch falls back to what bb does natively — the raw text, to a project you name:

FailureEffect
No intake lane set, or its thread errorsno classification, no expansion — pass --project
Enhancer command fails, or template lacks $ARGUMENTSno expansion; request used as written
Classifier returns an unknown project idignored; pass --project

Threads run in the project checkout, not an isolated worktree.environment: host + workspace: unmanaged with a null path. Right default for "go fix this"; if you want every dispatch branch-isolated, change the spawn call.

The CLI cannot prompt. A plugin command returns stdout and has no interactive channel, so the preview/--go two-step is the confirmation.

Settings

There are none — bb plugin config dispatch reports "This plugin declares no settings." Both things that need configuring outgrew what a settings descriptor can express, so each lives where it can be edited properly:

WhatWhereHeadless
Intake laneSettings → Plugins → Dispatch (bb's composer)bb dispatch lane
Enhancer templateThe Dispatch panelbb dispatch enhancer
Dispatch historyThe Dispatch panel (below the composer)

Reload after changing either: bb plugin reload dispatch.

Prompt enhancer

Two sources, chosen from a dropdown in the panel:

  • Text (default) — a template edited in a textarea. Ships with a working default, so a fresh install needs no vault and no external tooling.
  • Command — a shell command whose stdout is the template. Use this to keep the prompt where it already lives instead of copying it: obsidian read path="Prompts/Enhancer.md". Fenced output (``` or ~~~) is unwrapped automatically.

Set it headlessly (the panel editor is the other way):

bb dispatch enhancer # show current
bb dispatch enhancer --command 'obsidian read path="Prompts/Enhancer.md"'
bb dispatch enhancer --source text # back to the bundled template

Setting it resolves the template immediately and fails loudly if the command does not produce one, rather than silently degrading at the next dispatch.

Either way the template must contain $ARGUMENTS, which is replaced with the request. Without it, expansion is skipped rather than sending a malformed prompt.

Command mode runs through a shell. That is the same trust level as the rest of the plugin — full-trust code on your own machine — but it is not sandboxed.

Reload after changing settings: bb plugin reload dispatch.

Developing

Installed from a path, so the backend loads server.ts directly — edit and bb plugin reload dispatch. Frontend changes need bb plugin build . first, or run bb plugin dev to watch.

Alias the SDK's experimental_ components before using them in JSX. A lowercase-leading tag resolves to an intrinsic HTML element, never to a component in scope, so <experimental_NewThreadComposer /> renders a literal <experimental_newthreadcomposer> custom element with every prop stringified onto it. It type-checks, logs nothing, and shows an empty gap where the component should be. Assign it to a capitalized name first.

About

Turn a one-liner into a scoped thread in the right bb project: classify, expand, spawn.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

bb-plugin-dispatch

Turn a one-liner into a scoped thread in the right project.

bb can spawn a thread, but only once you have already decided both the project and the full prompt. This automates that intake step — the part bb has no surface for. (The Workflows plugin is the execution layer and presupposes both: it must run from an existing project thread and takes a written script.)

bb dispatch "fix the flaky auth test"# preview — nothing spawns
bb dispatch "fix the flaky auth test" --go # spawn
bb dispatch "..." --project proj_abc123 --raw --go # skip both intake steps

Or the Dispatch entry in the sidebar, which does the same thing and drops you into the new thread.

Screenshots

dispatch

Dispatch: turn a one-liner into a scoped thread in the right project.

How it works

  1. Classify — asks a model which of your registered bb projects the request belongs to, and reports its own confidence.
  2. Expand — runs the request through the prompt enhancer (see below).
  3. Spawnsdk.threads.spawn() into the chosen project, which inherits that project's remembered provider/model defaults.

The intake lane

Plugins cannot reach bb's internal inference (PluginHosts exposes only port and tunnel methods), so intake calls a model the only way a plugin can: it spawns a hidden bb thread, waits, reads the output, and archives it. That thread runs on the intake lane — one saved set of selections covering project, provider, model, reasoning level, permission mode, and environment.

Pick it with bb's own new-thread composer, in Settings → Plugins → Dispatch. Submitting saves the selections; the prompt you type is discarded and no thread is started.

Why a composer and not settings fields:

  • pi alone publishes 373 models, so a select descriptor would be worse than free text, and the composer is the only host-owned picker the SDK exports.
  • The composer marks every selection caller-explicit in executionInputSources, and the whole request is stored and spread back into threads.spawn verbatim. That is load-bearing: the server drops a requested providerId/model that arrives with no provenance and re-derives it from the project's stored defaults. A hand-built spawn can therefore run intake on a lane you never chose, silently and at someone else's cost.

The lane lives in plugin kv rather than in settings, because a plugin can read its settings but not write them — the same reason the enhancer template lives there. The plugin declares no settings at all; the composer is the whole configuration surface.

Headless equivalent, for a machine with no UI:

bb dispatch lane # show current
bb dispatch lane --provider pi --model litellm/bulk-primary --project proj_abc123

Intake threads are archived, not deleted — bb refuses destructive actions without an interactive confirmation, so deleting them fails silently and leaks hidden threads.

One path, not two

An earlier version could also POST to an OpenAI-compatible proxy instead of spawning a thread. It was faster, and it could force JSON through response_format where an agent can only be asked. It was still removed: it only ran on a machine with LiteLLM in front of it, so it was a second code path most installs could never take, and it cost four settings descriptors to configure.

The thread path's weaknesses are therefore accepted rather than routed around. An agent has tools and a personality, so the prompts forbid both and parsing is tolerant of narration. Intake is a spawn plus a turn, not one request.

If you do run LiteLLM, none of that is a loss — pi exposes its aliases to bb's model selector (litellm/bulk-primary and friends), so the lane reaches exactly the same proxy without the plugin knowing it exists.

The CLI form synthesises the provenance and a default host environment, since a shell has no picker to resolve them from. It marks only providerId and model explicit; reasoning and permission mode fall through to the usual defaults.

Limitations

It can only route to registered bb projects — but it can tell you what is missing. bb dispatch projects lists repos bb has discovered on this host that are not registered, ranked with last activity and whether an agent has already worked there:

bb dispatch projects # list the gap
bb dispatch projects --register relay,agenda-sync
bb dispatch projects --register all

This is the discovery half of routing fidelity, and it matters more than the classifier. Two requests that misrouted at 0.4 and 0.85 confidence both went to the correct project at 1.0 once the repos they referred to were registered — the classifier had been picking the nearest available project because the right one did not exist.

Nothing here reaches GitHub for repos you have not cloned; those are not dispatchable anyway, since bb needs a local checkout.

Unregistered work is invisible. The classifier picks from sdk.projects.list(). Work that lives in a directory bb does not know about is invisible to it, and the classifier will confidently pick the nearest registered project instead. If dispatch keeps choosing the wrong target, check whether the right one is even a project:

bb project list --include-personal
bb project create --name <name> --root <path> --machine <machine>

Classification accuracy is bounded by context. It sees only project name and path. Observed confidence on real requests has been 0.3–0.4 — honest, but thin. Preview-by-default exists because of this. If it stays low with a complete project list, feed each project's CLAUDE.md/AGENTS.md summary into the classifier prompt.

Intake degrades, it does not guess. Each failure is reported as a note: line and dispatch falls back to what bb does natively — the raw text, to a project you name:

FailureEffect
No intake lane set, or its thread errorsno classification, no expansion — pass --project
Enhancer command fails, or template lacks $ARGUMENTSno expansion; request used as written
Classifier returns an unknown project idignored; pass --project

Threads run in the project checkout, not an isolated worktree.environment: host + workspace: unmanaged with a null path. Right default for "go fix this"; if you want every dispatch branch-isolated, change the spawn call.

The CLI cannot prompt. A plugin command returns stdout and has no interactive channel, so the preview/--go two-step is the confirmation.

Settings

There are none — bb plugin config dispatch reports "This plugin declares no settings." Both things that need configuring outgrew what a settings descriptor can express, so each lives where it can be edited properly:

WhatWhereHeadless
Intake laneSettings → Plugins → Dispatch (bb's composer)bb dispatch lane
Enhancer templateThe Dispatch panelbb dispatch enhancer
Dispatch historyThe Dispatch panel (below the composer)

Reload after changing either: bb plugin reload dispatch.

Prompt enhancer

Two sources, chosen from a dropdown in the panel:

  • Text (default) — a template edited in a textarea. Ships with a working default, so a fresh install needs no vault and no external tooling.
  • Command — a shell command whose stdout is the template. Use this to keep the prompt where it already lives instead of copying it: obsidian read path="Prompts/Enhancer.md". Fenced output (``` or ~~~) is unwrapped automatically.

Set it headlessly (the panel editor is the other way):

bb dispatch enhancer # show current
bb dispatch enhancer --command 'obsidian read path="Prompts/Enhancer.md"'
bb dispatch enhancer --source text # back to the bundled template

Setting it resolves the template immediately and fails loudly if the command does not produce one, rather than silently degrading at the next dispatch.

Either way the template must contain $ARGUMENTS, which is replaced with the request. Without it, expansion is skipped rather than sending a malformed prompt.

Command mode runs through a shell. That is the same trust level as the rest of the plugin — full-trust code on your own machine — but it is not sandboxed.

Reload after changing settings: bb plugin reload dispatch.

Developing

Installed from a path, so the backend loads server.ts directly — edit and bb plugin reload dispatch. Frontend changes need bb plugin build . first, or run bb plugin dev to watch.

Alias the SDK's experimental_ components before using them in JSX. A lowercase-leading tag resolves to an intrinsic HTML element, never to a component in scope, so <experimental_NewThreadComposer /> renders a literal <experimental_newthreadcomposer> custom element with every prop stringified onto it. It type-checks, logs nothing, and shows an empty gap where the component should be. Assign it to a capitalized name first.

About

Turn a one-liner into a scoped thread in the right bb project: classify, expand, spawn.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

bb-plugin-dispatch

Turn a one-liner into a scoped thread in the right project.

bb can spawn a thread, but only once you have already decided both the project and the full prompt. This automates that intake step — the part bb has no surface for. (The Workflows plugin is the execution layer and presupposes both: it must run from an existing project thread and takes a written script.)

bb dispatch "fix the flaky auth test"# preview — nothing spawns
bb dispatch "fix the flaky auth test" --go # spawn
bb dispatch "..." --project proj_abc123 --raw --go # skip both intake steps

Or the Dispatch entry in the sidebar, which does the same thing and drops you into the new thread.

Screenshots

dispatch

Dispatch: turn a one-liner into a scoped thread in the right project.

How it works

  1. Classify — asks a model which of your registered bb projects the request belongs to, and reports its own confidence.
  2. Expand — runs the request through the prompt enhancer (see below).
  3. Spawnsdk.threads.spawn() into the chosen project, which inherits that project's remembered provider/model defaults.

The intake lane

Plugins cannot reach bb's internal inference (PluginHosts exposes only port and tunnel methods), so intake calls a model the only way a plugin can: it spawns a hidden bb thread, waits, reads the output, and archives it. That thread runs on the intake lane — one saved set of selections covering project, provider, model, reasoning level, permission mode, and environment.

Pick it with bb's own new-thread composer, in Settings → Plugins → Dispatch. Submitting saves the selections; the prompt you type is discarded and no thread is started.

Why a composer and not settings fields:

  • pi alone publishes 373 models, so a select descriptor would be worse than free text, and the composer is the only host-owned picker the SDK exports.
  • The composer marks every selection caller-explicit in executionInputSources, and the whole request is stored and spread back into threads.spawn verbatim. That is load-bearing: the server drops a requested providerId/model that arrives with no provenance and re-derives it from the project's stored defaults. A hand-built spawn can therefore run intake on a lane you never chose, silently and at someone else's cost.

The lane lives in plugin kv rather than in settings, because a plugin can read its settings but not write them — the same reason the enhancer template lives there. The plugin declares no settings at all; the composer is the whole configuration surface.

Headless equivalent, for a machine with no UI:

bb dispatch lane # show current
bb dispatch lane --provider pi --model litellm/bulk-primary --project proj_abc123

Intake threads are archived, not deleted — bb refuses destructive actions without an interactive confirmation, so deleting them fails silently and leaks hidden threads.

One path, not two

An earlier version could also POST to an OpenAI-compatible proxy instead of spawning a thread. It was faster, and it could force JSON through response_format where an agent can only be asked. It was still removed: it only ran on a machine with LiteLLM in front of it, so it was a second code path most installs could never take, and it cost four settings descriptors to configure.

The thread path's weaknesses are therefore accepted rather than routed around. An agent has tools and a personality, so the prompts forbid both and parsing is tolerant of narration. Intake is a spawn plus a turn, not one request.

If you do run LiteLLM, none of that is a loss — pi exposes its aliases to bb's model selector (litellm/bulk-primary and friends), so the lane reaches exactly the same proxy without the plugin knowing it exists.

The CLI form synthesises the provenance and a default host environment, since a shell has no picker to resolve them from. It marks only providerId and model explicit; reasoning and permission mode fall through to the usual defaults.

Limitations

It can only route to registered bb projects — but it can tell you what is missing. bb dispatch projects lists repos bb has discovered on this host that are not registered, ranked with last activity and whether an agent has already worked there:

bb dispatch projects # list the gap
bb dispatch projects --register relay,agenda-sync
bb dispatch projects --register all

This is the discovery half of routing fidelity, and it matters more than the classifier. Two requests that misrouted at 0.4 and 0.85 confidence both went to the correct project at 1.0 once the repos they referred to were registered — the classifier had been picking the nearest available project because the right one did not exist.

Nothing here reaches GitHub for repos you have not cloned; those are not dispatchable anyway, since bb needs a local checkout.

Unregistered work is invisible. The classifier picks from sdk.projects.list(). Work that lives in a directory bb does not know about is invisible to it, and the classifier will confidently pick the nearest registered project instead. If dispatch keeps choosing the wrong target, check whether the right one is even a project:

bb project list --include-personal
bb project create --name <name> --root <path> --machine <machine>

Classification accuracy is bounded by context. It sees only project name and path. Observed confidence on real requests has been 0.3–0.4 — honest, but thin. Preview-by-default exists because of this. If it stays low with a complete project list, feed each project's CLAUDE.md/AGENTS.md summary into the classifier prompt.

Intake degrades, it does not guess. Each failure is reported as a note: line and dispatch falls back to what bb does natively — the raw text, to a project you name:

FailureEffect
No intake lane set, or its thread errorsno classification, no expansion — pass --project
Enhancer command fails, or template lacks $ARGUMENTSno expansion; request used as written
Classifier returns an unknown project idignored; pass --project

Threads run in the project checkout, not an isolated worktree.environment: host + workspace: unmanaged with a null path. Right default for "go fix this"; if you want every dispatch branch-isolated, change the spawn call.

The CLI cannot prompt. A plugin command returns stdout and has no interactive channel, so the preview/--go two-step is the confirmation.

Settings

There are none — bb plugin config dispatch reports "This plugin declares no settings." Both things that need configuring outgrew what a settings descriptor can express, so each lives where it can be edited properly:

WhatWhereHeadless
Intake laneSettings → Plugins → Dispatch (bb's composer)bb dispatch lane
Enhancer templateThe Dispatch panelbb dispatch enhancer
Dispatch historyThe Dispatch panel (below the composer)

Reload after changing either: bb plugin reload dispatch.

Prompt enhancer

Two sources, chosen from a dropdown in the panel:

  • Text (default) — a template edited in a textarea. Ships with a working default, so a fresh install needs no vault and no external tooling.
  • Command — a shell command whose stdout is the template. Use this to keep the prompt where it already lives instead of copying it: obsidian read path="Prompts/Enhancer.md". Fenced output (``` or ~~~) is unwrapped automatically.

Set it headlessly (the panel editor is the other way):

bb dispatch enhancer # show current
bb dispatch enhancer --command 'obsidian read path="Prompts/Enhancer.md"'
bb dispatch enhancer --source text # back to the bundled template

Setting it resolves the template immediately and fails loudly if the command does not produce one, rather than silently degrading at the next dispatch.

Either way the template must contain $ARGUMENTS, which is replaced with the request. Without it, expansion is skipped rather than sending a malformed prompt.

Command mode runs through a shell. That is the same trust level as the rest of the plugin — full-trust code on your own machine — but it is not sandboxed.

Reload after changing settings: bb plugin reload dispatch.

Developing

Installed from a path, so the backend loads server.ts directly — edit and bb plugin reload dispatch. Frontend changes need bb plugin build . first, or run bb plugin dev to watch.

Alias the SDK's experimental_ components before using them in JSX. A lowercase-leading tag resolves to an intrinsic HTML element, never to a component in scope, so <experimental_NewThreadComposer /> renders a literal <experimental_newthreadcomposer> custom element with every prop stringified onto it. It type-checks, logs nothing, and shows an empty gap where the component should be. Assign it to a capitalized name first.

About

Turn a one-liner into a scoped thread in the right bb project: classify, expand, spawn.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages