okstra 0.72.0 → 0.74.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.
Files changed (44) hide show
  1. package/docs/kr/architecture.md +2 -1
  2. package/docs/kr/cli.md +1 -1
  3. package/docs/kr/performance-improvement-plan-v2.md +27 -7
  4. package/docs/project-structure-overview.md +1 -0
  5. package/docs/superpowers/plans/2026-06-12-html-plan-approval.md +1000 -0
  6. package/docs/superpowers/specs/2026-06-12-html-plan-approval-design.md +85 -0
  7. package/package.json +1 -1
  8. package/runtime/BUILD.json +2 -2
  9. package/runtime/agents/SKILL.md +3 -3
  10. package/runtime/agents/workers/codex-worker.md +5 -5
  11. package/runtime/agents/workers/gemini-worker.md +5 -5
  12. package/runtime/prompts/profiles/_implementation-executor.md +1 -0
  13. package/runtime/prompts/profiles/_implementation-verifier.md +1 -0
  14. package/runtime/prompts/profiles/error-analysis.md +1 -1
  15. package/runtime/prompts/profiles/final-verification.md +1 -1
  16. package/runtime/prompts/profiles/implementation-planning.md +1 -1
  17. package/runtime/prompts/profiles/requirements-discovery.md +1 -1
  18. package/runtime/prompts/wizard/prompts.ko.json +11 -2
  19. package/runtime/python/okstra_ctl/clarification_items.py +32 -22
  20. package/runtime/python/okstra_ctl/context_cost.py +32 -5
  21. package/runtime/python/okstra_ctl/md_table.py +56 -0
  22. package/runtime/python/okstra_ctl/render_final_report.py +8 -1
  23. package/runtime/python/okstra_ctl/report_views.py +218 -26
  24. package/runtime/python/okstra_ctl/run.py +4 -4
  25. package/runtime/python/okstra_ctl/user_response.py +55 -0
  26. package/runtime/python/okstra_ctl/wizard.py +116 -11
  27. package/runtime/python/okstra_token_usage/claude.py +22 -0
  28. package/runtime/python/okstra_token_usage/collect.py +51 -3
  29. package/runtime/python/okstra_token_usage/cursor.py +3 -1
  30. package/runtime/schemas/final-report-v1.0.schema.json +2 -1
  31. package/runtime/skills/okstra-convergence/SKILL.md +8 -4
  32. package/runtime/skills/okstra-inspect/SKILL.md +20 -0
  33. package/runtime/skills/okstra-report-writer/SKILL.md +4 -3
  34. package/runtime/skills/okstra-run/SKILL.md +1 -1
  35. package/runtime/skills/okstra-team-contract/SKILL.md +1 -1
  36. package/runtime/templates/reports/final-report.template.md +57 -52
  37. package/runtime/templates/reports/report.css +21 -7
  38. package/runtime/templates/reports/report.js +56 -8
  39. package/runtime/templates/reports/user-response.template.md +15 -0
  40. package/runtime/validators/validate-implementation-plan-stages.py +9 -2
  41. package/runtime/validators/validate-report-views.py +2 -1
  42. package/runtime/validators/validate-run.py +6 -17
  43. package/runtime/validators/validate-schedule.py +9 -2
  44. package/runtime/validators/validate_improvement_report.py +2 -1
