okstra 0.14.2 → 0.16.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/bin/okstra +13 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/skills/okstra-run/SKILL.md +43 -74
- package/runtime/skills/okstra-setup/SKILL.md +5 -17
- package/runtime/templates/reports/settings.template.json +3 -0
- package/src/_python-helper.mjs +42 -0
- package/src/plan-validate.mjs +68 -0
- package/src/render-bundle.mjs +60 -0
- package/src/task-list.mjs +86 -0
- package/src/task-show.mjs +120 -0
- package/src/worktree-lookup.mjs +91 -0
package/bin/okstra
CHANGED
|
@@ -9,6 +9,11 @@ const COMMANDS = new Map([
|
|
|
9
9
|
["doctor", () => import("../src/doctor.mjs").then((m) => m.run)],
|
|
10
10
|
["setup", () => import("../src/setup.mjs").then((m) => m.run)],
|
|
11
11
|
["check-project", () => import("../src/check-project.mjs").then((m) => m.run)],
|
|
12
|
+
["task-list", () => import("../src/task-list.mjs").then((m) => m.run)],
|
|
13
|
+
["task-show", () => import("../src/task-show.mjs").then((m) => m.run)],
|
|
14
|
+
["worktree-lookup", () => import("../src/worktree-lookup.mjs").then((m) => m.run)],
|
|
15
|
+
["plan-validate", () => import("../src/plan-validate.mjs").then((m) => m.run)],
|
|
16
|
+
["render-bundle", () => import("../src/render-bundle.mjs").then((m) => m.run)],
|
|
12
17
|
]);
|
|
13
18
|
|
|
14
19
|
const USAGE = `okstra — multi-agent cross-verification orchestrator for Claude Code
|
|
@@ -37,6 +42,14 @@ Admin commands:
|
|
|
37
42
|
check-project Verify the current project has been registered with setup
|
|
38
43
|
paths Print runtime paths (workspace/agents/pythonpath/bin/home/version)
|
|
39
44
|
|
|
45
|
+
Introspection commands (JSON output, used by skills to avoid python heredocs):
|
|
46
|
+
task-list List tasks registered in the current project
|
|
47
|
+
task-show Summarize a task's manifest + workflow phase state
|
|
48
|
+
worktree-lookup Look up registered worktree for a task-key
|
|
49
|
+
plan-validate Check an approved-plan file for the approval marker
|
|
50
|
+
render-bundle Preview prepare_task_bundle() output (forwards to
|
|
51
|
+
python3 -m okstra_ctl.run --render-only)
|
|
52
|
+
|
|
40
53
|
Global options:
|
|
41
54
|
--version Print okstra version and exit
|
|
42
55
|
--help Print this help
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -75,35 +75,23 @@ After Step 0 the following are guaranteed:
|
|
|
75
75
|
|
|
76
76
|
## Step 1: Resolve PROJECT_ROOT and projectId
|
|
77
77
|
|
|
78
|
+
Prefer `$OKSTRA_PROJECT_INFO` from Step 0 — it already carries `{ok, projectRoot, projectJsonPath, projectId}`. Only re-resolve when that JSON's `ok` is false (cwd outside an okstra project):
|
|
79
|
+
|
|
78
80
|
```bash
|
|
79
|
-
|
|
80
|
-
import sys, json
|
|
81
|
-
from okstra_project import resolve_project_root, ResolverError
|
|
82
|
-
try:
|
|
83
|
-
pr = resolve_project_root(explicit_root="", cwd=".")
|
|
84
|
-
except ResolverError as e:
|
|
85
|
-
print(f"FAIL\t{e}"); raise SystemExit(0)
|
|
86
|
-
print(f"OK\t{pr}")
|
|
87
|
-
PY
|
|
81
|
+
okstra check-project --cwd "$(pwd)"
|
|
88
82
|
```
|
|
89
83
|
|
|
90
|
-
- If `
|
|
91
|
-
- If `
|
|
84
|
+
- If `ok: true`: read `projectRoot` and `projectId` from the JSON.
|
|
85
|
+
- If `ok: false`: ask the user (`AskUserQuestion`, free text) for an absolute project-root path; rerun with `okstra check-project --cwd <their input>`.
|
|
92
86
|
|
|
93
87
|
## Step 2: Choose task — existing vs new
|
|
94
88
|
|
|
95
89
|
```bash
|
|
96
|
-
|
|
97
|
-
import json, sys
|
|
98
|
-
from pathlib import Path
|
|
99
|
-
from okstra_project import list_project_tasks, read_latest_task
|
|
100
|
-
pr = Path(sys.argv[1])
|
|
101
|
-
tasks = list_project_tasks(pr)
|
|
102
|
-
latest = read_latest_task(pr)
|
|
103
|
-
print(json.dumps({'tasks': tasks, 'latest': latest}))
|
|
104
|
-
" "$PROJECT_ROOT"
|
|
90
|
+
okstra task-list --project "$PROJECT_ROOT"
|
|
105
91
|
```
|
|
106
92
|
|
|
93
|
+
Output is JSON `{ok, projectRoot, tasks: [...], latest: {...}|null}`.
|
|
94
|
+
|
|
107
95
|
Use `AskUserQuestion`:
|
|
108
96
|
|
|
109
97
|
- **Label**: "Which task?"
|
|
@@ -151,21 +139,12 @@ silently inherits an unrelated branch you happen to be checked out on.
|
|
|
151
139
|
First, decide whether to ask:
|
|
152
140
|
|
|
153
141
|
```bash
|
|
154
|
-
|
|
155
|
-
import sys
|
|
156
|
-
from pathlib import Path
|
|
157
|
-
sys.path.insert(0, "$OKSTRA_PYTHONPATH".split(":")[0])
|
|
158
|
-
from okstra_ctl import worktree_registry
|
|
159
|
-
from okstra_ctl.ids import _safe_fs_segment
|
|
160
|
-
entry = worktree_registry.lookup(
|
|
161
|
-
_safe_fs_segment("<project-id>"),
|
|
162
|
-
_safe_fs_segment("<task-group>"),
|
|
163
|
-
_safe_fs_segment("<task-id>"),
|
|
164
|
-
)
|
|
165
|
-
print("REUSE" if (entry and entry.status == "active") else "ASK")
|
|
166
|
-
PY
|
|
142
|
+
okstra worktree-lookup "<project-id>" "<task-group>" "<task-id>"
|
|
167
143
|
```
|
|
168
144
|
|
|
145
|
+
Output JSON: `{ok: true, entry: null}` means no active worktree → **ASK**. A
|
|
146
|
+
non-null `entry` with `status: "active"` → **REUSE**.
|
|
147
|
+
|
|
169
148
|
- `REUSE` → the registered worktree is reused; set `base_ref=""` and skip the
|
|
170
149
|
question (the registered base is authoritative).
|
|
171
150
|
- `ASK` → this is the first phase for this task-key. Continue.
|
|
@@ -235,7 +214,7 @@ For prompts whose target worker is NOT in the resolved workers list (after overr
|
|
|
235
214
|
|
|
236
215
|
## Step 6.5: Confirm selections before rendering
|
|
237
216
|
|
|
238
|
-
Before
|
|
217
|
+
Before invoking `okstra render-bundle`, echo the resolved selections back to the user in a compact block so they can verify what will be passed. Show the **effective** values, not the raw input — i.e. when the user left a field blank, display `default` (and where known, the actual default such as `opus` / `sonnet`). Example for an `implementation` run:
|
|
239
218
|
|
|
240
219
|
```
|
|
241
220
|
선택 확인:
|
|
@@ -255,50 +234,40 @@ Before calling `prepare_task_bundle`, echo the resolved selections back to the u
|
|
|
255
234
|
|
|
256
235
|
Then `AskUserQuestion`: `"이대로 진행할까요?"` with options `Proceed` / `Edit`. On `Edit`, return to the relevant Step 6 sub-prompt.
|
|
257
236
|
|
|
258
|
-
## Step 7: Call `
|
|
237
|
+
## Step 7: Call `okstra render-bundle`
|
|
259
238
|
|
|
260
|
-
This is the single
|
|
239
|
+
This is the single command that materializes the entire task bundle. The
|
|
240
|
+
subcommand auto-supplies `--workspace-root` (from `okstra paths --field
|
|
241
|
+
workspace`) and forces `--render-only`, so the current claude session itself
|
|
242
|
+
takes over as lead — no new claude is spawned.
|
|
261
243
|
|
|
262
244
|
```bash
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
task_type="<task-type>",
|
|
280
|
-
brief_path=brief_abs,
|
|
281
|
-
directive="<directive or empty>",
|
|
282
|
-
workers_override="<comma-separated worker list, or empty for profile default; MUST be empty for implementation>",
|
|
283
|
-
lead_model="...", claude_model="...", codex_model="...",
|
|
284
|
-
gemini_model="...", report_writer_model="...",
|
|
285
|
-
related_tasks_raw="...",
|
|
286
|
-
executor="<claude|codex|gemini or empty>", # implementation only; empty → default (claude / OKSTRA_DEFAULT_EXECUTOR)
|
|
287
|
-
base_ref="<chosen-ref-from-step-4.6 or empty when reusing existing worktree>",
|
|
288
|
-
approved_plan_path="<approved-plan-or-empty>",
|
|
289
|
-
clarification_response_path=str(clarification_abs) if clarification_abs else "",
|
|
290
|
-
render_only=True,
|
|
291
|
-
))
|
|
292
|
-
|
|
293
|
-
# Print key paths so the next step can read them.
|
|
294
|
-
ctx = out.ctx
|
|
295
|
-
print("TASK_ROOT", ctx["TASK_ROOT"])
|
|
296
|
-
print("INSTRUCTION_SET_DIR", ctx["INSTRUCTION_SET_DIR"])
|
|
297
|
-
print("LEAD_PROMPT", str(Path(ctx["INSTRUCTION_SET_DIR"]) / "claude-execution-prompt.md"))
|
|
298
|
-
PY
|
|
245
|
+
okstra render-bundle \
|
|
246
|
+
--project-root "<project-root>" \
|
|
247
|
+
--project-id "<project-id>" \
|
|
248
|
+
--task-group "<task-group>" \
|
|
249
|
+
--task-id "<task-id>" \
|
|
250
|
+
--task-type "<task-type>" \
|
|
251
|
+
--task-brief "<brief-path-from-user>" \
|
|
252
|
+
--executor "<claude|codex|gemini or empty for default>" \
|
|
253
|
+
--approved-plan "<approved-plan-or-empty>" \
|
|
254
|
+
--base-ref "<chosen-ref-from-step-4.6 or empty when reusing existing worktree>" \
|
|
255
|
+
--workers "<comma-separated worker list, or empty for profile default; MUST be empty for implementation>" \
|
|
256
|
+
--directive "<directive or empty>" \
|
|
257
|
+
--lead-model "..." --claude-model "..." --codex-model "..." \
|
|
258
|
+
--gemini-model "..." --report-writer-model "..." \
|
|
259
|
+
--related-tasks "..." \
|
|
260
|
+
--clarification-response "<clarification-or-empty>"
|
|
299
261
|
```
|
|
300
262
|
|
|
301
|
-
|
|
263
|
+
Stdout prints `okstra task root:`, `okstra instruction-set:`, and the full
|
|
264
|
+
rendered lead-prompt text (because `--render-only` is on). Parse the labelled
|
|
265
|
+
lines to get `TASK_ROOT`, `INSTRUCTION_SET_DIR`, and from there the
|
|
266
|
+
`claude-execution-prompt.md` path used by Step 8.
|
|
267
|
+
|
|
268
|
+
The python function underneath is mutex-protected (`~/.okstra/.locks/<task-key>.lock`),
|
|
269
|
+
writes `run-context-*.json` + `run-inputs-*.json` + all manifests + discovery
|
|
270
|
+
files, and registers the run in `~/.okstra/recent.jsonl` with status `prepared`.
|
|
302
271
|
|
|
303
272
|
## Step 8: Take over as Claude lead
|
|
304
273
|
|
|
@@ -327,7 +296,7 @@ Inform the user with one short line:
|
|
|
327
296
|
|---|---|---|
|
|
328
297
|
| `okstra runtime missing: ...` | First run on this machine, or stale install | `npx okstra@latest install` once, retry. |
|
|
329
298
|
| `OKSTRA_PYTHONPATH unbound` / `ModuleNotFoundError: okstra_project` | Step 0 was skipped or env vars dropped | Re-run Step 0; never invoke python without exporting `PYTHONPATH=$OKSTRA_PYTHONPATH`. |
|
|
330
|
-
| `task root not found for <key>` | catalog entry stale or task-key typo | Re-run Step 2
|
|
299
|
+
| `task root not found for <key>` | catalog entry stale or task-key typo | Re-run Step 2 (`okstra task-list`) and show available keys |
|
|
331
300
|
| `PROJECT_ROOT 를 해석할 수 없습니다` | cwd outside okstra project, no git toplevel | Ask user for absolute path |
|
|
332
301
|
| `approved plan has no recognised user-approval marker` | `implementation` without proper approval | Ask user to add `APPROVED` to the plan, or pick a different task-type |
|
|
333
302
|
| `task brief not found` | brief-path doesn't resolve relative to cwd or project-root | Re-ask Step 5 |
|
|
@@ -72,19 +72,12 @@ them from the env vars.
|
|
|
72
72
|
## Step 3: Resolve PROJECT_ROOT
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
|
-
|
|
76
|
-
from okstra_project import resolve_project_root, ResolverError
|
|
77
|
-
try:
|
|
78
|
-
pr = resolve_project_root(explicit_root="", cwd=".")
|
|
79
|
-
print(f"OK\t{pr}")
|
|
80
|
-
except ResolverError as e:
|
|
81
|
-
print(f"FAIL\t{e}")
|
|
82
|
-
PY
|
|
75
|
+
okstra check-project --cwd "$(pwd)"
|
|
83
76
|
```
|
|
84
77
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
The JSON includes `projectRoot` on success. On failure (`ok: false`,
|
|
79
|
+
`stage: "resolve"`) ask the user (`AskUserQuestion`, free text) for an
|
|
80
|
+
absolute project root and rerun with `--cwd <their answer>`.
|
|
88
81
|
|
|
89
82
|
## Step 4: Inspect or create `project.json`
|
|
90
83
|
|
|
@@ -108,12 +101,7 @@ If the file does NOT exist, ask via `AskUserQuestion`:
|
|
|
108
101
|
Then create the file:
|
|
109
102
|
|
|
110
103
|
```bash
|
|
111
|
-
|
|
112
|
-
from pathlib import Path
|
|
113
|
-
from okstra_project import upsert_project_json
|
|
114
|
-
result = upsert_project_json(Path("$PROJECT_ROOT"), "$PROJECT_ID")
|
|
115
|
-
print(result)
|
|
116
|
-
PY
|
|
104
|
+
okstra setup --yes --project-root "$PROJECT_ROOT" --project-id "$PROJECT_ID"
|
|
117
105
|
```
|
|
118
106
|
|
|
119
107
|
## Step 4.5 (optional): customise worktree sync dirs
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
"Bash(pytest:*)",
|
|
47
47
|
"Bash(bash:*)",
|
|
48
48
|
"Bash(sh:*)",
|
|
49
|
+
"Bash(eval:*)",
|
|
50
|
+
"Bash(cd:*)",
|
|
49
51
|
"Bash(printf:*)",
|
|
50
52
|
"Bash(echo:*)",
|
|
51
53
|
"Bash(export:*)",
|
|
@@ -57,6 +59,7 @@
|
|
|
57
59
|
"Bash(false)",
|
|
58
60
|
"Bash(codex:*)",
|
|
59
61
|
"Bash(codex exec:*)",
|
|
62
|
+
"Bash(okstra:*)",
|
|
60
63
|
"Bash($HOME/.okstra/bin/okstra-codex-exec.sh:*)",
|
|
61
64
|
"Bash(gemini:*)",
|
|
62
65
|
"Bash($HOME/.okstra/bin/okstra-gemini-exec.sh:*)",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { resolvePaths } from "./paths.mjs";
|
|
3
|
+
|
|
4
|
+
export async function runPythonSnippet({ script, args = [], extraEnv = {} }) {
|
|
5
|
+
const paths = await resolvePaths();
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const child = spawn("python3", ["-c", script, ...args], {
|
|
8
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9
|
+
env: { ...process.env, PYTHONPATH: paths.pythonpath, ...extraEnv },
|
|
10
|
+
});
|
|
11
|
+
let stdout = "";
|
|
12
|
+
let stderr = "";
|
|
13
|
+
child.stdout.on("data", (b) => (stdout += b.toString()));
|
|
14
|
+
child.stderr.on("data", (b) => (stderr += b.toString()));
|
|
15
|
+
child.on("error", (err) => resolve({ code: -1, stdout, stderr: err.message }));
|
|
16
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function runPythonModule({ module, args = [], extraEnv = {}, stdio = "inherit-stdout" }) {
|
|
21
|
+
const paths = await resolvePaths();
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const child = spawn("python3", ["-m", module, ...args], {
|
|
24
|
+
stdio: stdio === "capture" ? ["ignore", "pipe", "pipe"] : ["ignore", "inherit", "inherit"],
|
|
25
|
+
env: { ...process.env, PYTHONPATH: paths.pythonpath, ...extraEnv },
|
|
26
|
+
});
|
|
27
|
+
let stdout = "";
|
|
28
|
+
let stderr = "";
|
|
29
|
+
if (stdio === "capture") {
|
|
30
|
+
child.stdout.on("data", (b) => (stdout += b.toString()));
|
|
31
|
+
child.stderr.on("data", (b) => (stderr += b.toString()));
|
|
32
|
+
}
|
|
33
|
+
child.on("error", (err) => resolve({ code: -1, stdout, stderr: err.message }));
|
|
34
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function emitJsonError({ stage, reason, extra = {} }) {
|
|
39
|
+
process.stdout.write(
|
|
40
|
+
JSON.stringify({ ok: false, stage, reason, ...extra }, null, 2) + "\n",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { runPythonSnippet, emitJsonError } from "./_python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra plan-validate — verify an approved-plan file has a recognised approval marker
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
okstra plan-validate <plan-path>
|
|
7
|
+
|
|
8
|
+
Output: JSON { ok: true, planPath } on success.
|
|
9
|
+
On failure: { ok: false, reason } with non-zero exit code.
|
|
10
|
+
|
|
11
|
+
Replaces the \`from okstra_ctl.run import _validate_approved_plan\` heredoc
|
|
12
|
+
pattern. Use this in flows that need to confirm a plan was approved
|
|
13
|
+
without invoking the full prepare_task_bundle pipeline.
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
function parseArgs(args) {
|
|
17
|
+
const positional = args.filter((a) => !a.startsWith("--"));
|
|
18
|
+
const unknown = args.filter((a) => a.startsWith("--"));
|
|
19
|
+
if (unknown.length) throw new Error(`unknown argument '${unknown[0]}'`);
|
|
20
|
+
if (positional.length !== 1) {
|
|
21
|
+
throw new Error("expected exactly one positional argument: <plan-path>");
|
|
22
|
+
}
|
|
23
|
+
return { planPath: positional[0] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SCRIPT = `
|
|
27
|
+
import json, sys
|
|
28
|
+
from okstra_ctl.run import _validate_approved_plan, PrepareError
|
|
29
|
+
|
|
30
|
+
path = sys.argv[1]
|
|
31
|
+
try:
|
|
32
|
+
_validate_approved_plan(path)
|
|
33
|
+
except PrepareError as e:
|
|
34
|
+
print(json.dumps({"ok": False, "stage": "validation", "reason": str(e), "planPath": path}))
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
|
|
37
|
+
print(json.dumps({"ok": True, "planPath": path}))
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
export async function run(args) {
|
|
41
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
42
|
+
process.stdout.write(USAGE);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
let opts;
|
|
46
|
+
try {
|
|
47
|
+
opts = parseArgs(args);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
process.stderr.write(`error: ${err.message}\n\n${USAGE}`);
|
|
50
|
+
return 2;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const result = await runPythonSnippet({
|
|
54
|
+
script: SCRIPT,
|
|
55
|
+
args: [opts.planPath],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (result.code !== 0 && !result.stdout.trim()) {
|
|
59
|
+
emitJsonError({
|
|
60
|
+
stage: "python",
|
|
61
|
+
reason: `python invocation failed: ${result.stderr.trim() || "no output"}`,
|
|
62
|
+
});
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
process.stdout.write(result.stdout);
|
|
67
|
+
return result.code === 0 ? 0 : result.code;
|
|
68
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { runPythonModule } from "./_python-helper.mjs";
|
|
2
|
+
import { resolvePaths } from "./paths.mjs";
|
|
3
|
+
|
|
4
|
+
const USAGE = `okstra render-bundle — preview the task bundle without launching claude
|
|
5
|
+
|
|
6
|
+
This is a thin shim over \`python3 -m okstra_ctl.run\` that forces
|
|
7
|
+
\`--render-only\`. Use it to preview where prepare_task_bundle() would
|
|
8
|
+
materialize files (INSTRUCTION_SET_DIR, RUN_DIR, lead prompt path,
|
|
9
|
+
final report template path, etc.) for a given task/phase, without
|
|
10
|
+
actually creating the run-state or spawning the launcher.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
okstra render-bundle --project-root <dir> --project-id <id> \\
|
|
14
|
+
--task-group <tg> --task-id <tid> --task-type <type> \\
|
|
15
|
+
--task-brief <path-or-rel> [--executor <name>] \\
|
|
16
|
+
[--approved-plan <path>] [--workers <list>] [--directive <text>] \\
|
|
17
|
+
[--lead-model <m>] [--claude-model <m>] [--codex-model <m>] \\
|
|
18
|
+
[--gemini-model <m>] [--report-writer-model <m>] \\
|
|
19
|
+
[--related-tasks <list>] [--base-ref <ref>] \\
|
|
20
|
+
[--clarification-response <path>] [--work-category <cat>]
|
|
21
|
+
|
|
22
|
+
All flags pass through unchanged to \`python3 -m okstra_ctl.run\`. The
|
|
23
|
+
shim auto-supplies \`--workspace-root\` (from \`okstra paths --field workspace\`)
|
|
24
|
+
and \`--render-only\`, so callers do not need to set those.
|
|
25
|
+
|
|
26
|
+
Replaces the long \`from okstra_ctl.run import PrepareInputs,
|
|
27
|
+
prepare_task_bundle\` heredoc pattern.
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
export async function run(args) {
|
|
31
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
32
|
+
process.stdout.write(USAGE);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
const paths = await resolvePaths();
|
|
36
|
+
|
|
37
|
+
// Callers should not pass these; we own them.
|
|
38
|
+
const forbidden = ["--workspace-root", "--render-only"];
|
|
39
|
+
for (const f of forbidden) {
|
|
40
|
+
if (args.includes(f)) {
|
|
41
|
+
process.stderr.write(
|
|
42
|
+
`error: ${f} is set by 'okstra render-bundle' itself — remove it from your args\n`,
|
|
43
|
+
);
|
|
44
|
+
return 2;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const finalArgs = [
|
|
49
|
+
"--workspace-root", paths.workspace,
|
|
50
|
+
"--render-only",
|
|
51
|
+
...args,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const result = await runPythonModule({
|
|
55
|
+
module: "okstra_ctl.run",
|
|
56
|
+
args: finalArgs,
|
|
57
|
+
stdio: "inherit-stdout",
|
|
58
|
+
});
|
|
59
|
+
return result.code ?? 0;
|
|
60
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { runPythonSnippet, emitJsonError } from "./_python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra task-list — list okstra tasks registered in this project
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
okstra task-list Resolve PROJECT_ROOT from cwd, list tasks
|
|
7
|
+
okstra task-list --cwd <dir> Start resolution from <dir> instead of cwd
|
|
8
|
+
okstra task-list --project <dir> Use <dir> directly as PROJECT_ROOT
|
|
9
|
+
|
|
10
|
+
Output: JSON { ok, projectRoot, tasks: [...], latest: {...}|null }.
|
|
11
|
+
|
|
12
|
+
Replaces the common pattern of loading okstra_project and calling
|
|
13
|
+
list_project_tasks + read_latest_task from a python heredoc.
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
function parseArgs(args) {
|
|
17
|
+
const opts = { cwd: process.cwd(), projectRoot: "" };
|
|
18
|
+
for (let i = 0; i < args.length; i++) {
|
|
19
|
+
const a = args[i];
|
|
20
|
+
if (a === "--cwd") {
|
|
21
|
+
opts.cwd = args[++i];
|
|
22
|
+
if (!opts.cwd) throw new Error("--cwd requires a path");
|
|
23
|
+
} else if (a === "--project") {
|
|
24
|
+
opts.projectRoot = args[++i];
|
|
25
|
+
if (!opts.projectRoot) throw new Error("--project requires a path");
|
|
26
|
+
} else {
|
|
27
|
+
throw new Error(`unknown argument '${a}'`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return opts;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SCRIPT = `
|
|
34
|
+
import json, sys
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from okstra_project import (
|
|
37
|
+
list_project_tasks, read_latest_task,
|
|
38
|
+
resolve_project_root, ResolverError,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
explicit = sys.argv[1]
|
|
42
|
+
cwd = sys.argv[2]
|
|
43
|
+
try:
|
|
44
|
+
pr = resolve_project_root(explicit_root=explicit, cwd=cwd)
|
|
45
|
+
except ResolverError as e:
|
|
46
|
+
print(json.dumps({"ok": False, "stage": "resolve", "reason": str(e)}))
|
|
47
|
+
sys.exit(2)
|
|
48
|
+
|
|
49
|
+
pr_path = Path(pr)
|
|
50
|
+
print(json.dumps({
|
|
51
|
+
"ok": True,
|
|
52
|
+
"projectRoot": str(pr_path),
|
|
53
|
+
"tasks": list_project_tasks(pr_path),
|
|
54
|
+
"latest": read_latest_task(pr_path),
|
|
55
|
+
}, default=str))
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
export async function run(args) {
|
|
59
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
60
|
+
process.stdout.write(USAGE);
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
let opts;
|
|
64
|
+
try {
|
|
65
|
+
opts = parseArgs(args);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
process.stderr.write(`error: ${err.message}\n\n${USAGE}`);
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const result = await runPythonSnippet({
|
|
72
|
+
script: SCRIPT,
|
|
73
|
+
args: [opts.projectRoot, opts.cwd],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (result.code !== 0 && !result.stdout.trim()) {
|
|
77
|
+
emitJsonError({
|
|
78
|
+
stage: "python",
|
|
79
|
+
reason: `python invocation failed: ${result.stderr.trim() || "no output"}`,
|
|
80
|
+
});
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
process.stdout.write(result.stdout);
|
|
85
|
+
return result.code === 0 ? 0 : result.code;
|
|
86
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { runPythonSnippet, emitJsonError } from "./_python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra task-show — summarize a task's manifest and workflow state
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
okstra task-show <task-key> task-key is project-id:task-group:task-id
|
|
7
|
+
okstra task-show <task-key> --cwd <dir> Resolve PROJECT_ROOT from <dir>
|
|
8
|
+
okstra task-show <task-key> --project <dir> Use <dir> directly as PROJECT_ROOT
|
|
9
|
+
|
|
10
|
+
Output: JSON with taskKey, taskType, taskRoot, brief path, workflow phases,
|
|
11
|
+
artifacts, modelAssignments, latestRunPath. Replaces the
|
|
12
|
+
\`cat task-manifest.json | python3 -c ...\` summarization pattern.
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
function parseArgs(args) {
|
|
16
|
+
const opts = { cwd: process.cwd(), projectRoot: "", taskKey: "" };
|
|
17
|
+
const positional = [];
|
|
18
|
+
for (let i = 0; i < args.length; i++) {
|
|
19
|
+
const a = args[i];
|
|
20
|
+
if (a === "--cwd") {
|
|
21
|
+
opts.cwd = args[++i];
|
|
22
|
+
if (!opts.cwd) throw new Error("--cwd requires a path");
|
|
23
|
+
} else if (a === "--project") {
|
|
24
|
+
opts.projectRoot = args[++i];
|
|
25
|
+
if (!opts.projectRoot) throw new Error("--project requires a path");
|
|
26
|
+
} else if (a.startsWith("--")) {
|
|
27
|
+
throw new Error(`unknown argument '${a}'`);
|
|
28
|
+
} else {
|
|
29
|
+
positional.push(a);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (positional.length !== 1) {
|
|
33
|
+
throw new Error("expected exactly one positional argument: <task-key>");
|
|
34
|
+
}
|
|
35
|
+
opts.taskKey = positional[0];
|
|
36
|
+
return opts;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SCRIPT = `
|
|
40
|
+
import json, sys
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from okstra_project import (
|
|
43
|
+
resolve_project_root, find_task_root, read_task_manifest,
|
|
44
|
+
ResolverError,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
explicit = sys.argv[1]
|
|
48
|
+
cwd = sys.argv[2]
|
|
49
|
+
task_key = sys.argv[3]
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
pr = resolve_project_root(explicit_root=explicit, cwd=cwd)
|
|
53
|
+
except ResolverError as e:
|
|
54
|
+
print(json.dumps({"ok": False, "stage": "resolve", "reason": str(e)}))
|
|
55
|
+
sys.exit(2)
|
|
56
|
+
|
|
57
|
+
pr_path = Path(pr)
|
|
58
|
+
task_root = find_task_root(pr_path, task_key)
|
|
59
|
+
if task_root is None:
|
|
60
|
+
print(json.dumps({"ok": False, "stage": "task_root_missing", "reason": f"no task root for {task_key}"}))
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
63
|
+
manifest = read_task_manifest(task_root)
|
|
64
|
+
if manifest is None:
|
|
65
|
+
print(json.dumps({"ok": False, "stage": "manifest_missing", "reason": f"task-manifest.json missing in {task_root}"}))
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
|
|
68
|
+
wf = manifest.get("workflow", {}) or {}
|
|
69
|
+
out = {
|
|
70
|
+
"ok": True,
|
|
71
|
+
"projectRoot": str(pr_path),
|
|
72
|
+
"taskKey": manifest.get("taskKey"),
|
|
73
|
+
"taskType": manifest.get("taskType"),
|
|
74
|
+
"taskRoot": str(task_root),
|
|
75
|
+
"taskBriefPath": manifest.get("taskBriefPath"),
|
|
76
|
+
"workflow": {
|
|
77
|
+
"currentPhase": wf.get("currentPhase"),
|
|
78
|
+
"currentPhaseState": wf.get("currentPhaseState"),
|
|
79
|
+
"lastCompletedPhase": wf.get("lastCompletedPhase"),
|
|
80
|
+
"nextRecommendedPhase": wf.get("nextRecommendedPhase"),
|
|
81
|
+
"routingStatus": wf.get("routingStatus"),
|
|
82
|
+
"phaseStates": wf.get("phaseStates"),
|
|
83
|
+
},
|
|
84
|
+
"resultContract": manifest.get("resultContract"),
|
|
85
|
+
"artifacts": manifest.get("artifacts"),
|
|
86
|
+
"modelAssignments": manifest.get("modelAssignments"),
|
|
87
|
+
"latestRunPath": manifest.get("latestRunPath"),
|
|
88
|
+
}
|
|
89
|
+
print(json.dumps(out, ensure_ascii=False, default=str, indent=2))
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
export async function run(args) {
|
|
93
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
94
|
+
process.stdout.write(USAGE);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
let opts;
|
|
98
|
+
try {
|
|
99
|
+
opts = parseArgs(args);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
process.stderr.write(`error: ${err.message}\n\n${USAGE}`);
|
|
102
|
+
return 2;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const result = await runPythonSnippet({
|
|
106
|
+
script: SCRIPT,
|
|
107
|
+
args: [opts.projectRoot, opts.cwd, opts.taskKey],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (result.code !== 0 && !result.stdout.trim()) {
|
|
111
|
+
emitJsonError({
|
|
112
|
+
stage: "python",
|
|
113
|
+
reason: `python invocation failed: ${result.stderr.trim() || "no output"}`,
|
|
114
|
+
});
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
process.stdout.write(result.stdout);
|
|
119
|
+
return result.code === 0 ? 0 : result.code;
|
|
120
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { runPythonSnippet, emitJsonError } from "./_python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra worktree-lookup — look up registered worktree for a task-key
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
okstra worktree-lookup <project-id> <task-group> <task-id>
|
|
7
|
+
|
|
8
|
+
The three identifiers are slugified by the runtime before lookup, so
|
|
9
|
+
case/whitespace variants are tolerated.
|
|
10
|
+
|
|
11
|
+
Output: JSON with the registry entry (worktreePath, branch, baseRef,
|
|
12
|
+
status, lastPhase, createdAt). Emits {"ok": true, "entry": null} when
|
|
13
|
+
the task-key has no active worktree.
|
|
14
|
+
|
|
15
|
+
Replaces the okstra_ctl.worktree_registry.lookup() heredoc pattern.
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
function parseArgs(args) {
|
|
19
|
+
const positional = args.filter((a) => !a.startsWith("--"));
|
|
20
|
+
const unknown = args.filter((a) => a.startsWith("--"));
|
|
21
|
+
if (unknown.length) throw new Error(`unknown argument '${unknown[0]}'`);
|
|
22
|
+
if (positional.length !== 3) {
|
|
23
|
+
throw new Error("expected exactly three positional arguments: <project-id> <task-group> <task-id>");
|
|
24
|
+
}
|
|
25
|
+
return { projectId: positional[0], taskGroup: positional[1], taskId: positional[2] };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SCRIPT = `
|
|
29
|
+
import json, sys
|
|
30
|
+
from dataclasses import asdict
|
|
31
|
+
from okstra_ctl import worktree_registry
|
|
32
|
+
from okstra_ctl.ids import _safe_fs_segment
|
|
33
|
+
|
|
34
|
+
pid, tg, tid = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
35
|
+
entry = worktree_registry.lookup(
|
|
36
|
+
_safe_fs_segment(pid),
|
|
37
|
+
_safe_fs_segment(tg),
|
|
38
|
+
_safe_fs_segment(tid),
|
|
39
|
+
)
|
|
40
|
+
if entry is None:
|
|
41
|
+
print(json.dumps({"ok": True, "entry": None}))
|
|
42
|
+
else:
|
|
43
|
+
d = asdict(entry)
|
|
44
|
+
# rename for camelCase API symmetry
|
|
45
|
+
out = {
|
|
46
|
+
"ok": True,
|
|
47
|
+
"entry": {
|
|
48
|
+
"taskKey": d.get("task_key"),
|
|
49
|
+
"projectId": d.get("project_id"),
|
|
50
|
+
"taskGroup": d.get("task_group"),
|
|
51
|
+
"taskId": d.get("task_id"),
|
|
52
|
+
"worktreePath": d.get("worktree_path"),
|
|
53
|
+
"branch": d.get("branch"),
|
|
54
|
+
"baseRef": d.get("base_ref"),
|
|
55
|
+
"createdAt": d.get("created_at"),
|
|
56
|
+
"lastPhase": d.get("last_phase"),
|
|
57
|
+
"status": d.get("status"),
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
print(json.dumps(out, ensure_ascii=False, default=str, indent=2))
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
export async function run(args) {
|
|
64
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
65
|
+
process.stdout.write(USAGE);
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
let opts;
|
|
69
|
+
try {
|
|
70
|
+
opts = parseArgs(args);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
process.stderr.write(`error: ${err.message}\n\n${USAGE}`);
|
|
73
|
+
return 2;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const result = await runPythonSnippet({
|
|
77
|
+
script: SCRIPT,
|
|
78
|
+
args: [opts.projectId, opts.taskGroup, opts.taskId],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (result.code !== 0 && !result.stdout.trim()) {
|
|
82
|
+
emitJsonError({
|
|
83
|
+
stage: "python",
|
|
84
|
+
reason: `python invocation failed: ${result.stderr.trim() || "no output"}`,
|
|
85
|
+
});
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
process.stdout.write(result.stdout);
|
|
90
|
+
return result.code === 0 ? 0 : result.code;
|
|
91
|
+
}
|