okstra 0.76.0 → 0.78.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 (65) hide show
  1. package/README.kr.md +5 -3
  2. package/README.md +5 -3
  3. package/bin/okstra +1 -117
  4. package/docs/contributor-change-matrix.md +12 -0
  5. package/docs/kr/architecture.md +1 -1
  6. package/docs/kr/cli.md +22 -5
  7. package/docs/pr-template-usage.md +3 -3
  8. package/docs/project-structure-overview.md +1 -1
  9. package/docs/superpowers/plans/2026-05-25-okstra-project-root-rename.md +1 -1
  10. package/docs/superpowers/plans/2026-06-13-repo-risk-hardening.md +493 -0
  11. package/docs/superpowers/specs/2026-06-12-codex-lead-adapter-design.md +358 -0
  12. package/docs/superpowers/specs/2026-06-13-forbidden-actions-ssot-design.md +134 -0
  13. package/docs/superpowers/specs/2026-06-13-neutral-tmux-lead-adapter-design.md +284 -0
  14. package/package.json +5 -2
  15. package/runtime/BUILD.json +2 -2
  16. package/runtime/DO_NOT_EDIT.md +21 -0
  17. package/runtime/agents/SKILL.md +3 -0
  18. package/runtime/bin/lib/okstra/cli.sh +5 -1
  19. package/runtime/bin/lib/okstra/globals.sh +1 -0
  20. package/runtime/bin/lib/okstra/usage.sh +6 -4
  21. package/runtime/bin/okstra-trace-cleanup.sh +16 -12
  22. package/runtime/bin/okstra.sh +1 -0
  23. package/runtime/prompts/launch.template.md +4 -13
  24. package/runtime/prompts/profiles/error-analysis.md +6 -0
  25. package/runtime/prompts/profiles/forbidden-actions.json +69 -0
  26. package/runtime/prompts/profiles/implementation.md +1 -8
  27. package/runtime/prompts/profiles/improvement-discovery.md +5 -0
  28. package/runtime/prompts/profiles/release-handoff.md +3 -17
  29. package/runtime/python/okstra_ctl/analysis_packet.py +17 -0
  30. package/runtime/python/okstra_ctl/codex_dispatch.py +1552 -0
  31. package/runtime/python/okstra_ctl/context_cost.py +1 -1
  32. package/runtime/python/okstra_ctl/dispatch_core.py +897 -0
  33. package/runtime/python/okstra_ctl/doctor.py +292 -0
  34. package/runtime/python/okstra_ctl/lead_events.py +129 -0
  35. package/runtime/python/okstra_ctl/lead_runtime.py +72 -0
  36. package/runtime/python/okstra_ctl/paths.py +3 -0
  37. package/runtime/python/okstra_ctl/pr_template.py +1 -1
  38. package/runtime/python/okstra_ctl/render.py +211 -29
  39. package/runtime/python/okstra_ctl/run.py +89 -18
  40. package/runtime/python/okstra_ctl/team.py +267 -0
  41. package/runtime/python/okstra_ctl/tmux.py +181 -10
  42. package/runtime/python/okstra_ctl/workflow.py +30 -71
  43. package/runtime/python/okstra_ctl/wrapper_status.py +55 -0
  44. package/runtime/python/okstra_token_usage/codex.py +12 -7
  45. package/runtime/python/okstra_token_usage/collect.py +243 -54
  46. package/runtime/python/okstra_token_usage/gemini.py +12 -7
  47. package/runtime/schemas/final-report-v1.0.schema.json +3 -1
  48. package/runtime/skills/okstra-convergence/SKILL.md +3 -0
  49. package/runtime/skills/okstra-report-writer/SKILL.md +3 -0
  50. package/runtime/skills/okstra-setup/SKILL.md +1 -1
  51. package/runtime/skills/okstra-team-contract/SKILL.md +12 -0
  52. package/runtime/validators/forbidden_actions.py +135 -0
  53. package/runtime/validators/validate-run.py +112 -22
  54. package/runtime/validators/validate_session_conformance.py +127 -5
  55. package/src/cli-registry.mjs +277 -0
  56. package/src/codex-dispatch.mjs +70 -0
  57. package/src/codex-run.mjs +68 -0
  58. package/src/doctor.mjs +130 -5
  59. package/src/install.mjs +123 -26
  60. package/src/paths.mjs +17 -3
  61. package/src/render-bundle.mjs +2 -0
  62. package/src/runtime-manifest.mjs +29 -0
  63. package/src/team.mjs +63 -0
  64. package/src/uninstall.mjs +27 -4
  65. /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";
