okstra 0.162.2 → 0.163.1

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.
@@ -294,6 +294,7 @@ Important modules:
294
294
  | `manager_sync.py` | One-way child project `.okstra` snapshot reader; corrupt child state becomes row-level `error` so other children continue |
295
295
  | `manager_launch.py` | Child launch packet and manager child context renderer; records `prepared` launch metadata/events without changing project-local task state |
296
296
  | `dispatch_core.py` | Backend-neutral worker dispatch core — worker execution/collection logic shared by any lead runtime (Claude/Codex/Antigravity/external); gates selected initial prompts through the shared cross-task contract before launch |
297
+ | `cmux.py` | cmux-pane worker backend — mirrors the tmux backend's contract (a worker that gets a pane frees the lead process; anything that stops a pane opening degrades quietly to the blocking wrapper, recording a surface UUID rather than a tmux pane id). Detects a usable cmux session before selecting the backend (CLI resolves + ping answers PONG + the lead's workspace is resolvable), derives placement from the workspace geometry each dispatch, relays lead/worker events to the cmux sidebar, and records the run's terminal backend in the manifest so both phases of a run land on one backend. A sandbox that hides cmux (`PermissionError` on the socket) stops dispatch with the remedy instead of degrading into the same broken fallback; a quit app (`FileNotFoundError`) still degrades |
297
298
  | `codex_dispatch.py` | Codex lead CLI-worker dispatcher — the `okstra codex-dispatch` backend. Reads the run manifest to run the Codex-side supported worker subset, applies the same cross-task initial-prompt gate, and performs token-usage substitution, view render, follow-up, and validation |
298
299
  | `analysis_packet.py` | assembles the compact analysis-worker input packet for a task run from worker-owned profile sections; report/lead procedure stays outside the packet |
299
300
  | `analysis_inputs.py` | shared input boundary for `project-analysis`, `feature-analysis`, and `change-impact-analysis` — validates evidence-report identity and review status, enforces the type-to-type relation allowlist, computes `exact`/`stale` freshness, and resolves free-text or `PF-NNN` feature targets for both wizard and prepare paths |
@@ -431,7 +432,7 @@ Boilerplate shared by several skills (bash invocation rule, outdated-CLI preflig
431
432
  | `agents/workers/kimi-worker.params.json` | Kimi read-only analyser/critic wrapper params |
432
433
  | `agents/workers/report-writer-worker.md` | data.json SSOT author and audit sidecar writer |
433
434
 
434
- The neutral lead lifecycle contract lives at `prompts/lead/okstra-lead-contract.md`. Host mappings live under `prompts/lead/adapters/`: `claude-code.md`, `codex.md`, `antigravity.md`, and `external.md`. All are runtime resources installed under `~/.okstra/prompts/lead/`, not agent skills.
435
+ The neutral lead lifecycle contract lives at `prompts/lead/okstra-lead-contract.md`. Host mappings live under `prompts/lead/adapters/`: `claude-code.md`, `codex.md`, `antigravity.md`, `external.md`, and `cmux.md` (the environment-selected cmux worker backend, read by every task type regardless of lead runtime). All are runtime resources installed under `~/.okstra/prompts/lead/`, not agent skills.
435
436
 
436
437
  ### 4.12 `tests/` and `tests-e2e/`
437
438
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.162.2",
3
+ "version": "0.163.1",
4
4
  "description": "Host-aware multi-provider cross-verification orchestrator runtime and agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.162.2",
3
- "builtAt": "2026-08-08T18:44:32.842Z",
2
+ "package": "0.163.1",
3
+ "builtAt": "2026-08-09T08:20:00.790Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -94,6 +94,10 @@ while [[ $# -gt 0 ]]; do
94
94
  REPORT_WRITER_MODEL_OVERRIDE="$(require_option_value --report-writer-model "${2-}")"
95
95
  shift 2
96
96
  ;;
97
+ --launch-only)
98
+ LAUNCH_ONLY="true"
99
+ shift
100
+ ;;
97
101
  --lead-runtime)
