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/src/run.mjs ADDED
@@ -0,0 +1,201 @@
1
+ import { runEnsureInstalled } from "./install.mjs";
2
+ import { runPythonModule } from "./_python-helper.mjs";
3
+ import { resolvePaths } from "./paths.mjs";
4
+ import { resolveRuntime } from "./runtime-resolver.mjs";
5
+
6
+ const USAGE = `okstra run — host-aware okstra execution front door
7
+
8
+ Usage:
9
+ okstra run --project-root <dir> --project-id <id> --task-group <tg> \\
10
+ --task-id <tid> --task-type <type> [--lead-runtime auto|claude-code|codex|external]
11
+
12
+ Claude Code native execution remains the /okstra-run skill front door. Bare
13
+ 'okstra run' orchestrates Codex and external/tmux CLI execution.
14
+ `;
15
+
16
+ function flagValue(args, flag) {
17
+ const equals = args.find((arg) => arg.startsWith(`${flag}=`));
18
+ if (equals) return equals.slice(flag.length + 1);
19
+ const index = args.indexOf(flag);
20
+ return index >= 0 ? args[index + 1] : "";
21
+ }
22
+
23
+ export function requestedRuntimeFromArgs(args) {
24
+ return flagValue(args, "--lead-runtime") || "auto";
25
+ }
26
+
27
+ export function withoutLeadRuntimeFlags(args) {
28
+ const out = [];
29
+ for (let i = 0; i < args.length; i += 1) {
30
+ const arg = args[i];
31
+ if (arg === "--lead-runtime") {
32
+ i += 1;
33
+ continue;
34
+ }
35
+ if (arg.startsWith("--lead-runtime=")) continue;
36
+ out.push(arg);
37
+ }
38
+ return out;
39
+ }
40
+
41
+ export function extractLabel(stdout, label) {
42
+ const prefix = `${label}:`;
43
+ const line = String(stdout || "").split("\n").find((item) => item.startsWith(prefix));
44
+ return line ? line.slice(prefix.length).trim() : "";
45
+ }
46
+
47
+ function projectRootFromArgs(args) {
48
+ return flagValue(args, "--project-root");
49
+ }
50
+
51
+ function withoutDispatchOnlyFlags(args) {
52
+ const out = [];
53
+ for (let i = 0; i < args.length; i += 1) {
54
+ const arg = args[i];
55
+ if (arg === "--enable-codex-report-writer") continue;
56
+ if (arg === "--report-writer-codex-model") {
57
+ i += 1;
58
+ continue;
59
+ }
60
+ if (arg.startsWith("--report-writer-codex-model=")) continue;
61
+ out.push(arg);
62
+ }
63
+ return out;
64
+ }
65
+
66
+ function renderArgs(args, resolution) {
67
+ return [
68
+ "--lead-runtime", resolution.resolvedRuntime,
69
+ "--lead-runtime-request", resolution.requestedRuntime,
70
+ "--runtime-resolution-json", JSON.stringify(resolution),
71
+ ...withoutDispatchOnlyFlags(withoutLeadRuntimeFlags(args)),
72
+ ];
73
+ }
74
+
75
+ function codexDispatchArgs(args, projectRoot, runManifestPath) {
76
+ const out = ["--project-root", projectRoot, "--run-manifest", runManifestPath];
77
+ for (let i = 0; i < args.length; i += 1) {
78
+ const arg = args[i];
79
+ if (arg === "--enable-codex-report-writer") {
80
+ out.push(arg);
81
+ continue;
82
+ }
83
+ if (arg === "--report-writer-codex-model") {
84
+ out.push(arg, args[i + 1] ?? "");
85
+ i += 1;
86
+ continue;
87
+ }
88
+ if (arg.startsWith("--report-writer-codex-model=")) out.push(arg);
89
+ }
90
+ return out;
91
+ }
92
+
93
+ export function buildRunPlan({ args, paths, resolution, runManifestPath = "" }) {
94
+ void paths;
95
+ const projectRoot = projectRootFromArgs(args);
96
+ if (resolution.resolvedRuntime === "claude-code") {
97
+ return {
98
+ ok: false,
99
+ reason: "Bare okstra run cannot invoke Claude Code TeamCreate / Agent(...). Use /okstra-run in Claude Code or pass --lead-runtime external|codex.",
100
+ commands: [],
101
+ };
102
+ }
103
+
104
+ if (resolution.resolvedRuntime === "external") {
105
+ return {
106
+ ok: true,
107
+ reason: resolution.reason,
108
+ commands: [
109
+ { name: "render-bundle", args: renderArgs(args, resolution), capture: true },
110
+ { name: "team dispatch", args: ["dispatch", "--project-root", projectRoot, "--run-manifest", runManifestPath], capture: false },
111
+ { name: "team await", args: ["await", "--project-root", projectRoot, "--run-manifest", runManifestPath], capture: false },
112
+ ],
113
+ };
114
+ }
115
+
116
+ if (resolution.resolvedRuntime === "codex") {
117
+ return {
118
+ ok: true,
119
+ reason: resolution.reason,
120
+ commands: [
121
+ { name: "codex-run", args: renderArgs(args, resolution), capture: true },
122
+ { name: "codex-dispatch", args: codexDispatchArgs(args, projectRoot, runManifestPath), capture: false },
123
+ ],
124
+ };
125
+ }
126
+
127
+ return { ok: false, reason: `unsupported resolved runtime: ${resolution.resolvedRuntime}`, commands: [] };
128
+ }
129
+
130
+ async function ensureRuntime(requestedRuntime) {
131
+ return await runEnsureInstalled(["--runtime", requestedRuntime, "-q"]);
132
+ }
133
+
134
+ async function runRenderCommand(command, paths) {
135
+ const result = await runPythonModule({
136
+ module: "okstra_ctl.run",
137
+ args: ["--workspace-root", paths.workspace, "--render-only", ...command.args],
138
+ stdio: "capture",
139
+ });
140
+ process.stdout.write(result.stdout);
141
+ if (result.stderr) process.stderr.write(result.stderr);
142
+ return result;
143
+ }
144
+
145
+ async function runTeam(args) {
146
+ const { run: runTeamCommand } = await import("./team.mjs");
147
+ return await runTeamCommand(args);
148
+ }
149
+
150
+ async function runCodexDispatch(args) {
151
+ const { run: runCodexDispatchCommand } = await import("./codex-dispatch.mjs");
152
+ return await runCodexDispatchCommand(args);
153
+ }
154
+
155
+ export async function run(args) {
156
+ if (args.includes("--help") || args.includes("-h")) {
157
+ process.stdout.write(USAGE);
158
+ return 0;
159
+ }
160
+
161
+ const paths = await resolvePaths();
162
+ const requestedRuntime = requestedRuntimeFromArgs(args);
163
+ const resolution = resolveRuntime({
164
+ requestedRuntime,
165
+ command: "run",
166
+ env: process.env,
167
+ capabilities: { tmuxAvailable: Boolean(process.env.TMUX) },
168
+ });
169
+ if (!resolution.ok) {
170
+ process.stderr.write(`error: ${resolution.reason}\n`);
171
+ return 2;
172
+ }
173
+
174
+ const firstPlan = buildRunPlan({ args, paths, resolution });
175
+ if (!firstPlan.ok) {
176
+ process.stderr.write(`error: ${firstPlan.reason}\n`);
177
+ return 2;
178
+ }
179
+
180
+ const ensureCode = await ensureRuntime(requestedRuntime);
181
+ if (ensureCode !== 0) return ensureCode;
182
+
183
+ const render = firstPlan.commands[0];
184
+ const prepared = await runRenderCommand(render, paths);
185
+ if (prepared.code !== 0) return prepared.code ?? 1;
186
+
187
+ const runManifestPath = extractLabel(prepared.stdout, "okstra run manifest");
188
+ if (!runManifestPath) {
189
+ process.stderr.write("error: render output did not include 'okstra run manifest:'\n");
190
+ return 1;
191
+ }
192
+
193
+ const plan = buildRunPlan({ args, paths, resolution, runManifestPath });
194
+ for (const command of plan.commands.slice(1)) {
195
+ const code = command.name.startsWith("team ")
196
+ ? await runTeam(command.args)
197
+ : await runCodexDispatch(command.args);
198
+ if (code !== 0) return code;
199
+ }
200
+ return 0;
201
+ }
@@ -1,29 +1,72 @@
1
1
  export const RUNTIMES_MANIFEST_REL = "installed-runtimes.json";
