okstra 0.101.1 → 0.102.1

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 (40) hide show
  1. package/README.kr.md +5 -5
  2. package/README.md +5 -5
  3. package/docs/for-ai/README.md +2 -3
  4. package/docs/for-ai/skills/{okstra-container.md → okstra-container-build.md} +3 -4
  5. package/docs/for-ai/skills/okstra-inspect.md +1 -2
  6. package/docs/kr/architecture/storage-model.md +1 -2
  7. package/docs/kr/architecture.md +6 -6
  8. package/docs/kr/cli.md +3 -3
  9. package/docs/kr/container.md +1 -1
  10. package/docs/project-structure-overview.md +5 -5
  11. package/docs/superpowers/plans/2026-06-15-internal-skill-resource-migration.md +1 -1
  12. package/docs/superpowers/plans/2026-06-24-cross-project-precondition.md +542 -0
  13. package/docs/superpowers/plans/2026-06-24-decision-drafts-render-gap.md +568 -0
  14. package/docs/superpowers/plans/2026-06-24-manual-user-test-section.md +203 -0
  15. package/docs/superpowers/plans/2026-06-24-okstra-container-build-rename.md +525 -0
  16. package/docs/superpowers/specs/2026-06-24-cross-project-precondition-design.md +129 -0
  17. package/docs/superpowers/specs/2026-06-24-decision-drafts-render-gap-design.md +185 -0
  18. package/docs/superpowers/specs/2026-06-24-manual-user-test-section-design.md +121 -0
  19. package/docs/superpowers/specs/2026-06-24-okstra-container-build-rename-design.md +110 -0
  20. package/package.json +1 -1
  21. package/runtime/BUILD.json +2 -2
  22. package/runtime/agents/workers/report-writer-worker.md +1 -0
  23. package/runtime/prompts/lead/report-writer.md +1 -1
  24. package/runtime/prompts/lead/team-contract.md +2 -2
  25. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -0
  26. package/runtime/prompts/profiles/final-verification.md +4 -3
  27. package/runtime/prompts/profiles/implementation-planning.md +12 -9
  28. package/runtime/python/okstra_ctl/run.py +0 -1
  29. package/runtime/python/okstra_token_usage/paths.py +1 -2
  30. package/runtime/schemas/final-report-v1.0.schema.json +106 -3
  31. package/runtime/skills/{okstra-container → okstra-container-build}/SKILL.md +3 -3
  32. package/runtime/templates/reports/final-report.template.md +63 -2
  33. package/runtime/templates/reports/final-verification-input.template.md +3 -3
  34. package/runtime/templates/reports/i18n/en.json +28 -1
  35. package/runtime/templates/reports/i18n/ko.json +28 -1
  36. package/runtime/templates/worker-prompt-preamble.md +1 -1
  37. package/runtime/validators/validate-run.py +70 -1
  38. package/src/commands/lifecycle/install.mjs +17 -18
  39. package/src/commands/lifecycle/uninstall.mjs +8 -6
  40. package/src/lib/skill-catalog.mjs +10 -9
@@ -1140,6 +1140,8 @@ PLANNING_REQUIRED_SECTIONS = (
1140
1140
  "Rollback",
1141
1141
  "Requirement Coverage",
1142
1142
  "Plan Body Verification",
1143
+ "Cross-Project Dependencies",
1144
+ "Decision Drafts",
1143
1145
  )
1144
1146
 
1145
1147
  # §5.7 implementation deliverables — substring scan against report body.
@@ -1152,6 +1154,7 @@ IMPLEMENTATION_REQUIRED_SECTIONS = (
1152
1154
  "Validation Evidence",
1153
1155
  "Verifier Results",
1154
1156
  "Rollback Verification",
1157
+ "Manual User Test (Draft)",
1155
1158
  "Routing Recommendation",
1156
1159
  )
1157
1160
 
