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
|
@@ -291,6 +291,10 @@ class PrepareInputs:
|
|
|
291
291
|
# prepare-time 에 task-level conformance 매니페스트 entry.waiver 를 채운다.
|
|
292
292
|
qa_waiver: str = ""
|
|
293
293
|
stage: str = "auto"
|
|
294
|
+
# release-handoff 전용: PR 로 내보낼 stage 묶음 (csv, 예: "2,3"). 빈 값 =
|
|
295
|
+
# whole-task 모드. `--stage`(impl/fv 의 Stage Map 실행/검증 선택)와는
|
|
296
|
+
# 별개 채널이다.
|
|
297
|
+
stages: str = ""
|
|
294
298
|
clarification_response_path: str = "" # absolute or empty
|
|
295
299
|
# release-handoff 전용: PR 본문 템플릿 1회성 override. 빈 문자열이면
|
|
296
300
|
# project.json → global config → 스킬 디폴트 순으로 해석된다.
|
|
@@ -1064,7 +1068,17 @@ def _validate_prepare_inputs(project_root: Path, inp: PrepareInputs) -> list:
|
|
|
1064
1068
|
를 검증하고, implementation 일 때 stage map 을 파싱해 돌려준다 (그 외엔 빈 리스트)."""
|
|
1065
1069
|
if not project_root.is_dir():
|
|
1066
1070
|
raise PrepareError(f"project root not found: {project_root}")
|
|
1067
|
-
if
|
|
1071
|
+
if inp.stages and inp.task_type != "release-handoff":
|
|
1072
|
+
raise PrepareError(
|
|
1073
|
+
"--stages is only meaningful with --task-type release-handoff; "
|
|
1074
|
+
f"got {inp.task_type}"
|
|
1075
|
+
)
|
|
1076
|
+
if inp.task_type == "release-handoff":
|
|
1077
|
+
# brief 는 entry phase 의 입력물 — release-handoff 는 검증 보고서를
|
|
1078
|
+
# 인용하는 input 문서를 prepare 가 직접 생성해 brief 자리에 채운다
|
|
1079
|
+
# (_materialize_release_handoff_input, 이 검증 직후 실행).
|
|
1080
|
+
pass
|
|
1081
|
+
elif not inp.brief_path.is_file():
|
|
1068
1082
|
raise PrepareError(f"task brief not found: {inp.brief_path}")
|
|
1069
1083
|
ctx_stage_map: list = []
|
|
1070
1084
|
# implementation 과 final-verification 은 둘 다 승인된 plan 의 Stage Map 을
|
|
@@ -1121,7 +1135,9 @@ def _validate_prepare_inputs(project_root: Path, inp: PrepareInputs) -> list:
|
|
|
1121
1135
|
"--implementation-option is only meaningful with --task-type "
|
|
1122
1136
|
"implementation and --approved-plan <path>"
|
|
1123
1137
|
)
|
|
1124
|
-
|
|
1138
|
+
# wizard 는 모든 flag 를 빈 값 포함 항상 전달하므로(`--stage ""`),
|
|
1139
|
+
# 빈 문자열은 "stage 미지정" 으로 받아들인다.
|
|
1140
|
+
if inp.stage not in ("", "auto"):
|
|
1125
1141
|
raise PrepareError(
|
|
1126
1142
|
"--stage is only meaningful with --task-type implementation or "
|
|
1127
1143
|
f"final-verification; got {inp.task_type}"
|
|
@@ -1133,6 +1149,113 @@ def _validate_prepare_inputs(project_root: Path, inp: PrepareInputs) -> list:
|
|
|
1133
1149
|
return ctx_stage_map
|
|
1134
1150
|
|
|
1135
1151
|
|
|
1152
|
+
def _collect_handoff_source_report_rows(
|
|
1153
|
+
rows: list, nums: list,
|
|
1154
|
+
) -> list:
|
|
1155
|
+
"""선택 stage 들의 마지막 verified 행에서 인용 표 행을 만든다 (last-wins)."""
|
|
1156
|
+
last_verified: dict = {}
|
|
1157
|
+
for r in rows:
|
|
1158
|
+
if r.get("status") == "verified" and isinstance(r.get("stage"), int):
|
|
1159
|
+
last_verified[r["stage"]] = r
|
|
1160
|
+
return [
|
|
1161
|
+
f"| {n} | {last_verified[n].get('report_path', '')} "
|
|
1162
|
+
f"| `{last_verified[n].get('verdict', '')}` |"
|
|
1163
|
+
for n in nums
|
|
1164
|
+
]
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
def _materialize_release_handoff_input(
|
|
1168
|
+
workspace_root: Path, project_root: Path, inp: "PrepareInputs",
|
|
1169
|
+
) -> dict:
|
|
1170
|
+
"""release-handoff 의 입력 문서를 검증 보고서 인용으로 자동 생성한다.
|
|
1171
|
+
|
|
1172
|
+
brief 는 entry phase 의 입력물이라 release-handoff 에는 없다 — 이 run 의
|
|
1173
|
+
`--task-brief` 자리는 여기서 생성된 input 문서가 채운다. stage 자격은
|
|
1174
|
+
okstra_ctl.handoff 의 SSOT 판정을 prepare 에서도 그대로 강제한다.
|
|
1175
|
+
반환: ctx 에 올릴 {"HANDOFF_MODE": ..., "HANDOFF_STAGES": ...}."""
|
|
1176
|
+
from .consumers import read_consumers
|
|
1177
|
+
from .handoff import (HandoffError, _require_eligible,
|
|
1178
|
+
latest_whole_task_fv_accepted)
|
|
1179
|
+
from .paths import task_dir
|
|
1180
|
+
from .render import render_template_with_ctx
|
|
1181
|
+
from .run_context import _now_task_date
|
|
1182
|
+
|
|
1183
|
+
# Path("") 는 "." 로 정규화된다 — 빈 brief 센티널은 둘 다로 들어올 수 있다.
|
|
1184
|
+
if str(inp.brief_path).strip() not in ("", "."):
|
|
1185
|
+
raise PrepareError(
|
|
1186
|
+
"--task-brief is not accepted for --task-type release-handoff — "
|
|
1187
|
+
"the input document is generated from the cited final-verification "
|
|
1188
|
+
"reports (pick stages via --stages, or leave it empty for "
|
|
1189
|
+
"whole-task)"
|
|
1190
|
+
)
|
|
1191
|
+
if not inp.approved_plan_path:
|
|
1192
|
+
raise PrepareError(
|
|
1193
|
+
"release-handoff requires --approved-plan "
|
|
1194
|
+
"<implementation-planning final-report> — the Stage Map and the "
|
|
1195
|
+
"consumers ledger drive stage eligibility"
|
|
1196
|
+
)
|
|
1197
|
+
plan = Path(inp.approved_plan_path)
|
|
1198
|
+
if not plan.is_file():
|
|
1199
|
+
raise PrepareError(f"approved plan not found: {plan}")
|
|
1200
|
+
stage_map = _parse_stage_map_into_ctx(str(plan))
|
|
1201
|
+
if not stage_map:
|
|
1202
|
+
raise PrepareError(f"approved plan has no parsable Stage Map: {plan}")
|
|
1203
|
+
rows = read_consumers(plan.resolve().parents[1])
|
|
1204
|
+
|
|
1205
|
+
if inp.stages:
|
|
1206
|
+
try:
|
|
1207
|
+
nums = sorted({int(x) for x in inp.stages.split(",") if x.strip()})
|
|
1208
|
+
except ValueError:
|
|
1209
|
+
raise PrepareError(
|
|
1210
|
+
f"--stages must be a comma-separated int list, got {inp.stages!r}"
|
|
1211
|
+
)
|
|
1212
|
+
if not nums:
|
|
1213
|
+
raise PrepareError("--stages must select at least one stage")
|
|
1214
|
+
try:
|
|
1215
|
+
_require_eligible(stage_map, rows, nums)
|
|
1216
|
+
except HandoffError as exc:
|
|
1217
|
+
raise PrepareError(str(exc)) from exc
|
|
1218
|
+
mode = "stage-group"
|
|
1219
|
+
stages_csv = ",".join(str(n) for n in nums)
|
|
1220
|
+
report_rows = _collect_handoff_source_report_rows(rows, nums)
|
|
1221
|
+
else:
|
|
1222
|
+
report = latest_whole_task_fv_accepted(
|
|
1223
|
+
project_root, inp.project_id, inp.task_group, inp.task_id)
|
|
1224
|
+
if not report:
|
|
1225
|
+
raise PrepareError(
|
|
1226
|
+
"whole-task release-handoff requires an accepted whole-task "
|
|
1227
|
+
"final-verification report — run final-verification first, "
|
|
1228
|
+
"or pass --stages <csv> for a stage-group PR"
|
|
1229
|
+
)
|
|
1230
|
+
mode = "whole-task"
|
|
1231
|
+
stages_csv = ""
|
|
1232
|
+
report_relative = relative_to_project_root(Path(report), project_root)
|
|
1233
|
+
report_rows = [f"| (all) | {report_relative or report} | `accepted` |"]
|
|
1234
|
+
|
|
1235
|
+
template = (workspace_root / "templates" / "reports"
|
|
1236
|
+
/ "release-handoff-input.template.md")
|
|
1237
|
+
if not template.is_file():
|
|
1238
|
+
raise PrepareError(
|
|
1239
|
+
f"release-handoff input template missing: {template}.{_INSTALL_HINT}"
|
|
1240
|
+
)
|
|
1241
|
+
out = (task_dir(project_root, inp.task_group, inp.task_id)
|
|
1242
|
+
/ "release-handoff-input.md")
|
|
1243
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
1244
|
+
render_template_with_ctx(str(template), str(out), {
|
|
1245
|
+
"TASK_KEY": f"{inp.project_id}:{inp.task_group}:{inp.task_id}",
|
|
1246
|
+
"TASK_ID": inp.task_id,
|
|
1247
|
+
"TASK_GROUP": inp.task_group,
|
|
1248
|
+
"PROJECT_ID": inp.project_id,
|
|
1249
|
+
"TASK_TYPE": inp.task_type,
|
|
1250
|
+
"TASK_DATE": _now_task_date(),
|
|
1251
|
+
"HANDOFF_MODE": mode,
|
|
1252
|
+
"HANDOFF_STAGES": stages_csv or "(all)",
|
|
1253
|
+
"HANDOFF_SOURCE_REPORTS": "\n".join(report_rows),
|
|
1254
|
+
})
|
|
1255
|
+
inp.brief_path = out
|
|
1256
|
+
return {"HANDOFF_MODE": mode, "HANDOFF_STAGES": stages_csv}
|
|
1257
|
+
|
|
1258
|
+
|
|
1136
1259
|
def _apply_qa_waiver_if_requested(inp: "PrepareInputs", project_root: Path) -> None:
|
|
1137
1260
|
"""`--qa-waiver` 가 있으면 task-level 매니페스트 entry 의 waiver 를 채운다."""
|
|
1138
1261
|
if not inp.qa_waiver:
|
|
@@ -1822,6 +1945,13 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
1822
1945
|
final_report_template = assets.final_report_template
|
|
1823
1946
|
ctx_stage_map = _validate_prepare_inputs(project_root, inp)
|
|
1824
1947
|
|
|
1948
|
+
# release-handoff: 검증 보고서 인용 input 문서를 생성해 brief 자리에 채운다.
|
|
1949
|
+
# 이후의 모든 brief 소비 경로(material/instruction-set 복사)는 그대로 동작한다.
|
|
1950
|
+
handoff_tokens = {"HANDOFF_MODE": "", "HANDOFF_STAGES": ""}
|
|
1951
|
+
if inp.task_type == "release-handoff":
|
|
1952
|
+
handoff_tokens = _materialize_release_handoff_input(
|
|
1953
|
+
workspace_root, project_root, inp)
|
|
1954
|
+
|
|
1825
1955
|
verify_installation(workspace_root)
|
|
1826
1956
|
_register_and_check_project(project_root, inp)
|
|
1827
1957
|
|
|
@@ -1868,7 +1998,13 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
1868
1998
|
):
|
|
1869
1999
|
if inp.task_type == "final-verification" and inp.stage and inp.stage != "auto":
|
|
1870
2000
|
worktree = _single_stage_final_verification_worktree(inp)
|
|
2001
|
+
# Single-stage final-verification namespaces its run path under
|
|
2002
|
+
# runs/final-verification/stage-<N> (same isolation as
|
|
2003
|
+
# implementation) so concurrent per-stage verifications never
|
|
2004
|
+
# share state/reports/worker-results.
|
|
2005
|
+
fv_stage_arg = int(inp.stage)
|
|
1871
2006
|
else:
|
|
2007
|
+
fv_stage_arg = None
|
|
1872
2008
|
try:
|
|
1873
2009
|
worktree = provision_task_worktree(
|
|
1874
2010
|
task_type=inp.task_type,
|
|
@@ -1890,8 +2026,9 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
1890
2026
|
# lands in runs/implementation/stage-<N>. The registry stage-key is
|
|
1891
2027
|
# reserved exactly once here (inside provision_stage_worktree), and
|
|
1892
2028
|
# the surrounding mutex makes the registry read in stage selection
|
|
1893
|
-
# and that reserve atomic.
|
|
1894
|
-
#
|
|
2029
|
+
# and that reserve atomic. Other task-types skip this selection;
|
|
2030
|
+
# single-stage final-verification threads its explicit stage via
|
|
2031
|
+
# fv_stage_arg, everything else keeps stage_arg=None (flat paths).
|
|
1895
2032
|
if inp.task_type == "implementation":
|
|
1896
2033
|
impl_stage_selection = _select_and_provision_implementation_stage(
|
|
1897
2034
|
inp, ctx_stage_map, task_group_segment, task_id_segment,
|
|
@@ -1904,7 +2041,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
1904
2041
|
_clear_stale_stage_waiver(inp, project_root, impl_stage_selection.stage)
|
|
1905
2042
|
else:
|
|
1906
2043
|
impl_stage_selection = None
|
|
1907
|
-
stage_arg =
|
|
2044
|
+
stage_arg = fv_stage_arg
|
|
1908
2045
|
|
|
1909
2046
|
ctx = compute_and_write_run_context(
|
|
1910
2047
|
workspace_root=workspace_root, project_root=project_root,
|
|
@@ -1982,6 +2119,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
1982
2119
|
"RECOMMENDED_ANALYSERS": selected_reviewers,
|
|
1983
2120
|
"PR_TEMPLATE_PATH": pr_template_path_str,
|
|
1984
2121
|
"PR_TEMPLATE_SOURCE": pr_template_source,
|
|
2122
|
+
**handoff_tokens,
|
|
1985
2123
|
"CLAUDE_SESSION_ID": claude_session_id,
|
|
1986
2124
|
"CLARIFICATION_RESPONSE_PATH": inp.clarification_response_path,
|
|
1987
2125
|
"CLARIFICATION_RESPONSE_RELATIVE_PATH": clarification_relative,
|
|
@@ -2106,7 +2244,14 @@ def main(argv: list[str]) -> int:
|
|
|
2106
2244
|
p.add_argument("--task-group", required=True)
|
|
2107
2245
|
p.add_argument("--task-id", required=True)
|
|
2108
2246
|
p.add_argument("--task-type", required=True)
|
|
2109
|
-
p.add_argument(
|
|
2247
|
+
p.add_argument(
|
|
2248
|
+
"--task-brief", default="", dest="task_brief",
|
|
2249
|
+
help=(
|
|
2250
|
+
"Required for every task-type except release-handoff (whose input "
|
|
2251
|
+
"document is generated by prepare from the cited "
|
|
2252
|
+
"final-verification reports)."
|
|
2253
|
+
),
|
|
2254
|
+
)
|
|
2110
2255
|
p.add_argument("--directive", default="")
|
|
2111
2256
|
p.add_argument("--workers", default="", dest="workers_override")
|
|
2112
2257
|
p.add_argument("--lead-model", default="")
|
|
@@ -2128,6 +2273,15 @@ def main(argv: list[str]) -> int:
|
|
|
2128
2273
|
"consumers.jsonl status:done. Numeric '<N>' = force that stage."
|
|
2129
2274
|
),
|
|
2130
2275
|
)
|
|
2276
|
+
p.add_argument(
|
|
2277
|
+
"--stages", default="", dest="stages",
|
|
2278
|
+
help=(
|
|
2279
|
+
"release-handoff only. Comma-separated stage numbers to bundle "
|
|
2280
|
+
"into one PR (stage-group mode); empty = whole-task mode. "
|
|
2281
|
+
"Distinct from --stage (implementation/final-verification "
|
|
2282
|
+
"Stage Map selection)."
|
|
2283
|
+
),
|
|
2284
|
+
)
|
|
2131
2285
|
p.add_argument(
|
|
2132
2286
|
"--approve",
|
|
2133
2287
|
action="store_true",
|
|
@@ -2199,10 +2353,22 @@ def main(argv: list[str]) -> int:
|
|
|
2199
2353
|
args = p.parse_args(argv)
|
|
2200
2354
|
|
|
2201
2355
|
project_root = Path(args.project_root).expanduser().resolve()
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2356
|
+
if args.task_type == "release-handoff":
|
|
2357
|
+
if args.task_brief:
|
|
2358
|
+
print(
|
|
2359
|
+
"--task-brief is not accepted for --task-type release-handoff "
|
|
2360
|
+
"— the input document is generated from the cited "
|
|
2361
|
+
"final-verification reports",
|
|
2362
|
+
file=__import__("sys").stderr,
|
|
2363
|
+
)
|
|
2364
|
+
return 1
|
|
2365
|
+
brief_abs = Path("") # prepare 가 input 문서를 생성해 채운다
|
|
2366
|
+
else:
|
|
2367
|
+
brief_abs = resolve_user_file(args.task_brief, project_root)
|
|
2368
|
+
if brief_abs is None:
|
|
2369
|
+
print(f"task brief not found: {args.task_brief}",
|
|
2370
|
+
file=__import__("sys").stderr)
|
|
2371
|
+
return 1
|
|
2206
2372
|
clarification_abs = ""
|
|
2207
2373
|
if args.clarification_response_path:
|
|
2208
2374
|
cr = resolve_user_file(args.clarification_response_path, project_root)
|
|
@@ -2237,6 +2403,7 @@ def main(argv: list[str]) -> int:
|
|
|
2237
2403
|
approved_plan_path=args.approved_plan_path,
|
|
2238
2404
|
qa_waiver=args.qa_waiver,
|
|
2239
2405
|
stage=args.stage,
|
|
2406
|
+
stages=args.stages,
|
|
2240
2407
|
clarification_response_path=clarification_abs,
|
|
2241
2408
|
pr_template_path=args.pr_template_path,
|
|
2242
2409
|
render_only=args.render_only,
|