okstra 0.186.5 → 0.186.7

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 (54) hide show
  1. package/docs/architecture.md +1 -1
  2. package/docs/cli.md +3 -3
  3. package/docs/for-ai/skills/okstra-user-response.md +1 -1
  4. package/package.json +1 -1
  5. package/runtime/BUILD.json +2 -2
  6. package/runtime/bin/okstra-render-report-views.py +6 -5
  7. package/runtime/prompts/launch.template.md +14 -0
  8. package/runtime/prompts/lead/convergence.md +2 -2
  9. package/runtime/prompts/lead/okstra-lead-contract.md +4 -14
  10. package/runtime/prompts/lead/plan-body-verification.md +4 -3
  11. package/runtime/prompts/lead/report-writer.md +3 -1
  12. package/runtime/prompts/profiles/_coverage-critic.md +1 -1
  13. package/runtime/prompts/profiles/error-analysis.md +2 -2
  14. package/runtime/prompts/profiles/final-verification.md +2 -2
  15. package/runtime/prompts/profiles/implementation-planning.md +3 -3
  16. package/runtime/prompts/profiles/requirements-discovery.md +2 -2
  17. package/runtime/prompts/wizard/prompts.ko.json +2 -4
  18. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -5
  19. package/runtime/python/okstra_ctl/agent_activity.py +6 -0
  20. package/runtime/python/okstra_ctl/clarification_items.py +67 -11
  21. package/runtime/python/okstra_ctl/next_phase.py +6 -3
  22. package/runtime/python/okstra_ctl/plan_items.py +32 -6
  23. package/runtime/python/okstra_ctl/plan_items_cli.py +58 -3
  24. package/runtime/python/okstra_ctl/render_final_report.py +3 -1
  25. package/runtime/python/okstra_ctl/report_assembly.py +9 -2
  26. package/runtime/python/okstra_ctl/report_contract.py +2 -0
  27. package/runtime/python/okstra_ctl/report_html/common.py +2 -7
  28. package/runtime/python/okstra_ctl/report_html/render.py +3 -0
  29. package/runtime/python/okstra_ctl/report_html/run_usage.py +5 -1
  30. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +2 -5
  31. package/runtime/python/okstra_ctl/report_projections.py +45 -4
  32. package/runtime/python/okstra_ctl/run.py +21 -6
  33. package/runtime/python/okstra_ctl/usage_cells.py +15 -0
  34. package/runtime/python/okstra_ctl/user_response.py +72 -6
  35. package/runtime/python/okstra_ctl/wizard.py +1 -5
  36. package/runtime/python/okstra_token_usage/codex.py +32 -3
  37. package/runtime/python/okstra_token_usage/collect.py +148 -15
  38. package/runtime/python/okstra_token_usage/grok.py +24 -5
  39. package/runtime/python/okstra_token_usage/report.py +12 -2
  40. package/runtime/schemas/final-report-v2.0.schema.json +4 -0
  41. package/runtime/schemas/final-report-v3.0.schema.json +4 -0
  42. package/runtime/skills/okstra-user-response/SKILL.md +4 -2
  43. package/runtime/templates/reports/html/assets/base.css +3 -9
  44. package/runtime/templates/reports/html/assets/base.js +0 -21
  45. package/runtime/templates/reports/html/base.template.html +14 -4
  46. package/runtime/templates/reports/html/i18n/en.json +19 -0
  47. package/runtime/templates/reports/html/i18n/ko.json +19 -0
  48. package/runtime/templates/reports/html/tasks/final-verification.template.html +2 -2
  49. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +20 -29
  50. package/runtime/templates/reports/html/tasks/implementation.template.html +1 -1
  51. package/runtime/validators/lib/runners.sh +5 -1
  52. package/runtime/validators/validate-report-views.py +2 -1
  53. package/runtime/validators/validate-run.py +97 -82
  54. package/runtime/validators/validate_session_conformance.py +71 -18
@@ -867,7 +867,7 @@ The manifest-provided `lead-events-*.jsonl` file is the canonical record for str
867
867
 
868
868
  The shared `okstra report-finalize` entrypoint reads canonical activity before translation source checking. Under contract v3 its in-process `project-activity` step assembles the report, filtering events by run identity and validating activity ID order before `agentActivity[]` is published. A schema-invalid composed record is still published so `validate-run` can scan it; the step itself fails. A historical v2 manifest retains the in-place projection path. A failed Phase 7 step does not skip later checks: `validate-run` still runs, and only `teardown-stages` is skipped. For a non-English report, the lead runs `token-usage`, `project-activity`, and `check-source` before translator dispatch, then resumes at `render-views` after the translation sidecar exists. Conformance compares the resulting `agentActivity[]` IDs, order, and core fields with the canonical events for every lead host.
869
869
 
870
- Approval blockers use `open`, `answered`, `resolved`, and `obsolete`. `open` blocks until the user judges. `answered` with `accept-risk` / `select` / `answer` does not block approval or the next phase; the DISAGREE votes stay on the plan item as evidence. A response sidecar with a proceeding disposition unblocks the same way. `request-revision` and `reject` still withhold the next phase.
870
+ Approval blockers use `open`, `answered`, `resolved`, and `obsolete`. `open` blocks until the user judges. `answered` with `accept-risk` / `select` / `answer` does not block approval or the next phase; the DISAGREE votes stay on the plan item as evidence. A response sidecar with a proceeding disposition unblocks the same way. `request-revision` and `reject` still withhold the next phase until this report's `supersessionLedger` records that the answer was incorporated (`superseded` or `no-dependent-statement`).
871
871
 
872
872
  An approval row classifies its cause as `user-decision`, `noncritical-dissent`, or `correctness-critical`. `accept-risk` is available to all three: it ends the gate and keeps the dissent on the record for later stages. `correctness-critical` can still take `request-revision` when the user wants the plan corrected and re-verified.
873
873
 
package/docs/cli.md CHANGED
@@ -64,7 +64,7 @@ The Node CLI requires Node.js 22 or newer. Its TypeScript sources are compiled f
64
64
  Base command for initial entry with full arguments:
65
65
 
66
66
  ```bash
67
- scripts/okstra.sh [--render-only] [--yes] [--no-plan-verification] --task-type <task-type> [--workers worker1,worker2] [--role-count <role>=<N>] [--role-model <role>=<modelRef>] [--lead-runtime <host-id-or-alias>] [--lead-provider <provider>] [--lead-model <model>] [--worker-model provider=model,...] [--report-writer-provider <provider>] [--report-writer-model <model>] [--executor claude|codex|antigravity|grok|kimi] [--critic off|claude|codex|antigravity|grok|kimi] [--related-tasks taskA,taskB] [--work-category bugfix|feature|refactor|ops|improvement|unknown] [--base-ref <branch|tag|sha>] [--clarification-response <previous-final-report>] [--selected-direction <selection-final-report.md>] [--approved-plan <plan-path>] [--approve] --project-id <project-id> --task-group <task-group> --task-id <task-id> --task-brief <brief-path> [--directive <directive>] [--fix-cycle <yes|no>]
67
+ scripts/okstra.sh [--render-only] [--yes] [--no-plan-verification] --task-type <task-type> [--workers worker1,worker2] [--role-count <role>=<N>] [--role-model <role>=<modelRef>] [--lead-runtime <host-id-or-alias>] [--lead-provider <provider>] [--lead-model <model>] [--worker-model provider=model,...] [--report-writer-provider <provider>] [--report-writer-model <model>] [--executor claude|codex|antigravity|grok|kimi] [--critic claude|codex|antigravity|grok|kimi] [--related-tasks taskA,taskB] [--work-category bugfix|feature|refactor|ops|improvement|unknown] [--base-ref <branch|tag|sha>] [--clarification-response <previous-final-report>] [--selected-direction <selection-final-report.md>] [--approved-plan <plan-path>] [--approve] --project-id <project-id> --task-group <task-group> --task-id <task-id> --task-brief <brief-path> [--directive <directive>] [--fix-cycle <yes|no>]
68
68
  ```
