okstra 0.38.1 → 0.40.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/docs/kr/architecture.md +18 -2
- package/docs/kr/cli.md +1 -1
- package/docs/project-structure-overview.md +2 -3
- package/docs/superpowers/plans/2026-06-02-final-verification-protocol-hardening.md +326 -0
- package/docs/superpowers/plans/2026-06-02-okstra-run-branch-confirm-step.md +337 -0
- package/docs/superpowers/plans/2026-06-02-okstra-run-phase-pane-cleanup.md +410 -0
- package/docs/superpowers/plans/2026-06-02-requirements-discovery-fanout.md +728 -0
- package/docs/superpowers/specs/2026-06-02-okstra-run-branch-confirm-step-design.md +113 -0
- package/docs/superpowers/specs/2026-06-02-okstra-run-phase-pane-cleanup-design.md +173 -0
- package/docs/superpowers/specs/2026-06-02-requirements-discovery-fanout-design.md +154 -0
- package/docs/task-process/requirements-discovery.md +1 -1
- package/package.json +3 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/{python → bin}/lib/okstra/usage.sh +3 -2
- package/runtime/bin/okstra-codex-exec.sh +3 -3
- package/runtime/bin/okstra-trace-cleanup.sh +64 -26
- package/runtime/prompts/profiles/_common-contract.md +9 -5
- package/runtime/prompts/profiles/final-verification.md +18 -16
- package/runtime/prompts/profiles/implementation-planning.md +1 -0
- package/runtime/prompts/profiles/requirements-discovery.md +18 -1
- package/runtime/prompts/wizard/prompts.ko.json +11 -0
- package/runtime/python/okstra_ctl/consumers.py +1 -1
- package/runtime/python/okstra_ctl/fanout.py +35 -0
- package/runtime/python/okstra_ctl/migrate.py +21 -42
- package/runtime/python/okstra_ctl/reconcile.py +2 -2
- package/runtime/python/okstra_ctl/render_final_report.py +0 -1
- package/runtime/python/okstra_ctl/run.py +0 -29
- package/runtime/python/okstra_ctl/run_context.py +9 -12
- package/runtime/python/okstra_ctl/seeding.py +0 -192
- package/runtime/python/okstra_ctl/wizard.py +70 -5
- package/runtime/python/okstra_ctl/work_categories.py +21 -0
- package/runtime/python/okstra_ctl/worktree.py +74 -77
- package/runtime/python/okstra_project/__init__.py +0 -6
- package/runtime/python/okstra_project/dirs.py +0 -8
- package/runtime/schemas/final-report-v1.0.schema.json +34 -27
- package/runtime/skills/okstra-context-loader/SKILL.md +1 -1
- package/runtime/skills/okstra-convergence/SKILL.md +1 -1
- package/runtime/skills/okstra-inspect/SKILL.md +1 -1
- package/runtime/skills/okstra-run/SKILL.md +2 -0
- package/runtime/templates/prd/brief.template.md +1 -1
- package/runtime/templates/reports/fan-out-unit.template.md +25 -0
- package/runtime/templates/reports/final-report.template.md +24 -13
- package/runtime/templates/reports/final-verification-input.template.md +16 -5
- package/runtime/templates/reports/i18n/en.json +6 -3
- package/runtime/templates/reports/i18n/ko.json +6 -3
- package/runtime/templates/worker-prompt-preamble.md +7 -0
- package/runtime/validators/lib/fixtures.sh +2 -2
- package/runtime/validators/lib/validate-assets.sh +9 -0
- package/runtime/validators/validate-implementation-plan-stages.py +19 -11
- package/runtime/validators/validate-run.py +114 -0
- package/runtime/validators/validate-schedule.py +4 -4
- package/runtime/validators/validate_fanout.py +99 -0
- package/src/_proc.mjs +31 -0
- package/src/check-project.mjs +1 -25
- package/src/config.mjs +7 -31
- package/src/doctor.mjs +10 -29
- package/src/install.mjs +8 -36
- package/src/migrate.mjs +1 -18
- package/src/okstra-dirs.mjs +0 -11
- package/src/setup.mjs +1 -154
- package/src/uninstall.mjs +6 -13
- package/runtime/templates/okstra.CLAUDE.md +0 -104
- /package/runtime/{python → bin}/lib/okstra/cli.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra/globals.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra/interactive.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra/project-resolver.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-batch.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-list.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-open.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-projects.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-reconcile.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-reindex.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-rerun.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-show.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-tail.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/main.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/prepare.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/usage.sh +0 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Validator for requirements-discovery fan-out packets + index.
|
|
2
|
+
|
|
3
|
+
Called by validators/validate-run.py when task_type == "requirements-discovery"
|
|
4
|
+
and a `fan-out/` directory exists under the run dir.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "scripts"
|
|
14
|
+
if str(_SCRIPTS_DIR) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
16
|
+
|
|
17
|
+
from okstra_ctl.work_categories import WORK_CATEGORIES # noqa: E402
|
|
18
|
+
from okstra_ctl.fanout import topological_order, CycleError # noqa: E402
|
|
19
|
+
|
|
20
|
+
_NEXT_PHASES = ("error-analysis", "implementation-planning")
|
|
21
|
+
_UNIT_RE = re.compile(r"unit-\d{3}")
|
|
22
|
+
# index.md 번호목록 항목에서만 unit-NNN 을 추출 — 내러티브 문장 중복 방지
|
|
23
|
+
_INDEX_ENTRY_RE = re.compile(r"^\s*\d+\.\s+(unit-\d{3})\b", re.MULTILINE)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ValidationResult:
|
|
28
|
+
ok: bool
|
|
29
|
+
errors: list[str] = field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _frontmatter(text: str) -> dict[str, str]:
|
|
33
|
+
if not text.startswith("---"):
|
|
34
|
+
return {}
|
|
35
|
+
end = text.find("\n---", 3)
|
|
36
|
+
if end == -1:
|
|
37
|
+
return {}
|
|
38
|
+
fm: dict[str, str] = {}
|
|
39
|
+
last_key: str | None = None
|
|
40
|
+
for line in text[3:end].splitlines():
|
|
41
|
+
if ":" in line and not line.startswith((" ", "\t")):
|
|
42
|
+
k, _, v = line.partition(":")
|
|
43
|
+
fm[k.strip()] = v.strip()
|
|
44
|
+
last_key = k.strip()
|
|
45
|
+
elif last_key and line.strip().startswith("- "):
|
|
46
|
+
# YAML 블록리스트 항목 — 해당 키 값에 공백으로 이어 붙임
|
|
47
|
+
token = line.strip()[2:].strip()
|
|
48
|
+
fm[last_key] = (fm.get(last_key, "") + " " + token).strip()
|
|
49
|
+
return fm
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _parse_deps(raw: str) -> list[str]:
|
|
53
|
+
return _UNIT_RE.findall(raw or "")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def validate_fanout(run_dir: Path) -> ValidationResult:
|
|
57
|
+
run_dir = Path(run_dir)
|
|
58
|
+
fo = run_dir / "fan-out"
|
|
59
|
+
if not fo.is_dir():
|
|
60
|
+
return ValidationResult(ok=True)
|
|
61
|
+
|
|
62
|
+
errors: list[str] = []
|
|
63
|
+
units: dict[str, list[str]] = {}
|
|
64
|
+
for pkt in sorted(fo.glob("unit-*.md")):
|
|
65
|
+
fm = _frontmatter(pkt.read_text(encoding="utf-8"))
|
|
66
|
+
uid = fm.get("unit-id", "")
|
|
67
|
+
if uid != pkt.stem:
|
|
68
|
+
errors.append(f"{pkt.name}: unit-id {uid!r} != filename stem {pkt.stem!r}")
|
|
69
|
+
if fm.get("domain") not in WORK_CATEGORIES:
|
|
70
|
+
errors.append(f"{pkt.name}: domain {fm.get('domain')!r} not in {WORK_CATEGORIES}")
|
|
71
|
+
if fm.get("recommended-next-phase") not in _NEXT_PHASES:
|
|
72
|
+
errors.append(f"{pkt.name}: recommended-next-phase not in {_NEXT_PHASES}")
|
|
73
|
+
units[pkt.stem] = _parse_deps(fm.get("depends-on", ""))
|
|
74
|
+
|
|
75
|
+
if not units:
|
|
76
|
+
errors.append("fan-out/: directory exists but no unit-*.md packets found")
|
|
77
|
+
return ValidationResult(ok=False, errors=errors)
|
|
78
|
+
|
|
79
|
+
order: list[str] = []
|
|
80
|
+
try:
|
|
81
|
+
order = topological_order(units)
|
|
82
|
+
except CycleError as exc:
|
|
83
|
+
errors.append(f"depends-on cycle: {exc}")
|
|
84
|
+
except ValueError as exc:
|
|
85
|
+
errors.append(f"depends-on unresolved: {exc}")
|
|
86
|
+
|
|
87
|
+
index = fo / "index.md"
|
|
88
|
+
if not index.is_file():
|
|
89
|
+
errors.append("fan-out/index.md missing")
|
|
90
|
+
else:
|
|
91
|
+
listed = _INDEX_ENTRY_RE.findall(index.read_text(encoding="utf-8"))
|
|
92
|
+
if set(listed) != set(units):
|
|
93
|
+
errors.append(
|
|
94
|
+
f"index.md units {sorted(set(listed))} != packets {sorted(units)}"
|
|
95
|
+
)
|
|
96
|
+
elif order and listed != order:
|
|
97
|
+
errors.append(f"index.md order {listed} is not the topological order {order}")
|
|
98
|
+
|
|
99
|
+
return ValidationResult(ok=not errors, errors=errors)
|
package/src/_proc.mjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Shared filesystem / subprocess helpers used by the Node command modules.
|
|
2
|
+
// Extracted to one place so a fix lands once instead of in each command file.
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { promises as fs } from "node:fs";
|
|
5
|
+
|
|
6
|
+
export async function fileExists(p) {
|
|
7
|
+
try {
|
|
8
|
+
await fs.access(p);
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Run `cmd args`, capturing stdout/stderr. `env` holds *additional* vars that
|
|
16
|
+
// are merged over the inherited process.env. Never rejects — failures resolve
|
|
17
|
+
// with { code: -1, stderr: <message> }.
|
|
18
|
+
export function runProcess(cmd, args, env = {}) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const child = spawn(cmd, args, {
|
|
21
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
22
|
+
env: { ...process.env, ...env },
|
|
23
|
+
});
|
|
24
|
+
let stdout = "";
|
|
25
|
+
let stderr = "";
|
|
26
|
+
child.stdout.on("data", (b) => (stdout += b.toString()));
|
|
27
|
+
child.stderr.on("data", (b) => (stderr += b.toString()));
|
|
28
|
+
child.on("error", (err) => resolve({ code: -1, stdout, stderr: err.message }));
|
|
29
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
30
|
+
});
|
|
31
|
+
}
|
package/src/check-project.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { buildPythonpath, resolvePaths } from "./paths.mjs";
|
|
4
|
+
import { fileExists, runProcess } from "./_proc.mjs";
|
|
5
5
|
|
|
6
6
|
const USAGE = `okstra check-project — verify that the current project has okstra setup
|
|
7
7
|
|
|
@@ -27,30 +27,6 @@ to proceed if the exit code is non-zero, directing the user to
|
|
|
27
27
|
'/okstra-setup' first.
|
|
28
28
|
`;
|
|
29
29
|
|
|
30
|
-
function runProcess(cmd, args, env) {
|
|
31
|
-
return new Promise((resolve) => {
|
|
32
|
-
const child = spawn(cmd, args, {
|
|
33
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
34
|
-
env: { ...process.env, ...env },
|
|
35
|
-
});
|
|
36
|
-
let stdout = "";
|
|
37
|
-
let stderr = "";
|
|
38
|
-
child.stdout.on("data", (b) => (stdout += b.toString()));
|
|
39
|
-
child.stderr.on("data", (b) => (stderr += b.toString()));
|
|
40
|
-
child.on("error", (err) => resolve({ code: -1, stdout, stderr: err.message }));
|
|
41
|
-
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function fileExists(p) {
|
|
46
|
-
try {
|
|
47
|
-
await fs.access(p);
|
|
48
|
-
return true;
|
|
49
|
-
} catch {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
30
|
function parseArgs(args) {
|
|
55
31
|
const opts = { cwd: process.cwd(), quiet: false, json: true };
|
|
56
32
|
for (let i = 0; i < args.length; i++) {
|
package/src/config.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join, resolve, isAbsolute } from "node:path";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
4
|
import { buildPythonpath, resolvePaths } from "./paths.mjs";
|
|
6
5
|
import { OKSTRA_DIR, projectJsonPath } from "./okstra-dirs.mjs";
|
|
6
|
+
import { fileExists, runProcess } from "./_proc.mjs";
|
|
7
7
|
|
|
8
8
|
const USAGE = `okstra config — read / write okstra settings (project + global scopes)
|
|
9
9
|
|
|
@@ -136,15 +136,6 @@ function projectConfigPath(projectRoot) {
|
|
|
136
136
|
return projectJsonPath(projectRoot);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
async function fileExists(p) {
|
|
140
|
-
try {
|
|
141
|
-
await fs.access(p);
|
|
142
|
-
return true;
|
|
143
|
-
} catch {
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
139
|
async function readJson(p) {
|
|
149
140
|
if (!(await fileExists(p))) return null;
|
|
150
141
|
const text = await fs.readFile(p, "utf8");
|
|
@@ -163,24 +154,9 @@ async function writeJsonAtomic(p, data) {
|
|
|
163
154
|
await fs.rename(tmp, p);
|
|
164
155
|
}
|
|
165
156
|
|
|
166
|
-
function spawnCapture(cmd, args, env) {
|
|
167
|
-
return new Promise((resolveProc) => {
|
|
168
|
-
const child = spawn(cmd, args, {
|
|
169
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
170
|
-
env: { ...process.env, ...env },
|
|
171
|
-
});
|
|
172
|
-
let stdout = "";
|
|
173
|
-
let stderr = "";
|
|
174
|
-
child.stdout.on("data", (b) => (stdout += b.toString()));
|
|
175
|
-
child.stderr.on("data", (b) => (stderr += b.toString()));
|
|
176
|
-
child.on("error", (err) => resolveProc({ code: -1, stdout, stderr: err.message }));
|
|
177
|
-
child.on("close", (code) => resolveProc({ code, stdout, stderr }));
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
157
|
async function resolveProjectRoot(cwd) {
|
|
182
158
|
const paths = await resolvePaths();
|
|
183
|
-
const result = await
|
|
159
|
+
const result = await runProcess(
|
|
184
160
|
"python3",
|
|
185
161
|
[
|
|
186
162
|
"-c",
|
|
@@ -214,7 +190,7 @@ async function effectivePrTemplatePath(projectRoot) {
|
|
|
214
190
|
// case where we just want to surface the chain in 'config get --scope all'.
|
|
215
191
|
// Python remains the source of truth at run time; this is informational.
|
|
216
192
|
const paths = await resolvePaths();
|
|
217
|
-
const result = await
|
|
193
|
+
const result = await runProcess(
|
|
218
194
|
"python3",
|
|
219
195
|
[
|
|
220
196
|
"-c",
|
|
@@ -306,11 +282,11 @@ async function opSet(opts) {
|
|
|
306
282
|
const err = spec.validate(opts.value, ctx);
|
|
307
283
|
if (err) throw new Error(err);
|
|
308
284
|
|
|
309
|
-
const targetPath =
|
|
310
|
-
scope === "project" ? projectConfigPath(projectRoot) : globalConfigPath();
|
|
311
285
|
if (scope === "project" && !projectRoot) {
|
|
312
286
|
throw new Error("cwd not inside an okstra project — cannot write project scope");
|
|
313
287
|
}
|
|
288
|
+
const targetPath =
|
|
289
|
+
scope === "project" ? projectConfigPath(projectRoot) : globalConfigPath();
|
|
314
290
|
const existing = (await readJson(targetPath)) ?? {};
|
|
315
291
|
existing[spec.jsonField] = opts.value;
|
|
316
292
|
await writeJsonAtomic(targetPath, existing);
|
|
@@ -326,11 +302,11 @@ async function opUnset(opts) {
|
|
|
326
302
|
scope = projectRoot ? "project" : null;
|
|
327
303
|
if (!scope) throw new Error("--scope is required (project|global)");
|
|
328
304
|
}
|
|
329
|
-
const targetPath =
|
|
330
|
-
scope === "project" ? projectConfigPath(projectRoot) : globalConfigPath();
|
|
331
305
|
if (scope === "project" && !projectRoot) {
|
|
332
306
|
throw new Error("cwd not inside an okstra project — cannot unset project scope");
|
|
333
307
|
}
|
|
308
|
+
const targetPath =
|
|
309
|
+
scope === "project" ? projectConfigPath(projectRoot) : globalConfigPath();
|
|
334
310
|
const existing = (await readJson(targetPath)) ?? {};
|
|
335
311
|
if (!(spec.jsonField in existing)) {
|
|
336
312
|
emit(opts, { ok: true, scope, path: targetPath, key: opts.key, removed: false });
|
package/src/doctor.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
2
|
import { homedir } from "node:os";
|
|
4
3
|
import { join } from "node:path";
|
|
5
4
|
import { resolvePaths } from "./paths.mjs";
|
|
5
|
+
import { fileExists, runProcess } from "./_proc.mjs";
|
|
6
6
|
|
|
7
7
|
const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
|
|
8
8
|
const REQUIRED_SKILLS = ["okstra-setup", "okstra-run"];
|
|
@@ -23,34 +23,13 @@ Checks:
|
|
|
23
23
|
- version stamp matches package version
|
|
24
24
|
`;
|
|
25
25
|
|
|
26
|
-
async function pathExists(p) {
|
|
27
|
-
try {
|
|
28
|
-
await fs.access(p);
|
|
29
|
-
return true;
|
|
30
|
-
} catch {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function runProcess(cmd, args, opts = {}) {
|
|
36
|
-
return new Promise((resolve) => {
|
|
37
|
-
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], ...opts });
|
|
38
|
-
let stdout = "";
|
|
39
|
-
let stderr = "";
|
|
40
|
-
child.stdout.on("data", (b) => (stdout += b.toString()));
|
|
41
|
-
child.stderr.on("data", (b) => (stderr += b.toString()));
|
|
42
|
-
child.on("error", (err) => resolve({ code: -1, stdout, stderr: err.message }));
|
|
43
|
-
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
26
|
async function checkPython3() {
|
|
48
27
|
const r = await runProcess("python3", ["--version"]);
|
|
49
28
|
if (r.code !== 0) return { ok: false, detail: `python3 not found: ${r.stderr.trim() || "missing binary"}` };
|
|
50
29
|
const out = (r.stdout + r.stderr).trim();
|
|
51
30
|
const m = out.match(/Python\s+(\d+)\.(\d+)/);
|
|
52
31
|
if (!m) return { ok: false, detail: `unparsable python version: ${out}` };
|
|
53
|
-
const [
|
|
32
|
+
const [, maj, min] = m.map(Number);
|
|
54
33
|
if (maj < 3 || (maj === 3 && min < 10)) {
|
|
55
34
|
return { ok: false, detail: `python ${maj}.${min} is too old (need 3.10+)` };
|
|
56
35
|
}
|
|
@@ -58,9 +37,11 @@ async function checkPython3() {
|
|
|
58
37
|
}
|
|
59
38
|
|
|
60
39
|
async function checkPythonImport(moduleName, pythonpath) {
|
|
61
|
-
const r = await runProcess(
|
|
62
|
-
|
|
63
|
-
|
|
40
|
+
const r = await runProcess(
|
|
41
|
+
"python3",
|
|
42
|
+
["-c", `import ${moduleName}; print(${moduleName}.__name__)`],
|
|
43
|
+
{ PYTHONPATH: pythonpath },
|
|
44
|
+
);
|
|
64
45
|
if (r.code !== 0) {
|
|
65
46
|
return { ok: false, detail: `import ${moduleName} failed: ${r.stderr.trim().split("\n").pop()}` };
|
|
66
47
|
}
|
|
@@ -69,7 +50,7 @@ async function checkPythonImport(moduleName, pythonpath) {
|
|
|
69
50
|
|
|
70
51
|
async function checkBashEntry(binDir, name) {
|
|
71
52
|
const p = join(binDir, name);
|
|
72
|
-
if (!(await
|
|
53
|
+
if (!(await fileExists(p))) return { ok: false, detail: `missing ${p}` };
|
|
73
54
|
try {
|
|
74
55
|
const st = await fs.stat(p);
|
|
75
56
|
if (!(st.mode & 0o111)) return { ok: false, detail: `${name} not executable` };
|
|
@@ -101,7 +82,7 @@ export async function run(args) {
|
|
|
101
82
|
await check("okstra_project import", () => checkPythonImport("okstra_project", paths.pythonpath)),
|
|
102
83
|
await check("okstra_ctl import", () => checkPythonImport("okstra_ctl", paths.pythonpath)),
|
|
103
84
|
await check("agents dir", async () =>
|
|
104
|
-
(await
|
|
85
|
+
(await fileExists(paths.agents))
|
|
105
86
|
? { ok: true, detail: paths.agents }
|
|
106
87
|
: { ok: false, detail: `not found: ${paths.agents}` },
|
|
107
88
|
),
|
|
@@ -113,7 +94,7 @@ export async function run(args) {
|
|
|
113
94
|
REQUIRED_SKILLS.map((name) =>
|
|
114
95
|
check(`skill: ${name}`, async () => {
|
|
115
96
|
const p = join(CLAUDE_SKILLS_DIR, name, "SKILL.md");
|
|
116
|
-
return (await
|
|
97
|
+
return (await fileExists(p))
|
|
117
98
|
? { ok: true, detail: p }
|
|
118
99
|
: { ok: false, detail: `not found: ${p}` };
|
|
119
100
|
}),
|
package/src/install.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { join, relative, resolve as resolveAbs } from "node:path";
|
|
5
5
|
import { getPackageRoot } from "./version.mjs";
|
|
6
6
|
import { resolvePaths } from "./paths.mjs";
|
|
7
|
+
import { fileExists } from "./_proc.mjs";
|
|
7
8
|
|
|
8
9
|
const SKILLS_MANIFEST_REL = "installed-skills.json";
|
|
9
10
|
const AGENTS_MANIFEST_REL = "installed-agents.json";
|
|
@@ -15,11 +16,7 @@ const SETTINGS_TEMPLATE_SRC_REL = ["templates", "reports", "settings.template.js
|
|
|
15
16
|
// Destination under ~/.okstra/. Project-local .claude/settings.local.json symlinks here.
|
|
16
17
|
const SETTINGS_TEMPLATE_DST_REL = ["templates", "settings.local.json"];
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
// symlinks here; <PROJECT>/CLAUDE.md gets an `@.okstra/CLAUDE.md` import line.
|
|
20
|
-
const CLAUDE_MD_TEMPLATE_REL = ["templates", "okstra.CLAUDE.md"];
|
|
21
|
-
|
|
22
|
-
const PYTHON_PACKAGES = ["okstra_project", "okstra_ctl", "okstra_token_usage", "okstra_vendor", "lib"];
|
|
19
|
+
const PYTHON_PACKAGES = ["okstra_project", "okstra_ctl", "okstra_token_usage", "okstra_vendor"];
|
|
23
20
|
const BIN_ENTRYPOINTS = [
|
|
24
21
|
"okstra.sh",
|
|
25
22
|
"okstra-codex-exec.sh",
|
|
@@ -46,7 +43,7 @@ Usage:
|
|
|
46
43
|
Effect (copy mode):
|
|
47
44
|
${"$HOME"}/.okstra/lib/python <- runtime/python
|
|
48
45
|
${"$HOME"}/.okstra/bin <- runtime/bin
|
|
49
|
-
${"$HOME"}/.okstra/templates <- runtime/templates (report.css / report.js / *.template.md
|
|
46
|
+
${"$HOME"}/.okstra/templates <- runtime/templates (report.css / report.js / *.template.md)
|
|
50
47
|
${"$HOME"}/.okstra/templates/settings.local.json <- runtime/templates/reports/settings.template.json
|
|
51
48
|
${"$HOME"}/.claude/skills/<name> <- runtime/skills/<name> (per skill)
|
|
52
49
|
${"$HOME"}/.claude/agents/<worker>.md <- runtime/agents/workers/<worker>.md
|
|
@@ -58,7 +55,6 @@ Effect (link mode):
|
|
|
58
55
|
${"$HOME"}/.okstra/lib/python/<pkg> -> <repo>/scripts/<pkg> (symlink)
|
|
59
56
|
${"$HOME"}/.okstra/bin/<name>.sh -> <repo>/scripts/<name>.sh
|
|
60
57
|
${"$HOME"}/.okstra/templates/settings.local.json -> <repo>/templates/reports/settings.template.json
|
|
61
|
-
${"$HOME"}/.okstra/templates/okstra.CLAUDE.md -> <repo>/templates/okstra.CLAUDE.md
|
|
62
58
|
${"$HOME"}/.claude/skills/<name> -> <repo>/skills/<name> (symlink dir)
|
|
63
59
|
${"$HOME"}/.claude/agents/<worker>.md -> <repo>/agents/workers/<worker>.md
|
|
64
60
|
${"$HOME"}/.okstra/dev-link <- <repo> path stamp
|
|
@@ -69,11 +65,6 @@ project-local <project>/.claude/settings.local.json that okstra-setup
|
|
|
69
65
|
provisions, granting per-project Claude Code permissions for okstra
|
|
70
66
|
worker wrapper scripts without modifying the user's global settings.
|
|
71
67
|
|
|
72
|
-
The okstra.CLAUDE.md file is the symlink target referenced by every
|
|
73
|
-
project-local <project>/.okstra/CLAUDE.md that okstra-setup provisions;
|
|
74
|
-
<project>/CLAUDE.md gets an "@.okstra/CLAUDE.md" import block so Claude
|
|
75
|
-
Code automatically picks up the okstra runtime guidance.
|
|
76
|
-
|
|
77
68
|
Worker agent definitions are installed into ${"$HOME"}/.claude/agents/ so
|
|
78
69
|
that Claude Code's subagent discovery picks them up; they cannot live
|
|
79
70
|
inside the package alone because the harness only scans ~/.claude/agents/
|
|
@@ -256,7 +247,6 @@ async function installLinkMode(repoPath, paths, opts) {
|
|
|
256
247
|
await writeAgentsManifest(paths.home, agentResult.installed, { dryRun });
|
|
257
248
|
|
|
258
249
|
await installNamedTemplate(repoAbs, paths, { mode: "link", dryRun, quiet }, SETTINGS_TEMPLATE_DESCRIPTOR);
|
|
259
|
-
await installNamedTemplate(repoAbs, paths, { mode: "link", dryRun, quiet }, CLAUDE_MD_TEMPLATE_DESCRIPTOR);
|
|
260
250
|
|
|
261
251
|
if (!dryRun) {
|
|
262
252
|
await writeFileAtomic(join(paths.home, "dev-link"), repoAbs + "\n", 0o644);
|
|
@@ -277,15 +267,6 @@ async function installLinkMode(repoPath, paths, opts) {
|
|
|
277
267
|
return 0;
|
|
278
268
|
}
|
|
279
269
|
|
|
280
|
-
async function fileExists(p) {
|
|
281
|
-
try {
|
|
282
|
-
await fs.access(p);
|
|
283
|
-
return true;
|
|
284
|
-
} catch {
|
|
285
|
-
return false;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
270
|
async function listSkillDirs(skillsRoot) {
|
|
290
271
|
try {
|
|
291
272
|
const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
|
|
@@ -458,12 +439,6 @@ const SETTINGS_TEMPLATE_DESCRIPTOR = {
|
|
|
458
439
|
label: "settings template",
|
|
459
440
|
};
|
|
460
441
|
|
|
461
|
-
const CLAUDE_MD_TEMPLATE_DESCRIPTOR = {
|
|
462
|
-
srcRel: CLAUDE_MD_TEMPLATE_REL,
|
|
463
|
-
dstRel: CLAUDE_MD_TEMPLATE_REL,
|
|
464
|
-
label: "claude.md template",
|
|
465
|
-
};
|
|
466
|
-
|
|
467
442
|
async function installSkillsCopy(runtimeRoot, opts) {
|
|
468
443
|
const { refresh, dryRun, quiet } = opts;
|
|
469
444
|
const srcRoot = join(runtimeRoot, "skills");
|
|
@@ -577,12 +552,11 @@ export async function runInstall(args) {
|
|
|
577
552
|
paths.bin,
|
|
578
553
|
{ refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o755 },
|
|
579
554
|
);
|
|
580
|
-
// templates/ tree — report.css / report.js / *.template.md
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
// installs miss every asset other than that single file. See
|
|
555
|
+
// templates/ tree — report.css / report.js / *.template.md are consumed at
|
|
556
|
+
// runtime by okstra-render-report-views.py and final-report assembly. They
|
|
557
|
+
// are NOT covered by installNamedTemplate (which only handles the renamed
|
|
558
|
+
// settings.local.json sidecar), so without this step copy-mode installs
|
|
559
|
+
// miss every asset other than that single file. See
|
|
586
560
|
// okstra-render-report-views.py _TEMPLATES_DIRS for the lookup path.
|
|
587
561
|
const templatesResult = await copyTreeIfChanged(
|
|
588
562
|
join(runtimeRoot, "templates"),
|
|
@@ -632,8 +606,6 @@ export async function runInstall(args) {
|
|
|
632
606
|
await writeAgentsManifest(paths.home, agentResult.installed, { dryRun: opts.dryRun });
|
|
633
607
|
|
|
634
608
|
await installNamedTemplate(runtimeRoot, paths, { mode: "copy", ...opts }, SETTINGS_TEMPLATE_DESCRIPTOR);
|
|
635
|
-
// okstra.CLAUDE.md is already covered by the runtime/templates tree copy
|
|
636
|
-
// above (same src/dst path, no rename). No second call needed in copy mode.
|
|
637
609
|
|
|
638
610
|
if (!opts.dryRun) {
|
|
639
611
|
await writeFileAtomic(join(paths.home, "version"), paths.package + "\n", 0o644);
|
package/src/migrate.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { buildPythonpath, resolvePaths } from "./paths.mjs";
|
|
3
2
|
import { LEGACY_OKSTRA_DIR, OKSTRA_DIR } from "./okstra-dirs.mjs";
|
|
3
|
+
import { runProcess } from "./_proc.mjs";
|
|
4
4
|
|
|
5
5
|
const USAGE = `okstra migrate — move project artifacts from ${LEGACY_OKSTRA_DIR}/ to ${OKSTRA_DIR}/
|
|
6
6
|
|
|
@@ -15,7 +15,6 @@ Usage:
|
|
|
15
15
|
okstra migrate --apply Execute the move and ancillary updates.
|
|
16
16
|
okstra migrate --cwd <dir> Search starting point (default: cwd).
|
|
17
17
|
okstra migrate --quiet Suppress stdout on success; exit code reports.
|
|
18
|
-
okstra migrate --json Force JSON output (default for both modes).
|
|
19
18
|
|
|
20
19
|
Exit codes:
|
|
21
20
|
0 plan prepared (dry-run) or migration applied successfully
|
|
@@ -28,28 +27,12 @@ exits 1 with "nothing to migrate". Safe to wire into CI as a one-shot
|
|
|
28
27
|
post-upgrade step.
|
|
29
28
|
`;
|
|
30
29
|
|
|
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
30
|
function parseArgs(args) {
|
|
47
31
|
const opts = { cwd: process.cwd(), apply: false, quiet: false };
|
|
48
32
|
for (let i = 0; i < args.length; i++) {
|
|
49
33
|
const a = args[i];
|
|
50
34
|
if (a === "--apply") opts.apply = true;
|
|
51
35
|
else if (a === "--quiet" || a === "-q") opts.quiet = true;
|
|
52
|
-
else if (a === "--json") opts.json = true;
|
|
53
36
|
else if (a === "--cwd") {
|
|
54
37
|
const next = args[i + 1];
|
|
55
38
|
if (!next || next.startsWith("--")) throw new Error("--cwd requires a path");
|
package/src/okstra-dirs.mjs
CHANGED
|
@@ -16,8 +16,6 @@ export const OKSTRA_DIR = ".okstra";
|
|
|
16
16
|
// 코드 path 빌드에는 절대 사용 금지 — `OKSTRA_DIR` 만. 오로지 migration
|
|
17
17
|
// 탐지/안내 메시지 용도.
|
|
18
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
19
|
|
|
22
20
|
export function okstraRoot(projectRoot) {
|
|
23
21
|
return join(projectRoot, OKSTRA_DIR);
|
|
@@ -26,12 +24,3 @@ export function okstraRoot(projectRoot) {
|
|
|
26
24
|
export function projectJsonPath(projectRoot) {
|
|
27
25
|
return join(okstraRoot(projectRoot), "project.json");
|
|
28
26
|
}
|
|
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
|
-
}
|