@@ -0,0 +1,85 @@
1
+ # final-report HTML 승인·옵션 선택 설계
2
+
3
+ - 날짜: 2026-06-12
4
+ - 상태: 설계 승인됨 (구현 전)
5
+ - 대상: implementation-planning final-report 의 HTML 뷰 + implementation 시작 마법사
6
+
7
+ ## 1. 배경과 목표
8
+
9
+ implementation-planning 보고서의 승인은 현재 (a) frontmatter `approved:` 수동 편집 또는 (b) CLI `--approve` 플래그로만 가능하고, 구현 옵션 선택은 `--implementation-option <name>` CLI 플래그뿐이다 (`scripts/okstra_ctl/run.py:305`, `scripts/okstra_ctl/run.py:744`). 사용자가 보고서를 검토하는 화면은 HTML 뷰인데, 그 화면에는 §1 clarification 폼만 있고 승인·옵션 선택 수단이 없다.
10
+
11
+ 목표: 사용자가 보고서 HTML 안에서 승인 의사와 구현 옵션을 기록하고, implementation 시작 시 그 기록이 확인 절차를 거쳐 기존 승인 경로로 적용되게 한다.
12
+
13
+ 설계 원칙 (기존 계약 유지):
14
+
15
+ - HTML 은 원본 md / data.json 을 **절대 직접 수정하지 않는다** — 기록은 user-response sidecar 로만 나간다 (`templates/reports/user-response.template.md:6`).
16
+ - data.json 이 SSOT — 적용은 data.json 플립 + md 재렌더 경로만 사용한다 (`scripts/okstra_ctl/run.py:222`).
17
+ - 승인 게이트 검증(`_validate_approved_plan`, `scripts/okstra_ctl/run.py:331`)은 무변경 — sidecar 는 검증을 우회하지 않는다.
18
+
19
+ ## 2. 기각한 대안
20
+
21
+ **§1 C-행 재사용안**: report-writer 가 승인/옵션을 §1 decision 행으로 발행해 기존 폼을 재사용하는 방법. §1 응답의 소비자는 resume-clarification(= planning 재실행)이라 적용 경로가 어긋나고, "§1 미해소 행 = 승인 차단" 게이트 규칙(`scripts/okstra_ctl/run.py:360`)과 자기모순이 생기므로 기각.
22
+
23
+ **run-prep 무확인 자동 적용안**: HTML 버튼 클릭이 사실상 승인 행위가 되어 명시적 승인 원칙에 어긋나므로 기각. 마법사 확인 단계를 거친다.
24
+
25
+ ## 3. 설계
26
+
27
+ ### 3.1 HTML 위젯 — "Plan Approval" 섹션
28
+
29
+ - 렌더 조건: `task-type == implementation-planning` **이고** sibling `.data.json` 이 존재할 때만. 둘 중 하나라도 아니면 섹션 자체를 생략한다.
30
+ - 데이터 소스: md 파싱이 아니라 sibling data.json 의 `implementationPlanning.optionCandidates[].name` 과 `recommendedOption.name` (`schemas/final-report-v1.0.schema.json:1062`, `schemas/final-report-v1.0.schema.json:1102`). 렌더 시점(`render_html_view`)에 Python 이 읽어 HTML 에 정적으로 박는다.
31
+ - 구성: 보고서 끝(footer 위)에
32
+ - 옵션 `<select>` — 후보 = optionCandidates 이름 목록, 기본 선택 = recommendedOption 이름에 "(권장)" 라벨.
33
+ - 승인 체크박스 — "이 plan 을 승인합니다".
34
+ - 활성 조건 (fail-closed, run-prep 게이트와 동일 기준): §1 의 `Blocks: approval` 행이 전부 `resolved`/`obsolete` 일 때만 활성. 미해소(open/answered) 행이 있으면 disabled + "§1 승인 차단 항목 N건 미해소 — 답변 후 resume-clarification 으로 다음 보고서에서 승인" 사유를 표시한다. 판정은 렌더 시 Python 이 §1 을 스캔(`scan_approval_gate` 재사용)해 박는다.
35
+ - Export: 기존 "Export user response" 버튼 하나에 합류 — 별도 버튼을 만들지 않는다.
36
+
37
+ ### 3.2 sidecar 스키마 확장
38
+
39
+ `user-response-<task-type>-<seq>.md` 본문 끝에 APPROVAL 블록 1개를 허용한다:
40
+
41
+ ```markdown
42
+ ## APPROVAL
43
+ - Approved: true
44
+ - Implementation-Option: <optionCandidates[].name 그대로>
45
+ ```
46
+
47
+ - 승인 체크 없이 Export 하면 블록 자체를 생략한다 (기존 출력과 동일).
48
+ - 옵션을 추천 그대로 두면 `Implementation-Option:` 라인을 생략한다 → 기존 Recommended 폴백 의미 유지 (`scripts/okstra_ctl/run.py:308`).
49
+ - Python `serialize_user_response` 와 JS `buildUserResponseMarkdown` 의 byte-identical 계약은 유지하고, 동일성 테스트를 APPROVAL 케이스로 확장한다 (`templates/reports/user-response.template.md:69`).
50
+
51
+ ### 3.2.1 user-responses/ 디렉토리 선생성
52
+
53
+ 사용자 확인(폼 입력 또는 승인)이 필요한 HTML 을 렌더하는 시점에 `render_html_view` 가 저장 대상 디렉토리 `runs/<task-type>/.../user-responses/` (reports/ 의 sibling) 를 함께 생성한다. 사용자가 Export 파일을 저장할 위치를 직접 만들 필요가 없어야 한다. HTML 을 생성하지 않는 보고서(인터랙티브 요소 없음)에는 만들지 않는다.
54
+
55
+ ### 3.3 소비 — implementation 마법사
56
+
57
+ `approved_plan` 단계(`skills/okstra-run/SKILL.md:126`)에서:
58
+
59
+ 1. 지정된 plan 이 `approved: false` 이고, 같은 run 의 `user-responses/` 에 source-report 와 seq 가 일치하며 APPROVAL 블록을 가진 sidecar 가 있으면, pick 단계를 노출한다:
60
+ - "HTML 승인 기록 발견(옵션: X) — 적용 (추천)" / "무시하고 진행" / "중단".
61
+ 2. "적용" 선택 시 기존 함수만 호출한다:
62
+ - `_set_data_json_approved_true_if_present` (`scripts/okstra_ctl/run.py:222`) — data.json 플립 + md 재렌더.
63
+ - sidecar 에 `Implementation-Option:` 이 있으면 `_apply_cli_implementation_option` (`scripts/okstra_ctl/run.py:744`).
64
+ - audit 라인에 HTML user-response 경유임을 명시 (기존 `--approve` audit 라인 형식 확장).
65
+ 3. 적용 후 게이트 검증(`_validate_approved_plan`)은 기존 그대로 수행된다.
66
+
67
+ ### 3.4 엣지 케이스
68
+
69
+ | 상황 | 처리 |
70
+ |---|---|
71
+ | sidecar 의 옵션 이름이 현 data.json optionCandidates 에 없음 | 적용 거부 + 에러 메시지에 유효 후보 나열 |
72
+ | sidecar seq ≠ 보고서 seq | 해당 sidecar 무시 (소스 보고서 기준) |
73
+ | APPROVAL 블록이 있는데 §1 차단 행 미해소 (조작/구버전 HTML) | 적용 시도해도 기존 게이트가 거부 — 추가 방어 불요 |
74
+ | plan 이 이미 `approved: true` | pick 단계 생략, 기존 흐름 그대로 |
75
+
76
+ ### 3.5 테스트
77
+
78
+ - `tests/test_report_views.py`: 위젯 렌더 조건 3종 — (a) planning + data.json + 차단 없음 → 활성 렌더, (b) 차단 행 존재 → disabled + 사유, (c) 타 task-type 또는 data.json 부재 → 섹션 생략.
79
+ - 직렬화 동일성: APPROVAL 블록 포함/생략 케이스에서 Python ↔ JS byte-identical.
80
+ - 마법사: sidecar 감지 → pick 노출, "적용" 경로가 data.json 플립 + 옵션 치환을 수행하는 단위 테스트.
81
+
82
+ ## 4. 범위 제외 (후속 과제)
83
+
84
+ - sidecar 없이 마법사 단독으로 옵션을 고르는 standalone pick 단계 — 이번 범위 아님 (현행 CLI 플래그 유지).
85
+ - implementation-planning 외 task-type 의 승인 위젯 — frontmatter `approved:` 를 소비하는 진입점이 implementation 뿐이므로 대상 없음.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.72.0",
3
+ "version": "0.74.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.72.0",
3
- "builtAt": "2026-06-11T07:01:57.638Z",
2
+ "package": "0.74.0",
3
+ "builtAt": "2026-06-11T18:11:19.238Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -42,7 +42,7 @@ This SKILL.md is the operating contract and phase index. Detailed procedures liv
42
42
  | 4. Execution | Spawn analysis workers (Teams preferred) | `okstra-team-contract` |
43
43
  | 5. Fallback | Sequential/background dispatch when Teams unavailable | `okstra-team-contract` |
44
44
  | 5.5 Convergence | Cross-verify findings across workers | `okstra-convergence` |
45
- | 5.6 Critic pass | (opt-in) reused-worker critic pass: coverage gaps (discovery/error-analysis/impl-planning) or acceptance devil's-advocate (final-verification), each verified one round | `okstra-convergence` "Coverage critic pass" / "Acceptance critic pass" |
45
+ | 5.6 Critic pass | (opt-in) reused-worker critic pass: coverage gaps (discovery/error-analysis/impl-planning) or acceptance devil's-advocate (final-verification). The critic dispatch fires concurrently with the first 5.5 reverify round (its input is fixed at Round 0); gap/blocker verification (one round) completes here | `okstra-convergence` "Coverage critic pass" / "Acceptance critic pass" |
46
46
  | 6. Synthesis | Dispatch Report writer worker, review draft. **For `implementation-planning`: then run the Phase 6 plan-body verification sub-step (see Phase 6 section below).** | `okstra-report-writer` + `okstra-convergence` (sub-step) |
47
47
  | 7. Persist | Run token-usage collector, update manifests, ask about residual tmux panes, then ask whether to disband the worker team (reconcile stale members + `TeamDelete` only on approval) | `okstra-report-writer` + `_common-contract.md` "Phase wrap-up" / "Run-end teammate teardown" |
48
48
 
@@ -94,7 +94,7 @@ Required checkpoints:
94
94
  - `PROGRESS: phase-5-poll pending=<n> done=<m>` — emitted on each wakeup while the pending set is non-empty.
95
95
  - `PROGRESS: phase-5-collect worker=<role> status=<terminal-status>` — once per worker, immediately after the result file is verified.
96
96
  - `PROGRESS: phase-5.5-convergence round=<N> queue=<count>` — at the start of each convergence round (Phase 5.5).
97
- - `PROGRESS: phase-5.6-critic provider=<provider> gaps=<n>` — when the coverage critic pass runs (Phase 5.6, opt-in). Omitted when `convergence.critic.enabled == false`.
97
+ - `PROGRESS: phase-5.6-critic provider=<provider> gaps=<n>` — after the critic result is collected (Phase 5.6, opt-in; the critic dispatch itself fires concurrently with the first 5.5 reverify round). Omitted when `convergence.critic.enabled == false`.
98
98
  - `PROGRESS: phase-6-synthesis dispatching report-writer-worker` — at the start of Phase 6.
