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.
- package/README.kr.md +11 -9
- package/README.md +11 -9
- package/docs/kr/architecture.md +7 -7
- package/docs/kr/cli.md +16 -4
- package/docs/project-structure-overview.md +6 -4
- 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/agents/SKILL.md +1 -0
- package/runtime/python/okstra_ctl/render.py +11 -0
- package/runtime/python/okstra_ctl/run.py +25 -0
- package/runtime/skills/okstra-brief/SKILL.md +2 -8
- package/runtime/skills/okstra-coding-preflight/SKILL.md +1 -0
- package/runtime/skills/okstra-context-loader/SKILL.md +1 -0
- package/runtime/skills/okstra-convergence/SKILL.md +1 -0
- package/runtime/skills/okstra-inspect/SKILL.md +2 -2
- package/runtime/skills/okstra-report-writer/SKILL.md +1 -0
- package/runtime/skills/okstra-run/SKILL.md +7 -6
- package/runtime/skills/okstra-schedule/SKILL.md +2 -2
- package/runtime/skills/okstra-setup/SKILL.md +2 -2
- package/runtime/skills/okstra-team-contract/SKILL.md +1 -0
- package/src/cli-registry.mjs +8 -1
- package/src/doctor.mjs +17 -3
- package/src/install.mjs +203 -94
- package/src/render-bundle.mjs +38 -1
- package/src/run.mjs +201 -0
- package/src/runtime-manifest.mjs +59 -16
- package/src/runtime-resolver.mjs +123 -0
- package/src/uninstall.mjs +69 -26
|
@@ -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
|
package/src/cli-registry.mjs
CHANGED
|
@@ -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;
|
|
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: "
|
|
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,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
|
|
13
|
-
const
|
|
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> <-
|
|
55
|
-
${"$HOME"}/.
|
|
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>
|
|
66
|
-
${"$HOME"}/.
|
|
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/
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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, {
|
|
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
|
|
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
|
|
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:
|
|
406
|
+
version: 2,
|
|
352
407
|
installedAt: new Date().toISOString(),
|
|
353
|
-
skills:
|
|
408
|
+
skills: installedSkillNames(targets),
|
|
409
|
+
targets: manifestTargets,
|
|
354
410
|
};
|
|
355
411
|
if (dryRun) {
|
|
356
|
-
process.stdout.write(
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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=${
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
627
|
+
const lead = await installLeadSkillLink(repoAbs, target, opts);
|
|
628
|
+
return { ...target, installed: [...names, ...lead] };
|
|
567
629
|
}
|
|
568
630
|
|
|
569
|
-
|
|
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(
|
|
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)
|
|
584
|
-
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
|
|
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)
|
|
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(
|
|
600
|
-
if (!dryRun) await fs.mkdir(join(
|
|
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: "
|
|
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
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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, {
|
|
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
|
|
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: "
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
}
|
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({
|