98
102
  LEAD_RUNTIME="$(require_option_value --lead-runtime "${2-}")"
99
103
  shift 2
@@ -164,3 +164,6 @@ REPORT_WRITER_MODEL_EXECUTION_VALUE=""
164
164
  DEFAULT_WORKERS="claude,codex,report-writer"
165
165
  DISPLAY_COMMAND_NAME="${OKSTRA_COMMAND_NAME:-$(basename "$0")}"
166
166
  DISPLAY_TOOL_NAME="${OKSTRA_TOOL_NAME:-okstra}"
167
+
168
+ # `okstra run <lead>` 전용: 번들을 준비하지 않고 리드 CLI 만 띄운다.
169
+ LAUNCH_ONLY="false"
@@ -71,6 +71,58 @@ if [[ "$RESUME_CLARIFICATION_MODE" == "true" ]]; then
71
71
  exit 0
72
72
  fi
73
73
  autofill_from_project_json
74
+
75
+ # `okstra run <lead>` — open the host, nothing else. The okstra-run skill inside
76
+ # it collects task inputs, which is how the Claude Code path has always worked.
77
+ # Preparing a bundle here would demand a task id before the user has a session
78
+ # to choose one in.
79
+ if [[ "$LAUNCH_ONLY" == "true" ]]; then
80
+ if [[ -z "$PROJECT_ROOT" ]]; then
81
+ PROJECT_ROOT="$(resolve_project_root_strict "$PROJECT_ROOT_OVERRIDE")" || exit 1
82
+ fi
83
+ read -r LEAD_EXECUTABLE SANDBOX_NOTE < <(
84
+ okstra_py - "$LEAD_RUNTIME" <<'PY'
85
+ import sys
86
+ from okstra_ctl.lead_runtime import lead_runtime_info
87
+ from okstra_ctl.models import bare_lead_launch_argv, lead_launch_spec
88
+ provider = lead_runtime_info(sys.argv[1]).agent
89
+ print(bare_lead_launch_argv(provider)[0], lead_launch_spec(provider).sandbox_waiver_note)
90
+ PY
91
+ )
92
+ if ! command -v "$LEAD_EXECUTABLE" >/dev/null 2>&1; then
93
+ printf '%s command not found\n' "$LEAD_EXECUTABLE" >&2
94
+ exit 1
95
+ fi
96
+ if [[ -n "$SANDBOX_NOTE" ]]; then
97
+ okstra_py - "$LEAD_RUNTIME" <<'PY' >&2
98
+ import sys
99
+ from okstra_ctl.lead_runtime import lead_runtime_info
100
+ from okstra_ctl.models import lead_launch_spec
101
+ print("\n" + lead_launch_spec(lead_runtime_info(sys.argv[1]).agent).sandbox_waiver_note)
102
+ PY
103
+ if [[ "$ASSUME_YES" != "true" ]]; then
104
+ printf 'Start %s without its sandbox? [y/N] ' "$LEAD_EXECUTABLE" >&2
105
+ read -r _sandbox_answer < /dev/tty || _sandbox_answer=""
106
+ if [[ "$_sandbox_answer" != "y" && "$_sandbox_answer" != "Y" ]]; then
107
+ printf 'okstra: cancelled\n' >&2
108
+ exit 1
109
+ fi
110
+ fi
111
+ fi
112
+ LAUNCH_ARGV=()
113
+ while IFS= read -r -d '' _arg; do LAUNCH_ARGV+=("$_arg"); done < <(
114
+ okstra_py - "$LEAD_RUNTIME" <<'PY'
115
+ import sys
116
+ from okstra_ctl.lead_runtime import lead_runtime_info
117
+ from okstra_ctl.models import bare_lead_launch_argv
118
+ for a in bare_lead_launch_argv(lead_runtime_info(sys.argv[1]).agent):
119
+ sys.stdout.write(a + "\0")
120
+ PY
121
+ )
122
+ cd "$PROJECT_ROOT"
123
+ exec "${LAUNCH_ARGV[@]}"
124
+ fi
125
+
74
126
  autofill_from_manifest
