okstra 0.191.2 → 0.192.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/docs/architecture.md +2 -2
- package/docs/cli.md +2 -1
- package/docs/project-structure-overview.md +3 -1
- package/docs/task-process/README.md +2 -2
- package/docs/task-process/common-flow.md +4 -5
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/translator-worker.md +1 -1
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/lead/convergence.md +7 -3
- package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
- package/runtime/prompts/lead/report-writer.md +11 -8
- package/runtime/prompts/wizard/prompts.ko.json +52 -23
- package/runtime/python/okstra_ctl/convergence.py +63 -1
- package/runtime/python/okstra_ctl/convergence_reverify_prompt.py +231 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +42 -22
- package/runtime/python/okstra_ctl/next_phase.py +18 -8
- package/runtime/python/okstra_ctl/plan_items.py +6 -4
- package/runtime/python/okstra_ctl/report_finalize.py +57 -10
- package/runtime/python/okstra_ctl/report_translation_dispatch.py +300 -0
- package/runtime/python/okstra_ctl/verdict_blocks.py +37 -7
- package/runtime/python/okstra_ctl/wizard/engine.py +16 -2
- package/runtime/python/okstra_ctl/wizard/registry.py +11 -2
- package/runtime/python/okstra_ctl/wizard/roles.py +364 -361
- package/runtime/python/okstra_ctl/wizard/state.py +39 -27
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +50 -8
- package/runtime/python/okstra_ctl/wizard/steps_roles.py +1 -0
- package/runtime/skills/okstra-run/SKILL.md +2 -2
- package/runtime/validators/validate-run.py +2 -2
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Reverify 지시문을 수렴 상태와 라운드 계획에서 결정적으로 렌더한다.
|
|
2
|
+
|
|
3
|
+
Phase 5.5 의 재검증 워커는 리드가 손으로 쓴 지시문을 받아 왔다. 그 손에서 두
|
|
4
|
+
가지가 반복해서 어긋났다(실측 2026-09-09, 다른 세션의 첫 reverify 보고).
|
|
5
|
+
|
|
6
|
+
- 응답 형식 — 리드가 `- Verdict:` 불릿을 적었고 워커는 시킨 대로 썼다. 파서는
|
|
7
|
+
이제 그 모양도 읽지만(`verdict_blocks._field_match`), 형식 블록을 리드가 매
|
|
8
|
+
라운드 다시 쓰는 구조 자체가 결함이다.
|
|
9
|
+
- 근거 축약 — 리드가 `**Cited evidence**` 줄에 원 워커가 인용한 근거의 일부만
|
|
10
|
+
옮겨 적었다. 검증자는 계약대로 그 줄만 열었고, `burden-not-met` 5건은 주장을
|
|
11
|
+
검증한 것이 아니라 리드의 전사(轉寫)를 검증한 것이었다.
|
|
12
|
+
|
|
13
|
+
이 모듈은 그 지시문을 코드로 만든다. finding 마다 그룹의 요약·원 워커·인용
|
|
14
|
+
근거 줄에 더해, **원 워커의 결과 파일과 그 안의 항목 id, 그리고 원 워커의 감사
|
|
15
|
+
사이드카**(실행한 읽기 전용 명령과 출력이 기록된 파일)를 싣고, 그 둘을 열어도
|
|
16
|
+
된다고 명시한다. 검증자는 리드의 요약이 아니라 원 워커가 실제로 인용한 것을
|
|
17
|
+
판단한다. 응답 형식은 `verdict_blocks` 가 읽는 정본 모양 그대로다.
|
|
18
|
+
|
|
19
|
+
산출물은 프롬프트 materializer 의 `--instruction` 이 받는 본문이다. `## Instructions`
|
|
20
|
+
로 시작하므로 `complete_reverify_instruction` 이 모델·task type·금지 목록을 그
|
|
21
|
+
앞에 붙이고, 출력 계약(`templates/reverify-output-contract.md`)을 뒤에 덧붙인다.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Mapping, Sequence
|
|
28
|
+
|
|
29
|
+
from .convergence_critic_prompt import analyser_results
|
|
30
|
+
from .worker_artifact_paths import WorkerArtifactPathError, audit_sidecar_rel
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ReverifyPromptError(ValueError):
|
|
34
|
+
"""지시문을 결정적으로 만들 수 없다."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class ReverifyFinding:
|
|
39
|
+
"""검증 큐의 finding 하나와, 그 원 워커의 실물 인용 위치."""
|
|
40
|
+
|
|
41
|
+
finding_id: str
|
|
42
|
+
summary: str
|
|
43
|
+
origin_worker: str
|
|
44
|
+
origin_item_id: str
|
|
45
|
+
origin_evidence: str
|
|
46
|
+
origin_result_path: str
|
|
47
|
+
origin_audit_path: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_ADVERSARIAL_MANDATE = """Your job is to BREAK each finding below, not to confirm it. For EACH finding,
|
|
51
|
+
open the cited evidence directly and actively search for evidence that the claim
|
|
52
|
+
is wrong, overstated, or unproven. Then respond with exactly one verdict:
|
|
53
|
+
|
|
54
|
+
- **REFUTED**: You broke the claim. State the basis:
|
|
55
|
+
- counter-evidence — you found contradicting evidence (give file:line or log line), OR
|
|
56
|
+
- burden-not-met — you re-inspected the cited evidence and could neither confirm
|
|
57
|
+
nor refute it (the claim has not proven itself).
|
|
58
|
+
- **SURVIVES**: You actively tried to refute it and failed — the claim withstood the attack.
|
|
59
|
+
- **SURVIVES-WITH-CAVEAT**: It holds, but a scope limit / extra condition / missing
|
|
60
|
+
precondition exists (state it).
|
|
61
|
+
- **UNVERIFIABLE**: Capability, credential, network, or service state prevents you
|
|
62
|
+
from opening or reproducing the cited evidence. Do not use REFUTED as a substitute.
|
|
63
|
+
|
|
64
|
+
The burden of proof is on the claim. If after inspecting the cited evidence you remain
|
|
65
|
+
uncertain, your verdict is REFUTED with basis = burden-not-met.
|
|
66
|
+
|
|
67
|
+
Inspect ONLY the evidence each finding cites and its immediate surroundings. Do NOT
|
|
68
|
+
re-read the task brief, instruction-set, or report template."""
|
|
69
|
+
|
|
70
|
+
_COLLABORATIVE_MANDATE = """Review the following findings discovered by other workers.
|
|
71
|
+
For EACH finding, respond with exactly one verdict:
|
|
72
|
+
|
|
73
|
+
- **AGREE**: The finding is valid based on the evidence presented
|
|
74
|
+
- **DISAGREE**: The finding is incorrect or unsupported (explain briefly why)
|
|
75
|
+
- **SUPPLEMENT**: The finding is valid AND you have additional supporting evidence or context
|
|
76
|
+
- **UNVERIFIABLE**: Capability, credential, network, or service state prevents you
|
|
77
|
+
from checking this finding. Explain the unavailable capability; do not substitute DISAGREE.
|
|
78
|
+
|
|
79
|
+
Do NOT re-analyze the original source materials. Judge based on the evidence provided."""
|
|
80
|
+
|
|
81
|
+
# 근거 접근 규칙. `**Cited evidence**` 는 리드의 요약이고, 완전한 인용은 원 워커의
|
|
82
|
+
# 결과 항목이다. 검증자가 요약만 열고 "이 근거로는 입증되지 않는다" 고 답하던
|
|
83
|
+
# 자리를 막는다.
|
|
84
|
+
_EVIDENCE_ACCESS = """The `**Cited evidence**` line is the lead's summary of what the origin worker cited.
|
|
85
|
+
The complete citation is the origin worker's own item: before judging, open the
|
|
86
|
+
`**Origin item**` file at the named `### <item-id>` section and read every path, line,
|
|
87
|
+
command, and quote it cites. The `**Origin audit sidecar**` records the read-only
|
|
88
|
+
commands that worker ran and their output; it counts as cited evidence and you may
|
|
89
|
+
open it. Judge the claim against what the origin worker actually cited, never against
|
|
90
|
+
the summary line alone."""
|
|
91
|
+
|
|
92
|
+
_ADVERSARIAL_RESPONSE = """### <finding-id>
|
|
93
|
+
**Verdict**: REFUTED | SURVIVES | SURVIVES-WITH-CAVEAT | UNVERIFIABLE
|
|
94
|
+
**Basis** (only if REFUTED): counter-evidence | burden-not-met
|
|
95
|
+
**Explanation**: <2-3 sentences; for counter-evidence include the file:line you found>"""
|
|
96
|
+
|
|
97
|
+
_COLLABORATIVE_RESPONSE = """### <finding-id>
|
|
98
|
+
**Verdict**: AGREE | DISAGREE | SUPPLEMENT | UNVERIFIABLE
|
|
99
|
+
**Explanation**: <2-3 sentences>"""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _nonempty_string(value: Any) -> str:
|
|
103
|
+
return value if isinstance(value, str) and value.strip() else ""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def plan_row_for_worker(plan: Mapping[str, Any], worker_id: str) -> Mapping[str, Any]:
|
|
107
|
+
"""계획의 `dispatches[]` 에서 이 워커의 행 하나. 검증기와 같은 규칙으로 맞춘다.
|
|
108
|
+
|
|
109
|
+
`validators/validate-run.py` `_plan_dispatch_finding_ids` 처럼 `worker` 가
|
|
110
|
+
그대로 같거나 `<worker>-worker` 가 같으면 그 행이다.
|
|
111
|
+
"""
|
|
112
|
+
dispatches = plan.get("dispatches")
|
|
113
|
+
if not isinstance(dispatches, list):
|
|
114
|
+
raise ReverifyPromptError("round plan has no dispatches array")
|
|
115
|
+
matched = [
|
|
116
|
+
row for row in dispatches
|
|
117
|
+
if isinstance(row, Mapping)
|
|
118
|
+
and (row.get("worker") == worker_id or f"{row.get('worker')}-worker" == worker_id)
|
|
119
|
+
]
|
|
120
|
+
if len(matched) != 1:
|
|
121
|
+
planned = ", ".join(
|
|
122
|
+
_nonempty_string(row.get("worker")) or "?" for row in dispatches
|
|
123
|
+
if isinstance(row, Mapping)
|
|
124
|
+
)
|
|
125
|
+
raise ReverifyPromptError(
|
|
126
|
+
f"round plan dispatches nothing to `{worker_id}`; planned workers: "
|
|
127
|
+
f"{planned or 'none'}"
|
|
128
|
+
)
|
|
129
|
+
finding_ids = matched[0].get("findingIds")
|
|
130
|
+
if not isinstance(finding_ids, list) or not finding_ids:
|
|
131
|
+
raise ReverifyPromptError(f"round plan row for `{worker_id}` has no findingIds")
|
|
132
|
+
return matched[0]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def reverify_findings(
|
|
136
|
+
groups: Mapping[str, Any],
|
|
137
|
+
plan: Mapping[str, Any],
|
|
138
|
+
worker_id: str,
|
|
139
|
+
*,
|
|
140
|
+
project_root: Path,
|
|
141
|
+
run_dir: Path,
|
|
142
|
+
) -> list[ReverifyFinding]:
|
|
143
|
+
"""계획 행의 finding 을 계획 순서대로, 원 워커의 실물 인용 위치와 함께."""
|
|
144
|
+
row = plan_row_for_worker(plan, worker_id)
|
|
145
|
+
by_id = {
|
|
146
|
+
_nonempty_string(group.get("findingId")): group
|
|
147
|
+
for group in groups.get("groups") or []
|
|
148
|
+
if isinstance(group, Mapping) and _nonempty_string(group.get("findingId"))
|
|
149
|
+
}
|
|
150
|
+
result_paths = {
|
|
151
|
+
result.worker_id: result.result_path
|
|
152
|
+
for result in analyser_results(groups, project_root=project_root, run_dir=run_dir)
|
|
153
|
+
}
|
|
154
|
+
findings: list[ReverifyFinding] = []
|
|
155
|
+
for finding_id in row["findingIds"]:
|
|
156
|
+
group = by_id.get(str(finding_id))
|
|
157
|
+
if group is None:
|
|
158
|
+
raise ReverifyPromptError(
|
|
159
|
+
f"round plan names `{finding_id}`, which the grouping does not carry"
|
|
160
|
+
)
|
|
161
|
+
origin = _nonempty_string(group.get("originWorker"))
|
|
162
|
+
discovered = group.get("discoveredBy")
|
|
163
|
+
origin_item = (
|
|
164
|
+
_nonempty_string((discovered.get(origin) or {}).get("itemId"))
|
|
165
|
+
if isinstance(discovered, Mapping) and isinstance(discovered.get(origin), Mapping)
|
|
166
|
+
else ""
|
|
167
|
+
)
|
|
168
|
+
result_path = result_paths.get(origin, "")
|
|
169
|
+
if not origin or not origin_item or not result_path:
|
|
170
|
+
raise ReverifyPromptError(
|
|
171
|
+
f"finding `{finding_id}` has no resolvable origin item "
|
|
172
|
+
f"(origin worker `{origin or '?'}`, item `{origin_item or '?'}`)"
|
|
173
|
+
)
|
|
174
|
+
try:
|
|
175
|
+
audit_path = audit_sidecar_rel(result_path)
|
|
176
|
+
except WorkerArtifactPathError as exc:
|
|
177
|
+
raise ReverifyPromptError(str(exc)) from exc
|
|
178
|
+
findings.append(ReverifyFinding(
|
|
179
|
+
finding_id=str(finding_id),
|
|
180
|
+
summary=_nonempty_string(group.get("summary")),
|
|
181
|
+
origin_worker=origin,
|
|
182
|
+
origin_item_id=origin_item,
|
|
183
|
+
origin_evidence=_nonempty_string(group.get("originEvidence")),
|
|
184
|
+
origin_result_path=result_path,
|
|
185
|
+
origin_audit_path=audit_path,
|
|
186
|
+
))
|
|
187
|
+
return findings
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def reverify_prompt_body(
|
|
191
|
+
*,
|
|
192
|
+
task_key: str,
|
|
193
|
+
round_number: int,
|
|
194
|
+
adversarial: bool,
|
|
195
|
+
findings: Sequence[ReverifyFinding],
|
|
196
|
+
) -> str:
|
|
197
|
+
"""reverify 지시문 파일 본문. 같은 입력이면 같은 바이트를 낸다."""
|
|
198
|
+
if not _nonempty_string(task_key):
|
|
199
|
+
raise ReverifyPromptError("convergence groups carry no taskKey")
|
|
200
|
+
if not findings:
|
|
201
|
+
raise ReverifyPromptError("no findings to verify")
|
|
202
|
+
mode = "ADVERSARIAL re-verification" if adversarial else "re-verification"
|
|
203
|
+
mandate = _ADVERSARIAL_MANDATE if adversarial else _COLLABORATIVE_MANDATE
|
|
204
|
+
response = _ADVERSARIAL_RESPONSE if adversarial else _COLLABORATIVE_RESPONSE
|
|
205
|
+
rows = [
|
|
206
|
+
"## Instructions\n\n",
|
|
207
|
+
f"Perform {mode} for {task_key} (round {round_number}).\n\n",
|
|
208
|
+
mandate, "\n\n",
|
|
209
|
+
_EVIDENCE_ACCESS, "\n\n",
|
|
210
|
+
"## Findings to verify\n",
|
|
211
|
+
]
|
|
212
|
+
for finding in findings:
|
|
213
|
+
rows.append(f"\n### {finding.finding_id}: {finding.summary or '(no summary)'}\n")
|
|
214
|
+
rows.append(f"**Origin**: {finding.origin_worker}\n")
|
|
215
|
+
rows.append(f"**Cited evidence**: {finding.origin_evidence or '(none recorded)'}\n")
|
|
216
|
+
rows.append(
|
|
217
|
+
f"**Origin item**: `{finding.origin_result_path}` — section "
|
|
218
|
+
f"`### {finding.origin_item_id}`\n"
|
|
219
|
+
)
|
|
220
|
+
rows.append(f"**Origin audit sidecar**: `{finding.origin_audit_path}`\n")
|
|
221
|
+
rows.append("\n## Response format\n\n")
|
|
222
|
+
rows.append(
|
|
223
|
+
"One block per finding, headed by the finding id at exactly three hashes. "
|
|
224
|
+
"Field labels are bold with the colon outside (`**Verdict**: …`); the "
|
|
225
|
+
"collector also reads `**Verdict:** …` and `- Verdict: …` as the same field.\n\n"
|
|
226
|
+
)
|
|
227
|
+
rows.append(response.replace("<finding-id>", findings[0].finding_id))
|
|
228
|
+
rows.append("\n")
|
|
229
|
+
if len(findings) > 1:
|
|
230
|
+
rows.append(f"\n### {findings[1].finding_id}\n**Verdict**: ...\n")
|
|
231
|
+
return "".join(rows)
|
|
@@ -1032,29 +1032,8 @@ def _translator_job_from_reservation(
|
|
|
1032
1032
|
raise DispatchError(
|
|
1033
1033
|
"translator dispatch requires a canonical translator role execution"
|
|
1034
1034
|
)
|
|
1035
|
-
|
|
1036
|
-
if not isinstance(contract, Mapping):
|
|
1037
|
-
raise DispatchError("translator dispatch requires an agent contract")
|
|
1038
|
-
reservation_root = _resolve_required_path(
|
|
1039
|
-
project_root, contract, "invocationReservationRootPath"
|
|
1040
|
-
)
|
|
1041
|
-
dispatched_ids = {
|
|
1042
|
-
str(row.get("dispatchId") or "")
|
|
1043
|
-
for collection in (
|
|
1044
|
-
team_state.get("workerDispatches") or [],
|
|
1045
|
-
team_state.get("agentDispatches") or [],
|
|
1046
|
-
)
|
|
1047
|
-
for row in collection
|
|
1048
|
-
if isinstance(row, Mapping)
|
|
1049
|
-
}
|
|
1035
|
+
candidates, seen = translator_reservations(project_root, manifest, team_state)
|
|
1050
1036
|
run_manifest_rel = _string_value(manifest.get("runManifestPath"))
|
|
1051
|
-
candidates, seen = _translator_reservation_candidates(
|
|
1052
|
-
project_root,
|
|
1053
|
-
reservation_root,
|
|
1054
|
-
execution=execution,
|
|
1055
|
-
dispatched_ids=dispatched_ids,
|
|
1056
|
-
run_manifest_rel=run_manifest_rel,
|
|
1057
|
-
)
|
|
1058
1037
|
if len(candidates) != 1:
|
|
1059
1038
|
run_label = run_manifest_rel or "run manifest path unknown"
|
|
1060
1039
|
raise DispatchError(
|
|
@@ -1120,6 +1099,47 @@ def _translator_job_from_reservation(
|
|
|
1120
1099
|
)
|
|
1121
1100
|
|
|
1122
1101
|
|
|
1102
|
+
def translator_reservations(
|
|
1103
|
+
project_root: Path,
|
|
1104
|
+
manifest: Mapping[str, Any],
|
|
1105
|
+
team_state: Mapping[str, Any],
|
|
1106
|
+
) -> tuple[list[Mapping[str, Any]], list[str]]:
|
|
1107
|
+
"""이 run 이 지금 디스패치할 수 있는 translator 예약과, 본 예약 전부의 요약.
|
|
1108
|
+
|
|
1109
|
+
두 소비자가 같은 답을 읽는다: `_translator_job_from_reservation` 은 그 하나를
|
|
1110
|
+
골라 띄우고, `report-finalize` 의 `translate` 단계
|
|
1111
|
+
(`okstra_ctl.report_translation_dispatch`) 는 0건이면 예약을 만든다. 후보를
|
|
1112
|
+
세는 규칙이 두 곳이면 한쪽만 고쳐져 "만들었는데 못 고른다" 가 나온다.
|
|
1113
|
+
"""
|
|
1114
|
+
execution = _canonical_translator_execution(manifest)
|
|
1115
|
+
if execution is None:
|
|
1116
|
+
raise DispatchError(
|
|
1117
|
+
"translator dispatch requires a canonical translator role execution"
|
|
1118
|
+
)
|
|
1119
|
+
contract = manifest.get("agentContract")
|
|
1120
|
+
if not isinstance(contract, Mapping):
|
|
1121
|
+
raise DispatchError("translator dispatch requires an agent contract")
|
|
1122
|
+
reservation_root = _resolve_required_path(
|
|
1123
|
+
project_root, contract, "invocationReservationRootPath"
|
|
1124
|
+
)
|
|
1125
|
+
dispatched_ids = {
|
|
1126
|
+
str(row.get("dispatchId") or "")
|
|
1127
|
+
for collection in (
|
|
1128
|
+
team_state.get("workerDispatches") or [],
|
|
1129
|
+
team_state.get("agentDispatches") or [],
|
|
1130
|
+
)
|
|
1131
|
+
for row in collection
|
|
1132
|
+
if isinstance(row, Mapping)
|
|
1133
|
+
}
|
|
1134
|
+
return _translator_reservation_candidates(
|
|
1135
|
+
project_root,
|
|
1136
|
+
reservation_root,
|
|
1137
|
+
execution=execution,
|
|
1138
|
+
dispatched_ids=dispatched_ids,
|
|
1139
|
+
run_manifest_rel=_string_value(manifest.get("runManifestPath")),
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
|
|
1123
1143
|
def _translator_reservation_candidates(
|
|
1124
1144
|
project_root: Path,
|
|
1125
1145
|
reservation_root: Path,
|
|
@@ -71,8 +71,9 @@ HANDLED_TASK_TYPES = frozenset(
|
|
|
71
71
|
)
|
|
72
72
|
|
|
73
73
|
# implementation-option-selection 의 routing enum 중 phase 가 아닌 값.
|
|
74
|
+
# `pending-direction-selection` 과 `blocked` 는 `_from_option_selection` 이
|
|
75
|
+
# 명시 분기로 다룬다.
|
|
74
76
|
_OPTION_SELECTION_NON_PHASE = {
|
|
75
|
-
"pending-direction-selection": STATUS_PENDING,
|
|
76
77
|
"blocked": STATUS_BLOCKED,
|
|
77
78
|
}
|
|
78
79
|
|
|
@@ -231,13 +232,22 @@ def _from_option_selection(report_data: Mapping[str, Any]) -> dict[str, str]:
|
|
|
231
232
|
# release-handoff 는 `routingRecommendation`.
|
|
232
233
|
return make(status=STATUS_BLOCKED, rationale=_selection_guidance(selection))
|
|
233
234
|
if routing == "pending-direction-selection":
|
|
234
|
-
# 이 상태는 **성공**이다 — 비교가
|
|
235
|
-
#
|
|
236
|
-
#
|
|
237
|
-
#
|
|
238
|
-
#
|
|
239
|
-
#
|
|
240
|
-
|
|
235
|
+
# 이 상태는 **성공**이다 — 비교가 끝났고, 방향은 계획 단계의 위저드가
|
|
236
|
+
# 고르게 한다(`selected_direction_pick`). 그러니 다음 phase 는 지금 바로
|
|
237
|
+
# 시작할 수 있는 `implementation-planning` 이고 status 는 `ready` 다.
|
|
238
|
+
# 종전에는 phase 없는 `pending` 이었다: task 선택 화면이 `next: --
|
|
239
|
+
# (pending)` 을 찍어 목적지를 지웠고, task-type 화면은 추천 없이 방금
|
|
240
|
+
# 끝난 phase 의 재실행을 1번에 올렸다(실측 2026-09-09) — 근거 문장은
|
|
241
|
+
# "다시 돌리지 마세요" 라고 말하는데 화면은 그 반대를 권한 셈이다.
|
|
242
|
+
# 근거는 후보 id 를 이름으로 싣는다. 실측(dev-10341): 1회차가
|
|
243
|
+
# IO-001·IO-002 를 내고 안내 없이 멈춰 같은 phase 가 세 번 더 돌았다.
|
|
244
|
+
# 후보가 0건이면 고를 것이 없으므로 종전대로 phase 없는 pending 이다.
|
|
245
|
+
rationale = _direction_selection_reason(selection)
|
|
246
|
+
if not rationale:
|
|
247
|
+
return make(status=STATUS_PENDING)
|
|
248
|
+
return make(
|
|
249
|
+
phase="implementation-planning", status=STATUS_READY, rationale=rationale
|
|
250
|
+
)
|
|
241
251
|
if routing in _OPTION_SELECTION_NON_PHASE:
|
|
242
252
|
return make(status=_OPTION_SELECTION_NON_PHASE[routing])
|
|
243
253
|
if not routing:
|
|
@@ -763,8 +763,9 @@ REVERIFY_PREAMBLE = (
|
|
|
763
763
|
# 정본 서술은 plan-body-verification.md §"Response format" 이지만 전달 채널은
|
|
764
764
|
# 이 출력이다 — 큐는 모든 검증자 프롬프트에 verbatim 으로 실리는 유일한
|
|
765
765
|
# 조각이라, 여기 실린 블록은 리드가 프롬프트를 어떻게 손 조립하든 도달한다.
|
|
766
|
-
#
|
|
767
|
-
#
|
|
766
|
+
# 실측 실패 모양 중 `## <id>` 2해시 헤딩은 본문이 직접 금지한다. 라벨 변형
|
|
767
|
+
# (`**Verdict:** AGREE`, `- Verdict: AGREE`)은 파서가 같은 필드로 읽으므로
|
|
768
|
+
# (`verdict_blocks._field_match`) 금지하지 않고 그렇게 적는다.
|
|
768
769
|
PLAN_VERIFY_RESPONSE_FORMAT = (
|
|
769
770
|
"\n## Response format\n"
|
|
770
771
|
"\n"
|
|
@@ -772,8 +773,9 @@ PLAN_VERIFY_RESPONSE_FORMAT = (
|
|
|
772
773
|
"item's own id at exactly three hashes (`### P-...`). The collector parses "
|
|
773
774
|
"`^### ` and nothing else — a block at any other depth is not an unparsed "
|
|
774
775
|
"block; it is a verdict that was never recorded, and the round is scored "
|
|
775
|
-
"on the items that remain. Field labels
|
|
776
|
-
"`**Verdict**: AGREE
|
|
776
|
+
"on the items that remain. Field labels are bold with the colon outside: "
|
|
777
|
+
"`**Verdict**: AGREE`; the collector also reads `**Verdict:** AGREE` and "
|
|
778
|
+
"`- Verdict: AGREE` as the same field.\n"
|
|
777
779
|
"\n"
|
|
778
780
|
"### <item-id>\n"
|
|
779
781
|
"**Verdict**: AGREE | DISAGREE(<a|b|c|d|e|f>) | SUPPLEMENT | UNVERIFIABLE\n"
|
|
@@ -25,10 +25,15 @@ placeholders, which a v2 report never carries because its numeric cells are
|
|
|
25
25
|
`null` until this step fills them. Substituting the tokens on a later retry
|
|
26
26
|
then leaves the already-rendered html stale for `validators/validate-report-views.py`.
|
|
27
27
|
|
|
28
|
-
The translation sidecar is
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
The translation sidecar is the `translate` step, between `check-source` and
|
|
29
|
+
`render-views`: `render-views` overlays it, and `check-source` has to pass first
|
|
30
|
+
because the extractor refuses a work list from a non-English source. It used to
|
|
31
|
+
be a manual lead sequence outside this command (materialize the translator
|
|
32
|
+
prompt, dispatch it, resume finalize at `render-views`), written only in a doc
|
|
33
|
+
the lead lazy-reads; a lead that ran the whole sequence at once skipped it and
|
|
34
|
+
the run kept an English view under a `ko` report (2026-09-09, fontsninja-v3-site
|
|
35
|
+
dev-10628-3). `okstra_ctl.report_translation_dispatch` owns the step; an English
|
|
36
|
+
report or an existing sidecar makes it a no-op.
|
|
32
37
|
|
|
33
38
|
Every lead adapter drives Phase 7 through this module: the Codex adapter calls
|
|
34
39
|
it in-process (``codex_dispatch``), and a Claude-led run reaches the same code
|
|
@@ -62,6 +67,7 @@ from .final_report_paths import (
|
|
|
62
67
|
from .paths import task_dir, task_manifest_file
|
|
63
68
|
from .report_view_artifacts import html_view_path
|
|
64
69
|
from .release_gate import release_handoff_allowed
|
|
70
|
+
from .report_translation_dispatch import TranslateOutcome, translate_report
|
|
65
71
|
from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
66
72
|
from .stage_integrate import IntegrateError
|
|
67
73
|
from .stage_targets import (
|
|
@@ -83,6 +89,7 @@ from okstra_project.phase_pointer import (
|
|
|
83
89
|
|
|
84
90
|
STEP_PROJECT_ACTIVITY = "project-activity"
|
|
85
91
|
STEP_CHECK_SOURCE = "check-source"
|
|
92
|
+
STEP_TRANSLATE = "translate"
|
|
86
93
|
STEP_TOKEN_USAGE = "token-usage"
|
|
87
94
|
STEP_RENDER_VIEWS = "render-views"
|
|
88
95
|
STEP_SPAWN_FOLLOWUPS = "spawn-followups"
|
|
@@ -96,6 +103,7 @@ STEP_ORDER = (
|
|
|
96
103
|
# a Korean SSOT into English chrome, spawning follow-ups from it, and
|
|
97
104
|
# validating it all succeed on a record the next phase cannot read.
|
|
98
105
|
STEP_CHECK_SOURCE,
|
|
106
|
+
STEP_TRANSLATE,
|
|
99
107
|
STEP_TOKEN_USAGE,
|
|
100
108
|
STEP_RENDER_VIEWS,
|
|
101
109
|
STEP_SPAWN_FOLLOWUPS,
|
|
@@ -112,6 +120,8 @@ V3_STEP_ORDER = (
|
|
|
112
120
|
STEP_TOKEN_USAGE,
|
|
113
121
|
STEP_PROJECT_ACTIVITY,
|
|
114
122
|
STEP_CHECK_SOURCE,
|
|
123
|
+
# After the English gate, before the render that overlays its sidecar.
|
|
124
|
+
STEP_TRANSLATE,
|
|
115
125
|
STEP_RENDER_VIEWS,
|
|
116
126
|
STEP_SPAWN_FOLLOWUPS,
|
|
117
127
|
STEP_VALIDATE_RUN,
|
|
@@ -337,6 +347,10 @@ def build_commands(ctx: FinalizeContext) -> list[tuple[str, list[str]]]:
|
|
|
337
347
|
str(ctx.data_path),
|
|
338
348
|
],
|
|
339
349
|
),
|
|
350
|
+
(
|
|
351
|
+
STEP_TRANSLATE,
|
|
352
|
+
["<in-process>", "translate", str(ctx.data_path)],
|
|
353
|
+
),
|
|
340
354
|
(
|
|
341
355
|
STEP_TOKEN_USAGE,
|
|
342
356
|
[
|
|
@@ -782,6 +796,8 @@ def _run_finalize_step(
|
|
|
782
796
|
"""한 Phase 7 단계를 실행하고 그 단계의 종료 코드만 돌려준다."""
|
|
783
797
|
if name == STEP_PROJECT_ACTIVITY:
|
|
784
798
|
return _run_project_activity(ctx, command)
|
|
799
|
+
if name == STEP_TRANSLATE:
|
|
800
|
+
return _run_translate(ctx, command)
|
|
785
801
|
if name == STEP_TEARDOWN_STAGES:
|
|
786
802
|
return _teardown_stage_worktrees(ctx, command)
|
|
787
803
|
if name == STEP_RECORD_GROUP_MEMORY:
|
|
@@ -801,6 +817,31 @@ def _run_finalize_step(
|
|
|
801
817
|
)
|
|
802
818
|
|
|
803
819
|
|
|
820
|
+
def _run_translate(
|
|
821
|
+
ctx: FinalizeContext,
|
|
822
|
+
command: Sequence[str],
|
|
823
|
+
) -> subprocess.CompletedProcess[str]:
|
|
824
|
+
"""비영어 리포트의 번역 사이드카를 만든다. 영어 리포트는 건너뛴다.
|
|
825
|
+
|
|
826
|
+
실패는 다른 단계처럼 기록만 하고 시퀀스는 계속 돈다 — `render-views` 는
|
|
827
|
+
사이드카 없이 영어 열람본을 내고, `validate-run` 은 권고를 남기며, 결과의
|
|
828
|
+
`--only translate --only render-views …` 재개 힌트가 그 둘을 다시 돌린다.
|
|
829
|
+
"""
|
|
830
|
+
try:
|
|
831
|
+
outcome = translate_report(
|
|
832
|
+
project_root=ctx.project_root,
|
|
833
|
+
workspace_root=ctx.workspace_root,
|
|
834
|
+
manifest_path=ctx.manifest_path,
|
|
835
|
+
manifest=_load_manifest(ctx.manifest_path),
|
|
836
|
+
data_path=ctx.data_path,
|
|
837
|
+
)
|
|
838
|
+
except (FinalizeError, OSError, JsonBoundaryError) as exc:
|
|
839
|
+
outcome = TranslateOutcome(1, "", f"translate failed: {exc}")
|
|
840
|
+
return subprocess.CompletedProcess(
|
|
841
|
+
command, outcome.returncode, outcome.stdout, outcome.stderr
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
|
|
804
845
|
def _run_project_activity(
|
|
805
846
|
ctx: FinalizeContext,
|
|
806
847
|
command: Sequence[str],
|
|
@@ -932,15 +973,21 @@ _CLI_EPILOG = r"""Usage:
|
|
|
932
973
|
okstra report-finalize --project-root <dir> --run-manifest <path> \
|
|
933
974
|
--report <final-report-<task-type>-<seq>.md> [--team-state <path>]
|
|
934
975
|
|
|
935
|
-
Runs the
|
|
976
|
+
Runs the Phase 7 steps in their contractual order against one final-report:
|
|
936
977
|
|
|
937
978
|
1. project-activity project the run's activity events into the data.json
|
|
938
979
|
2. check-source verify the data.json is the English SSOT
|
|
939
|
-
3.
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
980
|
+
3. translate for a non-English reportLanguage, materialize and
|
|
981
|
+
dispatch the translator worker and require its
|
|
982
|
+
*.i18n.<lang>.json sidecar; a no-op for English or
|
|
983
|
+
when the sidecar already exists
|
|
984
|
+
4. token-usage substitute real token/cost numbers into the data.json
|
|
985
|
+
5. render-views write the schema-v2 task-specific *.html sibling with
|
|
986
|
+
the translation overlaid; v1 keeps the
|
|
987
|
+
legacy conditional interactive view
|
|
988
|
+
6. spawn-followups turn section 4 rows into task stubs
|
|
989
|
+
7. validate-run validate the finished run artifacts
|
|
990
|
+
8. record-group-memory / 9. teardown-stages after a clean validation
|
|
944
991
|
|
|
945
992
|
Every step is idempotent, so re-running after a fixed failure is safe. The
|
|
946
993
|
sequence stops at the first non-zero exit and reports which step failed, except
|