okstra 0.218.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 (92) 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/commands/pr/pr.d.mts +6 -0
  7. package/dist/commands/pr/pr.mjs +58 -0
  8. package/dist/commands/pr/pr.mjs.map +1 -1
  9. package/dist/lib/runtime-payload.mjs +1 -2
  10. package/dist/lib/runtime-payload.mjs.map +1 -1
  11. package/docs/architecture/storage-model.md +1 -1
  12. package/docs/architecture.md +9 -7
  13. package/docs/cli.md +3 -4
  14. package/docs/project-structure-overview.md +4 -5
  15. package/package.json +1 -1
  16. package/runtime/BUILD.json +2 -2
  17. package/runtime/prompts/launch.template.md +1 -1
  18. package/runtime/prompts/lead/okstra-lead-contract.md +1 -3
  19. package/runtime/prompts/lead/report-writer.md +10 -17
  20. package/runtime/prompts/lead/team-contract.md +1 -2
  21. package/runtime/prompts/wizard/prompts.ko.json +0 -1
  22. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
  23. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +1 -1
  24. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +0 -1
  25. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +1 -1
  26. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +0 -1
  27. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +0 -1
  28. package/runtime/python/okstra_ctl/agent/invocation.py +1 -2
  29. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +2 -2
  30. package/runtime/python/okstra_ctl/agent/prompt_cli/jobs.py +0 -2
  31. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +2 -27
  32. package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -4
  33. package/runtime/python/okstra_ctl/dispatch_core.py +3 -349
  34. package/runtime/python/okstra_ctl/dispatch_state.py +0 -13
  35. package/runtime/python/okstra_ctl/domain/provider.py +1 -1
  36. package/runtime/python/okstra_ctl/domain/role.py +0 -6
  37. package/runtime/python/okstra_ctl/final_report_paths.py +0 -16
  38. package/runtime/python/okstra_ctl/i18n.py +2 -2
  39. package/runtime/python/okstra_ctl/path_hints.py +0 -4
  40. package/runtime/python/okstra_ctl/paths.py +0 -3
  41. package/runtime/python/okstra_ctl/phases/final_verification/profile.md +2 -0
  42. package/runtime/python/okstra_ctl/phases/final_verification/spec.md +2 -0
  43. package/runtime/python/okstra_ctl/phases/final_verification/target.py +17 -1
  44. package/runtime/python/okstra_ctl/phases/implementation/entry.py +12 -0
  45. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +1 -0
  46. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md +1 -0
  47. package/runtime/python/okstra_ctl/phases/implementation/spec.md +4 -0
  48. package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +1 -1
  49. package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +24 -3
  50. package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +11 -11
  51. package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +4 -2
  52. package/runtime/python/okstra_ctl/render.py +0 -10
  53. package/runtime/python/okstra_ctl/render_final_report.py +4 -4
  54. package/runtime/python/okstra_ctl/report_finalize.py +37 -76
  55. package/runtime/python/okstra_ctl/report_html/render.py +4 -44
  56. package/runtime/python/okstra_ctl/report_language.py +3 -3
  57. package/runtime/python/okstra_ctl/report_prose_keys.py +153 -0
  58. package/runtime/python/okstra_ctl/report_synthesis_packet.py +1 -1
  59. package/runtime/python/okstra_ctl/run.py +1 -21
  60. package/runtime/python/okstra_ctl/stage_integrate.py +37 -2
  61. package/runtime/python/okstra_ctl/stage_rebase.py +189 -0
  62. package/runtime/python/okstra_ctl/stage_targets.py +29 -62
  63. package/runtime/python/okstra_ctl/team.py +1 -6
  64. package/runtime/python/okstra_ctl/wizard/confirmation.py +0 -8
  65. package/runtime/python/okstra_ctl/wizard/registry.py +2 -3
  66. package/runtime/python/okstra_ctl/wizard/sources.py +10 -0
  67. package/runtime/python/okstra_ctl/worker_prompt_policy.py +3 -12
  68. package/runtime/python/okstra_ctl/worktree/__init__.py +1 -1
  69. package/runtime/python/okstra_ctl/worktree/decisions.py +3 -4
  70. package/runtime/python/okstra_ctl/worktree/provision.py +27 -0
  71. package/runtime/python/okstra_ctl/worktree/rebase.py +108 -0
  72. package/runtime/python/okstra_ctl/worktree_registry.py +14 -0
  73. package/runtime/skills/_fragments/wizard-render-bundle.md +1 -1
  74. package/runtime/skills/okstra-code-review/SKILL.md +1 -1
  75. package/runtime/skills/okstra-discussion/SKILL.md +1 -1
  76. package/runtime/skills/okstra-pr-gen/SKILL.md +9 -0
  77. package/runtime/skills/okstra-run/SKILL.md +1 -1
  78. package/runtime/skills/okstra-setup/SKILL.md +2 -2
  79. package/runtime/skills/okstra-setup/references/project-config.md +2 -2
  80. package/runtime/templates/pr/drafting-rules.md +4 -0
  81. package/runtime/templates/report-writer-prompt-preamble.md +1 -1
  82. package/runtime/templates/reports/html/i18n/en.json +0 -1
  83. package/runtime/templates/reports/html/i18n/ko.json +0 -1
  84. package/runtime/templates/worker-prompt-preamble.md +1 -1
  85. package/runtime/validators/validate-run.py +1 -28
  86. package/runtime/agents/operations/report-translation.json +0 -6
  87. package/runtime/agents/roles/translator.json +0 -19
  88. package/runtime/bin/okstra-report-translate.py +0 -404
  89. package/runtime/prompts/duties/translator.json +0 -32
  90. package/runtime/python/okstra_ctl/report_translation.py +0 -509
  91. package/runtime/python/okstra_ctl/report_translation_dispatch.py +0 -356
  92. package/runtime/templates/translator-prompt-preamble.md +0 -36
