okstra 0.78.2 → 0.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/doctor.mjs CHANGED
@@ -5,6 +5,7 @@ import { resolvePaths } from "./paths.mjs";
5
5
  import { fileExists, runProcess } from "./_proc.mjs";
6
6
  import { runPythonSnippet } from "./_python-helper.mjs";
7
7
  import { runtimeIncludesClaudeAssets, validateInstallRuntime } from "./install.mjs";
8
+ import { resolveRuntime } from "./runtime-resolver.mjs";
8
9
 
9
10
  const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
10
11
  const REQUIRED_SKILLS = ["okstra-setup", "okstra-run"];
@@ -38,7 +39,7 @@ export function parseDoctorArgs(args) {
38
39
  const result = {
39
40
  jsonMode: false,
40
41
  phase: null,
41
- runtime: "claude-code",
42
+ runtime: "auto",
42
43
  };
43
44
  for (let i = 0; i < args.length; i++) {
44
45
  const arg = args[i];
@@ -182,6 +183,17 @@ export async function run(args) {
182
183
  return 2;
183
184
  }
184
185
  const paths = await resolvePaths();
186
+ const runtimeResolution = resolveRuntime({
187
+ requestedRuntime: opts.runtime,
188
+ command: "doctor",
189
+ env: process.env,
190
+ capabilities: { tmuxAvailable: Boolean(process.env.TMUX) },
191
+ });
192
+ if (!runtimeResolution.ok) {
193
+ process.stderr.write(`error: ${runtimeResolution.reason}\n`);
194
+ return 2;
195
+ }
196
+ const resolvedRuntime = runtimeResolution.resolvedRuntime;
185
197
 
186
198
  const results = [
187
199
  await check("python3", checkPython3),
@@ -197,7 +209,7 @@ export async function run(args) {
197
209
  await check("bin: okstra-gemini-exec.sh", () => checkBashEntry(paths.bin, "okstra-gemini-exec.sh")),
198
210
  await check("bin: okstra-central.sh", () => checkBashEntry(paths.bin, "okstra-central.sh")),
199
211
  ...(await Promise.all(
200
- requiredSkillNamesForRuntime(opts.runtime).map((name) =>
212
+ requiredSkillNamesForRuntime(resolvedRuntime).map((name) =>
201
213
  check(`skill: ${name}`, async () => {
202
214
  const p = join(CLAUDE_SKILLS_DIR, name, "SKILL.md");
203
215
  return (await fileExists(p))
@@ -229,7 +241,9 @@ export async function run(args) {
229
241
  process.stdout.write(
230
242
  `${JSON.stringify({
231
243
  ok: allOk,
232
- runtime: opts.runtime,
244
+ runtimeRequest: opts.runtime,
245
+ runtime: resolvedRuntime,
246
+ runtimeResolution,
233
247
  phase: opts.phase,
234
248
  paths,
235
249
  phaseDiagnostics,
package/src/install.mjs CHANGED
@@ -6,6 +6,7 @@ import { getPackageRoot } from "./version.mjs";
6
6
  import { resolvePaths } from "./paths.mjs";
7
7
  import { fileExists } from "./_proc.mjs";
8
8
  import { buildRuntimeManifest, RUNTIMES_MANIFEST_REL } from "./runtime-manifest.mjs";
9
+ import { normalizeRuntimeRequest, resolveRuntime } from "./runtime-resolver.mjs";
9
10
 
10
11
  const SKILLS_MANIFEST_REL = "installed-skills.json";
11
12
  const AGENTS_MANIFEST_REL = "installed-agents.json";
@@ -88,10 +89,11 @@ Usage:
88
89
  Skills call this on every run. Returns 0 quickly when the install is fresh.
89
90
  `;
90
91
 
91
- const INSTALL_RUNTIMES = new Set(["claude-code", "codex", "external", "all"]);
92
+ const INSTALL_RUNTIMES = new Set(["auto", "claude-code", "codex", "external", "all"]);
92
93
 
93
- export function runtimeIncludesClaudeAssets(runtime) {
94
- return runtime === "claude-code" || runtime === "all";
94
+ export function runtimeIncludesClaudeAssets(runtime, resolution = null) {
95
+ if (runtime === "claude-code" || runtime === "all") return true;
96
+ return runtime === "auto" && resolution?.resolvedRuntime === "claude-code";
95
97
  }
96
98
 
97
99
  function readRuntimeFlag(args, index) {
@@ -107,10 +109,11 @@ function readRuntimeFlag(args, index) {
107
109
  }
108
110
 
109
111
  export function validateInstallRuntime(value) {
110
- if (!INSTALL_RUNTIMES.has(value)) {
111
- throw new Error(`unknown runtime '${value}' (expected: claude-code, codex, external, all)`);
112
+ const runtime = normalizeRuntimeRequest(value);
113
+ if (!INSTALL_RUNTIMES.has(runtime)) {
114
+ throw new Error(`unknown runtime '${runtime}' (expected: auto, claude-code, codex, external, all)`);
112
115
  }
113
- return value;
116
+ return runtime;
114
117
  }
115
118
 
116
119
  async function hashFile(path) {
@@ -302,7 +305,7 @@ async function installLinkMode(repoPath, paths, opts) {
302
305
  if (!quiet) process.stdout.write(` bin/${name}: ${action}\n`);
303
306
  }
304
307
 
305
- if (runtimeIncludesClaudeAssets(runtime)) {
308
+ if (runtimeIncludesClaudeAssets(runtime, opts.resolution)) {
306
309
  const skillResult = await installSkillsLink(repoAbs, { dryRun, quiet });
307
310
  const leadSkillResult = await installLeadSkillLink(repoAbs, { dryRun, quiet });
308
311
  await writeSkillsManifest(paths.home, [...skillResult.installed, ...leadSkillResult], { dryRun });
@@ -314,7 +317,7 @@ async function installLinkMode(repoPath, paths, opts) {
314
317
  }
315
318
 
316
319
  await installNamedTemplate(repoAbs, paths, { mode: "link", dryRun, quiet }, SETTINGS_TEMPLATE_DESCRIPTOR);
317
- await writeRuntimeManifest(paths.home, runtime, { dryRun });
320
+ await writeRuntimeManifest(paths.home, runtime, { dryRun, resolution: opts.resolution });
318
321
 
319
322
  if (!dryRun) {
320
323
  await writeFileAtomic(join(paths.home, "dev-link"), repoAbs + "\n", 0o644);
@@ -382,10 +385,10 @@ async function writeAgentsManifest(home, names, opts) {
382
385
  }
383
386
 
384
387
  async function writeRuntimeManifest(home, runtime, opts) {
385
- const { dryRun = false } = opts ?? {};
386
- const data = buildRuntimeManifest(runtime);
388
+ const { dryRun = false, resolution = null } = opts ?? {};
389
+ const data = buildRuntimeManifest(runtime, new Date().toISOString(), resolution);
387
390
  if (dryRun) {
388
- process.stdout.write(`[dry-run] write runtime manifest: ${data.runtimes.join(",")}\n`);
391
+ process.stdout.write(`[dry-run] write runtime manifest: ${data.installedRuntimes.join(",")}\n`);
389
392
  return;
390
393
  }
391
394
  await writeFileAtomic(
@@ -609,7 +612,7 @@ export function parseInstallArgs(args) {
609
612
  refresh: false,
610
613
  quiet: false,
611
614
  linkRepo: null,
612
- runtime: "claude-code",
615
+ runtime: "auto",
613
616
  };
614
617
  for (let i = 0; i < args.length; i++) {
615
618
  const a = args[i];
@@ -650,13 +653,24 @@ export async function runInstall(args) {
650
653
  const pkgRoot = getPackageRoot();
651
654
  const runtimeRoot = join(pkgRoot, "runtime");
652
655
  const paths = await resolvePaths();
656
+ const runtimeResolution = resolveRuntime({
657
+ requestedRuntime: opts.runtime,
658
+ command: "install",
659
+ env: process.env,
660
+ capabilities: { tmuxAvailable: Boolean(process.env.TMUX) },
661
+ });
662
+ if (!runtimeResolution.ok) {
663
+ process.stderr.write(`error: ${runtimeResolution.reason}\n`);
664
+ return 2;
665
+ }
666
+ const resolvedRuntime = runtimeResolution.resolvedRuntime;
653
667
 
654
668
  if (!opts.dryRun) {
655
669
  await fs.mkdir(paths.home, { recursive: true });
656
670
  }
657
671
 
658
672
  if (opts.linkRepo) {
659
- return await installLinkMode(opts.linkRepo, paths, opts);
673
+ return await installLinkMode(opts.linkRepo, paths, { ...opts, resolution: runtimeResolution });
660
674
  }
661
675
 
662
676
  // copy mode — if a prior dev-link is present, refuse rather than silently overwriting.
@@ -668,7 +682,7 @@ export async function runInstall(args) {
668
682
 
669
683
  if (!opts.quiet) {
670
684
  process.stdout.write(`installing okstra runtime (package ${paths.package})\n`);
671
- process.stdout.write(` runtime: ${opts.runtime}\n`);
685
+ process.stdout.write(` runtime: ${opts.runtime} -> ${resolvedRuntime}\n`);
672
686
  process.stdout.write(` source: ${runtimeRoot}\n`);
673
687
  process.stdout.write(` home: ${paths.home}\n`);
674
688
  }
@@ -750,7 +764,7 @@ export async function runInstall(args) {
750
764
  );
751
765
  }
752
766
 
753
- if (runtimeIncludesClaudeAssets(opts.runtime)) {
767
+ if (runtimeIncludesClaudeAssets(opts.runtime, runtimeResolution)) {
754
768
  const skillResult = await installSkillsCopy(runtimeRoot, opts);
755
769
  const leadSkillResult = await installLeadSkillCopy(runtimeRoot, opts);
756
770
  await writeSkillsManifest(
@@ -762,11 +776,11 @@ export async function runInstall(args) {
762
776
  const agentResult = await installAgentsCopy(runtimeRoot, opts);
763
777
  await writeAgentsManifest(paths.home, agentResult.installed, { dryRun: opts.dryRun });
764
778
  } else if (!opts.quiet) {
765
- process.stdout.write(" claude assets: skipped for runtime=codex\n");
779
+ process.stdout.write(` claude assets: skipped for runtime=${resolvedRuntime}\n`);
766
780
  }
767
781
 
768
782
  await installNamedTemplate(runtimeRoot, paths, { mode: "copy", ...opts }, SETTINGS_TEMPLATE_DESCRIPTOR);
769
- await writeRuntimeManifest(paths.home, opts.runtime, { dryRun: opts.dryRun });
783
+ await writeRuntimeManifest(paths.home, opts.runtime, { dryRun: opts.dryRun, resolution: runtimeResolution });
770
784
 
771
785
  if (!opts.dryRun) {
772
786
  await writeFileAtomic(join(paths.home, "version"), paths.package + "\n", 0o644);
@@ -821,7 +835,7 @@ function summarise(label, result, target) {
821
835
  export function parseEnsureInstalledArgs(args) {
822
836
  const result = {
823
837
  quiet: false,
824
- runtime: "claude-code",
838
+ runtime: "auto",
825
839
  };
826
840
  for (let i = 0; i < args.length; i++) {
827
841
  const arg = args[i];
@@ -851,6 +865,16 @@ export async function runEnsureInstalled(args) {
851
865
  return 2;
852
866
  }
853
867
  const paths = await resolvePaths();
868
+ const runtimeResolution = resolveRuntime({
869
+ requestedRuntime: opts.runtime,
870
+ command: "ensure-installed",
871
+ env: process.env,
872
+ capabilities: { tmuxAvailable: Boolean(process.env.TMUX) },
873
+ });
874
+ if (!runtimeResolution.ok) {
875
+ process.stderr.write(`error: ${runtimeResolution.reason}\n`);
876
+ return 2;
877
+ }
854
878
 
855
879
  const reasons = [];
856
880
  if (!paths.version) reasons.push("no version stamp");
@@ -862,7 +886,7 @@ export async function runEnsureInstalled(args) {
862
886
  if (!(await fileExists(join(paths.home, "schemas", "final-report-v1.0.schema.json")))) {
863
887
  reasons.push(`missing ${join(paths.home, "schemas", "final-report-v1.0.schema.json")}`);
864
888
  }
865
- if (runtimeIncludesClaudeAssets(opts.runtime)) {
889
+ if (runtimeIncludesClaudeAssets(opts.runtime, runtimeResolution)) {
866
890
  if (!(await fileExists(join(CLAUDE_SKILLS_DIR, "okstra-setup", "SKILL.md")))) {
867
891
  reasons.push(`missing ${CLAUDE_SKILLS_DIR}/okstra-setup/SKILL.md`);
868
892
  }
@@ -1,5 +1,6 @@
1
1
  import { runPythonModule } from "./_python-helper.mjs";
2
2
  import { resolvePaths } from "./paths.mjs";
3
+ import { resolveRuntime } from "./runtime-resolver.mjs";
3
4
 
4
5
  const USAGE = `okstra render-bundle — preview the task bundle without launching claude
5
6
 
@@ -41,6 +42,27 @@ Replaces the long \`from okstra_ctl.run import PrepareInputs,
41
42
  prepare_task_bundle\` heredoc pattern.
42
43
  `;
43
44
 
45
+ function findFlagValue(args, flag) {
46
+ const equals = args.find((arg) => arg.startsWith(`${flag}=`));
47
+ if (equals) return equals.slice(flag.length + 1);
48
+ const index = args.indexOf(flag);
49
+ return index >= 0 ? args[index + 1] : "";
50
+ }
51
+
52
+ function withoutLeadRuntime(args) {
53
+ const out = [];
54
+ for (let i = 0; i < args.length; i += 1) {
55
+ const arg = args[i];
56
+ if (arg === "--lead-runtime") {
57
+ i += 1;
58
+ continue;
59
+ }
60
+ if (arg.startsWith("--lead-runtime=")) continue;
61
+ out.push(arg);
62
+ }
63
+ return out;
64
+ }
65
+
44
66
  export async function run(args) {
45
67
  if (args.includes("--help") || args.includes("-h")) {
46
68
  process.stdout.write(USAGE);
@@ -59,10 +81,25 @@ export async function run(args) {
59
81
  }
60
82
  }
61
83
 
84
+ const requestedRuntime = findFlagValue(args, "--lead-runtime") || "auto";
85
+ const runtimeResolution = resolveRuntime({
86
+ requestedRuntime,
87
+ command: "render",
88
+ env: process.env,
89
+ capabilities: { tmuxAvailable: Boolean(process.env.TMUX) },
90
+ });
91
+ if (!runtimeResolution.ok) {
92
+ process.stderr.write(`error: ${runtimeResolution.reason}\n`);
93
+ return 2;
94
+ }
95
+
62
96
  const finalArgs = [
63
97
  "--workspace-root", paths.workspace,
64
98
  "--render-only",
65
- ...args,
99
+ "--lead-runtime", runtimeResolution.resolvedRuntime,
100
+ "--lead-runtime-request", requestedRuntime,
101
+ "--runtime-resolution-json", JSON.stringify(runtimeResolution),
102
+ ...withoutLeadRuntime(args),
66
103
  ];
67
104
 
68
105
  const result = await runPythonModule({
package/src/run.mjs ADDED
@@ -0,0 +1,201 @@
1
+ import { runEnsureInstalled } from "./install.mjs";
2
+ import { runPythonModule } from "./_python-helper.mjs";
3
+ import { resolvePaths } from "./paths.mjs";
4
+ import { resolveRuntime } from "./runtime-resolver.mjs";
5
+
6
+ const USAGE = `okstra run — host-aware okstra execution front door
7
+
8
+ 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|external]
11
+
12
+ Claude Code native execution remains the /okstra-run skill front door. Bare
13
+ 'okstra run' orchestrates Codex and external/tmux CLI execution.
14
+ `;
15
+
16
+ function flagValue(args, flag) {
17
+ const equals = args.find((arg) => arg.startsWith(`${flag}=`));
18
+ if (equals) return equals.slice(flag.length + 1);
19
+ const index = args.indexOf(flag);
20
+ return index >= 0 ? args[index + 1] : "";
21
+ }
22
+
23
+ export function requestedRuntimeFromArgs(args) {
24
+ return flagValue(args, "--lead-runtime") || "auto";
25
+ }
26
+
27
+ export function withoutLeadRuntimeFlags(args) {
28
+ const out = [];
29
+ for (let i = 0; i < args.length; i += 1) {
30
+ const arg = args[i];
31
+ if (arg === "--lead-runtime") {
32
+ i += 1;
33
+ continue;
34
+ }
35
+ if (arg.startsWith("--lead-runtime=")) continue;
36
+ out.push(arg);
37
+ }
38
+ return out;
39
+ }
40
+
41
+ export function extractLabel(stdout, label) {
42
+ const prefix = `${label}:`;
43
+ const line = String(stdout || "").split("\n").find((item) => item.startsWith(prefix));
44
+ return line ? line.slice(prefix.length).trim() : "";
45
+ }
46
+
47
+ function projectRootFromArgs(args) {
48
+ return flagValue(args, "--project-root");
49
+ }
50
+
51
+ function withoutDispatchOnlyFlags(args) {
52
+ const out = [];
53
+ for (let i = 0; i < args.length; i += 1) {
54
+ const arg = args[i];
55
+ if (arg === "--enable-codex-report-writer") continue;
56
+ if (arg === "--report-writer-codex-model") {
57
+ i += 1;
58
+ continue;
59
+ }
60
+ if (arg.startsWith("--report-writer-codex-model=")) continue;
61
+ out.push(arg);
62
+ }
63
+ return out;
64
+ }
65
+
66
+ function renderArgs(args, resolution) {
67
+ return [
68
+ "--lead-runtime", resolution.resolvedRuntime,
69
+ "--lead-runtime-request", resolution.requestedRuntime,
70
+ "--runtime-resolution-json", JSON.stringify(resolution),
71
+ ...withoutDispatchOnlyFlags(withoutLeadRuntimeFlags(args)),
72
+ ];
73
+ }
74
+
75
+ function codexDispatchArgs(args, projectRoot, runManifestPath) {
76
+ const out = ["--project-root", projectRoot, "--run-manifest", runManifestPath];
77
+ for (let i = 0; i < args.length; i += 1) {
78
+ const arg = args[i];
79
+ if (arg === "--enable-codex-report-writer") {
80
+ out.push(arg);
81
+ continue;
82
+ }
83
+ if (arg === "--report-writer-codex-model") {
84
+ out.push(arg, args[i + 1] ?? "");
85
+ i += 1;
86
+ continue;
87
+ }
88
+ if (arg.startsWith("--report-writer-codex-model=")) out.push(arg);
89
+ }
90
+ return out;
91
+ }
92
+
93
+ export function buildRunPlan({ args, paths, resolution, runManifestPath = "" }) {
94
+ void paths;
95
+ const projectRoot = projectRootFromArgs(args);
96
+ if (resolution.resolvedRuntime === "claude-code") {
97
+ return {
98
+ ok: false,
99
+ reason: "Bare okstra run cannot invoke Claude Code TeamCreate / Agent(...). Use /okstra-run in Claude Code or pass --lead-runtime external|codex.",
100
+ commands: [],
101
+ };
102
+ }
103
+
104
+ if (resolution.resolvedRuntime === "external") {
105
+ return {
106
+ ok: true,
107
+ reason: resolution.reason,
108
+ commands: [
109
+ { name: "render-bundle", args: renderArgs(args, resolution), capture: true },
110
+ { name: "team dispatch", args: ["dispatch", "--project-root", projectRoot, "--run-manifest", runManifestPath], capture: false },
111
+ { name: "team await", args: ["await", "--project-root", projectRoot, "--run-manifest", runManifestPath], capture: false },
112
+ ],
113
+ };
114
+ }
115
+
116
+ if (resolution.resolvedRuntime === "codex") {
117
+ return {
118
+ ok: true,
119
+ reason: resolution.reason,
120
+ commands: [
121
+ { name: "codex-run", args: renderArgs(args, resolution), capture: true },
122
+ { name: "codex-dispatch", args: codexDispatchArgs(args, projectRoot, runManifestPath), capture: false },
123
+ ],
124
+ };
125
+ }
126
+
127
+ return { ok: false, reason: `unsupported resolved runtime: ${resolution.resolvedRuntime}`, commands: [] };
128
+ }
129
+
130
+ async function ensureRuntime(requestedRuntime) {
131
+ return await runEnsureInstalled(["--runtime", requestedRuntime, "-q"]);
132
+ }
133
+
134
+ async function runRenderCommand(command, paths) {
135
+ const result = await runPythonModule({
136
+ module: "okstra_ctl.run",
137
+ args: ["--workspace-root", paths.workspace, "--render-only", ...command.args],
138
+ stdio: "capture",
139
+ });
140
+ process.stdout.write(result.stdout);
141
+ if (result.stderr) process.stderr.write(result.stderr);
142
+ return result;
143
+ }
144
+
145
+ async function runTeam(args) {
146
+ const { run: runTeamCommand } = await import("./team.mjs");
147
+ return await runTeamCommand(args);
148
+ }
149
+
150
+ async function runCodexDispatch(args) {
151
+ const { run: runCodexDispatchCommand } = await import("./codex-dispatch.mjs");
152
+ return await runCodexDispatchCommand(args);
153
+ }
154
+
155
+ export async function run(args) {
156
+ if (args.includes("--help") || args.includes("-h")) {
157
+ process.stdout.write(USAGE);
158
+ return 0;
159
+ }
160
+
161
+ const paths = await resolvePaths();
162
+ const requestedRuntime = requestedRuntimeFromArgs(args);
163
+ const resolution = resolveRuntime({
164
+ requestedRuntime,
165
+ command: "run",
166
+ env: process.env,
167
+ capabilities: { tmuxAvailable: Boolean(process.env.TMUX) },
168
+ });
169
+ if (!resolution.ok) {
170
+ process.stderr.write(`error: ${resolution.reason}\n`);
171
+ return 2;
172
+ }
173
+
174
+ const firstPlan = buildRunPlan({ args, paths, resolution });
175
+ if (!firstPlan.ok) {
176
+ process.stderr.write(`error: ${firstPlan.reason}\n`);
177
+ return 2;
178
+ }
179
+
180
+ const ensureCode = await ensureRuntime(requestedRuntime);
181
+ if (ensureCode !== 0) return ensureCode;
182
+
183
+ const render = firstPlan.commands[0];
184
+ const prepared = await runRenderCommand(render, paths);
185
+ if (prepared.code !== 0) return prepared.code ?? 1;
186
+
187
+ const runManifestPath = extractLabel(prepared.stdout, "okstra run manifest");
188
+ if (!runManifestPath) {
189
+ process.stderr.write("error: render output did not include 'okstra run manifest:'\n");
190
+ return 1;
191
+ }
192
+
193
+ const plan = buildRunPlan({ args, paths, resolution, runManifestPath });
194
+ for (const command of plan.commands.slice(1)) {
195
+ const code = command.name.startsWith("team ")
196
+ ? await runTeam(command.args)
197
+ : await runCodexDispatch(command.args);
198
+ if (code !== 0) return code;
199
+ }
200
+ return 0;
201
+ }
@@ -1,29 +1,66 @@
1
1
  export const RUNTIMES_MANIFEST_REL = "installed-runtimes.json";
2
+ export const RUNTIME_MANIFEST_SCHEMA_VERSION = 2;
3
+ export const EXPLICIT_RUNTIME_VALUES = ["claude-code", "codex", "external"];
2
4
 
3
- export function runtimeListForSelection(runtime) {
4
- if (runtime === "all") return ["claude-code", "codex", "external"];
5
+ export function runtimeListForSelection(runtime, resolution = null) {
6
+ if (runtime === "all") return [...EXPLICIT_RUNTIME_VALUES];
7
+ if (runtime === "auto") {
8
+ const resolved = resolution?.resolvedRuntime;
9
+ return resolved && resolved !== "all" ? [resolved] : [];
10
+ }
5
11
  return [runtime];
6
12
  }
7
13
 
8
- export function buildRuntimeManifest(runtime, installedAt = new Date().toISOString()) {
9
- const includesClaude = runtime === "claude-code" || runtime === "all";
10
- const includesCodex = runtime === "codex" || runtime === "all";
11
- const includesExternal = runtime === "external" || runtime === "all";
14
+ function includesClaudeRuntime(runtime, resolution = null) {
15
+ if (runtime === "all") return true;
16
+ if (runtime === "claude-code") return true;
17
+ return runtime === "auto" && resolution?.resolvedRuntime === "claude-code";
18
+ }
19
+
20
+ function defaultResolution(runtime) {
21
+ if (runtime === "auto") return null;
22
+ return {
23
+ requestedRuntime: runtime,
24
+ resolvedRuntime: runtime,
25
+ host: "explicit",
26
+ confidence: "high",
27
+ reason: `Explicit runtime '${runtime}' requested.`,
28
+ fallbackFrom: null,
29
+ warnings: [],
30
+ };
31
+ }
32
+
33
+ export function buildRuntimeManifest(runtime, installedAt = new Date().toISOString(), resolution = null) {
34
+ const runtimeResolution = resolution ?? defaultResolution(runtime);
35
+ const includesClaude = includesClaudeRuntime(runtime, runtimeResolution);
12
36
  return {
37
+ schemaVersion: RUNTIME_MANIFEST_SCHEMA_VERSION,
13
38
  installedAt,
14
- runtime,
15
- runtimes: runtimeListForSelection(runtime),
16
- assets: {
39
+ installRequest: runtime,
40
+ runtimeResolution,
41
+ installedRuntimes: runtimeListForSelection(runtime, runtimeResolution),
42
+ installedAssets: {
17
43
  sharedRuntime: true,
18
44
  claudeSkills: includesClaude,
19
45
  claudeAgents: includesClaude,
20
- codexAdapter: includesCodex,
21
- externalAdapter: includesExternal,
46
+ binWrappers: true,
47
+ templates: true,
48
+ schemas: true,
49
+ validators: true,
22
50
  },
23
- version: 1,
24
51
  };
25
52
  }
26
53
 
54
+ export function installedRuntimesFromManifest(data) {
55
+ if (Array.isArray(data?.installedRuntimes)) return data.installedRuntimes;
56
+ if (Array.isArray(data?.runtimes)) return data.runtimes;
57
+ if (typeof data?.runtime === "string" && data.runtime) return runtimeListForSelection(data.runtime);
58
+ return [];
59
+ }
60
+
27
61
  export function runtimeManifestIncludesClaudeAssets(data) {
62
+ if (data?.schemaVersion === RUNTIME_MANIFEST_SCHEMA_VERSION) {
63
+ return data?.installedAssets?.claudeSkills === true || data?.installedAssets?.claudeAgents === true;
64
+ }
28
65
  return data?.assets?.claudeSkills === true || data?.assets?.claudeAgents === true;
29
66
  }
@@ -0,0 +1,123 @@
1
+ export const RUNTIME_AUTO = "auto";
2
+ export const EXPLICIT_RUNTIMES = ["claude-code", "codex", "external"];
3
+ export const VALID_RUNTIME_REQUESTS = [RUNTIME_AUTO, ...EXPLICIT_RUNTIMES, "all"];
4
+
5
+ const VALID_HOSTS = new Set(EXPLICIT_RUNTIMES);
6
+ const VALID_REQUESTS = new Set(VALID_RUNTIME_REQUESTS);
7
+
8
+ function clean(value) {
9
+ return String(value ?? "").trim();
10
+ }
11
+
12
+ export function normalizeRuntimeRequest(value) {
13
+ const runtime = clean(value) || RUNTIME_AUTO;
14
+ if (!VALID_REQUESTS.has(runtime)) {
15
+ throw new Error(`unknown runtime '${runtime}' (expected: ${VALID_RUNTIME_REQUESTS.join(", ")})`);
16
+ }
17
+ return runtime;
18
+ }
19
+
20
+ function normalizeCapabilities(capabilities = {}) {
21
+ return {
22
+ claudeAssetsInstalled: capabilities.claudeAssetsInstalled === true,
23
+ codexCliAvailable: capabilities.codexCliAvailable === true,
24
+ tmuxAvailable: capabilities.tmuxAvailable === true,
25
+ claudeSkillHandoff: capabilities.claudeSkillHandoff === true,
26
+ };
27
+ }
28
+
29
+ function explicitResult(runtime) {
30
+ return {
31
+ ok: true,
32
+ requestedRuntime: runtime,
33
+ resolvedRuntime: runtime,
34
+ host: "explicit",
35
+ confidence: "high",
36
+ reason: `Explicit runtime '${runtime}' requested.`,
37
+ fallbackFrom: null,
38
+ warnings: [],
39
+ };
40
+ }
41
+
42
+ const RUNTIME_FLAG_COMMANDS = new Set(["install", "ensure-installed", "doctor"]);
43
+
44
+ function explicitRuntimeFlag(command) {
45
+ return RUNTIME_FLAG_COMMANDS.has(command) ? "--runtime" : "--lead-runtime";
46
+ }
47
+
48
+ function okResult({ requestedRuntime, resolvedRuntime, host, reason, warnings = [] }) {
49
+ return {
50
+ ok: true,
51
+ requestedRuntime,
52
+ resolvedRuntime,
53
+ host,
54
+ confidence: "high",
55
+ reason,
56
+ fallbackFrom: null,
57
+ warnings,
58
+ };
59
+ }
60
+
61
+ function failResult(requestedRuntime, reason, warnings = []) {
62
+ return {
63
+ ok: false,
64
+ requestedRuntime,
65
+ resolvedRuntime: null,
66
+ host: "unknown",
67
+ confidence: "low",
68
+ reason,
69
+ fallbackFrom: null,
70
+ warnings,
71
+ };
72
+ }
73
+
74
+ export function resolveRuntime({
75
+ requestedRuntime = RUNTIME_AUTO,
76
+ command = "run",
77
+ env = {},
78
+ capabilities = {},
79
+ } = {}) {
80
+ const request = normalizeRuntimeRequest(requestedRuntime);
81
+ const caps = normalizeCapabilities(capabilities);
82
+
83
+ if (request !== RUNTIME_AUTO) return explicitResult(request);
84
+
85
+ const rawHost = clean(env?.OKSTRA_RUNTIME_HOST);
86
+ if (rawHost) {
87
+ if (!VALID_HOSTS.has(rawHost)) {
88
+ return failResult(
89
+ request,
90
+ `invalid OKSTRA_RUNTIME_HOST '${rawHost}' (expected: ${EXPLICIT_RUNTIMES.join("|")}).`,
91
+ );
92
+ }
93
+ return okResult({
94
+ requestedRuntime: request,
95
+ resolvedRuntime: rawHost,
96
+ host: rawHost,
97
+ reason: `OKSTRA_RUNTIME_HOST=${rawHost} selected runtime '${rawHost}'.`,
98
+ });
99
+ }
100
+
101
+ if (caps.claudeSkillHandoff) {
102
+ return okResult({
103
+ requestedRuntime: request,
104
+ resolvedRuntime: "claude-code",
105
+ host: "claude-code",
106
+ reason: "Claude Code skill handoff selected runtime 'claude-code'.",
107
+ });
108
+ }
109
+
110
+ if (caps.tmuxAvailable) {
111
+ return okResult({
112
+ requestedRuntime: request,
113
+ resolvedRuntime: "external",
114
+ host: "generic-terminal",
115
+ reason: "No Claude Code or Codex host signal; tmux is available.",
116
+ });
117
+ }
118
+
119
+ return failResult(
120
+ request,
121
+ `host unknown: set OKSTRA_RUNTIME_HOST=claude-code|codex|external or pass ${explicitRuntimeFlag(command)} explicitly.`,
122
+ );
123
+ }