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.
@@ -225,6 +225,40 @@ revert 경로와 롤백 트리거 신호를 표로 정리합니다. 추상적
225
225
  - 본 섹션에는 승인 결정에 영향을 주는 *플랜 측 보충 메모*만 적습니다(예: 위험을 줄이기 위한 사전 작업, 승인 전 사용자가 확인해 두어야 할 사항). 승인 마커는 본 섹션이 아니라 상단 블록의 체크박스로만 부여합니다.
226
226
  - 사용자가 답하거나 자료를 첨부해야만 승인이 가능한 항목은 **이 섹션에 적지 않습니다** — `## 5. Clarification Items` 표에 한 행으로 등록하고 `Blocks=approval` 로 표시하세요. 같은 항목을 두 위치에 적으면 sync 가 깨집니다.
227
227
 
228
+ ### 4.5.9 Plan Body Verification (워커 사후 검증)
229
+
230
+ > **이 sub-section 은 `task-type` = `implementation-planning` 실행 결과에만 포함하세요.** Phase 6 에서 report-writer 가 합성한 4.5 본문(Option Candidates / Stepwise Execution Order / Dependency / Validation Checklist / Rollback)을 lead 가 plan-item 단위로 쪼개 워커들에게 다시 던지고 `AGREE / DISAGREE / SUPPLEMENT` 평결을 수집한 결과입니다. 자세한 라운드 프로토콜은 `skills/okstra-convergence/SKILL.md` "Plan-body verification mode" 섹션을 참고하세요.
231
+
232
+ - **Round count**: `<N>` (default: 1)
233
+ - **Gate result**: `<passed | passed-with-dissent | blocked-by-disagreement | aborted-non-result>`
234
+ - `passed` → 본 보고서 상단 `User Approval Request` 체크박스가 렌더됩니다.
235
+ - `passed-with-dissent` → 상단 체크박스가 렌더되되, 반대 의견은 아래 `Dissent log` 에 기록.
236
+ - `blocked-by-disagreement` → 상단 체크박스 라인 자체를 **렌더하지 않습니다**. majority DISAGREE 인 plan item 마다 `## 5. Clarification Items` 에 `Blocks=approval` row 가 추가되며, 사용자가 응답해야 다음 phase 로 넘어갈 수 있습니다.
237
+ - `aborted-non-result` → 워커 dispatch 가 모두 non-result (timeout / error). 상단 체크박스 라인 렌더하지 않음 + `## 5. Clarification Items` 에 "plan-body verification could not run" row 가 추가됩니다.
238
+
239
+ #### Verdict table
240
+
241
+ 각 plan item 1 행. `Plan item` 열은 `P-Opt-<N>` / `P-Step-<N>` / `P-Dep-<N>` / `P-Val-<N>` / `P-Rb-<N>` prefix 사용. `Section` 열은 4.5 내부 sub-section 번호 (예: `4.5.4`).
242
+
243
+ | Plan item | Ticket ID | Section | <worker1> | <worker2> | ... | Classification |
244
+ |-----------|-----------|---------|-----------|-----------|-----|----------------|
245
+ | P-Opt-1 | `<id>` | 4.5.1 | AGREE | AGREE | ... | full-consensus |
246
+ | P-Step-3 | `<id>` | 4.5.4 | DISAGREE(a) | DISAGREE(a) | ... | majority-disagree → C-7 |
247
+
248
+ - DISAGREE 셀에는 spec 의 `(a|b|c|d|e)` breakage kind 를 함께 표기 (예: `DISAGREE(a)` = 참조 file path / symbol 불일치).
249
+ - 마지막 열 `Classification` ∈ `{full-consensus, partial-consensus, worker-unique, majority-disagree → C-<N>}`. `majority-disagree → C-<N>` 의 `C-<N>` 은 본 보고서 `## 5. Clarification Items` 표에서 해당 변환 row 의 ID 와 일치해야 합니다.
250
+
251
+ #### Dissent log
252
+
253
+ `partial-consensus` 와 `worker-unique` 로 분류된 plan item 의 반대 의견을 plan item 별로 묶어 적습니다. `majority-disagree` 항목의 반대 의견은 본 섹션 대신 `## 5. Clarification Items` 의 해당 row 의 `Statement` 컬럼에 옮겨 적습니다.
254
+
255
+ - **P-XXX-N**: `<worker-role>` — `<반대 의견 본문 2-3 sentences>`
256
+
257
+ #### Notes
258
+
259
+ - 본 sub-section 이 누락된 채로 task-type = implementation-planning final report 가 생성되면 validator 가 `contract-violated` 로 종료합니다.
260
+ - `Gate result` 가 `blocked-by-disagreement` / `aborted-non-result` 인데 상단 `User Approval Request` 체크박스 라인이 존재하면 동일하게 contract violation 입니다.
261
+
228
262
  ## 4.6 Release Handoff Deliverables (release-handoff runs only)
