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
@@ -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
 
@@ -6,7 +6,6 @@ from pathlib import Path
6
6
 
7
7
  DATA_JSON_SUFFIX = ".data.json"
8
8
  MARKDOWN_SUFFIX = ".md"
9
- TRANSLATION_SOURCE_SUFFIX = ".translation-source.json"
10
9
 
11
10
 
12
11
  def _stem(data_path: Path) -> str:
@@ -106,21 +105,6 @@ def timeline_report_record_rel(run: dict) -> str:
106
105
  return report_record_rel_from_legacy_pointer(str(run.get("reportPath") or ""))
107
106
 
108
107
 
109
- def translation_source_path(data_path: Path) -> Path:
110
- """Return the translator's work list for a final-report data.json path."""
111
- return data_path.with_name(_stem(data_path) + TRANSLATION_SOURCE_SUFFIX)
112
-
113
-
114
- def translation_sidecar_path(data_path: Path, lang: str) -> Path:
115
- """Return the filled translation sidecar the HTML renderer overlays.
116
-
117
- One file per language beside the report it translates, so the renderer
118
- finds it from the data.json alone and a run for another language does not
119
- overwrite it.
120
- """
121
- return data_path.with_name(f"{_stem(data_path)}.i18n.{lang}.json")
122
-
123
-
124
108
  def sidecar_source_rel(path: Path) -> str:
125
109
  """사이드카 프론트매터가 `source-report` / `source-data` 에 적는 경로.
126
110
 
@@ -47,8 +47,8 @@ def load_dictionary(lang: str, rel: tuple[str, ...] = DICTIONARY_REL) -> dict[st
47
47
  """`<lang>.json` 이 있으면 그것, 없으면 영어 사전.
48
48
 
49
49
  사전은 페이지 크롬(표 머리글·빈 상태·enum 라벨)의 고정 문자열이고 본문은
