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
@@ -13,11 +13,12 @@ are collected and convergence finished. Phase 1-5 do not need it.
13
13
  - **Plan link & approval evidence**: path to the approved `final-report.md`, the exact quoted approval marker, AND the executed stage number / title quoted from the Stage Map row. For a selected-direction plan, also quote `selectedDirectionRef.optionId`, `snapshotPath`, and the validated snapshot digest; for a legacy plan, quote the effective `implementation-option` or the Recommended Option fallback.
14
14
  - **Commit list**: each commit's SHA (or short SHA), message, and the plan step(s) / TDD cycle it satisfies. A lead minor fix commit is listed too, with `planStep` `Lead minor fix (<finding ids>)`.
15
15
  - **Lead minor fixes**: when the lead fixed verifier findings in this run, pass each `implementation.leadFixes[]` entry to the report writer as recorded (§"Lead minor fix"). Omit the field when there was none.
16
+ - **Correction rounds**: when this run ran correction rounds, pass one `implementation.correctionRounds[]` entry per round to the report writer — `round`, `findingIds`, `baseCommit`, `commit`, each verifier's re-verification `verifierVerdicts`, and `result` (§"Correction round"). `verifierResults` then carries the last round's verdicts. Omit the field when there was none.
16
17
  - **Diff summary**: capture `git diff --stat <base>..<captured-head>` and `git diff --numstat <base>..<captured-head>` for the full implementation stage, including after a fix run. Supply both outputs to the report writer; put each file's added/deleted counts alone in `diffSummary.files[].lines` (`+150/-0`) and one sentence on what the change in that file does in `diffSummary.files[].summary` — the report renders `summary` as the file's "what it does" column and `lines` as a separate number column. Do not substitute a fix-run aggregate or carry forward older statistics. If the captured commits are unavailable, identify the missing reference in the evidence instead of inventing counts.
17
18
  - **Change narrative scope**: `userNarrative.changeExplanation` describes the same scope as `diffSummary` — the whole stage from its base, every file the table lists. On a fix run, state what the fix run itself changed (its commits and files) as a separate sentence; never present the fix-run delta as the stage's change.
18
19
  - **Out-of-plan edits block**: every file edited that was not in the approved plan's file list, with rationale (empty block is acceptable and preferred)
19
20
  - **Clarification answers carried in**: when `instruction-set/clarification-response.md` exists, it holds the user's answers to the approved plan's `## 1. Clarification Items` rows. The report MUST render the `## 0. Clarification Response Carried In From Previous Run` section (set `clarificationCarryIn.sourceFile` to that path) and, for every carried-over `## 1.` row whose answer appears there, record the answer in the `User input` cell and flip `Status` to `resolved`. A row left `open`/`answered` despite a matching user answer is a contract violation; an answer that materially changes scope beyond the approved plan is routed to a new `implementation-planning` run, not silently executed.
20
- - **Stage sidecar evidence**: set `stageSidecarEvidence.carryJson` to the path `runs/<impl-task-key>/carry/stage-<N>.json` followed by a one-line summary of its payload — stage number, step results by status, stage commit range, number of files changed. Do not paste the payload itself: the file on disk is the record (`_validate_stage_carry_sidecar_exists` checks that it exists), and a pasted one-line JSON is unreadable in the report. Quote the `consumers.jsonl` rows this run appended line-by-line in `consumerRows`.
21
+ - **Stage sidecar evidence**: set `stageSidecarEvidence.carryJson` to the `carryPath` that `okstra stage-done` printed — the flat `runs/implementation/carry/stage-<N>.json`, never a path under the run's `stage-<N>/` directory — followed by a one-line summary of its payload — stage number, step results by status, stage commit range, number of files changed. Do not paste the payload itself: the file on disk is the record (`_validate_stage_carry_sidecar_exists` checks that it exists), and a pasted one-line JSON is unreadable in the report. Quote the `consumers.jsonl` rows this run appended line-by-line in `consumerRows`.
21
22
  - **Validation evidence**: actual command output (stdout/stderr) for every `pre / mid / post` validation command from the plan. Truncated output is acceptable but the command line and exit code MUST be exact. No paraphrasing of test results. Set `checkId` to the plan's `validationChecklist` id (`VC-007`) when the row runs a checklist command, and `advisory: true` on a row whose check is advisory (an external Tier 3 conformance entry, for example) so a non-zero exit there is not shown as a failure.
22
23
  - **TDD evidence (when applicable)**: for steps that should be TDD-ordered, show the failing-test output captured BEFORE the merged commit and the passing-test output after, citing the single `feat|fix` commit SHA (test and implementation share one commit).
23
24
  - **Verifier results**: a section per verifier present in the resolved roster (`Claude verifier`, `Codex verifier`, and `Antigravity verifier` when opted in) containing:
@@ -46,7 +47,7 @@ are collected and convergence finished. Phase 1-5 do not need it.
46
47
 
47
48
  When the synthesised verdict would be `FAIL` only because of verifier findings that are small and local, the lead fixes them itself in this run instead of ending it and opening a fix run. Decide after Phase 5.5 convergence, before building the report-writer dispatch prompt and before "Lead post-stage persistence" below. No worker dispatch is open then, so the edit falls inside no read-only worker's mutation audit window. Never edit while a verifier, convergence, or report-writer dispatch is running: its audit reports `readonly source changed` and discards that worker's result.
48
49
 
49
- **Eligibility.** Every rule holds for every blocking finding of the round, or the lead fixes none of them and the normal FAIL → fix-run path applies:
50
+ **Eligibility.** Every rule holds for every blocking finding of the round, or the lead fixes none of them and §"Correction round" decides whether the executor fixes them in this run:
50
51
 
51
52
  1. The fix is a code change at, or next to, the location the verifier cited.
52
53
  2. The lead's total diff is at most 30 changed lines (added plus deleted, `git diff --numstat`).
