okstra 0.82.0 → 0.83.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 (70) hide show
  1. package/README.kr.md +7 -6
  2. package/README.md +6 -6
  3. package/docs/kr/architecture.md +8 -7
  4. package/docs/kr/cli.md +4 -4
  5. package/docs/kr/performance-improvement-plan-v2.md +14 -14
  6. package/docs/project-structure-overview.md +8 -9
  7. package/docs/superpowers/plans/2026-06-15-coding-preflight-pack-dispatch-path.md +504 -0
  8. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-final-fixups.md +342 -0
  9. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-fixups.md +258 -0
  10. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-remaining-fixups.md +387 -0
  11. package/docs/superpowers/plans/2026-06-15-internal-skill-resource-migration.md +749 -0
  12. package/docs/superpowers/plans/2026-06-15-worker-prompt-anchor-final-fixups.md +828 -0
  13. package/docs/superpowers/plans/2026-06-15-worker-prompt-header-error-contract.md +490 -0
  14. package/docs/task-process/README.md +1 -1
  15. package/docs/task-process/error-analysis.md +1 -1
  16. package/docs/task-process/final-verification.md +1 -1
  17. package/docs/task-process/implementation-planning.md +1 -1
  18. package/docs/task-process/implementation.md +1 -1
  19. package/docs/task-process/release-handoff.md +1 -1
  20. package/docs/task-process/requirements-discovery.md +2 -2
  21. package/package.json +1 -1
  22. package/runtime/BUILD.json +2 -2
  23. package/runtime/agents/TODO.md +2 -0
  24. package/runtime/agents/workers/claude-worker.md +8 -8
  25. package/runtime/agents/workers/codex-worker.md +8 -8
  26. package/runtime/agents/workers/gemini-worker.md +8 -8
  27. package/runtime/agents/workers/report-writer-worker.md +2 -2
  28. package/runtime/bin/lib/okstra/globals.sh +0 -1
  29. package/runtime/bin/okstra-wrapper-status.py +1 -1
  30. package/runtime/{skills/okstra-coding-preflight → prompts/coding-preflight}/languages/python.md +2 -2
  31. package/runtime/{skills/okstra-coding-preflight → prompts/coding-preflight}/languages/rust.md +1 -1
  32. package/runtime/{skills/okstra-coding-preflight/SKILL.md → prompts/coding-preflight/overview.md} +27 -38
  33. package/runtime/prompts/launch.template.md +5 -3
  34. package/runtime/{skills/okstra-context-loader/SKILL.md → prompts/lead/context-loader.md} +7 -14
  35. package/runtime/{skills/okstra-convergence/SKILL.md → prompts/lead/convergence.md} +12 -19
  36. package/runtime/{agents/SKILL.md → prompts/lead/okstra-lead-contract.md} +53 -59
  37. package/runtime/{skills/okstra-report-writer/SKILL.md → prompts/lead/report-writer.md} +12 -19
  38. package/runtime/{skills/okstra-team-contract/SKILL.md → prompts/lead/team-contract.md} +13 -19
  39. package/runtime/prompts/profiles/_coding-conventions-preflight.md +2 -2
  40. package/runtime/prompts/profiles/_common-contract.md +2 -2
  41. package/runtime/prompts/profiles/_implementation-executor.md +2 -2
  42. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  43. package/runtime/prompts/profiles/error-analysis.md +2 -2
  44. package/runtime/prompts/profiles/final-verification.md +1 -1
  45. package/runtime/prompts/profiles/implementation-planning.md +4 -4
  46. package/runtime/prompts/profiles/requirements-discovery.md +2 -2
  47. package/runtime/python/okstra_ctl/codex_dispatch.py +12 -61
  48. package/runtime/python/okstra_ctl/context_cost.py +14 -11
  49. package/runtime/python/okstra_ctl/dispatch_core.py +36 -13
  50. package/runtime/python/okstra_ctl/paths.py +27 -1
  51. package/runtime/python/okstra_ctl/render.py +62 -8
  52. package/runtime/python/okstra_ctl/run.py +5 -5
  53. package/runtime/python/okstra_ctl/worker_prompt_headers.py +126 -0
  54. package/runtime/python/okstra_token_usage/claude.py +1 -1
  55. package/runtime/python/okstra_token_usage/collect.py +1 -1
  56. package/runtime/templates/reports/task-brief.template.md +2 -2
  57. package/runtime/templates/worker-prompt-preamble.md +2 -2
  58. package/runtime/validators/lib/validate-assets.sh +12 -4
  59. package/runtime/validators/validate-run.py +3 -3
  60. package/runtime/validators/validate_session_conformance.py +11 -11
  61. package/src/install.mjs +129 -98
  62. package/src/skill-catalog.mjs +35 -0
  63. package/src/uninstall.mjs +5 -0
  64. /package/runtime/{skills/okstra-coding-preflight/architecture → prompts/coding-preflight/architectures}/hexagonal.md +0 -0
  65. /package/runtime/{skills/okstra-coding-preflight → prompts/coding-preflight}/clean-code.md +0 -0
  66. /package/runtime/{skills/okstra-coding-preflight/languages/nodejs.md → prompts/coding-preflight/frameworks/node-server.md} +0 -0
  67. /package/runtime/{skills/okstra-coding-preflight → prompts/coding-preflight}/languages/java.md +0 -0
  68. /package/runtime/{skills/okstra-coding-preflight → prompts/coding-preflight}/languages/javascript-typescript.md +0 -0
  69. /package/runtime/{skills/okstra-coding-preflight → prompts/coding-preflight}/languages/kotlin.md +0 -0
  70. /package/runtime/{skills/okstra-coding-preflight → prompts/coding-preflight}/languages/sql.md +0 -0
