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
@@ -13,7 +13,7 @@ Okstra tasks use one lead plus the exact worker assignments selected in the prep
13
13
 
14
14
  ### Role Definitions
15
15
 
16
- The start screen assigns a **model ref** (`provider/model`) to each **canonical role** slot: `leader`, `analyser`, `critic`, `designer`, `planner`, `implementer`, `verifier`, `report-writer`, `translator`. A provider name is the front of a model ref, not a role. Every `analyser` in the run shares an identical core responsibility. Specialization is additive — it lives in optional Section 7 of the worker output, NOT in differentiated core questions. Cross-verification only converges if every rostered analyser answers the same questions against the same brief.
16
+ The start screen assigns a **model ref** (`provider/model`) to each **canonical role** slot: `leader`, `analyser`, `critic`, `designer`, `planner`, `implementer`, `verifier`, `report-writer`. A provider name is the front of a model ref, not a role. Every `analyser` in the run shares an identical core responsibility. Specialization is additive — it lives in optional Section 7 of the worker output, NOT in differentiated core questions. Cross-verification only converges if every rostered analyser answers the same questions against the same brief.
17
17
 
18
18
  | Canonical role | Core responsibility | Notes |
19
19
  |------|------|------|
@@ -25,7 +25,6 @@ The start screen assigns a **model ref** (`provider/model`) to each **canonical
25
25
  | `implementer` | Sole change author for one approved stage | Used by `implementation` |
26
26
  | `verifier` | Independent review of the assigned duty's subject | Duty varies by task type |
27
27
  | `report-writer` | **Authors** the final-report file in Phase 6 | Excluded from Phase 4/5 and convergence |
28
- | `translator` | Translates the designated sidecar only | Phase 7 |
29
28
 
30
29
  **Dispatch does not invent a missing model assignment.** Launch-time empty slots are filled by the model default chain before the run starts. At dispatch the model for every role comes from `resultContract.workerRoles[*].modelExecutionValue` in `task-manifest.json` (and lead model metadata). There is no per-role hard-coded fallback — see "Model Assignment Rules" below.
31
30
 
@@ -569,7 +569,6 @@
569
569
  },
