@chllming/wave-orchestration 0.5.3 → 0.5.4

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 (79) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +56 -509
  3. package/docs/README.md +39 -0
  4. package/docs/concepts/context7-vs-skills.md +94 -0
  5. package/docs/concepts/operating-modes.md +91 -0
  6. package/docs/concepts/runtime-agnostic-orchestration.md +95 -0
  7. package/docs/concepts/what-is-a-wave.md +133 -0
  8. package/docs/guides/planner.md +113 -0
  9. package/docs/guides/terminal-surfaces.md +80 -0
  10. package/docs/image.png +0 -0
  11. package/docs/plans/context7-wave-orchestrator.md +2 -0
  12. package/docs/plans/current-state.md +10 -0
  13. package/docs/plans/master-plan.md +3 -3
  14. package/docs/plans/migration.md +4 -3
  15. package/docs/plans/wave-orchestrator.md +27 -3
  16. package/docs/reference/runtime-config/README.md +19 -0
  17. package/docs/reference/skills.md +156 -0
  18. package/docs/roadmap.md +160 -564
  19. package/package.json +2 -1
  20. package/releases/manifest.json +17 -0
  21. package/scripts/wave-orchestrator/config.mjs +17 -0
  22. package/scripts/wave-orchestrator/context7.mjs +9 -0
  23. package/scripts/wave-orchestrator/coordination.mjs +16 -0
  24. package/scripts/wave-orchestrator/executors.mjs +24 -11
  25. package/scripts/wave-orchestrator/install.mjs +41 -2
  26. package/scripts/wave-orchestrator/launcher.mjs +113 -20
  27. package/scripts/wave-orchestrator/planner.mjs +1328 -0
  28. package/scripts/wave-orchestrator/project-profile.mjs +190 -0
  29. package/scripts/wave-orchestrator/shared.mjs +2 -0
  30. package/scripts/wave-orchestrator/skills.mjs +448 -0
  31. package/scripts/wave-orchestrator/terminals.mjs +16 -0
  32. package/scripts/wave-orchestrator/traces.mjs +23 -0
  33. package/scripts/wave-orchestrator/wave-files.mjs +299 -84
  34. package/scripts/wave.mjs +11 -0
  35. package/skills/provider-aws/SKILL.md +6 -0
  36. package/skills/provider-aws/skill.json +5 -0
  37. package/skills/provider-custom-deploy/SKILL.md +5 -0
  38. package/skills/provider-custom-deploy/skill.json +5 -0
  39. package/skills/provider-docker-compose/SKILL.md +6 -0
  40. package/skills/provider-docker-compose/skill.json +5 -0
  41. package/skills/provider-github-release/SKILL.md +6 -0
  42. package/skills/provider-github-release/skill.json +5 -0
  43. package/skills/provider-kubernetes/SKILL.md +6 -0
  44. package/skills/provider-kubernetes/skill.json +5 -0
  45. package/skills/provider-railway/SKILL.md +6 -0
  46. package/skills/provider-railway/adapters/claude.md +1 -0
  47. package/skills/provider-railway/adapters/codex.md +1 -0
  48. package/skills/provider-railway/adapters/local.md +1 -0
  49. package/skills/provider-railway/adapters/opencode.md +1 -0
  50. package/skills/provider-railway/skill.json +5 -0
  51. package/skills/provider-ssh-manual/SKILL.md +6 -0
  52. package/skills/provider-ssh-manual/skill.json +5 -0
  53. package/skills/repo-coding-rules/SKILL.md +7 -0
  54. package/skills/repo-coding-rules/skill.json +5 -0
  55. package/skills/role-deploy/SKILL.md +6 -0
  56. package/skills/role-deploy/skill.json +5 -0
  57. package/skills/role-documentation/SKILL.md +6 -0
  58. package/skills/role-documentation/skill.json +5 -0
  59. package/skills/role-evaluator/SKILL.md +6 -0
  60. package/skills/role-evaluator/skill.json +5 -0
  61. package/skills/role-implementation/SKILL.md +6 -0
  62. package/skills/role-implementation/skill.json +5 -0
  63. package/skills/role-infra/SKILL.md +6 -0
  64. package/skills/role-infra/skill.json +5 -0
  65. package/skills/role-integration/SKILL.md +6 -0
  66. package/skills/role-integration/skill.json +5 -0
  67. package/skills/role-research/SKILL.md +6 -0
  68. package/skills/role-research/skill.json +5 -0
  69. package/skills/runtime-claude/SKILL.md +6 -0
  70. package/skills/runtime-claude/skill.json +5 -0
  71. package/skills/runtime-codex/SKILL.md +6 -0
  72. package/skills/runtime-codex/skill.json +5 -0
  73. package/skills/runtime-local/SKILL.md +5 -0
  74. package/skills/runtime-local/skill.json +5 -0
  75. package/skills/runtime-opencode/SKILL.md +6 -0
  76. package/skills/runtime-opencode/skill.json +5 -0
  77. package/skills/wave-core/SKILL.md +7 -0
  78. package/skills/wave-core/skill.json +5 -0
  79. package/wave.config.json +27 -0
