okstra 0.219.0 → 0.219.1

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 (86) hide show
  1. package/README.md +5 -5
  2. package/dist/cli-registry.mjs +0 -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 +2 -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 +1 -3
  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/dispatch_core.py +3 -349
  31. package/runtime/python/okstra_ctl/dispatch_state.py +0 -13
  32. package/runtime/python/okstra_ctl/domain/provider.py +1 -1
  33. package/runtime/python/okstra_ctl/domain/role.py +0 -6
  34. package/runtime/python/okstra_ctl/final_report_paths.py +0 -16
  35. package/runtime/python/okstra_ctl/i18n.py +2 -2
  36. package/runtime/python/okstra_ctl/path_hints.py +0 -4
  37. package/runtime/python/okstra_ctl/paths.py +0 -3
  38. package/runtime/python/okstra_ctl/phases/final_verification/profile.md +1 -0
  39. package/runtime/python/okstra_ctl/phases/final_verification/spec.md +2 -0
  40. package/runtime/python/okstra_ctl/phases/final_verification/target.py +17 -1
  41. package/runtime/python/okstra_ctl/phases/implementation/entry.py +12 -0
  42. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +1 -0
  43. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md +1 -0
  44. package/runtime/python/okstra_ctl/phases/implementation/spec.md +4 -0
  45. package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +1 -1
  46. package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +24 -3
  47. package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +9 -9
  48. package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +4 -2
  49. package/runtime/python/okstra_ctl/render.py +0 -10
  50. package/runtime/python/okstra_ctl/render_final_report.py +4 -4
  51. package/runtime/python/okstra_ctl/report_finalize.py +37 -76
  52. package/runtime/python/okstra_ctl/report_html/render.py +4 -44
  53. package/runtime/python/okstra_ctl/report_language.py +3 -3
  54. package/runtime/python/okstra_ctl/report_prose_keys.py +153 -0
  55. package/runtime/python/okstra_ctl/report_synthesis_packet.py +1 -1
  56. package/runtime/python/okstra_ctl/run.py +1 -21
  57. package/runtime/python/okstra_ctl/stage_integrate.py +37 -2
  58. package/runtime/python/okstra_ctl/stage_rebase.py +189 -0
  59. package/runtime/python/okstra_ctl/stage_targets.py +29 -62
  60. package/runtime/python/okstra_ctl/team.py +1 -6
  61. package/runtime/python/okstra_ctl/wizard/confirmation.py +0 -8
  62. package/runtime/python/okstra_ctl/wizard/registry.py +2 -3
  63. package/runtime/python/okstra_ctl/wizard/sources.py +10 -0
  64. package/runtime/python/okstra_ctl/worker_prompt_policy.py +3 -12
  65. package/runtime/python/okstra_ctl/worktree/__init__.py +1 -1
  66. package/runtime/python/okstra_ctl/worktree/decisions.py +3 -4
  67. package/runtime/python/okstra_ctl/worktree/provision.py +27 -0
  68. package/runtime/python/okstra_ctl/worktree/rebase.py +108 -0
  69. package/runtime/python/okstra_ctl/worktree_registry.py +14 -0
  70. package/runtime/skills/_fragments/wizard-render-bundle.md +1 -1
  71. package/runtime/skills/okstra-discussion/SKILL.md +1 -1
  72. package/runtime/skills/okstra-run/SKILL.md +1 -1
  73. package/runtime/skills/okstra-setup/SKILL.md +2 -2
  74. package/runtime/skills/okstra-setup/references/project-config.md +2 -2
  75. package/runtime/templates/report-writer-prompt-preamble.md +1 -1
  76. package/runtime/templates/reports/html/i18n/en.json +0 -1
  77. package/runtime/templates/reports/html/i18n/ko.json +0 -1
  78. package/runtime/templates/worker-prompt-preamble.md +1 -1
  79. package/runtime/validators/validate-run.py +1 -28
  80. package/runtime/agents/operations/report-translation.json +0 -6
  81. package/runtime/agents/roles/translator.json +0 -19
  82. package/runtime/bin/okstra-report-translate.py +0 -404
  83. package/runtime/prompts/duties/translator.json +0 -32
  84. package/runtime/python/okstra_ctl/report_translation.py +0 -509
  85. package/runtime/python/okstra_ctl/report_translation_dispatch.py +0 -356
  86. package/runtime/templates/translator-prompt-preamble.md +0 -36