75
127
  collect_required_arguments
76
128
 
@@ -200,4 +200,7 @@
200
200
  9. **Cross-project dependency check** — confirm you have not missed a dependency on another repo / another top-level deployable module / a published package. If `dependencyMigrationRisk` has a `kind: cross-project` row, confirm a matching `direction: upstream-precondition` `XP-NNN` row exists in `crossProjectDependencies`, and re-read as a reviewer whether its `requiredWork` is the concrete work the other side must actually build rather than an abstract phrase ("other side's work done") — validator S only checks existence, so concreteness is the self-review's responsibility. Confirm cross-repo work is split into a separate run + XP row instead of being crammed into one task's stages, and that the cross-project substance is not duplicated in `§3 Recommended Next Steps` but lives only in `§5.4 Cross-Project Dependencies`.
201
201
  10. **Decision-draft materialization check** — when `decisionDrafts` is non-empty, confirm as a reviewer which stage's stepwise order contains the matching materialization step (creating `.okstra/decisions/<NNNN>-<slug>.md`) and that the number of drafts corresponds 1:1 with the materialization steps. The validator only checks the *existence* of the step, so the `<NNNN>-<slug>` correctness and count correspondence are the self-review's responsibility.
202
202
  11. **Variation-point & seam check** — read `variationPointAnalysis` as a skeptic. Is `hasMultipleImplementations` honest against the brief and the sibling code you inspected during pre-planning, or was `false` chosen because it is the cheaper field to fill? For every point with `extract: true`, confirm the `extractionDecision` names a real interface (a `port` for a hexagonal project, not a shared helper) and a `coveredBy` stage that exists in the Stage Map — an interface no stage builds is a decision nobody executes. Then read the recommended option's `testSeams`: each `injectedAs` must name a construction or wiring point a test can actually substitute at, not a symbol the test would have to re-implement — a seam nothing can be injected into leaves the executor writing self-mocks. An empty `testSeams` array is only acceptable when you can defend it in one sentence; the validator accepts it either way, so this is the check that catches an unfilled field posing as a decision.
203
- 12. **Approval blast-radius check (BLOCKING).** Every `Blocks=approval` clarification row must be reachable *from* the plan, not only *into* it: at least one `planItems[]` entry carrying that id as `clarificationId`, or one `requirementCoverage` row blocked on it in `status` or `approvalDisposition`. Item 7 covers only rows this run promoted from a majority-disagree plan item; a blocker raised any other way can still withhold approval while recording nothing it affects. The cost lands on the re-run: `okstra incremental-scope` resolves impacted stages from exactly these two links and treats an id that traces to no stage as grounds to re-verify every stage, so one unlinked blocker turns an incremental re-run into a full one. **Enforced:** `validators/validate-run.py` `_validate_approval_clarification_backtrace`.
203
+ 12. **Approval blast-radius check (BLOCKING).** Every `Blocks=approval` clarification row must be reachable *from* the plan, not only *into* it: at least one `planItems[]` entry carrying that id as `clarificationId`, or one `requirementCoverage` row blocked on it in `status` or `approvalDisposition`. Item 7 covers only rows this run promoted from a majority-disagree plan item; a blocker raised any other way can still withhold approval while recording nothing it affects. The cost lands on the re-run: `okstra incremental-scope` resolves impacted stages from exactly these two links and treats an id that traces to no stage as grounds to re-verify every stage, so one unlinked blocker turns an incremental re-run into a full one.
204
+ - **The link must resolve to a stage, not merely exist.** `incremental-scope` reads the stage number out of a `P-Step-<stage>.<step>` / `P-Prep-S<stage>-<kind>` plan-item id, or out of a `Stage N` citation in the blocked coverage row's `coveredBy`. Every other plan-item prefix (`P-Req-*`, `P-Val-*`, `P-Opt-*`, `P-Dep-*`, `P-Rb-*`) is numbered by position in its own array and carries no stage, so a blocker linked only that way MUST also have its coverage row cite the stage in `coveredBy`. Writing the blocked row's `coveredBy` as prose with no `Stage N` in it — `No stage.`, `Partly covered — …` — satisfies nothing: the row passes the link check and the re-run still re-verifies everything.
205
+ - What to write when no stage covers the requirement yet: name the stage the answer will change, not the stage that satisfies the requirement today. A `Blocks=approval` row is admissible only when, absent an answer, `implementation` would produce wrong or unsafe code (see the admissibility rule above) — so some stage's code is at stake by construction. If you genuinely cannot name one, the row fails the admissibility test and belongs in `## 5. Missing Information and Risks` with `Blocks=none`, not in the approval gate.
206
+ **Enforced:** `validators/validate-run.py` `_validate_approval_clarification_backtrace` — one failure for a missing link, a separate one for a link that resolves to no stage.
@@ -589,9 +589,12 @@
589
589
  "worktree_impl_new": " worktree : stage {stage} 새 worktree `{path}` (브랜치 `{branch}`, base 는 run 준비 시 해소)",