69
69
 
70
70
  Analysis input ownership is narrower than the base shell command. The `/okstra-run` wizard collects `--analysis-target` and `--evidence-inputs` values and passes them internally to `node bin/okstra render-bundle`. `scripts/okstra.sh` does not accept either flag. Because `feature-analysis` requires a target, start that task type with the in-host skill; the two option sections below document the internal Node render inputs, not standalone shell options.
@@ -583,7 +583,7 @@ scripts/okstra.sh --task-type implementation \
583
583
 
584
584
  ### `--critic`
585
585
 
586
- Selects the provider for the opt-in Phase 5.6 critic pass. The value is `off`, `claude`, `codex`, `antigravity`, `grok`, or `kimi`; the default is `off`. The selected critic receives its own role-default model even when that provider is not in the initial analyser roster.
586
+ Selects the provider for the required critic slot on `requirements-discovery`, `error-analysis`, `implementation-planning`, and `final-verification`. The value is `claude`, `codex`, `antigravity`, `grok`, or `kimi`. `--critic off` is rejected. Prefer `--role-model critic=<provider>/<model>` so the user picks the model. The critic also settles plan-body 1-1 splits in `implementation-planning` (`critic-worker` on `--tie-vote` items).
587
587
 
588
588
  - Critic dispatch runs concurrently with the first convergence reverify round in Phase 5.5, with critic input fixed to the integrated Round 0 result. One gap/blocker verification round runs in Phase 5.6 after convergence finishes. It detects coverage gaps in discovery, error-analysis, and implementation-planning, and acts as an acceptance devil's advocate in final-verification. Follow the "Coverage critic pass" and "Acceptance critic pass" sections of `prompts/lead/convergence.md` for the detailed contract.
589
589
  - It shares the same value space as the critic-selection step in the in-session `okstra-run` wizard. `_resolve_model_bindings` in `prepare_task_bundle` validates the value; anything else is rejected immediately with `PrepareError`.
@@ -820,7 +820,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
820
820
  | `okstra plan-items seed --narrative <report-narrative.md> --state <plan-body-verification.json>` | Create the convergence-owned `planBodyVerification.planItems[]` rows every verdict lands in, from the same deterministic extraction `extract` uses. The historical v2 form is `--data <data.json>`. Idempotent by id: an existing row keeps its verdicts and carried fields. Reports `seeded` / `existing` counts. |
821
821
  | `okstra plan-items apply-verdicts --state <plan-body-verification.json> --result <worker-id>=<result.md>… --round <N>` | Read each worker's Markdown verdict directly, validate every current `P-*` id, reject duplicate worker submissions, and overwrite that round's stored verdicts. The historical `--verdicts <file>` form remains automation compatibility only. |
822
822
  | `okstra plan-items complete-round --state <plan-body-verification.json> --run-manifest <current-run-manifest.json> --round <N> [--self-fix-note <item-id>=<markdown-file>]… [--self-fix-group <cause-file>=<item-id>[,<item-id>...]]… [--self-fix-stop-reason <all-resolved\|no-progress\|max-rounds-reached>]` | After `plan-verify` succeeds, atomically derive and record the round's per-item votes, gate result, participant counts from the actual assigned roster, immutable completion time, convergence history, and optional self-fix notes/groups read from Markdown files. Models do not write the state JSON. Stdout also carries `nextDispatch`. |
823
- | `okstra plan-items next-dispatch --state <plan-body-verification.json> [--run-manifest <path>]` | After `apply-verdicts`, decide whether this round opens a worker batch. `kind: none` — missing-dependency `UNVERIFIABLE` only, no new batch. `kind: worker-correction` — re-prompt only those workers; peers stay idle. `kind: queue-reverify` — unsettled ties, those item ids only. A missing path is never environment-unverifiable. |
823
+ | `okstra plan-items next-dispatch --state <plan-body-verification.json> [--run-manifest <path>]` | After `apply-verdicts`, decide whether this round opens a worker batch. `kind: none` — missing-dependency `UNVERIFIABLE` only, no new batch. `kind: worker-correction` — re-prompt only those workers; peers stay idle. `kind: critic-tie` — unsettled analyser 1-1, `critic-worker` on those item ids only. A missing path is never environment-unverifiable. |
824
824
  | `okstra plan-items correction-prompt --state <plan-body-verification.json> --run-manifest <path> --worker <id>` | Emit the planning-time environment-gap paragraph, then the assigned queue. The environment exception is first. Exits 2 unless `next-dispatch` named that worker as a blanket-UNVERIFIABLE correction target. |
825
825
  | `okstra error-log append-observed --out <errors.jsonl> --task-key <key> --phase <phase> --agent <assigned-worker-id> --agent-role worker --model <model> --error-type tool-failure --command-file <markdown-file> --command-kind <kind> --message-file <markdown-file> [--cause <cause> --evidence-file <kind=file>]…` | Worker-facing typed error recording surface. Python validates and serializes the JSONL record; a worker supplies scalar identity fields plus Markdown files for free-form command, message, and probe content, never a JSON sidecar or JSON argument. `sandbox-denied` requires both `targetProbe` and `controlProbe` evidence files. |
826
826
  | `okstra config <get\|set\|unset\|show> [key] [value] [--scope project\|global\|all]` | Manage persistent settings such as `pr-template-path` with atomic JSON writes |
@@ -27,7 +27,7 @@ The legacy `list` and `show` JSON commands remain for automation compatibility.
27
27
  ## Flow
28
28
 
29
29
  1. Run `okstra preflight --runtime <host-runtime>` for the current harness. On `Okstra preflight: failed`, show `Reason` and `Recovery`, then stop. On `Okstra preflight: ready`, carry `Project root`, `Project ID`, `Runtime`, and `Relay contract`, then run `okstra paths --field home`.
