okstra 0.160.0 → 0.162.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.
@@ -1,3 +1,6 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createInterface } from "node:readline/promises";
3
+
1
4
  import { runEnsureInstalled } from "../lifecycle/install.mjs";
2
5
  import { runPythonModule } from "../../lib/python-helper.mjs";
3
6
  import { resolvePaths } from "../../lib/paths.mjs";
@@ -6,13 +9,31 @@ import { resolveRuntime } from "../../lib/runtime-resolver.mjs";
6
9
  const USAGE = `okstra run — host-aware okstra execution front door
7
10
 
8
11
  Usage:
9
- okstra run --project-root <dir> --project-id <id> --task-group <tg> \\
10
- --task-id <tid> --task-type <type> [--lead-runtime auto|claude-code|codex|antigravity|external]
11
-
12
- Host-native Claude Code, Codex, and Antigravity execution use the okstra-run
13
- skill front door. Bare 'okstra run' orchestrates only external/tmux all-CLI execution.
12
+ okstra run [claude|codex|antigravity|external] --project-root <dir> \\
13
+ --project-id <id> --task-group <tg> --task-id <tid> --task-type <type> \\
14
+ [--lead-runtime auto|claude-code|codex|antigravity|external] [--yes]
15
+
16
+ The leading word selects the lead and is an alias for --lead-runtime, so
17
+ 'okstra run codex' starts a Codex lead. Naming a lead starts that CLI with the
18
+ prepared prompt; 'external' instead orchestrates an all-CLI run through okstra
19
+ team. Starting a lead whose CLI sandboxes itself asks for confirmation first,
20
+ which --yes answers.
14
21
  `;
15
22
 