@@ -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).
@@ -59,12 +59,12 @@
59
59
  - produce **two artifacts per stage** before showing them to the user:
60
60
  1. **PR title** — by default the subject of that stage's most recent implementation commit, or a concise Conventional Commits-style summary of the stage's committed range.
61
61
  2. **PR body** — markdown filled from `PR_TEMPLATE_PATH`, and nothing else. Do NOT append a stack plan, a stage checklist, a sentence about where this PR stops in the stack, or a merge-policy line: the stack's shape and merge rule belong to the final report and the closeout, not to a reviewer's PR. The user-confirmation step's diff (Q3 `edit then proceed`) is computed against the filled template, not against the raw template file.
62
- - **Filled-body self-check (runs on each drafted body, before Q3 shows it).** "Fill in the placeholders" is only observable if someone checks that they were filled, so scan the drafted body for these and carry the result into Q3: a residual HTML comment or template marker (`<!-- … -->`, `_Describe your changes…_`, a bare `TODO` / `FIXME` the diff did not introduce), an unchecked `- [ ]` box with no inline N/A justification, a section heading whose body is empty, and a whole body short enough to carry no substance. Each hit is reported to the user with its line — never silently left in, and never auto-filled with invented content. The user remains free to accept it; the point is that they see it before choosing `use as-is`.
62
+ - **Filled-body self-check (runs on each drafted body, before Q3 shows it).** First write the drafted title and body to a file in this run's artifact directory and run `okstra pr check --body-file <that file>` from the stage worktree or the MAIN worktree; each `L<line>: <pattern>: <match>` row it prints is an okstra identifier a reviewer cannot open, reported as a hit with its line (exit 1 means hits, not a failure of the run). Then, because "fill in the placeholders" is only observable if someone checks that they were filled, scan the drafted body for these and carry the result into Q3: a residual HTML comment or template marker (`<!-- … -->`, `_Describe your changes…_`, a bare `TODO` / `FIXME` the diff did not introduce), an unchecked `- [ ]` box with no inline N/A justification, a section heading whose body is empty, and a whole body short enough to carry no substance. Each hit is reported to the user with its line — never silently left in, and never auto-filled with invented content. The user remains free to accept it; the point is that they see it before choosing `use as-is`.
63
63
  - Allowed actions during the run (Okstra lead only):
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>`.
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
 
@@ -25,14 +25,6 @@ placeholders, which a v2 report never carries because its numeric cells are
25
25
  `null` until this step fills them. Substituting the tokens on a later retry
26
26
  then leaves the already-rendered html stale for `validators/validate-report-views.py`.
27
27
 
28
- The translation sidecar is the `translate` step, directly before `render-views`,
29
- which overlays it. It used to be a manual lead sequence outside this command (materialize the translator
30
- prompt, dispatch it, resume finalize at `render-views`), written only in a doc
31
- the lead lazy-reads; a lead that ran the whole sequence at once skipped it and
32
- the run kept an English view under a `ko` report (2026-09-09, fontsninja-v3-site
33
- dev-10628-3). `okstra_ctl.report_translation_dispatch` owns the step; an English
34
- report or an existing sidecar makes it a no-op.
35
-
36
28
  Every lead adapter drives Phase 7 through this module: the Codex adapter calls
37
29
  it in-process (``codex_dispatch``), and a Claude-led run reaches the same code
38
30
  through the ``okstra report-finalize`` CLI. Neither reimplements the sequence.
@@ -69,12 +61,11 @@ from .handoff_error import HandoffError
69
61
  from .paths import RunRef, task_dir, task_manifest_file
70
62
  from .report_view_artifacts import html_view_path
71
63
  from .release_gate import release_handoff_allowed
72
- from .report_translation_dispatch import TranslateOutcome, translate_report
73
64
  from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
74
65
  from .stage_integrate import IntegrateError
75
66
  from .stage_targets import (
76
67
  StageTargetError,
77
- integrate_and_teardown_whole_task,
68
+ teardown_completed_stage_worktrees,
78
69
  )
79
70
  from .session import observe_lead_session
80
71
  from .lead_progress import record_checkpoint
@@ -92,7 +83,6 @@ from okstra_project.phase_pointer import (
92
83
 
93
84
 
94
85
  STEP_PROJECT_ACTIVITY = "project-activity"
95
- STEP_TRANSLATE = "translate"
96
86
  STEP_TOKEN_USAGE = "token-usage"
97
87
  STEP_RENDER_VIEWS = "render-views"
98
88
  STEP_SPAWN_FOLLOWUPS = "spawn-followups"
@@ -105,7 +95,6 @@ STEP_PRUNE_RUN_ARTIFACTS = "prune-run-artifacts"
105
95
 
106
96
  STEP_ORDER = (
107
97
  STEP_PROJECT_ACTIVITY,
108
- STEP_TRANSLATE,
109
98
  STEP_TOKEN_USAGE,
110
99
  STEP_RENDER_VIEWS,
111
100
  STEP_SPAWN_FOLLOWUPS,
@@ -124,8 +113,6 @@ V3_STEP_ORDER = (
124
113
  STEP_TOKEN_USAGE,
125
114
  STEP_PROJECT_ACTIVITY,
126
115
  STEP_PREFLIGHT,
127
- # Before the render that overlays its sidecar.
128
- STEP_TRANSLATE,
129
116
  STEP_RENDER_VIEWS,
130
117
  STEP_SPAWN_FOLLOWUPS,
131
118
  # Needs the assembled record; `validate-run` requires the row it writes.
@@ -341,10 +328,6 @@ def build_commands(ctx: FinalizeContext) -> list[tuple[str, list[str]]]:
341
328
  str(ctx.data_path),
342
329
  ],
343
330
  ),
344
- (
345
- STEP_TRANSLATE,
346
- ["<in-process>", "translate", str(ctx.data_path)],
347
- ),
348
331
  (
349
332
  STEP_TOKEN_USAGE,
350
333
  [
@@ -571,38 +554,53 @@ def _teardown_stage_worktrees(
571
554
  ctx: FinalizeContext,
572
555
  command: list[str],
573
556
  ) -> subprocess.CompletedProcess:
574
- """판정이 릴리스로 향할 때만 stage worktree 와 registry 키를 정리한다.
575
-
576
- whole-task 진입이 통합만 하고 정리를 남겨두므로(`stage_targets`), 정리는 판정이
577
- 나온 뒤인 여기서 한다. `blocked` 이거나 릴리스를 막는 조건이 남은 판정에서는
578
- stage 작업물을 그대로 둬서 재작업이 바로 이어지게 한다. 이미 정리된 뒤 재실행돼도
579
- 같은 결과를 낸다 — 병합은 `already_merged` 로, 없는 worktree 는 건너뛴다.
580
- """
557
+ """검증된 보고서와 완료 기록이 일치하는 단계 작업 트리를 회수한다."""
581
558
  def _done(payload: Mapping[str, Any]) -> subprocess.CompletedProcess:
582
559
  return subprocess.CompletedProcess(command, 0, json.dumps(payload), "")
583
560
 
584
- if ctx.task_type != "final-verification":
585
- return _done({"skipped": "not a final-verification run"})
561
+ if ctx.task_type not in {"implementation", "final-verification"}:
562
+ return _done({"skipped": "not an implementation or final-verification run"})
586
563
  try:
587
564
  data = load_owned_object(Path(ctx.data_path), artifact="final report record")
588
565
  except JsonBoundaryError as exc:
589
566
  return subprocess.CompletedProcess(
590
567
  command, 1, "", f"cannot read final-report data.json: {exc}")
591
- if data.get("verificationScope") != "whole-task":
592
- return _done({"skipped": "single-stage verification owns no teardown"})
593
- if not release_handoff_allowed(data):
568
+ if ctx.task_type == "final-verification" and not release_handoff_allowed(data):
594
569
  return _done({"skipped": "verdict does not clear the work for release"})
570
+ heads = _stage_cleanup_heads(ctx.task_type, data)
571
+ if heads == {}:
572
+ return _done({"skipped": "report names no completed stage commit"})
595
573
  try:
596
- result = integrate_and_teardown_whole_task(
574
+ result = teardown_completed_stage_worktrees(
597
575
  project_root=ctx.project_root,
598
576
  task_group=ctx.task_group,
599
577
  task_id=ctx.task_id,
578
+ expected_heads=heads,
600
579
  )
601
- except (IntegrateError, StageTargetError, OSError) as exc:
580
+ except (IntegrateError, StageTargetError, JsonBoundaryError, OSError) as exc:
602
581
  return subprocess.CompletedProcess(command, 1, "", str(exc))
603
582
  return _done(result)
604
583
 
605
584
 
585
+ def _stage_cleanup_heads(task_type: str, data: Mapping[str, Any]) -> dict[int, str] | None:
586
+ if task_type == "implementation":
587
+ implementation = data.get("implementation") or {}
588
+ evidence = implementation.get("stageSidecarEvidence") or {}
589
+ stage = evidence.get("stageNumber")
590
+ commits = implementation.get("commitList") or []
591
+ head = commits[-1].get("fullSha", "") if commits else ""
592
+ return {stage: head} if isinstance(stage, int) and head and not evidence.get("withheld") else {}
593
+ if data.get("verificationScope") == "whole-task":
594
+ return None
595
+ verification = data.get("finalVerification") or {}
596
+ stages = verification.get("stageReports") or []
597
+ head = (verification.get("sourceImplementationReport") or {}).get("capturedHeadSha", "")
598
+ if data.get("verificationScope") == "single-stage" and len(stages) == 1 and head:
599
+ stage = stages[0].get("stage")
600
+ return {stage: head} if isinstance(stage, int) else {}
601
+ return {}
602
+
603
+
606
604
  def _prune_run_artifacts(
607
605
  ctx: FinalizeContext,
608
606
  command: list[str],
@@ -684,8 +682,6 @@ def run_finalize(
684
682
  if only:
685
683
  selected = set(only)
686
684
  if ctx.report_contract_version == "3.0":
687
- if STEP_TRANSLATE in selected:
688
- selected.add(STEP_PREFLIGHT)
689
685
  if selected - {STEP_TOKEN_USAGE}:
690
686
  # 부분 재개도 이전 조립본이 아닌 현재 소유자 입력을 소비한다.
691
687
  selected.add(STEP_PROJECT_ACTIVITY)
@@ -799,7 +795,6 @@ def _execute_finalize_steps(
799
795
  if consumer != STEP_TOKEN_USAGE or ctx.report_contract_version != "3.0":
800
796
  blocked[consumer] = name
801
797
  elif name == STEP_PREFLIGHT:
802
- blocked[STEP_TRANSLATE] = name
803
798
  blocked[STEP_SPAWN_FOLLOWUPS] = name
804
799
  elif name == STEP_RENDER_VIEWS:
805
800
  blocked[STEP_VALIDATE_RUN] = name
@@ -865,8 +860,6 @@ def _finalize_recovery(ctx: FinalizeContext, result: Mapping[str, Any]) -> dict[
865
860
  "Repair the reported causes in this run using the owning input's correction command, "
866
861
  "then execute resumeCommand. Preserve approvals, model choices and completed evidence. "
867
862
  "If plan content changes, re-verify the affected items before finalizing. "
868
- "After a narrative correction, check existing translations with report-translate check-data "
869
- "and regenerate them if their source no longer matches. "
870
863
  "Ask only for an unresolved user decision or an actual external prerequisite."
871
864
  ),
872
865
  }
@@ -1018,8 +1011,6 @@ def _run_finalize_step(
1018
1011
  """한 Phase 7 단계를 실행하고 그 단계의 종료 코드만 돌려준다."""
1019
1012
  if name == STEP_PROJECT_ACTIVITY:
1020
1013
  return _run_project_activity(ctx, command)
1021
- if name == STEP_TRANSLATE:
1022
- return _run_translate(ctx, command)
1023
1014
  if name == STEP_TEARDOWN_STAGES:
1024
1015
  return _teardown_stage_worktrees(ctx, command)
1025
1016
  if name == STEP_PRUNE_RUN_ARTIFACTS:
@@ -1043,31 +1034,6 @@ def _run_finalize_step(
1043
1034
  )
1044
1035
 
1045
1036
 
1046
- def _run_translate(
1047
- ctx: FinalizeContext,
1048
- command: Sequence[str],
1049
- ) -> subprocess.CompletedProcess[str]:
1050
- """비영어 리포트의 번역 사이드카를 만든다. 영어 리포트는 건너뛴다.
1051
-
1052
- 실패는 다른 단계처럼 기록만 하고 시퀀스는 계속 돈다 — `render-views` 는
1053
- 사이드카 없이 영어 열람본을 내고, `validate-run` 은 권고를 남기며, 결과의
1054
- `--only translate --only render-views …` 재개 힌트가 그 둘을 다시 돌린다.
1055
- """
1056
- try:
1057
- outcome = translate_report(
1058
- project_root=ctx.project_root,
1059
- workspace_root=ctx.workspace_root,
1060
- manifest_path=ctx.manifest_path,
1061
- manifest=_load_manifest(ctx.manifest_path),
1062
- data_path=ctx.data_path,
1063
- )
1064
- except (FinalizeError, OSError, JsonBoundaryError) as exc:
1065
- outcome = TranslateOutcome(1, "", f"translate failed: {exc}")
1066
- return subprocess.CompletedProcess(
1067
- command, outcome.returncode, outcome.stdout, outcome.stderr
1068
- )
1069
-
1070
-
1071
1037
  def _run_project_activity(
1072
1038
  ctx: FinalizeContext,
1073
1039
  command: Sequence[str],
@@ -1206,18 +1172,13 @@ _CLI_EPILOG = r"""Usage:
1206
1172
  Runs the Phase 7 steps in their contractual order against one final-report:
1207
1173
 
1208
1174
  1. project-activity project the run's activity events into the data.json
1209
- 2. translate for a non-English reportLanguage, materialize and
1210
- dispatch the translator worker and require its
1211
- *.i18n.<lang>.json sidecar; a no-op for English or
1212
- when the sidecar already exists
1213
- 3. token-usage substitute real token/cost numbers into the data.json
1214
- 4. render-views write the schema-v2 task-specific *.html sibling with
1215
- the translation overlaid; v1 keeps the
1216
- legacy conditional interactive view
1217
- 5. spawn-followups turn section 4 rows into task stubs
1218
- 6. validate-run validate the finished run artifacts
1219
- 7. record-group-memory / 8. teardown-stages after a clean validation
1220
- 9. prune-run-artifacts remove node_modules and .next under this run
1175
+ 2. token-usage substitute real token/cost numbers into the data.json
1176
+ 3. render-views write the schema-v2 task-specific *.html sibling;
1177
+ v1 keeps the legacy conditional interactive view
1178
+ 4. spawn-followups turn section 4 rows into task stubs
1179
+ 5. validate-run validate the finished run artifacts
1180
+ 6. record-group-memory / 7. teardown-stages after a clean validation
1181
+ 8. prune-run-artifacts remove node_modules and .next under this run
1221
1182
  directory, and the dispatch snapshots and publication
1222
1183
  locks of a validated run; runs after a failure too
1223
1184
 
@@ -3,16 +3,14 @@ from __future__ import annotations
3
3
 
4
4
  import hashlib
5
5
  import json
6
- import sys
7
6
  from pathlib import Path
8
7
 
9
8
  import okstra_vendor # noqa: F401 # registers vendored dependency aliases
10
9
  from jinja2 import Environment, StrictUndefined, select_autoescape
11
10
 
12
- from ..final_report_paths import final_report_data_path, translation_sidecar_path
11
+ from ..final_report_paths import final_report_data_path
13
12
  from ..final_report_schema import load_schema_for_data, validate
14
13
  from ..i18n import HTML_DICTIONARY_REL, load_dictionary, make_jinja_global
15
- from ..report_translation import overlay
16
14
  from ..report_view_artifacts import user_responses_dir_for_report
17
15
  from ..usage_cells import format_duration_ms
18
16
  from ..json_boundary import load_owned_object
@@ -76,44 +74,8 @@ def _report_meta(data: dict, run_meta: HtmlRunMeta) -> dict[str, object]:
76
74
  }
77
75
 
78
76
 
79
- def _localize(data: dict, data_path: Path) -> tuple[dict, str]:
80
- """이 리포트 언어의 번역 사이드카를 메모리 복사본에 덮어쓴다.
81
-
82
- data.json 은 영어 정본이고 고치지 않는다. 사람이 읽는 HTML 만 독자 언어다.
83
- 사이드카가 빠진 포인터는 영어로 남긴다.
84
-
85
- 사이드카 파일 자체가 없으면 요청 언어를 버리고 전부 영어로 렌더한다.
86
- 종전에는 여기서 렌더를 거부했다 — 크롬만 번역된 반쪽 페이지를 내보내지
87
- 않겠다는 이유였고, 그 판단 자체는 옳다. 틀린 것은 남은 선택지였다: 번역
88
- 워커가 못 도는 이유(호스트 승인 게이트, 공급자 부재)는 리포트의 품질과
89
- 무관한데, 거부는 열람본을 통째로 없애고 `validate-run` 이 그 부재를 차단
90
- 실패로 삼아 run 을 `contract-violated` 로 끝냈다 — 분석도 리포트도 끝난
91
- run 이 그렇게 끝났다(2026-09-08 FontsNinja/app/jobs dev-10784
92
- error-analysis). 언어를 낮추면 크롬과 본문이 같은 영어라 반쪽 페이지도
93
- 아니고, 독자는 읽을 것을 갖는다. 번역이 나중에 도착하면 다시 렌더해
94
- 덮어쓴다.
95
- """
96
- lang = str((data.get("meta") or {}).get("reportLanguage") or "en")
97
- if lang == "en":
98
- return data, lang
99
- sidecar_file = translation_sidecar_path(data_path, lang)
100
- if not sidecar_file.is_file():
101
- sys.stdout.write(
102
- f"no {lang} translation sidecar at {sidecar_file.name}; "
103
- "rendering the English source\n"
104
- )
105
- return data, "en"
106
- payload = load_owned_object(sidecar_file, artifact="translation sidecar")
107
- strings = payload.get("strings")
108
- if not isinstance(strings, dict):
109
- raise HtmlRenderError(f"translation sidecar has no 'strings' object: {sidecar_file}")
110
- localized, report = overlay(data, strings)
111
- sys.stdout.write(
112
- f"translated {report.applied} string(s) into {lang}"
113
- f" ({len(report.untranslated)} left in English"
114
- f", {len(report.unresolved)} unresolved)\n"
115
- )
116
- return localized, lang
77
+ def _report_language(data: dict) -> str:
78
+ return str((data.get("meta") or {}).get("reportLanguage") or "en")
117
79
 
118
80
 
119
81
  def _html_path(data_path: Path) -> Path:
@@ -140,9 +102,7 @@ def render_v2_html_view(
140
102
  errors = validate(data, load_schema_for_data(data))
141
103
  if errors:
142
104
  raise HtmlRenderError("invalid v2 final-report data: " + "; ".join(errors[:5]))
143
- # Validate the SSOT, then localize — the sidecar carries presentation and
144
- # has no say in whether the report is well-formed.
145
- data, lang = _localize(data, data_path)
105
+ lang = _report_language(data)
146
106
  route = resolve_html_route(run_meta.task_type)
147
107
  view = route.view_builder(data)
148
108
  root = _templates_root(templates_root)
@@ -6,9 +6,9 @@ the run manifest, which report assembly copies into `meta.reportLanguage` and
6
6
  the HTML renderer reads. When only the prompt called this, a project could ask
7
7
  for `ko`, get a Korean-instructed writer, and still ship an English report.
8
8
 
9
- The value is a language tag (`en`, `ko`, `fr`, `pt-BR`, …). The Phase 7
10
- translator receives that tag and translates into it; nothing here knows any
11
- particular language. Precedence: project config → global config → `en`.
9
+ The value is a language tag (`en`, `ko`, `fr`, `pt-BR`, …). The report writer
10
+ authors the report prose in that language; nothing here knows any particular
11
+ language. Precedence: project config → global config → `en`.
12
12
  """
13
13
  from __future__ import annotations
14
14