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
@@ -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;
@@ -0,0 +1,70 @@
1
+ import { runPythonModule } from "./_python-helper.mjs";
2
+ import { resolvePaths } from "./paths.mjs";
3
+
4
+ const USAGE = `okstra codex-dispatch — dispatch CLI-backed workers for a prepared Codex run
5
+
6
+ Usage:
7
+ okstra codex-dispatch --project-root <dir> --run-manifest <path> \\
8
+ [--workers codex[,gemini[,report-writer]]] [--dry-run] \\
9
+ [--idle-timeout-seconds <n>] \\
10
+ [--enable-codex-report-writer --report-writer-codex-model <model>]
11
+
12
+ This command reads an existing run manifest produced by 'okstra codex-run' and
13
+ dispatches only Codex-side supported workers. When --workers is omitted, the
14
+ command selects the supported subset of the run roster (codex/gemini, plus
15
+ report-writer only when explicitly opted in). Codex and gemini use their
16
+ existing wrappers. The report-writer role is available only through explicit
17
+ Codex opt-in because the prepared manifest still records the Claude-side report
18
+ writer model by default. Explicitly requesting unsupported workers such as
19
+ claude fails.
20
+
21
+ Missing worker prompt files are generated automatically from the run manifest
22
+ and active-run-context. Existing prompt files are never overwritten.
23
+ When the Codex report-writer path completes, this command also runs the
24
+ idempotent post-report steps: token-usage substitution, render-views,
25
+ spawn-followups, and validate-run.
26
+
27
+ --workspace-root and --okstra-bin are owned by this command.
28
+ `;
29
+
30
+ const OWNED_FLAGS = new Set(["--workspace-root", "--okstra-bin"]);
31
+
32
+ function isOwnedFlag(arg) {
33
+ if (OWNED_FLAGS.has(arg)) return true;
34
+ return [...OWNED_FLAGS].some((flag) => arg.startsWith(`${flag}=`));
35
+ }
36
+
37
+ export function buildCodexDispatchArgs(args, paths) {
38
+ return [
39
+ "--workspace-root", paths.workspace,
40
+ "--okstra-bin", paths.bin,
41
+ ...args,
42
+ ];
43
+ }
44
+
45
+ export async function run(args) {
46
+ if (args.includes("--help") || args.includes("-h")) {
47
+ process.stdout.write(USAGE);
48
+ return 0;
49
+ }
50
+ if (args.length === 0) {
51
+ process.stdout.write(USAGE);
52
+ return 2;
53
+ }
54
+
55
+ const forbidden = args.find(isOwnedFlag);
56
+ if (forbidden) {
57
+ process.stderr.write(
58
+ `error: ${forbidden} is set by 'okstra codex-dispatch' itself — remove it from your args\n`,
59
+ );
60
+ return 2;
61
+ }
62
+
63
+ const paths = await resolvePaths();
64
+ const result = await runPythonModule({
65
+ module: "okstra_ctl.codex_dispatch",
66
+ args: buildCodexDispatchArgs(args, paths),
67
+ stdio: "inherit-stdout",
68
+ });
69
+ return result.code ?? 0;
70
+ }
@@ -0,0 +1,68 @@
1
+ import { runPythonModule } from "./_python-helper.mjs";
2
+ import { resolvePaths } from "./paths.mjs";
3
+
4
+ const USAGE = `okstra codex-run — prepare a render-only Codex lead task bundle
5
+
6
+ This is the dry-run entry point for the Codex lead adapter. It forwards to
7
+ \`python3 -m okstra_ctl.run --render-only --lead-runtime codex\`, materializes
8
+ the normal okstra task bundle, and prints the rendered lead prompt without
9
+ dispatching workers.
10
+
11
+ Usage:
12
+ okstra codex-run --project-root <dir> --project-id <id> \\
13
+ --task-group <tg> --task-id <tid> --task-type <type> \\
14
+ --task-brief <path-or-rel> [--workers <list>] [--directive <text>] \\
15
+ [--lead-model <m>] [--claude-model <m>] [--codex-model <m>] \\
16
+ [--gemini-model <m>] [--report-writer-model <m>] \\
17
+ [--related-tasks <list>] [--base-ref <ref>] \\
18
+ [--clarification-response <path>] [--work-category <cat>] \\
19
+ [--stage <auto|N>] [--stages <csv>] [--pr-template-path <path>] \\
20
+ [--fix-cycle <yes|no>]
21
+
22
+ --render-only and --lead-runtime are owned by this command.
23
+ Worker dispatch is intentionally disabled here; use 'okstra codex-dispatch'
24
+ with the rendered run manifest for CLI-backed codex/gemini workers.
25
+ `;
26
+
27
+ const OWNED_FLAGS = new Set(["--workspace-root", "--render-only", "--lead-runtime"]);
28
+
29
+ function isOwnedFlag(arg) {
30
+ if (OWNED_FLAGS.has(arg)) return true;
31
+ return [...OWNED_FLAGS].some((flag) => arg.startsWith(`${flag}=`));
32
+ }
33
+
34
+ export function buildCodexRunArgs(args, workspaceRoot) {
35
+ return [
36
+ "--workspace-root", workspaceRoot,
37
+ "--render-only",
38
+ "--lead-runtime", "codex",
39
+ ...args,
40
+ ];
41
+ }
42
+
43
+ export async function run(args) {
44
+ if (args.includes("--help") || args.includes("-h")) {
45
+ process.stdout.write(USAGE);
46
+ return 0;
47
+ }
48
+ if (args.length === 0) {
49
+ process.stdout.write(USAGE);
50
+ return 2;
51
+ }
52
+
53
+ const forbidden = args.find(isOwnedFlag);
54
+ if (forbidden) {
55
+ process.stderr.write(
56
+ `error: ${forbidden} is set by 'okstra codex-run' itself — remove it from your args\n`,
57
+ );
58
+ return 2;
59
+ }
60
+
61
+ const paths = await resolvePaths();
62
+ const result = await runPythonModule({
63
+ module: "okstra_ctl.run",
64
+ args: buildCodexRunArgs(args, paths.workspace),
65
+ stdio: "inherit-stdout",
66
+ });
67
+ return result.code ?? 0;
68
+ }
package/src/doctor.mjs CHANGED
@@ -3,6 +3,8 @@ 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";
7
+ import { runtimeIncludesClaudeAssets, validateInstallRuntime } from "./install.mjs";
6
8
 
