okstra 0.219.0 → 0.220.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 (98) hide show
  1. package/README.md +5 -5
  2. package/dist/cli-registry.mjs +18 -10
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/lifecycle/config.mjs +2 -3
  5. package/dist/commands/lifecycle/config.mjs.map +1 -1
  6. package/dist/lib/runtime-payload.mjs +1 -2
  7. package/dist/lib/runtime-payload.mjs.map +1 -1
  8. package/docs/architecture/storage-model.md +1 -1
  9. package/docs/architecture.md +9 -7
  10. package/docs/cli.md +5 -3
  11. package/docs/project-structure-overview.md +3 -4
  12. package/package.json +1 -1
  13. package/runtime/BUILD.json +2 -2
  14. package/runtime/prompts/launch.template.md +1 -1
  15. package/runtime/prompts/lead/okstra-lead-contract.md +5 -4
  16. package/runtime/prompts/lead/report-writer.md +10 -17
  17. package/runtime/prompts/lead/team-contract.md +1 -2
  18. package/runtime/prompts/wizard/prompts.ko.json +0 -1
  19. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
  20. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +1 -1
  21. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +0 -1
  22. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +1 -1
  23. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +0 -1
  24. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +0 -1
  25. package/runtime/python/okstra_ctl/agent/invocation.py +1 -2
  26. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +2 -2
  27. package/runtime/python/okstra_ctl/agent/prompt_cli/jobs.py +0 -2
  28. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +2 -27
  29. package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -4
  30. package/runtime/python/okstra_ctl/blocking_checks.py +8 -0
  31. package/runtime/python/okstra_ctl/convergence_provenance.py +8 -2
  32. package/runtime/python/okstra_ctl/correction_cli.py +59 -0
  33. package/runtime/python/okstra_ctl/correction_round.py +168 -0
  34. package/runtime/python/okstra_ctl/dispatch_core.py +21 -349
  35. package/runtime/python/okstra_ctl/dispatch_state.py +0 -13
  36. package/runtime/python/okstra_ctl/domain/provider.py +1 -1
  37. package/runtime/python/okstra_ctl/domain/role.py +0 -6
  38. package/runtime/python/okstra_ctl/final_report_paths.py +0 -16
  39. package/runtime/python/okstra_ctl/i18n.py +2 -2
  40. package/runtime/python/okstra_ctl/path_hints.py +0 -4
  41. package/runtime/python/okstra_ctl/paths.py +0 -3
  42. package/runtime/python/okstra_ctl/phases/final_verification/profile.md +1 -0
  43. package/runtime/python/okstra_ctl/phases/final_verification/spec.md +2 -0
  44. package/runtime/python/okstra_ctl/phases/final_verification/target.py +17 -1
  45. package/runtime/python/okstra_ctl/phases/implementation/entry.py +12 -0
  46. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +30 -6
  47. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md +1 -0
  48. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md +2 -0
  49. package/runtime/python/okstra_ctl/phases/implementation/profile.md +2 -1
  50. package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation.template.html +6 -0
  51. package/runtime/python/okstra_ctl/phases/implementation/spec.md +6 -1
  52. package/runtime/python/okstra_ctl/phases/implementation/validation.py +62 -1
  53. package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +1 -1
  54. package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +24 -3
  55. package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +9 -9
  56. package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +4 -2
  57. package/runtime/python/okstra_ctl/render.py +0 -10
  58. package/runtime/python/okstra_ctl/render_final_report.py +4 -4
  59. package/runtime/python/okstra_ctl/report_finalize.py +37 -76
  60. package/runtime/python/okstra_ctl/report_html/render.py +4 -44
  61. package/runtime/python/okstra_ctl/report_language.py +3 -3
  62. package/runtime/python/okstra_ctl/report_prose_keys.py +153 -0
  63. package/runtime/python/okstra_ctl/report_synthesis_packet.py +1 -1
  64. package/runtime/python/okstra_ctl/run.py +1 -21
  65. package/runtime/python/okstra_ctl/stage_done.py +183 -0
  66. package/runtime/python/okstra_ctl/stage_fail.py +114 -0
  67. package/runtime/python/okstra_ctl/stage_integrate.py +37 -2
  68. package/runtime/python/okstra_ctl/stage_rebase.py +189 -0
  69. package/runtime/python/okstra_ctl/stage_reopen.py +12 -8
  70. package/runtime/python/okstra_ctl/stage_targets.py +29 -62
  71. package/runtime/python/okstra_ctl/team.py +1 -6
  72. package/runtime/python/okstra_ctl/wizard/confirmation.py +0 -8
  73. package/runtime/python/okstra_ctl/wizard/registry.py +2 -3
  74. package/runtime/python/okstra_ctl/wizard/sources.py +10 -0
  75. package/runtime/python/okstra_ctl/worker_prompt_policy.py +3 -12
  76. package/runtime/python/okstra_ctl/worktree/__init__.py +1 -1
  77. package/runtime/python/okstra_ctl/worktree/decisions.py +3 -4
  78. package/runtime/python/okstra_ctl/worktree/provision.py +27 -0
  79. package/runtime/python/okstra_ctl/worktree/rebase.py +108 -0
  80. package/runtime/python/okstra_ctl/worktree_registry.py +14 -0
  81. package/runtime/schemas/final-report-v3.0.schema.json +78 -0
  82. package/runtime/skills/_fragments/wizard-render-bundle.md +1 -1
  83. package/runtime/skills/okstra-discussion/SKILL.md +1 -1
  84. package/runtime/skills/okstra-run/SKILL.md +1 -1
  85. package/runtime/skills/okstra-setup/SKILL.md +2 -2
  86. package/runtime/skills/okstra-setup/references/project-config.md +2 -2
  87. package/runtime/templates/report-writer-prompt-preamble.md +1 -1
  88. package/runtime/templates/reports/html/i18n/en.json +6 -1
  89. package/runtime/templates/reports/html/i18n/ko.json +6 -1
  90. package/runtime/templates/worker-prompt-preamble.md +1 -1
  91. package/runtime/validators/validate-run.py +3 -28
  92. package/runtime/agents/operations/report-translation.json +0 -6
  93. package/runtime/agents/roles/translator.json +0 -19
  94. package/runtime/bin/okstra-report-translate.py +0 -404
  95. package/runtime/prompts/duties/translator.json +0 -32
  96. package/runtime/python/okstra_ctl/report_translation.py +0 -509
  97. package/runtime/python/okstra_ctl/report_translation_dispatch.py +0 -356
  98. package/runtime/templates/translator-prompt-preamble.md +0 -36