23
+ // Leads okstra can start itself. `external` is not one: it names a harness
24
+ // okstra does not own, so those runs are orchestrated rather than launched.
25
+ const LAUNCHABLE_RUNTIMES = new Set(["claude-code", "codex", "antigravity"]);
26
+
27
+ // The leading word users actually type. `claude` is the provider's name while
28
+ // the runtime is `claude-code`; the rest coincide.
29
+ const RUNTIME_ALIASES = new Map([
30
+ ["claude", "claude-code"],
31
+ ["claude-code", "claude-code"],
32
+ ["codex", "codex"],
33
+ ["antigravity", "antigravity"],
34
+ ["external", "external"],
35
+ ]);
36
+
16
37
  function flagValue(args, flag) {
17
38
  const equals = args.find((arg) => arg.startsWith(`${flag}=`));
18
39
  if (equals) return equals.slice(flag.length + 1);
@@ -20,14 +41,21 @@ function flagValue(args, flag) {
20
41
  return index >= 0 ? args[index + 1] : "";
21
42
  }
22
43
 
44
+ export function positionalRuntime(args) {
45
+ const first = args[0];
46
+ if (!first || first.startsWith("-")) return "";
47
+ return RUNTIME_ALIASES.get(first) || "";
48
+ }
49
+
23
50
  export function requestedRuntimeFromArgs(args) {
24
- return flagValue(args, "--lead-runtime") || "auto";
51
+ return flagValue(args, "--lead-runtime") || positionalRuntime(args) || "auto";
25
52
  }
26
53
 
27
- export function withoutLeadRuntimeFlags(args) {
54
+ export function withoutRuntimeSelection(args) {
55
+ const rest = positionalRuntime(args) ? args.slice(1) : args;
28
56
  const out = [];
29
- for (let i = 0; i < args.length; i += 1) {
30
- const arg = args[i];
57
+ for (let i = 0; i < rest.length; i += 1) {
58
+ const arg = rest[i];
31
59
  if (arg === "--lead-runtime") {
32
60
  i += 1;
33
61
  continue;
@@ -53,24 +81,17 @@ function renderArgs(args, resolution) {
53
81
  "--lead-runtime", resolution.resolvedRuntime,
54
82
  "--lead-runtime-request", resolution.requestedRuntime,
55
83
  "--runtime-resolution-json", JSON.stringify(resolution),
56
- ...withoutLeadRuntimeFlags(args),
84
+ ...withoutRuntimeSelection(args),
57
85
  ];
58
86
  }
59
87
 
60
88
  export function buildRunPlan({ args, paths, resolution, runManifestPath = "" }) {
61
89
  void paths;
62
90
  const projectRoot = projectRootFromArgs(args);
63
- if (resolution.resolvedRuntime === "claude-code") {
64
- return {
65
- ok: false,
66
- reason: "Bare okstra run cannot invoke host-native Claude workers. Use the okstra-run skill in Claude Code or pass --lead-runtime external.",
67
- commands: [],
68
- };
69
- }
70
-
71
91
  if (resolution.resolvedRuntime === "external") {
72
92
  return {
73
93
  ok: true,
94
+ kind: "team",
74
95
  reason: resolution.reason,
75
96
  commands: [
76
97
  { name: "render-bundle", args: renderArgs(args, resolution), capture: true },
@@ -80,19 +101,14 @@ export function buildRunPlan({ args, paths, resolution, runManifestPath = "" })
80
101
  };
81
102
  }
82
103
 
83
- if (resolution.resolvedRuntime === "codex") {
104
+ if (LAUNCHABLE_RUNTIMES.has(resolution.resolvedRuntime)) {
84
105
  return {
85
- ok: false,
86
- reason: "Bare okstra run cannot invoke host-native Codex workers. Use the okstra-run skill in Codex or pass --lead-runtime external for an all-CLI run.",
87
- commands: [],
88
- };
89
- }
90
-
91
- if (resolution.resolvedRuntime === "antigravity") {
92
- return {
93
- ok: false,
94
- reason: "Bare okstra run cannot invoke host-native Antigravity workers. Use the okstra-run skill in Antigravity or pass --lead-runtime external for an all-CLI run.",
95
- commands: [],
106
+ ok: true,
107
+ kind: "launch-lead",
108
+ reason: resolution.reason,
109
+ // Not render-only: the lead is actually started, so prepare must commit
110
+ // the run and emit the launch metadata rather than preview it.
111
+ commands: [{ name: "prepare", args: renderArgs(args, resolution), capture: true }],
96
112
  };
97
113
  }
98
114
 
@@ -114,6 +130,73 @@ async function runRenderCommand(command, paths) {
114
130
  return result;
115
131
  }
116
132
 
133
+ export function parseLaunchMetadata(stdout) {
134
+ const marker = "__OKSTRA_LAUNCH__ ";
135
+ const line = String(stdout || "")
136
+ .split("\n")
137
+ .reverse()
138
+ .find((item) => item.startsWith(marker));
139
+ return line ? JSON.parse(line.slice(marker.length)) : null;
140
+ }
141
+
142
+ export function withoutLaunchMetadata(stdout) {
143
+ return String(stdout || "")
144
+ .split("\n")
145
+ .filter((line) => !line.startsWith("__OKSTRA_LAUNCH__ "))
146
+ .join("\n");
147
+ }
148
+
149
+ async function confirmSandboxWaiver(launch, args) {
150
+ if (!launch.sandboxWaiverNote) return true;
151
+ process.stderr.write(`\n${launch.sandboxWaiverNote}\n`);
152
+ if (args.includes("--yes")) {
153
+ process.stderr.write("Proceeding without the sandbox (--yes).\n\n");
154
+ return true;
155
+ }
156
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
157
+ try {
158
+ const answer = await rl.question(
159
+ `Start ${launch.leadExecutable} without its sandbox? [y/N] `,
160
+ );
161
+ return answer.trim().toLowerCase() === "y";
162
+ } finally {
163
+ rl.close();
164
+ }
165
+ }
166
+
167
+ async function launchLead(command, paths, args) {
168
+ const prepared = await runPythonModule({
169
+ module: "okstra_ctl.run",
170
+ args: ["--workspace-root", paths.workspace, ...command.args],
171
+ stdio: "capture",
172
+ });
173
+ process.stdout.write(withoutLaunchMetadata(prepared.stdout));
174
+ if (prepared.stderr) process.stderr.write(prepared.stderr);
175
+ if (prepared.code !== 0) return prepared.code ?? 1;
176
+
177
+ const launch = parseLaunchMetadata(prepared.stdout);
178
+ if (!launch) {
179
+ process.stderr.write("error: prepare did not emit launch metadata\n");
180
+ return 1;
181
+ }
182
+ if (!(await confirmSandboxWaiver(launch, args))) {
183
+ process.stderr.write(
184
+ "okstra: cancelled. The task bundle is prepared; rerun to resume it.\n",
185
+ );
186
+ return 1;
187
+ }
188
+ const [executable, ...rest] = launch.launchArgv;
189
+ const started = spawnSync(executable, rest, {
190
+ cwd: launch.projectRoot,
191
+ stdio: "inherit",
192
+ });
193
+ if (started.error) {
194
+ process.stderr.write(`error: could not start ${executable}: ${started.error.message}\n`);
195
+ return 1;
196
+ }
197
+ return started.status ?? 1;
198
+ }
199
+
117
200
  async function runTeam(args) {
118
201
  const { run: runTeamCommand } = await import("./team.mjs");
119
202
  return await runTeamCommand(args);
@@ -147,6 +230,10 @@ export async function run(args) {
147
230
  const ensureCode = await ensureRuntime(requestedRuntime);
148
231
  if (ensureCode !== 0) return ensureCode;
149
232
 
233
+ if (firstPlan.kind === "launch-lead") {
234
+ return await launchLead(firstPlan.commands[0], paths, args);
235
+ }
236
+
150
237
  const render = firstPlan.commands[0];
151
238
  const prepared = await runRenderCommand(render, paths);
152
239
  if (prepared.code !== 0) return prepared.code ?? 1;