30
- 2. Read the relay `Wizard interaction relay` JSON. When `native-single` is available and the option count fits `nativeLimits`, call `interactions.native-single.function` (`AskUserQuestion` / `ask_user_question` / `request_user_input` from that field). Do not print a numbered list in chat while the native tool is available. Otherwise render a numbered Markdown list. Do not substitute one host function name for another. Pass only the choices this step already owns. Do not append `Enter directly`. Claude Other, Grok `z`, and Codex's free-form row are `Enters an answer`; on a numbered list, so is a next message that is not a listed label or its 1-based number. Do not ask a second question for the custom value.
30
+ 2. Read the relay `Wizard interaction relay` JSON. When `native-single` is available and the option count fits `nativeLimits`, call `interactions.native-single.function` (`AskUserQuestion` / `ask_user_question` / `request_user_input` from that field). Do not print a numbered list in chat while the native tool is available. Otherwise render a numbered Markdown list. Do not substitute one host function name for another. Copy the view's `Picker:` `- Label:` / `Description:` pairs into that function in that order. Do not rebuild labels from the `Options:` dump. `--option-number` is the 1-based `Option N:` index, which is the same order as `Picker:`. The HTML report's `<select>` uses the same `option.answer` values. Pass only the `list-view` `Picker:` rows, the report `Picker:` rows, or the two confirmation labels. Do not append `Enter directly`. Claude Other, Grok `z`, and Codex's free-form row are `Enters an answer`; on a numbered list, so is a next message that is not a listed label or its 1-based number. Do not ask a second question for the custom value.
31
31
  3. Select a task from `list-view` through that host picker. A host free-text row or unmatched next message is the report path or task key.
32
32
  4. Read only `show-view --report <reportPath> --project-root <projectRoot>` for report facts. The view also prints `Why asked`, `Linked plan items`, and `Cited artifacts`.
33
33
  5. Read every cited `path:line` under the project root and every linked plan-item definition before asking. Do not search beyond that list. Investigation explains; it never changes `options[]`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.186.5",
3
+ "version": "0.186.7",
4
4
  "description": "Host-aware multi-provider cross-verification orchestrator runtime and agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.186.5",
3
- "builtAt": "2026-08-24T06:52:38.243Z",
2
+ "package": "0.186.7",
3
+ "builtAt": "2026-08-24T14:17:01.495Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -8,10 +8,10 @@ Usage:
8
8
  [--seq <NNN>]
9
9
  [--source-report <relative-path>]
10
10
 
11
- Schema-v2 ``.data.json`` input is rendered directly into the dedicated task
12
- HTML template and always produces an HTML sibling. Markdown input resolves its
13
- data sibling first; schema-v1 and quick reports keep the legacy conditional
14
- Markdown renderer.
11
+ Structured ``.data.json`` input (schema 2.0 or 3.0) is rendered directly into
12
+ the dedicated task HTML template and always produces an HTML sibling. Markdown
13
+ input resolves its data sibling first; schema-v1 and quick reports keep the
14
+ legacy conditional Markdown renderer.
15
15
 
16
16
  Output (idempotent — overwrites):
17
17
  - <stem>.html — single-file self-contained HTML view
@@ -48,6 +48,7 @@ if (SCRIPTS_DIR / "okstra_ctl" / "report_views.py").is_file():
48
48
  elif HOME_LIB.is_dir() and str(HOME_LIB) not in sys.path:
49
49
  sys.path.insert(0, str(HOME_LIB))
50
50
 
51
+ from okstra_ctl.clarification_items import STRUCTURED_REPORT_VERSIONS # noqa: E402
51
52
  from okstra_ctl.report_views import infer_run_meta, render_html_view # noqa: E402
