okstra 0.163.2 → 0.164.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.
Files changed (112) hide show
  1. package/README.md +7 -5
  2. package/docs/architecture.md +12 -7
  3. package/docs/cli.md +8 -5
  4. package/docs/project-structure-overview.md +7 -5
  5. package/package.json +1 -1
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/agents/workers/claude-worker.md +1 -0
  8. package/runtime/bin/lib/okstra/cli.sh +5 -0
  9. package/runtime/bin/lib/okstra/globals.sh +2 -0
  10. package/runtime/bin/lib/okstra/usage.sh +5 -5
  11. package/runtime/bin/okstra.sh +87 -91
  12. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  13. package/runtime/prompts/lead/okstra-lead-contract.md +1 -0
  14. package/runtime/prompts/profiles/implementation-planning.md +1 -1
  15. package/runtime/python/okstra_ctl/adapters/accounting/__init__.py +11 -0
  16. package/runtime/python/okstra_ctl/adapters/accounting/claude_jsonl.py +17 -0
  17. package/runtime/python/okstra_ctl/adapters/accounting/cli_artifact.py +17 -0
  18. package/runtime/python/okstra_ctl/adapters/accounting/unavailable.py +19 -0
  19. package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +92 -0
  20. package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +54 -0
  21. package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +68 -0
  22. package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
  23. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +60 -0
  24. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/manifest.json +1 -0
  25. package/runtime/{prompts/lead/adapters/antigravity.md → python/okstra_ctl/adapters/hosts/antigravity/relay.md} +52 -0
  26. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +292 -0
  27. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +120 -0
  28. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -0
  29. package/runtime/{prompts/lead/adapters/claude-code.md → python/okstra_ctl/adapters/hosts/claude-code/relay.md} +111 -0
  30. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +60 -0
  31. package/runtime/python/okstra_ctl/adapters/hosts/codex/manifest.json +1 -0
  32. package/runtime/{prompts/lead/adapters/codex.md → python/okstra_ctl/adapters/hosts/codex/relay.md} +52 -0
  33. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +72 -0
  34. package/runtime/python/okstra_ctl/adapters/hosts/external/manifest.json +1 -0
  35. package/runtime/{prompts/lead/adapters/external.md → python/okstra_ctl/adapters/hosts/external/relay.md} +52 -0
  36. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +63 -0
  37. package/runtime/python/okstra_ctl/adapters/hosts/grok/manifest.json +1 -0
  38. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +90 -0
  39. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +63 -0
  40. package/runtime/python/okstra_ctl/adapters/hosts/kimi/manifest.json +1 -0
  41. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +90 -0
  42. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +35 -0
  43. package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
  44. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +55 -0
  45. package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
  46. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +43 -0
  47. package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
  48. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +32 -0
  49. package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
  50. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +38 -0
  51. package/runtime/python/okstra_ctl/adapters/providers/kimi/manifest.json +1 -0
  52. package/runtime/python/okstra_ctl/application/__init__.py +1 -0
  53. package/runtime/python/okstra_ctl/application/advance_wizard.py +25 -0
  54. package/runtime/python/okstra_ctl/application/collect_usage.py +15 -0
  55. package/runtime/python/okstra_ctl/application/dispatch_assignments.py +15 -0
  56. package/runtime/python/okstra_ctl/application/resolve_assignment.py +93 -0
  57. package/runtime/python/okstra_ctl/application/resume_run.py +21 -0
  58. package/runtime/python/okstra_ctl/application/start_run.py +21 -0
  59. package/runtime/python/okstra_ctl/codex_dispatch.py +49 -826
  60. package/runtime/python/okstra_ctl/dispatch_core.py +240 -29
  61. package/runtime/python/okstra_ctl/dispatch_state.py +17 -0
  62. package/runtime/python/okstra_ctl/domain/__init__.py +34 -0
  63. package/runtime/python/okstra_ctl/domain/host.py +100 -0
  64. package/runtime/python/okstra_ctl/domain/provider.py +66 -0
  65. package/runtime/python/okstra_ctl/domain/wizard/__init__.py +19 -0
  66. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +140 -0
  67. package/runtime/python/okstra_ctl/entrypoints/__init__.py +1 -0
  68. package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
  69. package/runtime/python/okstra_ctl/models.py +54 -269
  70. package/runtime/python/okstra_ctl/ports/__init__.py +15 -0
  71. package/runtime/python/okstra_ctl/ports/host.py +32 -0
  72. package/runtime/python/okstra_ctl/ports/interaction.py +15 -0
  73. package/runtime/python/okstra_ctl/ports/lead_session.py +25 -0
  74. package/runtime/python/okstra_ctl/ports/usage_accounting.py +23 -0
  75. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +32 -0
  76. package/runtime/python/okstra_ctl/registry/__init__.py +13 -0
  77. package/runtime/python/okstra_ctl/registry/factory_loader.py +32 -0
  78. package/runtime/python/okstra_ctl/registry/host_discovery.py +124 -0
  79. package/runtime/python/okstra_ctl/registry/host_registry.py +365 -0
  80. package/runtime/python/okstra_ctl/registry/provider_registry.py +149 -0
  81. package/runtime/python/okstra_ctl/render.py +145 -47
  82. package/runtime/python/okstra_ctl/run.py +80 -58
  83. package/runtime/python/okstra_ctl/session.py +1 -1
  84. package/runtime/python/okstra_ctl/team.py +44 -32
  85. package/runtime/python/okstra_ctl/wizard.py +162 -64
  86. package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
  87. package/runtime/python/okstra_token_usage/collect.py +34 -6
  88. package/runtime/schemas/final-report-v1.0.schema.json +3989 -1085
  89. package/runtime/schemas/final-report-v2.0.schema.json +5622 -1451
  90. package/runtime/skills/okstra-run/SKILL.md +73 -28
  91. package/runtime/templates/implementation-worker-preamble.md +1 -1
  92. package/runtime/templates/reports/final-report.template.md +3 -3
  93. package/runtime/templates/reports/html/i18n/en.json +3 -1
  94. package/runtime/templates/reports/html/i18n/ko.json +3 -1
  95. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +1 -1
  96. package/runtime/templates/worker-prompt-preamble.md +1 -1
  97. package/runtime/validators/validate-implementation-plan-stages.py +17 -22
  98. package/runtime/validators/validate-report-views.py +0 -39
  99. package/runtime/validators/validate-run.py +8 -7
  100. package/runtime/validators/validate_session_conformance.py +7 -2
  101. package/src/commands/execute/render-bundle.mjs +4 -4
  102. package/src/commands/execute/run.mjs +8 -25
  103. package/src/commands/execute/wizard.mjs +33 -13
  104. package/src/commands/lifecycle/doctor.mjs +10 -10
  105. package/src/commands/lifecycle/install.mjs +53 -30
  106. package/src/commands/lifecycle/preflight.mjs +14 -4
  107. package/src/lib/host-registry-client.mjs +176 -0
  108. package/src/lib/runtime-manifest.mjs +6 -8
  109. package/runtime/python/okstra_ctl/lead_runtime.py +0 -115
  110. package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
  111. package/src/lib/runtime-readiness.mjs +0 -90
  112. package/src/lib/runtime-resolver.mjs +0 -123