99
99
  - `PROGRESS: phase-7-persist updating manifests` — at the start of Phase 7.
100
100
  - `PROGRESS: phase-7-teardown disbanding team` — after token-usage collection and after the pane-disposition prompt, only when the user approved worker teammate cleanup, immediately before reconciling stale members + `TeamDelete` (Teams mode only; see `_common-contract.md` "Run-end teammate teardown"). Skipped in the no-`team_name` fallback or when the user keeps the team.
@@ -224,7 +224,7 @@ Use agent and subagent names that map cleanly to the selected worker roles. Do n
224
224
 
225
225
  Spawn **analysis workers only** in the same turn (Phase 4 in Teams mode; Phase 5 with `run_in_background: true` and no `team_name` when Teams unavailable). Preserve exact roster, role labels, assigned models from the task bundle.
226
226
 
227
- **Agent `name` on dispatch (BLOCKING — token-usage attribution depends on it).** Every analysis-worker `Agent(...)` call MUST set `name: "<workerId>-worker"` — `name: "claude-worker"` / `name: "codex-worker"` / `name: "gemini-worker"` — exactly as the report-writer dispatch sets `name: "report-writer"` ([okstra-report-writer](./skills/okstra-report-writer/SKILL.md)). The Agent harness records this `name` as `agentName` in the subagent session jsonl, and the Phase 7 token collector matches each worker's session by that `agentName` (`okstra_token_usage/collect.py`). A worker dispatched **without** `name` produces a session with no `agentName`; the collector cannot attribute it and records the worker as `source: "unavailable"` even though the session exists and is team-tagged (observed in `dev-9692` error-analysis: `claude`/`codex` workers dispatched without `name` → both `unavailable`, while the named `report-writer` collected normally). Convergence reverify dispatches keep the prefix (`<workerId>-worker-reverify-r<N>`); implementation executor/verifier variants keep `<workerId>-worker` / `<workerId>-executor`.
227
+ **Agent `name` on dispatch (BLOCKING — token-usage attribution depends on it).** Every analysis-worker `Agent(...)` call MUST set `name: "<workerId>-worker"` — `name: "claude-worker"` / `name: "codex-worker"` / `name: "gemini-worker"` — exactly as the report-writer dispatch sets `name: "report-writer"` ([okstra-report-writer](./skills/okstra-report-writer/SKILL.md)). The Agent harness records this `name` as `agentName` in the subagent session jsonl, and the Phase 7 token collector matches each worker's session by that `agentName` (`okstra_token_usage/collect.py`). A worker dispatched **without** `name` produces a session with no `agentName`; the collector cannot attribute it and records the worker as `source: "unavailable"` even though the session exists and is team-tagged (observed in `dev-9692` error-analysis: `claude`/`codex` workers dispatched without `name` → both `unavailable`, while the named `report-writer` collected normally). Convergence reverify dispatches keep the prefix (`<workerId>-worker-reverify-r<N>`). For `implementation` and `final-verification` dispatches the `name` carries the **functional role** so the FleetView teammate pill and trace panes name the actual job instead of a generic `worker`: the Executor dispatch sets `name: "<workerId>-executor"` (e.g. `codex-executor`) and every verifier dispatch sets `name: "<workerId>-verifier"` (e.g. `claude-verifier`). These role suffixes still attribute correctly — the collector matches any `agentName` beginning `<workerId>-` (`okstra_token_usage/collect.py` `match_prefixes`). **For CLI (Codex / Gemini) executor/verifier dispatches, Lead MUST also inject a `**Pane role:** executor` (or `verifier`) line into the dispatched prompt body** — the wrapper subagent reads it and passes it as the wrapper's 5th `<role>` argument so the tmux trace pane title reads `<cli>-<role>-<pid>` instead of `<cli>-worker-<pid>` (see `agents/workers/_cli-wrapper-template.md`). Analysis-phase worker dispatches are unchanged: `name: "<workerId>-worker"`, no `**Pane role:**` line (the wrapper defaults to `worker`).
228
228
 
229
229
  **Agent `model:` on dispatch (BLOCKING — assignment is otherwise ignored).** The `Claude worker` `Agent(...)` call MUST set `model: "<family token of that role's modelExecutionValue>"` (`fable` / `opus` / `sonnet` / `haiku`), per [okstra-team-contract](./skills/okstra-team-contract/SKILL.md) "Model Assignment Rules" #3–#4. The claude-worker definition is `model: inherit`, so omitting this parameter makes the worker silently run on the lead's model instead of its manifest assignment — the assigned-vs-actual deviation. `Codex worker` / `Gemini worker` are exempt: their CLI model is applied via the wrapper's own `--model` argument, so leave their Agent `model:` at `inherit` (rule #5).
230
230
 
@@ -30,7 +30,7 @@ You are a Codex worker agent. Your job is to execute the OpenAI Codex CLI and re
30
30
  $HOME/.okstra/bin/okstra-codex-exec.sh "<absolute-project-root>" "<assigned-model-execution-value>" "<absolute-prompt-history-path>" [<absolute-worktree-path>] [<role>]
31
31
  ```
32
32
 
33
- The fifth argument `<role>` is folded into both the caller (worker) pane title `codex-<role>-<pid>` and the sibling trace-pane title `codex-<role>-<pid>-tail` (`<pid>` = the wrapper's PID, present so concurrent dispatches of the same role can be told apart). Pass the literal string `worker` for every dispatch from this subagent. The wrapper defaults to `worker` when the argument is omitted, but pass it explicitly so the dispatch is self-describing.
33
+ The fifth argument `<role>` is folded into both the caller (worker) pane title `codex-<role>-<pid>` and the sibling trace-pane title `codex-<role>-<pid>-tail` (`<pid>` = the wrapper's PID, present so concurrent dispatches of the same role can be told apart). Pass the value of the dispatch prompt's `**Pane role:**` line verbatim — Lead injects `**Pane role:** executor` on an `implementation` Executor dispatch and `**Pane role:** verifier` on an `implementation` / `final-verification` verifier dispatch, so the trace pane names the actual job (`codex-executor-<pid>` / `codex-verifier-<pid>`). When the prompt carries no `**Pane role:**` line (analysis phases), pass the literal `worker`. The wrapper defaults to `worker` when the argument is omitted, but pass it explicitly so the pane title is self-describing.
34
34
 
35
35
  The fourth argument is **mandatory for implementation phase** and optional otherwise. It must be the literal `EXECUTOR_WORKTREE_PATH` recorded in the run context; the wrapper forwards it to codex as `--add-dir`, which grants the codex sandbox write access to the worktree (where all implementation-phase mutations occur). Without it, codex's `workspace-write` sandbox is anchored only at `<project-root>` and rejects every Edit/Write that targets the worktree (EPERM), which is the failure pattern that originally motivated this argument.
36
36
 
@@ -75,9 +75,9 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
75
75
 
76
76
  **Dispatch (background, no foreground timeout):**
77
77
  ```bash
