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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""선행 기준이 이동한 단계 브랜치를 의존 순서로 재배치한다."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from . import consumers, worktree_registry
|
|
8
|
+
from .stage_targets import (
|
|
9
|
+
StageWorktreeIdentity,
|
|
10
|
+
order_stage_closure,
|
|
11
|
+
resolve_stage_base_commit,
|
|
12
|
+
)
|
|
13
|
+
from .worktree import is_ancestor
|
|
14
|
+
from .worktree.rebase import _checked_git, rebase_stage_branch
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _rebase_completed_stage(
|
|
18
|
+
project_root: Path,
|
|
19
|
+
plan_run_root: Path,
|
|
20
|
+
identity: StageWorktreeIdentity,
|
|
21
|
+
row: dict,
|
|
22
|
+
done_row: dict,
|
|
23
|
+
base: str,
|
|
24
|
+
) -> dict:
|
|
25
|
+
number, branch = row["stage"], row["branch"]
|
|
26
|
+
tip = _checked_git(project_root, "rev-parse", f"refs/heads/{branch}")
|
|
27
|
+
if tip != done_row.get("head_commit"):
|
|
28
|
+
raise RuntimeError(
|
|
29
|
+
f"stage {number} branch moved beyond its done commit; reconcile it before rebasing"
|
|
30
|
+
)
|
|
31
|
+
head = rebase_stage_branch(project_root, branch, row["base_ref"], base)
|
|
32
|
+
worktree_registry.update_stage_base(
|
|
33
|
+
identity.project_id,
|
|
34
|
+
identity.task_group,
|
|
35
|
+
identity.task_id,
|
|
36
|
+
number,
|
|
37
|
+
expected_base=row["base_ref"],
|
|
38
|
+
base_ref=base,
|
|
39
|
+
)
|
|
40
|
+
consumers.append_consumer(
|
|
41
|
+
plan_run_root,
|
|
42
|
+
impl_task_key=done_row["impl_task_key"],
|
|
43
|
+
stage=number,
|
|
44
|
+
status="done",
|
|
45
|
+
force_reappend=True,
|
|
46
|
+
head_commit=head,
|
|
47
|
+
replaced_commit=tip,
|
|
48
|
+
reconciled=True,
|
|
49
|
+
reconcile_reason="dependency-rebase",
|
|
50
|
+
report_path=done_row.get("report_path", ""),
|
|
51
|
+
)
|
|
52
|
+
return {**done_row, "head_commit": head}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _refresh_predecessor_head(
|
|
56
|
+
root: Path,
|
|
57
|
+
plan: Path,
|
|
58
|
+
row: dict,
|
|
59
|
+
done_row: dict,
|
|
60
|
+
active: bool,
|
|
61
|
+
) -> dict:
|
|
62
|
+
tip = _checked_git(root, "rev-parse", f"refs/heads/{row['branch']}")
|
|
63
|
+
previous = done_row["head_commit"]
|
|
64
|
+
if tip == previous:
|
|
65
|
+
return done_row
|
|
66
|
+
if active or not is_ancestor(root, previous, tip):
|
|
67
|
+
raise RuntimeError(
|
|
68
|
+
f"predecessor stage {row['stage']} is active or rewritten; finish or reconcile it first"
|
|
69
|
+
)
|
|
70
|
+
consumers.append_consumer(
|
|
71
|
+
plan,
|
|
72
|
+
impl_task_key=done_row["impl_task_key"],
|
|
73
|
+
stage=row["stage"],
|
|
74
|
+
status="done",
|
|
75
|
+
force_reappend=True,
|
|
76
|
+
head_commit=tip,
|
|
77
|
+
replaced_commit=previous,
|
|
78
|
+
reconciled=True,
|
|
79
|
+
reconcile_reason="predecessor-advance",
|
|
80
|
+
report_path=done_row.get("report_path", ""),
|
|
81
|
+
)
|
|
82
|
+
return {**done_row, "head_commit": tip}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def synchronize_stage_stack(
|
|
86
|
+
*,
|
|
87
|
+
project_root: Path,
|
|
88
|
+
plan_run_root: Path,
|
|
89
|
+
stage_map: list[dict],
|
|
90
|
+
identity: StageWorktreeIdentity,
|
|
91
|
+
) -> list[int]:
|
|
92
|
+
"""호출자가 task-key provision 잠금을 잡은 상태에서 실행한다."""
|
|
93
|
+
rows = consumers.read_consumers(plan_run_root)
|
|
94
|
+
done = consumers.latest_done_by_stage(rows)
|
|
95
|
+
statuses = consumers.last_lifecycle_status_by_stage(rows)
|
|
96
|
+
anchor = (
|
|
97
|
+
worktree_registry.get_implementation_base(
|
|
98
|
+
identity.project_id,
|
|
99
|
+
identity.task_group,
|
|
100
|
+
identity.task_id,
|
|
101
|
+
)
|
|
102
|
+
or ""
|
|
103
|
+
)
|
|
104
|
+
graph = [(s["stage_number"], s.get("depends_on") or []) for s in stage_map]
|
|
105
|
+
order = order_stage_closure(graph, {n for n, _ in graph}, set())
|
|
106
|
+
if len(order) != len(graph):
|
|
107
|
+
raise RuntimeError("cannot synchronize a cyclic Stage Map")
|
|
108
|
+
by_number = {s["stage_number"]: s for s in stage_map}
|
|
109
|
+
predecessors = {dep for _, deps in graph for dep in deps}
|
|
110
|
+
changed = []
|
|
111
|
+
for number in order:
|
|
112
|
+
row = worktree_registry.get_stage_row(
|
|
113
|
+
identity.project_id,
|
|
114
|
+
identity.task_group,
|
|
115
|
+
identity.task_id,
|
|
116
|
+
number,
|
|
117
|
+
)
|
|
118
|
+
stage = by_number[number]
|
|
119
|
+
if not row or number not in done or stage.get("cancelled"):
|
|
120
|
+
continue
|
|
121
|
+
active = statuses.get(number) == "started" or row.get("status") == "active"
|
|
122
|
+
if _synchronize_recorded_stage(
|
|
123
|
+
project_root,
|
|
124
|
+
plan_run_root,
|
|
125
|
+
identity,
|
|
126
|
+
stage,
|
|
127
|
+
row,
|
|
128
|
+
done,
|
|
129
|
+
anchor,
|
|
130
|
+
active,
|
|
131
|
+
number in predecessors,
|
|
132
|
+
):
|
|
133
|
+
changed.append(number)
|
|
134
|
+
return changed
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _synchronize_recorded_stage(
|
|
138
|
+
root: Path,
|
|
139
|
+
plan: Path,
|
|
140
|
+
identity: StageWorktreeIdentity,
|
|
141
|
+
stage: dict,
|
|
142
|
+
row: dict,
|
|
143
|
+
done: dict[int, dict],
|
|
144
|
+
anchor: str,
|
|
145
|
+
active: bool,
|
|
146
|
+
predecessor: bool,
|
|
147
|
+
) -> bool:
|
|
148
|
+
number = stage["stage_number"]
|
|
149
|
+
previous = done[number]["head_commit"]
|
|
150
|
+
if predecessor:
|
|
151
|
+
done[number] = _refresh_predecessor_head(root, plan, row, done[number], active)
|
|
152
|
+
if stage.get("depends_on"):
|
|
153
|
+
done[number] = _synchronize_dependent(
|
|
154
|
+
root,
|
|
155
|
+
plan,
|
|
156
|
+
identity,
|
|
157
|
+
stage,
|
|
158
|
+
row,
|
|
159
|
+
done,
|
|
160
|
+
anchor,
|
|
161
|
+
active,
|
|
162
|
+
)
|
|
163
|
+
return previous != done[number]["head_commit"]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _synchronize_dependent(
|
|
167
|
+
root: Path,
|
|
168
|
+
plan: Path,
|
|
169
|
+
identity: StageWorktreeIdentity,
|
|
170
|
+
stage: dict,
|
|
171
|
+
row: dict,
|
|
172
|
+
done: dict[int, dict],
|
|
173
|
+
anchor: str,
|
|
174
|
+
active: bool,
|
|
175
|
+
) -> dict:
|
|
176
|
+
number = stage["stage_number"]
|
|
177
|
+
base = resolve_stage_base_commit(
|
|
178
|
+
stage,
|
|
179
|
+
list(done.values()),
|
|
180
|
+
anchor,
|
|
181
|
+
project_root=root,
|
|
182
|
+
plan_run_root=plan,
|
|
183
|
+
identity=identity,
|
|
184
|
+
)
|
|
185
|
+
if row["base_ref"] == base:
|
|
186
|
+
return done[number]
|
|
187
|
+
if active:
|
|
188
|
+
raise RuntimeError(f"cannot rebase active stage {number}; finish its run first")
|
|
189
|
+
return _rebase_completed_stage(root, plan, identity, row, done[number], base)
|
|
@@ -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:
|
|
@@ -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
|
|