@@ -41,28 +41,29 @@ Every non-terminal `next` carries a `progress` object (`done` / `aborted` omit i
41
41
 
42
42
  On `ok: false`, re-prompt with the same `current.step` using the error message. The wizard never advances on validation failure; the user retries the same step. **`current` may be `null`** when the current step itself cannot render (e.g. the approved plan's Stage Map is corrupt) — that case is terminal: show `error` and stop, the user must fix the plan file before retrying. Never re-prompt off a `null` `current`.
43
43
 
44
- The wizard tells you *which UI to use* via `kind` (and the optional `multi` flag on `pick`):
45
-
46
- - `kind: "pick"` + `presentation: "numbered-text"` → do not call `AskUserQuestion`. Render every option as a numbered Markdown list in its original order, then consume the user's next message as the answer. Submit that message unchanged with `--answer`; the wizard accepts a 1-based number, an exact option value, or an exact option label and resolves it to the canonical value. An invalid, out-of-range, or ambiguous answer returns `ok: false` and must re-prompt the same complete list.
47
- - `kind: "pick"` + `multi: false` (default) → render `AskUserQuestion` with `label`, `options[].label`, and `multiSelect: false`. Use the chosen `options[].value` (single string) as the answer.
48
- - `kind: "pick"` + `multi: true` → render `AskUserQuestion` with `label`, `options[].label`, and `multiSelect: true`. Join the chosen `options[].value` entries with `,` into a single CSV string and submit that as `--answer "csv,values"`. If the user selects nothing, still submit `--answer ""` — the wizard will reply `ok: false` and re-prompt the same step (do not skip the call).
49
- - `kind: "pick_group"` → render a SINGLE `AskUserQuestion` whose questions array maps 1:1 to the wizard's `questions[]`. For each entry use `questions[].label`, `questions[].options[].label`, and `multiSelect: questions[].multi`. Collect the user's chosen `options[].value` per tab, build a JSON object keyed by each `questions[].step`, and submit it as a single literal `--answer '{"lead_model":"opus","claude_model":"default",...}'`. A tab the user leaves at its default still gets its `"default"`/`""` value in the JSON. Never split a `pick_group` into multiple `AskUserQuestion` calls — the wizard already capped it at 4 tabs and emits any remainder as the next prompt.
50
- - `kind: "text"` → write `label` as a plain text message and consume the user's NEXT message as the answer.
44
+ The wizard tells you which relay operation to use via `next.interaction.kind`. Step 1 loads the current registered host's relay contract. Use the matching entry under its `interactions` object exactly; if the entry is absent, stop instead of inventing a host function or falling back silently. The only exception is the explicit `Legacy text relay compatibility` mapping selected in Step 1 when the preflight response has no `relayContract`.
45
+
46
+ - `native-single` → use the current registered host's relay contract for its native single-select function. Pass every option in its original order and submit the selected `options[].value`.
47
+ - `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the selected values as one CSV answer; submit an empty selection as `--answer ""`.
48
+ - `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`; when a grouped question is multi-select, its step value is the selected option values joined as one CSV string, not a JSON array.
49
+ - `numbered-single` → render every option as a 1-based numbered Markdown list and submit the user's next message unchanged.
50
+ - `numbered-multi` → render every option as a 1-based numbered Markdown list and submit the user's comma-separated reply unchanged.
51
+ - `sequential-group` → render every question in order, preserve each complete option list, collect every answer, and submit one JSON object keyed by `questions[].step`.
52
+ - `plain-text` → write `label` as plain text and submit the user's next message unchanged.
51
53
  - `kind: "done"` → input collection finished; move to Step 5.
52
54
  - `kind: "aborted"` → the user picked abort; the wizard is terminally cancelled. Tell the user on one short line that the run setup was aborted, delete the state file (`rm` with the literal path), and stop this skill — do NOT call `render-args` or `render-bundle` (the wizard rejects `render-args` on an aborted state).
53
55
 
56
+ Submit the answer shape required by `interaction.answerProtocol`; do not add normalization that the protocol does not request. Invalid, out-of-range, or ambiguous answers return `ok: false` and must re-render the same complete interaction.
57
+
54
58
  The final `confirm` step is a normal `pick` step with three options — `Proceed` / `Edit` / `Abort`(abort) — and is rendered the same way (no special handling). `Edit` rewinds to any earlier step (including `base-ref`); `Abort` terminally cancels the wizard. The branch/worktree decision the run will actually use (for `implementation`, the **stage worktree** — not the task-key directory) is folded into the Step 4 confirmation summary block as a `worktree` line, so there is no separate branch-confirm prompt.
55
59
 
56
- Never invent additional questions. Never reorder. **Never drop, hide, or merge a `pick` / `pick_group` option** — render every `options[]` entry, including entries that carry a `(default)` / `(recommended)` suffix. Use an `AskUserQuestion` choice unless the wizard explicitly sets `presentation: "numbered-text"`; that presentation preserves overflow choices as a numbered Markdown list. Do NOT collapse a multi-option pick into a "recommended + Enter directly / Other" shortlist: the wizard's `options[]` array IS the complete, authoritative choice set. Example: if a pick's `options[]` carries N entries, render all N — never abbreviate a multi-option step down to one recommended value. The run-prompt recommendation rule (1–2 recommendations + Enter directly) applies ONLY to prompts this skill authors itself (e.g. the conformance-waiver picker), never to wizard-provided `options[]`. Never use `AskUserQuestion` for `text` prompts — the wizard explicitly chose `text` to avoid the picker-Other re-render lag.
60
+ Never invent additional questions. **Never drop, hide, merge, reorder, or truncate** a `pick` / `pick_group` option — relay every `options[]` entry, including entries that carry a `(default)` / `(recommended)` suffix. Do not collapse a multi-option pick into a "recommended + Enter directly / Other" shortlist. The wizard's arrays are the complete authoritative choice sets, regardless of the current host UI's usual option limit. The run-prompt recommendation rule (1–2 recommendations + Enter directly) applies only to prompts this skill authors itself, never to wizard-provided options.
57
61
 
58
62
  ## Step 1: Preflight
59
63
 
60
- Resolve `<host-runtime>` from the host that is executing this skill. This is a harness capability declaration, never a `which`/`PATH` inference and never a worker-provider choice:
64
+ Use the launcher's `OKSTRA_RUNTIME_HOST` value when it is present. Otherwise use the registered host ID that the current harness declares for this session. This is a harness capability declaration, never a worker-provider choice. Do not infer it from an executable or `PATH`, and do not silently substitute another host.
61
65
 
62
- - Claude Code host → `claude-code`
63
- - Codex host → `codex`
64
- - Antigravity CLI host → `antigravity`
65
- - other host → `external`
66
+ Record the semantic functions that the live harness can actually perform. Always include `plain_text_input`. Record `native_single_select`, `native_multi_select`, or `native_question_group` only when the current harness exposes that function. Do not infer functions from the host ID; the effective declaration is computed from the relay contract after preflight.
66
67
 
67
68
  The host-native provider runs as the current lead session when its role is supported. Other selected providers run through their registered CLI wrappers. Claude remains only the model-policy default; it is not the process owner.
68
69
 
@@ -72,15 +73,61 @@ Run one Bash tool call (Bash invocation rule from the top of this file applies):
72
73
  okstra preflight --runtime <host-runtime> --json
73
74
  ```
74
75
 
75
- Parse the stdout JSON. `ok: false` (or `okstra` not on `PATH` at all) → tell the user: "okstra not set up — run `/okstra-setup` first." Then stop. If the call fails with `unknown command: preflight`, the `okstra` binary on PATH predates this skill — tell the user to update it (`npm i -g okstra@latest`), then stop (`/okstra-setup` does not update the binary). Do **not** try to invoke `npx -y okstra@latest ...` as a fallback — `npx` is not on the literal-token allow-list and will force a confirmation prompt on every wizard call afterward. Every subsequent `okstra <subcmd>` call self-bootstraps its Python path, so never `export PYTHONPATH=...`.
76
+ Parse the stdout JSON. If `ok: false` with `stage: runtime_readiness`, show `runtimeReadiness.status` and every readiness check, including each adapter-provided `action`, then stop before Step 2. Do not relabel a host login, license, daemon, trust, or other readiness failure as missing Okstra setup. Any other `ok: false` response (or `okstra` not on `PATH` at all) → tell the user: "okstra not set up — run `/okstra-setup` first." Then stop. If the call fails with `unknown command: preflight`, the `okstra` binary on PATH predates this skill — tell the user to update it (`npm i -g okstra@latest`), then stop (`/okstra-setup` does not update the binary). Do **not** try to invoke `npx -y okstra@latest ...` as a fallback — `npx` is not on the literal-token allow-list and will force a confirmation prompt on every wizard call afterward. Every subsequent `okstra <subcmd>` call self-bootstraps its Python path, so never `export PYTHONPATH=...`.
77
+
78
+ When `runtimeReadiness` is present on an `ok: true` response, require `runtimeReadiness.status: ready` before Step 2. These checks come from the selected host adapter and are independent of worker-provider choices. A `blocked` or `unknown` status is a host readiness failure regardless of check IDs: show the status and checks, then stop before Step 2. Any other readiness status is unrecognized runtime output: show it and stop before Step 2 instead of guessing that the host is ready.
79
+
80
+ For a ready response, read the absolute file path in `runtimeReadiness.relayContract` with the current host's file-read primitive. Do not derive the path from the host ID or search `PATH`. In that file, find the `Wizard interaction relay` JSON block, require `schemaVersion: 1` and `runtime` equal to `runtimeReadiness.runtime`, then take its `semanticFunctions` allowlist and intersect it with the functions the live harness exposes. Pass only that intersection to Step 2; `plain_text_input` must be present. Keep the parsed `interactions` object for Step 3's function/input/response conversion. An unreadable file, malformed block, runtime mismatch, absent `plain_text_input`, or later interaction kind missing from the object is a host relay contract failure: show the problem and stop rather than guessing.
81
+
82
+ If `runtimeReadiness` is absent, preserve compatibility with an older preflight response: carry `projectRoot` and `projectId` into Step 2 and treat it as a response with no `relayContract`. If readiness exists but `relayContract` is absent, also enter the compatibility branch below. In that branch only, declare `plain_text_input` and keep its built-in `interactions` mapping for Step 3; do not assume a native tool from the host ID. The existing `unknown command: preflight` branch remains the authoritative stale-CLI failure.
76
83
 
77
- On `ok: true`, inspect `runtimeReadiness.checks` for the check whose `id` is `workspace-trust`. This check is host-specific and independent of the selected worker providers.
84
+ ### Legacy text relay compatibility
85
+
86
+ Use this built-in mapping only when the successful preflight response omitted `relayContract`. It is not a fallback for an unreadable, malformed, mismatched, or incomplete relay contract. If a present relay contract omits the wizard's interaction kind, keep the fail-closed rule above and stop.
87
+
88
+ ```json
89
+ {
90
+ "scope": "preflight-response-without-relayContract-only",
91
+ "semanticFunctions": ["plain_text_input"],
92
+ "interactions": {
93
+ "numbered-single": {
94
+ "function": "host-text",
95
+ "input": {
96
+ "question": "label-with-progress",
97
+ "options": "all-in-original-order-as-numbered-markdown-label-and-description"
98
+ },
99
+ "response": { "source": "next-message", "submit": "raw" }
100
+ },
101
+ "numbered-multi": {
102
+ "function": "host-text",
103
+ "input": {
104
+ "question": "label-with-progress",
105
+ "options": "all-in-original-order-as-numbered-markdown-label-and-description"
106
+ },
107
+ "response": { "source": "next-message", "submit": "raw" }
108
+ },
109
+ "sequential-group": {
110
+ "function": "host-text",
111
+ "input": {
112
+ "questions": "all-one-at-a-time-in-original-order",
113
+ "question": "label-with-progress",
114
+ "options": "all-in-original-order-as-numbered-markdown-label-and-description"
115
+ },
116
+ "response": {
117
+ "source": "next-message-by-question-position",
118
+ "submit": "compact-step-json-raw"
119
+ }
120
+ },
121
+ "plain-text": {
122
+ "function": "host-text",
123
+ "input": { "question": "label-with-progress" },
124
+ "response": { "source": "next-message", "submit": "raw" }
125
+ }
126
+ }
127
+ }
128
+ ```
78
129
 
79
- - On `claude-code`, `status: required` with `action: restart-and-trust` → tell the user: "Claude Code must trust `<projectRoot>` before Okstra can dispatch workers. Close this session, reopen that project in Claude Code, choose `Yes, I trust this folder`, then run `/okstra-run` again." Then stop before Step 2. Never start the wizard or dispatch a worker from this session.
80
- - On `claude-code`, `status: unavailable` → tell the user that Claude Code workspace trust could not be verified, ask them to open `<projectRoot>` directly in Claude Code and complete any trust prompt, then run `/okstra-run` again. Then stop before Step 2.
81
- - `status: accepted` or `status: not-applicable` → carry `projectRoot` and `projectId` as literal strings into Step 2.
82
- - If `runtimeReadiness` is absent, preserve compatibility with an older preflight response: carry `projectRoot` and `projectId` into Step 2. The existing `unknown command: preflight` branch remains the authoritative stale-CLI failure.
83
- - Any other `workspace-trust` status is unrecognized runtime output: show the status and stop before Step 2 instead of guessing that trust exists.
130
+ For numbered interactions, render every option as a 1-based Markdown item containing its label followed by its description verbatim, in original order. For `sequential-group`, ask every question in order and build one compact JSON object keyed by `questions[].step`. Submit each user message unchanged; the wizard owns numbered and grouped normalization.
84
131
 
85
132
  ## Step 2: Initialize the wizard
86
133
 
@@ -99,7 +146,10 @@ okstra wizard init \
99
146
  --state-file /var/folders/.../okstra-wizard.AbCd.json \
100
147
  --project-root /abs/path/to/project \
101
148
  --project-id my-project-id \
102
- --host-runtime <host-runtime>
149
+ --host-runtime <host-runtime> \
150
+ --entry-mode current-session \
151
+ --available-function plain_text_input \
152
+ --available-function <each-additional-function-in-the-effective-intersection>
103
153
  ```
104
154
 
105
155
  Output: the same `{ok, next}` JSON described above. The first `next` is always `step: "task_pick"`.
@@ -108,12 +158,7 @@ Output: the same `{ok, next}` JSON described above. The first `next` is always `
108
158
 
109
159
  Repeat until `next.kind == "done"` (or `"aborted"` — terminal cancel, see "How the wizard talks to you"):
110
160
 
111
- 1. **Render** the prompt according to `kind` (and `multi` for pick). **Always append the progress marker to the rendered question label** (the `AskUserQuestion` question text, or the `text`-prompt message): suffix it with ` (<next.progress.label>)` — render `progress.label` exactly as the wizard sent it, never recompute it. Example: label `Step 8/11 · 3 steps remaining` → `Select a model (Step 8/11 · 3 steps remaining)`. Re-prompts after `ok: false` reuse `current.progress.label` the same way. The progress marker is presentation-only — never send it back to the wizard as part of an answer.
112
- - `pick` + `presentation: "numbered-text"` → plain text containing `label`, followed by every option as a numbered Markdown list. Consume the user's next message verbatim as the answer string.
113
- - `pick` + `multi: false` → `AskUserQuestion` with `multiSelect: false`, `label`, and `options`. The user's chosen option's `value` is the answer string.
114
- - `pick` + `multi: true` → `AskUserQuestion` with `multiSelect: true`, `label`, and `options`. Join the selected `value`s with `,` into a single literal CSV string (e.g. `"claude,codex,grok,kimi"`) and submit it as one `--answer` value. Empty selection submits `--answer ""` and the wizard re-prompts.
115
- - `pick_group` → one `AskUserQuestion` with one question per `questions[]` entry (tab). Map each tab's selected `value` back by `questions[].step`, assemble a JSON object, and submit it as a single literal `--answer '<json>'`.
116
- - `text` → plain text message containing `label`. Consume the user's next reply verbatim as the answer string (empty reply = empty string).
161
+ 1. **Render** the prompt according to `next.interaction.kind` using the relay rules above. **Always append the progress marker to the rendered question label**: suffix it with ` (<next.progress.label>)` — render `progress.label` exactly as the wizard sent it, never recompute it. Example: label `Step 8/11 · 3 steps remaining` → `Select a model (Step 8/11 · 3 steps remaining)`. Re-prompts after `ok: false` reuse `current.progress.label` the same way. The progress marker is presentation-only — never send it back to the wizard as part of an answer.
117
162
  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 `$(...)`):
118
163
  ```bash
119
164
  okstra wizard step --state-file /var/folders/.../okstra-wizard.AbCd.json --answer preprod
@@ -217,7 +262,7 @@ okstra render-bundle \
217
262
 
218
263
  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`.
219
264
 
220
- 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.
265
+ You can delete the literal state-file path after this point — its job is done. Invoke `command rm` with the literal path (e.g. `command rm /var/folders/.../okstra-wizard.AbCd.json`), not a shell variable. `command` is what keeps a `rm='rm -i'` alias from turning this into a confirmation prompt nobody is there to answer.
221
266
 
222
267
  <!-- BEGIN FRAGMENT: host-orchestration-implementation -->
223
268
  ## Host orchestration rules — implementation
@@ -31,7 +31,7 @@ When `**Evidence ledger:** required-v1` is present, append one canonical row to
31
31
 
32
32
  - Evidence read: `<project-relative path without a line suffix>`
33
33
 
34
- Every file citation in the result MUST use backticks and a line suffix, for example `src/config/env.ts:1-22`. A cited path without a matching ledger row fails Phase 7 in `validators/validate-run.py` `validate_worker_results_audit()`. Do not add a row for a file you did not open.
34
+ Every file citation in the result MUST use backticks, a line suffix, and the same project-relative path its ledger row carries — for example `src/config/env.ts:1-22`. A bare filename (`env.ts:1-22`) does not match its row and fails exactly like a file you never opened, however many times you cited the full path earlier. A cited path without a matching ledger row fails Phase 7 in `validators/validate-run.py` `validate_worker_results_audit()`. Do not add a row for a file you did not open.
35
35
 
36
36
  ## Anchor headers
37
37
 
@@ -380,10 +380,10 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
380
380
 
381
381
  ### Stepwise Execution Order
382
382
 
383
- | step | action | files | command | expected |
384
- |------|--------|-------|---------|----------|
383
+ | step | action | files | command | outcome | expected |
384
+ |------|--------|-------|---------|---------|----------|
385
385
  {% for row in stage.stepwiseExecution -%}
386
- | {{ row.step | mdcell }} | {{ row.action | mdcell }} | `{{ row.files | mdcell }}` | `{{ row.command | mdcell }}` | {{ row.expected | mdcell }} |
386
+ | {{ row.step | mdcell }} | {{ row.action | mdcell }} | `{{ row.files | mdcell }}` | `{{ row.command | mdcell }}` | {{ row.outcome | mdcell }} | {{ row.expectedDetail | mdcell }} |
387
387
  {% endfor %}
388
388
 
389
389
  ### Stage Exit Contract
@@ -297,7 +297,9 @@
297
297
  "topic": "Topic",
298
298
  "why": "Why",
299
299
  "open-decisions-before-approval": "Open decisions before approval",
300
- "no-open-decision-is-holding-approval": "No open decision is holding approval."
300
+ "no-open-decision-is-holding-approval": "No open decision is holding approval.",
301
+ "uniform-verifier-label": "Uniform verifier",
302
+ "uniform-verifier-legend": "This verifier answered the same verdict to every item it voted on, so this round's refutation signal came from its peers alone — the gate above reads as a wider cross-check than it was. A unanimous round is a legitimate outcome; confirm from the worker's audit sidecar that it actually opened the cited evidence."
301
303
  },
