okstra 0.79.0 → 0.80.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.
@@ -0,0 +1,404 @@
1
+ # Host 기반 runtime auto-detection + auto-execution 설계
2
+
3
+ - 날짜: 2026-06-14
4
+ - 상태: 설계 초안
5
+ - 대상: `okstra install`, `okstra doctor`, `okstra run`, lead runtime resolution, runtime manifest
6
+ - 선행 문서:
7
+ - [`2026-06-12-codex-lead-adapter-design.md`](2026-06-12-codex-lead-adapter-design.md)
8
+ - [`2026-06-13-neutral-tmux-lead-adapter-design.md`](2026-06-13-neutral-tmux-lead-adapter-design.md)
9
+
10
+ ## 1. 배경
11
+
12
+ 현재 runtime 선택은 자동 감지가 아니다. `okstra install` 은 기본값 `claude-code` 를 사용하고, `installed-runtimes.json` 의 `codexAdapter` / `externalAdapter` 는 실제 adapter 실행 가능성이 아니라 install 인자 선택값만 반영한다.
13
+
14
+ 확인된 현재 상태:
15
+
16
+ - `installed-runtimes.json` 은 `runtime === "codex" || "all"` 일 때만 `codexAdapter:true`, `runtime === "external" || "all"` 일 때만 `externalAdapter:true` 를 쓴다([runtime-manifest.mjs:8](../../../src/runtime-manifest.mjs:8)).
17
+ - `okstra install` 기본 runtime 은 `claude-code` 다([install.mjs:606](../../../src/install.mjs:606)).
18
+ - `okstra doctor` 기본 runtime 도 `claude-code` 다([doctor.mjs:37](../../../src/doctor.mjs:37)).
19
+ - Python prepare layer 는 `leadRuntime != "claude-code"` 이고 non-render 이면 막는다([run.py:2053](../../../scripts/okstra_ctl/run.py:2053)).
20
+ - `--lead-runtime` 기본값도 `claude-code` 다([run.py:2416](../../../scripts/okstra_ctl/run.py:2416)).
21
+ - `okstra team` 은 `leadRuntime=external` bundle 을 받아 tmux-pane worker dispatch/await/teardown 을 수행하는 Node→Python CLI 경계다([team.mjs:4](../../../src/team.mjs:4)).
22
+
23
+ 사용자 결정:
24
+
25
+ 1. 자동 감지 범위는 **실행까지 자동**이다.
26
+ 2. runtime 우선순위는 **host 기반**이다.
27
+ 3. fallback 은 **safe fallback** 이다. 의도와 다른 runtime 으로 조용히 넘어가지 않는다.
28
+
29
+ ## 2. 문제
30
+
31
+ 현재 이름과 동작이 서로 맞지 않는다.
32
+
33
+ - `codexAdapter:false` / `externalAdapter:false` 는 adapter 미설치 또는 CLI 부재처럼 보이지만, 실제 의미는 "이번 install 선택값이 `claude-code`" 이다.
34
+ - 공유 runtime 은 항상 `~/.okstra/bin` 으로 복사되므로 wrapper 파일 존재와 manifest flag 가 일치하지 않는다.
35
+ - 사용자는 `okstra install` / `okstra run` 이 현재 host 를 이해한다고 기대하지만, 실제로는 `claude-code` 로 고정된다.
36
+ - `external` 실행은 Python non-render gate 때문에 직접 full-run 으로 열 수 없고, Node driver 가 render-only prepare → team dispatch/await 를 orchestration 해야 한다.
37
+
38
+ ## 3. 결정
39
+
40
+ `auto` 를 runtime 값 자체가 아니라 **resolver 입력값**으로 도입한다.
41
+
42
+ ```text
43
+ explicit runtime: claude-code | codex | external | all
44
+ auto request: host/context 를 감지해 위 runtime 중 하나로 resolve
45
+ ```
46
+
47
+ 새 단일 참조점:
48
+
49
+ ```text
50
+ src/runtime-resolver.mjs
51
+ ```
52
+
53
+ 역할:
54
+
55
+ 1. host signal 수집
56
+ 2. 설치 자산과 실행 capability 검사
57
+ 3. command 별 허용 runtime 계산
58
+ 4. safe fallback 적용
59
+ 5. 최종 runtime, 이유, 경고를 구조화해서 반환
60
+
61
+ 예상 API:
62
+
63
+ ```js
64
+ resolveRuntime({
65
+ requestedRuntime: "auto" | "claude-code" | "codex" | "external" | "all",
66
+ command: "install" | "ensure-installed" | "doctor" | "render" | "run" | "dispatch",
67
+ env: process.env,
68
+ paths,
69
+ })
70
+ ```
71
+
72
+ 반환:
73
+
74
+ ```json
75
+ {
76
+ "requestedRuntime": "auto",
77
+ "resolvedRuntime": "external",
78
+ "host": "generic-terminal",
79
+ "confidence": "high",
80
+ "reason": "No Claude Code or Codex host signal; tmux is available.",
81
+ "fallbackFrom": null,
82
+ "warnings": []
83
+ }
84
+ ```
85
+
86
+ 핵심 규칙:
87
+
88
+ - 명시 runtime 은 자동 override 하지 않는다.
89
+ - `auto` 일 때만 감지한다.
90
+ - resolver 결과는 install manifest, run manifest, doctor JSON, console output 에 남긴다.
91
+ - `codexAdapter` / `externalAdapter` 는 새 manifest 에서 쓰지 않는다. v1 manifest read compatibility 에서만 해석한다.
92
+
93
+ ## 4. 용어
94
+
95
+ | 용어 | 의미 |
96
+ |---|---|
97
+ | install runtime | 어떤 asset set 을 설치/보장할지 결정하는 값 |
98
+ | lead runtime | 이번 run 의 lead adapter 정체성: `claude-code`, `codex`, `external` |
99
+ | host | 현재 `okstra` 를 실행 중인 환경: Claude Code session, Codex CLI/session, generic terminal |
100
+ | dispatch backend | worker 를 어떻게 실행하는지: Claude Teams, CLI wrapper, tmux pane |
101
+ | capability | 현재 시점에서 실행 가능한 조건: CLI binary, tmux, installed assets, phase support |
102
+ | safe fallback | 같은 intent 를 보존할 수 있을 때만 fallback 하는 정책 |
103
+
104
+ ## 5. Host 감지
105
+
106
+ Resolver 는 allowlist 기반 signal 만 사용한다. 불명확한 signal 은 runtime 을 결정하지 않고 warning 으로 남긴다.
107
+
108
+ 감지 우선순위:
109
+
110
+ 1. **Explicit runtime**
111
+ - CLI flag 또는 env override 가 `auto` 가 아니면 그대로 사용한다.
112
+ - 예: `--lead-runtime external`, `OKSTRA_LEAD_RUNTIME=codex`.
113
+ 2. **Explicit host override**
114
+ - 새 env `OKSTRA_RUNTIME_HOST=claude-code|codex|external`.
115
+ - 테스트와 wrapper 가 안정적으로 host 를 주입할 수 있는 public contract 다.
116
+ 3. **Claude Code host**
117
+ - Claude Code slash skill 또는 agent contract 가 `OKSTRA_RUNTIME_HOST=claude-code` 를 넘기는 방식이 primary signal 이다.
118
+ - 환경 fingerprint 는 보조 signal 로만 사용한다.
119
+ 4. **Codex host**
120
+ - Codex wrapper/entrypoint 가 `OKSTRA_RUNTIME_HOST=codex` 를 넘기는 방식이 primary signal 이다.
121
+ - `codex` executable 존재만으로 Codex host 라고 판단하지 않는다. binary availability 는 capability 일 뿐 host 가 아니다.
122
+ 5. **External host**
123
+ - 명시 host 가 없고, 일반 terminal 에서 `tmux` 가 사용 가능하면 `external`.
124
+ 6. **Unknown**
125
+ - host 와 capability 가 모두 부족하면 fail-fast.
126
+
127
+ 이 설계는 "binary가 설치되어 있으니 그 runtime으로 실행" 을 피한다. host 기반 결정과 capability 검사를 분리해야 예측 가능하다.
128
+
129
+ ## 6. Safe fallback matrix
130
+
131
+ Fallback 은 `requestedRuntime:auto` 에서만 동작한다. 명시 runtime 은 실패를 사용자에게 보여주고 중단한다.
132
+
133
+ | resolved candidate | 실행 불가 이유 | fallback |
134
+ |---|---|---|
135
+ | `claude-code` | Claude Code host signal 은 있으나 Claude assets 누락 | `ensure-installed --runtime claude-code` 로 보강 후 재시도 |
136
+ | `claude-code` | Claude Code host 가 아닌 것으로 판정 | `external` if tmux 가능, 아니면 fail |
137
+ | `codex` | Codex host signal 은 있으나 Codex CLI 없음 | fail. Claude/external 로 조용히 전환하지 않음 |
138
+ | `codex` | requested roster 에 unsupported worker 포함 | fail with supported worker/report-writer opt-in 안내 |
139
+ | `external` | tmux 없음 | prepare/render-only 까지는 가능, dispatch 는 fail |
140
+ | `external` | `claude` worker 포함, Claude CLI wrapper 실행 불가 | 해당 worker dispatch fail. roster 를 몰래 줄이지 않음 |
141
+ | unknown | host signal 없음 + tmux 없음 | fail with next-command suggestion |
142
+
143
+ Fallback reason 은 run manifest 에 남긴다.
144
+
145
+ ## 7. CLI UX
146
+
147
+ ### 7.1 기본값
148
+
149
+ 다음 command 의 기본 runtime 을 `auto` 로 바꾼다.
150
+
151
+ ```bash
152
+ okstra install
153
+ okstra ensure-installed
154
+ okstra doctor
155
+ okstra render-bundle ...
156
+ okstra run ...
157
+ ```
158
+
159
+ 명시 선택은 유지한다.
160
+
161
+ ```bash
162
+ okstra install --runtime claude-code
163
+ okstra install --runtime codex
164
+ okstra install --runtime external
165
+ okstra install --runtime all
166
+ okstra run --lead-runtime external ...
167
+ ```
168
+
169
+ `all` 은 감지값이 아니다. 모든 runtime asset set 을 명시 설치/검사하는 admin mode 로 유지한다.
170
+
171
+ ### 7.2 Execution front doors
172
+
173
+ Auto execution has two front doors because Claude Code native tools are not available to a plain Node process.
174
+
175
+ | Front door | Host | Responsibility |
176
+ |---|---|---|
177
+ | `/okstra-run` skill | Claude Code | Uses resolver context, keeps the existing Claude Code interactive/team path |
178
+ | `okstra run` Node CLI | Codex / generic terminal | Resolves runtime and orchestrates CLI-backed `codex` or `external` execution |
179
+
180
+ New Node driver `okstra run` is still useful, but it must not pretend it can call Claude Code `TeamCreate` / `Agent(...)` from a terminal. If `okstra run` resolves `claude-code` without being invoked by a Claude Code skill handoff, it fails with an instruction to use `/okstra-run` or pass another explicit runtime.
181
+
182
+ ```text
183
+ okstra run ...
184
+ → resolveRuntime(requestedRuntime=auto by default, command=run)
185
+ → if resolvedRuntime=codex: codex-run → codex-dispatch
186
+ → if resolvedRuntime=external: render-bundle → team dispatch → team await
187
+ → if resolvedRuntime=claude-code outside skill handoff: fail with /okstra-run guidance
188
+ ```
189
+
190
+ `external` 은 Python `prepare_task_bundle()` 의 non-render gate 를 열지 않는다. Node `okstra run` 이 render-only prepare 이후 dispatch/await 를 소유한다.
191
+
192
+ ### 7.3 기존 command 와의 관계
193
+
194
+ - `okstra codex-run`: 유지. Codex lead prompt materialization 의 explicit low-level command.
195
+ - `okstra codex-dispatch`: 유지. Prepared Codex run 의 explicit dispatch command.
196
+ - `okstra team dispatch/await/teardown`: 유지. External/tmux backend 의 explicit low-level command.
197
+ - `okstra render-bundle`: 유지. Preview/debug 목적의 low-level prepare command.
198
+ - `okstra run`: 위 command 들을 host-aware 하게 묶는 high-level command.
199
+
200
+ ## 8. Manifest 변경
201
+
202
+ ### 8.1 installed-runtimes.json v2
203
+
204
+ 현재 v1 의 `codexAdapter` / `externalAdapter` 는 쓰지 않는다. 새 manifest 는 설치 요청과 설치 자산을 분리한다.
205
+
206
+ ```json
207
+ {
208
+ "schemaVersion": 2,
209
+ "installedAt": "2026-06-14T00:00:00.000Z",
210
+ "installRequest": "auto",
211
+ "runtimeResolution": {
212
+ "requestedRuntime": "auto",
213
+ "resolvedRuntime": "external",
214
+ "host": "generic-terminal",
215
+ "reason": "No Claude Code or Codex host signal; tmux is available.",
216
+ "fallbackFrom": null,
217
+ "warnings": []
218
+ },
219
+ "installedRuntimes": ["external"],
220
+ "installedAssets": {
221
+ "sharedRuntime": true,
222
+ "claudeSkills": false,
223
+ "claudeAgents": false,
224
+ "binWrappers": true,
225
+ "templates": true,
226
+ "schemas": true,
227
+ "validators": true
228
+ }
229
+ }
230
+ ```
231
+
232
+ Dynamic capability 는 install manifest 에 고정하지 않는다. `tmux`, `codex`, `claude` CLI availability 는 시간이 지나면 바뀌므로 `doctor` 와 `run` 이 매번 검사한다.
233
+
234
+ ### 8.2 run manifest additions
235
+
236
+ Run manifest 에 runtime request/resolution 을 기록한다.
237
+
238
+ ```json
239
+ {
240
+ "leadRuntimeRequest": "auto",
241
+ "leadRuntime": "external",
242
+ "runtimeResolution": {
243
+ "requestedRuntime": "auto",
244
+ "resolvedRuntime": "external",
245
+ "host": "generic-terminal",
246
+ "reason": "No Claude Code or Codex host signal; tmux is available.",
247
+ "fallbackFrom": null,
248
+ "warnings": []
249
+ }
250
+ }
251
+ ```
252
+
253
+ `leadRuntime` 은 기존 consumer 호환을 위해 최종 resolved runtime 을 유지한다.
254
+
255
+ ## 9. Install / ensure / doctor 동작
256
+
257
+ ### 9.1 install
258
+
259
+ `okstra install` 기본값은 `--runtime auto` 와 같다.
260
+
261
+ - Claude Code host → `claude-code` assets 설치: shared runtime + Claude skills/agents.
262
+ - Codex host → `codex` assets 설치: shared runtime only.
263
+ - Generic terminal + tmux → `external` assets 설치: shared runtime only.
264
+ - `--runtime all` → shared runtime + Claude skills/agents + all installedRuntimes 기록.
265
+
266
+ 공유 runtime 은 모든 install mode 에서 복사한다. 이 때문에 `bin/okstra-codex-exec.sh` 같은 wrapper 존재만으로 `codex` runtime 설치를 의미하지 않는다. v2 manifest 는 이 혼동을 `installedAssets` 와 `installedRuntimes` 로 나눠 제거한다.
267
+
268
+ ### 9.2 ensure-installed
269
+
270
+ `okstra ensure-installed` 기본값도 auto다.
271
+
272
+ - 현재 host 에 필요한 asset 이 없으면 같은 host-resolved runtime 으로 install 을 수행한다.
273
+ - generic terminal 에서 `external` 로 설치된 뒤 Claude Code 에서 `/okstra-run` 이 실행되면, Claude Code host signal 이 `claude-code` 로 resolve 되고 Claude assets 를 보강 설치한다.
274
+ - v1 manifest 는 읽을 수 있어야 한다. v1 에서 `runtime:"claude-code"` 이면 `installedRuntimes:["claude-code"]` 로 해석한다.
275
+
276
+ ### 9.3 doctor
277
+
278
+ `okstra doctor --runtime auto` 는 현재 host 에 필요한 readiness 만 검사한다.
279
+
280
+ `okstra doctor --runtime all` 은 admin check 로 다음을 모두 검사한다.
281
+
282
+ - shared runtime files
283
+ - Claude assets
284
+ - Codex CLI availability
285
+ - external/tmux availability
286
+ - manifest v1/v2 해석
287
+
288
+ JSON output 에 `runtimeResolution` 을 포함한다.
289
+
290
+ ## 10. Execution flow
291
+
292
+ ### 10.1 Claude Code host
293
+
294
+ ```text
295
+ /okstra-run
296
+ → resolver context = claude-code
297
+ → ensure-installed claude-code
298
+ → existing Claude Code interactive/team path
299
+ ```
300
+
301
+ Bare `okstra run` must not claim Claude Code execution unless it is part of a Claude Code skill handoff. A Node process cannot invoke native `TeamCreate` / `Agent(...)` directly.
302
+
303
+ 이 경로는 기존 slash skill UX 를 깨지 않는다.
304
+
305
+ ### 10.2 Codex host
306
+
307
+ ```text
308
+ okstra run ...
309
+ → resolveRuntime = codex
310
+ → ensure-installed codex
311
+ → okstra codex-run ...
312
+ → okstra codex-dispatch ...
313
+ ```
314
+
315
+ Report-writer 는 기존 Codex opt-in 정책을 따른다. 필요한 phase 에서 report-writer 가 required 인데 opt-in 이 없으면 fail-fast 한다. Worker roster 를 몰래 줄이지 않는다.
316
+
317
+ ### 10.3 External/generic terminal host
318
+
319
+ ```text
320
+ okstra run ...
321
+ → resolveRuntime = external
322
+ → ensure-installed external
323
+ → okstra render-bundle ... --lead-runtime external
324
+ → read produced run manifest
325
+ → okstra team dispatch --run-manifest <path>
326
+ → okstra team await --run-manifest <path>
327
+ → post-processing / validation
328
+ ```
329
+
330
+ Python `prepare_task_bundle()` 의 non-render gate 는 유지한다. Full external execution 은 Node driver 가 orchestration 한다.
331
+
332
+ ## 11. Error handling
333
+
334
+ All resolver failures should be actionable.
335
+
336
+ Examples:
337
+
338
+ - `auto resolved codex but codex CLI is unavailable. Install Codex CLI or run with --lead-runtime external.`
339
+ - `auto resolved external but tmux is unavailable. Run render-only or install tmux.`
340
+ - `explicit --lead-runtime codex requested, but worker claude is unsupported by codex-dispatch. Choose supported workers or use claude-code/external.`
341
+ - `host unknown. Set OKSTRA_RUNTIME_HOST=claude-code|codex|external or pass --lead-runtime explicitly.`
342
+
343
+ No fallback may change worker roster, phase, task type, executor, or report-writer role without explicit user input.
344
+
345
+ ## 12. Backward compatibility
346
+
347
+ - Existing explicit flags keep working.
348
+ - Existing `okstra codex-run`, `okstra codex-dispatch`, `okstra team *`, `okstra render-bundle` keep working.
349
+ - v1 `installed-runtimes.json` remains readable.
350
+ - New installs write v2.
351
+ - Existing v1 `codexAdapter` / `externalAdapter` are interpreted only while reading old manifests.
352
+ - `leadRuntime` in run manifest remains final resolved runtime, so validators that already read `leadRuntime` do not need a broad schema break.
353
+
354
+ ## 13. Tests
355
+
356
+ Required test groups:
357
+
358
+ 1. `runtime-resolver` unit tests
359
+ - explicit runtime bypasses auto
360
+ - host override resolves each runtime
361
+ - generic terminal + tmux resolves external
362
+ - binary availability alone does not imply host
363
+ - safe fallback matrix
364
+ 2. install tests
365
+ - default install writes v2 auto manifest
366
+ - Claude host installs Claude assets
367
+ - external host skips Claude assets
368
+ - v1 manifest read compatibility
369
+ 3. doctor tests
370
+ - `--runtime auto` reports runtimeResolution
371
+ - `--runtime all` checks all readiness categories
372
+ 4. run driver tests
373
+ - auto external builds render-only bundle and invokes team dispatch/await in dry-run/fake mode
374
+ - auto codex invokes codex-run/codex-dispatch in dry-run/fake mode
375
+ - bare `okstra run` resolving `claude-code` fails with `/okstra-run` guidance
376
+ - unsupported Codex worker fails without roster shrink
377
+ 5. docs contract tests
378
+ - default runtime is auto
379
+ - host-based priority documented
380
+ - safe fallback documented
381
+ - manifest v2 no longer documents `codexAdapter` / `externalAdapter` as live fields
382
+
383
+ ## 14. Rollout plan
384
+
385
+ Recommended implementation order:
386
+
387
+ 1. Add `runtime-resolver.mjs` and tests.
388
+ 2. Add v2 runtime manifest writer/reader while preserving v1 read path.
389
+ 3. Switch install/ensure/doctor defaults from `claude-code` to `auto`.
390
+ 4. Add `okstra run` high-level driver with dry-run/fake subprocess tests.
391
+ 5. Wire external execution path through render-bundle → team dispatch → team await.
392
+ 6. Wire codex execution path through codex-run → codex-dispatch.
393
+ 7. Update docs and CLI help.
394
+ 8. Keep explicit low-level commands unchanged.
395
+
396
+ ## 15. Non-goals
397
+
398
+ - Do not remove explicit runtime flags.
399
+ - Do not remove Claude Code slash skill UX.
400
+ - Do not make binary availability alone choose a host.
401
+ - Do not open Python `prepare_task_bundle()` non-render for `external` in this change.
402
+ - Do not silently shrink worker roster for fallback.
403
+ - Do not implement non-tmux external dispatch backends.
404
+ - Do not change phase profile/schema/validator contracts by provider fork.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.79.0",
3
+ "version": "0.80.0",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.79.0",
3
- "builtAt": "2026-06-14T09:17:32.918Z",
2
+ "package": "0.80.0",
3
+ "builtAt": "2026-06-14T13:33:02.891Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -91,6 +91,15 @@ def _lead_adapter(ctx: dict) -> dict:
91
91
  return _lead_info(ctx).adapter_payload()
