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.
- package/README.md +7 -5
- package/docs/architecture.md +12 -7
- package/docs/cli.md +8 -5
- package/docs/project-structure-overview.md +7 -5
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +1 -0
- package/runtime/bin/lib/okstra/cli.sh +5 -0
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- package/runtime/bin/okstra.sh +87 -91
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -0
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- package/runtime/python/okstra_ctl/adapters/accounting/__init__.py +11 -0
- package/runtime/python/okstra_ctl/adapters/accounting/claude_jsonl.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/cli_artifact.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/unavailable.py +19 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +92 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +54 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +68 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/antigravity.md → python/okstra_ctl/adapters/hosts/antigravity/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +292 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +120 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/claude-code.md → python/okstra_ctl/adapters/hosts/claude-code/relay.md} +111 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/codex.md → python/okstra_ctl/adapters/hosts/codex/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +72 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/external.md → python/okstra_ctl/adapters/hosts/external/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +35 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +55 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +43 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +32 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +38 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/application/__init__.py +1 -0
- package/runtime/python/okstra_ctl/application/advance_wizard.py +25 -0
- package/runtime/python/okstra_ctl/application/collect_usage.py +15 -0
- package/runtime/python/okstra_ctl/application/dispatch_assignments.py +15 -0
- package/runtime/python/okstra_ctl/application/resolve_assignment.py +93 -0
- package/runtime/python/okstra_ctl/application/resume_run.py +21 -0
- package/runtime/python/okstra_ctl/application/start_run.py +21 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +49 -826
- package/runtime/python/okstra_ctl/dispatch_core.py +240 -29
- package/runtime/python/okstra_ctl/dispatch_state.py +17 -0
- package/runtime/python/okstra_ctl/domain/__init__.py +34 -0
- package/runtime/python/okstra_ctl/domain/host.py +100 -0
- package/runtime/python/okstra_ctl/domain/provider.py +66 -0
- package/runtime/python/okstra_ctl/domain/wizard/__init__.py +19 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +140 -0
- package/runtime/python/okstra_ctl/entrypoints/__init__.py +1 -0
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
- package/runtime/python/okstra_ctl/models.py +54 -269
- package/runtime/python/okstra_ctl/ports/__init__.py +15 -0
- package/runtime/python/okstra_ctl/ports/host.py +32 -0
- package/runtime/python/okstra_ctl/ports/interaction.py +15 -0
- package/runtime/python/okstra_ctl/ports/lead_session.py +25 -0
- package/runtime/python/okstra_ctl/ports/usage_accounting.py +23 -0
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +32 -0
- package/runtime/python/okstra_ctl/registry/__init__.py +13 -0
- package/runtime/python/okstra_ctl/registry/factory_loader.py +32 -0
- package/runtime/python/okstra_ctl/registry/host_discovery.py +124 -0
- package/runtime/python/okstra_ctl/registry/host_registry.py +365 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +149 -0
- package/runtime/python/okstra_ctl/render.py +145 -47
- package/runtime/python/okstra_ctl/run.py +80 -58
- package/runtime/python/okstra_ctl/session.py +1 -1
- package/runtime/python/okstra_ctl/team.py +44 -32
- package/runtime/python/okstra_ctl/wizard.py +162 -64
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
- package/runtime/python/okstra_token_usage/collect.py +34 -6
- package/runtime/schemas/final-report-v1.0.schema.json +3989 -1085
- package/runtime/schemas/final-report-v2.0.schema.json +5622 -1451
- package/runtime/skills/okstra-run/SKILL.md +73 -28
- package/runtime/templates/implementation-worker-preamble.md +1 -1
- package/runtime/templates/reports/final-report.template.md +3 -3
- package/runtime/templates/reports/html/i18n/en.json +3 -1
- package/runtime/templates/reports/html/i18n/ko.json +3 -1
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +1 -1
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +17 -22
- package/runtime/validators/validate-report-views.py +0 -39
- package/runtime/validators/validate-run.py +8 -7
- package/runtime/validators/validate_session_conformance.py +7 -2
- package/src/commands/execute/render-bundle.mjs +4 -4
- package/src/commands/execute/run.mjs +8 -25
- package/src/commands/execute/wizard.mjs +33 -13
- package/src/commands/lifecycle/doctor.mjs +10 -10
- package/src/commands/lifecycle/install.mjs +53 -30
- package/src/commands/lifecycle/preflight.mjs +14 -4
- package/src/lib/host-registry-client.mjs +176 -0
- package/src/lib/runtime-manifest.mjs +6 -8
- package/runtime/python/okstra_ctl/lead_runtime.py +0 -115
- package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
- package/src/lib/runtime-readiness.mjs +0 -90
- 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
|
|
45
|
-
|
|
46
|
-
- `
|
|
47
|
-
- `
|
|
48
|
-
- `
|
|
49
|
-
- `
|
|
50
|
-
- `
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
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
|
|
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.
|
|
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('
|
|
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
|
|
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 `
|
|
239
|
-
its prefix (RED → FAIL, GREEN → PASS).
|
|
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,
|
|
252
|
-
|
|
253
|
-
if action.startswith("RED:") and "FAIL"
|
|
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
|
|
256
|
-
|
|
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
|
|
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
|
-
|
|
437
|
-
|
|
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.
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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/
|
|
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
|
|
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
|
|
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/
|
|
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 [
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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
|
|
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,
|