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.
- package/docs/superpowers/plans/2026-06-05-compact-markdown-report-tables.md +323 -0
- package/docs/superpowers/specs/2026-06-05-compact-markdown-report-tables-design.md +87 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- 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-trace-cleanup.sh +13 -1
- package/runtime/prompts/profiles/_common-contract.md +4 -4
- package/runtime/python/okstra_ctl/report_views.py +16 -29
- package/runtime/templates/project-docs/task-index.template.md +1 -8
- package/runtime/templates/reports/final-report.template.md +24 -28
- package/runtime/templates/reports/i18n/en.json +14 -15
- package/runtime/templates/reports/i18n/ko.json +14 -15
- package/runtime/templates/reports/schedule.template.md +3 -7
|
@@ -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 "<br>" 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 <br> by html.escape above; restore the tag.
|
|
235
|
+
out = out.replace("<br>", "<br>").replace("<br/>", "<br>").replace("<br />", "<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 `<br>` 아님),
|
|
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>`→`<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 통과 + 실제 재렌더 육안 확인.
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -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
|
|
193
|
-
#
|
|
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
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
#
|
|
206
|
-
#
|
|
207
|
-
caller_pane="
|
|
208
|
-
if
|
|
209
|
-
|
|
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 —
|
|
141
|
-
# `<RUN_DIR>` from the prompt path (paths.py SSOT) to
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
#
|
|
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="
|
|
149
|
-
if
|
|
150
|
-
|
|
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
|
-
|
|
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
|
|
34
|
-
- The lead MUST run once, at run start: `mkdir -p "<RUN_DIR>/state" &&
|
|
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.
|
|
45
|
-
3. Call `TeamDelete()
|
|
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
|
-
"""
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
#
|
|
433
|
-
|
|
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 Items — keep 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
|
-
|
|
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 <br>; restore the tag.
|
|
757
|
+
out = out.replace("<br>", "<br>").replace("<br/>", "<br>").replace("<br />", "<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
|
-
- 이 문서는
|
|
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
|
-
|
|
|
57
|
-
|
|
56
|
+
| {{ t("columns.recordMeta") }} | {{ t("columns.summary") }} |
|
|
57
|
+
|--------|------------|
|
|
58
58
|
{% for row in summary -%}
|
|
59
|
-
| {{ row.id }}
|
|
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
|
-
|
|
|
87
|
-
|
|
84
|
+
| {{ t("columns.recordMeta") }} | Summary of Key Findings |
|
|
85
|
+
|--------|-------------------------|
|
|
88
86
|
{% for row in executionStatus -%}
|
|
89
|
-
| {{ row.agent }}
|
|
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
|
-
|
|
|
134
|
-
|
|
131
|
+
| {{ t("columns.recordMeta") }} | Statement | Evidence (path:line / log / worker report) |
|
|
132
|
+
|--------|-----------|---------------------------------------------|
|
|
135
133
|
{% for row in crossVerification.consensus -%}
|
|
136
|
-
| {{ row.id }}
|
|
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
|
-
|
|
|
148
|
-
|
|
145
|
+
| {{ t("columns.recordMeta") }} | Disagreement | Evidence |
|
|
146
|
+
|--------|--------------|----------|
|
|
149
147
|
{% for row in crossVerification.differences -%}
|
|
150
|
-
| {{ row.id }}
|
|
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
|
-
|
|
|
174
|
-
|
|
171
|
+
| {{ t("columns.recordMeta") }} | Evidence |
|
|
172
|
+
|--------|----------|
|
|
175
173
|
{% for row in evidence.primary -%}
|
|
176
|
-
| {{ row.id }}
|
|
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
|
-
|
|
|
188
|
-
|
|
185
|
+
| {{ t("columns.recordMeta") }} | Hypothesis or supporting evidence | Source / confidence |
|
|
186
|
+
|--------|-----------------------------------|---------------------|
|
|
189
187
|
{% for row in evidence.secondary -%}
|
|
190
|
-
| {{ row.id }}
|
|
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
|
-
|
|
|
200
|
-
|
|
197
|
+
| {{ t("columns.recordMeta") }} | Item | Risk if ignored | Mitigation Owner |
|
|
198
|
+
|--------|------|-----------------|------------------|
|
|
201
199
|
{% for row in missingInformation -%}
|
|
202
|
-
| {{ row.id }}
|
|
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
|
-
|
|
|
602
|
-
|
|
597
|
+
| {{ t("columns.recordMeta") }} | Title | Scope (files/areas) | Reason / Why deferred |
|
|
598
|
+
|--------|-------|---------------------|------------------------|
|
|
603
599
|
{% for row in followUpTasks -%}
|
|
604
|
-
| {{ row.id }}
|
|
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
|
|
42
|
-
"clarificationCarryIn": "
|
|
43
|
-
"ticketCoverage": "
|
|
44
|
-
"executionStatus": "
|
|
45
|
-
"sourceItemsRule": "`Source items`
|
|
46
|
-
"
|
|
47
|
-
"
|
|
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": "
|
|
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": "
|
|
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
|
|
74
|
+
"sourceItemsColumnNote": "The `Source items` column is described in §1.1."
|
|
76
75
|
},
|
|
77
76
|
"roundHistory": {
|
|
78
|
-
"round2SkippedReasonNote": "
|
|
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
|
|
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": "한눈에 보는 결과
|
|
42
|
-
"clarificationCarryIn": "이전 보고서의
|
|
43
|
-
"ticketCoverage": "
|
|
44
|
-
"executionStatus": "각 worker 의
|
|
45
|
-
"sourceItemsRule": "`Source items`
|
|
46
|
-
"
|
|
47
|
-
"
|
|
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": "
|
|
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": "최종 결론과 권장
|
|
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`
|
|
74
|
+
"sourceItemsColumnNote": "`Source items` 열 설명은 §1.1 과 동일합니다."
|
|
76
75
|
},
|
|
77
76
|
"roundHistory": {
|
|
78
|
-
"round2SkippedReasonNote": "
|
|
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": "컬럼
|
|
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
|
-
> 가로축은 **상대
|
|
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
|
|