# Spec D — research-fanout Workflow Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans. Steps use checkbox (`- [ ]`) syntax. > **Commits excluded** per user instruction. **OPT-IN BOUNDARY:** Tasks marked `[OPT-IN]` require the `Workflow` tool (explicit opt-in / "ultracode") to *run*; Task 4 (prose edit) needs no opt-in and can run anytime. **Goal:** Provide a Claude-only `research-fanout` Workflow that runs the bounded per-decision alternatives-research burst (branch-spec §5) as a real deep-research-style pipeline — parallel `agent({schema})` + cap + deferred + funnel stats — without disturbing the 3-platform prose command. **Architecture:** A standalone Workflow script (`.claude/workflows/research-fanout.js`) that takes `args.decisions = [{topic, parentBranch, constraints, n}]`, caps at 6, fans out autonomous schema-enforced web research in parallel, and returns comparison matrices + `branchNoteInput` + funnel `stats`. The prose `/branch-spec §5` gains one optional line pointing to it (Claude only). Codex/Antigravity keep sequential dispatch — no parity change. **Tech Stack:** Claude Code `Workflow` tool (JS script, not TypeScript), `agent()`/`parallel()`/`phase()`/`log()`. WebSearch/WebFetch inside agents. **Spec:** `docs/superpowers/specs/2026-06-06-spec-d-research-fanout-workflow-design.md` --- ## File Structure - **Create** `.claude/workflows/research-fanout.js` — the Workflow script (spec §4). `[OPT-IN]` - **Modify** `.claude/commands/branch-spec.md` — one optional line in §5 (Claude-only Workflow call). (no opt-in) > Invocation note: at opt-in, run via `Workflow({name: 'research-fanout', args: {...}})` if the file is registered as a named workflow, or `Workflow({scriptPath: '.claude/workflows/research-fanout.js', args: {...}})`. Confirm the registration path the first run (the Workflow tool persists inline scripts and reports the path). --- ## Task 1 `[OPT-IN]`: Author the Workflow script **Files:** - Create: `.claude/workflows/research-fanout.js` - [ ] **Step 1: Write the script verbatim from spec §4** Create `.claude/workflows/research-fanout.js` with exactly the spec §4 script: ```js export const meta = { name: 'research-fanout', description: 'Bounded parallel alternatives-research for N branch decisions (autonomous, schema-enforced)', phases: [ { title: 'Scope', detail: 'validate + cap decisions (max 6), split deferred' }, { title: 'Research', detail: 'parallel autonomous web research per decision, schema-enforced' }, { title: 'Synthesize', detail: 'comparison matrices + funnel stats + branch-note DEM input' }, ], } const MAX_DECISIONS = 6 const ALT_SCHEMA = { type: "object", required: ["decision", "alternatives", "recommendation", "confidence"], properties: { decision: { type: "string" }, alternatives: { type: "array", minItems: 2, maxItems: 5, items: { type: "object", required: ["name", "pros", "cons", "sources"], properties: { name: { type: "string" }, pros: { type: "string" }, cons: { type: "string" }, sources: { type: "array", items: { type: "string" } }, quote: { type: "string" }, sourceType: { enum: ["official-doc", "company-tech-blog", "personal-blog", "unknown"] }, }, }}, recommendation: { type: "string" }, confidence: { enum: ["high", "medium", "low"] }, unsupported: { type: "boolean" }, }, } phase("Scope") const decisions = (args && Array.isArray(args.decisions)) ? args.decisions : [] if (!decisions.length) { return { error: "No decisions. Pass args.decisions = [{topic, parentBranch, constraints, n}]." } } const capped = decisions.slice(0, MAX_DECISIONS) const deferred = decisions.slice(MAX_DECISIONS) log(`${decisions.length} decisions → research ${capped.length}, defer ${deferred.length}`) const researchPrompt = (d) => "## Alternatives Researcher (autonomous)\n\n" + "Branch decision: \"" + d.topic + "\"\n" + "Parent branch: " + (d.parentBranch || "(none)") + "\n" + "Constraints: " + (d.constraints || "(none)") + "\n\n" + "## Task\n" + "1. WebSearch official docs + 대기업 기술블로그 for " + (d.n || 3) + " viable alternatives.\n" + "2. WebFetch each; extract Pros/Cons + a verbatim quote + source URL + sourceType.\n" + "3. company-tech-blog 만으로 '공식 best practice' 승격 금지(독립 사례 2+ 또는 official 병행).\n" + "4. 근거가 한쪽으로 명확하면 그대로 recommendation. 가짜 5:5 균형 금지.\n" + "5. 조사 후에도 근거 부족하면 unsupported=true.\n\nStructured output only." phase("Research") const results = (await parallel( capped.map(d => () => agent(researchPrompt(d), { label: "research:" + d.topic.slice(0, 30), phase: "Research", schema: ALT_SCHEMA }) .then(r => r ? { ...r, topic: d.topic } : null) ) )).filter(Boolean) phase("Synthesize") const failures = capped.length - results.length const stats = { found: decisions.length, processed: results.length, dropped: deferred.length + failures, dropped_reason: [ deferred.length ? `${deferred.length} over cap(${MAX_DECISIONS})` : null, failures ? `${failures} research failed/skipped` : null, ].filter(Boolean).join("; ") || "none", } log(`done: ${results.length} researched, ${stats.dropped} dropped`) return { matrices: results, deferred: deferred.map(d => d.topic), unsupported: results.filter(r => r.unsupported).map(r => r.topic), stats, branchNoteInput: results.map(r => ({ decision: r.topic, recommendation: r.recommendation, confidence: r.confidence, sources: r.alternatives.flatMap(a => a.sources), unsupported: !!r.unsupported, })), } ``` - [ ] **Step 2: Syntax sanity (no opt-in needed — node parse only)** Run: `node --check .claude/workflows/research-fanout.js && echo "syntax OK"` Expected: `syntax OK`. (Note: `node --check` validates JS syntax; the Workflow runtime — `agent`/`parallel`/`phase`/`log`/`args` globals — is provided by the Workflow tool, not node, so this only checks parse-ability, not execution.) If `node` is unavailable, skip and rely on the Workflow tool's own parse at run time. - [ ] **Step 3: Checkpoint (no commit).** --- ## Task 2 `[OPT-IN]`: Run — happy path (2-3 decisions) **Files:** none (Workflow execution) - [ ] **Step 1: Invoke the Workflow with a small decisions list** Via the `Workflow` tool: ``` Workflow({ name: 'research-fanout', args: { decisions: [ { topic: "outbox polling vs CDC for transactional outbox", parentBranch: "feature-domain-event-outbox-contract", constraints: "Spring Boot, Postgres", n: 3 }, { topic: "idempotency key storage: dedicated table vs redis", parentBranch: "feature-idempotency-key", constraints: "at-least-once delivery", n: 3 } ]}}) ``` (If `name` resolution fails, use `scriptPath: '.claude/workflows/research-fanout.js'`.) - [ ] **Step 2: Verify acceptance §6.1, §6.4** Expected return: `matrices` length 2; each matrix has `alternatives` ≥2 with `sources` arrays + `recommendation` + `confidence`; `stats.found == 2` and `stats.processed + stats.dropped == 2`. - [ ] **Step 3: Verify §6.3 — empty input graceful** Invoke `Workflow({ name: 'research-fanout', args: { decisions: [] }})`. Expected: `{ error: "No decisions. ..." }` (no crash). - [ ] **Step 4: Checkpoint (no commit).** --- ## Task 3 `[OPT-IN]`: Run — cap + deferred (8 decisions) **Files:** none - [ ] **Step 1: Invoke with 8 decisions** `Workflow({ name: 'research-fanout', args: { decisions: [ /* 8 objects, each {topic, n:2} */ ]}})`. - [ ] **Step 2: Verify acceptance §6.2** Expected: `matrices` length ≤6; `deferred` length 2 (the 7th, 8th topics); `stats.dropped >= 2` and `stats.dropped_reason` contains `over cap(6)`; `stats.found == 8` and `processed + dropped == 8`. - [ ] **Step 3: Checkpoint (no commit).** --- ## Task 4 (no opt-in): Add the optional call line to `/branch-spec §5` **Files:** - Modify: `.claude/commands/branch-spec.md` (§5, after the bound line) - [ ] **Step 1: Add the Claude-only option line** In `.claude/commands/branch-spec.md`, in step 5 (after the line ` - 조사는 **개수가 아니라 근거** ...`), add: ```markdown - **(옵션, Claude 전용)** 결정 수가 많거나 빠른 병렬 조사를 원하면 `research-fanout` Workflow 를 호출한다 (`Workflow({name:'research-fanout', args:{decisions:[{topic,parentBranch,constraints,n}, ...]}})`, ultracode/opt-in 필요). 반환된 `matrices`/`branchNoteInput` 으로 §7 Decision Evidence Map 을 채우고 `stats`/`deferred` 를 §9 요약에 반영한다. **autonomous 조사라 user-approval 이 없으므로** 승인-gated 가 필요하면 기존 `wiki-decision-researcher` 순차 dispatch 를 쓴다. Codex/Antigravity 는 항상 순차 dispatch (Workflow 는 Claude 전용). ``` - [ ] **Step 2: Verify** Run: `grep -c "research-fanout" .claude/commands/branch-spec.md` Expected: ≥1. - [ ] **Step 3: 3-platform note (no edit)** `/branch-spec` 의 Codex/Antigravity variant 에는 이 줄을 **미러하지 않는다** (Workflow 는 Claude 전용). 미러 생략이 의도임을 확인만 한다 — variant 는 기존 순차 dispatch 유지. - [ ] **Step 4: Checkpoint (no commit).** --- ## Task 5 `[OPT-IN]`: Full acceptance smoke **Files:** none - [ ] **Step 1: Re-confirm §6.1-6.6** - §6.1/6.4 (Task 2), §6.2 (Task 3), §6.3 (Task 2 Step 3) — done. - §6.5: inspect a Task 2 return — `branchNoteInput[i]` has `{decision, recommendation, sources, unsupported}` usable for prose §7 fill. - §6.6: confirm `/branch-spec` prose sequential path (without Workflow) is unchanged — read §5, verify the original `wiki-decision-researcher` dispatch line is intact and the new line is clearly "옵션". - [ ] **Step 2: Report results; leave changes in working tree (no commit).** --- ## Self-Review (completed by plan author) - **Spec coverage:** §4 script → Task 1. §4.1 prose line → Task 4. §6 acceptance 1-6 → Tasks 2/3/5. §7 opt-in boundary → `[OPT-IN]` tags + Task 4 non-opt-in. No gap. - **Placeholder scan:** Task 1 carries the full verbatim script; Task 4 carries the exact line; run tasks give exact `Workflow({...})` invocations + expected returns. The only intentional deferral is execution (opt-in), explicitly flagged — not a placeholder. - **Type/name consistency:** `decisions`/`topic`/`parentBranch`/`constraints`/`n` arg shape consistent across script, invocations, and `branchNoteInput`; `stats.{found,processed,dropped,dropped_reason}` matches the C-spec funnel contract; `MAX_DECISIONS=6` matches the "8→6+2 deferred" acceptance. - **Note:** Task 4 is the only non-opt-in change; if the user never opts into Workflow, the prose command still gains a (currently-dormant) Claude-only pointer that degrades gracefully to the existing sequential path.