92
92
 
93
93
 
94
+ def _runtime_resolution(ctx: dict) -> dict:
95
+ raw = ctx.get("RUNTIME_RESOLUTION_JSON", "") or "{}"
96
+ try:
97
+ payload = json.loads(raw)
98
+ except Exception:
99
+ return {}
100
+ return payload if isinstance(payload, dict) else {}
101
+
102
+
94
103
  _PHASE_BLOCK_RE = re.compile(
95
104
  r"\{% if header\.taskType == '(implementation-planning|release-handoff|implementation|final-verification)' %\}\n(.*?)\{% endif %\}\n",
96
105
  re.DOTALL,
@@ -1185,6 +1194,8 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
1185
1194
  "taskKey": ctx.get("TASK_KEY", ""),
1186
1195
  "taskType": ctx.get("TASK_TYPE", ""),
1187
1196
  "leadRuntime": _lead_runtime(ctx),
1197
+ "leadRuntimeRequest": ctx.get("LEAD_RUNTIME_REQUEST", "") or _lead_runtime(ctx),
1198
+ "runtimeResolution": _runtime_resolution(ctx),
1188
1199
  "leadAdapter": _lead_adapter(ctx),
1189
1200
  "workCategory": task_manifest.get(
1190
1201
  "workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")
@@ -296,6 +296,8 @@ class PrepareInputs:
296
296
  gemini_model: str = ""
297
297
  report_writer_model: str = ""
298
298
  lead_runtime: str = "claude-code"
299
+ lead_runtime_request: str = ""
300
+ runtime_resolution_json: str = ""
299
301
  executor: str = ""
300
302
  critic: str = ""
301
303
  related_tasks_raw: str = ""
@@ -2055,6 +2057,12 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
2055
2057
  workspace_root = Path(inp.workspace_root)
2056
2058
  project_root = Path(inp.project_root)
2057
2059
  lead_runtime = _normalize_lead_runtime(inp.lead_runtime)
2060
+ lead_runtime_request = (inp.lead_runtime_request or lead_runtime).strip()
2061
+ runtime_resolution_json = (inp.runtime_resolution_json or "{}").strip()
2062
+ try:
2063
+ json.loads(runtime_resolution_json or "{}")
2064
+ except json.JSONDecodeError as exc:
2065
+ raise PrepareError(f"invalid --runtime-resolution-json: {exc}") from exc
2058
2066
  if lead_runtime != "claude-code" and not inp.render_only:
2059
2067
  raise PrepareError(
2060
2068
  f"lead runtime `{lead_runtime}` is currently render-only; "
@@ -2247,6 +2255,8 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
2247
2255
  "ANALYSIS_PROFILE": inp.task_type,
2248
2256
  "RECOMMENDED_ANALYSERS": selected_reviewers,
2249
2257
  "LEAD_RUNTIME": lead_runtime,
2258
+ "LEAD_RUNTIME_REQUEST": lead_runtime_request,
2259
+ "RUNTIME_RESOLUTION_JSON": runtime_resolution_json or "{}",
2250
2260
  "PR_TEMPLATE_PATH": pr_template_path_str,
2251
2261
  "PR_TEMPLATE_SOURCE": pr_template_source,
2252
2262
  **handoff_tokens,
@@ -2424,6 +2434,18 @@ def main(argv: list[str]) -> int:
2424
2434
  "bundles for manifest inspection until a dispatch backend is enabled."
2425
2435
  ),
2426
2436
  )