package/src/install.mjs CHANGED
@@ -7,6 +7,7 @@ import { resolvePaths } from "./paths.mjs";
7
7
  import { fileExists } from "./_proc.mjs";
8
8
  import { buildRuntimeManifest, RUNTIMES_MANIFEST_REL } from "./runtime-manifest.mjs";
9
9
  import { normalizeRuntimeRequest, resolveRuntime } from "./runtime-resolver.mjs";
10
+ import { OBSOLETE_INTERNAL_SKILL_NAMES, USER_SKILL_NAMES } from "./skill-catalog.mjs";
10
11
 
11
12
  const SKILLS_MANIFEST_REL = "installed-skills.json";
12
13
  const AGENTS_MANIFEST_REL = "installed-agents.json";
@@ -41,6 +42,26 @@ const BIN_ENTRYPOINTS = [
41
42
  "okstra-spawn-followups.py",
42
43
  ];
43
44
 
45
+ // Runtime prompt resources the launch prompt and routed coding-preflight pack
46
+ // depend on. Intentionally an exact allowlist, not a directory wildcard.
47
+ const REQUIRED_PROMPT_RESOURCE_FILES = Object.freeze([
48
+ ["prompts", "lead", "okstra-lead-contract.md"],
49
+ ["prompts", "lead", "context-loader.md"],
50
+ ["prompts", "lead", "team-contract.md"],
51
+ ["prompts", "lead", "convergence.md"],
52
+ ["prompts", "lead", "report-writer.md"],
53
+ ["prompts", "coding-preflight", "overview.md"],
54
+ ["prompts", "coding-preflight", "clean-code.md"],
55
+ ["prompts", "coding-preflight", "languages", "javascript-typescript.md"],
56
+ ["prompts", "coding-preflight", "languages", "python.md"],
57
+ ["prompts", "coding-preflight", "languages", "rust.md"],
58
+ ["prompts", "coding-preflight", "languages", "java.md"],
59
+ ["prompts", "coding-preflight", "languages", "kotlin.md"],
60
+ ["prompts", "coding-preflight", "languages", "sql.md"],
61
+ ["prompts", "coding-preflight", "frameworks", "node-server.md"],
62
+ ["prompts", "coding-preflight", "architectures", "hexagonal.md"],
63
+ ]);
64
+
44
65
  const INSTALL_USAGE = `okstra install — install runtime into ~/.okstra
45
66
 
46
67
  Usage:
@@ -57,12 +78,13 @@ Effect (copy mode):
57
78
  ${"$HOME"}/.okstra/lib/validators <- runtime/validators (stage/report structure validators)
58
79
  ${"$HOME"}/.okstra/bin <- runtime/bin
59
80
  ${"$HOME"}/.okstra/templates <- runtime/templates (report.css / report.js / *.template.md)
81
+ ${"$HOME"}/.okstra/prompts <- runtime/prompts (lead contracts + coding-preflight pack)
60
82
  ${"$HOME"}/.okstra/templates/settings.local.json <- runtime/templates/reports/settings.template.json
61
83
  ${"$HOME"}/.claude/skills/<name> <- skills when ${"$HOME"}/.claude exists
62
84
  ${"$HOME"}/.codex/skills/<name> <- skills when ${"$HOME"}/.codex exists
63
85
  ${"$HOME"}/.omp/agent/skills/<name> <- skills fallback when Claude/Codex homes are absent
64
86
  ${"$HOME"}/.claude/agents/<worker>.md <- worker agents when ${"$HOME"}/.claude exists
65
- ${"$HOME"}/.okstra/installed-runtimes.json <- manifest of installed runtime adapters
87
+ ${"$HOME"}/.okstra/installed-runtimes.json <- install request/resolution manifest
66
88
  ${"$HOME"}/.okstra/installed-skills.json <- target-aware manifest of installed skills
67
89
  ${"$HOME"}/.okstra/installed-agents.json <- manifest of installed workers
68
90
  ${"$HOME"}/.okstra/version <- installed package version stamp
@@ -70,6 +92,7 @@ Effect (copy mode):
70
92
  Effect (link mode):
71
93
  ${"$HOME"}/.okstra/lib/python/<pkg> -> <repo>/scripts/<pkg> (symlink)
72
94
  ${"$HOME"}/.okstra/bin/<name>.sh -> <repo>/scripts/<name>.sh
95
+ ${"$HOME"}/.okstra/prompts -> <repo>/prompts
73
96
  ${"$HOME"}/.okstra/templates/settings.local.json -> <repo>/templates/reports/settings.template.json
74
97
  ${"$HOME"}/.claude/skills/<name> -> <repo>/skills/<name> when ${"$HOME"}/.claude exists
75
98
  ${"$HOME"}/.codex/skills/<name> -> <repo>/skills/<name> when ${"$HOME"}/.codex exists
@@ -86,6 +109,9 @@ worker wrapper scripts without modifying the user's global settings.
86
109
  Worker agent definitions are installed into ${"$HOME"}/.claude/agents/ only
87
110
  when ${"$HOME"}/.claude exists, so Claude Code's subagent discovery can pick
88
111
  them up. Codex/OMP skill targets receive skill markdown only.
112
+
113
+ Default auto runtime installation does not require host detection; it still
114
+ installs shared runtime files and any agent-home skill targets it finds.
89
115
  `;