302
304
  "implementation": {
303
305
  "what-was-delivered": "What was delivered",
@@ -297,7 +297,9 @@
297
297
  "topic": "주제",
298
298
  "why": "이유",
299
299
  "open-decisions-before-approval": "승인 전에 남은 결정",
300
- "no-open-decision-is-holding-approval": "승인을 막는 미결 결정이 없습니다."
300
+ "no-open-decision-is-holding-approval": "승인을 막는 미결 결정이 없습니다.",
301
+ "uniform-verifier-label": "단일 판정 검증자",
302
+ "uniform-verifier-legend": "이 검증자는 자기가 투표한 모든 항목에 같은 판정을 냈습니다. 그래서 이번 라운드의 반증 신호는 나머지 검증자에게서만 나왔고, 위의 게이트 값은 실제보다 넓은 교차 검증처럼 읽힙니다. 만장일치도 정당한 결과이므로, 그 워커의 audit 사이드카에서 인용한 근거를 실제로 열어봤는지 확인하십시오."
301
303
  },
302
304
  "implementation": {
303
305
  "what-was-delivered": "무엇을 만들었나",
@@ -69,7 +69,7 @@
69
69
  <h2>{{ t('tasks.implementation-planning.plan-body-verification') }}</h2>
70
70
  <p><strong>{{ t('tasks.implementation-planning.verdict') }}</strong> — {{ planning.planBodyVerification.gateResult | inline_code }} ({{ planning.planBodyVerification.roundCount }} rounds{% if planning.planBodyVerification.get("selfFixRoundsApplied") is not none %}, {{ planning.planBodyVerification.selfFixRoundsApplied }} self-fix rounds{% if planning.planBodyVerification.get("selfFixStopReason") %} · {{ planning.planBodyVerification.selfFixStopReason | inline_code }}{% endif %}{% endif %})</p>
71
71
  {% if planning.planBodyVerification.get("gateBlockedBy") %}<p><strong>{{ t('tasks.implementation-planning.what-it-blocked') }}</strong> — {{ planning.planBodyVerification.gateBlockedBy | join(", ") | inline_code }}</p>{% endif %}
72
- {% if planning.planBodyVerification.get("uniformVerifiers") %}<p><strong>{{ t('implementationPlanning.planBodyUniformVerifierLabel') }}</strong> — {% for u in planning.planBodyVerification.uniformVerifiers %}{{ u.worker | inline_code }} → {{ u.verdict | inline_code }} ({{ u.itemCount }}){% if not loop.last %}, {% endif %}{% endfor %}<br><small>{{ t('implementationPlanning.planBodyUniformVerifierLegend') }}</small></p>{% endif %}
72
+ {% if planning.planBodyVerification.get("uniformVerifiers") %}<p><strong>{{ t('tasks.implementation-planning.uniform-verifier-label') }}</strong> — {% for u in planning.planBodyVerification.uniformVerifiers %}{{ u.worker | inline_code }} → {{ u.verdict | inline_code }} ({{ u.itemCount }}){% if not loop.last %}, {% endif %}{% endfor %}<br><small>{{ t('tasks.implementation-planning.uniform-verifier-legend') }}</small></p>{% endif %}
73
73
  <table><thead><tr><th>{{ t('tasks.implementation-planning.plan-item') }}</th><th>{{ t('tasks.implementation-planning.subject') }}</th></tr></thead><tbody>{% for row in planning.planBodyVerification.planItems %}<tr id="id-{{ row.id }}">{{ row_key(pairs=[("ID", row.id), ("Source section", row.sourceSection)]) }}<td>{{ row.subject | inline_code }}</td></tr>{% endfor %}</tbody></table>
74
74
  {% if planning.planBodyVerification.get("dissentLog") %}<h3>{{ t('tasks.implementation-planning.dissent-left-standing') }}</h3>
75
75
  <table><thead><tr><th>{{ t('tasks.implementation-planning.subject') }}</th><th>{{ t('tasks.implementation-planning.body') }}</th></tr></thead><tbody>{% for row in planning.planBodyVerification.dissentLog %}<tr>{{ row_key(pairs=[("Subject", row.planItem), ("Worker", row.workerRole)]) }}<td>{{ row.body | inline_code }}</td></tr>{% endfor %}</tbody></table>{% endif %}
@@ -29,7 +29,7 @@ When `**Evidence ledger:** required-v1` is present, append one canonical row to
29
29
 
30
30
  - Evidence read: `<project-relative path without a line suffix>`
31
31
 
32
- Every file citation in the result MUST use backticks and a line suffix, for example `src/config/env.ts:1-22`. A cited path without a matching ledger row fails Phase 7 in `validators/validate-run.py` `validate_worker_results_audit()`. Do not add a row for a file you did not open.
32
+ Every file citation in the result MUST use backticks, a line suffix, and the same project-relative path its ledger row carries — for example `src/config/env.ts:1-22`. A bare filename (`env.ts:1-22`) does not match its row and fails exactly like a file you never opened, however many times you cited the full path earlier. A cited path without a matching ledger row fails Phase 7 in `validators/validate-run.py` `validate_worker_results_audit()`. Do not add a row for a file you did not open.
33
33
 
34
34
  ## Anchor headers (lead-injected, BLOCKING)
35
35
 
@@ -235,8 +235,14 @@ def _check_slice_tdd(text: str, stages: List[StageMapStage]) -> List[ValidationE
235
235
 
236
236
 
237
237
  def _check_red_green_steps(section: str, stage_number: int) -> List[ValidationError]:
238
- """S10c: RED-first + later GREEN, with each step's `expected` cell matching
239
- its prefix (RED → FAIL, GREEN → PASS). Only certain violations fire."""
238
+ """S10c: RED-first + later GREEN, with each step's `outcome` cell matching
239
+ its prefix (RED → FAIL, GREEN → PASS).
240
+
241
+ `outcome` is its own column, holding one of two words; the sentence about
242
+ what that looks like lives in `expected` beside it. Written as one cell the
243
+ author had to bury a literal token inside prose, and the rule fired on
244
+ fourteen rows of one measured plan whose prose was correct throughout.
245
+ """
240
246
  rows = _effective_step_rows(section)
241
247
  steps = [(r[1], r[4]) for r in rows if len(r) > 4]
242
248
  actions = [a for a, _ in steps]
@@ -248,16 +254,14 @@ def _check_red_green_steps(section: str, stage_number: int) -> List[ValidationEr
248
254
  "S10c: first step action must start with 'RED:' and some "
249
255
  "step action with 'GREEN:', or add a 'TDD exemption:' line"))
250
256
  return errs
251
- for action, expected in steps:
252
- exp = expected.upper()
253
- if action.startswith("RED:") and "FAIL" not in exp:
257
+ for action, outcome in steps:
258
+ cell = outcome.strip().upper()
259
+ if action.startswith("RED:") and cell != "FAIL":
254
260
  errs.append(ValidationError("S10", stage_number,
255
- "S10c: 'RED:' step's expected cell must read FAIL, got "
256
- f"'{expected}'"))
257
- elif action.startswith("GREEN:") and "PASS" not in exp:
261
+ f"S10c: 'RED:' step's outcome cell must be FAIL, got '{outcome}'"))
262
+ elif action.startswith("GREEN:") and cell != "PASS":
258
263
  errs.append(ValidationError("S10", stage_number,
259
- "S10c: 'GREEN:' step's expected cell must read PASS, got "
260
- f"'{expected}'"))
264
+ f"S10c: 'GREEN:' step's outcome cell must be PASS, got '{outcome}'"))
261
265
  return errs
