okstra 0.77.0 → 0.78.1

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 (42) hide show
  1. package/README.kr.md +1 -1
  2. package/README.md +1 -1
  3. package/bin/okstra +1 -125
  4. package/docs/contributor-change-matrix.md +12 -0
  5. package/docs/kr/cli.md +5 -4
  6. package/docs/project-structure-overview.md +1 -1
  7. package/docs/superpowers/plans/2026-06-13-repo-risk-hardening.md +493 -0
  8. package/docs/superpowers/specs/2026-06-13-forbidden-actions-ssot-design.md +134 -0
  9. package/docs/superpowers/specs/2026-06-13-neutral-tmux-lead-adapter-design.md +284 -0
  10. package/package.json +5 -2
  11. package/runtime/BUILD.json +2 -2
  12. package/runtime/DO_NOT_EDIT.md +21 -0
  13. package/runtime/agents/SKILL.md +3 -0
  14. package/runtime/bin/okstra-trace-cleanup.sh +16 -12
  15. package/runtime/prompts/profiles/forbidden-actions.json +69 -0
  16. package/runtime/prompts/profiles/implementation.md +1 -8
  17. package/runtime/prompts/profiles/release-handoff.md +1 -15
  18. package/runtime/python/okstra_ctl/codex_dispatch.py +70 -2
  19. package/runtime/python/okstra_ctl/context_cost.py +1 -1
  20. package/runtime/python/okstra_ctl/dispatch_core.py +897 -0
  21. package/runtime/python/okstra_ctl/doctor.py +292 -0
  22. package/runtime/python/okstra_ctl/lead_runtime.py +72 -0
  23. package/runtime/python/okstra_ctl/pr_template.py +4 -3
  24. package/runtime/python/okstra_ctl/render.py +67 -16
  25. package/runtime/python/okstra_ctl/run.py +20 -12
  26. package/runtime/python/okstra_ctl/team.py +267 -0
  27. package/runtime/python/okstra_ctl/tmux.py +181 -10
  28. package/runtime/python/okstra_ctl/workflow.py +30 -71
  29. package/runtime/python/okstra_ctl/wrapper_status.py +55 -0
  30. package/runtime/schemas/final-report-v1.0.schema.json +3 -1
  31. package/runtime/skills/okstra-convergence/SKILL.md +3 -0
  32. package/runtime/skills/okstra-report-writer/SKILL.md +3 -0
  33. package/runtime/skills/okstra-team-contract/SKILL.md +12 -0
  34. package/runtime/validators/forbidden_actions.py +135 -0
  35. package/runtime/validators/validate-run.py +65 -9
  36. package/runtime/validators/validate_session_conformance.py +15 -10
  37. package/src/cli-registry.mjs +277 -0
  38. package/src/doctor.mjs +93 -4
  39. package/src/install.mjs +6 -5
  40. package/src/runtime-manifest.mjs +5 -2
  41. package/src/team.mjs +63 -0
  42. package/src/uninstall.mjs +1 -0