7
9
  const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
8
10
  const REQUIRED_SKILLS = ["okstra-setup", "okstra-run"];
@@ -11,8 +13,9 @@ const USAGE = `okstra doctor — diagnose the installed runtime
11
13
 
12
14
  Usage:
13
15
  okstra doctor Run all checks and print a summary
16
+ okstra doctor --runtime <claude-code|codex|external|all>
17
+ okstra doctor --phase <phase>
14
18
  okstra doctor --json Machine-readable result
15
-
16
19
  Checks:
17
20
  - python3 binary and version (>= 3.10)
18
21
  - okstra_project module importable from $HOME/.okstra/lib/python
@@ -20,9 +23,70 @@ Checks:
20
23
  - bash entrypoints present and executable in $HOME/.okstra/bin
21
24
  - agents/ directory exists inside the okstra package
22
25
  - canonical skills present at $HOME/.claude/skills/<name>/SKILL.md
26
+ (skipped with --runtime codex)
23
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
24
35
  `;
25
36
 
37
+ export function parseDoctorArgs(args) {
38
+ const result = {
39
+ jsonMode: false,
40
+ phase: null,
41
+ runtime: "claude-code",
42
+ };
43
+ for (let i = 0; i < args.length; i++) {
44
+ const arg = args[i];
45
+ if (arg === "--json") {
46
+ result.jsonMode = true;
47
+ } else if (arg === "--runtime" || arg.startsWith("--runtime=")) {
48
+ const value = arg.startsWith("--runtime=") ? arg.slice("--runtime=".length) : args[i + 1];
49
+ if (!value || value.startsWith("--")) {
50
+ throw new Error("--runtime requires one of: claude-code, codex, external, all");
51
+ }
52
+ result.runtime = validateInstallRuntime(value);
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++;
61
+ } else {
62
+ throw new Error(`unknown argument '${arg}'`);
63
+ }
64
+ }
65
+ return result;
66
+ }
67
+
68
+ export function requiredSkillNamesForRuntime(runtime) {
69
+ return runtimeIncludesClaudeAssets(runtime) ? REQUIRED_SKILLS : [];
70
+ }
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
+
26
90
  async function checkPython3() {
27
91
  const r = await runProcess("python3", ["--version"]);
28
92
  if (r.code !== 0) return { ok: false, detail: `python3 not found: ${r.stderr.trim() || "missing binary"}` };
@@ -69,12 +133,54 @@ async function check(name, fn) {
69
133
  }
70
134
  }
71
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
+
72
172
  export async function run(args) {
73
173
  if (args.includes("--help") || args.includes("-h")) {
74
174
  process.stdout.write(USAGE);
75
175
  return 0;
76
176
  }
77
- const jsonMode = args.includes("--json");
177
+ let opts;
178
+ try {
179
+ opts = parseDoctorArgs(args);
180
+ } catch (err) {
181
+ process.stderr.write(`error: ${err.message}\n`);
182
+ return 2;
183
+ }
78
184
  const paths = await resolvePaths();
79
185
 
80
186
  const results = [
@@ -91,7 +197,7 @@ export async function run(args) {
91
197
  await check("bin: okstra-gemini-exec.sh", () => checkBashEntry(paths.bin, "okstra-gemini-exec.sh")),
92
198
  await check("bin: okstra-central.sh", () => checkBashEntry(paths.bin, "okstra-central.sh")),
93
199
  ...(await Promise.all(
94
- REQUIRED_SKILLS.map((name) =>
200
+ requiredSkillNamesForRuntime(opts.runtime).map((name) =>
95
201
  check(`skill: ${name}`, async () => {
96
202
  const p = join(CLAUDE_SKILLS_DIR, name, "SKILL.md");
97
203
  return (await fileExists(p))
@@ -107,10 +213,29 @@ export async function run(args) {
107
213
  ),
108
214
  ];
109
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
+
110
226
  const allOk = results.every((r) => r.ok);
111
227
 
112
- if (jsonMode) {
113
- process.stdout.write(`${JSON.stringify({ ok: allOk, paths, checks: results }, null, 2)}\n`);
228
+ if (opts.jsonMode) {
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
+ );
114
239
  return allOk ? 0 : 1;
115
240
  }
116
241