@@ -129,29 +129,22 @@ For historical schema-v1 Markdown only, the following heading table remains a re
129
129
 
130
130
  **Enforced:** `okstra_ctl.report_finalize.V3_STEP_ORDER` is the order — `report-finalize` runs the steps from that tuple, so the sequence cannot be reordered by a caller. Running the steps by hand is what this rule forbids, and that path is not reachable through the CLI.
131
131
 
132
- Do not run the eleven steps below manually. Invoke `okstra report-finalize`; contract 3.0 runs them in this order:
132
+ Do not run the ten steps below manually. Invoke `okstra report-finalize`; contract 3.0 runs them in this order:
133
133
 
134
134
  1. **`token-usage`** — collect usage into team state without touching the final record.
135
135
  2. **`project-activity`** — report assembly validates every owner input and publishes the final record once.
136
- 3. **`preflight`** — run the same schema and conformance checks used by final validation before translation, without writing run status. Structural failure skips translation; rendering and final validation still record the failure. `--only translate` also includes this check.
137
- 4. **`translate`** — for a non-English `reportLanguage`, materialize and dispatch the translator worker and require its `*.i18n.<lang>.json` sidecar; a no-op for English or when the sidecar already exists.
138
- 5. **`render-views`** — render the Markdown reading copy and human HTML, with the translation sidecar overlaid.
139
- 6. **`spawn-followups`** — materialize registered follow-up tasks.
140
- 7. **`record-verified`** — for a final-verification whose verdict clears the work for release, run `okstra handoff record-verified` once per stage in `finalVerification.stageReports`, so the `verified` rows exist before validation reads them; a no-op for other task types and for a verdict that blocks release. Do not run `handoff record-verified` by hand.
141
- 8. **`validate-run`** — validate the record, views, run manifest, and team state.
142
- 9. **`record-group-memory`** — write this run's conclusion (headline, decisions, watch-outs, open follow-ups, record path, next phase) and the group's start order into the task-group's `group-context.md` okstra region, creating the file when the group has none; skipped, like teardown, when an earlier step failed. Sibling tasks read it as `## Task-Group Memory`.
143
- 10. **`teardown-stages`** — remove eligible stage worktrees after successful validation.
144
- 11. **`prune-run-artifacts`** — remove every `node_modules` and `.next` directory under the run directory, and the dispatch snapshots (`*.mutation-audit.json`) and publication locks (`*.publish.lock`) recorded by a run whose validation passed or by an earlier run in the same run directory as a passed run; it runs even after a failed step.
136
+ 3. **`preflight`** — run the same schema and conformance checks used by final validation before rendering, without writing run status. Structural failure skips `spawn-followups`; rendering and final validation still record the failure.
137
+ 4. **`render-views`** — render the Markdown reading copy and human HTML from the record. The report writer already wrote the prose in `reportLanguage`; nothing translates it.
138
+ 5. **`spawn-followups`** — materialize registered follow-up tasks.
139
+ 6. **`record-verified`** — for a final-verification whose verdict clears the work for release, run `okstra handoff record-verified` once per stage in `finalVerification.stageReports`, so the `verified` rows exist before validation reads them; a no-op for other task types and for a verdict that blocks release. Do not run `handoff record-verified` by hand.
140
+ 7. **`validate-run`** — validate the record, views, run manifest, and team state.
141
+ 8. **`record-group-memory`** — write this run's conclusion (headline, decisions, watch-outs, open follow-ups, record path, next phase) and the group's start order into the task-group's `group-context.md` okstra region, creating the file when the group has none; skipped, like teardown, when an earlier step failed. Sibling tasks read it as `## Task-Group Memory`.
142
+ 9. **`teardown-stages`** — remove eligible stage worktrees after successful validation.
143
+ 10. **`prune-run-artifacts`** — remove every `node_modules` and `.next` directory under the run directory, and the dispatch snapshots (`*.mutation-audit.json`) and publication locks (`*.publish.lock`) recorded by a run whose validation passed or by an earlier run in the same run directory as a passed run; it runs even after a failed step.
145
144
 