78
- $HOME/.okstra/bin/okstra-codex-exec.sh "<absolute-project-root>" "<assigned-model-execution-value>" "<absolute-prompt-history-path>" "<absolute-worktree-path>" "worker"
78
+ $HOME/.okstra/bin/okstra-codex-exec.sh "<absolute-project-root>" "<assigned-model-execution-value>" "<absolute-prompt-history-path>" "<absolute-worktree-path>" "<pane-role>"
79
79
  ```
80
- Call `Bash` with `run_in_background: true`. Capture the returned `bash_id` (a.k.a. `shell_id`). Pass the positional arguments verbatim — do NOT use environment variables, `cd`, `&&` chains, or pipes from `cat`. Substitute the literal extracted Project Root, model execution value, prompt-history path, and worktree path. The fourth argument is **mandatory for implementation phase** (extract from `EXECUTOR_WORKTREE_PATH` in the lead prompt's run context or the `**Worktree:**` / `cwd for every mutating command:` line) and **may be omitted only for non-implementation analysis phases** that do not mutate the worktree. The wrapper handles `-C`, `--add-dir`, `--model`, `--sandbox workspace-write`, the stdin redirect from the prompt file, and stderr suppression internally. Calling `codex exec` directly (without the wrapper) is an error in this skill: the redirect tokens disqualify the prefix match against `Bash(codex exec:*)` and produce a permission prompt every dispatch.
80
+ Call `Bash` with `run_in_background: true`. Capture the returned `bash_id` (a.k.a. `shell_id`). Pass the positional arguments verbatim — do NOT use environment variables, `cd`, `&&` chains, or pipes from `cat`. Substitute the literal extracted Project Root, model execution value, prompt-history path, and worktree path, plus the `**Pane role:**` value (`executor` / `verifier`, or `worker` when the line is absent). The fourth argument is **mandatory for implementation phase** (extract from `EXECUTOR_WORKTREE_PATH` in the lead prompt's run context or the `**Worktree:**` / `cwd for every mutating command:` line) and **may be omitted only for non-implementation analysis phases** that do not mutate the worktree. The wrapper handles `-C`, `--add-dir`, `--model`, `--sandbox workspace-write`, the stdin redirect from the prompt file, and stderr suppression internally. Calling `codex exec` directly (without the wrapper) is an error in this skill: the redirect tokens disqualify the prefix match against `Bash(codex exec:*)` and produce a permission prompt every dispatch.
81
81
 
82
82
  **Poll loop (BashOutput-only, 30-minute cap):**
83
83
  - Record `start_ts` at dispatch time via a single `Bash` call: `date +%s` (output captured).
@@ -139,7 +139,7 @@ This wrapper does NOT invoke MCP tools directly. MCP availability inside the Cod
139
139
  - Include context (code, diff, file paths) if provided.
140
140
  - For long prompts, dispatch through the wrapper with literal absolute paths (plus the worktree path for implementation phase):
141
141
  ```bash
142
- $HOME/.okstra/bin/okstra-codex-exec.sh "<literal-project-root>" "<assigned-model-execution-value>" "<literal-prompt-history-path>" "<literal-worktree-path>" "worker"
142
+ $HOME/.okstra/bin/okstra-codex-exec.sh "<literal-project-root>" "<assigned-model-execution-value>" "<literal-prompt-history-path>" "<literal-worktree-path>" "<pane-role>"
143
143
  ```
144
144
  - If the parent directory does not exist yet, create it before writing the prompt file.
145
145
 
@@ -214,7 +214,7 @@ and the run-level error log staying empty.
214
214
  --agent codex-worker --agent-role worker \
215
215
  --model "<assigned-model-execution-value>" \
216
216
  --error-type cli-failure \
217
- --command "$HOME/.okstra/bin/okstra-codex-exec.sh <project-root> <m> <prompt-path> <worktree-path> worker" \
217
+ --command "$HOME/.okstra/bin/okstra-codex-exec.sh <project-root> <m> <prompt-path> <worktree-path> <pane-role>" \
218
218
  --command-kind cli-invoke \
219
219
  --exit-code <N> --duration-ms <ms> \
220
220
  --message "<one-line summary>" \
@@ -30,7 +30,7 @@ You are a Gemini worker agent. Your job is to execute the Google Gemini CLI and
30
30
  $HOME/.okstra/bin/okstra-gemini-exec.sh "<absolute-project-root>" "<assigned-model-execution-value>" "<absolute-prompt-history-path>" [<absolute-worktree-path>] [<role>]
31
31
  ```
32
32
 
