okstra 0.48.0 → 0.49.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,323 @@
1
+ # Compact markdown final-report tables (option X) 구현 계획
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:** 정본 final-report `.md` 의 narrative 표를 `meta(<br> stack) + prose` 형태로 재구성해 markdown 에디터에서도 핵심 본문이 넓게 읽히게 한다. §5 Clarification 은 평면 유지.
6
+
7
+ **Architecture:** 변경은 (1) jinja 템플릿 레이아웃 + i18n 라벨 1개, (2) `report_views._inline` 의 `<br>` 보존 + 이제 불필요한 grouping 분기 제거(§5 만 유지)에 한정. `data.json`·report-writer 계약 불변.
8
+
9
+ **Tech Stack:** jinja2 템플릿, Python 3 (report_views, pytest), JSON i18n. 빌드 `npm run build`.
10
+
11
+ **설계 근거:** [`docs/superpowers/specs/2026-06-05-compact-markdown-report-tables-design.md`](../specs/2026-06-05-compact-markdown-report-tables-design.md)
12
+
13
+ **범위 메모:** spec 표 목록(§1, §1.1, §1.2, §3.1, §3.2, §4, Execution Status)에 더해 **§7 Follow-up Tasks** 도 동일 안전 md-merge 대상에 포함한다(코드가 컬럼 파싱하지 않음 — §5 와 달리 안전, 일관성 확보). §5 만 평면 예외.
14
+
15
+ ---
16
+
17
+ ## 파일 구조
18
+
19
+ | 파일 | 책임 | 작업 |
20
+ |---|---|---|
21
+ | [`templates/reports/final-report.template.md`](../../../templates/reports/final-report.template.md) | §1/§1.1/§1.2/§3.1/§3.2/§4/§7/Exec 표를 meta+prose 로 | Modify |
22
+ | [`templates/reports/i18n/ko.json`](../../../templates/reports/i18n/ko.json) · [`en.json`](../../../templates/reports/i18n/en.json) | `columns.recordMeta` 키 추가 | Modify |
23
+ | [`scripts/okstra_ctl/report_views.py`](../../../scripts/okstra_ctl/report_views.py) | `_inline` `<br>` 보존; generic/Exec/§7 grouping 분기 제거(§5 유지) | Modify |
24
+ | [`tests/test_report_views.py`](../../../tests/test_report_views.py) | `<br>` 보존 + §5 유지 + §1/§3/§4 plain 테스트 | Modify |
25
+ | [`CHANGES.md`](../../../CHANGES.md) | 사용자 영향 항목 | Modify |
26
+
27
+ 작업 순서: 템플릿+i18n(.md 구조) → report_views(`<br>` 보존 + 분기 정리) → CHANGES + 전체 검증 + 실제 재렌더.
28
+
29
+ ---
30
+
31
+ ### Task 1: 템플릿 + i18n — narrative 표를 meta(`<br>`)+prose 로
32
+
33
+ **Files:**
34
+ - Modify: `templates/reports/i18n/ko.json`, `templates/reports/i18n/en.json`
35
+ - Modify: `templates/reports/final-report.template.md`
36
+
37
+ - [ ] **Step 1: i18n 에 meta 헤더 키 추가**
38
+
39
+ `templates/reports/i18n/ko.json` 의 `"columns"` 객체에 추가: `"recordMeta": "항목"`.
40
+ `templates/reports/i18n/en.json` 의 `"columns"` 객체에 추가: `"recordMeta": "Record"`.
41
+
42
+ - [ ] **Step 2: §1 Summary 표 교체**
43
+
44
+ `templates/reports/final-report.template.md` 의 §1 표 블록
45
+ ```
46
+ | ID | Ticket ID | {{ t("columns.summary") }} | {{ t("columns.source") }} |
47
+ |----|-----------|------------|----------------------------|
48
+ {% for row in summary -%}
49
+ | {{ row.id }} | `{{ row.ticketId }}` | {{ row.summary }} | {{ row.source }} |
50
+ {% endfor %}
51
+ ```
52
+ 를 다음으로 교체:
53
+ ```
54
+ | {{ t("columns.recordMeta") }} | {{ t("columns.summary") }} |
55
+ |--------|------------|
56
+ {% for row in summary -%}
57
+ | **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}`<br>{{ t("columns.source") }}: {{ row.source }} | {{ row.summary }} |
58
+ {% endfor %}
59
+ ```
60
+
61
+ - [ ] **Step 3: §1.1 Consensus 표 교체**
62
+
63
+ ```
64
+ | ID | Ticket ID | Statement | Source items (worker:item) | Evidence (path:line / log / worker report) |
65
+ |----|-----------|-----------|----------------------------|---------------------------------------------|
66
+ {% for row in crossVerification.consensus -%}
67
+ | {{ row.id }} | `{{ row.ticketId }}` | {{ row.statement }} | {{ row.sourceItems | join(', ') }} | {{ row.evidence }} |
68
+ {% endfor %}
69
+ ```
70
+ 를:
71
+ ```
72
+ | {{ t("columns.recordMeta") }} | Statement | Evidence (path:line / log / worker report) |
73
+ |--------|-----------|---------------------------------------------|
74
+ {% for row in crossVerification.consensus -%}
75
+ | **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}`<br>Source items: {{ row.sourceItems | join(', ') }} | {{ row.statement }} | {{ row.evidence }} |
76
+ {% endfor %}
77
+ ```
78
+
79
+ - [ ] **Step 4: §1.2 Differences 표 교체**
80
+
81
+ ```
82
+ | ID | Ticket ID | Disagreement | Workers (position + item) | Evidence |
83
+ |----|-----------|--------------|---------------------------|----------|
84
+ {% for row in crossVerification.differences -%}
85
+ | {{ row.id }} | `{{ row.ticketId }}` | {{ row.disagreement }} | {% for w in row.workersPosition %}{{ w.worker }}:{{ w.itemId }} ({{ w.position }}){% if not loop.last %} / {% endif %}{% endfor %} | {{ row.evidence }} |
86
+ {% endfor %}
87
+ ```
88
+ 를:
89
+ ```
90
+ | {{ t("columns.recordMeta") }} | Disagreement | Evidence |
91
+ |--------|--------------|----------|
92
+ {% for row in crossVerification.differences -%}
93
+ | **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}`<br>Workers: {% for w in row.workersPosition %}{{ w.worker }}:{{ w.itemId }} ({{ w.position }}){% if not loop.last %} / {% endif %}{% endfor %} | {{ row.disagreement }} | {{ row.evidence }} |
94
+ {% endfor %}
95
+ ```
96
+
97
+ - [ ] **Step 5: §3.1 Primary Evidence 표 교체**
98
+
99
+ ```
100
+ | ID | Ticket ID | Evidence | Source items (worker:item) | Source (path:line / log) |
101
+ |----|-----------|----------|----------------------------|---------------------------|
102
+ {% for row in evidence.primary -%}
103
+ | {{ row.id }} | `{{ row.ticketId }}` | {{ row.evidence }} | {{ row.sourceItems | join(', ') }} | {{ row.source }} |
104
+ {% endfor %}
105
+ ```
106
+ 를:
107
+ ```
108
+ | {{ t("columns.recordMeta") }} | Evidence |
109
+ |--------|----------|
110
+ {% for row in evidence.primary -%}
111
+ | **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}`<br>Source items: {{ row.sourceItems | join(', ') }}<br>Source: {{ row.source }} | {{ row.evidence }} |
112
+ {% endfor %}
113
+ ```
114
+
115
+ - [ ] **Step 6: §3.2 Secondary 표 교체**
116
+
117
+ ```
118
+ | ID | Ticket ID | Hypothesis or supporting evidence | Source / confidence |
119
+ |----|-----------|-----------------------------------|---------------------|
120
+ {% for row in evidence.secondary -%}
121
+ | {{ row.id }} | `{{ row.ticketId }}` | {{ row.hypothesis }} | {{ row.confidence }} |
122
+ {% endfor %}
123
+ ```
124
+ 를:
125
+ ```
126
+ | {{ t("columns.recordMeta") }} | Hypothesis or supporting evidence | Source / confidence |
127
+ |--------|-----------------------------------|---------------------|
128
+ {% for row in evidence.secondary -%}
129
+ | **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}` | {{ row.hypothesis }} | {{ row.confidence }} |
130
+ {% endfor %}
131
+ ```
132
+
133
+ - [ ] **Step 7: §4 Risks 표 교체**
134
+
135
+ ```
136
+ | ID | Ticket ID | Item | Risk if ignored | Mitigation Owner |
137
+ |----|-----------|------|-----------------|------------------|
138
+ {% for row in missingInformation -%}
139
+ | {{ row.id }} | `{{ row.ticketId }}` | {{ row.item }} | {{ row.risk }} | {{ row.owner }} |
140
+ ```
141
+ 의 헤더/구분/row 3줄을:
142
+ ```
143
+ | {{ t("columns.recordMeta") }} | Item | Risk if ignored | Mitigation Owner |
144
+ |--------|------|-----------------|------------------|
145
+ {% for row in missingInformation -%}
146
+ | **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}` | {{ row.item }} | {{ row.risk }} | {{ row.owner }} |
147
+ ```
148
+ (`{% endfor %}` 이후는 그대로.)
149
+
150
+ - [ ] **Step 8: Execution Status 표 교체**
151
+
152
+ ```
153
+ | Agent | Role | Model | Status | {{ t("columns.rawTokens") }} | {{ t("columns.billableTokens") }} | {{ t("columns.cost") }} | Duration | Summary of Key Findings |
154
+ |-------|------|-------|--------|-----------|-----------|------------|----------|-------------------------|
155
+ {% for row in executionStatus -%}
156
+ | {{ row.agent }} | {{ row.role }} | {{ row.model }} | {{ row.status }} | {{ row.totalTokens | format_int }}{% if row.cliTotalTokens %} (CLI: {{ row.cliTotalTokens | format_int }}){% endif %} | {{ row.billableTokens | format_int }} | {{ row.costUsd | format_usd }}{% if row.cliCostUsd %} (+ CLI {{ row.cliCostUsd | format_usd }}){% endif %} | {{ row.durationMs | format_duration_ms }} | {{ row.summary }} |
157
+ {% endfor %}
158
+ ```
159
+ 를:
160
+ ```
161
+ | {{ t("columns.recordMeta") }} | Summary of Key Findings |
162
+ |--------|-------------------------|
163
+ {% for row in executionStatus -%}
164
+ | **{{ row.agent }}**<br>Role: {{ row.role }}<br>Model: {{ row.model }}<br>Status: {{ row.status }}<br>{{ t("columns.rawTokens") }}: {{ row.totalTokens | format_int }}{% if row.cliTotalTokens %} (CLI: {{ row.cliTotalTokens | format_int }}){% endif %}<br>{{ t("columns.billableTokens") }}: {{ row.billableTokens | format_int }}<br>{{ t("columns.cost") }}: {{ row.costUsd | format_usd }}{% if row.cliCostUsd %} (+ CLI {{ row.cliCostUsd | format_usd }}){% endif %}<br>Duration: {{ row.durationMs | format_duration_ms }} | {{ row.summary }} |
165
+ {% endfor %}
166
+ ```
167
+
168
+ - [ ] **Step 9: §7 Follow-up Tasks 표 교체**
169
+
170
+ §7 표를 찾아(헤더에 `Title` / `Scope` / `Reason`), 짧은 컬럼(ID, Ticket ID, Origin, New Task ID, Suggested task-type, Priority, Auto-spawn 등)을 meta 셀에 `<br>` stack 하고 Title/Scope/Reason 을 prose 컬럼으로 둔다. 헤더를 `| {{ t("columns.recordMeta") }} | Title | Scope (files/areas) | Reason / Why deferred |` 로, row 의 meta 셀은 `**{{ row.id }}**<br>Ticket: \`{{ row.ticketId }}\`<br>Origin: {{ row.origin }}<br>New Task ID: {{ row.newTaskId }}<br>Type: {{ row.suggestedTaskType }}<br>Priority: {{ row.priority }}<br>Auto-spawn: {{ row.autoSpawn }}` 로 구성(현 row 의 필드명을 그대로 사용 — 먼저 현 §7 row 의 jinja 필드명을 읽어 정확히 매핑할 것). 빈 상태 분기 유지.
171
+
172
+ - [ ] **Step 10: §5 는 건드리지 않음 (확인)**
173
+
174
+ §5 Clarification Items 의 8-컬럼 표(`| ID | Ticket ID | Kind | Statement | Expected form | Blocks | Status | User input |`)는 **변경하지 않는다**. grep 으로 §5 표가 8-컬럼 그대로인지 확인.
175
+
176
+ - [ ] **Step 11: 렌더 스모크 + 커밋**
177
+
178
+ 기존 렌더 테스트로 회귀 확인 후, 픽스처 data.json 으로 렌더해 §1/§1.1/§1.2/§3.1/§3.2/§4/§7/Exec 가 `recordMeta` 헤더 + `<br>` 를 포함하고 §5 가 8-컬럼인지 확인:
179
+ ```bash
180
+ python3 -m pytest tests/test_render_final_report.py tests/test_template_full_render_both_langs.py -q
181
+ ```
182
+ Expected: PASS (템플릿 문법 OK). 실패 시 jinja 문법/필드명 교정.
183
+
184
+ ```bash
185
+ git add templates/reports/final-report.template.md templates/reports/i18n/ko.json templates/reports/i18n/en.json
186
+ git commit -m "feat(report-template): compact narrative tables as meta(<br>)+prose; §5 stays flat"
187
+ ```
188
+
189
+ ---
190
+
191
+ ### Task 2: report_views — `<br>` 보존 + grouping 분기 정리
192
+
193
+ **Files:**
194
+ - Modify: `scripts/okstra_ctl/report_views.py`
195
+ - Modify: `tests/test_report_views.py`
196
+
197
+ - [ ] **Step 1: 실패 테스트 작성 (`<br>` 보존 + §1 plain)**
198
+
199
+ `tests/test_report_views.py` 끝에 추가:
200
+ ```python
201
+ def test_inline_preserves_br_tags():
202
+ from okstra_ctl.report_views import _inline # noqa: PLC0415
203
+ out = _inline("**C-1**<br>Ticket: `DEV-1`<br>Source items: claude:F-001")
204
+ assert "<br>" in out
205
+ assert "&lt;br&gt;" not in out
206
+ assert "<strong>C-1</strong>" in out
207
+ assert "<code>DEV-1</code>" in out
208
+
209
+
210
+ def test_summary_meta_table_renders_plain_with_br():
211
+ # §1 Summary is now pre-merged in the .md (meta col + Summary col); the
212
+ # grouped-table branch must NOT fire (no separate `Ticket ID` column) and
213
+ # the <br> in the meta cell survives.
214
+ html_out = _emit(
215
+ "| 항목 | 한 줄 요약 |",
216
+ "| **P-001**<br>Ticket: `DEV-9184`<br>출처: task-brief.md:19 | " + ("on-the-fly 계산 전환 핵심 변경 " * 4) + " |",
217
+ section="1. Summary of the Problem or Verification Target",
218
+ )
219
+ assert 'class="grouped-table"' not in html_out
220
+ assert "<br>" in html_out
221
+ ```
222
+
223
+ - [ ] **Step 2: 실패 확인**
224
+
225
+ Run: `python3 -m pytest tests/test_report_views.py -q -k "inline_preserves_br or summary_meta_table"`
226
+ Expected: FAIL — `_inline` escapes `<br>`; §1 still hits the generic grouping branch (grouped-table present).
227
+
228
+ - [ ] **Step 3: `_inline` 가 `<br>` 보존**
229
+
230
+ `scripts/okstra_ctl/report_views.py` `_inline` 의 `return out` 직전에 추가:
231
+ ```python
232
+ # Preserve explicit <br> line breaks used inside compact meta cells (the
233
+ # markdown source intentionally stacks short fields with <br>). They are
234
+ # escaped to &lt;br&gt; by html.escape above; restore the tag.
235
+ out = out.replace("&lt;br&gt;", "<br>").replace("&lt;br/&gt;", "<br>").replace("&lt;br /&gt;", "<br>")
236
+ ```
237
+
238
+ - [ ] **Step 4: generic/Exec/§7 grouping 분기 제거 (§5 유지)**
239
+
240
+ `_grouped_table_spec` 에서 다음을 **삭제**:
241
+ - "Execution Status by Agent" 분기 (`if len(norm) >= 3 and norm[0] == "Agent" ...`).
242
+ - "§7 Follow-up Tasks" 분기 (`if any("Follow-up Tasks" in h ...)`).
243
+ - generic "Ticket ID" 분기 (`if any(h == "Ticket ID" for h in norm): ...`) + 그 헬퍼 `_is_force_meta`, `_column_is_wide`, 상수 `_WIDE_PROSE_TOKENS`, `_FORCE_META_TOKENS`, `_WIDE_CONTENT_THRESHOLD`, `_FOLLOWUP_WIDE_PREFIXES` (이제 미사용).
244
+ - **유지:** §5 Clarification 분기(Expected form/Statement/User input wide) + signature 의 `rows` 파라미터(호출부 호환). `rows` 가 더는 분기 로직에 안 쓰이면, 호출부 `_grouped_table_spec(header_cells, rows, section_path)` 도 `_grouped_table_spec(header_cells, section_path)` 로 되돌리고 시그니처에서 `rows` 제거(미사용 인자 정리).
245
+
246
+ docstring 을 "§5 Clarification Items 만 grouped (interactive form). 나머지 narrative 표는 템플릿에서 이미 compact 하게 렌더되므로 여기서 grouping 하지 않는다." 로 갱신.
247
+
248
+ - [ ] **Step 5: 통과 확인 + 기존 grouped 테스트 정리**
249
+
250
+ Run: `python3 -m pytest tests/test_report_views.py -q`
251
+ Expected: 신규 2개 PASS. **단, Task 직전(2343e30)에 추가한 §1/§3/§4 grouped 테스트**(`test_summary_table_groups_short_cols_and_widens_prose`, `test_risks_table_widens_item_risk_mitigation`)는 이제 의도가 바뀌었으므로 제거하거나 plain+`<br>` 기대로 갱신한다. `test_clarification_expected_form_is_wide_not_meta` 는 **유지**(§5 grouping 살아있음). 모든 report_views 테스트 PASS 확인.
252
+
253
+ - [ ] **Step 6: 빌드 + 커밋**
254
+
255
+ ```bash
256
+ npm run build && bash validators/validate-workflow.sh
257
+ git add scripts/okstra_ctl/report_views.py tests/test_report_views.py runtime/
258
+ git commit -m "refactor(report-views): preserve <br>; group only §5 (narrative tables compact in md)"
259
+ ```
260
+
261
+ ---
262
+
263
+ ### Task 3: CHANGES + 전체 검증 + 실제 재렌더
264
+
265
+ **Files:**
266
+ - Modify: `CHANGES.md`
267
+
268
+ - [ ] **Step 1: CHANGES 항목 추가**
269
+
270
+ `## 2026-06-05` 아래(직전 report-views 항목 근처)에 삽입:
271
+ ```markdown
272
+ ### feat(report-template): 정본 final-report `.md` 표를 compact 하게 (meta + prose)
273
+
274
+ - markdown 표는 컬럼 병합이 안 돼 ID·Ticket·Source 같은 짧은 코드 컬럼이 칸을 차지하면 요약·근거·이견·위험 같은 긴 본문이 좁아져 뭉개졌다(에디터에서 한 글자/줄). 이제 §1 Summary·§1.1 Consensus·§1.2 Differences·§3.1/§3.2 Evidence·§4 Risks·§7 Follow-up·Execution Status 표를 **짧은 코드 필드는 `<br>` 로 한 meta 셀에 stack + 긴 본문은 별도 컬럼**으로 렌더한다. §5 Clarification 은 carry-in 파서·validator 8-컬럼 계약 때문에 평면 유지(§5 compact 는 HTML view grouping 담당). `data.json`·report-writer 계약은 불변(템플릿 레이아웃만). HTML self-contained view 도 `_inline` 이 `<br>` 를 보존해 동일하게 compact 하게 보인다.
275
+ - 사용자 영향: 다음 release + `npx -y okstra@latest install` 후 적용. 이제 final-report 를 어떤 markdown 에디터로 열어도 핵심 본문이 넓게 읽힌다. `.md`↔HTML 레이아웃이 일관된다.
276
+ ```
277
+
278
+ - [ ] **Step 2: 전체 검증**
279
+
280
+ ```bash
281
+ npm run build
282
+ python3 -m pytest tests/ -q
283
+ bash validators/validate-workflow.sh
284
+ node bin/okstra --version
285
+ ```
286
+ ALL must pass (worktree 격리 flake 예외는 단독 재실행 확인). 그 외 실패 → STOP/BLOCKED.
287
+
288
+ - [ ] **Step 3: 실제 재렌더 검증 (BLOCKING — 육안)**
289
+
290
+ 기존 사용자 리포트를 /tmp 복사본으로 재렌더하고 구조 확인:
291
+ ```bash
292
+ SRC="/Volumes/Workspaces/workspace/projects/FontsNinja/app/fontradar-v2-api/.okstra/tasks/calcule-des-prix-1-1/dev-9184/runs/requirements-discovery/reports/final-report-requirements-discovery-001.data.json"
293
+ ```
294
+ NOTE: HTML view 는 `.md` 에서 파생되므로, **새 템플릿으로 `.md` 를 다시 렌더**해야 한다. data.json → md 렌더는 `scripts/okstra-render-final-report.py`(또는 report-writer 경로) 사용. 해당 CLI 의 인자를 `--help` 로 확인 후, /tmp 에 새 `.md` 를 렌더하고:
295
+ - 새 `.md` 의 §1/§1.1/§1.2/§3.1/§3.2/§4/§7/Exec 가 `**ID**<br>Ticket: …` meta 셀 + prose 컬럼인지,
296
+ - §5 가 여전히 `| ID | Ticket ID | Kind | Statement | Expected form | Blocks | Status | User input |` 8-컬럼인지,
297
+ - 그 `.md` 로 `okstra-render-report-views.py` 를 돌려 HTML 의 meta 셀이 `<br>` 줄바꿈으로 보이는지(literal `&lt;br&gt;` 아님),
298
+ - `python3 -c "import sys; sys.path.insert(0,'scripts'); from okstra_ctl.clarification_items import parse_clarification_items; print(len(parse_clarification_items(open('<new-md>').read()) or []))"` 로 §5 carry-in 파서가 여전히 행을 파싱하는지
299
+ 확인. /tmp 산출물은 정리(사용자 프로젝트 파일은 건드리지 않음).
300
+
301
+ - [ ] **Step 4: 최종 커밋**
302
+
303
+ ```bash
304
+ git add CHANGES.md
305
+ git commit -m "docs(changes): log compact markdown final-report tables"
306
+ ```
307
+
308
+ ---
309
+
310
+ ## Self-Review (작성자 체크리스트)
311
+
312
+ **1. Spec coverage**
313
+ - §2.1 대상 표 7종 + §7 → Task 1 Step 2–9.
314
+ - §2.1 §5 평면 유지 → Task 1 Step 10 + 미변경.
315
+ - §2.2 meta `<br>` 포맷 + i18n → Task 1 Step 1–9.
316
+ - §2.3 `_inline` `<br>` 보존 → Task 2 Step 3.
317
+ - §2.3 generic/Exec 분기 제거, §5 유지 → Task 2 Step 4(§7 도 제거).
318
+ - §2.4 계약 불변 → Task 1(템플릿만), §5 미변경.
319
+ - §4 검증(실제 재렌더 + §5 파서) → Task 3 Step 2–3.
320
+
321
+ **2. Placeholder scan:** §7(Step 9)은 현 row 필드명을 "먼저 읽어 매핑"하라고 명시 — 구현자가 실제 필드명 확인 후 작성(추측 금지). 그 외 모든 jinja/코드 블록은 실제 내용. TBD 없음.
322
+
323
+ **3. 식별자 일관성:** `columns.recordMeta`(i18n, Task1) ↔ 템플릿 헤더 사용 동일. `_inline`(Task2) ↔ report_views. `_grouped_table_spec` 시그니처에서 `rows` 제거 시 호출부도 함께(Task2 Step4 명시).
@@ -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>`→`&lt;br&gt;` 로 깨뜨린다. escape 후 `&lt;br&gt;` / `&lt;br/&gt;` / `&lt;br /&gt;` 를 `<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 통과 + 실제 재렌더 육안 확인.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.48.0",
3
+ "version": "0.49.0",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.48.0",
3
- "builtAt": "2026-06-04T17:18:36.379Z",
2
+ "package": "0.49.0",
3
+ "builtAt": "2026-06-05T09:11:13.878Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env bash
2
+ # Resolve the tmux pane that the CURRENT process actually runs in.
3
+ #
4
+ # Why this exists: Claude Code's Bash tool strips $TMUX and $TMUX_PANE, so a
5
+ # bare `tmux display-message -p '#{pane_id}'` does NOT return the caller's pane
6
+ # — it returns the pane of the most-recently-active tmux *client*, which (when
7
+ # the user has several attached sessions) is frequently a DIFFERENT session
8
+ # than the one the okstra run lives in. Earlier trace-pane fixes all trusted
9
+ # `display-message` and therefore mis-placed (or dropped) the tail pane.
10
+ #
11
+ # This resolver instead walks the process's own ancestor PIDs and matches them
12
+ # against the tmux server's pane_pids. That is deterministic and correct
13
+ # regardless of $TMUX/$TMUX_PANE or which client is active: when the process is
14
+ # a descendant of a tmux pane's shell it finds exactly that pane; when it is not
15
+ # inside any tmux pane (e.g. Claude launched from the macOS GUI app) no ancestor
16
+ # matches and the function prints nothing.
17
+ #
18
+ # Usage: pane="$(okstra_resolve_caller_pane)" # empty => not in a tmux pane
19
+ # Optional arg: a starting PID (defaults to $$) — used by the regression test.
20
+ # bash 3.2 safe (no associative arrays).
21
+ okstra_resolve_caller_pane() {
22
+ command -v tmux >/dev/null 2>&1 || return 0
23
+ local panes
24
+ panes="$(tmux list-panes -a -F '#{pane_pid} #{pane_id}' 2>/dev/null)" || return 0
25
+ [ -n "$panes" ] || return 0
26
+
27
+ local pid="${1:-$$}"
28
+ local depth=0
29
+ local hit
30
+ while [ -n "$pid" ] && [ "$pid" != "0" ] && [ "$depth" -lt 16 ]; do
31
+ hit="$(printf '%s\n' "$panes" | awk -v p="$pid" '$1==p {print $2; exit}')"
32
+ if [ -n "$hit" ]; then
33
+ printf '%s\n' "$hit"
34
+ return 0
35
+ fi
36
+ pid="$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')"
37
+ depth=$((depth + 1))
38
+ done
39
+ return 0
40
+ }
@@ -183,36 +183,32 @@ status_path="${prompt_path%.md}.status.json"
183
183
  [[ "$status_path" == "$prompt_path" ]] && status_path="${prompt_path}.status.json"