2437
+ p.add_argument(
2438
+ "--lead-runtime-request",
2439
+ default="",
2440
+ dest="lead_runtime_request",
2441
+ help="Original lead runtime request before Node-side resolution, e.g. auto.",
2442
+ )
2443
+ p.add_argument(
2444
+ "--runtime-resolution-json",
2445
+ default="",
2446
+ dest="runtime_resolution_json",
2447
+ help="Node-side runtime resolution payload serialized as JSON.",
2448
+ )
2427
2449
  p.add_argument("--executor", default="")
2428
2450
  p.add_argument("--critic", default="")
2429
2451
  p.add_argument("--related-tasks", default="", dest="related_tasks_raw")
@@ -2561,6 +2583,8 @@ def main(argv: list[str]) -> int:
2561
2583
  gemini_model=args.gemini_model,
2562
2584
  report_writer_model=args.report_writer_model,
2563
2585
  lead_runtime=args.lead_runtime,
2586
+ lead_runtime_request=args.lead_runtime_request,
2587
+ runtime_resolution_json=args.runtime_resolution_json,
2564
2588
  executor=args.executor,
2565
2589
  critic=args.critic,
2566
2590
  related_tasks_raw=args.related_tasks_raw,
@@ -2591,6 +2615,7 @@ def main(argv: list[str]) -> int:
2591
2615
  print(f"okstra latest task discovery file: {ctx['OKSTRA_LATEST_TASK_FILE']}")
