219 lines
11 KiB
Markdown
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.
|