229
263
 
230
264
  > **이 섹션은 `task-type` = `release-handoff` 실행 결과에만 포함하세요. 다른 task-type에서는 섹션 전체를 삭제하고 5번 섹션으로 넘어갑니다.**
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import argparse
6
6
  import json
7
7
  import os
8
+ import re
8
9
  import sys
9
10
  from datetime import datetime, timezone
10
11
  from pathlib import Path
@@ -527,6 +528,35 @@ PLANNING_REQUIRED_SECTIONS = (
527
528
  "Validation Checklist",
528
529
  "Rollback",
529
530
  "User Approval Request",
531
+ "Plan Body Verification",
532
+ )
533
+
534
+ PLAN_VERIFY_GATE_VALUES = (
535
+ "passed",
536
+ "passed-with-dissent",
537
+ "blocked-by-disagreement",
538
+ "aborted-non-result",
539
+ )
540
+
541
+ # `Gate result:` line in §4.5.9 of the final report — captures the value
542
+ # token that follows. Tolerates arbitrary markdown formatting between the
543
+ # label and the value (backticks for inline code, double-asterisks for
544
+ # bold, colons, hyphens, whitespace). The captured value is then
545
+ # validated against `PLAN_VERIFY_GATE_VALUES` below so typo'd or unknown
546
+ # values surface as their own failure rather than silently no-matching.
547
+ _GATE_RESULT_RE = re.compile(
548
+ r"Gate result[^A-Za-z\n]+(?P<value>[a-z][a-z\-]+)",
549
+ re.IGNORECASE,
550
+ )
551
+
552
+ # Approval marker line — the bullet that toggles to grant approval. Both
553
+ # unchecked (`- [ ] Approved`) and checked (`- [x] Approved`) forms count
554
+ # as "checkbox present" for this gate. Line-anchored to avoid false
555
+ # positives from inline-code prose examples elsewhere in the template
556
+ # (mirrors `APPROVED_PLAN_PATTERN` in scripts/okstra_ctl/run.py:79).
557
+ _APPROVAL_CHECKBOX_RE = re.compile(
558
+ r"^[ \t]*(?:[-*+][ \t]+)?`?\[[ xX]\][ \t]*Approved`?[ \t]*$",
559
+ re.MULTILINE,
530
560
  )
531
561
 
532
562
 