90
116
 
91
117
  const ENSURE_USAGE = `okstra ensure-installed — idempotent install check
@@ -125,6 +151,31 @@ export function validateInstallRuntime(value) {
125
151
  return runtime;
126
152
  }
127
153
 
154
+ function resolveInstallRuntime(runtime, command) {
155
+ const resolution = resolveRuntime({
156
+ requestedRuntime: runtime,
157
+ command,
158
+ env: process.env,
159
+ capabilities: { tmuxAvailable: Boolean(process.env.TMUX) },
160
+ });
161
+ if (resolution.ok || runtime !== "auto") return resolution;
162
+ return {
163
+ ok: false,
164
+ requestedRuntime: "auto",
165
+ resolvedRuntime: null,
166
+ host: "unknown",
167
+ confidence: "none",
168
+ reason: resolution.reason,
169
+ fallbackFrom: null,
170
+ warnings: [resolution.reason],
171
+ };
172
+ }
173
+
174
+ function formatInstallRuntimeLabel(runtime, resolution) {
175
+ if (resolution.resolvedRuntime) return `${runtime} -> ${resolution.resolvedRuntime}`;
176
+ return `${runtime} (host unresolved; installing target assets)`;
177
+ }
178
+
128
179
  async function hashFile(path) {
129
180
  const buf = await fs.readFile(path);
130
181
  return createHash("sha256").update(buf).digest("hex");
@@ -139,6 +190,16 @@ async function dirExists(path) {
139
190
  }
140
191
  }
141
192
 
193
+ async function pathEntryExists(path) {
194
+ try {
195
+ await fs.lstat(path);
196
+ return true;
197
+ } catch (err) {
198
+ if (err.code === "ENOENT") return false;
199
+ throw err;
200
+ }
201
+ }
202
+
142
203
  async function* walkFiles(root) {
143
204
  let entries;
144
205
  try {
@@ -209,34 +270,6 @@ async function copyTreeIfChanged(srcRoot, dstRoot, opts) {
209
270
  return { copied, skipped, missingSource };
210
271
  }
211
272
 
212
- // Single-file variant of copyTreeIfChanged, used for the lead `okstra` skill
213
- // whose source is one file (agents/SKILL.md) rather than a skill directory.
214
- async function copyFileIfChanged(src, dst, opts) {
215
- const { refresh = false, dryRun = false, mode } = opts ?? {};
216
- let srcBuf;
217
- try {
218
- srcBuf = await fs.readFile(src);
219
- } catch {
220
- return { copied: 0, skipped: 0, missingSource: true };
221
- }
222
- let needsCopy = refresh;
223
- if (!needsCopy) {
224
- try {
225
- const [srcHash, dstHash] = await Promise.all([hashFile(src), hashFile(dst)]);
226
- needsCopy = srcHash !== dstHash;
227
- } catch {
228
- needsCopy = true;
229
- }
230
- }
231
- if (!needsCopy) return { copied: 0, skipped: 1, missingSource: false };
232
- if (dryRun) {
233
- process.stdout.write(`[dry-run] copy ${src} -> ${dst}\n`);
234
- return { copied: 1, skipped: 0, missingSource: false };
235
- }
236
- await writeFileAtomic(dst, srcBuf, mode);
237
- return { copied: 1, skipped: 0, missingSource: false };
238
- }
239
-
240
273
  async function ensureSymlink(target, linkPath, opts) {
241
274
  const { dryRun = false } = opts ?? {};
242
275
  try {
@@ -313,6 +346,15 @@ async function installLinkMode(repoPath, paths, opts) {
313
346
  const action = await ensureSymlink(src, dst, { dryRun });
314
347
  if (!quiet) process.stdout.write(` bin/${name}: ${action}\n`);
315
348
  }
349
+ const promptsSrc = join(repoAbs, "prompts");
350
+ const promptsDst = join(paths.home, "prompts");
351
+ if (await dirExists(promptsSrc)) {
352
+ const action = await ensureSymlink(promptsSrc, promptsDst, { dryRun });
353
+ if (!quiet) process.stdout.write(` prompts: ${action}\n`);
354
+ } else if (!quiet) {
355
+ process.stdout.write(" prompts: missing in repo — skipped\n");
356
+ }
357
+
316
358
 
317
359
  const skillTargets = await detectSkillTargets();
318
360
  const installedSkillTargets = await installSkillTargetsLink(repoAbs, skillTargets, { dryRun, quiet });
@@ -348,16 +390,7 @@ async function installLinkMode(repoPath, paths, opts) {
348
390
  );
349
391
  }
350
392
  return 0;
351
- }
352
393
 
353
- async function listSkillDirs(skillsRoot) {
354
- try {
355
- const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
356
- return entries.filter((e) => e.isDirectory()).map((e) => e.name);
357
- } catch (err) {
358
- if (err.code === "ENOENT") return [];
359
- throw err;
360
- }
361
394
  }
362
395
 
363
396
  async function detectSkillTargets() {
@@ -584,15 +617,12 @@ const SETTINGS_TEMPLATE_DESCRIPTOR = {
584
617
  label: "settings template",
585
618
  };
586
619
 
587
- const LEAD_SKILL_NAME = "okstra";
588
-
589
620
  async function installSkillsCopy(runtimeRoot, target, opts) {
590
621
  const { refresh, dryRun, quiet } = opts;
591
622
  const srcRoot = join(runtimeRoot, "skills");
592
- const names = await listSkillDirs(srcRoot);
593
623
  let copied = 0;
594
624
  let skipped = 0;
595
- for (const name of names) {
625
+ for (const name of USER_SKILL_NAMES) {
596
626
  const r = await copyTreeIfChanged(
597
627
  join(srcRoot, name),
598
628
  join(target.skillsDir, name),
@@ -601,65 +631,48 @@ async function installSkillsCopy(runtimeRoot, target, opts) {
601
631
  copied += r.copied;
602
632
  skipped += r.skipped;
603
633
  }
604
- const lead = await installLeadSkillCopy(runtimeRoot, target, opts);
634
+ const pruned = await pruneObsoleteInternalSkills(target, opts);
605
635
  if (!quiet) {
606
- const totalCopied = copied + lead.copied;
607
- const totalSkipped = skipped + lead.skipped;
608
- const totalSkills = names.length + lead.installed.length;
609
636
  process.stdout.write(
610
- ` ${target.provider} skills: copied=${totalCopied} skipped=${totalSkipped} -> ${target.skillsDir}/ (${totalSkills} skills)\n`,
637
+ ` ${target.provider} skills: copied=${copied} skipped=${skipped} pruned=${pruned} -> ${target.skillsDir}/ (${USER_SKILL_NAMES.length} skills)\n`,
611
638
  );
612
639
  }
613
- return { ...target, installed: [...names, ...lead.installed] };
640
+ return { ...target, installed: [...USER_SKILL_NAMES] };
614
641
  }
615
642
 
616
643
  async function installSkillsLink(repoAbs, target, opts) {
617
644
  const { dryRun, quiet } = opts;
618
645
  const srcRoot = join(repoAbs, "skills");
619
- const names = await listSkillDirs(srcRoot);
620
646
  if (!dryRun) await fs.mkdir(target.skillsDir, { recursive: true });
621
- for (const name of names) {
647
+ for (const name of USER_SKILL_NAMES) {
622
648
  const src = join(srcRoot, name);
623
649
  const dst = join(target.skillsDir, name);
624
650
  const action = await ensureSymlink(src, dst, { dryRun });
625
651
  if (!quiet) process.stdout.write(` ${target.provider} skills/${name}: ${action}\n`);
626
652
  }
627
- const lead = await installLeadSkillLink(repoAbs, target, opts);
628
- return { ...target, installed: [...names, ...lead] };
629
- }
630
-
631
- async function installLeadSkillCopy(runtimeRoot, target, opts) {
632
- const { refresh, dryRun, quiet } = opts;
633
- const src = join(runtimeRoot, "agents", "SKILL.md");
634
- const dst = join(target.skillsDir, LEAD_SKILL_NAME, "SKILL.md");
635
- const r = await copyFileIfChanged(src, dst, { refresh, dryRun, mode: 0o644 });
636
- if (r.missingSource) {
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 };
653
+ const pruned = await pruneObsoleteInternalSkills(target, opts);
654
+ if (!quiet && pruned > 0) {
655
+ process.stdout.write(` ${target.provider} obsolete internal skills: pruned=${pruned}\n`);
643
656
  }
644
- return { installed: [LEAD_SKILL_NAME], copied: r.copied, skipped: r.skipped };
657
+ return { ...target, installed: [...USER_SKILL_NAMES] };
645
658
  }
646
659
 
647
- async function installLeadSkillLink(repoAbs, target, opts) {
648
- const { dryRun, quiet } = opts;
649
- const src = join(repoAbs, "agents", "SKILL.md");
650
- if (!(await fileExists(src))) {
651
- if (!quiet) {
652
- process.stdout.write(
653
- ` ${target.provider} skills/okstra: <repo>/agents/SKILL.md missing — skipped\n`,
654
- );
660
+ // Remove former internal skill directories left by older installs. Exact-name
661
+ // only (OBSOLETE_INTERNAL_SKILL_NAMES) those contracts now ship as runtime
662
+ // resources under ~/.okstra/prompts/, not as agent skills.
663
+ async function pruneObsoleteInternalSkills(target, opts) {
664
+ let pruned = 0;
665
+ for (const name of OBSOLETE_INTERNAL_SKILL_NAMES) {
666
+ const path = join(target.skillsDir, name);
667
+ if (!(await pathEntryExists(path))) continue;
668
+ pruned += 1;
669
+ if (opts.dryRun) {
670
+ process.stdout.write(`[dry-run] rm ${path}\n`);
671
+ continue;
655
672
  }
656
- return [];
673
+ await fs.rm(path, { recursive: true, force: true });
657
674
  }
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 });
660
- const action = await ensureSymlink(src, dst, { dryRun });
661
- if (!quiet) process.stdout.write(` ${target.provider} skills/okstra/SKILL.md: ${action}\n`);
662
- return [LEAD_SKILL_NAME];
675
+ return pruned;
663
676
  }
664
677
 
665
678
  async function installSkillTargetsCopy(runtimeRoot, targets, opts) {
@@ -725,17 +738,11 @@ export async function runInstall(args) {
725
738
  const pkgRoot = getPackageRoot();
726
739
  const runtimeRoot = join(pkgRoot, "runtime");
727
740
  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) {
741
+ const runtimeResolution = resolveInstallRuntime(opts.runtime, "install");
742
+ if (!runtimeResolution.ok && opts.runtime !== "auto") {
735
743
  process.stderr.write(`error: ${runtimeResolution.reason}\n`);
736
744
  return 2;
737
745
  }
738
- const resolvedRuntime = runtimeResolution.resolvedRuntime;
739
746
 
740
747
  if (!opts.dryRun) {
741
748
  await fs.mkdir(paths.home, { recursive: true });
@@ -754,7 +761,7 @@ export async function runInstall(args) {
754
761
 
755
762
  if (!opts.quiet) {
756
763
  process.stdout.write(`installing okstra runtime (package ${paths.package})\n`);
757
- process.stdout.write(` runtime: ${opts.runtime} -> ${resolvedRuntime}\n`);
764
+ process.stdout.write(` runtime: ${formatInstallRuntimeLabel(opts.runtime, runtimeResolution)}\n`);
758
765
  process.stdout.write(` source: ${runtimeRoot}\n`);
759
766
  process.stdout.write(` home: ${paths.home}\n`);
760
767
  }
@@ -806,6 +813,17 @@ export async function runInstall(args) {
806
813
  join(paths.home, "lib", "validators"),
807
814
  { refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o755 },
808
815
  );
816
+ // prompts/ tree — lead/internal operating contracts (prompts/lead/*.md) and
817
+ // the coding-preflight resource pack (prompts/coding-preflight/*) are read at
818
+ // runtime by the lead and executor via absolute ~/.okstra/prompts/ paths
819
+ // (paths.py OKSTRA_LEAD_CONTRACT_PATH / OKSTRA_CODING_PREFLIGHT_DIR). Run-prep
820
+ // reads prompts/profiles + launch.template from the package runtime dir, but
821
+ // the lead's runtime Read needs them under the home tree like templates.
822
+ const promptsResult = await copyTreeIfChanged(
823
+ join(runtimeRoot, "prompts"),
824
+ join(paths.home, "prompts"),
825
+ { refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o644 },
826
+ );
809
827
 
810
828
  if (!opts.quiet) {
811
829
  summarise("python", pythonResult, paths.pythonpath);
@@ -813,6 +831,7 @@ export async function runInstall(args) {
813
831
  summarise("templates", templatesResult, join(paths.home, "templates"));
814
832
  summarise("schemas", schemasResult, join(paths.home, "schemas"));
815
833
  summarise("validators", validatorsResult, join(paths.home, "lib", "validators"));
834
+ summarise("prompts", promptsResult, join(paths.home, "prompts"));
816
835
  }
817
836
 
818
837
  if (pythonResult.missingSource && binResult.missingSource) {
@@ -835,6 +854,11 @@ export async function runInstall(args) {
835
854
  "warning: runtime/validators is empty. stage-structure / report validation will use stale or missing validators — re-run the build step.\n",
836
855
  );
837
856
  }
857
+ if (promptsResult.missingSource) {
858
+ process.stderr.write(
859
+ "warning: runtime/prompts is empty. lead contracts + coding-preflight resources will be missing — re-run the build step.\n",
860
+ );
861
+ }
838
862
 
839
863
  const skillTargets = await detectSkillTargets();
840
864
  const installedSkillTargets = await installSkillTargetsCopy(runtimeRoot, skillTargets, opts);
@@ -873,7 +897,7 @@ export async function runInstall(args) {
873
897
  async function skillTargetDriftReasons(targets) {
874
898
  const reasons = [];
875
899
  for (const target of targets) {
876
- for (const name of ["okstra-run", LEAD_SKILL_NAME]) {
900
+ for (const name of USER_SKILL_NAMES) {
877
901
  const skillPath = join(target.skillsDir, name, "SKILL.md");
878
902
  if (!(await fileExists(skillPath))) {
879
903
  reasons.push(`missing ${skillPath}`);
@@ -883,6 +907,15 @@ async function skillTargetDriftReasons(targets) {
883
907
  return reasons;
884
908
  }
885
909
 
910
+ async function promptResourceDriftReasons(paths) {
911
+ const reasons = [];
912
+ for (const parts of REQUIRED_PROMPT_RESOURCE_FILES) {
913
+ const path = join(paths.home, ...parts);
914
+ if (!(await fileExists(path))) reasons.push(`missing ${path}`);
915
+ }
916
+ return reasons;
917
+ }
918
+
886
919
  async function agentDriftReasons(paths) {
887
920
  const reasons = [];
888
921
  const workersDir = join(paths.agents, "workers");
@@ -949,13 +982,8 @@ export async function runEnsureInstalled(args) {
949
982
  return 2;
950
983
  }
951
984
  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) {
985
+ const runtimeResolution = resolveInstallRuntime(opts.runtime, "ensure-installed");
986
+ if (!runtimeResolution.ok && opts.runtime !== "auto") {
959
987
  process.stderr.write(`error: ${runtimeResolution.reason}\n`);
960
988
  return 2;
961
989
  }
@@ -970,6 +998,9 @@ export async function runEnsureInstalled(args) {
970
998
  if (!(await fileExists(join(paths.home, "schemas", "final-report-v1.0.schema.json")))) {
971
999
  reasons.push(`missing ${join(paths.home, "schemas", "final-report-v1.0.schema.json")}`);
972
1000
  }
1001
+ for (const reason of await promptResourceDriftReasons(paths)) {
1002
+ reasons.push(reason);
1003
+ }
973
1004
  const skillTargets = await detectSkillTargets();
974
1005
  for (const reason of await skillTargetDriftReasons(skillTargets)) {
975
1006
  reasons.push(reason);
@@ -0,0 +1,35 @@
1
+ // Single source of truth for which okstra skills are user-facing and which
2
+ // former internal skill directories must be pruned from agent skill homes on
3
+ // (re)install. The Claude plugin manifest (.claude-plugin/plugin.json) and the
4
+ // installer both derive their skill list from here, so the public surface
5
+ // cannot drift between discovery and install.
6
+
7
+ export const USER_SKILL_NAMES = Object.freeze([
8
+ "okstra-setup",
9
+ "okstra-brief",
10
+ "okstra-run",
11
+ "okstra-memory",
12
+ "okstra-inspect",
13
+ "okstra-schedule",
14
+ ]);
15
+
16
+ // Names okstra used to install as skills before lead/internal contracts moved
17
+ // into the runtime resource tree (~/.okstra/prompts/). Install prunes these
18
+ // exact directories from each target skill home so upgrades do not leave the
19
+ // old internal skills behind. Exact-name only — never wildcard.
20
+ export const OBSOLETE_INTERNAL_SKILL_NAMES = Object.freeze([
21
+ "okstra",
22
+ "okstra-context-loader",
23
+ "okstra-team-contract",
24
+ "okstra-convergence",
25
+ "okstra-report-writer",
26
+ "okstra-coding-preflight",
27
+ ]);
28
+
29
+ export function userSkillNames() {
30
+ return [...USER_SKILL_NAMES];
31
+ }
32
+
33
+ export function obsoleteInternalSkillNames() {
34
+ return [...OBSOLETE_INTERNAL_SKILL_NAMES];
35
+ }
package/src/uninstall.mjs CHANGED
@@ -206,6 +206,11 @@ export async function runUninstall(args) {
206
206
  // never removed it, leaving a stale final-report schema behind.
207
207
  await removePath(join(paths.home, "schemas"), opts);
208
208
 
209
+ // prompts/ tree — installed by copy mode (runtime/prompts): lead contracts
210
+ // + coding-preflight resource pack. Remove so an upgrade never serves stale
211
+ // operating contracts.
212
+ await removePath(join(paths.home, "prompts"), opts);
213
+
209
214
  // Remove the skills we own. Manifest v2 records every target root; legacy
210
215
  // manifests fall back to Claude-only names when the runtime manifest says
211
216
  // Claude assets may have been installed.