okstra 0.25.1 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.kr.md +1 -0
- package/README.md +1 -0
- package/docs/kr/architecture.md +1 -1
- package/docs/kr/cli.md +10 -1
- package/docs/superpowers/specs/2026-05-15-implementation-plan-verification-design.md +254 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +30 -2
- package/runtime/bin/okstra.sh +1 -0
- package/runtime/prompts/profiles/_common-contract.md +5 -0
- package/runtime/prompts/profiles/implementation-planning.md +3 -0
- package/runtime/python/lib/okstra/cli.sh +8 -1
- package/runtime/python/lib/okstra/globals.sh +3 -0
- package/runtime/python/lib/okstra/usage.sh +8 -1
- package/runtime/python/okstra_ctl/render.py +32 -0
- package/runtime/python/okstra_ctl/run.py +27 -0
- package/runtime/python/okstra_ctl/wizard.py +234 -8
- package/runtime/skills/okstra-convergence/SKILL.md +203 -0
- package/runtime/skills/okstra-run/SKILL.md +27 -14
- package/runtime/templates/reports/final-report.template.md +34 -0
- package/runtime/validators/validate-run.py +71 -0
- package/src/wizard.mjs +21 -5
|
@@ -84,6 +84,7 @@ S_BRIEF_KEEP = "brief_keep"
|
|
|
84
84
|
S_BRIEF_PATH = "brief_path"
|
|
85
85
|
S_BASE_REF_PICK = "base_ref_pick"
|
|
86
86
|
S_BASE_REF_TEXT = "base_ref_text"
|
|
87
|
+
S_APPROVED_PLAN_PICK = "approved_plan_pick"
|
|
87
88
|
S_APPROVED_PLAN = "approved_plan"
|
|
88
89
|
S_EXECUTOR = "executor"
|
|
89
90
|
S_DEFAULTS_OR_CUSTOM = "defaults_or_custom"
|
|
@@ -94,9 +95,13 @@ S_CLAUDE_MODEL = "claude_model"
|
|
|
94
95
|
S_CODEX_MODEL = "codex_model"
|
|
95
96
|
S_GEMINI_MODEL = "gemini_model"
|
|
96
97
|
S_REPORT_WRITER_MODEL = "report_writer_model"
|
|
98
|
+
S_DIRECTIVE_PICK = "directive_pick"
|
|
97
99
|
S_DIRECTIVE = "directive"
|
|
100
|
+
S_RELATED_TASKS_PICK = "related_tasks_pick"
|
|
98
101
|
S_RELATED_TASKS = "related_tasks"
|
|
102
|
+
S_CLARIFICATION_PICK = "clarification_pick"
|
|
99
103
|
S_CLARIFICATION = "clarification"
|
|
104
|
+
S_PR_TEMPLATE_PICK = "pr_template_pick"
|
|
100
105
|
S_PR_TEMPLATE = "pr_template"
|
|
101
106
|
S_PR_TEMPLATE_SCOPE = "pr_template_scope"
|
|
102
107
|
S_CONFIRM = "confirm"
|
|
@@ -134,6 +139,7 @@ class WizardState:
|
|
|
134
139
|
|
|
135
140
|
# impl extras
|
|
136
141
|
approved_plan_path: str = ""
|
|
142
|
+
approved_plan_pending_text: bool = False
|
|
137
143
|
executor: str = ""
|
|
138
144
|
|
|
139
145
|
# customize
|
|
@@ -145,9 +151,13 @@ class WizardState:
|
|
|
145
151
|
gemini_model: str = ""
|
|
146
152
|
report_writer_model: str = ""
|
|
147
153
|
directive: str = ""
|
|
154
|
+
directive_pending_text: bool = False
|
|
148
155
|
related_tasks_raw: str = ""
|
|
156
|
+
related_tasks_pending_text: bool = False
|
|
149
157
|
clarification_response_path: str = ""
|
|
158
|
+
clarification_pending_text: bool = False
|
|
150
159
|
pr_template_path: str = ""
|
|
160
|
+
pr_template_pending_text: bool = False
|
|
151
161
|
pr_template_scope: str = "" # "once" | "project" | "global"
|
|
152
162
|
|
|
153
163
|
# confirm / edit
|
|
@@ -533,6 +543,81 @@ def _submit_base_ref_text(state: WizardState, value: str) -> Optional[str]:
|
|
|
533
543
|
return f"base-ref: {ref}"
|
|
534
544
|
|
|
535
545
|
|
|
546
|
+
PICK_USE_DEFAULT = "__use_default__"
|
|
547
|
+
PICK_OTHER = "__other__"
|
|
548
|
+
PICK_SKIP = "__skip__"
|
|
549
|
+
PICK_ENTER = "__enter__"
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _latest_implementation_planning_report(state: WizardState) -> Optional[Path]:
|
|
553
|
+
"""Find the latest ``final-report-implementation-planning-<seq>.md`` under
|
|
554
|
+
the current task's runs directory.
|
|
555
|
+
|
|
556
|
+
Returns the path relative to ``project_root`` if found, otherwise ``None``.
|
|
557
|
+
"""
|
|
558
|
+
if not state.task_group or not state.task_id or not state.project_root:
|
|
559
|
+
return None
|
|
560
|
+
base = (Path(state.project_root) / ".project-docs" / "okstra" / "tasks"
|
|
561
|
+
/ slugify_task_segment(state.task_group)
|
|
562
|
+
/ slugify_task_segment(state.task_id)
|
|
563
|
+
/ "runs" / "implementation-planning")
|
|
564
|
+
if not base.is_dir():
|
|
565
|
+
return None
|
|
566
|
+
pat = re.compile(r"^final-report-implementation-planning-(\d+)\.md$")
|
|
567
|
+
best: tuple[int, Path] | None = None
|
|
568
|
+
for run_dir in base.iterdir():
|
|
569
|
+
reports = run_dir / "reports"
|
|
570
|
+
if not reports.is_dir():
|
|
571
|
+
continue
|
|
572
|
+
for child in reports.iterdir():
|
|
573
|
+
m = pat.match(child.name)
|
|
574
|
+
if not m:
|
|
575
|
+
continue
|
|
576
|
+
n = int(m.group(1))
|
|
577
|
+
if best is None or n > best[0]:
|
|
578
|
+
best = (n, child)
|
|
579
|
+
if best is None:
|
|
580
|
+
return None
|
|
581
|
+
try:
|
|
582
|
+
return best[1].relative_to(Path(state.project_root))
|
|
583
|
+
except ValueError:
|
|
584
|
+
return best[1]
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _build_approved_plan_pick(state: WizardState) -> Prompt:
|
|
588
|
+
default = _latest_implementation_planning_report(state)
|
|
589
|
+
options = [
|
|
590
|
+
_opt(PICK_USE_DEFAULT, f"기본 경로 사용: {default}"),
|
|
591
|
+
_opt(PICK_OTHER, "다른 경로 입력"),
|
|
592
|
+
]
|
|
593
|
+
return Prompt(
|
|
594
|
+
step=S_APPROVED_PLAN_PICK, kind="pick",
|
|
595
|
+
label=f"approved final-report 경로 (기본: {default})",
|
|
596
|
+
options=options,
|
|
597
|
+
echo_template="approved-plan(pick): {value}",
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _submit_approved_plan_pick(state: WizardState, value: str) -> Optional[str]:
|
|
602
|
+
if value == PICK_USE_DEFAULT:
|
|
603
|
+
default = _latest_implementation_planning_report(state)
|
|
604
|
+
if default is None:
|
|
605
|
+
raise WizardError(
|
|
606
|
+
"기본 approved-plan 경로를 찾을 수 없습니다. '다른 경로 입력'을 선택하세요."
|
|
607
|
+
)
|
|
608
|
+
p = _validate_approved_plan(str(default), Path(state.project_root))
|
|
609
|
+
state.approved_plan_path = str(p)
|
|
610
|
+
state.approved_plan_pending_text = False
|
|
611
|
+
return f"approved-plan: {p}"
|
|
612
|
+
if value == PICK_OTHER:
|
|
613
|
+
state.approved_plan_pending_text = True
|
|
614
|
+
state.approved_plan_path = ""
|
|
615
|
+
return None
|
|
616
|
+
raise WizardError(
|
|
617
|
+
f"expected '{PICK_USE_DEFAULT}' or '{PICK_OTHER}', got: {value!r}"
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
|
|
536
621
|
def _build_approved_plan(state: WizardState) -> Prompt:
|
|
537
622
|
return Prompt(
|
|
538
623
|
step=S_APPROVED_PLAN, kind="text",
|
|
@@ -544,9 +629,103 @@ def _build_approved_plan(state: WizardState) -> Prompt:
|
|
|
544
629
|
def _submit_approved_plan(state: WizardState, value: str) -> Optional[str]:
|
|
545
630
|
p = _validate_approved_plan(value, Path(state.project_root))
|
|
546
631
|
state.approved_plan_path = str(p)
|
|
632
|
+
state.approved_plan_pending_text = False
|
|
547
633
|
return f"approved-plan: {p}"
|
|
548
634
|
|
|
549
635
|
|
|
636
|
+
def _build_directive_pick(state: WizardState) -> Prompt:
|
|
637
|
+
return Prompt(
|
|
638
|
+
step=S_DIRECTIVE_PICK, kind="pick",
|
|
639
|
+
label="추가 directive 가 있나요?",
|
|
640
|
+
options=[
|
|
641
|
+
_opt(PICK_SKIP, "없음 (건너뛰기)"),
|
|
642
|
+
_opt(PICK_ENTER, "있음 (입력)"),
|
|
643
|
+
],
|
|
644
|
+
echo_template="directive(pick): {value}",
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _submit_directive_pick(state: WizardState, value: str) -> Optional[str]:
|
|
649
|
+
if value == PICK_SKIP:
|
|
650
|
+
state.directive = ""
|
|
651
|
+
state.directive_pending_text = False
|
|
652
|
+
return "directive: (none)"
|
|
653
|
+
if value == PICK_ENTER:
|
|
654
|
+
state.directive_pending_text = True
|
|
655
|
+
return None
|
|
656
|
+
raise WizardError(f"expected '{PICK_SKIP}' or '{PICK_ENTER}', got: {value!r}")
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _build_related_tasks_pick(state: WizardState) -> Prompt:
|
|
660
|
+
return Prompt(
|
|
661
|
+
step=S_RELATED_TASKS_PICK, kind="pick",
|
|
662
|
+
label="관련 task id 목록이 있나요?",
|
|
663
|
+
options=[
|
|
664
|
+
_opt(PICK_SKIP, "없음 (건너뛰기)"),
|
|
665
|
+
_opt(PICK_ENTER, "있음 (입력)"),
|
|
666
|
+
],
|
|
667
|
+
echo_template="related-tasks(pick): {value}",
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _submit_related_tasks_pick(state: WizardState, value: str) -> Optional[str]:
|
|
672
|
+
if value == PICK_SKIP:
|
|
673
|
+
state.related_tasks_raw = ""
|
|
674
|
+
state.related_tasks_pending_text = False
|
|
675
|
+
return "related-tasks: (none)"
|
|
676
|
+
if value == PICK_ENTER:
|
|
677
|
+
state.related_tasks_pending_text = True
|
|
678
|
+
return None
|
|
679
|
+
raise WizardError(f"expected '{PICK_SKIP}' or '{PICK_ENTER}', got: {value!r}")
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _build_clarification_pick(state: WizardState) -> Prompt:
|
|
683
|
+
return Prompt(
|
|
684
|
+
step=S_CLARIFICATION_PICK, kind="pick",
|
|
685
|
+
label="clarification-response 파일 경로가 있나요? (follow-up 시에만)",
|
|
686
|
+
options=[
|
|
687
|
+
_opt(PICK_SKIP, "없음 (건너뛰기)"),
|
|
688
|
+
_opt(PICK_ENTER, "있음 (입력)"),
|
|
689
|
+
],
|
|
690
|
+
echo_template="clarification(pick): {value}",
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _submit_clarification_pick(state: WizardState, value: str) -> Optional[str]:
|
|
695
|
+
if value == PICK_SKIP:
|
|
696
|
+
state.clarification_response_path = ""
|
|
697
|
+
state.clarification_pending_text = False
|
|
698
|
+
return "clarification: (none)"
|
|
699
|
+
if value == PICK_ENTER:
|
|
700
|
+
state.clarification_pending_text = True
|
|
701
|
+
return None
|
|
702
|
+
raise WizardError(f"expected '{PICK_SKIP}' or '{PICK_ENTER}', got: {value!r}")
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _build_pr_template_pick(state: WizardState) -> Prompt:
|
|
706
|
+
return Prompt(
|
|
707
|
+
step=S_PR_TEMPLATE_PICK, kind="pick",
|
|
708
|
+
label="PR 본문 템플릿 경로를 직접 지정할까요?",
|
|
709
|
+
options=[
|
|
710
|
+
_opt(PICK_SKIP, "자동 해석 (project.json → config → 기본)"),
|
|
711
|
+
_opt(PICK_ENTER, "직접 경로 입력 (1회성 override)"),
|
|
712
|
+
],
|
|
713
|
+
echo_template="pr-template(pick): {value}",
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _submit_pr_template_pick(state: WizardState, value: str) -> Optional[str]:
|
|
718
|
+
if value == PICK_SKIP:
|
|
719
|
+
state.pr_template_path = ""
|
|
720
|
+
state.pr_template_scope = ""
|
|
721
|
+
state.pr_template_pending_text = False
|
|
722
|
+
return "pr-template: (auto-resolve)"
|
|
723
|
+
if value == PICK_ENTER:
|
|
724
|
+
state.pr_template_pending_text = True
|
|
725
|
+
return None
|
|
726
|
+
raise WizardError(f"expected '{PICK_SKIP}' or '{PICK_ENTER}', got: {value!r}")
|
|
727
|
+
|
|
728
|
+
|
|
550
729
|
def _build_executor(state: WizardState) -> Prompt:
|
|
551
730
|
options = [_opt(e, e + (" (default)" if e == "claude" else ""))
|
|
552
731
|
for e in EXECUTORS]
|
|
@@ -689,6 +868,7 @@ def _build_directive(state: WizardState) -> Prompt:
|
|
|
689
868
|
|
|
690
869
|
def _submit_directive(state: WizardState, value: str) -> Optional[str]:
|
|
691
870
|
state.directive = (value or "").strip()
|
|
871
|
+
state.directive_pending_text = False
|
|
692
872
|
return f"directive: {state.directive or '(none)'}"
|
|
693
873
|
|
|
694
874
|
|
|
@@ -702,6 +882,7 @@ def _build_related_tasks(state: WizardState) -> Prompt:
|
|
|
702
882
|
|
|
703
883
|
def _submit_related_tasks(state: WizardState, value: str) -> Optional[str]:
|
|
704
884
|
state.related_tasks_raw = (value or "").strip()
|
|
885
|
+
state.related_tasks_pending_text = False
|
|
705
886
|
return f"related-tasks: {state.related_tasks_raw or '(none)'}"
|
|
706
887
|
|
|
707
888
|
|
|
@@ -715,6 +896,7 @@ def _build_clarification(state: WizardState) -> Prompt:
|
|
|
715
896
|
|
|
716
897
|
def _submit_clarification(state: WizardState, value: str) -> Optional[str]:
|
|
717
898
|
val = (value or "").strip()
|
|
899
|
+
state.clarification_pending_text = False
|
|
718
900
|
if not val:
|
|
719
901
|
state.clarification_response_path = ""
|
|
720
902
|
return "clarification: (none)"
|
|
@@ -734,6 +916,7 @@ def _build_pr_template(state: WizardState) -> Prompt:
|
|
|
734
916
|
|
|
735
917
|
def _submit_pr_template(state: WizardState, value: str) -> Optional[str]:
|
|
736
918
|
val = (value or "").strip()
|
|
919
|
+
state.pr_template_pending_text = False
|
|
737
920
|
if not val:
|
|
738
921
|
state.pr_template_path = ""
|
|
739
922
|
state.pr_template_scope = ""
|
|
@@ -860,15 +1043,29 @@ STEPS: list[Step] = [
|
|
|
860
1043
|
applies=lambda s: s.base_ref_pending_text,
|
|
861
1044
|
build=_build_base_ref_text, submit=_submit_base_ref_text,
|
|
862
1045
|
owns=("base_ref", "base_ref_pending_text")),
|
|
1046
|
+
Step(S_APPROVED_PLAN_PICK,
|
|
1047
|
+
applies=lambda s: (s.task_type == "implementation"
|
|
1048
|
+
and not s.approved_plan_path
|
|
1049
|
+
and not s.approved_plan_pending_text
|
|
1050
|
+
and S_APPROVED_PLAN_PICK not in s.answered
|
|
1051
|
+
and bool(s.brief_path)
|
|
1052
|
+
and (s.reuse_worktree is True
|
|
1053
|
+
or S_BASE_REF_PICK in s.answered)
|
|
1054
|
+
and not s.base_ref_pending_text
|
|
1055
|
+
and _latest_implementation_planning_report(s) is not None),
|
|
1056
|
+
build=_build_approved_plan_pick, submit=_submit_approved_plan_pick,
|
|
1057
|
+
owns=("approved_plan_path", "approved_plan_pending_text")),
|
|
863
1058
|
Step(S_APPROVED_PLAN,
|
|
864
1059
|
applies=lambda s: (s.task_type == "implementation"
|
|
865
1060
|
and not s.approved_plan_path
|
|
866
1061
|
and bool(s.brief_path)
|
|
867
1062
|
and (s.reuse_worktree is True
|
|
868
1063
|
or S_BASE_REF_PICK in s.answered)
|
|
869
|
-
and not s.base_ref_pending_text
|
|
1064
|
+
and not s.base_ref_pending_text
|
|
1065
|
+
and (s.approved_plan_pending_text
|
|
1066
|
+
or _latest_implementation_planning_report(s) is None)),
|
|
870
1067
|
build=_build_approved_plan, submit=_submit_approved_plan,
|
|
871
|
-
owns=("approved_plan_path",)),
|
|
1068
|
+
owns=("approved_plan_path", "approved_plan_pending_text")),
|
|
872
1069
|
Step(S_EXECUTOR,
|
|
873
1070
|
applies=lambda s: (s.task_type == "implementation"
|
|
874
1071
|
and bool(s.approved_plan_path)
|
|
@@ -927,27 +1124,52 @@ STEPS: list[Step] = [
|
|
|
927
1124
|
and S_REPORT_WRITER_MODEL not in s.answered),
|
|
928
1125
|
build=_build_report_writer_model, submit=_submit_report_writer_model,
|
|
929
1126
|
owns=("report_writer_model",)),
|
|
1127
|
+
Step(S_DIRECTIVE_PICK,
|
|
1128
|
+
applies=lambda s: (s.use_defaults is False
|
|
1129
|
+
and S_DIRECTIVE_PICK not in s.answered),
|
|
1130
|
+
build=_build_directive_pick, submit=_submit_directive_pick,
|
|
1131
|
+
owns=("directive", "directive_pending_text")),
|
|
930
1132
|
Step(S_DIRECTIVE,
|
|
931
1133
|
applies=lambda s: (s.use_defaults is False
|
|
1134
|
+
and s.directive_pending_text
|
|
932
1135
|
and S_DIRECTIVE not in s.answered),
|
|
933
1136
|
build=_build_directive, submit=_submit_directive,
|
|
934
|
-
owns=("directive",)),
|
|
1137
|
+
owns=("directive", "directive_pending_text")),
|
|
1138
|
+
Step(S_RELATED_TASKS_PICK,
|
|
1139
|
+
applies=lambda s: (s.use_defaults is False
|
|
1140
|
+
and S_RELATED_TASKS_PICK not in s.answered),
|
|
1141
|
+
build=_build_related_tasks_pick, submit=_submit_related_tasks_pick,
|
|
1142
|
+
owns=("related_tasks_raw", "related_tasks_pending_text")),
|
|
935
1143
|
Step(S_RELATED_TASKS,
|
|
936
1144
|
applies=lambda s: (s.use_defaults is False
|
|
1145
|
+
and s.related_tasks_pending_text
|
|
937
1146
|
and S_RELATED_TASKS not in s.answered),
|
|
938
1147
|
build=_build_related_tasks, submit=_submit_related_tasks,
|
|
939
|
-
owns=("related_tasks_raw",)),
|
|
1148
|
+
owns=("related_tasks_raw", "related_tasks_pending_text")),
|
|
1149
|
+
Step(S_CLARIFICATION_PICK,
|
|
1150
|
+
applies=lambda s: (s.use_defaults is False
|
|
1151
|
+
and S_CLARIFICATION_PICK not in s.answered),
|
|
1152
|
+
build=_build_clarification_pick, submit=_submit_clarification_pick,
|
|
1153
|
+
owns=("clarification_response_path", "clarification_pending_text")),
|
|
940
1154
|
Step(S_CLARIFICATION,
|
|
941
1155
|
applies=lambda s: (s.use_defaults is False
|
|
1156
|
+
and s.clarification_pending_text
|
|
942
1157
|
and S_CLARIFICATION not in s.answered),
|
|
943
1158
|
build=_build_clarification, submit=_submit_clarification,
|
|
944
|
-
owns=("clarification_response_path",)),
|
|
1159
|
+
owns=("clarification_response_path", "clarification_pending_text")),
|
|
1160
|
+
Step(S_PR_TEMPLATE_PICK,
|
|
1161
|
+
applies=lambda s: (s.use_defaults is False
|
|
1162
|
+
and s.task_type == "release-handoff"
|
|
1163
|
+
and S_PR_TEMPLATE_PICK not in s.answered),
|
|
1164
|
+
build=_build_pr_template_pick, submit=_submit_pr_template_pick,
|
|
1165
|
+
owns=("pr_template_path", "pr_template_scope", "pr_template_pending_text")),
|
|
945
1166
|
Step(S_PR_TEMPLATE,
|
|
946
1167
|
applies=lambda s: (s.use_defaults is False
|
|
947
1168
|
and s.task_type == "release-handoff"
|
|
1169
|
+
and s.pr_template_pending_text
|
|
948
1170
|
and S_PR_TEMPLATE not in s.answered),
|
|
949
1171
|
build=_build_pr_template, submit=_submit_pr_template,
|
|
950
|
-
owns=("pr_template_path", "pr_template_scope")),
|
|
1172
|
+
owns=("pr_template_path", "pr_template_scope", "pr_template_pending_text")),
|
|
951
1173
|
Step(S_PR_TEMPLATE_SCOPE,
|
|
952
1174
|
applies=lambda s: (s.use_defaults is False
|
|
953
1175
|
and s.task_type == "release-handoff"
|
|
@@ -1021,11 +1243,15 @@ _FIELD_DEFAULTS: dict[str, Any] = {
|
|
|
1021
1243
|
"profile_workers": [], "keep_existing_brief": None,
|
|
1022
1244
|
"brief_path": "", "reuse_worktree": None, "base_ref": "",
|
|
1023
1245
|
"base_ref_pending_text": False, "approved_plan_path": "",
|
|
1246
|
+
"approved_plan_pending_text": False,
|
|
1024
1247
|
"executor": "", "use_defaults": None, "workers_override": "",
|
|
1025
1248
|
"lead_model": "", "claude_model": "", "codex_model": "",
|
|
1026
1249
|
"gemini_model": "", "report_writer_model": "", "directive": "",
|
|
1027
|
-
"
|
|
1028
|
-
"
|
|
1250
|
+
"directive_pending_text": False,
|
|
1251
|
+
"related_tasks_raw": "", "related_tasks_pending_text": False,
|
|
1252
|
+
"clarification_response_path": "", "clarification_pending_text": False,
|
|
1253
|
+
"pr_template_path": "", "pr_template_pending_text": False,
|
|
1254
|
+
"pr_template_scope": "",
|
|
1029
1255
|
"confirmed": None, "edit_target": "",
|
|
1030
1256
|
}
|
|
1031
1257
|
|
|
@@ -407,3 +407,206 @@ Information to be passed to Phase 6 after executing this skill:
|
|
|
407
407
|
## Convergence Disabled
|
|
408
408
|
|
|
409
409
|
If `convergence.enabled: false`, this skill is skipped. Phase 6 operates using the existing consensus/divergence method.
|
|
410
|
+
|
|
411
|
+
## Plan-body verification mode (implementation-planning only)
|
|
412
|
+
|
|
413
|
+
This section defines a **second, independent** convergence round that fires only for `task-type = implementation-planning`. The round verifies the *consolidated plan* that the report-writer worker has authored, not the worker findings that were already reconciled earlier.
|
|
414
|
+
|
|
415
|
+
### Lifecycle position (BLOCKING)
|
|
416
|
+
|
|
417
|
+
Plan-body verification runs **after** finding convergence and **after** the report-writer draft is written. Sequence inside a single implementation-planning run:
|
|
418
|
+
|
|
419
|
+
```
|
|
420
|
+
Phase 4 workers produce independent analyses (Findings F-001…)
|
|
421
|
+
→ Phase 5.5 FINDING convergence (this skill, sections "Convergence Algorithm" through "Convergence State Artifact")
|
|
422
|
+
→ Phase 6 report-writer authors final-report draft (consolidated Option Candidates / Stepwise Execution Order / Dependency / Validation Checklist / Rollback)
|
|
423
|
+
→ PLAN-BODY VERIFICATION ROUND ← new — described below
|
|
424
|
+
→ User Approval gate (top-of-report `- [ ] Approved` marker is rendered only when this round's Gate result is `passed` or `passed-with-dissent`)
|
|
425
|
+
→ implementation phase (separate run)
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Plan-body verification MUST NOT replace, precede, or be conflated with the Phase 5.5 finding convergence above. They are two distinct rounds with different inputs (findings vs. consolidated plan body), different ID schemes (`F-*` vs. `P-*`), and different state files.
|
|
429
|
+
|
|
430
|
+
### MUTUAL EXCLUSION (BLOCKING)
|
|
431
|
+
|
|
432
|
+
The finding queue (Phase 5.5) and the plan-item queue (this section) are **disjoint**:
|
|
433
|
+
|
|
434
|
+
- A finding-convergence reverify prompt MUST NOT contain any `P-*` item.
|
|
435
|
+
- A plan-body verification prompt MUST NOT contain any `F-*` finding.
|
|
436
|
+
- The two rounds write to **different state files**: `runs/<task-type>/state/convergence-state.json` (findings) vs. `runs/<task-type>/state/plan-body-verification.json` (plan items).
|
|
437
|
+
- Aggregation logic (verdict counting, classification) MUST NOT carry votes from one queue into the other.
|
|
438
|
+
|
|
439
|
+
Mixing the two queues — for example, parsing a Phase 6 draft's Stepwise Execution Order step as if it were an `F-*` finding — is a contract violation. Future Claude reading this skill: if you find yourself tempted to "just reuse the finding queue for plan items, they're similar enough", stop. They are not similar enough; the verdict semantics differ (see §"Plan-body verdict semantics" below).
|
|
440
|
+
|
|
441
|
+
### Configuration
|
|
442
|
+
|
|
443
|
+
Plan-body verification is configured under `convergence.planBodyVerification` in `task-manifest.json`:
|
|
444
|
+
|
|
445
|
+
| Setting | Default | Description |
|
|
446
|
+
|---------|---------|-------------|
|
|
447
|
+
| `enabled` | `true` | If `false`, the round is skipped and the top-of-report Approval marker is rendered unconditionally (legacy behaviour). |
|
|
448
|
+
| `maxRounds` | `1` | Hard upper bound. Plan-body verification is consistency / completeness checking, not fact checking — additional rounds rarely help. Range 1–3. |
|
|
449
|
+
| `gating` | `true` | If `true` (default), `majority-disagree` blocks the Approval marker. If `false`, the round is advisory-only and the marker always renders. |
|
|
450
|
+
|
|
451
|
+
Default values are emitted into the manifest by `scripts/okstra_ctl/render.py` (`_build_convergence_block`). The ctx knob `OKSTRA_PLAN_VERIFICATION=false` flips `planBodyVerification.enabled` to false.
|
|
452
|
+
|
|
453
|
+
### Plan-item extraction (Round 0 equivalent)
|
|
454
|
+
|
|
455
|
+
From the report-writer's draft of `## 4.5 Implementation Plan Deliverables`, lead extracts plan items with the following prefixes (see also `templates/reports/final-report.template.md` §4.5.9):
|
|
456
|
+
|
|
457
|
+
| Prefix | Source sub-section | One row per |
|
|
458
|
+
|--------|--------------------|-------------|
|
|
459
|
+
| `P-Opt-<N>` | `4.5.1 Option Candidates` | one Option (its File Structure list + interfaces + blast radius) |
|
|
460
|
+
| `P-Step-<N>` | `4.5.4 Stepwise Execution Order` | one step (path + command + success signal) |
|
|
461
|
+
| `P-Dep-<N>` | `4.5.5 Dependency / Migration Risk` | one dependency row |
|
|
462
|
+
| `P-Val-<N>` | `4.5.6 Validation Checklist` | one checklist item |
|
|
463
|
+
| `P-Rb-<N>` | `4.5.7 Rollback Strategy` | one rollback path |
|
|
464
|
+
|
|
465
|
+
`4.5.2 Trade-off Matrix` and `4.5.3 Recommended Option` are NOT extracted as standalone plan items — the trade-off matrix is evaluated implicitly through each option's `P-Opt-*` verification, and the recommended option is one of those `P-Opt-*` rows.
|
|
466
|
+
|
|
467
|
+
Each plan item inherits the `[TICKETID: ...]` tag of its source section (per the standard ticket-tagging contract).
|
|
468
|
+
|
|
469
|
+
### Plan-body verdict semantics
|
|
470
|
+
|
|
471
|
+
The verdict tokens `AGREE` / `DISAGREE` / `SUPPLEMENT` are reused, but their meaning is plan-specific:
|
|
472
|
+
|
|
473
|
+
- **AGREE**: the item is executable as written *and* internally consistent with other items in the plan.
|
|
474
|
+
- **DISAGREE(<kind>)**: the item is broken. `<kind>` MUST be one of:
|
|
475
|
+
- `a` — referenced file path / symbol mismatches another step or option's File Structure list
|
|
476
|
+
- `b` — command is not executable or is ambiguous
|
|
477
|
+
- `c` — validation signal is not observable
|
|
478
|
+
- `d` — rollback violates commit / dependency order
|
|
479
|
+
- `e` — item contradicts the trade-off matrix
|
|
480
|
+
- **SUPPLEMENT**: the item is sound but is missing a dependency / edge case / precondition.
|
|
481
|
+
|
|
482
|
+
Worker non-result handling (`timeout`, `error`, no result file, wrapper `cli-failure`) is identical to finding convergence: do NOT aggregate as DISAGREE, record `contract-violation`, and apply the round-level abort rule below.
|
|
483
|
+
|
|
484
|
+
### Mode constraint
|
|
485
|
+
|
|
486
|
+
Plan-body verification only supports **lightweight mode** (defined in §"Verification Mode" above). `full-reanalysis` is not meaningful here because the "original source materials" for a plan item are the worker's own analysis plus the lead-mediated synthesis — there is no independent ground truth to re-read. The manifest's top-level `verificationMode` is ignored for this round; lightweight is always used.
|
|
487
|
+
|
|
488
|
+
### Round protocol (single round at default `maxRounds=1`)
|
|
489
|
+
|
|
490
|
+
1. Lead parses the report-writer draft and extracts the `P-*` plan items.
|
|
491
|
+
2. For each analyser worker in the roster (`claude`, `codex`, and `gemini` if opted in), lead constructs a reverify prompt using the template in §"Plan-body reverify prompt" below.
|
|
492
|
+
3. Dispatch uses the same wrapper infrastructure as finding convergence. The `--role-slug` is `<role>-plan-verify-r<N>`. Result file path: `runs/<task-type>/worker-results/<role-slug>-plan-verify-r<N>-implementation-planning-<seq>.md`.
|
|
493
|
+
4. After all dispatches return, lead aggregates verdicts per `P-*` item across workers and classifies each:
|
|
494
|
+
- `full-consensus` — all participating analysers `AGREE` (SUPPLEMENT counts as agree on the item itself).
|
|
495
|
+
- `partial-consensus` — majority `AGREE`, dissenting `DISAGREE` recorded.
|
|
496
|
+
- `worker-unique` — only one worker `DISAGREE`s, others `AGREE` — treat as `partial-consensus` for gate purposes; record dissent.
|
|
497
|
+
- `majority-disagree` — majority of analysers `DISAGREE` on this item. This is the only classification that **blocks the Approval marker**.
|
|
498
|
+
- `contested` only meaningful when `maxRounds > 1`; at default `maxRounds=1`, fold any unresolved item into `partial-consensus`.
|
|
499
|
+
5. Gate result resolution:
|
|
500
|
+
- any `majority-disagree` item present AND `gating=true` → `blocked-by-disagreement`
|
|
501
|
+
- all dispatches non-result → `aborted-non-result`
|
|
502
|
+
- any `partial-consensus` / `worker-unique` present, no `majority-disagree` → `passed-with-dissent`
|
|
503
|
+
- all items `full-consensus` → `passed`
|
|
504
|
+
6. Lead writes `runs/<task-type>/state/plan-body-verification.json` (schema below) and populates `### 4.5.9 Plan Body Verification` in the final report (template at `templates/reports/final-report.template.md`).
|
|
505
|
+
7. For every `majority-disagree` item, lead adds a row to `## 5. Clarification Items` with:
|
|
506
|
+
- new `C-<N>` ID (numbering continues from any existing rows)
|
|
507
|
+
- `Statement` summarising the disagreement and the worker breakage `<kind>`
|
|
508
|
+
- `Kind` chosen per the standard policy (usually `decision` for option-level conflicts, `data-point` for path/symbol mismatches)
|
|
509
|
+
- `Blocks=approval`
|
|
510
|
+
- the §4.5.9 verdict table's `Classification` column for that row reads `majority-disagree → C-<N>` (1:1 ID match — orphan on either side is a contract violation per `prompts/profiles/implementation-planning.md` self-review step 6).
|
|
511
|
+
8. The top-of-report `- [ ] Approved` marker line is rendered if and only if the Gate result is `passed` or `passed-with-dissent`. `validators/validate-run.py` `validate_phase_boundary` enforces this correspondence; manually adding the marker line when the gate did not pass is a contract violation.
|
|
512
|
+
|
|
513
|
+
### `plan-body-verification.json` schema
|
|
514
|
+
|
|
515
|
+
```json
|
|
516
|
+
{
|
|
517
|
+
"schemaVersion": "1.0",
|
|
518
|
+
"phase": "implementation-planning",
|
|
519
|
+
"round": 1,
|
|
520
|
+
"effectiveMaxRounds": 1,
|
|
521
|
+
"gating": true,
|
|
522
|
+
"verificationMode": "lightweight",
|
|
523
|
+
"gateResult": "passed | passed-with-dissent | blocked-by-disagreement | aborted-non-result",
|
|
524
|
+
"planItems": [
|
|
525
|
+
{
|
|
526
|
+
"id": "P-Opt-1",
|
|
527
|
+
"sourceSection": "4.5.1",
|
|
528
|
+
"ticketId": "<id-or-unknown>",
|
|
529
|
+
"votes": {"claude-worker": "AGREE", "codex-worker": "AGREE"},
|
|
530
|
+
"classification": "full-consensus",
|
|
531
|
+
"clarificationId": null
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
"id": "P-Step-3",
|
|
535
|
+
"sourceSection": "4.5.4",
|
|
536
|
+
"ticketId": "TICKET-123",
|
|
537
|
+
"votes": {"claude-worker": "DISAGREE(a)", "codex-worker": "DISAGREE(a)"},
|
|
538
|
+
"classification": "majority-disagree",
|
|
539
|
+
"clarificationId": "C-7"
|
|
540
|
+
}
|
|
541
|
+
],
|
|
542
|
+
"dispatches": [
|
|
543
|
+
{"role": "claude-worker", "resultPath": "...", "terminalStatus": "completed"}
|
|
544
|
+
]
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
`dispatches[].terminalStatus` mirrors finding convergence (`completed | timeout | error | not-run | cli-failure`).
|
|
549
|
+
|
|
550
|
+
### Plan-body reverify prompt
|
|
551
|
+
|
|
552
|
+
Required prompt anchor headers are identical to finding convergence (see §"Required reverify-prompt anchor headers"). The prompt body changes from F-* listing to P-* listing:
|
|
553
|
+
|
|
554
|
+
```
|
|
555
|
+
You are <worker-role> performing plan-body verification for <task-key> (round 1).
|
|
556
|
+
|
|
557
|
+
## Instructions
|
|
558
|
+
|
|
559
|
+
Review the following items extracted from the consolidated implementation plan
|
|
560
|
+
authored after your initial analysis. For EACH item, respond with exactly one
|
|
561
|
+
verdict:
|
|
562
|
+
|
|
563
|
+
- **AGREE**: The item is executable as written and internally consistent with
|
|
564
|
+
other items in the plan.
|
|
565
|
+
- **DISAGREE(<kind>)**: The item is broken. Cite which kind:
|
|
566
|
+
(a) referenced file path / symbol mismatches another step or option,
|
|
567
|
+
(b) command is not executable or is ambiguous,
|
|
568
|
+
(c) validation signal is not observable,
|
|
569
|
+
(d) rollback violates commit / dependency order,
|
|
570
|
+
(e) item contradicts the trade-off matrix.
|
|
571
|
+
- **SUPPLEMENT**: The item is sound but a dependency / edge case / precondition
|
|
572
|
+
is missing.
|
|
573
|
+
|
|
574
|
+
Do NOT re-analyze the original requirements. Judge solely from plan internal
|
|
575
|
+
consistency and stated commands / paths. Do NOT inspect the original task brief
|
|
576
|
+
or worker analyses for this round.
|
|
577
|
+
|
|
578
|
+
## Plan items to verify
|
|
579
|
+
|
|
580
|
+
### P-Step-3 [TICKETID: <id>]: <one-line summary>
|
|
581
|
+
**From section**: 4.5.4 Stepwise Execution Order
|
|
582
|
+
**Original text**:
|
|
583
|
+
> <verbatim quote of the step>
|
|
584
|
+
|
|
585
|
+
**Check**:
|
|
586
|
+
- Are referenced file paths consistent with the option's File Structure list?
|
|
587
|
+
- Is the named command executable as written?
|
|
588
|
+
- Does the success criterion produce an observable signal?
|
|
589
|
+
|
|
590
|
+
### P-Opt-2 [TICKETID: <id>]: <one-line summary>
|
|
591
|
+
...
|
|
592
|
+
|
|
593
|
+
## Response format
|
|
594
|
+
|
|
595
|
+
### P-Step-3
|
|
596
|
+
**Verdict**: AGREE | DISAGREE(<a|b|c|d|e>) | SUPPLEMENT
|
|
597
|
+
**Explanation**: <2-3 sentences>
|
|
598
|
+
|
|
599
|
+
### P-Opt-2
|
|
600
|
+
...
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
The "Reverify prompt: required-reading suppression (BLOCKING)" rule (lightweight mode does NOT inject a `[Required reading]` clause) applies here as well.
|
|
604
|
+
|
|
605
|
+
### Worker non-result handling in plan-body round (BLOCKING)
|
|
606
|
+
|
|
607
|
+
Mirrors finding convergence (§"Worker failure handling in reverify"). Concretely:
|
|
608
|
+
|
|
609
|
+
- A dispatch that returns terminal non-result MUST NOT be aggregated as `DISAGREE`.
|
|
610
|
+
- If at least one dispatch was issued AND **all** plan-body dispatches return non-result, the Gate result is `aborted-non-result`. Record one `contract-violation` event per non-result dispatch.
|
|
611
|
+
- The Approval marker is NOT rendered when the gate is `aborted-non-result`. A single row is added to `## 5. Clarification Items` with `Statement="plan-body verification could not run — all workers returned non-result"`, `Kind=decision`, `Blocks=approval`, allowing the user to either retry the phase or override by manually approving the plan (via `--approve` on the resume command).
|
|
612
|
+
|
|
@@ -9,6 +9,8 @@ Launch an okstra task — gather inputs interactively via the **wizard state mac
|
|
|
9
9
|
|
|
10
10
|
**Single authority**: this skill drives `okstra wizard`, which owns every step (ordering, branching, validation). The skill is just a thin prompt-relay loop — it never decides "what to ask next" itself. If the flow needs to change, edit `scripts/okstra_ctl/wizard.py`, not this file.
|
|
11
11
|
|
|
12
|
+
**Bash invocation rule (permission-friendly)**: every Bash command in this skill MUST begin with the literal token `okstra` (or another already-allowed binary) and pass literal argument values. Do not introduce shell variables (`$STATE_FILE`, `$ANSWER`, `$projectRoot`, ...), `$(...)` command substitution, or leading `VAR=...` assignments — any of those make the leading token non-literal, defeat the `Bash(okstra:*)` permission match, and force a confirmation prompt on every wizard call. When a prior tool call emitted a path or value, read it from the tool output and paste the literal string into the next command.
|
|
13
|
+
|
|
12
14
|
## When to Use
|
|
13
15
|
|
|
14
16
|
- The user is inside a Claude Code session and asks to start an okstra task ("run okstra here", "start an error-analysis on this branch", "okstra implementation-planning for INV-1234").
|
|
@@ -66,13 +68,23 @@ Parse `projectRoot` and `projectId` from that JSON output.
|
|
|
66
68
|
|
|
67
69
|
## Step 2: Initialize the wizard
|
|
68
70
|
|
|
71
|
+
> **Permission-friendly invocation rule**: every `okstra wizard ...` / `okstra render-bundle ...` call below MUST start with the literal token `okstra` and use literal argument values copied from prior tool outputs. Do **not** introduce shell variables (`$STATE_FILE`, `$ANSWER`, `$projectRoot`, ...), `$(...)` command substitution, or leading assignments — they break the `Bash(okstra:*)` permission match and force a confirmation prompt on every call.
|
|
72
|
+
|
|
73
|
+
First, generate a state-file path:
|
|
74
|
+
|
|
69
75
|
```bash
|
|
70
|
-
|
|
76
|
+
okstra wizard new-state-file
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This prints one absolute path on stdout (e.g. `/var/folders/.../okstra-wizard.AbCd.json`). Read that path from the tool output and **paste it literally** into every subsequent `--state-file` argument.
|
|
80
|
+
|
|
81
|
+
Then initialize the wizard with the literal `projectRoot` / `projectId` you parsed from Step 1 and the literal state-file path from above:
|
|
71
82
|
|
|
83
|
+
```bash
|
|
72
84
|
okstra wizard init \
|
|
73
|
-
--state-file
|
|
74
|
-
--project-root
|
|
75
|
-
--project-id
|
|
85
|
+
--state-file /var/folders/.../okstra-wizard.AbCd.json \
|
|
86
|
+
--project-root /abs/path/to/project \
|
|
87
|
+
--project-id my-project-id
|
|
76
88
|
```
|
|
77
89
|
|
|
78
90
|
Output: the same `{ok, next}` JSON described above. The first `next` is always `step: "task_pick"`.
|
|
@@ -84,10 +96,11 @@ Repeat until `next.kind == "done"`:
|
|
|
84
96
|
1. **Render** the prompt according to `kind`:
|
|
85
97
|
- `pick` → `AskUserQuestion` with `label` and `options`. The user's chosen option's `value` is the answer string.
|
|
86
98
|
- `text` → plain text message containing `label`. Consume the user's next reply verbatim as the answer string (empty reply = empty string).
|
|
87
|
-
2. **Submit** the answer:
|
|
99
|
+
2. **Submit** the answer — call `okstra wizard step` with the literal state-file path from Step 2 and the literal user answer (no shell variables, no `$(...)`):
|
|
88
100
|
```bash
|
|
89
|
-
okstra wizard step --state-file
|
|
101
|
+
okstra wizard step --state-file /var/folders/.../okstra-wizard.AbCd.json --answer preprod
|
|
90
102
|
```
|
|
103
|
+
If the answer contains spaces or shell metacharacters, wrap it in double quotes around the literal string only — never inside `"$VAR"`.
|
|
91
104
|
3. **Handle result**:
|
|
92
105
|
- `ok: true` → echo `result.echo` to the user on one short line, then loop with `result.next`.
|
|
93
106
|
- `ok: false` → show `result.error` to the user verbatim, then loop with `result.current` (re-prompt the same step).
|
|
@@ -110,9 +123,11 @@ Do not second-guess the wizard. If the next prompt seems out of place, the bug i
|
|
|
110
123
|
When `next.step == "confirm"`, before relaying the picker, fetch the human-readable selection summary:
|
|
111
124
|
|
|
112
125
|
```bash
|
|
113
|
-
okstra wizard confirmation --state-file
|
|
126
|
+
okstra wizard confirmation --state-file /var/folders/.../okstra-wizard.AbCd.json
|
|
114
127
|
```
|
|
115
128
|
|
|
129
|
+
(Substitute the literal state-file path captured in Step 2 — no `$STATE_FILE`.)
|
|
130
|
+
|
|
116
131
|
Output: `{ok: true, text: "선택 확인:\n task-type : ...\n ..."}`. Print `text` to the user, then render the `confirm` picker (Proceed / Edit).
|
|
117
132
|
|
|
118
133
|
## Step 5: Render the task bundle
|
|
@@ -120,9 +135,11 @@ Output: `{ok: true, text: "선택 확인:\n task-type : ...\n ..."}`. Prin
|
|
|
120
135
|
When `next.kind == "done"`, fetch the final args:
|
|
121
136
|
|
|
122
137
|
```bash
|
|
123
|
-
okstra wizard render-args --state-file
|
|
138
|
+
okstra wizard render-args --state-file /var/folders/.../okstra-wizard.AbCd.json
|
|
124
139
|
```
|
|
125
140
|
|
|
141
|
+
(Again: literal state-file path, no `$STATE_FILE`.)
|
|
142
|
+
|
|
126
143
|
Output: `{ok: true, args: {"project-root": "...", "task-type": "...", ...}}`. Build the `okstra render-bundle` invocation from `args`, passing each key as `--<key>` and the value verbatim (including empty strings — they are intentional `use phase default` markers).
|
|
127
144
|
|
|
128
145
|
```bash
|
|
@@ -152,7 +169,7 @@ okstra render-bundle \
|
|
|
152
169
|
|
|
153
170
|
The python function underneath is mutex-protected (`~/.okstra/.locks/<task-key>.lock`), writes `run-context-*.json` + `run-inputs-*.json` + all manifests + discovery files, and registers the run in `~/.okstra/recent.jsonl` with status `prepared`.
|
|
154
171
|
|
|
155
|
-
You can delete
|
|
172
|
+
You can delete the literal state-file path after this point — its job is done. Invoke `rm` with the literal path (e.g. `rm /var/folders/.../okstra-wizard.AbCd.json`), not a shell variable.
|
|
156
173
|
|
|
157
174
|
## Step 6: Take over as Claude lead
|
|
158
175
|
|
|
@@ -182,11 +199,7 @@ okstra config set pr-template-path "<path>" --scope global
|
|
|
182
199
|
|
|
183
200
|
The scope is exposed via `wizard render-args` only as the `pr-template-path` value (1-shot override); the persist hint lives in the wizard state. Read it with:
|
|
184
201
|
|
|
185
|
-
|
|
186
|
-
python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('pr_template_scope',''))" "$STATE_FILE"
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
(or just inspect the JSON state file directly — it is a plain serialized `WizardState`).
|
|
202
|
+
Read the JSON state file directly with the `Read` tool (literal path captured in Step 2) and inspect the `pr_template_scope` field — it is a plain serialized `WizardState`. Avoid `python3 -c "...$STATE_FILE"` style commands; they trip Bash static analysis.
|
|
190
203
|
|
|
191
204
|
## Concurrency
|
|
192
205
|
|