184
184
  started_ts=$(date +%s)
185
185
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
186
+ # Trace-pane caller resolution helper (okstra_resolve_caller_pane). The lib dir
187
+ # is a bin-sibling in both repo (scripts/lib/...) and installed
188
+ # (~/.okstra/bin/lib/...) layouts; degrade silently if absent.
189
+ [ -r "$script_dir/lib/okstra/tmux-pane.sh" ] && . "$script_dir/lib/okstra/tmux-pane.sh"
186
190
  python3 "$script_dir/okstra-wrapper-status.py" \
187
191
  init "$status_path" "$(basename "$0")" "$role" "$$" "$started_ts" "$log_path" \
188
192
  >>"$log_path" 2>&1 || true
189
193
 
190
194
  # Derive the okstra run dir from the prompt path. paths.py is the SSOT:
191
195
  # dispatched prompts live at `<RUN_DIR>/prompts/<cli>-worker-prompt<NNN>.md`,
192
- # so the run dir is two levels up. Used to (a) read the lead pane the lead
193
- # recorded in its own foreground pane and (b) tag the trace pane so cleanup
194
- # can find exactly this run's panes without any tmux env var. Empty if the
196
+ # so the run dir is two levels up. Used to tag the trace pane so cleanup can
197
+ # find exactly this run's panes without any tmux env var. Empty if the
195
198
  # derivation fails — every dependent step below then degrades to a no-op.