2592
2616
  print(f"okstra task catalog file: {ctx['OKSTRA_TASK_CATALOG_FILE']}")
2593
2617
  print(f"okstra instruction-set: {ctx['INSTRUCTION_SET_PATH']}")
2618
+ print(f"okstra run manifest: {ctx['RUN_MANIFEST_PATH']}")
2594
2619
  print(f"okstra reference expectations: {ctx['REFERENCE_EXPECTATIONS_FILE']}")
2595
2620
  print(f"okstra final report template: {ctx['FINAL_REPORT_TEMPLATE_PATH']}")
2596
2621
  cr = out.extras.get("concurrent_run", {})
@@ -21,7 +21,7 @@ Single read-side entry point for okstra runtime inspection plus the one write-si
21
21
 
22
22
  Before any sub-command, run each of the following commands as a **separate Bash tool call**. Each command starts with the literal token `okstra` so the `Bash(okstra:*)` permission match succeeds. Do **not** wrap any of them in `if`, `eval`, `export`, `$(...)`, `VAR=...`, `||`, or `&&`, and do **not** introduce a `$OKSTRA_CMD` variable or an `npx -y okstra@latest` fallback — those leading tokens defeat the permission match and force a confirmation prompt on every call. The LLM (you) inspects each command's output and decides what to do next in natural language — never in shell.
