okstra 0.170.2 → 0.170.3

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/docs/cli.md CHANGED
@@ -742,7 +742,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
742
742
  | `okstra doctor [--runtime claude-code\|codex\|antigravity\|external\|all] [--phase <phase>] [--json]` | Diagnose the runtime, Python imports, and skill/agent installation. The `codex`, `antigravity`, and `external` runtimes omit Claude skill checks. `--phase` adds readiness checks for `implementation`, `final-verification`, `release-handoff`, or `improvement-discovery` |
743
743
  | `okstra setup --project-id <id>` | Create or update `.okstra/project.json` in the current project |
744
744
  | `okstra check-project [--json]` | Verify that the current project is registered |
745
- | `okstra preflight [--runtime <name>] [--cwd <dir>] [--json]` | Single skill-preflight call combining `ensure-installed`, with silent reinstall when stale, `check-project`, and host-specific `runtimeReadiness` into one JSON response. A `claude-code` host checks project workspace trust; `codex`, `antigravity`, and `external` hosts return ready without reading Claude Code state. Step 0 of every project-scoped skill converges on this command |
745
+ | `okstra preflight [--runtime <name>] [--cwd <dir>] [--json]` | Single skill-preflight call combining `ensure-installed`, with silent reinstall when stale, `check-project`, and host-specific `runtimeReadiness` into one JSON response. A `claude-code` host checks project workspace trust. A `codex` current-session host verifies write access to `~/.okstra/worktrees/registry.lock`; a sandbox denial blocks before the wizard with the `switch-codex-to-full-access-and-rerun` action. `antigravity` and `external` hosts return ready without reading Claude Code state. Step 0 of every project-scoped skill converges on this command |
746
746
  | `okstra convergence seed --groups <path> --work-state <path> --final-state <path> --migration-dir <dir> [--restart-from-round0]` | Create, resume, reuse, or explicitly recover deterministic convergence state |
747
747
  | `okstra convergence plan-round --work-state <path> --plan <path>` | Persist the next roster-aware dispatch plan without mutating working state |
748
748
  | `okstra convergence collect-results --plan <round-plan.json> --mode <adversarial\|collaborative> --result <worker>=<path>… --dispatch <worker>=<status>:<durationMs>… --output <round-results.json>` | Read one round's worker responses into the `apply-round --results` shape. `--mode` picks the verdict vocabulary — the adversarial prompt answers `REFUTED` / `SURVIVES` / `SURVIVES-WITH-CAVEAT` / `UNVERIFIABLE`, which this maps to `disagree` / `agree` / `supplement` / `unverifiable`, and copies `**Basis**` into `disagreeBasis`. `--dispatch` supplies the terminal status and duration, which live in the dispatch rather than the response; a worker that never returned gets a `--dispatch` and no `--result`. Exits 2 on a dispatched finding with no verdict, a verdict for a finding the plan did not dispatch to that worker, a planned worker with no recorded outcome, or a vote with no explanation |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.170.2",
3
+ "version": "0.170.3",
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.170.2",
3
- "builtAt": "2026-08-14T17:23:09.226Z",
2
+ "package": "0.170.3",
3
+ "builtAt": "2026-08-14T18:24:32.087Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -1,6 +1,7 @@
1
1
  """Bundled Codex host strategy."""
2
2
  from __future__ import annotations
3
3
 
4
+ import os
4
5
  import shutil
5
6
  from collections.abc import Callable
6
7
  from pathlib import Path
@@ -35,6 +36,27 @@ DESCRIPTOR = HostDescriptor(
35
36
  )
36
37
 
37
38
 