@@ -65,10 +66,32 @@ A finding that needs no code change (convergence settled it as a verifier misrea
65
66
 
66
67
  **Record.** One `implementation.leadFixes[]` entry per fixed finding group: `findingIds`, `verifiers` (the `verifierResults[].verifier` names whose FAIL it answers — every `FAIL` row is named by a passing entry before the stage may settle), `summary` (one sentence on what changed), `commit` (full SHA), `files`, `changedLines`, `commands[]` (`command`, `exitCode`, `outputTail`), and `result`. Keep each verifier's `FAIL` row as written. Leave the fixed findings out of `humanSummary.blockers`, and state in `userNarrative.validationExplanation` that the lead fixed them in this run.
67
68
 
68
- **Persistence after a passing fix.** The executor's `### Stage Carry Evidence` was emitted before the fix and names the pre-fix head. Before writing the carry sidecar, set `stageCommitRange.head` to the post-fix HEAD (`git rev-parse HEAD` in the stage worktree), add the fixed files to `filesChanged`, and append one `notes` line naming the lead commit and the finding ids. `stepResults` stays as the executor wrote it: a lead fix is not a plan step. Append the `done` row after the fix commit, so its `head_commit` is the post-fix HEAD, and capture `diffSummary` against the same head.
69
+ **Persistence after a passing fix.** The executor's `### Stage Carry Evidence` was emitted before the fix and names the pre-fix head. Before passing the carry to `okstra stage-done`, set `stageCommitRange.head` to the post-fix HEAD (`git rev-parse HEAD` in the stage worktree), add the fixed files to `filesChanged`, and append one `notes` line naming the lead commit and the finding ids. `stepResults` stays as the executor wrote it: a lead fix is not a plan step. Append the `done` row after the fix commit, so its `head_commit` is the post-fix HEAD, and capture `diffSummary` against the same head.
69
70
 
70
71
  **Enforced:** `validators/validate-run.py` `_validate_lead_fixes` (blocking) fails an entry recorded `pass` whose command exited non-zero or that has no command, an entry over 30 changed lines, and a settled stage holding a `result: fail` entry; it reports (advisory) a settled stage whose `FAIL` row no passing entry names in `verifiers` — advisory because validate-run runs after the `done` row is appended, so blocking would undo nothing. `_validate_lead_fix_carry_head` (advisory) reports a carry sidecar whose head is not a passing lead fix commit. Eligibility rules 1, 3, 4 and 5 and the match between findings and `findingIds` are instructions only: the report carries no structured finding id or planned-path list to compare against.
71
72
 
73
+ ## Correction round (decided after convergence, before the report-writer dispatch)
74
+
75
+ When the synthesised verdict is `FAIL` and the lead minor fix does not apply, the executor fixes the confirmed findings in this run and the verifiers re-verify the fix, instead of ending the run and opening a fix run. At most **2** rounds per run.
76
+
77
+ **Entry.** Every blocking finding of the round meets all of these, or the run ends `FAIL` as before:
78
+
79
+ 1. A verifier cited where it is (`path:line`) and what the correct result is.
80
+ 2. The fix stays inside this stage's `plannedPaths` or files the executor already changed.
81
+ 3. It needs no new plan step, dependency, schema, migration, or config of record — a finding that does is a plan change: end the run `FAIL` and route by `prompts/lead/phase-routing.md`.
82
+
83
+ Changing a test's expected value is allowed here when the finding shows the oracle is wrong, because the verifiers re-verify the fix.
84
+
85
+ **Procedure for round `<N>`.**
86
+
87
+ 1. Record the stage HEAD as `<base>` (`git rev-parse HEAD` in the stage worktree).
88
+ 2. Write the executor instruction with `okstra correction prompt --run-manifest <path> --round <N> --role executor --base <base> --finding <F-id> [--finding <F-id> ...]` into a file under the run's `state/`, then materialize it with `okstra agent-prompt materialize` — `--audience implementation-executor`, `--assignment-ref initial/<executor worker id>`, `--dispatch-kind correction-r<N>`, a new `--invocation-id` ending `-correction-r<N>-<executor worker id>`, and a result path `<executor worker id>-worker-correction-r<N>-implementation-<seq>.md`. Build a jobs file with `okstra agent-prompt jobs --dispatch-kind correction-r<N>`, dispatch it alone with `okstra team dispatch --jobs-file <path> --dispatch-kind correction-r<N>`, and settle it with `okstra team await`.
89
+ 3. Do the same for every verifier with `--role verifier`, `--audience implementation-verifier`, its own `initial/<verifier worker id>` assignment, and result `<verifier worker id>-worker-correction-r<N>-implementation-<seq>.md`; dispatch the verifiers in one batch without the executor.
90
+ 4. Converge the re-verification results the way the initial round was converged, restarting Phase 5.5 with `okstra convergence seed ... --restart-from-round0`.
91
+ 5. No re-verification verdict is `FAIL`: the round `result` is `pass` and the run continues to persistence with the post-fix head. Otherwise `result: fail`; start round `<N+1>` if the cap allows and the entry rules still hold, or end the run `FAIL`.
92
+
93
+ **Enforced:** `scripts/okstra_ctl/dispatch_core.py` `_validate_correction_rounds` (via `correction_round.validate_correction_dispatch`) refuses a round past the cap, a verifier dispatch before that round's executor returned an `ok` result, a second executor fix in the same round, and round `<N+1>` before round `<N>` was re-verified. `validators/validate-run.py` `_validate_correction_rounds` (blocking) fails a report whose `correctionRounds` differ from the correction rounds the run dispatched, whose rounds are not 1, 2 in order, whose `result` contradicts its verdicts, or whose `verifierResults` are not the last round's verdicts. The entry rules are instructions only.
94
+
72
95
  ## Self-review pass before finalising the report (the Okstra lead runs this; do not delegate it)
73
96
 
74
97
  1. **Plan coverage** — for a selected-direction plan, every step in the approved `plan-ready` stage must point to a commit (or an explicit `Skipped: <reason>` entry), and the diff must preserve the selected snapshot's mechanism and invariants. For a legacy plan, every step in the effective implementation option (explicit frontmatter value or Recommended Option fallback) must point to a commit or an explicit skip. List gaps. A `RED:` step and its `GREEN:` step pointing to the same merged commit SHA is NOT a coverage gap — one SHA may be shared by both.
@@ -90,10 +113,11 @@ A finding that needs no code change (convergence settled it as a verifier misrea
90
113
 
91
114
  - Parse the executor's `### Stage Carry Evidence` JSON block. If absent or unparsable, end with status `contract-violated` and route to a follow-up `error-analysis`.
92
115
  - The `### Stage Carry Evidence` JSON may include `designPrepEvidence[]`. Emit a row only when this stage produced concrete evidence that refines an effective PREP item: `itemId`, the injected `assessmentFingerprint`, `resolution`, and non-empty `evidence[]` are required; `overrides` is optional and only records observed, non-authoritative refinements. Carry evidence never represents user approval. Downstream resolution accepts it only from transitive dependency stages with the matching fingerprint.
93
- - **A `FAIL` synthesised verdict withholds the two writes below.** A verifier `FAIL` that a passing lead minor fix answered does not make the verdict `FAIL` (§"Lead minor fix"); persist with the post-fix head it names. They are what marks the stage `done`, so performing them on a stage whose verifier found a blocking defect stacks the next stage on a confirmed regression. When the synthesised verdict is `FAIL`: write NO carry sidecar, and append a `status:"failed"` row in place of the `done` row — same `okstra_ctl.consumers.append_consumer` call, carrying `report_path` and the SHA of HEAD. That row is terminal *without* completion: dependent stages stay blocked because this stage is not done, while its worktree-registry occupancy is released so a fix run can re-enter the same stage number — `--stage <N>` reuses the preserved worktree and branch instead of provisioning a new one. State the reason in the report's `Stage sidecar evidence` section as `withheld`. **Enforced:** `validators/validate-run.py` `_validate_stage_carry_sidecar_exists` accepts a missing carry file only when that field is non-empty, so silently skipping the sidecar still fails the run.
116
+ - **A `FAIL` synthesised verdict withholds the two writes below.** A verifier `FAIL` that a passing lead minor fix answered does not make the verdict `FAIL` (§"Lead minor fix"); persist with the post-fix head it names. They are what marks the stage `done`, so performing them on a stage whose verifier found a blocking defect stacks the next stage on a confirmed regression. When the synthesised verdict is `FAIL`: write NO carry sidecar, and append a `status:"failed"` row in place of the `done` row with `okstra stage-fail <impl-task-key> --stage <N> --head-commit <HEAD sha> --report <this run's final-report path> --reason "<blocking finding ids>"` (it calls `append_consumer`, so the consumers lock and the registry release apply; do not call python directly). That row is terminal *without* completion: dependent stages stay blocked because this stage is not done, while its worktree-registry occupancy is released so a fix run can re-enter the same stage number — `--stage <N>` reuses the preserved worktree and branch instead of provisioning a new one. State the reason in the report's `Stage sidecar evidence` section as `withheld`. **Enforced:** `validators/validate-run.py` `_validate_stage_carry_sidecar_exists` accepts a missing carry file only when that field is non-empty, so silently skipping the sidecar still fails the run.
94
117
  - **A run that ends with no verifier result still closes.** When the stage ends before any verifier returned a verdict — the executor stopped without a change and the user chose not to verify, or every dispatched verifier ended without a result — append the `failed` row as above, record each roster verifier `not-run` with its reason (`okstra worker-state transition --team-state <path> --worker <worker-id> --status not-run --reason <text>`; a worker the dispatch skipped already carries one), and write the convergence state with `okstra convergence skip --run-manifest <path> --reason <text>` before dispatching the report writer. Without that state the report-writer packet refuses with `convergence … required source is missing`. The command refuses while any analyser, verifier, or critic attempt returned a result or is still running; such a run converges normally. In the report, write each verifier's `verifierResults[]` row as `verdict: not-run`, and state the reason under `Stage sidecar evidence` as `withheld`.
95
- - On a non-`FAIL` verdict, for this run's single stage: write its JSON verbatim to `runs/<impl-task-key>/carry/stage-<N>.json` — after a passing lead minor fix, with the head, files, and note that §"Lead minor fix" adds. Refuse to overwrite an existing file (one stage = one sidecar; a fix run re-entering after a `failed` row writes the first one, because a withheld stage never wrote it). A stage reopened after a `done` row (see "Reopening a settled stage" below) still has the sidecar of the run that first settled it, naming a head the stage has since moved past: move it unchanged to `runs/<impl-task-key>/carry/superseded/stage-<N>-from-<that run's task-type>-<seq>.json` before writing the new one, and name both paths in `Stage sidecar evidence`. Do not rename it inside `carry/`: the readers glob `carry/stage-*.json` (`okstra_ctl.stage_map`, `okstra_ctl.consumers.backfill_done_from_carry`), so a renamed file there is read again as a live sidecar. A reopened stage whose run ends `FAIL` writes nothing and leaves the old file where it is.
96
- - On a non-`FAIL` verdict, for this run's single stage: append a `status:"done"` row to `runs/<plan-task-key>/consumers.jsonl` with `completed_at`, `carry_path`, `report_path` (this run's final-report path relative to the run root), and the SHA of HEAD. Append it with `okstra_ctl.consumers.append_consumer` (NOT a raw filesystem write) — that call honours the consumers lock AND releases this stage's worktree-registry occupancy, so later runs stop seeing a finished stage as a concurrent run. `report_path` lets `final-verification` cite each stage's originating report when assembling its Source Implementation Report list.
118
+ - On a non-`FAIL` verdict, for this run's single stage: save the executor's `### Stage Carry Evidence` JSON (after a passing lead minor fix, with the head, files, and note that §"Lead minor fix" adds; after a correction round, the last round's executor block) to a file under this run's `state/`, then run `okstra stage-done <impl-task-key> --stage <N> --carry <that file> --report <this run's final-report path>`. It writes the sidecar to the flat `runs/implementation/carry/stage-<N>.json` (the readers glob `carry/stage-*.json`: `okstra_ctl.stage_map`, `okstra_ctl.consumers.backfill_done_from_carry`, and validate-run's `RunRef.carry`), refuses to replace another head's sidecar of a stage that was never settled, and for a stage reopened after a `done` row (see "Reopening a settled stage" below) moves the previous sidecar unchanged to `carry/superseded/stage-<N>-from-<task-type>-<seq>.json` first. Name the `carryPath` and any `supersededCarryPath` it prints in `Stage sidecar evidence`. A reopened stage whose run ends `FAIL` writes nothing and leaves the old file where it is.
119
+ - The same `okstra stage-done` call appends the `status:"done"` row to `runs/<plan-task-key>/consumers.jsonl` with `completed_at`, `carry_path`, `report_path` (relative to the report's run directory) and the carry's head, through `append_consumer` — so the consumers lock applies and this stage's worktree-registry occupancy is released. Never call `append_consumer` from python. `report_path` lets `final-verification` cite each stage's originating report when assembling its Source Implementation Report list.
120
+ - **Automatic stage worktree cleanup:** after the report passes finalization, `report-finalize` reclaims this completed stage's clean worktree. It compares the report's final commit with the completion ledger and branch tip, preserves active or unfinished stages and dirty or nested worktrees, and keeps the branch and registry base for dependency rebases and later re-entry. Never remove the directory by hand before finalization. A later implementation or final-verification entry restores it from the preserved branch. After finalization, use the main project root or absolute artifact paths: this run's stage cwd may no longer exist. Enforcement: `report_finalize._teardown_stage_worktrees`, `stage_targets.teardown_completed_stage_worktrees`, and the cleanup/restoration tests in `tests/worktree/test_stage_rebase.py`.
97
121
  - **Reopening a settled stage.** Appending a `status:"failed"` row after a `done` one withdraws that stage: `--stage N` accepts it again, its dependents stop resolving a base from the withdrawn head, and whole-task final-verification blocks until it is settled again. Write it with `okstra stage-reopen <task-key> --stage <N> --reason "<why>" [--evidence <report>]`, which records the reason and the report that found the defect, and re-run the stage rather than repairing the tree outside okstra — a stage reverted outside the ledger leaves the recorded `head_commit` and the carry sidecar naming a tree that no longer exists, and the next stage branches from it.
98
122
  - The verifier round, Phase 5.5 convergence, and this Phase 6 report run **once per run** over this stage's diff — NOT per step.
99
123
  - Record this stage's new contents in the final report's `Stage sidecar evidence` deliverable section: the sidecar by path with its one-line summary, and the new consumers row quoted by itself.
@@ -9,6 +9,7 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
9
9
 
10
10
  ## Executor role binding (carried over from the thin core)
11
11
 
12
+ - **Moving dependency bases:** never run `git merge <predecessor>` or merge an old origin branch into this stage to update its base. Runtime preparation rebases downstream stages in dependency order through `okstra_ctl.stage_rebase`; a predecessor moving during execution requires stopping and re-entering preparation after the current stage claim is released. Do not rewrite another active stage or repair registry/ledger SHAs by hand. Enforcement of preparation ordering and conflict recovery is covered by `tests/worktree/test_stage_rebase.py`.
12
13
  - The `Executor` (bound in `implementation.md` thin core) is the **only worker permitted to mutate project files**. All other workers run read-only. A `runner=native-session` executor uses the selected host adapter's native edit and command primitives. A `runner=cli-wrapper` executor mutates files inside its provider CLI's auto-edit mode. The safety rules in this sidecar apply identically to both runners.
13
14
  - When the thin core's Task worktree block resolves status to `created` or `reused`, the Executor MUST run every Edit / Write / build / test / commit command with the worktree path as cwd. Treat it as `project_root` for the duration of this run. Do NOT mutate the caller's original checkout. Do NOT `cd` out of the worktree to reach files. If a file outside the worktree is genuinely needed, treat it as a planning gap: record it in `Out-of-plan edits` and continue.
14
15
  - **How to set the working directory**: every command and native edit MUST target `{{EXECUTOR_WORKTREE_PATH}}`, never the lead session's original project directory. The selected runtime adapter owns the exact native command syntax. Provider CLI wrappers inject the worktree at the CLI layer. For tools that accept an explicit working-directory flag (`git -C <path>`, `cargo --manifest-path`, `pytest --rootdir`), prefer that form.
@@ -220,6 +220,8 @@ Re-running commands proves the diff *builds and passes*; it does NOT prove the d
220
220
 
221
221
  ### Fix-run incremental scope (applies when the profile carries a "Fix-Run Carry" block)
222
222
 
223
+ A correction-round re-verification (`--dispatch-kind correction-r<N>`, a "Correction Round" block in the task instructions) takes the same scope: the block's findings and the correction diff it names, with every validation command re-run.
224
+
223
225
  A fix run re-verifies a stage whose previous run already passed a full static
224
226
  sweep and failed only on cited blocking findings. Re-sweeping the whole stage
225
227
  diff re-buys wall-clock without new information, so the static scope narrows —
@@ -1,9 +1,10 @@
1
1
  # Implementation Profile
2
2
 
3
3
  - Purpose: realise the approved `implementation-planning` deliverable as actual source changes, with cross-model verification, while keeping the run reversible
4
- - **Run-level fixed cost:** the verifier set, Phase 5.5 convergence, and the Phase 6 report-writer run exactly once per implementation run, over this run's single stage diff — never once per step.
4
+ - **Run-level fixed cost:** the verifier set and Phase 5.5 convergence run once per implementation run over this run's single stage diff — never once per step — plus once per correction round; the Phase 6 report-writer runs exactly once.
5
5
  - **Fix run (profile carries a "Fix-Run Carry" block):** the executor's scope is the carried blocking findings plus the previous routing recommendation — it MUST NOT re-execute plan steps the previous run completed. Verifiers apply the "Fix-run incremental scope" section of `_implementation-verifier.md`. The full validation-command re-run is NOT reduced.
6
6
  - **Lead minor fix:** a verifier FAIL whose findings are small and local is fixed by the lead inside this run, after convergence and before the report-writer dispatch, under `_implementation-deliverable.md` "Lead minor fix"; a passing fix settles the stage without a fix run.
7
+ - **Correction round:** a verifier FAIL whose findings are confirmed, located, and inside the stage's plan is fixed by the executor and re-verified inside this run, at most 2 rounds, under `_implementation-deliverable.md` "Correction round"; a finding that needs a plan change still ends the run `FAIL`.
7
8
  - **Executor binding (resolved at run-prep time, fixed for this run):**
8
9
  - Executor display name: `{{EXECUTOR_DISPLAY_NAME}}`
9
10
  - Executor worker ID: `{{EXECUTOR_WORKER_ID}}`
@@ -32,6 +32,12 @@
32
32
  {% for row in implementation.leadFixes %}<article class="summary-card tone-{{ 'neutral' if row.result == 'pass' else 'important' }}"><p class="eyebrow">{{ row.findingIds | join(', ') | inline_code }} · <span class="status status-{{ row.result }}">{{ t('tasks.implementation.lead-fix-result-' ~ row.result) }}</span></p><p>{{ row.summary | inline_code }}</p><table><tbody><tr><th>{{ t('tasks.implementation.commit') }}</th><td><code>{{ row.commit }}</code></td></tr><tr><th>{{ t('tasks.implementation.lead-fix-verifiers') }}</th><td>{{ row.verifiers | join(', ') | inline_code }}</td></tr><tr><th>{{ t('tasks.implementation.files') }}</th><td>{% for path in row.files %}<code>{{ path }}</code>{% if not loop.last %}, {% endif %}{% endfor %}</td></tr><tr><th>{{ t('tasks.implementation.lead-fix-changed-lines') }}</th><td class="figure">{{ row.changedLines }}</td></tr></tbody></table>{% for command in row.commands %}<details><summary><code title="{{ command.command }}">{{ command.command }}</code> · {{ t('tasks.implementation.exit-code') }} {{ command.exitCode }}</summary><pre>{{ command.outputTail }}</pre></details>{% endfor %}</article>{% endfor %}
33
33
  </section>{% endif %}
34
34
 
35
+ {% if implementation.get("correctionRounds") %}<section data-report-section="correction-rounds" data-report-field="implementation.correctionRounds">
36
+ <h2>{{ t('tasks.implementation.correction-rounds') }}</h2>
37
+ <p>{{ t('tasks.implementation.correction-rounds-intro') }}</p>
38
+ {% for row in implementation.correctionRounds %}<article class="summary-card tone-{{ 'neutral' if row.result == 'pass' else 'important' }}"><p class="eyebrow">{{ t('tasks.implementation.correction-round') }} {{ row.round }} · {{ row.findingIds | join(', ') | inline_code }} · <span class="status status-{{ row.result }}">{{ t('tasks.implementation.correction-result-' ~ row.result) }}</span></p><table><tbody><tr><th>{{ t('tasks.implementation.correction-range') }}</th><td><code>{{ row.baseCommit }}</code>..<code>{{ row.commit }}</code></td></tr>{% for verdict in row.verifierVerdicts %}<tr><th>{{ verdict.verifier | inline_code }}</th><td><span class="status status-{{ verdict.verdict | lower }}">{{ verdict.verdict }}</span></td></tr>{% endfor %}</tbody></table></article>{% endfor %}
39
+ </section>{% endif %}
40
+
35
41
  <section data-report-section="remaining-issues">
36
42
  <h2>{{ t('tasks.implementation.what-is-left') }}</h2>
37
43
  {{ render_narrative(narrative.remainingIssues, "implementation.userNarrative.remainingIssues") }}
@@ -148,7 +148,7 @@ flowchart TD
148
148
  Verdict --> Report[Final report preserves dissent]
149
149
  ```
150
150
 
151
- Only the executor may mutate project files, with one exception: after the verifier round and convergence, the lead may fix small, local verifier FAIL findings itself (at most 30 changed lines, no test-expectation, dependency, schema, migration or config-of-record change), re-run the checks that found them, and settle the stage when they pass, instead of opening a fix run (`instructions/_implementation-deliverable.md` "Lead minor fix"). The verifier independently re-runs the diff and validation command read-only in the same worktree. Even a verifier with the same provider as the executor runs again in a separate fresh CLI session. This is to prevent a structure where the same session approves a diff the same session wrote.
151
+ Only the executor may mutate project files, with one exception: after the verifier round and convergence, the lead may fix small, local verifier FAIL findings itself (at most 30 changed lines, no test-expectation, dependency, schema, migration or config-of-record change), re-run the checks that found them, and settle the stage when they pass, instead of opening a fix run (`instructions/_implementation-deliverable.md` "Lead minor fix"). Findings outside those limits but inside the stage's plan go to a correction round: the executor fixes them under dispatch kind `correction-r<N>` and the verifiers re-verify the fix, at most 2 rounds per run, before the report is written once (`"Correction round"`). The verifier independently re-runs the diff and validation command read-only in the same worktree. Even a verifier with the same provider as the executor runs again in a separate fresh CLI session. This is to prevent a structure where the same session approves a diff the same session wrote.
152
152
 
153
153
  ## 5. stage and consumers
154
154
 
@@ -172,6 +172,10 @@ Stage selection is `auto` or a number. If `--stage` comes from another task-type
172
172
 
173
173
  The stage worktree base is decided by dependency shape. An independent stage uses the task-key worktree HEAD fixed at first implementation entry as its anchor, and a single-dependency stage branches from the predecessor stage's done `head_commit`. A multi-dependency stage branches from a commit holding exactly its predecessors: the predecessor commit that already contains the others, or else an integration branch merging them (`<work-category-namespace>/<task-id-segment>-g<n1>-<n2>`). okstra creates that branch itself, so a predecessor that has not been merged into the task branch no longer blocks the stage; release-handoff later reuses the same branch as that stage's PR base.
174
174
 
175
+ When a predecessor advances, implementation entry synchronizes existing completed stage branches in dependency order through `okstra_ctl.stage_rebase.synchronize_stage_stack`. Each downstream stage is rebased with `git rebase --onto <new-base> <recorded-old-base>`; merging a moving predecessor into that stage is not an update strategy. Re-entering an unfinished stage also rebases its existing worktree when the computed base changes. Old heads are preserved under `refs/okstra/rebase-backups/`, registry bases and completion records are updated, and previous verification no longer qualifies. Conflicts abort the current rebase and stop the chain; active runs and dirty worktrees are not rewritten. Tests: `tests/worktree/test_stage_rebase.py`.
176
+
177
+ Successful implementation report finalization automatically removes that stage's clean worktree after its `done` record is persisted. The report's last commit, the completion ledger, and the current branch tip must agree for cleanup (`stage_integrate.reclaim_completed_stage`). Failed, active, dirty, nested, or moved stages are preserved with a reason in the finalization step result. Cleanup retains the branch and registry base; implementation re-entry and final-verification restore the worktree from that branch. The task worktree and other stages are outside this scoped cleanup. Tests: `tests/worktree/test_stage_rebase.py` and `tests/report/test_report_finalize.py`.
178
+
175
179
  ## 6. Deliverables
176
180
 
177
181
  ```mermaid
@@ -196,6 +200,7 @@ The final report requires at least the following.
196
200
  - TDD failing-then-passing evidence
197
201
  - per-verifier independent validation rerun result
198
202
  - lead minor fixes, when any: finding ids, commit, files, changed lines, and re-run checks (`implementation.leadFixes`)
203
+ - correction rounds, when any: round, finding ids, fix commit range, each verifier's re-verification verdict, and result (`implementation.correctionRounds`)
199
204
  - `carry/stage-<N>.json` evidence sidecar and `consumers.jsonl` started/done row
200
205
  - rollback verification
201
206
  - `implementation.manualUserTest` finalized against the actual diff and whether it is executable
@@ -2,7 +2,10 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import re
5
- from collections.abc import Callable
5
+ from collections.abc import Callable, Mapping
6
+ from typing import Any
7
+
8
+ from ...correction_round import correction_round
6
9
 
7
10
  _PASSING_VERDICT_TOKENS = frozenset({"accepted", "conditional-accept"})
8
11
  _CHECKLIST_ID_RE = re.compile(r"\bVC-\d{3,}\b")
@@ -266,6 +269,64 @@ def _validate_lead_fixes(data: dict, failures: list[str]) -> None:
266
269
  )
267
270
 
268
271
 
272
+ _CORRECTION_BLOCKING = "correction-round BLOCKING:"
273
+
274
+
275
+ def _validate_correction_rounds(
276
+ data: dict, run_manifest: Mapping[str, Any], failures: list[str],
277
+ ) -> None:
278
+ """수정 라운드 기록을 실제 dispatch 와, 마지막 라운드 verdict 를 verifierResults 와 맞춘다.
279
+
280
+ `_implementation-deliverable.md` "Correction round": verifierResults 는 마지막 라운드의
281
+ verdict 이고 앞선 FAIL 은 correctionRounds 에만 남는다. 원장에 있는 라운드가 기록에서
282
+ 빠지면 그 FAIL 이 보고서에서 사라진다.
283
+ """
284
+ implementation = data.get("implementation")
285
+ if not isinstance(implementation, dict):
286
+ return
287
+ rows = [row for row in implementation.get("correctionRounds") or [] if isinstance(row, dict)]
288
+ recorded = [row.get("round") for row in rows]
289
+ dispatched = sorted({
290
+ number for invocation in run_manifest.get("invocations") or []
291
+ if isinstance(invocation, Mapping)
292
+ and (number := correction_round(str(invocation.get("dispatchKind") or ""))) is not None
293
+ })
294
+ if sorted(recorded) != dispatched:
295
+ failures.append(
296
+ f"{_CORRECTION_BLOCKING} implementation.correctionRounds records rounds "
297
+ f"{sorted(recorded)} but the run dispatched correction rounds {dispatched}. "
298
+ "Record one entry per dispatched round, with its re-verification verdicts."
299
+ )
300
+ if recorded != list(range(1, len(recorded) + 1)):
301
+ failures.append(
302
+ f"{_CORRECTION_BLOCKING} implementation.correctionRounds rounds are {recorded}; "
303
+ "they run 1, 2 in order."
304
+ )
305
+ for row in rows:
306
+ verdicts = [v for v in row.get("verifierVerdicts") or [] if isinstance(v, dict)]
307
+ any_fail = any(v.get("verdict") == "FAIL" for v in verdicts)
308
+ if (row.get("result") == "pass") == any_fail:
309
+ failures.append(
310
+ f"{_CORRECTION_BLOCKING} implementation.correctionRounds round "
311
+ f"{row.get('round')} records `result: {row.get('result')}` but its "
312
+ f"re-verification verdicts are {[v.get('verdict') for v in verdicts]}; "
313
+ "`pass` means no verdict is FAIL."
314
+ )
315
+ if not rows:
316
+ return
317
+ last = {
318
+ str(v.get("verifier")): v.get("verdict")
319
+ for v in rows[-1].get("verifierVerdicts") or [] if isinstance(v, dict)
320
+ }
321
+ reported = {who: row.get("verdict") for who, row in _verifier_rows(data)}
322
+ if last != reported:
323
+ failures.append(
324
+ f"{_CORRECTION_BLOCKING} implementation.verifierResults verdicts {reported} differ "
325
+ f"from the last correction round's {last}. verifierResults carries the last "
326
+ "round's verdicts; earlier ones stay in correctionRounds."
327
+ )
328
+
329
+
269
330
  def _validate_verifier_fail_blocks_verdict(data: dict, failures: list[str]) -> None:
270
331
  """A verifier FAIL cannot be dropped during synthesis.
271
332
 
@@ -2,7 +2,7 @@
2
2
  "allowed": " - entering this phase only when the cited final-verification report's verdict is release-ready — `Verdict Token` exactly `accepted`, or exactly `conditional-accept` with every Conditional Acceptance Condition row declaring `blocksReleaseHandoff: false` (`okstra_ctl.release_gate.release_handoff_allowed`)\n - asking the user (via `AskUserQuestion` / interactive prompt) which delivery action to take: `local checkout`, `push + PR`, or `skip` (end the run)\n - asking the user to pick a PR base branch from `staging` | `preprod` | `prod` | `main` | `dev` | a user-supplied branch name\n - drafting PR title and PR body **inline as the Okstra lead** (no drafter worker and no delegated dispatch); the lead reviews its own draft with the user before any mutating git / gh command runs\n - read-only git inspection: `git status`, `git diff`, `git log`, `git rev-parse`\n - pushing the current feature branch to its origin remote via `git push -u origin <current-branch>` (the feature branch only — NEVER the base branch)\n - creating a pull request via `gh pr create --base <chosen-base> --head <current-branch>`; if a PR with the same head already exists, surface its URL and skip creation\n - the lead writes the final report directly (no `Report writer worker` dispatch); the report still conforms to the standard final-report template",
3
3
  "forbidden": [
4
4
  "entering this phase when the cited final-verification `Verdict Token` is `conditional-accept` or `blocked`, or when no final-verification report is cited",
5
- "local commit commands of any kind (`git add`, `git commit`, `git restore --staged`, `git stash`), and any direct `git merge` / `git rebase` / `git rebase --onto` / `git cherry-pick` / `git commit --amend` run by the lead. The single exception is the merge commits `okstra handoff pr-plan` itself creates on a `merge-base` branch — the lead never merges by hand. Rewriting a stage branch breaks the PR stack that sits on it.",
5
+ "local commit commands of any kind (`git add`, `git commit`, `git restore --staged`, `git stash`), and any direct `git merge` / `git rebase` / `git rebase --onto` / `git cherry-pick` / `git commit --amend` run by the lead. Runtime exceptions are dependency-ordered rebases by `okstra_ctl.stage_rebase` (which invalidate verification and stop PR delivery), and merge commits on a dedicated multi-predecessor `merge-base` branch. Never merge a moving predecessor or origin branch into a downstream stage to update its base.",
6
6
  "any git push variant that rewrites remote history, regardless of intent or whether the user said \"force it\": `git push --force`, `git push --force-with-lease`, `git push -f`, `git push +<refspec>`, or any other history-rewriting invocation",
7
7
  "pushing directly to a release base branch — i.e. `git push origin <branch>` where `<branch>` is `main`, `master`, `prod`, `preprod`, `staging`, `dev`, or the branch the user chose as the release base in this run. The only permitted push targets are the branches `okstra handoff pr-plan` listed for this run (each stage `head_branch`, and any `merge-base` branch).",
8
8
  "bypassing repo safeguards: `--no-verify` / `-n` on `git push`, bypassing GPG signing, disabling safeguards via equivalent flags, or any hook bypass.",
@@ -5,9 +5,14 @@ import subprocess
5
5
  from pathlib import Path
6
6
  from typing import Any, Dict, List, Optional
7
7
 
8
+ from okstra_project.dirs import okstra_home
9
+
8
10
  from okstra_ctl import consumers, stage_targets, worktree_registry
9
11
  from okstra_ctl.handoff_verification import verification_row_is_current
10
12
  from okstra_ctl.handoff_error import HandoffError
13
+ from okstra_ctl.locks import worktree_provision_mutex
14
+ from okstra_ctl.stage_rebase import synchronize_stage_stack
15
+ from okstra_ctl.stage_targets import StageWorktreeIdentity
11
16
  from okstra_ctl.stage_integrate import (
12
17
  IntegrateError, MergeBranchConflict, ensure_stage_merge_branch,
13
18
  )
@@ -306,15 +311,31 @@ def _reconcile_with_origin(
306
311
  def pr_plan(*, project_root, plan_run_root, stage_map, stages, base_branch,
307
312
  work_category, project_id, task_group, task_id) -> Dict[str, Any]:
308
313
  """선택한 stage 각각의 PR head/base 를 정하고, 필요한 선행 머지 브랜치를
309
- 만든다. PR 은 stage 당 하나이고 head 는 그 stage 의 스택 브랜치다 — 커밋
310
- 히스토리를 접지 않으므로 이 경로는 stage 브랜치를 고쳐 쓰지 않는다."""
311
- stages = sorted(set(stages))
314
+ 만든다. 이동한 의존 기준은 먼저 재배치하고 재검증 전에는 전달하지 않는다."""
315
+ try:
316
+ with worktree_provision_mutex(okstra_home(), project_id, task_group, task_id):
317
+ rebased = synchronize_stage_stack(
318
+ project_root=Path(project_root), plan_run_root=Path(plan_run_root),
319
+ stage_map=stage_map,
320
+ identity=StageWorktreeIdentity(project_id, task_group, task_id, work_category),
321
+ )
322
+ except RuntimeError as exc:
323
+ raise HandoffError(str(exc)) from exc
324
+ if rebased:
325
+ raise HandoffError(
326
+ f"stages {rebased} were rebased onto updated dependencies; "
327
+ "re-run final-verification before release-handoff"
328
+ )
312
329
  rows = consumers.read_consumers(Path(plan_run_root))
313
330
  _require_eligible(stage_map, rows, stages)
314
331
  _run_git(["fetch", "origin", base_branch], project_root)
315
332
  done = consumers.latest_done_by_stage(rows)
316
333
  pr_covered = consumers.pr_covered_stages(rows)
317
334
  by_n = {s["stage_number"]: s for s in stage_map}
335
+ stages = stage_targets.order_stage_closure(
336
+ [(n, s.get("depends_on") or []) for n, s in by_n.items()],
337
+ set(stages), set(by_n) - set(stages),
338
+ )
318
339
  anchor_base = worktree_registry.get_implementation_base(
319
340
  project_id, task_group, task_id) or ""
320
341
 
@@ -3,7 +3,7 @@
3
3
  - Record the handoff shape in `releaseHandoff.handoffScope`: the `stages` this run delivered and
4
4
  the `releaseBase` the user picked. The report is the only place a reader learns which stages
5
5
  shipped and in what order their PRs must be merged — the branch names do not say.
6
- - Purpose: take a release-ready single-stage final-verification verdict for each already-committed implementation stage and turn it into a delivered push and/or pull request, with explicit user selection at every mutating step. **One stage is one PR.** The PR head is that stage's stack branch (`<prefix>-<task-id>-s<N>`); nothing is squashed, rebased, or collected into a bundle branch.
6
+ - Purpose: take a release-ready single-stage final-verification verdict for each already-committed implementation stage and turn it into a delivered push and/or pull request, with explicit user selection at every delivery step. **One stage is one PR.** The PR head is that stage's stack branch (`<prefix>-<task-id>-s<N>`); nothing is squashed or collected into a bundle branch. Runtime dependency rebases happen before delivery and invalidate the prior verification.
7
7
  - **Execution model: single-lead, no worker dispatch.** This phase is a thin orchestrator over `git` / `gh`; it does NOT dispatch teammates, does NOT dispatch analysis or drafter sub-agents, and does NOT run convergence. The host-native Okstra lead performs every step inline (drafting PR text, asking the user, running git / gh, writing the final report) — see "Lead-only contract" below.
8
8
  - Worker roster: none — `profile.json` declares no roles; the run is executed entirely by the Okstra lead.
9
9
  - Lead-only contract (replaces the shared team contract for this phase):
@@ -20,7 +20,7 @@
20
20
  - the lead MUST capture `git status --short` and confirm the working tree is clean. Dirty state aborts the run; release-handoff packages the commits produced by `implementation`, it does not stage or commit changes.
21
21
  - the lead MUST capture `git rev-parse --abbrev-ref HEAD` and record it as the run's current branch. It is evidence, not a PR head: every PR head comes from `okstra handoff pr-plan`. If the current branch is `main`, `master`, `prod`, `preprod`, `staging`, or `dev`, record it and do not push it — release-handoff never pushes a base branch.
22
22
  - before pushing, run `okstra handoff pr-plan` again if anything mutated since entry, and confirm each selected stage is still present in its output. `pr-plan` re-checks eligibility and each stage's recorded verified commit. Record the pr-plan output in Executed Commands. These pre-push comparisons are lead instructions; the automated entry checks are enforced by `okstra_ctl.handoff_verification` and the handoff tests.
23
- - **Commit history is preserved — structurally, not as a preference.** These PRs are a stack: stage N's PR base is stage N-1's branch. Squash-merging a PR in the stack replaces its commits with a new one, so the next PR's base points at commits that no longer exist on the target branch and its diff re-shows the predecessor's changes. Therefore: okstra never rebases, squashes, amends, or cherry-picks a stage branch, and the merge policy — **merge commit or rebase-merge, never squash, and merge in ascending stage order** — is stated to the operator in the final report's `Stage PR Plan` and next-action narrative. It does NOT go into the PR body: the reviewer of one stage's PR is not the person merging the stack, and the block was noise they deleted by hand (2026-09-24, dev-10860 PRs #1487-#1490).
23
+ - **Dependency updates use rebase.** When a predecessor advances, the runtime replays each dependent stage's own commits onto the new base in dependency order with `git rebase --onto`; never merge a moving predecessor into a downstream stage. `okstra_ctl.stage_rebase.synchronize_stage_stack` enforces this at implementation entry, verification target acquisition, and PR planning. It preserves the old head under `refs/okstra/rebase-backups/`, updates the registry base and completion ledger, and invalidates prior verification. Conflicts abort the current rebase and stop the remaining stages. The lead does not rebase or merge by hand. A rebase during `pr-plan` stops delivery and requires fresh final-verification. Remote divergence stops delivery for explicit user instructions; never repair it by merging origin back into the rebased stage. Merge PRs in dependency order, using merge commits or rebase-merge rather than squash. State that order and rule in the final report and closeout, not in individual PR bodies.
24
24
  - User interaction protocol (Okstra lead — performed in order, using the selected runtime adapter's interactive prompt):
25
25
  1. **Action selection** — present three choices and capture exactly one:
26
26
  - `local checkout` — bring one stage's branch into the MAIN worktree for local testing (no push, no PR). See step 1c.
@@ -39,8 +39,8 @@
39
39
  `okstra handoff pr-plan --plan-run-root <...> --approved-plan <...> --project-root <project root> --project-id <id> --task-group <g> --task-id <t> --work-category <c> --stages <HANDOFF_STAGES> --base <chosen-base>`.
40
40
  It returns one row per stage with `head_branch`, `head_commit`, `base_kind` (`release-base` | `stage` | `merge-base`), `base_branch` and `base_commit`. Exit 2 means the predecessors of a multi-dependency stage conflict with each other: show the `conflicts` paths and stop (route: resolve manually, then retry). Exit 1 means an eligibility or dependency violation — most often a stage whose predecessor is neither in this run nor already PR'd nor merged into the release base: show the error verbatim and re-ask the action selection. On success these rows are authoritative for every subsequent step; the lead does not recompute a base by hand.
41
41
  A `merge-base` row means `pr-plan` created a branch merging that stage's predecessors. That branch is a PR base only — it is never a PR head — and it MUST be pushed before the PR that targets it.
42
- The output also carries `push_plan` and `warnings`. `push_plan` is every branch the PRs sit on, already in push order (each `merge-base` first, then the stage heads in ascending order), with the `commit` it must reach and an `action` of `create` (origin does not have it), `fast-forward` (origin is behind) or `up-to-date` (origin already has that commit — do not push it). GitHub resolves a PR's head and base by name on origin, so this list, not the local branch set, is what makes the stack real. A `warnings` row means `pr-plan` fast-forwarded a local branch to origin because origin was ahead: the commits origin adds are outside what this run verified. Quote every `warnings` row verbatim in Executed Commands and say so in the branch/PR explanation — the user is deciding whether to release commits this run did not verify. `pr-plan` refuses outright when a branch moved outside okstra or diverged from origin, since only a force push could deliver it.
43
- 2b. **Pre-merge conflict probe** (only when the user picked `push + PR`) — before pushing, for each planned stage in ascending order:
42
+ The output also carries `push_plan` and `warnings`. `push_plan` is every branch the PRs sit on, already in push order (each `merge-base` first, then the stage heads in dependency order), with the `commit` it must reach and an `action` of `create` (origin does not have it), `fast-forward` (origin is behind) or `up-to-date` (origin already has that commit — do not push it). GitHub resolves a PR's head and base by name on origin, so this list, not the local branch set, is what makes the stack real. A `warnings` row means `pr-plan` fast-forwarded a local branch to origin because origin was ahead: the commits origin adds are outside what this run verified. Quote every `warnings` row verbatim in Executed Commands and say so in the branch/PR explanation — the user is deciding whether to release commits this run did not verify. `pr-plan` refuses outright when a branch moved outside okstra or diverged from origin, since only a force push could deliver it.
43
+ 2b. **Pre-merge conflict probe** (only when the user picked `push + PR`) — before pushing, for each planned stage in dependency order:
44
44
  - run `git fetch origin <chosen-base>` once (read-only on the local working tree).
45
45
  - run `git merge-tree --write-tree <head_branch> <base_branch>` for that stage, using the `base_branch` from the pr-plan row (`origin/<chosen-base>` for a `release-base` row). Exit 0 means clean. Exit 1 with a result-tree object ID on the first stdout line means a merge conflict. A missing result tree, or any other exit code, is an execution error: stop and report it, without offering permission to proceed as if it were a content conflict. An invalid ref can also return exit 1, so the exit code alone is insufficient.
46
46
  - If no stage conflicts, proceed silently to Q3 (do NOT add a confirmation prompt — keep the happy path frictionless).
@@ -63,8 +63,8 @@
63
63
  - Allowed actions during the run (Okstra lead only):
64
64
  - read-only inspection: `git status`, `git status --short`, `git diff`, `git log`, `git rev-parse`, `git ls-remote --heads origin <name>`, `gh pr list --head <branch>`, `gh pr view <url>`, and the drafting bundle `okstra pr gen --base <base_commit> --head <head_commit> --template-file <PR_TEMPLATE_PATH>`, and the body check `okstra pr check --body-file <draft file>`.
65
65
  - merge-conflict probe (only when the user picked `push + PR`): `git fetch origin <chosen-base>` and the exact `git merge-tree` commands from step 2b. Both preserve the working tree and branch tips.
66
- - branch push (only when the user picked `push + PR`): `git push -u origin <branch>` for each `push_plan` row whose `action` is `create` or `fast-forward`, in the order `push_plan` lists them (each `merge-base` branch before the PR that targets it, then the stage heads in ascending order). Skip an `up-to-date` row — origin already holds that commit. The pushed ref MUST come from a `push_plan` row — never the release base branch, and never a ref that list does not name.
67
- - PR creation, one per stage (only when the user picked `push + PR` AND no PR with the same head already exists on origin): `gh pr create --base <row base_branch> --head <row head_branch> --title "<title>" --body "<body>"`. The title and body are the user-confirmed PR draft for that stage. Open them in ascending stage order so each base branch already exists on origin.
66
+ - branch push (only when the user picked `push + PR`): `git push -u origin <branch>` for each `push_plan` row whose `action` is `create` or `fast-forward`, in the order `push_plan` lists them (each `merge-base` branch before the PR that targets it, then the stage heads in dependency order). Skip an `up-to-date` row — origin already holds that commit. The pushed ref MUST come from a `push_plan` row — never the release base branch, and never a ref that list does not name.
67
+ - PR creation, one per stage (only when the user picked `push + PR` AND no PR with the same head already exists on origin): `gh pr create --base <row base_branch> --head <row head_branch> --title "<title>" --body "<body>"`. The title and body are the user-confirmed PR draft for that stage. Open them in dependency order so each base branch already exists on origin.
68
68
  - PR reuse: if `gh pr list --head <branch> --state open --json url --jq '.[0].url'` returns a URL, treat that PR as already existing — record the URL in the final report and SKIP `gh pr create` for that stage.
69
69
  - after each `gh pr create` succeeds (or an existing PR is reused), the lead MUST run `okstra handoff record-pr --plan-run-root <...> --stage <N> --branch <head branch> --base <base branch> --url <pr url>` and quote the command + exit code in the final report. One record-pr call per stage — the consumers ledger is what stops the same stage being PR'd twice.
70
70
  - stage helpers: `okstra handoff eligible`, `okstra handoff pr-plan`, `okstra handoff record-pr`, `okstra handoff local-checkout`. `pr-plan` is the ONLY path that may create commits in this phase (merge commits on a `merge-base` branch, and only for a multi-dependency stage).
@@ -73,8 +73,8 @@
73
73
  - Required deliverable shape (final report, in addition to the standard sections):
74
74
  - **Source Verification Reports**: one row per selected stage — relative path of that stage's originating `final-verification` final-report plus the literal quoted `Verdict Token` row, and — when that token is `conditional-accept` — every Conditional Acceptance Condition row quoted with its `blocksReleaseHandoff` value.
75
75
  - **Feature Branch & Working-Tree State**: the branch from `git rev-parse --abbrev-ref HEAD` at run start and the output of `git status --short` at run start.
76
- - **Stage PR Plan**: the pr-plan rows — stage, head branch, head commit, base kind, base branch, base commit — plus, for every `merge-base` row, which predecessor stages it merges and the merge commits it created. This is the table a reader uses to merge the stack in the right order, so it carries the merge rule with it: merge in ascending stage order, with a merge commit or rebase-merge, never a squash.
77
- - **User narrative `nextAction`**: when this run opened or reused any PR, it names the merge order (ascending stage number) and the merge rule (merge commit or rebase-merge, never squash), and says why — squashing one PR in the stack invalidates the next PR's base. This is where the operator reads the rule; the PR bodies do not carry it.
76
+ - **Stage PR Plan**: the pr-plan rows — stage, head branch, head commit, base kind, base branch, base commit — plus, for every `merge-base` row, which predecessor stages it merges and the merge commits it created. This is the table a reader uses to merge the stack in the right order, so it carries the merge rule with it: merge in dependency order, with a merge commit or rebase-merge, never a squash.
77
+ - **User narrative `nextAction`**: when this run opened or reused any PR, it names the merge order (dependency order) and the merge rule (merge commit or rebase-merge, never squash), and says why — squashing one PR in the stack invalidates the next PR's base. This is where the operator reads the rule; the PR bodies do not carry it.
78
78
  - **User Selections**: a block recording each prompt and the user's verbatim answer.
79
79
  - Q1 action: `local checkout` | `push + PR` | `skip`.
80
80
  - Q1c checkout stage (only when Q1 is `local checkout`): the `--stage <N>` value this answer produced. Record it in the `User Selections` row `H1c` (`userSelections.h1c`); omit the row entirely when Q1 was not `local checkout`.
@@ -108,7 +108,7 @@
108
108
  7. **Merge-conflict probe audit** — for any `push + PR` run, confirm the report's `Merge Conflict Probe` section is present and either records `Clean` or records `Conflicts detected` with the user's verbatim choice. A missing or unparseable probe entry on a `push + PR` run is a contract violation.
109
109
  - Non-goals:
110
110
  - re-litigating a final-verification verdict — release-handoff trusts the cited release-ready verdicts and does not reopen acceptance checks. Restating a conditional-accept's conditions in the PR body is reporting, not verification: the lead does not check whether a condition has been satisfied.
111
- - creating, amending, squashing, rebasing, or rewriting commits. Commit production belongs to `implementation`; the only commits this phase may create are the merge commits `okstra handoff pr-plan` makes on a `merge-base` branch.
111
+ - manual commit creation, amendment, squash, or rebase by the lead. Runtime dependency rebases are owned by `okstra_ctl.stage_rebase`; `pr-plan` stops delivery after any such change until final-verification is repeated. Combining several independent predecessors on a dedicated `merge-base` branch remains separate from updating a downstream stage; never merge that base into the downstream stage.
112
112
  - bundling several stages into one PR, or building a branch that collects every stage. One stage is one PR.
113
113
  - opening releases or deployments beyond the per-stage PRs the user chose to create.
114
114
  - merging the PRs. Merging is a separate, manual step performed by the user (or by repo automation) after release-handoff ends; the lead MUST NOT call `gh pr merge`.
@@ -27,9 +27,11 @@ Report shapes remain in `schemas/final-report-v2.0.schema.json` and `schemas/fin
27
27
 
28
28
  ## 1. Purpose
29
29
 
30
- `release-handoff` is the terminal phase that pushes already-committed implementation stages with a release-ready verdict, or hands them off as pull requests. **One stage is one PR.** The PR head is that stage's stack branch; the base comes from its `depends-on` — the release base for a stage with no live dependency, the predecessor's branch for a stage with one (a stacked PR), and a branch merging the predecessors for a stage with several. The only commits this phase may create are the merge commits `okstra handoff pr-plan` makes on such a merge-base branch.
30
+ `release-handoff` is the terminal phase that pushes already-committed implementation stages with a release-ready verdict, or hands them off as pull requests. **One stage is one PR.** The PR head is that stage's stack branch; the base comes from its `depends-on` — the release base for a stage with no live dependency, the predecessor's branch for a stage with one (a stacked PR), and a branch merging the predecessors for a stage with several.
31
31
 
32
- Because the PRs are a stack, they must be merged in ascending stage order with a merge commit or a rebase-merge. A squash replaces the commits the next PR's base points at, so the stack breaks — every PR body states this, and the lead never merges.
32
+ When a predecessor advances, `okstra_ctl.stage_rebase.synchronize_stage_stack` rebases downstream stages in dependency order rather than merging the predecessor into them. It records the new bases and completion commits, preserving prior heads under `refs/okstra/rebase-backups/`. Implementation entry and verification target acquisition use the same operation. PR planning stops after a stack change and requires fresh final-verification; the new completion rows invalidate earlier verification through `consumers.verified_accepted_stages`. Dirty or active stages, rewritten unproven predecessors, and rebase conflicts stop synchronization. The tests in `tests/worktree/test_stage_rebase.py` cover ordering, preservation, and verification invalidation. Origin divergence remains a delivery error, never a reason to merge old origin history into the rebased branch.
33
+
34
+ Because the PRs are a stack, they must be merged in dependency order with a merge commit or a rebase-merge. A squash replaces the commits the next PR's base points at, so the stack breaks — every PR body states this, and the lead never merges.
33
35
 
34
36
  This phase has no worker dispatch. It does not use a provider or report-writer roster; the host-native Okstra lead performs git/gh inspection, user questions, the PR draft, and the final report inline.
35
37
 
@@ -697,8 +697,6 @@ def _active_runtime_resources(ctx: dict) -> dict:
697
697
  or str(runtime_home / "templates" / "implementation-worker-preamble.md"),
698
698
  "report-writer": ctx.get("REPORT_WRITER_PREAMBLE_PATH")
699
699
  or str(runtime_home / "templates" / "report-writer-prompt-preamble.md"),
700
- "translator": ctx.get("TRANSLATOR_PREAMBLE_PATH")
701
- or str(runtime_home / "templates" / "translator-prompt-preamble.md"),
702
700
  },
703
701
  "workerErrorContractPath": ctx.get("WORKER_ERROR_CONTRACT_PATH")
704
702
  or str(runtime_home / "templates" / "worker-error-contract.md"),
@@ -2638,9 +2636,6 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
2638
2636
  report_writer_preamble = ctx.get("REPORT_WRITER_PREAMBLE_PATH") or str(
2639
2637
  runtime_templates / "report-writer-prompt-preamble.md"
2640
2638
  )
2641
- translator_preamble = ctx.get("TRANSLATOR_PREAMBLE_PATH") or str(
2642
- runtime_templates / "translator-prompt-preamble.md"
2643
- )
2644
2639
  worker_error_contract = ctx.get("WORKER_ERROR_CONTRACT_PATH") or str(
2645
2640
  runtime_templates / "worker-error-contract.md"
2646
2641
  )
@@ -2663,7 +2658,6 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
2663
2658
  f"- Analysis worker preamble: `{analysis_preamble}`\n"
2664
2659
  f"- Implementation worker preamble: `{implementation_preamble}`\n"
2665
2660
  f"- Report writer preamble: `{report_writer_preamble}`\n"
2666
- f"- Translator preamble: `{translator_preamble}`\n"
2667
2661
 
2668
2662
  f"- Shared worker error contract: `{worker_error_contract}`"
2669
2663
  )
@@ -2886,10 +2880,6 @@ def apply_lead_prompt_defaults(ctx: dict) -> None:
2886
2880
  "REPORT_WRITER_PREAMBLE_PATH",
2887
2881
  str(runtime_home / "templates" / "report-writer-prompt-preamble.md"),
2888
2882
  )
2889
- ctx.setdefault(
2890
- "TRANSLATOR_PREAMBLE_PATH",
2891
- str(runtime_home / "templates" / "translator-prompt-preamble.md"),
2892
- )
2893
2883
  ctx.setdefault(
2894
2884
  "WORKER_ERROR_CONTRACT_PATH",
2895
2885
  str(runtime_home / "templates" / "worker-error-contract.md"),
@@ -588,10 +588,10 @@ def _markdown_task_template(task_type: str) -> str:
588
588
  return ""
589
589
 
590
590
 
591
- # The Markdown is the AI handoff sibling of the data.json — same content, same
592
- # audience, so it renders in the SSOT's language and nothing else. The field
593
- # still has to be well-formed here because it decides whether Phase 7 pays for
594
- # a translator, and a malformed one would surface at the very end of the run.
591
+ # The Markdown is the AI handoff sibling of the data.json. Its prose is the
592
+ # record's own, already in the report language; only its fixed labels come from
593
+ # the English dictionary. A malformed tag is still rejected here, before the
594
+ # HTML render meets it at the very end of the run.
595
595
  MARKDOWN_LANG = "en"
596
596
 
597
597