@@ -20,6 +21,7 @@ const PYTHON_PACKAGES = ["okstra_project", "okstra_ctl", "okstra_token_usage", "
20
21
  const BIN_ENTRYPOINTS = [
21
22
  "okstra.sh",
22
23
  "okstra-codex-exec.sh",
24
+ "okstra-claude-exec.sh",
23
25
  "okstra-gemini-exec.sh",
24
26
  "okstra-trace-cleanup.sh",
25
27
  "okstra-team-reconcile.sh",
@@ -36,6 +38,7 @@ const INSTALL_USAGE = `okstra install — install runtime into ~/.okstra
36
38
 
37
39
  Usage:
38
40
  okstra install Install/update runtime (copy mode)
41
+ okstra install --runtime <claude-code|codex|external|all>
39
42
  okstra install --link <p> Dev only: symlink ~/.okstra/{lib/python,bin}
40
43
  to the source repo at <p>. agents/ resolves
41
44
  to <p>/agents. Not advertised to end users.
@@ -50,6 +53,7 @@ Effect (copy mode):
50
53
  ${"$HOME"}/.okstra/templates/settings.local.json <- runtime/templates/reports/settings.template.json
51
54
  ${"$HOME"}/.claude/skills/<name> <- runtime/skills/<name> (per skill)
52
55
  ${"$HOME"}/.claude/agents/<worker>.md <- runtime/agents/workers/<worker>.md
56
+ ${"$HOME"}/.okstra/installed-runtimes.json <- manifest of installed runtime adapters
53
57
  ${"$HOME"}/.okstra/installed-skills.json <- manifest of installed skills
54
58
  ${"$HOME"}/.okstra/installed-agents.json <- manifest of installed workers
55
59
  ${"$HOME"}/.okstra/version <- installed package version stamp
@@ -78,11 +82,37 @@ const ENSURE_USAGE = `okstra ensure-installed — idempotent install check
78
82
 
79
83
  Usage:
80
84
  okstra ensure-installed Verify install; reinstall if version/files drift
85
+ okstra ensure-installed --runtime <claude-code|codex|external|all>
81
86
  okstra ensure-installed -q Same, but suppress success output
82
87
 
83
88
  Skills call this on every run. Returns 0 quickly when the install is fresh.
84
89
  `;
85
90
 
91
+ const INSTALL_RUNTIMES = new Set(["claude-code", "codex", "external", "all"]);
92
+
93
+ export function runtimeIncludesClaudeAssets(runtime) {
94
+ return runtime === "claude-code" || runtime === "all";
95
+ }
96
+
97
+ function readRuntimeFlag(args, index) {
98
+ const arg = args[index];
99
+ if (arg.startsWith("--runtime=")) {
100
+ return { value: arg.slice("--runtime=".length), nextIndex: index };
101
+ }
102
+ const next = args[index + 1];
103
+ if (!next || next.startsWith("--")) {
104
+ throw new Error("--runtime requires one of: claude-code, codex, external, all");
105
+ }
106
+ return { value: next, nextIndex: index + 1 };
107
+ }
108
+
109
+ export function validateInstallRuntime(value) {
110
+ if (!INSTALL_RUNTIMES.has(value)) {
111
+ throw new Error(`unknown runtime '${value}' (expected: claude-code, codex, external, all)`);
112
+ }
113
+ return value;
114
+ }
115
+
86
116
  async function hashFile(path) {
87
117
  const buf = await fs.readFile(path);
88
118
  return createHash("sha256").update(buf).digest("hex");
@@ -223,7 +253,7 @@ async function ensureSymlink(target, linkPath, opts) {
223
253
  }
224
254
 
225
255
  async function installLinkMode(repoPath, paths, opts) {
226
- const { dryRun, quiet } = opts;
256
+ const { dryRun, quiet, runtime } = opts;
227
257
  const repoAbs = resolveAbs(repoPath);
228
258
 
229
259
  if (!(await dirExists(repoAbs))) {
@@ -240,6 +270,7 @@ async function installLinkMode(repoPath, paths, opts) {
240
270
 
241
271
  if (!quiet) {
242
272
  process.stdout.write(`installing okstra runtime in LINK mode (package ${paths.package})\n`);
273
+ process.stdout.write(` runtime: ${runtime}\n`);
243
274
  process.stdout.write(` repo: ${repoAbs}\n`);
244
275
  process.stdout.write(` home: ${paths.home}\n`);
245
276
  }
@@ -271,14 +302,19 @@ async function installLinkMode(repoPath, paths, opts) {
271
302
  if (!quiet) process.stdout.write(` bin/${name}: ${action}\n`);
272
303
  }
273
304
 
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 });
305
+ if (runtimeIncludesClaudeAssets(runtime)) {
306
+ const skillResult = await installSkillsLink(repoAbs, { dryRun, quiet });
307
+ const leadSkillResult = await installLeadSkillLink(repoAbs, { dryRun, quiet });
308
+ await writeSkillsManifest(paths.home, [...skillResult.installed, ...leadSkillResult], { dryRun });
277
309
 
278
- const agentResult = await installAgentsLink(repoAbs, { dryRun, quiet });
279
- await writeAgentsManifest(paths.home, agentResult.installed, { dryRun });
310
+ const agentResult = await installAgentsLink(repoAbs, { dryRun, quiet });
311
+ await writeAgentsManifest(paths.home, agentResult.installed, { dryRun });
312
+ } else if (!quiet) {
313
+ process.stdout.write(" claude assets: skipped for runtime=codex\n");
314
+ }
280
315
 
281
316
  await installNamedTemplate(repoAbs, paths, { mode: "link", dryRun, quiet }, SETTINGS_TEMPLATE_DESCRIPTOR);
317
+ await writeRuntimeManifest(paths.home, runtime, { dryRun });
282
318
 
283
319
  if (!dryRun) {
284
320
  await writeFileAtomic(join(paths.home, "dev-link"), repoAbs + "\n", 0o644);
@@ -345,6 +381,20 @@ async function writeAgentsManifest(home, names, opts) {
345
381
  );
346
382
  }
347
383
 
384
+ async function writeRuntimeManifest(home, runtime, opts) {
385
+ const { dryRun = false } = opts ?? {};
386
+ const data = buildRuntimeManifest(runtime);
387
+ if (dryRun) {
388
+ process.stdout.write(`[dry-run] write runtime manifest: ${data.runtimes.join(",")}\n`);
389
+ return;
390
+ }
391
+ await writeFileAtomic(
392
+ join(home, RUNTIMES_MANIFEST_REL),
393
+ JSON.stringify(data, null, 2) + "\n",
394
+ 0o644,
395
+ );
396
+ }
397
+
348
398
  async function listWorkerFiles(workersRoot) {
349
399
  try {
350
400
  const entries = await fs.readdir(workersRoot, { withFileTypes: true });
@@ -553,19 +603,24 @@ async function installLeadSkillLink(repoAbs, opts) {
553
603
  return [LEAD_SKILL_NAME];
554
604
  }
555
605
 
556
- function parseInstallArgs(args) {
606
+ export function parseInstallArgs(args) {
557
607
  const result = {
558
608
  dryRun: false,
559
609
  refresh: false,
560
610
  quiet: false,
561
611
  linkRepo: null,
612
+ runtime: "claude-code",
562
613
  };
563
614
  for (let i = 0; i < args.length; i++) {
564
615
  const a = args[i];
565
616
  if (a === "--dry-run") result.dryRun = true;
566
617
  else if (a === "--refresh") result.refresh = true;
567
618
  else if (a === "-q" || a === "--quiet") result.quiet = true;
568
- else if (a === "--link") {
619
+ else if (a === "--runtime" || a.startsWith("--runtime=")) {
620
+ const parsed = readRuntimeFlag(args, i);
621
+ result.runtime = validateInstallRuntime(parsed.value);
622
+ i = parsed.nextIndex;
623
+ } else if (a === "--link") {
569
624
  const next = args[i + 1];
570
625
  if (!next || next.startsWith("--")) {
571
626
  throw new Error("--link requires a repo path");
@@ -585,7 +640,13 @@ export async function runInstall(args) {
585
640
  return 0;
586
641
  }
587
642
 
588
- const opts = parseInstallArgs(args);
643
+ let opts;
644
+ try {
645
+ opts = parseInstallArgs(args);
646
+ } catch (err) {
647
+ process.stderr.write(`error: ${err.message}\n`);
648
+ return 2;
649
+ }
589
650
  const pkgRoot = getPackageRoot();
590
651
  const runtimeRoot = join(pkgRoot, "runtime");
591
652
  const paths = await resolvePaths();
@@ -607,6 +668,7 @@ export async function runInstall(args) {
607
668
 
608
669
  if (!opts.quiet) {
609
670
  process.stdout.write(`installing okstra runtime (package ${paths.package})\n`);
671
+ process.stdout.write(` runtime: ${opts.runtime}\n`);
610
672
  process.stdout.write(` source: ${runtimeRoot}\n`);
611
673
  process.stdout.write(` home: ${paths.home}\n`);
612
674
  }
@@ -688,18 +750,23 @@ export async function runInstall(args) {
688
750
  );
689
751
  }
690
752
 
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
- );
753
+ if (runtimeIncludesClaudeAssets(opts.runtime)) {
754
+ const skillResult = await installSkillsCopy(runtimeRoot, opts);
755
+ const leadSkillResult = await installLeadSkillCopy(runtimeRoot, opts);
756
+ await writeSkillsManifest(
757
+ paths.home,
758
+ [...skillResult.installed, ...leadSkillResult],
759
+ { dryRun: opts.dryRun },
760
+ );
698
761
 
699
- const agentResult = await installAgentsCopy(runtimeRoot, opts);
700
- await writeAgentsManifest(paths.home, agentResult.installed, { dryRun: opts.dryRun });
762
+ const agentResult = await installAgentsCopy(runtimeRoot, opts);
763
+ await writeAgentsManifest(paths.home, agentResult.installed, { dryRun: opts.dryRun });
764
+ } else if (!opts.quiet) {
765
+ process.stdout.write(" claude assets: skipped for runtime=codex\n");
766
+ }
701
767
 
702
768
  await installNamedTemplate(runtimeRoot, paths, { mode: "copy", ...opts }, SETTINGS_TEMPLATE_DESCRIPTOR);
769
+ await writeRuntimeManifest(paths.home, opts.runtime, { dryRun: opts.dryRun });
703
770
 
704
771
  if (!opts.dryRun) {
705
772
  await writeFileAtomic(join(paths.home, "version"), paths.package + "\n", 0o644);
@@ -751,12 +818,38 @@ function summarise(label, result, target) {
751
818
  );
752
819
  }
753
820
 
821
+ export function parseEnsureInstalledArgs(args) {
822
+ const result = {
823
+ quiet: false,
824
+ runtime: "claude-code",
825
+ };
826
+ for (let i = 0; i < args.length; i++) {
827
+ const arg = args[i];
828
+ if (arg === "-q" || arg === "--quiet") {
829
+ result.quiet = true;
830
+ } else if (arg === "--runtime" || arg.startsWith("--runtime=")) {
831
+ const parsed = readRuntimeFlag(args, i);
832
+ result.runtime = validateInstallRuntime(parsed.value);
833
+ i = parsed.nextIndex;
834
+ } else {
835
+ throw new Error(`unknown argument '${arg}'`);
836
+ }
837
+ }
838
+ return result;
839
+ }
840
+
754
841
  export async function runEnsureInstalled(args) {
755
842
  if (args.includes("--help") || args.includes("-h")) {
756
843
  process.stdout.write(ENSURE_USAGE);
757
844
  return 0;
758
845
  }
759
- const quiet = args.includes("-q") || args.includes("--quiet");
846
+ let opts;
847
+ try {
848
+ opts = parseEnsureInstalledArgs(args);
849
+ } catch (err) {
850
+ process.stderr.write(`error: ${err.message}\n`);
851
+ return 2;
852
+ }
760
853
  const paths = await resolvePaths();
761
854
 
762
855
  const reasons = [];
@@ -769,18 +862,22 @@ export async function runEnsureInstalled(args) {
769
862
  if (!(await fileExists(join(paths.home, "schemas", "final-report-v1.0.schema.json")))) {
770
863
  reasons.push(`missing ${join(paths.home, "schemas", "final-report-v1.0.schema.json")}`);
771
864
  }
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);
865
+ if (runtimeIncludesClaudeAssets(opts.runtime)) {
866
+ if (!(await fileExists(join(CLAUDE_SKILLS_DIR, "okstra-setup", "SKILL.md")))) {
867
+ reasons.push(`missing ${CLAUDE_SKILLS_DIR}/okstra-setup/SKILL.md`);
868
+ }
869
+ for (const reason of await agentDriftReasons(paths)) {
870
+ reasons.push(reason);
871
+ }
777
872
  }
778
873
 
779
874
  if (reasons.length === 0) {
780
- if (!quiet) process.stdout.write(`okstra runtime OK (package ${paths.package})\n`);
875
+ if (!opts.quiet) process.stdout.write(`okstra runtime OK (package ${paths.package})\n`);
781
876
  return 0;
782
877
  }
783
878
 
784
879
  process.stderr.write(`okstra runtime needs install: ${reasons.join("; ")}\n`);
785
- return await runInstall(quiet ? ["-q"] : []);
880
+ const installArgs = ["--runtime", opts.runtime];
881
+ if (opts.quiet) installArgs.push("-q");
882
+ return await runInstall(installArgs);
786
883
  }
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,29 @@
1
+ export const RUNTIMES_MANIFEST_REL = "installed-runtimes.json";
2
+
3
+ export function runtimeListForSelection(runtime) {
4
+ if (runtime === "all") return ["claude-code", "codex", "external"];
5
+ return [runtime];
6
+ }
7
+
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";
12
+ return {
13
+ 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,
24
+ };
25
+ }
26
+
27
+ export function runtimeManifestIncludesClaudeAssets(data) {
28
+ return data?.assets?.claudeSkills === true || data?.assets?.claudeAgents === true;
29
+ }
package/src/team.mjs ADDED
@@ -0,0 +1,63 @@
1
+ import { runPythonModule } from "./_python-helper.mjs";
2
+ import { resolvePaths } from "./paths.mjs";
3
+
4
+ const USAGE = `okstra team — dispatch and await tmux-pane workers for external leads
5
+
6
+ Usage:
7
+ okstra team dispatch --project-root <dir> --run-manifest <path> \
8
+ [--workers <csv>] [--jobs-file <path>] [--dry-run] \
9
+ [--idle-timeout-seconds <n>] [--dispatch-kind <kind>]
10
+ okstra team await --project-root <dir> --run-manifest <path> \
11
+ [--poll-interval-seconds <n>] [--timeout-seconds <n>] \
12
+ [--heartbeat-seconds <n>] [--json]
13
+ okstra team teardown --project-root <dir> --run-manifest <path> \
14
+ [--dry-run] [--json]
15
+
16
+ This command is for leadRuntime=external bundles. It dispatches okstra-owned
17
+ worker panes through tmux when possible and degrades to CLI wrappers when tmux is
18
+ unavailable to the lead harness.
19
+
20
+ --workspace-root and --okstra-bin are owned by this command.
21
+ `;
22
+
23
+ const OWNED_FLAGS = new Set(["--workspace-root", "--okstra-bin"]);
24
+
25
+ function isOwnedFlag(arg) {
26
+ if (OWNED_FLAGS.has(arg)) return true;
27
+ return [...OWNED_FLAGS].some((flag) => arg.startsWith(`${flag}=`));
28
+ }
29
+
30
+ export function buildTeamArgs(args, paths) {
31
+ return [
32
+ "--workspace-root", paths.workspace,
33
+ "--okstra-bin", paths.bin,
34
+ ...args,
35
+ ];
36
+ }
37
+
38
+ export async function run(args) {
39
+ if (args.includes("--help") || args.includes("-h")) {
40
+ process.stdout.write(USAGE);
41
+ return 0;
42
+ }
43
+ if (args.length === 0) {
44
+ process.stdout.write(USAGE);
45
+ return 2;
46
+ }
47
+
48
+ const forbidden = args.find(isOwnedFlag);
49
+ if (forbidden) {
50
+ process.stderr.write(
51
+ `error: ${forbidden} is set by 'okstra team' itself — remove it from your args\n`,
52
+ );
53
+ return 2;
54
+ }
55
+
56
+ const paths = await resolvePaths();
57
+ const result = await runPythonModule({
58
+ module: "okstra_ctl.team",
59
+ args: buildTeamArgs(args, paths),
60
+ stdio: "inherit-stdout",
61
+ });
62
+ return result.code ?? 0;
63
+ }
package/src/uninstall.mjs CHANGED
@@ -2,10 +2,15 @@ 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",
8
12
  "okstra-codex-exec.sh",
13
+ "okstra-claude-exec.sh",
9
14
  "okstra-gemini-exec.sh",
10
15
  "okstra-trace-cleanup.sh",
11
16
  "okstra-team-reconcile.sh",
@@ -56,8 +61,8 @@ const USAGE = `okstra uninstall — remove installed runtime from ~/.okstra, ~/.
56
61
  Usage:
57
62
  okstra uninstall Remove ~/.okstra/{lib (python + validators),
58
63
  bin/<known>, schemas, templates, version,
59
- dev-link, installed-skills.json,
60
- installed-agents.json} AND
64
+ dev-link, installed-runtimes.json,
65
+ installed-skills.json, installed-agents.json} AND
61
66
  ~/.claude/skills/<name> AND
62
67
  ~/.claude/agents/<worker>.md for every
63
68
  entry in the install manifests (fallback:
@@ -188,7 +193,9 @@ export async function runUninstall(args) {
188
193
  // Remove the skills we own. Manifest is authoritative; fall back to the
189
194
  // hard-coded okstra-* names if it is missing (e.g. an install from an old
190
195
  // version that did not write the manifest).
191
- const skillNames = await readSkillsManifest(paths.home);
196
+ const runtimeManifest = await readRuntimeManifest(paths.home);
197
+ const shouldRemoveClaudeAssets = shouldUseClaudeAssetFallback(runtimeManifest);
198
+ const skillNames = shouldRemoveClaudeAssets ? await readSkillsManifest(paths.home) : [];
192
199
  if (!opts.quiet) {
193
200
  process.stdout.write(` skills: removing ${skillNames.length} entries from ${CLAUDE_SKILLS_DIR}\n`);
194
201
  }
@@ -198,7 +205,7 @@ export async function runUninstall(args) {
198
205
  await removePath(join(paths.home, SKILLS_MANIFEST_REL), opts);
199
206
 
200
207
  // Remove worker agents we own. Same authoritative-manifest pattern as skills.
201
- const agentNames = await readAgentsManifest(paths.home);
208
+ const agentNames = shouldRemoveClaudeAssets ? await readAgentsManifest(paths.home) : [];
202
209
  if (!opts.quiet) {
203
210
  process.stdout.write(` agents: removing ${agentNames.length} entries from ${CLAUDE_AGENTS_DIR}\n`);
204
211
  }
@@ -218,6 +225,7 @@ export async function runUninstall(args) {
218
225
 
219
226
  await removePath(join(paths.home, "version"), opts);
220
227
  await removePath(join(paths.home, "dev-link"), opts);
228
+ await removePath(join(paths.home, RUNTIMES_MANIFEST_REL), opts);
221
229
 
222
230
  if (!opts.quiet) {
223
231
  process.stdout.write("done. user data preserved (recent.jsonl, projects/, archive/, ...).\n");
@@ -225,6 +233,21 @@ export async function runUninstall(args) {
225
233
  return 0;
226
234
  }
227
235
 
236
+ async function readRuntimeManifest(home) {
237
+ try {
238
+ const raw = await fs.readFile(join(home, RUNTIMES_MANIFEST_REL), "utf8");
239
+ const data = JSON.parse(raw);
240
+ return data && typeof data === "object" ? data : null;
241
+ } catch {
242
+ return null;
243
+ }
244
+ }
245
+
246
+ export function shouldUseClaudeAssetFallback(runtimeManifest) {
247
+ if (runtimeManifest === null) return true;
248
+ return runtimeManifestIncludesClaudeAssets(runtimeManifest);
249
+ }
250
+
228
251
  async function readSkillsManifest(home) {
229
252
  try {
230
253
  const raw = await fs.readFile(join(home, SKILLS_MANIFEST_REL), "utf8");