590
590
  "worktree_impl_reuse": " worktree : 기존 stage {stage} worktree `{path}` (브랜치 `{branch}`)",
591
591
  "worktree_impl_auto": " worktree : stage 자동 선택 — `{path}` 아래 stage-<N>/ worktree 생성/재사용",
592
+ "clarification_sidecars_empty": " user-responses: 없음 — final-report 만 첨부됩니다",
593
+ "clarification_sidecars_attached": " user-responses: 사이드카 {files}개 · 답변 {count}개 함께 첨부 — {ids}",
594
+ "clarification_sidecars_none_parsed": "답변으로 셀 항목 없음 (reframe 등)",
592
595
  "reverify_scope_incremental": " reverify-scope: incremental 가능 — 답변된 항목이 모두 직전 리포트의 stage 에 연결됨 (최종 확정은 run 시점 base-ref 비교)",
593
- "reverify_scope_unlinked": " reverify-scope: full 예상 — {ids} 이(가) 직전 리포트의 어느 stage 에도 연결되지 않아 범위를 좁힐 없음",
594
- "reverify_scope_full": " reverify-scope: full 예상 — {reason}",
596
+ "reverify_scope_unlinked": " reverify-scope: full 예상 — {ids} 이(가) 직전 리포트의 어느 stage 에도 연결되지 않아 범위를 좁히지 못함\n (좁히지 못하는 것은 재검증 범위이지 답변이 아닙니다 — 답변은 모두 carry-in 되어 반영되고, 대신 stage 전체를 다시 검증합니다)",
597
+ "reverify_scope_full": " reverify-scope: full 예상 — {reason}\n (좁히지 못하는 것은 재검증 범위이지 답변이 아닙니다 — 답변은 모두 carry-in 되어 반영되고, 대신 stage 전체를 다시 검증합니다)",
595
598
  "reverify_scope_user_full": " reverify-scope: full (사용자 지정 — 전체 재검증)",
596
599
  "reverify_scope_user_stages": " reverify-scope: stage {stages} 재검증 지정 (사용자 지정 — 하위 stage 포함, 나머지는 직전 판정 이월)",
597
600
  "stage_whole_task": "전체 task",
@@ -247,6 +247,18 @@ def lead_launch_spec(provider: str) -> LeadLaunchSpec:
247
247
  return spec.lead_launch
248
248
 
249
249
 
250
+ def bare_lead_launch_argv(provider: str) -> list[str]:
251
+ """Start this provider's CLI with nothing to do yet.
252
+
253
+ `okstra run <lead>` opens the host; the okstra-run skill inside it collects
254
+ task inputs, exactly as the Claude Code path already works. So there is no
255
+ prompt and no model to pin here — only the flags that make the session
256
+ usable to okstra at all.
257
+ """
258
+ launch = lead_launch_spec(provider)
259
+ return [launch.executable, *launch.sandbox_waiver]
260
+
261
+
250
262
  def lead_launch_argv(
251
263
  provider: str,
252
264
  *,
@@ -4809,6 +4809,33 @@ def render_args(state: WizardState) -> dict[str, str]:
4809
4809
  }
