okstra 0.48.0 → 0.50.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.
- package/docs/kr/architecture.md +8 -8
- package/docs/kr/cli.md +2 -2
- package/docs/project-structure-overview.md +3 -3
- package/docs/superpowers/plans/2026-06-05-compact-markdown-report-tables.md +323 -0
- package/docs/superpowers/plans/2026-06-05-wizard-batch-prompts.md +559 -0
- package/docs/superpowers/specs/2026-06-05-compact-markdown-report-tables-design.md +87 -0
- package/docs/superpowers/specs/2026-06-05-wizard-batch-prompts-design.md +121 -0
- package/docs/task-process/error-analysis.md +1 -1
- package/docs/task-process/final-verification.md +1 -1
- package/docs/task-process/release-handoff.md +1 -1
- package/docs/task-process/requirements-discovery.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +3 -3
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +1 -1
- package/runtime/agents/workers/gemini-worker.md +1 -1
- package/runtime/agents/workers/report-writer-worker.md +3 -3
- package/runtime/bin/lib/okstra/tmux-pane.sh +40 -0
- package/runtime/bin/okstra-codex-exec.sh +17 -21
- package/runtime/bin/okstra-gemini-exec.sh +12 -15
- package/runtime/bin/okstra-render-report-views.py +1 -1
- package/runtime/bin/okstra-trace-cleanup.sh +13 -1
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +15 -15
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +2 -2
- package/runtime/prompts/profiles/implementation-planning.md +9 -9
- package/runtime/prompts/profiles/improvement-discovery.md +5 -5
- package/runtime/prompts/profiles/release-handoff.md +2 -2
- package/runtime/prompts/profiles/requirements-discovery.md +2 -2
- package/runtime/python/okstra_ctl/clarification_items.py +11 -11
- package/runtime/python/okstra_ctl/render.py +1 -1
- package/runtime/python/okstra_ctl/render_final_report.py +1 -1
- package/runtime/python/okstra_ctl/report_views.py +26 -39
- package/runtime/python/okstra_ctl/run.py +3 -3
- package/runtime/python/okstra_ctl/wizard.py +90 -3
- package/runtime/python/okstra_ctl/workflow.py +1 -1
- package/runtime/skills/okstra-brief/SKILL.md +1 -1
- package/runtime/skills/okstra-convergence/SKILL.md +8 -8
- package/runtime/skills/okstra-report-writer/SKILL.md +22 -22
- package/runtime/skills/okstra-run/SKILL.md +2 -0
- package/runtime/skills/okstra-team-contract/SKILL.md +1 -1
- package/runtime/templates/project-docs/task-index.template.md +1 -8
- package/runtime/templates/reports/final-report.template.md +194 -198
- package/runtime/templates/reports/i18n/en.json +16 -17
- package/runtime/templates/reports/i18n/ko.json +16 -17
- package/runtime/templates/reports/implementation-planning-input.template.md +1 -1
- package/runtime/templates/reports/release-handoff-input.template.md +1 -1
- package/runtime/templates/reports/schedule.template.md +3 -7
- package/runtime/templates/reports/user-response.template.md +1 -1
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/lib/fixtures.sh +2 -2
- package/runtime/validators/validate-implementation-plan-stages.py +9 -9
- package/runtime/validators/validate-report-views.py +10 -10
- package/runtime/validators/validate-run.py +36 -36
- package/runtime/validators/validate_improvement_report.py +8 -8
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
# okstra wizard 멀티탭 배치 프롬프트 구현 계획
|
|
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:** customize 분기에서 서로 의존 없는 픽 step들을 멀티탭 `AskUserQuestion` 한 번으로 묶어 wizard 입력 왕복 수를 줄인다.
|
|
6
|
+
|
|
7
|
+
**Architecture:** 기존 `Step` 레지스트리는 그대로 두고 "방출 계층"에만 그룹 개념을 추가한다. `next_prompt`가 첫 적용 가능한 미답변 step이 그룹 멤버이면 같은 그룹의 적용가능·미답변 픽 멤버를 최대 4개까지 모아 새 `kind="pick_group"` 프롬프트로 내보내고, `submit`은 JSON `--answer`를 각 멤버 `submit()`으로 라우팅한다. `answered`/`owns`/edit-rewind/`_ready_for_confirm`은 전부 개별 step id 단위로 유지된다.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Python 3 (`scripts/okstra_ctl/wizard.py`), pytest (`tests/`), okstra-run 스킬 마크다운.
|
|
10
|
+
|
|
11
|
+
설계 문서: [docs/superpowers/specs/2026-06-05-wizard-batch-prompts-design.md](../specs/2026-06-05-wizard-batch-prompts-design.md)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## File Structure
|
|
16
|
+
|
|
17
|
+
- Modify: `scripts/okstra_ctl/wizard.py`
|
|
18
|
+
- `Prompt` 데이터클래스 — `questions` 필드 + `to_json` 확장 ([wizard.py:299](../../../scripts/okstra_ctl/wizard.py))
|
|
19
|
+
- 그룹 상수/정의 — S_* 상수 블록 직후 ([wizard.py:206](../../../scripts/okstra_ctl/wizard.py))
|
|
20
|
+
- `_build_group_prompt` 신규 + `next_prompt` 수정 ([wizard.py:2221](../../../scripts/okstra_ctl/wizard.py))
|
|
21
|
+
- `_submit_group` 신규 + `submit` 수정 ([wizard.py:2232](../../../scripts/okstra_ctl/wizard.py))
|
|
22
|
+
- Modify: `skills/okstra-run/SKILL.md` — `pick_group` 렌더 규칙 ([SKILL.md:41](../../../skills/okstra-run/SKILL.md))
|
|
23
|
+
- Modify: `tests/test_okstra_ctl_wizard.py` — 그룹 흐름 반영 (`test_error_analysis_full_customize` 등)
|
|
24
|
+
- Create: `tests/test_wizard_pick_group.py` — 그룹 방출/제출 단위 테스트
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Task 1: `pick_group` 데이터 모델
|
|
29
|
+
|
|
30
|
+
**Files:**
|
|
31
|
+
- Modify: `scripts/okstra_ctl/wizard.py:299-318` (`Prompt`)
|
|
32
|
+
- Modify: `scripts/okstra_ctl/wizard.py:206` (그룹 상수)
|
|
33
|
+
- Test: `tests/test_wizard_pick_group.py`
|
|
34
|
+
|
|
35
|
+
- [ ] **Step 1: 실패 테스트 작성**
|
|
36
|
+
|
|
37
|
+
`tests/test_wizard_pick_group.py` 생성:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
"""pick_group 방출/제출 단위 테스트."""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import json
|
|
44
|
+
import sys
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
LIB_DIR = Path(__file__).resolve().parent.parent / "scripts"
|
|
48
|
+
sys.path.insert(0, str(LIB_DIR))
|
|
49
|
+
|
|
50
|
+
from okstra_ctl.wizard import ( # noqa: E402
|
|
51
|
+
GROUP_MAX_TABS,
|
|
52
|
+
GROUP_MODELS,
|
|
53
|
+
PROMPT_GROUPS,
|
|
54
|
+
Option,
|
|
55
|
+
Prompt,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_pick_group_to_json_carries_questions():
|
|
60
|
+
members = [
|
|
61
|
+
Prompt(step="lead_model", kind="pick", label="Lead",
|
|
62
|
+
options=[Option("default", "default")]),
|
|
63
|
+
Prompt(step="claude_model", kind="pick", label="Claude",
|
|
64
|
+
options=[Option("sonnet", "sonnet")]),
|
|
65
|
+
]
|
|
66
|
+
p = Prompt(step=GROUP_MODELS, kind="pick_group",
|
|
67
|
+
label="모델", questions=members)
|
|
68
|
+
out = p.to_json()
|
|
69
|
+
assert out["kind"] == "pick_group"
|
|
70
|
+
assert [q["step"] for q in out["questions"]] == ["lead_model", "claude_model"]
|
|
71
|
+
assert out["questions"][0]["options"][0]["value"] == "default"
|
|
72
|
+
assert out["questions"][1]["multi"] is False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_group_definitions_cover_model_and_option_picks():
|
|
76
|
+
assert GROUP_MAX_TABS == 4
|
|
77
|
+
assert "lead_model" in PROMPT_GROUPS[GROUP_MODELS]
|
|
78
|
+
assert "report_writer_model" in PROMPT_GROUPS[GROUP_MODELS]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- [ ] **Step 2: 실패 확인**
|
|
82
|
+
|
|
83
|
+
Run: `python3 -m pytest tests/test_wizard_pick_group.py -v`
|
|
84
|
+
Expected: FAIL — `ImportError: cannot import name 'GROUP_MODELS'` / `Prompt` has no `questions`.
|
|
85
|
+
|
|
86
|
+
- [ ] **Step 3: 구현 — `Prompt.questions` + 그룹 상수**
|
|
87
|
+
|
|
88
|
+
`scripts/okstra_ctl/wizard.py` `Prompt` 데이터클래스 ([:299](../../../scripts/okstra_ctl/wizard.py)) 에 필드 추가 (`multi` 줄 바로 아래):
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
multi: bool = False # only meaningful when kind == "pick"
|
|
92
|
+
# only meaningful when kind == "pick_group": one entry per AskUserQuestion tab
|
|
93
|
+
questions: list["Prompt"] = field(default_factory=list)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`to_json` ([:309](../../../scripts/okstra_ctl/wizard.py)) 를 교체:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
def to_json(self) -> dict[str, Any]:
|
|
100
|
+
out = {
|
|
101
|
+
"step": self.step,
|
|
102
|
+
"kind": self.kind,
|
|
103
|
+
"label": self.label,
|
|
104
|
+
"options": [asdict(o) for o in self.options],
|
|
105
|
+
"help": self.help,
|
|
106
|
+
"echoTemplate": self.echo_template,
|
|
107
|
+
"multi": self.multi,
|
|
108
|
+
}
|
|
109
|
+
if self.kind == "pick_group":
|
|
110
|
+
out["questions"] = [
|
|
111
|
+
{"step": q.step, "label": q.label,
|
|
112
|
+
"options": [asdict(o) for o in q.options],
|
|
113
|
+
"multi": q.multi}
|
|
114
|
+
for q in self.questions
|
|
115
|
+
]
|
|
116
|
+
return out
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
S_* 상수 블록 직후 (`S_DONE = "done"` 다음, [:206](../../../scripts/okstra_ctl/wizard.py)) 에 그룹 정의 추가:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
# ---- 멀티탭 배치 프롬프트 그룹 (방출 계층 전용) ----
|
|
123
|
+
# 그룹 id 는 S_* 가 아니므로 prompts JSON SOT / step-id 동기화 검사 대상이 아니다.
|
|
124
|
+
GROUP_MODELS = "models"
|
|
125
|
+
GROUP_OPTIONS = "options"
|
|
126
|
+
GROUP_MAX_TABS = 4 # AskUserQuestion 의 질문(탭) 수 한도
|
|
127
|
+
|
|
128
|
+
# 멤버는 모두 서로 의존이 없는 단일선택 픽 step 이어야 한다.
|
|
129
|
+
# *_TEXT 후속 / workers_override / pr_template_scope 는 의존성 때문에 개별 유지.
|
|
130
|
+
PROMPT_GROUPS: dict[str, tuple[str, ...]] = {
|
|
131
|
+
GROUP_MODELS: (S_LEAD_MODEL, S_EXECUTOR_MODEL, S_CLAUDE_MODEL,
|
|
132
|
+
S_CODEX_MODEL, S_GEMINI_MODEL, S_REPORT_WRITER_MODEL),
|
|
133
|
+
GROUP_OPTIONS: (S_DIRECTIVE_PICK, S_RELATED_TASKS_PICK,
|
|
134
|
+
S_CLARIFICATION_PICK, S_PR_TEMPLATE_PICK),
|
|
135
|
+
}
|
|
136
|
+
GROUP_LABELS: dict[str, str] = {
|
|
137
|
+
GROUP_MODELS: "모델 선택 (탭별로 선택)",
|
|
138
|
+
GROUP_OPTIONS: "추가 옵션 (탭별로 선택)",
|
|
139
|
+
}
|
|
140
|
+
_STEP_TO_GROUP: dict[str, str] = {
|
|
141
|
+
sid: gid for gid, ids in PROMPT_GROUPS.items() for sid in ids
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- [ ] **Step 4: 통과 확인**
|
|
146
|
+
|
|
147
|
+
Run: `python3 -m pytest tests/test_wizard_pick_group.py -v`
|
|
148
|
+
Expected: PASS (2 passed)
|
|
149
|
+
|
|
150
|
+
- [ ] **Step 5: 커밋**
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
git add scripts/okstra_ctl/wizard.py tests/test_wizard_pick_group.py
|
|
154
|
+
git commit -m "feat(wizard): add pick_group prompt model and group definitions"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Task 2: `next_prompt` 그룹 방출
|
|
160
|
+
|
|
161
|
+
**Files:**
|
|
162
|
+
- Modify: `scripts/okstra_ctl/wizard.py:2221-2229` (`next_prompt`)
|
|
163
|
+
- Test: `tests/test_wizard_pick_group.py`
|
|
164
|
+
|
|
165
|
+
- [ ] **Step 1: 실패 테스트 작성**
|
|
166
|
+
|
|
167
|
+
`tests/test_wizard_pick_group.py` 에 추가:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from okstra_ctl.wizard import next_prompt # noqa: E402
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _models_state():
|
|
174
|
+
"""모델 그룹 직전까지 진행된 상태를 직접 구성한다 (비-implementation)."""
|
|
175
|
+
from okstra_ctl.wizard import WizardState
|
|
176
|
+
s = WizardState(workspace_root=".", project_root=".", project_id="p")
|
|
177
|
+
s.task_type = "error-analysis"
|
|
178
|
+
s.brief_path = "/tmp/brief.md"
|
|
179
|
+
s.base_ref = "main"
|
|
180
|
+
s.profile_workers = ["claude", "codex", "report-writer"]
|
|
181
|
+
s.profile_optional_workers = ["gemini"]
|
|
182
|
+
s.use_defaults = False
|
|
183
|
+
s.workers_override = "claude,codex,report-writer"
|
|
184
|
+
s.critic = "off"
|
|
185
|
+
# identity/critic/workers 단계를 answered 로 마킹해 모델 그룹이 첫 미답변이 되게 한다.
|
|
186
|
+
s.answered = [
|
|
187
|
+
"task_pick", "brief_path", "task_group", "task_id", "task_type",
|
|
188
|
+
"base_ref_pick", "critic_pick", "defaults_or_custom",
|
|
189
|
+
"workers_override",
|
|
190
|
+
]
|
|
191
|
+
return s
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_next_prompt_emits_models_group():
|
|
195
|
+
p = next_prompt(_models_state())
|
|
196
|
+
assert p.kind == "pick_group"
|
|
197
|
+
assert p.step == GROUP_MODELS
|
|
198
|
+
steps = [q.step for q in p.questions]
|
|
199
|
+
# 로스터에 gemini 없음 → gemini_model 제외, executor 는 비-impl 이라 제외
|
|
200
|
+
assert steps == ["lead_model", "claude_model", "codex_model",
|
|
201
|
+
"report_writer_model"]
|
|
202
|
+
assert len(steps) <= GROUP_MAX_TABS
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_models_group_caps_at_four_then_emits_remainder():
|
|
206
|
+
s = _models_state()
|
|
207
|
+
s.profile_workers = ["claude", "codex", "gemini", "report-writer"]
|
|
208
|
+
s.profile_optional_workers = []
|
|
209
|
+
s.workers_override = "claude,codex,gemini,report-writer"
|
|
210
|
+
p = next_prompt(s)
|
|
211
|
+
assert p.kind == "pick_group"
|
|
212
|
+
assert [q.step for q in p.questions] == [
|
|
213
|
+
"lead_model", "claude_model", "codex_model", "gemini_model"]
|
|
214
|
+
# 첫 4개를 answered 처리하면 5번째(report_writer)는 단일 픽으로 방출
|
|
215
|
+
s.answered += ["lead_model", "claude_model", "codex_model", "gemini_model"]
|
|
216
|
+
p2 = next_prompt(s)
|
|
217
|
+
assert p2.kind == "pick"
|
|
218
|
+
assert p2.step == "report_writer_model"
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
- [ ] **Step 2: 실패 확인**
|
|
222
|
+
|
|
223
|
+
Run: `python3 -m pytest tests/test_wizard_pick_group.py -k next_prompt -v`
|
|
224
|
+
Expected: FAIL — `next_prompt` 가 `kind == "pick"` (S_LEAD_MODEL) 를 반환.
|
|
225
|
+
|
|
226
|
+
- [ ] **Step 3: 구현**
|
|
227
|
+
|
|
228
|
+
`scripts/okstra_ctl/wizard.py` `next_prompt` ([:2221](../../../scripts/okstra_ctl/wizard.py)) 를 교체하고 헬퍼를 그 위에 추가:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
def _build_group_prompt(state: WizardState, group_id: str) -> Prompt:
|
|
232
|
+
"""그룹의 적용가능·미답변 픽 멤버를 최대 GROUP_MAX_TABS 개 모은다.
|
|
233
|
+
|
|
234
|
+
멤버가 1개뿐이면 멀티탭 UI가 불필요하므로 그 멤버의 평범한 픽을 반환한다.
|
|
235
|
+
"""
|
|
236
|
+
members: list[Prompt] = []
|
|
237
|
+
for sid in PROMPT_GROUPS[group_id]:
|
|
238
|
+
if sid in state.answered:
|
|
239
|
+
continue
|
|
240
|
+
step = STEP_BY_ID[sid]
|
|
241
|
+
if not step.applies(state):
|
|
242
|
+
continue
|
|
243
|
+
members.append(step.build(state))
|
|
244
|
+
if len(members) >= GROUP_MAX_TABS:
|
|
245
|
+
break
|
|
246
|
+
if len(members) == 1:
|
|
247
|
+
return members[0]
|
|
248
|
+
return Prompt(step=group_id, kind="pick_group",
|
|
249
|
+
label=GROUP_LABELS[group_id], questions=members)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def next_prompt(state: WizardState) -> Prompt:
|
|
253
|
+
if state.confirmed:
|
|
254
|
+
return Prompt(step=S_DONE, kind="done")
|
|
255
|
+
for step in STEPS:
|
|
256
|
+
if step.id in state.answered:
|
|
257
|
+
continue
|
|
258
|
+
if step.applies(state):
|
|
259
|
+
group_id = _STEP_TO_GROUP.get(step.id)
|
|
260
|
+
if group_id is not None:
|
|
261
|
+
return _build_group_prompt(state, group_id)
|
|
262
|
+
return step.build(state)
|
|
263
|
+
return Prompt(step=S_DONE, kind="done")
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
- [ ] **Step 4: 통과 확인**
|
|
267
|
+
|
|
268
|
+
Run: `python3 -m pytest tests/test_wizard_pick_group.py -k next_prompt -v`
|
|
269
|
+
Expected: PASS (2 passed)
|
|
270
|
+
|
|
271
|
+
- [ ] **Step 5: 커밋**
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
git add scripts/okstra_ctl/wizard.py tests/test_wizard_pick_group.py
|
|
275
|
+
git commit -m "feat(wizard): emit pick_group in next_prompt with 4-tab cap"
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Task 3: `submit` 그룹 라우팅
|
|
281
|
+
|
|
282
|
+
**Files:**
|
|
283
|
+
- Modify: `scripts/okstra_ctl/wizard.py:2232-2246` (`submit`)
|
|
284
|
+
- Test: `tests/test_wizard_pick_group.py`
|
|
285
|
+
|
|
286
|
+
- [ ] **Step 1: 실패 테스트 작성**
|
|
287
|
+
|
|
288
|
+
`tests/test_wizard_pick_group.py` 에 추가:
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
import pytest # noqa: E402
|
|
292
|
+
|
|
293
|
+
from okstra_ctl.wizard import WizardError, submit # noqa: E402
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_submit_group_routes_json_to_members():
|
|
297
|
+
s = _models_state()
|
|
298
|
+
answer = json.dumps({
|
|
299
|
+
"lead_model": "default",
|
|
300
|
+
"claude_model": "sonnet",
|
|
301
|
+
"codex_model": "gpt-5.5",
|
|
302
|
+
"report_writer_model": "default",
|
|
303
|
+
})
|
|
304
|
+
result = submit(s, answer)
|
|
305
|
+
assert s.claude_model == "sonnet"
|
|
306
|
+
assert s.codex_model == "gpt-5.5"
|
|
307
|
+
assert s.lead_model == "" # default → 빈 문자열
|
|
308
|
+
assert s.report_writer_model == ""
|
|
309
|
+
# 모든 멤버가 개별적으로 answered 처리됨
|
|
310
|
+
for sid in ("lead_model", "claude_model", "codex_model",
|
|
311
|
+
"report_writer_model"):
|
|
312
|
+
assert sid in s.answered
|
|
313
|
+
assert result["next"]["step"] != GROUP_MODELS
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def test_submit_group_missing_key_defaults():
|
|
317
|
+
s = _models_state()
|
|
318
|
+
result = submit(s, json.dumps({"claude_model": "sonnet"}))
|
|
319
|
+
assert s.claude_model == "sonnet"
|
|
320
|
+
assert s.lead_model == ""
|
|
321
|
+
assert "lead_model" in s.answered
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def test_submit_group_invalid_json_raises():
|
|
325
|
+
s = _models_state()
|
|
326
|
+
with pytest.raises(WizardError):
|
|
327
|
+
submit(s, "not-json")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def test_submit_group_invalid_model_raises_and_no_marking():
|
|
331
|
+
s = _models_state()
|
|
332
|
+
with pytest.raises(WizardError):
|
|
333
|
+
submit(s, json.dumps({"claude_model": "totally-unknown-model"}))
|
|
334
|
+
# 검증 실패 → 어떤 멤버도 answered 로 마킹되지 않음(전부 재-프롬프트)
|
|
335
|
+
assert "claude_model" not in s.answered
|
|
336
|
+
assert "lead_model" not in s.answered
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
- [ ] **Step 2: 실패 확인**
|
|
340
|
+
|
|
341
|
+
Run: `python3 -m pytest tests/test_wizard_pick_group.py -k submit -v`
|
|
342
|
+
Expected: FAIL — `submit` 가 group step을 `STEP_BY_ID[prompt.step]` 에서 찾다 `KeyError`.
|
|
343
|
+
|
|
344
|
+
- [ ] **Step 3: 구현**
|
|
345
|
+
|
|
346
|
+
`scripts/okstra_ctl/wizard.py` `submit` ([:2232](../../../scripts/okstra_ctl/wizard.py)) 를 교체하고 헬퍼를 그 위에 추가:
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
def _submit_group(state: WizardState, prompt: Prompt, value: str) -> dict[str, Any]:
|
|
350
|
+
"""pick_group 답(JSON 객체)을 각 멤버 submit() 으로 라우팅한다.
|
|
351
|
+
|
|
352
|
+
전부 검증 통과한 뒤에만 멤버들을 answered 로 마킹한다(부분 적용 금지).
|
|
353
|
+
멤버 submit 이 WizardError 를 던지면 그대로 전파되어 같은 그룹을 재-프롬프트한다.
|
|
354
|
+
"""
|
|
355
|
+
try:
|
|
356
|
+
answers = json.loads(value or "{}")
|
|
357
|
+
except json.JSONDecodeError as exc:
|
|
358
|
+
raise WizardError(f"pick_group answer must be a JSON object: {exc}")
|
|
359
|
+
if not isinstance(answers, dict):
|
|
360
|
+
raise WizardError("pick_group answer must be a JSON object")
|
|
361
|
+
echoes: list[str] = []
|
|
362
|
+
for q in prompt.questions:
|
|
363
|
+
echo = STEP_BY_ID[q.step].submit(state, str(answers.get(q.step, "") or ""))
|
|
364
|
+
if echo:
|
|
365
|
+
echoes.append(echo)
|
|
366
|
+
for q in prompt.questions:
|
|
367
|
+
if q.step not in state.answered:
|
|
368
|
+
state.answered.append(q.step)
|
|
369
|
+
nxt = next_prompt(state)
|
|
370
|
+
return {"echo": "; ".join(echoes), "next": nxt.to_json()}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def submit(state: WizardState, value: str) -> dict[str, Any]:
|
|
374
|
+
"""Validate the answer for the *currently active* step and advance.
|
|
375
|
+
|
|
376
|
+
Returns {"echo": "...", "next": <Prompt JSON>}. Raises WizardError on
|
|
377
|
+
validation failure (caller may re-prompt).
|
|
378
|
+
"""
|
|
379
|
+
prompt = next_prompt(state)
|
|
380
|
+
if prompt.kind == "done":
|
|
381
|
+
return {"echo": "", "next": prompt.to_json()}
|
|
382
|
+
if prompt.kind == "pick_group":
|
|
383
|
+
return _submit_group(state, prompt, value)
|
|
384
|
+
step = STEP_BY_ID[prompt.step]
|
|
385
|
+
echo = step.submit(state, value or "")
|
|
386
|
+
if prompt.step not in state.answered:
|
|
387
|
+
state.answered.append(prompt.step)
|
|
388
|
+
nxt = next_prompt(state)
|
|
389
|
+
return {"echo": echo or "", "next": nxt.to_json()}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
> 참고: `_validate_model` 가 WizardError 를 던지면 그 시점 이전 멤버의 state 필드는 이미
|
|
393
|
+
> 설정되어 있지만 `answered` 에는 추가되지 않는다. 재-프롬프트 시 같은 그룹이 다시 나오고
|
|
394
|
+
> 사용자가 다시 고른 값으로 덮어쓰므로 결과적으로 안전하다.
|
|
395
|
+
|
|
396
|
+
- [ ] **Step 4: 통과 확인**
|
|
397
|
+
|
|
398
|
+
Run: `python3 -m pytest tests/test_wizard_pick_group.py -v`
|
|
399
|
+
Expected: PASS (전체)
|
|
400
|
+
|
|
401
|
+
- [ ] **Step 5: 커밋**
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
git add scripts/okstra_ctl/wizard.py tests/test_wizard_pick_group.py
|
|
405
|
+
git commit -m "feat(wizard): route pick_group JSON answers to member submits"
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Task 4: 기존 customize 흐름 테스트 갱신
|
|
411
|
+
|
|
412
|
+
**Files:**
|
|
413
|
+
- Modify: `tests/test_okstra_ctl_wizard.py:389-501` (`test_error_analysis_full_customize`)
|
|
414
|
+
- Modify: `tests/test_okstra_ctl_wizard.py` (그 외 `S_LEAD_MODEL`/`directive_pick` 단일픽을 가정하는 테스트)
|
|
415
|
+
|
|
416
|
+
- [ ] **Step 1: 영향 테스트 식별**
|
|
417
|
+
|
|
418
|
+
Run: `python3 -m pytest tests/test_okstra_ctl_wizard.py -v`
|
|
419
|
+
Expected: 모델/옵션 단일픽을 가정하던 테스트가 FAIL (now `pick_group`). 실패 목록을 확인한다.
|
|
420
|
+
|
|
421
|
+
- [ ] **Step 2: `test_error_analysis_full_customize` 갱신**
|
|
422
|
+
|
|
423
|
+
`tests/test_okstra_ctl_wizard.py:444-466` (lead~report-writer 개별 블록) 을 그룹 제출로 교체:
|
|
424
|
+
|
|
425
|
+
```python
|
|
426
|
+
# 7) 모델 그룹 (lead + claude + codex + report-writer, 로스터에 gemini 없음)
|
|
427
|
+
p = next_prompt(state)
|
|
428
|
+
assert p.kind == "pick_group"
|
|
429
|
+
assert p.step == "models"
|
|
430
|
+
assert [q.step for q in p.questions] == [
|
|
431
|
+
"lead_model", "claude_model", "codex_model", "report_writer_model"]
|
|
432
|
+
submit(state, json.dumps({
|
|
433
|
+
"lead_model": "default",
|
|
434
|
+
"claude_model": "sonnet",
|
|
435
|
+
"codex_model": "gpt-5.5",
|
|
436
|
+
"report_writer_model": "default",
|
|
437
|
+
}))
|
|
438
|
+
assert state.lead_model == ""
|
|
439
|
+
assert state.claude_model == "sonnet"
|
|
440
|
+
assert state.codex_model == "gpt-5.5"
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
이어서 `tests/test_okstra_ctl_wizard.py:468-481` (directive/related/clarification 개별 블록) 을 옵션 그룹 제출로 교체:
|
|
444
|
+
|
|
445
|
+
```python
|
|
446
|
+
# 8) 옵션 그룹 (directive + related-tasks + clarification 픽)
|
|
447
|
+
p = next_prompt(state)
|
|
448
|
+
assert p.kind == "pick_group"
|
|
449
|
+
assert p.step == "options"
|
|
450
|
+
assert [q.step for q in p.questions] == [
|
|
451
|
+
"directive_pick", "related_tasks_pick", "clarification_pick"]
|
|
452
|
+
submit(state, json.dumps({
|
|
453
|
+
"directive_pick": "__skip__",
|
|
454
|
+
"related_tasks_pick": "__skip__",
|
|
455
|
+
"clarification_pick": "__skip__",
|
|
456
|
+
}))
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
파일 상단 import 에 `import json` 이 없으면 추가한다 (이미 있으면 생략).
|
|
460
|
+
|
|
461
|
+
- [ ] **Step 3: 다른 영향 테스트 갱신**
|
|
462
|
+
|
|
463
|
+
Step 1 에서 FAIL 한 나머지 테스트(예: `test_implementation_customize_has_executor_model_and_no_workers_override` [:611](../../../tests/test_okstra_ctl_wizard.py), 그 외 `assert p.step == S_LEAD_MODEL` / `== "directive_pick"` 를 직접 가정하는 테스트)를 동일 패턴으로 갱신한다. implementation 모델 그룹 멤버는 `["lead_model", "executor_model", "report_writer_model"]` 이다.
|
|
464
|
+
|
|
465
|
+
각 실패 테스트에서:
|
|
466
|
+
- `assert p.step == S_LEAD_MODEL` → 그룹 방출 (`p.kind == "pick_group"`, `p.step == "models"`) 로 변경하고, 개별 `submit(state, "...")` 호출을 단일 JSON `submit` 으로 합친다.
|
|
467
|
+
- `assert p.step == "directive_pick"` → `options` 그룹 방출로 변경.
|
|
468
|
+
|
|
469
|
+
- [ ] **Step 4: 전체 통과 확인**
|
|
470
|
+
|
|
471
|
+
Run: `python3 -m pytest tests/test_okstra_ctl_wizard.py tests/test_wizard_pick_group.py -v`
|
|
472
|
+
Expected: PASS (전체)
|
|
473
|
+
|
|
474
|
+
- [ ] **Step 5: 커밋**
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
git add tests/test_okstra_ctl_wizard.py
|
|
478
|
+
git commit -m "test(wizard): adapt customize-branch tests to grouped prompts"
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Task 5: SKILL.md `pick_group` 렌더 규칙
|
|
484
|
+
|
|
485
|
+
**Files:**
|
|
486
|
+
- Modify: `skills/okstra-run/SKILL.md:41-50`, `:96-98`
|
|
487
|
+
|
|
488
|
+
- [ ] **Step 1: "How the wizard talks to you" 섹션에 규칙 추가**
|
|
489
|
+
|
|
490
|
+
`skills/okstra-run/SKILL.md` 의 `kind` 설명 목록 ([:43](../../../skills/okstra-run/SKILL.md)) 바로 아래(`kind: "pick"` + `multi: true` 항목 다음)에 추가:
|
|
491
|
+
|
|
492
|
+
```markdown
|
|
493
|
+
- `kind: "pick_group"` → render a SINGLE `AskUserQuestion` whose `questions` array maps 1:1 to the wizard's `questions[]`. For each entry use `questions[].label`, `questions[].options[].label`, and `multiSelect: questions[].multi`. Collect the user's chosen `options[].value` per tab, build a JSON object keyed by each `questions[].step`, and submit it as a single literal `--answer '{"lead_model":"opus","claude_model":"default",...}'`. A tab the user leaves at its default still gets its `"default"`/`""` value in the JSON. Never split a `pick_group` into multiple `AskUserQuestion` calls — the wizard already capped it at 4 tabs and will emit any remainder as the next prompt.
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
- [ ] **Step 2: 프롬프트 루프 섹션에 동일 분기 추가**
|
|
497
|
+
|
|
498
|
+
`skills/okstra-run/SKILL.md` Step 3 의 Render 목록 ([:96](../../../skills/okstra-run/SKILL.md)) 에 추가:
|
|
499
|
+
|
|
500
|
+
```markdown
|
|
501
|
+
- `pick_group` → one `AskUserQuestion` with one question per `questions[]` entry (tab). Map each tab's selected `value` back by `questions[].step`, assemble a JSON object, and submit it as a single literal `--answer '<json>'`.
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
- [ ] **Step 3: 빌드 동기화**
|
|
505
|
+
|
|
506
|
+
Run: `npm run build`
|
|
507
|
+
Expected: 성공, `runtime/` 에 SKILL.md 반영.
|
|
508
|
+
|
|
509
|
+
- [ ] **Step 4: 커밋**
|
|
510
|
+
|
|
511
|
+
```bash
|
|
512
|
+
git add skills/okstra-run/SKILL.md runtime/
|
|
513
|
+
git commit -m "docs(skills/okstra-run): render pick_group as one multi-tab AskUserQuestion"
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
## Task 6: 회귀 검증
|
|
519
|
+
|
|
520
|
+
**Files:** (없음 — 검증만)
|
|
521
|
+
|
|
522
|
+
- [ ] **Step 1: 전체 단위 테스트**
|
|
523
|
+
|
|
524
|
+
Run: `python3 -m pytest tests/ -q`
|
|
525
|
+
Expected: 전체 PASS (특히 `tests/test_wizard_prompts.py` step-id 동기화/orphan 검사 — 그룹 상수는 `S_*` 가 아니므로 영향 없음).
|
|
526
|
+
|
|
527
|
+
- [ ] **Step 2: phase contract validator**
|
|
528
|
+
|
|
529
|
+
Run: `bash validators/validate-workflow.sh`
|
|
530
|
+
Expected: 통과.
|
|
531
|
+
|
|
532
|
+
- [ ] **Step 3: CLI smoke**
|
|
533
|
+
|
|
534
|
+
Run: `node bin/okstra --version`
|
|
535
|
+
Expected: 버전 출력.
|
|
536
|
+
|
|
537
|
+
- [ ] **Step 4: CHANGES.md 항목 추가**
|
|
538
|
+
|
|
539
|
+
`CHANGES.md` 상단에 사용자 영향 항목 추가:
|
|
540
|
+
|
|
541
|
+
```markdown
|
|
542
|
+
- okstra-run: customize 단계의 모델/옵션 선택을 멀티탭으로 묶어 입력 왕복 횟수를 줄임.
|
|
543
|
+
사용자 영향: lead/worker/report 모델과 directive/related-tasks/clarification 픽을 한 화면에서 선택.
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
- [ ] **Step 5: 커밋**
|
|
547
|
+
|
|
548
|
+
```bash
|
|
549
|
+
git add CHANGES.md
|
|
550
|
+
git commit -m "docs(changes): log multi-tab wizard prompts"
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
## Self-Review 메모
|
|
556
|
+
|
|
557
|
+
- **Spec coverage**: pick_group 모델(§3.1)=Task1, 그룹정의(§3.2)=Task1, next_prompt(§3.3)=Task2, 제출(§3.4)=Task3, SKILL(§3.5)=Task5, 테스트(§5)=Task3·4·6. 전 항목 매핑됨.
|
|
558
|
+
- **Type 일관성**: `GROUP_MODELS`/`GROUP_OPTIONS`/`GROUP_MAX_TABS`/`PROMPT_GROUPS`/`GROUP_LABELS`/`_STEP_TO_GROUP`/`_build_group_prompt`/`_submit_group`/`Prompt.questions` 식별자가 전 태스크에서 동일 표기.
|
|
559
|
+
- **비영향**: `use_defaults=True` 경로, identity 단계, confirm/edit-target, `render_args`, `_ready_for_confirm` 의 개별 step 검사 모두 불변.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# 정본 final-report `.md` 표를 compact 하게 (옵션 X) — 설계
|
|
2
|
+
|
|
3
|
+
- 작성일: 2026-06-05
|
|
4
|
+
- 범위: final-report 정본 `.md` 의 narrative 표(§1 Summary, §1.1 Consensus, §1.2 Differences, §3.1 Primary Evidence, §3.2 Secondary, §4 Risks, Execution Status by Agent)를 **짧은 코드 컬럼을 `<br>` 로 한 셀에 stack 한 meta 컬럼 + 긴 prose 컬럼은 별도 컬럼** 으로 재구성해, markdown 에디터에서도 핵심 본문(요약·근거·이견·위험)이 넓게 읽히도록 한다. 이는 [`templates/reports/final-report.template.md`](../../../templates/reports/final-report.template.md) 의 jinja 레이아웃만 바꾸며, `data.json` 스키마·report-writer 계약은 불변이다.
|
|
5
|
+
- 비범위
|
|
6
|
+
- **§5 Clarification Items 는 평면 8-컬럼 유지** — [`scripts/okstra_ctl/clarification_items.py`](../../../scripts/okstra_ctl/clarification_items.py) 가 `--resume-clarification` carry-in 을 위해 `|` 8-컬럼으로 파싱하고 validator 가 8-컬럼 스키마를 BLOCKING 으로 강제. §5 의 compact 는 HTML view 의 기존 grouping(이미 Expected form wide 까지 교정됨)이 담당한다.
|
|
7
|
+
- `data.json` 스키마·report-writer worker 의 출력 계약·convergence 상태 변경 없음 (같은 필드를 템플릿이 다르게 배치할 뿐).
|
|
8
|
+
- implementation-planning §4.5 deliverable 표(Stage Map / Stepwise 등)는 비대상 — validator 가 그 컬럼/헤딩을 substring 검사하므로 손대지 않는다.
|
|
9
|
+
- 관계: 직전 작업(브랜치 `fix/report-table-grouping`, 커밋 `2343e30`)은 **HTML view 에서만** §1/§3/§4 를 grouped 로 만들었다. 본 설계(X)는 §1/§3/§4 를 `.md` 자체에서 compact 하게 만들어 그 HTML-only 접근을 **대체**한다 — 해당 표들은 더 이상 별도 `Ticket ID` 컬럼이 없어 report_views 의 generic grouping 분기가 발동하지 않으므로, 그 분기를 정리한다. §5 grouping(+Expected form wide) 은 유지된다.
|
|
10
|
+
|
|
11
|
+
## 1. 동기
|
|
12
|
+
|
|
13
|
+
사용자는 final-report 를 **`.md` 파일로 markdown 에디터에서 읽는다.** markdown 표는 colspan 이 없어, `ID·Ticket ID·Source·Kind·Status` 같은 짧은 코드 컬럼이 각각 한 칸씩 차지하면 정작 긴 prose 컬럼(요약·Statement·Evidence·Disagreement·Item·Risk)이 좁아져 세로로 한 글자씩 뭉개진다(실측: §1 Summary 의 "한 줄 요약"이 1글자/줄). HTML self-contained view 는 grouping 으로 해결되지만 `.md` 를 읽는 사용자에겐 닿지 않는다. 따라서 `.md` 자체에서 짧은 컬럼을 `<br>` 로 한 셀에 모아 컬럼 수를 줄이고 prose 에 폭을 준다.
|
|
14
|
+
|
|
15
|
+
## 2. 핵심 설계
|
|
16
|
+
|
|
17
|
+
### 2.1 대상 표와 meta/wide 분해
|
|
18
|
+
|
|
19
|
+
각 표를 `[meta 컬럼] + [prose 컬럼들]` 로 재구성한다(meta = 짧은 코드 필드를 `<br>` stack):
|
|
20
|
+
|
|
21
|
+
| 표 | meta 컬럼(한 셀에 `<br>` stack) | 별도 prose 컬럼 |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| §1 Summary | ID, Ticket ID, 출처 | 한 줄 요약 |
|
|
24
|
+
| §1.1 Consensus | ID, Ticket ID, Source items | Statement, Evidence |
|
|
25
|
+
| §1.2 Differences | ID, Ticket ID, Workers(position+item) | Disagreement, Evidence |
|
|
26
|
+
| §3.1 Primary Evidence | ID, Ticket ID, Source items, Source(path:line) | Evidence |
|
|
27
|
+
| §3.2 Secondary | ID, Ticket ID | Hypothesis or supporting evidence, Source / confidence |
|
|
28
|
+
| §4 Risks | ID, Ticket ID | Item, Risk if ignored, Mitigation Owner |
|
|
29
|
+
| Execution Status | Agent, Role, Model, Status, raw/billable tokens, cost, Duration | Summary of Key Findings |
|
|
30
|
+
|
|
31
|
+
빈 상태(empty) 분기는 현행 그대로 유지(`emptyState.*`).
|
|
32
|
+
|
|
33
|
+
### 2.2 meta 셀 포맷 (`<br>` stack, i18n)
|
|
34
|
+
|
|
35
|
+
meta 셀은 headline(주 식별자) + `<br>` 로 이어지는 `라벨: 값` 들로 구성한다. 예 §1.1 한 row:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
**C-1**<br>{ticketLabel}: `DEV-9184`<br>{sourceLabel}: claude:F-001, codex:1.1
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- headline 은 ID(또는 Agent/Step)를 `**굵게**`.
|
|
42
|
+
- 라벨은 기존 i18n 키를 재사용(`columns.ticketId`, `columns.source`, …); 없으면 신규 키 추가(예 `columns.recordMeta` = meta 컬럼 헤더 "항목"/"Record").
|
|
43
|
+
- 값에 코드성인 것(ticket 등)은 기존처럼 백틱 유지.
|
|
44
|
+
- meta 컬럼 헤더는 짧은 라벨(예 "항목" / "Record") — 신규 i18n 키.
|
|
45
|
+
|
|
46
|
+
### 2.3 report_views 정합
|
|
47
|
+
|
|
48
|
+
1. **`_inline` 가 `<br>` 를 보존**: 현재 `html.escape` 가 `<br>`→`<br>` 로 깨뜨린다. escape 후 `<br>` / `<br/>` / `<br />` 를 `<br>` 로 복원(bold/code/link 복원과 동일 패턴). 이로써 HTML view 도 meta 셀이 줄바꿈으로 보인다.
|
|
49
|
+
2. **generic Ticket-ID grouping 분기 제거**: §1/§3/§4 는 이제 `Ticket ID` 단독 컬럼이 없어 그 분기가 발동하지 않는다 — commit `2343e30` 가 추가한 generic 분기(+ 관련 헬퍼·테스트)를 정리한다. Execution Status 도 `.md` 에서 merged 되므로 그 explicit 분기 제거. **§5 Clarification grouping(+Expected form wide)만 남긴다**(§5 는 `.md` 평면 유지 → HTML 에서 grouping).
|
|
50
|
+
3. **plain-table 폭 보강**: merged meta 컬럼은 좁게, prose 컬럼은 넓게 나오도록 plain 경로의 컬럼 폭 처리를 점검(필요 시 meta 컬럼에 narrow, prose 에 min-width).
|
|
51
|
+
|
|
52
|
+
### 2.4 계약 영향
|
|
53
|
+
|
|
54
|
+
- `data.json`·report-writer 계약·convergence 상태: **불변**. 템플릿이 같은 데이터를 다르게 배치.
|
|
55
|
+
- §5 파서(`clarification_items.py`)·§5 8-컬럼 validator: **불변**(§5 평면 유지).
|
|
56
|
+
- §1/§3/§4 컬럼 헤더를 substring 검사하는 validator/테스트는 없음(확인됨); 구현 단계에서 grep + 전체 테스트로 재확인.
|
|
57
|
+
- carry-in 으로 다음 run 에 들어가는 `.md` 의 §1/§3/§4 는 LLM 이 컨텍스트로 읽을 뿐 코드가 컬럼 파싱하지 않으므로 `<br>` stack 도 안전.
|
|
58
|
+
|
|
59
|
+
## 3. 변경 파일
|
|
60
|
+
|
|
61
|
+
1. [`templates/reports/final-report.template.md`](../../../templates/reports/final-report.template.md) — §1/§1.1/§1.2/§3.1/§3.2/§4 + Execution Status 표를 meta(`<br>` stack) + prose 형태로 재작성.
|
|
62
|
+
2. [`templates/reports/report.i18n.*`](../../../templates/reports/) (또는 i18n SOT) — meta 컬럼 헤더 + 필요한 라벨 키 추가(ko/en).
|
|
63
|
+
3. [`scripts/okstra_ctl/report_views.py`](../../../scripts/okstra_ctl/report_views.py) — `_inline` `<br>` 보존; generic Ticket-ID grouping 분기 + Execution Status 분기 제거(§5 grouping 유지); plain 폭 보강.
|
|
64
|
+
4. [`tests/test_report_views.py`](../../../tests/test_report_views.py) — `<br>` 보존 테스트; §1/§3/§4 가 더는 grouped 분기 안 타고 `<br>` 가 보존되는지; §5 는 여전히 grouped + Expected form wide.
|
|
65
|
+
5. (필요 시) [`tests/test_render_*`](../../../tests/) — 템플릿 렌더 결과의 표 구조 스냅샷/스모크.
|
|
66
|
+
6. [`CHANGES.md`](../../../CHANGES.md) — 사용자 영향 항목.
|
|
67
|
+
|
|
68
|
+
## 4. Enforcement / 검증
|
|
69
|
+
|
|
70
|
+
- 단위: `test_report_views.py` 로 `_inline` `<br>` 보존 + §5 grouping 유지를 잠금.
|
|
71
|
+
- 템플릿 렌더: 실제 `data.json`(또는 픽스처)로 렌더해 §1/§3/§4/Exec 표가 meta+prose 형태로 나오는지 + §5 가 평면 8-컬럼인지 확인.
|
|
72
|
+
- **실제 재렌더 검증(BLOCKING)**: 기존 사용자 리포트(또는 픽스처)를 렌더해 `.md` 와 HTML view 양쪽이 compact 하게 나오고 §5 carry-in 파서가 여전히 8-컬럼을 파싱하는지 실행 확인.
|
|
73
|
+
- `python3 -m pytest tests/` + `bash validators/validate-workflow.sh` + `npm run build` 통과.
|
|
74
|
+
|
|
75
|
+
## 5. 트레이드오프 / 리스크
|
|
76
|
+
|
|
77
|
+
- **트레이드오프:** §1/§3/§4 의 HTML view 가 grp-meta "key: value" 폴리시 대신 `<br>` stack 형태가 된다(살짝 덜 꾸며짐). 대신 `.md`↔HTML 레이아웃이 일관되고 `.md` 자체가 어떤 에디터에서도 읽기 쉽다(사용자 목표).
|
|
78
|
+
- **리스크 — 전 task-type 영향:** 템플릿이 공유라 모든 phase 의 final-report `.md` 구조가 바뀐다. 기계 파싱은 §5 뿐이라 안전하나, 구현 시 전체 테스트 + 실제 렌더로 회귀 확인.
|
|
79
|
+
- **리스크 — `<br>` 미지원 에디터:** 드물게 표 셀 `<br>` 를 literal 로 보이는 렌더러가 있을 수 있음. 주류(GitHub/Obsidian/Typora/VS Code)는 지원. 정본 가독성 목표상 수용.
|
|
80
|
+
|
|
81
|
+
## 6. 수용 기준
|
|
82
|
+
|
|
83
|
+
1. final-report `.md` 의 §1/§1.1/§1.2/§3.1/§3.2/§4 + Execution Status 가 meta(`<br>` stack) + prose 컬럼 형태로 렌더된다.
|
|
84
|
+
2. §5 Clarification 은 평면 8-컬럼 유지, carry-in 파서·validator 통과.
|
|
85
|
+
3. HTML view 가 meta 셀의 `<br>` 를 줄바꿈으로 보여준다(`_inline` 보존).
|
|
86
|
+
4. report_views 의 §1/§3/§4 generic grouping·Execution Status 분기는 제거되고 §5 grouping(+Expected form wide)은 유지.
|
|
87
|
+
5. `python3 -m pytest tests/` + validator + build 통과 + 실제 재렌더 육안 확인.
|