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
@@ -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
 
@@ -0,0 +1,153 @@
1
+ """Final-report leaf keys whose values a reader reads as prose and nothing else.
2
+
3
+ A value under one of these keys is displayed text; ids, enum values, paths and
4
+ anchor targets live under other keys.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ PROSE_KEYS = frozenset({
9
+ "acceptance",
10
+ "action",
11
+ "actions",
12
+ "addedWork",
13
+ "alternativesConsidered",
14
+ "answer",
15
+ "approach",
16
+ "approvalDisposition",
17
+ "approvalEvidence",
18
+ "architectureBoundaries",
19
+ "assumptions",
20
+ "behavior",
21
+ "blastRadius",
22
+ "blockReason",
23
+ "blockedReason",
24
+ "blockers",
25
+ "body",
26
+ "boundary",
27
+ "briefEvidence",
28
+ "canonical",
29
+ "carryIn",
30
+ "causeSummary",
31
+ "change",
32
+ "check",
33
+ "claim",
34
+ "commitment",
35
+ "complexity",
36
+ "condition",
37
+ "confirmingSignal",
38
+ "conformanceExemption",
39
+ "consequences",
40
+ "constraint",
41
+ "context",
42
+ "coreMechanism",
43
+ "coreReason",
44
+ "counterevidence",
45
+ "decision",
46
+ "decisions",
47
+ "declinedFixRecommendations",
48
+ "description",
49
+ "details",
50
+ "directionChange",
51
+ "disagreement",
52
+ "discrepancy",
53
+ "disproveWith",
54
+ "environmentSetup",
55
+ "escalationTrigger",
56
+ "evidence",
57
+ "evidenceRequired",
58
+ "exemptionReaffirm",
59
+ "exemptionReason",
60
+ "exitContract",
61
+ "exitContractSummary",
62
+ "expected",
63
+ "expectedBehaviorAfter",
64
+ "expectedChangeAreas",
65
+ "expectedForm",
66
+ "expectedOutcome",
67
+ "expectedResult",
68
+ "expectedVerification",
69
+ "explanation",
70
+ "fact",
71
+ "failureHandling",
72
+ "falsifyingEvidenceChecked",
73
+ "finalConclusion",
74
+ "goal",
75
+ "guardrails",
76
+ "headline",
77
+ "howToStart",
78
+ "hypothesis",
79
+ "ifStillOpen",
80
+ "injectedAs",
81
+ "interfaces",
82
+ "item",
83
+ "justification",
84
+ "knownFacts",
85
+ "label",
86
+ "mitigation",
87
+ "motivation",
88
+ "name",
89
+ "need",
90
+ "nextStep",
91
+ "noVariationRationale",
92
+ "notApplicableReason",
93
+ "note",
94
+ "observableFailure",
95
+ "observed",
96
+ "option",
97
+ "outcome",
98
+ "outputSummary",
99
+ "performed",
100
+ "planItem",
101
+ "position",
102
+ "problem",
103
+ "question",
104
+ "rationale",
105
+ "readyWhen",
106
+ "reason",
107
+ "rejectedSummary",
108
+ "rejectingSignal",
109
+ "relation",
110
+ "replacedInTest",
111
+ "replacedWith",
112
+ "requestKind",
113
+ "requiredDecision",
114
+ "requiredWork",
115
+ "requirement",
116
+ "resolution",
117
+ "responsibility",
118
+ "reversibility",
119
+ "risk",
120
+ "rolloutCost",
121
+ "routingRecommendation",
122
+ "satisfaction",
123
+ "scope",
124
+ "selfFixNote",
125
+ "sliceValue",
126
+ "stageTitle",
127
+ "stageValidation",
128
+ "statement",
129
+ "suggestedAction",
130
+ "summary",
131
+ "supersededStatement",
132
+ "symptom",
133
+ "systemInterpretation",
134
+ "testCaseBoundary",
135
+ "testCaseFailure",
136
+ "testCaseSuccess",
137
+ "testCoverageCost",
138
+ "text",
139
+ "title",
140
+ "topic",
141
+ "trigger",
142
+ "triggerSignal",
143
+ "unblockCondition",
144
+ "userChoice",
145
+ "userConstraints",
146
+ "userInput",
147
+ "userText",
148
+ "verification",
149
+ "verificationMethod",
150
+ "verificationSignal",
151
+ "whyItMatters",
152
+ "workingAssumption",
153
+ })
@@ -866,7 +866,7 @@ def _attempt_result_specs(
866
866
  for row in invocations_value
867
867
  if isinstance(row, Mapping) and _string(row.get("invocationRef"))
868
868
  }
869
- excluded_duties = {"lead", "report-writer", "translator"}
869
+ excluded_duties = {"lead", "report-writer"}
870
870
  specs: list[tuple[str, str, str, bool]] = []
871
871
  seen: set[str] = set()
872
872
  for attempt in attempts_value:
@@ -1896,19 +1896,12 @@ def _build_static_execution_manifest(
1896
1896
  plan: AssignmentPlan,
1897
1897
  context: AssignmentContext,
1898
1898
  lead: ResolvedAssignment,
1899
- translator: ResolvedAssignment,
1900
1899
  ) -> ExecutionManifest:
1901
1900
  participants: list[ParticipantAssignment] = []
1902
1901
  roles: list[RoleExecution] = []
1903
- # The translator is resolved outside the role plan, like the lead, and both
1904
- # are named in `invocationAssignments`. Without a role execution here,
1905
- # `agent-prompt materialize --audience translator` has no canonical
1906
- # identity to bind to and refuses — so a `reportLanguage: ko` run could
1907
- # never write its translation sidecar and rendered the English source.
1908
1902
  assignments = (
1909
1903
  lead,
1910
1904
  *(row for row in plan.assignments if row.role != "leader"),
1911
- translator,
1912
1905
  )
1913
1906
  for index, assignment in enumerate(assignments, start=1):
1914
1907
  participant_ref = f"participant-{index:03d}"
@@ -2387,7 +2380,6 @@ class _ModelBindings:
2387
2380
  lead_assignment: RoleAssignment
2388
2381
  worker_assignments: tuple[RoleAssignment, ...]
2389
2382
  invocation_assignments: dict[str, dict[str, object]]
2390
- translator: ResolvedAssignment
2391
2383
 
2392
2384
 
2393
2385
  @dataclass(frozen=True)
@@ -2622,13 +2614,6 @@ def _project_model_bindings(
2622
2614
  if executor_rows
2623
2615
  else None
2624
2616
  )
2625
- translator = _model_for_role(
2626
- context=context,
2627
- provider=report_writer.provider_id,
2628
- raw_value=report_writer.model_id,
2629
- role="translator",
2630
- duty_id="translator",
2631
- )
2632
2617
  projections = {
2633
2618
  provider: _legacy_projection_from_plan(provider, plan, context)
2634
2619
  for provider in ("claude", "codex", "antigravity")
@@ -2637,7 +2622,6 @@ def _project_model_bindings(
2637
2622
  lead_assignment,
2638
2623
  workers,
2639
2624
  critic_assignment,
2640
- _role_assignment(resolved=translator, role="translator"),
2641
2625
  inp.task_type,
2642
2626
  )
2643
2627
  worker_by_provider = {row.provider: row for row in workers}
@@ -2668,7 +2652,6 @@ def _project_model_bindings(
2668
2652
  lead_assignment=lead_assignment,
2669
2653
  worker_assignments=workers,
2670
2654
  invocation_assignments=invocation_assignments,
2671
- translator=translator,
2672
2655
  )
2673
2656
 
2674
2657
 
@@ -2759,7 +2742,6 @@ def _build_invocation_assignments(
2759
2742
  lead: RoleAssignment,
2760
2743
  workers: tuple[RoleAssignment, ...],
2761
2744
  critic: RoleAssignment | None,
2762
- translator: RoleAssignment,
2763
2745
  task_type: str,
2764
2746
  ) -> dict[str, dict[str, object]]:
2765
2747
  assignments = {"lead": lead.to_model_payload()}
@@ -2776,7 +2758,6 @@ def _build_invocation_assignments(
2776
2758
  # role 라벨을 만들기 때문에 두 행의 이름이 겹쳐 validate-run 이
2777
2759
  # `duplicate worker role detected` 로 런을 실패시킨다.
2778
2760
  assignments[critic_assignment_ref(task_type)] = critic.to_model_payload()
2779
- assignments["translator"] = translator.to_model_payload()
2780
2761
  return assignments
2781
2762
 
2782
2763
 
@@ -3660,7 +3641,7 @@ def _allowed_agent_audiences(
3660
3641
  inp: PrepareInputs,
3661
3642
  models: _ModelBindings,
3662
3643
  ) -> list[str]:
3663
- audiences = {"lead", "translator"}
3644
+ audiences = {"lead"}
3664
3645
  if models.worker_assignments:
3665
3646
  audiences.add("reverification-worker")
3666
3647
  if any(item.worker_id == "report-writer" for item in models.worker_assignments):
@@ -4162,7 +4143,6 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
4162
4143
  assignment_plan,
4163
4144
  assignment_context,
4164
4145
  models.lead,
4165
- models.translator,
4166
4146
  )
4167
4147
  dynamic_roles = tuple(
4168
4148
  requirement.role
@@ -283,8 +283,14 @@ def _teardown_all(project_id, task_group, task_id, task_wt: str,
283
283
 
284
284
  def _remove_clean_worktree(task_wt: str, wt: str, stage_n: int) -> tuple[str, str] | None:
285
285
  """clean stage worktree 를 물리 제거. dirty/실패면 (outcome, detail), 성공이면 None."""
286
- from .worktree import is_dirty_excluding_okstra, remove_worktree_force
287
-
286
+ from .worktree import (
287
+ is_dirty_excluding_okstra,
288
+ nested_worktree_excludes,
289
+ remove_worktree_force,
290
+ )
291
+
292
+ if nested_worktree_excludes(wt):
293
+ return ("skipped", "nested worktrees remain — preserved")
288
294
  if is_dirty_excluding_okstra(wt):
289
295
  return ("skipped", "dirty — 미커밋 변경 보존")
290
296
  rm = remove_worktree_force(task_wt, wt)
@@ -293,6 +299,35 @@ def _remove_clean_worktree(task_wt: str, wt: str, stage_n: int) -> tuple[str, st
293
299
  return None
294
300
 
295
301
 
302
+ def reclaim_completed_stage(
303
+ project_root: Path, project_id: str, task_group: str, task_id: str,
304
+ stage: int, expected_head: str, done: dict | None, status: str,
305
+ ) -> tuple[str, str]:
306
+ from . import worktree_registry
307
+ from .worktree import main_worktree_path
308
+
309
+ row = worktree_registry.get_stage_row(project_id, task_group, task_id, stage)
310
+ if not row:
311
+ return "skipped", "stage worktree is not registered"
312
+ if status != "done" or row.get("status") == "active":
313
+ return "skipped", "stage is unfinished or active"
314
+ if not expected_head or (done or {}).get("head_commit") != expected_head:
315
+ return "skipped", "completion commit does not match this report"
316
+ tip = _git(project_root, "rev-parse", "--verify", f"refs/heads/{row['branch']}")
317
+ if tip.returncode or tip.stdout.strip() != expected_head:
318
+ return "skipped", "stage branch moved after completion"
319
+ wt = Path(row["worktree_path"])
320
+ if wt.resolve() == main_worktree_path(project_root).resolve():
321
+ return "skipped", "main worktree is preserved"
322
+ if wt.exists():
323
+ current = _git(wt, "symbolic-ref", "--short", "HEAD")
324
+ if current.returncode or current.stdout.strip() != row["branch"]:
325
+ return "skipped", "worktree no longer holds the registered stage branch"
326
+ return _teardown_stage(
327
+ project_id, task_group, task_id, str(project_root), stage, row, expected_head,
328
+ )
329
+
330
+
296
331
  def _unmerged_branch_tip_warning(task_wt: str, stage_n: int, branch: str,
297
332
  verified_head: str = "") -> str:
298
333
  """Warn text when the stage branch tip is not an ancestor of the commit