2
+ export const RUNTIME_MANIFEST_SCHEMA_VERSION = 2;
3
+ export const EXPLICIT_RUNTIME_VALUES = ["claude-code", "codex", "external"];
2
4
 
3
- export function runtimeListForSelection(runtime) {
4
- if (runtime === "all") return ["claude-code", "codex", "external"];
5
+ export function runtimeListForSelection(runtime, resolution = null) {
6
+ if (runtime === "all") return [...EXPLICIT_RUNTIME_VALUES];
7
+ if (runtime === "auto") {
8
+ const resolved = resolution?.resolvedRuntime;
9
+ return resolved && resolved !== "all" ? [resolved] : [];
10
+ }
5
11
  return [runtime];
6
12
  }
7
13
 
8
- export function buildRuntimeManifest(runtime, installedAt = new Date().toISOString()) {
9
- const includesClaude = runtime === "claude-code" || runtime === "all";
10
- const includesCodex = runtime === "codex" || runtime === "all";
11
- const includesExternal = runtime === "external" || runtime === "all";
14
+ function includesClaudeRuntime(runtime, resolution = null) {
15
+ if (runtime === "all") return true;
16
+ if (runtime === "claude-code") return true;
17
+ return runtime === "auto" && resolution?.resolvedRuntime === "claude-code";
18
+ }
19
+
20
+ function defaultResolution(runtime) {
21
+ if (runtime === "auto") return null;
12
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(
34
+ runtime,
35
+ installedAt = new Date().toISOString(),
36
+ resolution = null,
37
+ installedAssets = null,
38
+ ) {
39
+ const runtimeResolution = resolution ?? defaultResolution(runtime);
40
+ const includesClaude = includesClaudeRuntime(runtime, runtimeResolution);
41
+ const defaultAssets = {
42
+ sharedRuntime: true,
43
+ claudeSkills: includesClaude,
44
+ claudeAgents: includesClaude,
45
+ binWrappers: true,
46
+ templates: true,
47
+ schemas: true,
48
+ validators: true,
49
+ };
50
+ return {
51
+ schemaVersion: RUNTIME_MANIFEST_SCHEMA_VERSION,
13
52
  installedAt,
14
- runtime,
15
- runtimes: runtimeListForSelection(runtime),
16
- assets: {
17
- sharedRuntime: true,
18
- claudeSkills: includesClaude,
19
- claudeAgents: includesClaude,
20
- codexAdapter: includesCodex,
21
- externalAdapter: includesExternal,
22
- },
23
- version: 1,
53
+ installRequest: runtime,
54
+ runtimeResolution,
55
+ installedRuntimes: runtimeListForSelection(runtime, runtimeResolution),
56
+ installedAssets: { ...defaultAssets, ...(installedAssets ?? {}) },
24
57
  };
25
58
  }
26
59
 
60
+ export function installedRuntimesFromManifest(data) {
61
+ if (Array.isArray(data?.installedRuntimes)) return data.installedRuntimes;
62
+ if (Array.isArray(data?.runtimes)) return data.runtimes;
63
+ if (typeof data?.runtime === "string" && data.runtime) return runtimeListForSelection(data.runtime);
64
+ return [];
65
+ }
66
+
27
67
  export function runtimeManifestIncludesClaudeAssets(data) {
68
+ if (data?.schemaVersion === RUNTIME_MANIFEST_SCHEMA_VERSION) {
69
+ return data?.installedAssets?.claudeSkills === true || data?.installedAssets?.claudeAgents === true;
70
+ }
28
71
  return data?.assets?.claudeSkills === true || data?.assets?.claudeAgents === true;
29
72
  }
@@ -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
+ }
package/src/uninstall.mjs CHANGED
@@ -51,22 +51,36 @@ const FALLBACK_AGENT_NAMES = [
51
51
  "report-writer-worker.md",
52
52
  ];
53
53
 
54
- const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
55
- const CLAUDE_AGENTS_DIR = join(homedir(), ".claude", "agents");
54
+ const USER_HOME = homedir();
55
+ const CLAUDE_HOME = join(USER_HOME, ".claude");
56
+ const CODEX_HOME = join(USER_HOME, ".codex");
57
+ const OMP_AGENT_HOME = join(USER_HOME, ".omp", "agent");
58
+ const CLAUDE_SKILLS_DIR = join(CLAUDE_HOME, "skills");
59
+ const CODEX_SKILLS_DIR = join(CODEX_HOME, "skills");
60
+ const OMP_SKILLS_DIR = join(OMP_AGENT_HOME, "skills");
61
+ const CLAUDE_AGENTS_DIR = join(CLAUDE_HOME, "agents");
62
+ const FALLBACK_SKILL_ROOTS = [
63
+ CLAUDE_SKILLS_DIR,
64
+ CODEX_SKILLS_DIR,
65
+ OMP_SKILLS_DIR,
66
+ ];
56
67
  const SKILLS_MANIFEST_REL = "installed-skills.json";
57
68
  const AGENTS_MANIFEST_REL = "installed-agents.json";
58
69
 
59
- const USAGE = `okstra uninstall — remove installed runtime from ~/.okstra, ~/.claude/skills, ~/.claude/agents
70
+ const USAGE = `okstra uninstall — remove installed runtime and okstra skills
60
71
 
61
72
  Usage:
62
73
  okstra uninstall Remove ~/.okstra/{lib (python + validators),
63
74
  bin/<known>, schemas, templates, version,
64
75
  dev-link, installed-runtimes.json,
65
76
  installed-skills.json, installed-agents.json} AND
66
- ~/.claude/skills/<name> AND
67
- ~/.claude/agents/<worker>.md for every
68
- entry in the install manifests (fallback:
69
- hard-coded okstra-* / *-worker.md names).
77
+ every skill target recorded in
78
+ installed-skills.json
79
+ (~/.claude/skills, ~/.codex/skills, or
80
+ ~/.omp/agent/skills). Claude worker agents are
81
+ removed from ~/.claude/agents when installed.
82
+ Missing manifests fall back to hard-coded
83
+ Claude okstra-* / *-worker.md names.
70
84
  Preserves user data: recent.jsonl,
71
85
  active.jsonl, projects/, archive/,
72
86
  state.json, .locks/
@@ -74,7 +88,7 @@ Usage:
74
88
  (<PROJECT>/.claude/settings.local.json symlink)
75
89
  are NOT touched — remove them manually if needed.
76
90
  okstra uninstall --purge Remove the entire ~/.okstra directory AND
77
- ~/.claude/skills/<okstra-*> AND
91
+ okstra skills in Claude/Codex/OMP homes AND
78
92
  ~/.claude/agents/<*-worker.md> (DESTRUCTIVE).
79
93
  Requires -y or an interactive confirmation
80
94
  okstra uninstall --dry-run Print the plan without touching disk
@@ -138,7 +152,7 @@ export async function runUninstall(args) {
138
152
  if (opts.purge) {
139
153
  if (!opts.yes && !opts.dryRun) {
140
154
  const ok = await promptConfirm(
141
- `purge entire ${paths.home} AND ~/.claude/skills/{okstra-*,okstra-setup}? user data will be lost.`,
155
+ `purge entire ${paths.home} AND okstra skills in Claude/Codex/OMP homes? user data will be lost.`,
142
156
  );
143
157
  if (!ok) {
144
158
  process.stdout.write("aborted.\n");
@@ -148,8 +162,10 @@ export async function runUninstall(args) {
148
162
  if (!opts.quiet) process.stdout.write(`purging ${paths.home}\n`);
149
163
  await removePath(paths.home, opts);
150
164
  // Skills and worker agents live outside ~/.okstra — purge those too.
151
- for (const name of FALLBACK_SKILL_NAMES) {
152
- await removePath(join(CLAUDE_SKILLS_DIR, name), opts);
165
+ for (const root of FALLBACK_SKILL_ROOTS) {
166
+ for (const name of FALLBACK_SKILL_NAMES) {
167
+ await removePath(join(root, name), opts);
168
+ }
153
169
  }
154
170
  for (const name of FALLBACK_AGENT_NAMES) {
155
171
  await removePath(join(CLAUDE_AGENTS_DIR, name), opts);
@@ -190,22 +206,26 @@ export async function runUninstall(args) {
190
206
  // never removed it, leaving a stale final-report schema behind.
191
207
  await removePath(join(paths.home, "schemas"), opts);
192
208
 
193
- // Remove the skills we own. Manifest is authoritative; fall back to the
194
- // hard-coded okstra-* names if it is missing (e.g. an install from an old
195
- // version that did not write the manifest).
209
+ // Remove the skills we own. Manifest v2 records every target root; legacy
210
+ // manifests fall back to Claude-only names when the runtime manifest says
211
+ // Claude assets may have been installed.
196
212
  const runtimeManifest = await readRuntimeManifest(paths.home);
197
- const shouldRemoveClaudeAssets = shouldUseClaudeAssetFallback(runtimeManifest);
198
- const skillNames = shouldRemoveClaudeAssets ? await readSkillsManifest(paths.home) : [];
199
- if (!opts.quiet) {
200
- process.stdout.write(` skills: removing ${skillNames.length} entries from ${CLAUDE_SKILLS_DIR}\n`);
201
- }
202
- for (const name of skillNames) {
203
- await removePath(join(CLAUDE_SKILLS_DIR, name), opts);
213
+ const skillTargets = await readSkillsManifest(paths.home, runtimeManifest);
214
+ for (const target of skillTargets) {
215
+ if (!opts.quiet) {
216
+ process.stdout.write(` skills: removing ${target.skills.length} entries from ${target.root}\n`);
217
+ }
218
+ for (const name of target.skills) {
219
+ await removePath(join(target.root, name), opts);
220
+ }
204
221
  }
205
222
  await removePath(join(paths.home, SKILLS_MANIFEST_REL), opts);
206
223
 
207
- // Remove worker agents we own. Same authoritative-manifest pattern as skills.
208
- const agentNames = shouldRemoveClaudeAssets ? await readAgentsManifest(paths.home) : [];
224
+ // Remove worker agents we own. Agents only live in Claude Code's agent home.
225
+ const shouldRemoveClaudeAgents =
226
+ skillTargets.some((target) => target.provider === "claude") ||
227
+ shouldUseClaudeAssetFallback(runtimeManifest);
228
+ const agentNames = shouldRemoveClaudeAgents ? await readAgentsManifest(paths.home) : [];
209
229
  if (!opts.quiet) {
210
230
  process.stdout.write(` agents: removing ${agentNames.length} entries from ${CLAUDE_AGENTS_DIR}\n`);
211
231
  }
@@ -248,15 +268,38 @@ export function shouldUseClaudeAssetFallback(runtimeManifest) {
248
268
  return runtimeManifestIncludesClaudeAssets(runtimeManifest);
249
269
  }
250
270
 
251
- async function readSkillsManifest(home) {
271
+ function normalizeSkillTargets(data) {
272
+ if (Array.isArray(data?.targets)) {
273
+ return data.targets
274
+ .filter((target) => typeof target?.root === "string" && Array.isArray(target?.skills))
275
+ .map((target) => ({
276
+ provider: typeof target.provider === "string" ? target.provider : "unknown",
277
+ root: target.root,
278
+ skills: target.skills.filter((name) => typeof name === "string"),
279
+ }));
280
+ }
281
+ if (Array.isArray(data?.skills)) {
282
+ return [{
283
+ provider: "claude",
284
+ root: CLAUDE_SKILLS_DIR,
285
+ skills: data.skills.filter((name) => typeof name === "string"),
286
+ }];
287
+ }
288
+ return null;
289
+ }
290
+
291
+ async function readSkillsManifest(home, runtimeManifest) {
252
292
  try {
253
293
  const raw = await fs.readFile(join(home, SKILLS_MANIFEST_REL), "utf8");
254
294
  const data = JSON.parse(raw);
255
- if (Array.isArray(data?.skills)) return data.skills;
295
+ const targets = normalizeSkillTargets(data);
296
+ if (targets !== null) return targets;
256
297
  } catch {
257
298
  /* fall through */
258
299
  }
259
- return FALLBACK_SKILL_NAMES;
300
+ return shouldUseClaudeAssetFallback(runtimeManifest)
301
+ ? [{ provider: "claude", root: CLAUDE_SKILLS_DIR, skills: FALLBACK_SKILL_NAMES }]
302
+ : [];
260
303
  }
261
304
 
262
305
  async function readAgentsManifest(home) {