52
53
  from okstra_ctl.final_report_paths import ( # noqa: E402
53
54
  final_report_data_path,
@@ -184,7 +185,7 @@ def main(argv: list[str] | None = None) -> int:
184
185
  data_path, markdown_path = _report_pair(report_path)
185
186
  data = _load_data_if_present(data_path)
186
187
 
187
- if data is not None and data.get("schemaVersion") == "2.0":
188
+ if data is not None and data.get("schemaVersion") in STRUCTURED_REPORT_VERSIONS:
188
189
  from okstra_ctl.report_html.render import render_v2_html_view
189
190
 
190
191
  html_path = render_v2_html_view(
@@ -25,6 +25,20 @@ For a new `implementation-planning` run, the plan-body sequence is initial verif
25
25
  - Phase advancement requires a new okstra invocation, launched with an explicit `--task-type` after this run's final report is written and approved. The target of that run comes from the pointer this run's report authors into `workflow.nextRecommendedPhase`, and only when that pointer's `status` says it can be started. The lead must not write source code, run builds/migrations/deployments, or otherwise produce artifacts of a different phase from inside this run.
26
26
  - See `Lifecycle Phase Boundaries` in the lifecycle core contract (`{{OKSTRA_LEAD_CONTRACT_PATH}}`) for the canonical rules and the phase-transition checklist.
27
27
 
28
+ ## User closeout (BLOCKING)
29
+
30
+ After Phase 7 persistence, the last user-facing message of this run is the next command. A status dump is not a close. A prohibition (`do not start implementation`) is not a next action. This applies to every task type.
31
+
32
+ Close with one command the user can run now. The first matching row wins:
33
+
34
+ - Open `blocks: approval` rows → `/okstra-user-response` (name the `C-NNN` ids). An `accept-risk` / `select` / `answer` already recorded is not an open blocker.
35
+ - `workflow.awaitingApproval` is true → `/okstra-run` → `implementation` or `--approve`. Do not propose another planning run.
36
+ - Phase 7 `validate-run` failed → one line naming the blocking cause, then `/okstra-run` to re-run this phase, or `/okstra-inspect recap`.
37
+ - Pointer `status: ready` → `/okstra-run` for that `phase`.
38
+ - Otherwise → `/okstra-inspect status` for this task.
39
+
40
+ Do not end the turn after the validator result. Task-qualified report paths and the same table live in the lifecycle core contract (`{{OKSTRA_LEAD_CONTRACT_PATH}}` "After persistence").
41
+
28
42
  {{TEAM_CREATION_GATE}}
29
43
 
30
44
  ## Project Root
@@ -596,7 +596,7 @@ Schema rules:
596
596
 
597
597
  ## Coverage critic pass
598
598
 
599
- Runs only when `convergence.critic.enabled == true` (set by `--critic <provider>` or the okstra-run `critic_pick` step; default off). Applies to the three finding-producing phases (`requirements-discovery`, `error-analysis`, `implementation-planning`); for `final-verification` the critic runs in a different mode — see §"Acceptance critic pass (final-verification)". This pass targets **scope in both directions** — findings that are missing (coverage) and work the findings propose that no requirement asked for (over-scope) — distinct from convergence, which targets **agreement quality** among the findings already raised. The pass keeps its `coverage` mode id and `gaps` vocabulary for both halves; the two are told apart by each candidate's `category`, so no schema or reducer distinguishes them.
599
+ Runs when `convergence.critic.enabled == true`. Critic is required on `requirements-discovery`, `error-analysis`, `implementation-planning`, and `final-verification` (role `min`/`max` 1; the user picks the model). `--critic off` is rejected. For `final-verification` the critic runs in a different mode — see §"Acceptance critic pass (final-verification)". This pass targets **scope in both directions** — findings that are missing (coverage) and work the findings propose that no requirement asked for (over-scope) — distinct from convergence, which targets **agreement quality** among the findings already raised. The pass keeps its `coverage` mode id and `gaps` vocabulary for both halves; the two are told apart by each candidate's `category`, so no schema or reducer distinguishes them. In `implementation-planning` the same critic slot also settles plan-body analyser 1-1 splits as `critic-worker`.
600
600
 
601
601
  ### When
602
602
 
@@ -710,7 +710,7 @@ The asymmetry is deliberate and runs the opposite way from the coverage half: a
710
710
 
711
711
  ## Acceptance critic pass (final-verification)
712
712
 
713
- The `final-verification` phase uses the same fresh one-shot `redispatch_worker` pattern and the same dispatch timing as §"Coverage critic pass" §"When" (provider + `config.critic.modelExecutionValue` from the `convergence.critic` block; default off; same model-unresolved skip rule) — the delivered work the critic inspects is likewise fixed before the reverify round starts. Only the prompt, the verification semantics, and the output sink differ — final-verification's findings are defects/blockers, so the critic acts as an **acceptance devil's advocate** (find reasons NOT to accept), and its candidate blockers are NEVER dropped (that would suppress real defects).
713
+ The `final-verification` phase uses the same fresh one-shot `redispatch_worker` pattern and the same dispatch timing as §"Coverage critic pass" §"When" (provider + `config.critic.modelExecutionValue` from the `convergence.critic` block; critic is required; same model-unresolved skip rule) — the delivered work the critic inspects is likewise fixed before the reverify round starts. Only the prompt, the verification semantics, and the output sink differ — final-verification's findings are defects/blockers, so the critic acts as an **acceptance devil's advocate** (find reasons NOT to accept), and its candidate blockers are NEVER dropped (that would suppress real defects).
714
714
 
715
715
  Before that call, write the acceptance-only task instructions and run `okstra
716
716
  agent-prompt materialize` with `--audience acceptance-critic`,
@@ -149,7 +149,7 @@ The approval state transitions are fixed:
149
149
 
150
150
  Do not move `answered` back to `open` because a check failed. The user's choice stands. Record the failed check on the row; later stages still see the DISAGREE votes.
151
151
 
152
- `open` blocks until the user judges. `answered` with `select` / `accept-risk` / `answer`, `resolved`, and `obsolete` do not block approval or the next phase. `request-revision` and `reject` still withhold the next phase. `accept-risk` does not require re-verification `AGREE`. The worker votes stay on the plan item so a later stage can still see the dissent. **Enforced:** `scripts/okstra_ctl/clarification_items.py` `row_blocks_progress`, `validators/validate-run.py` `_user_accepted_plan_item_ids`.
152
+ `open` blocks until the user judges. `answered` with `select` / `accept-risk` / `answer`, `resolved`, and `obsolete` do not block approval or the next phase. `request-revision` and `reject` still withhold the next phase until this report's `supersessionLedger` records that the answer was incorporated (`superseded` or `no-dependent-statement`). `accept-risk` does not require re-verification `AGREE`. The worker votes stay on the plan item so a later stage can still see the dissent. **Enforced:** `scripts/okstra_ctl/clarification_items.py` `row_blocks_progress`, `validators/validate-run.py` `_user_accepted_plan_item_ids`.
153
153
 
154
154
  When a terminal row preserves a pre-correction dissent classification, keep superseded votes in `state/plan-body-verification-implementation-planning-<seq>.json`. Activities that implement or check the decision record the exact `C-NNN` in `clarificationRefs` and the affected `P-*` identifiers in `planItemIds`. Report assembly verifies that every resolution `checkRefs` value names an existing activity and derives each plan item's `clarificationRefs`; the lead never copies those references into `approvalContext`. A corrected coverage-only blocker keeps its `C-NNN` in the non-blocking Requirement Coverage row's `decisionRefs`. An `obsolete` row is invalid while its disagreement or coverage blocker remains active in the current plan. **Enforced:** `scripts/okstra_ctl/report_assembly.py` `_clarification_row` / `_attach_plan_backlinks` and `validators/validate-run.py` `_validate_v3_approval_context`.
155
155
 
@@ -414,19 +414,7 @@ Order of operations:
414
414
 
415
415
  Keep the assigned worker prompt history paths stable in `team-state`, `run-manifest`, and `task-manifest`. Do not rewrite prompt artifacts to `/tmp` or omit prompt metadata for attempted workers.
416
416
 
417
- After persistence, the run-level error log lives at `<runDir>/logs/errors-<task-type>-<seq>.jsonl`. Useful jq one-liners for retrospective review:
418
-
419
- ```bash
420
- # Counts per agent
421
- jq -s 'group_by(.agent) | map({agent: .[0].agent, count: length})' <runDir>/logs/errors-<task-type>-<seq>.jsonl
422
-
423
- # Counts per errorType
424
- jq -s 'group_by(.errorType) | map({type: .[0].errorType, count: length})' <runDir>/logs/errors-<task-type>-<seq>.jsonl
425
- ```
426
-
427
- The errors log is informational. Its presence/absence does not affect the final verdict. Do not block report writing on it.
428
-
429
- After persistence, reply briefly in the resolved Report Language. **Lead this reply with the run's task identity** — state `<task-group>/<task-id>` (or the full `taskKey`) first. Then: completion status, the task-qualified human report path, the report record path, validator result, any remaining blocker. **Close with the user's next action** — one command they can run now. A prohibition (`do not start implementation`) is not a next action. A status dump is not a close.
417
+ After persistence, reply briefly in the resolved Report Language. **Lead this reply with the run's task identity** state `<task-group>/<task-id>` (or the full `taskKey`) first. Then: completion status, the task-qualified human report path, the report record path, validator result, any remaining blocker. **Close with the user's next action** — one command they can run now. A prohibition (`do not start implementation`) is not a next action. A status dump is not a close. This closeout is also in the launch prompt (`prompts/launch.template.md` "User closeout (BLOCKING)") so it is not lazy-read.
430
418
 
431
419
  Pick the next action from this table; the first matching row wins:
432
420
 
@@ -440,6 +428,8 @@ When the host native picker is available and two of those rows could apply, ask
440
428
 
441
429
  **Every run-artifact path in this reply MUST be task-qualified** — report the human report as `.okstra/tasks/<task-group>/<task-id>/runs/<task-type>/reports/final-report-<task-type>-<seq>.html` rooted at the task bundle, NOT the bare `runs/<task-type>/reports/...` form (byte-for-byte identical across every task of the same task-type, so it cannot identify the task). Under that, cite the report record (`.data.json`) and one line to render the full reading copy: `okstra render-final-report <task-qualified data.json>`. The same task-qualified rule applies to the team-state path, resume command path, and any other run-artifact path this reply cites.
442
430
 
431
+ The run-level error log lives at `<runDir>/logs/errors-<task-type>-<seq>.jsonl`. It is informational. Its presence or absence does not affect the final verdict. Do not block report writing on it.
432
+
443
433
  ## Run-scoped worker-resource lifecycle
444
434
 
445
435
  - At run start, call the selected adapter's setup required to distinguish lead-owned resources from worker-owned resources.
@@ -364,7 +364,7 @@ round before any host or provider process starts.
364
364
  - `dissent-isolated` — only one worker `DISAGREE`s, others `AGREE`. On a blocking kind (`b` / `c` / `e`, and kind `a` on `P-Var-*`) this is scored `majority-disagree` and **blocks approval**. Advisory-only `DISAGREE(d)` and `P-Rb-*` stay recorded dissent and do not block. (Distinct from finding-convergence `worker-unique`, which means the *opposite*: only one worker AGREEs.)
365
365
  - `majority-disagree` — a *majority* of analysers `DISAGREE` (majority needs ≥2 participating non-error votes; rollback-ordering `DISAGREE(d)` votes are advisory and excluded from the tally), OR any blocking-kind dissent with ≥2 participating votes (a minority `DISAGREE` is not outvoted), OR any single-vote-blocking kind fires: one reproduced `DISAGREE(a)` on any item other than a `P-Var-*` one, or one reproduced `DISAGREE(f)` on a `P-Req-*` item (see §"Single-vote-blocking kinds"). This classification **blocks approval**.
366
366
  - `needs-reverify` — one of two shapes the round could not settle.
367
- - **An even split on a blocking kind.** The majority test is strict, so a panel splitting evenly (1-AGREE / 1-DISAGREE, 2-2, …) reaches neither `full-consensus` nor `majority-disagree`. Until this shape existed it folded into `has-dissent` and the gate passed: two verifiers read the same plan, disagreed on a defect that is not advisory, and the split was recorded and never acted on. An even panel is not only the two-analyser roster one `UNVERIFIABLE` or one lost dispatch makes any roster even for that item. Re-dispatch those items and record the votes with `--round 2`; a split that survives that round becomes `majority-disagree` and goes to the user, because nothing further is going to settle it. **The round is not optional**: `needs-reverify` folds into `passed-with-dissent`, so without the re-verification this classification would be a label and nothing else. **Enforced:** `validators/validate-run.py` `_validate_unresolved_tie_was_reverified` fails a gate declared over a tie that was never re-verified, and `_classify_plan_item_gate` promotes a tie carrying a round-2 verdict to `majority-disagree`.
367
+ - **An even split on a blocking kind.** The majority test is strict, so a panel splitting evenly (1-AGREE / 1-DISAGREE, 2-2, …) reaches neither `full-consensus` nor `majority-disagree`. Until this shape existed it folded into `has-dissent` and the gate passed. Do **not** re-run the original two. Dispatch `critic-worker` immediately on those items only (`okstra plan-items prepare --tie-vote`, then `okstra plan-items prompt`). The prompt carries the analyser split and no other plan items. Record the critic vote as `verdicts[].worker = critic-worker` with `okstra plan-items apply-verdicts --append`. Critic `AGREE` / `SUPPLEMENT` settles the split to `has-dissent`. Critic `DISAGREE` on a blocking kind is `majority-disagree`. **Enforced:** `validators/validate-run.py` `_classify_plan_item_gate` / `_validate_tie_received_extra_vote` / `_validate_unresolved_tie_was_reverified`, `okstra_ctl.plan_items.next_dispatch` kind `critic-tie`.
368
368
  - **A lone dissent nobody cross-verified** — a single-vote-blocking kind fired but the item has **fewer than 2 participating non-error votes**, i.e. the lone dissent was never cross-verified because its peer returned `verification-error`. A single-vote-blocking kind means "one *confirmed* DISAGREE is enough"; an unconfirmed one is not, and on a `P-Var-*` item none fires at all — its kind `a` never blocks on one vote and takes a majority like `b` / `e`. This does **not** block approval — blocking on it would make a worker failure produce a stricter gate than a healthy roster, the same paradox the ≥2-vote majority rule already rules out. The item is re-dispatched in the next round (step 7); if it survives the round budget it is promoted per step 8 with a Statement that says verification never completed. **Enforced:** `validators/validate-run.py` `_classify_plan_item_gate` returns `needs-reverify` for this shape and `_recompute_plan_body_gate` folds it into `passed-with-dissent`.
369
369
  - `contested` only meaningful when `maxRounds > 1`; at default `maxRounds=1`, fold any unresolved item into `partial-consensus`.
370
370
  5. Gate result resolution:
@@ -394,7 +394,8 @@ round before any host or provider process starts.
394
394
  |---|---|
395
395
  | `none` | Do not add a worker batch. A round whose only failures are missing-dependency command runs — `UNVERIFIABLE` on a declared `npm` / `pytest` / equivalent, §"Planning-time environment gap" — is this shape. |
396
396
  | `worker-correction` | Re-dispatch **only** those workers. Peers are not re-run. The queue does not become a new round. Place the output of `okstra plan-items correction-prompt --worker <id> --run-manifest … --state …` first in that worker's prompt — the environment-exception paragraph is first. A byte-identical re-dispatch reproduces the same failure; a corrected one recovered 37 substantive verdicts from a worker whose first attempt answered `UNVERIFIABLE` to all 80 items. |
397
- | `queue-reverify` | An unsettled tie on a blocking kind. Re-dispatch those `itemIds` only. |
397
+ | `queue-reverify` | An unsettled tie on a blocking kind. Legacy kind; current scoring emits `critic-tie` instead. |
398
+ | `critic-tie` | An unsettled analyser tie. Dispatch `critic-worker` on those `itemIds` only. The critic's verdict settles the split. Do not re-run the original two. |
398
399
 
399
400
  A referenced **path** that does not exist is still `DISAGREE(b)` / a fact probe, never environment-unverifiable. **Enforced:** `okstra_ctl.plan_items.next_dispatch` / `correction_prompt_text`.
400
401
 
@@ -439,7 +440,7 @@ round before any host or provider process starts.
439
440
  - `answered → resolved` only after the selected disposition is applied and its checks pass
440
441
  - `answered → open` when application or checking fails
441
442
  - `open → obsolete` only when a plan change removes the question
442
- `open` blocks until the user judges. `answered` with a proceeding disposition (`accept-risk` / `select` / `answer`) does not block. `request-revision` / `reject` still withhold the next phase. A user-directed correction does not consume the automatic self-fix limit, and a failed check does not restart the automatic loop or reopen the row.
443
+ `open` blocks until the user judges. `answered` with a proceeding disposition (`accept-risk` / `select` / `answer`) does not block. `request-revision` / `reject` still withhold the next phase until this report's `supersessionLedger` records that the answer was incorporated (`superseded` or `no-dependent-statement`). A user-directed correction does not consume the automatic self-fix limit, and a failed check does not restart the automatic loop or reopen the row.
443
444
  - A terminal row preserves its original dissent classification only from the convergence-owned state history. Every `user-decision-required` / `user-decision-evaluated` activity cites the row's `C-NNN` in `clarificationRefs` and affected plan items in `planItemIds`. A resolved decision names only existing `A-NNN` checks. Report assembly validates those links and derives the report backtraces; it does not accept copied IDs from the approval ledger. When an independent coverage-only blocker is corrected, keep the `C-NNN` in the non-blocking Requirement Coverage row's `decisionRefs`. `obsolete` is valid only after current evidence shows that the question or blocker disappeared.
444
445
  9. Approval lives in the report record `frontmatter.approved` field — there is no in-body marker line. The user may set it to `true` (via `--approve` or the in-session wizard) when remaining `Blocks=approval` rows are user-proceeded (`accept-risk` / `select` / `answer`) even if the recorded `gateResult` is still `blocked-by-disagreement`. `aborted-non-result` still withholds approval. **Enforced:** run-prep (`scripts/okstra_ctl/run.py` `_validate_approved_plan` / `_blocking_gate_survives_user_decision`) and `validators/validate-run.py` `_validate_plan_body_gate_recompute`.
445
446
 
@@ -104,6 +104,8 @@ Do not run the seven steps below manually. Invoke `okstra report-finalize`; cont
104
104
  6. **`validate-run`** — validate the record, views, run manifest, and team state.
105
105
  7. **`teardown-stages`** — remove eligible stage worktrees after successful validation.
106
106
 
107
+ After `report-finalize` returns, the lead — not the report writer — closes the run with the launch prompt's User closeout: one command the user can run now.
108
+
107
109
  ### Before `report-finalize`: the translation sidecar
108
110
 
109
111
  Never dispatch the translator before report assembly and `check-source`. For a non-English human report, first run `report-finalize --only token-usage --only project-activity --only check-source`; the extraction command refuses to build a work list from a non-English source. Then dispatch the translator worker with `okstra agent-prompt materialize --audience translator`, `okstra agent-prompt record-dispatch`, `okstra worker-dispatch --audience translator`, and `okstra agent-prompt link-result`. Resume with `report-finalize --only render-views --only spawn-followups --only validate-run --only teardown-stages`; do not assemble the record a second time.
@@ -112,7 +114,7 @@ Never dispatch the translator before report assembly and `check-source`. For a n
112
114
 
113
115
  `workflow.nextRecommendedPhase` has `phase`, `status`, and `rationale`. The status vocabulary is `ready`, `pending`, `blocked`, and `terminal`.
114
116
 
115
- `phase` is non-empty only for an authored `ready` pointer. `prepare` may lower `ready` to `pending` while retaining `phase` in `scripts/okstra_ctl/render.py::_derive_next_recommended_phase`; readers must use `status` for launchability. When `finalVerification.routingRecommendation.target` is `release-handoff(stage-group)`, write `phase` as `release-handoff`. When `implementationPlanning.outcome` is `plan-ready` and a `Blocks=approval` row still blocks progress (`open`, or `request-revision` / `reject`), or the plan-body gate is `aborted-non-result`, write `status` `blocked` (empty `phase`). `blocked-by-disagreement` with every approval row user-proceeded (`accept-risk` / `select` / `answer`) is approvable — write `ready` at `implementation`. The user still has to approve it separately.
117
+ `phase` is non-empty only for an authored `ready` pointer. `prepare` may lower `ready` to `pending` while retaining `phase` in `scripts/okstra_ctl/render.py::_derive_next_recommended_phase`; readers must use `status` for launchability. When `finalVerification.routingRecommendation.target` is `release-handoff(stage-group)`, write `phase` as `release-handoff`. When `implementationPlanning.outcome` is `plan-ready` and a `Blocks=approval` row still blocks progress (`open`, or an unincorporated `request-revision` / `reject`), or the plan-body gate is `aborted-non-result`, write `status` `blocked` (empty `phase`). `blocked-by-disagreement` with every approval row user-proceeded (`accept-risk` / `select` / `answer`) or incorporated in `supersessionLedger` is approvable — write `ready` at `implementation`. The user still has to approve it separately.
116
118
 
117
119
  ## Compatibility
118
120
 
@@ -14,4 +14,4 @@ mode:" when the include directive (placed at column 0) is resolved in-place.
14
14
  Do NOT write the literal include directive token in this file's body — the
15
15
  resolver matches it anywhere and would recurse on this file itself.
16
16
  -->
17
- - **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 **both** findings nobody covered and work the findings propose that no requirement asked for (`category: "unrequested-scope"`); its candidates are judged only after a 1-round adversarial reverify that follows convergence. The two halves are disposed of differently — a contested coverage gap is dropped as a hallucination, while a contested over-scope candidate is recorded as a `## 5. Missing Information and Risks` row instead of vanishing. See `prompts/lead/convergence.md` "Coverage critic pass".
17
+ - **Coverage critic (required, one slot)**: critic `min`/`recommended`/`max` are 1. The user picks the model at launch (`--role-model critic=<provider>/<model>` or the wizard role-model step). `--critic off` is rejected. A reused-worker critic pass is dispatched concurrently with the first convergence reverify round to surface **both** findings nobody covered and work the findings propose that no requirement asked for (`category: "unrequested-scope"`); its candidates are judged only after a 1-round adversarial reverify that follows convergence. The two halves are disposed of differently — a contested coverage gap is dropped as a hallucination, while a contested over-scope candidate is recorded as a `## 5. Missing Information and Risks` row instead of vanishing. In `implementation-planning`, the same critic slot also settles plan-body 1-1 splits (`critic-worker` on `--tie-vote` items only). See `prompts/lead/convergence.md` "Coverage critic pass" and `prompts/lead/plan-body-verification.md` even-split rule.
@@ -8,8 +8,8 @@ roles:
8
8
  max: 5
9
9
  duty: diagnosis-worker
10
10
  - role: critic
11
- min: 0
12
- recommended: 0
11
+ min: 1
12
+ recommended: 1
13
13
  max: 1
14
14
  duty: scope-critic
15
15
  - role: report-writer
@@ -8,7 +8,7 @@ roles:
8
8
  max: 5
9
9
  duty: acceptance-verifier
10
10
  - role: critic
11
- min: 0
11
+ min: 1
12
12
  recommended: 1
13
13
  max: 1
14
14
  duty: acceptance-critic
@@ -95,7 +95,7 @@ roles:
95
95
  4. **Verifier dissent preserved** — if workers reach different verdicts, the disagreement is visible in section 1.2; synthesis hides nothing.
96
96
  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.
97
97
  - Cross-verification mode:
98
- - **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; unconfirmed → a `Residual Risk` row (never dropped). See `prompts/lead/convergence.md` "Acceptance critic pass (final-verification)".
98
+ - **Acceptance critic (required, one slot)**: the user picks the critic model at launch. 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; unconfirmed → a `Residual Risk` row (never dropped). See `prompts/lead/convergence.md` "Acceptance critic pass (final-verification)".
99
99
  - Non-goals:
100
100
  - proposing unrelated refactors beyond the delivered scope
101
101
  - **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`, `implementation-option-selection`, or `implementation-planning` run according to whether the cause, direction, or detailed plan is invalid
@@ -8,8 +8,8 @@ roles:
8
8
  max: 5
9
9
  duty: planning-worker
10
10
  - role: critic
11
- min: 0
12
- recommended: 0
11
+ min: 1
12
+ recommended: 1
13
13
  max: 1
14
14
  duty: acceptance-critic
15
15
  - role: report-writer
@@ -234,7 +234,7 @@ roles:
234
234
  - `open → answered` when the raw user response is recorded
235
235
  - `answered → resolved` after the selected disposition is applied, when that work completed
236
236
  - `open → obsolete` only when a plan change removes the question
237
- `open` blocks until the user judges. `answered` with `accept-risk` / `select` / `answer` does not block. Do not move `answered` back to `open` because a check failed. **Enforced:** `scripts/okstra_ctl/clarification_items.py` `row_blocks_progress`, `validators/validate-run.py` `_validate_approval_context`, run-prep `scripts/okstra_ctl/run.py` `_validate_approved_plan`.
237
+ `open` blocks until the user judges. `answered` with `accept-risk` / `select` / `answer` does not block. `request-revision` / `reject` still block unless this report's `supersessionLedger` already incorporated that id. Do not move `answered` back to `open` because a check failed. **Enforced:** `scripts/okstra_ctl/clarification_items.py` `row_blocks_progress`, `validators/validate-run.py` `_validate_approval_context`, run-prep `scripts/okstra_ctl/run.py` `_validate_approved_plan`.
238
238
  - **Terminal approval evidence.** A resolved correctness-critical decision names a later successful evaluation through `resolutionInput.checkRefs`. Each referenced activity carries the same `C-NNN` in `clarificationRefs[]`, the affected `planItemIds[]`, zero-exit commands, and the plan-body state result. Report assembly rejects a missing activity or reverse link before publication. **Enforced:** `scripts/okstra_ctl/report_assembly.py::_clarification_row` and `_attach_plan_backlinks`.
239
239
  - **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 chosen realization introduces against the three criteria:
240
240
  1. **Hard to reverse** — would changing the decision later cost meaningfully more than deciding now?
@@ -8,8 +8,8 @@ roles:
8
8
  max: 5
9
9
  duty: discovery-worker
10
10
  - role: critic
11
- min: 0
12
- recommended: 0
11
+ min: 1
12
+ recommended: 1
13
13
  max: 1
14
14
  duty: scope-critic
15
15
  - role: report-writer
@@ -461,11 +461,9 @@
461
461
  }
462
462
  },
463
463
  "critic_pick": {
464
- "label": "추가 critic 패스를 돌릴까요? (놓친 finding/blocker 캐는 검증 패스 opt-in)",
464
+ "label": "critic 모델을 고르세요 ( 1명. 동수 계획 항목을 역할이 가릅니다)",
465
465
  "echo_template": "critic: {value}",
466
- "options": {
467
- "off": "사용 안 함 (기본·추천)"
468
- },
466
+ "options": {},
469
467
  "labels": {
470
468
  "provider_recommended": "{provider} critic (추천)",
471
469
  "provider": "{provider} critic"
@@ -5,7 +5,7 @@ import shutil
5
5
  from collections.abc import Callable
6
6
  from pathlib import Path
7
7
 
8
- from okstra_ctl.adapters.accounting import UnavailableUsageAccountingPort
8
+ from okstra_ctl.adapters.accounting import CliArtifactUsageAccountingPort
9
9
  from okstra_ctl.adapters.hosts.capability_adapter import (
10
10
  INTERACTION_FUNCTIONS,
11
11
  PENDING_HOST_PORT,
@@ -41,10 +41,7 @@ def create_adapter(
41
41
  interaction_port=None,
42
42
  lead_session_port=PENDING_HOST_PORT,
43
43
  worker_dispatch_port=PENDING_HOST_PORT,
44
- usage_accounting_port=UnavailableUsageAccountingPort(
45
- "Grok host usage accounting is unavailable because no session "
46
- "transcript or CLI usage artifact contract is registered."
47
- ),
44
+ usage_accounting_port=CliArtifactUsageAccountingPort(),
48
45
  provider_registry: ProviderRegistry | None = None,
49
46
  host_model_port=None,
50
47
  ) -> CapabilityHostAdapter:
@@ -347,6 +347,12 @@ def _append(args: argparse.Namespace) -> int:
347
347
  "evidenceRefs": args.evidence_ref,
348
348
  "outcome": args.outcome,
349
349
  }
350
+ clarification_refs = [
351
+ ref for ref in args.evidence_ref
352
+ if isinstance(ref, str) and re.fullmatch(r"C-\d{3,}", ref)
353
+ ]
354
+ if clarification_refs:
355
+ details["clarificationRefs"] = clarification_refs
350
356
  if args.request_ref is not None:
351
357
  details["activityRequestRef"] = args.request_ref
352
358
  event = record_activity(args.project_root, args.run_manifest, details)
@@ -5,6 +5,9 @@ a single data point. Each row carries a ``Blocks`` value out of
5
5
  ``{approval, next-phase, none}``. Rows with ``Blocks=approval`` are the
6
6
  approval gate: they MUST resolve before the user flips the frontmatter
7
7
  ``approved`` field to ``true`` and starts the next ``implementation`` run.
8
+ A ``request-revision`` / ``reject`` answer still blocks the report that has
9
+ not yet incorporated it. The report that recorded the same id in
10
+ ``supersessionLedger`` has already absorbed that return and does not block.
8
11
 
9
12
  The two schemas store those rows in different places, and that is why the
10
13
  read functions take a report **path**, not its text:
@@ -400,6 +403,10 @@ ANSWER_DISPOSITIONS = frozenset({
400
403
  PROCEEDING_DISPOSITIONS = frozenset({"answer", "select", "accept-risk"})
401
404
  # 사용자가 이 계획으로 진행하지 않겠다고 고른 처분.
402
405
  RETURN_DISPOSITIONS = frozenset({"request-revision", "reject"})
406
+ # 이 런이 그 답을 본문에 반영했다고 원장이 적은 처분.
407
+ INCORPORATED_LEDGER_DISPOSITIONS = frozenset(
408
+ {"superseded", "no-dependent-statement"}
409
+ )
403
410
 
404
411
 
405
412
  def clarification_disposition(row: Mapping[str, object]) -> str:
@@ -425,13 +432,46 @@ def clarification_disposition(row: Mapping[str, object]) -> str:
425
432
  return ""
426
433
 
427
434
 
428
- def row_blocks_progress(status: str, disposition: str = "") -> bool:
435
+ def incorporated_clarification_ids(
436
+ report_data: Mapping[str, object] | None,
437
+ ) -> frozenset[str]:
438
+ """이 런이 답을 본문에 반영했다고 원장에 적은 C-id.
439
+
440
+ `superseded` 와 `no-dependent-statement` 만 센다. 원장에 없는
441
+ `request-revision` / `reject` 는 아직 다음 계획을 막는 되돌림이다.
442
+ """
443
+ if not isinstance(report_data, Mapping):
444
+ return frozenset()
445
+ planning = report_data.get("implementationPlanning")
446
+ if not isinstance(planning, Mapping):
447
+ return frozenset()
448
+ ledger = planning.get("supersessionLedger")
449
+ if not isinstance(ledger, list):
450
+ return frozenset()
451
+ ids: set[str] = set()
452
+ for entry in ledger:
453
+ if not isinstance(entry, Mapping):
454
+ continue
455
+ row_id = str(entry.get("clarificationId") or "").strip()
456
+ disposition = str(entry.get("disposition") or "").strip().lower()
457
+ if row_id and disposition in INCORPORATED_LEDGER_DISPOSITIONS:
458
+ ids.add(row_id)
459
+ return frozenset(ids)
460
+
461
+
462
+ def row_blocks_progress(
463
+ status: str,
464
+ disposition: str = "",
465
+ *,
466
+ incorporated: bool = False,
467
+ ) -> bool:
429
468
  """이 행이 승인·다음 단계 진입을 막는가.
430
469
 
431
470
  진행 처분(`accept-risk` / `select` / `answer`)은 고치지 않은 DISAGREE 를
432
471
  행과 투표에 남긴 채로 게이트만 내린다. `request-revision` / `reject` 는
433
- 사용자가 진행을 거절한 것이므로 막는다. 처분이 없는 `answered` 판단
434
- 기록이므로 막지 않는다.
472
+ 사용자가 진행을 거절한 것이므로 막는다. 다만 보고서 원장이 그 답을
473
+ 이미 반영했으면(`incorporated`) 같은 되돌림이 다음 계획 런을 강제하지
474
+ 않는다. 처분이 없는 `answered` 도 판단 기록이므로 막지 않는다.
435
475
  """
436
476
  normalized_status = status.strip().lower()
437
477
  normalized_disposition = disposition.strip().lower()
@@ -440,17 +480,23 @@ def row_blocks_progress(status: str, disposition: str = "") -> bool:
440
480
  if normalized_disposition in PROCEEDING_DISPOSITIONS:
441
481
  return False
442
482
  if normalized_disposition in RETURN_DISPOSITIONS:
443
- return True
483
+ return not incorporated
444
484
  return normalized_status not in {"answered", "resolved"}
445
485
 
446
486
 
447
487
  def progress_blocking_ids(
448
488
  rows: object,
449
489
  blocking_values: frozenset[str] = APPROVAL_BLOCKS,
490
+ *,
491
+ report_data: Mapping[str, object] | None = None,
450
492
  ) -> list[str]:
451
- """게이트를 아직 막는 행 id. 사용자 진행 처분이 있는 행은 빠진다."""
493
+ """게이트를 아직 막는 행 id. 사용자 진행 처분이 있는 행은 빠진다.
494
+
495
+ ``report_data`` 가 있으면 원장에 반영된 되돌림 행도 빠진다.
496
+ """
452
497
  if not isinstance(rows, list):
453
498
  return []
499
+ incorporated = incorporated_clarification_ids(report_data)
454
500
  ids: list[str] = []
455
501
  for row in rows:
456
502
  if not isinstance(row, Mapping):
@@ -460,7 +506,11 @@ def progress_blocking_ids(
460
506
  row_id = row.get("id")
461
507
  if (
462
508
  blocks in blocking_values
463
- and row_blocks_progress(status, clarification_disposition(row))
509
+ and row_blocks_progress(
510
+ status,
511
+ clarification_disposition(row),
512
+ incorporated=isinstance(row_id, str) and row_id in incorporated,
513
+ )
464
514
  and isinstance(row_id, str)
465
515
  and row_id
466
516
  ):
@@ -487,8 +537,9 @@ def scan_approval_gate(report_path: Path) -> ClarificationScan:
487
537
 
488
538
  A recorded user proceeding disposition (`accept-risk` / `select` /
489
539
  `answer`), including one that lives only in the sidecar, does not block.
490
- ``request-revision`` / ``reject`` still block. The scan refuses to guess
491
- whenever the schema drifted.
540
+ ``request-revision`` / ``reject`` still block unless this report's
541
+ ``supersessionLedger`` already incorporated that id. The scan refuses to
542
+ guess whenever the schema drifted.
492
543
  """
493
544
  return scan_clarification_blockers(
494
545
  report_path,
@@ -516,8 +567,10 @@ def scan_clarification_blockers(
516
567
  """Shared fail-closed clarification walk for both gates above — schema-v2
517
568
  reads its rows from the data sibling and schema-v1 from the §1 table.
518
569
  ``honor_sidecar_answers`` hides rows the sidecar already answered.
519
- ``sidecar_unblocks_proceeding_only`` keeps ``request-revision`` / ``reject``
520
- as blockers so a return choice cannot start the next phase.
570
+ ``sidecar_unblocks_proceeding_only`` keeps an unincorporated
571
+ ``request-revision`` / ``reject`` as a blocker so a return choice cannot
572
+ start the next phase. A ledger entry for that id means this report already
573
+ absorbed the return.
521
574
  """
522
575
  v2_scan = _scan_v2_blockers(report_path, blocking_values)
523
576
  scan = (
@@ -634,11 +687,14 @@ def _scan_v2_blockers(
634
687
  f"schema-v2 data.json has {unparsed} `clarificationItems` row(s) "
635
688
  "missing id/blocks/status"
636
689
  ))
690
+ incorporated = incorporated_clarification_ids(data)
637
691
  blockers = [
638
692
  row["item"] for row in rows
639
693
  if row["item"].blocks in blocking_values
640
694
  and row_blocks_progress(
641
- row["item"].status, str(row.get("disposition") or "")
695
+ row["item"].status,
696
+ str(row.get("disposition") or ""),
697
+ incorporated=row["item"].row_id in incorporated,
642
698
  )
643
699
  ]
644
700
  return ClarificationScan(blockers, None)
@@ -223,7 +223,9 @@ def _from_option_selection(report_data: Mapping[str, Any]) -> dict[str, str]:
223
223
 
224
224
  def _unresolved_approval_ids(report_data: Mapping[str, Any]) -> list[str]:
225
225
  return progress_blocking_ids(
226
- report_data.get("clarificationItems"), APPROVAL_BLOCKS
226
+ report_data.get("clarificationItems"),
227
+ APPROVAL_BLOCKS,
228
+ report_data=report_data,
227
229
  )
228
230
 
229
231
 
@@ -234,8 +236,9 @@ def _planning_approval_block_reason(
234
236
 
235
237
  자문 게이트(`passed-with-dissent`)와 재현 실패 `has-dissent` 는 여기 안
236
238
  들어온다. 차단은 `aborted-non-result` 와, 사용자가 아직 진행 처분을
237
- 고르지 않은 `Blocks=approval` 행이다. `blocked-by-disagreement` 는 그
238
- 행들이 전부 `accept-risk` / `select` / `answer` 이면 증거가 된 뒤라
239
+ 고르지 않았고 이 런 원장에도 반영되지 않은 `Blocks=approval` 행이다.
240
+ `blocked-by-disagreement` 는 그 행들이 전부 `accept-risk` / `select` /
241
+ `answer` 이거나 원장이 되돌림 답을 반영했으면 증거가 된 뒤라
239
242
  포인터를 막지 않는다.
240
243
  """
241
244
  ids = _unresolved_approval_ids(report_data)