146
145
  After `report-finalize` returns with `ok: true`, the lead closes the run with the launch prompt's User closeout. A generated HTML file alone does not establish successful validation.
147
146
 
148
- When the result carries `recovery.mode: same-run`, continue the authorized work in this run. Read every `recovery.issues` entry and failed step output. Correct writer-owned fields through the correction ledger procedure above; correct orchestrator-owned inputs through their owning commands. If plan content changes, prepare and verify the affected items before finalizing. Then execute `recovery.resumeCommand`, which retains this run manifest and resumes from the earliest failure, including reassembly of contract 3.0 inputs. After a narrative correction, check an existing translation with `okstra report-translate check-data --run-manifest <manifest>` and regenerate it if its source no longer matches. Keep previous approvals, user decisions, model selections and completed evidence. Do not ask for another `/okstra-run` to repair references, coverage summaries, narrative syntax or command spelling. Ask only when an unresolved user decision or an actual external prerequisite prevents the correction. Do not edit an installed runtime or another repository while recovering a consuming project's task. **Enforced:** `report_finalize._finalize_recovery` returns the same-run command and complete owner issues, `closeout_command` suppresses the new-run closeout for that result, and `tests/report/test_report_finalize.py` checks recovery after a failed step. Execution of the correction remains the lead's responsibility.
149
-
150
- ### The translation sidecar: the `translate` step
151
-
152
- `report-finalize` dispatches the translator itself. Its `translate` step runs directly before `render-views` (which overlays the sidecar), and it is a no-op when `reportLanguage` is `en` or the `*.i18n.<lang>.json` sidecar already exists. When the sidecar is missing, the step reuses this run's undispatched translator reservation if the lead already materialized one, otherwise it materializes the prompt itself — instruction file `state/translator-instructions-<task-type>-<seq>.md` (kept when the lead wrote one), prompt `prompts/translator-worker-prompt-<task-type>-<seq>.md`, `--result worker-results/translator-translations-<task-type>-<seq>.md`, `--audit-source worker-results/translator-worker-<task-type>-<seq>.md`, invocation id `<task-type>-<seq>-translator` (`-r2`, `-r3` after a failed attempt) — then dispatches the worker and waits for it: on a `terminalBackend: cmux-pane` run into a cmux pane like every other worker (the same code as `okstra team dispatch` followed by `okstra team await`; run-end `okstra team teardown` closes that pane), otherwise through the CLI-wrapper dispatcher. Either path records the dispatch and links the result. Do not run `agent-prompt materialize --audience translator` or `worker-dispatch --workers translator` by hand before `report-finalize`; the sequence used to be a manual lead step and was skipped in practice (2026-09-09, fontsninja-v3-site dev-10628-3: a `ko` run finalized in one call, zero translator reservations, English HTML). **Enforced:** `okstra_ctl.report_finalize.V3_STEP_ORDER` places the step; `okstra_ctl.report_translation_dispatch.translate_report` owns it; `_translator_job_from_reservation` in `scripts/okstra_ctl/dispatch_core.py` still refuses two undispatched reservations for one run, which only a hand-made second reservation produces.
153
-
154
- The step succeeds only when the sidecar exists afterwards; a translator that exits 0 without publishing it fails the step. A failed `translate` does not stop the sequence: `render-views` still writes the view from the English body, `validate-run` records the missing sidecar as an advisory, and the result's resume hint starts at `--only translate --only render-views …` — run that once the cause (a host approval gate, an unavailable provider) is cleared rather than closing the run on an English view.
147
+ When the result carries `recovery.mode: same-run`, continue the authorized work in this run. Read every `recovery.issues` entry and failed step output. Correct writer-owned fields through the correction ledger procedure above; correct orchestrator-owned inputs through their owning commands. If plan content changes, prepare and verify the affected items before finalizing. Then execute `recovery.resumeCommand`, which retains this run manifest and resumes from the earliest failure, including reassembly of contract 3.0 inputs. Keep previous approvals, user decisions, model selections and completed evidence. Do not ask for another `/okstra-run` to repair references, coverage summaries, narrative syntax or command spelling. Ask only when an unresolved user decision or an actual external prerequisite prevents the correction. Do not edit an installed runtime or another repository while recovering a consuming project's task. **Enforced:** `report_finalize._finalize_recovery` returns the same-run command and complete owner issues, `closeout_command` suppresses the new-run closeout for that result, and `tests/report/test_report_finalize.py` checks recovery after a failed step. Execution of the correction remains the lead's responsibility.
155
148
 
156
149
  ## Routing pointer
157
150
 
@@ -13,7 +13,7 @@ Okstra tasks use one lead plus the exact worker assignments selected in the prep
13
13
 
14
14
  ### Role Definitions
15
15
 
16
- The start screen assigns a **model ref** (`provider/model`) to each **canonical role** slot: `leader`, `analyser`, `critic`, `designer`, `planner`, `implementer`, `verifier`, `report-writer`, `translator`. A provider name is the front of a model ref, not a role. Every `analyser` in the run shares an identical core responsibility. Specialization is additive — it lives in optional Section 7 of the worker output, NOT in differentiated core questions. Cross-verification only converges if every rostered analyser answers the same questions against the same brief.
16
+ The start screen assigns a **model ref** (`provider/model`) to each **canonical role** slot: `leader`, `analyser`, `critic`, `designer`, `planner`, `implementer`, `verifier`, `report-writer`. A provider name is the front of a model ref, not a role. Every `analyser` in the run shares an identical core responsibility. Specialization is additive — it lives in optional Section 7 of the worker output, NOT in differentiated core questions. Cross-verification only converges if every rostered analyser answers the same questions against the same brief.
17
17
 