23
23
 
24
- 1. `okstra ensure-installed`
24
+ 1. `okstra ensure-installed --runtime claude-code`
25
25
  If this exits non-zero, tell the user: "okstra not installed — run `npx okstra@latest install` once, then retry this skill." Then stop. Do **not** try to invoke `npx -y okstra@latest ...` as a fallback.
26
26
 
27
27
  2. `okstra check-project --json`
@@ -55,7 +55,7 @@ Never invent additional questions. Never reorder. **Never drop, hide, or merge a
55
55
 
56
56
  Run each of the following commands as a **separate Bash tool call**. Each command starts with the literal token `okstra` so the `Bash(okstra:*)` permission match succeeds. Do **not** wrap any of them in `if`, `eval`, `export`, `$(...)`, `VAR=...`, `||`, or `&&` — those leading tokens defeat the permission match and force a confirmation prompt on every call. The LLM (you) inspects each command's JSON output and decides what to do next in natural language — never in shell.
57
57
 
58
- 1. `okstra ensure-installed`
58
+ 1. `okstra ensure-installed --runtime claude-code`
59
59
  If this exits non-zero, tell the user: "okstra runtime missing — run `npx okstra@latest install` once and retry." Then stop.
60
60
 
61
61
  2. `okstra paths --json`
@@ -155,8 +155,11 @@ Output: `{ok: true, args: {"project-root": "...", "task-type": "...", ...}}`. Bu
155
155
 