@@ -0,0 +1,277 @@
1
+ const SUMMARY_COLUMN = 25;
2
+
3
+ export const COMMAND_REGISTRY = [
4
+ {
5
+ name: "install",
6
+ module: "./install.mjs",
7
+ export: "runInstall",
8
+ category: "admin",
9
+ summary: ["Install runtime; --runtime codex skips ~/.claude assets"],
10
+ },
11
+ {
12
+ name: "ensure-installed",
13
+ module: "./install.mjs",
14
+ export: "runEnsureInstalled",
15
+ category: "admin",
16
+ summary: ["Verify install is fresh; reinstall if stale (idempotent)"],
17
+ },
18
+ {
19
+ name: "uninstall",
20
+ module: "./uninstall.mjs",
21
+ export: "runUninstall",
22
+ category: "admin",
23
+ summary: ["Remove runtime + skills (user data preserved by default)"],
24
+ },
25
+ {
26
+ name: "setup",
27
+ module: "./setup.mjs",
28
+ export: "run",
29
+ category: "admin",
30
+ summary: ["Register the current project (.okstra/project.json)"],
31
+ },
32
+ {
33
+ name: "doctor",
34
+ module: "./doctor.mjs",
35
+ export: "run",
36
+ category: "admin",
37
+ summary: [
38
+ "Diagnostic check; --runtime selects assets, --phase checks readiness",
39
+ ],
40
+ },
41
+ {
42
+ name: "check-project",
43
+ module: "./check-project.mjs",
44
+ export: "run",
45
+ category: "admin",
46
+ summary: ["Verify the current project has been registered with setup"],
47
+ },
48
+ {
49
+ name: "config",
50
+ module: "./config.mjs",
51
+ export: "run",
52
+ category: "admin",
53
+ summary: ["Read / write okstra settings (e.g. PR template path)"],
54
+ },
55
+ {
56
+ name: "migrate",
57
+ module: "./migrate.mjs",
58
+ export: "run",
59
+ category: "admin",
60
+ summary: ["Move legacy .project-docs/okstra/ to .okstra/ (one-shot)"],
61
+ },
62
+ {
63
+ name: "git-reconcile",
64
+ module: "./git-reconcile.mjs",
65
+ export: "run",
66
+ category: "admin",
67
+ summary: ["Reconcile stale stage SHAs after external git history changes"],
68
+ },
69
+ {
70
+ name: "handoff",
71
+ module: "./handoff.mjs",
72
+ export: "run",
73
+ category: "admin",
74
+ summary: ["Stage-group release-handoff helpers (eligible/assemble/record)"],
75
+ },
76
+ {
77
+ name: "paths",
78
+ module: "./paths.mjs",
79
+ export: "run",
80
+ category: "admin",
81
+ summary: [
82
+ "Print runtime paths (workspace/agents/pythonpath/bin/home/version)",
83
+ ],
84
+ },
85
+ {
86
+ name: "task-list",
87
+ module: "./task-list.mjs",
88
+ export: "run",
89
+ category: "introspection",
90
+ summary: ["List tasks registered in the current project"],
91
+ },
92
+ {
93
+ name: "task-show",
94
+ module: "./task-show.mjs",
95
+ export: "run",
96
+ category: "introspection",
97
+ summary: ["Summarize a task's manifest + workflow phase state"],
98
+ },
99
+ {
100
+ name: "context-cost",
101
+ module: "./context-cost.mjs",
102
+ export: "run",
103
+ category: "introspection",
104
+ summary: ["Estimate file/read context cost for a task bundle"],
105
+ },
106
+ {
107
+ name: "worktree-lookup",
108
+ module: "./worktree-lookup.mjs",
109
+ export: "run",
110
+ category: "introspection",
111
+ summary: ["Look up registered worktree for a task-key"],
112
+ },
113
+ {
114
+ name: "plan-validate",
115
+ module: "./plan-validate.mjs",
116
+ export: "run",
117
+ category: "introspection",
118
+ summary: ["Check an approved-plan file for the approval marker"],
119
+ },
120
+ {
121
+ name: "render-bundle",
122
+ module: "./render-bundle.mjs",
123
+ export: "run",
124
+ category: "introspection",
125
+ summary: [
126
+ "Preview prepare_task_bundle() output (forwards to",
127
+ "python3 -m okstra_ctl.run --render-only)",
128
+ ],
129
+ },
130
+ {
131
+ name: "codex-run",
132
+ module: "./codex-run.mjs",
133
+ export: "run",
134
+ category: "introspection",
135
+ summary: [
136
+ "Prepare a render-only Codex lead task bundle",
137
+ "(--lead-runtime codex, no worker dispatch)",
138
+ ],
139
+ },
140
+ {
141
+ name: "codex-dispatch",
142
+ module: "./codex-dispatch.mjs",
143
+ export: "run",
144
+ category: "introspection",
145
+ summary: ["Dispatch CLI-backed workers for a prepared Codex run"],
146
+ },
147
+ {
148
+ name: "team",
149
+ module: "./team.mjs",
150
+ export: "run",
151
+ category: "introspection",
152
+ summary: ["Dispatch and await tmux-pane workers for external leads"],
153
+ },
154
+ {
155
+ name: "wizard",
156
+ module: "./wizard.mjs",
157
+ export: "run",
158
+ category: "introspection",
159
+ summary: [
160
+ "Drive the okstra-run interactive input state machine",
161
+ "(init / step / render-args / confirmation)",
162
+ ],
163
+ },
164
+ {
165
+ name: "token-usage",
166
+ module: "./token-usage.mjs",
167
+ export: "run",
168
+ category: "introspection",
169
+ summary: [
170
+ "Collect token usage for a run (wraps the installed",
171
+ "okstra-token-usage.py so skills avoid emitting",
172
+ 'python3 "$HOME/..." invocations).',
173
+ ],
174
+ },
175
+ {
176
+ name: "render-views",
177
+ module: "./render-views.mjs",
178
+ export: "run",
179
+ category: "introspection",
180
+ summary: ["Render slim AI + self-contained HTML views of a final report"],
181
+ },
182
+ {
183
+ name: "render-final-report",
184
+ module: "./render-final-report.mjs",
185
+ export: "run",
186
+ category: "introspection",
187
+ summary: ["Render the markdown sibling of a final-report data.json"],
188
+ },
189
+ {
190
+ name: "inject-report-index",
191
+ module: "./inject-report-index.mjs",
192
+ export: "run",
193
+ category: "introspection",
194
+ summary: ["Add the top-of-report Index + scroll anchors to a report"],
195
+ },
196
+ {
197
+ name: "spawn-followups",
198
+ module: "./spawn-followups.mjs",
199
+ export: "run",
200
+ category: "introspection",
201
+ summary: ["Create follow-up task bundles from a final report"],
202
+ },
203
+ {
204
+ name: "error-log",
205
+ module: "./error-log.mjs",
206
+ export: "run",
207
+ category: "introspection",
208
+ summary: ["Append run error events to the run error log"],
209
+ },
210
+ {
211
+ name: "memory",
212
+ module: "./memory.mjs",
213
+ export: "run",
214
+ category: "introspection",
215
+ summary: [
216
+ "Store and find user-home conversation memory under",
217
+ "~/.okstra/memory-book.",
218
+ ],
219
+ },
220
+ ];
221
+
222
+ export const COMMANDS = new Map(
223
+ COMMAND_REGISTRY.map((c) => [
224
+ c.name,
225
+ () => import(c.module).then((m) => m[c.export]),
226
+ ]),
227
+ );
228
+
229
+ const PREAMBLE = `okstra — multi-agent cross-verification orchestrator for Claude Code
230
+
231
+ This CLI is the installer/admin tool. Day-to-day usage happens inside a
232
+ Claude Code session via slash commands (/okstra-setup, /okstra-run, ...).
233
+
234
+ Quick start (CLI):
235
+ 1. npx -y okstra@latest install # one-time, this machine
236
+ 2. cd <your project>
237
+ 3. npx -y okstra@latest setup --project-id <id> # one-time, this project
238
+ 4. open a Claude Code session and run /okstra-run # start a task
239
+
240
+ Inside a Claude Code session you can replace step 3 with /okstra-setup —
241
+ both write the same <PROJECT_ROOT>/.okstra/project.json.
242
+
243
+ Usage:
244
+ okstra <command> [options]
245
+ `;
246
+
247
+ const GLOBAL_OPTIONS = ` --version Print okstra version and exit
248
+ --help Print this help`;
249
+
250
+ const FOOTER = `
251
+
252
+ Run 'okstra <command> --help' for command-specific options.
253
+ Docs: https://github.com/Devonshin/okstra#readme
254
+ `;
255
+
256
+ function renderRow(command) {
257
+ const [first, ...rest] = command.summary;
258
+ const head = " " + command.name.padEnd(SUMMARY_COLUMN - 2) + first;
259
+ const continuations = rest.map((line) => " ".repeat(SUMMARY_COLUMN) + line);
260
+ return [head, ...continuations].join("\n");
261
+ }
262
+
263
+ function renderRows(category) {
264
+ return COMMAND_REGISTRY.filter((c) => c.category === category)
265
+ .map(renderRow)
266
+ .join("\n");
267
+ }
268
+
269
+ export const USAGE =
270
+ PREAMBLE +
271
+ "\nAdmin commands:\n" +
272
+ renderRows("admin") +
273
+ "\n\nIntrospection commands (JSON output, used by skills to avoid python heredocs):\n" +
274
+ renderRows("introspection") +
275
+ "\n\nGlobal options:\n" +
276
+ GLOBAL_OPTIONS +
277
+ FOOTER;
package/src/doctor.mjs CHANGED
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { resolvePaths } from "./paths.mjs";
5
5
  import { fileExists, runProcess } from "./_proc.mjs";