196
199
  run_dir="$(cd "$(dirname "$prompt_path")/.." 2>/dev/null && pwd -P || true)"
197
- lead_pane_file="${run_dir:+$run_dir/state/lead-pane.id}"
198
200
 
199
- # Resolve the pane to anchor the trace split to. Claude Code's Bash tool now
200
- # strips BOTH `$TMUX` and `$TMUX_PANE`, and this wrapper frequently runs
201
- # backgrounded so the bare active-pane probe can land on whatever pane the
202
- # user happens to be looking at now, not Claude's. Prefer the lead pane the
203
- # lead captured ONCE in its own foreground pane (reliable, see
204
- # `_common-contract.md`); fall back to `$TMUX_PANE`, then the active-pane
205
- # probe. A stale recorded id (pane since closed) is rejected via a liveness
206
- # check so we never anchor the split to a dead pane.
207
- caller_pane="${TMUX_PANE:-}"
208
- if [[ -z "$caller_pane" && -n "$lead_pane_file" && -r "$lead_pane_file" ]]; then
209
- cand="$(head -n1 "$lead_pane_file" 2>/dev/null || true)"
210
- if [[ -n "$cand" ]] && tmux display-message -p -t "$cand" '#{pane_id}' >/dev/null 2>&1; then
211
- caller_pane="$cand"
212
- fi
213
- fi
214
- if [[ -z "$caller_pane" ]]; then
215
- caller_pane=$(tmux display-message -p '#{pane_id}' 2>/dev/null || true)
201
+ # Resolve the pane THIS wrapper actually runs in by walking our ancestor PIDs
202
+ # and matching tmux pane_pids (see lib/okstra/tmux-pane.sh). Reliable
203
+ # regardless of $TMUX/$TMUX_PANE (stripped by Claude Code's Bash tool) and of
204
+ # which tmux client is currently active a bare `tmux display-message` would
205
+ # instead return the most-recently-active client's pane, frequently a DIFFERENT
206
+ # session than the okstra run, which is why earlier approaches mis-placed or
207
+ # dropped the trace pane. Empty = not inside a tmux pane (e.g. Claude launched
208
+ # from the GUI app) the trace split below is skipped.
209
+ caller_pane=""
210
+ if type okstra_resolve_caller_pane >/dev/null 2>&1; then
211
+ caller_pane="$(okstra_resolve_caller_pane)"
216
212
  fi