50
- 번역 사이드카가 덮는다. 사전이 없는 언어의 리포트는 본문은 그 언어, 크롬은
51
- 영어로 나온다 — 사전이 없다고 렌더를 거절하면 독자는 아무것도 못 받는다.
50
+ report-writer 가 리포트 언어로 쓴다. 사전이 없는 언어의 리포트는 본문은 그
51
+ 언어, 크롬은 영어로 나온다 — 사전이 없다고 렌더를 거절하면 독자는 아무것도 못 받는다.
52
52
  """
53
53
  directory = _i18n_dir(rel)
54
54
  path = directory / f"{lang}.json"
@@ -119,7 +119,6 @@ def hydrate_active_run_context(payload: Mapping[str, Any]) -> dict[str, Any]:
119
119
  "IMPLEMENTATION_WORKER_PREAMBLE_PATH", ""
120
120
  ),
121
121
  "report-writer": ctx.get("REPORT_WRITER_PREAMBLE_PATH", ""),
122
- "translator": ctx.get("TRANSLATOR_PREAMBLE_PATH", ""),
123
122
  },
124
123
  "workerErrorContractPath": ctx.get("WORKER_ERROR_CONTRACT_PATH", ""),
125
124
  "hostSessionContractPath": (
@@ -515,9 +514,6 @@ def _runtime_fields(paths: Mapping[str, Any]) -> dict[str, str]:
515
514
  "REPORT_WRITER_PREAMBLE_PATH": str(
516
515
  runtime_home / "templates" / "report-writer-prompt-preamble.md"
517
516
  ),
518
- "TRANSLATOR_PREAMBLE_PATH": str(
519
- runtime_home / "templates" / "translator-prompt-preamble.md"
520
- ),
521
517
  "WORKER_ERROR_CONTRACT_PATH": str(
522
518
  runtime_home / "templates" / "worker-error-contract.md"
523
519
  ),
@@ -663,9 +663,6 @@ def compute_run_paths(
663
663
  "REPORT_WRITER_PREAMBLE_PATH": str(
664
664
  runtime_home / "templates" / "report-writer-prompt-preamble.md"
665
665
  ),
666
- "TRANSLATOR_PREAMBLE_PATH": str(
667
- runtime_home / "templates" / "translator-prompt-preamble.md"
668
- ),
669
666
  "WORKER_ERROR_CONTRACT_PATH": str(
670
667
  runtime_home / "templates" / "worker-error-contract.md"
671
668
  ),
@@ -12,6 +12,7 @@
12
12
  - DB / IO / SQL real-execution evidence — trigger: the diff touches DB/IO/SQL (ORM / query-builder, `*.repository.*`, model / `migrations/**` / `*.sql`, or changed query strings). Then Validation Evidence MUST cite a real (or faithful-replica) DB execution — the `db-test` command + exit code — not a mock-only suite. Rationale: a mock-only suite cannot observe the SQL actually emitted (observed failure class: `prompts/profiles/_implementation-verifier.md` §"DB / IO / SQL change — real-execution gate"). A DB-touching change whose only evidence is mocked, or for which no `db-test` ran, is an **Acceptance Blocker** (`major`+; per the Verdict vocabulary below, any blocker moves the verdict off `accepted`). This gate stops an unverified DB change from reaching `release-handoff` and being pushed.
13
13
  - **External Tier 3 de-duplication exception.** A DB/IO/SQL surface covered by an in-scope Tier 3 entry whose `requires` include `db`, `http`, or `external` is governed by the External QA outcome policy. Its non-PASS or unavailable result MUST NOT generate a second legacy db-test-not-configured or mock-only blocker solely for that same Tier 3 non-PASS or unavailable result. Tier 1 or Tier 2 failures remain blocking, and DB surfaces without declared external Tier 3 coverage remain blocking.
14
14
  - no new defects introduced — the diff does not break previously-working behaviour and adds no new bug (logic/off-by-one, null/empty handling, resource leaks, broken error paths)
15
+ - no security defect on input from outside the process — for every changed file that reads a request body, query, header, upload, message or webhook, or acts on its behalf: work that grows faster than the input (pairwise uniqueness, nested scans, a backtracking regex, input-driven recursion) runs only after a size/count bound has rejected oversized input — check the validation framework's configuration, since class-validator without `stopAtFirstError` still runs `@ArrayUnique` after `@ArrayMaxSize` fails and a byte-size body limit does not bound element count; input reaches SQL, shell commands, file paths, server-side fetch targets, templates and regexes only through parameters, allowlists or escaping; a new route / handler / resolver carries the project's auth guard and compares the caller with the owner of an input-chosen id; no secret, full token, password or personal data reaches source, logs, error messages or responses; nothing evaluates or unsafely deserializes input. Each hit is an Acceptance Blocker citing the triggering input and its effect. The same rules, with examples, are `clean-code.md` "Untrusted input and security" in the installed coding-preflight pack (`~/.okstra/prompts/coding-preflight/clean-code.md`).
15
16
  - scope conformance — the delivered diff stays within the approved plan's scope; flag out-of-scope edits, unrelated file changes, leftover debug/commented-out code, and unintended deletions
16
17
  - project review-rule packs — a pack applies when either source names it: the task brief's `Source Material` / `Reporter Confirmations` cites its exact `SKILL.md` path, or `<PROJECT_ROOT>/.okstra/project.json` lists it under `reviewRulePacks` (the project's standing standard, applying whether or not the brief mentions it). The two sources are a union. Read only those files and the `references/*.md` files they directly name. Do not search parent directories or host skill catalogs. Apply the rules as an acceptance overlay (record `project-review-rules: <paths read>`, `project-review-rules: declared <path> unreadable`, or `project-review-rules: none declared or cited`). This is a static review pass, not a PR-comment workflow — do NOT dispatch reviewer subagents. Because this phase verifies the **whole-task merged diff**, it is the gate that catches **cross-stage findings a per-stage `implementation` verifier structurally cannot see** (each implementation run reviews only its own stage diff): most importantly two cross-stage conditions: (a) the same helper stack / transform / domain rule duplicated across stages or services — byte-identical duplication is always an Acceptance Blocker, and semantically-equivalent transforms across services are blockers unless the approved plan explicitly justified keeping them separate; (b) an API newly orphaned because its only caller was removed in a different stage. A confirmed cross-stage duplication of this kind is an Acceptance Blocker (`major`+) that cites every `path:line` location and names the shared-module location to converge on. (Single-stage scope sees only one stage, so it cannot raise cross-stage findings — note that limitation rather than implying coverage.)
17
18
  - Residual-tracked — note as Residual Risk unless severe enough to block:
@@ -25,6 +26,7 @@
25
26
  - **whole-task mutates only when it has to.** When one stage branch already contains every other done stage's commit — the usual shape of a linear plan, since each stage branches from its predecessor's done commit — that branch IS the whole task, and verification runs in that stage's worktree with no merge at all (`stage_targets.containing_stage`). Only when the stage graph has several tips does entry auto-merge (with `--no-ff`) the done stages into the task branch to create an integration commit; that case is the mutating one. The stage worktrees are NOT removed on entry: they are reclaimed after the verdict, by the Phase 7 `teardown-stages` step, and only when the verdict clears the work for release (`accepted`, or `conditional-accept` with no condition blocking release). A `blocked` verdict therefore leaves every stage worktree in place, so the rework it routes to can start immediately. The stage branches are kept as the reviewable stack (the target of `okstra handoff local-checkout --stage <N>`). If a merge conflict occurs it reports the conflicting files and aborts (the user resolves them manually and retries). A stage worktree with uncommitted changes remaining is preserved. Therefore the "fully-merged, clean target" the entry gate above refers to is the state after this auto-integration step completes, and whole-task final-verification must be treated as a mutating phase that creates the integration commit.
26
27
  - **single-stage scope** (`--stage N`): prep verified stage N is `status:done` and its isolated stage worktree exists and is clean. Other stages' state is irrelevant. A single-stage run is a partial verification of one stage, and that is exactly the unit release-handoff ships: an `accepted` verdict here makes the stage PR-eligible, so `release-handoff` is the routing target. It says nothing about any other stage.
27
28
  - the lead still captures `git status --short` from the injected worktree to confirm the analysis ran against the delivered work-tree state; an unexpected divergence (dirty tree outside `.okstra/`, missing worktree) is a `tool-failure`, not a silent proceed.
29
+ - **Stage worktree lifecycle:** implementation finalization may already have removed a completed stage's directory. Preparation restores the preserved stage branch for single-stage verification, or the containing tip branch for a linear whole-task target. After successful report validation, a release-ready single-stage verdict reclaims that stage's clean worktree; a whole-task verdict reclaims completed stages without any cleanup-time merge. Blocked, active, dirty, nested, or changed stages remain available for rework. The branch and registry base survive cleanup. After finalization use the main project root or absolute artifact paths because the verification cwd may have been removed. Enforcement: `report_finalize._teardown_stage_worktrees`, `stage_targets.teardown_completed_stage_worktrees`, and `tests/worktree/test_stage_rebase.py`.
28
30
  - Worker verification procedure:
29
31
  - **Target confirmation:** analyse the injected target and nothing else. Read `verification-target.md` for the stage/report mapping and the complete diff stat. Prepare fixed that target and `phases/final_verification/validation.py` `validate_verification_target_match` (called by `validators/validate-run.py`) re-checks the report against its digest, so the procedure to follow here is simply: if the worktree you can see does not match the injected target, record a `tool-failure` — never reselect a target.
30
32
  - **Evidence:** attach file:line, exact command + exit code, log excerpt, or MCP SELECT evidence to every finding. Mark a requirement as covered only when the cited artifact demonstrates it.
@@ -184,6 +184,8 @@ flowchart TD
184
184
 
185
185
  The stage merge of whole-task mode is a runtime-owned integration step that prepare performs; the matching teardown is a runtime-owned step too, but it runs after the verdict (Phase 7 `teardown-stages`) and only when the verdict clears the work for release. After that, lead verification is read-only. Source edit, follow-up fix, and scope expansion are all forbidden. When a defect is found, it is not fixed within the current run. The lead chooses the next phase from `prompts/lead/phase-routing.md` using the blocker facts this phase recorded.
186
186
 
187
+ Implementation finalization may already have reclaimed completed stage worktrees. Target acquisition restores a missing single-stage worktree, or the containing tip-stage worktree for a linear whole-task target, from its preserved branch. Restoration does not merge into the task branch. After a release-ready single-stage verdict, `report-finalize` also reclaims that stage's worktree using the captured verification head; a blocked verdict keeps the restored worktree for rework. Cleanup never merges stages: `stage_targets.teardown_completed_stage_worktrees` only reclaims completed, inactive worktrees whose tips match the selected completion commits. Tests: `tests/worktree/test_stage_rebase.py`.
188
+
187
189
  ### 8. Verified code
188
190
 
189
191
  - `scripts/okstra_ctl/phases/final_verification/profile.md`
@@ -1,6 +1,6 @@
1
1
  """final-verification 이 검증할 대상을 고른다.
