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.
- package/README.kr.md +5 -3
- package/README.md +5 -3
- package/bin/okstra +1 -117
- package/docs/contributor-change-matrix.md +12 -0
- package/docs/kr/architecture.md +1 -1
- package/docs/kr/cli.md +22 -5
- package/docs/pr-template-usage.md +3 -3
- package/docs/project-structure-overview.md +1 -1
- package/docs/superpowers/plans/2026-05-25-okstra-project-root-rename.md +1 -1
- package/docs/superpowers/plans/2026-06-13-repo-risk-hardening.md +493 -0
- package/docs/superpowers/specs/2026-06-12-codex-lead-adapter-design.md +358 -0
- package/docs/superpowers/specs/2026-06-13-forbidden-actions-ssot-design.md +134 -0
- package/docs/superpowers/specs/2026-06-13-neutral-tmux-lead-adapter-design.md +284 -0
- package/package.json +5 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/DO_NOT_EDIT.md +21 -0
- package/runtime/agents/SKILL.md +3 -0
- package/runtime/bin/lib/okstra/cli.sh +5 -1
- package/runtime/bin/lib/okstra/globals.sh +1 -0
- package/runtime/bin/lib/okstra/usage.sh +6 -4
- package/runtime/bin/okstra-trace-cleanup.sh +16 -12
- package/runtime/bin/okstra.sh +1 -0
- package/runtime/prompts/launch.template.md +4 -13
- package/runtime/prompts/profiles/error-analysis.md +6 -0
- package/runtime/prompts/profiles/forbidden-actions.json +69 -0
- package/runtime/prompts/profiles/implementation.md +1 -8
- package/runtime/prompts/profiles/improvement-discovery.md +5 -0
- package/runtime/prompts/profiles/release-handoff.md +3 -17
- package/runtime/python/okstra_ctl/analysis_packet.py +17 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +1552 -0
- package/runtime/python/okstra_ctl/context_cost.py +1 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +897 -0
- package/runtime/python/okstra_ctl/doctor.py +292 -0
- package/runtime/python/okstra_ctl/lead_events.py +129 -0
- package/runtime/python/okstra_ctl/lead_runtime.py +72 -0
- package/runtime/python/okstra_ctl/paths.py +3 -0
- package/runtime/python/okstra_ctl/pr_template.py +1 -1
- package/runtime/python/okstra_ctl/render.py +211 -29
- package/runtime/python/okstra_ctl/run.py +89 -18
- package/runtime/python/okstra_ctl/team.py +267 -0
- package/runtime/python/okstra_ctl/tmux.py +181 -10
- package/runtime/python/okstra_ctl/workflow.py +30 -71
- package/runtime/python/okstra_ctl/wrapper_status.py +55 -0
- package/runtime/python/okstra_token_usage/codex.py +12 -7
- package/runtime/python/okstra_token_usage/collect.py +243 -54
- package/runtime/python/okstra_token_usage/gemini.py +12 -7
- package/runtime/schemas/final-report-v1.0.schema.json +3 -1
- package/runtime/skills/okstra-convergence/SKILL.md +3 -0
- package/runtime/skills/okstra-report-writer/SKILL.md +3 -0
- package/runtime/skills/okstra-setup/SKILL.md +1 -1
- package/runtime/skills/okstra-team-contract/SKILL.md +12 -0
- package/runtime/validators/forbidden_actions.py +135 -0
- package/runtime/validators/validate-run.py +112 -22
- package/runtime/validators/validate_session_conformance.py +127 -5
- package/src/cli-registry.mjs +277 -0
- package/src/codex-dispatch.mjs +70 -0
- package/src/codex-run.mjs +68 -0
- package/src/doctor.mjs +130 -5
- package/src/install.mjs +123 -26
- package/src/paths.mjs +17 -3
- package/src/render-bundle.mjs +2 -0
- package/src/runtime-manifest.mjs +29 -0
- package/src/team.mjs +63 -0
- package/src/uninstall.mjs +27 -4
- /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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|