217
213
 
218
214
  # Pane titles: worker (caller) pane gets `codex-<role>-<pid>`; the sibling
@@ -132,28 +132,25 @@ status_path="${prompt_path%.md}.status.json"
132
132
  [[ "$status_path" == "$prompt_path" ]] && status_path="${prompt_path}.status.json"
133
133
  started_ts=$(date +%s)
134
134
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
135
+ # Trace-pane caller resolution helper (okstra_resolve_caller_pane). The lib dir
136
+ # is a bin-sibling in both repo (scripts/lib/...) and installed
137
+ # (~/.okstra/bin/lib/...) layouts; degrade silently if absent.
138
+ [ -r "$script_dir/lib/okstra/tmux-pane.sh" ] && . "$script_dir/lib/okstra/tmux-pane.sh"
135
139
  python3 "$script_dir/okstra-wrapper-status.py" \
136
140
  init "$status_path" "$(basename "$0")" "$role" "$$" "$started_ts" "$log_path" \
137
141
  >>"$log_path" 2>&1 || true
138
142
 
139
143
  # Resolve the run dir and the trace-split anchor pane. See
140
- # `okstra-codex-exec.sh` for the full rationale — kept in lock-step: derive
141
- # `<RUN_DIR>` from the prompt path (paths.py SSOT) to read the lead-recorded
142
- # pane and to tag the trace pane; prefer that lead pane over the unreliable
143
- # active-pane probe (this wrapper runs backgrounded and `$TMUX`/`$TMUX_PANE`
144
- # are stripped).
144
+ # `okstra-codex-exec.sh` / `lib/okstra/tmux-pane.sh` for the full rationale —
145
+ # kept in lock-step: derive `<RUN_DIR>` from the prompt path (paths.py SSOT) to
146
+ # tag the trace pane, and resolve the caller pane by walking our ancestor PIDs
147
+ # against tmux pane_pids (reliable even though `$TMUX`/`$TMUX_PANE` are stripped
148
+ # and the wrapper runs backgrounded). Empty = not inside a tmux pane → skip.
145
149
  run_dir="$(cd "$(dirname "$prompt_path")/.." 2>/dev/null && pwd -P || true)"
146
- lead_pane_file="${run_dir:+$run_dir/state/lead-pane.id}"
147
150
 
148
- caller_pane="${TMUX_PANE:-}"
149
- if [[ -z "$caller_pane" && -n "$lead_pane_file" && -r "$lead_pane_file" ]]; then
150
- cand="$(head -n1 "$lead_pane_file" 2>/dev/null || true)"
151
- if [[ -n "$cand" ]] && tmux display-message -p -t "$cand" '#{pane_id}' >/dev/null 2>&1; then
152
- caller_pane="$cand"
153
- fi
154
- fi
155
- if [[ -z "$caller_pane" ]]; then
156
- caller_pane=$(tmux display-message -p '#{pane_id}' 2>/dev/null || true)
151
+ caller_pane=""
152
+ if type okstra_resolve_caller_pane >/dev/null 2>&1; then
153
+ caller_pane="$(okstra_resolve_caller_pane)"
157
154
  fi
158
155
 
159
156
  # Pane titles: worker (caller) pane gets `gemini-<role>-<pid>`; the sibling
@@ -37,6 +37,14 @@
37
37
 
38
38
  set -u
39
39
 
40
+ # Trace-pane caller resolution helper (okstra_resolve_caller_pane) — see
41
+ # lib/okstra/tmux-pane.sh. Used as the lead-pane fallback below so a missing /
42
+ # stale lead-pane.id resolves to the pane THIS process actually runs in (via
43
+ # ancestor-PID ↔ tmux pane_pid matching), never a foreign active-client pane.
44
+ # Bin-sibling path in repo + installed layouts; degrade silently if absent.
45
+ _clean_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
46
+ [ -r "$_clean_script_dir/lib/okstra/tmux-pane.sh" ] && . "$_clean_script_dir/lib/okstra/tmux-pane.sh"
47
+
40
48
  MODE="kill" # kill | list
41
49
  REAP=0
42
50
  run_dir=""
@@ -88,7 +96,11 @@ if [[ "$REAP" -eq 0 ]]; then
88
96
  [[ -r "$lead_pane_file" ]] && lead_pane="$(head -n1 "$lead_pane_file" 2>/dev/null || true)"
89
97
  fi
90
98
  if [[ -z "$lead_pane" ]] || ! tmux display-message -p -t "$lead_pane" '#{pane_id}' >/dev/null 2>&1; then
91
- lead_pane="$(tmux display-message -p '#{pane_id}' 2>/dev/null || true)"
99
+ if type okstra_resolve_caller_pane >/dev/null 2>&1; then
100
+ lead_pane="$(okstra_resolve_caller_pane 2>/dev/null || true)"
101
+ else
102
+ lead_pane="$(tmux display-message -p '#{pane_id}' 2>/dev/null || true)"
103
+ fi
92
104
  fi
93
105
 
94
106
  # Does a trace pane's tag belong to the set we are closing?
@@ -30,8 +30,8 @@ profile document.
30
30
  - Anti-escalation rule (shared):
31
31
  - treating "다음 단계 진행해" or equivalent user phrases as authorisation to start a *different* lifecycle phase is forbidden. The next phase begins only in a separate okstra run launched with the new `--task-type`. Per-profile documents may further restrict this within their own scope.