6
+ import { runPythonSnippet } from "./_python-helper.mjs";
6
7
  import { runtimeIncludesClaudeAssets, validateInstallRuntime } from "./install.mjs";
7
8
 
8
9
  const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
@@ -12,9 +13,9 @@ const USAGE = `okstra doctor — diagnose the installed runtime
12
13
 
13
14
  Usage:
14
15
  okstra doctor Run all checks and print a summary
15
- okstra doctor --runtime <claude-code|codex|all>
16
+ okstra doctor --runtime <claude-code|codex|external|all>
17
+ okstra doctor --phase <phase>
16
18
  okstra doctor --json Machine-readable result
17
-
18
19
  Checks:
19
20
  - python3 binary and version (>= 3.10)
20
21
  - okstra_project module importable from $HOME/.okstra/lib/python
@@ -24,11 +25,19 @@ Checks:
24
25
  - canonical skills present at $HOME/.claude/skills/<name>/SKILL.md
25
26
  (skipped with --runtime codex)
26
27
  - version stamp matches package version
28
+ - optional phase readiness checks with --phase <phase>
29
+
30
+ Supported phase readiness checks:
31
+ - implementation: git worktree support, approved-plan validator, worker agents
32
+ - final-verification: worktree registry, validators, final-report assets
33
+ - release-handoff: gh auth, clean git tree, remote, non-base branch
34
+ - improvement-discovery: required workers and lens whitelist
27
35
  `;