2
2
 
3
- 단일 stage 검증은 그 stage 의 worktree 를 읽기만 한다. 전체 작업 검증은 완료된
3
+ 검증 대상 고정 전에 이동한 의존 기준을 재배치한다. 전체 작업 검증은 완료된
4
4
  stage 전부를 담은 stage 브랜치가 있으면 그것을 쓰고, 없으면 task 브랜치에 통합한
5
5
  결과를 쓴다. 원장·registry·Git·통합은 공통 모듈이 제공하고, 여기서는 어느 것을
6
6
  언제 부를지만 정한다. 전체 과정은 task 키 잠금 하나 안에서 돈다.
@@ -115,6 +115,14 @@ def _read_final_verification_done_rows(
115
115
  ),
116
116
  plan_run_root,
117
117
  )
118
+ from okstra_ctl.stage_rebase import synchronize_stage_stack
119
+ from okstra_ctl.stage_targets import StageWorktreeIdentity
120
+
121
+ synchronize_stage_stack(
122
+ project_root=request.project_root, plan_run_root=plan_run_root,
123
+ stage_map=list(request.stage_map),
124
+ identity=StageWorktreeIdentity(project_id, task_group, task_id, request.work_category),
125
+ )
118
126
  return read_stage_consumer_state(plan_run_root).done_rows