32
32
  - Run-start pane recording (shared — runs ONCE at run start, before the FIRST worker dispatch):
33
- - The wrappers anchor each trace pane to the lead's pane and the cleanup scopes the worker-agent scan to it, but Claude Code's Bash tool strips `$TMUX`/`$TMUX_PANE`, so the lead MUST record its own pane explicitly. Because the lead runs this in its OWN foreground pane, the active pane IS the lead's reliable, unlike a backgrounded wrapper's later probe.
34
- - The lead MUST run once, at run start: `mkdir -p "<RUN_DIR>/state" && tmux display-message -p '#{pane_id}' > "<RUN_DIR>/state/lead-pane.id" 2>/dev/null || true` (substitute the run's absolute `RUN_DIR`). Outside tmux this writes nothing and every pane step below silently no-ops — that empty/absent file is the single signal that the lead is not in tmux.
33
+ - The codex/gemini wrappers now self-anchor their trace pane by walking their own ancestor PIDs against tmux `pane_pid`s (see `lib/okstra/tmux-pane.sh`), so they no longer depend on this file. The lead still records its own pane id here for the cleanup steps below (which-pane-to-never-kill) and as the "am I in tmux" gate. A bare `tmux display-message -p '#{pane_id}'` is NOT reliable for this — Claude Code's Bash tool strips `$TMUX`/`$TMUX_PANE`, so that command returns the most-recently-active *client's* pane (often a different session, or a foreign pane when the lead is launched outside tmux entirely). The lead therefore records via the same ancestry resolver.
34
+ - The lead MUST run once, at run start: `mkdir -p "<RUN_DIR>/state" && { . "$HOME/.okstra/bin/lib/okstra/tmux-pane.sh" 2>/dev/null && okstra_resolve_caller_pane; } > "<RUN_DIR>/state/lead-pane.id" 2>/dev/null || true` (substitute the run's absolute `RUN_DIR`). When the lead is not inside a tmux pane (e.g. Claude launched from the GUI app) no ancestor matches a pane, the file is empty, and every pane step below silently no-ops — that empty/absent file is the single signal that the lead is not in tmux.
35
35
  - Phase-start pane reset (shared — runs BEFORE dispatching each new worker batch):
36
36
  - okstra creates two kinds of tmux pane per run: (a) **worker-agent panes** the harness gives to dispatched subagents (titled `claude-worker` / `codex-worker` / `gemini-worker` / `report-writer-worker`), and (b) **trace panes** the codex/gemini wrappers spawn (`<cli>-<role>-<pid>-tail`). Both accumulate across internal phases because each new phase dispatches a fresh worker batch and the prior panes are never reclaimed.
37
37
  - When `<RUN_DIR>/state/lead-pane.id` is non-empty (the lead is in tmux), the lead MUST run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"` **immediately before** dispatching the next phase's workers — i.e. just before emitting each `PROGRESS: phase-5.5-convergence round=<N>` marker and just before `PROGRESS: phase-6-synthesis dispatching report-writer-worker`. This closes every prior-phase okstra pane (worker-agent + trace) for this run, while NEVER killing the lead's own pane.
@@ -41,8 +41,8 @@ profile document.
41
41
  - This step is **automatic and silent** — NO user prompt (workers are idle sessions that have already delivered their results; there is nothing for the user to preserve). It runs only when team-state's `teamCreate.status == "ok"` (Teams mode was actually used); in the no-`team_name` fallback there is no team to delete, so silent-skip.
42
42
  - Sequence (token-usage collection MUST already be complete — `TeamDelete` removes `~/.claude/teams/<team>/` + `~/.claude/tasks/<team>/` but NOT the `~/.claude/projects/` jsonls Phase 7 reads, yet the read MUST precede teardown):
43
43
  1. Read `~/.claude/teams/okstra-<task-key>/config.json` and, for every `members` entry whose name is not the lead, `SendMessage(to: <name>, message: { type: "shutdown_request" })` to terminate it gracefully.
44
- 2. Wait for the shutdown confirmations / idle notifications from all addressed teammates.
45
- 3. Call `TeamDelete()`. If it errors with an active-members message, a teammate has not finished shutting down wait briefly and retry `TeamDelete()` once.
44
+ 2. These workers already delivered their results and terminated when their `Agent()` dispatch returned (the lead's completion evidence is the returned output + the existing result/final-report file, not a teardown ack) — a terminated session emits NO shutdown confirmation. Treat `shutdown_request` as best-effort (fire-and-forget); the lead MUST NOT block waiting for acks from addressed teammates. Proceed immediately to step 3.
45
+ 3. Call `TeamDelete()` — the single synchronization point for teardown. If it errors with an active-members message, one teammate is genuinely still shutting down: wait briefly, retry `TeamDelete()` once, then proceed regardless of the result. NEVER loop or re-send `shutdown_request`; teardown must never block run completion once the work and final report already exist.
46
46
  - Report it in one short line (e.g. `worker 6명 종료 + 팀 해제`) and proceed. Emit `PROGRESS: phase-7-teardown disbanding team` immediately before step 1.
47
47
  - Phase wrap-up — okstra pane disposition (shared, MUST be the *last* step before returning control to the user):
48
48
  - At run end the only residual okstra panes are the LAST phase's (e.g. the `report-writer-worker` agent pane and any codex/gemini trace pane). `okstra-trace-cleanup.sh --list --run-dir "<RUN_DIR>"` returns one tab-separated `<pane_id>\t<pane_title>` line per residual okstra pane (worker-agent + trace) for this run.
@@ -407,21 +407,12 @@ class _GroupedSpec:
407
407
  user_input_col: int = -1
408
408
 
409
409
 
410
- _FOLLOWUP_WIDE_PREFIXES: tuple[str, ...] = ("title", "scope", "reason")
411
-
412
-
413
410
  def _grouped_table_spec(
414
411
  header_cells: list[str], section_path: list[str]
415
412
  ) -> Optional[_GroupedSpec]:
416
- """Return a ``_GroupedSpec`` for the three wide final-report tables
417
- that benefit from the compact layout Execution Status, §5
418
- Clarification Items, §7 Follow-up Tasks or ``None`` for every other
419
- table (which keeps the default per-cell ``td-narrow`` rendering).
420
-
421
- Each table is identified by stable header tokens (the i18n token/cost
422
- columns are never used as anchors). ``wide_cols`` lists the long-prose
423
- columns that must keep a guaranteed min-width; everything else short
424
- collapses into the leading metadata cell."""
413
+ """Only §5 Clarification Items is grouped in the HTML view (it keeps the
414
+ interactive form and stays flat in the .md). All other narrative tables are
415
+ already rendered compactly by the template, so no grouping is applied here."""
425
416
  norm = [h.strip() for h in header_cells]
426
417
 
427
418
  def _spec(headline: int, wide: tuple[int, ...], **kw) -> _GroupedSpec:
@@ -429,12 +420,8 @@ def _grouped_table_spec(
429
420
  group = tuple(c for c in range(len(norm)) if c != headline and c not in wide_set)
430
421
  return _GroupedSpec(headline_col=headline, group_cols=group, wide_cols=wide, **kw)
431
422
 
432
- # Execution Status by Agent Agent Summary of Key Findings.
433
- if len(norm) >= 3 and norm[0] == "Agent" and norm[-1] == "Summary of Key Findings":
434
- return _spec(0, (len(norm) - 1,), kind="plain")
435
-
436
- # §5 Clarification Items — keep the interactive form, but collapse the
437
- # short ID/Kind/Status/… columns and widen Statement + User input.
423
+ # §5 Clarification Itemskeep the interactive form, and widen the three
424
+ # long-prose columns (Expected form is prose too, not a code column).
438
425
  if (
439
426
  any("Clarification Items" in h for h in section_path)
440
427
  and not _section_forbids_form(section_path)
@@ -444,9 +431,15 @@ def _grouped_table_spec(
444
431
  ):
445
432
  statement_col = next(i for i, h in enumerate(norm) if h.startswith("Statement"))
446
433
  user_input_col = norm.index("User input")
434
+ expected_col = next(
435
+ (i for i, h in enumerate(norm) if h.startswith("Expected form")), -1
436
+ )
437
+ wide_cols = tuple(
438
+ c for c in (expected_col, statement_col, user_input_col) if c >= 0
439
+ )
447
440
  return _spec(
448
441
  norm.index("ID"),
449
- (statement_col, user_input_col),
442
+ wide_cols,
450
443
  kind="clarification",
451
444
  id_col=norm.index("ID"),
452
445
  kind_col=norm.index("Kind") if "Kind" in norm else -1,
@@ -455,16 +448,6 @@ def _grouped_table_spec(
455
448
  user_input_col=user_input_col,
456
449
  )
457
450
 
458
- # §7 Follow-up Tasks — widen Title / Scope / Reason, collapse the rest.
459
- if any("Follow-up Tasks" in h for h in section_path) and "ID" in norm:
460
- wide = tuple(
461
- i
462
- for i, h in enumerate(norm)
463
- if any(h.lower().startswith(p) for p in _FOLLOWUP_WIDE_PREFIXES)
464
- )
465
- if wide:
466
- return _spec(norm.index("ID"), wide, kind="plain")
467
-
468
451
  return None
469
452
 
470
453
 
@@ -768,6 +751,10 @@ def _inline(text: str) -> str:
768
751
  out = _LINK_PATTERN.sub(
769
752
  lambda m: f'<a href="{m.group(2)}">{m.group(1)}</a>', out
770
753
  )
754
+ # Preserve explicit <br> line breaks used inside compact meta cells (the
755
+ # markdown source intentionally stacks short fields with <br>). html.escape
756
+ # above turned them into &lt;br&gt;; restore the tag.
757
+ out = out.replace("&lt;br&gt;", "<br>").replace("&lt;br/&gt;", "<br>").replace("&lt;br /&gt;", "<br>")
771
758
  return out
772
759
 
773
760
 
@@ -68,11 +68,4 @@ taskType: "{{FM_TASK_TYPE}}"
68
68
 
69
69
  ## Notes
70
70
 
71
- - 이 문서는 사람용 quick summary입니다.
72
- - canonical metadata는 항상 `task-manifest.json`을 기준으로 확인합니다.
73
- - 동일한 버그나 작업을 다시 이어갈 때는 같은 `Task Group`과 `Task ID`를 재사용합니다.
74
- - `requirements-discovery`는 work category와 다음 phase routing 결정을 남기기 위한 초기 triage 단계입니다.
75
- - run directory는 `runs/<task-type>/` 규칙을 사용하여 서로 다른 task-type 실행을 경로 수준에서 분리합니다.
76
- - run directory 내부는 `manifests/`, `state/`, `prompts/`, `reports/`, `status/`, `sessions/`, `worker-results/`처럼 유형별 하위 폴더로 나뉩니다.
77
- - current run의 `prompts/` 디렉터리는 lead prompt snapshot과 worker prompt history file의 canonical 저장 위치입니다.
78
- - run-level artifact와 result 파일명은 같은 task-type 재실행을 구분하기 위해 `-YYYY-MM-DD_HH-MM-SS` suffix를 사용합니다.
71
+ - 이 문서는 사람이 빠르게 훑어보기 위한 요약입니다. 정본·상세 metadata 는 `task-manifest.json` 을 참조하세요.
@@ -53,10 +53,10 @@ approved: {{ frontmatter.approved | yaml_scalar }}
53
53
 
54
54
  {{ t("sectionIntro.ticketCoverage") }}
55
55
 
56
- | ID | Ticket ID | {{ t("columns.summary") }} | {{ t("columns.source") }} |
57
- |----|-----------|------------|----------------------------|
56
+ | {{ t("columns.recordMeta") }} | {{ t("columns.summary") }} |
57
+ |--------|------------|
58
58
  {% for row in summary -%}
59
- | {{ row.id }} | `{{ row.ticketId }}` | {{ row.summary }} | {{ row.source }} |
59
+ | **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}`<br>{{ t("columns.source") }}: {{ row.source }} | {{ row.summary }} |
60
60
  {% endfor %}
61
61
 
62
62
  {% if ticketCoverage.omit %}
@@ -76,17 +76,15 @@ approved: {{ frontmatter.approved | yaml_scalar }}
76
76
  {% endfor %}
77
77
  {%- endif %}
78
78
 
79
- {{ t("ticketCoverage.ruleNote") }}
80
-
81
79
  {% endif %}
82
80
  ## Execution Status by Agent
83
81
 
84
82
  {{ t("sectionIntro.executionStatus") }}
85
83
 
86
- | Agent | Role | Model | Status | {{ t("columns.rawTokens") }} | {{ t("columns.billableTokens") }} | {{ t("columns.cost") }} | Duration | Summary of Key Findings |
87
- |-------|------|-------|--------|-----------|-----------|------------|----------|-------------------------|
84
+ | {{ t("columns.recordMeta") }} | Summary of Key Findings |
85
+ |--------|-------------------------|
88
86
  {% for row in executionStatus -%}
89
- | {{ row.agent }} | {{ row.role }} | {{ row.model }} | {{ row.status }} | {{ row.totalTokens | format_int }}{% if row.cliTotalTokens %} (CLI: {{ row.cliTotalTokens | format_int }}){% endif %} | {{ row.billableTokens | format_int }} | {{ row.costUsd | format_usd }}{% if row.cliCostUsd %} (+ CLI {{ row.cliCostUsd | format_usd }}){% endif %} | {{ row.durationMs | format_duration_ms }} | {{ row.summary }} |
87
+ | **{{ row.agent }}**<br>Role: {{ row.role }}<br>Model: {{ row.model }}<br>Status: {{ row.status }}<br>{{ t("columns.rawTokens") }}: {{ row.totalTokens | format_int }}{% if row.cliTotalTokens %} (CLI: {{ row.cliTotalTokens | format_int }}){% endif %}<br>{{ t("columns.billableTokens") }}: {{ row.billableTokens | format_int }}<br>{{ t("columns.cost") }}: {{ row.costUsd | format_usd }}{% if row.cliCostUsd %} (+ CLI {{ row.cliCostUsd | format_usd }}){% endif %}<br>Duration: {{ row.durationMs | format_duration_ms }} | {{ row.summary }} |
90
88
  {% endfor %}
91
89
 
92
90
  ## {{ t("tokenSummary.heading") }}
@@ -130,10 +128,10 @@ approved: {{ frontmatter.approved | yaml_scalar }}
130
128
  {% if crossVerification.consensus | length == 0 -%}
131
129
  {{ t("emptyState.consensusItems") }}
132
130
  {%- else %}
133
- | ID | Ticket ID | Statement | Source items (worker:item) | Evidence (path:line / log / worker report) |
134
- |----|-----------|-----------|----------------------------|---------------------------------------------|
131
+ | {{ t("columns.recordMeta") }} | Statement | Evidence (path:line / log / worker report) |
132
+ |--------|-----------|---------------------------------------------|
135
133
  {% for row in crossVerification.consensus -%}
136
- | {{ row.id }} | `{{ row.ticketId }}` | {{ row.statement }} | {{ row.sourceItems | join(', ') }} | {{ row.evidence }} |
134
+ | **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}`<br>Source items: {{ row.sourceItems | join(', ') }} | {{ row.statement }} | {{ row.evidence }} |
137
135
  {% endfor %}
138
136
  {%- endif %}
139
137
 
@@ -144,10 +142,10 @@ approved: {{ frontmatter.approved | yaml_scalar }}
144
142
  {% if crossVerification.differences | length == 0 -%}
145
143
  {{ t("emptyState.differences") }}
146
144
  {%- else %}
147
- | ID | Ticket ID | Disagreement | Workers (position + item) | Evidence |
148
- |----|-----------|--------------|---------------------------|----------|
145
+ | {{ t("columns.recordMeta") }} | Disagreement | Evidence |
146
+ |--------|--------------|----------|
149
147
  {% for row in crossVerification.differences -%}
150
- | {{ row.id }} | `{{ row.ticketId }}` | {{ row.disagreement }} | {% for w in row.workersPosition %}{{ w.worker }}:{{ w.itemId }} ({{ w.position }}){% if not loop.last %} / {% endif %}{% endfor %} | {{ row.evidence }} |
148
+ | **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}`<br>Workers: {% for w in row.workersPosition %}{{ w.worker }}:{{ w.itemId }} ({{ w.position }}){% if not loop.last %} / {% endif %}{% endfor %} | {{ row.disagreement }} | {{ row.evidence }} |
151
149
  {% endfor %}
152
150
  {%- endif %}
153
151
 
@@ -170,10 +168,10 @@ approved: {{ frontmatter.approved | yaml_scalar }}
170
168
  {% if evidence.primary | length == 0 -%}
171
169
  {{ t("emptyState.primaryEvidence") }}
172
170
  {%- else %}
173
- | ID | Ticket ID | Evidence | Source items (worker:item) | Source (path:line / log) |
174
- |----|-----------|----------|----------------------------|---------------------------|
171
+ | {{ t("columns.recordMeta") }} | Evidence |
172
+ |--------|----------|
175
173
  {% for row in evidence.primary -%}
176
- | {{ row.id }} | `{{ row.ticketId }}` | {{ row.evidence }} | {{ row.sourceItems | join(', ') }} | {{ row.source }} |
174
+ | **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}`<br>Source items: {{ row.sourceItems | join(', ') }}<br>Source: {{ row.source }} | {{ row.evidence }} |
177
175
  {% endfor %}
178
176
  {%- endif %}
179
177
 
@@ -184,10 +182,10 @@ approved: {{ frontmatter.approved | yaml_scalar }}
184
182
  {% if not evidence.secondary or evidence.secondary | length == 0 -%}
185
183
  {{ t("emptyState.secondaryEvidence") }}
186
184
  {%- else %}
187
- | ID | Ticket ID | Hypothesis or supporting evidence | Source / confidence |
188
- |----|-----------|-----------------------------------|---------------------|
185
+ | {{ t("columns.recordMeta") }} | Hypothesis or supporting evidence | Source / confidence |
186
+ |--------|-----------------------------------|---------------------|
189
187
  {% for row in evidence.secondary -%}
190
- | {{ row.id }} | `{{ row.ticketId }}` | {{ row.hypothesis }} | {{ row.confidence }} |
188
+ | **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}` | {{ row.hypothesis }} | {{ row.confidence }} |
191
189
  {% endfor %}
192
190
  {%- endif %}
193
191
 
@@ -196,10 +194,10 @@ approved: {{ frontmatter.approved | yaml_scalar }}
196
194
  {% if missingInformation | length == 0 -%}
197
195
  {{ t("emptyState.risks") }}
198
196
  {%- else %}
199
- | ID | Ticket ID | Item | Risk if ignored | Mitigation Owner |
200
- |----|-----------|------|-----------------|------------------|
197
+ | {{ t("columns.recordMeta") }} | Item | Risk if ignored | Mitigation Owner |
198
+ |--------|------|-----------------|------------------|
201
199
  {% for row in missingInformation -%}
202
- | {{ row.id }} | `{{ row.ticketId }}` | {{ row.item }} | {{ row.risk }} | {{ row.owner }} |
200
+ | **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}` | {{ row.item }} | {{ row.risk }} | {{ row.owner }} |
203
201
  {% endfor %}
204
202
  {%- endif %}
205
203
 
@@ -249,8 +247,6 @@ approved: {{ frontmatter.approved | yaml_scalar }}
249
247
  | {{ row.step }} | `{{ row.ticketId }}` | {{ row.action }} | `{{ row.files }}` | `{{ row.commandOrTest }}` | {{ row.expectedOutcome }} |
250
248
  {% endfor %}
251
249
 
252
- {{ t("sectionIntro.stepRule") }}
253
-
254
250
  ### 4.5.5 Dependency / Migration Risk{% if t("sectionAside.dependencyRisk") != "Dependency / Migration Risk" %} ({{ t("sectionAside.dependencyRisk") }}){% endif %}
255
251
 
256
252
  {% if implementationPlanning.dependencyMigrationRisk | length == 0 -%}
@@ -598,9 +594,9 @@ approved: {{ frontmatter.approved | yaml_scalar }}
598
594
  {% if followUpTasks | length == 0 -%}
599
595
  {{ t("emptyState.noFollowUp") }}
600
596
  {%- else %}
601
- | ID | Ticket ID | Origin | New Task ID | Title | Suggested task-type | Scope (files/areas) | Reason / Why deferred | Priority (P0/P1/P2) | Auto-spawn? |
602
- |----|-----------|--------|-------------|-------|---------------------|---------------------|------------------------|---------------------|-------------|
597
+ | {{ t("columns.recordMeta") }} | Title | Scope (files/areas) | Reason / Why deferred |
598
+ |--------|-------|---------------------|------------------------|
603
599
  {% for row in followUpTasks -%}
604
- | {{ row.id }} | `{{ row.ticketId }}` | `{{ row.origin }}` | `{{ row.newTaskId }}` | {{ row.title }} | `{{ row.suggestedTaskType }}` | `{{ row.scope }}` | {{ row.reason }} | `{{ row.priority }}` | `{{ row.autoSpawn }}` |
600
+ | **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}`<br>Origin: `{{ row.origin }}`<br>New Task ID: `{{ row.newTaskId }}`<br>Type: `{{ row.suggestedTaskType }}`<br>Priority: `{{ row.priority }}`<br>Auto-spawn: `{{ row.autoSpawn }}` | {{ row.title }} | {{ row.scope }} | {{ row.reason }} |
605
601
  {% endfor %}
606
602
  {%- endif %}
@@ -19,6 +19,7 @@
19
19
  "noFollowUp": "- No follow-up tasks. The next phase for this run is in §6 (Recommended Next Steps)."
20
20
  },
21
21
  "columns": {
22
+ "recordMeta": "Record",
22
23
  "summary": "Summary",
23
24
  "source": "Source (brief/source/worker)",
24
25
  "rawTokens": "Raw tokens",
@@ -38,14 +39,13 @@
38
39
  "stepwiseExecutionOrder": "Stepwise Execution Order"
39
40
  },
40
41
  "sectionIntro": {
41
- "verdictCard": "At-a-glance verdict card. Every value in this table MUST exactly match the authoritative values in `## 2. Final Verdict` and `## 6. Recommended Next Steps`.",
42
- "clarificationCarryIn": "Walk every C-* row of the prior report's `## 5. Clarification Items` table against new evidence. Update each row's `Status` to `resolved` or `obsolete` and carry it into this run's `## 5.` table. Cite the resolution evidence (file:line / log / worker result) inline.",
43
- "ticketCoverage": "Summarize 3-5 core problems / requirements / verification targets as a table. Base this on the brief, source material, and worker results.",
44
- "executionStatus": "Aggregate each worker's status, assigned model, and key findings into one table. Do NOT replace worker artifacts with ungrounded claims.",
45
- "sourceItemsRule": "`Source items` rule: list which worker items this consensus row was synthesised from, as a comma-list of `<worker>:<item-id>` pairs. Full policy: `prompts/profiles/_common-contract.md` \"Cross-worker traceability\" SSOT.",
46
- "stepRule": "Rule: each step is roughly 2-5 minutes. Every step MUST include exact file paths and commands.",
47
- "planBodyVerification": "Result of Phase 6 report-writer's synthesised 4.5 body re-cast to workers by the lead on a plan-item basis, with verdicts collected.",
48
- "clarificationItems": "Track items that must be answered by the user or backed by attached material before the next run advances, **all inside one table**."
42
+ "verdictCard": "At-a-glance verdict card a summary that mirrors the values in `## 2. Final Verdict` and `## 6. Recommended Next Steps`.",
43
+ "clarificationCarryIn": "Unresolved `Clarification Items` from the prior report, re-examined against new evidence and carried in with updated status.",
44
+ "ticketCoverage": "Summary of the core problems, requirements, and verification targets this run covered.",
45
+ "executionStatus": "At-a-glance table of each worker's status, assigned model, and key findings.",
46
+ "sourceItemsRule": "The `Source items` column shows which worker items each consensus row was synthesised from, as `<worker>:<item-id>` pairs.",
47
+ "planBodyVerification": "Cross-verification results for each item in the plan body.",
48
+ "clarificationItems": "Items that need your answer or supporting material before the next step."
49
49
  },
50
50
  "tokenSummary": {
51
51
  "heading": "Token Usage Summary",
@@ -63,19 +63,18 @@
63
63
  "nextStepLabel": "Next step"
64
64
  },
65
65
  "ticketCoverage": {
66
- "intro": "Reverse index of tickets covered in this run. All body items are linked to a ticket via the `Ticket ID` column or `[TICKETID: <id>]` tag.",
66
+ "intro": "The tickets this run covered, with the sections and related items where each ticket appears.",
67
67
  "columnSections": "Sections",
68
- "columnRelatedIds": "Related item IDs",
69
- "ruleNote": "Rule: `Ticket ID` must match the ticket key exactly as it appears in the body. When `Issue / Ticket` is empty and falls back, use the `Task ID` value as-is without prefix (e.g. `8852`). Use `unknown` when not identifiable."
68
+ "columnRelatedIds": "Related item IDs"
70
69
  },
71
70
  "finalVerdict": {
72
- "intro": "State the final conclusion and recommended direction in one table. `Direction` `continue-investigation / begin-implementation / approve / reject / hold`. When `task-type` is `final-verification`, `Verdict Token` must be one of `accepted / conditional-accept / blocked`; `release-handoff` uses this value as an entry gate. For all other task-types use `not-applicable`."
71
+ "intro": "Final conclusion and recommended direction. `Direction` values: `continue-investigation / begin-implementation / approve / reject / hold`. When `task-type` is `final-verification`, `Verdict Token` is one of `accepted / conditional-accept / blocked` and serves as the `release-handoff` entry gate. For all other task-types: `not-applicable`."
73
72
  },
74
73
  "evidence": {
75
- "sourceItemsColumnNote": "`Source items` column rule is the same as §1.1."
74
+ "sourceItemsColumnNote": "The `Source items` column is described in §1.1."
76
75
  },
77
76
  "roundHistory": {
78
- "round2SkippedReasonNote": "value is one of `queue-empty | max-rounds-1 | all-reverify-non-result | not-skipped | convergence-disabled | single-analyser-only`",
77
+ "round2SkippedReasonNote": "(one of: `queue-empty`, `max-rounds-1`, `all-reverify-non-result`, `not-skipped`, `convergence-disabled`, `single-analyser-only`)",
79
78
  "singleRoundPrefix": "Single round —",
80
79
  "noRoundsNote": "No reverify rounds executed (all findings reached consensus at grouping)."
81
80
  },
@@ -128,7 +127,7 @@
128
127
  "clarification": {
129
128
  "fillAndRerun": "Fill in your answers then re-run the same phase:",
130
129
  "separateTerminalLabel": "Separate terminal",
131
- "columnGuide": "Column guide (full definitions: `prompts/profiles/_common-contract.md §Clarification request policy` SSOT):"
130
+ "columnGuide": "Column descriptions:"
132
131
  },
133
132
  "followUpTasks": {
134
133
  "headingAside": "Follow-up Tasks"
@@ -19,6 +19,7 @@
19
19
  "noFollowUp": "- 후속 작업 없음. 본 run 의 다음 phase 는 §6 (Recommended Next Steps) 참고."
20
20
  },
21
21
  "columns": {
22
+ "recordMeta": "항목",
22
23
  "summary": "한 줄 요약",
23
24
  "source": "출처 (brief/source/worker)",
24
25
  "rawTokens": "처리 토큰",
@@ -38,14 +39,13 @@
38
39
  "stepwiseExecutionOrder": "단계별 실행 순서"
39
40
  },
40
41
  "sectionIntro": {
41
- "verdictCard": "한눈에 보는 결과 카드. 표의 모든 값은 `## 2. Final Verdict` `## 6. Recommended Next Steps` 의 권위 있는 값과 정확히 일치해야 합니다.",
42
- "clarificationCarryIn": "이전 보고서의 `## 5. Clarification Items` 매 행(`C-001`, `C-002`, …) 을 증거에 비추어 검토하고, 각 행의 `Status` 를 `resolved` 또는 `obsolete` 로 갱신한 run 의 `## 5.` 표에 carry-in 합니다. 해소 근거(파일:라인 / 로그 / 워커 결과) 를 함께 인용합니다.",
43
- "ticketCoverage": "3~5 row 핵심 문제·요구사항·검증 대상을 표로 정리합니다. brief, 소스 자료, worker 결과를 근거로 작성합니다.",
44
- "executionStatus": "각 worker 의 status, 배정 모델, key finding 을 표에 모읍니다. worker 산출물을 근거 없는 주장으로 대체하지 않습니다.",
45
- "sourceItemsRule": "`Source items` 규칙: 합의 row 어느 워커의 어느 항목들에서 합성됐는지를 `<worker>:<item-id>` 페어 콤마-리스트로 적습니다. 자세한 정책은 `prompts/profiles/_common-contract.md` \"Cross-worker traceability\" SSOT.",
46
- "stepRule": "규칙: step 2~5 분. 모든 step 은 정확한 파일 경로와 명령어 포함.",
47
- "planBodyVerification": "Phase 6 에서 report-writer 합성한 4.5 본문을 lead 가 plan-item 단위로 워커들에게 다시 던지고 평결을 수집한 결과.",
48
- "clarificationItems": "다음 run 으로 넘어가기 전에 사용자가 답하거나 자료를 첨부해야 하는 항목을 **한 표 안에서** 추적합니다."
42
+ "verdictCard": "한눈에 보는 결과 카드 `## 2. Final Verdict` `## 6. Recommended Next Steps` 의 값을 그대로 옮긴 요약입니다.",
43
+ "clarificationCarryIn": "이전 보고서의 미해결 `Clarification Items` 증거로 재검토해 해소·갱신한이어받은 항목입니다.",
44
+ "ticketCoverage": " run 다룬 핵심 문제·요구사항·검증 대상 요약입니다.",
45
+ "executionStatus": "각 worker 의 상태·배정 모델·핵심 finding 을 한눈에 보는 표입니다.",
46
+ "sourceItemsRule": "`Source items` 열은 합의 항목이 어느 워커의 어느 항목에서 합성됐는지를 `<worker>:<item-id>` 형식으로 표기합니다.",
47
+ "planBodyVerification": "계획 본문의 항목을 plan-item 단위로 교차 검증한 결과입니다.",
48
+ "clarificationItems": "다음 진행 전에 사용자의 답변이나 자료 첨부가 필요한 항목입니다."
49
49
  },
50
50
  "tokenSummary": {
51
51
  "heading": "토큰 사용량 요약",
@@ -63,19 +63,18 @@
63
63
  "nextStepLabel": "다음 단계"
64
64
  },
65
65
  "ticketCoverage": {
66
- "intro": " run 이 다룬 ticket 역방향 인덱스. 본문 항목들은 모두 `Ticket ID` 컬럼 또는 `[TICKETID: <id>]` 태그로 ticket 과 묶여 있습니다.",
66
+ "intro": " run 이 다룬 ticket 과, ticket 등장한 섹션·관련 항목 목록입니다.",
67
67
  "columnSections": "등장 섹션",
68
- "columnRelatedIds": "관련 항목 IDs",
69
- "ruleNote": "규칙: `Ticket ID` 는 본문에서 등장한 ticket 키와 정확히 동일 문자열. `Issue / Ticket` 이 비어 폴백된 경우 `Task ID` 값을 prefix 없이 그대로 (예: `8852`). 식별 불가는 `unknown`."
68
+ "columnRelatedIds": "관련 항목 IDs"
70
69
  },
71
70
  "finalVerdict": {
72
- "intro": "최종 결론과 권장 방향을 한 표로 명시합니다. `Direction` `continue-investigation / begin-implementation / approve / reject / hold`. `task-type` 이 `final-verification` 이면 `Verdict Token` 은 `accepted / conditional-accept / blocked` 중 하나여야 하며, `release-handoff` 는 이 값을 진입 게이트로 사용합니다. 다른 task-type 에서는 `not-applicable`."
71
+ "intro": "최종 결론과 권장 방향입니다. `Direction` 값: `continue-investigation / begin-implementation / approve / reject / hold`. `task-type` 이 `final-verification` `Verdict Token` 은 `accepted / conditional-accept / blocked` 중 하나이며 `release-handoff` 진입 게이트로 쓰입니다. task-type 에서는 `not-applicable`."
73
72
  },
74
73
  "evidence": {
75
- "sourceItemsColumnNote": "`Source items` 컬럼 규칙은 §1.1 과 동일."
74
+ "sourceItemsColumnNote": "`Source items` 설명은 §1.1 과 동일합니다."
76
75
  },
77
76
  "roundHistory": {
78
- "round2SkippedReasonNote": "값은 `queue-empty | max-rounds-1 | all-reverify-non-result | not-skipped | convergence-disabled | single-analyser-only` 중 하나.",
77
+ "round2SkippedReasonNote": "(허용 값: `queue-empty`, `max-rounds-1`, `all-reverify-non-result`, `not-skipped`, `convergence-disabled`, `single-analyser-only`)",
79
78
  "singleRoundPrefix": "단일 라운드 —",
80
79
  "noRoundsNote": "재검증 라운드 미실행 (그룹핑 단계에서 전부 합의)."
81
80
  },
@@ -128,7 +127,7 @@
128
127
  "clarification": {
129
128
  "fillAndRerun": "답을 채우신 뒤 같은 phase 를 다시 실행:",
130
129
  "separateTerminalLabel": "별도 터미널",
131
- "columnGuide": "컬럼 가이드 (전체 정의는 `prompts/profiles/_common-contract.md §Clarification request policy` SSOT 참조):"
130
+ "columnGuide": "컬럼 설명:"
132
131
  },
133
132
  "followUpTasks": {
134
133
  "headingAside": "후속 작업"
@@ -82,7 +82,8 @@ _의존 정보 없음_
82
82
  If included, MUST use this exact heading literal `## Gantt Chart`.
83
83
  Render as ASCII inside a fenced ``` block with NO language tag.
84
84
  Mermaid / PlantUML / Graphviz are forbidden — see SKILL §"ASCII Gantt format".
85
- The axis is RELATIVE DAY-COUNTS (Day 1 / J1, …) — never calendar dates. -->
85
+ The axis is RELATIVE DAY-COUNTS (Day 1 / J1, …) — never calendar dates.
86
+ If day estimates are missing, omit this whole section and replace it with one blockquote: `> _Gantt Chart 생략: <reason>_` (SKILL §"MUST — emit a skip-reason note"). -->
86
87
 
87
88
  ```
88
89
  Day: 1 5 10 15 20 25 30
@@ -96,12 +97,7 @@ Phase 3
96
97
  <TASK-ID> (<size>) ████ (after <TASK-ID>)
97
98
  ```
98
99
 
99
- > 가로축은 **상대 일수** (Day 1 시작 = Phase 1 시작). 오늘 날짜·요일·달력일자 사용 금지.
100
- > 임계 경로(critical path) 항목은 `! crit` 주석, 추정 배분은 `est` 주석으로 표기.
101
- > 막대는 `█`(확정) + `░`(상한/불확실) 조합. 단일 mid-point bar 도 허용.
102
- > 데이터가 없어 이 섹션을 생략할 때는, `## Gantt Chart` 헤딩과 ASCII 블록 전체를
103
- > 다음 한 줄 blockquote로 대체한다 (SKILL §"MUST — emit a skip-reason note" 참조):
104
- > `> _Gantt Chart 생략: <구체 사유 — 예: "단일 task (DEV-9187) + XL effort 5–15d 범위로 정밀 day 추정 부재."_`
100
+ > 가로축은 **상대 일수**입니다 (Day 1 = Phase 1 시작). 범례: `! crit` = 임계 경로(critical path), `est` = 추정 배분, `█` = 확정 구간, `░` = 상한/불확실 구간.
105
101
 
106
102
  ---
107
103