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.
- package/docs/kr/architecture.md +2 -1
- package/docs/kr/cli.md +1 -1
- package/docs/kr/performance-improvement-plan-v2.md +27 -7
- package/docs/project-structure-overview.md +1 -0
- package/docs/superpowers/plans/2026-06-12-html-plan-approval.md +1000 -0
- package/docs/superpowers/specs/2026-06-12-html-plan-approval-design.md +85 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +3 -3
- package/runtime/agents/workers/codex-worker.md +5 -5
- package/runtime/agents/workers/gemini-worker.md +5 -5
- package/runtime/prompts/profiles/_implementation-executor.md +1 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -0
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- package/runtime/prompts/profiles/requirements-discovery.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +11 -2
- package/runtime/python/okstra_ctl/clarification_items.py +32 -22
- package/runtime/python/okstra_ctl/context_cost.py +32 -5
- package/runtime/python/okstra_ctl/md_table.py +56 -0
- package/runtime/python/okstra_ctl/render_final_report.py +8 -1
- package/runtime/python/okstra_ctl/report_views.py +218 -26
- package/runtime/python/okstra_ctl/run.py +4 -4
- package/runtime/python/okstra_ctl/user_response.py +55 -0
- package/runtime/python/okstra_ctl/wizard.py +116 -11
- package/runtime/python/okstra_token_usage/claude.py +22 -0
- package/runtime/python/okstra_token_usage/collect.py +51 -3
- package/runtime/python/okstra_token_usage/cursor.py +3 -1
- package/runtime/schemas/final-report-v1.0.schema.json +2 -1
- package/runtime/skills/okstra-convergence/SKILL.md +8 -4
- package/runtime/skills/okstra-inspect/SKILL.md +20 -0
- package/runtime/skills/okstra-report-writer/SKILL.md +4 -3
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/skills/okstra-team-contract/SKILL.md +1 -1
- package/runtime/templates/reports/final-report.template.md +57 -52
- package/runtime/templates/reports/report.css +21 -7
- package/runtime/templates/reports/report.js +56 -8
- package/runtime/templates/reports/user-response.template.md +15 -0
- package/runtime/validators/validate-implementation-plan-stages.py +9 -2
- package/runtime/validators/validate-report-views.py +2 -1
- package/runtime/validators/validate-run.py +6 -17
- package/runtime/validators/validate-schedule.py +9 -2
- 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
package/runtime/BUILD.json
CHANGED
package/runtime/agents/SKILL.md
CHANGED
|
@@ -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)
|
|
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>` —
|
|
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>`)
|
|
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
|
|
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>" "
|
|
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>" "
|
|
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>
|
|
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
|
|
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>" "
|
|
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>" "
|
|
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>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"""
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|