119
127
 
120
128
 
@@ -141,6 +149,10 @@ def _acquire_single_stage_target(
141
149
  assert stage is not None
142
150
  row = worktree_registry.get_stage_row(*registry_coordinates, stage)
143
151
  worktree_path = (row or {}).get("worktree_path", "")
152
+ if worktree_path and not Path(worktree_path).exists():
153
+ from okstra_ctl.worktree.provision import restore_stage_worktree
154
+
155
+ restore_stage_worktree(request.project_root, worktree_path, row["branch"])
144
156
  head = ""
145
157
  if worktree_path and Path(worktree_path).is_dir():
146
158
  head_result = _git(worktree_path, "rev-parse", "HEAD")
@@ -183,6 +195,10 @@ def _acquire_tip_stage_target(
183
195
  return None
184
196
  row = worktree_registry.get_stage_row(*registry_coordinates, tip) or {}
185
197
  worktree_path = row.get("worktree_path", "")
198
+ if worktree_path and not Path(worktree_path).exists():
199
+ from okstra_ctl.worktree.provision import restore_stage_worktree
200
+
201
+ restore_stage_worktree(request.project_root, worktree_path, row["branch"])
186
202
  if not worktree_path or not Path(worktree_path).is_dir():
187
203
  return None
188
204
  head_result = _git(worktree_path, "rev-parse", "HEAD")
@@ -95,6 +95,18 @@ def claim_implementation_stage_run(
95
95
  plan_run_root = Path(inp.approved_plan_path).resolve().parents[1]
96
96
  backfill_done_from_carry(plan_run_root)
97
97
  auto_reconcile_best_effort(inp, plan_run_root)
98
+ from okstra_ctl.stage_rebase import synchronize_stage_stack
99
+
100
+ try:
101
+ synchronize_stage_stack(
102
+ project_root=Path(inp.project_root), plan_run_root=plan_run_root,
103
+ stage_map=ctx_stage_map,
104
+ identity=stage_targets.StageWorktreeIdentity(
105
+ inp.project_id, task_group_segment, task_id_segment, inp.work_category,
106
+ ),
107
+ )
108
+ except RuntimeError as exc:
109
+ raise ImplementationStageError(str(exc)) from exc
98
110
  reserved_stages = _reg.list_active_stage_numbers(
99
111
  inp.project_id,
100
112
  inp.task_group,
@@ -94,6 +94,7 @@ A finding that needs no code change (convergence settled it as a verifier misrea
94
94
  - **A run that ends with no verifier result still closes.** When the stage ends before any verifier returned a verdict — the executor stopped without a change and the user chose not to verify, or every dispatched verifier ended without a result — append the `failed` row as above, record each roster verifier `not-run` with its reason (`okstra worker-state transition --team-state <path> --worker <worker-id> --status not-run --reason <text>`; a worker the dispatch skipped already carries one), and write the convergence state with `okstra convergence skip --run-manifest <path> --reason <text>` before dispatching the report writer. Without that state the report-writer packet refuses with `convergence … required source is missing`. The command refuses while any analyser, verifier, or critic attempt returned a result or is still running; such a run converges normally. In the report, write each verifier's `verifierResults[]` row as `verdict: not-run`, and state the reason under `Stage sidecar evidence` as `withheld`.
95
95
  - On a non-`FAIL` verdict, for this run's single stage: write its JSON verbatim to `runs/<impl-task-key>/carry/stage-<N>.json` — after a passing lead minor fix, with the head, files, and note that §"Lead minor fix" adds. Refuse to overwrite an existing file (one stage = one sidecar; a fix run re-entering after a `failed` row writes the first one, because a withheld stage never wrote it). A stage reopened after a `done` row (see "Reopening a settled stage" below) still has the sidecar of the run that first settled it, naming a head the stage has since moved past: move it unchanged to `runs/<impl-task-key>/carry/superseded/stage-<N>-from-<that run's task-type>-<seq>.json` before writing the new one, and name both paths in `Stage sidecar evidence`. Do not rename it inside `carry/`: the readers glob `carry/stage-*.json` (`okstra_ctl.stage_map`, `okstra_ctl.consumers.backfill_done_from_carry`), so a renamed file there is read again as a live sidecar. A reopened stage whose run ends `FAIL` writes nothing and leaves the old file where it is.
96
96
  - On a non-`FAIL` verdict, for this run's single stage: append a `status:"done"` row to `runs/<plan-task-key>/consumers.jsonl` with `completed_at`, `carry_path`, `report_path` (this run's final-report path relative to the run root), and the SHA of HEAD. Append it with `okstra_ctl.consumers.append_consumer` (NOT a raw filesystem write) — that call honours the consumers lock AND releases this stage's worktree-registry occupancy, so later runs stop seeing a finished stage as a concurrent run. `report_path` lets `final-verification` cite each stage's originating report when assembling its Source Implementation Report list.
97
+ - **Automatic stage worktree cleanup:** after the report passes finalization, `report-finalize` reclaims this completed stage's clean worktree. It compares the report's final commit with the completion ledger and branch tip, preserves active or unfinished stages and dirty or nested worktrees, and keeps the branch and registry base for dependency rebases and later re-entry. Never remove the directory by hand before finalization. A later implementation or final-verification entry restores it from the preserved branch. After finalization, use the main project root or absolute artifact paths: this run's stage cwd may no longer exist. Enforcement: `report_finalize._teardown_stage_worktrees`, `stage_targets.teardown_completed_stage_worktrees`, and the cleanup/restoration tests in `tests/worktree/test_stage_rebase.py`.
97
98
  - **Reopening a settled stage.** Appending a `status:"failed"` row after a `done` one withdraws that stage: `--stage N` accepts it again, its dependents stop resolving a base from the withdrawn head, and whole-task final-verification blocks until it is settled again. Write it with `okstra stage-reopen <task-key> --stage <N> --reason "<why>" [--evidence <report>]`, which records the reason and the report that found the defect, and re-run the stage rather than repairing the tree outside okstra — a stage reverted outside the ledger leaves the recorded `head_commit` and the carry sidecar naming a tree that no longer exists, and the next stage branches from it.
98
99
  - The verifier round, Phase 5.5 convergence, and this Phase 6 report run **once per run** over this stage's diff — NOT per step.
99
100
  - Record this stage's new contents in the final report's `Stage sidecar evidence` deliverable section: the sidecar by path with its one-line summary, and the new consumers row quoted by itself.
@@ -9,6 +9,7 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
9
9
 
10
10
  ## Executor role binding (carried over from the thin core)
11
11
 
12
+ - **Moving dependency bases:** never run `git merge <predecessor>` or merge an old origin branch into this stage to update its base. Runtime preparation rebases downstream stages in dependency order through `okstra_ctl.stage_rebase`; a predecessor moving during execution requires stopping and re-entering preparation after the current stage claim is released. Do not rewrite another active stage or repair registry/ledger SHAs by hand. Enforcement of preparation ordering and conflict recovery is covered by `tests/worktree/test_stage_rebase.py`.
12
13
  - The `Executor` (bound in `implementation.md` thin core) is the **only worker permitted to mutate project files**. All other workers run read-only. A `runner=native-session` executor uses the selected host adapter's native edit and command primitives. A `runner=cli-wrapper` executor mutates files inside its provider CLI's auto-edit mode. The safety rules in this sidecar apply identically to both runners.
13
14
  - When the thin core's Task worktree block resolves status to `created` or `reused`, the Executor MUST run every Edit / Write / build / test / commit command with the worktree path as cwd. Treat it as `project_root` for the duration of this run. Do NOT mutate the caller's original checkout. Do NOT `cd` out of the worktree to reach files. If a file outside the worktree is genuinely needed, treat it as a planning gap: record it in `Out-of-plan edits` and continue.
14
15
  - **How to set the working directory**: every command and native edit MUST target `{{EXECUTOR_WORKTREE_PATH}}`, never the lead session's original project directory. The selected runtime adapter owns the exact native command syntax. Provider CLI wrappers inject the worktree at the CLI layer. For tools that accept an explicit working-directory flag (`git -C <path>`, `cargo --manifest-path`, `pytest --rootdir`), prefer that form.