33
- The fifth argument `<role>` is folded into both the caller (worker) pane title `gemini-<role>-<pid>` and the sibling trace-pane title `gemini-<role>-<pid>-tail` (`<pid>` = the wrapper's PID, present so concurrent dispatches of the same role can be told apart). Pass the literal string `worker` for every dispatch from this subagent. The wrapper defaults to `worker` when the argument is omitted, but pass it explicitly so the dispatch is self-describing.
33
+ The fifth argument `<role>` is folded into both the caller (worker) pane title `gemini-<role>-<pid>` and the sibling trace-pane title `gemini-<role>-<pid>-tail` (`<pid>` = the wrapper's PID, present so concurrent dispatches of the same role can be told apart). Pass the value of the dispatch prompt's `**Pane role:**` line verbatim — Lead injects `**Pane role:** executor` on an `implementation` Executor dispatch and `**Pane role:** verifier` on an `implementation` / `final-verification` verifier dispatch, so the trace pane names the actual job (`gemini-executor-<pid>` / `gemini-verifier-<pid>`). When the prompt carries no `**Pane role:**` line (analysis phases), pass the literal `worker`. The wrapper defaults to `worker` when the argument is omitted, but pass it explicitly so the pane title is self-describing.
34
34
 
35
35
  The fourth argument is **mandatory for implementation phase** and optional otherwise. It must be the literal `EXECUTOR_WORKTREE_PATH` recorded in the run context; the wrapper appends it to gemini's `--include-directories` list so the model can both read and operate on the worktree alongside project-root.
36
36
 
@@ -75,9 +75,9 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
75
75
 
76
76
  **Dispatch (background, no foreground timeout):**
77
77
  ```bash
78
- $HOME/.okstra/bin/okstra-gemini-exec.sh "<absolute-project-root>" "<assigned-model-execution-value>" "<absolute-prompt-history-path>" "<absolute-worktree-path>" "worker"
78
+ $HOME/.okstra/bin/okstra-gemini-exec.sh "<absolute-project-root>" "<assigned-model-execution-value>" "<absolute-prompt-history-path>" "<absolute-worktree-path>" "<pane-role>"
79
79
  ```
80
- Call `Bash` with `run_in_background: true`. Capture the returned `bash_id` (a.k.a. `shell_id`). Pass the positional arguments verbatim — do NOT use environment variables, `cd`, `&&` chains, or pipes from `cat`. Substitute the literal extracted Project Root, model execution value, prompt-history path, and worktree path. The fourth argument is **mandatory for implementation phase** (extract from `EXECUTOR_WORKTREE_PATH` in the lead prompt's run context or the `**Worktree:**` / `cwd for every mutating command:` line) and **may be omitted only for non-implementation analysis phases** that do not mutate the worktree. The wrapper handles `-p -`, `-m`, `-o text`, `--include-directories`, the stdin redirect from the prompt file, and stderr suppression internally. Calling `gemini` directly (without the wrapper) is an error in this skill: the redirect tokens disqualify the prefix match against `Bash(gemini:*)` and produce a permission prompt every dispatch.
80
+ Call `Bash` with `run_in_background: true`. Capture the returned `bash_id` (a.k.a. `shell_id`). Pass the positional arguments verbatim — do NOT use environment variables, `cd`, `&&` chains, or pipes from `cat`. Substitute the literal extracted Project Root, model execution value, prompt-history path, and worktree path, plus the `**Pane role:**` value (`executor` / `verifier`, or `worker` when the line is absent). The fourth argument is **mandatory for implementation phase** (extract from `EXECUTOR_WORKTREE_PATH` in the lead prompt's run context or the `**Worktree:**` / `cwd for every mutating command:` line) and **may be omitted only for non-implementation analysis phases** that do not mutate the worktree. The wrapper handles `-p -`, `-m`, `-o text`, `--include-directories`, the stdin redirect from the prompt file, and stderr suppression internally. Calling `gemini` directly (without the wrapper) is an error in this skill: the redirect tokens disqualify the prefix match against `Bash(gemini:*)` and produce a permission prompt every dispatch.
81
81
 
82
82
  **Poll loop (BashOutput-only, 30-minute cap):**
83
83
  - Record `start_ts` at dispatch time via a single `Bash` call: `date +%s` (output captured).
@@ -139,7 +139,7 @@ This wrapper does NOT invoke MCP tools directly. MCP availability inside the Gem
139
139
  - Include context (code, diff, file paths) if provided.
140
140
  - For long prompts, dispatch through the wrapper with literal absolute paths (plus the worktree path for implementation phase):
141
141
  ```bash
142
- $HOME/.okstra/bin/okstra-gemini-exec.sh "<literal-project-root>" "<assigned-model-execution-value>" "<literal-prompt-history-path>" "<literal-worktree-path>" "worker"
142
+ $HOME/.okstra/bin/okstra-gemini-exec.sh "<literal-project-root>" "<assigned-model-execution-value>" "<literal-prompt-history-path>" "<literal-worktree-path>" "<pane-role>"
143
143
  ```
144
144
  - If the parent directory does not exist yet, create it before writing the prompt file.
145
145
 
@@ -214,7 +214,7 @@ and the run-level error log staying empty.
214
214
  --agent gemini-worker --agent-role worker \
215
215
  --model "<assigned-model-execution-value>" \
216
216
  --error-type cli-failure \
217
- --command "$HOME/.okstra/bin/okstra-gemini-exec.sh <project-root> <m> <prompt-path> <worktree-path> worker" \
217
+ --command "$HOME/.okstra/bin/okstra-gemini-exec.sh <project-root> <m> <prompt-path> <worktree-path> <pane-role>" \
218
218
  --command-kind cli-invoke \
219
219
  --exit-code <N> --duration-ms <ms> \
220
220
  --message "<one-line summary>" \
@@ -11,6 +11,7 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
11
11
 
12
12
  ## Executor role binding (carried over from the thin core)
13
13
 
14
+ - **Executor dispatch labelling.** When Lead dispatches the Executor it MUST set the Agent `name` to `<provider>-executor` (e.g. `codex-executor`, `claude-executor`) — NOT the generic `<provider>-worker` — so the FleetView teammate pill names the job; see `agents/SKILL.md` Phase 4 "Agent `name` on dispatch". For a `codex` / `gemini` Executor, Lead MUST additionally inject a `**Pane role:** executor` line into the dispatched prompt body so the CLI wrapper titles its tmux trace pane `<cli>-executor-<pid>` (the wrapper reads that line per `agents/workers/_cli-wrapper-template.md`). Token attribution is unaffected — the collector matches any `agentName` beginning `<provider>-`.
14
15
  - The `Executor` (bound in `implementation.md` thin core) is the **only worker permitted to use Edit / Write / state-mutating Bash commands** on project files. All other workers run read-only. When the executor provider is `codex` or `gemini`, the actual file mutation happens inside the executor CLI's own auto-edit mode (e.g. `codex exec --sandbox workspace-write`, gemini's equivalent) — not through Claude-side Edit/Write tools — but the safety rules in this sidecar still apply identically.
15
16
  - Worktree cwd handling — when the thin core's Task worktree block resolves status to `created` or `reused`, the Executor MUST run every Edit / Write / build / test / commit command with the worktree path as cwd. Treat it as `project_root` for the duration of this run. Do NOT mutate the caller's original checkout. Do NOT `cd` out of the worktree to reach files; if a file outside the worktree is needed, the dependency is a planning gap — record it in `Out-of-plan edits` and continue.
16
17
  - **How to set cwd per Bash call**: the Claude Bash tool inherits its cwd from the lead session, which is NOT the worktree. To put cwd-sensitive toolchains (`cargo`, `npm`, `pnpm`, `bun`, `pytest`, `make`, `go`) into the worktree, prefix the command with `cd {{EXECUTOR_WORKTREE_PATH}} && ` inside the same Bash invocation — e.g. `cd {{EXECUTOR_WORKTREE_PATH}} && cargo test -p foo`. **Never wrap in `bash -lc "..."` or `bash -c "..."`** — the wrapper hides the leading `cd` token from Claude Code's permission auto-allow layer (causing prompts on every call) without any safety benefit. For tools that accept an explicit working-directory flag (`git -C <path>`, `cargo --manifest-path`, `pytest --rootdir`), prefer that form over the `cd && ` chain. Edit / Write / Read tool calls already use absolute paths and need no cwd handling. The codex / gemini executor CLI wrappers (`okstra-codex-exec.sh -C`, `okstra-gemini-exec.sh --include-directories`) already inject worktree cwd at the CLI layer, so this rule applies primarily to the Claude executor.
@@ -9,6 +9,7 @@ at Phase 5, BEFORE constructing the verifier worker dispatch prompts.
9
9
 
10
10
  ## Verifier roles (resolved at run-prep time)
11
11
 
12
+ - **Verifier dispatch labelling.** When Lead dispatches a verifier (here, and identically in `final-verification`) it MUST set the Agent `name` to `<provider>-verifier` (e.g. `claude-verifier`, `codex-verifier`) — NOT the generic `<provider>-worker` — so the FleetView teammate pill names the job; see `agents/SKILL.md` Phase 4 "Agent `name` on dispatch". For a `codex` / `gemini` verifier, Lead MUST additionally inject a `**Pane role:** verifier` line into the dispatched prompt body so the CLI wrapper titles its tmux trace pane `<cli>-verifier-<pid>` (the wrapper reads that line per `agents/workers/_cli-wrapper-template.md`). Token attribution is unaffected — the collector matches any `agentName` beginning `<provider>-`.
12
13
  - The verifier slots are `Claude verifier` and `Codex verifier`, plus `Gemini verifier` **only when `gemini` is in the resolved `--workers` roster**. Every verifier in the resolved roster is dispatched regardless of which provider holds the executor role; the executor's own provider is run *separately* as a verifier (a fresh CLI session with no shared context) so that no verdict is produced from the same session that wrote the diff. Verifiers MUST NOT call Edit, Write, or any Bash command that mutates files outside the run's artifact directories. If a verifier wants a fix, it records the recommendation in its worker result; it does not apply the fix itself.
13
14
  - Session isolation — not model-variant divergence — is the primary self-review safeguard: each verifier is a separate CLI invocation with its own context window, so reusing the same model variant for executor and same-provider verifier is acceptable. Different model variants (e.g. executor=opus / Claude verifier=sonnet) remain recommended when available.
14
15
  - Phase-specific model defaults override the shared defaults: `Claude verifier`=`opus`, `Codex verifier`=`gpt-5.5`, `Gemini verifier`=`auto` (only when present in the roster). The `Executor`'s model is taken from the provider-specific worker model corresponding to `--executor`: claude→`--claude-model` (default `opus`), codex→`--codex-model` (default `gpt-5.5`), gemini→`--gemini-model` (default `auto`).
@@ -32,7 +32,7 @@
32
32
  - **Evidence note required inside `Statement`**: every clarification row includes `Evidence checked: <path:line>` or `Evidence checked: none — <reporter-only reason>` in the `Statement` cell. `none` is allowed ONLY when the row's nature is "only the reporter can answer this" (reporter-side data, business priority, environment they observed). A row with `none` that *could* have been answered by code or logs is a defect.
33
33
  - Cross-verification mode:
34
34
  - Phase 5.5 convergence runs in **adversarial mode** for this phase (`convergence.adversarial=true`). Verifiers actively try to refute each root-cause / reproduction claim by directly re-inspecting the cited code, logs, or config; the burden of proof sits on the claim. See `skills/okstra-convergence/SKILL.md` §"Adversarial Verification Mode". A single evidence-backed refutation prevents a finding from reaching consensus.
35
- - **Coverage critic (opt-in)**: when `convergence.critic.enabled=true` (chosen via the okstra-run picker or `--critic`), a reused-worker critic pass runs after convergence to surface missed findings; its gaps are merged only after a 1-round adversarial reverify. See `skills/okstra-convergence/SKILL.md` "Coverage critic pass".
35
+ - **Coverage critic (opt-in)**: when `convergence.critic.enabled=true` (chosen via the okstra-run picker or `--critic`), a reused-worker critic pass is dispatched concurrently with the first convergence reverify round to surface missed findings; its gaps are merged only after a 1-round adversarial reverify that follows convergence. See `skills/okstra-convergence/SKILL.md` "Coverage critic pass".
36
36
  - Non-goals:
37
37
  - implementation details unless they are necessary to validate the cause
38
38
  - **source code edits, builds, migrations, or deployments** — this run produces evidence and cause analysis only; the fix belongs to a later `implementation-planning` run followed by an `implementation` run
@@ -48,7 +48,7 @@
48
48
  4. **Verifier dissent preserved** — if workers reach different verdicts, the disagreement is visible in section 1.2; synthesis hides nothing.
49
49
  5. **No source-mutation audit** — scan the run's session transcripts for Edit / Write or state-mutating Bash commands that touch paths OUTSIDE `<PROJECT_ROOT>/.okstra/**` and outside the assigned run-artifact paths. Writes to worker prompts, audit sidecars, team-state, the final-report `data.json`, and rendered reports under the run directory are allowed okstra artifacts. Any source/schema/deployment mutation means the run has crossed into implementation and MUST be re-routed; do NOT silently strip the evidence.
50
50
  - Cross-verification mode:
51
- - **Acceptance critic (opt-in)**: when `convergence.critic.enabled=true` (chosen via the okstra-run picker or `--critic`), a reused-worker **acceptance devil's-advocate** pass runs after convergence to surface candidate acceptance blockers the verifiers may have missed. Each candidate is verified **confirm-or-downgrade**: confirmed → an `Acceptance Blockers` row (which, since `accepted` requires zero blockers, moves the verdict to `conditional-accept` / `blocked`); unconfirmed → a `Residual Risk` row (never dropped). See `skills/okstra-convergence/SKILL.md` "Acceptance critic pass (final-verification)".
51
+ - **Acceptance critic (opt-in)**: when `convergence.critic.enabled=true` (chosen via the okstra-run picker or `--critic`), a reused-worker **acceptance devil's-advocate** pass is dispatched concurrently with the first convergence reverify round to surface candidate acceptance blockers the verifiers may have missed; candidates are verified only after convergence completes. Each candidate is verified **confirm-or-downgrade**: confirmed → an `Acceptance Blockers` row (which, since `accepted` requires zero blockers, moves the verdict to `conditional-accept` / `blocked`); unconfirmed → a `Residual Risk` row (never dropped). See `skills/okstra-convergence/SKILL.md` "Acceptance critic pass (final-verification)".
52
52
  - Non-goals:
53
53
  - proposing unrelated refactors beyond the delivered scope
54
54
  - **source code edits, follow-up bug fixes, or scope expansion** — this run renders a verdict only; defects detected here become inputs to a new `error-analysis` or `implementation-planning` run
@@ -41,7 +41,7 @@
41
41
  - Cross-verification mode:
42
42
  - Phase 5.5 finding convergence runs in **adversarial mode** for this phase (`convergence.adversarial=true`). Verifiers actively try to refute each worker finding (requirement gap / risk / option) by re-inspecting its cited evidence; the burden of proof sits on the claim. See `skills/okstra-convergence/SKILL.md` §"Adversarial Verification Mode".
43
43
  - §5.5.9 plan-body verification runs with an **adversarial posture** (`skills/okstra-convergence/SKILL.md` §"Adversarial plan-body posture"): verifiers open and confirm every cited path / command and put the burden of proof on the plan. The gate threshold is unchanged — a *majority* `DISAGREE` (`majority-disagree`) is still required to block approval; a single dissent does not.
44
- - **Coverage critic (opt-in)**: when `convergence.critic.enabled=true` (chosen via the okstra-run picker or `--critic`), a reused-worker critic pass runs after convergence to surface missed findings; its gaps are merged only after a 1-round adversarial reverify. See `skills/okstra-convergence/SKILL.md` "Coverage critic pass".
44
+ - **Coverage critic (opt-in)**: when `convergence.critic.enabled=true` (chosen via the okstra-run picker or `--critic`), a reused-worker critic pass is dispatched concurrently with the first convergence reverify round to surface missed findings; its gaps are merged only after a 1-round adversarial reverify that follows convergence. See `skills/okstra-convergence/SKILL.md` "Coverage critic pass".
45
45
  - Non-goals:
46
46
  - code-level micro-optimization unless it changes the implementation approach
47
47
  - **source code edits of any kind** — this run produces a plan document only; Edit/Write on project source files is forbidden until the plan is approved and a separate `implementation` run starts
@@ -53,7 +53,7 @@
53
53
  - **Evidence note required inside `Statement`**: every clarification row includes `Evidence checked: <path:line>` or `Evidence checked: none — <human-only reason>` in the `Statement` cell. `none` is allowed ONLY when the row's nature is "only a human can answer this" (reporter intent, business priority, external authority). A row with `none` that *could* have been answered by the codebase is a defect.
54
54
  - Cross-verification mode:
55
55
  - Phase 5.5 convergence runs in **adversarial mode** for this phase (`convergence.adversarial=true`). Verifiers actively try to refute each worker's finding by directly re-inspecting the cited evidence; the burden of proof sits on the claim. See `skills/okstra-convergence/SKILL.md` §"Adversarial Verification Mode". A single evidence-backed refutation prevents a finding from reaching consensus.
56
- - **Coverage critic (opt-in)**: when `convergence.critic.enabled=true` (chosen via the okstra-run picker or `--critic`), a reused-worker critic pass runs after convergence to surface missed findings; its gaps are merged only after a 1-round adversarial reverify. See `skills/okstra-convergence/SKILL.md` "Coverage critic pass".
56
+ - **Coverage critic (opt-in)**: when `convergence.critic.enabled=true` (chosen via the okstra-run picker or `--critic`), a reused-worker critic pass is dispatched concurrently with the first convergence reverify round to surface missed findings; its gaps are merged only after a 1-round adversarial reverify that follows convergence. See `skills/okstra-convergence/SKILL.md` "Coverage critic pass".
57
57
  - Non-goals:
58
58
  - full implementation design unless it is required to decide the next phase
59
59
  - **source code edits, plan authoring, builds, or deployments** — this run only classifies the work and routes it; deeper analysis and planning belong to subsequent phases
@@ -217,18 +217,27 @@
217
217
  },
218
218
  "approve_plan_confirm": {
219
219
  "label": "이 플랜으로 진행할까요?\n {path}\n· 예 — 진행합니다. 플랜이 아직 승인 전이면 지금 data.json(정본) + 리포트를 함께 approved 로 처리한 뒤 진행합니다. (markdown 만 손으로 고치면 일관성 검증에서 거부되므로 이 경로로 승인하세요.)\n· 아니오 — 진행하지 않습니다.",
220
+ "html_approval_note": "\nHTML 승인 기록 발견: {sidecar}\n 선택 옵션: {option}\n· 예(기록 적용) — 승인과 함께 위 옵션을 frontmatter 에 기록합니다.",
221
+ "html_approval_note_default_option": "(추천 옵션 그대로)",
220
222
  "echo_template": "approve-plan: {value}",
221
223
  "options": {
222
224
  "yes": "예 — 승인하고 진행",
223
225
  "no": "아니오 — 진행하지 않음"
224
226
  },
227
+ "options_html_approval": {
228
+ "yes_apply": "예 — HTML 기록대로 승인 + 옵션 적용 (추천)",
229
+ "yes": "예 — 승인만 (HTML 기록 무시)",
230
+ "no": "아니오 — 진행하지 않음"
231
+ },
225
232
  "echo_variants": {
226
233
  "selected": "plan 선택: {path} — 다음 단계에서 승인·진행 여부를 확인합니다",
227
- "approved": "approved-plan: {path} (승인·진행 확인됨)"
234
+ "approved": "approved-plan: {path} (승인·진행 확인됨)",
235
+ "approved_with_option": "approved-plan: {path} (승인 + 옵션 `{option}` 적용됨)"
228
236
  },
229
237
  "errors": {
230
238
  "declined": "진행을 선택하지 않으면 다음 단계로 넘어갈 수 없습니다. 진행(예)하거나 위저드를 종료하세요.",
231
- "still_unapproved": "approve-plan: 승인 처리 후에도 승인 상태가 아닙니다 (data.json/markdown 불일치): {path}"
239
+ "still_unapproved": "approve-plan: 승인 처리 후에도 승인 상태가 아닙니다 (data.json/markdown 불일치): {path}",
240
+ "unknown_option": "HTML 승인 기록의 옵션 `{option}` 이 plan 의 Option Candidates 에 없습니다. 유효 후보: {candidates}. 보고서 HTML 에서 다시 Export 하거나 '예 — 승인만' 을 선택하세요."
232
241
  }
233
242
  },
234
243
  "stage_pick": {
@@ -27,6 +27,8 @@ from dataclasses import dataclass
27
27
  from pathlib import Path
28
28
  from typing import Optional
29
29
 
30
+ from okstra_ctl.md_table import is_separator_row, split_pipe_row
31
+
30
32
 
31
33
  # The final-report renderer (render_final_report.py:_inject_anchors) appends a
32
34
  # ` <a id="slug"></a>` scroll anchor to every heading. The §1 slice MUST tolerate
@@ -73,27 +75,9 @@ def _strip_backticks(cell: str) -> str:
73
75
 
74
76
 
75
77
  def _split_pipe_row(line: str) -> list[str]:
76
- """Split a markdown pipe-table row into cells (strip outer pipes + cell whitespace)."""
77
- stripped = line.strip()
78
- if stripped.startswith("|"):
79
- stripped = stripped[1:]
80
- if stripped.endswith("|"):
81
- stripped = stripped[:-1]
82
- return [_strip_backticks(c) for c in stripped.split("|")]
83
-
84
-
85
- def _is_separator_row(line: str) -> bool:
86
- """Detect ``|---|---|---|`` divider lines that separate header from body."""
87
- stripped = line.strip()
88
- if not stripped.startswith("|"):
89
- return False
90
- inner = stripped.strip("|").strip()
91
- # A separator cell is dashes optionally framed by colons; anything else
92
- # (real text) means this is a data row, not a separator.
93
- for cell in inner.split("|"):
94
- if not re.fullmatch(r"\s*:?-{2,}:?\s*", cell):
95
- return False
96
- return True
78
+ """``md_table.split_pipe_row`` + §1-specific cell normalization
79
+ (scroll-anchor removal, outer-backtick unwrap)."""
80
+ return [_strip_backticks(c) for c in split_pipe_row(line)]
97
81
 
98
82
 
99
83
  def _section_1_slice(report_text: str) -> Optional[str]:
@@ -193,7 +177,7 @@ def _walk_section_1_table(section: str) -> _Section1Table:
193
177
  if body_started:
194
178
  break
195
179
  continue
196
- if _is_separator_row(line):
180
+ if is_separator_row(line):
197
181
  body_started = True
198
182
  continue
199
183
  if not body_started:
@@ -299,3 +283,29 @@ def section_1_present_but_unparsed(report_text: str) -> bool:
299
283
  if SECTION_HEADING_PATTERN.search(report_text):
300
284
  return False
301
285
  return bool(_LOOSE_SECTION_1_RE.search(report_text))
286
+
287
+
288
+ def clarification_response_with_sidecars(source: Path) -> str:
289
+ """clarification-response 원문 뒤에 `user-responses/` 사이드카를 덧붙인 본문.
290
+
291
+ ``source`` 가 ``runs/<task-type>/reports/final-report-*.md`` 레이아웃일 때만
292
+ 형제 ``user-responses/`` 디렉토리를 찾는다(HTML 뷰의 `Export user response`
293
+ 가 내려준 파일을 사용자가 거기 저장). 그 외 경로·사이드카 부재 시 원문 그대로 —
294
+ 호출자는 항상 이 함수를 거쳐 instruction-set 사본을 쓰면 된다.
295
+ """
296
+ text = source.read_text(encoding="utf-8")
297
+ responses_dir = source.parent.parent / "user-responses"
298
+ if source.parent.name != "reports" or not responses_dir.is_dir():
299
+ return text
300
+ sidecars = sorted(
301
+ p for p in responses_dir.glob("user-response-*.md") if p.is_file()
302
+ )
303
+ if not sidecars:
304
+ return text
305
+ parts = [text.rstrip("\n"), "\n\n---\n\n# Attached User Responses\n"]
306
+ for sidecar in sidecars:
307
+ parts.append(
308
+ f"\n## {sidecar.name}\n\n"
309
+ f"{sidecar.read_text(encoding='utf-8').strip()}\n"
310
+ )
311
+ return "".join(parts)
@@ -306,19 +306,46 @@ def _analysis_worker_metric(task_root: Path, project_root: Path) -> dict:
306
306
  }
307
307
 
308
308
 
309
+ _SEQ_RESULT_RE = re.compile(r"-(\d{3,})\.md$")
310
+
311
+
312
+ def _current_seq_worker_results(run_dir: Path) -> list[Path]:
313
+ """현재 run seq 의 분석 worker 결과만 추린다.
314
+
315
+ 같은 worker-results 디렉토리에 seq-less 레거시(`codex-worker.md`)와 이전
316
+ seq 결과가 공존한다(관측: dev-9186 에 5월 레거시 72KB 혼입 → reportWriter
317
+ 표면 과대 측정). 실 dispatch 계약은 현재 seq 파일만 열거하므로 max seq 로
318
+ 스코핑한다."""
319
+ candidates = [
320
+ path for path in (run_dir / "worker-results").glob("*.md")
321
+ if "-audit-" not in path.name and "report-writer" not in path.name
322
+ ]
323
+ by_seq: dict[str, list[Path]] = {}
324
+ for path in candidates:
325
+ match = _SEQ_RESULT_RE.search(path.name)
326
+ if match:
327
+ by_seq.setdefault(match.group(1), []).append(path)
328
+ if not by_seq:
329
+ return []
330
+ return sorted(by_seq[max(by_seq)])
331
+
332
+
309
333
  def _report_writer_metric(run_dir: Path | None, task_root: Path, project_root: Path) -> dict:
310
334
  instruction_set = task_root / "instruction-set"
311
335
  files = [instruction_set / name for name in INPUT_FILES]
336
+ # 실 dispatch 계약(report-writer prompt 의 required reading)은 packet 이
337
+ # 있으면 analysis-material 대신 analysis-packet 을 읽는다 — worker metric
338
+ # 의 packet-primary 규칙과 동일.
339
+ packet = instruction_set / "analysis-packet.md"
340
+ if packet.is_file():
341
+ files = [path for path in files if path.name != "analysis-material.md"]
342
+ files.append(packet)
312
343
  files.extend([
313
344
  instruction_set / "final-report-template.md",
314
345
  instruction_set / "final-report-schema.json",
315
346
  ])
316
347
  if run_dir is not None:
317
- worker_results = run_dir / "worker-results"
318
- files.extend(
319
- path for path in worker_results.glob("*.md")
320
- if "-audit-" not in path.name and "report-writer" not in path.name
321
- )
348
+ files.extend(_current_seq_worker_results(run_dir))
322
349
  convergence = _latest_matching_file(
323
350
  run_dir / "state", f"convergence-{run_dir.name}-*.json"
324
351
  )
@@ -0,0 +1,56 @@
1
+ """Markdown pipe-table primitives — the single reference point for every
2
+ producer/consumer of final-report (and schedule) tables.
3
+
4
+ GFM treats every unescaped ``|`` inside a cell as a column boundary — even
5
+ inside inline-code spans — and renders ``\\|`` back as a literal ``|``.
6
+ Producers therefore escape cell text with ``escape_pipes`` (exposed as the
7
+ ``mdcell`` Jinja filter in ``render_final_report``), and consumers split rows
8
+ with ``split_pipe_row``, which honours the same ``\\|`` convention.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from typing import Any
14
+
15
+ # A `|` not preceded by a backslash: a real column boundary.
16
+ UNESCAPED_PIPE_RE = re.compile(r"(?<!\\)\|")
17
+
18
+ _SEPARATOR_CELL_RE = re.compile(r"\s*:?-{2,}:?\s*")
19
+
20
+
21
+ def escape_pipes(value: Any) -> str:
22
+ """Escape literal ``|`` as ``\\|`` for use inside a markdown table cell.
23
+
24
+ Idempotent: pipes that are already escaped are left alone, so re-rendering
25
+ (Phase 7) never stacks backslashes. ``None`` renders as the empty string —
26
+ the same contract as the template's ``or ''`` fallbacks.
27
+ """
28
+ if value is None:
29
+ return ""
30
+ return UNESCAPED_PIPE_RE.sub(r"\\|", str(value))
31
+
32
+
33
+ def split_pipe_row(line: str) -> list[str]:
34
+ """Split a markdown pipe-table row into whitespace-stripped cell texts.
35
+
36
+ Outer pipes are dropped; ``\\|`` is an escaped literal pipe — it never
37
+ splits a cell and is unescaped to ``|`` in the returned cell text.
38
+ """
39
+ stripped = line.strip()
40
+ if stripped.startswith("|"):
41
+ stripped = stripped[1:]
42
+ if stripped.endswith("|") and not stripped.endswith("\\|"):
43
+ stripped = stripped[:-1]
44
+ return [
45
+ cell.replace("\\|", "|").strip()
46
+ for cell in UNESCAPED_PIPE_RE.split(stripped)
47
+ ]
48
+
49
+
50
+ def is_separator_row(line: str) -> bool:
51
+ """Detect ``|---|:---:|`` divider lines that separate header from body."""
52
+ stripped = line.strip()
53
+ if not stripped.startswith("|"):
54
+ return False
55
+ inner = stripped.strip("|").strip()
56
+ return all(_SEPARATOR_CELL_RE.fullmatch(cell) for cell in inner.split("|"))
@@ -50,6 +50,7 @@ from jinja2 import ChainableUndefined, Environment, FileSystemLoader
50
50
 
51
51
  from okstra_ctl.final_report_schema import SchemaError, load_schema, validate as schema_validate
52
52
  from okstra_ctl.i18n import I18nError, SUPPORTED_LANGS, load_dictionary, make_jinja_global
53
+ from okstra_ctl.md_table import UNESCAPED_PIPE_RE, escape_pipes
53
54
  from okstra_ctl.models import UnknownModelError, resolve_model_metadata
54
55
 
55
56
 
@@ -177,7 +178,9 @@ def _code_line_mask(lines: list[str]) -> list[bool]:
177
178
  def _first_cell(line: str) -> str | None:
178
179
  if not line.lstrip().startswith("|"):
179
180
  return None
180
- parts = line.split("|")
181
+ # Split on unescaped pipes only — an `\|` escaped by the `mdcell` filter
182
+ # stays inside its cell instead of truncating it.
183
+ parts = UNESCAPED_PIPE_RE.split(line)
181
184
  return parts[1] if len(parts) >= 3 else None
182
185
 
183
186
 
@@ -411,6 +414,10 @@ def _build_environment(template_dir: Path) -> Environment:
411
414
  env.filters["yaml_scalar"] = _yaml_scalar
412
415
  env.filters["yaml_inline_list"] = _yaml_inline_list
413
416
  env.filters["model_detail"] = _model_detail
417
+ # `mdcell` escapes literal `|` in worker prose so a value like "PASS|FAIL"
418
+ # cannot split a markdown table row. Table-cell interpolations only —
419
+ # never code blocks / headings / prose, where `\|` would render verbatim.
420
+ env.filters["mdcell"] = escape_pipes
414
421
  return env
415
422
 
416
423