okstra 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/docs/kr/architecture.md +8 -8
  2. package/docs/kr/cli.md +2 -2
  3. package/docs/project-structure-overview.md +3 -3
  4. package/docs/superpowers/plans/2026-06-05-compact-markdown-report-tables.md +323 -0
  5. package/docs/superpowers/plans/2026-06-05-wizard-batch-prompts.md +559 -0
  6. package/docs/superpowers/specs/2026-06-05-compact-markdown-report-tables-design.md +87 -0
  7. package/docs/superpowers/specs/2026-06-05-wizard-batch-prompts-design.md +121 -0
  8. package/docs/task-process/error-analysis.md +1 -1
  9. package/docs/task-process/final-verification.md +1 -1
  10. package/docs/task-process/release-handoff.md +1 -1
  11. package/docs/task-process/requirements-discovery.md +1 -1
  12. package/package.json +1 -1
  13. package/runtime/BUILD.json +2 -2
  14. package/runtime/agents/SKILL.md +3 -3
  15. package/runtime/agents/workers/claude-worker.md +1 -1
  16. package/runtime/agents/workers/codex-worker.md +1 -1
  17. package/runtime/agents/workers/gemini-worker.md +1 -1
  18. package/runtime/agents/workers/report-writer-worker.md +3 -3
  19. package/runtime/bin/lib/okstra/tmux-pane.sh +40 -0
  20. package/runtime/bin/okstra-codex-exec.sh +17 -21
  21. package/runtime/bin/okstra-gemini-exec.sh +12 -15
  22. package/runtime/bin/okstra-render-report-views.py +1 -1
  23. package/runtime/bin/okstra-trace-cleanup.sh +13 -1
  24. package/runtime/prompts/launch.template.md +1 -1
  25. package/runtime/prompts/profiles/_common-contract.md +15 -15
  26. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
  27. package/runtime/prompts/profiles/_implementation-executor.md +1 -1
  28. package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
  29. package/runtime/prompts/profiles/error-analysis.md +1 -1
  30. package/runtime/prompts/profiles/final-verification.md +2 -2
  31. package/runtime/prompts/profiles/implementation-planning.md +9 -9
  32. package/runtime/prompts/profiles/improvement-discovery.md +5 -5
  33. package/runtime/prompts/profiles/release-handoff.md +2 -2
  34. package/runtime/prompts/profiles/requirements-discovery.md +2 -2
  35. package/runtime/python/okstra_ctl/clarification_items.py +11 -11
  36. package/runtime/python/okstra_ctl/render.py +1 -1
  37. package/runtime/python/okstra_ctl/render_final_report.py +1 -1
  38. package/runtime/python/okstra_ctl/report_views.py +26 -39
  39. package/runtime/python/okstra_ctl/run.py +3 -3
  40. package/runtime/python/okstra_ctl/wizard.py +90 -3
  41. package/runtime/python/okstra_ctl/workflow.py +1 -1
  42. package/runtime/skills/okstra-brief/SKILL.md +1 -1
  43. package/runtime/skills/okstra-convergence/SKILL.md +8 -8
  44. package/runtime/skills/okstra-report-writer/SKILL.md +22 -22
  45. package/runtime/skills/okstra-run/SKILL.md +2 -0
  46. package/runtime/skills/okstra-team-contract/SKILL.md +1 -1
  47. package/runtime/templates/project-docs/task-index.template.md +1 -8
  48. package/runtime/templates/reports/final-report.template.md +194 -198
  49. package/runtime/templates/reports/i18n/en.json +16 -17
  50. package/runtime/templates/reports/i18n/ko.json +16 -17
  51. package/runtime/templates/reports/implementation-planning-input.template.md +1 -1
  52. package/runtime/templates/reports/release-handoff-input.template.md +1 -1
  53. package/runtime/templates/reports/schedule.template.md +3 -7
  54. package/runtime/templates/reports/user-response.template.md +1 -1
  55. package/runtime/templates/worker-prompt-preamble.md +1 -1
  56. package/runtime/validators/lib/fixtures.sh +2 -2
  57. package/runtime/validators/validate-implementation-plan-stages.py +9 -9
  58. package/runtime/validators/validate-report-views.py +10 -10
  59. package/runtime/validators/validate-run.py +36 -36
  60. package/runtime/validators/validate_improvement_report.py +8 -8
@@ -83,7 +83,7 @@ A mocked unit test cannot observe the SQL a query builder actually emits — `co
83
83
  - **Requirement when fired.** The verifier MUST reproduce a real-DB execution: run the `db-test` tier (Tier 1 = plan `validation` db step; else Tier 2 = `project.json.qaCommands.db-test`) against a **local / replica** datastore (same engine + schema — never shared / staging / prod, consistent with the verifier forbidden-actions list) and record its exact command + exit code. A mock, an in-memory shim that does not parse real SQL, or static reasoning does NOT satisfy this.
84
84
  - **No `db-test` command available → blocking, not a passive skip.** If neither tier declares a `db-test` command, the verifier records the blocking finding `db-test not configured — DB change unverified (mock-only)` and sets the verdict to `FAIL`; it MUST NOT emit only the passive `qa-command not configured` note and pass. Recommended fix: declare a `db-test` command in `project.json.qaCommands` or the plan's validation set.
85
85
  - **Mock-only evidence → unverified.** If the diff's only DB coverage is mocked, the verifier labels the DB portion `정적 분석상 …, 미검증(실행 안 함)` (never `검증됨`), records it as a blocking finding, and sets `FAIL`. Never downplay the real run as "too heavy / static proof suffices".
86
- - **Surface it at every layer.** The finding is copied verbatim into the verifier result and MUST survive into the final report's `## 1.` and Verdict Card, so the user sees the DB-unverified state continuously — it is the load-bearing reason a downstream `final-verification` cannot reach `accepted` and `release-handoff` cannot push.
86
+ - **Surface it at every layer.** The finding is copied verbatim into the verifier result and MUST survive into the final report's `## 6.` and Verdict Card, so the user sees the DB-unverified state continuously — it is the load-bearing reason a downstream `final-verification` cannot reach `accepted` and `release-handoff` cannot push.
87
87
 
88
88
  ## All-verifier-failure policy
89
89
 
@@ -25,7 +25,7 @@
25
25
  - uncertainty boundaries
26
26
  - practical next diagnostic steps
27
27
  - Clarification request policy (phase-specific addenda — shared policy is in `_common-contract.md`):
28
- - if any blocking uncertainty remains at the time of writing the final report, populate `## 5. Clarification Items` in `final-report-template.md` (a single unified table; `Blocks=next-phase` for items the next run cannot start without)
28
+ - if any blocking uncertainty remains at the time of writing the final report, populate `## 1. Clarification Items` in `final-report-template.md` (a single unified table; `Blocks=next-phase` for items the next run cannot start without)
29
29
  - prefer plain Korean over abbreviations (e.g. write "초당 평균 요청 수" instead of "QPS", "재현 절차" instead of "repro")
30
30
  - every clarification row carries a recommended answer + one-line rationale inside the `Expected form` cell; rows that lack a recommendation are rejected as half-formed.
31
31
  - **Codebase-first ambiguity resolution (defect rule)**: any ambiguity about repro, file behavior, or symbol semantics that can be answered by `Read` / `Grep` / log inspection MUST be resolved that way and recorded with file:line (or log-line) evidence. Writing a clarification row for something the codebase or shipped logs already answer is a defect of this phase.