@@ -1163,6 +1166,7 @@ FINAL_VERIFICATION_REQUIRED_SECTIONS = (
1163
1166
  "Validation Evidence",
1164
1167
  "Read-only Command Log",
1165
1168
  "Conditional Acceptance Conditions",
1169
+ "Manual User Test Results",
1166
1170
  "Routing Recommendation",
1167
1171
  )
1168
1172
 
@@ -1483,8 +1487,12 @@ def validate_final_report_data(report_path: Path, failures: list[str]) -> None:
1483
1487
 
1484
1488
  _validate_rationale_evidence(data, failures)
1485
1489
 
1486
- if (data.get("header") or {}).get("taskType") == "final-verification":
1490
+ task_type = (data.get("header") or {}).get("taskType")
1491
+ if task_type == "final-verification":
1487
1492
  _validate_final_verification_consistency(data, failures)
1493
+ elif task_type == "implementation-planning":
1494
+ _validate_implementation_planning_cross_project(data, failures)
1495
+ _validate_implementation_planning_decision_drafts(data, failures)
1488
1496
 
1489
1497
 
1490
1498
  # path:line (foo.service.ts:268), report ID (C-001 / F-013 / G-crit-002 / RC-1),
@@ -1525,6 +1533,67 @@ def _validate_rationale_evidence(data: dict, failures: list[str]) -> None:
1525
1533
  )
1526
1534
 
1527
1535
 
1536
+ def _validate_implementation_planning_cross_project(data: dict, failures: list[str]) -> None:
1537
+ """타 프로젝트 의존을 DM 행(`kind == 'cross-project'`)으로 선언했다면
1538
+ `crossProjectDependencies` 에 `direction == 'upstream-precondition'` 행이
1539
+ 반드시 있어야 한다 — 실제 선행 필수 의존이 soft `recommendedNextSteps`
1540
+ 추천으로 새어나가지 못하게 강제한다. 각 XP 행의 필드 비-빈은 스키마가
1541
+ 보장하고, 이 검사는 cross-project DM 신호가 있을 때 XP 행이 *존재*하는지를 본다.
1542
+ """
1543
+ ip = data.get("implementationPlanning")
1544
+ if not isinstance(ip, dict):
1545
+ return
1546
+ dm_rows = ip.get("dependencyMigrationRisk") or []
1547
+ has_cross_project_dm = any(
1548
+ isinstance(r, dict) and r.get("kind") == "cross-project" for r in dm_rows
1549
+ )
1550
+ if not has_cross_project_dm:
1551
+ return
1552
+ xp_rows = ip.get("crossProjectDependencies") or []
1553
+ has_upstream = any(
1554
+ isinstance(r, dict) and r.get("direction") == "upstream-precondition"
1555
+ for r in xp_rows
1556
+ )
1557
+ if not has_upstream:
1558
+ failures.append(
1559
+ "final-report data.json: a dependencyMigrationRisk row has "
1560
+ "kind='cross-project' but crossProjectDependencies carries no "
1561
+ "direction='upstream-precondition' entry. Record the cross-project "
1562
+ "dependency as a mandatory precondition (concrete requiredWork / "
1563
+ "verificationSignal / howToStart) — not a soft Recommended Next Step."
1564
+ )
1565
+
1566
+
1567
+ def _validate_implementation_planning_decision_drafts(data: dict, failures: list[str]) -> None:
1568
+ """`decisionDrafts` 가 비어있지 않으면 어느 stage 의 stepwiseExecution step 이
1569
+ `.okstra/decisions/` 파일을 생성하는 materialization step 을 포함해야 한다.
1570
+ 프로파일 Decision-record evaluation 의 "approved plan stepwise MUST include
1571
+ Create .okstra/decisions/<NNNN>-<slug>.md" 를 실제 강제한다. draft 존재 자체가
1572
+ trigger 이므로 self-contained 하다.
1573
+ """
1574
+ ip = data.get("implementationPlanning")
1575
+ if not isinstance(ip, dict):
1576
+ return
1577
+ if not (ip.get("decisionDrafts") or []):
1578
+ return
1579
+ has_materialization = any(
1580
+ ".okstra/decisions/" in (step.get("files") or "")
1581
+ or ".okstra/decisions/" in (step.get("action") or "")
1582
+ for stage in (ip.get("stages") or [])
1583
+ if isinstance(stage, dict)
1584
+ for step in (stage.get("stepwiseExecution") or [])
1585
+ if isinstance(step, dict)
1586
+ )
1587
+ if not has_materialization:
1588
+ failures.append(
1589
+ "final-report data.json: implementationPlanning.decisionDrafts is "
1590
+ "non-empty but no stage's stepwiseExecution creates a "
1591
+ "`.okstra/decisions/<NNNN>-<slug>.md` file. The approved plan must "
1592
+ "materialize each decision draft via a stepwise step (profile "
1593
+ "Decision-record evaluation)."
1594
+ )
1595
+
1596
+
1528
1597
  def _validate_final_verification_consistency(data: dict, failures: list[str]) -> None:
