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

219 lines
11 KiB
Markdown

# 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.