570
570
  "confirmation": {
571
571
  "header": "선택 확인:",
572
- "translation_scope": " 번역 작업자: {model} — 보고서 작성자와 같은 제공자·모델을 사용합니다. 선택 언어의 보고서가 필요하면 보고서 본문과 관련 분석·검증 결과를 전달해 번역하고 최종 보고서를 생성합니다. 같은 작업 범위의 재검증·수정·재시도에도 위에서 선택한 제공자·모델을 사용합니다.",
573
572
  "provider_data_scope": "\n전달 대상: 위 역할·모델 목록의 제공자(현재 세션 및 선택한 외부 모델 제공자).\n전달 자료: `{project_root}`의 이 작업에 대한 작업 개요, 작업 수행에 필요한 저장소 소스·문서, 선택한 근거 자료 및 실행 중 생성되는 관련 분석·검증 결과.\n진행을 선택하면 위 대상에 해당 자료를 전달하여 선택한 작업을 실행하는 것을 승인합니다. 선택에 없는 제공자나 작업과 무관한 자료는 승인 범위에 포함되지 않습니다. 실행 환경의 권한 검토는 별도로 적용됩니다.",
574
573
  "static_role": " static-role : {role}#{ordinal} / {model}",
575
574
  "dynamic_role": " dynamic-role : {role} / reuse selected participant model",
@@ -190,7 +190,7 @@ If preparation has already failed with a Git lock creation error or `Operation n
190
190
 
191
191
  ### Permission at the dispatch boundary
192
192
 
193
- Read `runManifest.userAuthorization` before a live dispatch, including `report-finalize`, which can start the translator internally. It preserves the confirmation text and the user response relayed through the wizard; quote its relevant recipient/material scope and response in the execution justification alongside the prepared assignment. The translator reuses the report writer's provider/model, as disclosed at confirmation. Do not wait until translation to inspect that authorization. A missing record is not consent, and the record does not override a host rejection. If review reports missing evidence already present in the record, submit that existing evidence through the permitted review mechanism; ask the user only when the actual approved scope does not cover the dispatch.
193
+ Read `runManifest.userAuthorization` before a live dispatch. It preserves the confirmation text and the user response relayed through the wizard; quote its relevant recipient/material scope and response in the execution justification alongside the prepared assignment. A missing record is not consent, and the record does not override a host rejection. If review reports missing evidence already present in the record, submit that existing evidence through the permitted review mechanism; ask the user only when the actual approved scope does not cover the dispatch.
194
194
 
195
195
  Before a live CLI worker dispatch, check the current host's declared sandbox and approval policy. In a restricted Codex session, request the host's supported execution permission on the dispatch command itself (`sandbox_permissions: "require_escalated"` for `exec_command` when that mechanism is available and permitted). Scope the request to the prepared run and explain that it starts the selected worker processes. Follow the host's approval decision; a wizard confirmation or a successful preflight does not grant this permission.
196
196
 
@@ -303,7 +303,7 @@ def create_provider() -> ProviderSpec:
303
303
  role: "gemini-3.1-pro"
304
304
  for role in (
305
305
  "lead", "analyser", "critic", "designer", "planner", "executor",
306
- "verifier", "report-writer", "translator",
306
+ "verifier", "report-writer",
307
307
  )
308
308
  },
309
309
  wrapper="okstra-antigravity-exec.sh",
@@ -168,7 +168,6 @@ def create_provider() -> ProviderSpec:
168
168
  "executor": "opus",
169
169
  "verifier": "opus",
170
170
  "report-writer": "sonnet",
171
- "translator": "sonnet",
172
171
  },
173
172
  wrapper="okstra-claude-exec.sh",
174
173
  supported_roles=ALL_ROLE_TOKENS,
@@ -133,7 +133,7 @@ def create_provider() -> ProviderSpec:
133
133
  models=CODEX,
134
134
  default_models={
135
135
  role: "gpt-6.1-sol"
136
- for role in ("lead", "analyser", "critic", "designer", "planner", "executor", "verifier", "report-writer", "translator")
136
+ for role in ("lead", "analyser", "critic", "designer", "planner", "executor", "verifier", "report-writer")
137
137
  },
138
138
  wrapper="okstra-codex-exec.sh",
139
139
  supported_roles=ALL_ROLE_TOKENS,
@@ -308,7 +308,6 @@ def create_provider() -> ProviderSpec:
308
308
  "executor",
309
309
  "verifier",
310
310
  "report-writer",
311
- "translator",
312
311
  )
313
312
  },
314
313
  wrapper="okstra-grok-exec.sh",
@@ -104,7 +104,6 @@ def create_provider() -> ProviderSpec:
104
104
  "executor": "kimi-k3",
105
105
  "verifier": "kimi-k3",
106
106
  "report-writer": "kimi-k3",
107
- "translator": "kimi-k3",
108
107
  },
109
108
  wrapper="okstra-kimi-exec.sh",
110
109
  supported_roles=ALL_ROLE_TOKENS,
@@ -39,7 +39,6 @@ AgentAudience = Literal[
39
39
  "scope-critic",
40
40
  "acceptance-critic",
41
41
  "report-writer",
42
- "translator",
43
42
  "code-reviewer",
44
43
  "schedule-verifier",
45
44
  "business-flow-investigator",
@@ -604,7 +603,7 @@ def load_role_duty_contract(duty_root: Path, audience: AgentAudience) -> DutyCon
604
603
  def digest_duty_catalog(duty_root: Path) -> str:
605
604
  """Return the canonical digest of every contract file in a snapshot.
606
605
 
607
- 역할 계약은 하위 `roles/` 에 둔다 — `report-writer` 와 `translator` 는 직무
606
+ 역할 계약은 하위 `roles/` 에 둔다 — `report-writer` 는 직무
608
607
  id 이면서 역할 id 라, 한 폴더에 두면 두 계약이 같은 파일 이름을 두고 겹친다.
609
608
  """
610
609
  names = [
@@ -114,8 +114,8 @@ def _add_materialize_parser(commands: argparse._SubParsersAction) -> None:
114
114
  )
115
115
  materialize.add_argument(
116
116
  "--audit-source",
117
- help=run_only + "; accepted only for report-writer and translator, whose "
118
- "result is not their own worker result",
117
+ help=run_only + "; accepted only for report-writer, whose result is "
118
+ "not its own worker result",
119
119
  )
120
120
  materialize.add_argument("--host-runtime", help=standalone_only)
121
121
  materialize.add_argument("--execution-runner", choices=("cli-wrapper",), help=standalone_only)
@@ -81,8 +81,6 @@ def _metadata_job(
81
81
  if metadata.get("executionIdentityVersion") != 2 or metadata["dispatchKind"] != dispatch_kind:
82
82
  raise AgentPromptCliError(f"{metadata_path}: expected v2 dispatchKind {dispatch_kind!r}")
83
83
  execution = next(row for row in manifest["roleExecutions"] if row["roleExecutionRef"] == metadata["roleExecutionRef"])
84
- if execution["role"] == "translator":
85
- raise AgentPromptCliError("translator uses canonical reservation dispatch: worker-dispatch --workers translator")
86
84
  prompt = _project_input(project, metadata["prompt"]["path"], "prompt")
87
85
  anchors = prompt_anchor_values(prompt, ("Result Path", "Worker Result Path"))
88
86
  result = anchors.get("Worker Result Path") or anchors.get("Result Path")
@@ -99,7 +99,7 @@ _AUDIENCES = frozenset(get_args(AgentAudience))
99
99
  # 결과 파일과 별개의 워커 결과 포인터를 가진 audience. 이들만 `--audit-source` 로
100
100
  # 두 번째 경로를 받고, 프롬프트에 `**Worker Result Path:**` 앵커가 생긴다.
101
101
  # 나머지 audience 의 결과는 하나뿐이고 감사 사이드카는 그 이름에서 파생된다.
102
- _POINTER_AUDIENCES = frozenset({"report-writer", "translator"})
102
+ _POINTER_AUDIENCES = frozenset({"report-writer"})
103
103
 
104
104
 
105
105
  def _materialize(args: argparse.Namespace) -> PreparedAgentInvocation:
@@ -196,7 +196,7 @@ def _materialize_run(
196
196
  "the same path in the jobs file's workerResultPath"
197
197
  )
198
198
  audit_source_path = result_path
199
- elif args.audience == "report-writer":
199
+ else:
200
200
  if audit_source_path is None:
201
201
  # The report writer is the one audience whose result path is not its
202
202
  # own worker result: it writes the report body, while the audit
@@ -234,31 +234,6 @@ def _materialize_run(
234
234
  result_path=result_path,
235
235
  audit_source_path=audit_source_path,
236
236
  )
237
- elif (
238
- audit_source_path is None
239
- or _normalized(audit_source_path) == _normalized(result_path)
240
- ):
241
- # translator 다. report-writer 와 같은 두-경로 audience 인데 종전엔 빠진
242
- # `--audit-source` 를 `--result` 로 조용히 접었다. 그러면 프롬프트에
243
- # `**Worker Result Path:**` 앵커가 안 생기는데, `worker-dispatch` 는 그
244
- # 앵커가 `**Result Path:**` 와 다를 것을 요구하므로 발행은 되지만 어떤
245
- # 디스패치도 받지 못하는 예약이 남는다. 예약은 회수 명령이 없어 리드가
246
- # 두 번째 id 로 다시 만드는 순간 "exactly one reservation" 이 영영
247
- # 깨졌다(2026-09-08 실측, fontsninja-v3-site dev-10627-2
248
- # requirements-discovery). 발행 시점에 거절한다.
249
- raise AgentPromptCliError(
250
- "translator materialization requires --audit-source distinct from "
251
- "--result: --result is the translator's own report "
252
- "(e.g. worker-results/translator-translations-<task-type>-<seq>.md) "
253
- "and --audit-source the worker-result .md the audit sidecar is "
254
- "derived from (worker-results/translator-worker-<task-type>-<seq>.md). "
255
- "`worker-dispatch --workers translator` reads both from the prompt's "
256
- "**Result Path:** and **Worker Result Path:** anchors and refuses a "
257
- "prompt without the second. To fix a prompt already published "
258
- "without it, re-run this command with the same --invocation-id and "
259
- "--prompt plus --replace-undispatched; a new invocation id would "
260
- "leave two reservations and block the translator for this run"
261
- )
262
237
  _validate_run_identity(
263
238
  manifest,
264
239
  worker_id=args.worker_id,
@@ -29,10 +29,6 @@ def _validate_run_identity(
29
29
  expected = "lead"
30
30
  if worker_id != "lead":
31
31
  raise AgentPromptCliError("lead assignment requires worker ID 'lead'")
32
- elif assignment_ref == "translator":
33
- expected = "translator"
34
- if worker_id != "translator":
35
- raise AgentPromptCliError("translator assignment requires worker ID 'translator'")
36
32
  elif assignment_ref.startswith("critic/"):
37
33
  scope = assignment_ref.split("/", 1)[1]
38
34
  expected = CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT.get(scope, "")
@@ -674,7 +674,7 @@ def _dispatch_cli_wrapper_batch(
674
674
  # (`_persisted_write_contract`) attempt 2 의 정책 digest 는 attempt 1 과
675
675
  # 다른데, 첫 라운드의 baseline 을 그대로 쓰면 마감의
676
676
  # `_validate_snapshot_authority` 가 `policyDigests` 불일치로 죽고 attempt-N
677
- # 사이드카는 끝내 안 써진다(실측 2026-09-08, translator attempt 2:
677
+ # 사이드카는 끝내 안 써진다(실측 2026-09-08, 워커 attempt 2:
678
678
  # 산출물 완전·exit 0 인데 dispatcher 예외). 앞 라운드에서 완주한 동료의
679
679
  # 변경은 그 attempt 의 감사가 이미 판정했으므로 새 baseline 에 흡수돼도
680
680
  # 맞다 — 재시도는 `failed-no-mutation` 뒤에만 오므로(`retry_allowed`)
@@ -920,15 +920,6 @@ def _jobs_from_roster(
920
920
  options,
921
921
  team_state=team_state,
922
922
  )
923
- if selected == ["translator"]:
924
- return [_translator_job_from_reservation(
925
- project_root,
926
- workspace_root,
927
- manifest,
928
- team_state,
929
- active_context,
930
- options,
931
- )]
932
923
  _mark_roster_skips(project_root, manifest, team_state, selected, requested_workers, options)
933
924
  worker_facts, prompt_paths = _materialize_roster_prompts(
934
925
  project_root,
@@ -962,29 +953,6 @@ def _select_workers(
962
953
  *,
963
954
  team_state: Mapping[str, Any] | None = None,
964
955
  ) -> list[str]:
965
- # 번역자 디스패치는 roster 를 읽지 않는다 — 근거는 prepare 가 심는 canonical
966
- # translator role execution 이다. 그래서 빈 roster 검사보다 앞에 둔다.
967
- # 종전에는 뒤에 있어서, roster 가 비는 phase 에서 이 분기에 닿을 수 없었다:
968
- # `release-handoff` 는 의도적으로 single-lead 라 `_resolve_roster` 가 빈
969
- # 목록을 강제하는데(`run.py`), `reportLanguage` 가 en 이 아니면 render-views 는
970
- # 번역 사이드카를 요구한다. 두 요구가 겹치면 예약 경로는 "run manifest has no
971
- # recommendedWorkers entries" 로, jobs-file 경로는 "jobs file translator must
972
- # use canonical reservation dispatch" 로 막혀 번역자를 띄울 방법이 없었다
973
- # (실측 2026-09-07, fontsninja-v3-site dev-10626 release-handoff 001 — 리드가
974
- # `okstra report-translate write` 로 우회했고, 그 run 의 매니페스트에는
975
- # translator role execution 과 invocationAssignments.translator 가 둘 다 있다).
976
- if tuple(requested_workers) == ("translator",):
977
- provider = _canonical_translator_provider(manifest)
978
- if provider is None:
979
- raise DispatchError(
980
- "translator dispatch requires a canonical translator role execution"
981
- )
982
- if provider not in options.supported_worker_wrappers:
983
- raise DispatchError(
984
- f"unsupported {options.unsupported_worker_label} translator provider: "
985
- f"{provider}"
986
- )
987
- return ["translator"]
988
956
  recommended = _string_list(manifest.get("recommendedWorkers"))
989
957
  if not recommended:
990
958
  raise DispatchError("run manifest has no recommendedWorkers entries")
@@ -1027,270 +995,6 @@ def _select_workers(
1027
995
  return selected
1028
996
 
1029
997
 
1030
- def _canonical_translator_provider(manifest: Mapping[str, Any]) -> str | None:
1031
- execution = _canonical_translator_execution(manifest)
1032
- return _string_value(execution.get("provider")) if execution else None
1033
-
1034
-
1035
- def _canonical_translator_execution(
1036
- manifest: Mapping[str, Any],
1037
- ) -> Mapping[str, Any] | None:
1038
- assignments = manifest.get("invocationAssignments")
1039
- assignment = (
1040
- assignments.get("translator") if isinstance(assignments, Mapping) else None
1041
- )
1042
- if not isinstance(assignment, Mapping):
1043
- return None
1044
- provider = _string_value(assignment.get("provider"))
1045
- model_execution = _string_value(assignment.get("modelExecutionValue"))
1046
- runner = _string_value(assignment.get("runner"))
1047
- if not provider or not model_execution or runner != BACKEND_CLI_WRAPPER:
1048
- return None
1049
- role_executions = manifest.get("roleExecutions")
1050
- matches = [
1051
- row
1052
- for row in role_executions
1053
- if isinstance(row, Mapping)
1054
- and row.get("role") == "translator"
1055
- and row.get("provider") == provider
1056
- and isinstance(row.get("binding"), Mapping)
1057
- and row["binding"].get("runner") == runner
1058
- and row["binding"].get("resolvedExecutionValue") == model_execution
1059
- ] if isinstance(role_executions, list) else []
1060
- return matches[0] if len(matches) == 1 else None
1061
-
1062
-
1063
- _TRANSLATOR_RESERVATION_KEYS = {
1064
- "schemaVersion", "executionIdentityVersion", "invocationId",
1065
- "assignmentRef", "audience", "dispatchKind", "promptPath",
1066
- "metadataPath", "invocationRef", "participantRef",
1067
- "roleExecutionRef", "dutyId", "attempt",
1068
- }
1069
-
1070
-
1071
- def _translator_job_from_reservation(
1072
- project_root: Path,
1073
- workspace_root: Path,
1074
- manifest: Mapping[str, Any],
1075
- team_state: Mapping[str, Any],
1076
- active_context: Mapping[str, Any],
1077
- options: _BuildOptions,
1078
- ) -> WorkerJob:
1079
- execution = _canonical_translator_execution(manifest)
1080
- if execution is None:
1081
- raise DispatchError(
1082
- "translator dispatch requires a canonical translator role execution"
1083
- )
1084
- candidates, seen = translator_reservations(project_root, manifest, team_state)
1085
- run_manifest_rel = _string_value(manifest.get("runManifestPath"))
1086
- if len(candidates) != 1:
1087
- run_label = run_manifest_rel or "run manifest path unknown"
1088
- raise DispatchError(
1089
- "translator dispatch requires exactly one canonical invocation "
1090
- f"reservation for this run ({run_label}); found {len(candidates)}. "
1091
- "Translator reservations seen: " + ("; ".join(seen) if seen else "none")
1092
- )
1093
- reservation = candidates[0]
1094
- prompt_path = _resolve_project_path(
1095
- project_root, _require_string(reservation, "promptPath")
1096
- )
1097
- metadata_path = _resolve_project_path(
1098
- project_root, _require_string(reservation, "metadataPath")
1099
- )
1100
- expected_metadata = prompt_path.with_name(prompt_path.name + ".meta.json")
1101
- if metadata_path != expected_metadata:
1102
- raise DispatchError("translator invocation metadata path is not prompt-adjacent")
1103
- metadata = _load_json_object(metadata_path, "agent invocation metadata")
1104
- invocation = _invocation_job_fields(metadata, prompt_path, manifest)
1105
- expected = {
1106
- "invocationId": invocation["invocation_id"],
1107
- "invocationRef": invocation["invocation_ref"],
1108
- "participantRef": invocation["participant_ref"],
1109
- "roleExecutionRef": invocation["role_execution_ref"],
1110
- "dutyId": invocation["duty_id"],
1111
- "attempt": invocation["attempt"],
1112
- }
1113
- if any(reservation.get(key) != value for key, value in expected.items()):
1114
- raise DispatchError("translator reservation and invocation metadata differ")
1115
- if (
1116
- invocation["audience"] != "translator"
1117
- or invocation["assignment_ref"] != "translator"
1118
- or metadata.get("dispatchKind") != "translator"
1119
- ):
1120
- raise DispatchError("translator invocation identity is invalid")
1121
- result_path, worker_result_path = _translator_prompt_outputs(
1122
- project_root, prompt_path
1123
- )
1124
- if result_path == worker_result_path:
1125
- raise DispatchError(
1126
- "translator result and standard worker result paths must differ"
1127
- )
1128
- provider = _require_string(execution, "provider")
1129
- binding = execution.get("binding")
1130
- if not isinstance(binding, Mapping):
1131
- raise DispatchError("translator role execution has no binding")
1132
- return WorkerJob(
1133
- worker_id="translator",
1134
- provider=provider,
1135
- backend=options.default_backend,
1136
- project_root=project_root,
1137
- model_execution_value=_require_string(binding, "resolvedExecutionValue"),
1138
- wrapper_path=_resolve_wrapper(provider, workspace_root, options),
1139
- prompt_path=prompt_path,
1140
- result_path=result_path,
1141
- worker_result_path=worker_result_path,
1142
- completion_paths=(result_path, worker_result_path),
1143
- worktree_path=_worktree_path(manifest, active_context),
1144
- role="translator",
1145
- idle_timeout_seconds=options.idle_timeout_seconds,
1146
- dispatch_kind="translator",
1147
- **invocation,
1148
- )
1149
-
1150
-
1151
- def translator_reservations(
1152
- project_root: Path,
1153
- manifest: Mapping[str, Any],
1154
- team_state: Mapping[str, Any],
1155
- ) -> tuple[list[Mapping[str, Any]], list[str]]:
1156
- """이 run 이 지금 디스패치할 수 있는 translator 예약과, 본 예약 전부의 요약.
1157
-
1158
- 두 소비자가 같은 답을 읽는다: `_translator_job_from_reservation` 은 그 하나를
1159
- 골라 띄우고, `report-finalize` 의 `translate` 단계
1160
- (`okstra_ctl.report_translation_dispatch`) 는 0건이면 예약을 만든다. 후보를
1161
- 세는 규칙이 두 곳이면 한쪽만 고쳐져 "만들었는데 못 고른다" 가 나온다.
1162
- """
1163
- execution = _canonical_translator_execution(manifest)
1164
- if execution is None:
1165
- raise DispatchError(
1166
- "translator dispatch requires a canonical translator role execution"
1167
- )
1168
- contract = manifest.get("agentContract")
1169
- if not isinstance(contract, Mapping):
1170
- raise DispatchError("translator dispatch requires an agent contract")
1171
- reservation_root = _resolve_required_path(
1172
- project_root, contract, "invocationReservationRootPath"
1173
- )
1174
- dispatched_ids = {
1175
- str(row.get("dispatchId") or "")
1176
- for collection in (
1177
- team_state.get("workerDispatches") or [],
1178
- team_state.get("agentDispatches") or [],
1179
- )
1180
- for row in collection
1181
- if isinstance(row, Mapping)
1182
- }
1183
- return _translator_reservation_candidates(
1184
- project_root,
1185
- reservation_root,
1186
- execution=execution,
1187
- dispatched_ids=dispatched_ids,
1188
- run_manifest_rel=_string_value(manifest.get("runManifestPath")),
1189
- )
1190
-
1191
-
1192
- def _translator_reservation_candidates(
1193
- project_root: Path,
1194
- reservation_root: Path,
1195
- *,
1196
- execution: Mapping[str, Any],
1197
- dispatched_ids: set[str],
1198
- run_manifest_rel: str,
1199
- ) -> tuple[list[Mapping[str, Any]], list[str]]:
1200
- """이 run 이 디스패치할 수 있는 translator 예약과, 본 예약 전부의 요약.
1201
-
1202
- 예약 디렉터리(`prompts/.agent-invocations/`)는 run 이 아니라 task type 단위라
1203
- 같은 task 의 이전 run 이 남긴 translator 예약이 함께 놓인다. translator 는
1204
- `assignmentRef` 가 접미 없는 `translator` 하나이고, 이전 run 의 디스패치는
1205
- 이번 run 의 team-state 에 없으므로 "아직 디스패치 안 됨" 으로 읽혀 후보가
1206
- 둘이 됐다 — 두 번째 run 부터 번역이 영영 막혔다(2026-09-03 실측,
1207
- fontsninja-v3-site dev-10626-1 r02·r04). 예약 자체는 run 을 모르지만 그 옆의
1208
- `.meta.json` 은 `contractSource.runManifestPath` 로 자기 run 을 안다. 그
1209
- 값을 이번 run 의 `runManifestPath` 와 대조한다. 매니페스트가 그 경로를 안
1210
- 가진 레거시 run 은 종전대로 run 을 가리지 않는다.
1211
- """
1212
- candidates: list[Mapping[str, Any]] = []
1213
- seen: list[str] = []
1214
- for path in sorted(reservation_root.glob("*.json")):
1215
- reservation = _load_json_object(path, "agent invocation reservation")
1216
- if not (
1217
- set(reservation) == _TRANSLATOR_RESERVATION_KEYS
1218
- and reservation.get("schemaVersion") == "2.0"
1219
- and reservation.get("executionIdentityVersion") == 2
1220
- and reservation.get("assignmentRef") == "translator"
1221
- and reservation.get("audience") == "translator"
1222
- and reservation.get("dispatchKind") == "translator"
1223
- and reservation.get("dutyId") == "translator"
1224
- ):
1225
- continue
1226
- invocation_id = str(reservation.get("invocationId") or "")
1227
- reservation_run = _reservation_run_manifest_rel(project_root, reservation)
1228
- dispatched = _build_dispatch_id(
1229
- invocation_id, int(reservation.get("attempt") or 0),
1230
- ) in dispatched_ids
1231
- seen.append(
1232
- f"{invocation_id} (run {reservation_run or 'unknown'}"
1233
- + (", dispatched" if dispatched else "")
1234
- + ")"
1235
- )
1236
- if dispatched:
1237
- continue
1238
- if (
1239
- reservation.get("participantRef") != execution.get("participantRef")
1240
- or reservation.get("roleExecutionRef") != execution.get("roleExecutionRef")
1241
- ):
1242
- continue
1243
- if run_manifest_rel and (
1244
- reservation_run is None
1245
- or os.path.normpath(reservation_run) != os.path.normpath(run_manifest_rel)
1246
- ):
1247
- continue
1248
- candidates.append(reservation)
1249
- return candidates, seen
1250
-
1251
-
1252
- def _reservation_run_manifest_rel(
1253
- project_root: Path, reservation: Mapping[str, Any],
1254
- ) -> str | None:
1255
- """예약 옆 `.meta.json` 의 `contractSource.runManifestPath`. 못 읽으면 None."""
1256
- metadata_value = _string_value(reservation.get("metadataPath"))
1257
- if not metadata_value:
1258
- return None
1259
- try:
1260
- metadata = _load_json_object(
1261
- _resolve_project_path(project_root, metadata_value),
1262
- "agent invocation metadata",
1263
- )
1264
- except DispatchError:
1265
- return None
1266
- source = metadata.get("contractSource")
1267
- if not isinstance(source, Mapping):
1268
- return None
1269
- return _string_value(source.get("runManifestPath")) or None
1270
-
1271
-
1272
- def _translator_prompt_outputs(
1273
- project_root: Path, prompt_path: Path,
1274
- ) -> tuple[Path, Path]:
1275
- try:
1276
- lines = prompt_path.read_text(encoding="utf-8").splitlines()
1277
- except OSError as exc:
1278
- raise DispatchError(f"cannot read translator prompt: {exc}") from exc
1279
- values: list[str] = []
1280
- for header in ("Result Path", "Worker Result Path"):
1281
- prefix = f"**{header}:** "
1282
- matches = [line[len(prefix):].strip() for line in lines if line.startswith(prefix)]
1283
- if len(matches) != 1 or not matches[0]:
1284
- raise DispatchError(
1285
- f"translator prompt requires exactly one non-empty {prefix.strip()} header"
1286
- )
1287
- values.append(matches[0])
1288
- return (
1289
- _resolve_project_path(project_root, values[0]),
1290
- _resolve_project_path(project_root, values[1]),
1291
- )
1292
-
1293
-
1294
998
  def _select_cli_wrapper_assignments(
1295
999
  recommended: Sequence[str],
1296
1000
  requested_workers: Sequence[str],
@@ -2878,27 +2582,6 @@ def _wrapper_log_tail(job: WorkerJob) -> str | None:
2878
2582
  return tail.decode("utf-8", errors="replace").strip() or None
2879
2583
 
2880
2584
 
2881
- def _projectionless_translator(plan: DispatchPlan, job: WorkerJob) -> bool:
2882
- if not (
2883
- job.has_execution_identity
2884
- and job.role == "translator"
2885
- and job.worker_id == "translator"
2886
- and job.audience == "translator"
2887
- and job.assignment_ref == "translator"
2888
- and job.duty_id == "translator"
2889
- and job.dispatch_kind == "translator"
2890
- ):
2891
- return False
2892
- execution = _canonical_translator_execution(plan.manifest)
2893
- return bool(
2894
- execution
2895
- and execution.get("roleExecutionRef") == job.role_execution_ref
2896
- and execution.get("participantRef") == job.participant_ref
2897
- and execution.get("executionLabel") == job.execution_label
2898
- and execution.get("provider") == job.provider
2899
- )
2900
-
2901
-
2902
2585
  def _transition_job_status(
2903
2586
  plan: DispatchPlan,
2904
2587
  job: WorkerJob,
@@ -2907,24 +2590,6 @@ def _transition_job_status(
2907
2590
  *,
2908
2591
  model_execution_value: str = "",
2909
2592
  ) -> None:
2910
- if not _projectionless_translator(plan, job):
2911
- _transition_worker_status(
2912
- plan.team_state_path,
2913
- job.worker_id,
2914
- status,
2915
- reason,
2916
- model_execution_value=model_execution_value,
2917
- )
2918
- return
2919
- team_state = _load_json_object(plan.team_state_path, "team-state")
2920
- workers = team_state.get("workers")
2921
- if not isinstance(workers, list):
2922
- raise DispatchError("team-state workers must be an array")
2923
- if not any(
2924
- isinstance(row, Mapping) and row.get("workerId") == job.worker_id
2925
- for row in workers
2926
- ):
2927
- return
2928
2593
  _transition_worker_status(
2929
2594
  plan.team_state_path,
2930
2595
  job.worker_id,
@@ -2945,20 +2610,9 @@ def _job_terminal_status(
2945
2610
  row for row in workers
2946
2611
  if isinstance(row, Mapping) and row.get("workerId") == job.worker_id
2947
2612
  ]
2948
- if len(matches) == 1:
2949
- return _string_value(matches[0].get("status"))
2950
- if matches or not _projectionless_translator(plan, job):
2613
+ if len(matches) != 1:
2951
2614
  raise DispatchError(f"team-state has no unique workerId={job.worker_id}")
2952
- dispatches = team_state.get("workerDispatches")
2953
- records = [
2954
- row for row in dispatches
2955
- if isinstance(row, Mapping)
2956
- and row.get("promptPath") == str(job.prompt_path)
2957
- and row.get("attempt") == attempt
2958
- ] if isinstance(dispatches, list) else []
2959
- if len(records) != 1:
2960
- raise DispatchError("translator dispatch has no unique status record")
2961
- return _string_value(records[0].get("status"))
2615
+ return _string_value(matches[0].get("status"))
2962
2616
 
2963
2617
 
2964
2618
  def _record_dispatch(
@@ -2221,19 +2221,6 @@ def _validate_v2_jobs_file_authority(
2221
2221
  )
2222
2222
  if errors:
2223
2223
  raise DispatchError("jobs file v2 authority: " + "; ".join(errors))
2224
- role_executions = {
2225
- string_value(row.get("roleExecutionRef")): row
2226
- for row in manifest.get("roleExecutions") or []
2227
- if isinstance(row, Mapping)
2228
- }
2229
- if any(
2230
- isinstance(role_executions.get(job.role_execution_ref), Mapping)
2231
- and role_executions[job.role_execution_ref].get("role") == "translator"
2232
- for job in jobs
2233
- ):
2234
- raise DispatchError(
2235
- "jobs file translator must use canonical reservation dispatch"
2236
- )
2237
2224
 
2238
2225
 
2239
2226
  def worker_jobs_from_file(
@@ -275,6 +275,6 @@ def _legacy_execution_capabilities(roles: frozenset[str]) -> frozenset[str]:
275
275
  capabilities.add("source-readonly")
276
276
  if role == "implementer":
277
277
  capabilities.add("project-mutation")
278
- if role in {"report-writer", "translator"}:
278
+ if role == "report-writer":
279
279
  capabilities.add("extended-artifact-authoring")
280
280
  return frozenset(capabilities)
@@ -35,11 +35,6 @@ ROLE_DEFINITIONS = (
35
35
  (),
36
36
  _READONLY_CAPABILITIES | frozenset({"extended-artifact-authoring"}),
37
37
  ),
38
- RoleDefinition(
39
- "translator",
40
- (),
41
- _READONLY_CAPABILITIES | frozenset({"extended-artifact-authoring"}),
42
- ),
43
38
  )
44
39
 
45
40
  DUTY_ROLE_IDS = {
@@ -61,7 +56,6 @@ DUTY_ROLE_IDS = {
61
56
  "schedule-verifier": "verifier",
62
57
  "business-flow-investigator": "analyser",
63
58
  "report-writer": "report-writer",
64
- "translator": "translator",
65
59
  }
66
60
 
67
61