okstra 0.219.0 → 0.220.0
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.
- package/README.md +5 -5
- package/dist/cli-registry.mjs +18 -10
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/lifecycle/config.mjs +2 -3
- package/dist/commands/lifecycle/config.mjs.map +1 -1
- package/dist/lib/runtime-payload.mjs +1 -2
- package/dist/lib/runtime-payload.mjs.map +1 -1
- package/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +9 -7
- package/docs/cli.md +5 -3
- package/docs/project-structure-overview.md +3 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +5 -4
- package/runtime/prompts/lead/report-writer.md +10 -17
- package/runtime/prompts/lead/team-contract.md +1 -2
- package/runtime/prompts/wizard/prompts.ko.json +0 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +1 -1
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +0 -1
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +1 -1
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +0 -1
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +0 -1
- package/runtime/python/okstra_ctl/agent/invocation.py +1 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +2 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/jobs.py +0 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +2 -27
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -4
- package/runtime/python/okstra_ctl/blocking_checks.py +8 -0
- package/runtime/python/okstra_ctl/convergence_provenance.py +8 -2
- package/runtime/python/okstra_ctl/correction_cli.py +59 -0
- package/runtime/python/okstra_ctl/correction_round.py +168 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +21 -349
- package/runtime/python/okstra_ctl/dispatch_state.py +0 -13
- package/runtime/python/okstra_ctl/domain/provider.py +1 -1
- package/runtime/python/okstra_ctl/domain/role.py +0 -6
- package/runtime/python/okstra_ctl/final_report_paths.py +0 -16
- package/runtime/python/okstra_ctl/i18n.py +2 -2
- package/runtime/python/okstra_ctl/path_hints.py +0 -4
- package/runtime/python/okstra_ctl/paths.py +0 -3
- package/runtime/python/okstra_ctl/phases/final_verification/profile.md +1 -0
- package/runtime/python/okstra_ctl/phases/final_verification/spec.md +2 -0
- package/runtime/python/okstra_ctl/phases/final_verification/target.py +17 -1
- package/runtime/python/okstra_ctl/phases/implementation/entry.py +12 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +30 -6
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md +1 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md +2 -0
- package/runtime/python/okstra_ctl/phases/implementation/profile.md +2 -1
- package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation.template.html +6 -0
- package/runtime/python/okstra_ctl/phases/implementation/spec.md +6 -1
- package/runtime/python/okstra_ctl/phases/implementation/validation.py +62 -1
- package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +1 -1
- package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +24 -3
- package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +9 -9
- package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +4 -2
- package/runtime/python/okstra_ctl/render.py +0 -10
- package/runtime/python/okstra_ctl/render_final_report.py +4 -4
- package/runtime/python/okstra_ctl/report_finalize.py +37 -76
- package/runtime/python/okstra_ctl/report_html/render.py +4 -44
- package/runtime/python/okstra_ctl/report_language.py +3 -3
- package/runtime/python/okstra_ctl/report_prose_keys.py +153 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +1 -1
- package/runtime/python/okstra_ctl/run.py +1 -21
- package/runtime/python/okstra_ctl/stage_done.py +183 -0
- package/runtime/python/okstra_ctl/stage_fail.py +114 -0
- package/runtime/python/okstra_ctl/stage_integrate.py +37 -2
- package/runtime/python/okstra_ctl/stage_rebase.py +189 -0
- package/runtime/python/okstra_ctl/stage_reopen.py +12 -8
- package/runtime/python/okstra_ctl/stage_targets.py +29 -62
- package/runtime/python/okstra_ctl/team.py +1 -6
- package/runtime/python/okstra_ctl/wizard/confirmation.py +0 -8
- package/runtime/python/okstra_ctl/wizard/registry.py +2 -3
- package/runtime/python/okstra_ctl/wizard/sources.py +10 -0
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +3 -12
- package/runtime/python/okstra_ctl/worktree/__init__.py +1 -1
- package/runtime/python/okstra_ctl/worktree/decisions.py +3 -4
- package/runtime/python/okstra_ctl/worktree/provision.py +27 -0
- package/runtime/python/okstra_ctl/worktree/rebase.py +108 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +14 -0
- package/runtime/schemas/final-report-v3.0.schema.json +78 -0
- package/runtime/skills/_fragments/wizard-render-bundle.md +1 -1
- package/runtime/skills/okstra-discussion/SKILL.md +1 -1
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/skills/okstra-setup/SKILL.md +2 -2
- package/runtime/skills/okstra-setup/references/project-config.md +2 -2
- package/runtime/templates/report-writer-prompt-preamble.md +1 -1
- package/runtime/templates/reports/html/i18n/en.json +6 -1
- package/runtime/templates/reports/html/i18n/ko.json +6 -1
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-run.py +3 -28
- package/runtime/agents/operations/report-translation.json +0 -6
- package/runtime/agents/roles/translator.json +0 -19
- package/runtime/bin/okstra-report-translate.py +0 -404
- package/runtime/prompts/duties/translator.json +0 -32
- package/runtime/python/okstra_ctl/report_translation.py +0 -509
- package/runtime/python/okstra_ctl/report_translation_dispatch.py +0 -356
- package/runtime/templates/translator-prompt-preamble.md +0 -36
|
@@ -74,6 +74,11 @@ from .domain.worker_runtime import (
|
|
|
74
74
|
)
|
|
75
75
|
from .ports.worker_runtime import WorkerRuntimePort
|
|
76
76
|
from .execution_identity import Attempt, ExecutionManifest, Invocation, RoleExecution
|
|
77
|
+
from .correction_round import (
|
|
78
|
+
CorrectionRoundError,
|
|
79
|
+
correction_round,
|
|
80
|
+
validate_correction_dispatch,
|
|
81
|
+
)
|
|
77
82
|
from .execution_manifest import (
|
|
78
83
|
ExecutionManifestError,
|
|
79
84
|
finish_attempt_mutation,
|
|
@@ -403,6 +408,7 @@ def build_dispatch_plan(
|
|
|
403
408
|
# Ahead of the per-prompt checks: a batch the lead must split is a plan-shape
|
|
404
409
|
# problem, and reporting a prompt-header error for a doomed batch buries it.
|
|
405
410
|
_validate_implementation_phase_order(jobs)
|
|
411
|
+
_validate_correction_rounds(manifest_path, jobs)
|
|
406
412
|
_validate_dispatch_prompts(manifest, active_context, jobs)
|
|
407
413
|
_reject_stale_schema_excerpt(project_root, manifest, jobs)
|
|
408
414
|
return DispatchPlan(
|
|
@@ -434,6 +440,7 @@ def dispatch_plan(
|
|
|
434
440
|
plan = _ensure_runtime_chain(plan)
|
|
435
441
|
_validate_report_writer_isolation(plan.jobs)
|
|
436
442
|
_validate_implementation_phase_order(plan.jobs)
|
|
443
|
+
_validate_correction_rounds(plan.manifest_path, plan.jobs)
|
|
437
444
|
_revalidate_plan_role_executions(plan)
|
|
438
445
|
pane_backends = sorted(
|
|
439
446
|
{job.backend for job in plan.jobs if job.backend == BACKEND_CMUX_PANE}
|
|
@@ -538,6 +545,7 @@ def dispatch_cli_wrapper_plan(
|
|
|
538
545
|
plan = _ensure_runtime_chain(plan)
|
|
539
546
|
_validate_report_writer_isolation(plan.jobs)
|
|
540
547
|
_validate_implementation_phase_order(plan.jobs)
|
|
548
|
+
_validate_correction_rounds(plan.manifest_path, plan.jobs)
|
|
541
549
|
_revalidate_plan_role_executions(plan)
|
|
542
550
|
if before_start is not None:
|
|
543
551
|
before_start(plan)
|
|
@@ -628,6 +636,16 @@ def _validate_implementation_phase_order(jobs: Sequence[WorkerJob]) -> None:
|
|
|
628
636
|
)
|
|
629
637
|
|
|
630
638
|
|
|
639
|
+
def _validate_correction_rounds(manifest_path: Path, jobs: Sequence[WorkerJob]) -> None:
|
|
640
|
+
"""A `correction-r<N>` job runs only in its round order and within the round cap."""
|
|
641
|
+
if not any(correction_round(job.dispatch_kind) is not None for job in jobs):
|
|
642
|
+
return
|
|
643
|
+
try:
|
|
644
|
+
validate_correction_dispatch(read_execution_manifest(manifest_path), jobs)
|
|
645
|
+
except CorrectionRoundError as exc:
|
|
646
|
+
raise DispatchError(str(exc)) from exc
|
|
647
|
+
|
|
648
|
+
|
|
631
649
|
def _named_role(worker_ids: Sequence[str], role: str) -> str:
|
|
632
650
|
"""`verifier codex` for one, `verifiers codex, antigravity` for several.
|
|
633
651
|
|
|
@@ -674,7 +692,7 @@ def _dispatch_cli_wrapper_batch(
|
|
|
674
692
|
# (`_persisted_write_contract`) attempt 2 의 정책 digest 는 attempt 1 과
|
|
675
693
|
# 다른데, 첫 라운드의 baseline 을 그대로 쓰면 마감의
|
|
676
694
|
# `_validate_snapshot_authority` 가 `policyDigests` 불일치로 죽고 attempt-N
|
|
677
|
-
# 사이드카는 끝내 안 써진다(실측 2026-09-08,
|
|
695
|
+
# 사이드카는 끝내 안 써진다(실측 2026-09-08, 워커 attempt 2:
|
|
678
696
|
# 산출물 완전·exit 0 인데 dispatcher 예외). 앞 라운드에서 완주한 동료의
|
|
679
697
|
# 변경은 그 attempt 의 감사가 이미 판정했으므로 새 baseline 에 흡수돼도
|
|
680
698
|
# 맞다 — 재시도는 `failed-no-mutation` 뒤에만 오므로(`retry_allowed`)
|
|
@@ -920,15 +938,6 @@ def _jobs_from_roster(
|
|
|
920
938
|
options,
|
|
921
939
|
team_state=team_state,
|
|
922
940
|
)
|
|
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
941
|
_mark_roster_skips(project_root, manifest, team_state, selected, requested_workers, options)
|
|
933
942
|
worker_facts, prompt_paths = _materialize_roster_prompts(
|
|
934
943
|
project_root,
|
|
@@ -962,29 +971,6 @@ def _select_workers(
|
|
|
962
971
|
*,
|
|
963
972
|
team_state: Mapping[str, Any] | None = None,
|
|
964
973
|
) -> 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
974
|
recommended = _string_list(manifest.get("recommendedWorkers"))
|
|
989
975
|
if not recommended:
|
|
990
976
|
raise DispatchError("run manifest has no recommendedWorkers entries")
|
|
@@ -1027,270 +1013,6 @@ def _select_workers(
|
|
|
1027
1013
|
return selected
|
|
1028
1014
|
|
|
1029
1015
|
|
|
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
1016
|
def _select_cli_wrapper_assignments(
|
|
1295
1017
|
recommended: Sequence[str],
|
|
1296
1018
|
requested_workers: Sequence[str],
|
|
@@ -2878,27 +2600,6 @@ def _wrapper_log_tail(job: WorkerJob) -> str | None:
|
|
|
2878
2600
|
return tail.decode("utf-8", errors="replace").strip() or None
|
|
2879
2601
|
|
|
2880
2602
|
|
|
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
2603
|
def _transition_job_status(
|
|
2903
2604
|
plan: DispatchPlan,
|
|
2904
2605
|
job: WorkerJob,
|
|
@@ -2907,24 +2608,6 @@ def _transition_job_status(
|
|
|
2907
2608
|
*,
|
|
2908
2609
|
model_execution_value: str = "",
|
|
2909
2610
|
) -> 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
2611
|
_transition_worker_status(
|
|
2929
2612
|
plan.team_state_path,
|
|
2930
2613
|
job.worker_id,
|
|
@@ -2945,20 +2628,9 @@ def _job_terminal_status(
|
|
|
2945
2628
|
row for row in workers
|
|
2946
2629
|
if isinstance(row, Mapping) and row.get("workerId") == job.worker_id
|
|
2947
2630
|
]
|
|
2948
|
-
if len(matches)
|
|
2949
|
-
return _string_value(matches[0].get("status"))
|
|
2950
|
-
if matches or not _projectionless_translator(plan, job):
|
|
2631
|
+
if len(matches) != 1:
|
|
2951
2632
|
raise DispatchError(f"team-state has no unique workerId={job.worker_id}")
|
|
2952
|
-
|
|
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"))
|
|
2633
|
+
return _string_value(matches[0].get("status"))
|
|
2962
2634
|
|
|
2963
2635
|
|
|
2964
2636
|
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
|
|
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
|
),
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
- **whole-task mutates only when it has to.** When one stage branch already contains every other done stage's commit — the usual shape of a linear plan, since each stage branches from its predecessor's done commit — that branch IS the whole task, and verification runs in that stage's worktree with no merge at all (`stage_targets.containing_stage`). Only when the stage graph has several tips does entry auto-merge (with `--no-ff`) the done stages into the task branch to create an integration commit; that case is the mutating one. The stage worktrees are NOT removed on entry: they are reclaimed after the verdict, by the Phase 7 `teardown-stages` step, and only when the verdict clears the work for release (`accepted`, or `conditional-accept` with no condition blocking release). A `blocked` verdict therefore leaves every stage worktree in place, so the rework it routes to can start immediately. The stage branches are kept as the reviewable stack (the target of `okstra handoff local-checkout --stage <N>`). If a merge conflict occurs it reports the conflicting files and aborts (the user resolves them manually and retries). A stage worktree with uncommitted changes remaining is preserved. Therefore the "fully-merged, clean target" the entry gate above refers to is the state after this auto-integration step completes, and whole-task final-verification must be treated as a mutating phase that creates the integration commit.
|
|
27
27
|
- **single-stage scope** (`--stage N`): prep verified stage N is `status:done` and its isolated stage worktree exists and is clean. Other stages' state is irrelevant. A single-stage run is a partial verification of one stage, and that is exactly the unit release-handoff ships: an `accepted` verdict here makes the stage PR-eligible, so `release-handoff` is the routing target. It says nothing about any other stage.
|
|
28
28
|
- the lead still captures `git status --short` from the injected worktree to confirm the analysis ran against the delivered work-tree state; an unexpected divergence (dirty tree outside `.okstra/`, missing worktree) is a `tool-failure`, not a silent proceed.
|
|
29
|
+
- **Stage worktree lifecycle:** implementation finalization may already have removed a completed stage's directory. Preparation restores the preserved stage branch for single-stage verification, or the containing tip branch for a linear whole-task target. After successful report validation, a release-ready single-stage verdict reclaims that stage's clean worktree; a whole-task verdict reclaims completed stages without any cleanup-time merge. Blocked, active, dirty, nested, or changed stages remain available for rework. The branch and registry base survive cleanup. After finalization use the main project root or absolute artifact paths because the verification cwd may have been removed. Enforcement: `report_finalize._teardown_stage_worktrees`, `stage_targets.teardown_completed_stage_worktrees`, and `tests/worktree/test_stage_rebase.py`.
|
|
29
30
|
- Worker verification procedure:
|
|
30
31
|
- **Target confirmation:** analyse the injected target and nothing else. Read `verification-target.md` for the stage/report mapping and the complete diff stat. Prepare fixed that target and `phases/final_verification/validation.py` `validate_verification_target_match` (called by `validators/validate-run.py`) re-checks the report against its digest, so the procedure to follow here is simply: if the worktree you can see does not match the injected target, record a `tool-failure` — never reselect a target.
|
|
31
32
|
- **Evidence:** attach file:line, exact command + exit code, log excerpt, or MCP SELECT evidence to every finding. Mark a requirement as covered only when the cited artifact demonstrates it.
|
|
@@ -184,6 +184,8 @@ flowchart TD
|
|
|
184
184
|
|
|
185
185
|
The stage merge of whole-task mode is a runtime-owned integration step that prepare performs; the matching teardown is a runtime-owned step too, but it runs after the verdict (Phase 7 `teardown-stages`) and only when the verdict clears the work for release. After that, lead verification is read-only. Source edit, follow-up fix, and scope expansion are all forbidden. When a defect is found, it is not fixed within the current run. The lead chooses the next phase from `prompts/lead/phase-routing.md` using the blocker facts this phase recorded.
|
|
186
186
|
|
|
187
|
+
Implementation finalization may already have reclaimed completed stage worktrees. Target acquisition restores a missing single-stage worktree, or the containing tip-stage worktree for a linear whole-task target, from its preserved branch. Restoration does not merge into the task branch. After a release-ready single-stage verdict, `report-finalize` also reclaims that stage's worktree using the captured verification head; a blocked verdict keeps the restored worktree for rework. Cleanup never merges stages: `stage_targets.teardown_completed_stage_worktrees` only reclaims completed, inactive worktrees whose tips match the selected completion commits. Tests: `tests/worktree/test_stage_rebase.py`.
|
|
188
|
+
|
|
187
189
|
### 8. Verified code
|
|
188
190
|
|
|
189
191
|
- `scripts/okstra_ctl/phases/final_verification/profile.md`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""final-verification 이 검증할 대상을 고른다.
|
|
2
2
|
|
|
3
|
-
|
|
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,
|