Skip to content

Phase 4 — Technical Design

Plan: i-want-to-ea3316 — pre-feature /brainstorm workflow Date: 2026-05-21 Phase status: completed (pending user acknowledgement) Inputs: 01-brainstorming.md, 02-requirements.md (with Phase-3 amendments), 03-research.md (DEC-1..DEC-8 + FR-21..25 / NFR-20..21 amendments) Approach: 3 parallel design explorers (minimal / clean / alternative) + 1 design critic; the hybrid below adopts the critic's reconciliation verdict.


0. Executive summary

We are building /brainstorm as a pre-feature workflow that drives 1–10 rounds of adversarial multi-angle questioning (UX, technical, edge-cases) against a user-supplied topic and emits two artifacts (00-brainstorming.md narrative + 00-brainstorming.context.md machine-readable digest) suitable for seeding /feature via --from-brainstorm <slug> or running inline via --deep-brainstorm. The selected shape is a single-file orchestrator (src/commands/brainstormCommand.ts, ~380 LOC) that walks its own stage list (DEC-8: no workflow controller), spawns inquisitors via the coordinator (DEC-5), and harvests their partial_result envelopes through a messageBus.subscribe tap (DEC-2).

We adopt two narrow extractions (loadProjectContextsrc/utils/projectContext.ts; validateSlug + resolveSafePlanDirsrc/commands/planDir.ts — the latter backports the SEC-3 path-traversal fix to /feature --resume per DEC-6 / FR-22) and reject two seductive extractions (agentWaiter — featureCommand's waiter is artifact-race-shaped, brainstorm needs event-based on agent.completed/agent.failed; UserGrilling — 90% boilerplate over a one-liner). MessageBusLike is widened (one line) with subscribe? instead of casting at the call site.

The single material salvage from Designer 3 is DEC-3 NARROWLY REOPENED: with DEC-8 in force, /brainstorm never registers a workflow controller, so the setActiveWorkflowController hostility (src/skills/workflowController.ts:56-61) that originally justified sibling-slug composition for --deep-brainstorm no longer applies. --deep-brainstorm therefore runs brainstorm inline as Phase 1 of /feature and writes 01-brainstorming.md into the feature slug (matches /feature's existing numbering convention). FR-16/17/18 are kept for the standalone → from-brainstorm flow (explicit user requirement at 01-brainstorming.md:121). Q-D4 is retired.

A new --agents <n> flag (n ∈ {0,1,2,3}, default 3) provides a single-coordinator escape hatch that satisfies the unmeasured OOS-14 ROI hypothesis without a v1.1 breaking change. Five questions remain open (OQ-1..OQ-5) — each paired with a default recommendation the design assumes if the user does not override.


1. Selected approach — single-file orchestrator + 2 targeted extractions

/brainstorm is a single-file command (~380 LOC) in src/commands/brainstormCommand.ts that:

  1. Parses its own args (FR-1, FR-11, FR-13, plus the new --agents <n>).
  2. Validates slug + resolves plan dir via the new planDir.ts helpers (DEC-6 / FR-22, closes SEC-3).
  3. Saves topology, sets hub (IR-7 / Q-D5), restores in finally (conditionally — see DEC-17).
  4. Walks rounds in a JS while-loop (DEC-8 — no workflow controller registered, so --deep-brainstorm inlining is safe).
  5. In round 1 (when agentsN > 0): registers a messageBus.subscribe('coordinator', handler) tap BEFORE sending the spawn prompt; sends a spawn prompt instructing the coordinator to call spawn_worker 3× in parallel (DEC-5); waits for the inquisitor agent.completed/agent.failed events (event-based, not artifact-polled); drains partial_result envelopes; disposes tap.
  6. Parses harvested JSON via shared tolerantJsonParse (DEC-7 / DEC-14, modeled on AgentOutcomeExtractor.ts:73-117); checks FR-25 threshold; auto-degrades to coordinator-only grilling if all inquisitors failed (DEC-16) rather than aborting.
  7. Dedupes + prioritises ≤8 questions; grills each via coordinator.askUser({question}) (T-153 deterministic path, FR-21-safe after this PR).
  8. In rounds 2+ (or when agentsN === 0): single-coordinator follow-ups via coordinator.sendAndWait(prompt) with prior transcript wrapped via wrapUntrusted (FR-23 / SEC-2).
  9. After round Nmin: termination gate askUser({choices: ['summarize', 'keep grilling']}) (FR-7).
  10. Synthesis: final sendAndWait asks coordinator to compose narrative + context-md strings; JS writes both via atomicWriteFile({mode: 0o644}) (SEC-5); sentinel .brainstorm-complete written last (FR-9, FR-24, NFR-20).

Two narrow extractions support reuse:

  • loadProjectContext from featureCommand.ts:126-141 → new src/utils/projectContext.ts (16 LOC).
  • validateSlug + resolveSafePlanDir added to src/commands/planDir.ts (~25 LOC) — backports the SEC-3 fix to /feature --resume (DEC-6 / FR-22).

2. The key design pivots (rejected alternatives in summary)

2.1 PIVOT: drop DEC-3 sibling-slug composition for --deep-brainstorm

Designer 3's verified insight: DEC-3's C-4 rationale (workflow-controller hostility at src/skills/workflowController.ts:56-61setActiveWorkflowController force-aborts any prior controller) was over-fitted to a pre-DEC-8 reality. With DEC-8 in force, /brainstorm never registers a controller → invoking brainstorm inline preserves the outer /feature controller, and there is no contention to design around.

Decision: --deep-brainstorm runs brainstorm INLINE as Phase 1 of /feature and writes 01-brainstorming.md into the feature slug dir (matches /feature's existing 01-…/02-…/03-… numbering). FR-16/17/18 are KEPT for the standalone → from-brainstorm flow (explicit user requirement at 01-brainstorming.md:121). FR-19 is rewritten to reflect the inlined branch. Q-D4 retired. Cost: ~5 LOC inline branch in featureCommand.handler calling runBrainstormInline(ctx, feature, planDir) (exported from brainstormCommand).

2.2 KEEP DEC-1..DEC-2, DEC-4..DEC-8 baseline

Rationale: critic verified each remains correct.

  • DEC-1 (multi-angle inquisitors) — preserves product identity vs the alt's single-coord ask.
  • DEC-2 (messageBus tap on partial_result) — verified workable (MessageBus.ts:226-251 supports multi-handler subscribe).
  • DEC-4 (atomic writes + sentinel) — unchanged; FR-24/NFR-20 carry through.
  • DEC-5 (coordinator-spawned inquisitors) — single source of truth for spawn rules.
  • DEC-6 (slug-validated plan dir) — extended to backport via planDir.ts.
  • DEC-7 (tolerant JSON parser) — promoted to shared util in DEC-14.
  • DEC-8 (no workflow controller — JS loop drives stages) — the prerequisite that unlocked DEC-3's reopen.

Single-file shape preferred over Designer 2's state-machine DI (BrainstormRoundRunner + interface-based message bus): YAGNI, single consumer, one-file deletability outweighs theoretical testability.

2.3 NEW: --agents <n> escape hatch

v1 ships --agents <n> where n ∈ {0,1,2,3}, default 3. n=0 = pure single-coordinator (no inquisitor spawn, no bus-tap subscriber, no JSON parser invocation). n=1..2 = subset (degradation order: drop edge-cases first, then technical, always keep ux). n=3 = full multi-angle (default).

This satisfies the user-intent ambiguity surfaced by Designer 3 (the user's "one coordinator" remark in 01-brainstorming.md:48) while preserving product identity by default, and gives the unmeasured OOS-14 ROI hypothesis a v1 A/B mechanism without a v1.1 breaking change. ~15 LOC plumbing.


3. Files to MODIFY (existing — exact change summary + LOC delta + rationale)

FileChangeLOC ΔCitation
src/commands/registry.tsImport createBrainstormCommand; push into BUILTIN_COMMANDS; add 'brainstorm' (+ aliases 'brain', 'bs') to Workflows category mapping+3:53-58, :178-215
src/commands/planDir.tsAdd SLUG_REGEX constant + validateSlug(slug) + resolveSafePlanDir(cwd, slug) helpers using isPathInsideDir from utils/fs.ts:40. Pure additions, no behavior change for existing callers.+25new helpers
src/commands/featureCommand.ts(a) parseFeatureArgs (:29-59): add --from-brainstorm <slug> and --deep-brainstorm flags. (b) resolvePlanSlug (:70-93): call validateSlug from planDir (FR-22, closes SEC-3 in /feature --resume). (c) When --from-brainstorm: call parseBrainstormContext on .plans/<src>/00-brainstorming.context.md, write Phase-1 pointer stub at 01-brainstorming.md ("This feature was seeded from …"), prepend formatBrainstormDigest(ctx) to the Phase-2 prompt. (d) When --deep-brainstorm: call runBrainstormInline(ctx, feature, planDir) (exported from brainstormCommand) which writes 01-brainstorming.md + 01-brainstorming.context.md in the feature slug; mark Phase-1 complete; proceed to Phase 2 normally. (e) Replace inline loadProjectContext (:126-141) with import from src/utils/projectContext.js.+40 / −16 (= +24 net)featureCommand.ts
src/coordinator/CoordinatorEngine.tsFR-21: in askUser direct path (:1074-1097), mirror the unattended directive logic from the tool path at :1338-1351. Factor into a private unattendedDirective(question, choices) to dedupe. Audit callers: currently only featureCommand.ts:487-503 (pre-stage grilling) consumes the direct path return value — patch it to early-out when the returned string starts with the directive marker.+35CoordinatorEngine.ts
src/types/commands.tsWiden MessageBusLike (:84-92) with optional subscribe?: (agentId: string, handler: (envelope: MessageEnvelope) => void) => () => void;. Optional so existing test stubs continue to compile; brainstorm guards on if (!context.messageBus.subscribe) { return error('messageBus does not support subscribe'); }.+2commands.ts
src/utils/index.tsRe-export loadProjectContext, hasFleetContext (from projectContext.ts), parseBrainstormContext, serializeBrainstormContext, formatBrainstormDigest (from brainstormContext.ts), tolerantJsonParse (from tolerantJson.ts).+4barrel

Total modified: 6 files; net ≈ +93 LOC.


4. Files to CREATE

FilePurposeLOCNotes
src/commands/brainstormCommand.tsSingle-file orchestrator. Owns: parseBrainstormArgs, resolveBrainstormSlug, createBrainstormCommand factory, runBrainstormInline (exported for /feature --deep-brainstorm), JS round-loop, topology save/restore, artifact writes, FR-11 unattended refusal, FR-24 sentinel, FR-25 threshold + auto-degrade, --agents <n> branching, waitForInquisitors(ids, timeoutMs) (~25 LOC, event-based on agent.completed/agent.failed for the known ID set — focused, not extracted), tap registration, accumulator merge logic.~380DEC-1, 2, 5, 8
src/commands/brainstormCommand.test.tsArg-parsing table (FR-1, FR-13 range guard, FR-11 unattended refusal, --agents range), single-round happy path with mocked coordinator+messageBus, summarize vs keep-grilling branches (FR-7), --agents 0/1/2/3 paths (n=0 and n=3 mandatory), topology save/restore (IR-7, Q-D5), tap-before-spawn ordering (regression), FR-25 threshold-met and threshold-not-met (auto-degrade vs hard-abort comparison), SEC-2 wrapUntrusted on round-2+ re-feed, NFR-12 coverage targets.~320
src/utils/projectContext.tsExtracted loadProjectContext + hasFleetContext from featureCommand (lift-and-shift, branded path types where applicable).16DEP-5
src/utils/projectContext.test.tsEmpty .fleet/context/, populated, neutralizer sanity, fs error path.50
src/utils/brainstormContext.tsparseBrainstormContext(filePath) (FR-14 YAML reader, FR-18 schema_version check), serializeBrainstormContext(ctx), formatBrainstormDigest(ctx) for /feature Phase-2 seed. Uses a focused ~15-LOC YAML reader to avoid promoting parseFrontmatter (currently private at src/skills/parser.ts:47) to public API.~80FR-14, 16, 18
src/utils/brainstormContext.test.tsYAML round-trip including qa_pairs (nested array of objects), schema_version=2 rejection (FR-18), malformed YAML graceful error, missing required fields graceful error.70NFR-14
src/utils/tolerantJson.tsShared tolerant JSON parser modeled on AgentOutcomeExtractor.ts:73-117: fence-strip → JSON.parse with error capture → optional dual-shape acceptance (object-with-key vs bare array) → per-item validation. Used by brainstorm immediately; AgentOutcomeExtractor + SteeringExtractor (:99) refactor to consume it is followup.~40DEC-7, DEC-14
src/utils/tolerantJson.test.tsFenced JSON json … , malformed (unclosed brace), missing keys, oversized string payload (NFR-21), dual-shape ({questions: […]} vs […]).80
src/skills/bundled/workflows/brainstorm.workflow.mdFrontmatter declares brainstorm metadata (name, description, coordinator role, preQuestions: ['What is the topic to brainstorm about?'] for standalone-launched-without-topic case). Stage list is documentation only — the JS loop drives execution (DEC-8).~30IR-3
src/skills/bundled/workflows/brainstorm/round.prompt.mdPer-round prompt template with ${topic}, ${round}, ${prior_qa}, ${angles} placeholders. Loaded and interpolated by the JS loop, sent via coordinator.sendAndWait.~50
src/skills/bundled/workflows/brainstorm/synthesize.prompt.mdSynthesis prompt instructing the coordinator to compose two artifact strings (narrative 00-brainstorming.md + machine-readable 00-brainstorming.context.md matching FR-14 schema). JS writes both files atomically.~40FR-8
src/skills/bundled/roles/inquisitor-ux.role.mdUX-angle inquisitor. Frontmatter agent_type: explorer, version: 1. Body enforces angle discipline, JSON output schema {"questions":[{"text","angle","priority"}]} (DEC-7), and "report ONCE via report_to_coordinator with report_type: partial_result then exit; no file writes".~50FR-4, IR-4
src/skills/bundled/roles/inquisitor-technical.role.mdSame shape, technical/integration angle.~50
src/skills/bundled/roles/inquisitor-edge-cases.role.mdSame shape, edge-cases / failure-mode angle.~50
src/skills/parser.brainstorm.test.tsParses brainstorm.workflow.md end-to-end through the existing skills parser; asserts NFR-14 dual-prompt rule for stages (per-stage .prompt.md exists).50NFR-14
src/skills/SkillRegistry.brainstorm.test.tsAll 3 inquisitor roles discoverable via registry.getRole('inquisitor-ux'), etc.; composeAgentPrompt('inquisitor-ux', []) succeeds.60

Total new: 16 files; ≈ 1,416 LOC (≈ 596 src + 580 tests + 240 bundled markdown).


5. Integration points (existing code touchpoints)

  • Command registration: src/commands/registry.ts:178-215 (BUILTIN_COMMANDS push) + :53-58 (Workflows category map). Auto-discovered by /help.
  • Slash-command runtime: brainstormCommand.handler(input, context) receives CommandContext (src/types/commands.ts:94-132); reads context.coordinator.askUser (:38), context.messageBus.subscribe (newly optional at :84-92), context.stateStore, context.registry, context.cwd, context.coordinator.sendAndWait.
  • Coordinator askUser path (T-153): context.coordinator.askUser({question})CoordinatorEngine.ts:1074-1097 (now FR-21-safe — returns rich unattended directive). The brainstorm loop calls this once per grilling question and once per termination gate. This is the direct path, not the ask_user tool — the T-153 fix pauses the hang-detector during user think-time.
  • Coordinator sendAndWait: round-1 spawn prompt + round-2+ follow-up generation + synthesis go through context.coordinator.sendAndWait(prompt, timeoutMs). The spawn invocation uses an XML-tagged prompt telling the coordinator to call spawn_worker 3× in parallel with role: inquisitor-ux|technical|edge-cases, agent_type: explorer.
  • MessageBus tap: context.messageBus.subscribe('coordinator', handler) registered BEFORE the spawn prompt is sent, disposed in the outer finally. Filter envelopes where envelope.from ∈ inquisitorIds && envelope.message.type === 'text' && content.startsWith('[partial_result] ') (prefix verified at src/fleet/FleetManager.ts:520,530). Unsubscribe via the closure returned from subscribe (MessageBus.ts:244-250).
  • Spawn-wait integration: waitForInquisitors(ids, timeoutMs) listens on stateStore.on('agent.completed', …) + stateStore.on('agent.failed', …) for the diff set; resolves when all known IDs are done OR timeout fires. ~25 LOC inline in brainstormCommand.ts. Followed by a ~500 ms post-quiesce drain so the last partial_result envelope is captured before the tap is torn down.
  • Topology save/restore (IR-7, Q-D5): context.stateStore.setState(p => ({...p, activeTopology: 'hub'}), {invalidates: ['meta']}) at entry; outer finally restores prior value. Conditional restore (only if current still equals what we set) to avoid clobbering concurrent /loop-driven changes — H-6 mitigation, DEC-17.
  • Atomic writes (FR-9, FR-24, NFR-20): atomicWriteFile(path, content, {mode: 0o644}) — explicit mode override per critic SEC-5 (default may vary across umasks). Sentinel .brainstorm-complete written AFTER both artifacts succeed.
  • Re-feed safety (FR-23 / SEC-2): wrapUntrusted(answer) from src/utils/promptInjectionGuard.ts:78 applied wherever transcript answers are templated into round-2+ follow-up prompts or into the synthesis prompt.
  • Plan-dir + slug (FR-22 / SEC-3): validateSlug + resolveSafePlanDir from planDir.ts close SEC-3 in /brainstorm --resume, in the /brainstorm --from-brainstorm source-slug parameter, and (via the same backport) in /feature --resume.
  • --deep-brainstorm chain: featureCommand.handler detects flag, calls runBrainstormInline(context, feature, planDir) (exported from brainstormCommand). Inline run writes 01-brainstorming.md + 01-brainstorming.context.md in the FEATURE slug dir; marks Phase 1 as completed; proceeds to Phase 2 normally. Because DEC-8 means brainstorm never registers a workflow controller, the outer /feature controller survives.
  • --from-brainstorm chain: featureCommand.handler reads source brainstorm via parseBrainstormContext('.plans/<src>/00-brainstorming.context.md'), writes pointer 01-brainstorming.md ("This feature was seeded from .plans/<src>/00-brainstorming.md"), prepends formatBrainstormDigest(ctx) to the Phase-2 prompt.

6. Component design (key abstractions, signatures)

ts
// src/commands/brainstormCommand.ts — public surface
export interface BrainstormArgs {
  topic: string;
  rounds: number;              // FR-13: 1 ≤ n ≤ 10, default 2
  agentsN: 0 | 1 | 2 | 3;      // default 3 (DEC-13)
  resumeSlug?: string;
  unattended: boolean;         // FR-11: presence => error
  error?: string;
}
export function parseBrainstormArgs(raw: string): BrainstormArgs;
export function createBrainstormCommand(): SlashCommand;

/**
 * Called from featureCommand when --deep-brainstorm is passed.
 * Writes 01-brainstorming.md + 01-brainstorming.context.md into the feature slug dir.
 */
export async function runBrainstormInline(
  context: CommandContext,
  topic: string,
  planDir: string,
  opts?: { agentsN?: 0|1|2|3; rounds?: number },
): Promise<{ ok: boolean; transcript: QAPair[]; error?: string }>;

// Internal data shapes
export type InquisitorAngle = 'ux' | 'technical' | 'edge-cases' | 'coordinator-followup';
export interface Question { text: string; angle: InquisitorAngle; priority: number; }
export interface QAPair {
  round: number;
  angle: InquisitorAngle;
  question: string;
  answer: string;
  askedAt: string;             // ISO timestamp
  confidence?: 'certain' | 'likely' | 'guess';
}
export interface InquisitorRecord {
  agentId: AgentId;
  angle: InquisitorAngle;
  status: 'success' | 'parse_error' | 'timeout' | 'empty';
  questions: Question[];
  rawBodies: string[];         // for NDJSON debug trail
  parseError?: string;
}
ts
// src/utils/brainstormContext.ts
export const CURRENT_SCHEMA_VERSION = 1 as const;

export interface BrainstormContext {
  schema_version: 1;
  topic: string;
  created_at: string;          // ISO
  rounds_completed: number;
  agents_n: 0 | 1 | 2 | 3;
  qa_pairs: QAPair[];
  open_questions: string[];    // FR-14: unresolved threads for /feature to pick up
  source_path: string;         // self-referential for traceability
}

export function parseBrainstormContext(
  filePath: string,
): { ok: true; ctx: BrainstormContext } | { ok: false; error: string };

export function serializeBrainstormContext(ctx: BrainstormContext): string;

/** Compact digest for /feature Phase-2 prompt seeding (FR-16). */
export function formatBrainstormDigest(ctx: BrainstormContext): string;
ts
// src/utils/tolerantJson.ts
export function tolerantJsonParse<T>(
  body: string,
  opts?: { acceptArray?: boolean; arrayKey?: string },
): { ok: true; value: T } | { ok: false; error: string };
ts
// src/commands/planDir.ts (additions)
export const SLUG_REGEX = /^[a-z0-9][a-z0-9-]{0,63}$/;

export function validateSlug(
  slug: string,
): { ok: true } | { ok: false; error: string };

/** Resolves cwd/.plans/<slug>, guarding with isPathInsideDir. Closes SEC-3. */
export function resolveSafePlanDir(
  cwd: string,
  slug: string,
): { ok: true; dir: string } | { ok: false; error: string };
ts
// src/utils/projectContext.ts (extracted from featureCommand.ts:126-141)
export function hasFleetContext(cwd: string): boolean;
export function loadProjectContext(cwd: string): string;
ts
// src/types/commands.ts (MessageBusLike widening, line ~92)
export interface MessageBusLike {
  // … existing fields …
  subscribe?(
    agentId: string,
    handler: (envelope: MessageEnvelope) => void,
  ): () => void;
}

7. The harvest mechanism (DEC-2 — concrete impl)

Registration order (CRITICAL — regression-tested):

  1. Snapshot existing agent IDs from stateStore.getState().agents.
  2. Register tap: tap = context.messageBus.subscribe('coordinator', handler) — BEFORE step 3.
  3. Send spawn prompt to coordinator via sendAndWait.
  4. Wait for new agent IDs (diff set vs step 1 snapshot) to appear in state.agents whose role begins inquisitor-.
  5. Wait via waitForInquisitors(inquisitorIds, perRoundTimeoutMs) for agent.completed/agent.failed events for each ID in the set.
  6. ~500 ms post-quiesce drain (let last partial_result envelope arrive).
  7. Check meetsThreshold() (FR-25): ≥1 inquisitor with status==='success' AND questions.length > 0.
  8. Always tap() in the outer finally.

Handler filter (~10 LOC):

ts
const handler = (env: MessageEnvelope) => {
  if (env.message.type !== 'text') return;
  const content = env.message.content;
  if (!content.startsWith('[partial_result] ')) return;
  if (!inquisitorIds.has(env.from)) return;
  const body = content.slice('[partial_result] '.length);
  const parsed = tolerantJsonParse<{ questions: Question[] }>(body, { arrayKey: 'questions' });
  ingestInquisitor(env.from, body, parsed);
};

Accumulator: Map<AgentId, InquisitorRecord> seeded with {angle, status: 'empty', questions: [], rawBodies: []} per spawned inquisitor (so timeout/no-emit cases are explicit, not silent). Multiple envelopes from the same inquisitor MERGE questions (concat), not replace, so streaming partial_results compose.

FR-25 threshold + AUTO-DEGRADE (H-1 mitigation / DEC-16): if zero inquisitors return parseable questions, do NOT hard-abort. Log yellow warning "All inquisitors failed (statuses: ux=parse_error, technical=timeout, edge-cases=empty); degrading to coordinator-only grilling" and proceed as if --agents 0 for this round (and subsequent). Record full raw bodies in the NDJSON transcript for post-hoc debugging.


8. The stage loop (DEC-8 — concrete impl)

JS while-loop (~50 LOC core):

ts
const topoBefore = stateStore.getState().activeTopology;
setTopology('hub');
try {
  let round = 1;
  const transcript: QAPair[] = [];
  let keepGoing = true;
  while (keepGoing && round <= args.rounds) {
    const questions =
      args.agentsN > 0 && !degraded
        ? await runRoundWithInquisitors(round, transcript, args.agentsN)
        : await runRoundWithCoordinator(round, transcript);
    const prioritized = dedupeAndPrioritize(questions, 8);
    for (const q of prioritized) {
      const ans = await context.coordinator.askUser({ question: q.text });
      transcript.push({ round, angle: q.angle, question: q.text, answer: ans, askedAt: nowIso() });
    }
    if (round >= MIN_ROUNDS) {
      const choice = await context.coordinator.askUser({
        question: `Round ${round} complete. Summarize now, or keep grilling?`,
        choices: ['summarize', 'keep grilling'],
      });
      if (/summari[sz]e/i.test(choice)) keepGoing = false;
    }
    round++;
  }
  await synthesizeAndWrite(transcript, planDir);
} finally {
  if (stateStore.getState().activeTopology === 'hub') setTopology(topoBefore); // conditional restore (DEC-17)
}
  • Outer try/finally wraps topology save/restore (conditional — DEC-17).
  • Round 1: if agentsN > 0, spawn N inquisitors + harvest; if agentsN === 0 or auto-degrade triggered, coordinator-only follow-ups (same code path as round 2+).
  • Rounds 2+: always single-coord follow-ups via sendAndWait with prior transcript wrapped via wrapUntrusted (FR-23 / SEC-2).
  • After each round: dedupe + prioritize ≤ 8 (Jaccard ≥ 0.6 similarity threshold); grill each via askUser; transcript.push.
  • Termination gate: rounds < MIN_ROUNDS (= 1) auto-continue; else askUser with choices: ['summarize', 'keep grilling']. Match /summari[sz]e/i.
  • Synthesis: final sendAndWait asks coordinator to compose narrative + context markdown strings; JS writes both via atomicWriteFile({mode: 0o644}); sentinel written last.

Termination policy chosen: FR-7 + FR-13 only (rounds cap + user-driven). No coordinator-detected exhaustion in v1 (Q-D3 deferred — would require an extra coordinator round-trip and a new prompt; not worth v1 scope).


9. The --agents <n> escape hatch (new)

nBehavior
0No inquisitor spawn. Round 1 uses coordinator-only follow-ups (same code path as round 2+). No bus-tap subscriber registered. Pure single-coordinator deep grilling.
1Spawn only inquisitor-ux. Drop technical + edge-cases.
2Spawn inquisitor-ux + inquisitor-technical. Drop edge-cases.
3Default. All three inquisitor roles spawned in parallel via the coordinator.

Implementation:

ts
const ANGLES = ['ux', 'technical', 'edge-cases'] as const;
const angles = ANGLES.slice(0, args.agentsN);
// conditional spawn loop on `angles`

~15 LOC plumbing. Mandatorily tested for n=0 and n=3 (NFR-12); n=1/n=2 covered via parameterized arg-parse table.


10. Design decisions (DEC-9..DEC-18 — new for Phase 4)

  • DEC-9Single-file brainstormCommand.ts (no state-machine, no DI). YAGNI; future need for state-machine extraction can be refactored when a second consumer appears.
  • DEC-102 extractions adopted (projectContext, slug validators in planDir); 2 rejected (agentWaiter — wrong shape; UserGrilling — 90% boilerplate over a one-liner).
  • DEC-11MessageBusLike.subscribe? optional widening (1 line in commands.ts) preferred over runtime cast at the call site. Brainstorm guards on presence and returns a user-facing error if absent.
  • DEC-12--deep-brainstorm INLINED into /feature Phase 1 (writes 01-brainstorming.md in feature slug). DEC-3 narrowly reopened with DEC-8 as the prerequisite.
  • DEC-13--agents <n> escape hatch shipped in v1. Default --agents 3. n=0 satisfies OOS-14 A/B mechanism without v1.1 breaking change.
  • DEC-14Tolerant JSON parser lifted to shared src/utils/tolerantJson.ts (~40 LOC) modeled on AgentOutcomeExtractor.ts:73-117. Adopted by brainstorm immediately; refactor of AgentOutcomeExtractor + SteeringExtractor to consume it is FOLLOWUP (out of v1 scope, tracked as tech-debt).
  • DEC-15FR-21 fix returns RICH directive (mirror of CoordinatorEngine.ts:1338-1351), not empty string. Callers updated (currently only featureCommand.ts:487-503).
  • DEC-16FR-25 threshold failure AUTO-DEGRADES to coordinator-only grilling for the round (and subsequent), does not abort. Yellow warning + raw bodies persisted in NDJSON for debugging.
  • DEC-17Topology restore is CONDITIONAL on current === we-set-this to avoid clobbering concurrent /loop-driven topology changes (H-6 mitigation).
  • DEC-18parseFrontmatter stays module-private at src/skills/parser.ts:47; brainstormContext.ts uses a focused ~15-LOC YAML reader for the FR-14 schema. Avoids public-API expansion for a single consumer.

11. Risks of this hybrid (mapped to mitigations)

#RiskMitigationCitation
H-1All inquisitors fail (parse_error / timeout / empty) → user sees abort with no questions, blames the featureDEC-16 auto-degrade to coordinator-only grilling; yellow warning; raw bodies persisted to NDJSON for debuggingbrainstormCommand.ts
H-2tap-before-spawn race: spawn prompt completes and inquisitors emit before subscribe is installed → silent question lossTap registered SYNCHRONOUSLY before the async sendAndWait — verified by a dedicated ordering test in brainstormCommand.test.tsMessageBus.ts:226-251 (sync subscribe)
H-3/feature --deep-brainstorm writes 01-brainstorming.md but the original Phase-1 stage of /feature also writes it → collisionInline path replaces the normal Phase-1 stage (sets phase1Completed=true then proceeds to Phase 2) — not "in addition to"featureCommand.ts (new branch)
H-4Concurrent /loop changes activeTopology while brainstorm holds it as hub; brainstorm finally clobbers the loop's changeDEC-17 conditional restore: only restore if current === 'hub' at finally-timestateStore.ts
H-5Out-of-tree consumer asserts /feature error-string formats; FR-22 slug-validation error changes break themDocumented in CHANGELOG; assumption logged. /feature is a slash command (not a library API), low real-world surfacefeatureCommand.ts
H-6Concurrent /loop topology change clobber (same as H-4 — different vector)Same mitigation (DEC-17)stateStore.ts
H-7parseBrainstormContext on a malformed source .context.md crashes /feature --from-brainstorm mid-flightParser returns {ok: false, error} tagged result; featureCommand surfaces "Brainstorm context invalid: <error>; aborting." pre-Phase-2brainstormContext.ts

12. Test plan (per-module)

ModuleSurfaceReused helpers
brainstormCommandparser table, full loop with mocked coordinator + messageBus, --agents 0 and --agents 3 paths, FR-25 threshold-met and threshold-not-met (auto-degrade), summarize vs keep-grilling, tap-before-spawn ordering, topology save/restore including conditional path (H-4), SEC-2 wrapUntrusted re-feedmakeContext(), makeMockRegistry(); defensive crew-hijack pattern from researchCommand.test.ts:214-235
projectContextempty .fleet/context/, populated, neutralizer sanity, fs errortmpdir
brainstormContextYAML round-trip (qa_pairs nested objects), schema_version=2 reject (FR-18), malformed YAML graceful error, missing required field graceful errortmpdir
tolerantJsonfenced JSON (json … ), malformed (unclosed brace), missing keys, oversized string payload (NFR-21), dual-shape {key:[…]} vs […]pure
planDir (validators)SEC-3 regression (e.g., ../etc, foo/bar, absolute path), regex enforcement (uppercase, leading dash, length cap)tmpdir
parser.brainstorm.test.tsend-to-end workflow parse via existing skills parser; NFR-14 dual-prompt rule for stagesT-153 parser fixtures
SkillRegistry.brainstorm.test.tsAll 3 inquisitor roles discoverable via registry.getRole(); composeAgentPrompt('inquisitor-ux', []) succeedsmakeMockRegistry

13. Assumptions (explicit ⚠️ / ✅ markers)

  • ⚠️ ASSUMPTION: bundled .workflow.md / .role.md / .prompt.md markdown assets are content, not code; LOC budget tracking is informal. — REASON: critic and Designer 1 both flagged.
  • ⚠️ ASSUMPTION: no out-of-tree consumer asserts /feature error-string formats. — REASON: /feature is a slash command, not a library API; CHANGELOG entry sufficient (H-5).
  • ⚠️ ASSUMPTION: parseFrontmatter can stay private; the FR-14 YAML schema is simple enough for a focused ~15-LOC reader. — REASON: avoids public-API expansion.
  • ⚠️ ASSUMPTION: multi-agent ROI hypothesis remains unmeasured. — REASON: no telemetry. Mitigated by --agents <n> flag — v1.1 default flip if telemetry contradicts (DEC-13).
  • ⚠️ ASSUMPTION: tap-before-spawn ordering is sufficient to capture all inquisitor envelopes. — REASON: messageBus.subscribe synchronously installs the handler; the spawn prompt is async (sendAndWait) and races nothing. Defended by an explicit ordering test (H-2).
  • VERIFIED: MessageBus.subscribe supports multiple handlers and returns an unsubscribe closure (MessageBus.ts:226-251).
  • VERIFIED: MessageBusLike lacks subscribe field today (commands.ts:84-92).
  • VERIFIED: SEC-3 path traversal exploitable today via /feature --resume (featureCommand.ts:75-80).
  • VERIFIED: askUser direct path lacks unattended check (CoordinatorEngine.ts:1074-1097); tool path has it (:1338-1351).
  • VERIFIED: workflowController force-aborts prior (workflowController.ts:56-61) — the original DEC-3 hostility rationale.
  • VERIFIED: report_to_coordinator prefix format is [<report_type>] (FleetManager.ts:520,530).
  • VERIFIED: prior LLM-JSON parser pattern exists at two sites (AgentOutcomeExtractor.ts:73-117, SteeringExtractor.ts:99) — justifies DEC-14 shared util.
  • VERIFIED: /research is prompt-and-return — no JS-side harvest, LLM does synthesis (researchCommand.ts:80-86, :95-107, :222-224); confirms /feature was the right template, not /research.

14. Open questions for user (Phase 5 / Tasks to answer)

⚠️ HARD-CONSTRAINT NOTE: These 5 questions are logged as ⚠️ OPEN QUESTIONs (not yet asked via ask_user) because we did not interrupt the workflow. Phase 5 (Tasks) should propagate these into the user-facing review. Each is paired with a default recommendation the design assumes if the user does not override.

  • ⚠️ OPEN QUESTION OQ-1: Inline --deep-brainstorm writes 01-brainstorming.md (matches /feature convention) vs 00-brainstorming.md (matches standalone). RECOMMENDATION: 01- when inlined; 00- standalone. Drives final FR-19 wording.
  • ⚠️ OPEN QUESTION OQ-2: --agents <n> range {0,1,2,3} (subset support) vs binary {0,3}. RECOMMENDATION: full {0,1,2,3} with degradation order ux → +technical → +edge-cases.
  • ⚠️ OPEN QUESTION OQ-3: Tolerant JSON parser as new shared src/utils/tolerantJson.ts (lift now, refactor AgentOutcomeExtractor + SteeringExtractor as followup) vs inline in brainstorm only. RECOMMENDATION: shared util now (40 LOC saved across 3 future sites; 1 net-new file).
  • ⚠️ OPEN QUESTION OQ-4: FR-22 backport breadth — apply isPathInsideDir + slug regex to all plan-slug consumers (/feature, /brainstorm, others if any) in the same PR, or scope to /feature + /brainstorm only and defer others. RECOMMENDATION: same PR for all; SEC-3 is HIGH and audit-while-it's-fresh is cheap.
  • ⚠️ OPEN QUESTION OQ-5: FR-21 direct-path askUser returns RICH directive (mirror of :1338-1351) vs empty string. RECOMMENDATION: rich directive (consistency); update featureCommand.ts:487-503 to handle.

15. Phase exit criteria

  • [x] 3 parallel design proposals delivered (minimal, clean, alternative).
  • [x] Design-critic cross-referenced and produced verified contradiction resolution.
  • [x] All 8 ratified Phase-3 decisions reviewed; DEC-3 narrowly reopened with evidence.
  • [x] 10 new decisions DEC-9..DEC-18 captured.
  • [x] 7 risks H-1..H-7 catalogued with mitigations.
  • [x] File-by-file modify/create plan with LOC estimates.
  • [x] Integration points enumerated with file:line citations.
  • [x] Test plan by module.
  • [x] 5 open questions explicitly logged for user / Phase 5 to resolve.
  • [x] Artifact written.