@@ -6,7 +6,6 @@ from pathlib import Path
6
6
 
7
7
  DATA_JSON_SUFFIX = ".data.json"
8
8
  MARKDOWN_SUFFIX = ".md"
9
- TRANSLATION_SOURCE_SUFFIX = ".translation-source.json"
10
9
 
11
10
 
12
11
  def _stem(data_path: Path) -> str:
@@ -106,21 +105,6 @@ def timeline_report_record_rel(run: dict) -> str:
106
105
  return report_record_rel_from_legacy_pointer(str(run.get("reportPath") or ""))
107
106
 
108
107
 
109
- def translation_source_path(data_path: Path) -> Path:
110
- """Return the translator's work list for a final-report data.json path."""
111
- return data_path.with_name(_stem(data_path) + TRANSLATION_SOURCE_SUFFIX)
112
-
113
-
114
- def translation_sidecar_path(data_path: Path, lang: str) -> Path:
115
- """Return the filled translation sidecar the HTML renderer overlays.
116
-
117
- One file per language beside the report it translates, so the renderer
118
- finds it from the data.json alone and a run for another language does not
119
- overwrite it.
120
- """
121
- return data_path.with_name(f"{_stem(data_path)}.i18n.{lang}.json")
122
-
123
-
124
108
  def sidecar_source_rel(path: Path) -> str:
125
109
  """사이드카 프론트매터가 `source-report` / `source-data` 에 적는 경로.
126
110
 
@@ -47,8 +47,8 @@ def load_dictionary(lang: str, rel: tuple[str, ...] = DICTIONARY_REL) -> dict[st
47
47
  """`<lang>.json` 이 있으면 그것, 없으면 영어 사전.
48
48
 
49
49
  사전은 페이지 크롬(표 머리글·빈 상태·enum 라벨)의 고정 문자열이고 본문은