@@ -29,7 +29,7 @@
29
29
  - if the cited implementation report is missing, lacks commits for delivered code changes, or the current checkout does not match the implementation report's commit list / diff summary, the run MUST end with status `blocked` and route back to `implementation` or `implementation-planning` rather than verifying an ambiguous target.
30
30
  - Required deliverable shape (final report, in addition to the standard sections):
31
31
  - **Source Implementation Report**: relative path of the originating `implementation` final-report file, the quoted commit list / diff summary used as the verification target, the worktree path inspected, and the base/head SHAs captured at run start. The lead injects this same target snapshot into every analyser prompt (`**Worktree:** / **Verification base ref:** / **Verification head SHA:** / **Verification diff stat:**`); a worker that cannot confirm its analysis ran against that exact head MUST record a `tool-failure` rather than verify an ambiguous target.
32
- - **Verdict vocabulary**: Section 2 (`Final Verdict`) MUST include a `Verdict Token` field whose value is exactly one of `accepted`, `conditional-accept`, or `blocked`. `conditional-accept` requires an explicit, exhaustive list of conditions; ambiguous verdicts ("looks good", "mostly ready") are not allowed. Each condition MUST be recorded as a row in the **Conditional Acceptance Conditions** deliverable (`id` `CA-NNN`, `condition`, `evidenceRequired`, `blocksReleaseHandoff`). The validator enforces verdict↔deliverable consistency: `accepted` ⇒ zero acceptance blockers, `blocked` ⇒ at least one, `conditional-accept` ⇒ at least one condition, and a `release-handoff` routing recommendation is allowed only when the verdict is `accepted`.
32
+ - **Verdict vocabulary**: Section 7 (`Final Verdict`) MUST include a `Verdict Token` field whose value is exactly one of `accepted`, `conditional-accept`, or `blocked`. `conditional-accept` requires an explicit, exhaustive list of conditions; ambiguous verdicts ("looks good", "mostly ready") are not allowed. Each condition MUST be recorded as a row in the **Conditional Acceptance Conditions** deliverable (`id` `CA-NNN`, `condition`, `evidenceRequired`, `blocksReleaseHandoff`). The validator enforces verdict↔deliverable consistency: `accepted` ⇒ zero acceptance blockers, `blocked` ⇒ at least one, `conditional-accept` ⇒ at least one condition, and a `release-handoff` routing recommendation is allowed only when the verdict is `accepted`.
33
33
  - **Acceptance Blockers block** (under section 4): one row per blocker with `id`, `severity` (`critical` / `major` / `minor`), evidence (file path, log excerpt, or test output), and the recommended follow-up phase (`error-analysis` or `implementation-planning`). Empty block is acceptable and preferred — render the single line `- No acceptance blockers found.`
34
34
  - **Residual Risk block** (under section 4): risks that are not blockers but should be tracked, each with mitigation owner and a trigger that would escalate them to a blocker.
35
35
  - **Validation Evidence**: for every requirement in the originating plan or task brief, cite the artifact (commit SHA, test output, log line, MCP SELECT result) that demonstrates coverage. Paraphrased "verified" claims without an artifact are rejected.
@@ -37,7 +37,7 @@
37
37
  - **Two-tier command lookup (shared with `implementation`):** when this phase performs its own independent re-validation, the command source is exactly the same two tiers `implementation` verifiers use — Tier 1 is the originating task brief / approved plan's `validation` set, Tier 2 is `<PROJECT_ROOT>/.okstra/project.json` under `qaCommands`. Auto-detecting tools from manifest files is forbidden; missing tiers are recorded as `qa-command not configured: <category>` and do NOT trigger a guess. The `cmd` deny-list (`--fix`, `--write`, ` -w`, ` -u`, `--snapshot-update`, `INSTA_UPDATE=<not-no>`, `cargo update`, `npm install` without `ci`, etc.) is enforced identically. NOTE: runtime fail-fast validation (`okstra_ctl.qa_commands.validate_qa_commands`) only fires at `--task-type implementation` run-prep, so this phase MUST self-check each `qaCommands` entry against the deny-list before executing it — if a denied token is present, skip the command and record it as a `Read-only command log` line `qa-command rejected (denied token: <token>): <label>`.
38
38
  - **Routing recommendation**: the next safe phase — one of `release-handoff`, `done`, `error-analysis`, `implementation-planning` — tied to the verdict and blocker list. `release-handoff` is allowed ONLY when the Verdict Token is `accepted`.
39
39
  - Clarification request policy (phase-specific addendum — shared policy is in `_common-contract.md`):
40
- - populate `## 5. Clarification Items` only when a blocker hinges on information only the user can supply (deployment intent, intended target environment, business-rule interpretation); use `Blocks=next-phase` for items that gate continuing to release-handoff
40
+ - populate `## 1. Clarification Items` only when a blocker hinges on information only the user can supply (deployment intent, intended target environment, business-rule interpretation); use `Blocks=next-phase` for items that gate continuing to release-handoff
41
41
  - Self-review pass before finalising the report (`Claude lead` runs this; do not delegate to a generic subagent):
42
42
  1. **Verdict precision** — section 2 includes `Verdict Token` with one of the three allowed verdict tokens; `conditional-accept` lists every condition as an actionable item.
43
43
  2. **Blocker traceability** — every blocker cites a concrete artifact (file:line, log excerpt, test exit code, MCP SELECT). Blockers without evidence are demoted to residual risk or removed.
@@ -17,7 +17,7 @@
17
17
  - inspect the current state of every file the task names (or the closest matching files if names are stale) — record current responsibilities, public interfaces, and known coupling points
18
18
  - skim recent commits touching those files (`git log -- <path>`) to surface in-flight work or contested areas
19
19
  - **codebase-first ambiguity resolution**: any ambiguity that can be answered by `Read` / `Grep` MUST be resolved that way and recorded with file:line evidence. Only ambiguities that genuinely require a human decision are escalated as `Clarification Items` rows. Writing a clarification row for something the code already answers is a defect of this phase.
20
- - flag any requirement that is ambiguous, contradictory, or missing success criteria — register each one as a row in the report's `## 5. Clarification Items` table with `Blocks=approval` instead of guessing
20
+ - flag any requirement that is ambiguous, contradictory, or missing success criteria — register each one as a row in the report's `## 1. Clarification Items` table with `Blocks=approval` instead of guessing
21
21
  - read `<PROJECT_ROOT>/.okstra/glossary.md` and `<PROJECT_ROOT>/.okstra/decisions/` titles if present. Absent okstra memory files are the normal state — do not error. Treat the brief's `terminology:*` resolutions from `requirements-discovery` (if any) as authoritative; if missing, resolve any remaining fuzzy term as a `Blocks=approval` clarification row.
22
22
  - Primary focus areas:
23
23
  - requirement gaps
@@ -39,7 +39,7 @@
39
39
  - The YAML frontmatter `approved: true|false` field is the only authorised approval gate. report-writer always emits `approved: false`. The user clears it either by (a) editing the frontmatter line to `approved: true` directly, or (b) invoking the next phase with `--approve` so the CLI flips the frontmatter on the user's behalf. `okstra_ctl.run._validate_approved_plan` reads this field and refuses entry until it is `true`.
40
40
  - Cross-verification mode:
41
41
  - 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".
42
- - §4.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.
42
+ - §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.
43
43
  - **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
44
  - Non-goals:
45
45
  - code-level micro-optimization unless it changes the implementation approach