18
18
  | Canonical role | Core responsibility | Notes |
19
19
  |------|------|------|
@@ -25,7 +25,6 @@ The start screen assigns a **model ref** (`provider/model`) to each **canonical
25
25
  | `implementer` | Sole change author for one approved stage | Used by `implementation` |
26
26
  | `verifier` | Independent review of the assigned duty's subject | Duty varies by task type |
27
27
  | `report-writer` | **Authors** the final-report file in Phase 6 | Excluded from Phase 4/5 and convergence |
28
- | `translator` | Translates the designated sidecar only | Phase 7 |
29
28
 
30
29
  **Dispatch does not invent a missing model assignment.** Launch-time empty slots are filled by the model default chain before the run starts. At dispatch the model for every role comes from `resultContract.workerRoles[*].modelExecutionValue` in `task-manifest.json` (and lead model metadata). There is no per-role hard-coded fallback — see "Model Assignment Rules" below.
31
30
 
@@ -569,7 +569,6 @@
569
569
  },
570
570
  "confirmation": {
571
571
  "header": "선택 확인:",
572
- "translation_scope": " 번역 작업자: {model} — 보고서 작성자와 같은 제공자·모델을 사용합니다. 선택 언어의 보고서가 필요하면 보고서 본문과 관련 분석·검증 결과를 전달해 번역하고 최종 보고서를 생성합니다. 같은 작업 범위의 재검증·수정·재시도에도 위에서 선택한 제공자·모델을 사용합니다.",
573
572
  "provider_data_scope": "\n전달 대상: 위 역할·모델 목록의 제공자(현재 세션 및 선택한 외부 모델 제공자).\n전달 자료: `{project_root}`의 이 작업에 대한 작업 개요, 작업 수행에 필요한 저장소 소스·문서, 선택한 근거 자료 및 실행 중 생성되는 관련 분석·검증 결과.\n진행을 선택하면 위 대상에 해당 자료를 전달하여 선택한 작업을 실행하는 것을 승인합니다. 선택에 없는 제공자나 작업과 무관한 자료는 승인 범위에 포함되지 않습니다. 실행 환경의 권한 검토는 별도로 적용됩니다.",
574
573
  "static_role": " static-role : {role}#{ordinal} / {model}",
575
574
  "dynamic_role": " dynamic-role : {role} / reuse selected participant model",
@@ -190,7 +190,7 @@ If preparation has already failed with a Git lock creation error or `Operation n
190
190
 
191
191
  ### Permission at the dispatch boundary
192
192
 
193
- Read `runManifest.userAuthorization` before a live dispatch, including `report-finalize`, which can start the translator internally. It preserves the confirmation text and the user response relayed through the wizard; quote its relevant recipient/material scope and response in the execution justification alongside the prepared assignment. The translator reuses the report writer's provider/model, as disclosed at confirmation. Do not wait until translation to inspect that authorization. A missing record is not consent, and the record does not override a host rejection. If review reports missing evidence already present in the record, submit that existing evidence through the permitted review mechanism; ask the user only when the actual approved scope does not cover the dispatch.
193
+ Read `runManifest.userAuthorization` before a live dispatch. It preserves the confirmation text and the user response relayed through the wizard; quote its relevant recipient/material scope and response in the execution justification alongside the prepared assignment. A missing record is not consent, and the record does not override a host rejection. If review reports missing evidence already present in the record, submit that existing evidence through the permitted review mechanism; ask the user only when the actual approved scope does not cover the dispatch.
194
194
 
195
195
  Before a live CLI worker dispatch, check the current host's declared sandbox and approval policy. In a restricted Codex session, request the host's supported execution permission on the dispatch command itself (`sandbox_permissions: "require_escalated"` for `exec_command` when that mechanism is available and permitted). Scope the request to the prepared run and explain that it starts the selected worker processes. Follow the host's approval decision; a wizard confirmation or a successful preflight does not grant this permission.
196
196
 
@@ -303,7 +303,7 @@ def create_provider() -> ProviderSpec:
303
303
  role: "gemini-3.1-pro"
304
304
  for role in (
305
305
  "lead", "analyser", "critic", "designer", "planner", "executor",
306
- "verifier", "report-writer", "translator",
306
+ "verifier", "report-writer",
307
307
  )
308
308
  },
309
309
  wrapper="okstra-antigravity-exec.sh",
@@ -168,7 +168,6 @@ def create_provider() -> ProviderSpec:
168
168
  "executor": "opus",
169
169
  "verifier": "opus",
170
170
  "report-writer": "sonnet",
171
- "translator": "sonnet",
172
171
  },
173
172
  wrapper="okstra-claude-exec.sh",
174
173
  supported_roles=ALL_ROLE_TOKENS,
@@ -133,7 +133,7 @@ def create_provider() -> ProviderSpec:
133
133
  models=CODEX,
134
134
  default_models={
135
135
  role: "gpt-6.1-sol"
136
- for role in ("lead", "analyser", "critic", "designer", "planner", "executor", "verifier", "report-writer", "translator")
136
+ for role in ("lead", "analyser", "critic", "designer", "planner", "executor", "verifier", "report-writer")
137
137
  },
138
138
  wrapper="okstra-codex-exec.sh",
139
139
  supported_roles=ALL_ROLE_TOKENS,
@@ -308,7 +308,6 @@ def create_provider() -> ProviderSpec:
308
308
  "executor",
309
309
  "verifier",
310
310
  "report-writer",
311
- "translator",
312
311
  )
313
312
  },
314
313
  wrapper="okstra-grok-exec.sh",
@@ -104,7 +104,6 @@ def create_provider() -> ProviderSpec:
104
104
  "executor": "kimi-k3",
105
105
  "verifier": "kimi-k3",
106
106
  "report-writer": "kimi-k3",
107
- "translator": "kimi-k3",
108
107
  },
109
108
  wrapper="okstra-kimi-exec.sh",
110
109
  supported_roles=ALL_ROLE_TOKENS,
@@ -39,7 +39,6 @@ AgentAudience = Literal[
39
39
  "scope-critic",
40
40
  "acceptance-critic",
41
41
  "report-writer",
42
- "translator",
43
42
  "code-reviewer",
44
43
  "schedule-verifier",
45
44
  "business-flow-investigator",
@@ -604,7 +603,7 @@ def load_role_duty_contract(duty_root: Path, audience: AgentAudience) -> DutyCon
604
603
  def digest_duty_catalog(duty_root: Path) -> str:
605
604
  """Return the canonical digest of every contract file in a snapshot.
606
605
 
607
- 역할 계약은 하위 `roles/` 에 둔다 — `report-writer` 와 `translator` 는 직무
606
+ 역할 계약은 하위 `roles/` 에 둔다 — `report-writer` 는 직무
608
607
  id 이면서 역할 id 라, 한 폴더에 두면 두 계약이 같은 파일 이름을 두고 겹친다.
609
608
  """
610
609
  names = [
@@ -114,8 +114,8 @@ def _add_materialize_parser(commands: argparse._SubParsersAction) -> None:
114
114
  )
115
115
  materialize.add_argument(
116
116
  "--audit-source",
117
- help=run_only + "; accepted only for report-writer and translator, whose "
118
- "result is not their own worker result",
117
+ help=run_only + "; accepted only for report-writer, whose result is "
118
+ "not its own worker result",
119
119
  )
120
120
  materialize.add_argument("--host-runtime", help=standalone_only)
121
121
  materialize.add_argument("--execution-runner", choices=("cli-wrapper",), help=standalone_only)
@@ -81,8 +81,6 @@ def _metadata_job(
81
81
  if metadata.get("executionIdentityVersion") != 2 or metadata["dispatchKind"] != dispatch_kind:
82
82
  raise AgentPromptCliError(f"{metadata_path}: expected v2 dispatchKind {dispatch_kind!r}")
83
83
  execution = next(row for row in manifest["roleExecutions"] if row["roleExecutionRef"] == metadata["roleExecutionRef"])
84
- if execution["role"] == "translator":
85
- raise AgentPromptCliError("translator uses canonical reservation dispatch: worker-dispatch --workers translator")
86
84
  prompt = _project_input(project, metadata["prompt"]["path"], "prompt")
87
85
  anchors = prompt_anchor_values(prompt, ("Result Path", "Worker Result Path"))
88
86
  result = anchors.get("Worker Result Path") or anchors.get("Result Path")
@@ -99,7 +99,7 @@ _AUDIENCES = frozenset(get_args(AgentAudience))
99
99
  # 결과 파일과 별개의 워커 결과 포인터를 가진 audience. 이들만 `--audit-source` 로
100
100
  # 두 번째 경로를 받고, 프롬프트에 `**Worker Result Path:**` 앵커가 생긴다.
101
101
  # 나머지 audience 의 결과는 하나뿐이고 감사 사이드카는 그 이름에서 파생된다.
102
- _POINTER_AUDIENCES = frozenset({"report-writer", "translator"})
102
+ _POINTER_AUDIENCES = frozenset({"report-writer"})
103
103
 
104
104
 
105
105
  def _materialize(args: argparse.Namespace) -> PreparedAgentInvocation:
@@ -196,7 +196,7 @@ def _materialize_run(
196
196
  "the same path in the jobs file's workerResultPath"
197
197
  )
198
198
  audit_source_path = result_path
199
- elif args.audience == "report-writer":
199
+ else:
200
200
  if audit_source_path is None:
201
201
  # The report writer is the one audience whose result path is not its
202
202
  # own worker result: it writes the report body, while the audit
@@ -234,31 +234,6 @@ def _materialize_run(
234
234
  result_path=result_path,
235
235
  audit_source_path=audit_source_path,
236
236
  )
237
- elif (
238
- audit_source_path is None
239
- or _normalized(audit_source_path) == _normalized(result_path)
240
- ):
241
- # translator 다. report-writer 와 같은 두-경로 audience 인데 종전엔 빠진
242
- # `--audit-source` 를 `--result` 로 조용히 접었다. 그러면 프롬프트에
243
- # `**Worker Result Path:**` 앵커가 안 생기는데, `worker-dispatch` 는 그
244
- # 앵커가 `**Result Path:**` 와 다를 것을 요구하므로 발행은 되지만 어떤
245
- # 디스패치도 받지 못하는 예약이 남는다. 예약은 회수 명령이 없어 리드가
246
- # 두 번째 id 로 다시 만드는 순간 "exactly one reservation" 이 영영
247
- # 깨졌다(2026-09-08 실측, fontsninja-v3-site dev-10627-2
248
- # requirements-discovery). 발행 시점에 거절한다.
249
- raise AgentPromptCliError(
250
- "translator materialization requires --audit-source distinct from "
251
- "--result: --result is the translator's own report "
252
- "(e.g. worker-results/translator-translations-<task-type>-<seq>.md) "
253
- "and --audit-source the worker-result .md the audit sidecar is "
254
- "derived from (worker-results/translator-worker-<task-type>-<seq>.md). "
255
- "`worker-dispatch --workers translator` reads both from the prompt's "
256
- "**Result Path:** and **Worker Result Path:** anchors and refuses a "
257
- "prompt without the second. To fix a prompt already published "
258
- "without it, re-run this command with the same --invocation-id and "
259
- "--prompt plus --replace-undispatched; a new invocation id would "
260
- "leave two reservations and block the translator for this run"
261
- )
262
237
  _validate_run_identity(
263
238
  manifest,
264
239
  worker_id=args.worker_id,
@@ -29,10 +29,6 @@ def _validate_run_identity(
29
29
  expected = "lead"
30
30
  if worker_id != "lead":
31
31
  raise AgentPromptCliError("lead assignment requires worker ID 'lead'")
32
- elif assignment_ref == "translator":
33
- expected = "translator"
34
- if worker_id != "translator":
35
- raise AgentPromptCliError("translator assignment requires worker ID 'translator'")
36
32
  elif assignment_ref.startswith("critic/"):
37
33
  scope = assignment_ref.split("/", 1)[1]
38
34
  expected = CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT.get(scope, "")
@@ -227,6 +227,14 @@ _BLOCKING: tuple[tuple[str, str], ...] = (
227
227
  "stage_fix_carry.py 는 통과로 기록된 리드 수정이 답한 FAIL 을 다시 "
228
228
  "fix run 으로 넘기지 않는다.",
229
229
  ),
230
+ # 수정 라운드의 FAIL 은 correctionRounds 에만 남는다(`_implementation-deliverable.md`
231
+ # "Correction round"). 기록이 원장과 어긋나면 그 FAIL 이 보고서에서 사라진다.
232
+ (
233
+ "correction-round BLOCKING:",
234
+ "수정 라운드 기록이 실제 dispatch 나 마지막 재검증 verdict 와 어긋난다 — "
235
+ "verifierResults 가 마지막 라운드의 verdict 가 아니면 release_gate.py 와 "
236
+ "stage_fix_carry.py 가 다른 라운드의 판정으로 stage 를 정착시키거나 fix run 을 연다.",
237
+ ),
230
238
  (
231
239
  "implementation run declares stage-",
232
240
  "stage 캐리 사이드카가 디스크에 없다 — consumers.py 의 "
@@ -21,6 +21,7 @@ from pathlib import Path
21
21
  from typing import Mapping
22
22
 
23
23
  from okstra_ctl.convergence_engine import grouped_input_digest
24
+ from okstra_ctl.correction_round import correction_round
24
25
  from okstra_ctl.execution_identity import Attempt, ExecutionManifest
25
26
  from okstra_ctl.execution_manifest import read_execution_manifest_view
26
27
  from okstra_ctl.json_boundary import (
@@ -101,7 +102,11 @@ def groups_digest_ok(state_dir: Path, suffix: str, document: dict) -> bool:
101
102
 
102
103
 
103
104
  def discovery_attempts(manifest: ExecutionManifest) -> dict[str, list[Attempt]]:
104
- """교정 실행은 발견 결과를 대체하지만 재검증 투표는 발견 출처가 아니다."""
105
+ """교정 실행과 수정 라운드 재검증은 발견 결과를 대체하지만 재검증 투표는 발견 출처가 아니다.
106
+
107
+ 수정 라운드(`correction-r<N>`) 뒤 수렴을 다시 시작하면 마지막 라운드의 verifier 결과가
108
+ 출처여야 한다. 빠지면 고치기 전 FAIL 결과로 다시 수렴한다.
109
+ """
105
110
  latest: dict[str, Attempt] = {}
106
111
  for attempt in manifest.attempts:
107
112
  current = latest.get(attempt.invocation_ref)
@@ -109,7 +114,8 @@ def discovery_attempts(manifest: ExecutionManifest) -> dict[str, list[Attempt]]:
109
114
  latest[attempt.invocation_ref] = attempt
110
115
  by_role: dict[str, list[Attempt]] = {}
111
116
  for invocation in manifest.invocations:
112
- if invocation.dispatch_kind not in {"initial", "correction"}:
117
+ if (invocation.dispatch_kind not in {"initial", "correction"}
118
+ and correction_round(invocation.dispatch_kind) is None):
113
119
  continue
114
120
  attempt = latest.get(invocation.invocation_ref)
115
121
  if attempt is not None:
@@ -0,0 +1,59 @@
1
+ """`okstra correction` CLI — prints one correction-round instruction body (`correction_round`)."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from .convergence import canonical_run_state_artifact, validated_run_authority
9
+ from .correction_round import CorrectionRoundError, correction_instruction
10
+
11
+
12
+ _CLI_EPILOG = r"""Usage:
13
+ okstra correction prompt --run-manifest <path> --round <N> --role <executor|verifier> \
14
+ --base <sha> --finding <F-id> [--finding <F-id> ...]
15
+
16
+ Prints the instruction body for one correction-round prompt; pass it to
17
+ `okstra agent-prompt materialize --instruction` with --dispatch-kind correction-r<N>.
18
+ Findings are read from the run's convergence record. Exit 1 on a refusal.
19
+ """
20
+
21
+
22
+ def main(argv: list[str] | None = None) -> int:
23
+ parser = argparse.ArgumentParser(
24
+ prog="okstra correction",
25
+ description="Fix confirmed verifier findings inside the same implementation run.",
26
+ epilog=_CLI_EPILOG,
27
+ formatter_class=argparse.RawDescriptionHelpFormatter,
28
+ )
29
+ commands = parser.add_subparsers(dest="command", required=True)
30
+ prompt = commands.add_parser("prompt", help="print one correction-round instruction body")
31
+ prompt.add_argument("--run-manifest", type=Path, required=True)
32
+ prompt.add_argument("--round", type=int, required=True)
33
+ prompt.add_argument("--role", choices=("executor", "verifier"), required=True)
34
+ prompt.add_argument("--base", required=True,
35
+ help="the stage HEAD before this round")
36
+ prompt.add_argument("--finding", action="append", default=[], required=True,
37
+ help="a convergence finding id to fix; repeat for several")
38
+ args = parser.parse_args(argv)
39
+
40
+ try:
41
+ authority = validated_run_authority(args.run_manifest)
42
+ if authority.task_type != "implementation":
43
+ raise CorrectionRoundError("correction rounds exist only in implementation runs")
44
+ convergence_path = canonical_run_state_artifact(
45
+ authority, manifest_field="convergenceStatePath", prefix="convergence",
46
+ label="convergence state",
47
+ )
48
+ print(correction_instruction(
49
+ convergence_path=convergence_path, round_number=args.round, role=args.role,
50
+ base=args.base, finding_ids=args.finding,
51
+ ))
52
+ except (CorrectionRoundError, OSError, ValueError) as exc:
53
+ print(f"error: {exc}", file=sys.stderr)
54
+ return 1
55
+ return 0
56
+
57
+
58
+ if __name__ == "__main__":
59
+ raise SystemExit(main())
@@ -0,0 +1,168 @@
1
+ """`okstra correction` — fix confirmed verifier findings inside the same implementation run.
2
+
3
+ A verifier FAIL used to end the run: FAIL report, `failed` row, finalize, a new wizard pass and
4
+ a fix run that repeated the executor, every verifier and the report writer. Measured
5
+ 2026-10-17 (figma-plugin dev-11377 stage 1): one field rename (2 source lines, 24 test
6
+ expectations) cost that whole cycle, because the lead minor fix may not change test
7
+ expectations. A correction round sends the executor a bounded fix and the verifiers a
8
+ re-verification of that fix, then the run writes its report once.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from pathlib import Path
14
+ from typing import Mapping, Sequence
15
+
16
+ from .execution_identity import ExecutionManifest
17
+ from .json_boundary import load_owned_object
18
+
19
+ MAX_CORRECTION_ROUNDS = 2
20
+ CORRECTION_DISPATCH_KIND_PREFIX = "correction-r"
21
+ _ROUND_RE = re.compile(r"^correction-r([1-9][0-9]*)$")
22
+
23
+
24
+ class CorrectionRoundError(ValueError):
25
+ """A correction dispatch or instruction request that the run cannot accept."""
26
+
27
+
28
+ def correction_round(dispatch_kind: str) -> int | None:
29
+ """`correction-r2` → 2; any other dispatch kind → None."""
30
+ match = _ROUND_RE.match(dispatch_kind)
31
+ return int(match.group(1)) if match else None
32
+
33
+
34
+ def validate_correction_dispatch(
35
+ manifest: ExecutionManifest, jobs: Sequence[object],
36
+ ) -> None:
37
+ """Refuse a correction dispatch that skips a step or exceeds the round cap.
38
+
39
+ The executor fix of round N comes after round N-1 was re-verified, and the verifiers of
40
+ round N re-verify only after the executor of round N returned a result; otherwise a
41
+ verifier reads a tree the fix has not reached, as the executor/verifier batch split
42
+ already guards for the initial round.
43
+ """
44
+ roles = {row.role_execution_ref: row.role for row in manifest.role_executions}
45
+ finished_ok = {
46
+ row.invocation_ref for row in manifest.attempts
47
+ if row.finished_at is not None and row.status == "ok"
48
+ }
49
+
50
+ def completed(round_number: int, role: str) -> bool:
51
+ return any(
52
+ correction_round(row.dispatch_kind) == round_number
53
+ and roles.get(row.role_execution_ref) == role
54
+ and row.invocation_ref in finished_ok
55
+ for row in manifest.invocations
56
+ )
57
+
58
+ for job in jobs:
59
+ round_number = correction_round(getattr(job, "dispatch_kind", ""))
60
+ if round_number is None:
61
+ continue
62
+ if round_number > MAX_CORRECTION_ROUNDS:
63
+ raise CorrectionRoundError(
64
+ f"{job.dispatch_kind}: a run holds at most {MAX_CORRECTION_ROUNDS} correction "
65
+ "rounds; end the run FAIL and let a fix run take the remaining findings"
66
+ )
67
+ audience = getattr(job, "audience", "")
68
+ if audience == "implementation-executor":
69
+ if round_number > 1 and not completed(round_number - 1, "verifier"):
70
+ raise CorrectionRoundError(
71
+ f"{job.dispatch_kind}: correction round {round_number - 1} has no "
72
+ "re-verification result yet; await its verifiers first"
73
+ )
74
+ if completed(round_number, "implementer"):
75
+ raise CorrectionRoundError(
76
+ f"{job.dispatch_kind}: the executor already returned this round's fix; "
77
+ "dispatch the verifiers, or start the next round"
78
+ )
79
+ elif audience == "implementation-verifier":
80
+ if not completed(round_number, "implementer"):
81
+ raise CorrectionRoundError(
82
+ f"{job.dispatch_kind}: the executor has not returned this round's fix; "
83
+ "dispatch and await it before re-verifying"
84
+ )
85
+ else:
86
+ raise CorrectionRoundError(
87
+ f"{job.dispatch_kind} is an implementation executor or verifier dispatch, "
88
+ f"not {audience or 'this audience'}"
89
+ )
90
+
91
+
92
+ def _finding_lines(findings: Sequence[Mapping[str, object]]) -> list[str]:
93
+ lines = []
94
+ for finding in findings:
95
+ workers = ", ".join(str(w) for w in finding.get("consensusWorkers") or []) or str(
96
+ finding.get("originWorker") or "")
97
+ lines.append(
98
+ f"- `{finding['findingId']}` ({finding.get('classification', '')}; {workers}): "
99
+ f"{finding.get('summary', '')} — evidence: {finding.get('originEvidence', '')}"
100
+ )
101
+ return lines
102
+
103
+
104
+ def correction_instruction(
105
+ *, convergence_path: Path, round_number: int, role: str, base: str,
106
+ finding_ids: Sequence[str],
107
+ ) -> str:
108
+ """The instruction body one correction-round prompt carries, read from the convergence record."""
109
+ if not 1 <= round_number <= MAX_CORRECTION_ROUNDS:
110
+ raise CorrectionRoundError(f"--round must be 1..{MAX_CORRECTION_ROUNDS}")
111
+ if not finding_ids:
112
+ raise CorrectionRoundError("name at least one --finding")
113
+ if not base.strip():
114
+ raise CorrectionRoundError("--base is required: the stage HEAD before this round")
115
+ record = load_owned_object(convergence_path, artifact="convergence record")
116
+ by_id = {
117
+ str(row.get("findingId")): row for row in record.get("findings") or []
118
+ if isinstance(row, Mapping)
119
+ }
120
+ missing = [fid for fid in finding_ids if fid not in by_id]
121
+ if missing:
122
+ raise CorrectionRoundError(
123
+ f"findings not in {convergence_path.name}: {', '.join(missing)}; "
124
+ f"it holds {', '.join(sorted(by_id)) or 'none'}"
125
+ )
126
+ findings = [by_id[fid] for fid in finding_ids]
127
+ results_dir = convergence_path.parent.parent / "worker-results"
128
+ header = [
129
+ f"- Convergence record: `{convergence_path}`",
130
+ f"- Verifier results (each finding's suggested fix): `{results_dir}`",
131
+ f"- Stage HEAD before this round: `{base.strip()}`",
132
+ "- Findings:",
133
+ *(" " + line for line in _finding_lines(findings)),
134
+ "",
135
+ ]
136
+ if role == "executor":
137
+ return "\n".join([
138
+ f"## Correction Round {round_number} — fix confirmed verifier findings",
139
+ "",
140
+ *header,
141
+ "Rules:",
142
+ "- Change only what these findings require, inside this stage's planned paths or "
143
+ "files this stage already changed. A finding that needs a new plan step, a new "
144
+ "dependency, or a schema or migration change is not fixed here: stop and say so.",
145
+ "- Update a test's expected value only where the finding shows the oracle itself is "
146
+ "wrong; never weaken or delete an assertion to make a test pass.",
147
+ "- Follow the executor sidecar's TDD, pre-commit diff review and self-check, and "
148
+ "commit with a Conventional Commit whose body names the finding ids.",
149
+ "- Re-run the plan validation commands that cover the fix, and end with a "
150
+ "`### Stage Carry Evidence` block whose `stageCommitRange.head` is the new HEAD.",
151
+ ])
152
+ if role == "verifier":
153
+ return "\n".join([
154
+ f"## Correction Round {round_number} — re-verify the fix",
155
+ "",
156
+ *header,
157
+ "Rules:",
158
+ f"- For each finding, state `fixed` or `not fixed` against `git diff {base.strip()}..HEAD`, "
159
+ "with `path:line` evidence.",
160
+ "- Apply the static review's blocking checks to the correction diff only; the rest of "
161
+ "the stage was reviewed in the initial round.",
162
+ "- Re-run every plan validation command, as the verifier sidecar's Fix-run "
163
+ "incremental scope requires.",
164
+ "- Verdict `PASS` when every finding is fixed and no blocking check hits the "
165
+ "correction diff; otherwise `FAIL`, with each remaining or new finding in the "
166
+ "usual format.",
167
+ ])
168
+ raise CorrectionRoundError("--role must be executor or verifier")