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 (loadProjectContext → src/utils/projectContext.ts; validateSlug + resolveSafePlanDir → src/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:
- Parses its own args (FR-1, FR-11, FR-13, plus the new
--agents <n>). - Validates slug + resolves plan dir via the new
planDir.tshelpers (DEC-6 / FR-22, closes SEC-3). - Saves topology, sets
hub(IR-7 / Q-D5), restores infinally(conditionally — see DEC-17). - Walks rounds in a JS while-loop (DEC-8 — no workflow controller registered, so
--deep-brainstorminlining is safe). - In round 1 (when
agentsN > 0): registers amessageBus.subscribe('coordinator', handler)tap BEFORE sending the spawn prompt; sends a spawn prompt instructing the coordinator to callspawn_worker3× in parallel (DEC-5); waits for the inquisitoragent.completed/agent.failedevents (event-based, not artifact-polled); drains partial_result envelopes; disposes tap. - Parses harvested JSON via shared
tolerantJsonParse(DEC-7 / DEC-14, modeled onAgentOutcomeExtractor.ts:73-117); checks FR-25 threshold; auto-degrades to coordinator-only grilling if all inquisitors failed (DEC-16) rather than aborting. - Dedupes + prioritises ≤8 questions; grills each via
coordinator.askUser({question})(T-153 deterministic path, FR-21-safe after this PR). - In rounds 2+ (or when
agentsN === 0): single-coordinator follow-ups viacoordinator.sendAndWait(prompt)with prior transcript wrapped viawrapUntrusted(FR-23 / SEC-2). - After round Nmin: termination gate
askUser({choices: ['summarize', 'keep grilling']})(FR-7). - Synthesis: final
sendAndWaitasks coordinator to compose narrative + context-md strings; JS writes both viaatomicWriteFile({mode: 0o644})(SEC-5); sentinel.brainstorm-completewritten last (FR-9, FR-24, NFR-20).
Two narrow extractions support reuse:
loadProjectContextfromfeatureCommand.ts:126-141→ newsrc/utils/projectContext.ts(16 LOC).validateSlug+resolveSafePlanDiradded tosrc/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-61 — setActiveWorkflowController 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-251supports 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)
| File | Change | LOC Δ | Citation |
|---|---|---|---|
src/commands/registry.ts | Import createBrainstormCommand; push into BUILTIN_COMMANDS; add 'brainstorm' (+ aliases 'brain', 'bs') to Workflows category mapping | +3 | :53-58, :178-215 |
src/commands/planDir.ts | Add SLUG_REGEX constant + validateSlug(slug) + resolveSafePlanDir(cwd, slug) helpers using isPathInsideDir from utils/fs.ts:40. Pure additions, no behavior change for existing callers. | +25 | new 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.ts | FR-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. | +35 | CoordinatorEngine.ts |
src/types/commands.ts | Widen 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'); }. | +2 | commands.ts |
src/utils/index.ts | Re-export loadProjectContext, hasFleetContext (from projectContext.ts), parseBrainstormContext, serializeBrainstormContext, formatBrainstormDigest (from brainstormContext.ts), tolerantJsonParse (from tolerantJson.ts). | +4 | barrel |
Total modified: 6 files; net ≈ +93 LOC.
4. Files to CREATE
| File | Purpose | LOC | Notes |
|---|---|---|---|
src/commands/brainstormCommand.ts | Single-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. | ~380 | DEC-1, 2, 5, 8 |
src/commands/brainstormCommand.test.ts | Arg-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.ts | Extracted loadProjectContext + hasFleetContext from featureCommand (lift-and-shift, branded path types where applicable). | 16 | DEP-5 |
src/utils/projectContext.test.ts | Empty .fleet/context/, populated, neutralizer sanity, fs error path. | 50 | |
src/utils/brainstormContext.ts | parseBrainstormContext(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. | ~80 | FR-14, 16, 18 |
src/utils/brainstormContext.test.ts | YAML round-trip including qa_pairs (nested array of objects), schema_version=2 rejection (FR-18), malformed YAML graceful error, missing required fields graceful error. | 70 | NFR-14 |
src/utils/tolerantJson.ts | Shared 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. | ~40 | DEC-7, DEC-14 |
src/utils/tolerantJson.test.ts | Fenced JSON json … , malformed (unclosed brace), missing keys, oversized string payload (NFR-21), dual-shape ({questions: […]} vs […]). | 80 | |
src/skills/bundled/workflows/brainstorm.workflow.md | Frontmatter 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). | ~30 | IR-3 |
src/skills/bundled/workflows/brainstorm/round.prompt.md | Per-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.md | Synthesis 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. | ~40 | FR-8 |
src/skills/bundled/roles/inquisitor-ux.role.md | UX-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". | ~50 | FR-4, IR-4 |
src/skills/bundled/roles/inquisitor-technical.role.md | Same shape, technical/integration angle. | ~50 | |
src/skills/bundled/roles/inquisitor-edge-cases.role.md | Same shape, edge-cases / failure-mode angle. | ~50 | |
src/skills/parser.brainstorm.test.ts | Parses brainstorm.workflow.md end-to-end through the existing skills parser; asserts NFR-14 dual-prompt rule for stages (per-stage .prompt.md exists). | 50 | NFR-14 |
src/skills/SkillRegistry.brainstorm.test.ts | All 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)receivesCommandContext(src/types/commands.ts:94-132); readscontext.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 theask_usertool — 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 callspawn_worker3× in parallel withrole: 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 outerfinally. Filter envelopes whereenvelope.from ∈ inquisitorIds && envelope.message.type === 'text' && content.startsWith('[partial_result] ')(prefix verified atsrc/fleet/FleetManager.ts:520,530). Unsubscribe via the closure returned fromsubscribe(MessageBus.ts:244-250). - Spawn-wait integration:
waitForInquisitors(ids, timeoutMs)listens onstateStore.on('agent.completed', …)+stateStore.on('agent.failed', …)for the diff set; resolves when all known IDs are done OR timeout fires. ~25 LOC inline inbrainstormCommand.ts. Followed by a ~500 ms post-quiesce drain so the lastpartial_resultenvelope 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; outerfinallyrestores 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-completewritten AFTER both artifacts succeed. - Re-feed safety (FR-23 / SEC-2):
wrapUntrusted(answer)fromsrc/utils/promptInjectionGuard.ts:78applied wherever transcript answers are templated into round-2+ follow-up prompts or into the synthesis prompt. - Plan-dir + slug (FR-22 / SEC-3):
validateSlug+resolveSafePlanDirfromplanDir.tsclose SEC-3 in/brainstorm --resume, in the/brainstorm --from-brainstormsource-slug parameter, and (via the same backport) in/feature --resume. --deep-brainstormchain:featureCommand.handlerdetects flag, callsrunBrainstormInline(context, feature, planDir)(exported from brainstormCommand). Inline run writes01-brainstorming.md+01-brainstorming.context.mdin 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/featurecontroller survives.--from-brainstormchain:featureCommand.handlerreads source brainstorm viaparseBrainstormContext('.plans/<src>/00-brainstorming.context.md'), writes pointer01-brainstorming.md("This feature was seeded from.plans/<src>/00-brainstorming.md"), prependsformatBrainstormDigest(ctx)to the Phase-2 prompt.
6. Component design (key abstractions, signatures)
// 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;
}// 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;// src/utils/tolerantJson.ts
export function tolerantJsonParse<T>(
body: string,
opts?: { acceptArray?: boolean; arrayKey?: string },
): { ok: true; value: T } | { ok: false; error: string };// 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 };// src/utils/projectContext.ts (extracted from featureCommand.ts:126-141)
export function hasFleetContext(cwd: string): boolean;
export function loadProjectContext(cwd: string): string;// 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):
- Snapshot existing agent IDs from
stateStore.getState().agents. - Register tap:
tap = context.messageBus.subscribe('coordinator', handler)— BEFORE step 3. - Send spawn prompt to coordinator via
sendAndWait. - Wait for new agent IDs (diff set vs step 1 snapshot) to appear in
state.agentswhoserolebeginsinquisitor-. - Wait via
waitForInquisitors(inquisitorIds, perRoundTimeoutMs)foragent.completed/agent.failedevents for each ID in the set. - ~500 ms post-quiesce drain (let last
partial_resultenvelope arrive). - Check
meetsThreshold()(FR-25):≥1inquisitor withstatus==='success'ANDquestions.length > 0. - Always
tap()in the outerfinally.
Handler filter (~10 LOC):
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):
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/finallywraps topology save/restore (conditional — DEC-17). - Round 1: if
agentsN > 0, spawn N inquisitors + harvest; ifagentsN === 0or auto-degrade triggered, coordinator-only follow-ups (same code path as round 2+). - Rounds 2+: always single-coord follow-ups via
sendAndWaitwith prior transcript wrapped viawrapUntrusted(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; elseaskUserwithchoices: ['summarize', 'keep grilling']. Match/summari[sz]e/i. - Synthesis: final
sendAndWaitasks coordinator to compose narrative + context markdown strings; JS writes both viaatomicWriteFile({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)
| n | Behavior |
|---|---|
| 0 | No 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. |
| 1 | Spawn only inquisitor-ux. Drop technical + edge-cases. |
| 2 | Spawn inquisitor-ux + inquisitor-technical. Drop edge-cases. |
| 3 | Default. All three inquisitor roles spawned in parallel via the coordinator. |
Implementation:
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-9 — Single-file
brainstormCommand.ts(no state-machine, no DI). YAGNI; future need for state-machine extraction can be refactored when a second consumer appears. - DEC-10 — 2 extractions adopted (
projectContext, slug validators inplanDir); 2 rejected (agentWaiter— wrong shape;UserGrilling— 90% boilerplate over a one-liner). - DEC-11 —
MessageBusLike.subscribe?optional widening (1 line incommands.ts) preferred over runtime cast at the call site. Brainstorm guards on presence and returns a user-facing error if absent. - DEC-12 —
--deep-brainstormINLINED into/featurePhase 1 (writes01-brainstorming.mdin 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=0satisfies OOS-14 A/B mechanism without v1.1 breaking change. - DEC-14 — Tolerant JSON parser lifted to shared
src/utils/tolerantJson.ts(~40 LOC) modeled onAgentOutcomeExtractor.ts:73-117. Adopted by brainstorm immediately; refactor ofAgentOutcomeExtractor+SteeringExtractorto consume it is FOLLOWUP (out of v1 scope, tracked as tech-debt). - DEC-15 — FR-21 fix returns RICH directive (mirror of
CoordinatorEngine.ts:1338-1351), not empty string. Callers updated (currently onlyfeatureCommand.ts:487-503). - DEC-16 — FR-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-17 — Topology restore is CONDITIONAL on
current === we-set-thisto avoid clobbering concurrent/loop-driven topology changes (H-6 mitigation). - DEC-18 —
parseFrontmatterstays module-private atsrc/skills/parser.ts:47;brainstormContext.tsuses 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)
| # | Risk | Mitigation | Citation |
|---|---|---|---|
| H-1 | All inquisitors fail (parse_error / timeout / empty) → user sees abort with no questions, blames the feature | DEC-16 auto-degrade to coordinator-only grilling; yellow warning; raw bodies persisted to NDJSON for debugging | brainstormCommand.ts |
| H-2 | tap-before-spawn race: spawn prompt completes and inquisitors emit before subscribe is installed → silent question loss | Tap registered SYNCHRONOUSLY before the async sendAndWait — verified by a dedicated ordering test in brainstormCommand.test.ts | MessageBus.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 → collision | Inline path replaces the normal Phase-1 stage (sets phase1Completed=true then proceeds to Phase 2) — not "in addition to" | featureCommand.ts (new branch) |
| H-4 | Concurrent /loop changes activeTopology while brainstorm holds it as hub; brainstorm finally clobbers the loop's change | DEC-17 conditional restore: only restore if current === 'hub' at finally-time | stateStore.ts |
| H-5 | Out-of-tree consumer asserts /feature error-string formats; FR-22 slug-validation error changes break them | Documented in CHANGELOG; assumption logged. /feature is a slash command (not a library API), low real-world surface | featureCommand.ts |
| H-6 | Concurrent /loop topology change clobber (same as H-4 — different vector) | Same mitigation (DEC-17) | stateStore.ts |
| H-7 | parseBrainstormContext on a malformed source .context.md crashes /feature --from-brainstorm mid-flight | Parser returns {ok: false, error} tagged result; featureCommand surfaces "Brainstorm context invalid: <error>; aborting." pre-Phase-2 | brainstormContext.ts |
12. Test plan (per-module)
| Module | Surface | Reused helpers |
|---|---|---|
brainstormCommand | parser 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-feed | makeContext(), makeMockRegistry(); defensive crew-hijack pattern from researchCommand.test.ts:214-235 |
projectContext | empty .fleet/context/, populated, neutralizer sanity, fs error | tmpdir |
brainstormContext | YAML round-trip (qa_pairs nested objects), schema_version=2 reject (FR-18), malformed YAML graceful error, missing required field graceful error | tmpdir |
tolerantJson | fenced 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.ts | end-to-end workflow parse via existing skills parser; NFR-14 dual-prompt rule for stages | T-153 parser fixtures |
SkillRegistry.brainstorm.test.ts | All 3 inquisitor roles discoverable via registry.getRole(); composeAgentPrompt('inquisitor-ux', []) succeeds | makeMockRegistry |
13. Assumptions (explicit ⚠️ / ✅ markers)
- ⚠️ ASSUMPTION: bundled
.workflow.md/.role.md/.prompt.mdmarkdown assets are content, not code; LOC budget tracking is informal. — REASON: critic and Designer 1 both flagged. - ⚠️ ASSUMPTION: no out-of-tree consumer asserts
/featureerror-string formats. — REASON:/featureis a slash command, not a library API; CHANGELOG entry sufficient (H-5). - ⚠️ ASSUMPTION:
parseFrontmattercan 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.subscribesynchronously installs the handler; the spawn prompt is async (sendAndWait) and races nothing. Defended by an explicit ordering test (H-2). - ✅ VERIFIED:
MessageBus.subscribesupports multiple handlers and returns an unsubscribe closure (MessageBus.ts:226-251). - ✅ VERIFIED:
MessageBusLikelackssubscribefield today (commands.ts:84-92). - ✅ VERIFIED: SEC-3 path traversal exploitable today via
/feature --resume(featureCommand.ts:75-80). - ✅ VERIFIED:
askUserdirect path lacks unattended check (CoordinatorEngine.ts:1074-1097); tool path has it (:1338-1351). - ✅ VERIFIED:
workflowControllerforce-aborts prior (workflowController.ts:56-61) — the original DEC-3 hostility rationale. - ✅ VERIFIED:
report_to_coordinatorprefix 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:
/researchis prompt-and-return — no JS-side harvest, LLM does synthesis (researchCommand.ts:80-86, :95-107, :222-224); confirms/featurewas 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-brainstormwrites01-brainstorming.md(matches/featureconvention) vs00-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, refactorAgentOutcomeExtractor+SteeringExtractoras 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+/brainstormonly 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
askUserreturns RICH directive (mirror of:1338-1351) vs empty string. RECOMMENDATION: rich directive (consistency); updatefeatureCommand.ts:487-503to 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.