50
- 번역 사이드카가 덮는다. 사전이 없는 언어의 리포트는 본문은 그 언어, 크롬은
51
- 영어로 나온다 — 사전이 없다고 렌더를 거절하면 독자는 아무것도 못 받는다.
50
+ report-writer 가 리포트 언어로 쓴다. 사전이 없는 언어의 리포트는 본문은 그
51
+ 언어, 크롬은 영어로 나온다 — 사전이 없다고 렌더를 거절하면 독자는 아무것도 못 받는다.
52
52
  """
53
53
  directory = _i18n_dir(rel)
54
54
  path = directory / f"{lang}.json"
@@ -119,7 +119,6 @@ def hydrate_active_run_context(payload: Mapping[str, Any]) -> dict[str, Any]:
119
119
  "IMPLEMENTATION_WORKER_PREAMBLE_PATH", ""
120
120
  ),
121
121
  "report-writer": ctx.get("REPORT_WRITER_PREAMBLE_PATH", ""),
122
- "translator": ctx.get("TRANSLATOR_PREAMBLE_PATH", ""),
123
122
  },
124
123
  "workerErrorContractPath": ctx.get("WORKER_ERROR_CONTRACT_PATH", ""),
125
124
  "hostSessionContractPath": (
@@ -515,9 +514,6 @@ def _runtime_fields(paths: Mapping[str, Any]) -> dict[str, str]:
515
514
  "REPORT_WRITER_PREAMBLE_PATH": str(
516
515
  runtime_home / "templates" / "report-writer-prompt-preamble.md"
517
516
  ),
518
- "TRANSLATOR_PREAMBLE_PATH": str(
519
- runtime_home / "templates" / "translator-prompt-preamble.md"
520
- ),
521
517
  "WORKER_ERROR_CONTRACT_PATH": str(
522
518
  runtime_home / "templates" / "worker-error-contract.md"
523
519
  ),
@@ -663,9 +663,6 @@ def compute_run_paths(
663
663
  "REPORT_WRITER_PREAMBLE_PATH": str(
664
664
  runtime_home / "templates" / "report-writer-prompt-preamble.md"
665
665
  ),
666
- "TRANSLATOR_PREAMBLE_PATH": str(
667
- runtime_home / "templates" / "translator-prompt-preamble.md"
668
- ),
669
666
  "WORKER_ERROR_CONTRACT_PATH": str(
670
667
  runtime_home / "templates" / "worker-error-contract.md"
671
668
  ),
@@ -26,6 +26,7 @@
26
26
  - **whole-task mutates only when it has to.** When one stage branch already contains every other done stage's commit — the usual shape of a linear plan, since each stage branches from its predecessor's done commit — that branch IS the whole task, and verification runs in that stage's worktree with no merge at all (`stage_targets.containing_stage`). Only when the stage graph has several tips does entry auto-merge (with `--no-ff`) the done stages into the task branch to create an integration commit; that case is the mutating one. The stage worktrees are NOT removed on entry: they are reclaimed after the verdict, by the Phase 7 `teardown-stages` step, and only when the verdict clears the work for release (`accepted`, or `conditional-accept` with no condition blocking release). A `blocked` verdict therefore leaves every stage worktree in place, so the rework it routes to can start immediately. The stage branches are kept as the reviewable stack (the target of `okstra handoff local-checkout --stage <N>`). If a merge conflict occurs it reports the conflicting files and aborts (the user resolves them manually and retries). A stage worktree with uncommitted changes remaining is preserved. Therefore the "fully-merged, clean target" the entry gate above refers to is the state after this auto-integration step completes, and whole-task final-verification must be treated as a mutating phase that creates the integration commit.
27
27
  - **single-stage scope** (`--stage N`): prep verified stage N is `status:done` and its isolated stage worktree exists and is clean. Other stages' state is irrelevant. A single-stage run is a partial verification of one stage, and that is exactly the unit release-handoff ships: an `accepted` verdict here makes the stage PR-eligible, so `release-handoff` is the routing target. It says nothing about any other stage.
28
28
  - the lead still captures `git status --short` from the injected worktree to confirm the analysis ran against the delivered work-tree state; an unexpected divergence (dirty tree outside `.okstra/`, missing worktree) is a `tool-failure`, not a silent proceed.
29
+ - **Stage worktree lifecycle:** implementation finalization may already have removed a completed stage's directory. Preparation restores the preserved stage branch for single-stage verification, or the containing tip branch for a linear whole-task target. After successful report validation, a release-ready single-stage verdict reclaims that stage's clean worktree; a whole-task verdict reclaims completed stages without any cleanup-time merge. Blocked, active, dirty, nested, or changed stages remain available for rework. The branch and registry base survive cleanup. After finalization use the main project root or absolute artifact paths because the verification cwd may have been removed. Enforcement: `report_finalize._teardown_stage_worktrees`, `stage_targets.teardown_completed_stage_worktrees`, and `tests/worktree/test_stage_rebase.py`.
29
30
  - Worker verification procedure:
30
31
  - **Target confirmation:** analyse the injected target and nothing else. Read `verification-target.md` for the stage/report mapping and the complete diff stat. Prepare fixed that target and `phases/final_verification/validation.py` `validate_verification_target_match` (called by `validators/validate-run.py`) re-checks the report against its digest, so the procedure to follow here is simply: if the worktree you can see does not match the injected target, record a `tool-failure` — never reselect a target.
31
32
  - **Evidence:** attach file:line, exact command + exit code, log excerpt, or MCP SELECT evidence to every finding. Mark a requirement as covered only when the cited artifact demonstrates it.
@@ -184,6 +184,8 @@ flowchart TD
184
184
 
185
185
  The stage merge of whole-task mode is a runtime-owned integration step that prepare performs; the matching teardown is a runtime-owned step too, but it runs after the verdict (Phase 7 `teardown-stages`) and only when the verdict clears the work for release. After that, lead verification is read-only. Source edit, follow-up fix, and scope expansion are all forbidden. When a defect is found, it is not fixed within the current run. The lead chooses the next phase from `prompts/lead/phase-routing.md` using the blocker facts this phase recorded.
186
186
 
187
+ Implementation finalization may already have reclaimed completed stage worktrees. Target acquisition restores a missing single-stage worktree, or the containing tip-stage worktree for a linear whole-task target, from its preserved branch. Restoration does not merge into the task branch. After a release-ready single-stage verdict, `report-finalize` also reclaims that stage's worktree using the captured verification head; a blocked verdict keeps the restored worktree for rework. Cleanup never merges stages: `stage_targets.teardown_completed_stage_worktrees` only reclaims completed, inactive worktrees whose tips match the selected completion commits. Tests: `tests/worktree/test_stage_rebase.py`.
188
+
187
189
  ### 8. Verified code
188
190
 
189
191
  - `scripts/okstra_ctl/phases/final_verification/profile.md`
@@ -1,6 +1,6 @@
1
1
  """final-verification 이 검증할 대상을 고른다.