4810
4810
 
4811
4811
 
4812
+ def _clarification_sidecar_line(state: WizardState) -> Optional[str]:
4813
+ """확인 블록에 찍는 `user-responses/` 첨부 현황.
4814
+
4815
+ picker 단계의 옵션 라벨에도 같은 사실이 붙지만 그 화면을 지나면 사라지고,
4816
+ 확인 블록에는 final-report 경로만 남았다. 그 줄 바로 밑에 "범위를 좁히지
4817
+ 못함" 이 오니 두 줄이 겹쳐 "답변이 안 붙었다" 로 읽혔다 — 실제로는 첨부돼
4818
+ 반영되고 있었다. 실행 직전 화면에서 답변 id 를 직접 보여 그 오해를 없앤다.
4819
+ """
4820
+ if not state.clarification_response_path or not state.project_root:
4821
+ return None
4822
+ report = _resolve_path(
4823
+ state.clarification_response_path, Path(state.project_root)
4824
+ )
4825
+ files = user_response_sidecars(report)
4826
+ if not files:
4827
+ return _msg(state.workspace_root, "confirmation",
4828
+ "clarification_sidecars_empty")
4829
+ answers = sorted(sidecar_answers(report))
4830
+ return _msg(
4831
+ state.workspace_root, "confirmation", "clarification_sidecars_attached",
4832
+ files=str(len(files)), count=str(len(answers)),
4833
+ ids=", ".join(answers) or _msg(
4834
+ state.workspace_root, "confirmation",
4835
+ "clarification_sidecars_none_parsed"),
4836
+ )
4837
+
4838
+
4812
4839
  def _reverify_scope_line(state: WizardState) -> Optional[str]:
4813
4840
  """이번 clarification 재실행이 좁혀질지 — 확인 단계에서 보여주는 줄.
4814
4841
 
@@ -4923,6 +4950,9 @@ def confirmation_block(state: WizardState) -> str:
4923
4950
  lines.append(f" stage : {stage}")
4924
4951
  if state.clarification_response_path:
4925
4952
  lines.append(f" clarification : {state.clarification_response_path}")
4953
+ sidecar_line = _clarification_sidecar_line(state)
4954
+ if sidecar_line is not None:
4955
+ lines.append(sidecar_line)
4926
4956
  reverify_line = _reverify_scope_line(state)
4927
4957
  if reverify_line is not None:
4928
4958
  lines.append(reverify_line)
@@ -66,7 +66,10 @@ from okstra_ctl.report_translation import ( # noqa: E402
66
66
  hangul_share,
67
67
  )
68
68
  from okstra_ctl.stage_citations import cited_stage_numbers # noqa: E402
69
- from okstra_ctl.incremental_scope import coverage_row_blocked_on # noqa: E402
69
+ from okstra_ctl.incremental_scope import ( # noqa: E402
70
+ coverage_row_blocked_on,
71
+ stages_for_clarification,
72
+ )
70
73
  from okstra_ctl.workflow import DEFAULT_NEXT_PHASE, PHASE_SEQUENCE # noqa: E402
71
74
  from okstra_ctl.md_table import ( # noqa: E402
72
75
  is_separator_row as _is_markdown_separator,
@@ -4155,6 +4158,13 @@ def _validate_approval_clarification_backtrace(
4155
4158
  `incremental-scope` resolves impacted stages from these links and treats an
4156
4159
  id that traces to no stage as grounds to re-verify everything, so one
4157
4160
  unlinked blocker turns a narrow re-run into a full one.
4161
+
4162
+ The link must also *resolve to a stage*, which is the thing the re-run
4163
+ actually reads. Checking only that a link exists let a row satisfy this
4164
+ gate and still force full: `P-Req-*` and `P-Val-*` ids are numbered by
4165
+ position in their own array, so they carry no stage, and a blocked
4166
+ coverage row whose `coveredBy` is prose cites none either. Both shapes
4167
+ passed while the re-run they were meant to narrow re-verified everything.
4158
4168
  """
