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.
Files changed (80) hide show
  1. package/README.kr.md +1 -1
  2. package/README.md +1 -1
  3. package/docs/kr/architecture.md +18 -2
  4. package/docs/kr/cli.md +1 -1
  5. package/docs/project-structure-overview.md +2 -3
  6. package/docs/superpowers/plans/2026-06-02-final-verification-protocol-hardening.md +326 -0
  7. package/docs/superpowers/plans/2026-06-02-okstra-run-branch-confirm-step.md +337 -0
  8. package/docs/superpowers/plans/2026-06-02-okstra-run-phase-pane-cleanup.md +410 -0
  9. package/docs/superpowers/plans/2026-06-02-requirements-discovery-fanout.md +728 -0
  10. package/docs/superpowers/specs/2026-06-02-okstra-run-branch-confirm-step-design.md +113 -0
  11. package/docs/superpowers/specs/2026-06-02-okstra-run-phase-pane-cleanup-design.md +173 -0
  12. package/docs/superpowers/specs/2026-06-02-requirements-discovery-fanout-design.md +154 -0
  13. package/docs/task-process/requirements-discovery.md +1 -1
  14. package/package.json +3 -2
  15. package/runtime/BUILD.json +2 -2
  16. package/runtime/{python → bin}/lib/okstra/usage.sh +3 -2
  17. package/runtime/bin/okstra-codex-exec.sh +3 -3
  18. package/runtime/bin/okstra-trace-cleanup.sh +64 -26
  19. package/runtime/prompts/profiles/_common-contract.md +9 -5
  20. package/runtime/prompts/profiles/final-verification.md +18 -16
  21. package/runtime/prompts/profiles/implementation-planning.md +1 -0
  22. package/runtime/prompts/profiles/requirements-discovery.md +18 -1
  23. package/runtime/prompts/wizard/prompts.ko.json +11 -0
  24. package/runtime/python/okstra_ctl/consumers.py +1 -1
  25. package/runtime/python/okstra_ctl/fanout.py +35 -0
  26. package/runtime/python/okstra_ctl/migrate.py +21 -42
  27. package/runtime/python/okstra_ctl/reconcile.py +2 -2
  28. package/runtime/python/okstra_ctl/render_final_report.py +0 -1
  29. package/runtime/python/okstra_ctl/run.py +0 -29
  30. package/runtime/python/okstra_ctl/run_context.py +9 -12
  31. package/runtime/python/okstra_ctl/seeding.py +0 -192
  32. package/runtime/python/okstra_ctl/wizard.py +70 -5
  33. package/runtime/python/okstra_ctl/work_categories.py +21 -0
  34. package/runtime/python/okstra_ctl/worktree.py +74 -77
  35. package/runtime/python/okstra_project/__init__.py +0 -6
  36. package/runtime/python/okstra_project/dirs.py +0 -8
  37. package/runtime/schemas/final-report-v1.0.schema.json +34 -27
  38. package/runtime/skills/okstra-context-loader/SKILL.md +1 -1
  39. package/runtime/skills/okstra-convergence/SKILL.md +1 -1
  40. package/runtime/skills/okstra-inspect/SKILL.md +1 -1
  41. package/runtime/skills/okstra-run/SKILL.md +2 -0
  42. package/runtime/templates/prd/brief.template.md +1 -1
  43. package/runtime/templates/reports/fan-out-unit.template.md +25 -0
  44. package/runtime/templates/reports/final-report.template.md +24 -13
  45. package/runtime/templates/reports/final-verification-input.template.md +16 -5
  46. package/runtime/templates/reports/i18n/en.json +6 -3
  47. package/runtime/templates/reports/i18n/ko.json +6 -3
  48. package/runtime/templates/worker-prompt-preamble.md +7 -0
  49. package/runtime/validators/lib/fixtures.sh +2 -2
  50. package/runtime/validators/lib/validate-assets.sh +9 -0
  51. package/runtime/validators/validate-implementation-plan-stages.py +19 -11
  52. package/runtime/validators/validate-run.py +114 -0
  53. package/runtime/validators/validate-schedule.py +4 -4
  54. package/runtime/validators/validate_fanout.py +99 -0
  55. package/src/_proc.mjs +31 -0
  56. package/src/check-project.mjs +1 -25
  57. package/src/config.mjs +7 -31
  58. package/src/doctor.mjs +10 -29
  59. package/src/install.mjs +8 -36
  60. package/src/migrate.mjs +1 -18
  61. package/src/okstra-dirs.mjs +0 -11
  62. package/src/setup.mjs +1 -154
  63. package/src/uninstall.mjs +6 -13
  64. package/runtime/templates/okstra.CLAUDE.md +0 -104
  65. /package/runtime/{python → bin}/lib/okstra/cli.sh +0 -0
  66. /package/runtime/{python → bin}/lib/okstra/globals.sh +0 -0
  67. /package/runtime/{python → bin}/lib/okstra/interactive.sh +0 -0
  68. /package/runtime/{python → bin}/lib/okstra/project-resolver.sh +0 -0
  69. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-batch.sh +0 -0
  70. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-list.sh +0 -0
  71. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-open.sh +0 -0
  72. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-projects.sh +0 -0
  73. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-reconcile.sh +0 -0
  74. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-reindex.sh +0 -0
  75. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-rerun.sh +0 -0
  76. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-show.sh +0 -0
  77. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-tail.sh +0 -0
  78. /package/runtime/{python → bin}/lib/okstra-ctl/main.sh +0 -0
  79. /package/runtime/{python → bin}/lib/okstra-ctl/prepare.sh +0 -0
  80. /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
+ }
@@ -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 spawnCapture(
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 spawnCapture(
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 [_, maj, min] = m.map(Number);
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("python3", ["-c", `import ${moduleName}; print(${moduleName}.__name__)`], {
62
- env: { ...process.env, PYTHONPATH: pythonpath },
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 pathExists(p))) return { ok: false, detail: `missing ${p}` };
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 pathExists(paths.agents))
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 pathExists(p))
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
- // okstra-managed CLAUDE.md template. Per-project <PROJECT>/.okstra/CLAUDE.md
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 / okstra.CLAUDE.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 / okstra.CLAUDE.md
581
- // are consumed at runtime by okstra-render-report-views.py, final-report
582
- // assembly, and the per-project .okstra/CLAUDE.md symlink provisioned by
583
- // setup. They are NOT covered by installNamedTemplate (which only handles
584
- // the renamed settings.local.json sidecar), so without this step copy-mode
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");
@@ -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
- }