okstra 0.75.0 → 0.77.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.
Files changed (46) hide show
  1. package/README.kr.md +5 -3
  2. package/README.md +5 -3
  3. package/bin/okstra +10 -2
  4. package/docs/kr/architecture.md +1 -1
  5. package/docs/kr/cli.md +20 -4
  6. package/docs/pr-template-usage.md +3 -3
  7. package/docs/superpowers/plans/2026-05-25-okstra-project-root-rename.md +1 -1
  8. package/docs/superpowers/specs/2026-06-12-codex-lead-adapter-design.md +358 -0
  9. package/package.json +1 -1
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/agents/workers/codex-worker.md +1 -1
  12. package/runtime/agents/workers/gemini-worker.md +1 -1
  13. package/runtime/bin/lib/okstra/cli.sh +5 -1
  14. package/runtime/bin/lib/okstra/globals.sh +1 -0
  15. package/runtime/bin/lib/okstra/usage.sh +6 -4
  16. package/runtime/bin/okstra.sh +1 -0
  17. package/runtime/prompts/launch.template.md +4 -13
  18. package/runtime/prompts/profiles/_coding-conventions-preflight.md +21 -0
  19. package/runtime/prompts/profiles/_implementation-executor.md +3 -6
  20. package/runtime/prompts/profiles/error-analysis.md +6 -0
  21. package/runtime/prompts/profiles/improvement-discovery.md +5 -0
  22. package/runtime/prompts/profiles/release-handoff.md +2 -2
  23. package/runtime/prompts/wizard/prompts.ko.json +12 -4
  24. package/runtime/python/okstra_ctl/analysis_packet.py +17 -0
  25. package/runtime/python/okstra_ctl/codex_dispatch.py +1484 -0
  26. package/runtime/python/okstra_ctl/lead_events.py +129 -0
  27. package/runtime/python/okstra_ctl/paths.py +3 -0
  28. package/runtime/python/okstra_ctl/pr_template.py +1 -1
  29. package/runtime/python/okstra_ctl/render.py +160 -29
  30. package/runtime/python/okstra_ctl/run.py +78 -15
  31. package/runtime/python/okstra_ctl/wizard.py +23 -9
  32. package/runtime/python/okstra_token_usage/codex.py +12 -7
  33. package/runtime/python/okstra_token_usage/collect.py +243 -54
  34. package/runtime/python/okstra_token_usage/gemini.py +12 -7
  35. package/runtime/skills/okstra-setup/SKILL.md +1 -1
  36. package/runtime/validators/validate-run.py +56 -22
  37. package/runtime/validators/validate_session_conformance.py +121 -4
  38. package/src/codex-dispatch.mjs +70 -0
  39. package/src/codex-run.mjs +68 -0
  40. package/src/doctor.mjs +40 -4
  41. package/src/install.mjs +122 -26
  42. package/src/paths.mjs +17 -3
  43. package/src/render-bundle.mjs +2 -0
  44. package/src/runtime-manifest.mjs +26 -0
  45. package/src/uninstall.mjs +26 -4
  46. /package/runtime/{skills/okstra-run/templates → templates/prd}/pr-body.template.md +0 -0
package/src/install.mjs CHANGED
@@ -5,6 +5,7 @@ import { join, relative, resolve as resolveAbs } from "node:path";
5
5
  import { getPackageRoot } from "./version.mjs";
6
6
  import { resolvePaths } from "./paths.mjs";
7
7
  import { fileExists } from "./_proc.mjs";
8
+ import { buildRuntimeManifest, RUNTIMES_MANIFEST_REL } from "./runtime-manifest.mjs";
8
9
 
9
10
  const SKILLS_MANIFEST_REL = "installed-skills.json";
10
11
  const AGENTS_MANIFEST_REL = "installed-agents.json";
