okstra 0.71.0 → 0.71.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/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +9 -4
- package/runtime/python/okstra_ctl/consumers.py +46 -16
- package/runtime/python/okstra_ctl/run.py +4 -1
- package/runtime/python/okstra_ctl/wizard.py +48 -1
- package/runtime/python/okstra_ctl/worktree.py +35 -0
- package/runtime/skills/okstra-run/SKILL.md +1 -1
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -50,6 +50,6 @@ are collected and convergence finished. Phase 1-5 do not need it.
|
|
|
50
50
|
|
|
51
51
|
- Parse the executor's `### Stage Carry Evidence` JSON block. If absent or unparsable, end with status `contract-violated` and route to a follow-up `error-analysis`.
|
|
52
52
|
- For this run's single stage: write its JSON verbatim to `runs/<impl-task-key>/carry/stage-<N>.json`. Refuse to overwrite an existing file (one stage = one sidecar; re-runs are out of scope for this version).
|
|
53
|
-
- For this run's single stage: append a `status:"done"` row to `runs/<plan-task-key>/consumers.jsonl` with `completed_at`, `carry_path`, `report_path` (this run's final-report path relative to the run root), and the SHA of HEAD.
|
|
53
|
+
- For this run's single stage: append a `status:"done"` row to `runs/<plan-task-key>/consumers.jsonl` with `completed_at`, `carry_path`, `report_path` (this run's final-report path relative to the run root), and the SHA of HEAD. Append it with `okstra_ctl.consumers.append_consumer` (NOT a raw filesystem write) — it honours the consumers lock AND releases this stage's worktree-registry occupancy, so later runs stop seeing a finished stage as a concurrent run. `report_path` lets `final-verification` cite each stage's originating report when assembling its Source Implementation Report list.
|
|
54
54
|
- The verifier round, Phase 5.5 convergence, and this Phase 6 report run **once per run** over this stage's diff — NOT per step.
|
|
55
55
|
- Quote this stage's new contents (the sidecar JSON in full and the new consumers row by itself) in the final report's `Stage sidecar evidence` deliverable section.
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
}
|
|
161
161
|
},
|
|
162
162
|
"base_ref_pick": {
|
|
163
|
-
"label": "이 task
|
|
163
|
+
"label": "이 task 의 base branch?",
|
|
164
164
|
"echo_template": "base-ref: {value}",
|
|
165
165
|
"options": {
|
|
166
166
|
"_RECOMMENDED_SUFFIX": " (recommended)",
|
|
@@ -170,10 +170,13 @@
|
|
|
170
170
|
"branch_confirm": {
|
|
171
171
|
"label": "{summary}",
|
|
172
172
|
"labels": {
|
|
173
|
-
"new": "새 브랜치 `{branch}` 를 base-ref `{base_ref}` 에서
|
|
174
|
-
"reuse": "
|
|
173
|
+
"new": "새 브랜치 `{branch}` 를 base-ref `{base_ref}` 에서 분기해 `{task_key}` 디렉터리(`{path}`)에 체크아웃합니다 — 진행할까요?",
|
|
174
|
+
"reuse": "기존 `{task_key}` 디렉터리(`{path}`, 브랜치 `{branch}`)에서 이어서 진행합니다 — 진행할까요?",
|
|
175
175
|
"in_worktree": "현재 worktree(`{path}`)에서 그대로 진행합니다(이미 non-main worktree) — 진행할까요?",
|
|
176
|
-
"not_git": "git 저장소가 아니므로 `{path}` 에서 직접 진행합니다 — 진행할까요?"
|
|
176
|
+
"not_git": "git 저장소가 아니므로 `{path}` 에서 직접 진행합니다 — 진행할까요?",
|
|
177
|
+
"impl_stage_new": "implementation 은 stage 격리로 동작합니다 — stage {stage} worktree 를 `{path}` 에 브랜치 `{branch}` 로 새로 만들고, base 커밋은 의존 stage 의 done 커밋 기준으로 run 준비 시점에 해소됩니다 — 진행할까요?",
|
|
178
|
+
"impl_stage_reuse": "기존 stage {stage} worktree(`{path}`, 브랜치 `{branch}`)에서 이어서 진행합니다 — 진행할까요?",
|
|
179
|
+
"impl_stage_auto": "implementation 은 stage 격리로 동작합니다 — stage 번호는 run 준비 시점에 자동 선택되고, `{task_key}` 디렉터리(`{path}`) 아래 `stage-<N>/` worktree 가 새로 만들어지거나 재사용됩니다 — 진행할까요?"
|
|
177
180
|
},
|
|
178
181
|
"options": { "proceed": "진행", "edit": "base-ref 다시 고르기", "abort": "중단" },
|
|
179
182
|
"echo_template": "branch-confirm: {value}"
|
|
@@ -407,6 +410,8 @@
|
|
|
407
410
|
"confirmation": {
|
|
408
411
|
"header": "선택 확인:",
|
|
409
412
|
"workers_implementation_default": " workers : (프로필 기본 — executor + verifier 2 + report-writer)",
|
|
413
|
+
"base_ref_stage_isolated": " base-ref : (stage 격리 — 의존 stage 기준으로 run 준비 시점에 자동 해소)",
|
|
414
|
+
"base_ref_reuse_task_dir": " base-ref : (기존 `{task_key}` 디렉터리 재사용 — 최초 base 유지)",
|
|
410
415
|
"stage_whole_task": "전체 task",
|
|
411
416
|
"handoff_scope_whole_task": "전체 task (whole-task 검증 기반)",
|
|
412
417
|
"handoff_scope_stage_group": "stage-group ({stages})"
|
|
@@ -50,22 +50,52 @@ def append_consumer(plan_run_root: Path, *, impl_task_key: str, stage: int,
|
|
|
50
50
|
if status not in ("started", "done"):
|
|
51
51
|
raise ValueError(f"status must be 'started' or 'done', got: {status!r}")
|
|
52
52
|
with consumers_mutex(plan_run_root):
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
53
|
+
if not _equivalent_row_exists(plan_run_root, impl_task_key, stage,
|
|
54
|
+
status, force_reappend,
|
|
55
|
+
fields.get("head_commit")):
|
|
56
|
+
record: Dict[str, Any] = {
|
|
57
|
+
"impl_task_key": impl_task_key,
|
|
58
|
+
"stage": stage,
|
|
59
|
+
"status": status,
|
|
60
|
+
**fields,
|
|
61
|
+
}
|
|
62
|
+
_append_row(plan_run_root, record)
|
|
63
|
+
# done 은 점유 해제 이벤트이기도 하다 — 중복 append(no-op)에서도 풀어야
|
|
64
|
+
# release 없이 done 만 기록된 과거 run 의 잔존 점유가 다음 호출에서 치유된다.
|
|
65
|
+
if status == "done":
|
|
66
|
+
_release_stage_reservation(impl_task_key, stage)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _equivalent_row_exists(plan_run_root: Path, impl_task_key: str, stage: int,
|
|
70
|
+
status: str, force_reappend: bool,
|
|
71
|
+
head_commit: Any) -> bool:
|
|
72
|
+
for row in read_consumers(plan_run_root):
|
|
73
|
+
if (row.get("impl_task_key") == impl_task_key
|
|
74
|
+
and row.get("stage") == stage
|
|
75
|
+
and row.get("status") == status):
|
|
76
|
+
if not force_reappend:
|
|
77
|
+
return True
|
|
78
|
+
if row.get("head_commit") == head_commit:
|
|
79
|
+
return True # 동일 보정의 중복 재-append 방지
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _release_stage_reservation(impl_task_key: str, stage: Any) -> None:
|
|
84
|
+
"""done 이 기록된 stage 의 worktree-registry 점유(stage-key)를 해제한다.
|
|
85
|
+
|
|
86
|
+
release 는 점유 표시만 푼다 — worktree 디렉토리·브랜치는 보존된다.
|
|
87
|
+
registry 좌표는 TASK_KEY(`project:group:task`) 각 segment 의
|
|
88
|
+
safe-segment 와 같다(stage 예약이 그렇게 만들어진다). 형식이 다르면
|
|
89
|
+
점유 주체가 아니므로 건너뛴다."""
|
|
90
|
+
parts = impl_task_key.split(":")
|
|
91
|
+
if len(parts) != 3 or not isinstance(stage, int):
|
|
92
|
+
return
|
|
93
|
+
from .ids import _safe_fs_segment
|
|
94
|
+
from . import worktree_registry
|
|
95
|
+
worktree_registry.release(
|
|
96
|
+
_safe_fs_segment(parts[0]), _safe_fs_segment(parts[1]),
|
|
97
|
+
_safe_fs_segment(parts[2]), stage_number=stage,
|
|
98
|
+
)
|
|
69
99
|
|
|
70
100
|
|
|
71
101
|
def _append_row(plan_run_root: Path, record: Dict[str, Any]) -> None:
|
|
@@ -1516,7 +1516,10 @@ def _select_and_provision_implementation_stage(
|
|
|
1516
1516
|
started_stages=started_stages, reserved_stages=reserved_stages,
|
|
1517
1517
|
)
|
|
1518
1518
|
selected = batch[0]
|
|
1519
|
-
|
|
1519
|
+
# done stage 는 동시 run 이 아니다 — done 기록 시 점유는 해제되지만
|
|
1520
|
+
# (consumers._release_stage_reservation), crash·구버전 기록의 잔존
|
|
1521
|
+
# 예약이 남을 수 있어 읽기에서도 차감한다.
|
|
1522
|
+
concurrent_stages = sorted(reserved_stages - done_stages - {selected})
|
|
1520
1523
|
|
|
1521
1524
|
# spec §2.1 degradation: 주변 흐름이 non-git / nested-worktree 로 skipped 면
|
|
1522
1525
|
# stage 격리도 동일하게 degrade — worktree 없이 project HEAD 만 기록.
|
|
@@ -52,8 +52,10 @@ from okstra_ctl.workers import (
|
|
|
52
52
|
from okstra_ctl.workflow import PHASE_SEQUENCE
|
|
53
53
|
from okstra_ctl import worktree_registry
|
|
54
54
|
from okstra_ctl.worktree import (
|
|
55
|
+
compute_worktree_path,
|
|
55
56
|
is_git_work_tree,
|
|
56
57
|
main_worktree_path,
|
|
58
|
+
preview_stage_worktree_decision,
|
|
57
59
|
preview_worktree_decision,
|
|
58
60
|
)
|
|
59
61
|
from okstra_ctl.paths import task_runs_dir
|
|
@@ -2423,6 +2425,8 @@ def _submit_pr_template_scope(state: WizardState, value: str) -> Optional[str]:
|
|
|
2423
2425
|
|
|
2424
2426
|
|
|
2425
2427
|
def _build_branch_confirm(state: WizardState) -> Prompt:
|
|
2428
|
+
if state.task_type == "implementation":
|
|
2429
|
+
return _build_branch_confirm_impl_stage(state)
|
|
2426
2430
|
decision = preview_worktree_decision(
|
|
2427
2431
|
project_root=Path(state.project_root),
|
|
2428
2432
|
project_id=state.project_id,
|
|
@@ -2443,6 +2447,7 @@ def _build_branch_confirm(state: WizardState) -> Prompt:
|
|
|
2443
2447
|
branch=decision.branch or "(none)",
|
|
2444
2448
|
base_ref=decision.base_ref or "(HEAD)",
|
|
2445
2449
|
path=decision.path,
|
|
2450
|
+
task_key=f"{state.task_group}/{state.task_id}",
|
|
2446
2451
|
)
|
|
2447
2452
|
# Pass the computed label as `summary` so _p's placeholder interpolation works.
|
|
2448
2453
|
t = _p(state.workspace_root, "branch_confirm", summary=label)
|
|
@@ -2455,6 +2460,43 @@ def _build_branch_confirm(state: WizardState) -> Prompt:
|
|
|
2455
2460
|
options=options, echo_template=t["echo_template"])
|
|
2456
2461
|
|
|
2457
2462
|
|
|
2463
|
+
def _build_branch_confirm_impl_stage(state: WizardState) -> Prompt:
|
|
2464
|
+
"""implementation 은 stage 격리로 동작하므로 task-key 디렉터리가 아니라
|
|
2465
|
+
이번 run 이 실제로 사용할 stage worktree 관점으로 미리보기를 보여준다."""
|
|
2466
|
+
raw = _load_wizard_root(state.workspace_root)["steps"]["branch_confirm"]
|
|
2467
|
+
task_key = f"{state.task_group}/{state.task_id}"
|
|
2468
|
+
stage = (state.selected_stage or "auto").strip()
|
|
2469
|
+
if stage == "auto":
|
|
2470
|
+
parent_dir = compute_worktree_path(
|
|
2471
|
+
project_id=state.project_id, task_group_segment=state.task_group,
|
|
2472
|
+
task_id_segment=state.task_id,
|
|
2473
|
+
)
|
|
2474
|
+
label = raw["labels"]["impl_stage_auto"].format(
|
|
2475
|
+
task_key=task_key, path=str(parent_dir),
|
|
2476
|
+
)
|
|
2477
|
+
decision_status = "new"
|
|
2478
|
+
else:
|
|
2479
|
+
decision = preview_stage_worktree_decision(
|
|
2480
|
+
project_id=state.project_id, task_group_segment=state.task_group,
|
|
2481
|
+
task_id_segment=state.task_id, work_category="",
|
|
2482
|
+
stage_number=int(stage),
|
|
2483
|
+
)
|
|
2484
|
+
key = "impl_stage_reuse" if decision.status == "reused" else "impl_stage_new"
|
|
2485
|
+
label = raw["labels"][key].format(
|
|
2486
|
+
stage=stage, path=decision.path, branch=decision.branch,
|
|
2487
|
+
task_key=task_key,
|
|
2488
|
+
)
|
|
2489
|
+
decision_status = decision.status
|
|
2490
|
+
t = _p(state.workspace_root, "branch_confirm", summary=label)
|
|
2491
|
+
opts = t["options"]
|
|
2492
|
+
options = [_opt("proceed", opts["proceed"])]
|
|
2493
|
+
if decision_status == "new" and _base_ref_required(state):
|
|
2494
|
+
options.append(_opt("edit", opts["edit"]))
|
|
2495
|
+
options.append(_opt("abort", opts["abort"]))
|
|
2496
|
+
return Prompt(step=S_BRANCH_CONFIRM, kind="pick", label=label,
|
|
2497
|
+
options=options, echo_template=t["echo_template"])
|
|
2498
|
+
|
|
2499
|
+
|
|
2458
2500
|
def _submit_branch_confirm(state: WizardState, value: str) -> Optional[str]:
|
|
2459
2501
|
if value == "edit":
|
|
2460
2502
|
_reset_from(state, S_BASE_REF_PICK)
|
|
@@ -3096,8 +3138,13 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
3096
3138
|
lines.append(f" brief : {state.brief_path or '(none)'}")
|
|
3097
3139
|
if state.task_type == "final-verification":
|
|
3098
3140
|
lines.append(" base-ref : (selected stage worktree)")
|
|
3141
|
+
elif state.task_type == "implementation" and state.reuse_worktree:
|
|
3142
|
+
lines.append(_msg(state.workspace_root, "confirmation",
|
|
3143
|
+
"base_ref_stage_isolated"))
|
|
3099
3144
|
elif state.reuse_worktree:
|
|
3100
|
-
lines.append(
|
|
3145
|
+
lines.append(_msg(state.workspace_root, "confirmation",
|
|
3146
|
+
"base_ref_reuse_task_dir",
|
|
3147
|
+
task_key=f"{state.task_group}/{state.task_id}"))
|
|
3101
3148
|
else:
|
|
3102
3149
|
lines.append(f" base-ref : {state.base_ref}")
|
|
3103
3150
|
if state.task_type == "implementation":
|
|
@@ -195,6 +195,41 @@ def preview_worktree_decision(
|
|
|
195
195
|
)
|
|
196
196
|
|
|
197
197
|
|
|
198
|
+
def preview_stage_worktree_decision(
|
|
199
|
+
*,
|
|
200
|
+
project_id: str,
|
|
201
|
+
task_group_segment: str,
|
|
202
|
+
task_id_segment: str,
|
|
203
|
+
work_category: str,
|
|
204
|
+
stage_number: int,
|
|
205
|
+
) -> "WorktreeDecision":
|
|
206
|
+
"""Side-effect-free: what provision_stage_worktree WOULD do (reuse vs new).
|
|
207
|
+
|
|
208
|
+
Mirrors provision_stage_worktree's registry-reuse branch exactly. A "new"
|
|
209
|
+
decision carries no base_ref — the stage base commit is resolved later by
|
|
210
|
+
the prepare flow from the stage's dependency anchors.
|
|
211
|
+
"""
|
|
212
|
+
safe_project = _safe_segment(project_id)
|
|
213
|
+
safe_group = _safe_segment(task_group_segment)
|
|
214
|
+
safe_task = _safe_segment(task_id_segment)
|
|
215
|
+
existing = worktree_registry.lookup(
|
|
216
|
+
safe_project, safe_group, safe_task, stage_number=stage_number)
|
|
217
|
+
if existing is not None and existing.status == "active":
|
|
218
|
+
return WorktreeDecision(
|
|
219
|
+
status="reused", path=existing.worktree_path,
|
|
220
|
+
branch=existing.branch, base_ref=existing.base_ref,
|
|
221
|
+
)
|
|
222
|
+
return WorktreeDecision(
|
|
223
|
+
status="new",
|
|
224
|
+
path=str(compute_worktree_path(
|
|
225
|
+
project_id=safe_project, task_group_segment=safe_group,
|
|
226
|
+
task_id_segment=safe_task, stage_number=stage_number)),
|
|
227
|
+
branch=compute_branch_name(
|
|
228
|
+
work_category=work_category, task_id_segment=safe_task,
|
|
229
|
+
stage_number=stage_number),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
198
233
|
def _safe_segment(value: str) -> str:
|
|
199
234
|
"""Sanitise a single path/branch segment.
|
|
200
235
|
|
|
@@ -47,7 +47,7 @@ The wizard tells you *which UI to use* via `kind` (and the optional `multi` flag
|
|
|
47
47
|
- `kind: "done"` → input collection finished; move to Step 5.
|
|
48
48
|
- `kind: "aborted"` → the user picked 중단; the wizard is terminally cancelled. Tell the user on one short line that the run setup was aborted, delete the state file (`rm` with the literal path), and stop this skill — do NOT call `render-args` or `render-bundle` (the wizard rejects `render-args` on an aborted state).
|
|
49
49
|
|
|
50
|
-
The `branch_confirm` step (shown just before `confirm`) is a normal `pick` step and is rendered the same way — no special handling needed. Its options always include `중단` (abort); `base-ref 다시 고르기` (edit) appears only when a new
|
|
50
|
+
The `branch_confirm` step (shown just before `confirm`) is a normal `pick` step and is rendered the same way — no special handling needed. Its options always include `중단` (abort); `base-ref 다시 고르기` (edit) appears only when a new branch would be created from the user-chosen base-ref. For `implementation` runs the wizard previews the **stage worktree** this run will actually use (not the task-key directory); render its label verbatim.
|
|
51
51
|
|
|
52
52
|
Never invent additional questions. Never reorder. **Never drop, hide, or merge a `pick` / `pick_group` option** — render every `options[]` entry as its own selectable `AskUserQuestion` choice, including entries that carry a `(default)` / `(recommended)` suffix. Do NOT collapse a multi-option pick into a "recommended + 직접 입력 / Other" shortlist: the wizard's `options[]` array IS the complete, authoritative choice set. Example: the `executor` step always emits `claude` / `codex` / `gemini` — show all three, never just `claude`. The run-prompt recommendation rule (1–2 추천 + 직접 입력) applies ONLY to prompts this skill authors itself (e.g. the conformance-waiver picker), never to wizard-provided `options[]`. Never use `AskUserQuestion` for `text` prompts — the wizard explicitly chose `text` to avoid the picker-Other re-render lag.
|
|
53
53
|
|