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.
@@ -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
- "related_tasks_raw": "", "clarification_response_path": "",
1028
- "pr_template_path": "", "pr_template_scope": "",
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
- STATE_FILE="$(mktemp -t okstra-wizard.XXXX.json)"
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 "$STATE_FILE" \
74
- --project-root "$projectRoot" \
75
- --project-id "$projectId"
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 "$STATE_FILE" --answer "$ANSWER"
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 "$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 "$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 `$STATE_FILE` after this point — its job is done.
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
- ```bash
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