@@ -36,6 +37,7 @@ const INSTALL_USAGE = `okstra install — install runtime into ~/.okstra
36
37
 
37
38
  Usage:
38
39
  okstra install Install/update runtime (copy mode)
40
+ okstra install --runtime <claude-code|codex|all>
39
41
  okstra install --link <p> Dev only: symlink ~/.okstra/{lib/python,bin}
40
42
  to the source repo at <p>. agents/ resolves
41
43
  to <p>/agents. Not advertised to end users.
@@ -50,6 +52,7 @@ Effect (copy mode):
50
52
  ${"$HOME"}/.okstra/templates/settings.local.json <- runtime/templates/reports/settings.template.json
51
53
  ${"$HOME"}/.claude/skills/<name> <- runtime/skills/<name> (per skill)
52
54
  ${"$HOME"}/.claude/agents/<worker>.md <- runtime/agents/workers/<worker>.md
55
+ ${"$HOME"}/.okstra/installed-runtimes.json <- manifest of installed runtime adapters
53
56
  ${"$HOME"}/.okstra/installed-skills.json <- manifest of installed skills
54
57
  ${"$HOME"}/.okstra/installed-agents.json <- manifest of installed workers
55
58
  ${"$HOME"}/.okstra/version <- installed package version stamp
@@ -78,11 +81,37 @@ const ENSURE_USAGE = `okstra ensure-installed — idempotent install check
78
81
 
79
82
  Usage:
80
83
  okstra ensure-installed Verify install; reinstall if version/files drift
84
+ okstra ensure-installed --runtime <claude-code|codex|all>
81
85
  okstra ensure-installed -q Same, but suppress success output
82
86
 
83
87
  Skills call this on every run. Returns 0 quickly when the install is fresh.
