okstra 0.77.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 +1 -1
- package/README.md +1 -1
- package/bin/okstra +1 -125
- package/docs/contributor-change-matrix.md +12 -0
- package/docs/kr/cli.md +5 -4
- package/docs/project-structure-overview.md +1 -1
- package/docs/superpowers/plans/2026-06-13-repo-risk-hardening.md +493 -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/okstra-trace-cleanup.sh +16 -12
- package/runtime/prompts/profiles/forbidden-actions.json +69 -0
- package/runtime/prompts/profiles/implementation.md +1 -8
- package/runtime/prompts/profiles/release-handoff.md +1 -15
- package/runtime/python/okstra_ctl/codex_dispatch.py +70 -2
- 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_runtime.py +72 -0
- package/runtime/python/okstra_ctl/render.py +67 -16
- package/runtime/python/okstra_ctl/run.py +20 -12
- 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/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-team-contract/SKILL.md +12 -0
- package/runtime/validators/forbidden_actions.py +135 -0
- package/runtime/validators/validate-run.py +65 -9
- package/runtime/validators/validate_session_conformance.py +15 -10
- package/src/cli-registry.mjs +277 -0
- package/src/doctor.mjs +93 -4
- package/src/install.mjs +6 -5
- package/src/runtime-manifest.mjs +5 -2
- package/src/team.mjs +63 -0
- 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(
|
|
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
|
}
|
package/src/runtime-manifest.mjs
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
export const RUNTIMES_MANIFEST_REL = "installed-runtimes.json";
|
|
2
2
|
|
|
3
3
|
export function runtimeListForSelection(runtime) {
|
|
4
|
-
|
|
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
|
+
}
|