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.
- package/README.kr.md +5 -5
- package/README.md +5 -5
- package/docs/for-ai/README.md +2 -3
- package/docs/for-ai/skills/{okstra-container.md → okstra-container-build.md} +3 -4
- package/docs/for-ai/skills/okstra-inspect.md +1 -2
- package/docs/kr/architecture/storage-model.md +1 -2
- package/docs/kr/architecture.md +6 -6
- package/docs/kr/cli.md +3 -3
- package/docs/kr/container.md +1 -1
- package/docs/project-structure-overview.md +5 -5
- package/docs/superpowers/plans/2026-06-15-internal-skill-resource-migration.md +1 -1
- package/docs/superpowers/plans/2026-06-24-cross-project-precondition.md +542 -0
- package/docs/superpowers/plans/2026-06-24-decision-drafts-render-gap.md +568 -0
- package/docs/superpowers/plans/2026-06-24-manual-user-test-section.md +203 -0
- package/docs/superpowers/plans/2026-06-24-okstra-container-build-rename.md +525 -0
- package/docs/superpowers/specs/2026-06-24-cross-project-precondition-design.md +129 -0
- package/docs/superpowers/specs/2026-06-24-decision-drafts-render-gap-design.md +185 -0
- package/docs/superpowers/specs/2026-06-24-manual-user-test-section-design.md +121 -0
- package/docs/superpowers/specs/2026-06-24-okstra-container-build-rename-design.md +110 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +1 -0
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/lead/team-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-deliverable.md +2 -0
- package/runtime/prompts/profiles/final-verification.md +4 -3
- package/runtime/prompts/profiles/implementation-planning.md +12 -9
- package/runtime/python/okstra_ctl/run.py +0 -1
- package/runtime/python/okstra_token_usage/paths.py +1 -2
- package/runtime/schemas/final-report-v1.0.schema.json +106 -3
- package/runtime/skills/{okstra-container → okstra-container-build}/SKILL.md +3 -3
- package/runtime/templates/reports/final-report.template.md +63 -2
- package/runtime/templates/reports/final-verification-input.template.md +3 -3
- package/runtime/templates/reports/i18n/en.json +28 -1
- package/runtime/templates/reports/i18n/ko.json +28 -1
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-run.py +70 -1
- package/src/commands/lifecycle/install.mjs +17 -18
- package/src/commands/lifecycle/uninstall.mjs +8 -6
- 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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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"}/.
|
|
86
|
-
${"$HOME"}/.omp/agent/skills/<name> <- skills fallback when Claude/
|
|
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"}/.
|
|
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,
|
|
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 (
|
|
409
|
-
targets.push({ provider: "
|
|
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
|
|
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
|
|
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
|
|
667
|
-
//
|
|
668
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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, ~/.
|
|
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/
|
|
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/
|
|
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
|
|
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
|
|
18
|
-
// into the runtime resource tree
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
export const
|
|
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
|
|
35
|
-
return [...
|
|
35
|
+
export function obsoleteSkillNames() {
|
|
36
|
+
return [...OBSOLETE_SKILL_NAMES];
|
|
36
37
|
}
|