@@ -541,6 +571,13 @@ def validate_phase_boundary(
541
571
  required deliverable sections; absence indicates a planning run that
542
572
  skipped its core outputs (or an implementation run that ran under the
543
573
  wrong task type).
574
+
575
+ Additionally enforces the Plan Body Verification gate (§4.5.9):
576
+ - gate ∈ {passed, passed-with-dissent} → top-level Approval checkbox
577
+ MUST be present.
578
+ - gate ∈ {blocked-by-disagreement, aborted-non-result} → checkbox
579
+ MUST be absent (lead converted findings into Clarification rows
580
+ instead of opening the gate).
544
581
  """
545
582
  if task_type != "implementation-planning":
546
583
  return
@@ -554,6 +591,40 @@ def validate_phase_boundary(
554
591
  f"`{needle}`"
555
592
  )
556
593
 
594
+ gate_match = _GATE_RESULT_RE.search(content)
595
+ if gate_match is None:
596
+ # The `Plan Body Verification` heading check above already covers
597
+ # the wholly-missing case; a heading present without a `Gate result`
598
+ # line is its own contract violation.
599
+ if "Plan Body Verification" in content:
600
+ failures.append(
601
+ "implementation-planning report has `Plan Body Verification` "
602
+ "section but no `Gate result:` line — required by §4.5.9."
603
+ )
604
+ return
605
+ gate_value = gate_match.group("value").strip().lower()
606
+ if gate_value not in PLAN_VERIFY_GATE_VALUES:
607
+ failures.append(
608
+ "implementation-planning report `Gate result` value "
609
+ f"`{gate_value}` is not one of "
610
+ f"{', '.join(PLAN_VERIFY_GATE_VALUES)}."
611
+ )
612
+ return
613
+ checkbox_present = _APPROVAL_CHECKBOX_RE.search(content) is not None
614
+ if gate_value in ("passed", "passed-with-dissent") and not checkbox_present:
615
+ failures.append(
616
+ "implementation-planning report Gate result is "
617
+ f"`{gate_value}` but the top-level `User Approval Request` "
618
+ "checkbox line (`- [ ] Approved` / `- [x] Approved`) is missing."
619
+ )
620
+ if gate_value in ("blocked-by-disagreement", "aborted-non-result") and checkbox_present:
621
+ failures.append(
622
+ "implementation-planning report Gate result is "
623
+ f"`{gate_value}` but a top-level `User Approval Request` "
624
+ "checkbox line is present — gate must NOT render the checkbox "
625
+ "for this gate result."
626
+ )
627
+
557
628
 
558
629
  def _refresh_task_catalog(project_root: Path, task_manifest: dict) -> tuple[bool, str]:
559
630
  """Regenerate `discovery/task-catalog.json` so it stops trailing the
package/src/wizard.mjs CHANGED
@@ -1,3 +1,7 @@
1
+ import { mkdtempSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
1
5
  import { runPythonModule } from "./_python-helper.mjs";
2
6
  import { resolvePaths } from "./paths.mjs";
3
7
 
@@ -7,12 +11,14 @@ Used by the okstra-run skill to drive the per-step prompt loop. Each
7
11
  subcommand round-trips a JSON state file held by the skill.
8
12
 
9
13
  Subcommands:
10
- init seed a fresh wizard state and emit the first prompt
11
- step submit an answer (or fetch the current prompt) and emit next
12
- render-args emit the final --flag/value map for 'okstra render-bundle'
13
- confirmation emit the multi-line confirmation echo block
14
+ new-state-file print a fresh absolute state-file path on stdout (no I/O on the file itself)
15
+ init seed a fresh wizard state and emit the first prompt
16
+ step submit an answer (or fetch the current prompt) and emit next
17
+ render-args emit the final --flag/value map for 'okstra render-bundle'
18
+ confirmation emit the multi-line confirmation echo block
14
19
 
15
20
  Usage:
21
+ okstra wizard new-state-file
16
22
  okstra wizard init --state-file <path> --project-root <p> --project-id <id>
17
23
  okstra wizard step --state-file <path> [--answer <value>]
18
24
  okstra wizard render-args --state-file <path>
@@ -50,11 +56,21 @@ export async function run(args) {
50
56
  }
51
57
 
52
58
  const [sub, ...rest] = args;
53
- if (!["init", "step", "render-args", "confirmation"].includes(sub)) {
59
+ if (!["new-state-file", "init", "step", "render-args", "confirmation"].includes(sub)) {
54
60
  process.stderr.write(`error: unknown wizard subcommand '${sub}'\n\n${USAGE}`);
55
61
  return 2;
56
62
  }
57
63
 
64
+ if (sub === "new-state-file") {
65
+ if (rest.length > 0) {
66
+ process.stderr.write("error: new-state-file takes no arguments\n");
67
+ return 2;
68
+ }
69
+ const dir = mkdtempSync(join(tmpdir(), "okstra-wizard-"));
70
+ process.stdout.write(join(dir, "state.json") + "\n");
71
+ return 0;
72
+ }
73
+
58
74
  let flags;
59
75
  try {
60
76
  flags = parseFlags(rest);