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
|
@@ -25,14 +25,6 @@ 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 the `translate` step, directly before `render-views`,
|
|
29
|
-
which overlays it. It used to be a manual lead sequence outside this command (materialize the translator
|
|
30
|
-
prompt, dispatch it, resume finalize at `render-views`), written only in a doc
|
|
31
|
-
the lead lazy-reads; a lead that ran the whole sequence at once skipped it and
|
|
32
|
-
the run kept an English view under a `ko` report (2026-09-09, fontsninja-v3-site
|
|
33
|
-
dev-10628-3). `okstra_ctl.report_translation_dispatch` owns the step; an English
|
|
34
|
-
report or an existing sidecar makes it a no-op.
|
|
35
|
-
|
|
36
28
|
Every lead adapter drives Phase 7 through this module: the Codex adapter calls
|
|
37
29
|
it in-process (``codex_dispatch``), and a Claude-led run reaches the same code
|
|
38
30
|
through the ``okstra report-finalize`` CLI. Neither reimplements the sequence.
|
|
@@ -69,12 +61,11 @@ from .handoff_error import HandoffError
|
|
|
69
61
|
from .paths import RunRef, task_dir, task_manifest_file
|
|
70
62
|
from .report_view_artifacts import html_view_path
|
|
71
63
|
from .release_gate import release_handoff_allowed
|
|
72
|
-
from .report_translation_dispatch import TranslateOutcome, translate_report
|
|
73
64
|
from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
74
65
|
from .stage_integrate import IntegrateError
|
|
75
66
|
from .stage_targets import (
|
|
76
67
|
StageTargetError,
|
|
77
|
-
|
|
68
|
+
teardown_completed_stage_worktrees,
|
|
78
69
|
)
|
|
79
70
|
from .session import observe_lead_session
|
|
80
71
|
from .lead_progress import record_checkpoint
|
|
@@ -92,7 +83,6 @@ from okstra_project.phase_pointer import (
|
|
|
92
83
|
|
|
93
84
|
|
|
94
85
|
STEP_PROJECT_ACTIVITY = "project-activity"
|
|
95
|
-
STEP_TRANSLATE = "translate"
|
|
96
86
|
STEP_TOKEN_USAGE = "token-usage"
|
|
97
87
|
STEP_RENDER_VIEWS = "render-views"
|
|
98
88
|
STEP_SPAWN_FOLLOWUPS = "spawn-followups"
|
|
@@ -105,7 +95,6 @@ STEP_PRUNE_RUN_ARTIFACTS = "prune-run-artifacts"
|
|
|
105
95
|
|
|
106
96
|
STEP_ORDER = (
|
|
107
97
|
STEP_PROJECT_ACTIVITY,
|
|
108
|
-
STEP_TRANSLATE,
|
|
109
98
|
STEP_TOKEN_USAGE,
|
|
110
99
|
STEP_RENDER_VIEWS,
|
|
111
100
|
STEP_SPAWN_FOLLOWUPS,
|
|
@@ -124,8 +113,6 @@ V3_STEP_ORDER = (
|
|
|
124
113
|
STEP_TOKEN_USAGE,
|
|
125
114
|
STEP_PROJECT_ACTIVITY,
|
|
126
115
|
STEP_PREFLIGHT,
|
|
127
|
-
# Before the render that overlays its sidecar.
|
|
128
|
-
STEP_TRANSLATE,
|
|
129
116
|
STEP_RENDER_VIEWS,
|
|
130
117
|
STEP_SPAWN_FOLLOWUPS,
|
|
131
118
|
# Needs the assembled record; `validate-run` requires the row it writes.
|
|
@@ -341,10 +328,6 @@ def build_commands(ctx: FinalizeContext) -> list[tuple[str, list[str]]]:
|
|
|
341
328
|
str(ctx.data_path),
|
|
342
329
|
],
|
|
343
330
|
),
|
|
344
|
-
(
|
|
345
|
-
STEP_TRANSLATE,
|
|
346
|
-
["<in-process>", "translate", str(ctx.data_path)],
|
|
347
|
-
),
|
|
348
331
|
(
|
|
349
332
|
STEP_TOKEN_USAGE,
|
|
350
333
|
[
|
|
@@ -571,38 +554,53 @@ def _teardown_stage_worktrees(
|
|
|
571
554
|
ctx: FinalizeContext,
|
|
572
555
|
command: list[str],
|
|
573
556
|
) -> subprocess.CompletedProcess:
|
|
574
|
-
"""
|
|
575
|
-
|
|
576
|
-
whole-task 진입이 통합만 하고 정리를 남겨두므로(`stage_targets`), 정리는 판정이
|
|
577
|
-
나온 뒤인 여기서 한다. `blocked` 이거나 릴리스를 막는 조건이 남은 판정에서는
|
|
578
|
-
stage 작업물을 그대로 둬서 재작업이 바로 이어지게 한다. 이미 정리된 뒤 재실행돼도
|
|
579
|
-
같은 결과를 낸다 — 병합은 `already_merged` 로, 없는 worktree 는 건너뛴다.
|
|
580
|
-
"""
|
|
557
|
+
"""검증된 보고서와 완료 기록이 일치하는 단계 작업 트리를 회수한다."""
|
|
581
558
|
def _done(payload: Mapping[str, Any]) -> subprocess.CompletedProcess:
|
|
582
559
|
return subprocess.CompletedProcess(command, 0, json.dumps(payload), "")
|
|
583
560
|
|
|
584
|
-
if ctx.task_type
|
|
585
|
-
return _done({"skipped": "not
|
|
561
|
+
if ctx.task_type not in {"implementation", "final-verification"}:
|
|
562
|
+
return _done({"skipped": "not an implementation or final-verification run"})
|
|
586
563
|
try:
|
|
587
564
|
data = load_owned_object(Path(ctx.data_path), artifact="final report record")
|
|
588
565
|
except JsonBoundaryError as exc:
|
|
589
566
|
return subprocess.CompletedProcess(
|
|
590
567
|
command, 1, "", f"cannot read final-report data.json: {exc}")
|
|
591
|
-
if
|
|
592
|
-
return _done({"skipped": "single-stage verification owns no teardown"})
|
|
593
|
-
if not release_handoff_allowed(data):
|
|
568
|
+
if ctx.task_type == "final-verification" and not release_handoff_allowed(data):
|
|
594
569
|
return _done({"skipped": "verdict does not clear the work for release"})
|
|
570
|
+
heads = _stage_cleanup_heads(ctx.task_type, data)
|
|
571
|
+
if heads == {}:
|
|
572
|
+
return _done({"skipped": "report names no completed stage commit"})
|
|
595
573
|
try:
|
|
596
|
-
result =
|
|
574
|
+
result = teardown_completed_stage_worktrees(
|
|
597
575
|
project_root=ctx.project_root,
|
|
598
576
|
task_group=ctx.task_group,
|
|
599
577
|
task_id=ctx.task_id,
|
|
578
|
+
expected_heads=heads,
|
|
600
579
|
)
|
|
601
|
-
except (IntegrateError, StageTargetError, OSError) as exc:
|
|
580
|
+
except (IntegrateError, StageTargetError, JsonBoundaryError, OSError) as exc:
|
|
602
581
|
return subprocess.CompletedProcess(command, 1, "", str(exc))
|
|
603
582
|
return _done(result)
|
|
604
583
|
|
|
605
584
|
|
|
585
|
+
def _stage_cleanup_heads(task_type: str, data: Mapping[str, Any]) -> dict[int, str] | None:
|
|
586
|
+
if task_type == "implementation":
|
|
587
|
+
implementation = data.get("implementation") or {}
|
|
588
|
+
evidence = implementation.get("stageSidecarEvidence") or {}
|
|
589
|
+
stage = evidence.get("stageNumber")
|
|
590
|
+
commits = implementation.get("commitList") or []
|
|
591
|
+
head = commits[-1].get("fullSha", "") if commits else ""
|
|
592
|
+
return {stage: head} if isinstance(stage, int) and head and not evidence.get("withheld") else {}
|
|
593
|
+
if data.get("verificationScope") == "whole-task":
|
|
594
|
+
return None
|
|
595
|
+
verification = data.get("finalVerification") or {}
|
|
596
|
+
stages = verification.get("stageReports") or []
|
|
597
|
+
head = (verification.get("sourceImplementationReport") or {}).get("capturedHeadSha", "")
|
|
598
|
+
if data.get("verificationScope") == "single-stage" and len(stages) == 1 and head:
|
|
599
|
+
stage = stages[0].get("stage")
|
|
600
|
+
return {stage: head} if isinstance(stage, int) else {}
|
|
601
|
+
return {}
|
|
602
|
+
|
|
603
|
+
|
|
606
604
|
def _prune_run_artifacts(
|
|
607
605
|
ctx: FinalizeContext,
|
|
608
606
|
command: list[str],
|
|
@@ -684,8 +682,6 @@ def run_finalize(
|
|
|
684
682
|
if only:
|
|
685
683
|
selected = set(only)
|
|
686
684
|
if ctx.report_contract_version == "3.0":
|
|
687
|
-
if STEP_TRANSLATE in selected:
|
|
688
|
-
selected.add(STEP_PREFLIGHT)
|
|
689
685
|
if selected - {STEP_TOKEN_USAGE}:
|
|
690
686
|
# 부분 재개도 이전 조립본이 아닌 현재 소유자 입력을 소비한다.
|
|
691
687
|
selected.add(STEP_PROJECT_ACTIVITY)
|
|
@@ -799,7 +795,6 @@ def _execute_finalize_steps(
|
|
|
799
795
|
if consumer != STEP_TOKEN_USAGE or ctx.report_contract_version != "3.0":
|
|
800
796
|
blocked[consumer] = name
|
|
801
797
|
elif name == STEP_PREFLIGHT:
|
|
802
|
-
blocked[STEP_TRANSLATE] = name
|
|
803
798
|
blocked[STEP_SPAWN_FOLLOWUPS] = name
|
|
804
799
|
elif name == STEP_RENDER_VIEWS:
|
|
805
800
|
blocked[STEP_VALIDATE_RUN] = name
|
|
@@ -865,8 +860,6 @@ def _finalize_recovery(ctx: FinalizeContext, result: Mapping[str, Any]) -> dict[
|
|
|
865
860
|
"Repair the reported causes in this run using the owning input's correction command, "
|
|
866
861
|
"then execute resumeCommand. Preserve approvals, model choices and completed evidence. "
|
|
867
862
|
"If plan content changes, re-verify the affected items before finalizing. "
|
|
868
|
-
"After a narrative correction, check existing translations with report-translate check-data "
|
|
869
|
-
"and regenerate them if their source no longer matches. "
|
|
870
863
|
"Ask only for an unresolved user decision or an actual external prerequisite."
|
|
871
864
|
),
|
|
872
865
|
}
|
|
@@ -1018,8 +1011,6 @@ def _run_finalize_step(
|
|
|
1018
1011
|
"""한 Phase 7 단계를 실행하고 그 단계의 종료 코드만 돌려준다."""
|
|
1019
1012
|
if name == STEP_PROJECT_ACTIVITY:
|
|
1020
1013
|
return _run_project_activity(ctx, command)
|
|
1021
|
-
if name == STEP_TRANSLATE:
|
|
1022
|
-
return _run_translate(ctx, command)
|
|
1023
1014
|
if name == STEP_TEARDOWN_STAGES:
|
|
1024
1015
|
return _teardown_stage_worktrees(ctx, command)
|
|
1025
1016
|
if name == STEP_PRUNE_RUN_ARTIFACTS:
|
|
@@ -1043,31 +1034,6 @@ def _run_finalize_step(
|
|
|
1043
1034
|
)
|
|
1044
1035
|
|
|
1045
1036
|
|
|
1046
|
-
def _run_translate(
|
|
1047
|
-
ctx: FinalizeContext,
|
|
1048
|
-
command: Sequence[str],
|
|
1049
|
-
) -> subprocess.CompletedProcess[str]:
|
|
1050
|
-
"""비영어 리포트의 번역 사이드카를 만든다. 영어 리포트는 건너뛴다.
|
|
1051
|
-
|
|
1052
|
-
실패는 다른 단계처럼 기록만 하고 시퀀스는 계속 돈다 — `render-views` 는
|
|
1053
|
-
사이드카 없이 영어 열람본을 내고, `validate-run` 은 권고를 남기며, 결과의
|
|
1054
|
-
`--only translate --only render-views …` 재개 힌트가 그 둘을 다시 돌린다.
|
|
1055
|
-
"""
|
|
1056
|
-
try:
|
|
1057
|
-
outcome = translate_report(
|
|
1058
|
-
project_root=ctx.project_root,
|
|
1059
|
-
workspace_root=ctx.workspace_root,
|
|
1060
|
-
manifest_path=ctx.manifest_path,
|
|
1061
|
-
manifest=_load_manifest(ctx.manifest_path),
|
|
1062
|
-
data_path=ctx.data_path,
|
|
1063
|
-
)
|
|
1064
|
-
except (FinalizeError, OSError, JsonBoundaryError) as exc:
|
|
1065
|
-
outcome = TranslateOutcome(1, "", f"translate failed: {exc}")
|
|
1066
|
-
return subprocess.CompletedProcess(
|
|
1067
|
-
command, outcome.returncode, outcome.stdout, outcome.stderr
|
|
1068
|
-
)
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
1037
|
def _run_project_activity(
|
|
1072
1038
|
ctx: FinalizeContext,
|
|
1073
1039
|
command: Sequence[str],
|
|
@@ -1206,18 +1172,13 @@ _CLI_EPILOG = r"""Usage:
|
|
|
1206
1172
|
Runs the Phase 7 steps in their contractual order against one final-report:
|
|
1207
1173
|
|
|
1208
1174
|
1. project-activity project the run's activity events into the data.json
|
|
1209
|
-
2.
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
legacy conditional interactive view
|
|
1217
|
-
5. spawn-followups turn section 4 rows into task stubs
|
|
1218
|
-
6. validate-run validate the finished run artifacts
|
|
1219
|
-
7. record-group-memory / 8. teardown-stages after a clean validation
|
|
1220
|
-
9. prune-run-artifacts remove node_modules and .next under this run
|
|
1175
|
+
2. token-usage substitute real token/cost numbers into the data.json
|
|
1176
|
+
3. render-views write the schema-v2 task-specific *.html sibling;
|
|
1177
|
+
v1 keeps the legacy conditional interactive view
|
|
1178
|
+
4. spawn-followups turn section 4 rows into task stubs
|
|
1179
|
+
5. validate-run validate the finished run artifacts
|
|
1180
|
+
6. record-group-memory / 7. teardown-stages after a clean validation
|
|
1181
|
+
8. prune-run-artifacts remove node_modules and .next under this run
|
|
1221
1182
|
directory, and the dispatch snapshots and publication
|
|
1222
1183
|
locks of a validated run; runs after a failure too
|
|
1223
1184
|
|
|
@@ -3,16 +3,14 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import hashlib
|
|
5
5
|
import json
|
|
6
|
-
import sys
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
|
|
9
8
|
import okstra_vendor # noqa: F401 # registers vendored dependency aliases
|
|
10
9
|
from jinja2 import Environment, StrictUndefined, select_autoescape
|
|
11
10
|
|
|
12
|
-
from ..final_report_paths import final_report_data_path
|
|
11
|
+
from ..final_report_paths import final_report_data_path
|
|
13
12
|
from ..final_report_schema import load_schema_for_data, validate
|
|
14
13
|
from ..i18n import HTML_DICTIONARY_REL, load_dictionary, make_jinja_global
|
|
15
|
-
from ..report_translation import overlay
|
|
16
14
|
from ..report_view_artifacts import user_responses_dir_for_report
|
|
17
15
|
from ..usage_cells import format_duration_ms
|
|
18
16
|
from ..json_boundary import load_owned_object
|
|
@@ -76,44 +74,8 @@ def _report_meta(data: dict, run_meta: HtmlRunMeta) -> dict[str, object]:
|
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
|
|
79
|
-
def
|
|
80
|
-
""
|
|
81
|
-
|
|
82
|
-
data.json 은 영어 정본이고 고치지 않는다. 사람이 읽는 HTML 만 독자 언어다.
|
|
83
|
-
사이드카가 빠진 포인터는 영어로 남긴다.
|
|
84
|
-
|
|
85
|
-
사이드카 파일 자체가 없으면 요청 언어를 버리고 전부 영어로 렌더한다.
|
|
86
|
-
종전에는 여기서 렌더를 거부했다 — 크롬만 번역된 반쪽 페이지를 내보내지
|
|
87
|
-
않겠다는 이유였고, 그 판단 자체는 옳다. 틀린 것은 남은 선택지였다: 번역
|
|
88
|
-
워커가 못 도는 이유(호스트 승인 게이트, 공급자 부재)는 리포트의 품질과
|
|
89
|
-
무관한데, 거부는 열람본을 통째로 없애고 `validate-run` 이 그 부재를 차단
|
|
90
|
-
실패로 삼아 run 을 `contract-violated` 로 끝냈다 — 분석도 리포트도 끝난
|
|
91
|
-
run 이 그렇게 끝났다(2026-09-08 FontsNinja/app/jobs dev-10784
|
|
92
|
-
error-analysis). 언어를 낮추면 크롬과 본문이 같은 영어라 반쪽 페이지도
|
|
93
|
-
아니고, 독자는 읽을 것을 갖는다. 번역이 나중에 도착하면 다시 렌더해
|
|
94
|
-
덮어쓴다.
|
|
95
|
-
"""
|
|
96
|
-
lang = str((data.get("meta") or {}).get("reportLanguage") or "en")
|
|
97
|
-
if lang == "en":
|
|
98
|
-
return data, lang
|
|
99
|
-
sidecar_file = translation_sidecar_path(data_path, lang)
|
|
100
|
-
if not sidecar_file.is_file():
|
|
101
|
-
sys.stdout.write(
|
|
102
|
-
f"no {lang} translation sidecar at {sidecar_file.name}; "
|
|
103
|
-
"rendering the English source\n"
|
|
104
|
-
)
|
|
105
|
-
return data, "en"
|
|
106
|
-
payload = load_owned_object(sidecar_file, artifact="translation sidecar")
|
|
107
|
-
strings = payload.get("strings")
|
|
108
|
-
if not isinstance(strings, dict):
|
|
109
|
-
raise HtmlRenderError(f"translation sidecar has no 'strings' object: {sidecar_file}")
|
|
110
|
-
localized, report = overlay(data, strings)
|
|
111
|
-
sys.stdout.write(
|
|
112
|
-
f"translated {report.applied} string(s) into {lang}"
|
|
113
|
-
f" ({len(report.untranslated)} left in English"
|
|
114
|
-
f", {len(report.unresolved)} unresolved)\n"
|
|
115
|
-
)
|
|
116
|
-
return localized, lang
|
|
77
|
+
def _report_language(data: dict) -> str:
|
|
78
|
+
return str((data.get("meta") or {}).get("reportLanguage") or "en")
|
|
117
79
|
|
|
118
80
|
|
|
119
81
|
def _html_path(data_path: Path) -> Path:
|
|
@@ -140,9 +102,7 @@ def render_v2_html_view(
|
|
|
140
102
|
errors = validate(data, load_schema_for_data(data))
|
|
141
103
|
if errors:
|
|
142
104
|
raise HtmlRenderError("invalid v2 final-report data: " + "; ".join(errors[:5]))
|
|
143
|
-
|
|
144
|
-
# has no say in whether the report is well-formed.
|
|
145
|
-
data, lang = _localize(data, data_path)
|
|
105
|
+
lang = _report_language(data)
|
|
146
106
|
route = resolve_html_route(run_meta.task_type)
|
|
147
107
|
view = route.view_builder(data)
|
|
148
108
|
root = _templates_root(templates_root)
|
|
@@ -6,9 +6,9 @@ the run manifest, which report assembly copies into `meta.reportLanguage` and
|
|
|
6
6
|
the HTML renderer reads. When only the prompt called this, a project could ask
|
|
7
7
|
for `ko`, get a Korean-instructed writer, and still ship an English report.
|
|
8
8
|
|
|
9
|
-
The value is a language tag (`en`, `ko`, `fr`, `pt-BR`, …). The
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
The value is a language tag (`en`, `ko`, `fr`, `pt-BR`, …). The report writer
|
|
10
|
+
authors the report prose in that language; nothing here knows any particular
|
|
11
|
+
language. Precedence: project config → global config → `en`.
|
|
12
12
|
"""
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Final-report leaf keys whose values a reader reads as prose and nothing else.
|
|
2
|
+
|
|
3
|
+
A value under one of these keys is displayed text; ids, enum values, paths and
|
|
4
|
+
anchor targets live under other keys.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
PROSE_KEYS = frozenset({
|
|
9
|
+
"acceptance",
|
|
10
|
+
"action",
|
|
11
|
+
"actions",
|
|
12
|
+
"addedWork",
|
|
13
|
+
"alternativesConsidered",
|
|
14
|
+
"answer",
|
|
15
|
+
"approach",
|
|
16
|
+
"approvalDisposition",
|
|
17
|
+
"approvalEvidence",
|
|
18
|
+
"architectureBoundaries",
|
|
19
|
+
"assumptions",
|
|
20
|
+
"behavior",
|
|
21
|
+
"blastRadius",
|
|
22
|
+
"blockReason",
|
|
23
|
+
"blockedReason",
|
|
24
|
+
"blockers",
|
|
25
|
+
"body",
|
|
26
|
+
"boundary",
|
|
27
|
+
"briefEvidence",
|
|
28
|
+
"canonical",
|
|
29
|
+
"carryIn",
|
|
30
|
+
"causeSummary",
|
|
31
|
+
"change",
|
|
32
|
+
"check",
|
|
33
|
+
"claim",
|
|
34
|
+
"commitment",
|
|
35
|
+
"complexity",
|
|
36
|
+
"condition",
|
|
37
|
+
"confirmingSignal",
|
|
38
|
+
"conformanceExemption",
|
|
39
|
+
"consequences",
|
|
40
|
+
"constraint",
|
|
41
|
+
"context",
|
|
42
|
+
"coreMechanism",
|
|
43
|
+
"coreReason",
|
|
44
|
+
"counterevidence",
|
|
45
|
+
"decision",
|
|
46
|
+
"decisions",
|
|
47
|
+
"declinedFixRecommendations",
|
|
48
|
+
"description",
|
|
49
|
+
"details",
|
|
50
|
+
"directionChange",
|
|
51
|
+
"disagreement",
|
|
52
|
+
"discrepancy",
|
|
53
|
+
"disproveWith",
|
|
54
|
+
"environmentSetup",
|
|
55
|
+
"escalationTrigger",
|
|
56
|
+
"evidence",
|
|
57
|
+
"evidenceRequired",
|
|
58
|
+
"exemptionReaffirm",
|
|
59
|
+
"exemptionReason",
|
|
60
|
+
"exitContract",
|
|
61
|
+
"exitContractSummary",
|
|
62
|
+
"expected",
|
|
63
|
+
"expectedBehaviorAfter",
|
|
64
|
+
"expectedChangeAreas",
|
|
65
|
+
"expectedForm",
|
|
66
|
+
"expectedOutcome",
|
|
67
|
+
"expectedResult",
|
|
68
|
+
"expectedVerification",
|
|
69
|
+
"explanation",
|
|
70
|
+
"fact",
|
|
71
|
+
"failureHandling",
|
|
72
|
+
"falsifyingEvidenceChecked",
|
|
73
|
+
"finalConclusion",
|
|
74
|
+
"goal",
|
|
75
|
+
"guardrails",
|
|
76
|
+
"headline",
|
|
77
|
+
"howToStart",
|
|
78
|
+
"hypothesis",
|
|
79
|
+
"ifStillOpen",
|
|
80
|
+
"injectedAs",
|
|
81
|
+
"interfaces",
|
|
82
|
+
"item",
|
|
83
|
+
"justification",
|
|
84
|
+
"knownFacts",
|
|
85
|
+
"label",
|
|
86
|
+
"mitigation",
|
|
87
|
+
"motivation",
|
|
88
|
+
"name",
|
|
89
|
+
"need",
|
|
90
|
+
"nextStep",
|
|
91
|
+
"noVariationRationale",
|
|
92
|
+
"notApplicableReason",
|
|
93
|
+
"note",
|
|
94
|
+
"observableFailure",
|
|
95
|
+
"observed",
|
|
96
|
+
"option",
|
|
97
|
+
"outcome",
|
|
98
|
+
"outputSummary",
|
|
99
|
+
"performed",
|
|
100
|
+
"planItem",
|
|
101
|
+
"position",
|
|
102
|
+
"problem",
|
|
103
|
+
"question",
|
|
104
|
+
"rationale",
|
|
105
|
+
"readyWhen",
|
|
106
|
+
"reason",
|
|
107
|
+
"rejectedSummary",
|
|
108
|
+
"rejectingSignal",
|
|
109
|
+
"relation",
|
|
110
|
+
"replacedInTest",
|
|
111
|
+
"replacedWith",
|
|
112
|
+
"requestKind",
|
|
113
|
+
"requiredDecision",
|
|
114
|
+
"requiredWork",
|
|
115
|
+
"requirement",
|
|
116
|
+
"resolution",
|
|
117
|
+
"responsibility",
|
|
118
|
+
"reversibility",
|
|
119
|
+
"risk",
|
|
120
|
+
"rolloutCost",
|
|
121
|
+
"routingRecommendation",
|
|
122
|
+
"satisfaction",
|
|
123
|
+
"scope",
|
|
124
|
+
"selfFixNote",
|
|
125
|
+
"sliceValue",
|
|
126
|
+
"stageTitle",
|
|
127
|
+
"stageValidation",
|
|
128
|
+
"statement",
|
|
129
|
+
"suggestedAction",
|
|
130
|
+
"summary",
|
|
131
|
+
"supersededStatement",
|
|
132
|
+
"symptom",
|
|
133
|
+
"systemInterpretation",
|
|
134
|
+
"testCaseBoundary",
|
|
135
|
+
"testCaseFailure",
|
|
136
|
+
"testCaseSuccess",
|
|
137
|
+
"testCoverageCost",
|
|
138
|
+
"text",
|
|
139
|
+
"title",
|
|
140
|
+
"topic",
|
|
141
|
+
"trigger",
|
|
142
|
+
"triggerSignal",
|
|
143
|
+
"unblockCondition",
|
|
144
|
+
"userChoice",
|
|
145
|
+
"userConstraints",
|
|
146
|
+
"userInput",
|
|
147
|
+
"userText",
|
|
148
|
+
"verification",
|
|
149
|
+
"verificationMethod",
|
|
150
|
+
"verificationSignal",
|
|
151
|
+
"whyItMatters",
|
|
152
|
+
"workingAssumption",
|
|
153
|
+
})
|
|
@@ -866,7 +866,7 @@ def _attempt_result_specs(
|
|
|
866
866
|
for row in invocations_value
|
|
867
867
|
if isinstance(row, Mapping) and _string(row.get("invocationRef"))
|
|
868
868
|
}
|
|
869
|
-
excluded_duties = {"lead", "report-writer"
|
|
869
|
+
excluded_duties = {"lead", "report-writer"}
|
|
870
870
|
specs: list[tuple[str, str, str, bool]] = []
|
|
871
871
|
seen: set[str] = set()
|
|
872
872
|
for attempt in attempts_value:
|
|
@@ -1896,19 +1896,12 @@ def _build_static_execution_manifest(
|
|
|
1896
1896
|
plan: AssignmentPlan,
|
|
1897
1897
|
context: AssignmentContext,
|
|
1898
1898
|
lead: ResolvedAssignment,
|
|
1899
|
-
translator: ResolvedAssignment,
|
|
1900
1899
|
) -> ExecutionManifest:
|
|
1901
1900
|
participants: list[ParticipantAssignment] = []
|
|
1902
1901
|
roles: list[RoleExecution] = []
|
|
1903
|
-
# The translator is resolved outside the role plan, like the lead, and both
|
|
1904
|
-
# are named in `invocationAssignments`. Without a role execution here,
|
|
1905
|
-
# `agent-prompt materialize --audience translator` has no canonical
|
|
1906
|
-
# identity to bind to and refuses — so a `reportLanguage: ko` run could
|
|
1907
|
-
# never write its translation sidecar and rendered the English source.
|
|
1908
1902
|
assignments = (
|
|
1909
1903
|
lead,
|
|
1910
1904
|
*(row for row in plan.assignments if row.role != "leader"),
|
|
1911
|
-
translator,
|
|
1912
1905
|
)
|
|
1913
1906
|
for index, assignment in enumerate(assignments, start=1):
|
|
1914
1907
|
participant_ref = f"participant-{index:03d}"
|
|
@@ -2387,7 +2380,6 @@ class _ModelBindings:
|
|
|
2387
2380
|
lead_assignment: RoleAssignment
|
|
2388
2381
|
worker_assignments: tuple[RoleAssignment, ...]
|
|
2389
2382
|
invocation_assignments: dict[str, dict[str, object]]
|
|
2390
|
-
translator: ResolvedAssignment
|
|
2391
2383
|
|
|
2392
2384
|
|
|
2393
2385
|
@dataclass(frozen=True)
|
|
@@ -2622,13 +2614,6 @@ def _project_model_bindings(
|
|
|
2622
2614
|
if executor_rows
|
|
2623
2615
|
else None
|
|
2624
2616
|
)
|
|
2625
|
-
translator = _model_for_role(
|
|
2626
|
-
context=context,
|
|
2627
|
-
provider=report_writer.provider_id,
|
|
2628
|
-
raw_value=report_writer.model_id,
|
|
2629
|
-
role="translator",
|
|
2630
|
-
duty_id="translator",
|
|
2631
|
-
)
|
|
2632
2617
|
projections = {
|
|
2633
2618
|
provider: _legacy_projection_from_plan(provider, plan, context)
|
|
2634
2619
|
for provider in ("claude", "codex", "antigravity")
|
|
@@ -2637,7 +2622,6 @@ def _project_model_bindings(
|
|
|
2637
2622
|
lead_assignment,
|
|
2638
2623
|
workers,
|
|
2639
2624
|
critic_assignment,
|
|
2640
|
-
_role_assignment(resolved=translator, role="translator"),
|
|
2641
2625
|
inp.task_type,
|
|
2642
2626
|
)
|
|
2643
2627
|
worker_by_provider = {row.provider: row for row in workers}
|
|
@@ -2668,7 +2652,6 @@ def _project_model_bindings(
|
|
|
2668
2652
|
lead_assignment=lead_assignment,
|
|
2669
2653
|
worker_assignments=workers,
|
|
2670
2654
|
invocation_assignments=invocation_assignments,
|
|
2671
|
-
translator=translator,
|
|
2672
2655
|
)
|
|
2673
2656
|
|
|
2674
2657
|
|
|
@@ -2759,7 +2742,6 @@ def _build_invocation_assignments(
|
|
|
2759
2742
|
lead: RoleAssignment,
|
|
2760
2743
|
workers: tuple[RoleAssignment, ...],
|
|
2761
2744
|
critic: RoleAssignment | None,
|
|
2762
|
-
translator: RoleAssignment,
|
|
2763
2745
|
task_type: str,
|
|
2764
2746
|
) -> dict[str, dict[str, object]]:
|
|
2765
2747
|
assignments = {"lead": lead.to_model_payload()}
|
|
@@ -2776,7 +2758,6 @@ def _build_invocation_assignments(
|
|
|
2776
2758
|
# role 라벨을 만들기 때문에 두 행의 이름이 겹쳐 validate-run 이
|
|
2777
2759
|
# `duplicate worker role detected` 로 런을 실패시킨다.
|
|
2778
2760
|
assignments[critic_assignment_ref(task_type)] = critic.to_model_payload()
|
|
2779
|
-
assignments["translator"] = translator.to_model_payload()
|
|
2780
2761
|
return assignments
|
|
2781
2762
|
|
|
2782
2763
|
|
|
@@ -3660,7 +3641,7 @@ def _allowed_agent_audiences(
|
|
|
3660
3641
|
inp: PrepareInputs,
|
|
3661
3642
|
models: _ModelBindings,
|
|
3662
3643
|
) -> list[str]:
|
|
3663
|
-
audiences = {"lead"
|
|
3644
|
+
audiences = {"lead"}
|
|
3664
3645
|
if models.worker_assignments:
|
|
3665
3646
|
audiences.add("reverification-worker")
|
|
3666
3647
|
if any(item.worker_id == "report-writer" for item in models.worker_assignments):
|
|
@@ -4162,7 +4143,6 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
4162
4143
|
assignment_plan,
|
|
4163
4144
|
assignment_context,
|
|
4164
4145
|
models.lead,
|
|
4165
|
-
models.translator,
|
|
4166
4146
|
)
|
|
4167
4147
|
dynamic_roles = tuple(
|
|
4168
4148
|
requirement.role
|
|
@@ -283,8 +283,14 @@ def _teardown_all(project_id, task_group, task_id, task_wt: str,
|
|
|
283
283
|
|
|
284
284
|
def _remove_clean_worktree(task_wt: str, wt: str, stage_n: int) -> tuple[str, str] | None:
|
|
285
285
|
"""clean stage worktree 를 물리 제거. dirty/실패면 (outcome, detail), 성공이면 None."""
|
|
286
|
-
from .worktree import
|
|
287
|
-
|
|
286
|
+
from .worktree import (
|
|
287
|
+
is_dirty_excluding_okstra,
|
|
288
|
+
nested_worktree_excludes,
|
|
289
|
+
remove_worktree_force,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
if nested_worktree_excludes(wt):
|
|
293
|
+
return ("skipped", "nested worktrees remain — preserved")
|
|
288
294
|
if is_dirty_excluding_okstra(wt):
|
|
289
295
|
return ("skipped", "dirty — 미커밋 변경 보존")
|
|
290
296
|
rm = remove_worktree_force(task_wt, wt)
|
|
@@ -293,6 +299,35 @@ def _remove_clean_worktree(task_wt: str, wt: str, stage_n: int) -> tuple[str, st
|
|
|
293
299
|
return None
|
|
294
300
|
|
|
295
301
|
|
|
302
|
+
def reclaim_completed_stage(
|
|
303
|
+
project_root: Path, project_id: str, task_group: str, task_id: str,
|
|
304
|
+
stage: int, expected_head: str, done: dict | None, status: str,
|
|
305
|
+
) -> tuple[str, str]:
|
|
306
|
+
from . import worktree_registry
|
|
307
|
+
from .worktree import main_worktree_path
|
|
308
|
+
|
|
309
|
+
row = worktree_registry.get_stage_row(project_id, task_group, task_id, stage)
|
|
310
|
+
if not row:
|
|
311
|
+
return "skipped", "stage worktree is not registered"
|
|
312
|
+
if status != "done" or row.get("status") == "active":
|
|
313
|
+
return "skipped", "stage is unfinished or active"
|
|
314
|
+
if not expected_head or (done or {}).get("head_commit") != expected_head:
|
|
315
|
+
return "skipped", "completion commit does not match this report"
|
|
316
|
+
tip = _git(project_root, "rev-parse", "--verify", f"refs/heads/{row['branch']}")
|
|
317
|
+
if tip.returncode or tip.stdout.strip() != expected_head:
|
|
318
|
+
return "skipped", "stage branch moved after completion"
|
|
319
|
+
wt = Path(row["worktree_path"])
|
|
320
|
+
if wt.resolve() == main_worktree_path(project_root).resolve():
|
|
321
|
+
return "skipped", "main worktree is preserved"
|
|
322
|
+
if wt.exists():
|
|
323
|
+
current = _git(wt, "symbolic-ref", "--short", "HEAD")
|
|
324
|
+
if current.returncode or current.stdout.strip() != row["branch"]:
|
|
325
|
+
return "skipped", "worktree no longer holds the registered stage branch"
|
|
326
|
+
return _teardown_stage(
|
|
327
|
+
project_id, task_group, task_id, str(project_root), stage, row, expected_head,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
296
331
|
def _unmerged_branch_tip_warning(task_wt: str, stage_n: int, branch: str,
|
|
297
332
|
verified_head: str = "") -> str:
|
|
298
333
|
"""Warn text when the stage branch tip is not an ancestor of the commit
|