okstra 0.25.0 → 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.
@@ -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").
@@ -48,39 +50,41 @@ Never invent additional questions. Never reorder. Never use `AskUserQuestion` fo
48
50
 
49
51
  ```bash
50
52
  if command -v okstra >/dev/null 2>&1; then
51
- OKSTRA_CMD="okstra"
53
+ okstra ensure-installed >/dev/null 2>&1 || { echo "FAIL: okstra ensure-installed failed" >&2; exit 1; }
54
+ eval "$(okstra paths --shell)"
55
+ export PYTHONPATH="$OKSTRA_PYTHONPATH"
56
+ okstra check-project --json || { echo "FAIL: this project has no okstra setup. Tell the user to run /okstra-setup first." >&2; exit 1; }
52
57
  else
53
- OKSTRA_CMD="npx -y okstra@latest"
58
+ npx -y okstra@latest ensure-installed >/dev/null 2>&1 || { echo "FAIL: okstra not installed; tell the user to run: npx okstra@latest install" >&2; exit 1; }
59
+ eval "$(npx -y okstra@latest paths --shell)"
60
+ export PYTHONPATH="$OKSTRA_PYTHONPATH"
61
+ npx -y okstra@latest check-project --json || { echo "FAIL: this project has no okstra setup. Tell the user to run /okstra-setup first." >&2; exit 1; }
54
62
  fi
55
-
56
- $OKSTRA_CMD ensure-installed >/dev/null 2>&1 || {
57
- echo "FAIL: okstra not installed; tell the user to run: npx okstra@latest install" >&2
58
- exit 1
59
- }
60
-
61
- eval "$($OKSTRA_CMD paths --shell)"
62
- export PYTHONPATH="$OKSTRA_PYTHONPATH"
63
-
64
- OKSTRA_PROJECT_INFO="$($OKSTRA_CMD check-project --json)" || {
65
- echo "FAIL: this project has no okstra setup. Tell the user to run /okstra-setup first." >&2
66
- echo "$OKSTRA_PROJECT_INFO" >&2
67
- exit 1
68
- }
69
63
  ```
70
64
 
71
- If `OKSTRA_PROJECT_INFO.ok` is `false`, ask the user with a **plain text prompt** for an absolute project-root path; rerun `okstra check-project --cwd <path>`. Re-prompt with plain text on failure.
65
+ The `check-project --json` output goes to stdout; read it from the tool result. If its `ok` field is `false`, ask the user with a **plain text prompt** for an absolute project-root path; rerun `okstra check-project --cwd <path> --json`. Re-prompt with plain text on failure.
72
66
 
73
- Parse `projectRoot` and `projectId` from `OKSTRA_PROJECT_INFO`.
67
+ Parse `projectRoot` and `projectId` from that JSON output.
74
68
 
75
69
  ## Step 2: Initialize the wizard
76
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
+
77
75
  ```bash
78
- 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.
79
80
 
81
+ Then initialize the wizard with the literal `projectRoot` / `projectId` you parsed from Step 1 and the literal state-file path from above:
82
+
83
+ ```bash
80
84
  okstra wizard init \
81
- --state-file "$STATE_FILE" \
82
- --project-root "$projectRoot" \
83
- --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
84
88
  ```
85
89
 
86
90
  Output: the same `{ok, next}` JSON described above. The first `next` is always `step: "task_pick"`.
@@ -92,10 +96,11 @@ Repeat until `next.kind == "done"`:
92
96
  1. **Render** the prompt according to `kind`:
93
97
  - `pick` → `AskUserQuestion` with `label` and `options`. The user's chosen option's `value` is the answer string.
94
98
  - `text` → plain text message containing `label`. Consume the user's next reply verbatim as the answer string (empty reply = empty string).
95
- 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 `$(...)`):
96
100
  ```bash
97
- okstra wizard step --state-file "$STATE_FILE" --answer "$ANSWER"
101
+ okstra wizard step --state-file /var/folders/.../okstra-wizard.AbCd.json --answer preprod
98
102
  ```
103
+ If the answer contains spaces or shell metacharacters, wrap it in double quotes around the literal string only — never inside `"$VAR"`.
99
104
  3. **Handle result**:
100
105
  - `ok: true` → echo `result.echo` to the user on one short line, then loop with `result.next`.
101
106
  - `ok: false` → show `result.error` to the user verbatim, then loop with `result.current` (re-prompt the same step).
@@ -118,9 +123,11 @@ Do not second-guess the wizard. If the next prompt seems out of place, the bug i
118
123
  When `next.step == "confirm"`, before relaying the picker, fetch the human-readable selection summary:
119
124
 
120
125
  ```bash
121
- okstra wizard confirmation --state-file "$STATE_FILE"
126
+ okstra wizard confirmation --state-file /var/folders/.../okstra-wizard.AbCd.json
122
127
  ```
123
128
 
129
+ (Substitute the literal state-file path captured in Step 2 — no `$STATE_FILE`.)
130
+
124
131
  Output: `{ok: true, text: "선택 확인:\n task-type : ...\n ..."}`. Print `text` to the user, then render the `confirm` picker (Proceed / Edit).
125
132
 
126
133
  ## Step 5: Render the task bundle
@@ -128,9 +135,11 @@ Output: `{ok: true, text: "선택 확인:\n task-type : ...\n ..."}`. Prin
128
135
  When `next.kind == "done"`, fetch the final args:
129
136
 
130
137
  ```bash
131
- okstra wizard render-args --state-file "$STATE_FILE"
138
+ okstra wizard render-args --state-file /var/folders/.../okstra-wizard.AbCd.json
132
139
  ```
133
140
 
141
+ (Again: literal state-file path, no `$STATE_FILE`.)
142
+
134
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).
135
144
 
136
145
  ```bash
@@ -160,7 +169,7 @@ okstra render-bundle \
160
169
 
161
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`.
162
171
 
163
- 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.
164
173
 
165
174
  ## Step 6: Take over as Claude lead
166
175
 
@@ -190,11 +199,7 @@ okstra config set pr-template-path "<path>" --scope global
190
199
 
191
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:
192
201
 
193
- ```bash
194
- python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('pr_template_scope',''))" "$STATE_FILE"
195
- ```
196
-
197
- (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.
198
203
 
199
204
  ## Concurrency
200
205
 
@@ -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);