feat(orchestrator): expandable flow + slash-menu explanations

Each flow row in the launcher and each command in the / slash picker now
shows an always-on one-liner with a chevron that expands a 1-2 sentence
what/when blurb (condensed from the Han skill descriptions). Launcher gets a
read-only pill and a per-row Run separate from expand; the fast/concise
toggle is now wired through to the conductor workers. Shared ChatInput, so
the slash explanations cover both BooChat and BooCoder. Web tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 16:17:37 +00:00
parent edc348baf3
commit 868d9db3f2
4 changed files with 422 additions and 151 deletions

View File

@@ -4,7 +4,7 @@ All notable changes per release tag. Most recent on top, ordered by tag creation
## v2.7.17-orchestrator — 2026-06-03
Brings the deterministic multi-agent "conductor" into the app as the **Orchestrator**: launch any read-only Han flow (research, code-review, investigate, architectural-analysis, security-review, …) from BooChat or BooCoder and watch each specialist agent stream live in a Paseo-style run pane, ending with an evidence-disciplined, adversarially-validated report — all on free local Qwen, persisted and resumable. Built and audited end-to-end via `paseo-epic` in an isolated worktree, on top of the prior `/opt/boocode/conductor` standalone CLI: the conductor's 22 flow definitions, Spine factory, and Han evidence/YAGNI contracts were re-homed into `apps/coder/src/conductor`, and a new DB-backed flow-runner (`flow_runs`/`flow_steps`) dispatches each step as a real BooCoder task through the existing dispatcher — reusing its streaming→WS-frame pipeline and worktree-as-read-snapshot, with an `onTaskTerminal` hook that advances the wave and a startup resume that re-dispatches in-flight steps after a coder restart. Read-only is enforced hard: every step is dispatched `qwen --approval-mode plan`, an adversarial-security review caught and closed a bypass where a qwen-unavailable task silently fell through to write-capable native inference (now fails closed), and the ACP path's mode-set was made fail-closed too. The UI adds a fourth `orchestrator` pane kind (collapsed agent roster, expand-one live stream, report on top), a Workflow button + slash flows on the shared `ChatInput` for full BooChat/BooCoder parity, a "New Orchestrator" entry in the + and split menus, a category-grouped launcher dialog, runs history, and export (copy / save-to-file / send-to-chat) — fed by two new `flow_run_*` WS frames on a coder user channel. Qwen-only by design (Claude Code remains the Claude path); the existing model-competition Arena stays a separate feature. Spec/plan in `openspec/changes/orchestrator`; coder 373 tests green (42 new scheduler/resume/read-only decision tests), contracts/coder/server builds + web tsc clean. Built on `v2.7.16-container-git-safedir`; pairs conceptually with the earlier `v2.7.12-audit-cleanup` multi-agent orchestration.
Brings the deterministic multi-agent "conductor" into the app as the **Orchestrator**: launch any read-only Han flow (research, code-review, investigate, architectural-analysis, security-review, …) from BooChat or BooCoder and watch each specialist agent stream live in a Paseo-style run pane, ending with an evidence-disciplined, adversarially-validated report — all on free local Qwen, persisted and resumable. Built and audited end-to-end via `paseo-epic` in an isolated worktree, on top of the prior `/opt/boocode/conductor` standalone CLI: the conductor's 22 flow definitions, Spine factory, and Han evidence/YAGNI contracts were re-homed into `apps/coder/src/conductor`, and a new DB-backed flow-runner (`flow_runs`/`flow_steps`) dispatches each step as a real BooCoder task through the existing dispatcher — reusing its streaming→WS-frame pipeline and worktree-as-read-snapshot, with an `onTaskTerminal` hook that advances the wave and a startup resume that re-dispatches in-flight steps after a coder restart. Read-only is enforced hard: every step is dispatched `qwen --approval-mode plan`, an adversarial-security review caught and closed a bypass where a qwen-unavailable task silently fell through to write-capable native inference (now fails closed), and the ACP path's mode-set was made fail-closed too. The UI adds a fourth `orchestrator` pane kind (collapsed agent roster, expand-one live stream, report on top), a Workflow button + slash flows on the shared `ChatInput` for full BooChat/BooCoder parity, a "New Orchestrator" entry in the + and split menus, a category-grouped launcher dialog, runs history, and export (copy / save-to-file / send-to-chat) — fed by two new `flow_run_*` WS frames on a coder user channel. Qwen-only by design (Claude Code remains the Claude path); the existing model-competition Arena stays a separate feature. The flow launcher and the `/` slash menu both carry chevron-expandable per-item explanations (an always-on one-liner expands to a 12 sentence what-it-does / when-to-use blurb, condensed from each Han skill's own description), with a "read-only" pill pinned in the launcher and the fast/concise toggle wired through to the workers. Spec/plan in `openspec/changes/orchestrator`; coder 373 tests green (42 new scheduler/resume/read-only decision tests), contracts/coder/server builds + web tsc clean. Built on `v2.7.16-container-git-safedir`; pairs conceptually with the earlier `v2.7.12-audit-cleanup` multi-agent orchestration.
## v2.7.16-container-git-safedir — 2026-06-03

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Zap } from 'lucide-react';
import { ChevronRight, Play, Loader2, ShieldCheck } from 'lucide-react';
import { toast } from 'sonner';
import {
Dialog,
@@ -14,6 +14,7 @@ import { Label } from '@/components/ui/label';
import { api } from '@/api/client';
import { sessionEvents } from '@/hooks/sessionEvents';
import { cn } from '@/lib/utils';
import { FLOW_INFO } from './flowBlurbs';
type Band = 'small' | 'medium' | 'large';
type Category = 'Analysis' | 'Discovery' | 'Planning' | 'Authoring' | 'Review';
@@ -71,11 +72,13 @@ export function FlowLauncherDialog() {
const [projectId, setProjectId] = useState<string>('');
const [placement, setPlacement] = useState<'new' | 'split'>('new');
const [category, setCategory] = useState<Category>('Analysis');
const [flowName, setFlowName] = useState<string>(FLOWS_BY_CATEGORY.Analysis[0]?.name ?? 'research');
const [band, setBand] = useState<Band>('small');
const [focus, setFocus] = useState('');
const [fast, setFast] = useState(false);
const [launching, setLaunching] = useState(false);
// Which flow row is expanded (one at a time) vs. which flow is mid-launch.
// Expand (chevron) and launch (Run) are deliberately separate actions.
const [expandedFlow, setExpandedFlow] = useState<string | null>(null);
const [launchingFlow, setLaunchingFlow] = useState<string | null>(null);
useEffect(() => {
return sessionEvents.subscribe((ev) => {
@@ -84,39 +87,42 @@ export function FlowLauncherDialog() {
setPlacement(ev.placement ?? 'new');
// Reset to defaults each time the dialog is opened.
setCategory('Analysis');
setFlowName(FLOWS_BY_CATEGORY.Analysis[0]?.name ?? 'research');
setBand('small');
setFocus('');
setFast(false);
setExpandedFlow(null);
setLaunchingFlow(null);
setOpen(true);
});
}, []);
function handleCategoryChange(cat: Category) {
setCategory(cat);
setFlowName(FLOWS_BY_CATEGORY[cat][0]?.name ?? '');
setExpandedFlow(null);
}
async function handleLaunch() {
if (!flowName || !projectId) return;
setLaunching(true);
// Per-row launch. Behaviour is identical to the old bottom Launch button:
// POST the run, then emit open_orchestrator_pane carrying the placement.
async function handleRun(name: string) {
if (!name || !projectId || launchingFlow) return;
setLaunchingFlow(name);
try {
const { run_id } = await api.runs.launch({
project_id: projectId,
flow_name: flowName,
flow_name: name,
band,
input: { question: focus },
input: { question: focus, ...(fast ? { concise: true } : {}) },
});
sessionEvents.emit({
type: 'open_orchestrator_pane',
state: { run_id, flow_name: flowName, band },
state: { run_id, flow_name: name, band },
placement,
});
setOpen(false);
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to launch flow');
} finally {
setLaunching(false);
setLaunchingFlow(null);
}
}
@@ -128,72 +134,19 @@ export function FlowLauncherDialog() {
className="flex flex-col gap-0 p-0 max-h-[85vh] sm:max-w-md grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden"
showCloseButton={false}
>
<DialogHeader className="px-4 pt-4 pb-3 border-b shrink-0">
<DialogHeader className="gap-2 px-4 pt-4 pb-3 border-b shrink-0">
<DialogTitle className="text-sm font-medium">Launch a flow</DialogTitle>
{/* Read-only assurance — flows never touch your code. */}
<div className="inline-flex w-fit items-center gap-1.5 rounded-md bg-muted/60 px-2 py-1 text-xs text-muted-foreground">
<ShieldCheck className="size-3.5 shrink-0 text-primary/70" />
<span>Flows are read-only they analyze and report, never modify your code.</span>
</div>
</DialogHeader>
{/* Scrollable body */}
<div className="flex flex-col gap-4 overflow-y-auto overscroll-contain px-4 py-3">
{/* Category tabs — horizontal-scroll strip on mobile */}
<div className="flex gap-1 overflow-x-auto no-scrollbar pb-0.5 shrink-0">
{CATEGORIES.map((cat) => (
<button
key={cat}
type="button"
onClick={() => handleCategoryChange(cat)}
className={cn(
'shrink-0 rounded-full border px-3 py-1 text-xs transition-colors whitespace-nowrap',
cat === category
? 'border-primary bg-primary text-primary-foreground'
: 'border-border text-muted-foreground hover:bg-muted hover:text-foreground'
)}
>
{cat}
</button>
))}
</div>
{/* Flow list */}
<div className="flex flex-col gap-0.5">
{flows.map((flow) => (
<button
key={flow.name}
type="button"
onClick={() => setFlowName(flow.name)}
className={cn(
'w-full rounded-lg px-3 py-2 text-left text-sm transition-colors',
flow.name === flowName
? 'bg-primary/10 text-primary font-medium'
: 'text-foreground hover:bg-muted'
)}
>
{flow.label}
</button>
))}
</div>
{/* Size selector */}
<div className="flex flex-col gap-1.5">
<Label className="text-xs text-muted-foreground">Size</Label>
<div className="flex gap-1.5">
{BAND_LABELS.map(({ value, label }) => (
<button
key={value}
type="button"
onClick={() => setBand(value)}
className={cn(
'flex-1 rounded-lg border py-1 text-xs transition-colors',
band === value
? 'border-primary bg-primary/10 text-primary font-medium'
: 'border-border text-muted-foreground hover:bg-muted hover:text-foreground'
)}
>
{label}
</button>
))}
</div>
</div>
{/* Run options — apply to whichever flow you Run below. */}
<div className="flex flex-col gap-3">
{/* Focus/question field */}
<div className="flex flex-col gap-1.5">
<Label htmlFor="flow-focus" className="text-xs text-muted-foreground">
@@ -205,15 +158,32 @@ export function FlowLauncherDialog() {
placeholder="What should the flow focus on?"
value={focus}
onChange={(e) => setFocus(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
void handleLaunch();
}
}}
/>
</div>
{/* Size selector */}
<div className="flex flex-col gap-1.5">
<Label className="text-xs text-muted-foreground">Size</Label>
<div className="flex gap-1.5">
{BAND_LABELS.map(({ value, label }) => (
<button
key={value}
type="button"
onClick={() => setBand(value)}
aria-pressed={band === value}
className={cn(
'flex-1 rounded-lg border py-1 text-xs transition-colors',
band === value
? 'border-primary bg-primary/10 text-primary font-medium'
: 'border-border text-muted-foreground hover:bg-muted hover:text-foreground'
)}
>
{label}
</button>
))}
</div>
</div>
{/* Fast mode toggle */}
<div className="flex items-center justify-between">
<div className="flex flex-col">
@@ -241,25 +211,115 @@ export function FlowLauncherDialog() {
</div>
</div>
<DialogFooter className="shrink-0" showCloseButton>
<Button
onClick={() => void handleLaunch()}
disabled={!flowName || launching}
size="sm"
>
{launching ? (
<>
<Zap className="size-3.5 animate-pulse" />
Launching
</>
) : (
<>
<Zap className="size-3.5" />
Launch
</>
<div className="border-t border-border/60" />
{/* Flow picker */}
<div className="flex flex-col gap-2">
<span className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
Choose a flow
</span>
{/* Category tabs — horizontal-scroll strip on mobile */}
<div className="flex gap-1 overflow-x-auto no-scrollbar pb-0.5 shrink-0">
{CATEGORIES.map((cat) => (
<button
key={cat}
type="button"
onClick={() => handleCategoryChange(cat)}
className={cn(
'shrink-0 rounded-full border px-3 py-1 text-xs transition-colors whitespace-nowrap',
cat === category
? 'border-primary bg-primary text-primary-foreground'
: 'border-border text-muted-foreground hover:bg-muted hover:text-foreground'
)}
>
{cat}
</button>
))}
</div>
{/* Flow list */}
<div className="flex flex-col gap-1">
{flows.map((flow) => {
const info = FLOW_INFO[flow.name];
const isExpanded = expandedFlow === flow.name;
const isLaunching = launchingFlow === flow.name;
const panelId = `flow-blurb-${flow.name}`;
return (
<div
key={flow.name}
className={cn(
'rounded-lg border transition-colors',
isExpanded ? 'border-border bg-muted/40' : 'border-transparent hover:bg-muted/40'
)}
>
<div className="flex items-center gap-1.5 pr-2">
{/* Expand toggle — reveals the blurb; never launches. */}
<button
type="button"
onClick={() => setExpandedFlow(isExpanded ? null : flow.name)}
aria-expanded={isExpanded}
aria-controls={panelId}
aria-label={
isExpanded ? `Hide details for ${flow.label}` : `Show details for ${flow.label}`
}
className="flex min-w-0 flex-1 items-start gap-2 rounded-lg px-2.5 py-2 text-left max-md:min-h-[44px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<ChevronRight
className={cn(
'mt-0.5 size-4 shrink-0 text-muted-foreground transition-transform duration-200 motion-reduce:transition-none',
isExpanded && 'rotate-90'
)}
/>
<span className="min-w-0 flex-1">
<span className="block text-sm font-medium text-foreground">{flow.label}</span>
<span className="block text-xs text-muted-foreground">
{info?.oneliner ?? ''}
</span>
</span>
</button>
{/* Run — the only launch action; clearly separate from expand. */}
<Button
type="button"
size="sm"
onClick={() => void handleRun(flow.name)}
disabled={launchingFlow !== null}
aria-label={`Run ${flow.label}`}
className="shrink-0 max-md:min-h-[44px]"
>
{isLaunching ? (
<Loader2 className="animate-spin" />
) : (
<Play />
)}
Run
</Button>
</DialogFooter>
</div>
{/* In-place blurb — grid-rows trick for a smooth, reduced-motion-safe expand. */}
<div
id={panelId}
role="region"
className={cn(
'grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none',
isExpanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
)}
>
<div className="overflow-hidden">
<p className="px-2.5 pb-2.5 pl-9 text-xs leading-relaxed text-muted-foreground">
{info?.blurb ?? ''}
</p>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
<DialogFooter className="shrink-0" showCloseButton />
</DialogContent>
</Dialog>
);

View File

@@ -1,11 +1,18 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import type { CSSProperties, ReactNode, RefObject } from 'react';
import { createPortal } from 'react-dom';
import { ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils';
export interface SlashCommandItem {
name: string;
// One-liner shown always; also the fallback body when the row is expanded.
description?: string;
// Optional fuller explanation revealed when the row's chevron is expanded.
// When absent, the expanded panel falls back to `description` (un-truncated)
// — we never fabricate detail for commands that don't carry it (e.g. raw
// agent `/help` passthrough rows).
details?: string;
}
export interface SlashCommandGroup {
@@ -45,6 +52,9 @@ export function SlashCommandPicker({
emptyLabel = 'No commands available',
}: Props) {
const [highlightIndex, setHighlightIndex] = useState(0);
// At most one row's detail panel is open at a time; null = all collapsed.
// Indexed against the flat `filtered` list (same index space as highlight).
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
const popoverRef = useRef<HTMLDivElement>(null);
// When grouped, filter each group and drop empties; otherwise the flat list.
const filteredGroups = useMemo(
@@ -67,7 +77,8 @@ export function SlashCommandPicker({
);
const [vvTick, setVvTick] = useState(0);
useEffect(() => { setHighlightIndex(0); }, [query]);
// Filtering reshuffles indices, so any open detail panel is stale — collapse it.
useEffect(() => { setHighlightIndex(0); setExpandedIndex(null); }, [query]);
useEffect(() => {
function recalc() {
@@ -99,6 +110,20 @@ export function SlashCommandPicker({
e.preventDefault();
const target = filtered[highlightIndex] ?? filtered[0];
if (target) onSelect(target.name);
} else if (e.key === 'ArrowRight') {
// Expand the highlighted row in place (separate affordance from select).
// Only acts when the row has something to reveal, so caret-right still
// works on non-expandable rows; Enter/Tab selection is untouched.
const target = filtered[highlightIndex];
if (!target || !(target.details ?? target.description)) return;
e.preventDefault();
setExpandedIndex(highlightIndex);
} else if (e.key === 'ArrowLeft') {
// Collapse the highlighted row if it's the open one; otherwise let the
// caret move as usual.
if (expandedIndex !== highlightIndex) return;
e.preventDefault();
setExpandedIndex(null);
} else if (e.key === 'Escape') {
e.preventDefault();
onClose();
@@ -106,7 +131,7 @@ export function SlashCommandPicker({
}
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [filtered, highlightIndex, onSelect, onClose]);
}, [filtered, highlightIndex, expandedIndex, onSelect, onClose]);
useEffect(() => {
function handleMouseDown(e: MouseEvent) {
@@ -123,6 +148,12 @@ export function SlashCommandPicker({
if (el) el.scrollIntoView({ block: 'nearest' });
}, [highlightIndex]);
useEffect(() => {
if (expandedIndex == null) return;
const el = popoverRef.current?.querySelector('[data-expanded="true"]');
if (el) el.scrollIntoView({ block: 'nearest' });
}, [expandedIndex]);
const style = useMemo<CSSProperties>(() => {
if (!rect) return { display: 'none' };
const vv = window.visualViewport;
@@ -155,12 +186,18 @@ export function SlashCommandPicker({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [rect, vvTick]);
const renderItem = (item: SlashCommandItem, i: number) => (
const renderItem = (item: SlashCommandItem, i: number) => {
const expanded = i === expandedIndex;
// Fuller text when provided, else the one-liner shown un-truncated. Never
// invented — a row with neither shows an honest placeholder.
const detailText = item.details ?? item.description ?? null;
return (
<div
key={`${i}-${item.name}`}
role="option"
aria-selected={i === highlightIndex}
data-highlighted={i === highlightIndex}
data-expanded={expanded}
className={cn(
'w-full text-left px-2.5 py-2 cursor-pointer block',
i === highlightIndex && 'bg-muted',
@@ -168,21 +205,53 @@ export function SlashCommandPicker({
onMouseEnter={() => setHighlightIndex(i)}
onClick={() => onSelect(item.name)}
>
<div className="flex items-start gap-2">
<div className="min-w-0 flex-1">
<div className="font-mono text-xs font-bold text-foreground">/{item.name}</div>
{item.description && (
<div
className="text-xs text-muted-foreground overflow-hidden"
style={{
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}}
>
{item.description}
</div>
<div className="text-xs text-muted-foreground truncate">{item.description}</div>
)}
</div>
{/* Chevron is a SEPARATE affordance: stopPropagation keeps the row's
click (select) from firing; preventDefault on mousedown keeps focus
on the composer so typing/arrow-nav continues uninterrupted. */}
<button
type="button"
aria-label={expanded ? `Hide details for /${item.name}` : `Show details for /${item.name}`}
aria-expanded={expanded}
onMouseDown={(e) => e.preventDefault()}
onClick={(e) => {
e.stopPropagation();
setHighlightIndex(i);
setExpandedIndex((prev) => (prev === i ? null : i));
}}
className="-mr-1 -mt-0.5 flex shrink-0 items-center justify-center rounded p-1 text-muted-foreground/60 transition-colors hover:bg-foreground/10 hover:text-foreground max-md:min-h-[36px] max-md:min-w-[36px]"
>
<ChevronRight
className={cn(
'size-3.5 transition-transform duration-200 motion-reduce:transition-none',
expanded && 'rotate-90',
)}
/>
</button>
</div>
{/* grid-rows 0fr→1fr animates height with no fixed value; motion-reduce
disables the transition for users who ask for less motion. */}
<div
className={cn(
'grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none',
expanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
)}
>
<div className="overflow-hidden">
<div className="mt-1.5 border-t border-border/40 pt-1.5 text-xs leading-relaxed text-muted-foreground whitespace-pre-wrap break-words">
{detailText ?? <span className="italic text-muted-foreground/70">No description provided.</span>}
</div>
</div>
</div>
</div>
);
};
let runningIndex = -1;
const popover = filtered.length === 0 ? (

View File

@@ -0,0 +1,142 @@
/**
* Per-flow copy for the Orchestrator flow launcher.
*
* - `oneliner` is the flow's own backend `description` (see
* apps/coder/src/conductor/flows/*), shown always-on under the flow name.
* - `blurb` is a 12 sentence expansion (what it does + when to use, plus a
* "not for…" clause where the underlying Han skill has one), condensed from
* each Han skill's SKILL.md frontmatter `description` in /opt/skills/han/<name>.
* For the four bespoke flows with no Han skill (security-review, data-review,
* devops-review, stakeholder-summary) and the two empty SKILL.md files
* (runbook, test-planning) the blurb is condensed from the flow definition.
*
* Keyed by flow name — keep in sync with apps/coder/src/conductor/flows.
*/
export interface FlowInfo {
/** Always-on one-liner (the flow's backend `description`). */
oneliner: string;
/** Expanded 12 sentence blurb: what it does + when to use / not. */
blurb: string;
}
export const FLOW_INFO: Record<string, FlowInfo> = {
// ── Analysis ──────────────────────────────────────────────────────────
research: {
oneliner: 'options, prior art, trade-offs → recommendation',
blurb:
'Surveys options, prior art, and trade-offs across the codebase, the web, and any material you provide, then recommends a direction backed by checkable evidence. Use to weigh approaches or understand how something works before committing — not to diagnose a bug (use Investigate) or compare two concrete artifacts (use Gap Analysis).',
},
investigate: {
oneliner: 'root-cause a bug/failure from evidence',
blurb:
'An evidence-based deep dive that traces a bug, failure, or integration issue to its root cause and adversarially validates the proposed fix. Use to debug, troubleshoot, or diagnose why something is broken — not to audit code quality (use Code Review) or assess architecture (use Architectural Analysis).',
},
'architectural-analysis': {
oneliner: 'structure + behaviour + concurrency → architecture synthesis',
blurb:
'Assesses an existing module or feature across structural coupling, data flow, concurrency, risk, and SOLID alignment, then synthesises recommended changes. Use to evaluate the design quality, dependency structure, or technical debt of a specific area — not to investigate a bug (use Investigate) or review code line-by-line (use Code Review).',
},
'security-review': {
oneliner: 'adversarial security analysis (exploit-path proof standard)',
blurb:
'An adversarial hunt for real, exploitable vulnerabilities — every finding needs a file:line plus a demonstrated exploit path or a CVE, never a theoretical risk. At medium and above it also flags code-level resilience failures such as missing timeouts, swallowed errors, and unbounded results.',
},
'gap-analysis': {
oneliner: 'gaps between two artifacts (impl vs spec, etc.)',
blurb:
'Compares a current state against a desired one (implementation vs spec, PRD vs shipped feature, design vs build) and reports what is missing, incomplete, conflicting, or assumed. Use to reconcile two artifacts — not to investigate a runtime bug (use Investigate) or assess architecture (use Architectural Analysis).',
},
'data-review': {
oneliner: 'schema / query / data-access audit',
blurb:
'Audits schemas, migrations, queries, and data-access code against normalization, indexing strategy, access patterns, migration safety, and PII/regulated-data handling, citing each location and its data-level impact. Use before shipping schema or query changes.',
},
'devops-review': {
oneliner: 'production-readiness / operability review',
blurb:
'Checks a change or feature for production readiness — twelve-factor concerns, observability via the four golden signals, rollout safety, secrets/PII handling, scale, and cost — citing each location and its blast radius. At medium and above it adds a code-level resilience pass.',
},
'issue-triage': {
oneliner: 'assess + prioritise a reported issue',
blurb:
'Turns a raw, vague issue or bug report into a structured document naming what is known, what is missing, severity, and reproducibility, then recommends the right next step. Use to prep an incoming issue for work — not to find root causes (use Investigate) or plan a build (use a Planning flow).',
},
// ── Discovery ─────────────────────────────────────────────────────────
'project-discovery': {
oneliner: 'discover a repo: stack, structure, tooling',
blurb:
'Scans a repository and reports its languages, frameworks, build and test tooling, configuration, entry points, and directory structure as a map a newcomer could navigate. Use to detect a projects tech stack and layout — not to write feature docs (use Project Documentation).',
},
'project-documentation': {
oneliner: 'draft docs for a feature/system (one-pass)',
blurb:
'Drafts clear documentation for a feature, system, or component, grounded in the real implementation so every claim traces to evidence. Use to document how something works — not to scan the tech stack (use Project Discovery), record a decision (use ADR), or write a standard (use Coding Standard).',
},
'test-planning': {
oneliner: 'behaviour-focused test plan',
blurb:
'Produces a prioritised, behaviour-focused test plan — observable inputs/outputs and collaborator interactions, recommended test doubles and test levels — rather than internal code paths. At medium and above it catalogs the edge cases and boundary values the plan must cover. Use to plan tests, not to write them (use TDD).',
},
// ── Planning ──────────────────────────────────────────────────────────
'plan-a-feature': {
oneliner: 'feature spec draft (one-pass; human-in-loop intended)',
blurb:
'Builds a first-draft feature specification — the problem, the user, in/out of scope, acceptance criteria, a build approach, and open questions — grounded in the codebase. Use to scope or design a new capability before implementation — not to refine an existing plan (use Iterative Plan Review) or document a built feature (use Project Documentation).',
},
'plan-implementation': {
oneliner: 'implementation plan draft (one-pass)',
blurb:
'Turns a feature specification into an implementation blueprint — the specific files to create or modify, component designs, data flow, an ordered build sequence, and a test strategy. Use once behaviour is specified — not to decide what the feature should do (use Plan a Feature) or review code (use Code Review).',
},
'plan-a-phased-build': {
oneliner: 'phased build plan draft (one-pass)',
blurb:
'Splits an initiative into a sequence of independently shippable, vertical-slice phases — each with a goal, its slice of work, dependencies, and a demoable outcome a real person can be shown. Use to sequence or roadmap a build — not for implementation detail (use Plan Implementation).',
},
'plan-work-items': {
oneliner: 'break work into tracked items (one-pass)',
blurb:
'Breaks a trusted implementation plan into independently-grabbable, atomic work items — each with a title, a one-line outcome, dependencies, and a rough size, ordered by dependency. Use to convert a plan into tickets — not when there is no trusted plan yet (produce one with Plan Implementation first).',
},
'iterative-plan-review': {
oneliner: 'stress-test an existing plan (one pass of the loop)',
blurb:
'Stress-tests an existing plan against the codebase — surfacing hidden assumptions, unstated prerequisites, muddied scope, and risk — to sharpen it before commitment. Use to refine, tighten, or verify the soundness of a plan — not to generate one from scratch (use a Planning flow) or implement it.',
},
// ── Authoring ─────────────────────────────────────────────────────────
adr: {
oneliner: 'architecture decision record draft (one-pass)',
blurb:
'Drafts an Architecture Decision Record — Context, the Decision, the Options considered with trade-offs, Consequences, and status — grounded in the real constraints. Use to record or update an architecture or design decision — not to write an enforceable standard (use Coding Standard) or feature docs (use Project Documentation).',
},
'coding-standard': {
oneliner: 'coding standard draft (one-pass)',
blurb:
'Drafts an enforceable coding standard — the rule stated imperatively, the failure it prevents, a correct and an incorrect example, and its scope. Use to create or update a convention — not to record a one-off decision (use ADR) or write feature documentation (use Project Documentation).',
},
runbook: {
oneliner: 'operational runbook draft (one-pass)',
blurb:
'Drafts an operational runbook for an incident scenario — detection signals, immediate mitigation, the diagnosis path, rollback/recovery, and escalation, with concrete commands where known. At medium and above it adds the failure modes and earliest signals the runbook must cover.',
},
tdd: {
oneliner: 'failing-test scaffold + plan (one-pass; not the full red-green loop)',
blurb:
'Writes the failing ("red") tests that specify a behaviour — observable inputs/outputs and collaborator interactions — and outlines the smallest implementation that would make them pass. A single pass, not the full interactive red-green-refactor loop. Use to drive code test-first — not to produce a test plan document (use Test Planning).',
},
'stakeholder-summary': {
oneliner: 'plain-language stakeholder summary (Han reporting)',
blurb:
'Writes a plain-language summary of a feature or piece of work for a non-technical stakeholder — what it is, why it matters, what changes for users, and the rough shape of the effort. No jargon, no implementation detail.',
},
// ── Review ────────────────────────────────────────────────────────────
'code-review': {
oneliner: 'per-dimension review → adversarially verify each dimension (drops false positives)',
blurb:
'Reviews changed code across multiple dimensions — correctness, structure, security, resilience, and concurrency — then adversarially verifies each finding so false positives are dropped before the report. Use to audit code quality, correctness, or security — not to analyse architecture (use Architectural Analysis) or investigate a bug (use Investigate).',
},
};