@@ -0,0 +1,190 @@
1
+ import path from "node:path";
2
+ import { loadWaveConfig } from "./config.mjs";
3
+ import { REPO_ROOT, ensureDirectory, readJsonOrNull, writeJsonAtomic } from "./shared.mjs";
4
+ import { normalizeTerminalSurface } from "./terminals.mjs";
5
+
6
+ export const PROJECT_PROFILE_SCHEMA_VERSION = 1;
7
+ export const PROJECT_PROFILE_PATH = path.join(REPO_ROOT, ".wave", "project-profile.json");
8
+ export const PROJECT_OVERSIGHT_MODES = ["oversight", "dark-factory"];
9
+ export const PROJECT_PROFILE_TERMINAL_SURFACES = ["vscode", "tmux"];
10
+ export const DEPLOY_ENVIRONMENT_KINDS = [
11
+ "railway-mcp",
12
+ "railway-cli",
13
+ "docker-compose",
14
+ "kubernetes",
15
+ "ssh-manual",
16
+ "custom",
17
+ ];
18
+ export const DRAFT_TEMPLATES = ["implementation", "qa", "infra", "release"];
19
+
20
+ function cleanText(value) {
21
+ return String(value ?? "").trim();
22
+ }
23
+
24
+ function normalizePathForProfile(value) {
25
+ const normalized = cleanText(value).replaceAll("\\", "/");
26
+ if (!normalized) {
27
+ return null;
28
+ }
29
+ return path.isAbsolute(normalized) ? path.relative(REPO_ROOT, normalized) : normalized;
30
+ }
31
+
32
+ function normalizeProjectTerminalSurface(value, label = "defaultTerminalSurface") {
33
+ const normalized = normalizeTerminalSurface(value, label);
34
+ return normalized === "none" ? "vscode" : normalized;
35
+ }
36
+
37
+ export function normalizeOversightMode(value, label = "oversight mode") {
38
+ const normalized = cleanText(value).toLowerCase();
39
+ if (!PROJECT_OVERSIGHT_MODES.includes(normalized)) {
40
+ throw new Error(`${label} must be one of: ${PROJECT_OVERSIGHT_MODES.join(", ")}`);
41
+ }
42
+ return normalized;
43
+ }
44
+
45
+ export function normalizeDraftTemplate(value, label = "draft template") {
46
+ const normalized = cleanText(value).toLowerCase();
47
+ if (!DRAFT_TEMPLATES.includes(normalized)) {
48
+ throw new Error(`${label} must be one of: ${DRAFT_TEMPLATES.join(", ")}`);
49
+ }
50
+ return normalized;
51
+ }
52
+
53
+ function normalizeDeployEnvironment(rawEnvironment, index) {
54
+ if (!rawEnvironment || typeof rawEnvironment !== "object" || Array.isArray(rawEnvironment)) {
55
+ throw new Error(`deployEnvironments[${index}] must be an object`);
56
+ }
57
+ const id = cleanText(rawEnvironment.id).toLowerCase();
58
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(id)) {
59
+ throw new Error(`deployEnvironments[${index}].id must be a lowercase repo-safe identifier`);
60
+ }
61
+ const kind = cleanText(rawEnvironment.kind).toLowerCase();
62
+ if (!DEPLOY_ENVIRONMENT_KINDS.includes(kind)) {
63
+ throw new Error(
64
+ `deployEnvironments[${index}].kind must be one of: ${DEPLOY_ENVIRONMENT_KINDS.join(", ")}`,
65
+ );
66
+ }
67
+ return {
68
+ id,
69
+ name: cleanText(rawEnvironment.name) || id,
70
+ kind,
71
+ isDefault: rawEnvironment.isDefault === true,
72
+ notes: cleanText(rawEnvironment.notes) || null,
73
+ };
74
+ }
75
+
76
+ export function buildDefaultProjectProfile(config = loadWaveConfig()) {
77
+ const now = new Date().toISOString();
78
+ return {
79
+ schemaVersion: PROJECT_PROFILE_SCHEMA_VERSION,
80
+ initializedAt: now,
81
+ updatedAt: now,
82
+ newProject: false,
83
+ defaultOversightMode: "oversight",
84
+ defaultTerminalSurface: "vscode",
85
+ deployEnvironments: [],
86
+ plannerDefaults: {
87
+ template: "implementation",
88
+ lane: cleanText(config.defaultLane) || "main",
89
+ },
90
+ source: {
91
+ projectName: cleanText(config.projectName) || "Wave Orchestrator",
92
+ configPath: normalizePathForProfile(config.configPath || "wave.config.json"),
93
+ },
94
+ };
95
+ }
96
+
97
+ export function normalizeProjectProfile(rawProfile, options = {}) {
98
+ if (!rawProfile || typeof rawProfile !== "object" || Array.isArray(rawProfile)) {
99
+ throw new Error(`Project profile is invalid: ${path.relative(REPO_ROOT, PROJECT_PROFILE_PATH)}`);
100
+ }
101
+ const config = options.config || loadWaveConfig();
102
+ const base = buildDefaultProjectProfile(config);
103
+ const deployEnvironments = Array.isArray(rawProfile.deployEnvironments)
104
+ ? rawProfile.deployEnvironments.map((entry, index) => normalizeDeployEnvironment(entry, index))
105
+ : [];
106
+ const defaultEnvironmentIndex = deployEnvironments.findIndex((entry) => entry.isDefault);
107
+ if (defaultEnvironmentIndex === -1 && deployEnvironments.length > 0) {
108
+ deployEnvironments[0].isDefault = true;
109
+ } else if (defaultEnvironmentIndex > -1) {
110
+ deployEnvironments.forEach((entry, index) => {
111
+ entry.isDefault = index === defaultEnvironmentIndex;
112
+ });
113
+ }
114
+ const plannerDefaults =
115
+ rawProfile.plannerDefaults && typeof rawProfile.plannerDefaults === "object"
116
+ ? rawProfile.plannerDefaults
117
+ : {};
118
+ return {
119
+ schemaVersion: PROJECT_PROFILE_SCHEMA_VERSION,
120
+ initializedAt: cleanText(rawProfile.initializedAt) || base.initializedAt,
121
+ updatedAt: cleanText(rawProfile.updatedAt) || base.updatedAt,
122
+ newProject: rawProfile.newProject === true,
123
+ defaultOversightMode: normalizeOversightMode(
124
+ rawProfile.defaultOversightMode || base.defaultOversightMode,
125
+ "defaultOversightMode",
126
+ ),
127
+ defaultTerminalSurface: normalizeProjectTerminalSurface(
128
+ rawProfile.defaultTerminalSurface || base.defaultTerminalSurface,
129
+ "defaultTerminalSurface",
130
+ ),
131
+ deployEnvironments,
132
+ plannerDefaults: {
133
+ template: normalizeDraftTemplate(
134
+ plannerDefaults.template || base.plannerDefaults.template,
135
+ "plannerDefaults.template",
136
+ ),
137
+ lane: cleanText(plannerDefaults.lane) || base.plannerDefaults.lane,
138
+ },
139
+ source: {
140
+ projectName: cleanText(rawProfile.source?.projectName) || base.source.projectName,
141
+ configPath: normalizePathForProfile(rawProfile.source?.configPath) || base.source.configPath,
142
+ },
143
+ };
144
+ }
145
+
146
+ export function readProjectProfile(options = {}) {
147
+ const payload = readJsonOrNull(PROJECT_PROFILE_PATH);
148
+ if (!payload) {
149
+ return null;
150
+ }
151
+ return normalizeProjectProfile(payload, options);
152
+ }
153
+
154
+ export function writeProjectProfile(profile, options = {}) {
155
+ const config = options.config || loadWaveConfig();
156
+ const now = new Date().toISOString();
157
+ const normalized = normalizeProjectProfile(
158
+ {
159
+ ...profile,
160
+ initializedAt: profile?.initializedAt || now,
161
+ updatedAt: now,
162
+ source: {
163
+ projectName: profile?.source?.projectName || config.projectName,
164
+ configPath: normalizePathForProfile(config.configPath || "wave.config.json"),
165
+ },
166
+ },
167
+ { config },
168
+ );
169
+ ensureDirectory(path.dirname(PROJECT_PROFILE_PATH));
170
+ writeJsonAtomic(PROJECT_PROFILE_PATH, normalized);
171
+ return normalized;
172
+ }
173
+
174
+ export function updateProjectProfile(mutator, options = {}) {
175
+ const config = options.config || loadWaveConfig();
176
+ const current = readProjectProfile({ config }) || buildDefaultProjectProfile(config);
177
+ const next = typeof mutator === "function" ? mutator(current) : { ...current, ...(mutator || {}) };
178
+ return writeProjectProfile(
179
+ {
180
+ ...current,
181
+ ...(next || {}),
182
+ initializedAt: current.initializedAt,
183
+ },
184
+ { config },
185
+ );
186
+ }
187
+
188
+ export function resolveDefaultTerminalSurface(profile) {
189
+ return normalizeProjectTerminalSurface(profile?.defaultTerminalSurface || "vscode");
190
+ }
@@ -128,6 +128,7 @@ export function buildLanePaths(laneInput = DEFAULT_WAVE_LANE, options = {}) {
128
128
  executorOverlaysDir: path.join(stateDir, "executors"),
129
129
  stateDir,
130
130
  terminalsPath: path.join(REPO_ROOT, laneProfile.paths.terminalsPath),
131
+ skillsDir: path.join(REPO_ROOT, laneProfile.skills?.dir || "skills"),
131
132
  context7BundleIndexPath: path.join(REPO_ROOT, laneProfile.paths.context7BundleIndexPath),
132
133
  componentCutoverMatrixDocPath: path.join(
133
134
  REPO_ROOT,
@@ -157,6 +158,7 @@ export function buildLanePaths(laneInput = DEFAULT_WAVE_LANE, options = {}) {
157
158
  laneProfile.validation.requireComponentPromotionsFromWave,
158
159
  requireAgentComponentsFromWave: laneProfile.validation.requireAgentComponentsFromWave,
159
160
  executors: laneProfile.executors,
161
+ skills: laneProfile.skills,
160
162
  capabilityRouting: laneProfile.capabilityRouting,
161
163
  defaultManifestPath: path.join(stateDir, "waves.manifest.json"),
162
164
  defaultRunStatePath: path.join(stateDir, "run-state.json"),
@@ -0,0 +1,448 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { WORKSPACE_ROOT } from "./roots.mjs";
5
+
6
+ const REPO_ROOT = WORKSPACE_ROOT;
7
+
8
+ export const DEFAULT_SKILLS_DIR = "skills";
9
+ export const SKILL_ID_REGEX = /^[a-z0-9][a-z0-9._-]*$/;
10
+ export const SUPPORTED_SKILL_RUNTIMES = ["codex", "claude", "opencode", "local"];
11
+
12
+ function cleanText(value) {
13
+ return String(value ?? "").trim();
14
+ }
15
+
16
+ function hashText(value) {
17
+ return crypto.createHash("sha256").update(String(value ?? ""), "utf8").digest("hex");
18
+ }
19
+
20
+ function hashBuffer(value) {
21
+ return crypto.createHash("sha256").update(value).digest("hex");
22
+ }
23
+
24
+ function ensureDirectory(dirPath) {
25
+ fs.mkdirSync(dirPath, { recursive: true });
26
+ }
27
+
28
+ function writeTextAtomic(filePath, text) {
29
+ ensureDirectory(path.dirname(filePath));
30
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
31
+ fs.writeFileSync(tempPath, text, "utf8");
32
+ fs.renameSync(tempPath, filePath);
33
+ }
34
+
35
+ function writeJsonAtomic(filePath, payload) {
36
+ writeTextAtomic(filePath, `${JSON.stringify(payload, null, 2)}\n`);
37
+ }
38
+
39
+ function normalizeRepoRelativePath(value, label) {
40
+ const raw = cleanText(value)
41
+ .replaceAll("\\", "/")
42
+ .replace(/^\.\/+/, "")
43
+ .replace(/\/+/g, "/")
44
+ .replace(/\/$/, "");
45
+ if (!raw) {
46
+ throw new Error(`${label} is required`);
47
+ }
48
+ if (raw.startsWith("/") || raw.startsWith("../") || raw.includes("/../")) {
49
+ throw new Error(`${label} must stay within the repository: ${value}`);
50
+ }
51
+ return raw;
52
+ }
53
+
54
+ function uniqueStrings(values) {
55
+ return Array.from(
56
+ new Set(
57
+ (Array.isArray(values) ? values : [])
58
+ .map((value) => cleanText(value))
59
+ .filter(Boolean),
60
+ ),
61
+ );
62
+ }
63
+
64
+ export function normalizeSkillId(value, label = "skill id") {
65
+ const normalized = cleanText(value).toLowerCase();
66
+ if (!SKILL_ID_REGEX.test(normalized)) {
67
+ throw new Error(`${label} must match ${SKILL_ID_REGEX}`);
68
+ }
69
+ return normalized;
70
+ }
71
+
72
+ export function normalizeSkillIdArray(values, label = "skills") {
73
+ if (values === undefined || values === null || values === "") {
74
+ return [];
75
+ }
76
+ if (!Array.isArray(values)) {
77
+ throw new Error(`${label} must be an array`);
78
+ }
79
+ return uniqueStrings(
80
+ values.map((value, index) => normalizeSkillId(value, `${label}[${index}]`)),
81
+ );
82
+ }
83
+
84
+ function normalizeSkillMap(rawMap = {}, label) {
85
+ if (!rawMap || typeof rawMap !== "object" || Array.isArray(rawMap)) {
86
+ return {};
87
+ }
88
+ return Object.fromEntries(
89
+ Object.entries(rawMap).map(([key, values]) => [
90
+ cleanText(key).toLowerCase(),
91
+ normalizeSkillIdArray(values, `${label}.${key}`),
92
+ ]),
93
+ );
94
+ }
95
+
96
+ export function emptySkillsConfig() {
97
+ return {
98
+ dir: DEFAULT_SKILLS_DIR,
99
+ base: [],
100
+ byRole: {},
101
+ byRuntime: {},
102
+ byDeployKind: {},
103
+ };
104
+ }
105
+
106
+ export function normalizeSkillsConfig(rawSkills = {}, label = "skills", options = {}) {
107
+ const skills =
108
+ rawSkills && typeof rawSkills === "object" && !Array.isArray(rawSkills) ? rawSkills : {};
109
+ const dir =
110
+ Object.prototype.hasOwnProperty.call(skills, "dir")
111
+ ? normalizeRepoRelativePath(skills.dir || DEFAULT_SKILLS_DIR, `${label}.dir`)
112
+ : options.preserveOmittedDir
113
+ ? null
114
+ : DEFAULT_SKILLS_DIR;
115
+ return {
116
+ dir,
117
+ base: normalizeSkillIdArray(skills.base, `${label}.base`),
118
+ byRole: normalizeSkillMap(skills.byRole, `${label}.byRole`),
119
+ byRuntime: normalizeSkillMap(skills.byRuntime, `${label}.byRuntime`),
120
+ byDeployKind: normalizeSkillMap(skills.byDeployKind, `${label}.byDeployKind`),
121
+ };
122
+ }
123
+
124
+ function mergeSkillMaps(baseMap = {}, overrideMap = {}) {
125
+ return Object.fromEntries(
126
+ Array.from(new Set([...Object.keys(baseMap || {}), ...Object.keys(overrideMap || {})]))
127
+ .sort((left, right) => left.localeCompare(right))
128
+ .map((key) => [key, uniqueStrings([...(baseMap[key] || []), ...(overrideMap[key] || [])])]),
129
+ );
130
+ }
131
+
132
+ export function mergeSkillsConfig(baseSkills = emptySkillsConfig(), overrideSkills = emptySkillsConfig()) {
133
+ return {
134
+ dir: overrideSkills.dir || baseSkills.dir || DEFAULT_SKILLS_DIR,
135
+ base: uniqueStrings([...(baseSkills.base || []), ...(overrideSkills.base || [])]).map((skillId) =>
136
+ normalizeSkillId(skillId),
137
+ ),
138
+ byRole: mergeSkillMaps(baseSkills.byRole, overrideSkills.byRole),
139
+ byRuntime: mergeSkillMaps(baseSkills.byRuntime, overrideSkills.byRuntime),
140
+ byDeployKind: mergeSkillMaps(baseSkills.byDeployKind, overrideSkills.byDeployKind),
141
+ };
142
+ }
143
+
144
+ function listFilesRecursively(rootDir) {
145
+ if (!fs.existsSync(rootDir)) {
146
+ return [];
147
+ }
148
+ const files = [];
149
+ const visit = (targetDir) => {
150
+ for (const entry of fs.readdirSync(targetDir, { withFileTypes: true })) {
151
+ const fullPath = path.join(targetDir, entry.name);
152
+ if (entry.isDirectory()) {
153
+ visit(fullPath);
154
+ } else {
155
+ files.push(fullPath);
156
+ }
157
+ }
158
+ };
159
+ visit(rootDir);
160
+ return files.toSorted();
161
+ }
162
+
163
+ function repoRelativePath(filePath) {
164
+ return path.relative(REPO_ROOT, filePath).replaceAll(path.sep, "/");
165
+ }
166
+
167
+ function readJsonObject(filePath, label) {
168
+ if (!fs.existsSync(filePath)) {
169
+ throw new Error(`${label} not found: ${repoRelativePath(filePath)}`);
170
+ }
171
+ let parsed;
172
+ try {
173
+ parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
174
+ } catch (error) {
175
+ throw new Error(`Invalid ${label} JSON at ${repoRelativePath(filePath)}: ${error.message}`);
176
+ }
177
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
178
+ throw new Error(`${label} must be a JSON object: ${repoRelativePath(filePath)}`);
179
+ }
180
+ return parsed;
181
+ }
182
+
183
+ function resolveSkillBundleDir(skillsDir, skillId) {
184
+ return path.join(REPO_ROOT, skillsDir, skillId);
185
+ }
186
+
187
+ export function loadSkillBundle(skillId, options = {}) {
188
+ const normalizedSkillId = normalizeSkillId(skillId);
189
+ const skillsDir = normalizeRepoRelativePath(
190
+ options.skillsDir || DEFAULT_SKILLS_DIR,
191
+ "skills.dir",
192
+ );
193
+ const bundleDir = resolveSkillBundleDir(skillsDir, normalizedSkillId);
194
+ if (!fs.existsSync(bundleDir) || !fs.statSync(bundleDir).isDirectory()) {
195
+ throw new Error(
196
+ `Missing skill bundle "${normalizedSkillId}" in ${repoRelativePath(path.join(REPO_ROOT, skillsDir))}`,
197
+ );
198
+ }
199
+ const manifestPath = path.join(bundleDir, "skill.json");
200
+ const skillPath = path.join(bundleDir, "SKILL.md");
201
+ const manifest = readJsonObject(manifestPath, "skill manifest");
202
+ if (normalizeSkillId(manifest.id || normalizedSkillId, `skills.${normalizedSkillId}.id`) !== normalizedSkillId) {
203
+ throw new Error(
204
+ `Skill manifest id mismatch for ${normalizedSkillId}: expected "${normalizedSkillId}"`,
205
+ );
206
+ }
207
+ if (!fs.existsSync(skillPath)) {
208
+ throw new Error(`Missing SKILL.md for skill "${normalizedSkillId}"`);
209
+ }
210
+ const skillText = fs.readFileSync(skillPath, "utf8").trim();
211
+ const adapterTextByRuntime = {};
212
+ const adapterPathByRuntime = {};
213
+ for (const runtimeId of SUPPORTED_SKILL_RUNTIMES) {
214
+ const adapterPath = path.join(bundleDir, "adapters", `${runtimeId}.md`);
215
+ if (!fs.existsSync(adapterPath)) {
216
+ continue;
217
+ }
218
+ adapterPathByRuntime[runtimeId] = repoRelativePath(adapterPath);
219
+ adapterTextByRuntime[runtimeId] = fs.readFileSync(adapterPath, "utf8").trim();
220
+ }
221
+ const sourceFiles = listFilesRecursively(bundleDir).map((filePath) => ({
222
+ path: repoRelativePath(filePath),
223
+ hash: hashBuffer(fs.readFileSync(filePath)),
224
+ }));
225
+ const bundleHash = hashText(
226
+ sourceFiles.map((entry) => `${entry.path}:${entry.hash}`).join("\n"),
227
+ );
228
+ return {
229
+ id: normalizedSkillId,
230
+ title: cleanText(manifest.title) || normalizedSkillId,
231
+ description: cleanText(manifest.description) || cleanText(manifest.summary) || null,
232
+ bundlePath: repoRelativePath(bundleDir),
233
+ manifestPath: repoRelativePath(manifestPath),
234
+ skillPath: repoRelativePath(skillPath),
235
+ skillText,
236
+ adapterPathByRuntime,
237
+ adapterTextByRuntime,
238
+ sourceFiles,
239
+ bundleHash,
240
+ };
241
+ }
242
+
243
+ function renderBundlePrompt(bundle, runtimeId) {
244
+ const lines = [`## Skill ${bundle.id}`];
245
+ if (bundle.description) {
246
+ lines.push(`- Summary: ${bundle.description}`);
247
+ }
248
+ lines.push(`- Bundle: ${bundle.bundlePath}`);
249
+ lines.push("### Canonical instructions");
250
+ lines.push("```text");
251
+ lines.push(bundle.skillText);
252
+ lines.push("```");
253
+ const adapterText = bundle.adapterTextByRuntime[runtimeId];
254
+ if (adapterText) {
255
+ lines.push("");
256
+ lines.push(`### ${runtimeId} adapter`);
257
+ lines.push("```text");
258
+ lines.push(adapterText);
259
+ lines.push("```");
260
+ }
261
+ lines.push("");
262
+ return lines;
263
+ }
264
+
265
+ function renderSkillPromptText(bundles, runtimeId) {
266
+ if (!Array.isArray(bundles) || bundles.length === 0) {
267
+ return "";
268
+ }
269
+ const lines = [
270
+ "Active skill packs for this run:",
271
+ ...bundles.map(
272
+ (bundle) => `- ${bundle.id}${bundle.description ? `: ${bundle.description}` : ""}`,
273
+ ),
274
+ "- Skills are additive guidance. Repository source, standing role prompts, shared summaries, and ownership boundaries remain authoritative.",
275
+ "",
276
+ ];
277
+ for (const bundle of bundles) {
278
+ lines.push(...renderBundlePrompt(bundle, runtimeId));
279
+ }
280
+ return lines.join("\n").trim();
281
+ }
282
+
283
+ function renderRuntimeOnlyText(bundles, runtimeId) {
284
+ const sections = [];
285
+ for (const bundle of bundles) {
286
+ const adapterText = bundle.adapterTextByRuntime[runtimeId];
287
+ if (!adapterText) {
288
+ continue;
289
+ }
290
+ sections.push(`Skill ${bundle.id}`);
291
+ sections.push("```text");
292
+ sections.push(adapterText);
293
+ sections.push("```");
294
+ sections.push("");
295
+ }
296
+ return sections.join("\n").trim();
297
+ }
298
+
299
+ function defaultDeployEnvironmentKind(wave) {
300
+ const environments = Array.isArray(wave?.deployEnvironments) ? wave.deployEnvironments : [];
301
+ if (environments.length === 0) {
302
+ return null;
303
+ }
304
+ return environments.find((environment) => environment.isDefault)?.kind || environments[0]?.kind || null;
305
+ }
306
+
307
+ export function resolveSkillIdsForAgent(agent, wave, laneProfile) {
308
+ const skillsConfig = laneProfile?.skills || emptySkillsConfig();
309
+ const role = cleanText(agent?.executorResolved?.role).toLowerCase() || null;
310
+ const runtimeId = cleanText(agent?.executorResolved?.id).toLowerCase() || null;
311
+ const deployKind = cleanText(defaultDeployEnvironmentKind(wave)).toLowerCase() || null;
312
+ return {
313
+ role,
314
+ runtimeId,
315
+ deployKind,
316
+ ids: uniqueStrings([
317
+ ...(skillsConfig.base || []),
318
+ ...(role ? skillsConfig.byRole?.[role] || [] : []),
319
+ ...(runtimeId ? skillsConfig.byRuntime?.[runtimeId] || [] : []),
320
+ ...(deployKind ? skillsConfig.byDeployKind?.[deployKind] || [] : []),
321
+ ...(Array.isArray(agent?.skills) ? agent.skills : []),
322
+ ]).map((skillId) => normalizeSkillId(skillId)),
323
+ };
324
+ }
325
+
326
+ export function resolveAgentSkills(agent, wave, options = {}) {
327
+ const laneProfile = options.laneProfile || {};
328
+ const skillsConfig = laneProfile.skills || emptySkillsConfig();
329
+ const { ids, role, runtimeId, deployKind } = resolveSkillIdsForAgent(agent, wave, laneProfile);
330
+ const bundles = ids.map((skillId) => loadSkillBundle(skillId, { skillsDir: skillsConfig.dir }));
331
+ const promptText = renderSkillPromptText(bundles, runtimeId || "local");
332
+ const runtimeText = renderRuntimeOnlyText(bundles, runtimeId || "local");
333
+ return {
334
+ dir: skillsConfig.dir,
335
+ ids,
336
+ role,
337
+ runtime: runtimeId,
338
+ deployKind,
339
+ promptText,
340
+ promptHash: hashText(promptText || JSON.stringify({ ids, role, runtimeId, deployKind })),
341
+ runtimeText,
342
+ bundles: bundles.map((bundle) => ({
343
+ id: bundle.id,
344
+ title: bundle.title,
345
+ description: bundle.description,
346
+ bundlePath: bundle.bundlePath,
347
+ manifestPath: bundle.manifestPath,
348
+ skillPath: bundle.skillPath,
349
+ adapterPath: bundle.adapterPathByRuntime[runtimeId] || null,
350
+ bundleHash: bundle.bundleHash,
351
+ sourceFiles: bundle.sourceFiles.map((entry) => entry.path),
352
+ })),
353
+ codexAddDirs: uniqueStrings(bundles.map((bundle) => bundle.bundlePath)),
354
+ opencodeFiles: uniqueStrings(
355
+ bundles.flatMap((bundle) => [
356
+ bundle.skillPath,
357
+ bundle.adapterPathByRuntime[runtimeId] || null,
358
+ ]),
359
+ ),
360
+ opencodeInstructions: promptText ? [promptText] : [],
361
+ };
362
+ }
363
+
364
+ export function summarizeResolvedSkills(resolvedSkills) {
365
+ if (!resolvedSkills || typeof resolvedSkills !== "object") {
366
+ return {
367
+ ids: [],
368
+ role: null,
369
+ runtime: null,
370
+ deployKind: null,
371
+ promptHash: null,
372
+ bundles: [],
373
+ artifacts: null,
374
+ };
375
+ }
376
+ return {
377
+ ids: Array.isArray(resolvedSkills.ids) ? resolvedSkills.ids.slice() : [],
378
+ role: resolvedSkills.role || null,
379
+ runtime: resolvedSkills.runtime || null,
380
+ deployKind: resolvedSkills.deployKind || null,
381
+ promptHash: resolvedSkills.promptHash || null,
382
+ bundles: Array.isArray(resolvedSkills.bundles)
383
+ ? resolvedSkills.bundles.map((bundle) => ({
384
+ id: bundle.id,
385
+ title: bundle.title || null,
386
+ description: bundle.description || null,
387
+ bundlePath: bundle.bundlePath,
388
+ manifestPath: bundle.manifestPath,
389
+ skillPath: bundle.skillPath,
390
+ adapterPath: bundle.adapterPath || null,
391
+ bundleHash: bundle.bundleHash || null,
392
+ sourceFiles: Array.isArray(bundle.sourceFiles) ? bundle.sourceFiles.slice() : [],
393
+ }))
394
+ : [],
395
+ artifacts: resolvedSkills.artifacts || null,
396
+ };
397
+ }
398
+
399
+ export function writeResolvedSkillArtifacts(overlayDir, resolvedSkills) {
400
+ if (!resolvedSkills || !Array.isArray(resolvedSkills.ids) || resolvedSkills.ids.length === 0) {
401
+ return null;
402
+ }
403
+ const promptPath = path.join(overlayDir, "skills.resolved.md");
404
+ const metadataPath = path.join(overlayDir, "skills.metadata.json");
405
+ const runtimePromptPath =
406
+ resolvedSkills.runtime
407
+ ? path.join(overlayDir, `${resolvedSkills.runtime}-skills.txt`)
408
+ : null;
409
+ const artifacts = {
410
+ promptPath: repoRelativePath(promptPath),
411
+ metadataPath: repoRelativePath(metadataPath),
412
+ runtimePromptPath: runtimePromptPath ? repoRelativePath(runtimePromptPath) : null,
413
+ };
414
+ writeTextAtomic(promptPath, `${resolvedSkills.promptText}\n`);
415
+ if (runtimePromptPath) {
416
+ writeTextAtomic(runtimePromptPath, `${resolvedSkills.runtimeText || resolvedSkills.promptText}\n`);
417
+ }
418
+ writeJsonAtomic(
419
+ metadataPath,
420
+ summarizeResolvedSkills({
421
+ ...resolvedSkills,
422
+ artifacts,
423
+ }),
424
+ );
425
+ return artifacts;
426
+ }
427
+
428
+ export function validateLaneSkillConfiguration(laneProfile) {
429
+ const skillsConfig = laneProfile?.skills || emptySkillsConfig();
430
+ const referencedSkillIds = uniqueStrings([
431
+ ...(skillsConfig.base || []),
432
+ ...Object.values(skillsConfig.byRole || {}).flat(),
433
+ ...Object.values(skillsConfig.byRuntime || {}).flat(),
434
+ ...Object.values(skillsConfig.byDeployKind || {}).flat(),
435
+ ]).map((skillId) => normalizeSkillId(skillId));
436
+ const errors = [];
437
+ for (const skillId of referencedSkillIds) {
438
+ try {
439
+ loadSkillBundle(skillId, { skillsDir: skillsConfig.dir });
440
+ } catch (error) {
441
+ errors.push(error instanceof Error ? error.message : String(error));
442
+ }
443
+ }
444
+ return {
445
+ ok: errors.length === 0,
446
+ errors,
447
+ };
448
+ }
@@ -12,6 +12,22 @@ import {
12
12
  writeJsonAtomic,
13
13
  } from "./shared.mjs";
14
14
 
15
+ export const TERMINAL_SURFACES = ["vscode", "tmux", "none"];
16
+
17
+ export function normalizeTerminalSurface(value, label = "terminal surface") {
18
+ const normalized = String(value || "")
19
+ .trim()
20
+ .toLowerCase();
21
+ if (!TERMINAL_SURFACES.includes(normalized)) {
22
+ throw new Error(`${label} must be one of: ${TERMINAL_SURFACES.join(", ")}`);
23
+ }
24
+ return normalized;
25
+ }
26
+
27
+ export function terminalSurfaceUsesTerminalRegistry(surface) {
28
+ return normalizeTerminalSurface(surface) === "vscode";
29
+ }
30
+
15
31
  function defaultTerminalsConfig() {
16
32
  return {
17
33
  terminals: [],