Files
llm-wiki/docs/superpowers/plans/2026-06-06-spec-d-research-fanout-workflow.md

11 KiB

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:

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:

   - **(옵션, 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.