2
2
 
3
- 단일 stage 검증은 그 stage 의 worktree 를 읽기만 한다. 전체 작업 검증은 완료된
3
+ 검증 대상 고정 전에 이동한 의존 기준을 재배치한다. 전체 작업 검증은 완료된
4
4
  stage 전부를 담은 stage 브랜치가 있으면 그것을 쓰고, 없으면 task 브랜치에 통합한
5
5
  결과를 쓴다. 원장·registry·Git·통합은 공통 모듈이 제공하고, 여기서는 어느 것을
6
6
  언제 부를지만 정한다. 전체 과정은 task 키 잠금 하나 안에서 돈다.
@@ -115,6 +115,14 @@ def _read_final_verification_done_rows(
115
115
  ),
116
116
  plan_run_root,
117
117
  )
118
+ from okstra_ctl.stage_rebase import synchronize_stage_stack
119
+ from okstra_ctl.stage_targets import StageWorktreeIdentity
120
+
121
+ synchronize_stage_stack(
122
+ project_root=request.project_root, plan_run_root=plan_run_root,
123
+ stage_map=list(request.stage_map),
124
+ identity=StageWorktreeIdentity(project_id, task_group, task_id, request.work_category),
125
+ )
118
126
  return read_stage_consumer_state(plan_run_root).done_rows
119
127
 
120
128
 
@@ -141,6 +149,10 @@ def _acquire_single_stage_target(
141
149
  assert stage is not None
142
150
  row = worktree_registry.get_stage_row(*registry_coordinates, stage)
143
151
  worktree_path = (row or {}).get("worktree_path", "")
152
+ if worktree_path and not Path(worktree_path).exists():
153
+ from okstra_ctl.worktree.provision import restore_stage_worktree
154
+
155
+ restore_stage_worktree(request.project_root, worktree_path, row["branch"])
144
156
  head = ""
145
157
  if worktree_path and Path(worktree_path).is_dir():
146
158
  head_result = _git(worktree_path, "rev-parse", "HEAD")
@@ -183,6 +195,10 @@ def _acquire_tip_stage_target(
183
195
  return None
184
196
  row = worktree_registry.get_stage_row(*registry_coordinates, tip) or {}
185
197
  worktree_path = row.get("worktree_path", "")
198
+ if worktree_path and not Path(worktree_path).exists():
199
+ from okstra_ctl.worktree.provision import restore_stage_worktree
200
+
201
+ restore_stage_worktree(request.project_root, worktree_path, row["branch"])
186
202
  if not worktree_path or not Path(worktree_path).is_dir():
187
203
  return None
188
204
  head_result = _git(worktree_path, "rev-parse", "HEAD")
@@ -95,6 +95,18 @@ def claim_implementation_stage_run(
95
95
  plan_run_root = Path(inp.approved_plan_path).resolve().parents[1]
96
96
  backfill_done_from_carry(plan_run_root)
97
97
  auto_reconcile_best_effort(inp, plan_run_root)
98
+ from okstra_ctl.stage_rebase import synchronize_stage_stack
99
+
100
+ try:
101
+ synchronize_stage_stack(
102
+ project_root=Path(inp.project_root), plan_run_root=plan_run_root,
103
+ stage_map=ctx_stage_map,
104
+ identity=stage_targets.StageWorktreeIdentity(
105
+ inp.project_id, task_group_segment, task_id_segment, inp.work_category,
106
+ ),
107
+ )
108
+ except RuntimeError as exc:
109
+ raise ImplementationStageError(str(exc)) from exc
98
110
  reserved_stages = _reg.list_active_stage_numbers(
99
111
  inp.project_id,
100
112
  inp.task_group,
@@ -94,6 +94,7 @@ A finding that needs no code change (convergence settled it as a verifier misrea
94
94
  - **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
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
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.
97
+ - **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
98
  - **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
99
  - 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
100
  - 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.
@@ -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
@@ -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