9.9 KiB
title, source_type, status, confidence, tags, last_reviewed
| title | source_type | status | confidence | tags | last_reviewed | |||||
|---|---|---|---|---|---|---|---|---|---|---|
| Spec D — research-fanout Workflow (scoped, Claude 전용, opt-in) | llm-generated | draft | medium |
|
2026-06-06 |
Spec D — research-fanout Workflow
상위 감사: 2026-06-06-harness-audit-report §3 G2. 확정 결정: scoped research-fanout Workflow(전체 명령 변환 아님), 승인 완료. 구현(파일 Write + Workflow 실행)은 opt-in 시.
1. 문제 (감사에서) + 재설계
- G2 (P2):
/branch-spec·/project-spec오케스트레이션이 산문-자문(번호 매긴 LLM 지시). controller 가 단계를 건너뛰어도 탐지 안 됨. deep-research 의 코드 barrier 와 대비. - 재설계 (브레인스토밍 발견): 두 명령은 비-기계적 부분이 본질적이다 —
project-spec §3의AskUserQuestion(사용자 소유 결정), ca-tmpl ground-truth 정독(판단), 노트 채움(user-content 보존), 다이어그램 작성. 전체를 Workflow 로 변환하면 이 상호작용·판단이 깨진다(YAGNI 위반). - 따라서 순수 fan-out 서브페이즈만 추출:
/branch-spec §5(결정별 bounded 자동조사 —wiki-decision-researcher×N, cap 6, deferred). 이것이 deep-research 의pipeline(angles → research → schema)와 정확히 동형이다.
2. 핵심 원리 / 비목표
- 산문 명령은 유지 (3-플랫폼). Claude 만 옵션으로 Workflow 를 호출해 §5 의 병렬 조사를 빠르고 schema-강제·cap-보장으로 수행. Codex/Antigravity 는 기존 순차 dispatch — parity 안 깨짐.
- A/B/C 메커니즘의 진짜 Workflow 형태: tool-layer
agent({schema})(B 를 hope→enforce 로) · funnelfound=processed+dropped(C) · cap+deferred(C no-silent-truncation). - autonomous 조사(deep-research 처럼 user-approval 없이) — 산문
wiki-decision-researcher(WebSearch→승인→fetch)와의 trade-off. 둘 병존, 사용자 선택. - 비목표: 전체 명령 변환 ·
project-spec변환(AskUserQuestion 본질적, v2 후보) · verify gate 의 Workflow 화(별도). branch-note 직접 편집(Workflow 는 input 만 반환, 산문 controller 가 §7 채움).
3. 설계 결정 (확정)
DD1 — scoped fan-out only
Workflow 는 §5(결정별 조사 burst)만. §14(전제·ground-truth·결정추출)와 §69(라벨·채움·게이트)는 산문 controller.
DD2 — autonomous + schema-enforced
각 결정을 agent({schema: ALT_SCHEMA}) 로 병렬 조사. user-approval 없음(autonomous). 결과는 구조화된 비교매트릭스.
DD3 — Claude 전용, 산문은 불변
.claude/workflows/research-fanout.* 신규. /branch-spec §5 에 "옵션: Claude 에서 research-fanout Workflow 호출" 한 줄만 추가(산문 흐름·3-플랫폼 미러 불변).
4. 아키텍처 — Workflow 스크립트
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" } }, // URL
quote: { type: "string" }, // verbatim 인용
sourceType: { enum: ["official-doc", "company-tech-blog", "personal-blog", "unknown"] },
},
}},
recommendation: { type: "string" },
confidence: { enum: ["high", "medium", "low"] },
unsupported: { type: "boolean" }, // 조사 후에도 근거 부족 → UNSUPPORTED_DECISION
},
}
// ── Scope: cap + deferred (no silent truncation) ──
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."
// ── Research: 진짜 병렬 + tool-layer schema 강제 ──
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)
// ── Synthesize: funnel + branch-note DEM input ──
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",
}
// 불변식: found = processed + dropped (decisions.length = results + deferred + failures) ✓
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,
// 산문 controller 가 §7 Decision Evidence Map 채움에 쓰는 input (Workflow 는 노트 직접 편집 안 함)
branchNoteInput: results.map(r => ({
decision: r.topic, recommendation: r.recommendation, confidence: r.confidence,
sources: r.alternatives.flatMap(a => a.sources), unsupported: !!r.unsupported,
})),
}
4.1 산문 /branch-spec 변경 (한 줄 — Claude 전용 옵션)
§5 에 추가:
(옵션, Claude 전용) 결정 수가 많거나 빠른 병렬 조사를 원하면
research-fanoutWorkflow 를 호출한다(Workflow({name:'research-fanout', args:{decisions:[...]}}), ultracode/opt-in 필요). 반환된matrices/branchNoteInput으로 §7 Decision Evidence Map 을 채우고,stats/deferred를 §9 요약에 반영. autonomous 조사라 user-approval 이 없으므로, 승인-gated 가 필요하면 기존wiki-decision-researcher순차 dispatch 를 쓴다. Codex/Antigravity 는 항상 순차 dispatch.
5. 위험
| 위험 | 완화 |
|---|---|
| autonomous 조사가 user-approval 우회 | 산문 wiki-decision-researcher(승인-gated)와 병존 — 사용자가 선택. §5 에 trade-off 명시. |
| Workflow Claude 전용 → parity | 산문 명령 불변(3-플랫폼). Workflow 는 추가 옵션. project 파이프라인처럼 명시적 Claude 예외. |
| cap 초과 silent 절단 | deferred 명시 반환 + funnel dropped_reason. |
| schema 강제로 agent 재시도 비용 | deep-research 와 동일 — tool-layer 검증이 신뢰성↑. cap 6 으로 비용 bound. |
| Workflow 실행 opt-in 필요 | spec/plan 은 지금, 실행은 ultracode opt-in. 산문 경로는 항상 가용(non-opt-in fallback). |
6. 수용 기준 (검증 가능 — opt-in 실행 시)
Workflow({name:'research-fanout', args:{decisions:[3개]}})→matrices3개 +stats.found=3, processed+dropped=3.- 8개 decisions → 6 researched +
deferred2개 명시 +stats.dropped≥2 with dropped_reason. - 빈 decisions →
{error}(graceful). - 각 matrix 가 ALT_SCHEMA 충족(alternatives ≥2, sources 배열, recommendation/confidence).
branchNoteInput이 산문 §7 채움에 바로 쓰일 형태(decision/recommendation/sources/unsupported).- 산문
/branch-spec의 기존 순차 경로(비-Workflow)가 여전히 동작(Workflow 미사용 fallback).
7. 구현 순서 (writing-plans — 실행은 opt-in)
- (opt-in)
.claude/workflows/research-fanout.*에 §4 스크립트 Write. - (opt-in)
Workflow({name:'research-fanout', args:{decisions:[테스트 2-3개]}})실행 → §6.1·6.4 검증. - (opt-in) 8개 decisions 로 cap/deferred 검증(§6.2).
/branch-spec §5에 옵션 호출 한 줄 추가(§4.1) — 이건 산문 편집이라 opt-in 불요(언제든).- (opt-in) §6 전 항목 스모크.
경계: 4번(산문 한 줄)은 비-opt-in 으로 지금 가능. 1~3·5(Workflow Write+실행)는
Workflow툴 opt-in 시. plan 이 이 경계를 task 별로 명시.
8. 메모
- 이건 "전체 오케스트레이션 결정론화"(원래 G2 야망)가 아니라 기계화 가능한 한 조각만 deep-research 형태로. 나머지 산문 오케스트레이션은 interactive/judgment 라 산문이 옳다 — 그 결론 자체가 D 의 산출.
- A(구조 게이트)·B(verdict schema)·C(stats)·D(research-fanout Workflow)로 deep-research 8원칙이 하네스 전반에 이식됨 — 단 P2(코드 오케스트레이션)는 기계화 가능한 부분에 한해.