okstra 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.kr.md +3 -0
  2. package/README.md +3 -0
  3. package/bin/okstra +5 -0
  4. package/docs/kr/architecture.md +2 -2
  5. package/docs/kr/cli.md +1 -0
  6. package/docs/project-structure-overview.md +4 -1
  7. package/package.json +1 -1
  8. package/runtime/BUILD.json +2 -2
  9. package/runtime/agents/workers/claude-worker.md +3 -1
  10. package/runtime/agents/workers/codex-worker.md +3 -1
  11. package/runtime/agents/workers/gemini-worker.md +3 -1
  12. package/runtime/agents/workers/report-writer-worker.md +17 -2
  13. package/runtime/prompts/profiles/release-handoff.md +16 -0
  14. package/runtime/python/okstra_ctl/render.py +25 -2
  15. package/runtime/python/okstra_ctl/wizard.py +1249 -0
  16. package/runtime/python/okstra_token_usage/collect.py +12 -1
  17. package/runtime/skills/okstra-report-writer/SKILL.md +1 -0
  18. package/runtime/skills/okstra-run/SKILL.md +115 -234
  19. package/runtime/skills/okstra-setup/SKILL.md +37 -0
  20. package/runtime/skills/okstra-team-contract/SKILL.md +47 -1
  21. package/runtime/templates/prd/brief.template.md +1 -0
  22. package/runtime/templates/project-docs/task-index.template.md +1 -0
  23. package/runtime/templates/reports/error-analysis-input.template.md +1 -0
  24. package/runtime/templates/reports/final-report.template.md +1 -0
  25. package/runtime/templates/reports/final-verification-input.template.md +1 -0
  26. package/runtime/templates/reports/implementation-input.template.md +1 -0
  27. package/runtime/templates/reports/implementation-planning-input.template.md +1 -0
  28. package/runtime/templates/reports/quick-input.template.md +1 -0
  29. package/runtime/templates/reports/release-handoff-input.template.md +1 -0
  30. package/runtime/templates/reports/schedule.template.md +1 -0
  31. package/runtime/templates/reports/task-brief.template.md +1 -0
  32. package/src/config.mjs +392 -0
  33. package/src/wizard.mjs +105 -0
@@ -8,6 +8,7 @@ date: {{TASK_DATE}}
8
8
  task-id: "{{TASK_ID}}"
9
9
  task-group: "{{TASK_GROUP}}"
10
10
  project-id: "{{PROJECT_ID}}"
11
+ taskType: "{{FM_TASK_TYPE}}"
11
12
  ---
12
13
 
13
14
  # OKSTRA Final Verification Input
@@ -8,6 +8,7 @@ date: {{TASK_DATE}}
8
8
  task-id: "{{TASK_ID}}"
9
9
  task-group: "{{TASK_GROUP}}"
10
10
  project-id: "{{PROJECT_ID}}"
11
+ taskType: "{{FM_TASK_TYPE}}"
11
12
  ---
12
13
 
13
14
  # OKSTRA Implementation Input
@@ -8,6 +8,7 @@ date: {{TASK_DATE}}
8
8
  task-id: "{{TASK_ID}}"
9
9
  task-group: "{{TASK_GROUP}}"
10
10
  project-id: "{{PROJECT_ID}}"
11
+ taskType: "{{FM_TASK_TYPE}}"
11
12
  ---
12
13
 
13
14
  # OKSTRA Implementation Planning Input
@@ -8,6 +8,7 @@ date: {{TASK_DATE}}
8
8
  task-id: "{{TASK_ID}}"
9
9
  task-group: "{{TASK_GROUP}}"
10
10
  project-id: "{{PROJECT_ID}}"
11
+ taskType: "{{FM_TASK_TYPE}}"
11
12
  ---
12
13
 
13
14
  # OKSTRA Quick Input
@@ -8,6 +8,7 @@ date: {{TASK_DATE}}
8
8
  task-id: "{{TASK_ID}}"
9
9
  task-group: "{{TASK_GROUP}}"
10
10
  project-id: "{{PROJECT_ID}}"
11
+ taskType: "{{FM_TASK_TYPE}}"
11
12
  ---
12
13
 
13
14
  # OKSTRA Release Handoff Input
@@ -8,6 +8,7 @@ date: {{TASK_DATE}}
8
8
  task-id: "{{TASK_ID}}"
9
9
  task-group: "{{TASK_GROUP}}"
10
10
  project-id: "{{PROJECT_ID}}"
11
+ taskType: "{{FM_TASK_TYPE}}"
11
12
  ---
12
13
 
13
14
  # <Title> — Work Schedule