39
+ def _okstra_home_write_checks(context) -> tuple[dict[str, object], ...]:
40
+ if context.entry_mode != "current-session":
41
+ return ({"id": "okstra-home-write", "status": "not-applicable"},)
42
+
43
+ home_dir = Path(os.environ.get("OKSTRA_PROBE_HOME_DIR", str(Path.home())))
44
+ registry_lock = home_dir / ".okstra" / "worktrees" / "registry.lock"
45
+ try:
46
+ registry_lock.parent.mkdir(parents=True, exist_ok=True)
47
+ if not registry_lock.exists():
48
+ registry_lock.touch()
49
+ with registry_lock.open("r+"):
50
+ pass
51
+ except PermissionError:
52
+ return ({
53
+ "id": "okstra-home-write",
54
+ "status": "required",
55
+ "action": "switch-codex-to-full-access-and-rerun",
56
+ },)
57
+ return ({"id": "okstra-home-write", "status": "accepted"},)
58
+
59
+
38
60
  def create_adapter(
39
61
  *,
40
62
  executable_finder: Callable[[str], str | None] = shutil.which,
@@ -59,6 +81,7 @@ def create_adapter(
59
81
  supported_functions=INTERACTION_FUNCTIONS,
60
82
  detector=no_automatic_claim,
61
83
  provider_registry=provider_registry,
84
+ readiness_probe=_okstra_home_write_checks,
62
85
  host_model_port=host_model_port or NativeExecutionValueHostModelBindingPort(
63
86
  DESCRIPTOR.id,
64
87
  DESCRIPTOR.native_provider_id,
@@ -255,7 +255,27 @@ def _content_blocks(event: Mapping[str, Any]) -> list[Mapping[str, Any]]:
255
255
  return [block for block in content if isinstance(block, Mapping)]
256
256
 
257
257
 
258
+ # A tool call's detail identifies itself at both ends and neither end alone. A
259
+ # path's run-directory prefix is shared by every file a worker touches, so the
260
+ # leaf is what tells two calls apart; a command's program name is at the front.
261
+ # Cutting the tail served only the second, and on a project whose run directory
262
+ # alone is 195 characters it rendered every file as the same visible string —
263
+ # `→ Read: /Volumes/…/tasks/analysis-…` for all of them — leaving the reader
264
+ # unable to tell one call from another. The head holds the tool name plus enough
265
+ # of the detail to read a command; the rest of the budget goes to the tail.
266
+ _HEAD_BUDGET = 32
267
+
268
+
258
269
  def _truncate(line: str, limit: int | None) -> str:
270
+ """Fold the middle, not the end.
271
+
272
+ The end is what distinguishes one line from the next, so it is the part the
273
+ screen must keep. Falls back to a tail cut only when the limit is too small
274
+ to hold a head, an ellipsis, and any tail at all.
275
+ """
259
276
  if limit is None or len(line) <= limit:
260
277
  return line
261
- return line[: limit - 1] + "…"
278
+ tail_budget = limit - _HEAD_BUDGET - 1
279
+ if tail_budget < 1:
280
+ return line[: limit - 1] + "…"
281
+ return line[:_HEAD_BUDGET] + "…" + line[-tail_budget:]
@@ -1418,10 +1418,22 @@ class _ModelBindings:
1418
1418
  invocation_assignments: dict[str, dict[str, object]]
1419
1419
 
1420
1420
 
1421
- def recommended_role_models() -> dict[str, str]:
1421
+ def recommended_role_models(*, lead_provider: str = "") -> dict[str, str]:
1422
1422
  """역할 → 추천 모델 display 값 (env override 반영). prepare 의 모델 해소와
1423
- wizard 의 안내 표기가 공유하는 단일 기준점."""
1424
- lead_default = _default("OKSTRA_DEFAULT_LEAD_MODEL", default_model("lead"))
1423
+ wizard 의 안내 표기가 공유하는 단일 기준점.
1424
+
1425
+ lead 는 호스트가 provider 를 정한다 — codex 호스트의 in-session lead 는
1426
+ codex 이고 다른 provider 요청은 거부된다(`resolve_lead_provider`). 그래서
1427
+ `lead_provider` 를 받아 그 provider 의 기본값으로 해소한다. 받지 않으면
1428
+ role 별 레거시 기본값(claude 계열)으로 떨어지는데, 그 값을 codex 호스트
1429
+ 화면에 그대로 쓰면 안내는 `opus` 인데 prepare 는 `gpt-5.6-sol` 을 배정한다.
1430
+ """
1431
+ lead_default = _default(
1432
+ "OKSTRA_DEFAULT_LEAD_MODEL",
1433
+ provider_default_model(lead_provider, "lead")
1434
+ if lead_provider
1435
+ else default_model("lead"),
1436
+ )
1425
1437
  recommendations = {
1426
1438
  "lead": lead_default,
1427
1439
  "claude": _default("OKSTRA_DEFAULT_CLAUDE_MODEL", default_model("claude")),
@@ -3445,7 +3445,14 @@ def _role_model_lines(state: WizardState) -> str:
3445
3445
  """이번 run 에서 실제로 모델을 고르게 되는 역할만, 추천 모델과 함께 나열한다.
3446
3446
  뒤따르는 *_model 단계의 등장 조건과 1:1 로 맞춰 안내와 실제 화면이 어긋나지
3447
3447
  않게 한다 (그래서 분석에 참여하지 않는 antigravity 는 executor 일 때만 나온다)."""
3448
- rec = recommended_role_models()
3448
+ # 화면의 lead 줄은 뒤따르는 lead-model picker 와 같은 provider 를
3449
+ # 봐야 한다 — picker 는 호스트의 native provider 만 제시한다.
3450
+ rec = recommended_role_models(
3451
+ lead_provider=default_host_registry()
3452
+ .resolve(state.host_runtime)
3453
+ .descriptor.native_provider_id
3454
+ or "",
3455
+ )
3449
3456
  roster = _resolved_roster(state)
3450
3457
  impl = state.task_type == "implementation"
3451
3458