28
36
 
29
37
  export function parseDoctorArgs(args) {
30
38
  const result = {
31
39
  jsonMode: false,
40
+ phase: null,
32
41
  runtime: "claude-code",
33
42
  };
34
43
  for (let i = 0; i < args.length; i++) {
@@ -38,10 +47,17 @@ export function parseDoctorArgs(args) {
38
47
  } else if (arg === "--runtime" || arg.startsWith("--runtime=")) {
39
48
  const value = arg.startsWith("--runtime=") ? arg.slice("--runtime=".length) : args[i + 1];
40
49
  if (!value || value.startsWith("--")) {
41
- throw new Error("--runtime requires one of: claude-code, codex, all");
50
+ throw new Error("--runtime requires one of: claude-code, codex, external, all");
42
51
  }
43
52
  result.runtime = validateInstallRuntime(value);
44
53
  if (arg === "--runtime") i++;
54
+ } else if (arg === "--phase" || arg.startsWith("--phase=")) {
55
+ const value = arg.startsWith("--phase=") ? arg.slice("--phase=".length) : args[i + 1];
56
+ if (!value || value.startsWith("--")) {
57
+ throw new Error("--phase requires a phase name");
58
+ }
59
+ result.phase = value;
60
+ if (arg === "--phase") i++;
45
61
  } else {
46
62
  throw new Error(`unknown argument '${arg}'`);
47
63
  }
@@ -53,6 +69,24 @@ export function requiredSkillNamesForRuntime(runtime) {
53
69
  return runtimeIncludesClaudeAssets(runtime) ? REQUIRED_SKILLS : [];
54
70
  }
55
71
 
72
+ export function phaseDiagnosticsToChecks(phase, phaseDiagnostics) {
73
+ const checks = Array.isArray(phaseDiagnostics.checks) ? phaseDiagnostics.checks : [];
74
+ if (checks.length === 0) {
75
+ return [
76
+ {
77
+ name: `phase ${phase}: diagnostics`,
78
+ ok: false,
79
+ detail: phaseDiagnostics.reason || "phase diagnostics returned no checks",
80
+ },
81
+ ];
82
+ }
83
+ return checks.map((item) => ({
84
+ name: `phase ${phase}: ${item.name}`,
85
+ ok: Boolean(item.ok),
86
+ detail: String(item.detail ?? ""),
87
+ }));
88
+ }
89
+
56
90
  async function checkPython3() {
57
91
  const r = await runProcess("python3", ["--version"]);
58
92
  if (r.code !== 0) return { ok: false, detail: `python3 not found: ${r.stderr.trim() || "missing binary"}` };
@@ -99,6 +133,42 @@ async function check(name, fn) {
99
133
  }
100
134
  }
101
135
 
136
+ async function loadPhaseDiagnostics(phase, paths) {
137
+ const script = [
138
+ "import json, sys",
139
+ "from okstra_ctl.doctor import phase_diagnostics",
140
+ "payload = phase_diagnostics(",
141
+ " sys.argv[1],",
142
+ " cwd=sys.argv[2],",
143
+ " workspace_root=sys.argv[3],",
144
+ " home=sys.argv[4],",
145
+ ")",
146
+ "print(json.dumps(payload, ensure_ascii=False))",
147
+ ].join("\n");
148
+ const result = await runPythonSnippet({
149
+ script,
150
+ args: [phase, process.cwd(), paths.workspace, paths.home],
151
+ });
152
+ if (result.code !== 0 && !result.stdout.trim()) {
153
+ return {
154
+ ok: false,
155
+ usageError: false,
156
+ reason: `python invocation failed: ${result.stderr.trim() || "no output"}`,
157
+ checks: [],
158
+ };
159
+ }
160
+ try {
161
+ return JSON.parse(result.stdout);
162
+ } catch (err) {
163
+ return {
164
+ ok: false,
165
+ usageError: false,
166
+ reason: `could not parse phase diagnostics output: ${err.message}`,
167
+ checks: [],
168
+ };
169
+ }
170
+ }
171
+
102
172
  export async function run(args) {
103
173
  if (args.includes("--help") || args.includes("-h")) {
104
174
  process.stdout.write(USAGE);
@@ -143,10 +213,29 @@ export async function run(args) {
143
213
  ),
144
214
  ];
145
215
 
216
+ let phaseDiagnostics = null;
217
+ if (opts.phase) {
218
+ phaseDiagnostics = await loadPhaseDiagnostics(opts.phase, paths);
219
+ if (phaseDiagnostics.usageError) {
220
+ process.stderr.write(`error: ${phaseDiagnostics.reason}\n`);
221
+ return 2;
222
+ }
223
+ results.push(...phaseDiagnosticsToChecks(opts.phase, phaseDiagnostics));
224
+ }
225
+
146
226
  const allOk = results.every((r) => r.ok);
147
227
 
148
228
  if (opts.jsonMode) {
149
- process.stdout.write(`${JSON.stringify({ ok: allOk, runtime: opts.runtime, paths, checks: results }, null, 2)}\n`);
229
+ process.stdout.write(
230
+ `${JSON.stringify({
231
+ ok: allOk,
232
+ runtime: opts.runtime,
233
+ phase: opts.phase,
234
+ paths,
235
+ phaseDiagnostics,
236
+ checks: results,
237
+ }, null, 2)}\n`,
238
+ );
150
239
  return allOk ? 0 : 1;
151
240
  }
152
241
 
package/src/install.mjs CHANGED
@@ -21,6 +21,7 @@ const PYTHON_PACKAGES = ["okstra_project", "okstra_ctl", "okstra_token_usage", "
21
21
  const BIN_ENTRYPOINTS = [
22
22
  "okstra.sh",
23
23
  "okstra-codex-exec.sh",
24
+ "okstra-claude-exec.sh",
24
25
  "okstra-gemini-exec.sh",
25
26
  "okstra-trace-cleanup.sh",
26
27
  "okstra-team-reconcile.sh",
@@ -37,7 +38,7 @@ const INSTALL_USAGE = `okstra install — install runtime into ~/.okstra
37
38
 
38
39
  Usage:
39
40
  okstra install Install/update runtime (copy mode)
40
- okstra install --runtime <claude-code|codex|all>
41
+ okstra install --runtime <claude-code|codex|external|all>
41
42
  okstra install --link <p> Dev only: symlink ~/.okstra/{lib/python,bin}
42
43
  to the source repo at <p>. agents/ resolves
43
44
  to <p>/agents. Not advertised to end users.
@@ -81,13 +82,13 @@ const ENSURE_USAGE = `okstra ensure-installed — idempotent install check
81
82
 
82
83
  Usage:
83
84
  okstra ensure-installed Verify install; reinstall if version/files drift
84
- okstra ensure-installed --runtime <claude-code|codex|all>
85
+ okstra ensure-installed --runtime <claude-code|codex|external|all>
85
86
  okstra ensure-installed -q Same, but suppress success output
86
87
 
87
88
  Skills call this on every run. Returns 0 quickly when the install is fresh.
88
89
  `;
89
90
 
90
- const INSTALL_RUNTIMES = new Set(["claude-code", "codex", "all"]);
91
+ const INSTALL_RUNTIMES = new Set(["claude-code", "codex", "external", "all"]);
91
92
 
92
93
  export function runtimeIncludesClaudeAssets(runtime) {
93
94
  return runtime === "claude-code" || runtime === "all";
@@ -100,14 +101,14 @@ function readRuntimeFlag(args, index) {
100
101
  }
101
102
  const next = args[index + 1];
102
103
  if (!next || next.startsWith("--")) {
103
- throw new Error("--runtime requires one of: claude-code, codex, all");
104
+ throw new Error("--runtime requires one of: claude-code, codex, external, all");
104
105
  }
105
106
  return { value: next, nextIndex: index + 1 };
106
107
  }
107
108
 
108
109
  export function validateInstallRuntime(value) {
109
110
  if (!INSTALL_RUNTIMES.has(value)) {
110
- throw new Error(`unknown runtime '${value}' (expected: claude-code, codex, all)`);
111
+ throw new Error(`unknown runtime '${value}' (expected: claude-code, codex, external, all)`);
111
112
  }
112
113
  return value;
113
114
  }
@@ -1,14 +1,15 @@
1
1
  export const RUNTIMES_MANIFEST_REL = "installed-runtimes.json";
2
2
 
3
3
  export function runtimeListForSelection(runtime) {
4
- return runtime === "all" ? ["claude-code", "codex"] : [runtime];
4
+ if (runtime === "all") return ["claude-code", "codex", "external"];
5
+ return [runtime];
5
6
  }
6
7
 
7
8
  export function buildRuntimeManifest(runtime, installedAt = new Date().toISOString()) {
8
9
  const includesClaude = runtime === "claude-code" || runtime === "all";
9
10
  const includesCodex = runtime === "codex" || runtime === "all";
11
+ const includesExternal = runtime === "external" || runtime === "all";
10
12
  return {
11
- version: 1,
12
13
  installedAt,
13
14
  runtime,
14
15
  runtimes: runtimeListForSelection(runtime),
@@ -17,7 +18,9 @@ export function buildRuntimeManifest(runtime, installedAt = new Date().toISOStri
17
18
  claudeSkills: includesClaude,
18
19
  claudeAgents: includesClaude,
19
20
  codexAdapter: includesCodex,
21
+ externalAdapter: includesExternal,
20
22
  },
23
+ version: 1,
21
24
  };
22
25
  }
23
26
 
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
@@ -10,6 +10,7 @@ import {
10
10
  const BIN_ENTRYPOINTS = [
11
11
  "okstra.sh",
12
12
  "okstra-codex-exec.sh",
13
+ "okstra-claude-exec.sh",
13
14
  "okstra-gemini-exec.sh",
14
15
  "okstra-trace-cleanup.sh",
15
16
  "okstra-team-reconcile.sh",