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.
- package/README.md +5 -5
- package/dist/cli-registry.mjs +0 -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 +2 -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 +1 -3
- 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/dispatch_core.py +3 -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 +1 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md +1 -0
- package/runtime/python/okstra_ctl/phases/implementation/spec.md +4 -0
- 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_integrate.py +37 -2
- package/runtime/python/okstra_ctl/stage_rebase.py +189 -0
- 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/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 +0 -1
- package/runtime/templates/reports/html/i18n/ko.json +0 -1
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-run.py +1 -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
|
@@ -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
|
|
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
|
|
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",
|
|
306
|
+
"verifier", "report-writer",
|
|
307
307
|
)
|
|
308
308
|
},
|
|
309
309
|
wrapper="okstra-antigravity-exec.sh",
|
|
@@ -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"
|
|
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,
|
|
@@ -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`
|
|
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
|
|
118
|
-
"
|
|
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"
|
|
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
|
-
|
|
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,
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
|