84
88
  `;
85
89
 
90
+ const INSTALL_RUNTIMES = new Set(["claude-code", "codex", "all"]);
91
+
92
+ export function runtimeIncludesClaudeAssets(runtime) {
93
+ return runtime === "claude-code" || runtime === "all";
94
+ }
95
+
96
+ function readRuntimeFlag(args, index) {
97
+ const arg = args[index];
98
+ if (arg.startsWith("--runtime=")) {
99
+ return { value: arg.slice("--runtime=".length), nextIndex: index };
100
+ }
101
+ const next = args[index + 1];
102
+ if (!next || next.startsWith("--")) {
103
+ throw new Error("--runtime requires one of: claude-code, codex, all");
104
+ }
105
+ return { value: next, nextIndex: index + 1 };
106
+ }
107
+
108
+ export function validateInstallRuntime(value) {
109
+ if (!INSTALL_RUNTIMES.has(value)) {
110
+ throw new Error(`unknown runtime '${value}' (expected: claude-code, codex, all)`);
111
+ }
112
+ return value;
113
+ }
114
+
86
115
  async function hashFile(path) {
87
116
  const buf = await fs.readFile(path);
88
117
  return createHash("sha256").update(buf).digest("hex");
@@ -223,7 +252,7 @@ async function ensureSymlink(target, linkPath, opts) {
223
252
  }
224
253
 
225
254
  async function installLinkMode(repoPath, paths, opts) {
226
- const { dryRun, quiet } = opts;
255
+ const { dryRun, quiet, runtime } = opts;
227
256
  const repoAbs = resolveAbs(repoPath);
228
257
 
229
258
  if (!(await dirExists(repoAbs))) {
@@ -240,6 +269,7 @@ async function installLinkMode(repoPath, paths, opts) {
240
269
 
241
270
  if (!quiet) {
242
271
  process.stdout.write(`installing okstra runtime in LINK mode (package ${paths.package})\n`);
272
+ process.stdout.write(` runtime: ${runtime}\n`);
243
273
  process.stdout.write(` repo: ${repoAbs}\n`);
244
274
  process.stdout.write(` home: ${paths.home}\n`);
245
275
  }
@@ -271,14 +301,19 @@ async function installLinkMode(repoPath, paths, opts) {
271
301
  if (!quiet) process.stdout.write(` bin/${name}: ${action}\n`);
272
302
  }
273
303
 
274
- const skillResult = await installSkillsLink(repoAbs, { dryRun, quiet });
275
- const leadSkillResult = await installLeadSkillLink(repoAbs, { dryRun, quiet });
276
- await writeSkillsManifest(paths.home, [...skillResult.installed, ...leadSkillResult], { dryRun });
304
+ if (runtimeIncludesClaudeAssets(runtime)) {
305
+ const skillResult = await installSkillsLink(repoAbs, { dryRun, quiet });
306
+ const leadSkillResult = await installLeadSkillLink(repoAbs, { dryRun, quiet });
307
+ await writeSkillsManifest(paths.home, [...skillResult.installed, ...leadSkillResult], { dryRun });
277
308
 
278
- const agentResult = await installAgentsLink(repoAbs, { dryRun, quiet });
279
- await writeAgentsManifest(paths.home, agentResult.installed, { dryRun });
309
+ const agentResult = await installAgentsLink(repoAbs, { dryRun, quiet });
310
+ await writeAgentsManifest(paths.home, agentResult.installed, { dryRun });
311
+ } else if (!quiet) {
312
+ process.stdout.write(" claude assets: skipped for runtime=codex\n");
313
+ }
280
314
 
281
315
  await installNamedTemplate(repoAbs, paths, { mode: "link", dryRun, quiet }, SETTINGS_TEMPLATE_DESCRIPTOR);
316
+ await writeRuntimeManifest(paths.home, runtime, { dryRun });
282
317
 
283
318
  if (!dryRun) {
284
319
  await writeFileAtomic(join(paths.home, "dev-link"), repoAbs + "\n", 0o644);
@@ -345,6 +380,20 @@ async function writeAgentsManifest(home, names, opts) {
345
380
  );
346
381
  }
347
382
 
383
+ async function writeRuntimeManifest(home, runtime, opts) {
384
+ const { dryRun = false } = opts ?? {};
385
+ const data = buildRuntimeManifest(runtime);
386
+ if (dryRun) {
387
+ process.stdout.write(`[dry-run] write runtime manifest: ${data.runtimes.join(",")}\n`);
388
+ return;
389
+ }
390
+ await writeFileAtomic(
391
+ join(home, RUNTIMES_MANIFEST_REL),
392
+ JSON.stringify(data, null, 2) + "\n",
393
+ 0o644,
394
+ );
395
+ }
396
+
348
397
  async function listWorkerFiles(workersRoot) {
349
398
  try {
350
399
  const entries = await fs.readdir(workersRoot, { withFileTypes: true });
@@ -553,19 +602,24 @@ async function installLeadSkillLink(repoAbs, opts) {
553
602
  return [LEAD_SKILL_NAME];
554
603
  }
555
604
 
556
- function parseInstallArgs(args) {
605
+ export function parseInstallArgs(args) {
557
606
  const result = {
558
607
  dryRun: false,
559
608
  refresh: false,
560
609
  quiet: false,
561
610
  linkRepo: null,
611
+ runtime: "claude-code",
562
612
  };
563
613
  for (let i = 0; i < args.length; i++) {
564
614
  const a = args[i];
565
615
  if (a === "--dry-run") result.dryRun = true;
566
616
  else if (a === "--refresh") result.refresh = true;
567
617
  else if (a === "-q" || a === "--quiet") result.quiet = true;
568
- else if (a === "--link") {
618
+ else if (a === "--runtime" || a.startsWith("--runtime=")) {
619
+ const parsed = readRuntimeFlag(args, i);
620
+ result.runtime = validateInstallRuntime(parsed.value);
621
+ i = parsed.nextIndex;
622
+ } else if (a === "--link") {
569
623
  const next = args[i + 1];
570
624
  if (!next || next.startsWith("--")) {
571
625
  throw new Error("--link requires a repo path");
@@ -585,7 +639,13 @@ export async function runInstall(args) {
585
639
  return 0;
586
640
  }
587
641
 
588
- const opts = parseInstallArgs(args);
642
+ let opts;
643
+ try {
644
+ opts = parseInstallArgs(args);
645
+ } catch (err) {
646
+ process.stderr.write(`error: ${err.message}\n`);
647
+ return 2;
648
+ }
589
649
  const pkgRoot = getPackageRoot();
590
650
  const runtimeRoot = join(pkgRoot, "runtime");
591
651
  const paths = await resolvePaths();
@@ -607,6 +667,7 @@ export async function runInstall(args) {
607
667
 
608
668
  if (!opts.quiet) {
609
669
  process.stdout.write(`installing okstra runtime (package ${paths.package})\n`);
670
+ process.stdout.write(` runtime: ${opts.runtime}\n`);
610
671
  process.stdout.write(` source: ${runtimeRoot}\n`);
611
672
  process.stdout.write(` home: ${paths.home}\n`);
612
673
  }
@@ -688,18 +749,23 @@ export async function runInstall(args) {
688
749
  );
689
750
  }
690
751
 
691
- const skillResult = await installSkillsCopy(runtimeRoot, opts);
692
- const leadSkillResult = await installLeadSkillCopy(runtimeRoot, opts);
693
- await writeSkillsManifest(
694
- paths.home,
695
- [...skillResult.installed, ...leadSkillResult],
696
- { dryRun: opts.dryRun },
697
- );
752
+ if (runtimeIncludesClaudeAssets(opts.runtime)) {
753
+ const skillResult = await installSkillsCopy(runtimeRoot, opts);
754
+ const leadSkillResult = await installLeadSkillCopy(runtimeRoot, opts);
755
+ await writeSkillsManifest(
756
+ paths.home,
757
+ [...skillResult.installed, ...leadSkillResult],
758
+ { dryRun: opts.dryRun },
759
+ );
698
760
 
699
- const agentResult = await installAgentsCopy(runtimeRoot, opts);
700
- await writeAgentsManifest(paths.home, agentResult.installed, { dryRun: opts.dryRun });
761
+ const agentResult = await installAgentsCopy(runtimeRoot, opts);
762
+ await writeAgentsManifest(paths.home, agentResult.installed, { dryRun: opts.dryRun });
763
+ } else if (!opts.quiet) {
764
+ process.stdout.write(" claude assets: skipped for runtime=codex\n");
765
+ }
701
766
 
702
767
  await installNamedTemplate(runtimeRoot, paths, { mode: "copy", ...opts }, SETTINGS_TEMPLATE_DESCRIPTOR);
768
+ await writeRuntimeManifest(paths.home, opts.runtime, { dryRun: opts.dryRun });
703
769
 
704
770
  if (!opts.dryRun) {
705
771
  await writeFileAtomic(join(paths.home, "version"), paths.package + "\n", 0o644);
@@ -751,12 +817,38 @@ function summarise(label, result, target) {
751
817
  );
752
818
  }
753
819
 
820
+ export function parseEnsureInstalledArgs(args) {
821
+ const result = {
822
+ quiet: false,
823
+ runtime: "claude-code",
824
+ };
825
+ for (let i = 0; i < args.length; i++) {
826
+ const arg = args[i];
827
+ if (arg === "-q" || arg === "--quiet") {
828
+ result.quiet = true;
829
+ } else if (arg === "--runtime" || arg.startsWith("--runtime=")) {
830
+ const parsed = readRuntimeFlag(args, i);
831
+ result.runtime = validateInstallRuntime(parsed.value);
832
+ i = parsed.nextIndex;
833
+ } else {
834
+ throw new Error(`unknown argument '${arg}'`);
835
+ }
836
+ }
837
+ return result;
838
+ }
839
+
754
840
  export async function runEnsureInstalled(args) {
755
841
  if (args.includes("--help") || args.includes("-h")) {
756
842
  process.stdout.write(ENSURE_USAGE);
757
843
  return 0;
758
844
  }
759
- const quiet = args.includes("-q") || args.includes("--quiet");
845
+ let opts;
846
+ try {
847
+ opts = parseEnsureInstalledArgs(args);
848
+ } catch (err) {
849
+ process.stderr.write(`error: ${err.message}\n`);
850
+ return 2;
851
+ }
760
852
  const paths = await resolvePaths();
761
853
 
762
854
  const reasons = [];
@@ -769,18 +861,22 @@ export async function runEnsureInstalled(args) {
769
861
  if (!(await fileExists(join(paths.home, "schemas", "final-report-v1.0.schema.json")))) {
770
862
  reasons.push(`missing ${join(paths.home, "schemas", "final-report-v1.0.schema.json")}`);
771
863
  }
772
- if (!(await fileExists(join(CLAUDE_SKILLS_DIR, "okstra-setup", "SKILL.md")))) {
773
- reasons.push(`missing ${CLAUDE_SKILLS_DIR}/okstra-setup/SKILL.md`);
774
- }
775
- for (const reason of await agentDriftReasons(paths)) {
776
- reasons.push(reason);
864
+ if (runtimeIncludesClaudeAssets(opts.runtime)) {
865
+ if (!(await fileExists(join(CLAUDE_SKILLS_DIR, "okstra-setup", "SKILL.md")))) {
866
+ reasons.push(`missing ${CLAUDE_SKILLS_DIR}/okstra-setup/SKILL.md`);
867
+ }
868
+ for (const reason of await agentDriftReasons(paths)) {
869
+ reasons.push(reason);
870
+ }
777
871
  }
778
872
 
779
873
  if (reasons.length === 0) {
780
- if (!quiet) process.stdout.write(`okstra runtime OK (package ${paths.package})\n`);
874
+ if (!opts.quiet) process.stdout.write(`okstra runtime OK (package ${paths.package})\n`);
781
875
  return 0;
782
876
  }
783
877
 
784
878
  process.stderr.write(`okstra runtime needs install: ${reasons.join("; ")}\n`);
785
- return await runInstall(quiet ? ["-q"] : []);
879
+ const installArgs = ["--runtime", opts.runtime];
880
+ if (opts.quiet) installArgs.push("-q");
881
+ return await runInstall(installArgs);
786
882
  }
package/src/paths.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
+ import { existsSync } from "node:fs";
3
4
  import { getPackageRoot, getPackageVersion } from "./version.mjs";
4
5
 
5
6
  const USAGE = `okstra paths — print runtime paths
