okstra 0.45.1 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,457 @@
1
+ # Plan B — 실행기 측 run batching (ready-set + 예산 8) 구현 계획
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** 한 `implementation` run 이 정확히 1 stage 대신, `depends-on` 이 모두 done 인 ready stage 들을 합산 effective step 예산 8 까지 batch 로 실행하게 하여, 교차검증·report 고정비를 stage 수가 아닌 run 수에 비례하게 만든다(사용자가 체감한 비용 절감의 본체).
6
+
7
+ **Architecture:** stage 선택 권한을 Python 으로 일원화한다(현재 `_resolve_effective_stage` 의 단일 int + 프롬프트의 lead 자가계산 이중 경로를 제거). `_resolve_effective_stages` 가 ready-set 배치 리스트를 반환하고, `prepare_task_bundle` 가 batch 의 각 stage 에 `started` consumer row 를 쓰고 `{{EFFECTIVE_STAGES}}` 를 launch 프롬프트에 주입한다. 프롬프트 계약은 "주입된 batch 의 stage 들을 오름차순으로 실행, 각 경계에서 stage 별 sidecar/consumers done row 방출, run 끝에 교차검증·report 1회, PR 1개" 로 바뀐다.
8
+
9
+ **Tech Stack:** Python 3 (`scripts/okstra_ctl/run.py`, `consumers.py`), pytest + e2e bash, jinja2 launch 템플릿, markdown 프로파일 프롬프트, Node 빌드(`npm run build`).
10
+
11
+ > **선행:** Plan A(응집 + S9 병렬-안전 불변식) 가 먼저 머지되어 있어야 batch 와 병렬 run 의 파일 충돌이 구조적으로 방지된다. 본 계획은 Plan A 브랜치(`feat/stage-cohesion-planner`) 위에서 이어 작업하거나 그것이 main 에 머지된 뒤 새 브랜치에서 시작한다.
12
+
13
+ > **명시적 비범위:** parallel-run started-exclusion. 현재 `_resolve_effective_stage` 는 다른 run 이 `started` 한 stage 를 배제하지 않는다(두 병렬 run 이 같은 ready-set 을 잡을 수 있음 — `tests/test_e2e_multi_stage_q1_q9.py::test_q7` 가 이 기존 동작을 문서화). batch 도입이 이 gap 을 악화시키지 않으며, 사용자의 실제 사용(순차 phase-continuation)에는 영향 없다. started-exclusion 은 별도 작업으로 남긴다.
14
+
15
+ ---
16
+
17
+ ## 파일 구조
18
+
19
+ | 파일 | 책임 | 작업 |
20
+ |---|---|---|
21
+ | `scripts/okstra_ctl/run.py` | bundle 준비, stage 선택, consumers started row, ctx 주입 | 수정 |
22
+ | `tests/test_resolve_effective_stages.py` | 순수 선택 함수 유닛 커버리지 | 신규 |
23
+ | `tests/test_auto_stage_selection.py` | 통합 선택 동작 | 수정(batch 의미) |
24
+ | `tests/test_e2e_multi_stage_q1_q9.py` | e2e Q1–Q9 | 수정(batch 의미) |
25
+ | `prompts/launch.template.md` | lead 런치 프롬프트(jinja) | 수정(`{{EFFECTIVE_STAGES}}` 주입) |
26
+ | `prompts/profiles/_implementation-executor.md` | executor 역할 계약 | 수정(batch 소비, one-PR-per-run) |
27
+ | `prompts/profiles/_implementation-deliverable.md` | Phase 6 산출 계약 | 수정(stage 별 done row, report 1회) |
28
+ | `prompts/profiles/implementation.md` | implementation 프로파일 | 수정(run 단위 검증 명시) |
29
+ | `docs/superpowers/specs/2026-06-04-stage-splitting-cost-aware-design.md` | 신규 설계 스펙 | 수정(구현 결과 반영: started-exclusion 비범위 명시) |
30
+
31
+ ---
32
+
33
+ ## Task 1: `_resolve_effective_stages` 순수 함수 (TDD)
34
+
35
+ **Files:**
36
+ - Create: `tests/test_resolve_effective_stages.py`
37
+ - Modify: `scripts/okstra_ctl/run.py`
38
+
39
+ `_resolve_effective_stage`(단일 int) 를 `_resolve_effective_stages`(list) 로 교체. ready = `depends-on` 이 모두 done 이고 자신은 not-done. batch = ready 를 stage 번호 순으로 누적 step_count ≤ 예산까지 (단 최소 1개 보장). numeric 요청은 단일 stage 리스트.
40
+
41
+ - [ ] **Step 1: 실패 테스트 작성**
42
+
43
+ Create `tests/test_resolve_effective_stages.py`:
44
+
45
+ ```python
46
+ """Unit coverage for run._resolve_effective_stages (ready-set batch selection)."""
47
+ import importlib.util
48
+ from pathlib import Path
49
+
50
+ REPO = Path(__file__).resolve().parents[1]
51
+ spec = importlib.util.spec_from_file_location(
52
+ "okstra_run", REPO / "scripts" / "okstra_ctl" / "run.py"
53
+ )
54
+ run = importlib.util.module_from_spec(spec)
55
+ spec.loader.exec_module(run)
56
+
57
+
58
+ def _stage(n, deps, steps):
59
+ return {"stage_number": n, "depends_on": deps, "step_count": steps}
60
+
61
+
62
+ def test_auto_batches_independent_ready_stages_within_budget():
63
+ stages = [_stage(1, [], 2), _stage(2, [], 3), _stage(3, [1, 2], 2)]
64
+ # stages 1+2 are ready (deps empty), 2+3=5 ≤ 8 → batch [1, 2]; 3 not ready.
65
+ assert run._resolve_effective_stages(stages, set(), "auto", budget=8) == [1, 2]
66
+
67
+
68
+ def test_auto_stops_at_budget():
69
+ stages = [_stage(1, [], 5), _stage(2, [], 5)]
70
+ # 5 + 5 = 10 > 8 → only stage 1 fits after the first.
71
+ assert run._resolve_effective_stages(stages, set(), "auto", budget=8) == [1]
72
+
73
+
74
+ def test_auto_guarantees_at_least_one_even_over_budget():
75
+ stages = [_stage(1, [], 6)]
76
+ # single ready stage at the cap; budget never drops it.
77
+ assert run._resolve_effective_stages(stages, set(), "auto", budget=8) == [1]
78
+
79
+
80
+ def test_auto_skips_done_and_unready():
81
+ stages = [_stage(1, [], 2), _stage(2, [1], 2), _stage(3, [1], 2)]
82
+ # stage 1 done → 2 and 3 become ready (deps satisfied), 2+2=4 ≤ 8 → [2, 3].
83
+ assert run._resolve_effective_stages(stages, {1}, "auto", budget=8) == [2, 3]
84
+
85
+
86
+ def test_auto_raises_when_nothing_ready():
87
+ stages = [_stage(1, [], 2), _stage(2, [1], 2)]
88
+ # stage 1 not done and stage 2 depends on it → with stage 1 done-set empty,
89
+ # stage 1 IS ready; to force "nothing ready" mark 1 done but 2 depends on a
90
+ # missing dep. Simulate by marking 1 done and 2 depending on un-done 3.
91
+ stages = [_stage(2, [3], 2)]
92
+ import pytest
93
+ with pytest.raises(run.PrepareError):
94
+ run._resolve_effective_stages(stages, set(), "auto", budget=8)
95
+
96
+
97
+ def test_numeric_returns_single_stage():
98
+ stages = [_stage(1, [], 2), _stage(2, [], 2)]
99
+ assert run._resolve_effective_stages(stages, set(), "2", budget=8) == [2]
100
+
101
+
102
+ def test_numeric_rejects_done_stage():
103
+ stages = [_stage(1, [], 2)]
104
+ import pytest
105
+ with pytest.raises(run.PrepareError):
106
+ run._resolve_effective_stages(stages, {1}, "1", budget=8)
107
+
108
+
109
+ def test_numeric_rejects_unknown_stage():
110
+ stages = [_stage(1, [], 2)]
111
+ import pytest
112
+ with pytest.raises(run.PrepareError):
113
+ run._resolve_effective_stages(stages, set(), "9", budget=8)
114
+ ```
115
+
116
+ - [ ] **Step 2: 실행 → 실패 확인**
117
+
118
+ Run: `python3 -m pytest tests/test_resolve_effective_stages.py -v`
119
+ Expected: collection 또는 AttributeError 실패 — `_resolve_effective_stages` 가 아직 없음.
120
+
121
+ - [ ] **Step 3: 함수 구현**
122
+
123
+ `scripts/okstra_ctl/run.py` 에서 기존 `_resolve_effective_stage`(line 211–246) 전체를 다음으로 교체:
124
+
125
+ ```python
126
+ RUN_STEP_BUDGET = 8
127
+
128
+
129
+ def _resolve_effective_stages(
130
+ stages: list,
131
+ done_stages: set,
132
+ requested: str,
133
+ budget: int = RUN_STEP_BUDGET,
134
+ ) -> list:
135
+ """Return the ordered list of stage numbers this run executes.
136
+
137
+ `requested` is "auto" or a decimal string. For "auto" the run batches all
138
+ ready stages (depends-on all done, itself not done) in stage-number order up
139
+ to `budget` effective steps — but always at least one. A numeric request is a
140
+ single forced stage. Raises PrepareError on rejection cases."""
141
+ if requested != "auto":
142
+ try:
143
+ n = int(requested)
144
+ except ValueError:
145
+ raise PrepareError(
146
+ f"--stage must be 'auto' or an integer, got {requested!r}"
147
+ )
148
+ target = next((s for s in stages if s["stage_number"] == n), None)
149
+ if target is None:
150
+ raise PrepareError(
151
+ f"--stage {n} not in Stage Map "
152
+ f"(have {[s['stage_number'] for s in stages]})"
153
+ )
154
+ if n in done_stages:
155
+ raise PrepareError(
156
+ f"--stage {n} already completed (consumers.jsonl status:done exists)"
157
+ )
158
+ return [n]
159
+
160
+ ready = [
161
+ s for s in stages
162
+ if s["stage_number"] not in done_stages
163
+ and all(d in done_stages for d in s["depends_on"])
164
+ ]
165
+ if not ready:
166
+ raise PrepareError(
167
+ "no stage is ready: every remaining stage has unsatisfied depends-on"
168
+ )
169
+ batch: list = []
170
+ total = 0
171
+ for s in ready:
172
+ sc = s.get("step_count", 0) or 0
173
+ if batch and total + sc > budget:
174
+ break
175
+ batch.append(s["stage_number"])
176
+ total += sc
177
+ return batch
178
+ ```
179
+
180
+ - [ ] **Step 4: 실행 → 통과 확인**
181
+
182
+ Run: `python3 -m pytest tests/test_resolve_effective_stages.py -v`
183
+ Expected: 8 passed.
184
+
185
+ - [ ] **Step 5: 커밋**
186
+
187
+ ```bash
188
+ git add scripts/okstra_ctl/run.py tests/test_resolve_effective_stages.py
189
+ git commit -m "feat(okstra_ctl/run): batch ready stages up to a run step budget"
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Task 2: `prepare_task_bundle` 배선 + 통합/e2e 테스트 batch 의미 반영
195
+
196
+ **Files:**
197
+ - Modify: `scripts/okstra_ctl/run.py:843–869` (callsite)
198
+ - Modify: `tests/test_auto_stage_selection.py`
199
+ - Modify: `tests/test_e2e_multi_stage_q1_q9.py`
200
+
201
+ - [ ] **Step 1: callsite 교체**
202
+
203
+ `scripts/okstra_ctl/run.py` 의 line 851–869 블록을 다음으로 교체:
204
+
205
+ ```python
206
+ effective = _resolve_effective_stages(
207
+ ctx["parsed_stage_map"], done_stages, inp.stage
208
+ )
209
+ ctx["effective_stages"] = effective
210
+ csv = ",".join(str(n) for n in effective)
211
+ ctx["EFFECTIVE_STAGES"] = csv
212
+ ctx["STAGE_BATCH_DIRECTIVE"] = (
213
+ f"- **Stage batch for this implementation run:** `{csv}` "
214
+ "(comma-separated stage numbers, ascending). Execute exactly these "
215
+ "Stage Map stages in this order — this is the authoritative scope. "
216
+ "Do NOT recompute the start stage from `consumers.jsonl`; the runtime "
217
+ "already selected and reserved this batch."
218
+ )
219
+ inp.stage = csv
220
+ print(f"selected stages: {csv}", file=sys.stdout)
221
+ head_proc = _subprocess.run(
222
+ ["git", "rev-parse", "HEAD"],
223
+ cwd=inp.project_root, capture_output=True, text=True,
224
+ )
225
+ head_sha = head_proc.stdout.strip() if head_proc.returncode == 0 else ""
226
+ now = _dt.datetime.now(_dt.timezone.utc).isoformat()
227
+ for stage_n in effective:
228
+ append_consumer(
229
+ plan_run_root,
230
+ impl_task_key=ctx["TASK_KEY"],
231
+ stage=stage_n,
232
+ status="started",
233
+ started_at=now,
234
+ head_commit=head_sha,
235
+ )
236
+ ```
237
+
238
+ (Non-impl 경로에서 `EFFECTIVE_STAGES` 가 비도록, ctx 기본값을 보장한다. ctx 초기화부에 `ctx.setdefault("EFFECTIVE_STAGES", "")` 가 없으면, jinja 렌더 시 KeyError 방지를 위해 launch 렌더 직전 `ctx.setdefault("EFFECTIVE_STAGES", "")` 한 줄을 추가 — Step 1b.)
239
+
240
+ - [ ] **Step 1b: 비-impl 기본값 보장**
241
+
242
+ `prepare_task_bundle` 에서 ctx 가 launch 렌더에 넘어가기 전(즉 `if inp.task_type == "implementation":` 블록 밖, 그 위) 한 줄 추가:
243
+
244
+ ```python
245
+ ctx.setdefault("EFFECTIVE_STAGES", "")
246
+ ctx.setdefault("STAGE_BATCH_DIRECTIVE", "")
247
+ ```
248
+
249
+ - [ ] **Step 2: `test_auto_stage_selection.py` 갱신**
250
+
251
+ 기존 단일-stage assertion 을 batch 의미로 교체. 다음 두 곳을 수정한다.
252
+
253
+ `test_auto_picks_stage_1_when_none_done` 의
254
+ ```python
255
+ assert "selected stage: 1" in r.stdout
256
+ ```
257
+
258
+ ```python
259
+ assert "selected stages: 1" in r.stdout
260
+ ```
261
+ 로. (이 테스트의 fixture 가 stage 1·2 를 둘 다 ready 로 만들고 합산 ≤8 이면 출력이 `selected stages: 1,2` 가 된다. 픽스처를 읽어 stage 2 가 ready 인지·step 합을 확인하고, 그렇다면 `assert "selected stages: 1,2" in r.stdout` 로, stage 2 가 stage 1 에 depends-on 이면 `selected stages: 1` 로 맞춘다.)
262
+
263
+ `test_consumers_started_row_appended_on_success` 의
264
+ ```python
265
+ started_rows = [row for row in rows if row.get("status") == "started"]
266
+ assert len(started_rows) == 1
267
+ assert started_rows[0]["stage"] == 1
268
+ ```
269
+ 를, 위에서 확인한 batch 크기에 맞춰: stage 1 단독 batch 면 그대로; stage 1+2 batch 면
270
+ ```python
271
+ started_rows = [row for row in rows if row.get("status") == "started"]
272
+ assert [row["stage"] for row in started_rows] == [1, 2]
273
+ ```
274
+ 로.
275
+
276
+ `test_auto_picks_stage_2_when_stage_1_done_and_stage_2_independent` 의
277
+ ```python
278
+ assert "selected stage: 2" in r.stdout
279
+ ```
280
+ 는 stage 1 done 후 ready 집합이 {2,3...} 이 되므로, fixture 의 stage 3 depends-on 을 보고 `selected stages: 2` 또는 `selected stages: 2,3` 로 맞춘다.
281
+
282
+ > 구현자 주의: 이 파일의 fixture 정의(인라인 plan 텍스트 또는 `tests/fixtures/plans/`)를 먼저 읽고 각 stage 의 `depends-on`·`step-count` 를 확인한 뒤, 위 규칙대로 기대 batch 를 계산해 assertion 을 채운다. 추측 금지.
283
+
284
+ - [ ] **Step 3: `test_e2e_multi_stage_q1_q9.py` 갱신**
285
+
286
+ batch 의미로 깨지는 assertion (Explore 맵 기준):
287
+ - `test_q1_one_stage_plan_happy_path` (1-stage plan): batch=[1] 이므로 `"selected stage: 1"` → `"selected stages: 1"`, `len(started)==1` 유지.
288
+ - `test_q2_three_stage_first_run_picks_stage_1`: 3-stage plan 의 stage 1·2 가 depends-on(none)·합산 ≤8 이면 batch=[1,2]. `"selected stage: 1"` → 실제 batch 문자열로, `len(rows)==1` → batch 크기로, `rows[0]["stage"]==1` → `[r["stage"] for r in rows]==<batch>` 로. (fixture 확인 후 확정.)
289
+ - `test_q3_after_stage_1_done_picks_stage_2_and_loads_sidecar`: stage 1 done 후 ready 집합 기준으로 `"selected stages: ..."` 갱신.
290
+ - `test_q7_parallel_runs_pick_distinct_none_stages`: 이 테스트는 started-exclusion 부재(비범위)를 문서화한다. 두 run 이 같은 batch 를 잡는 동작은 유지되나 출력 문자열이 `selected stages:` 로 바뀐다 — `"selected stage: 1"` → `"selected stages: ..."`. `len(started)==2` 와 `all(row["stage"]==1 ...)` 는 batch 크기에 따라 갱신(두 run 이 각각 같은 batch 를 started 하므로 started row 수 = 2 × batch 크기). fixture 확인 후 확정.
291
+ - `test_q8_partial_depends_on_blocks_higher_stage`: `"selected stage: 1"` → `"selected stages: 1"` (stage 2·3 이 미충족 depends-on 으로 ready 가 아니면 batch=[1]).
292
+ - S4/S5/S6/S8 거부 테스트(q4,q5,q6,q9)는 prepare 이전 validator 단계라 영향 없음 — 변경 금지.
293
+
294
+ > 구현자 주의: 각 Q 테스트가 사용하는 plan fixture 의 stage depends-on·step-count 를 읽고 batch 를 계산해 정확한 문자열·카운트로 채운다.
295
+
296
+ - [ ] **Step 4: 실행 → 통과 확인**
297
+
298
+ Run: `python3 -m pytest tests/test_auto_stage_selection.py tests/test_e2e_multi_stage_q1_q9.py -v`
299
+ Expected: 전부 통과 (batch 의미 반영).
300
+
301
+ - [ ] **Step 5: 커밋**
302
+
303
+ ```bash
304
+ git add scripts/okstra_ctl/run.py tests/test_auto_stage_selection.py tests/test_e2e_multi_stage_q1_q9.py
305
+ git commit -m "feat(okstra_ctl/run): wire ready-set batch into bundle prep and consumers"
306
+ ```
307
+
308
+ ---
309
+
310
+ ## Task 3: 프롬프트 계약 — batch 소비 + one-PR-per-run + run 단위 검증
311
+
312
+ **Files:**
313
+ - Modify: `prompts/launch.template.md` (EFFECTIVE_STAGES 주입)
314
+ - Modify: `prompts/profiles/_implementation-executor.md:30–49`
315
+ - Modify: `prompts/profiles/_implementation-deliverable.md:51–52`
316
+ - Modify: `prompts/profiles/implementation.md`
317
+
318
+ 프로즈 변경(유닛 테스트 없음) — 검증은 빌드 동기화 + 사람 읽기 + e2e.
319
+
320
+ - [ ] **Step 1: launch 프롬프트에 batch 주입**
321
+
322
+ `launch.template.md` 는 `{% %}` 제어구문을 쓰지 않고 `{{ }}` 치환만 쓴다(확인됨). 따라서 조건 분기 없이, Python 이 만든 완성 지시문 ctx 변수를 무조건 렌더한다 — 비-impl 에서는 `STAGE_BATCH_DIRECTIVE` 가 빈 문자열이라 빈 줄만 남는다.
323
+
324
+ `prompts/launch.template.md` 의 `- This run executes \`{{WORKFLOW_CURRENT_PHASE}}\` only.` 줄(현재 line 17) 바로 아래에 추가:
325
+
326
+ ```markdown
327
+ {{STAGE_BATCH_DIRECTIVE}}
328
+ ```
329
+
330
+ - [ ] **Step 2: `_implementation-executor.md` batch 계약**
331
+
332
+ line 30–33 의 "Determine **start stage**" 블록을 batch 소비로 교체:
333
+
334
+ ```markdown
335
+ - read the **Stage batch** injected in the launch prompt (`Stage batch for this implementation run`). It lists the stage numbers this run owns, ascending. The runtime already selected and reserved them — do NOT recompute from `consumers.jsonl`.
336
+ - for each stage in the batch, load every `runs/<plan-key>/carry/stage-<i>.json` for `i ∈ depends-on(stage)` and inject them as runtime carry-in. `depends-on (none)` stages need no sidecar — task-brief only.
337
+ - the batch's stages are mutually independent (each one's depends-on are all already `status:done`, never another batch member), so execute them in ascending order; each stage's file list, step order, Stage Validation, Stage Exit Contract, and rollback path are the authoritative scope for that stage.
338
+ ```
339
+
340
+ line 41 의 헤딩
341
+ ```markdown
342
+ ## Stage execution contract (this run owns exactly one stage of the plan)
343
+ ```
344
+
345
+ ```markdown
346
+ ## Stage execution contract (this run owns the injected stage batch)
347
+ ```
348
+ 로.
349
+
350
+ line 43–44 의 sidecar/reverse-link 규칙을 stage 별 반복으로:
351
+
352
+ ```markdown
353
+ - **Sidecar evidence writer (BLOCKING, per stage).** For each stage in the batch, when that stage's Stage Validation `post` commands all succeed, the Executor MUST emit the JSON object (schema: `docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md` §3.2) and the lead MUST persist it to `runs/<impl-task-key>/carry/stage-<N>.json`. Each file MUST NOT exist before the run starts (overwrite refused).
354
+ - **Reverse link (BLOCKING, per stage).** The runtime already appended a `status:"started"` row per batch stage before this run began. On each stage's completion, append a `status:"done"` row with `carry_path` populated for that stage number.
355
+ ```
356
+
357
+ line 45–49 의 One-PR-per-stage 를 one-PR-per-run 으로:
358
+
359
+ ```markdown
360
+ - **One-PR-per-run.** This run creates exactly one PR titled `Stages <first>–<last>: <run summary>` (or `Stage <N>: <title>` when the batch is a single stage). The PR body MUST include one `## Stage <N>` section per batched stage (title, files, validation result), and `## Previous run` / `## Next run` links so a reviewer can navigate the run chain.
361
+ ```
362
+
363
+ - [ ] **Step 3: `_implementation-deliverable.md` done row + report 1회**
364
+
365
+ line 51–52 의 sidecar/done-row 문구를 batch 인지로:
366
+
367
+ ```markdown
368
+ - For EACH stage in this run's batch: write the carry JSON verbatim to `runs/<impl-key>/carry/stage-<N>.json` (refuse to overwrite an existing file), then append a `status:"done"` row to `runs/<plan-task-key>/consumers.jsonl` with `completed_at`, `carry_path`, and the HEAD SHA for that stage.
369
+ - The verifier round, Phase 5.5 convergence, and this Phase 6 report run **once per run** over the batch's combined diff — NOT per stage. The single final report covers every batched stage, with a per-stage subsection.
370
+ ```
371
+
372
+ - [ ] **Step 4: `implementation.md` run 단위 검증 명시**
373
+
374
+ `prompts/profiles/implementation.md` 의 `- Required workers:` 블록 바로 위 또는 Purpose 줄(line 3) 아래에 한 줄 추가:
375
+
376
+ ```markdown
377
+ - **Run-level fixed cost:** the verifier set, Phase 5.5 convergence, and the Phase 6 report-writer run exactly once per run, over the combined diff of all stages in this run's batch — never once per stage.
378
+ ```
379
+
380
+ - [ ] **Step 5: 토큰 정합성 확인**
381
+
382
+ Run: `grep -rn "owns exactly one stage\|One-PR-per-stage\|EFFECTIVE_STAGES" prompts/profiles/ prompts/launch.template.md`
383
+ Expected: `owns exactly one stage` / `One-PR-per-stage` 잔재 없음, `EFFECTIVE_STAGES` 는 launch + executor 에서 의미 일치.
384
+
385
+ - [ ] **Step 6: 커밋**
386
+
387
+ ```bash
388
+ git add prompts/launch.template.md prompts/profiles/_implementation-executor.md prompts/profiles/_implementation-deliverable.md prompts/profiles/implementation.md
389
+ git commit -m "feat(prompts/implementation): consume injected stage batch, one PR and one verification per run"
390
+ ```
391
+
392
+ ---
393
+
394
+ ## Task 4: 신규 설계 스펙 — started-exclusion 비범위 명시
395
+
396
+ **Files:**
397
+ - Modify: `docs/superpowers/specs/2026-06-04-stage-splitting-cost-aware-design.md`
398
+
399
+ 구현 중 확정된 사실(started-exclusion 비범위, drift 일원화)을 스펙에 반영해 문서-구현 정합을 맞춘다.
400
+
401
+ - [ ] **Step 1: §4 비범위에 한 줄 추가**
402
+
403
+ `## 4. 비범위 / 향후` 섹션의 마지막 bullet 아래에 추가:
404
+
405
+ ```markdown
406
+ - **parallel-run started-exclusion 은 비범위.** 현재 ready-set 선택은 다른 run 이 `started` 한 stage 를 배제하지 않는다(두 병렬 run 이 같은 batch 를 잡을 수 있음 — 기존 동작, `tests/test_e2e_multi_stage_q1_q9.py::test_q7` 가 문서화). 사용자의 순차 phase-continuation 사용에는 영향 없으며, 진짜 충돌 backstop 은 §2.2 의 파일-서로소 불변식 + worktree 직렬성이다. started-exclusion 도입은 별도 작업.
407
+ - **stage 선택은 Python SOT.** `_resolve_effective_stages`(run.py) 가 batch 를 선택·예약하고 `{{EFFECTIVE_STAGES}}` 로 lead 에 주입한다. 기존의 "lead 가 consumers.jsonl 로 자가계산" 이중 경로(drift)는 제거됐다.
408
+ ```
409
+
410
+ - [ ] **Step 2: 커밋**
411
+
412
+ ```bash
413
+ git add docs/superpowers/specs/2026-06-04-stage-splitting-cost-aware-design.md
414
+ git commit -m "docs(specs): record started-exclusion non-goal and Python-SOT stage selection"
415
+ ```
416
+
417
+ ---
418
+
419
+ ## Task 5: 빌드 동기화 + 전체 회귀 + e2e
420
+
421
+ **Files:** (없음 — 빌드·검증만)
422
+
423
+ - [ ] **Step 1: runtime 동기화**
424
+
425
+ Run: `npm run build`
426
+ Expected: 종료 코드 0, 22/22 동기화.
427
+
428
+ - [ ] **Step 2: stage 관련 + 전체 테스트**
429
+
430
+ Run: `python3 -m pytest tests/`
431
+ Expected: 전부 통과. 특히 `test_resolve_effective_stages.py`, `test_auto_stage_selection.py`, `test_e2e_multi_stage_q1_q9.py`, `test_wizard_stage_pick.py`, `test_run_stage_arg.py` 통과.
432
+
433
+ - [ ] **Step 3: e2e 시나리오(있다면)**
434
+
435
+ Run: `ls tests-e2e/ | grep -i stage` — stage 관련 e2e 시나리오가 있으면 실행:
436
+ `bash tests-e2e/<scenario>.sh`
437
+ Expected: 종료 코드 0. (없으면 이 step 생략하고 그 사실을 기록.)
438
+
439
+ - [ ] **Step 4: 워크플로 validator**
440
+
441
+ Run: `bash validators/validate-workflow.sh`
442
+ Expected: 종료 코드 0.
443
+
444
+ - [ ] **Step 5: CLI 스모크**
445
+
446
+ Run: `node bin/okstra --version && node bin/okstra doctor`
447
+ Expected: 버전 출력 + doctor 진단 정상.
448
+
449
+ ---
450
+
451
+ ## Self-Review (작성자 체크 — 실행 전 1회)
452
+
453
+ - **Spec coverage:** 신규 스펙 §2.1(stage/run 분리)→Task 1+3, §2.3(ready-set 예산 8)→Task 1, §2.4(run 단위 검증, stage 별 sidecar)→Task 3, started-exclusion 비범위→Task 4. Plan A 의 S9·응집은 본 계획 비범위(이미 완료).
454
+ - **Placeholder scan:** Python·테스트·프롬프트 step 에 실제 코드/문자열/명령 포함. 단, Task 2 의 일부 테스트 assertion 은 fixture 의 depends-on·step-count 에 의존하므로 "구현자가 fixture 를 읽고 batch 를 계산해 채운다" 는 명시적 규칙으로 대체 — 이는 placeholder 가 아니라 데이터-의존 변환 규칙이며, 변환 규칙 자체는 완전 명세돼 있다.
455
+ - **Type consistency:** `_resolve_effective_stages` → `list[int]` 반환. callsite 는 `effective`(list), `ctx["EFFECTIVE_STAGES"]`(CSV str), `inp.stage`(CSV str), started row 루프. 프롬프트는 `{{EFFECTIVE_STAGES}}`(CSV) 소비. 함수명 `_resolve_effective_stages`(복수형) 로 통일 — 옛 단수형 `_resolve_effective_stage` 호출 잔재가 없는지 Task 2 Step 1 에서 grep 으로 확인할 것.
456
+ - **잔재 확인 권고:** Task 2 착수 전 `grep -rn "_resolve_effective_stage\b" scripts/ tests/` 로 단수형 호출처가 callsite(run.py:851) 외에 없음을 확인.
457
+ - **검증 한계(실DB/IO 규칙):** 본 변경은 DB/IO 를 직접 건드리지 않으나, e2e(`tests-e2e/`)와 실제 `implementation` run 의 lead 동작(프롬프트 계약)은 pytest 로 완전 재현되지 않는다. 프롬프트 계약 변경의 "검증" 은 빌드 동기화 + e2e 시나리오까지이며, lead 의 실제 batch 실행은 다음 실 run 에서 관측해야 최종 확인된다 — 그 전까지는 "정적·e2e 상 통과, 실 run 미관측" 으로 보고한다.
@@ -35,6 +35,8 @@ planner LLM 은 **항상** Stage Map + N 개의 Stage 섹션으로 산출한다.
35
35
 
