okstra 0.69.0 → 0.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/kr/architecture.md +6 -1
- package/docs/kr/cli.md +9 -2
- package/docs/superpowers/plans/2026-06-11-wizard-whole-task-final-verification.md +526 -0
- package/docs/superpowers/specs/2026-06-11-brief-entry-only-handoff-stage-entry-design.md +158 -0
- package/docs/superpowers/specs/2026-06-11-wizard-whole-task-final-verification-design.md +89 -0
- package/docs/task-process/release-handoff.md +6 -5
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/cli.sh +8 -1
- package/runtime/bin/lib/okstra/interactive.sh +8 -5
- package/runtime/bin/okstra.sh +1 -0
- package/runtime/prompts/profiles/release-handoff.md +5 -5
- package/runtime/prompts/wizard/prompts.ko.json +44 -2
- package/runtime/python/okstra_ctl/handoff.py +29 -0
- package/runtime/python/okstra_ctl/paths.py +7 -4
- package/runtime/python/okstra_ctl/render.py +10 -3
- package/runtime/python/okstra_ctl/run.py +177 -10
- package/runtime/python/okstra_ctl/wizard.py +320 -54
- package/runtime/skills/okstra-convergence/SKILL.md +1 -1
- package/runtime/skills/okstra-inspect/SKILL.md +1 -1
- package/runtime/skills/okstra-run/SKILL.md +3 -1
- package/runtime/templates/reports/release-handoff-input.template.md +10 -6
- package/src/render-bundle.mjs +9 -1
|
@@ -88,6 +88,13 @@ EXECUTORS = ["claude", "codex", "gemini"]
|
|
|
88
88
|
# (or the whole task via `auto`). Both gate the approved-plan + stage steps.
|
|
89
89
|
_STAGE_SCOPED_TASK_TYPES = ("implementation", "final-verification")
|
|
90
90
|
|
|
91
|
+
# brief 는 entry phase 의 입력물(비개발자/타 팀의 아이디어·에러 초안)이다.
|
|
92
|
+
# downstream phase 는 task manifest 의 taskBriefPath 를 자동 carry-in 하며
|
|
93
|
+
# 위저드에서 brief 를 묻지 않는다 (release-handoff 는 prepare 가 검증 보고서
|
|
94
|
+
# 인용 input 문서를 자동 생성하므로 brief 자체가 없다).
|
|
95
|
+
_BRIEF_ENTRY_TASK_TYPES = ("requirements-discovery", "improvement-discovery",
|
|
96
|
+
"error-analysis")
|
|
97
|
+
|
|
91
98
|
CANONICAL_BASE_REFS = ["main", "dev", "staging", "preprod", "prod"]
|
|
92
99
|
BASE_REF_FREE_INPUT_TOKEN = "__free_input__"
|
|
93
100
|
|
|
@@ -230,12 +237,14 @@ S_TASK_TYPE_TEXT = "task_type_text"
|
|
|
230
237
|
S_BRIEF_KEEP = "brief_keep"
|
|
231
238
|
S_BRIEF_PATH_PICK = "brief_path_pick"
|
|
232
239
|
S_BRIEF_PATH = "brief_path"
|
|
240
|
+
S_BRIEF_CARRY = "brief_carry"
|
|
233
241
|
S_BASE_REF_PICK = "base_ref_pick"
|
|
234
242
|
S_BASE_REF_TEXT = "base_ref_text"
|
|
235
243
|
S_APPROVED_PLAN_PICK = "approved_plan_pick"
|
|
236
244
|
S_APPROVED_PLAN = "approved_plan"
|
|
237
245
|
S_APPROVE_PLAN_CONFIRM = "approve_plan_confirm"
|
|
238
246
|
S_STAGE_PICK = "stage_pick"
|
|
247
|
+
S_HANDOFF_STAGE_PICK = "handoff_stage_pick"
|
|
239
248
|
S_EXECUTOR = "executor"
|
|
240
249
|
S_CRITIC_PICK = "critic_pick"
|
|
241
250
|
S_CRITIC_TEXT = "critic_text"
|
|
@@ -334,6 +343,11 @@ class WizardState:
|
|
|
334
343
|
critic: str = ""
|
|
335
344
|
critic_pending_text: bool = False
|
|
336
345
|
|
|
346
|
+
# release-handoff: PR 로 내보낼 범위. mode 는 stages 선택의 파생값이다 —
|
|
347
|
+
# whole-task = 빈 stages(whole-task 검증 기반 단일 PR), stage-group = csv.
|
|
348
|
+
handoff_mode: str = "" # "" | "whole-task" | "stage-group"
|
|
349
|
+
handoff_stages: str = "" # csv ("2,3"), whole-task 면 ""
|
|
350
|
+
|
|
337
351
|
# resume: 직전 run-inputs 재사용 여부 (None=미응답, True=재사용, False=재입력)
|
|
338
352
|
reuse_previous: Optional[bool] = None
|
|
339
353
|
|
|
@@ -767,6 +781,11 @@ def _base_ref_required(state: WizardState) -> bool:
|
|
|
767
781
|
return state.task_type != "final-verification" and state.reuse_worktree is False
|
|
768
782
|
|
|
769
783
|
|
|
784
|
+
def _brief_resolved(state: WizardState) -> bool:
|
|
785
|
+
"""brief 입력이 끝났는가. release-handoff 는 brief 가 없는 phase 라 항상 True."""
|
|
786
|
+
return bool(state.brief_path) or state.task_type == "release-handoff"
|
|
787
|
+
|
|
788
|
+
|
|
770
789
|
def _base_ref_ready(state: WizardState) -> bool:
|
|
771
790
|
return not _base_ref_required(state) or S_BASE_REF_PICK in state.answered
|
|
772
791
|
|
|
@@ -779,6 +798,54 @@ def _stage_auto_allowed(state: WizardState) -> bool:
|
|
|
779
798
|
return state.task_type == "implementation"
|
|
780
799
|
|
|
781
800
|
|
|
801
|
+
def _parse_stage_objects(state: WizardState) -> list:
|
|
802
|
+
"""승인 plan 의 Stage Map stage 객체 목록. validator 의 _parse_stage_map 재사용.
|
|
803
|
+
`_build_stage_pick` 과 `_whole_task_allowed` 가 공유한다."""
|
|
804
|
+
import importlib.util as _ilu
|
|
805
|
+
import sys as _sys
|
|
806
|
+
plan_text = Path(state.approved_plan_path).read_text(encoding="utf-8")
|
|
807
|
+
validator_path = (Path(state.workspace_root) / "validators"
|
|
808
|
+
/ "validate-implementation-plan-stages.py")
|
|
809
|
+
spec = _ilu.spec_from_file_location("_ip_stage_v_wizard", str(validator_path))
|
|
810
|
+
if spec is None or spec.loader is None:
|
|
811
|
+
raise WizardError(f"cannot load stage validator at {validator_path}")
|
|
812
|
+
mod = _ilu.module_from_spec(spec)
|
|
813
|
+
_sys.modules["_ip_stage_v_wizard"] = mod
|
|
814
|
+
try:
|
|
815
|
+
spec.loader.exec_module(mod)
|
|
816
|
+
stages, _errs = mod._parse_stage_map(plan_text)
|
|
817
|
+
finally:
|
|
818
|
+
_sys.modules.pop("_ip_stage_v_wizard", None)
|
|
819
|
+
return stages
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def _done_stage_numbers(state: WizardState) -> set:
|
|
823
|
+
"""approved plan 을 소비한 implementation run 들의 consumers.jsonl 에서
|
|
824
|
+
done 처리된 stage 번호 집합. git 호출 없음 — 파일 읽기만(prepare 와 동일 SSOT)."""
|
|
825
|
+
if not state.approved_plan_path:
|
|
826
|
+
return set()
|
|
827
|
+
from .consumers import (read_consumers, backfill_done_from_carry,
|
|
828
|
+
latest_done_by_stage)
|
|
829
|
+
plan_run_root = Path(state.approved_plan_path).resolve().parents[1]
|
|
830
|
+
backfill_done_from_carry(plan_run_root)
|
|
831
|
+
rows = read_consumers(plan_run_root)
|
|
832
|
+
return set(latest_done_by_stage(rows).keys())
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def _whole_task_allowed(state: WizardState) -> bool:
|
|
836
|
+
"""final-verification 이고 Stage Map 의 모든 stage 가 done 일 때만 True.
|
|
837
|
+
위저드는 done 만 본다 — 머지/clean/active 는 prepare 게이트가 강제한다."""
|
|
838
|
+
if state.task_type != "final-verification":
|
|
839
|
+
return False
|
|
840
|
+
if not state.approved_plan_path:
|
|
841
|
+
return False
|
|
842
|
+
stages = _parse_stage_objects(state)
|
|
843
|
+
if not stages:
|
|
844
|
+
return False
|
|
845
|
+
done = _done_stage_numbers(state)
|
|
846
|
+
return all(s.stage_number in done for s in stages)
|
|
847
|
+
|
|
848
|
+
|
|
782
849
|
def _existing_task_brief(project_root: Path, task_key: str) -> str:
|
|
783
850
|
"""Read taskBriefPath from manifest for an existing task. Empty if none."""
|
|
784
851
|
root = find_task_root(project_root, task_key)
|
|
@@ -1167,6 +1234,23 @@ def _build_task_type(state: WizardState) -> Prompt:
|
|
|
1167
1234
|
echo_template=t["echo_template"])
|
|
1168
1235
|
|
|
1169
1236
|
|
|
1237
|
+
def _carry_in_existing_brief(state: WizardState) -> str:
|
|
1238
|
+
"""downstream task-type 이 등록된 brief 를 자동 carry-in 한다. 주입한 경로(또는 '')."""
|
|
1239
|
+
if state.task_type in _BRIEF_ENTRY_TASK_TYPES:
|
|
1240
|
+
return ""
|
|
1241
|
+
if state.task_type == "release-handoff":
|
|
1242
|
+
return "" # prepare 가 검증 보고서 인용 input 문서를 자동 생성한다
|
|
1243
|
+
if state.brief_path or not state.existing_brief_path:
|
|
1244
|
+
return ""
|
|
1245
|
+
p = Path(state.existing_brief_path)
|
|
1246
|
+
if not p.is_absolute():
|
|
1247
|
+
p = Path(state.project_root) / p
|
|
1248
|
+
if not p.is_file():
|
|
1249
|
+
return "" # manifest 경로가 깨졌으면 brief_carry 단계가 처리한다
|
|
1250
|
+
state.brief_path = state.existing_brief_path
|
|
1251
|
+
return state.brief_path
|
|
1252
|
+
|
|
1253
|
+
|
|
1170
1254
|
def _apply_task_type(state: WizardState, value: str) -> str:
|
|
1171
1255
|
if value not in TASK_TYPE_VALUES:
|
|
1172
1256
|
raise WizardError(
|
|
@@ -1174,6 +1258,11 @@ def _apply_task_type(state: WizardState, value: str) -> str:
|
|
|
1174
1258
|
f"(expected one of: {', '.join(TASK_TYPE_VALUES)})"
|
|
1175
1259
|
)
|
|
1176
1260
|
state.task_type = value
|
|
1261
|
+
# brief_carry 의 "entry phase 로 전환" 이 task-type 을 리셋한 뒤 submit() 이
|
|
1262
|
+
# brief_carry 를 answered 로 되돌려 놓는다 — task-type 을 다시 고르는 시점에
|
|
1263
|
+
# 퍼지해야 downstream 재선택 시 carry 단계가 다시 나온다.
|
|
1264
|
+
state.answered = [a for a in state.answered if a != S_BRIEF_CARRY]
|
|
1265
|
+
carried = _carry_in_existing_brief(state)
|
|
1177
1266
|
state.profile_workers = _load_profile_workers(
|
|
1178
1267
|
Path(state.workspace_root), value
|
|
1179
1268
|
)
|
|
@@ -1183,6 +1272,8 @@ def _apply_task_type(state: WizardState, value: str) -> str:
|
|
|
1183
1272
|
# Reuse-worktree is decided once identity is final. Recompute here so
|
|
1184
1273
|
# subsequent base-ref step knows whether to apply.
|
|
1185
1274
|
state.reuse_worktree = _resolve_reuse_worktree(state)
|
|
1275
|
+
if carried:
|
|
1276
|
+
return f"task-type: {value}\nbrief (carry-in): {carried}"
|
|
1186
1277
|
return f"task-type: {value}"
|
|
1187
1278
|
|
|
1188
1279
|
|
|
@@ -1309,6 +1400,37 @@ def _submit_brief_path(state: WizardState, value: str) -> Optional[str]:
|
|
|
1309
1400
|
return f"brief: {p}"
|
|
1310
1401
|
|
|
1311
1402
|
|
|
1403
|
+
BRIEF_CARRY_SWITCH_ENTRY = "__switch_entry__"
|
|
1404
|
+
BRIEF_CARRY_ABORT = "__abort__"
|
|
1405
|
+
|
|
1406
|
+
|
|
1407
|
+
def _build_brief_carry(state: WizardState) -> Prompt:
|
|
1408
|
+
t = _p(state.workspace_root, "brief_carry", task_type=state.task_type)
|
|
1409
|
+
return Prompt(
|
|
1410
|
+
step=S_BRIEF_CARRY, kind="pick",
|
|
1411
|
+
label=t["label"],
|
|
1412
|
+
options=[_opt(k, v) for k, v in t["options"].items()],
|
|
1413
|
+
echo_template=t["echo_template"],
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
def _submit_brief_carry(state: WizardState, value: str) -> Optional[str]:
|
|
1418
|
+
t = _p(state.workspace_root, "brief_carry", task_type=state.task_type)
|
|
1419
|
+
if value == BRIEF_CARRY_SWITCH_ENTRY:
|
|
1420
|
+
_reset_from(state, S_TASK_TYPE)
|
|
1421
|
+
return t["echo_variants"]["switch_entry"]
|
|
1422
|
+
if value == PICK_TYPE_CUSTOM:
|
|
1423
|
+
state.brief_path_pending_text = True
|
|
1424
|
+
return None
|
|
1425
|
+
if value == BRIEF_CARRY_ABORT:
|
|
1426
|
+
state.aborted = True
|
|
1427
|
+
return t["echo_variants"]["abort"]
|
|
1428
|
+
raise WizardError(
|
|
1429
|
+
f"expected '{BRIEF_CARRY_SWITCH_ENTRY}', {PICK_TYPE_CUSTOM!r}, "
|
|
1430
|
+
f"or '{BRIEF_CARRY_ABORT}', got: {value!r}"
|
|
1431
|
+
)
|
|
1432
|
+
|
|
1433
|
+
|
|
1312
1434
|
def _build_base_ref_pick(state: WizardState) -> Prompt:
|
|
1313
1435
|
t = _p(state.workspace_root, "base_ref_pick")
|
|
1314
1436
|
recommended_suffix = t["options"].get("_RECOMMENDED_SUFFIX", "")
|
|
@@ -1358,6 +1480,7 @@ _REUSE_LAST_TOKEN = "__reuse_last__"
|
|
|
1358
1480
|
_SIBLINGS_TOKEN = "__siblings__"
|
|
1359
1481
|
_LATEST_REPORT_TOKEN = "__latest_report__"
|
|
1360
1482
|
_PROJECT_DEFAULT_TOKEN = "__project_default__"
|
|
1483
|
+
WHOLE_TASK_STAGE = "__whole_task__"
|
|
1361
1484
|
|
|
1362
1485
|
|
|
1363
1486
|
def _list_implementation_planning_reports(
|
|
@@ -1493,37 +1616,31 @@ def _submit_approve_plan_confirm(state: WizardState, value: str) -> Optional[str
|
|
|
1493
1616
|
|
|
1494
1617
|
def _build_stage_pick(state: WizardState) -> Prompt:
|
|
1495
1618
|
"""Parse the Stage Map from the approved plan and build the stage picker."""
|
|
1496
|
-
import importlib.util as _ilu
|
|
1497
|
-
import sys as _sys
|
|
1498
1619
|
t = _p(state.workspace_root, "stage_pick")
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
Path(state.workspace_root) / "validators"
|
|
1502
|
-
/ "validate-implementation-plan-stages.py"
|
|
1503
|
-
)
|
|
1504
|
-
spec = _ilu.spec_from_file_location("_ip_stage_v_wizard", str(validator_path))
|
|
1505
|
-
if spec is None or spec.loader is None:
|
|
1506
|
-
raise WizardError(f"cannot load stage validator at {validator_path}")
|
|
1507
|
-
mod = _ilu.module_from_spec(spec)
|
|
1508
|
-
_sys.modules["_ip_stage_v_wizard"] = mod
|
|
1509
|
-
try:
|
|
1510
|
-
spec.loader.exec_module(mod)
|
|
1511
|
-
stages, _errs = mod._parse_stage_map(plan_text)
|
|
1512
|
-
finally:
|
|
1513
|
-
_sys.modules.pop("_ip_stage_v_wizard", None)
|
|
1620
|
+
stages = _parse_stage_objects(state)
|
|
1621
|
+
is_fv = state.task_type == "final-verification"
|
|
1514
1622
|
label = (
|
|
1515
1623
|
t.get("label_final_verification", t["label"])
|
|
1516
|
-
if
|
|
1517
|
-
else t["label"]
|
|
1624
|
+
if is_fv else t["label"]
|
|
1518
1625
|
)
|
|
1626
|
+
done = _done_stage_numbers(state) if is_fv else set()
|
|
1519
1627
|
options = []
|
|
1520
1628
|
if _stage_auto_allowed(state):
|
|
1521
1629
|
options.append(_opt("auto", t["options"]["auto"]))
|
|
1630
|
+
if _whole_task_allowed(state):
|
|
1631
|
+
options.append(_opt(WHOLE_TASK_STAGE, t["options"]["whole_task"]))
|
|
1522
1632
|
for s in stages:
|
|
1523
1633
|
depends = ",".join(map(str, s.depends_on)) or "(none)"
|
|
1634
|
+
suffix = ""
|
|
1635
|
+
if is_fv:
|
|
1636
|
+
mark = (t["options"]["done_mark"]
|
|
1637
|
+
if s.stage_number in done
|
|
1638
|
+
else t["options"]["undone_mark"])
|
|
1639
|
+
suffix = f" {mark}"
|
|
1524
1640
|
options.append(_opt(
|
|
1525
1641
|
str(s.stage_number),
|
|
1526
|
-
f"{s.stage_number}: {s.title}
|
|
1642
|
+
f"{s.stage_number}: {s.title} "
|
|
1643
|
+
f"[depends-on: {depends} | steps: {s.step_count}]{suffix}",
|
|
1527
1644
|
))
|
|
1528
1645
|
return Prompt(
|
|
1529
1646
|
step=S_STAGE_PICK, kind="pick",
|
|
@@ -1541,17 +1658,123 @@ def _submit_stage_pick(state: WizardState, answer: str) -> Optional[str]:
|
|
|
1541
1658
|
raise WizardError(
|
|
1542
1659
|
"final-verification requires an explicit stage number"
|
|
1543
1660
|
)
|
|
1661
|
+
elif answer == WHOLE_TASK_STAGE:
|
|
1662
|
+
if not _whole_task_allowed(state):
|
|
1663
|
+
raise WizardError(
|
|
1664
|
+
"whole-task verification requires final-verification "
|
|
1665
|
+
"with all stages done"
|
|
1666
|
+
)
|
|
1544
1667
|
else:
|
|
1545
1668
|
try:
|
|
1546
1669
|
int(answer)
|
|
1547
1670
|
except ValueError:
|
|
1548
1671
|
raise WizardError(
|
|
1549
|
-
f"answer must be 'auto' or a stage number,
|
|
1672
|
+
f"answer must be 'auto', whole-task, or a stage number, "
|
|
1673
|
+
f"got {answer!r}"
|
|
1550
1674
|
)
|
|
1551
1675
|
state.selected_stage = answer
|
|
1552
1676
|
return f"stage: {answer}"
|
|
1553
1677
|
|
|
1554
1678
|
|
|
1679
|
+
def _handoff_msgs(state: WizardState) -> dict:
|
|
1680
|
+
"""handoff_stage_pick 의 JSON 텍스트 묶음 (label 미사용 조회용)."""
|
|
1681
|
+
return _p(state.workspace_root, "handoff_stage_pick", blocked="")
|
|
1682
|
+
|
|
1683
|
+
|
|
1684
|
+
def _resolve_handoff_plan(state: WizardState) -> Path:
|
|
1685
|
+
"""release-handoff 의 approved plan 을 질문 없이 자동 해소한다.
|
|
1686
|
+
|
|
1687
|
+
plan 미존재/미승인은 사용자가 고칠 대상이 아니라 라이프사이클 선행 단계
|
|
1688
|
+
누락이므로 picker 대신 안내 메시지로 즉시 실패한다."""
|
|
1689
|
+
if state.approved_plan_path:
|
|
1690
|
+
return Path(state.approved_plan_path)
|
|
1691
|
+
t = _handoff_msgs(state)
|
|
1692
|
+
latest = _latest_implementation_planning_report(state)
|
|
1693
|
+
if latest is None:
|
|
1694
|
+
raise WizardError(t["errors"]["no_plan"])
|
|
1695
|
+
p, fully_approved = _classify_approved_plan(
|
|
1696
|
+
str(latest), Path(state.project_root))
|
|
1697
|
+
if not fully_approved:
|
|
1698
|
+
raise WizardError(t["errors"]["plan_not_approved"].format(plan=p))
|
|
1699
|
+
state.approved_plan_path = str(p)
|
|
1700
|
+
return p
|
|
1701
|
+
|
|
1702
|
+
|
|
1703
|
+
def _handoff_eligibility(state: WizardState) -> list:
|
|
1704
|
+
"""stage 별 PR 자격 — okstra_ctl.handoff 의 SSOT 판정을 그대로 재사용한다."""
|
|
1705
|
+
from okstra_ctl.handoff import compute_eligibility
|
|
1706
|
+
from okstra_ctl.run import _parse_stage_map_into_ctx
|
|
1707
|
+
from okstra_ctl.consumers import read_consumers
|
|
1708
|
+
plan = _resolve_handoff_plan(state)
|
|
1709
|
+
stage_map = _parse_stage_map_into_ctx(str(plan))
|
|
1710
|
+
if not stage_map:
|
|
1711
|
+
raise WizardError(
|
|
1712
|
+
_handoff_msgs(state)["errors"]["no_stage_map"].format(plan=plan))
|
|
1713
|
+
rows = read_consumers(plan.resolve().parents[1])
|
|
1714
|
+
return compute_eligibility(stage_map, rows)
|
|
1715
|
+
|
|
1716
|
+
|
|
1717
|
+
def _latest_whole_task_fv_accepted(state: WizardState) -> str:
|
|
1718
|
+
"""accepted whole-task final-verification 보고서 경로 — handoff 모듈 SSOT 위임."""
|
|
1719
|
+
from okstra_ctl.handoff import latest_whole_task_fv_accepted
|
|
1720
|
+
return latest_whole_task_fv_accepted(
|
|
1721
|
+
state.project_root, state.project_id, state.task_group, state.task_id)
|
|
1722
|
+
|
|
1723
|
+
|
|
1724
|
+
def _build_handoff_stage_pick(state: WizardState) -> Prompt:
|
|
1725
|
+
elig = _handoff_eligibility(state)
|
|
1726
|
+
eligible = [e for e in elig if e["eligible"]]
|
|
1727
|
+
blocked = [e for e in elig if not e["eligible"]]
|
|
1728
|
+
whole_task_report = _latest_whole_task_fv_accepted(state)
|
|
1729
|
+
msgs = _handoff_msgs(state)
|
|
1730
|
+
blocked_summary = ("; ".join(
|
|
1731
|
+
f"stage {e['stage']} ({', '.join(e['reasons'])})" for e in blocked)
|
|
1732
|
+
or msgs["labels"]["blocked_none"])
|
|
1733
|
+
if not eligible and not whole_task_report:
|
|
1734
|
+
raise WizardError(
|
|
1735
|
+
msgs["errors"]["nothing_eligible"].format(blocked=blocked_summary))
|
|
1736
|
+
t = _p(state.workspace_root, "handoff_stage_pick", blocked=blocked_summary)
|
|
1737
|
+
options: list[Option] = []
|
|
1738
|
+
if whole_task_report:
|
|
1739
|
+
options.append(_opt(WHOLE_TASK_STAGE, t["labels"]["whole_task"]))
|
|
1740
|
+
stage_label = t["labels"]["stage"]
|
|
1741
|
+
for e in eligible:
|
|
1742
|
+
deps = ", ".join(str(d) for d in e["depends_on"]) or "-"
|
|
1743
|
+
options.append(_opt(str(e["stage"]),
|
|
1744
|
+
stage_label.format(stage=e["stage"], deps=deps)))
|
|
1745
|
+
return Prompt(
|
|
1746
|
+
step=S_HANDOFF_STAGE_PICK, kind="pick", multi=True,
|
|
1747
|
+
label=t["label"], options=options,
|
|
1748
|
+
echo_template=t["echo_template"],
|
|
1749
|
+
)
|
|
1750
|
+
|
|
1751
|
+
|
|
1752
|
+
def _submit_handoff_stage_pick(state: WizardState, value: str) -> Optional[str]:
|
|
1753
|
+
t = _handoff_msgs(state)
|
|
1754
|
+
picks = [v.strip() for v in (value or "").split(",") if v.strip()]
|
|
1755
|
+
if not picks:
|
|
1756
|
+
raise WizardError(t["errors"]["none_selected"])
|
|
1757
|
+
if WHOLE_TASK_STAGE in picks:
|
|
1758
|
+
if len(picks) > 1:
|
|
1759
|
+
raise WizardError(t["errors"]["whole_task_exclusive"])
|
|
1760
|
+
if not _latest_whole_task_fv_accepted(state):
|
|
1761
|
+
raise WizardError(t["errors"]["whole_task_missing"])
|
|
1762
|
+
state.handoff_mode = "whole-task"
|
|
1763
|
+
state.handoff_stages = ""
|
|
1764
|
+
return t["echo_variants"]["whole_task"]
|
|
1765
|
+
eligible = {str(e["stage"]) for e in _handoff_eligibility(state)
|
|
1766
|
+
if e["eligible"]}
|
|
1767
|
+
bad = [p for p in picks if p not in eligible]
|
|
1768
|
+
if bad:
|
|
1769
|
+
raise WizardError(t["errors"]["not_eligible"].format(
|
|
1770
|
+
bad=", ".join(bad), eligible=", ".join(sorted(eligible))))
|
|
1771
|
+
nums = sorted({int(p) for p in picks})
|
|
1772
|
+
state.handoff_mode = "stage-group"
|
|
1773
|
+
state.handoff_stages = ",".join(str(n) for n in nums)
|
|
1774
|
+
return t["echo_variants"]["stage_group"].format(
|
|
1775
|
+
stages=state.handoff_stages)
|
|
1776
|
+
|
|
1777
|
+
|
|
1555
1778
|
def _suggest_latest_final_report(state: WizardState) -> str:
|
|
1556
1779
|
"""task 의 모든 phase runs 디렉토리에서 가장 최근 final-report-*.md 의 relpath 를 반환.
|
|
1557
1780
|
|
|
@@ -2314,16 +2537,40 @@ STEPS: list[Step] = [
|
|
|
2314
2537
|
and s.task_group_pending_text),
|
|
2315
2538
|
build=_build_task_group_text, submit=_submit_task_group_text,
|
|
2316
2539
|
owns=("task_group", "task_group_pending_text")),
|
|
2540
|
+
# 신규 task 흐름 순서: task-group → task-type → (entry 면 brief) → task-id.
|
|
2541
|
+
# brief 는 entry phase 전용 입력이므로 task-type 이 정해진 뒤에만 물을 수 있다.
|
|
2542
|
+
Step(S_TASK_TYPE,
|
|
2543
|
+
applies=lambda s: (s.is_new_task is not None
|
|
2544
|
+
and (s.is_new_task is False or bool(s.task_group))
|
|
2545
|
+
and S_TASK_TYPE not in s.answered),
|
|
2546
|
+
build=_build_task_type, submit=_submit_task_type,
|
|
2547
|
+
owns=("task_type", "profile_workers", "profile_optional_workers",
|
|
2548
|
+
"reuse_worktree")),
|
|
2549
|
+
Step(S_TASK_TYPE_TEXT,
|
|
2550
|
+
applies=lambda s: (S_TASK_TYPE in s.answered
|
|
2551
|
+
and not s.task_type
|
|
2552
|
+
and S_TASK_TYPE_TEXT not in s.answered),
|
|
2553
|
+
build=_build_task_type_text, submit=_submit_task_type_text,
|
|
2554
|
+
owns=("task_type", "profile_workers", "profile_optional_workers",
|
|
2555
|
+
"reuse_worktree")),
|
|
2556
|
+
Step(S_BRIEF_KEEP,
|
|
2557
|
+
applies=lambda s: (not s.is_new_task
|
|
2558
|
+
and s.task_type in _BRIEF_ENTRY_TASK_TYPES
|
|
2559
|
+
and bool(s.existing_brief_path)
|
|
2560
|
+
and s.keep_existing_brief is None
|
|
2561
|
+
and S_TASK_TYPE in s.answered),
|
|
2562
|
+
build=_build_brief_keep, submit=_submit_brief_keep,
|
|
2563
|
+
owns=("keep_existing_brief",)),
|
|
2317
2564
|
Step(S_BRIEF_PATH_PICK,
|
|
2318
2565
|
applies=lambda s: (
|
|
2319
2566
|
not s.brief_path
|
|
2320
2567
|
and not s.brief_path_pending_text
|
|
2321
2568
|
and S_BRIEF_PATH_PICK not in s.answered
|
|
2569
|
+
and S_TASK_TYPE in s.answered
|
|
2570
|
+
and s.task_type in _BRIEF_ENTRY_TASK_TYPES
|
|
2322
2571
|
and (
|
|
2323
|
-
|
|
2572
|
+
s.is_new_task is True
|
|
2324
2573
|
or (s.is_new_task is False
|
|
2325
|
-
and S_TASK_TYPE in s.answered
|
|
2326
|
-
and bool(s.task_type)
|
|
2327
2574
|
and (s.keep_existing_brief is False
|
|
2328
2575
|
or not s.existing_brief_path))
|
|
2329
2576
|
)
|
|
@@ -2335,49 +2582,39 @@ STEPS: list[Step] = [
|
|
|
2335
2582
|
and S_BRIEF_PATH not in s.answered,
|
|
2336
2583
|
build=_build_brief_path, submit=_submit_brief_path,
|
|
2337
2584
|
owns=("brief_path", "task_group_suggestion", "task_id_suggestion")),
|
|
2585
|
+
# downstream task-type 인데 carry-in 할 brief 가 없을 때의 fallback.
|
|
2586
|
+
# release-handoff 는 brief 자체가 없는 phase 라 대상에서 빠진다.
|
|
2587
|
+
Step(S_BRIEF_CARRY,
|
|
2588
|
+
applies=lambda s: (S_TASK_TYPE in s.answered
|
|
2589
|
+
and bool(s.task_type)
|
|
2590
|
+
and s.task_type not in _BRIEF_ENTRY_TASK_TYPES
|
|
2591
|
+
and s.task_type != "release-handoff"
|
|
2592
|
+
and not s.brief_path
|
|
2593
|
+
and not s.brief_path_pending_text),
|
|
2594
|
+
build=_build_brief_carry, submit=_submit_brief_carry,
|
|
2595
|
+
owns=("brief_path_pending_text",)),
|
|
2338
2596
|
Step(S_TASK_ID,
|
|
2339
2597
|
applies=lambda s: (bool(s.is_new_task)
|
|
2340
|
-
and bool(s.brief_path)
|
|
2341
2598
|
and bool(s.task_group)
|
|
2599
|
+
and _brief_resolved(s)
|
|
2342
2600
|
and not s.task_id
|
|
2343
2601
|
and not s.task_id_pending_text),
|
|
2344
2602
|
build=_build_task_id, submit=_submit_task_id,
|
|
2345
2603
|
owns=("task_id", "task_id_pending_text")),
|
|
2346
2604
|
Step(S_TASK_ID_TEXT,
|
|
2347
2605
|
applies=lambda s: (bool(s.is_new_task)
|
|
2348
|
-
and bool(s.brief_path)
|
|
2349
2606
|
and bool(s.task_group)
|
|
2607
|
+
and _brief_resolved(s)
|
|
2350
2608
|
and not s.task_id
|
|
2351
2609
|
and s.task_id_pending_text),
|
|
2352
2610
|
build=_build_task_id_text, submit=_submit_task_id_text,
|
|
2353
2611
|
owns=("task_id", "task_id_pending_text")),
|
|
2354
|
-
Step(S_TASK_TYPE,
|
|
2355
|
-
applies=lambda s: (s.is_new_task is not None
|
|
2356
|
-
and (s.is_new_task is False or bool(s.task_id))
|
|
2357
|
-
and S_TASK_TYPE not in s.answered),
|
|
2358
|
-
build=_build_task_type, submit=_submit_task_type,
|
|
2359
|
-
owns=("task_type", "profile_workers", "profile_optional_workers",
|
|
2360
|
-
"reuse_worktree")),
|
|
2361
|
-
Step(S_TASK_TYPE_TEXT,
|
|
2362
|
-
applies=lambda s: (S_TASK_TYPE in s.answered
|
|
2363
|
-
and not s.task_type
|
|
2364
|
-
and S_TASK_TYPE_TEXT not in s.answered),
|
|
2365
|
-
build=_build_task_type_text, submit=_submit_task_type_text,
|
|
2366
|
-
owns=("task_type", "profile_workers", "profile_optional_workers",
|
|
2367
|
-
"reuse_worktree")),
|
|
2368
|
-
Step(S_BRIEF_KEEP,
|
|
2369
|
-
applies=lambda s: (not s.is_new_task
|
|
2370
|
-
and bool(s.existing_brief_path)
|
|
2371
|
-
and s.keep_existing_brief is None
|
|
2372
|
-
and S_TASK_TYPE in s.answered
|
|
2373
|
-
and bool(s.task_type)),
|
|
2374
|
-
build=_build_brief_keep, submit=_submit_brief_keep,
|
|
2375
|
-
owns=("keep_existing_brief",)),
|
|
2376
2612
|
Step(S_BASE_REF_PICK,
|
|
2377
2613
|
applies=lambda s: (S_TASK_TYPE in s.answered
|
|
2378
2614
|
and _base_ref_required(s)
|
|
2379
2615
|
and S_BASE_REF_PICK not in s.answered
|
|
2380
|
-
and
|
|
2616
|
+
and _brief_resolved(s)
|
|
2617
|
+
and (not s.is_new_task or bool(s.task_id))),
|
|
2381
2618
|
build=_build_base_ref_pick, submit=_submit_base_ref_pick,
|
|
2382
2619
|
owns=("base_ref", "base_ref_pending_text")),
|
|
2383
2620
|
Step(S_BASE_REF_TEXT,
|
|
@@ -2418,6 +2655,13 @@ STEPS: list[Step] = [
|
|
|
2418
2655
|
and S_STAGE_PICK not in s.answered),
|
|
2419
2656
|
build=_build_stage_pick, submit=_submit_stage_pick,
|
|
2420
2657
|
owns=("selected_stage",)),
|
|
2658
|
+
Step(S_HANDOFF_STAGE_PICK,
|
|
2659
|
+
applies=lambda s: (s.task_type == "release-handoff"
|
|
2660
|
+
and bool(s.task_group)
|
|
2661
|
+
and bool(s.task_id)
|
|
2662
|
+
and S_HANDOFF_STAGE_PICK not in s.answered),
|
|
2663
|
+
build=_build_handoff_stage_pick, submit=_submit_handoff_stage_pick,
|
|
2664
|
+
owns=("handoff_mode", "handoff_stages", "approved_plan_path")),
|
|
2421
2665
|
Step(S_EXECUTOR,
|
|
2422
2666
|
applies=lambda s: (s.task_type == "implementation"
|
|
2423
2667
|
and bool(s.approved_plan_path)
|
|
@@ -2594,7 +2838,8 @@ def _identity_ready(s: WizardState) -> bool:
|
|
|
2594
2838
|
"""All identity questions (task pick → executor for impl) answered."""
|
|
2595
2839
|
if not s.task_type:
|
|
2596
2840
|
return False
|
|
2597
|
-
|
|
2841
|
+
# release-handoff 는 brief 가 없다 — prepare 가 검증 보고서 인용 input 을 생성한다.
|
|
2842
|
+
if not s.brief_path and s.task_type != "release-handoff":
|
|
2598
2843
|
return False
|
|
2599
2844
|
if _base_ref_required(s) and S_BASE_REF_PICK not in s.answered:
|
|
2600
2845
|
return False
|
|
@@ -2606,6 +2851,9 @@ def _identity_ready(s: WizardState) -> bool:
|
|
|
2606
2851
|
if s.task_type == "implementation":
|
|
2607
2852
|
if not s.executor:
|
|
2608
2853
|
return False
|
|
2854
|
+
if (s.task_type == "release-handoff"
|
|
2855
|
+
and S_HANDOFF_STAGE_PICK not in s.answered):
|
|
2856
|
+
return False
|
|
2609
2857
|
return True
|
|
2610
2858
|
|
|
2611
2859
|
|
|
@@ -2658,6 +2906,7 @@ _FIELD_DEFAULTS: dict[str, Any] = {
|
|
|
2658
2906
|
"base_ref_pending_text": False, "approved_plan_path": "",
|
|
2659
2907
|
"approved_plan_pending_text": False,
|
|
2660
2908
|
"selected_stage": "auto",
|
|
2909
|
+
"handoff_mode": "", "handoff_stages": "",
|
|
2661
2910
|
"executor": "", "critic": "", "critic_pending_text": False,
|
|
2662
2911
|
"reuse_previous": None,
|
|
2663
2912
|
"use_defaults": None, "workers_override": "",
|
|
@@ -2797,11 +3046,14 @@ def render_args(state: WizardState) -> dict[str, str]:
|
|
|
2797
3046
|
if state.task_type == "implementation":
|
|
2798
3047
|
stage = state.selected_stage or "auto"
|
|
2799
3048
|
elif state.task_type == "final-verification":
|
|
2800
|
-
if
|
|
3049
|
+
if state.selected_stage == WHOLE_TASK_STAGE:
|
|
3050
|
+
stage = "" # prepare 가 빈 stage 를 whole-task 로 해석
|
|
3051
|
+
elif not state.selected_stage or state.selected_stage == "auto":
|
|
2801
3052
|
raise WizardError(
|
|
2802
3053
|
"final-verification requires an explicit stage number"
|
|
2803
3054
|
)
|
|
2804
|
-
|
|
3055
|
+
else:
|
|
3056
|
+
stage = state.selected_stage
|
|
2805
3057
|
else:
|
|
2806
3058
|
stage = ""
|
|
2807
3059
|
pr_template = (
|
|
@@ -2820,6 +3072,7 @@ def render_args(state: WizardState) -> dict[str, str]:
|
|
|
2820
3072
|
"critic": state.critic,
|
|
2821
3073
|
"approved-plan": state.approved_plan_path,
|
|
2822
3074
|
"stage": stage,
|
|
3075
|
+
"stages": state.handoff_stages,
|
|
2823
3076
|
"base-ref": base_ref,
|
|
2824
3077
|
"workers": workers,
|
|
2825
3078
|
"directive": state.directive,
|
|
@@ -2872,12 +3125,25 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
2872
3125
|
if state.task_type in _STAGE_SCOPED_TASK_TYPES:
|
|
2873
3126
|
lines.append(f" approved-plan : {state.approved_plan_path}")
|
|
2874
3127
|
stage = (
|
|
2875
|
-
state.
|
|
2876
|
-
|
|
3128
|
+
_msg(state.workspace_root, "confirmation", "stage_whole_task")
|
|
3129
|
+
if state.selected_stage == WHOLE_TASK_STAGE
|
|
3130
|
+
else (state.selected_stage
|
|
3131
|
+
or ("auto" if state.task_type == "implementation"
|
|
3132
|
+
else "(not selected)"))
|
|
2877
3133
|
)
|
|
2878
3134
|
lines.append(f" stage : {stage}")
|
|
2879
3135
|
if state.clarification_response_path:
|
|
2880
3136
|
lines.append(f" clarification : {state.clarification_response_path}")
|
|
3137
|
+
if state.task_type == "release-handoff" and state.handoff_mode:
|
|
3138
|
+
scope = (
|
|
3139
|
+
_msg(state.workspace_root, "confirmation",
|
|
3140
|
+
"handoff_scope_whole_task")
|
|
3141
|
+
if state.handoff_mode == "whole-task"
|
|
3142
|
+
else _msg(state.workspace_root, "confirmation",
|
|
3143
|
+
"handoff_scope_stage_group",
|
|
3144
|
+
stages=state.handoff_stages)
|
|
3145
|
+
)
|
|
3146
|
+
lines.append(f" handoff scope : {scope}")
|
|
2881
3147
|
if state.task_type == "release-handoff" and state.pr_template_path:
|
|
2882
3148
|
lines.append(f" pr-template : {state.pr_template_path} ({state.pr_template_scope or 'once'})")
|
|
2883
3149
|
return "\n".join(lines)
|
|
@@ -572,7 +572,7 @@ Promoted blockers enter `## 5.8 Acceptance Blockers`; since `accepted` requires
|
|
|
572
572
|
|
|
573
573
|
### State
|
|
574
574
|
|
|
575
|
-
Critic output lives
|
|
575
|
+
Critic output lives in the run's `worker-results/` directory (`runs/final-verification/worker-results/` for whole-task verification, `runs/final-verification/stage-<N>/worker-results/` for single-stage), filename `<provider>-critic-final-verification-<seq>.md`. The convergence state `config.critic` summary (see §"Coverage critic pass") records `mode: "acceptance-devils-advocate"`, `candidatesProposed`, `confirmedBlockers`, `downgradedToResidual` (optional v1.2 fields; readers treat absence as null).
|
|
576
576
|
|
|
577
577
|
## Output
|
|
578
578
|
|
|
@@ -299,7 +299,7 @@ B. **`task-manifest.json` (direct):** if catalog missing, slugify task-group / t
|
|
|
299
299
|
|
|
300
300
|
C. **`timeline.json` (specific run):** for a specific date or run, read `.okstra/tasks/<group-segment>/<id-segment>/history/timeline.json`, filter `runs[]` by `runTimestamp` / `status` / `taskType`, use `runs[].reportPath`.
|
|
301
301
|
|
|
302
|
-
D. **Specific task-type (fallback):** `latestReportPath` is task-type-agnostic. For a specific task-type's latest report, look under `.okstra/tasks/<group-segment>/<id-segment>/runs/<task-type-segment>/reports/`, filename pattern `final-report-<task-type-segment>-<NNN>.md` per `scripts/okstra_ctl/sequence.py:31`. Highest seq is latest. Cross-verify with `timeline.json`'s `runs[].taskType` filter.
|
|
302
|
+
D. **Specific task-type (fallback):** `latestReportPath` is task-type-agnostic. For a specific task-type's latest report, look under `.okstra/tasks/<group-segment>/<id-segment>/runs/<task-type-segment>/reports/`, filename pattern `final-report-<task-type-segment>-<NNN>.md` per `scripts/okstra_ctl/sequence.py:31`. Highest seq is latest. Stage-isolated runs (`implementation`, single-stage `final-verification`) keep their reports one level deeper at `runs/<task-type-segment>/stage-<N>/reports/` — scan those subdirectories too; seq is independent per stage. Cross-verify with `timeline.json`'s `runs[].taskType` filter.
|
|
303
303
|
|
|
304
304
|
### report.2 — Confirm existence
|
|
305
305
|
|
|
@@ -121,9 +121,10 @@ That is the entire interactive flow. The wizard handles:
|
|
|
121
121
|
|
|
122
122
|
- new-vs-existing task split (남은 작업 — `workStatus != done` — 최신순 3개 추천 + 직접 입력), task-group / task-id slug validation (각각 최근 후보 3개 추천 + 직접 입력),
|
|
123
123
|
- task-type pick (추천 3개 — `nextRecommendedPhase` recommended / 현재 phase 재실행 / 라이프사이클 다음 단계 — + 직접 입력; 직접 입력은 후속 `text` 단계에서 전체 task-type 화이트리스트로 검증),
|
|
124
|
-
- brief path
|
|
124
|
+
- brief path — **entry task-type(requirements-discovery / error-analysis / improvement-discovery)에서만 질문** (same-group `.okstra/briefs/<task-group>/**/*.md` candidates first, direct input last; `유지 / 변경` for existing entry tasks). downstream task-type 은 manifest 의 brief 를 자동 carry-in 하고, 등록 brief 가 없으면 `brief_carry` 3-옵션(entry 전환 추천 / 직접 입력 / 중단)이 뜬다. `release-handoff` 는 brief 단계가 아예 없다 — prepare 가 검증 보고서 인용 input 문서를 생성한다,
|
|
125
125
|
- base-ref pick + git rev-parse validation (skipped when reusing an active worktree),
|
|
126
126
|
- `implementation`-only sub-flow: approved-plan path (frontmatter `approved: true` check) + stage pick (`auto` = 의존성 충족된 가장 빠른 미완료 stage, 또는 특정 stage 번호) + executor pick,
|
|
127
|
+
- `release-handoff`-only sub-flow: approved plan 자동 해소 후 `handoff_stage_pick` 멀티선택 — eligible stage 묶음(stage-group) 또는 전체 task(accepted whole-task 검증 보고서 존재 시) 선택; 결과는 render-args 의 `stages` 키(csv, whole-task 면 빈 값)로 나간다,
|
|
127
128
|
- `Use defaults / Customize` branch with profile-aware worker/model questions,
|
|
128
129
|
- `release-handoff` PR template override + persist scope,
|
|
129
130
|
- final `Proceed / Edit` confirmation; on `Edit` the wizard asks which step to rewind to and clears every later answer.
|
|
@@ -166,6 +167,7 @@ okstra render-bundle \
|
|
|
166
167
|
--critic "<args.critic>" \
|
|
167
168
|
--approved-plan "<args.approved-plan>" \
|
|
168
169
|
--stage "<args.stage>" \
|
|
170
|
+
--stages "<args.stages>" \
|
|
169
171
|
--base-ref "<args.base-ref>" \
|
|
170
172
|
--workers "<args.workers>" \
|
|
171
173
|
--directive "<args.directive>" \
|
|
@@ -25,11 +25,15 @@ taskType: "{{FM_TASK_TYPE}}"
|
|
|
25
25
|
|
|
26
26
|
## Source Verification Report
|
|
27
27
|
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
28
|
+
- Mode: `{{HANDOFF_MODE}}`
|
|
29
|
+
- Stages: `{{HANDOFF_STAGES}}`
|
|
30
|
+
- Reports (one row per cited `final-verification` final-report; every Verdict Token MUST be `accepted`):
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
| Stage | Report path (project-relative) | Verdict Token |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
{{HANDOFF_SOURCE_REPORTS}}
|
|
35
|
+
|
|
36
|
+
> This section is generated by `okstra render-bundle` from the consumers ledger (`verified` rows) / the latest accepted whole-task report — the lead re-confirms each cited report's `Verdict Token` and MUST end the run immediately (route back to `final-verification`) if any token is not `accepted`. Release-handoff never operates on `conditional-accept` or `blocked` outcomes.
|
|
33
37
|
|
|
34
38
|
## Working-Tree Snapshot (filled at run start)
|
|
35
39
|
|
|
@@ -83,5 +87,5 @@ taskType: "{{FM_TASK_TYPE}}"
|
|
|
83
87
|
|
|
84
88
|
## Conversion Note
|
|
85
89
|
|
|
86
|
-
- This
|
|
87
|
-
-
|
|
90
|
+
- This document is generated by `okstra render-bundle --task-type release-handoff` and serves as the run's input in place of a task brief (briefs belong to entry phases only).
|
|
91
|
+
- It reuses the same `Task Group` and `Task ID` as the originating implementation / final-verification runs so the handoff stays attached to the same task history.
|
package/src/render-bundle.mjs
CHANGED
|
@@ -18,7 +18,15 @@ Usage:
|
|
|
18
18
|
[--gemini-model <m>] [--report-writer-model <m>] \\
|
|
19
19
|
[--related-tasks <list>] [--base-ref <ref>] \\
|
|
20
20
|
[--clarification-response <path>] [--work-category <cat>] \\
|
|
21
|
-
[--pr-template-path <path>]
|
|
21
|
+
[--stage <auto|N>] [--stages <csv>] [--pr-template-path <path>]
|
|
22
|
+
|
|
23
|
+
--stage implementation / final-verification only
|
|
24
|
+
--stages release-handoff only: PR stage bundle csv (empty = whole-task)
|
|
25
|
+
--pr-template-path release-handoff only
|
|
26
|
+
|
|
27
|
+
release-handoff takes NO --task-brief (briefs belong to entry phases) —
|
|
28
|
+
prepare generates the run's input document from the cited verification
|
|
29
|
+
reports instead.
|
|
22
30
|
|
|
23
31
|
All flags pass through unchanged to \`python3 -m okstra_ctl.run\`. The
|
|
24
32
|
shim auto-supplies \`--workspace-root\` (from \`okstra paths --field workspace\`)
|