@@ -52,7 +53,7 @@ const SHELL_EXPORTS = [
52
53
  ];
53
54
 
54
55
  export async function resolvePaths() {
55
- const home = join(homedir(), ".okstra");
56
+ const home = process.env.OKSTRA_HOME?.trim() || join(homedir(), ".okstra");
56
57
  const pkgRoot = getPackageRoot();
57
58
  const devLink = await readSimpleFile(join(home, "dev-link"));
58
59
  const runtimeRoot = devLink || join(pkgRoot, "runtime");
@@ -68,18 +69,31 @@ export async function resolvePaths() {
68
69
  };
69
70
  }
70
71
 
72
+ function workspacePythonpath(paths) {
73
+ const candidates = [
74
+ join(paths.workspace, "python"),
75
+ join(paths.workspace, "scripts"),
76
+ ];
77
+ return candidates.find((candidate) => existsSync(candidate)) || "";
78
+ }
79
+
71
80
  // Compose the PYTHONPATH spawned python3 subprocesses should see.
72
81
  //
73
82
  // Precedence (first wins):
74
83
  // 1. process.env.PYTHONPATH (user override — e.g. in-repo `PYTHONPATH=scripts`
75
84
  // for dev work without re-running `okstra install`).
76
- // 2. The installed user-runtime path (~/.okstra/lib/python).
85
+ // 2. The package/dev-link workspace runtime selected by `okstra paths`.
86
+ // 3. The installed user-runtime path (~/.okstra/lib/python).
77
87
  //