262
266
 
263
267
 
@@ -432,18 +436,9 @@ def _check_data_slice_tdd(stage: dict) -> List[ValidationError]:
432
436
  return [ValidationError("S10", number,
433
437
  "S10c: first stepwiseExecution action must start with 'RED:' and "
434
438
  "some action with 'GREEN:', or declare a 'tddExemption'")]
435
-
436
- errs: List[ValidationError] = []
437
- for step in steps:
438
- action = str(step.get("action") or "")
439
- expected = str(step.get("expected") or "")
440
- if action.startswith("RED:") and "FAIL" not in expected.upper():
441
- errs.append(ValidationError("S10", number,
442
- f"S10c: 'RED:' step's expected must read FAIL, got '{expected}'"))
443
- elif action.startswith("GREEN:") and "PASS" not in expected.upper():
444
- errs.append(ValidationError("S10", number,
445
- f"S10c: 'GREEN:' step's expected must read PASS, got '{expected}'"))
446
- return errs
439
+ # Whether each row's own `outcome` agrees with its RED/GREEN prefix is a
440
+ # schema conditional now (`StageStepRow.allOf`), not a string search here.
441
+ return []
447
442
 
448
443
 
449
444
  def _check_data_step_counts(
@@ -23,7 +23,6 @@ per line to stderr.
23
23
  from __future__ import annotations
24
24
 
25
25
  import argparse
26
- import hashlib
27
26
  import json
28
27
  import re
29
28
  import sys
@@ -40,9 +39,7 @@ from okstra_ctl.clarification_items import ( # noqa: E402
40
39
  )
41
40
  from okstra_ctl.report_views import ( # noqa: E402
42
41
  analysis_review_context,
43
- extract_html_digest,
44
42
  plan_approval_context,
45
- source_digest,
46
43
  )
47
44
  from okstra_ctl.report_view_artifacts import html_view_path # noqa: E402
48
45
  from okstra_ctl.final_report_paths import final_report_data_path # noqa: E402
@@ -76,15 +73,6 @@ def _main_body(html_text: str) -> str:
76
73
  return match.group("body")
77
74
 
78
75
 
79
- def _sha256(path: Path) -> str:
80
- return hashlib.sha256(path.read_bytes()).hexdigest()
81
-
82
-
83
- def _digest_from_run_meta(html_text: str, key: str) -> str:
84
- match = re.search(rf'"{re.escape(key)}"\s*:\s*"([0-9a-f]{{64}})"', html_text)
85
- return match.group(1) if match else ""
86
-
87
-
88
76
  def _load_v2_data(report_path: Path) -> tuple[Path, dict] | None:
89
77
  data_path = final_report_data_path(report_path)
90
78
  if not data_path.is_file():
@@ -106,17 +94,6 @@ def _validate_v2(report_path: Path, data_path: Path, data: dict) -> list[str]:
106
94
  main_body = _main_body(html_text)
107
95
  failures: list[str] = []
108
96
 
109
- for key, path in (
110
- ("source-data-sha256", data_path),
111
- ("source-md-sha256", report_path),
112
- ):
113
- recorded = _digest_from_run_meta(html_text, key)
114
- expected = _sha256(path)
115
- if not recorded:
116
- failures.append(f"html missing {key} in run-meta")
117
- elif recorded != expected:
118
- failures.append(f"stale html: {key} does not match current source")
119
-
120
97
  task_type = data.get("header", {}).get("taskType", "")
121
98
  if f'data-task-template="{task_type}"' not in html_text:
122
99
  failures.append(f"v2 html task template mismatch for {task_type}")
@@ -236,22 +213,6 @@ def validate(report_path: Path) -> list[str]:
236
213
  html_text = html_path.read_text(encoding="utf-8")
237
214
  html_body = _main_body(html_text)
238
215
 
239
- # (2) source-digest staleness — html's run-meta source-sha256 must
240
- # match the current MD's digest. Mismatch means the html was
241
- # rendered against an older MD body and Phase 7 step 1.5 was not
242
- # re-run.
243
- expected_md_digest = source_digest(md)
244
- html_digest = extract_html_digest(html_text)
245
- if html_digest is None:
246
- failures.append(
247
- "html missing source-sha256 in run-meta — re-render with okstra render-views"
248
- )
249
- elif html_digest != expected_md_digest:
250
- failures.append(
251
- f"stale html: source-sha256 {html_digest[:12]}… does not match "
252
- f"current MD {expected_md_digest[:12]}…"
253
- )
254
-
255
216
  # (3) deliverable sections contain no form controls
256
217
  for chunk in _no_form_sections(html_body):
257
218
  if "<textarea" in chunk or "<input" in chunk or "<select" in chunk:
@@ -51,7 +51,8 @@ from okstra_ctl.conformance import ( # noqa: E402
51
51
  validate_conformance_manifest,
52
52
  )
53
53
  from okstra_ctl.paths import RunRef # noqa: E402
54
- from okstra_ctl.lead_runtime import ARTIFACT_ONLY_LEAD_RUNTIMES # noqa: E402
54
+ from okstra_ctl.domain.host import HostNotRegistered # noqa: E402
55
+ from okstra_ctl.registry.host_registry import default_host_registry # noqa: E402
55
56
  from okstra_ctl.build_tools import ( # noqa: E402
56
57
  command_invokes_build_tool,
57
58
  resolve_build_tool_tokens,
@@ -170,11 +171,11 @@ def _session_accounting(team_state: dict) -> str:
170
171
  value = str(adapter.get("sessionAccounting", "")).strip()
171
172
  if value:
172
173
  return value
173
- return (
174
- "artifact-only"
175
- if team_state.get("leadRuntime") in ARTIFACT_ONLY_LEAD_RUNTIMES
176
- else "claude-jsonl"
177
- )
174
+ runtime = str(team_state.get("leadRuntime", "")).strip()
175
+ try:
176
+ return default_host_registry().resolve(runtime).descriptor.session_accounting
177
+ except HostNotRegistered:
178
+ return "claude-jsonl"
178
179
 
179
180
 
180
181
 
@@ -742,7 +743,7 @@ def validate_team_state(
742
743
  dispatch_mode = str(team_state.get("dispatchMode", "")).strip()
743
744
  if (
744
745
  dispatch_mode in WORKER_DISPATCH_MODES
745
- or team_state.get("leadRuntime") in ARTIFACT_ONLY_LEAD_RUNTIMES
746
+ or _session_accounting(team_state) == "artifact-only"
746
747
  ):
747
748
  if dispatch_mode not in WORKER_DISPATCH_MODES:
748
749
  expected = ", ".join(sorted(WORKER_DISPATCH_MODES))
@@ -39,7 +39,8 @@ from okstra_ctl.worker_heartbeat import ( # noqa: E402
39
39
  IN_STAGE_PREFIX,
40
40
  max_gap_seconds_after,
41
41
  )
42
- from okstra_ctl.lead_runtime import ARTIFACT_ONLY_LEAD_RUNTIMES # noqa: E402
42
+ from okstra_ctl.domain.host import HostNotRegistered # noqa: E402
43
+ from okstra_ctl.registry.host_registry import default_host_registry # noqa: E402
43
44
  from okstra_ctl.wrapper_status import read_wrapper_status # noqa: E402
44
45
 
45
46
  _DISPATCHED_STATUSES = {"completed", "timeout", "error", "in-progress"}
@@ -414,7 +415,11 @@ def _conformance_evidence_source(
414
415
  return None, f"unsupported sessionAccounting: `{accounting}`"
415
416
 
416
417
  legacy_runtime = str(team_state.get("leadRuntime", "") or "claude-code")
417
- if legacy_runtime in ARTIFACT_ONLY_LEAD_RUNTIMES:
418
+ try:
419
+ accounting = default_host_registry().resolve(legacy_runtime).descriptor.session_accounting
420
+ except HostNotRegistered:
421
+ accounting = "claude-jsonl"
422
+ if accounting == "artifact-only":
418
423
  return "artifact-only", None
419
424
  return "claude-jsonl", None
420
425
 
@@ -1,6 +1,6 @@
1
1
  import { runPythonModule } from "../../lib/python-helper.mjs";
2
2
  import { resolvePaths } from "../../lib/paths.mjs";
3
- import { resolveRuntime } from "../../lib/runtime-resolver.mjs";
3
+ import { resolveRuntime } from "../../lib/host-registry-client.mjs";
4
4
 
5
5
  const USAGE = `okstra render-bundle — preview the task bundle without launching a lead
6
6
 
@@ -18,7 +18,7 @@ Usage:
18
18
  [--analysis-target <text>] [--evidence-inputs <csv>] \\
19
19
  [--lead-model <m>] [--claude-model <m>] [--codex-model <m>] \\
20
20
  [--antigravity-model <m>] [--report-writer-model <m>] \\
21
- [--lead-runtime claude-code|codex|antigravity|external] \\
21
+ [--lead-runtime auto|<host-id-or-alias>] \\
22
22
  [--related-tasks <list>] [--base-ref <ref>] \\
23
23
  [--clarification-response <path>] [--work-category <cat>] \\
24
24
  [--stage <auto|N>] [--stages <csv>] [--pr-template-path <path>] \\
@@ -26,7 +26,7 @@ Usage:
26
26
 
27
27
  --stage implementation / final-verification only
28
28
  --stages release-handoff only: PR stage bundle csv (empty = whole-task)
29
- --lead-runtime host-native lead adapter marker
29
+ --lead-runtime registered host ID or alias
30
30
  --pr-template-path release-handoff only
31
31
  --fix-cycle entry phase only: record this re-entry of a done task as a
32
32
  bug-fix cycle (yes opens/continues the cycle)
@@ -85,7 +85,7 @@ export async function run(args) {
85
85
  }
86
86
 
87
87
  const requestedRuntime = findFlagValue(args, "--lead-runtime") || "auto";
88
- const runtimeResolution = resolveRuntime({
88
+ const runtimeResolution = await resolveRuntime({
89
89
  requestedRuntime,
90
90
  command: "render",
91
91
  env: process.env,
@@ -5,14 +5,14 @@ import { join } from "node:path";
5
5
  import { runEnsureInstalled } from "../lifecycle/install.mjs";
6
6
  import { runPythonModule } from "../../lib/python-helper.mjs";
7
7
  import { resolvePaths } from "../../lib/paths.mjs";
8
- import { resolveRuntime } from "../../lib/runtime-resolver.mjs";
8
+ import { resolveRuntime } from "../../lib/host-registry-client.mjs";
9
9
 
10
10
  const USAGE = `okstra run — host-aware okstra execution front door
11
11
 
12
12
  Usage:
13
- okstra run [claude|codex|antigravity|external] --project-root <dir> \\
13
+ okstra run [<host-id-or-alias>] --project-root <dir> \\
14
14
  --project-id <id> --task-group <tg> --task-id <tid> --task-type <type> \\
15
- [--lead-runtime auto|claude-code|codex|antigravity|external] [--yes]
15
+ [--lead-runtime auto|<host-id-or-alias>] [--yes]
16
16
 
17
17
  The leading word selects the lead and is an alias for --lead-runtime, so
18
18
  'okstra run codex' starts a Codex lead. Naming a lead starts that CLI with the
@@ -21,22 +21,6 @@ team. Starting a lead whose CLI sandboxes itself asks for confirmation first,
21
21
  which --yes answers.
22
22
  `;
23
23
 
24
- // Leads okstra can start itself. `external` is not one: it names a harness
25
- // okstra does not own, so those runs are orchestrated rather than launched.
26
- const LAUNCHABLE_RUNTIMES = new Set(["claude-code", "codex", "antigravity"]);
27
-
28
- // The leading word users actually type. `claude` is the provider's name while
29
- // the runtime is `claude-code`; the rest coincide.
30
- const RUNTIME_ALIASES = new Map([
31
- ["claude", "claude-code"],
32
- ["claude-code", "claude-code"],
33
- ["codex", "codex"],
34
- ["antigravity", "antigravity"],
35
- ["avg", "antigravity"],
36
- ["agy", "antigravity"],
37
- ["external", "external"],
38
- ]);
39
-
40
24
  function flagValue(args, flag) {
41
25
  const equals = args.find((arg) => arg.startsWith(`${flag}=`));
42
26
  if (equals) return equals.slice(flag.length + 1);
@@ -46,8 +30,7 @@ function flagValue(args, flag) {
46
30
 
47
31
  export function positionalRuntime(args) {
48
32
  const first = args[0];
49
- if (!first || first.startsWith("-")) return "";
50
- return RUNTIME_ALIASES.get(first) || "";
33
+ return !first || first.startsWith("-") ? "" : first;
51
34
  }
52
35
 
53
36
  export function requestedRuntimeFromArgs(args) {
@@ -91,7 +74,7 @@ function renderArgs(args, resolution) {
91
74
  export function buildRunPlan({ args, paths, resolution, runManifestPath = "" }) {
92
75
  void paths;
93
76
  const projectRoot = projectRootFromArgs(args);
94
- if (resolution.resolvedRuntime === "external") {
77
+ if (resolution.launchMode === "team") {
95
78
  return {
96
79
  ok: true,
97
80
  kind: "team",
@@ -104,7 +87,7 @@ export function buildRunPlan({ args, paths, resolution, runManifestPath = "" })
104
87
  };
105
88
  }
106
89
 
107
- if (LAUNCHABLE_RUNTIMES.has(resolution.resolvedRuntime)) {
90
+ if (resolution.launchMode === "lead") {
108
91
  return {
109
92
  ok: true,
110
93
  kind: "launch-lead",
@@ -130,7 +113,7 @@ export function buildRunPlan({ args, paths, resolution, runManifestPath = "" })
130
113
  };
131
114
  }
132
115
 
133
- return { ok: false, reason: `unsupported resolved runtime: ${resolution.resolvedRuntime}`, commands: [] };
116
+ return { ok: false, reason: `unsupported launch mode: ${resolution.launchMode}`, commands: [] };
134
117
  }
135
118
 
136
119
  async function ensureRuntime(requestedRuntime) {
@@ -182,7 +165,7 @@ export async function run(args) {
182
165
 
183
166
  const paths = await resolvePaths();
184
167
  const requestedRuntime = requestedRuntimeFromArgs(args);
185
- const resolution = resolveRuntime({
168
+ const resolution = await resolveRuntime({
186
169
  requestedRuntime,
187
170
  command: "run",
188
171
  env: process.env,