okstra 0.36.2 → 0.38.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 +6 -6
- package/README.md +6 -6
- package/bin/okstra +4 -2
- package/docs/kr/architecture.md +31 -31
- package/docs/kr/cli.md +10 -9
- package/docs/pr-template-usage.md +2 -2
- package/docs/project-structure-overview.md +4 -4
- package/docs/superpowers/plans/2026-05-25-okstra-project-root-rename.md +159 -0
- package/docs/superpowers/plans/2026-05-26-wizard-3-option-picker.md +860 -0
- package/docs/task-process/common-flow.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +16 -14
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +7 -7
- package/runtime/prompts/profiles/_implementation-executor.md +2 -2
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +5 -5
- package/runtime/prompts/profiles/release-handoff.md +1 -1
- package/runtime/prompts/profiles/requirements-discovery.md +3 -3
- package/runtime/prompts/wizard/prompts.ko.json +80 -6
- package/runtime/python/lib/okstra/globals.sh +2 -2
- package/runtime/python/lib/okstra/interactive.sh +2 -2
- package/runtime/python/lib/okstra/project-resolver.sh +1 -1
- package/runtime/python/lib/okstra/usage.sh +9 -9
- package/runtime/python/lib/okstra-ctl/cmd-rerun.sh +1 -1
- package/runtime/python/okstra_ctl/backfill.py +5 -3
- package/runtime/python/okstra_ctl/migrate.py +408 -0
- package/runtime/python/okstra_ctl/paths.py +12 -3
- package/runtime/python/okstra_ctl/pr_template.py +4 -2
- package/runtime/python/okstra_ctl/render.py +8 -6
- package/runtime/python/okstra_ctl/run.py +7 -7
- package/runtime/python/okstra_ctl/seeding.py +12 -6
- package/runtime/python/okstra_ctl/sequence.py +3 -1
- package/runtime/python/okstra_ctl/wizard.py +412 -77
- package/runtime/python/okstra_ctl/worktree.py +8 -6
- package/runtime/python/okstra_project/__init__.py +35 -5
- package/runtime/python/okstra_project/dirs.py +67 -0
- package/runtime/python/okstra_project/resolver.py +8 -8
- package/runtime/python/okstra_project/state.py +11 -9
- package/runtime/python/okstra_token_usage/collect.py +3 -1
- package/runtime/skills/okstra-brief/SKILL.md +30 -30
- package/runtime/skills/okstra-context-loader/SKILL.md +7 -7
- package/runtime/skills/okstra-inspect/SKILL.md +25 -25
- package/runtime/skills/okstra-run/templates/pr-body.template.md +1 -1
- package/runtime/skills/okstra-schedule/SKILL.md +7 -7
- package/runtime/skills/okstra-setup/SKILL.md +8 -8
- package/runtime/skills/okstra-team-contract/SKILL.md +4 -4
- package/runtime/templates/okstra.CLAUDE.md +4 -4
- package/runtime/templates/reports/brief.template.md +5 -5
- package/runtime/templates/reports/task-brief.template.md +1 -1
- package/runtime/validators/lib/fixtures.sh +2 -2
- package/runtime/validators/lib/paths.sh +9 -3
- package/runtime/validators/validate-brief.py +2 -2
- package/runtime/validators/validate-brief.sh +1 -1
- package/runtime/validators/validate-run.py +3 -1
- package/runtime/validators/validate-workflow.sh +2 -2
- package/src/check-project.mjs +3 -3
- package/src/config.mjs +6 -5
- package/src/install.mjs +5 -5
- package/src/migrate.mjs +163 -0
- package/src/okstra-dirs.mjs +37 -0
- package/src/paths.mjs +17 -0
- package/src/setup.mjs +8 -4
- package/src/uninstall.mjs +3 -3
package/src/migrate.mjs
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { buildPythonpath, resolvePaths } from "./paths.mjs";
|
|
3
|
+
import { LEGACY_OKSTRA_DIR, OKSTRA_DIR } from "./okstra-dirs.mjs";
|
|
4
|
+
|
|
5
|
+
const USAGE = `okstra migrate — move project artifacts from ${LEGACY_OKSTRA_DIR}/ to ${OKSTRA_DIR}/
|
|
6
|
+
|
|
7
|
+
Run this once per project that was set up before v0.37. The migrator moves
|
|
8
|
+
the okstra-managed directory tree, rewrites the import line in your
|
|
9
|
+
project's CLAUDE.md, updates .gitignore, and refreshes okstra-home
|
|
10
|
+
registries (~/.okstra/{recent,active}.jsonl, ~/.okstra/worktrees/registry.json)
|
|
11
|
+
so the existing run history keeps resolving to the new path.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
okstra migrate Dry-run by default. Prints the plan as JSON.
|
|
15
|
+
okstra migrate --apply Execute the move and ancillary updates.
|
|
16
|
+
okstra migrate --cwd <dir> Search starting point (default: cwd).
|
|
17
|
+
okstra migrate --quiet Suppress stdout on success; exit code reports.
|
|
18
|
+
okstra migrate --json Force JSON output (default for both modes).
|
|
19
|
+
|
|
20
|
+
Exit codes:
|
|
21
|
+
0 plan prepared (dry-run) or migration applied successfully
|
|
22
|
+
1 pre-condition rejected (e.g. ${OKSTRA_DIR}/ already exists, or no
|
|
23
|
+
${LEGACY_OKSTRA_DIR}/ to migrate); reason printed on stderr
|
|
24
|
+
2 internal error (python invocation failed, etc.)
|
|
25
|
+
|
|
26
|
+
The command is idempotent in spirit: re-running after a successful apply
|
|
27
|
+
exits 1 with "nothing to migrate". Safe to wire into CI as a one-shot
|
|
28
|
+
post-upgrade step.
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
function runProcess(cmd, args, env) {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const child = spawn(cmd, args, {
|
|
34
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
35
|
+
env: { ...process.env, ...env },
|
|
36
|
+
});
|
|
37
|
+
let stdout = "";
|
|
38
|
+
let stderr = "";
|
|
39
|
+
child.stdout.on("data", (b) => (stdout += b.toString()));
|
|
40
|
+
child.stderr.on("data", (b) => (stderr += b.toString()));
|
|
41
|
+
child.on("error", (err) => resolve({ code: -1, stdout, stderr: err.message }));
|
|
42
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseArgs(args) {
|
|
47
|
+
const opts = { cwd: process.cwd(), apply: false, quiet: false };
|
|
48
|
+
for (let i = 0; i < args.length; i++) {
|
|
49
|
+
const a = args[i];
|
|
50
|
+
if (a === "--apply") opts.apply = true;
|
|
51
|
+
else if (a === "--quiet" || a === "-q") opts.quiet = true;
|
|
52
|
+
else if (a === "--json") opts.json = true;
|
|
53
|
+
else if (a === "--cwd") {
|
|
54
|
+
const next = args[i + 1];
|
|
55
|
+
if (!next || next.startsWith("--")) throw new Error("--cwd requires a path");
|
|
56
|
+
opts.cwd = next;
|
|
57
|
+
i++;
|
|
58
|
+
} else {
|
|
59
|
+
throw new Error(`unknown argument '${a}'`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return opts;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function emit(opts, payload) {
|
|
66
|
+
if (opts.quiet) return;
|
|
67
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function run(args) {
|
|
71
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
72
|
+
process.stdout.write(USAGE);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let opts;
|
|
77
|
+
try {
|
|
78
|
+
opts = parseArgs(args);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
process.stderr.write(`error: ${err.message}\n\n${USAGE}`);
|
|
81
|
+
return 2;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const paths = await resolvePaths();
|
|
85
|
+
|
|
86
|
+
// We resolve project root via the python resolver — the same one used by
|
|
87
|
+
// every other okstra command — to keep "what project this is" consistent
|
|
88
|
+
// across CLI surfaces. The resolver knows three lookup strategies; the
|
|
89
|
+
// migrator only needs whichever ancestor it returns.
|
|
90
|
+
const py = await runProcess(
|
|
91
|
+
"python3",
|
|
92
|
+
[
|
|
93
|
+
"-c",
|
|
94
|
+
[
|
|
95
|
+
"import json, sys",
|
|
96
|
+
"from pathlib import Path",
|
|
97
|
+
"from okstra_project.resolver import resolve_project_root, ResolverError",
|
|
98
|
+
"from okstra_ctl.migrate import (",
|
|
99
|
+
" prepare_migration_plan, apply_migration_plan, MigrationRefused,",
|
|
100
|
+
")",
|
|
101
|
+
"cwd, apply = sys.argv[1], sys.argv[2] == 'apply'",
|
|
102
|
+
"try:",
|
|
103
|
+
" # Resolver requires .okstra/project.json by default; for migrate",
|
|
104
|
+
" # we fall back to the cwd directly because the legacy layout has",
|
|
105
|
+
" # no .okstra/project.json yet.",
|
|
106
|
+
" try:",
|
|
107
|
+
" pr = resolve_project_root(explicit_root='', cwd=cwd)",
|
|
108
|
+
" except ResolverError:",
|
|
109
|
+
" pr = Path(cwd).resolve()",
|
|
110
|
+
" plan = prepare_migration_plan(pr)",
|
|
111
|
+
" if apply:",
|
|
112
|
+
" result = apply_migration_plan(plan, dry_run=False)",
|
|
113
|
+
" else:",
|
|
114
|
+
" result = apply_migration_plan(plan, dry_run=True)",
|
|
115
|
+
" print('OK')",
|
|
116
|
+
" print(json.dumps({'plan': plan.to_dict(), 'result': result.to_dict()}, ensure_ascii=False))",
|
|
117
|
+
"except MigrationRefused as e:",
|
|
118
|
+
" print('REFUSED')",
|
|
119
|
+
" print(str(e))",
|
|
120
|
+
" sys.exit(1)",
|
|
121
|
+
].join("\n"),
|
|
122
|
+
opts.cwd,
|
|
123
|
+
opts.apply ? "apply" : "dry",
|
|
124
|
+
],
|
|
125
|
+
{ PYTHONPATH: buildPythonpath(paths) },
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (py.code === 1) {
|
|
129
|
+
// Refused — pre-condition fail. Stderr message already on second line.
|
|
130
|
+
const reason = py.stdout.split("\n").slice(1).join("\n").trim() || py.stderr.trim();
|
|
131
|
+
if (!opts.quiet) process.stderr.write(`migrate refused: ${reason}\n`);
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
if (py.code !== 0) {
|
|
135
|
+
process.stderr.write(`error: python invocation failed: ${py.stderr.trim() || py.stdout.trim()}\n`);
|
|
136
|
+
return 2;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const lines = py.stdout.split("\n");
|
|
140
|
+
if (lines[0]?.trim() !== "OK") {
|
|
141
|
+
process.stderr.write(`error: unexpected python output: ${py.stdout}\n`);
|
|
142
|
+
return 2;
|
|
143
|
+
}
|
|
144
|
+
let payload;
|
|
145
|
+
try {
|
|
146
|
+
payload = JSON.parse(lines.slice(1).join("\n"));
|
|
147
|
+
} catch (err) {
|
|
148
|
+
process.stderr.write(`error: failed to parse python output: ${err.message}\n`);
|
|
149
|
+
return 2;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
emit(opts, {
|
|
153
|
+
ok: true,
|
|
154
|
+
mode: opts.apply ? "apply" : "dry-run",
|
|
155
|
+
...payload,
|
|
156
|
+
});
|
|
157
|
+
if (!opts.apply && !opts.quiet) {
|
|
158
|
+
process.stderr.write(
|
|
159
|
+
"\nNo changes written. Re-run with --apply to perform the migration.\n",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Single source of truth for the okstra-relative project directory name.
|
|
2
|
+
//
|
|
3
|
+
// okstra 가 한 프로젝트 안에 만드는 모든 산출물은 `<PROJECT_ROOT>/<OKSTRA_DIR>/`
|
|
4
|
+
// 한 곳 아래에 모인다. 이 모듈은 그 이름과 자주 쓰이는 path 조합을 한 곳에서
|
|
5
|
+
// export 한다. Python 쪽의 `okstra_project/dirs.py` 와 값이 정확히 일치해야
|
|
6
|
+
// 한다 (두 언어를 함께 운영하는 마이그레이션 명령이 같은 path 를 가리켜야
|
|
7
|
+
// 하므로). 디렉토리 이름을 바꾸려면 두 파일의 한 줄씩만 수정한다.
|
|
8
|
+
//
|
|
9
|
+
// 의존성은 `node:path` 만 — `src/install.mjs` / `src/setup.mjs` 등 무거운
|
|
10
|
+
// 모듈에서 import 할 때 순환 위험을 피한다.
|
|
11
|
+
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
export const OKSTRA_DIR = ".okstra";
|
|
15
|
+
// Pre-v0.37 layout. `okstra migrate` reads this to find unmigrated projects.
|
|
16
|
+
// 코드 path 빌드에는 절대 사용 금지 — `OKSTRA_DIR` 만. 오로지 migration
|
|
17
|
+
// 탐지/안내 메시지 용도.
|
|
18
|
+
export const LEGACY_OKSTRA_DIR = ".project-docs/okstra";
|
|
19
|
+
export const CLAUDE_MD_IMPORT_LINE = `@${OKSTRA_DIR}/CLAUDE.md`;
|
|
20
|
+
export const LEGACY_CLAUDE_MD_IMPORT_LINE = `@${LEGACY_OKSTRA_DIR}/CLAUDE.md`;
|
|
21
|
+
|
|
22
|
+
export function okstraRoot(projectRoot) {
|
|
23
|
+
return join(projectRoot, OKSTRA_DIR);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function projectJsonPath(projectRoot) {
|
|
27
|
+
return join(okstraRoot(projectRoot), "project.json");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function claudeMdSymlinkPath(projectRoot) {
|
|
31
|
+
return join(okstraRoot(projectRoot), "CLAUDE.md");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function claudeMdSymlinkRelative() {
|
|
35
|
+
// setup.mjs 의 기존 `CLAUDE_MD_SYMLINK_REL` 와 동일한 project-relative 값.
|
|
36
|
+
return join(OKSTRA_DIR, "CLAUDE.md");
|
|
37
|
+
}
|
package/src/paths.mjs
CHANGED
|
@@ -68,6 +68,23 @@ export async function resolvePaths() {
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// Compose the PYTHONPATH spawned python3 subprocesses should see.
|
|
72
|
+
//
|
|
73
|
+
// Precedence (first wins):
|
|
74
|
+
// 1. process.env.PYTHONPATH (user override — e.g. in-repo `PYTHONPATH=scripts`
|
|
75
|
+
// for dev work without re-running `okstra install`).
|
|
76
|
+
// 2. The installed user-runtime path (~/.okstra/lib/python).
|
|
77
|
+
//
|
|
78
|
+
// This is a colon-joined string suitable for direct assignment into the child
|
|
79
|
+
// process env. Empty segments are dropped so a missing env var does not produce
|
|
80
|
+
// a leading ':' (which Python treats as 'current directory').
|
|
81
|
+
export function buildPythonpath(paths) {
|
|
82
|
+
const fragments = [process.env.PYTHONPATH, paths.pythonpath].filter(
|
|
83
|
+
(s) => typeof s === "string" && s.length > 0,
|
|
84
|
+
);
|
|
85
|
+
return fragments.join(":");
|
|
86
|
+
}
|
|
87
|
+
|
|
71
88
|
async function readSimpleFile(path) {
|
|
72
89
|
try {
|
|
73
90
|
const { readFile } = await import("node:fs/promises");
|
package/src/setup.mjs
CHANGED
|
@@ -4,10 +4,14 @@ import { createInterface } from "node:readline";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join, resolve as resolvePath } from "node:path";
|
|
6
6
|
import { resolvePaths } from "./paths.mjs";
|
|
7
|
+
import {
|
|
8
|
+
CLAUDE_MD_IMPORT_LINE as DIRS_CLAUDE_MD_IMPORT_LINE,
|
|
9
|
+
claudeMdSymlinkRelative,
|
|
10
|
+
} from "./okstra-dirs.mjs";
|
|
7
11
|
|
|
8
12
|
const USAGE = `okstra setup — register the current project with okstra
|
|
9
13
|
|
|
10
|
-
Writes <PROJECT_ROOT>/.
|
|
14
|
+
Writes <PROJECT_ROOT>/.okstra/project.json. This is the
|
|
11
15
|
project-level companion to 'okstra install' (which is machine-level).
|
|
12
16
|
Inside a Claude Code session this is also exposed as the /okstra-setup
|
|
13
17
|
slash command.
|
|
@@ -298,7 +302,7 @@ export async function run(args) {
|
|
|
298
302
|
claudeMd = await ensureProjectClaudeMd(projectRoot);
|
|
299
303
|
} catch (err) {
|
|
300
304
|
process.stderr.write(
|
|
301
|
-
`warning: failed to wire <PROJECT>/CLAUDE.md @ .
|
|
305
|
+
`warning: failed to wire <PROJECT>/CLAUDE.md @ .okstra/CLAUDE.md import — ` +
|
|
302
306
|
`Claude Code sessions in this project will not auto-load okstra guidance. (${err.message})\n`,
|
|
303
307
|
);
|
|
304
308
|
}
|
|
@@ -378,8 +382,8 @@ async function backupAndReplace(target, template) {
|
|
|
378
382
|
}
|
|
379
383
|
|
|
380
384
|
const CLAUDE_MD_TEMPLATE_PATH = join(homedir(), ".okstra", "templates", "okstra.CLAUDE.md");
|
|
381
|
-
const CLAUDE_MD_SYMLINK_REL =
|
|
382
|
-
const CLAUDE_MD_IMPORT_LINE =
|
|
385
|
+
const CLAUDE_MD_SYMLINK_REL = claudeMdSymlinkRelative();
|
|
386
|
+
const CLAUDE_MD_IMPORT_LINE = DIRS_CLAUDE_MD_IMPORT_LINE;
|
|
383
387
|
const CLAUDE_MD_MARKER_BEGIN = "<!-- okstra:claude-md:begin (managed by okstra setup — do not edit) -->";
|
|
384
388
|
const CLAUDE_MD_MARKER_END = "<!-- okstra:claude-md:end -->";
|
|
385
389
|
|
package/src/uninstall.mjs
CHANGED
|
@@ -64,7 +64,7 @@ Usage:
|
|
|
64
64
|
active.jsonl, projects/, archive/,
|
|
65
65
|
state.json, .locks/
|
|
66
66
|
Per-project artifacts created by 'okstra setup'
|
|
67
|
-
(<PROJECT>/.
|
|
67
|
+
(<PROJECT>/.okstra/CLAUDE.md symlink,
|
|
68
68
|
<PROJECT>/CLAUDE.md import block,
|
|
69
69
|
<PROJECT>/AGENTS.md symlink,
|
|
70
70
|
<PROJECT>/.claude/settings.local.json symlink)
|
|
@@ -200,8 +200,8 @@ export async function runUninstall(args) {
|
|
|
200
200
|
|
|
201
201
|
await removePath(join(paths.home, "templates", "settings.local.json"), opts);
|
|
202
202
|
await removePath(join(paths.home, "templates", "okstra.CLAUDE.md"), opts);
|
|
203
|
-
// Per-project artifacts (<PROJECT>/.
|
|
204
|
-
// <PROJECT>/AGENTS.md symlink, and the @.
|
|
203
|
+
// Per-project artifacts (<PROJECT>/.okstra/CLAUDE.md symlink,
|
|
204
|
+
// <PROJECT>/AGENTS.md symlink, and the @.okstra/CLAUDE.md import
|
|
205
205
|
// block inside <PROJECT>/CLAUDE.md) are NOT removed here — uninstall is
|
|
206
206
|
// machine-level and does not know which projects opted in. Symlinks will
|
|
207
207
|
// dangle until the user removes them manually or re-runs okstra install +
|