@@ -8,6 +8,7 @@ date: {{TASK_DATE}}
8
8
  task-id: "{{TASK_ID}}"
9
9
  task-group: "{{TASK_GROUP}}"
10
10
  project-id: "{{PROJECT_ID}}"
11
+ taskType: "{{FM_TASK_TYPE}}"
11
12
  ---
12
13
 
13
14
  # OKSTRA Task Brief
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/wizard.mjs ADDED
@@ -0,0 +1,105 @@
1
+ import { runPythonModule } from "./_python-helper.mjs";
2
+ import { resolvePaths } from "./paths.mjs";
3
+
4
+ const USAGE = `okstra wizard — interactive okstra-run input collector
5
+
6
+ Used by the okstra-run skill to drive the per-step prompt loop. Each
7
+ subcommand round-trips a JSON state file held by the skill.
8
+
9
+ Subcommands:
10
+ init seed a fresh wizard state and emit the first prompt
11
+ step submit an answer (or fetch the current prompt) and emit next
12
+ render-args emit the final --flag/value map for 'okstra render-bundle'
13
+ confirmation emit the multi-line confirmation echo block
14
+
15
+ Usage:
16
+ okstra wizard init --state-file <path> --project-root <p> --project-id <id>
17
+ okstra wizard step --state-file <path> [--answer <value>]
18
+ okstra wizard render-args --state-file <path>
19
+ okstra wizard confirmation --state-file <path>
20
+
21
+ 'init' auto-fills --workspace-root from 'okstra paths --field workspace',
22
+ so callers do not pass it.
23
+
24
+ All subcommands emit a single JSON object on stdout. On validation failure
25
+ 'step' returns {ok:false, error, current} so the skill can re-prompt.
26
+ `;
27
+
28
+ function parseFlags(args) {
29
+ const out = {};
30
+ for (let i = 0; i < args.length; i += 1) {
31
+ const a = args[i];
32
+ if (!a.startsWith("--")) {
33
+ throw new Error(`unexpected positional argument: ${a}`);
34
+ }
35
+ const key = a.slice(2);
36
+ const next = args[i + 1];
37
+ if (next === undefined || next.startsWith("--")) {
38
+ throw new Error(`flag --${key} requires a value`);
39
+ }
40
+ out[key] = next;
41
+ i += 1;
42
+ }
43
+ return out;
44
+ }
45
+
46
+ export async function run(args) {
47
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
48
+ process.stdout.write(USAGE);
49
+ return args.length === 0 ? 2 : 0;
50
+ }
51
+
52
+ const [sub, ...rest] = args;
53
+ if (!["init", "step", "render-args", "confirmation"].includes(sub)) {
54
+ process.stderr.write(`error: unknown wizard subcommand '${sub}'\n\n${USAGE}`);
55
+ return 2;
56
+ }
57
+
58
+ let flags;
59
+ try {
60
+ flags = parseFlags(rest);
61
+ } catch (err) {
62
+ process.stderr.write(`error: ${err.message}\n\n${USAGE}`);
63
+ return 2;
64
+ }
65
+
66
+ if (!flags["state-file"]) {
67
+ process.stderr.write("error: --state-file is required\n");
68
+ return 2;
69
+ }
70
+
71
+ // build python argv
72
+ const pyArgs = [sub, "--state-file", flags["state-file"]];
73
+ if (sub === "init") {
74
+ if (!flags["project-root"] || !flags["project-id"]) {
75
+ process.stderr.write("error: init requires --project-root and --project-id\n");
76
+ return 2;
77
+ }
78
+ const paths = await resolvePaths();
79
+ pyArgs.push("--workspace-root", paths.workspace);
80
+ pyArgs.push("--project-root", flags["project-root"]);
81
+ pyArgs.push("--project-id", flags["project-id"]);
82
+ } else if (sub === "step" && flags.answer !== undefined) {
83
+ pyArgs.push("--answer", flags.answer);
84
+ }
85
+
86
+ const result = await runPythonModule({
87
+ module: "okstra_ctl.wizard",
88
+ args: pyArgs,
89
+ stdio: "capture",
90
+ });
91
+
92
+ if (result.code !== 0 && !result.stdout.trim()) {
93
+ process.stdout.write(
94
+ JSON.stringify(
95
+ { ok: false, stage: "python", reason: result.stderr.trim() || "no output" },
96
+ null,
97
+ 2,
98
+ ) + "\n",
99
+ );
100
+ return 1;
101
+ }
102
+
103
+ process.stdout.write(result.stdout);
104
+ return result.code === 0 ? 0 : result.code;
105
+ }