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/README.kr.md +3 -3
- package/README.md +3 -3
- package/docs/kr/architecture.md +1 -1
- package/docs/kr/cli.md +18 -1
- package/docs/project-structure-overview.md +15 -2
- package/docs/superpowers/plans/2026-06-14-host-runtime-auto-execution.md +1775 -0
- package/docs/superpowers/specs/2026-06-14-host-runtime-auto-execution-design.md +404 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/okstra-claude-exec.sh +71 -0
- package/runtime/python/okstra_ctl/render.py +11 -0
- package/runtime/python/okstra_ctl/run.py +25 -0
- package/runtime/skills/okstra-inspect/SKILL.md +1 -1
- package/runtime/skills/okstra-run/SKILL.md +4 -1
- package/runtime/skills/okstra-schedule/SKILL.md +1 -1
- package/runtime/skills/okstra-setup/SKILL.md +2 -2
- package/src/cli-registry.mjs +8 -1
- package/src/doctor.mjs +17 -3
- package/src/install.mjs +43 -19
- package/src/render-bundle.mjs +38 -1
- package/src/run.mjs +201 -0
- package/src/runtime-manifest.mjs +49 -12
- package/src/runtime-resolver.mjs +123 -0
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: "
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
|
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.
|
|
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: "
|
|
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(
|
|
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: "
|
|
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
|
}
|
package/src/render-bundle.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/runtime-manifest.mjs
CHANGED
|
@@ -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 [
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
+
}
|