156
156
  **Escaping rule**: inside a double-quoted value, escape any literal `"` as `\"`. Do not collapse `--key ""` into `--key` even when the value is empty.
157
157
 
158
+ The okstra-run skill is the Claude Code execution front door, so it marks the host explicitly with `--runtime claude-code` / `--lead-runtime claude-code`; bare `okstra run` cannot invoke Claude Code `TeamCreate` / `Agent(...)` from a terminal.
159
+
158
160
  ```bash
159
161
  okstra render-bundle \
162
+ --lead-runtime claude-code \
160
163
  --project-root "<args.project-root>" \
161
164
  --project-id "<args.project-id>" \
162
165
  --task-group "<args.task-group>" \
@@ -39,7 +39,7 @@ If `--title` is omitted, derive a default title from `task-group` (e.g. `uploadF
39
39
 
40
40
  Before anything else in this skill, run each of the following commands as a **separate Bash tool call**. Each command starts with the literal token `okstra` so the `Bash(okstra:*)` permission match succeeds. Do **not** wrap any of them in `if`, `eval`, `export`, `$(...)`, `VAR=...`, `||`, or `&&`, and do **not** introduce a `$OKSTRA_CMD` variable or an `npx -y okstra@latest` fallback — those leading tokens defeat the permission match and force a confirmation prompt on every call. The LLM (you) inspects each command's output and decides what to do next in natural language — never in shell.
