okstra 0.79.0 → 0.81.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.
@@ -2,6 +2,7 @@
2
2
  name: okstra-team-contract
3
3
  description: Use when okstra is in Phases 2–5 and needs team operating rules for the Claude lead + worker structure, or when verifying worker roster and model assignments.
4
4
  user-invocable: false
5
+ hide: true
5
6
  ---
6
7
 
7
8
  # OKSTRA Team Contract
@@ -6,7 +6,7 @@ export const COMMAND_REGISTRY = [
6
6
  module: "./install.mjs",
7
7
  export: "runInstall",
8
8
  category: "admin",
9
- summary: ["Install runtime; --runtime codex skips ~/.claude assets"],
9
+ summary: ["Install runtime; seed skills into existing agent homes"],
10
10
  },
11
11
  {
12
12
  name: "ensure-installed",
@@ -127,6 +127,13 @@ export const COMMAND_REGISTRY = [
127
127
  "python3 -m okstra_ctl.run --render-only)",
128
128
  ],
129
129
  },
130
+ {
131
+ name: "run",
132
+ module: "./run.mjs",
133
+ export: "run",
134
+ category: "introspection",
135
+ summary: ["Host-aware auto execution front door"],
136
+ },
130
137
  {
131
138
  name: "codex-run",
132
139
  module: "./codex-run.mjs",
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,11 +6,18 @@ 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";
12
- const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
13
- const CLAUDE_AGENTS_DIR = join(homedir(), ".claude", "agents");
13
+ const USER_HOME = homedir();
14
+ const CLAUDE_HOME = join(USER_HOME, ".claude");
15
+ const CODEX_HOME = join(USER_HOME, ".codex");
16
+ const OMP_AGENT_HOME = join(USER_HOME, ".omp", "agent");
17
+ const CLAUDE_SKILLS_DIR = join(CLAUDE_HOME, "skills");
18
+ const CODEX_SKILLS_DIR = join(CODEX_HOME, "skills");
19
+ const OMP_SKILLS_DIR = join(OMP_AGENT_HOME, "skills");
20
+ const CLAUDE_AGENTS_DIR = join(CLAUDE_HOME, "agents");
14
21
 
15
22
  // Source template (relative to runtime root in copy mode, or repo root in link mode).
16
23
  const SETTINGS_TEMPLATE_SRC_REL = ["templates", "reports", "settings.template.json"];
@@ -51,10 +58,12 @@ Effect (copy mode):
51
58
  ${"$HOME"}/.okstra/bin <- runtime/bin
52
59
  ${"$HOME"}/.okstra/templates <- runtime/templates (report.css / report.js / *.template.md)
53
60
  ${"$HOME"}/.okstra/templates/settings.local.json <- runtime/templates/reports/settings.template.json
54
- ${"$HOME"}/.claude/skills/<name> <- runtime/skills/<name> (per skill)
55
- ${"$HOME"}/.claude/agents/<worker>.md <- runtime/agents/workers/<worker>.md
61
+ ${"$HOME"}/.claude/skills/<name> <- skills when ${"$HOME"}/.claude exists
62
+ ${"$HOME"}/.codex/skills/<name> <- skills when ${"$HOME"}/.codex exists
63
+ ${"$HOME"}/.omp/agent/skills/<name> <- skills fallback when Claude/Codex homes are absent
64
+ ${"$HOME"}/.claude/agents/<worker>.md <- worker agents when ${"$HOME"}/.claude exists
56
65
  ${"$HOME"}/.okstra/installed-runtimes.json <- manifest of installed runtime adapters
57
- ${"$HOME"}/.okstra/installed-skills.json <- manifest of installed skills
66
+ ${"$HOME"}/.okstra/installed-skills.json <- target-aware manifest of installed skills
58
67
  ${"$HOME"}/.okstra/installed-agents.json <- manifest of installed workers
59
68
  ${"$HOME"}/.okstra/version <- installed package version stamp
60
69
 
@@ -62,8 +71,10 @@ Effect (link mode):
62
71
  ${"$HOME"}/.okstra/lib/python/<pkg> -> <repo>/scripts/<pkg> (symlink)
63
72
  ${"$HOME"}/.okstra/bin/<name>.sh -> <repo>/scripts/<name>.sh
64
73
  ${"$HOME"}/.okstra/templates/settings.local.json -> <repo>/templates/reports/settings.template.json
65
- ${"$HOME"}/.claude/skills/<name> -> <repo>/skills/<name> (symlink dir)
66
- ${"$HOME"}/.claude/agents/<worker>.md -> <repo>/agents/workers/<worker>.md
74
+ ${"$HOME"}/.claude/skills/<name> -> <repo>/skills/<name> when ${"$HOME"}/.claude exists
75
+ ${"$HOME"}/.codex/skills/<name> -> <repo>/skills/<name> when ${"$HOME"}/.codex exists
76
+ ${"$HOME"}/.omp/agent/skills/<name> -> <repo>/skills/<name> fallback
77
+ ${"$HOME"}/.claude/agents/<worker>.md -> <repo>/agents/workers/<worker>.md when ${"$HOME"}/.claude exists
67
78
  ${"$HOME"}/.okstra/dev-link <- <repo> path stamp
68
79
  ${"$HOME"}/.okstra/version <- installed package version stamp
69
80
 
@@ -72,10 +83,9 @@ project-local <project>/.claude/settings.local.json that okstra-setup
72
83
  provisions, granting per-project Claude Code permissions for okstra
73
84
  worker wrapper scripts without modifying the user's global settings.
74
85
 
75
- Worker agent definitions are installed into ${"$HOME"}/.claude/agents/ so
76
- that Claude Code's subagent discovery picks them up; they cannot live
77
- inside the package alone because the harness only scans ~/.claude/agents/
78
- and <project>/.claude/agents/.
86
+ Worker agent definitions are installed into ${"$HOME"}/.claude/agents/ only
87
+ when ${"$HOME"}/.claude exists, so Claude Code's subagent discovery can pick
88
+ them up. Codex/OMP skill targets receive skill markdown only.
79
89
  `;
80
90
 
81
91
  const ENSURE_USAGE = `okstra ensure-installed — idempotent install check
@@ -88,10 +98,11 @@ Usage:
88
98
  Skills call this on every run. Returns 0 quickly when the install is fresh.
89
99
  `;
90
100
 
91
- const INSTALL_RUNTIMES = new Set(["claude-code", "codex", "external", "all"]);
101
+ const INSTALL_RUNTIMES = new Set(["auto", "claude-code", "codex", "external", "all"]);
92
102
 
93
- export function runtimeIncludesClaudeAssets(runtime) {
94
- return runtime === "claude-code" || runtime === "all";
103
+ export function runtimeIncludesClaudeAssets(runtime, resolution = null) {
104
+ if (runtime === "claude-code" || runtime === "all") return true;
105
+ return runtime === "auto" && resolution?.resolvedRuntime === "claude-code";
95
106
  }
96
107
 
97
108
  function readRuntimeFlag(args, index) {
@@ -107,10 +118,11 @@ function readRuntimeFlag(args, index) {
107
118
  }
108
119
 
109
120
  export function validateInstallRuntime(value) {
110
- if (!INSTALL_RUNTIMES.has(value)) {
111
- throw new Error(`unknown runtime '${value}' (expected: claude-code, codex, external, all)`);
121
+ const runtime = normalizeRuntimeRequest(value);
122
+ if (!INSTALL_RUNTIMES.has(runtime)) {
123
+ throw new Error(`unknown runtime '${runtime}' (expected: auto, claude-code, codex, external, all)`);
112
124
  }
113
- return value;
125
+ return runtime;
114
126
  }
115
127
 
116
128
  async function hashFile(path) {
@@ -302,19 +314,22 @@ async function installLinkMode(repoPath, paths, opts) {
302
314
  if (!quiet) process.stdout.write(` bin/${name}: ${action}\n`);
303
315
  }
304
316
 
305
- if (runtimeIncludesClaudeAssets(runtime)) {
306
- const skillResult = await installSkillsLink(repoAbs, { dryRun, quiet });
307
- const leadSkillResult = await installLeadSkillLink(repoAbs, { dryRun, quiet });
308
- await writeSkillsManifest(paths.home, [...skillResult.installed, ...leadSkillResult], { dryRun });
317
+ const skillTargets = await detectSkillTargets();
318
+ const installedSkillTargets = await installSkillTargetsLink(repoAbs, skillTargets, { dryRun, quiet });
319
+ await writeSkillsManifest(paths.home, installedSkillTargets, { dryRun });
309
320
 
310
- const agentResult = await installAgentsLink(repoAbs, { dryRun, quiet });
311
- await writeAgentsManifest(paths.home, agentResult.installed, { dryRun });
312
- } else if (!quiet) {
313
- process.stdout.write(" claude assets: skipped for runtime=codex\n");
321
+ let agentResult = { installed: [] };
322
+ if (skillTargets.some((target) => target.provider === "claude")) {
323
+ agentResult = await installAgentsLink(repoAbs, { dryRun, quiet });
314
324
  }
325
+ await writeAgentsManifest(paths.home, agentResult.installed, { dryRun });
315
326
 
316
327
  await installNamedTemplate(repoAbs, paths, { mode: "link", dryRun, quiet }, SETTINGS_TEMPLATE_DESCRIPTOR);
317
- await writeRuntimeManifest(paths.home, runtime, { dryRun });
328
+ await writeRuntimeManifest(paths.home, runtime, {
329
+ dryRun,
330
+ resolution: opts.resolution,
331
+ installedAssets: installedAssetOverrides(installedSkillTargets, agentResult.installed),
332
+ });
318
333
 
319
334
  if (!dryRun) {
320
335
  await writeFileAtomic(join(paths.home, "dev-link"), repoAbs + "\n", 0o644);
@@ -327,7 +342,7 @@ async function installLinkMode(repoPath, paths, opts) {
327
342
  process.stdout.write(
328
343
  "\nNext step: register the current project.\n" +
329
344
  " cd <your project> && npx -y okstra@latest setup --project-id <id>\n" +
330
- " (or run /okstra-setup inside a Claude Code session)\n" +
345
+ " (or run /okstra-setup inside a Harness Agent session (e.g. claude codex, codex, pi.. ))\n" +
331
346
  "\nTip: to use a bare 'okstra' command instead of npx, run:\n" +
332
347
  " npm i -g okstra\n",
333
348
  );
@@ -345,15 +360,58 @@ async function listSkillDirs(skillsRoot) {
345
360
  }
346
361
  }
347
362
 
348
- async function writeSkillsManifest(home, names, opts) {
363
+ async function detectSkillTargets() {
364
+ const targets = [];
365
+ const [hasClaude, hasCodex] = await Promise.all([dirExists(CLAUDE_HOME), dirExists(CODEX_HOME)]);
366
+ if (hasClaude) {
367
+ targets.push({ provider: "claude", home: CLAUDE_HOME, skillsDir: CLAUDE_SKILLS_DIR });
368
+ }
369
+ if (hasCodex) {
370
+ targets.push({ provider: "codex", home: CODEX_HOME, skillsDir: CODEX_SKILLS_DIR });
371
+ }
372
+ if (targets.length === 0 && (await dirExists(OMP_AGENT_HOME))) {
373
+ targets.push({ provider: "omp", home: OMP_AGENT_HOME, skillsDir: OMP_SKILLS_DIR });
374
+ }
375
+ return targets;
376
+ }
377
+
378
+ function skillTargetManifest(targets) {
379
+ return targets.map((target) => ({
380
+ provider: target.provider,
381
+ root: target.skillsDir,
382
+ skills: Array.from(new Set(target.installed)).sort(),
383
+ }));
384
+ }
385
+
386
+ function installedAssetOverrides(skillTargets, agentNames) {
387
+ const providers = new Set(skillTargets.map((target) => target.provider));
388
+ return {
389
+ claudeSkills: providers.has("claude"),
390
+ claudeAgents: agentNames.length > 0,
391
+ codexSkills: providers.has("codex"),
392
+ ompSkills: providers.has("omp"),
393
+ skillTargets: Array.from(providers).sort(),
394
+ };
395
+ }
396
+
397
+ function installedSkillNames(targets) {
398
+ return Array.from(new Set(targets.flatMap((target) => target.installed))).sort();
399
+ }
400
+
401
+
402
+ async function writeSkillsManifest(home, targets, opts) {
349
403
  const { dryRun = false } = opts ?? {};
404
+ const manifestTargets = skillTargetManifest(targets);
350
405
  const data = {
351
- version: 1,
406
+ version: 2,
352
407
  installedAt: new Date().toISOString(),
353
- skills: Array.from(new Set(names)).sort(),
408
+ skills: installedSkillNames(targets),
409
+ targets: manifestTargets,
354
410
  };
355
411
  if (dryRun) {
356
- process.stdout.write(`[dry-run] write skills manifest: ${data.skills.length} entries\n`);
412
+ process.stdout.write(
413
+ `[dry-run] write skills manifest: ${data.skills.length} skills across ${data.targets.length} targets\n`,
414
+ );
357
415
  return;
358
416
  }
359
417
  await writeFileAtomic(
@@ -382,10 +440,15 @@ async function writeAgentsManifest(home, names, opts) {
382
440
  }
383
441
 
384
442
  async function writeRuntimeManifest(home, runtime, opts) {
385
- const { dryRun = false } = opts ?? {};
386
- const data = buildRuntimeManifest(runtime);
443
+ const { dryRun = false, resolution = null, installedAssets = null } = opts ?? {};
444
+ const data = buildRuntimeManifest(
445
+ runtime,
446
+ new Date().toISOString(),
447
+ resolution,
448
+ installedAssets,
449
+ );
387
450
  if (dryRun) {
388
- process.stdout.write(`[dry-run] write runtime manifest: ${data.runtimes.join(",")}\n`);
451
+ process.stdout.write(`[dry-run] write runtime manifest: ${data.installedRuntimes.join(",")}\n`);
389
452
  return;
390
453
  }
391
454
  await writeFileAtomic(
@@ -521,95 +584,107 @@ const SETTINGS_TEMPLATE_DESCRIPTOR = {
521
584
  label: "settings template",
522
585
  };
523
586
 
524
- async function installSkillsCopy(runtimeRoot, opts) {
587
+ const LEAD_SKILL_NAME = "okstra";
588
+
589
+ async function installSkillsCopy(runtimeRoot, target, opts) {
525
590
  const { refresh, dryRun, quiet } = opts;
526
591
  const srcRoot = join(runtimeRoot, "skills");
527
592
  const names = await listSkillDirs(srcRoot);
528
- if (names.length === 0) {
529
- if (!quiet) process.stdout.write(" skills: runtime/skills empty — skipped\n");
530
- return { installed: [] };
531
- }
532
593
  let copied = 0;
533
594
  let skipped = 0;
534
595
  for (const name of names) {
535
596
  const r = await copyTreeIfChanged(
536
597
  join(srcRoot, name),
537
- join(CLAUDE_SKILLS_DIR, name),
598
+ join(target.skillsDir, name),
538
599
  { refresh, dryRun, mode: 0o644 },
539
600
  );
540
601
  copied += r.copied;
541
602
  skipped += r.skipped;
542
603
  }
604
+ const lead = await installLeadSkillCopy(runtimeRoot, target, opts);
543
605
  if (!quiet) {
606
+ const totalCopied = copied + lead.copied;
607
+ const totalSkipped = skipped + lead.skipped;
608
+ const totalSkills = names.length + lead.installed.length;
544
609
  process.stdout.write(
545
- ` skills: copied=${copied} skipped=${skipped} -> ${CLAUDE_SKILLS_DIR}/ (${names.length} skills)\n`,
610
+ ` ${target.provider} skills: copied=${totalCopied} skipped=${totalSkipped} -> ${target.skillsDir}/ (${totalSkills} skills)\n`,
546
611
  );
547
612
  }
548
- return { installed: names };
613
+ return { ...target, installed: [...names, ...lead.installed] };
549
614
  }
550
615
 
551
- async function installSkillsLink(repoAbs, opts) {
616
+ async function installSkillsLink(repoAbs, target, opts) {
552
617
  const { dryRun, quiet } = opts;
553
618
  const srcRoot = join(repoAbs, "skills");
554
619
  const names = await listSkillDirs(srcRoot);
555
- if (names.length === 0) {
556
- if (!quiet) process.stdout.write(" skills: <repo>/skills missing — skipped\n");
557
- return { installed: [] };
558
- }
559
- if (!dryRun) await fs.mkdir(CLAUDE_SKILLS_DIR, { recursive: true });
620
+ if (!dryRun) await fs.mkdir(target.skillsDir, { recursive: true });
560
621
  for (const name of names) {
561
622
  const src = join(srcRoot, name);
562
- const dst = join(CLAUDE_SKILLS_DIR, name);
623
+ const dst = join(target.skillsDir, name);
563
624
  const action = await ensureSymlink(src, dst, { dryRun });
564
- if (!quiet) process.stdout.write(` skills/${name}: ${action}\n`);
625
+ if (!quiet) process.stdout.write(` ${target.provider} skills/${name}: ${action}\n`);
565
626
  }
566
- return { installed: names };
627
+ const lead = await installLeadSkillLink(repoAbs, target, opts);
628
+ return { ...target, installed: [...names, ...lead] };
567
629
  }
568
630
 
569
- // The lead `okstra` skill source is agents/SKILL.md (frontmatter `name: okstra`);
570
- // it doubles as the lead agent contract, so it lives under agents/ rather than
571
- // skills/. Claude Code only discovers skills at ~/.claude/skills/<name>/SKILL.md,
572
- // so it must be installed there as its own skill dir — otherwise install ships
573
- // every okstra-* helper skill but not the lead skill, and edits to agents/SKILL.md
574
- // never reach the running lead.
575
- const LEAD_SKILL_NAME = "okstra";
576
-
577
- async function installLeadSkillCopy(runtimeRoot, opts) {
631
+ async function installLeadSkillCopy(runtimeRoot, target, opts) {
578
632
  const { refresh, dryRun, quiet } = opts;
579
633
  const src = join(runtimeRoot, "agents", "SKILL.md");
580
- const dst = join(CLAUDE_SKILLS_DIR, LEAD_SKILL_NAME, "SKILL.md");
634
+ const dst = join(target.skillsDir, LEAD_SKILL_NAME, "SKILL.md");
581
635
  const r = await copyFileIfChanged(src, dst, { refresh, dryRun, mode: 0o644 });
582
636
  if (r.missingSource) {
583
- if (!quiet) process.stdout.write(" skills/okstra: runtime/agents/SKILL.md missing — skipped\n");
584
- return [];
585
- }
586
- if (!quiet) {
587
- process.stdout.write(` skills/okstra: ${r.copied ? "copied" : "unchanged"} -> ${dst}\n`);
637
+ if (!quiet) {
638
+ process.stdout.write(
639
+ ` ${target.provider} skills/okstra: runtime/agents/SKILL.md missing — skipped\n`,
640
+ );
641
+ }
642
+ return { installed: [], copied: 0, skipped: 0 };
588
643
  }
589
- return [LEAD_SKILL_NAME];
644
+ return { installed: [LEAD_SKILL_NAME], copied: r.copied, skipped: r.skipped };
590
645
  }
591
646
 
592
- async function installLeadSkillLink(repoAbs, opts) {
647
+ async function installLeadSkillLink(repoAbs, target, opts) {
593
648
  const { dryRun, quiet } = opts;
594
649
  const src = join(repoAbs, "agents", "SKILL.md");
595
650
  if (!(await fileExists(src))) {
596
- if (!quiet) process.stdout.write(" skills/okstra: <repo>/agents/SKILL.md missing — skipped\n");
651
+ if (!quiet) {
652
+ process.stdout.write(
653
+ ` ${target.provider} skills/okstra: <repo>/agents/SKILL.md missing — skipped\n`,
654
+ );
655
+ }
597
656
  return [];
598
657
  }
599
- const dst = join(CLAUDE_SKILLS_DIR, LEAD_SKILL_NAME, "SKILL.md");
600
- if (!dryRun) await fs.mkdir(join(CLAUDE_SKILLS_DIR, LEAD_SKILL_NAME), { recursive: true });
658
+ const dst = join(target.skillsDir, LEAD_SKILL_NAME, "SKILL.md");
659
+ if (!dryRun) await fs.mkdir(join(target.skillsDir, LEAD_SKILL_NAME), { recursive: true });
601
660
  const action = await ensureSymlink(src, dst, { dryRun });
602
- if (!quiet) process.stdout.write(` skills/okstra/SKILL.md: ${action}\n`);
661
+ if (!quiet) process.stdout.write(` ${target.provider} skills/okstra/SKILL.md: ${action}\n`);
603
662
  return [LEAD_SKILL_NAME];
604
663
  }
605
664
 
665
+ async function installSkillTargetsCopy(runtimeRoot, targets, opts) {
666
+ if (targets.length === 0) {
667
+ if (!opts.quiet) process.stdout.write(" skills: no Claude, Codex, or OMP agent home found — skipped\n");
668
+ return [];
669
+ }
670
+ return await Promise.all(targets.map((target) => installSkillsCopy(runtimeRoot, target, opts)));
671
+ }
672
+
673
+ async function installSkillTargetsLink(repoAbs, targets, opts) {
674
+ if (targets.length === 0) {
675
+ if (!opts.quiet) process.stdout.write(" skills: no Claude, Codex, or OMP agent home found — skipped\n");
676
+ return [];
677
+ }
678
+ return await Promise.all(targets.map((target) => installSkillsLink(repoAbs, target, opts)));
679
+ }
680
+
606
681
  export function parseInstallArgs(args) {
607
682
  const result = {
608
683
  dryRun: false,
609
684
  refresh: false,
610
685
  quiet: false,
611
686
  linkRepo: null,
612
- runtime: "claude-code",
687
+ runtime: "auto",
613
688
  };
614
689
  for (let i = 0; i < args.length; i++) {
615
690
  const a = args[i];
@@ -650,13 +725,24 @@ export async function runInstall(args) {
650
725
  const pkgRoot = getPackageRoot();
651
726
  const runtimeRoot = join(pkgRoot, "runtime");
652
727
  const paths = await resolvePaths();
728
+ const runtimeResolution = resolveRuntime({
729
+ requestedRuntime: opts.runtime,
730
+ command: "install",
731
+ env: process.env,
732
+ capabilities: { tmuxAvailable: Boolean(process.env.TMUX) },
733
+ });
734
+ if (!runtimeResolution.ok) {
735
+ process.stderr.write(`error: ${runtimeResolution.reason}\n`);
736
+ return 2;
737
+ }
738
+ const resolvedRuntime = runtimeResolution.resolvedRuntime;
653
739
 
654
740
  if (!opts.dryRun) {
655
741
  await fs.mkdir(paths.home, { recursive: true });
656
742
  }
657
743
 
658
744
  if (opts.linkRepo) {
659
- return await installLinkMode(opts.linkRepo, paths, opts);
745
+ return await installLinkMode(opts.linkRepo, paths, { ...opts, resolution: runtimeResolution });
660
746
  }
661
747
 
662
748
  // copy mode — if a prior dev-link is present, refuse rather than silently overwriting.
@@ -668,7 +754,7 @@ export async function runInstall(args) {
668
754
 
669
755
  if (!opts.quiet) {
670
756
  process.stdout.write(`installing okstra runtime (package ${paths.package})\n`);
671
- process.stdout.write(` runtime: ${opts.runtime}\n`);
757
+ process.stdout.write(` runtime: ${opts.runtime} -> ${resolvedRuntime}\n`);
672
758
  process.stdout.write(` source: ${runtimeRoot}\n`);
673
759
  process.stdout.write(` home: ${paths.home}\n`);
674
760
  }
@@ -750,23 +836,22 @@ export async function runInstall(args) {
750
836
  );
751
837
  }
752
838
 
753
- if (runtimeIncludesClaudeAssets(opts.runtime)) {
754
- const skillResult = await installSkillsCopy(runtimeRoot, opts);
755
- const leadSkillResult = await installLeadSkillCopy(runtimeRoot, opts);
756
- await writeSkillsManifest(
757
- paths.home,
758
- [...skillResult.installed, ...leadSkillResult],
759
- { dryRun: opts.dryRun },
760
- );
839
+ const skillTargets = await detectSkillTargets();
840
+ const installedSkillTargets = await installSkillTargetsCopy(runtimeRoot, skillTargets, opts);
841
+ await writeSkillsManifest(paths.home, installedSkillTargets, { dryRun: opts.dryRun });
761
842
 
762
- const agentResult = await installAgentsCopy(runtimeRoot, opts);
763
- await writeAgentsManifest(paths.home, agentResult.installed, { dryRun: opts.dryRun });
764
- } else if (!opts.quiet) {
765
- process.stdout.write(" claude assets: skipped for runtime=codex\n");
843
+ let agentResult = { installed: [] };
844
+ if (skillTargets.some((target) => target.provider === "claude")) {
845
+ agentResult = await installAgentsCopy(runtimeRoot, opts);
766
846
  }
847
+ await writeAgentsManifest(paths.home, agentResult.installed, { dryRun: opts.dryRun });
767
848
 
768
849
  await installNamedTemplate(runtimeRoot, paths, { mode: "copy", ...opts }, SETTINGS_TEMPLATE_DESCRIPTOR);
769
- await writeRuntimeManifest(paths.home, opts.runtime, { dryRun: opts.dryRun });
850
+ await writeRuntimeManifest(paths.home, opts.runtime, {
851
+ dryRun: opts.dryRun,
852
+ resolution: runtimeResolution,
853
+ installedAssets: installedAssetOverrides(installedSkillTargets, agentResult.installed),
854
+ });
770
855
 
771
856
  if (!opts.dryRun) {
772
857
  await writeFileAtomic(join(paths.home, "version"), paths.package + "\n", 0o644);
@@ -777,7 +862,7 @@ export async function runInstall(args) {
777
862
  process.stdout.write(
778
863
  "\nNext step: register the current project.\n" +
779
864
  " cd <your project> && npx -y okstra@latest setup --project-id <id>\n" +
780
- " (or run /okstra-setup inside a Claude Code session)\n" +
865
+ " (or run /okstra-setup inside a Harness Agent session (e.g. claude codex, codex, pi.. ))\n" +
781
866
  "\nTip: to use a bare 'okstra' command instead of npx, run:\n" +
782
867
  " npm i -g okstra\n",
783
868
  );
@@ -785,6 +870,19 @@ export async function runInstall(args) {
785
870
  return 0;
786
871
  }
787
872
 
873
+ async function skillTargetDriftReasons(targets) {
874
+ const reasons = [];
875
+ for (const target of targets) {
876
+ for (const name of ["okstra-run", LEAD_SKILL_NAME]) {
877
+ const skillPath = join(target.skillsDir, name, "SKILL.md");
878
+ if (!(await fileExists(skillPath))) {
879
+ reasons.push(`missing ${skillPath}`);
880
+ }
881
+ }
882
+ }
883
+ return reasons;
884
+ }
885
+
788
886
  async function agentDriftReasons(paths) {
789
887
  const reasons = [];
790
888
  const workersDir = join(paths.agents, "workers");
@@ -821,7 +919,7 @@ function summarise(label, result, target) {
821
919
  export function parseEnsureInstalledArgs(args) {
822
920
  const result = {
823
921
  quiet: false,
824
- runtime: "claude-code",
922
+ runtime: "auto",
825
923
  };
826
924
  for (let i = 0; i < args.length; i++) {
827
925
  const arg = args[i];
@@ -851,6 +949,16 @@ export async function runEnsureInstalled(args) {
851
949
  return 2;
852
950
  }
853
951
  const paths = await resolvePaths();
952
+ const runtimeResolution = resolveRuntime({
953
+ requestedRuntime: opts.runtime,
954
+ command: "ensure-installed",
955
+ env: process.env,
956
+ capabilities: { tmuxAvailable: Boolean(process.env.TMUX) },
957
+ });
958
+ if (!runtimeResolution.ok) {
959
+ process.stderr.write(`error: ${runtimeResolution.reason}\n`);
960
+ return 2;
961
+ }
854
962
 
855
963
  const reasons = [];
856
964
  if (!paths.version) reasons.push("no version stamp");
@@ -862,10 +970,11 @@ export async function runEnsureInstalled(args) {
862
970
  if (!(await fileExists(join(paths.home, "schemas", "final-report-v1.0.schema.json")))) {
863
971
  reasons.push(`missing ${join(paths.home, "schemas", "final-report-v1.0.schema.json")}`);
864
972
  }
865
- if (runtimeIncludesClaudeAssets(opts.runtime)) {
866
- if (!(await fileExists(join(CLAUDE_SKILLS_DIR, "okstra-setup", "SKILL.md")))) {
867
- reasons.push(`missing ${CLAUDE_SKILLS_DIR}/okstra-setup/SKILL.md`);
868
- }
973
+ const skillTargets = await detectSkillTargets();
974
+ for (const reason of await skillTargetDriftReasons(skillTargets)) {
975
+ reasons.push(reason);
976
+ }
977
+ if (skillTargets.some((target) => target.provider === "claude")) {
869
978
  for (const reason of await agentDriftReasons(paths)) {
870
979
  reasons.push(reason);
871
980
  }
@@ -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({