okstra 0.21.1 → 0.23.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 +3 -0
- package/README.md +3 -0
- package/bin/okstra +2 -0
- package/docs/kr/architecture.md +1 -0
- package/docs/project-structure-overview.md +53 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +3 -1
- package/runtime/agents/workers/codex-worker.md +3 -1
- package/runtime/agents/workers/gemini-worker.md +3 -1
- package/runtime/agents/workers/report-writer-worker.md +16 -1
- package/runtime/bin/okstra-trace-cleanup.sh +44 -11
- package/runtime/prompts/profiles/_common-contract.md +12 -0
- package/runtime/prompts/profiles/release-handoff.md +18 -1
- package/runtime/python/okstra_ctl/index.py +2 -1
- package/runtime/python/okstra_ctl/pr_template.py +126 -0
- package/runtime/python/okstra_ctl/render.py +28 -2
- package/runtime/python/okstra_ctl/run.py +41 -2
- package/runtime/python/okstra_ctl/seeding.py +19 -0
- package/runtime/python/okstra_ctl/workers.py +20 -0
- package/runtime/skills/okstra-run/SKILL.md +68 -32
- package/runtime/skills/okstra-run/templates/pr-body.template.md +41 -0
- package/runtime/skills/okstra-setup/SKILL.md +37 -0
- package/runtime/skills/okstra-team-contract/SKILL.md +46 -1
- package/runtime/templates/prd/brief.template.md +1 -0
- package/runtime/templates/project-docs/task-index.template.md +1 -0
- package/runtime/templates/reports/error-analysis-input.template.md +1 -0
- package/runtime/templates/reports/final-report.template.md +2 -0
- package/runtime/templates/reports/final-verification-input.template.md +1 -0
- package/runtime/templates/reports/implementation-input.template.md +1 -0
- package/runtime/templates/reports/implementation-planning-input.template.md +1 -0
- package/runtime/templates/reports/quick-input.template.md +1 -0
- package/runtime/templates/reports/release-handoff-input.template.md +1 -0
- package/runtime/templates/reports/schedule.template.md +1 -0
- package/runtime/templates/reports/settings.template.json +4 -2
- package/runtime/templates/reports/task-brief.template.md +1 -0
- package/src/config.mjs +392 -0
- package/src/render-bundle.mjs +2 -1
package/src/config.mjs
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve, isAbsolute } from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { resolvePaths } from "./paths.mjs";
|
|
6
|
+
|
|
7
|
+
const USAGE = `okstra config — read / write okstra settings (project + global scopes)
|
|
8
|
+
|
|
9
|
+
Settings are persisted as JSON keys in one of two files:
|
|
10
|
+
project: <project-root>/.project-docs/okstra/project.json
|
|
11
|
+
global: ~/.okstra/config.json
|
|
12
|
+
|
|
13
|
+
Supported keys (CLI alias -> JSON field):
|
|
14
|
+
pr-template-path -> prTemplatePath
|
|
15
|
+
PR body template used by the release-handoff phase.
|
|
16
|
+
Path may be absolute, ~/-prefixed, or (project scope
|
|
17
|
+
only) relative to <project-root>.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
okstra config get <key> [--scope project|global|all]
|
|
21
|
+
Print the value at the requested scope.
|
|
22
|
+
'all' prints every scope, plus the effective resolved value where
|
|
23
|
+
available (for pr-template-path the resolver may fall back to the
|
|
24
|
+
bundled skill default).
|
|
25
|
+
|
|
26
|
+
okstra config set <key> <value> [--scope project|global]
|
|
27
|
+
Write the value. Scope is required unless cwd is inside an okstra
|
|
28
|
+
project (then defaults to 'project'). Global scope only accepts
|
|
29
|
+
absolute or ~/-prefixed paths.
|
|
30
|
+
|
|
31
|
+
okstra config unset <key> [--scope project|global]
|
|
32
|
+
Remove the key from the chosen scope.
|
|
33
|
+
|
|
34
|
+
okstra config show [--scope project|global|all]
|
|
35
|
+
Dump every okstra-managed key from the chosen scope as JSON.
|
|
36
|
+
|
|
37
|
+
Common options:
|
|
38
|
+
--cwd <dir> Search starting point for project root (default: pwd)
|
|
39
|
+
--json Force JSON output for get/show (default for show)
|
|
40
|
+
--quiet Suppress informational stdout on set/unset; exit
|
|
41
|
+
code alone reports success
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
// CLI-key -> { jsonField, scopes, validate }
|
|
45
|
+
// validate(value, ctx) returns null on success or an error string.
|
|
46
|
+
const KEYS = {
|
|
47
|
+
"pr-template-path": {
|
|
48
|
+
jsonField: "prTemplatePath",
|
|
49
|
+
scopes: ["project", "global"],
|
|
50
|
+
validate(value, ctx) {
|
|
51
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
52
|
+
return "value must be a non-empty path";
|
|
53
|
+
}
|
|
54
|
+
const v = value.trim();
|
|
55
|
+
if (ctx.scope === "global") {
|
|
56
|
+
if (!v.startsWith("~/") && !v.startsWith("~\\") && !isAbsolute(v)) {
|
|
57
|
+
return (
|
|
58
|
+
"global scope requires an absolute path or '~/'-prefixed path " +
|
|
59
|
+
`(got ${JSON.stringify(v)}). Use --scope project for project-root-relative paths.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function parseArgs(args) {
|
|
69
|
+
const opts = {
|
|
70
|
+
op: null,
|
|
71
|
+
key: null,
|
|
72
|
+
value: null,
|
|
73
|
+
scope: null,
|
|
74
|
+
cwd: process.cwd(),
|
|
75
|
+
json: false,
|
|
76
|
+
quiet: false,
|
|
77
|
+
};
|
|
78
|
+
const positional = [];
|
|
79
|
+
for (let i = 0; i < args.length; i++) {
|
|
80
|
+
const a = args[i];
|
|
81
|
+
if (a === "--scope") {
|
|
82
|
+
const next = args[i + 1];
|
|
83
|
+
if (!next) throw new Error("--scope requires a value (project|global|all)");
|
|
84
|
+
opts.scope = next;
|
|
85
|
+
i++;
|
|
86
|
+
} else if (a === "--cwd") {
|
|
87
|
+
const next = args[i + 1];
|
|
88
|
+
if (!next) throw new Error("--cwd requires a path");
|
|
89
|
+
opts.cwd = next;
|
|
90
|
+
i++;
|
|
91
|
+
} else if (a === "--json") {
|
|
92
|
+
opts.json = true;
|
|
93
|
+
} else if (a === "--quiet" || a === "-q") {
|
|
94
|
+
opts.quiet = true;
|
|
95
|
+
} else if (a.startsWith("--")) {
|
|
96
|
+
throw new Error(`unknown flag ${a}`);
|
|
97
|
+
} else {
|
|
98
|
+
positional.push(a);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
opts.op = positional[0] ?? null;
|
|
102
|
+
opts.key = positional[1] ?? null;
|
|
103
|
+
opts.value = positional[2] ?? null;
|
|
104
|
+
return opts;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function okstraHome() {
|
|
108
|
+
const override = (process.env.OKSTRA_HOME || "").trim();
|
|
109
|
+
return override !== "" ? override : join(homedir(), ".okstra");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function globalConfigPath() {
|
|
113
|
+
return join(okstraHome(), "config.json");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function projectConfigPath(projectRoot) {
|
|
117
|
+
return join(projectRoot, ".project-docs", "okstra", "project.json");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function fileExists(p) {
|
|
121
|
+
try {
|
|
122
|
+
await fs.access(p);
|
|
123
|
+
return true;
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function readJson(p) {
|
|
130
|
+
if (!(await fileExists(p))) return null;
|
|
131
|
+
const text = await fs.readFile(p, "utf8");
|
|
132
|
+
try {
|
|
133
|
+
const data = JSON.parse(text);
|
|
134
|
+
return typeof data === "object" && data !== null ? data : {};
|
|
135
|
+
} catch (err) {
|
|
136
|
+
throw new Error(`failed to parse ${p}: ${err.message}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function writeJsonAtomic(p, data) {
|
|
141
|
+
await fs.mkdir(dirname(p), { recursive: true });
|
|
142
|
+
const tmp = `${p}.tmp.${process.pid}`;
|
|
143
|
+
await fs.writeFile(tmp, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
144
|
+
await fs.rename(tmp, p);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function spawnCapture(cmd, args, env) {
|
|
148
|
+
return new Promise((resolveProc) => {
|
|
149
|
+
const child = spawn(cmd, args, {
|
|
150
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
151
|
+
env: { ...process.env, ...env },
|
|
152
|
+
});
|
|
153
|
+
let stdout = "";
|
|
154
|
+
let stderr = "";
|
|
155
|
+
child.stdout.on("data", (b) => (stdout += b.toString()));
|
|
156
|
+
child.stderr.on("data", (b) => (stderr += b.toString()));
|
|
157
|
+
child.on("error", (err) => resolveProc({ code: -1, stdout, stderr: err.message }));
|
|
158
|
+
child.on("close", (code) => resolveProc({ code, stdout, stderr }));
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function resolveProjectRoot(cwd) {
|
|
163
|
+
const paths = await resolvePaths();
|
|
164
|
+
const result = await spawnCapture(
|
|
165
|
+
"python3",
|
|
166
|
+
[
|
|
167
|
+
"-c",
|
|
168
|
+
[
|
|
169
|
+
"import sys",
|
|
170
|
+
"from okstra_project import resolve_project_root, ResolverError",
|
|
171
|
+
"try:",
|
|
172
|
+
" print(resolve_project_root(explicit_root='', cwd=sys.argv[1]))",
|
|
173
|
+
"except ResolverError as e:",
|
|
174
|
+
" sys.stderr.write(str(e)+'\\n'); sys.exit(2)",
|
|
175
|
+
].join("\n"),
|
|
176
|
+
cwd,
|
|
177
|
+
],
|
|
178
|
+
{ PYTHONPATH: paths.pythonpath },
|
|
179
|
+
);
|
|
180
|
+
if (result.code === 0) return result.stdout.trim();
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function emit(opts, payload) {
|
|
185
|
+
if (opts.quiet) return;
|
|
186
|
+
if (opts.json || typeof payload !== "string") {
|
|
187
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
188
|
+
} else {
|
|
189
|
+
process.stdout.write(`${payload}\n`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function effectivePrTemplatePath(projectRoot) {
|
|
194
|
+
// Mirror the python resolver order without depending on it for the simple
|
|
195
|
+
// case where we just want to surface the chain in 'config get --scope all'.
|
|
196
|
+
// Python remains the source of truth at run time; this is informational.
|
|
197
|
+
const paths = await resolvePaths();
|
|
198
|
+
const result = await spawnCapture(
|
|
199
|
+
"python3",
|
|
200
|
+
[
|
|
201
|
+
"-c",
|
|
202
|
+
[
|
|
203
|
+
"import json, sys",
|
|
204
|
+
"from pathlib import Path",
|
|
205
|
+
"from okstra_ctl.pr_template import resolve_pr_template_path, PrTemplateError",
|
|
206
|
+
"try:",
|
|
207
|
+
" r = resolve_pr_template_path(Path(sys.argv[1]))",
|
|
208
|
+
" print(json.dumps({'ok': True, 'path': str(r.path), 'source': r.source}))",
|
|
209
|
+
"except PrTemplateError as e:",
|
|
210
|
+
" print(json.dumps({'ok': False, 'reason': str(e)}))",
|
|
211
|
+
].join("\n"),
|
|
212
|
+
projectRoot ?? process.cwd(),
|
|
213
|
+
],
|
|
214
|
+
{ PYTHONPATH: paths.pythonpath },
|
|
215
|
+
);
|
|
216
|
+
if (result.code !== 0) return null;
|
|
217
|
+
try {
|
|
218
|
+
return JSON.parse(result.stdout.trim());
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function opGet(opts) {
|
|
225
|
+
const spec = KEYS[opts.key];
|
|
226
|
+
const scope = opts.scope ?? "all";
|
|
227
|
+
const projectRoot = await resolveProjectRoot(opts.cwd);
|
|
228
|
+
|
|
229
|
+
const out = {};
|
|
230
|
+
if (scope === "project" || scope === "all") {
|
|
231
|
+
if (!projectRoot) {
|
|
232
|
+
out.project = { available: false, reason: "cwd not inside an okstra project" };
|
|
233
|
+
} else {
|
|
234
|
+
const cfg = (await readJson(projectConfigPath(projectRoot))) ?? {};
|
|
235
|
+
out.project = {
|
|
236
|
+
available: true,
|
|
237
|
+
path: projectConfigPath(projectRoot),
|
|
238
|
+
value: cfg[spec.jsonField] ?? null,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (scope === "global" || scope === "all") {
|
|
243
|
+
const cfg = (await readJson(globalConfigPath())) ?? {};
|
|
244
|
+
out.global = {
|
|
245
|
+
available: true,
|
|
246
|
+
path: globalConfigPath(),
|
|
247
|
+
value: cfg[spec.jsonField] ?? null,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (scope === "all" && opts.key === "pr-template-path") {
|
|
252
|
+
const effective = await effectivePrTemplatePath(projectRoot);
|
|
253
|
+
if (effective) out.effective = effective;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (scope !== "all" && !opts.json) {
|
|
257
|
+
const entry = out[scope];
|
|
258
|
+
if (entry?.available && entry.value !== null) {
|
|
259
|
+
emit(opts, entry.value);
|
|
260
|
+
} else {
|
|
261
|
+
emit(opts, "");
|
|
262
|
+
}
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
emit(opts, out);
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function opSet(opts) {
|
|
270
|
+
const spec = KEYS[opts.key];
|
|
271
|
+
if (opts.value === null) throw new Error(`'config set ${opts.key}' requires a value argument`);
|
|
272
|
+
let scope = opts.scope;
|
|
273
|
+
const projectRoot = await resolveProjectRoot(opts.cwd);
|
|
274
|
+
if (!scope) {
|
|
275
|
+
scope = projectRoot ? "project" : null;
|
|
276
|
+
if (!scope) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
"--scope is required when cwd is not inside an okstra project. " +
|
|
279
|
+
"Pass --scope project or --scope global.",
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (!spec.scopes.includes(scope)) {
|
|
284
|
+
throw new Error(`key '${opts.key}' does not support scope '${scope}'`);
|
|
285
|
+
}
|
|
286
|
+
const ctx = { scope, projectRoot };
|
|
287
|
+
const err = spec.validate(opts.value, ctx);
|
|
288
|
+
if (err) throw new Error(err);
|
|
289
|
+
|
|
290
|
+
const targetPath =
|
|
291
|
+
scope === "project" ? projectConfigPath(projectRoot) : globalConfigPath();
|
|
292
|
+
if (scope === "project" && !projectRoot) {
|
|
293
|
+
throw new Error("cwd not inside an okstra project — cannot write project scope");
|
|
294
|
+
}
|
|
295
|
+
const existing = (await readJson(targetPath)) ?? {};
|
|
296
|
+
existing[spec.jsonField] = opts.value;
|
|
297
|
+
await writeJsonAtomic(targetPath, existing);
|
|
298
|
+
emit(opts, { ok: true, scope, path: targetPath, key: opts.key, value: opts.value });
|
|
299
|
+
return 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function opUnset(opts) {
|
|
303
|
+
const spec = KEYS[opts.key];
|
|
304
|
+
let scope = opts.scope;
|
|
305
|
+
const projectRoot = await resolveProjectRoot(opts.cwd);
|
|
306
|
+
if (!scope) {
|
|
307
|
+
scope = projectRoot ? "project" : null;
|
|
308
|
+
if (!scope) throw new Error("--scope is required (project|global)");
|
|
309
|
+
}
|
|
310
|
+
const targetPath =
|
|
311
|
+
scope === "project" ? projectConfigPath(projectRoot) : globalConfigPath();
|
|
312
|
+
if (scope === "project" && !projectRoot) {
|
|
313
|
+
throw new Error("cwd not inside an okstra project — cannot unset project scope");
|
|
314
|
+
}
|
|
315
|
+
const existing = (await readJson(targetPath)) ?? {};
|
|
316
|
+
if (!(spec.jsonField in existing)) {
|
|
317
|
+
emit(opts, { ok: true, scope, path: targetPath, key: opts.key, removed: false });
|
|
318
|
+
return 0;
|
|
319
|
+
}
|
|
320
|
+
delete existing[spec.jsonField];
|
|
321
|
+
await writeJsonAtomic(targetPath, existing);
|
|
322
|
+
emit(opts, { ok: true, scope, path: targetPath, key: opts.key, removed: true });
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function opShow(opts) {
|
|
327
|
+
const scope = opts.scope ?? "all";
|
|
328
|
+
const projectRoot = await resolveProjectRoot(opts.cwd);
|
|
329
|
+
const out = {};
|
|
330
|
+
if (scope === "project" || scope === "all") {
|
|
331
|
+
out.project = projectRoot
|
|
332
|
+
? {
|
|
333
|
+
path: projectConfigPath(projectRoot),
|
|
334
|
+
data: (await readJson(projectConfigPath(projectRoot))) ?? {},
|
|
335
|
+
}
|
|
336
|
+
: { available: false, reason: "cwd not inside an okstra project" };
|
|
337
|
+
}
|
|
338
|
+
if (scope === "global" || scope === "all") {
|
|
339
|
+
out.global = {
|
|
340
|
+
path: globalConfigPath(),
|
|
341
|
+
data: (await readJson(globalConfigPath())) ?? {},
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
emit({ ...opts, json: true }, out);
|
|
345
|
+
return 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export async function run(args) {
|
|
349
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
350
|
+
process.stdout.write(USAGE);
|
|
351
|
+
return args.length === 0 ? 2 : 0;
|
|
352
|
+
}
|
|
353
|
+
const opts = parseArgs(args);
|
|
354
|
+
if (!opts.op) {
|
|
355
|
+
process.stderr.write("missing subcommand (get|set|unset|show)\n");
|
|
356
|
+
return 2;
|
|
357
|
+
}
|
|
358
|
+
if (opts.scope !== null && !["project", "global", "all"].includes(opts.scope)) {
|
|
359
|
+
process.stderr.write(`invalid --scope: ${opts.scope}\n`);
|
|
360
|
+
return 2;
|
|
361
|
+
}
|
|
362
|
+
if (["get", "set", "unset"].includes(opts.op)) {
|
|
363
|
+
if (!opts.key) {
|
|
364
|
+
process.stderr.write(`'config ${opts.op}' requires a key argument\n`);
|
|
365
|
+
return 2;
|
|
366
|
+
}
|
|
367
|
+
if (!KEYS[opts.key]) {
|
|
368
|
+
process.stderr.write(
|
|
369
|
+
`unknown key '${opts.key}'. Supported: ${Object.keys(KEYS).join(", ")}\n`,
|
|
370
|
+
);
|
|
371
|
+
return 2;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
switch (opts.op) {
|
|
376
|
+
case "get":
|
|
377
|
+
return await opGet(opts);
|
|
378
|
+
case "set":
|
|
379
|
+
return await opSet(opts);
|
|
380
|
+
case "unset":
|
|
381
|
+
return await opUnset(opts);
|
|
382
|
+
case "show":
|
|
383
|
+
return await opShow(opts);
|
|
384
|
+
default:
|
|
385
|
+
process.stderr.write(`unknown subcommand '${opts.op}'\n`);
|
|
386
|
+
return 2;
|
|
387
|
+
}
|
|
388
|
+
} catch (err) {
|
|
389
|
+
process.stderr.write(`error: ${err.message}\n`);
|
|
390
|
+
return 1;
|
|
391
|
+
}
|
|
392
|
+
}
|
package/src/render-bundle.mjs
CHANGED
|
@@ -17,7 +17,8 @@ Usage:
|
|
|
17
17
|
[--lead-model <m>] [--claude-model <m>] [--codex-model <m>] \\
|
|
18
18
|
[--gemini-model <m>] [--report-writer-model <m>] \\
|
|
19
19
|
[--related-tasks <list>] [--base-ref <ref>] \\
|
|
20
|
-
[--clarification-response <path>] [--work-category <cat>]
|
|
20
|
+
[--clarification-response <path>] [--work-category <cat>] \\
|
|
21
|
+
[--pr-template-path <path>] # release-handoff only
|
|
21
22
|
|
|
22
23
|
All flags pass through unchanged to \`python3 -m okstra_ctl.run\`. The
|
|
23
24
|
shim auto-supplies \`--workspace-root\` (from \`okstra paths --field workspace\`)
|