@@ -55,7 +55,7 @@
55
55
  - The final report MUST include section headings containing each of the following exact strings: `Option Candidates`, `Trade-off`, `Recommended Option`, `Stage Map`, `Stage Exit Contract`, `Stage Validation`, `Dependency`, `Validation Checklist`, `Rollback`. (Approval is no longer a body section — it is the YAML frontmatter `approved` field.)
56
56
  - Korean translations are allowed in parentheses (e.g. `### Recommended Option (권장 옵션)`), but the English keyword must be present verbatim in the heading line.
57
57
  - The shape and ordering follow `final-report-template.md` section 4.5 (`Implementation Plan Deliverables`). Do NOT translate the heading keywords — `validators/validate-run.py` does substring matching on the raw report text and 7-of-8 missing strings is a real, repeatedly observed failure mode (root cause: writer translated the headings to Korean).
58
- - Beyond substring matching, when the Plan Body Verification gate result is `passed` / `passed-with-dissent`, `validators/validate-run.py` runs the **structural** Stage Map validator (`validators/validate-implementation-plan-stages.py`) at the planning boundary — the exact `## 4.5 Stage Map` heading, each `## 4.5.<i> Stage <i>:` section with its four required subsections, the per-stage effective step count (≤6), and the `depends-on` DAG are all enforced here, not deferred to the `implementation` entry gate.
58
+ - Beyond substring matching, when the Plan Body Verification gate result is `passed` / `passed-with-dissent`, `validators/validate-run.py` runs the **structural** Stage Map validator (`validators/validate-implementation-plan-stages.py`) at the planning boundary — the exact `## 5.5 Stage Map` heading, each `## 5.5.<i> Stage <i>:` section with its four required subsections, the per-stage effective step count (≤6), and the `depends-on` DAG are all enforced here, not deferred to the `implementation` entry gate.
59
59
  - Required deliverable shape (final report, in addition to the standard sections):
60
60
  - at least two implementation options. **Each option must include**:
61
61
  - **File Structure**: an explicit list of files to create / modify / delete with each file's responsibility (one-line each). Use the form `Create: path — responsibility` / `Modify: path:line-range — change summary` / `Delete: path — reason`.
@@ -64,7 +64,7 @@
64
64
  - trade-off matrix across options (rows = options, columns at minimum: complexity, risk, reversibility, test coverage cost, rollout cost)
65
65
  - recommended option with rationale tied to the design principles above
66
66
  - **Stage Map (mandatory — always emitted, even when N=1):** a table of all stages with `stage | title | depends-on | step-count | exit-contract-summary`. `depends-on` is `(none)` or a comma-separated stage number list. Stages with `depends-on (none)` can be implemented in parallel by two simultaneous `implementation` runs.
67
- - **Per-stage subsections** (`## 4.5.<i> Stage <i>: <title>` for each `i`), each containing the four required subsections:
67
+ - **Per-stage subsections** (`## 5.5.<i> Stage <i>: <title>` for each `i`), each containing the four required subsections:
68
68
  - `### Carry-In` — for `depends-on (none)`: task-brief only. Otherwise: each depended-on stage's static exit contract + runtime sidecar path `runs/<impl-key>/carry/stage-<i>.json` placeholder.
69
69
  - `### Stepwise Execution Order` — bite-sized table with `step | action | files | command | expected`. **Effective row count ≤ 6** (excluding header / divider / blank). Each step is one action completable in 2–5 minutes; for code steps include actual code or diff sketch; prefer TDD ordering (failing test → implementation → green → commit).
70
70
  - `### Stage Exit Contract` — predicted added/modified files, newly exposed identifiers/types/endpoints, downstream-usable resources.
@@ -76,9 +76,9 @@
76
76
  - validation checklist (pre / mid / post) — each item is an exact command or observable outcome
77
77
  - rollback strategy — exact revert path (commits, flags, migrations) and the signal that triggers rollback
78
78
  - the YAML frontmatter MUST include the line `approved: false` (report-writer always emits the unflipped value). The user authorises the next `implementation` run by flipping it to `approved: true` (manual edit or `--approve` CLI). Do NOT recreate any `User Approval Request` body block — the validator fails reports that contain one (see `validators/validate-run.py` deprecated patterns).