41
41
 
42
- 1. `okstra ensure-installed`
42
+ 1. `okstra ensure-installed --runtime claude-code`
43
43
  If this exits non-zero, tell the user: "okstra not installed — run `npx okstra@latest install` once, then retry this skill." Then stop. Do **not** try to invoke `npx -y okstra@latest ...` as a fallback.
44
44
 
45
45
  2. `okstra check-project --json`
@@ -34,7 +34,7 @@ Inform the user up-front and confirm before continuing:
34
34
  ## Step 1: Install okstra
35
35
 
36
36
  ```bash
37
- npx -y okstra@latest install
37
+ npx -y okstra@latest install --runtime claude-code
38
38
  ```
39
39
 
40
40
  This single command populates everything the user needs:
@@ -316,7 +316,7 @@ If the user picks 1/2/3, run:
316
316
  ## Step 5: Verify
317
317
 
318
318
  ```bash
319
- okstra doctor
319
+ okstra doctor --runtime claude-code
320
320
  ```
321
321
 
322
322
  If all checks return `OK`, the setup is complete. If any check fails, surface
@@ -127,6 +127,13 @@ export const COMMAND_REGISTRY = [
127
127
  "python3 -m okstra_ctl.run --render-only)",
128
128
  ],
129
129
  },
130
+ {
131
+ name: "run",
132
+ module: "./run.mjs",
133
+ export: "run",
134
+ category: "introspection",
135
+ summary: ["Host-aware auto execution front door"],
136
+ },
130
137
  {
131
138
  name: "codex-run",
132
139
  module: "./codex-run.mjs",
@@ -232,7 +239,7 @@ This CLI is the installer/admin tool. Day-to-day usage happens inside a
232
239
  Claude Code session via slash commands (/okstra-setup, /okstra-run, ...).
233
240
 
234
241
  Quick start (CLI):
235
- 1. npx -y okstra@latest install # one-time, this machine
242
+ 1. npx -y okstra@latest install --runtime claude-code # one-time, this machine
236
243
  2. cd <your project>
237
244
  3. npx -y okstra@latest setup --project-id <id> # one-time, this project
238
245
  4. open a Claude Code session and run /okstra-run # start a task
package/src/doctor.mjs CHANGED
@@ -5,6 +5,7 @@ import { resolvePaths } from "./paths.mjs";
5
5
  import { fileExists, runProcess } from "./_proc.mjs";
6
6
  import { runPythonSnippet } from "./_python-helper.mjs";
7
7
  import { runtimeIncludesClaudeAssets, validateInstallRuntime } from "./install.mjs";
8
+ import { resolveRuntime } from "./runtime-resolver.mjs";
8
9
 
9
10
  const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
10
11
  const REQUIRED_SKILLS = ["okstra-setup", "okstra-run"];
@@ -38,7 +39,7 @@ export function parseDoctorArgs(args) {
38
39
  const result = {
39
40
  jsonMode: false,
40
41
  phase: null,
41
- runtime: "claude-code",
42
+ runtime: "auto",
42
43
  };
43
44
  for (let i = 0; i < args.length; i++) {
44
45
  const arg = args[i];
@@ -182,6 +183,17 @@ export async function run(args) {
182
183
  return 2;
183
184
  }
184
185
  const paths = await resolvePaths();
186
+ const runtimeResolution = resolveRuntime({
187
+ requestedRuntime: opts.runtime,
188
+ command: "doctor",
189
+ env: process.env,
190
+ capabilities: { tmuxAvailable: Boolean(process.env.TMUX) },
191
+ });
192
+ if (!runtimeResolution.ok) {
193
+ process.stderr.write(`error: ${runtimeResolution.reason}\n`);
194
+ return 2;
195
+ }
196
+ const resolvedRuntime = runtimeResolution.resolvedRuntime;
185
197
 
186
198
  const results = [
187
199
  await check("python3", checkPython3),
@@ -197,7 +209,7 @@ export async function run(args) {
197
209
  await check("bin: okstra-gemini-exec.sh", () => checkBashEntry(paths.bin, "okstra-gemini-exec.sh")),
198
210
  await check("bin: okstra-central.sh", () => checkBashEntry(paths.bin, "okstra-central.sh")),
199
211
  ...(await Promise.all(
200
- requiredSkillNamesForRuntime(opts.runtime).map((name) =>
212
+ requiredSkillNamesForRuntime(resolvedRuntime).map((name) =>
201
213
  check(`skill: ${name}`, async () => {
202
214
  const p = join(CLAUDE_SKILLS_DIR, name, "SKILL.md");
203
215
  return (await fileExists(p))
@@ -229,7 +241,9 @@ export async function run(args) {
229
241
  process.stdout.write(
230
242
  `${JSON.stringify({
231
243
  ok: allOk,
232
- runtime: opts.runtime,
244
+ runtimeRequest: opts.runtime,
245
+ runtime: resolvedRuntime,
246
+ runtimeResolution,
233
247
  phase: opts.phase,
234
248
  paths,
235
249
  phaseDiagnostics,