78
88
  // This is a colon-joined string suitable for direct assignment into the child
79
89
  // process env. Empty segments are dropped so a missing env var does not produce
80
90
  // a leading ':' (which Python treats as 'current directory').
81
91
  export function buildPythonpath(paths) {
82
- const fragments = [process.env.PYTHONPATH, paths.pythonpath].filter(
92
+ const fragments = [
93
+ process.env.PYTHONPATH,
94
+ workspacePythonpath(paths),
95
+ paths.pythonpath,
96
+ ].filter(
83
97
  (s) => typeof s === "string" && s.length > 0,
84
98
  );
85
99
  return fragments.join(":");
@@ -16,6 +16,7 @@ Usage:
16
16
  [--approved-plan <path>] [--workers <list>] [--directive <text>] \\
17
17
  [--lead-model <m>] [--claude-model <m>] [--codex-model <m>] \\
18
18
  [--gemini-model <m>] [--report-writer-model <m>] \\
19
+ [--lead-runtime claude-code|codex] \\
19
20
  [--related-tasks <list>] [--base-ref <ref>] \\
20
21
  [--clarification-response <path>] [--work-category <cat>] \\
21
22
  [--stage <auto|N>] [--stages <csv>] [--pr-template-path <path>] \\
@@ -23,6 +24,7 @@ Usage:
23
24
 
24
25
  --stage implementation / final-verification only
25
26
  --stages release-handoff only: PR stage bundle csv (empty = whole-task)
27
+ --lead-runtime lead adapter marker; codex is render-only in this version
26
28
  --pr-template-path release-handoff only
27
29
  --fix-cycle entry phase only: record this re-entry of a done task as a
28
30
  bug-fix cycle (yes opens/continues the cycle)
@@ -0,0 +1,26 @@
1
+ export const RUNTIMES_MANIFEST_REL = "installed-runtimes.json";
2
+
3
+ export function runtimeListForSelection(runtime) {
4
+ return runtime === "all" ? ["claude-code", "codex"] : [runtime];
5
+ }
6
+
7
+ export function buildRuntimeManifest(runtime, installedAt = new Date().toISOString()) {
8
+ const includesClaude = runtime === "claude-code" || runtime === "all";
9
+ const includesCodex = runtime === "codex" || runtime === "all";
10
+ return {
11
+ version: 1,
12
+ installedAt,
13
+ runtime,
14
+ runtimes: runtimeListForSelection(runtime),
15
+ assets: {
16
+ sharedRuntime: true,
17
+ claudeSkills: includesClaude,
18
+ claudeAgents: includesClaude,
19
+ codexAdapter: includesCodex,
20
+ },
21
+ };
22
+ }
23
+
24
+ export function runtimeManifestIncludesClaudeAssets(data) {
25
+ return data?.assets?.claudeSkills === true || data?.assets?.claudeAgents === true;
26
+ }
package/src/uninstall.mjs CHANGED
@@ -2,6 +2,10 @@ import { promises as fs } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { resolvePaths } from "./paths.mjs";
5
+ import {
6
+ RUNTIMES_MANIFEST_REL,
7
+ runtimeManifestIncludesClaudeAssets,
8
+ } from "./runtime-manifest.mjs";
5
9
 
6
10
  const BIN_ENTRYPOINTS = [
7
11
  "okstra.sh",
@@ -56,8 +60,8 @@ const USAGE = `okstra uninstall — remove installed runtime from ~/.okstra, ~/.
56
60
  Usage:
57
61
  okstra uninstall Remove ~/.okstra/{lib (python + validators),
58
62
  bin/<known>, schemas, templates, version,
59
- dev-link, installed-skills.json,
60
- installed-agents.json} AND
63
+ dev-link, installed-runtimes.json,
64
+ installed-skills.json, installed-agents.json} AND
61
65
  ~/.claude/skills/<name> AND
62
66
  ~/.claude/agents/<worker>.md for every
63
67
  entry in the install manifests (fallback:
@@ -188,7 +192,9 @@ export async function runUninstall(args) {
188
192
  // Remove the skills we own. Manifest is authoritative; fall back to the
189
193
  // hard-coded okstra-* names if it is missing (e.g. an install from an old
190
194
  // version that did not write the manifest).
191
- const skillNames = await readSkillsManifest(paths.home);
195
+ const runtimeManifest = await readRuntimeManifest(paths.home);
196
+ const shouldRemoveClaudeAssets = shouldUseClaudeAssetFallback(runtimeManifest);
197
+ const skillNames = shouldRemoveClaudeAssets ? await readSkillsManifest(paths.home) : [];
192
198
  if (!opts.quiet) {
193
199
  process.stdout.write(` skills: removing ${skillNames.length} entries from ${CLAUDE_SKILLS_DIR}\n`);
194
200
  }
@@ -198,7 +204,7 @@ export async function runUninstall(args) {
198
204
  await removePath(join(paths.home, SKILLS_MANIFEST_REL), opts);
199
205
 
200
206
  // Remove worker agents we own. Same authoritative-manifest pattern as skills.
201
- const agentNames = await readAgentsManifest(paths.home);
207
+ const agentNames = shouldRemoveClaudeAssets ? await readAgentsManifest(paths.home) : [];
202
208
  if (!opts.quiet) {
203
209
  process.stdout.write(` agents: removing ${agentNames.length} entries from ${CLAUDE_AGENTS_DIR}\n`);
204
210
  }
@@ -218,6 +224,7 @@ export async function runUninstall(args) {
218
224
 
219
225
  await removePath(join(paths.home, "version"), opts);
220
226
  await removePath(join(paths.home, "dev-link"), opts);
227
+ await removePath(join(paths.home, RUNTIMES_MANIFEST_REL), opts);
221
228
 
222
229
  if (!opts.quiet) {
223
230
  process.stdout.write("done. user data preserved (recent.jsonl, projects/, archive/, ...).\n");
@@ -225,6 +232,21 @@ export async function runUninstall(args) {
225
232
  return 0;
226
233
  }
227
234
 
235
+ async function readRuntimeManifest(home) {
236
+ try {
237
+ const raw = await fs.readFile(join(home, RUNTIMES_MANIFEST_REL), "utf8");
238
+ const data = JSON.parse(raw);
239
+ return data && typeof data === "object" ? data : null;
240
+ } catch {
241
+ return null;
242
+ }
243
+ }
244
+
245
+ export function shouldUseClaudeAssetFallback(runtimeManifest) {
246
+ if (runtimeManifest === null) return true;
247
+ return runtimeManifestIncludesClaudeAssets(runtimeManifest);
248
+ }
249
+
228
250
  async function readSkillsManifest(home) {
229
251
  try {
230
252
  const raw = await fs.readFile(join(home, SKILLS_MANIFEST_REL), "utf8");