79
- - **the frontmatter `approved: false` line is rendered unconditionally; if the plan-body verification gate (§4.5.9) returns `blocked-by-disagreement` or `aborted-non-result`, the writer MUST keep `approved: false` and the validator refuses any report that ships with `approved: true` under such a gate result.**
80
- - every ambiguity flagged during pre-planning that the user must resolve before approval registered as a `Blocks=approval` row in the `## 5. Clarification Items` table (do NOT create a separate `Open Questions` block under `4.5.x` — the unified table is the single home)
81
- - **§4.5.9 Plan Body Verification (BLOCKING).** After report-writer finishes the draft, the lead MUST run a worker peer-review round on the consolidated plan body (sections 4.5.1 – 4.5.7) and populate `### 4.5.9 Plan Body Verification` in the final report. The round protocol, plan-item ID scheme (`P-Opt-*` / `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*`), verdict semantics, gate-result classification, and dissent log format are defined in `skills/okstra-convergence/SKILL.md` "Plan-body verification mode". The four gate-result values are `passed`, `passed-with-dissent`, `blocked-by-disagreement`, `aborted-non-result`. When the gate would have been `blocked-by-disagreement` or `aborted-non-result`, the lead MUST NOT silently flip it to one of the passing values to "unblock" the run — that is a contract violation. When `convergence.adversarial=true` (the default for this phase), this round uses the adversarial posture — verifiers confirm cited paths/commands and the burden of proof is on the plan — but the gate threshold stays `majority-disagree` (see that skill's §"Adversarial plan-body posture").
79
+ - **the frontmatter `approved: false` line is rendered unconditionally; if the plan-body verification gate (§5.5.9) returns `blocked-by-disagreement` or `aborted-non-result`, the writer MUST keep `approved: false` and the validator refuses any report that ships with `approved: true` under such a gate result.**
80
+ - every ambiguity flagged during pre-planning that the user must resolve before approval registered as a `Blocks=approval` row in the `## 1. Clarification Items` table (do NOT create a separate `Open Questions` block under `4.5.x` — the unified table is the single home)
81
+ - **§5.5.9 Plan Body Verification (BLOCKING).** After report-writer finishes the draft, the lead MUST run a worker peer-review round on the consolidated plan body (sections 4.5.1 – 4.5.7) and populate `### 5.5.9 Plan Body Verification` in the final report. The round protocol, plan-item ID scheme (`P-Opt-*` / `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*`), verdict semantics, gate-result classification, and dissent log format are defined in `skills/okstra-convergence/SKILL.md` "Plan-body verification mode". The four gate-result values are `passed`, `passed-with-dissent`, `blocked-by-disagreement`, `aborted-non-result`. When the gate would have been `blocked-by-disagreement` or `aborted-non-result`, the lead MUST NOT silently flip it to one of the passing values to "unblock" the run — that is a contract violation. When `convergence.adversarial=true` (the default for this phase), this round uses the adversarial posture — verifiers confirm cited paths/commands and the burden of proof is on the plan — but the gate threshold stays `majority-disagree` (see that skill's §"Adversarial plan-body posture").
82
82
  - **Decision-record evaluation (sole owner)**: this phase is the **single owner** of decision-record evaluation in the okstra lifecycle. The brief never evaluates or drafts decision records — it only forwards `adr-candidate:*` signals. Every `adr-candidate:*` entry inherited from the brief's `Open Questions` is a mandatory evaluation target. In addition, evaluate every decision the recommended option introduces against the three criteria:
83
83
  1. **Hard to reverse** — would changing the decision later cost meaningfully more than deciding now?
84
84
  2. **Surprising without context** — would a future reader, seeing only the code, wonder "why was it built this way?"?
@@ -95,7 +95,7 @@
95
95
  1. **Spec coverage** — for every requirement in the task brief, point to the option(s) and step(s) that satisfy it. List gaps explicitly.
96
96
  2. **Placeholder scan** — search the report for the patterns in the No-placeholder rule above and fix inline.
97
97
  3. **Internal consistency** — option file lists, trade-off matrix, and recommended step list must agree on file paths, names, and signatures. A symbol called `clearLayers()` in the matrix and `clearFullLayers()` in the steps is a bug.
98
- 4. **Ambiguity check** — any requirement that could be read two ways must be made explicit or moved to the `## 5. Clarification Items` table as a `Blocks=approval` row.
98
+ 4. **Ambiguity check** — any requirement that could be read two ways must be made explicit or moved to the `## 1. Clarification Items` table as a `Blocks=approval` row.
99
99
  5. **Scope check** — if the recommended plan now spans multiple independent subsystems, recommend splitting into separate planning runs rather than shipping an oversized plan.
100
- 6. **Plan-body verification reconciliation (BLOCKING for implementation-planning).** Inspect the `### 4.5.9 Plan Body Verification` verdict table. For every plan-item row classified as `majority-disagree → C-<N>`, the corresponding `C-<N>` row MUST exist in `## 5. Clarification Items` with `Kind` chosen per the standard policy and `Blocks=approval`. Do NOT create a parallel `### 4.5.x Open Questions` block — the unified table is the single home. Conversely, the `Classification` column's `C-<N>` reference and the `## 5. Clarification Items` `ID` column MUST match 1:1; an orphan on either side is a contract violation. For `partial-consensus` and `worker-unique` plan-items, the dissenting opinion lives in §4.5.9 `Dissent log` and is NOT promoted to §5.
100
+ 6. **Plan-body verification reconciliation (BLOCKING for implementation-planning).** Inspect the `### 5.5.9 Plan Body Verification` verdict table. For every plan-item row classified as `majority-disagree → C-<N>`, the corresponding `C-<N>` row MUST exist in `## 1. Clarification Items` with `Kind` chosen per the standard policy and `Blocks=approval`. Do NOT create a parallel `### 5.5.x Open Questions` block — the unified table is the single home. Conversely, the `Classification` column's `C-<N>` reference and the `## 1. Clarification Items` `ID` column MUST match 1:1; an orphan on either side is a contract violation. For `partial-consensus` and `worker-unique` plan-items, the dissenting opinion lives in §5.5.9 `Dissent log` and is NOT promoted to §5.
101
101
  7. **Stage Map self-check** — for every stage, count the effective rows of its `Stepwise Execution Order` table by hand; reject the draft if any stage exceeds 6. Walk the `depends-on` graph and confirm it is a DAG (no cycle, no self-reference). For each `depends-on` link, confirm it encodes a real data/contract dependency — do NOT add links to serialise unrelated work, and do NOT split a stage merely to create more parallel stages. **Parallel-safety:** for every pair of `depends-on (none)` stages, confirm their `Stage Exit Contract` predicted file sets are disjoint; if they share a file, merge them or add a `depends-on` link (validator S9 rejects overlap).
@@ -13,7 +13,7 @@
13
13
  - this phase REQUIRES a codebase-scan brief whose frontmatter contains `scope: codebase`. A brief without that marker is rejected before worker dispatch.
14
14
  - the brief's `priority-lenses` MUST be a non-empty subset (size 1..4) of the lens whitelist defined in `scripts/okstra_ctl/improvement_lenses.py`. Lenses outside the whitelist are rejected.
15
15
  - the brief's `scan-scope` defines the only paths workers may read for candidate evidence. `out-of-scope` paths MUST be ignored even when the codebase is otherwise reachable.
16
- - the brief's `candidate-cap` (default 8 if absent, absolute cap 12) bounds the number of rows in `## 4.9 Improvement Candidates`.
16
+ - the brief's `candidate-cap` (default 8 if absent, absolute cap 12) bounds the number of rows in `## 5.9 Improvement Candidates`.
17
17
  - Apply the shared reporter-confirmation precondition as written. For this phase any unresolved `intent-check:` / `conversion-block:` row uses `Blocks=next-phase`.
18
18
  - Primary focus areas:
19
19
  - candidate discovery within the lens whitelist
@@ -29,11 +29,11 @@
29
29
  - Decision-tree walk (bounded):
30
30
  - When candidates branch on a structural question (e.g. "is module X meant to own this responsibility?"), resolve via `Read` / `Grep` first. Only escalate to the user inside the Phase 1.5 budget.
31
31
  - Expected output emphasis:
32
- - the `## 4.9 Improvement Candidates` table populated with rows that obey the 10-column schema from `validators/validate-improvement-report.py` (Cand ID `I-NNN`, Lens from whitelist, Title, Scope ⊆ scan-scope, Severity, Effort, Consensus, Source workers `<worker>:<id>` from {claude, codex, gemini}, Recommended next-phase ∈ {requirements-discovery, implementation-planning, error-analysis}, Evidence as path:line list)
33
- - `## 2. Final Verdict` Verdict Token ∈ {`candidates-ready`, `no-candidates`, `blocked`}; Direction `routing`; Next Step "사용자에게 후보 K개 선택 의뢰 (## 4.9 표 참조)"
34
- - `## 6. Recommended Next Steps` first entry summarises per-candidate routing and proposes new task-key names of the form `<task-group>/imp-<Cand-ID>`
32
+ - the `## 5.9 Improvement Candidates` table populated with rows that obey the 10-column schema from `validators/validate-improvement-report.py` (Cand ID `I-NNN`, Lens from whitelist, Title, Scope ⊆ scan-scope, Severity, Effort, Consensus, Source workers `<worker>:<id>` from {claude, codex, gemini}, Recommended next-phase ∈ {requirements-discovery, implementation-planning, error-analysis}, Evidence as path:line list)
33
+ - `## 7. Final Verdict` Verdict Token ∈ {`candidates-ready`, `no-candidates`, `blocked`}; Direction `routing`; Next Step "사용자에게 후보 K개 선택 의뢰 (## 5.9 표 참조)"
34
+ - `## 3. Recommended Next Steps` first entry summarises per-candidate routing and proposes new task-key names of the form `<task-group>/imp-<Cand-ID>`
35
35
  - Clarification request policy (phase-specific addenda — shared policy is in `_common-contract.md`):
36
- - if scan-scope or priority-lenses cannot be made concrete during Phase 1.5, end the run with Verdict Token `blocked`, populate `## 5. Clarification Items` with `Blocks=next-phase` rows, and do not run worker dispatch
36
+ - if scan-scope or priority-lenses cannot be made concrete during Phase 1.5, end the run with Verdict Token `blocked`, populate `## 1. Clarification Items` with `Blocks=next-phase` rows, and do not run worker dispatch
37
37
  - every clarification row carries a recommended answer + one-line rationale inside the `Expected form` cell
38
38
  - Non-goals:
39
39
  - concrete implementation plans, cost estimates, or code edits for any candidate
@@ -6,12 +6,12 @@
6
6
  - Lead-only contract (replaces the shared team contract for this phase):
7
7
  - The Claude lead is the sole agent for this run. No `Agent(...)` worker dispatch, no `TeamCreate`, no parallel sub-agents, no convergence loop.
8
8
  - The lead drafts the PR title and PR body **inline** by reading the run brief, the cited final-verification report, `git log --oneline <base>..HEAD`, and `git diff <base>..HEAD --stat`. No drafter worker is dispatched.
9
- - The lead authors the final-report file directly (no `Report writer worker` dispatch). The report still conforms to the standard `okstra-final-report.template.md` structure, including the `## 4.6 Release Handoff Deliverables` section.
9
+ - The lead authors the final-report file directly (no `Report writer worker` dispatch). The report still conforms to the standard `okstra-final-report.template.md` structure, including the `## 5.6 Release Handoff Deliverables` section.
10
10
  - The shared anti-escalation rule from the common contract still applies: do not start any other lifecycle phase from inside this run.
11
11
  - The shared "authority & permissions assumption" rule from the common contract still applies: assume the user holds every permission needed; do not block on hypothetical approvals.
12
12
  - The shared "MCP read-only" rule still applies if the brief lists MCP servers, though most release-handoff runs do not use MCP.
13
13
  - Pre-handoff entry gate (mandatory — refuse to start if any item fails):
14
- - the task brief MUST cite the originating `final-verification` final-report path under `## Source Verification Report`. The lead opens that file and confirms section `## 2. Final Verdict` contains a `Verdict Token` field whose value is exactly `accepted`.
14
+ - the task brief MUST cite the originating `final-verification` final-report path under `## Source Verification Report`. The lead opens that file and confirms section `## 7. Final Verdict` contains a `Verdict Token` field whose value is exactly `accepted`.
15
15
  - if the verdict is `conditional-accept`, `blocked`, or any other token (including ambiguous phrasing like "looks good"), the run MUST end immediately with status `blocked` and a routing recommendation back to `error-analysis` or `implementation-planning`. Do NOT prompt the user; Do NOT run any git command.
16
16
  - the lead MUST capture `git status --short` and confirm the working tree is clean. Dirty state aborts the run; release-handoff packages the commits produced by `implementation`, it does not stage or commit changes.
17
17
  - the lead MUST capture `git rev-parse --abbrev-ref HEAD` and record it as the **feature branch**. If the current branch is itself `main`, `master`, `prod`, `preprod`, `staging`, or `dev`, the run MUST end immediately — release-handoff never operates on a base branch.
@@ -39,14 +39,14 @@
39
39
  - When the brief's `Desired Outcome`, classification, or routing target depends on a chain of decisions, walk that chain one branch at a time. Each branch is one `Clarification Items` row, not a free-form interview.
40
40
  - For every clarification row, put the single best answer and one-line rationale in `Expected form` as `Recommended: ...`. Put other options and one-sentence consequences in the same cell as `Alternatives: ...`.
41
41
  - **Codebase-first rule**: if a branch can be resolved by `Read` / `Grep` / file inspection, resolve it that way and record `Evidence checked: <path:line>` in the `Statement` cell. Do NOT escalate to the user.
42
- - Budget: the unified `## 5. Clarification Items` table caps at the smaller of (a) one row per unresolved decision branch, (b) 8 rows total. Beyond the cap, fold remaining ambiguity into the routing recommendation's risk notes.
42
+ - Budget: the unified `## 1. Clarification Items` table caps at the smaller of (a) one row per unresolved decision branch, (b) 8 rows total. Beyond the cap, fold remaining ambiguity into the routing recommendation's risk notes.
43
43
  - Expected output emphasis:
44
44
  - evidence-backed routing decision
45
45
  - uncertainty boundaries and missing inputs
46
46
  - next recommended phase and safe resume guidance
47
47
  - canonical-term resolution for every `terminology:*` brief item, written as a one-line `<term> = <definition>` line in a new `Domain Alignment` subsection of the final report; alongside each, propose whether `<PROJECT_ROOT>/.okstra/glossary.md` should be updated (proposal only — actual writes happen via `okstra-brief` Step 4.5 on a subsequent run)
48
48
  - Clarification request policy (phase-specific addenda — shared policy is in `_common-contract.md`):
49
- - if any blocking input is missing at the time of writing the final report, populate `## 5. Clarification Items` in `final-report-template.md` (a single unified table; `Blocks=next-phase` for items the next run cannot start without)
49
+ - if any blocking input is missing at the time of writing the final report, populate `## 1. Clarification Items` in `final-report-template.md` (a single unified table; `Blocks=next-phase` for items the next run cannot start without)
50
50
  - prefer concrete questions whose answers map directly to a routing decision (`bugfix` vs `feature`, `error-analysis` vs `implementation-planning`, etc.). State each option in plain language with one sentence describing what choosing it would mean for the next phase.
51
51
  - every clarification row carries a recommended answer + one-line rationale inside the `Expected form` cell; rows that lack a recommendation are rejected as half-formed.
52
52
  - **Codebase-first ambiguity resolution (defect rule)**: any ambiguity that can be answered by `Read` / `Grep` / file inspection MUST be resolved that way and recorded with file:line evidence. Writing a clarification row for something the codebase already answers is a defect of this phase.
@@ -1,6 +1,6 @@
1
- """Parse the ``## 5. Clarification Items`` table from a final-report markdown.
1
+ """Parse the ``## 1. Clarification Items`` table from a final-report markdown.
2
2
 
3
- The unified §5 table (introduced when §4.5.9 / §5.1 / §5.2 collapsed into a
3
+ The unified §1 table (introduced when §4.5.9 / §5.1 / §5.2 collapsed into a
4
4
  single section) is the canonical home for every clarification an
5
5
  implementation-planning run owes the user — decisions, file attachments,
6
6
  single data points. Each row carries a ``Blocks`` column whose value picks
@@ -12,7 +12,7 @@ This module exposes one read function for that gate so both
12
12
  ``_validate_approved_plan`` (pre-implementation run-prep) and any later
13
13
  validator can share the same parsing logic.
14
14
 
15
- Legacy compatibility: reports written before the §5 unification used
15
+ Legacy compatibility: reports written before the §1 unification used
16
16
  ``4.5.9 Open Questions`` + ``5.1 Additional Materials`` + ``5.2 Questions
17
17
  for the User`` and lacked a ``Blocks`` column. Those reports cannot be
18
18
  gate-checked by Blocks; the parser returns ``None`` to signal "schema
@@ -26,13 +26,13 @@ from pathlib import Path
26
26
  from typing import Optional
27
27
 
28
28
 
29
- SECTION_HEADING_PATTERN = re.compile(r"^##\s+5\.\s+Clarification Items\s*$", re.MULTILINE)
30
- NEXT_TOP_LEVEL_HEADING_PATTERN = re.compile(r"^##\s+(?!5\.)", re.MULTILINE)
29
+ SECTION_HEADING_PATTERN = re.compile(r"^##\s+1\.\s+Clarification Items\s*$", re.MULTILINE)
30
+ NEXT_TOP_LEVEL_HEADING_PATTERN = re.compile(r"^##\s+(?!1\.)", re.MULTILINE)
31
31
 
32
32
 
33
33
  @dataclass(frozen=True)
34
34
  class ClarificationItem:
35
- """One row of the §5 table.
35
+ """One row of the §1 table.
36
36
 
37
37
  ``raw_*`` fields preserve the exact cell text (after backtick stripping)
38
38
  for diagnostics; canonical lowercased versions live in ``blocks`` /
@@ -77,9 +77,9 @@ def _is_separator_row(line: str) -> bool:
77
77
  return True
78
78
 
79
79
 
80
- def _section_5_slice(report_text: str) -> Optional[str]:
81
- """Return the substring spanning the §5 section (heading exclusive of the
82
- next ``##`` heading), or None if §5 is absent."""
80
+ def _section_1_slice(report_text: str) -> Optional[str]:
81
+ """Return the substring spanning the §1 section (heading exclusive of the
82
+ next ``##`` heading), or None if §1 is absent."""
83
83
  start_match = SECTION_HEADING_PATTERN.search(report_text)
84
84
  if not start_match:
85
85
  return None
@@ -89,7 +89,7 @@ def _section_5_slice(report_text: str) -> Optional[str]:
89
89
 
90
90
 
91
91
  def parse_clarification_items(report_text: str) -> Optional[list[ClarificationItem]]:
92
- """Return the list of §5 rows. ``None`` means "no unified §5 table
92
+ """Return the list of §1 rows. ``None`` means "no unified §1 table
93
93
  detected" (legacy report or missing section) — caller must NOT treat
94
94
  that as "table is empty".
95
95
 
@@ -97,7 +97,7 @@ def parse_clarification_items(report_text: str) -> Optional[list[ClarificationIt
97
97
  just the ``- 추가 정보 요청 없음.`` placeholder); that IS a confident
98
98
  "no approval-blocking items".
99
99
  """
100
- section = _section_5_slice(report_text)
100
+ section = _section_1_slice(report_text)
101
101
  if section is None:
102
102
  return None
103
103
 
@@ -75,7 +75,7 @@ def _strip_phase_blocks(text: str, current_phase: str) -> str:
75
75
  entirely. When *current_phase* is empty or not one of the four
76
76
  block-targetable phases (e.g. `requirements-discovery`,
77
77
  `error-analysis`), every block is dropped — correct because none of
78
- the `## 4.5` / `4.6` / `4.7` / `4.8` deliverable sections apply
78
+ the `## 5.5` / `5.6` / `5.7` / `5.8` deliverable sections apply
79
79
  there.
80
80
 
81
81
  Observed (fontsninja-classifier-v2 RD run): the raw final-report
@@ -9,7 +9,7 @@ the canonical user-facing markdown.
9
9
 
10
10
  Why this exists: prior to v0.32, report-writer-worker wrote the markdown
11
11
  directly. Free-form authoring led to silent contract violations — missing
12
- columns in the Execution Status table, omitted §7 phase-continuation
12
+ columns in the Execution Status table, omitted §4 phase-continuation
13
13
  rows, invented ``## Index`` sections. Routing everything through one
14
14
  template + schema cuts those failure modes to zero.
15
15
 
@@ -3,9 +3,9 @@
3
3
  Single product, single source of truth:
4
4
 
5
5
  * ``render_html(src_md, *, run_meta)`` — deterministic self-contained
6
- HTML renderer for human readers. Sections §567 user-actionable
7
- rows (those reachable from §5 ``C-*`` IDs) get embedded ``<form>``
8
- controls. §4.6 / §4.7 / §4.8 deliverable sub-sections are explicitly
6
+ HTML renderer for human readers. Sections §134 user-actionable
7
+ rows (those reachable from §1 ``C-*`` IDs) get embedded ``<form>``
8
+ controls. §5.6 / §5.7 / §5.8 deliverable sub-sections are explicitly
9
9
  excluded from form attachment — they are read-only deliverables.
10
10
 
11
11
  User responses are NEVER merged back into the original report. The HTML
@@ -57,7 +57,7 @@ def _strip_leading_frontmatter(text: str) -> str:
57
57
 
58
58
  from .clarification_items import (
59
59
  _is_separator_row,
60
- _section_5_slice,
60
+ _section_1_slice,
61
61
  _split_pipe_row,
62
62
  parse_clarification_items,
63
63
  )
@@ -79,7 +79,7 @@ _LINK_PATTERN = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
79
79
 
80
80
  # Sections whose Response-ID-bearing rows must NOT get form attachment
81
81
  # (read-only deliverables — see plan §1.4).
82
- _NO_FORM_SECTION_PREFIXES = ("## 4.6", "### 4.6", "## 4.7", "### 4.7", "## 4.8", "### 4.8")
82
+ _NO_FORM_SECTION_PREFIXES = ("## 5.6", "### 5.6", "## 5.7", "### 5.7", "## 5.8", "### 5.8")
83
83
 
84
84
 
85
85
  @dataclass(frozen=True)
@@ -161,7 +161,7 @@ def _markdown_to_html(
161
161
  headings: list[tuple[int, str, str]] = []
162
162
  i = 0
163
163
  n = len(lines)
164
- current_section_path: list[str] = [] # ['## 5. ...', '### 5.1 ...'] etc.
164
+ current_section_path: list[str] = [] # ['## 1. ...', '### 1.1 ...'] etc.
165
165
 
166
166
  while i < n:
167
167
  line = lines[i]
@@ -394,7 +394,7 @@ class _GroupedSpec:
394
394
  value`` metadata cell led by ``headline_col``; the long columns
395
395
  (``wide_cols``) each keep their own min-width column.
396
396
 
397
- ``kind == "clarification"`` additionally re-attaches the §5 form
397
+ ``kind == "clarification"`` additionally re-attaches the §1 form
398
398
  widget to the ``user_input_col`` cell and the ``data-*`` row attrs."""
399
399
  headline_col: int
400
400
  group_cols: tuple[int, ...]
@@ -407,21 +407,12 @@ class _GroupedSpec:
407
407
  user_input_col: int = -1
408
408
 
409
409
 
410
- _FOLLOWUP_WIDE_PREFIXES: tuple[str, ...] = ("title", "scope", "reason")
411
-
412
-
413
410
  def _grouped_table_spec(
414
411
  header_cells: list[str], section_path: list[str]
415
412
  ) -> Optional[_GroupedSpec]:
416
- """Return a ``_GroupedSpec`` for the three wide final-report tables
417
- that benefit from the compact layout Execution Status, §5
418
- Clarification Items, §7 Follow-up Tasks or ``None`` for every other
419
- table (which keeps the default per-cell ``td-narrow`` rendering).
420
-
421
- Each table is identified by stable header tokens (the i18n token/cost
422
- columns are never used as anchors). ``wide_cols`` lists the long-prose
423
- columns that must keep a guaranteed min-width; everything else short
424
- collapses into the leading metadata cell."""
413
+ """Only §1 Clarification Items is grouped in the HTML view (it keeps the
414
+ interactive form and stays flat in the .md). All other narrative tables are
415
+ already rendered compactly by the template, so no grouping is applied here."""
425
416
  norm = [h.strip() for h in header_cells]
426
417
 
427
418
  def _spec(headline: int, wide: tuple[int, ...], **kw) -> _GroupedSpec:
@@ -429,12 +420,8 @@ def _grouped_table_spec(
429
420
  group = tuple(c for c in range(len(norm)) if c != headline and c not in wide_set)
430
421
  return _GroupedSpec(headline_col=headline, group_cols=group, wide_cols=wide, **kw)
431
422
 
432
- # Execution Status by Agent Agent Summary of Key Findings.
433
- if len(norm) >= 3 and norm[0] == "Agent" and norm[-1] == "Summary of Key Findings":
434
- return _spec(0, (len(norm) - 1,), kind="plain")
435
-
436
- # §5 Clarification Items — keep the interactive form, but collapse the
437
- # short ID/Kind/Status/… columns and widen Statement + User input.
423
+ # §1 Clarification Itemskeep the interactive form, and widen the three
424
+ # long-prose columns (Expected form is prose too, not a code column).
438
425
  if (
439
426
  any("Clarification Items" in h for h in section_path)
440
427
  and not _section_forbids_form(section_path)
@@ -444,9 +431,15 @@ def _grouped_table_spec(
444
431
  ):
445
432
  statement_col = next(i for i, h in enumerate(norm) if h.startswith("Statement"))
446
433
  user_input_col = norm.index("User input")
434
+ expected_col = next(
435
+ (i for i, h in enumerate(norm) if h.startswith("Expected form")), -1
436
+ )
437
+ wide_cols = tuple(
438
+ c for c in (expected_col, statement_col, user_input_col) if c >= 0
439
+ )
447
440
  return _spec(
448
441
  norm.index("ID"),
449
- (statement_col, user_input_col),
442
+ wide_cols,
450
443
  kind="clarification",
451
444
  id_col=norm.index("ID"),
452
445
  kind_col=norm.index("Kind") if "Kind" in norm else -1,
@@ -455,16 +448,6 @@ def _grouped_table_spec(
455
448
  user_input_col=user_input_col,
456
449
  )
457
450
 
458
- # §7 Follow-up Tasks — widen Title / Scope / Reason, collapse the rest.
459
- if any("Follow-up Tasks" in h for h in section_path) and "ID" in norm:
460
- wide = tuple(
461
- i
462
- for i, h in enumerate(norm)
463
- if any(h.lower().startswith(p) for p in _FOLLOWUP_WIDE_PREFIXES)
464
- )
465
- if wide:
466
- return _spec(norm.index("ID"), wide, kind="plain")
467
-
468
451
  return None
469
452
 
470
453
 
@@ -491,7 +474,7 @@ def _grouped_meta_cell(
491
474
  def _grouped_clarification_row(
492
475
  row: list[str], spec: _GroupedSpec
493
476
  ) -> tuple[str, str]:
494
- """Return ``(tr_attrs, wide_cells_html)`` for one §5 row, re-attaching
477
+ """Return ``(tr_attrs, wide_cells_html)`` for one §1 row, re-attaching
495
478
  the form widget + ``data-*`` attrs to ``C-\\d+`` rows exactly as the
496
479
  non-grouped path does."""
497
480
  rid = row[spec.id_col] if 0 <= spec.id_col < len(row) else ""
@@ -768,6 +751,10 @@ def _inline(text: str) -> str:
768
751
  out = _LINK_PATTERN.sub(
769
752
  lambda m: f'<a href="{m.group(2)}">{m.group(1)}</a>', out
770
753
  )
754
+ # Preserve explicit <br> line breaks used inside compact meta cells (the
755
+ # markdown source intentionally stacks short fields with <br>). html.escape
756
+ # above turned them into &lt;br&gt;; restore the tag.
757
+ out = out.replace("&lt;br&gt;", "<br>").replace("&lt;br/&gt;", "<br>").replace("&lt;br /&gt;", "<br>")
771
758
  return out
772
759
 
773
760
 
@@ -835,7 +822,7 @@ def serialize_user_response(
835
822
  # --------------------------------------------------------------------------- #
836
823
 
837
824
  def report_has_clarification_items(src_md: str) -> bool:
838
- """True when the final-report MD has at least one §5 ``C-*``
825
+ """True when the final-report MD has at least one §1``C-*``
839
826
  clarification row. This is the single predicate that gates HTML-view
840
827
  generation: the self-contained html's only value over the markdown is
841
828
  the embedded ``<form>`` widgets for those rows, so a clarification-free
@@ -857,7 +844,7 @@ def render_html_view(
857
844
  ) -> Path | None:
858
845
  """Write ``<stem>.html`` next to ``src_md_path`` and return its path,
859
846
  or return ``None`` when generation is skipped because the report has
860
- no §5 clarification rows (see ``report_has_clarification_items``).
847
+ no §1 clarification rows (see ``report_has_clarification_items``).
861
848
  Idempotent — overwrites an existing html sibling, and removes a stale
862
849
  one when a previously-clarification-bearing report no longer has rows."""
863
850
  src_text = src_md_path.read_text(encoding="utf-8")
@@ -175,14 +175,14 @@ def _validate_approved_plan(path: str) -> None:
175
175
  f"approved plan is not yet approved (frontmatter `approved: {m.group(1)}`): {path}\n"
176
176
  " open the report and change the frontmatter line to `approved: true`, "
177
177
  "or re-run okstra with `--approve` to flip it from the CLI.\n"
178
- " resolve any `Blocks=approval` rows in `## 5. Clarification Items` first."
178
+ " resolve any `Blocks=approval` rows in `## 1. Clarification Items` first."
179
179
  )
180
- # frontmatter approved == true 상태. §5 Clarification Items 의
180
+ # frontmatter approved == true 상태. §1 Clarification Items 의
181
181
  # Blocks=approval 행이 아직 open/answered 면 승인을 무효화한다.
182
182
  blockers = unresolved_approval_blockers(body)
183
183
  if blockers:
184
184
  lines = [
185
- f"approved plan frontmatter has `approved: true` but §5 has {len(blockers)} "
185
+ f"approved plan frontmatter has `approved: true` but §1 has {len(blockers)} "
186
186
  f"unresolved `Blocks=approval` row(s); resolve them or mark them obsolete first:",
187
187
  ]
188
188
  for b in blockers:
@@ -205,6 +205,28 @@ S_CONFIRM = "confirm"
205
205
  S_EDIT_TARGET = "edit_target"
206
206
  S_DONE = "done"
207
207
 
208
+ # ---- 멀티탭 배치 프롬프트 그룹 (방출 계층 전용) ----
209
+ # 그룹 id 는 S_* 가 아니므로 prompts JSON SOT / step-id 동기화 검사 대상이 아니다.
210
+ GROUP_MODELS = "models"
211
+ GROUP_OPTIONS = "options"
212
+ GROUP_MAX_TABS = 4 # AskUserQuestion 의 질문(탭) 수 한도
213
+
214
+ # 멤버는 모두 서로 의존이 없는 단일선택 픽 step 이어야 한다.
215
+ # *_TEXT 후속 / workers_override / pr_template_scope 는 의존성 때문에 개별 유지.
216
+ PROMPT_GROUPS: dict[str, tuple[str, ...]] = {
217
+ GROUP_MODELS: (S_LEAD_MODEL, S_EXECUTOR_MODEL, S_CLAUDE_MODEL,
218
+ S_CODEX_MODEL, S_GEMINI_MODEL, S_REPORT_WRITER_MODEL),
219
+ GROUP_OPTIONS: (S_DIRECTIVE_PICK, S_RELATED_TASKS_PICK,
220
+ S_CLARIFICATION_PICK, S_PR_TEMPLATE_PICK),
221
+ }
222
+ GROUP_LABELS: dict[str, str] = {
223
+ GROUP_MODELS: "모델 선택 (탭별로 선택)",
224
+ GROUP_OPTIONS: "추가 옵션 (탭별로 선택)",
225
+ }
226
+ _STEP_TO_GROUP: dict[str, str] = {
227
+ sid: gid for gid, ids in PROMPT_GROUPS.items() for sid in ids
228
+ }
229
+
208
230
 
209
231
  # ---- Data types ----------------------------------------------------------
210
232
 
@@ -305,9 +327,11 @@ class Prompt:
305
327
  help: str = ""
306
328
  echo_template: str = "" # e.g. "task-group: {value}"
307
329
  multi: bool = False # only meaningful when kind == "pick"
330
+ # only meaningful when kind == "pick_group": one entry per AskUserQuestion tab
331
+ questions: list["Prompt"] = field(default_factory=list)
308
332
 
309
333
  def to_json(self) -> dict[str, Any]:
310
- return {
334
+ out = {
311
335
  "step": self.step,
312
336
  "kind": self.kind,
313
337
  "label": self.label,
@@ -316,6 +340,14 @@ class Prompt:
316
340
  "echoTemplate": self.echo_template,
317
341
  "multi": self.multi,
318
342
  }
343
+ if self.kind == "pick_group":
344
+ out["questions"] = [
345
+ {"step": q.step, "label": q.label,
346
+ "options": [asdict(o) for o in q.options],
347
+ "multi": q.multi}
348
+ for q in self.questions
349
+ ]
350
+ return out
319
351
 
320
352
 
321
353
  class WizardError(Exception):
@@ -373,12 +405,12 @@ def _validate_approved_plan(path_str: str, project_root: Path) -> Path:
373
405
  " edit the report and change the line to `approved: true`, or re-run "
374
406
  "okstra with `--approve` to flip it from the CLI."
375
407
  )
376
- # frontmatter approved == true 라도 §5 의 Blocks=approval 행이 미해결이면
408
+ # frontmatter approved == true 라도 §1 의 Blocks=approval 행이 미해결이면
377
409
  # 승인이 무효 — prepare_task_bundle 의 _validate_approved_plan 과 동일 규약.
378
410
  blockers = unresolved_approval_blockers(body)
379
411
  if blockers:
380
412
  lines = [
381
- f"approved plan frontmatter has `approved: true` but §5 has {len(blockers)} "
413
+ f"approved plan frontmatter has `approved: true` but §1 has {len(blockers)} "
382
414
  f"unresolved `Blocks=approval` row(s); resolve them or mark them obsolete first:",
383
415
  ]
384
416
  for b in blockers:
@@ -2218,6 +2250,30 @@ def init_state(
2218
2250
  )
2219
2251
 
2220
2252
 
2253
+ def _build_group_prompt(state: WizardState, group_id: str) -> Prompt:
2254
+ """그룹의 적용가능·미답변 픽 멤버를 최대 GROUP_MAX_TABS 개 모은다.
2255
+
2256
+ 멤버가 1개뿐이면 멀티탭 UI가 불필요하므로 그 멤버의 평범한 픽을 반환한다.
2257
+ 호출부(next_prompt)는 적용 가능한 멤버가 최소 1개일 때만 진입하므로 빈 그룹은
2258
+ 도달 불가다.
2259
+ """
2260
+ members: list[Prompt] = []
2261
+ for sid in PROMPT_GROUPS[group_id]:
2262
+ if sid in state.answered:
2263
+ continue
2264
+ step = STEP_BY_ID[sid]
2265
+ if not step.applies(state):
2266
+ continue
2267
+ members.append(step.build(state))
2268
+ if len(members) >= GROUP_MAX_TABS:
2269
+ break
2270
+ assert members, f"group {group_id!r} reached with no applicable members"
2271
+ if len(members) == 1:
2272
+ return members[0]
2273
+ return Prompt(step=group_id, kind="pick_group",
2274
+ label=GROUP_LABELS[group_id], questions=members)
2275
+
2276
+
2221
2277
  def next_prompt(state: WizardState) -> Prompt:
2222
2278
  if state.confirmed:
2223
2279
  return Prompt(step=S_DONE, kind="done")
@@ -2225,10 +2281,39 @@ def next_prompt(state: WizardState) -> Prompt:
2225
2281
  if step.id in state.answered:
2226
2282
  continue
2227
2283
  if step.applies(state):
2284
+ group_id = _STEP_TO_GROUP.get(step.id)
2285
+ if group_id is not None:
2286
+ return _build_group_prompt(state, group_id)
2228
2287
  return step.build(state)
2229
2288
  return Prompt(step=S_DONE, kind="done")
2230
2289
 
2231
2290
 
2291
+ def _submit_group(state: WizardState, prompt: Prompt, value: str) -> dict[str, Any]:
2292
+ """pick_group 답(JSON 객체)을 각 멤버 submit() 으로 라우팅한다.
2293
+
2294
+ 멤버 submit 이 WizardError 를 던지면 그대로 전파되어 같은 그룹을 재-프롬프트한다.
2295
+ answered 마킹은 모든 멤버 submit 이 통과한 뒤에만 일괄 수행한다(answered 단위의
2296
+ 전부-아니면-전무). 개별 멤버가 변경한 state 필드는 롤백하지 않지만, 재-프롬프트 시
2297
+ 같은 그룹이 다시 나와 사용자 입력으로 덮어쓰므로 무해하다.
2298
+ """
2299
+ try:
2300
+ answers = json.loads(value or "{}")
2301
+ except json.JSONDecodeError as exc:
2302
+ raise WizardError(f"pick_group answer must be a JSON object: {exc}")
2303
+ if not isinstance(answers, dict):
2304
+ raise WizardError("pick_group answer must be a JSON object")
2305
+ echoes: list[str] = []
2306
+ for q in prompt.questions:
2307
+ echo = STEP_BY_ID[q.step].submit(state, str(answers.get(q.step, "") or ""))
2308
+ if echo:
2309
+ echoes.append(echo)
2310
+ for q in prompt.questions:
2311
+ if q.step not in state.answered:
2312
+ state.answered.append(q.step)
2313
+ nxt = next_prompt(state)
2314
+ return {"echo": "; ".join(echoes), "next": nxt.to_json()}
2315
+
2316
+
2232
2317
  def submit(state: WizardState, value: str) -> dict[str, Any]:
2233
2318
  """Validate the answer for the *currently active* step and advance.
2234
2319
 
@@ -2238,6 +2323,8 @@ def submit(state: WizardState, value: str) -> dict[str, Any]:
2238
2323
  prompt = next_prompt(state)
2239
2324
  if prompt.kind == "done":
2240
2325
  return {"echo": "", "next": prompt.to_json()}
2326
+ if prompt.kind == "pick_group":
2327
+ return _submit_group(state, prompt, value)
2241
2328
  step = STEP_BY_ID[prompt.step]
2242
2329
  echo = step.submit(state, value or "")
2243
2330
  if prompt.step not in state.answered:
@@ -87,7 +87,7 @@ PHASE_RULES: dict[str, dict[str, str]] = {
87
87
  " - trade-off matrix across options (complexity, risk, reversibility, test cost, rollout cost) and recommended option with rationale tied to isolation / single-responsibility / YAGNI principles\n"
88
88
  " - bite-sized stepwise execution order for the recommended option (each step ~2-5 min, exact file paths and commands, TDD ordering when applicable, no placeholders)\n"
89
89
  " - dependency / migration risk assessment, validation checklist (pre / mid / post with exact commands), rollback strategy with revert path and trigger signal\n"
90
- " - every unresolved ambiguity registered as a `Blocks=approval` row in the `## 5. Clarification Items` table (do NOT create a separate `Open Questions` block under `4.5.x` — the unified table is the single home)\n"
90
+ " - every unresolved ambiguity registered as a `Blocks=approval` row in the `## 1. Clarification Items` table (do NOT create a separate `Open Questions` block under `5.5.x` — the unified table is the single home)\n"
91
91
  " - YAML frontmatter line `approved: false` awaiting human flip to `true`\n"
92
92
  " - self-review confirmation (spec coverage, placeholder scan, internal consistency, ambiguity, scope)"
93
93
  ),