36
36
  ### 2.3 병렬화 최대화 우선 (분할 1 급 기준)
37
37
 
38
+ > **[2026-06-04 대체됨]** 본 절의 "병렬화 최대화 우선" 은 [`2026-06-04-stage-splitting-cost-aware-design.md`](2026-06-04-stage-splitting-cost-aware-design.md) §2.2 의 "응집 기준점=파일/모듈 근접성, ≤6 cap 유일 분할기" 로 대체되었다. 병렬화는 더 이상 분할의 1급 기준이 아니다. 아래 원문은 역사 참조용. (같은 문서의 `step ≤6 cap`(§2.3 line 46–50)·carry-in(§2.4)·데이터 모델(§3) 은 유효.)
39
+
38
40
  stage·step 을 구성할 때 **종속을 최소화해 병렬 가능 단위를 최대화하는 것을 1 순위 기준**으로 삼는다. 두 분할 안이 같은 step 수를 갖는다면, `depends-on` 링크가 더 적은(=병렬 가능 stage 가 더 많은) 쪽을 채택한다.
39
41
 
40
42
  구체적 가이드:
@@ -0,0 +1,176 @@
1
+ # Phase 5.5 적대적 검증 (adversarial verification) — 설계
2
+
3
+ - 작성일: 2026-06-04
4
+ - 범위: `requirements-discovery` / `error-analysis` 두 phase 의 **Phase 5.5 convergence 재검증**을, 검증자가 다른 워커의 주장을 적극적으로 반박(refute)하려 시도하고 입증 책임을 주장 쪽에 두는 **적대적 검증** 구조로 전환한다. 별도 검증자 에이전트나 새 스테이지를 만들지 않고, 기존 convergence 재검증 루프를 phase-조건부 적대적 모드로 재구성한다.
5
+ - 비범위
6
+ - 신규 worker/agent 추가 없음. `requirements-discovery` / `error-analysis` 의 `Required workers:` 로스터 불변.
7
+ - `implementation-planning` / `implementation` / `final-verification` / `release-handoff` 의 convergence 동작 불변 — 이들은 현행 협조적(collaborative) 재검증을 그대로 유지한다.
8
+ - `implementation-planning` 의 plan-body verification(`P-*` 큐) 불변 — 본 설계는 finding 큐(`F-*`)만 다룬다.
9
+ - convergence 라운드/큐 구조 자체(Round 0 grouping, queue-pruned 루프, Round 2 gate)는 그대로 재사용한다.
10
+ - 관계: 본 문서는 [`skills/okstra-convergence/SKILL.md`](../../../skills/okstra-convergence/SKILL.md) 의 §"Verification Mode" 와 §"Lightweight Re-verification Prompt" 를 **두 phase 에 한해** 적대적 변형으로 확장한다. 협조적 모드 정의는 다른 phase 를 위해 그대로 남는다.
11
+
12
+ ## 1. 동기 — 현재 재검증은 협조적이라 거짓 합의를 만든다
13
+
14
+ 현재 Phase 5.5 의 재검증은 본질적으로 "동의 기본값" 구조다.
15
+
16
+ 1. **프롬프트가 협조적이다.** lightweight reverify 프롬프트([`skills/okstra-convergence/SKILL.md:247`](../../../skills/okstra-convergence/SKILL.md)) 는 `AGREE / DISAGREE / SUPPLEMENT` 를 묻고, "제시된 증거에 기반해 유효한가" 를 판단하게 한다. 적극적으로 깨뜨리라는 압력이 없으므로 AGREE 가 저비용 기본값이 된다.
17
+ 2. **집계가 반박자에게 입증 책임을 지운다.** 집계 규칙([`skills/okstra-convergence/SKILL.md:120`](../../../skills/okstra-convergence/SKILL.md)) 은 "다수가 AGREE → consensus" 다. 즉 주장은 다수가 적극적으로 반박해야만 강등된다. 틀린 주장이라도 아무도 적극 반박하지 않으면 `full-consensus` 로 살아남는다.
18
+ 3. **lightweight 는 텍스트만 본다.** 검증자는 원본 코드/로그를 재조사하지 않고 "제시된 증거"만 본다([`skills/okstra-convergence/SKILL.md:183`](../../../skills/okstra-convergence/SKILL.md)). 잘못된 증거 인용이 그대로 통과한다.
19
+
20
+ 특히 `requirements-discovery`(라우팅 결정)와 `error-analysis`(근본 원인 분석)는 **틀린 주장이 다음 phase 전체를 오도**하는 지점이다. 이 두 phase 에서 거짓 합의의 비용이 가장 크다. 따라서 검증의 기본 자세를 "동의" 에서 "반박 시도" 로 뒤집는다.
21
+
22
+ ## 2. 핵심 원칙
23
+
24
+ ### 2.1 phase-조건부 적대적 모드
25
+
26
+ 적대적 검증은 **`requirements-discovery` 와 `error-analysis` 두 phase 에만** 적용한다. convergence skill 은 모든 phase 가 공유하므로, 모드 분기는 manifest 의 `convergence` 블록에 새 플래그로 표현한다.
27
+
28
+ | 키 | 두 적대적 phase 기본값 | 그 외 phase 기본값 |
29
+ |---|---|---|
30
+ | `convergence.adversarial` | `true` | `false` |
31
+ | `convergence.verificationMode` | `"full-reanalysis"` | `"lightweight"` |
32
+ | `convergence.maxRounds` | req-discovery=`1`, error-analysis=`2` (현행 유지) | 현행 유지 |
33
+
34
+ 이 기본값은 [`scripts/okstra_ctl/render.py:899`](../../../scripts/okstra_ctl/render.py) `_build_convergence_block` 가 주입한다. 기존 `maxRounds` 의 phase-aware 분기(`1 if requirements-discovery else 2`) 와 동일한 패턴을 따른다. manifest 가 키를 명시하면 그 값을 우선한다(다른 phase 에서 적대적 검증을 실험적으로 켜는 것은 manifest override 로 가능 — 그러나 기본값으로 권하지 않는다).
35
+
36
+ `adversarial=false` 이면 본 설계의 모든 변경은 비활성이고 현행 협조적 동작이 그대로 돈다.
37
+
38
+ ### 2.2 적대적 재검증 프롬프트 — 반박이 임무다
39
+
40
+ `adversarial=true` 일 때 lead 는 §"Lightweight Re-verification Prompt" 대신 **적대적 프롬프트**를 사용한다. 핵심 지시:
41
+
42
+ - "너의 임무는 이 주장을 **깨뜨리는 것**이다. 인용된 원본 증거를 직접 열어 재조사하고, 주장을 무너뜨릴 반대 증거를 적극적으로 찾아라."
43
+ - verdict 라벨(프롬프트 표면):
44
+ - **REFUTED** — 주장을 반박했다. 반드시 근거를 댄다(아래 `disagreeBasis`).
45
+ - **SURVIVES** — 적극적으로 반박을 시도했으나 깨지 못했다. 주장이 공격을 견뎠다.
46
+ - **SURVIVES-WITH-CAVEAT** — 견디나 범위 한정/추가 조건/전제를 발견했다.
47
+ - **불확실성 처리(BLOCKING):** 원본 증거를 재조사한 뒤에도 주장을 **확인할 수도, 반증할 수도 없으면** 기본 verdict 는 **REFUTED** 다(`disagreeBasis = burden-not-met`). 입증 책임은 주장 쪽에 있으므로, 스스로 입증되지 않은 주장은 살아남지 못한다.
48
+
49
+ ### 2.3 verdict 매핑 — 영속 enum 불변, 신규 필드로 적대성 기록
50
+
51
+ 상태 아티팩트의 `verdict` enum 은 `{agree, disagree, supplement, verification-error}` 를 **그대로 유지**한다(contract 테스트 enum 변경 최소화). 프롬프트 라벨은 아래로 매핑해 영속한다:
52
+
53
+ | 프롬프트 라벨 | 영속 `verdict` |
54
+ |---|---|
55
+ | SURVIVES | `agree` |
56
+ | SURVIVES-WITH-CAVEAT | `supplement` |
57
+ | REFUTED | `disagree` |
58
+
59
+ 적대성의 핵심 정보는 vote 에 추가하는 신규 필드 **`disagreeBasis`** 로 기록한다:
60
+
61
+ | 값 | 의미 |
62
+ |---|---|
63
+ | `counter-evidence` | 반대 증거를 `file:line`(또는 로그 라인)으로 인용한 **강한 반박**. 인용은 `votes.<worker>.explanation` 에 포함한다. |
64
+ | `burden-not-met` | 재조사했으나 확인도 반증도 못 함 → 주장이 입증 책임을 다하지 못함(= "불확실하면 기각"). |
65
+ | `null` | verdict 가 `disagree` 가 아닐 때(=agree/supplement/verification-error). |
66
+
67
+ `adversarial=true` 인데 verdict 가 `disagree` 이고 `disagreeBasis` 가 null 이면 contract 위반이다(§5 참조). 즉 적대적 모드의 모든 반박은 둘 중 하나의 근거를 반드시 가진다 — 근거 없는 "그냥 반대" 는 허용하지 않는다.
68
+
69
+ ### 2.4 적대적 집계 규칙 — 입증 책임을 주장 쪽으로
70
+
71
+ `adversarial=true` 일 때 §"Convergence Algorithm" 의 분류 로직을 다음으로 대체한다(협조적 모드 로직은 `adversarial=false` 에서 그대로). 한 finding `F` 에 대해, `verification-error` 표는 분자·분모 모두에서 제외한다(현행과 동일):
72
+
73
+ ```text
74
+ disagrees = [v for v in non-error votes if v.verdict == "disagree"]
75
+ hard_refutes = [v for v in disagrees if v.disagreeBasis == "counter-evidence"]
76
+
77
+ IF len(disagrees) == 0:
78
+ # 아무도 깨지 못함 → 주장이 공격을 견딤
79
+ F.classification = "full-consensus"
80
+ (단, supplement(=caveat)가 있으면 "partial-consensus")
81
+ ELIF len(hard_refutes) >= 1:
82
+ # 증거 기반 반박이 1건이라도 성립 → 즉시 강등 (다수결 무관)
83
+ IF 비-발견자 전원이 disagree:
84
+ F.classification = "worker-unique" # 사실상 기각
85
+ ELSE:
86
+ F.classification = "contested"
87
+ ELSE:
88
+ # disagree 는 있으나 전부 burden-not-met (강한 반박 0건)
89
+ IF 비-발견자 전원이 disagree:
90
+ F.classification = "worker-unique"
91
+ ELIF burden-not-met disagree 가 다수(비-error 표의 과반):
92
+ F.classification = "contested"
93
+ ELSE:
94
+ F.classification = "partial-consensus" # 소수의 약한 의심 — 견딘 것으로 본다
95
+ ```
96
+
97
+ 설계 의도:
98
+ - **`counter-evidence` 반박 1건 = 강등.** 사용자가 명시한 "증거 기반 반박이 1건이라도 성립하면 강등". 다수가 동의해도 누군가 반대 증거를 `file:line` 으로 제시하면 그 주장은 무조건 `contested` 이상으로 내려간다.
99
+ - **`burden-not-met` 은 다수일 때만 강등.** 한 검증자가 "잘 모르겠다" 한 것만으로 주장을 죽이지는 않되, 과반이 입증 실패를 보고하면 주장은 입증 책임을 못 다한 것으로 강등한다. 이로써 "불확실하면 기각 쪽으로 기운다" 를 구현한다.
100
+ - 반박의 두 종류를 구분 영속하므로, 최종 리포트에서 "왜 강등됐는가"(반대 증거 발견 vs 입증 실패)를 추적할 수 있다.
101
+
102
+ multi-라운드(error-analysis maxRounds=2)에서 라운드 간 carry-forward·최종 분류는 현행 규칙을 그대로 따르되, 각 라운드의 분류 판정에 위 적대적 로직을 적용한다.
103
+
104
+ ### 2.5 full-reanalysis 의 범위 한정 — 비용 폭증 방지
105
+
106
+ 선택된 `verificationMode="full-reanalysis"` 는 검증자가 원본 증거를 직접 재조사하게 한다. 그러나 [`skills/okstra-convergence/SKILL.md:245`](../../../skills/okstra-convergence/SKILL.md) 는 lightweight 를 "requirements-discovery·error-analysis 에서 가장 큰 회피 가능 비용" 이라 명시한다. 전체 instruction-set 재독으로 회귀하면 이 비용을 정면으로 되살린다.
107
+
108
+ **해소:** 적대적 full-reanalysis 의 재조사 범위를 **"해당 finding 이 인용한 증거 파일 + 그 인접부"로 한정**한다. 전체 task brief / instruction-set / `final-report-template.md` 재독은 금지한다. 즉 검증자는 공격 대상 주장이 가리키는 코드/로그만 직접 열어 반대 증거를 찾는다.
109
+
110
+ - §"Reverify prompt: required-reading suppression (BLOCKING)" 의 full-reanalysis 분기를 적대적 모드용으로 좁힌다: analysis-worker 파일 목록 전체가 아니라 **인용된 증거 경로만** 주입한다.
111
+ - maxRounds 는 현행 유지(req-discovery=1, error-analysis=2). 적대적 1라운드면 "한 번 깨뜨려 보기" 에 충분하고, 비용을 라운드 수로 곱하지 않는다.
112
+
113
+ ## 3. 데이터 모델
114
+
115
+ ### 3.1 convergence 상태 아티팩트 (`runs/<task-type>/state/convergence-<task-type>-<seq>.json`)
116
+
117
+ - `schemaVersion` 을 `"1.2"` 로 올린다. reader 는 `"1.0"`/`"1.1"` 을 계속 수용하고 누락 필드는 `null` 로 취급한다.
118
+ - `config` 에 신규 키 추가:
119
+ - `adversarial`: boolean. 이 run 이 적대적 모드였는지. 현행 두 적대적 phase 는 `true`.
120
+ - `findings[].rounds[].votes.<worker>` 에 신규 키 추가:
121
+ - `disagreeBasis`: enum `counter-evidence | burden-not-met | null`. §2.3 의 규칙을 따른다.
122
+ - 기존 필드(`verdict` enum, `classification` enum, `finalState` 등)는 불변.
123
+
124
+ ### 3.2 render.py 가 주입하는 manifest `convergence` 블록
125
+
126
+ `_build_convergence_block`([`scripts/okstra_ctl/render.py:899`](../../../scripts/okstra_ctl/render.py)) 가 다음을 추가로 결정한다:
127
+
128
+ ```python
129
+ adversarial_phases = {"requirements-discovery", "error-analysis"}
130
+ is_adversarial = task_type in adversarial_phases
131
+ # ...
132
+ "adversarial": is_adversarial,
133
+ "verificationMode": "full-reanalysis" if is_adversarial else "lightweight",
134
+ ```
135
+
136
+ `maxRounds` 의 기존 분기는 그대로 둔다.
137
+
138
+ ## 4. 변경 대상 파일 (모두 source — `runtime/` 직접 수정 없음)
139
+
140
+ 1. [`skills/okstra-convergence/SKILL.md`](../../../skills/okstra-convergence/SKILL.md)
141
+ - §"Configuration" 표에 `adversarial` 키 추가, 두 phase 기본값 명시.
142
+ - §"Verification Mode" 에 적대적 모드 설명 추가(범위 한정 full-reanalysis 포함).
143
+ - §"Convergence Algorithm" 에 `adversarial=true` 분기 집계 로직(§2.4) 추가. 협조적 로직은 그대로 유지.
144
+ - §"Lightweight Re-verification Prompt" 옆에 "Adversarial Re-verification Prompt"(§2.2) 신설.
145
+ - §"Reverify prompt: required-reading suppression" 의 full-reanalysis 분기를 적대적 모드용 인용-증거-한정으로 좁힘.
146
+ - §"Convergence State Artifact" 스키마를 1.2 로 갱신: `config.adversarial`, `votes.<worker>.disagreeBasis`.
147
+ 2. [`scripts/okstra_ctl/render.py:899`](../../../scripts/okstra_ctl/render.py) `_build_convergence_block` — §3.2.
148
+ 3. [`prompts/profiles/requirements-discovery.md`](../../../prompts/profiles/requirements-discovery.md) + [`prompts/profiles/error-analysis.md`](../../../prompts/profiles/error-analysis.md) — Phase 5.5 가 적대적으로 돈다는 선언 1줄(프로필이 동작의 authoritative 선언처임).
149
+ 4. [`prompts/profiles/_common-contract.md:16`](../../../prompts/profiles/_common-contract.md) — "Worker interaction model" 의 Phase 5.5 설명에, 두 phase 는 적대적 peer review 라는 한 줄 추가.
150
+ 5. [`tests/test_convergence_state_contract.py`](../../../tests/test_convergence_state_contract.py) + `tests/fixtures/convergence/` — `1.2` 수용, `disagreeBasis` enum 검증, `config.adversarial` 존재 검증, 적대적 fixture 1개 추가(`counter-evidence` 반박 1건 → `contested` 케이스).
151
+ 6. [`CHANGES.md`](../../../CHANGES.md) — `사용자 영향:` 항목.
152
+
153
+ ## 5. Enforcement — 선언과 강제의 구분
154
+
155
+ 정직한 enforcement 경계:
156
+
157
+ - **적대적 *행동* 자체(lead 가 실제로 반박을 시도했는지, 검증자가 증거를 재조사했는지)는 런타임으로 강제할 수 없다.** lead 와 워커는 LLM 이므로, 적대성은 skill/프롬프트의 선언과 지시로만 유도된다. 이 한계를 문서에 명시한다.
158
+ - **강제되는 것은 아티팩트의 *형태* 뿐이다.** `tests/test_convergence_state_contract.py` 가 fixture 에 대해 검증:
159
+ - `config.adversarial` 가 boolean 으로 존재.
160
+ - `disagreeBasis` 가 enum `{counter-evidence, burden-not-met, null}` 안에 있음.
161
+ - `adversarial==true` 인 fixture 에서, verdict 가 `disagree` 이면 `disagreeBasis != null`.
162
+ - convergence 상태는 런타임 `validators/validate-run.py` 가 검사하지 않는다(현행과 동일). 따라서 본 설계는 런타임 run 에 대한 적대성 강제를 **약속하지 않는다** — fixture contract 테스트가 유일한 자동 검증 지점이다.
163
+
164
+ ## 6. 비용·리스크
165
+
166
+ - **비용:** full-reanalysis 로의 전환은 lightweight 대비 라운드당 비용을 올린다. §2.5 의 인용-증거-한정으로 폭증을 막고, maxRounds 를 현행 유지(req-discovery=1)해 라운드 곱을 억제한다.
167
+ - **리스크 — 거짓 강등(false negative):** 적대적 모드는 참인 주장을 `contested` 로 강등할 수 있다(검증자가 잘못된 반대 증거를 제시). 완화: `counter-evidence` 반박은 반드시 `file:line` 인용을 요구하므로(§2.3), 강등 사유가 리포트에 기록되어 사용자가 추적·반박할 수 있다. `contested` 는 기각이 아니라 "다툼 있음" 분류이므로 finding 은 리포트에 남는다.
168
+ - **리스크 — burden-not-met 남용:** 검증자가 게으르게 "잘 모르겠다" 로 일관하면 다수 burden-not-met 으로 멀쩡한 주장이 강등될 수 있다. 완화: 프롬프트가 "재조사 후" 에만 burden-not-met 을 허용하도록 지시하고, 단일 burden-not-met 은 강등시키지 않는다(과반 필요, §2.4).
169
+
170
+ ## 7. 수용 기준
171
+
172
+ 1. `requirements-discovery` / `error-analysis` 의 manifest `convergence` 블록에 `adversarial: true`, `verificationMode: "full-reanalysis"` 가 주입된다. 그 외 phase 는 `adversarial: false`, `lightweight` 유지.
173
+ 2. convergence skill 이 `adversarial=true` 분기에서 적대적 프롬프트·적대적 집계·인용-증거-한정 재조사를 정의한다. `adversarial=false` 동작은 byte 단위로 현행과 동일.
174
+ 3. 상태 스키마 1.2 가 `config.adversarial` 와 `votes.<worker>.disagreeBasis` 를 문서화하고, contract 테스트가 §5 의 형태 규칙을 강제한다.
175
+ 4. `python3 -m pytest tests/` 와 `bash validators/validate-workflow.sh` 통과.
176
+ 5. 두 프로필과 `_common-contract.md` 가 적대적 Phase 5.5 를 선언한다.