@kata-sh/cli 0.1.0 → 0.1.2
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/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +56 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +95 -0
- package/dist/resource-loader.d.ts +18 -0
- package/dist/resource-loader.js +50 -0
- package/dist/wizard.d.ts +15 -0
- package/dist/wizard.js +159 -0
- package/package.json +50 -21
- package/pkg/dist/modes/interactive/theme/dark.json +85 -0
- package/pkg/dist/modes/interactive/theme/light.json +84 -0
- package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.js +949 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
- package/pkg/package.json +8 -0
- package/scripts/postinstall.js +45 -0
- package/src/resources/AGENTS.md +108 -0
- package/src/resources/KATA-WORKFLOW.md +661 -0
- package/src/resources/agents/researcher.md +29 -0
- package/src/resources/agents/scout.md +56 -0
- package/src/resources/agents/worker.md +31 -0
- package/src/resources/extensions/ask-user-questions.ts +200 -0
- package/src/resources/extensions/bg-shell/index.ts +2758 -0
- package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/src/resources/extensions/browser-tools/core.js +1057 -0
- package/src/resources/extensions/browser-tools/index.ts +4916 -0
- package/src/resources/extensions/browser-tools/package.json +20 -0
- package/src/resources/extensions/context7/index.ts +428 -0
- package/src/resources/extensions/context7/package.json +11 -0
- package/src/resources/extensions/get-secrets-from-user.ts +352 -0
- package/src/resources/extensions/github/formatters.ts +207 -0
- package/src/resources/extensions/github/gh-api.ts +537 -0
- package/src/resources/extensions/github/index.ts +778 -0
- package/src/resources/extensions/kata/activity-log.ts +88 -0
- package/src/resources/extensions/kata/auto.ts +2786 -0
- package/src/resources/extensions/kata/commands.ts +355 -0
- package/src/resources/extensions/kata/crash-recovery.ts +85 -0
- package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
- package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
- package/src/resources/extensions/kata/doctor.ts +683 -0
- package/src/resources/extensions/kata/files.ts +730 -0
- package/src/resources/extensions/kata/gitignore.ts +165 -0
- package/src/resources/extensions/kata/guided-flow.ts +976 -0
- package/src/resources/extensions/kata/index.ts +556 -0
- package/src/resources/extensions/kata/metrics.ts +397 -0
- package/src/resources/extensions/kata/observability-validator.ts +408 -0
- package/src/resources/extensions/kata/package.json +11 -0
- package/src/resources/extensions/kata/paths.ts +346 -0
- package/src/resources/extensions/kata/preferences.ts +695 -0
- package/src/resources/extensions/kata/prompt-loader.ts +50 -0
- package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
- package/src/resources/extensions/kata/prompts/discuss.md +151 -0
- package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
- package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
- package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
- package/src/resources/extensions/kata/prompts/queue.md +85 -0
- package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
- package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
- package/src/resources/extensions/kata/prompts/system.md +341 -0
- package/src/resources/extensions/kata/session-forensics.ts +550 -0
- package/src/resources/extensions/kata/skill-discovery.ts +137 -0
- package/src/resources/extensions/kata/state.ts +509 -0
- package/src/resources/extensions/kata/templates/context.md +76 -0
- package/src/resources/extensions/kata/templates/decisions.md +8 -0
- package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/kata/templates/plan.md +133 -0
- package/src/resources/extensions/kata/templates/preferences.md +15 -0
- package/src/resources/extensions/kata/templates/project.md +31 -0
- package/src/resources/extensions/kata/templates/reassessment.md +28 -0
- package/src/resources/extensions/kata/templates/requirements.md +81 -0
- package/src/resources/extensions/kata/templates/research.md +46 -0
- package/src/resources/extensions/kata/templates/roadmap.md +118 -0
- package/src/resources/extensions/kata/templates/slice-context.md +58 -0
- package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
- package/src/resources/extensions/kata/templates/state.md +19 -0
- package/src/resources/extensions/kata/templates/task-plan.md +52 -0
- package/src/resources/extensions/kata/templates/task-summary.md +57 -0
- package/src/resources/extensions/kata/templates/uat.md +54 -0
- package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
- package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
- package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
- package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
- package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
- package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
- package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
- package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
- package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
- package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
- package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
- package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
- package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
- package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
- package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
- package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
- package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
- package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
- package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
- package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
- package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
- package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
- package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
- package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
- package/src/resources/extensions/kata/types.ts +159 -0
- package/src/resources/extensions/kata/unit-runtime.ts +163 -0
- package/src/resources/extensions/kata/workspace-index.ts +203 -0
- package/src/resources/extensions/kata/worktree.ts +182 -0
- package/src/resources/extensions/mac-tools/index.ts +852 -0
- package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
- package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
- package/src/resources/extensions/search-the-web/cache.ts +78 -0
- package/src/resources/extensions/search-the-web/format.ts +258 -0
- package/src/resources/extensions/search-the-web/http.ts +238 -0
- package/src/resources/extensions/search-the-web/index.ts +68 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +404 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +503 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/confirm-ui.ts +126 -0
- package/src/resources/extensions/shared/interview-ui.ts +822 -0
- package/src/resources/extensions/shared/next-action-ui.ts +235 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -0
- package/src/resources/extensions/shared/thinking-widget.ts +107 -0
- package/src/resources/extensions/shared/ui.ts +400 -0
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/slash-commands/audit.ts +92 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1293 -0
- package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
- package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
- package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
- package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
- package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
- package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
- package/src/resources/skills/frontend-design/SKILL.md +45 -0
- package/src/resources/skills/swiftui/SKILL.md +208 -0
- package/src/resources/skills/swiftui/references/animations.md +921 -0
- package/src/resources/skills/swiftui/references/architecture.md +1561 -0
- package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
- package/src/resources/skills/swiftui/references/navigation.md +1492 -0
- package/src/resources/skills/swiftui/references/networking-async.md +214 -0
- package/src/resources/skills/swiftui/references/performance.md +1706 -0
- package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
- package/src/resources/skills/swiftui/references/state-management.md +1443 -0
- package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
- package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
- package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
- package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
- package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
- package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
- package/dist/commands/task.d.ts +0 -9
- package/dist/commands/task.d.ts.map +0 -1
- package/dist/commands/task.js +0 -129
- package/dist/commands/task.js.map +0 -1
- package/dist/commands/task.test.d.ts +0 -2
- package/dist/commands/task.test.d.ts.map +0 -1
- package/dist/commands/task.test.js +0 -169
- package/dist/commands/task.test.js.map +0 -1
- package/dist/e2e/task-e2e.test.d.ts +0 -2
- package/dist/e2e/task-e2e.test.d.ts.map +0 -1
- package/dist/e2e/task-e2e.test.js +0 -173
- package/dist/e2e/task-e2e.test.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -93
- package/dist/index.js.map +0 -1
- package/dist/slug.d.ts +0 -2
- package/dist/slug.d.ts.map +0 -1
- package/dist/slug.js +0 -12
- package/dist/slug.js.map +0 -1
- package/dist/slug.test.d.ts +0 -2
- package/dist/slug.test.d.ts.map +0 -1
- package/dist/slug.test.js +0 -32
- package/dist/slug.test.js.map +0 -1
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
const GLOBAL_PREFERENCES_PATH = join(homedir(), ".kata-cli", "preferences.md");
|
|
7
|
+
const LEGACY_GLOBAL_PREFERENCES_PATH = join(
|
|
8
|
+
homedir(),
|
|
9
|
+
".pi",
|
|
10
|
+
"agent",
|
|
11
|
+
"kata-preferences.md",
|
|
12
|
+
);
|
|
13
|
+
const PROJECT_PREFERENCES_PATH = join(process.cwd(), ".kata", "preferences.md");
|
|
14
|
+
const SKILL_ACTIONS = new Set(["use", "prefer", "avoid"]);
|
|
15
|
+
|
|
16
|
+
export interface KataSkillRule {
|
|
17
|
+
when: string;
|
|
18
|
+
use?: string[];
|
|
19
|
+
prefer?: string[];
|
|
20
|
+
avoid?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface KataModelConfig {
|
|
24
|
+
research?: string; // e.g. "claude-sonnet-4-6"
|
|
25
|
+
planning?: string; // e.g. "claude-opus-4-6"
|
|
26
|
+
execution?: string; // e.g. "claude-sonnet-4-6"
|
|
27
|
+
completion?: string; // e.g. "claude-sonnet-4-6"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type SkillDiscoveryMode = "auto" | "suggest" | "off";
|
|
31
|
+
|
|
32
|
+
export interface AutoSupervisorConfig {
|
|
33
|
+
model?: string;
|
|
34
|
+
soft_timeout_minutes?: number;
|
|
35
|
+
idle_timeout_minutes?: number;
|
|
36
|
+
hard_timeout_minutes?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface KataPreferences {
|
|
40
|
+
version?: number;
|
|
41
|
+
always_use_skills?: string[];
|
|
42
|
+
prefer_skills?: string[];
|
|
43
|
+
avoid_skills?: string[];
|
|
44
|
+
skill_rules?: KataSkillRule[];
|
|
45
|
+
custom_instructions?: string[];
|
|
46
|
+
models?: KataModelConfig;
|
|
47
|
+
skill_discovery?: SkillDiscoveryMode;
|
|
48
|
+
auto_supervisor?: AutoSupervisorConfig;
|
|
49
|
+
uat_dispatch?: boolean;
|
|
50
|
+
budget_ceiling?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface LoadedKataPreferences {
|
|
54
|
+
path: string;
|
|
55
|
+
scope: "global" | "project";
|
|
56
|
+
preferences: KataPreferences;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getGlobalKataPreferencesPath(): string {
|
|
60
|
+
return GLOBAL_PREFERENCES_PATH;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getLegacyGlobalKataPreferencesPath(): string {
|
|
64
|
+
return LEGACY_GLOBAL_PREFERENCES_PATH;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getProjectKataPreferencesPath(): string {
|
|
68
|
+
return PROJECT_PREFERENCES_PATH;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function loadGlobalKataPreferences(): LoadedKataPreferences | null {
|
|
72
|
+
return (
|
|
73
|
+
loadPreferencesFile(GLOBAL_PREFERENCES_PATH, "global") ??
|
|
74
|
+
loadPreferencesFile(LEGACY_GLOBAL_PREFERENCES_PATH, "global")
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function loadProjectKataPreferences(): LoadedKataPreferences | null {
|
|
79
|
+
return loadPreferencesFile(PROJECT_PREFERENCES_PATH, "project");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function loadEffectiveKataPreferences(): LoadedKataPreferences | null {
|
|
83
|
+
const globalPreferences = loadGlobalKataPreferences();
|
|
84
|
+
const projectPreferences = loadProjectKataPreferences();
|
|
85
|
+
|
|
86
|
+
if (!globalPreferences && !projectPreferences) return null;
|
|
87
|
+
if (!globalPreferences) return projectPreferences;
|
|
88
|
+
if (!projectPreferences) return globalPreferences;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
path: projectPreferences.path,
|
|
92
|
+
scope: "project",
|
|
93
|
+
preferences: mergePreferences(
|
|
94
|
+
globalPreferences.preferences,
|
|
95
|
+
projectPreferences.preferences,
|
|
96
|
+
),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Skill Reference Resolution ───────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
export interface SkillResolution {
|
|
103
|
+
/** The original reference from preferences (bare name or path). */
|
|
104
|
+
original: string;
|
|
105
|
+
/** The resolved absolute path to the SKILL.md file, or null if unresolved. */
|
|
106
|
+
resolvedPath: string | null;
|
|
107
|
+
/** How it was resolved. */
|
|
108
|
+
method:
|
|
109
|
+
| "absolute-path"
|
|
110
|
+
| "absolute-dir"
|
|
111
|
+
| "user-skill"
|
|
112
|
+
| "project-skill"
|
|
113
|
+
| "unresolved";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface SkillResolutionReport {
|
|
117
|
+
/** All resolution results, keyed by original reference. */
|
|
118
|
+
resolutions: Map<string, SkillResolution>;
|
|
119
|
+
/** References that could not be resolved. */
|
|
120
|
+
warnings: string[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Known skill directories, in priority order.
|
|
125
|
+
* User skills (~/.kata-cli/agent/skills/) take precedence over project skills.
|
|
126
|
+
*/
|
|
127
|
+
function getSkillSearchDirs(
|
|
128
|
+
cwd: string,
|
|
129
|
+
): Array<{ dir: string; method: SkillResolution["method"] }> {
|
|
130
|
+
return [
|
|
131
|
+
{ dir: join(getAgentDir(), "skills"), method: "user-skill" },
|
|
132
|
+
{ dir: join(cwd, ".pi", "agent", "skills"), method: "project-skill" },
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resolve a single skill reference to an absolute path.
|
|
138
|
+
*
|
|
139
|
+
* Resolution order:
|
|
140
|
+
* 1. Absolute path to a file → check existsSync
|
|
141
|
+
* 2. Absolute path to a directory → check for SKILL.md inside
|
|
142
|
+
* 3. Bare name → scan known skill directories for <name>/SKILL.md
|
|
143
|
+
*/
|
|
144
|
+
function resolveSkillReference(ref: string, cwd: string): SkillResolution {
|
|
145
|
+
const trimmed = ref.trim();
|
|
146
|
+
|
|
147
|
+
// Expand tilde
|
|
148
|
+
const expanded = trimmed.startsWith("~/")
|
|
149
|
+
? join(homedir(), trimmed.slice(2))
|
|
150
|
+
: trimmed;
|
|
151
|
+
|
|
152
|
+
// Absolute path
|
|
153
|
+
if (isAbsolute(expanded)) {
|
|
154
|
+
// Direct file reference
|
|
155
|
+
if (existsSync(expanded)) {
|
|
156
|
+
// Check if it's a directory — look for SKILL.md inside
|
|
157
|
+
try {
|
|
158
|
+
const stat = statSync(expanded);
|
|
159
|
+
if (stat.isDirectory()) {
|
|
160
|
+
const skillFile = join(expanded, "SKILL.md");
|
|
161
|
+
if (existsSync(skillFile)) {
|
|
162
|
+
return {
|
|
163
|
+
original: ref,
|
|
164
|
+
resolvedPath: skillFile,
|
|
165
|
+
method: "absolute-dir",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return { original: ref, resolvedPath: null, method: "unresolved" };
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
/* fall through */
|
|
172
|
+
}
|
|
173
|
+
return { original: ref, resolvedPath: expanded, method: "absolute-path" };
|
|
174
|
+
}
|
|
175
|
+
// Maybe it's a directory path without SKILL.md suffix
|
|
176
|
+
const withSkillMd = join(expanded, "SKILL.md");
|
|
177
|
+
if (existsSync(withSkillMd)) {
|
|
178
|
+
return {
|
|
179
|
+
original: ref,
|
|
180
|
+
resolvedPath: withSkillMd,
|
|
181
|
+
method: "absolute-dir",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return { original: ref, resolvedPath: null, method: "unresolved" };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Bare name — scan known skill directories
|
|
188
|
+
for (const { dir, method } of getSkillSearchDirs(cwd)) {
|
|
189
|
+
if (!existsSync(dir)) continue;
|
|
190
|
+
try {
|
|
191
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
if (!entry.isDirectory()) continue;
|
|
194
|
+
if (entry.name === expanded) {
|
|
195
|
+
const skillFile = join(dir, entry.name, "SKILL.md");
|
|
196
|
+
if (existsSync(skillFile)) {
|
|
197
|
+
return { original: ref, resolvedPath: skillFile, method };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
/* directory not readable — skip */
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { original: ref, resolvedPath: null, method: "unresolved" };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Resolve all skill references in a preferences object.
|
|
211
|
+
* Caches resolution per reference string to avoid redundant filesystem scans.
|
|
212
|
+
*/
|
|
213
|
+
export function resolveAllSkillReferences(
|
|
214
|
+
preferences: KataPreferences,
|
|
215
|
+
cwd: string,
|
|
216
|
+
): SkillResolutionReport {
|
|
217
|
+
const validated = validatePreferences(preferences).preferences;
|
|
218
|
+
preferences = validated;
|
|
219
|
+
|
|
220
|
+
const resolutions = new Map<string, SkillResolution>();
|
|
221
|
+
const warnings: string[] = [];
|
|
222
|
+
|
|
223
|
+
function resolve(ref: string): SkillResolution {
|
|
224
|
+
const existing = resolutions.get(ref);
|
|
225
|
+
if (existing) return existing;
|
|
226
|
+
const result = resolveSkillReference(ref, cwd);
|
|
227
|
+
resolutions.set(ref, result);
|
|
228
|
+
if (result.method === "unresolved") {
|
|
229
|
+
warnings.push(ref);
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Resolve all skill lists
|
|
235
|
+
for (const skill of preferences.always_use_skills ?? []) resolve(skill);
|
|
236
|
+
for (const skill of preferences.prefer_skills ?? []) resolve(skill);
|
|
237
|
+
for (const skill of preferences.avoid_skills ?? []) resolve(skill);
|
|
238
|
+
|
|
239
|
+
// Resolve skill rules
|
|
240
|
+
for (const rule of preferences.skill_rules ?? []) {
|
|
241
|
+
for (const skill of rule.use ?? []) resolve(skill);
|
|
242
|
+
for (const skill of rule.prefer ?? []) resolve(skill);
|
|
243
|
+
for (const skill of rule.avoid ?? []) resolve(skill);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { resolutions, warnings };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Format a skill reference for the system prompt.
|
|
251
|
+
* If resolved, shows the path so the agent knows exactly where to read.
|
|
252
|
+
* If unresolved, marks it clearly.
|
|
253
|
+
*/
|
|
254
|
+
function formatSkillRef(
|
|
255
|
+
ref: string,
|
|
256
|
+
resolutions: Map<string, SkillResolution>,
|
|
257
|
+
): string {
|
|
258
|
+
const resolution = resolutions.get(ref);
|
|
259
|
+
if (!resolution || resolution.method === "unresolved") {
|
|
260
|
+
return `${ref} (⚠ not found — check skill name or path)`;
|
|
261
|
+
}
|
|
262
|
+
// For absolute paths where SKILL.md is just appended, don't clutter the output
|
|
263
|
+
if (
|
|
264
|
+
resolution.method === "absolute-path" ||
|
|
265
|
+
resolution.method === "absolute-dir"
|
|
266
|
+
) {
|
|
267
|
+
return ref;
|
|
268
|
+
}
|
|
269
|
+
// For bare names resolved from skill directories, show the resolved path
|
|
270
|
+
return `${ref} → \`${resolution.resolvedPath}\``;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ─── System Prompt Rendering ──────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
export function renderPreferencesForSystemPrompt(
|
|
276
|
+
preferences: KataPreferences,
|
|
277
|
+
resolutions?: Map<string, SkillResolution>,
|
|
278
|
+
): string {
|
|
279
|
+
const validated = validatePreferences(preferences);
|
|
280
|
+
const lines: string[] = ["## Kata Skill Preferences"];
|
|
281
|
+
|
|
282
|
+
if (validated.errors.length > 0) {
|
|
283
|
+
lines.push(
|
|
284
|
+
"- Validation: some preference values were ignored because they were invalid.",
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
preferences = validated.preferences;
|
|
289
|
+
|
|
290
|
+
lines.push(
|
|
291
|
+
"- Treat these as explicit skill-selection policy for Kata work.",
|
|
292
|
+
"- If a listed skill exists and is relevant, load and follow it instead of treating it as a vague suggestion.",
|
|
293
|
+
"- Current user instructions still override these defaults.",
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const fmt = (ref: string) =>
|
|
297
|
+
resolutions ? formatSkillRef(ref, resolutions) : ref;
|
|
298
|
+
|
|
299
|
+
if (
|
|
300
|
+
preferences.always_use_skills &&
|
|
301
|
+
preferences.always_use_skills.length > 0
|
|
302
|
+
) {
|
|
303
|
+
lines.push("- Always use these skills when relevant:");
|
|
304
|
+
for (const skill of preferences.always_use_skills) {
|
|
305
|
+
lines.push(` - ${fmt(skill)}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (preferences.prefer_skills && preferences.prefer_skills.length > 0) {
|
|
310
|
+
lines.push("- Prefer these skills when relevant:");
|
|
311
|
+
for (const skill of preferences.prefer_skills) {
|
|
312
|
+
lines.push(` - ${fmt(skill)}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (preferences.avoid_skills && preferences.avoid_skills.length > 0) {
|
|
317
|
+
lines.push("- Avoid these skills unless clearly needed:");
|
|
318
|
+
for (const skill of preferences.avoid_skills) {
|
|
319
|
+
lines.push(` - ${fmt(skill)}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (preferences.skill_rules && preferences.skill_rules.length > 0) {
|
|
324
|
+
lines.push("- Situational rules:");
|
|
325
|
+
for (const rule of preferences.skill_rules) {
|
|
326
|
+
lines.push(` - When ${rule.when}:`);
|
|
327
|
+
if (rule.use && rule.use.length > 0) {
|
|
328
|
+
lines.push(` - use: ${rule.use.map(fmt).join(", ")}`);
|
|
329
|
+
}
|
|
330
|
+
if (rule.prefer && rule.prefer.length > 0) {
|
|
331
|
+
lines.push(` - prefer: ${rule.prefer.map(fmt).join(", ")}`);
|
|
332
|
+
}
|
|
333
|
+
if (rule.avoid && rule.avoid.length > 0) {
|
|
334
|
+
lines.push(` - avoid: ${rule.avoid.map(fmt).join(", ")}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (
|
|
340
|
+
preferences.custom_instructions &&
|
|
341
|
+
preferences.custom_instructions.length > 0
|
|
342
|
+
) {
|
|
343
|
+
lines.push("- Additional instructions:");
|
|
344
|
+
for (const instruction of preferences.custom_instructions) {
|
|
345
|
+
lines.push(` - ${instruction}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return lines.join("\n");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function loadPreferencesFile(
|
|
353
|
+
path: string,
|
|
354
|
+
scope: "global" | "project",
|
|
355
|
+
): LoadedKataPreferences | null {
|
|
356
|
+
if (!existsSync(path)) return null;
|
|
357
|
+
|
|
358
|
+
const raw = readFileSync(path, "utf-8");
|
|
359
|
+
const preferences = parsePreferencesMarkdown(raw);
|
|
360
|
+
if (!preferences) return null;
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
path,
|
|
364
|
+
scope,
|
|
365
|
+
preferences,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function parsePreferencesMarkdown(content: string): KataPreferences | null {
|
|
370
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
371
|
+
if (!match) return null;
|
|
372
|
+
return parseFrontmatterBlock(match[1]);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function parseFrontmatterBlock(frontmatter: string): KataPreferences {
|
|
376
|
+
const root: Record<string, unknown> = {};
|
|
377
|
+
const stack: Array<{ indent: number; value: Record<string, unknown> }> = [
|
|
378
|
+
{ indent: -1, value: root },
|
|
379
|
+
];
|
|
380
|
+
|
|
381
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
382
|
+
for (let i = 0; i < lines.length; i++) {
|
|
383
|
+
const line = lines[i];
|
|
384
|
+
if (!line.trim()) continue;
|
|
385
|
+
|
|
386
|
+
const indent = line.match(/^\s*/)?.[0].length ?? 0;
|
|
387
|
+
const trimmed = line.trim();
|
|
388
|
+
|
|
389
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
|
|
390
|
+
stack.pop();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const current = stack[stack.length - 1].value;
|
|
394
|
+
const keyMatch = trimmed.match(/^([A-Za-z0-9_]+):(.*)$/);
|
|
395
|
+
if (!keyMatch) continue;
|
|
396
|
+
|
|
397
|
+
const [, key, remainder] = keyMatch;
|
|
398
|
+
const valuePart = remainder.trim();
|
|
399
|
+
|
|
400
|
+
if (valuePart === "") {
|
|
401
|
+
const nextLine = lines[i + 1] ?? "";
|
|
402
|
+
const nextTrimmed = nextLine.trim();
|
|
403
|
+
if (nextTrimmed.startsWith("- ")) {
|
|
404
|
+
const items: unknown[] = [];
|
|
405
|
+
let j = i + 1;
|
|
406
|
+
while (j < lines.length) {
|
|
407
|
+
const candidate = lines[j];
|
|
408
|
+
const candidateIndent = candidate.match(/^\s*/)?.[0].length ?? 0;
|
|
409
|
+
const candidateTrimmed = candidate.trim();
|
|
410
|
+
if (!candidateTrimmed) {
|
|
411
|
+
j++;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (candidateIndent <= indent || !candidateTrimmed.startsWith("- "))
|
|
415
|
+
break;
|
|
416
|
+
|
|
417
|
+
const itemText = candidateTrimmed.slice(2).trim();
|
|
418
|
+
const nextCandidate = lines[j + 1] ?? "";
|
|
419
|
+
const nextCandidateIndent =
|
|
420
|
+
nextCandidate.match(/^\s*/)?.[0].length ?? 0;
|
|
421
|
+
const nextCandidateTrimmed = nextCandidate.trim();
|
|
422
|
+
|
|
423
|
+
if (
|
|
424
|
+
itemText.includes(":") ||
|
|
425
|
+
(nextCandidateTrimmed && nextCandidateIndent > candidateIndent)
|
|
426
|
+
) {
|
|
427
|
+
const obj: Record<string, unknown> = {};
|
|
428
|
+
const firstMatch = itemText.match(/^([A-Za-z0-9_]+):(.*)$/);
|
|
429
|
+
if (firstMatch) {
|
|
430
|
+
obj[firstMatch[1]] = parseScalar(firstMatch[2].trim());
|
|
431
|
+
}
|
|
432
|
+
j++;
|
|
433
|
+
while (j < lines.length) {
|
|
434
|
+
const nested = lines[j];
|
|
435
|
+
const nestedIndent = nested.match(/^\s*/)?.[0].length ?? 0;
|
|
436
|
+
const nestedTrimmed = nested.trim();
|
|
437
|
+
if (!nestedTrimmed) {
|
|
438
|
+
j++;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (nestedIndent <= candidateIndent) break;
|
|
442
|
+
const nestedMatch = nestedTrimmed.match(/^([A-Za-z0-9_]+):(.*)$/);
|
|
443
|
+
if (nestedMatch) {
|
|
444
|
+
const nestedValue = nestedMatch[2].trim();
|
|
445
|
+
if (nestedValue === "") {
|
|
446
|
+
const nestedItems: string[] = [];
|
|
447
|
+
j++;
|
|
448
|
+
while (j < lines.length) {
|
|
449
|
+
const nestedArrayLine = lines[j];
|
|
450
|
+
const nestedArrayIndent =
|
|
451
|
+
nestedArrayLine.match(/^\s*/)?.[0].length ?? 0;
|
|
452
|
+
const nestedArrayTrimmed = nestedArrayLine.trim();
|
|
453
|
+
if (!nestedArrayTrimmed) {
|
|
454
|
+
j++;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (
|
|
458
|
+
nestedArrayIndent <= nestedIndent ||
|
|
459
|
+
!nestedArrayTrimmed.startsWith("- ")
|
|
460
|
+
)
|
|
461
|
+
break;
|
|
462
|
+
nestedItems.push(
|
|
463
|
+
String(parseScalar(nestedArrayTrimmed.slice(2).trim())),
|
|
464
|
+
);
|
|
465
|
+
j++;
|
|
466
|
+
}
|
|
467
|
+
obj[nestedMatch[1]] = nestedItems;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
obj[nestedMatch[1]] = parseScalar(nestedValue);
|
|
471
|
+
}
|
|
472
|
+
j++;
|
|
473
|
+
}
|
|
474
|
+
items.push(obj);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
items.push(parseScalar(itemText));
|
|
479
|
+
j++;
|
|
480
|
+
}
|
|
481
|
+
current[key] = items;
|
|
482
|
+
i = j - 1;
|
|
483
|
+
} else {
|
|
484
|
+
const obj: Record<string, unknown> = {};
|
|
485
|
+
current[key] = obj;
|
|
486
|
+
stack.push({ indent, value: obj });
|
|
487
|
+
}
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
current[key] = parseScalar(valuePart);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return root as KataPreferences;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function parseScalar(value: string): string | number | boolean {
|
|
498
|
+
if (value === "true") return true;
|
|
499
|
+
if (value === "false") return false;
|
|
500
|
+
if (/^-?\d+$/.test(value)) return Number(value);
|
|
501
|
+
return value.replace(/^['\"]|['\"]$/g, "");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Resolve the skill discovery mode from effective preferences.
|
|
506
|
+
* Defaults to "suggest" — skills are identified during research but not installed automatically.
|
|
507
|
+
*/
|
|
508
|
+
export function resolveSkillDiscoveryMode(): SkillDiscoveryMode {
|
|
509
|
+
const prefs = loadEffectiveKataPreferences();
|
|
510
|
+
return prefs?.preferences.skill_discovery ?? "suggest";
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Resolve which model ID to use for a given auto-mode unit type.
|
|
515
|
+
* Returns undefined if no model preference is set for this unit type.
|
|
516
|
+
*/
|
|
517
|
+
export function resolveModelForUnit(unitType: string): string | undefined {
|
|
518
|
+
const prefs = loadEffectiveKataPreferences();
|
|
519
|
+
if (!prefs?.preferences.models) return undefined;
|
|
520
|
+
const m = prefs.preferences.models;
|
|
521
|
+
|
|
522
|
+
switch (unitType) {
|
|
523
|
+
case "research-milestone":
|
|
524
|
+
case "research-slice":
|
|
525
|
+
return m.research;
|
|
526
|
+
case "plan-milestone":
|
|
527
|
+
case "plan-slice":
|
|
528
|
+
case "replan-slice":
|
|
529
|
+
return m.planning;
|
|
530
|
+
case "execute-task":
|
|
531
|
+
return m.execution;
|
|
532
|
+
case "complete-slice":
|
|
533
|
+
case "run-uat":
|
|
534
|
+
return m.completion;
|
|
535
|
+
default:
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export function resolveAutoSupervisorConfig(): AutoSupervisorConfig {
|
|
541
|
+
const prefs = loadEffectiveKataPreferences();
|
|
542
|
+
const configured = prefs?.preferences.auto_supervisor ?? {};
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
soft_timeout_minutes: configured.soft_timeout_minutes ?? 20,
|
|
546
|
+
idle_timeout_minutes: configured.idle_timeout_minutes ?? 10,
|
|
547
|
+
hard_timeout_minutes: configured.hard_timeout_minutes ?? 30,
|
|
548
|
+
...(configured.model ? { model: configured.model } : {}),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function mergePreferences(
|
|
553
|
+
base: KataPreferences,
|
|
554
|
+
override: KataPreferences,
|
|
555
|
+
): KataPreferences {
|
|
556
|
+
return {
|
|
557
|
+
version: override.version ?? base.version,
|
|
558
|
+
always_use_skills: mergeStringLists(
|
|
559
|
+
base.always_use_skills,
|
|
560
|
+
override.always_use_skills,
|
|
561
|
+
),
|
|
562
|
+
prefer_skills: mergeStringLists(base.prefer_skills, override.prefer_skills),
|
|
563
|
+
avoid_skills: mergeStringLists(base.avoid_skills, override.avoid_skills),
|
|
564
|
+
skill_rules: [...(base.skill_rules ?? []), ...(override.skill_rules ?? [])],
|
|
565
|
+
custom_instructions: mergeStringLists(
|
|
566
|
+
base.custom_instructions,
|
|
567
|
+
override.custom_instructions,
|
|
568
|
+
),
|
|
569
|
+
models: { ...(base.models ?? {}), ...(override.models ?? {}) },
|
|
570
|
+
skill_discovery: override.skill_discovery ?? base.skill_discovery,
|
|
571
|
+
auto_supervisor: {
|
|
572
|
+
...(base.auto_supervisor ?? {}),
|
|
573
|
+
...(override.auto_supervisor ?? {}),
|
|
574
|
+
},
|
|
575
|
+
uat_dispatch: override.uat_dispatch ?? base.uat_dispatch,
|
|
576
|
+
budget_ceiling: override.budget_ceiling ?? base.budget_ceiling,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function validatePreferences(preferences: KataPreferences): {
|
|
581
|
+
preferences: KataPreferences;
|
|
582
|
+
errors: string[];
|
|
583
|
+
} {
|
|
584
|
+
const errors: string[] = [];
|
|
585
|
+
const validated: KataPreferences = {};
|
|
586
|
+
|
|
587
|
+
if (preferences.version !== undefined) {
|
|
588
|
+
if (preferences.version === 1) {
|
|
589
|
+
validated.version = 1;
|
|
590
|
+
} else {
|
|
591
|
+
errors.push(`unsupported version ${preferences.version}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const validDiscoveryModes = new Set(["auto", "suggest", "off"]);
|
|
596
|
+
if (preferences.skill_discovery) {
|
|
597
|
+
if (validDiscoveryModes.has(preferences.skill_discovery)) {
|
|
598
|
+
validated.skill_discovery = preferences.skill_discovery;
|
|
599
|
+
} else {
|
|
600
|
+
errors.push(
|
|
601
|
+
`invalid skill_discovery value: ${preferences.skill_discovery}`,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
validated.always_use_skills = normalizeStringList(
|
|
607
|
+
preferences.always_use_skills,
|
|
608
|
+
);
|
|
609
|
+
validated.prefer_skills = normalizeStringList(preferences.prefer_skills);
|
|
610
|
+
validated.avoid_skills = normalizeStringList(preferences.avoid_skills);
|
|
611
|
+
validated.custom_instructions = normalizeStringList(
|
|
612
|
+
preferences.custom_instructions,
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
if (preferences.skill_rules) {
|
|
616
|
+
const validRules: KataSkillRule[] = [];
|
|
617
|
+
for (const rule of preferences.skill_rules) {
|
|
618
|
+
if (!rule || typeof rule !== "object") {
|
|
619
|
+
errors.push("invalid skill_rules entry");
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
const when = typeof rule.when === "string" ? rule.when.trim() : "";
|
|
623
|
+
if (!when) {
|
|
624
|
+
errors.push("skill_rules entry missing when");
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
const validatedRule: KataSkillRule = { when };
|
|
628
|
+
for (const action of SKILL_ACTIONS) {
|
|
629
|
+
const values = normalizeStringList(
|
|
630
|
+
(rule as Record<string, unknown>)[action],
|
|
631
|
+
);
|
|
632
|
+
if (values.length > 0) {
|
|
633
|
+
validatedRule[action as keyof KataSkillRule] = values as never;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (!validatedRule.use && !validatedRule.prefer && !validatedRule.avoid) {
|
|
637
|
+
errors.push(`skill rule has no actions: ${when}`);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
validRules.push(validatedRule);
|
|
641
|
+
}
|
|
642
|
+
if (validRules.length > 0) {
|
|
643
|
+
validated.skill_rules = validRules;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
for (const key of [
|
|
648
|
+
"always_use_skills",
|
|
649
|
+
"prefer_skills",
|
|
650
|
+
"avoid_skills",
|
|
651
|
+
"custom_instructions",
|
|
652
|
+
] as const) {
|
|
653
|
+
if (validated[key] && validated[key]!.length === 0) {
|
|
654
|
+
delete validated[key];
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (preferences.uat_dispatch !== undefined) {
|
|
659
|
+
validated.uat_dispatch = !!preferences.uat_dispatch;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (preferences.budget_ceiling !== undefined) {
|
|
663
|
+
const raw = preferences.budget_ceiling;
|
|
664
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
665
|
+
validated.budget_ceiling = raw;
|
|
666
|
+
} else if (typeof raw === "string" && Number.isFinite(Number(raw))) {
|
|
667
|
+
validated.budget_ceiling = Number(raw);
|
|
668
|
+
} else {
|
|
669
|
+
errors.push("budget_ceiling must be a finite number");
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return { preferences: validated, errors };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function mergeStringLists(
|
|
677
|
+
base?: unknown,
|
|
678
|
+
override?: unknown,
|
|
679
|
+
): string[] | undefined {
|
|
680
|
+
const merged = [
|
|
681
|
+
...normalizeStringList(base),
|
|
682
|
+
...normalizeStringList(override),
|
|
683
|
+
]
|
|
684
|
+
.map((item) => item.trim())
|
|
685
|
+
.filter(Boolean);
|
|
686
|
+
return merged.length > 0 ? Array.from(new Set(merged)) : undefined;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function normalizeStringList(value: unknown): string[] {
|
|
690
|
+
if (!Array.isArray(value)) return [];
|
|
691
|
+
return value
|
|
692
|
+
.filter((item): item is string => typeof item === "string")
|
|
693
|
+
.map((item) => item.trim())
|
|
694
|
+
.filter(Boolean);
|
|
695
|
+
}
|