4159
4169
  if (data.get("header") or {}).get("taskType") != "implementation-planning":
4160
4170
  return
@@ -4170,15 +4180,28 @@ def _validate_approval_clarification_backtrace(
4170
4180
  if not isinstance(row, dict) or row.get("blocks") != "approval":
4171
4181
  continue
4172
4182
  row_id = str(row.get("id") or "<unknown>")
4173
- if _has_clarification_backtrace(row_id, plan_items, coverage):
4183
+ if not _has_clarification_backtrace(row_id, plan_items, coverage):
4184
+ failures.append(
4185
+ f"final-report data.json: clarification `{row_id}` blocks approval "
4186
+ "but has no back-trace into the plan — no plan item carries it as "
4187
+ "`clarificationId`, and no requirement-coverage row is `blocked "
4188
+ f"{row_id}` in its `status` or `approvalDisposition`. An item that "
4189
+ "withholds approval without recording what it affects forces the "
4190
+ "next re-run to re-verify everything."
4191
+ )
4192
+ continue
4193
+ if stages_for_clarification(data, row_id):
4174
4194
  continue
4175
4195
  failures.append(
4176
4196
  f"final-report data.json: clarification `{row_id}` blocks approval "
4177
- "but has no back-trace into the plan no plan item carries it as "
4178
- "`clarificationId`, and no requirement-coverage row is `blocked "
4179
- f"{row_id}` in its `status` or `approvalDisposition`. An item that "
4180
- "withholds approval without recording what it affects forces the "
4181
- "next re-run to re-verify everything."
4197
+ "and is linked, but the link resolves to no stage. `incremental-"
4198
+ "scope` reads the stage from a `P-Step-<stage>.<step>` / `P-Prep-"
4199
+ "S<stage>-<kind>` plan-item id, or from a `Stage N` citation in the "
4200
+ f"blocked coverage row's `coveredBy`. A `P-Req-*` / `P-Val-*` id "
4201
+ "carries no stage number, so a row linked only that way must cite "
4202
+ "the stage in `coveredBy`. A blocker whose blast radius resolves to "
4203
+ "no stage costs exactly what an unlinked one does — the next re-run "
4204
+ "re-verifies every stage."
4182
4205
  )
4183
4206
 
4184
4207
 
@@ -32,6 +32,8 @@ const RUNTIME_ALIASES = new Map([
32
32
  ["claude-code", "claude-code"],
33
33
  ["codex", "codex"],
34
34
  ["antigravity", "antigravity"],
35
+ ["avg", "antigravity"],
36
+ ["agy", "antigravity"],
35
37
  ["external", "external"],
36
38
  ]);
37
39
 
@@ -110,10 +112,18 @@ export function buildRunPlan({ args, paths, resolution, runManifestPath = "" })
110
112
  // Handed to okstra.sh, which owns input collection and the launch. Only
111
113
  // the runtime is decided here; everything else is passed through so the
112
114
  // launcher can prompt for whatever is still missing.
115
+ // --launch-only: open the host and stop. Task inputs are collected by the
116
+ // okstra-run skill inside the session, which is how the Claude Code path
117
+ // has always worked — demanding a task id out here asks for it before the
118
+ // user has a session to pick one in.
113
119
  commands: [
114
120
  {
115
121
  name: "launcher",
116
- args: ["--lead-runtime", resolution.resolvedRuntime, ...withoutRuntimeSelection(args)],
122
+ args: [
123
+ "--launch-only",
124
+ "--lead-runtime", resolution.resolvedRuntime,
125
+ ...withoutRuntimeSelection(args),
126
+ ],
117
127
  capture: false,
118
128
  },
119
129
  ],