okstra 0.219.0 → 0.220.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/cli-registry.mjs +18 -10
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/lifecycle/config.mjs +2 -3
- package/dist/commands/lifecycle/config.mjs.map +1 -1
- package/dist/lib/runtime-payload.mjs +1 -2
- package/dist/lib/runtime-payload.mjs.map +1 -1
- package/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +9 -7
- package/docs/cli.md +5 -3
- package/docs/project-structure-overview.md +3 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +5 -4
- package/runtime/prompts/lead/report-writer.md +10 -17
- package/runtime/prompts/lead/team-contract.md +1 -2
- package/runtime/prompts/wizard/prompts.ko.json +0 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +1 -1
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +0 -1
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +1 -1
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +0 -1
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +0 -1
- package/runtime/python/okstra_ctl/agent/invocation.py +1 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +2 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/jobs.py +0 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +2 -27
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -4
- package/runtime/python/okstra_ctl/blocking_checks.py +8 -0
- package/runtime/python/okstra_ctl/convergence_provenance.py +8 -2
- package/runtime/python/okstra_ctl/correction_cli.py +59 -0
- package/runtime/python/okstra_ctl/correction_round.py +168 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +21 -349
- package/runtime/python/okstra_ctl/dispatch_state.py +0 -13
- package/runtime/python/okstra_ctl/domain/provider.py +1 -1
- package/runtime/python/okstra_ctl/domain/role.py +0 -6
- package/runtime/python/okstra_ctl/final_report_paths.py +0 -16
- package/runtime/python/okstra_ctl/i18n.py +2 -2
- package/runtime/python/okstra_ctl/path_hints.py +0 -4
- package/runtime/python/okstra_ctl/paths.py +0 -3
- package/runtime/python/okstra_ctl/phases/final_verification/profile.md +1 -0
- package/runtime/python/okstra_ctl/phases/final_verification/spec.md +2 -0
- package/runtime/python/okstra_ctl/phases/final_verification/target.py +17 -1
- package/runtime/python/okstra_ctl/phases/implementation/entry.py +12 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +30 -6
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md +1 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md +2 -0
- package/runtime/python/okstra_ctl/phases/implementation/profile.md +2 -1
- package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation.template.html +6 -0
- package/runtime/python/okstra_ctl/phases/implementation/spec.md +6 -1
- package/runtime/python/okstra_ctl/phases/implementation/validation.py +62 -1
- package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +1 -1
- package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +24 -3
- package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +9 -9
- package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +4 -2
- package/runtime/python/okstra_ctl/render.py +0 -10
- package/runtime/python/okstra_ctl/render_final_report.py +4 -4
- package/runtime/python/okstra_ctl/report_finalize.py +37 -76
- package/runtime/python/okstra_ctl/report_html/render.py +4 -44
- package/runtime/python/okstra_ctl/report_language.py +3 -3
- package/runtime/python/okstra_ctl/report_prose_keys.py +153 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +1 -1
- package/runtime/python/okstra_ctl/run.py +1 -21
- package/runtime/python/okstra_ctl/stage_done.py +183 -0
- package/runtime/python/okstra_ctl/stage_fail.py +114 -0
- package/runtime/python/okstra_ctl/stage_integrate.py +37 -2
- package/runtime/python/okstra_ctl/stage_rebase.py +189 -0
- package/runtime/python/okstra_ctl/stage_reopen.py +12 -8
- package/runtime/python/okstra_ctl/stage_targets.py +29 -62
- package/runtime/python/okstra_ctl/team.py +1 -6
- package/runtime/python/okstra_ctl/wizard/confirmation.py +0 -8
- package/runtime/python/okstra_ctl/wizard/registry.py +2 -3
- package/runtime/python/okstra_ctl/wizard/sources.py +10 -0
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +3 -12
- package/runtime/python/okstra_ctl/worktree/__init__.py +1 -1
- package/runtime/python/okstra_ctl/worktree/decisions.py +3 -4
- package/runtime/python/okstra_ctl/worktree/provision.py +27 -0
- package/runtime/python/okstra_ctl/worktree/rebase.py +108 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +14 -0
- package/runtime/schemas/final-report-v3.0.schema.json +78 -0
- package/runtime/skills/_fragments/wizard-render-bundle.md +1 -1
- package/runtime/skills/okstra-discussion/SKILL.md +1 -1
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/skills/okstra-setup/SKILL.md +2 -2
- package/runtime/skills/okstra-setup/references/project-config.md +2 -2
- package/runtime/templates/report-writer-prompt-preamble.md +1 -1
- package/runtime/templates/reports/html/i18n/en.json +6 -1
- package/runtime/templates/reports/html/i18n/ko.json +6 -1
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-run.py +3 -28
- package/runtime/agents/operations/report-translation.json +0 -6
- package/runtime/agents/roles/translator.json +0 -19
- package/runtime/bin/okstra-report-translate.py +0 -404
- package/runtime/prompts/duties/translator.json +0 -32
- package/runtime/python/okstra_ctl/report_translation.py +0 -509
- package/runtime/python/okstra_ctl/report_translation_dispatch.py +0 -356
- package/runtime/templates/translator-prompt-preamble.md +0 -36
|
@@ -831,76 +831,43 @@ def containing_stage(project_root: Path,
|
|
|
831
831
|
return single_containing_head(project_root, heads)
|
|
832
832
|
|
|
833
833
|
|
|
834
|
-
def
|
|
834
|
+
def teardown_completed_stage_worktrees(
|
|
835
835
|
*, project_root: Path, task_group: str, task_id: str,
|
|
836
|
+
expected_heads: dict[int, str] | None = None,
|
|
836
837
|
) -> dict[str, Any]:
|
|
837
|
-
"""
|
|
838
|
-
|
|
839
|
-
진입은 통합만 하고 정리를 남겨 두므로(`phases/final_verification/target.py`), 정리는
|
|
840
|
-
판정 뒤인 Phase 7 에서 여기로 들어온다. 통합은 이미 끝나 있어 Phase A 는 전부
|
|
841
|
-
`already_merged` 로 지나가고 Phase B 만 실제 일을 한다. 두 번 불려도 결과는 같다 —
|
|
842
|
-
사라진 worktree 는 건너뛰고, 미커밋 변경이 남은 stage 트리는 보존한다.
|
|
843
|
-
|
|
844
|
-
stage_map 은 done 행에서 만든다. 정리 대상은 완료된 stage 뿐이고, 계획에만 있고
|
|
845
|
-
완료되지 않은 stage 는 Phase A 가 어차피 건너뛰기 때문이다.
|
|
846
|
-
"""
|
|
838
|
+
"""완료된 단계의 작업 트리만 회수하고 브랜치와 기준 기록은 보존한다."""
|
|
847
839
|
from okstra_project.dirs import okstra_home, project_json_path
|
|
848
840
|
|
|
849
|
-
from . import consumers
|
|
841
|
+
from . import consumers
|
|
850
842
|
from .json_boundary import load_owned_object
|
|
851
843
|
from .locks import worktree_provision_mutex
|
|
852
844
|
from .paths import task_runs_dir
|
|
853
|
-
from .stage_integrate import
|
|
845
|
+
from .stage_integrate import reclaim_completed_stage
|
|
854
846
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
except (OSError, ValueError):
|
|
860
|
-
project_id = ""
|
|
847
|
+
config = project_json_path(project_root)
|
|
848
|
+
if not config.is_file():
|
|
849
|
+
return {"skipped": "project.json is missing"}
|
|
850
|
+
project_id = load_owned_object(config, artifact="project config").get("projectId", "")
|
|
861
851
|
if not project_id:
|
|
862
852
|
return {"skipped": "project.json declares no projectId"}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
project_root, task_group, task_id
|
|
866
|
-
) / "implementation-planning"
|
|
867
|
-
done_rows = [
|
|
868
|
-
row for row in consumers.read_consumers(plan_run_root)
|
|
869
|
-
if row.get("status") == "done"
|
|
870
|
-
]
|
|
871
|
-
if not done_rows:
|
|
872
|
-
return {"skipped": "no done stage rows to reclaim"}
|
|
873
|
-
stage_map = [
|
|
874
|
-
{"stage_number": stage}
|
|
875
|
-
for stage in sorted(consumers.latest_done_by_stage(done_rows))
|
|
876
|
-
]
|
|
877
|
-
|
|
878
|
-
entry = worktree_registry.lookup(project_id, task_group, task_id)
|
|
879
|
-
if entry is None:
|
|
880
|
-
return {"skipped": "task worktree is no longer registered"}
|
|
881
|
-
|
|
882
|
-
done_by_stage = consumers.latest_done_by_stage(done_rows)
|
|
883
|
-
tip = containing_stage(project_root, done_by_stage)
|
|
853
|
+
plan = task_runs_dir(project_root, task_group, task_id) / "implementation-planning"
|
|
854
|
+
removed, skipped, warnings = [], [], []
|
|
884
855
|
with worktree_provision_mutex(okstra_home(), project_id, task_group, task_id):
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
for stage, reason in result.teardown_skipped
|
|
904
|
-
],
|
|
905
|
-
"warnings": result.warnings,
|
|
906
|
-
}
|
|
856
|
+
rows = consumers.read_consumers(plan)
|
|
857
|
+
done = consumers.latest_done_by_stage(rows)
|
|
858
|
+
statuses = consumers.last_lifecycle_status_by_stage(rows)
|
|
859
|
+
heads = expected_heads if expected_heads is not None else {
|
|
860
|
+
stage: row.get("head_commit", "") for stage, row in done.items()
|
|
861
|
+
}
|
|
862
|
+
for stage, head in sorted(heads.items()):
|
|
863
|
+
outcome, detail = reclaim_completed_stage(
|
|
864
|
+
project_root, project_id, task_group, task_id, stage, head,
|
|
865
|
+
done.get(stage), statuses.get(stage, ""),
|
|
866
|
+
)
|
|
867
|
+
if outcome == "torn_down":
|
|
868
|
+
removed.append(stage)
|
|
869
|
+
elif outcome == "skipped":
|
|
870
|
+
skipped.append({"stage": stage, "reason": detail})
|
|
871
|
+
else:
|
|
872
|
+
warnings.append(detail)
|
|
873
|
+
return {"tornDown": removed, "teardownSkipped": skipped, "warnings": warnings}
|
|
@@ -228,12 +228,7 @@ def _record_dispatch(args, manifest: Mapping[str, Any], prepared) -> list[str]:
|
|
|
228
228
|
|
|
229
229
|
|
|
230
230
|
def team_dispatch_port(manifest: Mapping[str, Any]) -> WorkerDispatchPort:
|
|
231
|
-
"""이 run 의 워커를 띄울 포트. cmux run 이면 pane, 아니면 리드 호스트의 포트.
|
|
232
|
-
|
|
233
|
-
`team dispatch` 와 finalize 의 `translate` 단계가 같이 쓴다 — 번역자만
|
|
234
|
-
CLI 래퍼 디스패처로 띄우던 동안 cmux run 에서도 번역자는 pane 없이 돌았다
|
|
235
|
-
(실측 2026-09-22, dev-10860 implementation-planning-001: 18건 중 번역자 1건).
|
|
236
|
-
"""
|
|
231
|
+
"""이 run 의 워커를 띄울 포트. cmux run 이면 pane, 아니면 리드 호스트의 포트."""
|
|
237
232
|
lead_runtime = _require_string(manifest, "leadRuntime")
|
|
238
233
|
fallback_port = default_host_registry().resolve(
|
|
239
234
|
lead_runtime
|
|
@@ -333,14 +333,6 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
333
333
|
lines.append(f" pr-template : {state.pr_template_path} ({state.pr_template_scope or 'once'})")
|
|
334
334
|
if state.fix_cycle:
|
|
335
335
|
lines.append(f" fix-cycle : {state.fix_cycle}")
|
|
336
|
-
report_writer_models = state.role_models.get("report-writer", [])
|
|
337
|
-
translator_model = (
|
|
338
|
-
report_writer_models[0] if report_writer_models
|
|
339
|
-
else "(selection required)"
|
|
340
|
-
)
|
|
341
|
-
lines.append(_msg(
|
|
342
|
-
state.workspace_root, "confirmation", "translation_scope", model=translator_model,
|
|
343
|
-
))
|
|
344
336
|
lines.append(_msg(
|
|
345
337
|
state.workspace_root, "confirmation", "provider_data_scope",
|
|
346
338
|
project_root=state.project_root,
|
|
@@ -93,10 +93,10 @@ from .state import (
|
|
|
93
93
|
from .prompts import _opt, _p
|
|
94
94
|
from .sources import (
|
|
95
95
|
_brief_resolved,
|
|
96
|
+
_clarification_pick_required,
|
|
96
97
|
_has_prior_run_inputs,
|
|
97
98
|
_latest_implementation_planning_report,
|
|
98
99
|
_reverify_scope_pick_required,
|
|
99
|
-
_suggest_latest_final_report,
|
|
100
100
|
)
|
|
101
101
|
from .roles import _clear_role_selection, _reset_role_selection_from, next_role_prompt
|
|
102
102
|
from .steps_identity import (
|
|
@@ -535,8 +535,7 @@ STEPS: list[Step] = [
|
|
|
535
535
|
Step(S_CLARIFICATION_PICK,
|
|
536
536
|
applies=lambda s: (S_CLARIFICATION_PICK not in s.answered
|
|
537
537
|
and _identity_ready(s)
|
|
538
|
-
and (s
|
|
539
|
-
or bool(_suggest_latest_final_report(s)))),
|
|
538
|
+
and _clarification_pick_required(s)),
|
|
540
539
|
build=_build_clarification_pick, submit=_submit_clarification_pick,
|
|
541
540
|
owns=("clarification_response_path", "clarification_pending_text")),
|
|
542
541
|
Step(S_CLARIFICATION,
|
|
@@ -600,6 +600,16 @@ def _latest_run_inputs(
|
|
|
600
600
|
phase_segment=phase_segment)
|
|
601
601
|
|
|
602
602
|
|
|
603
|
+
def _clarification_pick_required(state: WizardState) -> bool:
|
|
604
|
+
if state.task_type == "release-handoff":
|
|
605
|
+
report = _suggest_latest_final_report(state)
|
|
606
|
+
return bool(report and sidecar_answers(Path(state.project_root) / report))
|
|
607
|
+
return (
|
|
608
|
+
state.task_type != "implementation-planning"
|
|
609
|
+
or bool(_suggest_latest_final_report(state))
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
603
613
|
def _carried_planning_report(state: WizardState) -> Optional[Path]:
|
|
604
614
|
"""이번 재실행이 이어받는 직전 implementation-planning 리포트 (없으면 None)."""
|
|
605
615
|
if state.task_type != "implementation-planning":
|
|
@@ -13,7 +13,6 @@ PromptAudience = Literal[
|
|
|
13
13
|
"implementation-executor",
|
|
14
14
|
"implementation-verifier",
|
|
15
15
|
"report-writer",
|
|
16
|
-
"translator",
|
|
17
16
|
"reverify",
|
|
18
17
|
"lead-only",
|
|
19
18
|
]
|
|
@@ -84,7 +83,6 @@ WORKER_PREAMBLE_FILENAME_BY_AUDIENCE = {
|
|
|
84
83
|
"implementation-executor": "implementation-worker-preamble.md",
|
|
85
84
|
"implementation-verifier": "implementation-worker-preamble.md",
|
|
86
85
|
"report-writer": "report-writer-prompt-preamble.md",
|
|
87
|
-
"translator": "translator-prompt-preamble.md",
|
|
88
86
|
}
|
|
89
87
|
WORKER_ERROR_CONTRACT_FILENAME = "worker-error-contract.md"
|
|
90
88
|
# coverage-critic gap 검증 라운드의 dispatch kind. 번호 라운드(`reverify-r<N>`)와
|
|
@@ -199,7 +197,7 @@ def resolve_prompt_plan(
|
|
|
199
197
|
"""Resolve prompt policy from the worker's role and the dispatch kind.
|
|
200
198
|
|
|
201
199
|
Neither the worker id nor the provider or model identity takes part. The id
|
|
202
|
-
used to: `report-writer
|
|
200
|
+
used to: `report-writer` and "the executor's worker id" were
|
|
203
201
|
compared by spelling, so a verifier that happened to share the executor's
|
|
204
202
|
id received the executor's prompt.
|
|
205
203
|
"""
|
|
@@ -210,12 +208,6 @@ def resolve_prompt_plan(
|
|
|
210
208
|
# 걸리지 않아 분석 계획으로 떨어졌고, 리드 프롬프트가 분석 워커 직무를
|
|
211
209
|
# 달고 나갔다.
|
|
212
210
|
return _plan("lead-only")
|
|
213
|
-
if dispatch_kind == "translator":
|
|
214
|
-
if role not in (None, "translator"):
|
|
215
|
-
raise ValueError(f"translator dispatch cannot run role {role}")
|
|
216
|
-
return _plan("translator", duty_audience="translator")
|
|
217
|
-
if role == "translator":
|
|
218
|
-
raise ValueError("translator role requires the translator dispatch kind")
|
|
219
211
|
if is_verification_dispatch_kind(dispatch_kind):
|
|
220
212
|
return _plan("reverify")
|
|
221
213
|
# A critic pass keeps the full analysis contract — worker anchor headers, the
|
|
@@ -307,8 +299,8 @@ def resolve_prompt_plan_for_manifest(
|
|
|
307
299
|
"""Resolve policy from the manifest's task type and the worker's roster role.
|
|
308
300
|
|
|
309
301
|
*worker_id* is the lookup key into `workerAssignments`; its spelling decides
|
|
310
|
-
nothing. A worker with no roster row —
|
|
311
|
-
|
|
302
|
+
nothing. A worker with no roster row — a verification pass whose kind
|
|
303
|
+
alone decides — resolves with no role.
|
|
312
304
|
"""
|
|
313
305
|
task_type = _required_string(manifest, "taskType")
|
|
314
306
|
if not worker_id.strip():
|
|
@@ -383,7 +375,6 @@ def _duty_audience(audience: PromptAudience) -> AgentAudience:
|
|
|
383
375
|
"implementation-executor": "implementation-executor",
|
|
384
376
|
"implementation-verifier": "implementation-verifier",
|
|
385
377
|
"report-writer": "report-writer",
|
|
386
|
-
"translator": "translator",
|
|
387
378
|
"reverify": "reverification-worker",
|
|
388
379
|
"lead-only": "lead",
|
|
389
380
|
}[audience]
|
|
@@ -34,7 +34,7 @@ Side effects:
|
|
|
34
34
|
`sync_config`(무엇을 옮길지), `git_ops`(git 사실) — 은 서로를 모른다. 그 위에
|
|
35
35
|
`cleanliness`(무엇이 더러운가) · `linking`(실제로 링크 걸기) ·
|
|
36
36
|
`decisions`(무엇이 일어날지 미리 답하기)가 있고, 부수 효과를 내는 것은
|
|
37
|
-
`provision`
|
|
37
|
+
`provision` 과 단계 브랜치를 재배치하는 `rebase` 뿐이다. 이 파일은 밖으로 나가는 이름만 모아 둔다.
|
|
38
38
|
"""
|
|
39
39
|
from __future__ import annotations
|
|
40
40
|
|
|
@@ -118,10 +118,9 @@ def _stage_entry_is_reusable(entry: worktree_registry.WorktreeEntry) -> bool:
|
|
|
118
118
|
the fix-run case: a stage whose verifier returned FAIL records a `failed`
|
|
119
119
|
consumers row, which frees the occupancy but deliberately keeps the worktree
|
|
120
120
|
and its branch as the reviewable stack. Re-entry MUST reuse that tree —
|
|
121
|
-
provisioning anew refuses on the existing
|
|
122
|
-
|
|
123
|
-
the stage provisions from scratch.
|
|
121
|
+
provisioning anew refuses on the existing branch. A removed directory is
|
|
122
|
+
restored from that branch on the next implementation or verification entry.
|
|
124
123
|
"""
|
|
125
124
|
if entry.status == "active":
|
|
126
125
|
return True
|
|
127
|
-
return entry.status == "released"
|
|
126
|
+
return entry.status == "released"
|
|
@@ -13,6 +13,7 @@ from pathlib import Path
|
|
|
13
13
|
|
|
14
14
|
from .. import worktree_registry
|
|
15
15
|
from .decisions import preview_worktree_decision, resolve_stage_worktree_decision
|
|
16
|
+
from .rebase import rebase_stage_branch
|
|
16
17
|
from .git_ops import (
|
|
17
18
|
_branch_exists,
|
|
18
19
|
_branch_exists_message,
|
|
@@ -259,6 +260,21 @@ def provision_task_worktree(
|
|
|
259
260
|
)
|
|
260
261
|
|
|
261
262
|
|
|
263
|
+
def restore_stage_worktree(project_root: Path, path: str, branch: str) -> None:
|
|
264
|
+
target = Path(path)
|
|
265
|
+
if target.exists():
|
|
266
|
+
return
|
|
267
|
+
root = main_worktree_path(project_root)
|
|
268
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
269
|
+
result = _git(root, "worktree", "add", str(target), branch)
|
|
270
|
+
if result.returncode:
|
|
271
|
+
raise RuntimeError(f"cannot restore stage worktree {target}: {result.stderr.strip()}")
|
|
272
|
+
_link_sync_dirs(root, target)
|
|
273
|
+
_link_sync_files(root, target)
|
|
274
|
+
_copy_snapshot_files(root, target)
|
|
275
|
+
_seed_worktree_settings_symlink(target)
|
|
276
|
+
|
|
277
|
+
|
|
262
278
|
def provision_stage_worktree(
|
|
263
279
|
*,
|
|
264
280
|
project_root: Path,
|
|
@@ -298,6 +314,17 @@ def provision_stage_worktree(
|
|
|
298
314
|
stage_number=stage_number,
|
|
299
315
|
)
|
|
300
316
|
if decision.status == "reused":
|
|
317
|
+
restore_stage_worktree(project_root, decision.path, decision.branch)
|
|
318
|
+
resolved_base = _resolve_commit_sha(project_root, base_commit)
|
|
319
|
+
if not resolved_base:
|
|
320
|
+
raise RuntimeError(f"could not resolve base_commit `{base_commit}`")
|
|
321
|
+
if decision.base_ref != resolved_base:
|
|
322
|
+
rebase_stage_branch(project_root, decision.branch, decision.base_ref, resolved_base)
|
|
323
|
+
worktree_registry.update_stage_base(
|
|
324
|
+
project_id, task_group_segment, task_id_segment, stage_number,
|
|
325
|
+
expected_base=decision.base_ref, base_ref=resolved_base,
|
|
326
|
+
)
|
|
327
|
+
decision.base_ref = resolved_base
|
|
301
328
|
drift = _sync_key_drift_note(
|
|
302
329
|
main_worktree_path(project_root),
|
|
303
330
|
worktree_registry.lookup(
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""단계 브랜치 하나를 새 기준 위로 재배치하는 git 작업.
|
|
2
|
+
|
|
3
|
+
`provision` 의 재진입 경로와 `stage_rebase` 의 의존 순서 재배치가 함께 쓴다.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import tempfile
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .cleanliness import is_dirty_excluding_okstra
|
|
13
|
+
from .git_ops import _git, is_ancestor
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _checked_git(root: Path, *args: str) -> str:
|
|
17
|
+
result = _git(root, *args)
|
|
18
|
+
if result.returncode:
|
|
19
|
+
raise RuntimeError(f"git {' '.join(args)} failed: {result.stderr.strip()}")
|
|
20
|
+
return result.stdout.strip()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@contextmanager
|
|
24
|
+
def _stage_worktree(project_root: Path, branch: str) -> Iterator[Path]:
|
|
25
|
+
worktrees = _checked_git(project_root, "worktree", "list", "--porcelain")
|
|
26
|
+
holder = None
|
|
27
|
+
current = None
|
|
28
|
+
for line in worktrees.splitlines():
|
|
29
|
+
if line.startswith("worktree "):
|
|
30
|
+
current = Path(line.removeprefix("worktree "))
|
|
31
|
+
elif line == f"branch refs/heads/{branch}":
|
|
32
|
+
holder = current
|
|
33
|
+
if holder is not None and is_dirty_excluding_okstra(holder):
|
|
34
|
+
raise RuntimeError(
|
|
35
|
+
f"cannot rebase {branch}: worktree {holder} has uncommitted changes"
|
|
36
|
+
)
|
|
37
|
+
temporary = holder is None
|
|
38
|
+
if temporary:
|
|
39
|
+
holder = Path(tempfile.mkdtemp(prefix="okstra-rebase-"))
|
|
40
|
+
holder.rmdir()
|
|
41
|
+
_checked_git(project_root, "worktree", "add", str(holder), branch)
|
|
42
|
+
try:
|
|
43
|
+
yield holder
|
|
44
|
+
finally:
|
|
45
|
+
if temporary:
|
|
46
|
+
# 복구에 실패한 작업 트리는 삭제하지 않는다.
|
|
47
|
+
states = ["rebase-merge", "rebase-apply"]
|
|
48
|
+
if not any(
|
|
49
|
+
Path(_checked_git(holder, "rev-parse", "--git-path", state)).exists()
|
|
50
|
+
for state in states
|
|
51
|
+
):
|
|
52
|
+
_checked_git(project_root, "worktree", "remove", str(holder))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def rebase_stage_branch(
|
|
56
|
+
project_root: Path,
|
|
57
|
+
branch: str,
|
|
58
|
+
old_base: str,
|
|
59
|
+
new_base: str,
|
|
60
|
+
) -> str:
|
|
61
|
+
old = _checked_git(project_root, "rev-parse", f"{old_base}^{{commit}}")
|
|
62
|
+
new = _checked_git(project_root, "rev-parse", f"{new_base}^{{commit}}")
|
|
63
|
+
head = _checked_git(project_root, "rev-parse", f"refs/heads/{branch}")
|
|
64
|
+
if old == new:
|
|
65
|
+
return head
|
|
66
|
+
if not is_ancestor(project_root, old, head):
|
|
67
|
+
raise RuntimeError(
|
|
68
|
+
f"cannot rebase {branch}: recorded base {old} is not an ancestor"
|
|
69
|
+
)
|
|
70
|
+
with _stage_worktree(project_root, branch) as holder:
|
|
71
|
+
for state in ["rebase-merge", "rebase-apply", "MERGE_HEAD", "CHERRY_PICK_HEAD"]:
|
|
72
|
+
path = Path(_checked_git(holder, "rev-parse", "--git-path", state))
|
|
73
|
+
if path.exists():
|
|
74
|
+
raise RuntimeError(
|
|
75
|
+
f"cannot rebase {branch}: unfinished git operation at {path}"
|
|
76
|
+
)
|
|
77
|
+
_checked_git(
|
|
78
|
+
project_root,
|
|
79
|
+
"update-ref",
|
|
80
|
+
f"refs/okstra/rebase-backups/{branch}/{head}",
|
|
81
|
+
head,
|
|
82
|
+
)
|
|
83
|
+
return _apply_rebase(holder, branch, old, new)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _apply_rebase(holder: Path, branch: str, old: str, new: str) -> str:
|
|
87
|
+
result = _git(
|
|
88
|
+
holder,
|
|
89
|
+
"rebase",
|
|
90
|
+
"--no-autostash",
|
|
91
|
+
"--no-autosquash",
|
|
92
|
+
"--no-update-refs",
|
|
93
|
+
"--no-rebase-merges",
|
|
94
|
+
"--onto",
|
|
95
|
+
new,
|
|
96
|
+
old,
|
|
97
|
+
)
|
|
98
|
+
if result.returncode:
|
|
99
|
+
abort = _git(holder, "rebase", "--abort")
|
|
100
|
+
detail = (result.stderr or result.stdout).strip()
|
|
101
|
+
if abort.returncode:
|
|
102
|
+
raise RuntimeError(
|
|
103
|
+
f"rebase failed for {branch}; recovery worktree: {holder}; {detail}; abort: {abort.stderr}"
|
|
104
|
+
)
|
|
105
|
+
raise RuntimeError(
|
|
106
|
+
f"rebase failed for {branch}; original branch restored: {detail}"
|
|
107
|
+
)
|
|
108
|
+
return _checked_git(holder, "rev-parse", "HEAD")
|
|
@@ -310,6 +310,20 @@ def get_stage_row(
|
|
|
310
310
|
return data["tasks"].get(key)
|
|
311
311
|
|
|
312
312
|
|
|
313
|
+
def update_stage_base(
|
|
314
|
+
project_id: str, task_group: str, task_id: str, stage: int, *,
|
|
315
|
+
expected_base: str, base_ref: str,
|
|
316
|
+
) -> None:
|
|
317
|
+
key = task_key(project_id, task_group, task_id, stage)
|
|
318
|
+
with _registry_lock():
|
|
319
|
+
data = _load()
|
|
320
|
+
row = data["tasks"].get(key)
|
|
321
|
+
if row is None or row.get("base_ref") != expected_base:
|
|
322
|
+
raise RuntimeError(f"stage base changed concurrently: {key}")
|
|
323
|
+
row["base_ref"] = base_ref
|
|
324
|
+
_save(data)
|
|
325
|
+
|
|
326
|
+
|
|
313
327
|
def list_active_stage_numbers(
|
|
314
328
|
project_id: str, task_group: str, task_id: str,
|
|
315
329
|
) -> set:
|
|
@@ -1355,6 +1355,14 @@
|
|
|
1355
1355
|
"$ref": "#/$defs/LeadFixRow"
|
|
1356
1356
|
}
|
|
1357
1357
|
},
|
|
1358
|
+
"correctionRounds": {
|
|
1359
|
+
"type": "array",
|
|
1360
|
+
"description": "Correction rounds run in this run after a verifier FAIL (`_implementation-deliverable.md` \"Correction round\"): each sends the executor a bounded fix and the verifiers a re-verification. `verifierResults` holds the last round's verdicts; earlier FAIL verdicts stay here. Optional so reports of tasks already in flight keep validating.",
|
|
1361
|
+
"maxItems": 2,
|
|
1362
|
+
"items": {
|
|
1363
|
+
"$ref": "#/$defs/CorrectionRoundRow"
|
|
1364
|
+
}
|
|
1365
|
+
},
|
|
1358
1366
|
"rollbackVerification": {
|
|
1359
1367
|
"type": "array",
|
|
1360
1368
|
"minItems": 1,
|
|
@@ -10159,6 +10167,76 @@
|
|
|
10159
10167
|
}
|
|
10160
10168
|
}
|
|
10161
10169
|
},
|
|
10170
|
+
"CorrectionRoundRow": {
|
|
10171
|
+
"type": "object",
|
|
10172
|
+
"description": "One correction round: the findings it fixed, the executor's commit range, and each verifier's re-verification verdict.",
|
|
10173
|
+
"required": [
|
|
10174
|
+
"round",
|
|
10175
|
+
"findingIds",
|
|
10176
|
+
"baseCommit",
|
|
10177
|
+
"commit",
|
|
10178
|
+
"verifierVerdicts",
|
|
10179
|
+
"result"
|
|
10180
|
+
],
|
|
10181
|
+
"additionalProperties": false,
|
|
10182
|
+
"properties": {
|
|
10183
|
+
"round": {
|
|
10184
|
+
"type": "integer",
|
|
10185
|
+
"minimum": 1,
|
|
10186
|
+
"maximum": 2
|
|
10187
|
+
},
|
|
10188
|
+
"findingIds": {
|
|
10189
|
+
"type": "array",
|
|
10190
|
+
"minItems": 1,
|
|
10191
|
+
"items": {
|
|
10192
|
+
"type": "string",
|
|
10193
|
+
"minLength": 1
|
|
10194
|
+
}
|
|
10195
|
+
},
|
|
10196
|
+
"baseCommit": {
|
|
10197
|
+
"description": "Stage HEAD before the round.",
|
|
10198
|
+
"type": "string",
|
|
10199
|
+
"pattern": "^[0-9a-f]{7,40}$"
|
|
10200
|
+
},
|
|
10201
|
+
"commit": {
|
|
10202
|
+
"description": "Stage HEAD after the executor's fix.",
|
|
10203
|
+
"type": "string",
|
|
10204
|
+
"pattern": "^[0-9a-f]{7,40}$"
|
|
10205
|
+
},
|
|
10206
|
+
"verifierVerdicts": {
|
|
10207
|
+
"type": "array",
|
|
10208
|
+
"minItems": 1,
|
|
10209
|
+
"items": {
|
|
10210
|
+
"type": "object",
|
|
10211
|
+
"required": [
|
|
10212
|
+
"verifier",
|
|
10213
|
+
"verdict"
|
|
10214
|
+
],
|
|
10215
|
+
"additionalProperties": false,
|
|
10216
|
+
"properties": {
|
|
10217
|
+
"verifier": {
|
|
10218
|
+
"type": "string",
|
|
10219
|
+
"minLength": 1
|
|
10220
|
+
},
|
|
10221
|
+
"verdict": {
|
|
10222
|
+
"enum": [
|
|
10223
|
+
"PASS",
|
|
10224
|
+
"CONCERNS",
|
|
10225
|
+
"FAIL"
|
|
10226
|
+
]
|
|
10227
|
+
}
|
|
10228
|
+
}
|
|
10229
|
+
}
|
|
10230
|
+
},
|
|
10231
|
+
"result": {
|
|
10232
|
+
"description": "`pass` when no re-verification verdict is FAIL.",
|
|
10233
|
+
"enum": [
|
|
10234
|
+
"pass",
|
|
10235
|
+
"fail"
|
|
10236
|
+
]
|
|
10237
|
+
}
|
|
10238
|
+
}
|
|
10239
|
+
},
|
|
10162
10240
|
"LeadFixRow": {
|
|
10163
10241
|
"type": "object",
|
|
10164
10242
|
"description": "One lead minor fix: the verifier findings it answers, the commit that carries it, and the re-run checks that decide whether the run may settle the stage. `changedLines` has no schema maximum on purpose: an over-limit fix must still be recordable, and `_validate_lead_fixes` reports it.",
|
|
@@ -10,7 +10,7 @@ Output: `{ok: true, outcome: {renderArgv: ["--lead-runtime", "...", ...], render
|
|
|
10
10
|
|
|
11
11
|
`renderArgv` is the canonical ordered `okstra render-bundle` argument list. Pass its tokens verbatim and in order. Repeated `--role-count` and `--role-model` flags are intentional. `renderArgs` is a compatibility and inspection view only; do not reconstruct an invocation from it. `orchestration` holds signals this skill acts on itself — never pass its entries to `render-bundle`.
|
|
12
12
|
|
|
13
|
-
`userAuthorization`, when present, preserves the confirmation prompt actually emitted by this wizard and the relayed `proceed` response. Its `--user-authorization-json` token carries that record into the run manifest; preserve it unchanged. Do not synthesize a receipt for older confirmed states.
|
|
13
|
+
`userAuthorization`, when present, preserves the confirmation prompt actually emitted by this wizard and the relayed `proceed` response. Its `--user-authorization-json` token carries that record into the run manifest; preserve it unchanged. Do not synthesize a receipt for older confirmed states. The record documents task scope; it does not grant host execution privileges.
|
|
14
14
|
|
|
15
15
|
Before retrying a worker in an existing run, inspect its last invocation attempt: await running work; append an attempt only after `failed-no-mutation` within the retry budget. Preserve every other terminal attempt. `okstra team dispatch` routes the legacy declaration-only implementer failure to a separate same-model evidence-recovery invocation; collect its result before independent verification. Other terminal failures need `okstra agent-prompt materialize` with a new `--invocation-id`, distinct `--prompt`/`--result` paths, and unchanged assignment/model for a bounded correction. Do not restart completed implementation or request renewed approval for unchanged scope. Enforcement: `dispatch_core._next_attempt` and `execution_manifest._validate_next_attempt`.
|
|
16
16
|
|
|
@@ -223,7 +223,7 @@ Output: `{ok: true, outcome: {renderArgv: ["--lead-runtime", "...", ...], render
|
|
|
223
223
|
|
|
224
224
|
`renderArgv` is the canonical ordered `okstra render-bundle` argument list. Pass its tokens verbatim and in order. Repeated `--role-count` and `--role-model` flags are intentional. `renderArgs` is a compatibility and inspection view only; do not reconstruct an invocation from it. `orchestration` holds signals this skill acts on itself — never pass its entries to `render-bundle`.
|
|
225
225
|
|
|
226
|
-
`userAuthorization`, when present, preserves the confirmation prompt actually emitted by this wizard and the relayed `proceed` response. Its `--user-authorization-json` token carries that record into the run manifest; preserve it unchanged. Do not synthesize a receipt for older confirmed states.
|
|
226
|
+
`userAuthorization`, when present, preserves the confirmation prompt actually emitted by this wizard and the relayed `proceed` response. Its `--user-authorization-json` token carries that record into the run manifest; preserve it unchanged. Do not synthesize a receipt for older confirmed states. The record documents task scope; it does not grant host execution privileges.
|
|
227
227
|
|
|
228
228
|
Before retrying a worker in an existing run, inspect its last invocation attempt: await running work; append an attempt only after `failed-no-mutation` within the retry budget. Preserve every other terminal attempt. `okstra team dispatch` routes the legacy declaration-only implementer failure to a separate same-model evidence-recovery invocation; collect its result before independent verification. Other terminal failures need `okstra agent-prompt materialize` with a new `--invocation-id`, distinct `--prompt`/`--result` paths, and unchanged assignment/model for a bounded correction. Do not restart completed implementation or request renewed approval for unchanged scope. Enforcement: `dispatch_core._next_attempt` and `execution_manifest._validate_next_attempt`.
|
|
229
229
|
|
|
@@ -256,7 +256,7 @@ Output: `{ok: true, outcome: {renderArgv: ["--lead-runtime", "...", ...], render
|
|
|
256
256
|
|
|
257
257
|
`renderArgv` is the canonical ordered `okstra render-bundle` argument list. Pass its tokens verbatim and in order. Repeated `--role-count` and `--role-model` flags are intentional. `renderArgs` is a compatibility and inspection view only; do not reconstruct an invocation from it. `orchestration` holds signals this skill acts on itself — never pass its entries to `render-bundle`.
|
|
258
258
|
|
|
259
|
-
`userAuthorization`, when present, preserves the confirmation prompt actually emitted by this wizard and the relayed `proceed` response. Its `--user-authorization-json` token carries that record into the run manifest; preserve it unchanged. Do not synthesize a receipt for older confirmed states.
|
|
259
|
+
`userAuthorization`, when present, preserves the confirmation prompt actually emitted by this wizard and the relayed `proceed` response. Its `--user-authorization-json` token carries that record into the run manifest; preserve it unchanged. Do not synthesize a receipt for older confirmed states. The record documents task scope; it does not grant host execution privileges.
|
|
260
260
|
|
|
261
261
|
Before retrying a worker in an existing run, inspect its last invocation attempt: await running work; append an attempt only after `failed-no-mutation` within the retry budget. Preserve every other terminal attempt. `okstra team dispatch` routes the legacy declaration-only implementer failure to a separate same-model evidence-recovery invocation; collect its result before independent verification. Other terminal failures need `okstra agent-prompt materialize` with a new `--invocation-id`, distinct `--prompt`/`--result` paths, and unchanged assignment/model for a bounded correction. Do not restart completed implementation or request renewed approval for unchanged scope. Enforcement: `dispatch_core._next_attempt` and `execution_manifest._validate_next_attempt`.
|
|
262
262
|
|
|
@@ -169,8 +169,8 @@ matching section:
|
|
|
169
169
|
- **D. PR body template** (release-handoff) — opt-in pre-registration; also
|
|
170
170
|
offered on the first release-handoff run.
|
|
171
171
|
- **E. final report language** (`reportLanguage`) — a language tag (`en`,
|
|
172
|
-
`ko`, `fr`, …); default `en`. The
|
|
173
|
-
|
|
172
|
+
`ko`, `fr`, …); default `en`. The report writer writes the report prose
|
|
173
|
+
directly in it.
|
|
174
174
|
- **F. declared architecture style** (`architecture.style`) — `hexagonal` /
|
|
175
175
|
`layered` / `none`; hand-added, default `none`. Declaring one makes that
|
|
176
176
|
architecture's placement rules binding in planning + verification.
|
|
@@ -196,8 +196,8 @@ per-run override prompt during the next release-handoff run.
|
|
|
196
196
|
|
|
197
197
|
## E. Final report language (`reportLanguage`)
|
|
198
198
|
|
|
199
|
-
The value is a language tag (`en`, `ko`, `fr`, `pt-BR`, …). The
|
|
200
|
-
|
|
199
|
+
The value is a language tag (`en`, `ko`, `fr`, `pt-BR`, …). The report writer
|
|
200
|
+
writes the report prose directly in that language; no translation step runs.
|
|
201
201
|
If you skip, no `reportLanguage` field is written and the runtime uses `en`.
|
|
202
202
|
|
|
203
203
|
AskUserQuestion (fixed options):
|
|
@@ -30,7 +30,7 @@ The narrative is not free-form Markdown. A narrative file is the line `# OKSTRA
|
|
|
30
30
|
|
|
31
31
|
Blank lines are ignored. **Every other line is rejected**, including Markdown headings (`#`, `##`, `###`), a pipe table written at column 0, code fences, bare paragraphs, JSON, YAML, and JSON Pointer. A heading or table that belongs in the reader's report is written as the text of a `> ` value, never as a narrative line of its own.
|
|
32
32
|
|
|
33
|
-
Follow the task-specific schema order
|
|
33
|
+
Follow the task-specific schema order. Write every `> ` prose value directly in the language named by `**Report Language:**`; do not draft in English first, because no translation step follows. Field names, `Verdict Token` and other enum or status values, finding and requirement ids, paths, symbols, commands, and model names stay exactly as the schema and sources spell them.
|
|
34
34
|
|
|
35
35
|
### Top-level fields you may author
|
|
36
36
|
|
|
@@ -184,7 +184,6 @@
|
|
|
184
184
|
"implementer": "Implementer",
|
|
185
185
|
"planner": "Planner",
|
|
186
186
|
"report-writer": "Report writer",
|
|
187
|
-
"translator": "Translator",
|
|
188
187
|
"verifier": "Verifier"
|
|
189
188
|
},
|
|
190
189
|
"requirementStatus": {
|
|
@@ -679,6 +678,12 @@
|
|
|
679
678
|
"lead-fix-changed-lines": "Changed lines",
|
|
680
679
|
"lead-fix-result-pass": "checks pass",
|
|
681
680
|
"lead-fix-result-fail": "checks still fail",
|
|
681
|
+
"correction-rounds": "Corrected within this run",
|
|
682
|
+
"correction-rounds-intro": "Verifiers rejected the change. Instead of starting a fix run, the executor fixed the confirmed findings in this run and the verifiers checked the fix again.",
|
|
683
|
+
"correction-round": "Round",
|
|
684
|
+
"correction-range": "Fix range",
|
|
685
|
+
"correction-result-pass": "re-verification passed",
|
|
686
|
+
"correction-result-fail": "re-verification failed",
|
|
682
687
|
"entry-count": "{count} entries",
|
|
683
688
|
"manual-user-test": "Manual user test",
|
|
684
689
|
"environment": "Environment:",
|
|
@@ -184,7 +184,6 @@
|
|
|
184
184
|
"implementer": "구현",
|
|
185
185
|
"planner": "계획",
|
|
186
186
|
"report-writer": "리포트 작성",
|
|
187
|
-
"translator": "번역",
|
|
188
187
|
"verifier": "검증"
|
|
189
188
|
},
|
|
190
189
|
"requirementStatus": {
|
|
@@ -679,6 +678,12 @@
|
|
|
679
678
|
"lead-fix-changed-lines": "바뀐 줄 수",
|
|
680
679
|
"lead-fix-result-pass": "검사 통과",
|
|
681
680
|
"lead-fix-result-fail": "검사 여전히 실패",
|
|
681
|
+
"correction-rounds": "이 run 안에서 고친 것",
|
|
682
|
+
"correction-rounds-intro": "검증자가 변경을 거부했습니다. fix run 을 새로 여는 대신 이 run 안에서 실행자가 확인된 finding 을 고치고, 검증자가 그 수정을 다시 검증했습니다.",
|
|
683
|
+
"correction-round": "라운드",
|
|
684
|
+
"correction-range": "수정 범위",
|
|
685
|
+
"correction-result-pass": "재검증 통과",
|
|
686
|
+
"correction-result-fail": "재검증 실패",
|
|
682
687
|
"entry-count": "{count}개 항목",
|
|
683
688
|
"manual-user-test": "수동 사용자 테스트",
|
|
684
689
|
"environment": "환경:",
|