1529
1598
  """Enforce verdict ↔ blocker/condition/routing consistency on the
1530
1599
  final-verification data.json (SSOT). The schema guarantees field SHAPE;
@@ -7,16 +7,16 @@ import { resolvePaths } from "../../lib/paths.mjs";
7
7
  import { fileExists } from "../../lib/proc.mjs";
8
8
  import { buildRuntimeManifest, RUNTIMES_MANIFEST_REL } from "../../lib/runtime-manifest.mjs";
9
9
  import { normalizeRuntimeRequest, resolveRuntime } from "../../lib/runtime-resolver.mjs";
10
- import { OBSOLETE_INTERNAL_SKILL_NAMES, USER_SKILL_NAMES } from "../../lib/skill-catalog.mjs";
10
+ import { OBSOLETE_SKILL_NAMES, USER_SKILL_NAMES } from "../../lib/skill-catalog.mjs";
11
11
 
12
12
  const SKILLS_MANIFEST_REL = "installed-skills.json";
13
13
  const AGENTS_MANIFEST_REL = "installed-agents.json";
14
14
  const USER_HOME = homedir();
15
15
  const CLAUDE_HOME = join(USER_HOME, ".claude");
16
- const CODEX_HOME = join(USER_HOME, ".codex");
16
+ const AGENT_HOME = join(USER_HOME, ".agent");
17
17
  const OMP_AGENT_HOME = join(USER_HOME, ".omp", "agent");
18
18
  const CLAUDE_SKILLS_DIR = join(CLAUDE_HOME, "skills");
19
- const CODEX_SKILLS_DIR = join(CODEX_HOME, "skills");
19
+ const AGENT_SKILLS_DIR = join(AGENT_HOME, "skills");
20
20
  const OMP_SKILLS_DIR = join(OMP_AGENT_HOME, "skills");
21
21
  const CLAUDE_AGENTS_DIR = join(CLAUDE_HOME, "agents");
22
22
 
@@ -82,8 +82,8 @@ Effect (copy mode):
82
82
  ${"$HOME"}/.okstra/prompts <- runtime/prompts (lead contracts + coding-preflight pack)
83
83
  ${"$HOME"}/.okstra/templates/settings.local.json <- runtime/templates/reports/settings.template.json
84
84
  ${"$HOME"}/.claude/skills/<name> <- skills when ${"$HOME"}/.claude exists
85
- ${"$HOME"}/.codex/skills/<name> <- skills when ${"$HOME"}/.codex exists
86
- ${"$HOME"}/.omp/agent/skills/<name> <- skills fallback when Claude/Codex homes are absent
85
+ ${"$HOME"}/.agent/skills/<name> <- skills when ${"$HOME"}/.agent exists
86
+ ${"$HOME"}/.omp/agent/skills/<name> <- skills fallback when Claude/Agent homes are absent
87
87
  ${"$HOME"}/.claude/agents/<worker>.md <- worker agents when ${"$HOME"}/.claude exists
88
88
  ${"$HOME"}/.okstra/installed-runtimes.json <- install request/resolution manifest
89
89
  ${"$HOME"}/.okstra/installed-skills.json <- target-aware manifest of installed skills
@@ -96,7 +96,7 @@ Effect (link mode):
96
96
  ${"$HOME"}/.okstra/prompts -> <repo>/prompts
97
97
  ${"$HOME"}/.okstra/templates/settings.local.json -> <repo>/templates/reports/settings.template.json
98
98
  ${"$HOME"}/.claude/skills/<name> -> <repo>/skills/<name> when ${"$HOME"}/.claude exists
99
- ${"$HOME"}/.codex/skills/<name> -> <repo>/skills/<name> when ${"$HOME"}/.codex exists
99
+ ${"$HOME"}/.agent/skills/<name> -> <repo>/skills/<name> when ${"$HOME"}/.agent exists
100
100
  ${"$HOME"}/.omp/agent/skills/<name> -> <repo>/skills/<name> fallback
101
101
  ${"$HOME"}/.claude/agents/<worker>.md -> <repo>/agents/workers/<worker>.md when ${"$HOME"}/.claude exists
102
102
  ${"$HOME"}/.okstra/dev-link <- <repo> path stamp
@@ -401,12 +401,12 @@ async function installLinkMode(repoPath, paths, opts) {
401
401
 
402
402
  async function detectSkillTargets() {
403
403
  const targets = [];
404
- const [hasClaude, hasCodex] = await Promise.all([dirExists(CLAUDE_HOME), dirExists(CODEX_HOME)]);
404
+ const [hasClaude, hasAgent] = await Promise.all([dirExists(CLAUDE_HOME), dirExists(AGENT_HOME)]);
405
405
  if (hasClaude) {
406
406
  targets.push({ provider: "claude", home: CLAUDE_HOME, skillsDir: CLAUDE_SKILLS_DIR });
407
407
  }
408
- if (hasCodex) {
409
- targets.push({ provider: "codex", home: CODEX_HOME, skillsDir: CODEX_SKILLS_DIR });
408
+ if (hasAgent) {
409
+ targets.push({ provider: "agent", home: AGENT_HOME, skillsDir: AGENT_SKILLS_DIR });
410
410
  }
411
411
  if (targets.length === 0 && (await dirExists(OMP_AGENT_HOME))) {
412
412
  targets.push({ provider: "omp", home: OMP_AGENT_HOME, skillsDir: OMP_SKILLS_DIR });
@@ -637,7 +637,7 @@ async function installSkillsCopy(runtimeRoot, target, opts) {
637
637
  copied += r.copied;
638
638
  skipped += r.skipped;
639
639
  }
640
- const pruned = await pruneObsoleteInternalSkills(target, opts);
640
+ const pruned = await pruneObsoleteSkills(target, opts);
641
641
  if (!quiet) {
642
642
  process.stdout.write(
643
643
  ` ${target.provider} skills: copied=${copied} skipped=${skipped} pruned=${pruned} -> ${target.skillsDir}/ (${USER_SKILL_NAMES.length} skills)\n`,
@@ -656,19 +656,18 @@ async function installSkillsLink(repoAbs, target, opts) {
656
656
  const action = await ensureSymlink(src, dst, { dryRun });
657
657
  if (!quiet) process.stdout.write(` ${target.provider} skills/${name}: ${action}\n`);
658
658
  }
659
- const pruned = await pruneObsoleteInternalSkills(target, opts);
659
+ const pruned = await pruneObsoleteSkills(target, opts);
660
660
  if (!quiet && pruned > 0) {
661
661
  process.stdout.write(` ${target.provider} obsolete internal skills: pruned=${pruned}\n`);
662
662
  }
663
663
  return { ...target, installed: [...USER_SKILL_NAMES] };
664
664
  }
665
665
 
666
- // Remove former internal skill directories left by older installs. Exact-name
667
- // only (OBSOLETE_INTERNAL_SKILL_NAMES) — those contracts now ship as runtime
668
- // resources under ~/.okstra/prompts/, not as agent skills.
669
- async function pruneObsoleteInternalSkills(target, opts) {
666
+ // Remove former skill directories left by older installs. Exact-name only
667
+ // (OBSOLETE_SKILL_NAMES) — never wildcard-delete okstra-*.
668
+ async function pruneObsoleteSkills(target, opts) {
670
669
  let pruned = 0;
671
- for (const name of OBSOLETE_INTERNAL_SKILL_NAMES) {
670
+ for (const name of OBSOLETE_SKILL_NAMES) {
672
671
  const path = join(target.skillsDir, name);
673
672
  if (!(await pathEntryExists(path))) continue;
674
673
  pruned += 1;
@@ -683,7 +682,7 @@ async function pruneObsoleteInternalSkills(target, opts) {
683
682
 
684
683
  async function installSkillTargetsCopy(runtimeRoot, targets, opts) {
685
684
  if (targets.length === 0) {
686
- if (!opts.quiet) process.stdout.write(" skills: no Claude, Codex, or OMP agent home found — skipped\n");
685
+ if (!opts.quiet) process.stdout.write(" skills: no Claude, Agent, or OMP agent home found — skipped\n");
687
686
  return [];
688
687
  }
689
688
  return await Promise.all(targets.map((target) => installSkillsCopy(runtimeRoot, target, opts)));
@@ -691,7 +690,7 @@ async function installSkillTargetsCopy(runtimeRoot, targets, opts) {
691
690
 
692
691
  async function installSkillTargetsLink(repoAbs, targets, opts) {
693
692
  if (targets.length === 0) {
694
- if (!opts.quiet) process.stdout.write(" skills: no Claude, Codex, or OMP agent home found — skipped\n");
693
+ if (!opts.quiet) process.stdout.write(" skills: no Claude, Agent, or OMP agent home found — skipped\n");
695
694
  return [];
696
695
  }
697
696
  return await Promise.all(targets.map((target) => installSkillsLink(repoAbs, target, opts)));
@@ -15,6 +15,8 @@ const FALLBACK_SKILL_NAMES = [
15
15
  "okstra-memory",
16
16
  "okstra-inspect",
17
17
  "okstra-schedule",
18
+ "okstra-container-build",
19
+ "okstra-container",
18
20
  "okstra-context-loader",
19
21
  "okstra-team-contract",
20
22
  "okstra-convergence",
@@ -38,15 +40,15 @@ const FALLBACK_AGENT_NAMES = [
38
40
 
39
41
  const USER_HOME = homedir();
40
42
  const CLAUDE_HOME = join(USER_HOME, ".claude");
41
- const CODEX_HOME = join(USER_HOME, ".codex");
43
+ const AGENT_HOME = join(USER_HOME, ".agent");
42
44
  const OMP_AGENT_HOME = join(USER_HOME, ".omp", "agent");
43
45
  const CLAUDE_SKILLS_DIR = join(CLAUDE_HOME, "skills");
44
- const CODEX_SKILLS_DIR = join(CODEX_HOME, "skills");
46
+ const AGENT_SKILLS_DIR = join(AGENT_HOME, "skills");
45
47
  const OMP_SKILLS_DIR = join(OMP_AGENT_HOME, "skills");
46
48
  const CLAUDE_AGENTS_DIR = join(CLAUDE_HOME, "agents");
47
49
  const FALLBACK_SKILL_ROOTS = [
48
50
  CLAUDE_SKILLS_DIR,
49
- CODEX_SKILLS_DIR,
51
+ AGENT_SKILLS_DIR,
50
52
  OMP_SKILLS_DIR,
51
53
  ];
52
54
  const SKILLS_MANIFEST_REL = "installed-skills.json";
@@ -61,7 +63,7 @@ Usage:
61
63
  installed-skills.json, installed-agents.json} AND
62
64
  every skill target recorded in
63
65
  installed-skills.json
64
- (~/.claude/skills, ~/.codex/skills, or
66
+ (~/.claude/skills, ~/.agent/skills, or
65
67
  ~/.omp/agent/skills). Claude worker agents are
66
68
  removed from ~/.claude/agents when installed.
67
69
  Missing manifests fall back to hard-coded
@@ -73,7 +75,7 @@ Usage:
73
75
  (<PROJECT>/.claude/settings.local.json symlink)
74
76
  are NOT touched — remove them manually if needed.
75
77
  okstra uninstall --purge Remove the entire ~/.okstra directory AND
76
- okstra skills in Claude/Codex/OMP homes AND
78
+ okstra skills in Claude/Agent/OMP homes AND
77
79
  ~/.claude/agents/<*-worker.md> (DESTRUCTIVE).
78
80
  Requires -y or an interactive confirmation
79
81
  okstra uninstall --dry-run Print the plan without touching disk
@@ -137,7 +139,7 @@ export async function runUninstall(args) {
137
139
  if (opts.purge) {
138
140
  if (!opts.yes && !opts.dryRun) {
139
141
  const ok = await promptConfirm(
140
- `purge entire ${paths.home} AND okstra skills in Claude/Codex/OMP homes? user data will be lost.`,
142
+ `purge entire ${paths.home} AND okstra skills in Claude/Agent/OMP homes? user data will be lost.`,
141
143
  );
142
144
  if (!ok) {
143
145
  process.stdout.write("aborted.\n");
@@ -1,5 +1,5 @@
1
1
  // Single source of truth for which okstra skills are user-facing and which
2
- // former internal skill directories must be pruned from agent skill homes on
2
+ // former or renamed skill directories must be pruned from agent skill homes on
3
3
  // (re)install. The Claude plugin manifest (.claude-plugin/plugin.json) and the
4
4
  // installer both derive their skill list from here, so the public surface
5
5
  // cannot drift between discovery and install.
@@ -11,26 +11,27 @@ export const USER_SKILL_NAMES = Object.freeze([
11
11
  "okstra-memory",
12
12
  "okstra-inspect",
13
13
  "okstra-schedule",
14
- "okstra-container",
14
+ "okstra-container-build",
15
15
  ]);
16
16
 
17
- // Names okstra used to install as skills before lead/internal contracts moved
18
- // into the runtime resource tree (~/.okstra/prompts/). Install prunes these
19
- // exact directories from each target skill home so upgrades do not leave the
20
- // old internal skills behind. Exact-name only — never wildcard.
21
- export const OBSOLETE_INTERNAL_SKILL_NAMES = Object.freeze([
17
+ // Names okstra used to install as skills before they were renamed or moved
18
+ // into the runtime resource tree. Install prunes these exact directories from
19
+ // each target skill home so upgrades do not leave stale slash commands behind.
20
+ // Exact-name only — never wildcard.
21
+ export const OBSOLETE_SKILL_NAMES = Object.freeze([
22
22
  "okstra",
23
23
  "okstra-context-loader",
24
24
  "okstra-team-contract",
25
25
  "okstra-convergence",
26
26
  "okstra-report-writer",
27
27
  "okstra-coding-preflight",
28
+ "okstra-container",
28
29
  ]);
29
30
 
30
31
  export function userSkillNames() {
31
32
  return [...USER_SKILL_NAMES];
32
33
  }
33
34
 
34
- export function obsoleteInternalSkillNames() {
35
- return [...OBSOLETE_INTERNAL_SKILL_NAMES];
35
+ export function obsoleteSkillNames() {
36
+ return [...OBSOLETE_SKILL_NAMES];
36
37
  }