@chllming/wave-orchestration 0.5.4 → 0.6.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 (126) hide show
  1. package/CHANGELOG.md +52 -3
  2. package/README.md +33 -5
  3. package/docs/README.md +18 -4
  4. package/docs/agents/wave-cont-eval-role.md +36 -0
  5. package/docs/agents/{wave-evaluator-role.md → wave-cont-qa-role.md} +14 -11
  6. package/docs/agents/wave-documentation-role.md +1 -1
  7. package/docs/agents/wave-infra-role.md +1 -1
  8. package/docs/agents/wave-integration-role.md +3 -3
  9. package/docs/agents/wave-launcher-role.md +4 -3
  10. package/docs/agents/wave-security-role.md +40 -0
  11. package/docs/concepts/context7-vs-skills.md +1 -1
  12. package/docs/concepts/what-is-a-wave.md +56 -6
  13. package/docs/evals/README.md +166 -0
  14. package/docs/evals/benchmark-catalog.json +663 -0
  15. package/docs/guides/author-and-run-waves.md +135 -0
  16. package/docs/guides/planner.md +5 -0
  17. package/docs/guides/terminal-surfaces.md +2 -0
  18. package/docs/plans/component-cutover-matrix.json +1 -1
  19. package/docs/plans/component-cutover-matrix.md +1 -1
  20. package/docs/plans/current-state.md +19 -1
  21. package/docs/plans/examples/wave-example-live-proof.md +435 -0
  22. package/docs/plans/migration.md +42 -0
  23. package/docs/plans/wave-orchestrator.md +46 -7
  24. package/docs/plans/waves/wave-0.md +4 -4
  25. package/docs/reference/live-proof-waves.md +177 -0
  26. package/docs/reference/migration-0.2-to-0.5.md +26 -19
  27. package/docs/reference/npmjs-trusted-publishing.md +6 -5
  28. package/docs/reference/runtime-config/README.md +14 -4
  29. package/docs/reference/sample-waves.md +87 -0
  30. package/docs/reference/skills.md +110 -42
  31. package/docs/research/agent-context-sources.md +130 -11
  32. package/docs/research/coordination-failure-review.md +266 -0
  33. package/docs/roadmap.md +6 -2
  34. package/package.json +2 -2
  35. package/releases/manifest.json +35 -2
  36. package/scripts/research/agent-context-archive.mjs +83 -1
  37. package/scripts/research/manifests/agent-context-expanded-2026-03-22.mjs +811 -0
  38. package/scripts/wave-orchestrator/adhoc.mjs +1331 -0
  39. package/scripts/wave-orchestrator/agent-state.mjs +358 -6
  40. package/scripts/wave-orchestrator/artifact-schemas.mjs +173 -0
  41. package/scripts/wave-orchestrator/clarification-triage.mjs +10 -3
  42. package/scripts/wave-orchestrator/config.mjs +48 -12
  43. package/scripts/wave-orchestrator/context7.mjs +2 -0
  44. package/scripts/wave-orchestrator/coord-cli.mjs +51 -19
  45. package/scripts/wave-orchestrator/coordination-store.mjs +26 -4
  46. package/scripts/wave-orchestrator/coordination.mjs +83 -9
  47. package/scripts/wave-orchestrator/dashboard-state.mjs +20 -8
  48. package/scripts/wave-orchestrator/dep-cli.mjs +5 -2
  49. package/scripts/wave-orchestrator/docs-queue.mjs +8 -2
  50. package/scripts/wave-orchestrator/evals.mjs +451 -0
  51. package/scripts/wave-orchestrator/feedback.mjs +15 -1
  52. package/scripts/wave-orchestrator/install.mjs +32 -9
  53. package/scripts/wave-orchestrator/launcher-closure.mjs +281 -0
  54. package/scripts/wave-orchestrator/launcher-runtime.mjs +334 -0
  55. package/scripts/wave-orchestrator/launcher.mjs +709 -601
  56. package/scripts/wave-orchestrator/ledger.mjs +123 -20
  57. package/scripts/wave-orchestrator/local-executor.mjs +99 -12
  58. package/scripts/wave-orchestrator/planner.mjs +177 -42
  59. package/scripts/wave-orchestrator/replay.mjs +6 -3
  60. package/scripts/wave-orchestrator/role-helpers.mjs +84 -0
  61. package/scripts/wave-orchestrator/shared.mjs +75 -11
  62. package/scripts/wave-orchestrator/skills.mjs +637 -106
  63. package/scripts/wave-orchestrator/traces.mjs +71 -48
  64. package/scripts/wave-orchestrator/wave-files.mjs +947 -101
  65. package/scripts/wave.mjs +9 -0
  66. package/skills/README.md +202 -0
  67. package/skills/provider-aws/SKILL.md +111 -0
  68. package/skills/provider-aws/adapters/claude.md +1 -0
  69. package/skills/provider-aws/adapters/codex.md +1 -0
  70. package/skills/provider-aws/references/service-verification.md +39 -0
  71. package/skills/provider-aws/skill.json +50 -1
  72. package/skills/provider-custom-deploy/SKILL.md +59 -0
  73. package/skills/provider-custom-deploy/skill.json +46 -1
  74. package/skills/provider-docker-compose/SKILL.md +90 -0
  75. package/skills/provider-docker-compose/adapters/local.md +1 -0
  76. package/skills/provider-docker-compose/skill.json +49 -1
  77. package/skills/provider-github-release/SKILL.md +116 -1
  78. package/skills/provider-github-release/adapters/claude.md +1 -0
  79. package/skills/provider-github-release/adapters/codex.md +1 -0
  80. package/skills/provider-github-release/skill.json +51 -1
  81. package/skills/provider-kubernetes/SKILL.md +137 -0
  82. package/skills/provider-kubernetes/adapters/claude.md +1 -0
  83. package/skills/provider-kubernetes/adapters/codex.md +1 -0
  84. package/skills/provider-kubernetes/references/kubectl-patterns.md +58 -0
  85. package/skills/provider-kubernetes/skill.json +48 -1
  86. package/skills/provider-railway/SKILL.md +118 -1
  87. package/skills/provider-railway/references/verification-commands.md +39 -0
  88. package/skills/provider-railway/skill.json +67 -1
  89. package/skills/provider-ssh-manual/SKILL.md +91 -0
  90. package/skills/provider-ssh-manual/skill.json +50 -1
  91. package/skills/repo-coding-rules/SKILL.md +84 -0
  92. package/skills/repo-coding-rules/skill.json +30 -1
  93. package/skills/role-cont-eval/SKILL.md +90 -0
  94. package/skills/role-cont-eval/adapters/codex.md +1 -0
  95. package/skills/role-cont-eval/skill.json +36 -0
  96. package/skills/role-cont-qa/SKILL.md +93 -0
  97. package/skills/role-cont-qa/adapters/claude.md +1 -0
  98. package/skills/role-cont-qa/skill.json +36 -0
  99. package/skills/role-deploy/SKILL.md +90 -0
  100. package/skills/role-deploy/skill.json +32 -1
  101. package/skills/role-documentation/SKILL.md +66 -0
  102. package/skills/role-documentation/skill.json +32 -1
  103. package/skills/role-implementation/SKILL.md +62 -0
  104. package/skills/role-implementation/skill.json +32 -1
  105. package/skills/role-infra/SKILL.md +74 -0
  106. package/skills/role-infra/skill.json +32 -1
  107. package/skills/role-integration/SKILL.md +79 -1
  108. package/skills/role-integration/skill.json +32 -1
  109. package/skills/role-research/SKILL.md +58 -0
  110. package/skills/role-research/skill.json +32 -1
  111. package/skills/role-security/SKILL.md +60 -0
  112. package/skills/role-security/skill.json +36 -0
  113. package/skills/runtime-claude/SKILL.md +60 -1
  114. package/skills/runtime-claude/skill.json +32 -1
  115. package/skills/runtime-codex/SKILL.md +52 -1
  116. package/skills/runtime-codex/skill.json +32 -1
  117. package/skills/runtime-local/SKILL.md +39 -0
  118. package/skills/runtime-local/skill.json +32 -1
  119. package/skills/runtime-opencode/SKILL.md +51 -0
  120. package/skills/runtime-opencode/skill.json +32 -1
  121. package/skills/wave-core/SKILL.md +107 -0
  122. package/skills/wave-core/references/marker-syntax.md +62 -0
  123. package/skills/wave-core/skill.json +31 -1
  124. package/wave.config.json +35 -6
  125. package/skills/role-evaluator/SKILL.md +0 -6
  126. package/skills/role-evaluator/skill.json +0 -5
@@ -8,6 +8,27 @@ const REPO_ROOT = WORKSPACE_ROOT;
8
8
  export const DEFAULT_SKILLS_DIR = "skills";
9
9
  export const SKILL_ID_REGEX = /^[a-z0-9][a-z0-9._-]*$/;
10
10
  export const SUPPORTED_SKILL_RUNTIMES = ["codex", "claude", "opencode", "local"];
11
+ export const SUPPORTED_SKILL_ROLES = [
12
+ "implementation",
13
+ "integration",
14
+ "documentation",
15
+ "cont-qa",
16
+ "cont-eval",
17
+ "security",
18
+ "infra",
19
+ "deploy",
20
+ "research",
21
+ ];
22
+ export const SUPPORTED_SKILL_DEPLOY_KINDS = [
23
+ "railway-cli",
24
+ "railway-mcp",
25
+ "docker-compose",
26
+ "kubernetes",
27
+ "ssh-manual",
28
+ "custom",
29
+ "aws",
30
+ "github-release",
31
+ ];
11
32
 
12
33
  function cleanText(value) {
13
34
  return String(value ?? "").trim();
@@ -51,16 +72,130 @@ function normalizeRepoRelativePath(value, label) {
51
72
  return raw;
52
73
  }
53
74
 
54
- function uniqueStrings(values) {
75
+ function uniqueStrings(values, options = {}) {
76
+ const lowerCase = options.lowerCase !== false;
55
77
  return Array.from(
56
78
  new Set(
57
79
  (Array.isArray(values) ? values : [])
58
- .map((value) => cleanText(value))
80
+ .map((value) => {
81
+ const normalized = cleanText(value);
82
+ return lowerCase ? normalized.toLowerCase() : normalized;
83
+ })
59
84
  .filter(Boolean),
60
85
  ),
61
86
  );
62
87
  }
63
88
 
89
+ function listFilesRecursively(rootDir) {
90
+ if (!fs.existsSync(rootDir)) {
91
+ return [];
92
+ }
93
+ const files = [];
94
+ const visit = (targetDir) => {
95
+ for (const entry of fs.readdirSync(targetDir, { withFileTypes: true })) {
96
+ const fullPath = path.join(targetDir, entry.name);
97
+ if (entry.isDirectory()) {
98
+ visit(fullPath);
99
+ } else {
100
+ files.push(fullPath);
101
+ }
102
+ }
103
+ };
104
+ visit(rootDir);
105
+ return files.toSorted();
106
+ }
107
+
108
+ function repoRelativePath(filePath) {
109
+ return path.relative(REPO_ROOT, filePath).replaceAll(path.sep, "/");
110
+ }
111
+
112
+ function readJsonObject(filePath, label) {
113
+ if (!fs.existsSync(filePath)) {
114
+ throw new Error(`${label} not found: ${repoRelativePath(filePath)}`);
115
+ }
116
+ let parsed;
117
+ try {
118
+ parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
119
+ } catch (error) {
120
+ throw new Error(`Invalid ${label} JSON at ${repoRelativePath(filePath)}: ${error.message}`);
121
+ }
122
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
123
+ throw new Error(`${label} must be a JSON object: ${repoRelativePath(filePath)}`);
124
+ }
125
+ return parsed;
126
+ }
127
+
128
+ function normalizeRequiredText(value, label) {
129
+ const normalized = cleanText(value);
130
+ if (!normalized) {
131
+ throw new Error(`${label} is required`);
132
+ }
133
+ return normalized;
134
+ }
135
+
136
+ function normalizeOptionalText(value) {
137
+ const normalized = cleanText(value);
138
+ return normalized || null;
139
+ }
140
+
141
+ function normalizeChoiceArray(values, label, allowedValues) {
142
+ if (values === undefined || values === null || values === "") {
143
+ return [];
144
+ }
145
+ if (!Array.isArray(values)) {
146
+ throw new Error(`${label} must be an array`);
147
+ }
148
+ const normalized = uniqueStrings(values);
149
+ const allowed = new Set(allowedValues);
150
+ for (const value of normalized) {
151
+ if (!allowed.has(value)) {
152
+ throw new Error(`${label} contains unsupported value "${value}"`);
153
+ }
154
+ }
155
+ return normalized;
156
+ }
157
+
158
+ function normalizeStringArray(values, label) {
159
+ if (values === undefined || values === null || values === "") {
160
+ return [];
161
+ }
162
+ if (!Array.isArray(values)) {
163
+ throw new Error(`${label} must be an array`);
164
+ }
165
+ return uniqueStrings(values, { lowerCase: false });
166
+ }
167
+
168
+ function normalizeDeployKind(value, label = "deploy kind") {
169
+ const normalized = cleanText(value).toLowerCase();
170
+ if (!SKILL_ID_REGEX.test(normalized)) {
171
+ throw new Error(`${label} must match ${SKILL_ID_REGEX}`);
172
+ }
173
+ return normalized;
174
+ }
175
+
176
+ function normalizeDeployKindArray(values, label, options = {}) {
177
+ if (values === undefined || values === null || values === "") {
178
+ return [];
179
+ }
180
+ if (!Array.isArray(values)) {
181
+ throw new Error(`${label} must be an array`);
182
+ }
183
+ const normalized = uniqueStrings(
184
+ values.map((value, index) => normalizeDeployKind(value, `${label}[${index}]`)),
185
+ );
186
+ const allowedValues = Array.isArray(options.allowedValues) ? options.allowedValues : [];
187
+ if (allowedValues.length === 0) {
188
+ return normalized;
189
+ }
190
+ const allowed = new Set(allowedValues.map((value) => normalizeDeployKind(value, label)));
191
+ for (const value of normalized) {
192
+ if (!allowed.has(value)) {
193
+ throw new Error(`${label} contains unsupported value "${value}"`);
194
+ }
195
+ }
196
+ return normalized;
197
+ }
198
+
64
199
  export function normalizeSkillId(value, label = "skill id") {
65
200
  const normalized = cleanText(value).toLowerCase();
66
201
  if (!SKILL_ID_REGEX.test(normalized)) {
@@ -81,18 +216,197 @@ export function normalizeSkillIdArray(values, label = "skills") {
81
216
  );
82
217
  }
83
218
 
84
- function normalizeSkillMap(rawMap = {}, label) {
219
+ function normalizeSkillMap(rawMap = {}, label, options = {}) {
85
220
  if (!rawMap || typeof rawMap !== "object" || Array.isArray(rawMap)) {
86
221
  return {};
87
222
  }
223
+ const allowedKeys = options.allowedKeys ? new Set(options.allowedKeys) : null;
88
224
  return Object.fromEntries(
89
- Object.entries(rawMap).map(([key, values]) => [
90
- cleanText(key).toLowerCase(),
91
- normalizeSkillIdArray(values, `${label}.${key}`),
92
- ]),
225
+ Object.entries(rawMap).map(([key, values]) => {
226
+ const normalizedKey = cleanText(key).toLowerCase();
227
+ if (!normalizedKey) {
228
+ throw new Error(`${label} keys must be non-empty`);
229
+ }
230
+ if (allowedKeys && !allowedKeys.has(normalizedKey)) {
231
+ throw new Error(`${label}.${key} is not a supported selector key`);
232
+ }
233
+ return [normalizedKey, normalizeSkillIdArray(values, `${label}.${key}`)];
234
+ }),
93
235
  );
94
236
  }
95
237
 
238
+ function normalizeSkillActivation(rawActivation, label) {
239
+ if (!rawActivation || typeof rawActivation !== "object" || Array.isArray(rawActivation)) {
240
+ throw new Error(`${label} is required and must be an object`);
241
+ }
242
+ return {
243
+ when: normalizeRequiredText(rawActivation.when, `${label}.when`),
244
+ roles: normalizeChoiceArray(rawActivation.roles, `${label}.roles`, SUPPORTED_SKILL_ROLES),
245
+ runtimes: normalizeChoiceArray(
246
+ rawActivation.runtimes,
247
+ `${label}.runtimes`,
248
+ SUPPORTED_SKILL_RUNTIMES,
249
+ ),
250
+ deployKinds: normalizeDeployKindArray(rawActivation.deployKinds, `${label}.deployKinds`),
251
+ };
252
+ }
253
+
254
+ function normalizeSkillTermination(rawTermination, label) {
255
+ if (rawTermination === undefined || rawTermination === null || rawTermination === "") {
256
+ return null;
257
+ }
258
+ if (typeof rawTermination === "string") {
259
+ return {
260
+ when: normalizeRequiredText(rawTermination, label),
261
+ };
262
+ }
263
+ if (!rawTermination || typeof rawTermination !== "object" || Array.isArray(rawTermination)) {
264
+ throw new Error(`${label} must be a string or an object`);
265
+ }
266
+ return {
267
+ when: normalizeRequiredText(rawTermination.when, `${label}.when`),
268
+ };
269
+ }
270
+
271
+ function normalizeSkillPermissions(rawPermissions, label) {
272
+ if (rawPermissions === undefined || rawPermissions === null || rawPermissions === "") {
273
+ return {
274
+ network: [],
275
+ shell: [],
276
+ mcpServers: [],
277
+ };
278
+ }
279
+ if (!rawPermissions || typeof rawPermissions !== "object" || Array.isArray(rawPermissions)) {
280
+ throw new Error(`${label} must be an object`);
281
+ }
282
+ return {
283
+ network: normalizeStringArray(rawPermissions.network, `${label}.network`),
284
+ shell: normalizeStringArray(rawPermissions.shell, `${label}.shell`),
285
+ mcpServers: normalizeStringArray(rawPermissions.mcpServers, `${label}.mcpServers`),
286
+ };
287
+ }
288
+
289
+ function normalizeTrustTier(value, label) {
290
+ const normalized = cleanText(value).toLowerCase();
291
+ if (!normalized) {
292
+ throw new Error(`${label} is required`);
293
+ }
294
+ if (!SKILL_ID_REGEX.test(normalized)) {
295
+ throw new Error(`${label} must match ${SKILL_ID_REGEX}`);
296
+ }
297
+ return normalized;
298
+ }
299
+
300
+ function normalizeSkillTrust(rawTrust, label) {
301
+ if (rawTrust === undefined || rawTrust === null || rawTrust === "") {
302
+ return null;
303
+ }
304
+ if (typeof rawTrust === "string") {
305
+ return {
306
+ tier: normalizeTrustTier(rawTrust, label),
307
+ };
308
+ }
309
+ if (!rawTrust || typeof rawTrust !== "object" || Array.isArray(rawTrust)) {
310
+ throw new Error(`${label} must be a string or an object`);
311
+ }
312
+ return {
313
+ tier: normalizeTrustTier(rawTrust.tier, `${label}.tier`),
314
+ };
315
+ }
316
+
317
+ function normalizeSkillEvalCase(rawEvalCase, label) {
318
+ if (!rawEvalCase || typeof rawEvalCase !== "object" || Array.isArray(rawEvalCase)) {
319
+ throw new Error(`${label} must be an object`);
320
+ }
321
+ const expectActive = rawEvalCase.expectActive;
322
+ if (typeof expectActive !== "boolean") {
323
+ throw new Error(`${label}.expectActive must be a boolean`);
324
+ }
325
+ return {
326
+ id: normalizeSkillId(rawEvalCase.id, `${label}.id`),
327
+ role: normalizeChoiceArray(
328
+ [normalizeRequiredText(rawEvalCase.role, `${label}.role`)],
329
+ `${label}.role`,
330
+ SUPPORTED_SKILL_ROLES,
331
+ )[0],
332
+ runtime: normalizeChoiceArray(
333
+ [normalizeRequiredText(rawEvalCase.runtime, `${label}.runtime`)],
334
+ `${label}.runtime`,
335
+ SUPPORTED_SKILL_RUNTIMES,
336
+ )[0],
337
+ deployKind:
338
+ rawEvalCase.deployKind === undefined || rawEvalCase.deployKind === null || rawEvalCase.deployKind === ""
339
+ ? null
340
+ : normalizeDeployKind(rawEvalCase.deployKind, `${label}.deployKind`),
341
+ expectActive,
342
+ };
343
+ }
344
+
345
+ function normalizeSkillEvalCaseArray(rawEvalCases, label) {
346
+ if (rawEvalCases === undefined || rawEvalCases === null || rawEvalCases === "") {
347
+ return [];
348
+ }
349
+ if (!Array.isArray(rawEvalCases)) {
350
+ throw new Error(`${label} must be an array`);
351
+ }
352
+ return rawEvalCases.map((entry, index) =>
353
+ normalizeSkillEvalCase(entry, `${label}[${index}]`),
354
+ );
355
+ }
356
+
357
+ function normalizeSkillManifest(rawManifest, normalizedSkillId) {
358
+ const label = `skills.${normalizedSkillId}`;
359
+ return {
360
+ id: normalizeSkillId(rawManifest.id || normalizedSkillId, `${label}.id`),
361
+ title: normalizeRequiredText(rawManifest.title, `${label}.title`),
362
+ description: normalizeRequiredText(
363
+ rawManifest.description || rawManifest.summary,
364
+ `${label}.description`,
365
+ ),
366
+ version: normalizeOptionalText(rawManifest.version),
367
+ tags: normalizeStringArray(rawManifest.tags, `${label}.tags`),
368
+ activation: normalizeSkillActivation(rawManifest.activation, `${label}.activation`),
369
+ termination: normalizeSkillTermination(rawManifest.termination, `${label}.termination`),
370
+ permissions: normalizeSkillPermissions(rawManifest.permissions, `${label}.permissions`),
371
+ trust: normalizeSkillTrust(rawManifest.trust, `${label}.trust`),
372
+ evalCases: normalizeSkillEvalCaseArray(rawManifest.evalCases, `${label}.evalCases`),
373
+ };
374
+ }
375
+
376
+ function formatScopeList(values, fallback = "any") {
377
+ return Array.isArray(values) && values.length > 0 ? values.join(", ") : fallback;
378
+ }
379
+
380
+ function formatPermissionSummary(permissions) {
381
+ const segments = [];
382
+ if (permissions.network.length > 0) {
383
+ segments.push(`network=${permissions.network.join(", ")}`);
384
+ }
385
+ if (permissions.shell.length > 0) {
386
+ segments.push(`shell=${permissions.shell.join(", ")}`);
387
+ }
388
+ if (permissions.mcpServers.length > 0) {
389
+ segments.push(`mcp=${permissions.mcpServers.join(", ")}`);
390
+ }
391
+ return segments.length > 0 ? segments.join("; ") : "none declared";
392
+ }
393
+
394
+ function resolveSkillBundleDir(skillsDir, skillId) {
395
+ return path.join(REPO_ROOT, skillsDir, skillId);
396
+ }
397
+
398
+ function listSkillBundleIds(skillsDir) {
399
+ const skillsRoot = path.join(REPO_ROOT, skillsDir);
400
+ if (!fs.existsSync(skillsRoot) || !fs.statSync(skillsRoot).isDirectory()) {
401
+ return [];
402
+ }
403
+ return fs
404
+ .readdirSync(skillsRoot, { withFileTypes: true })
405
+ .filter((entry) => entry.isDirectory() && SKILL_ID_REGEX.test(entry.name))
406
+ .map((entry) => entry.name)
407
+ .toSorted();
408
+ }
409
+
96
410
  export function emptySkillsConfig() {
97
411
  return {
98
412
  dir: DEFAULT_SKILLS_DIR,
@@ -106,17 +420,30 @@ export function emptySkillsConfig() {
106
420
  export function normalizeSkillsConfig(rawSkills = {}, label = "skills", options = {}) {
107
421
  const skills =
108
422
  rawSkills && typeof rawSkills === "object" && !Array.isArray(rawSkills) ? rawSkills : {};
423
+ if (
424
+ skills.byRole &&
425
+ typeof skills.byRole === "object" &&
426
+ !Array.isArray(skills.byRole) &&
427
+ Object.keys(skills.byRole).some((key) => cleanText(key).toLowerCase() === "evaluator")
428
+ ) {
429
+ throw new Error(`${label}.byRole.evaluator was renamed to ${label}.byRole.cont-qa`);
430
+ }
109
431
  const dir =
110
432
  Object.prototype.hasOwnProperty.call(skills, "dir")
111
433
  ? normalizeRepoRelativePath(skills.dir || DEFAULT_SKILLS_DIR, `${label}.dir`)
112
434
  : options.preserveOmittedDir
113
435
  ? null
114
436
  : DEFAULT_SKILLS_DIR;
437
+ const byRole = normalizeSkillMap(skills.byRole, `${label}.byRole`, {
438
+ allowedKeys: SUPPORTED_SKILL_ROLES,
439
+ });
115
440
  return {
116
441
  dir,
117
442
  base: normalizeSkillIdArray(skills.base, `${label}.base`),
118
- byRole: normalizeSkillMap(skills.byRole, `${label}.byRole`),
119
- byRuntime: normalizeSkillMap(skills.byRuntime, `${label}.byRuntime`),
443
+ byRole,
444
+ byRuntime: normalizeSkillMap(skills.byRuntime, `${label}.byRuntime`, {
445
+ allowedKeys: SUPPORTED_SKILL_RUNTIMES,
446
+ }),
120
447
  byDeployKind: normalizeSkillMap(skills.byDeployKind, `${label}.byDeployKind`),
121
448
  };
122
449
  }
@@ -129,11 +456,14 @@ function mergeSkillMaps(baseMap = {}, overrideMap = {}) {
129
456
  );
130
457
  }
131
458
 
132
- export function mergeSkillsConfig(baseSkills = emptySkillsConfig(), overrideSkills = emptySkillsConfig()) {
459
+ export function mergeSkillsConfig(
460
+ baseSkills = emptySkillsConfig(),
461
+ overrideSkills = emptySkillsConfig(),
462
+ ) {
133
463
  return {
134
464
  dir: overrideSkills.dir || baseSkills.dir || DEFAULT_SKILLS_DIR,
135
- base: uniqueStrings([...(baseSkills.base || []), ...(overrideSkills.base || [])]).map((skillId) =>
136
- normalizeSkillId(skillId),
465
+ base: uniqueStrings([...(baseSkills.base || []), ...(overrideSkills.base || [])]).map(
466
+ (skillId) => normalizeSkillId(skillId),
137
467
  ),
138
468
  byRole: mergeSkillMaps(baseSkills.byRole, overrideSkills.byRole),
139
469
  byRuntime: mergeSkillMaps(baseSkills.byRuntime, overrideSkills.byRuntime),
@@ -141,55 +471,54 @@ export function mergeSkillsConfig(baseSkills = emptySkillsConfig(), overrideSkil
141
471
  };
142
472
  }
143
473
 
144
- function listFilesRecursively(rootDir) {
145
- if (!fs.existsSync(rootDir)) {
146
- return [];
474
+ export function skillMatchesActivation(bundle, context = {}) {
475
+ if (!bundle?.activation) {
476
+ return true;
147
477
  }
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)}`);
478
+ const role = cleanText(context.role).toLowerCase() || null;
479
+ const runtimeId = cleanText(context.runtimeId).toLowerCase() || null;
480
+ const deployKind = cleanText(context.deployKind).toLowerCase() || null;
481
+ if (bundle.activation.roles.length > 0 && (!role || !bundle.activation.roles.includes(role))) {
482
+ return false;
170
483
  }
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}`);
484
+ if (
485
+ bundle.activation.runtimes.length > 0 &&
486
+ (!runtimeId || !bundle.activation.runtimes.includes(runtimeId))
487
+ ) {
488
+ return false;
176
489
  }
177
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
178
- throw new Error(`${label} must be a JSON object: ${repoRelativePath(filePath)}`);
490
+ if (
491
+ bundle.activation.deployKinds.length > 0 &&
492
+ (!deployKind || !bundle.activation.deployKinds.includes(deployKind))
493
+ ) {
494
+ return false;
179
495
  }
180
- return parsed;
496
+ return true;
181
497
  }
182
498
 
183
- function resolveSkillBundleDir(skillsDir, skillId) {
184
- return path.join(REPO_ROOT, skillsDir, skillId);
499
+ export function evaluateSkillBundleCases(bundle) {
500
+ const errors = [];
501
+ for (const evalCase of bundle.evalCases || []) {
502
+ const actual = skillMatchesActivation(bundle, {
503
+ role: evalCase.role,
504
+ runtimeId: evalCase.runtime,
505
+ deployKind: evalCase.deployKind,
506
+ });
507
+ if (actual !== evalCase.expectActive) {
508
+ errors.push(
509
+ `Skill "${bundle.id}" eval case "${evalCase.id}" expected active=${evalCase.expectActive} but resolved ${actual}`,
510
+ );
511
+ }
512
+ }
513
+ return {
514
+ errors,
515
+ evaluatedCases: Array.isArray(bundle.evalCases) ? bundle.evalCases.length : 0,
516
+ };
185
517
  }
186
518
 
187
519
  export function loadSkillBundle(skillId, options = {}) {
188
520
  const normalizedSkillId = normalizeSkillId(skillId);
189
- const skillsDir = normalizeRepoRelativePath(
190
- options.skillsDir || DEFAULT_SKILLS_DIR,
191
- "skills.dir",
192
- );
521
+ const skillsDir = normalizeRepoRelativePath(options.skillsDir || DEFAULT_SKILLS_DIR, "skills.dir");
193
522
  const bundleDir = resolveSkillBundleDir(skillsDir, normalizedSkillId);
194
523
  if (!fs.existsSync(bundleDir) || !fs.statSync(bundleDir).isDirectory()) {
195
524
  throw new Error(
@@ -198,11 +527,10 @@ export function loadSkillBundle(skillId, options = {}) {
198
527
  }
199
528
  const manifestPath = path.join(bundleDir, "skill.json");
200
529
  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
- );
530
+ const rawManifest = readJsonObject(manifestPath, "skill manifest");
531
+ const manifest = normalizeSkillManifest(rawManifest, normalizedSkillId);
532
+ if (manifest.id !== normalizedSkillId) {
533
+ throw new Error(`Skill manifest id mismatch for ${normalizedSkillId}: expected "${normalizedSkillId}"`);
206
534
  }
207
535
  if (!fs.existsSync(skillPath)) {
208
536
  throw new Error(`Missing SKILL.md for skill "${normalizedSkillId}"`);
@@ -218,34 +546,63 @@ export function loadSkillBundle(skillId, options = {}) {
218
546
  adapterPathByRuntime[runtimeId] = repoRelativePath(adapterPath);
219
547
  adapterTextByRuntime[runtimeId] = fs.readFileSync(adapterPath, "utf8").trim();
220
548
  }
549
+ const referencesDir = path.join(bundleDir, "references");
550
+ const referencePaths = listFilesRecursively(referencesDir).map((filePath) =>
551
+ repoRelativePath(filePath),
552
+ );
221
553
  const sourceFiles = listFilesRecursively(bundleDir).map((filePath) => ({
222
554
  path: repoRelativePath(filePath),
223
555
  hash: hashBuffer(fs.readFileSync(filePath)),
224
556
  }));
225
- const bundleHash = hashText(
226
- sourceFiles.map((entry) => `${entry.path}:${entry.hash}`).join("\n"),
227
- );
557
+ const bundleHash = hashText(sourceFiles.map((entry) => `${entry.path}:${entry.hash}`).join("\n"));
228
558
  return {
229
- id: normalizedSkillId,
230
- title: cleanText(manifest.title) || normalizedSkillId,
231
- description: cleanText(manifest.description) || cleanText(manifest.summary) || null,
559
+ ...manifest,
232
560
  bundlePath: repoRelativePath(bundleDir),
233
561
  manifestPath: repoRelativePath(manifestPath),
234
562
  skillPath: repoRelativePath(skillPath),
235
563
  skillText,
236
564
  adapterPathByRuntime,
237
565
  adapterTextByRuntime,
566
+ referencePaths,
238
567
  sourceFiles,
239
568
  bundleHash,
240
569
  };
241
570
  }
242
571
 
243
- function renderBundlePrompt(bundle, runtimeId) {
572
+ function renderBundleCatalog(bundle, runtimeId) {
244
573
  const lines = [`## Skill ${bundle.id}`];
245
- if (bundle.description) {
246
- lines.push(`- Summary: ${bundle.description}`);
247
- }
574
+ lines.push(`- Summary: ${bundle.description}`);
248
575
  lines.push(`- Bundle: ${bundle.bundlePath}`);
576
+ lines.push(`- Manifest: ${bundle.manifestPath}`);
577
+ lines.push(`- Canonical instructions: ${bundle.skillPath}`);
578
+ lines.push(`- Activation: ${bundle.activation.when}`);
579
+ lines.push(`- Activation roles: ${formatScopeList(bundle.activation.roles)}`);
580
+ lines.push(`- Activation runtimes: ${formatScopeList(bundle.activation.runtimes)}`);
581
+ lines.push(`- Activation deploy kinds: ${formatScopeList(bundle.activation.deployKinds)}`);
582
+ if (bundle.termination?.when) {
583
+ lines.push(`- Termination: ${bundle.termination.when}`);
584
+ }
585
+ if (bundle.trust?.tier) {
586
+ lines.push(`- Trust tier: ${bundle.trust.tier}`);
587
+ }
588
+ lines.push(`- Permissions: ${formatPermissionSummary(bundle.permissions)}`);
589
+ lines.push(
590
+ `- Runtime adapter (${runtimeId}): ${bundle.adapterPathByRuntime[runtimeId] || "none"}`,
591
+ );
592
+ if (Array.isArray(bundle.referencePaths) && bundle.referencePaths.length > 0) {
593
+ for (const refPath of bundle.referencePaths) {
594
+ lines.push(`- Reference: ${refPath}`);
595
+ }
596
+ }
597
+ lines.push("");
598
+ return lines;
599
+ }
600
+
601
+ function renderBundleExpandedPrompt(bundle, runtimeId) {
602
+ const lines = [`## Skill ${bundle.id}`];
603
+ lines.push(`- Summary: ${bundle.description}`);
604
+ lines.push(`- Manifest: ${bundle.manifestPath}`);
605
+ lines.push(`- Canonical instructions: ${bundle.skillPath}`);
249
606
  lines.push("### Canonical instructions");
250
607
  lines.push("```text");
251
608
  lines.push(bundle.skillText);
@@ -253,11 +610,18 @@ function renderBundlePrompt(bundle, runtimeId) {
253
610
  const adapterText = bundle.adapterTextByRuntime[runtimeId];
254
611
  if (adapterText) {
255
612
  lines.push("");
256
- lines.push(`### ${runtimeId} adapter`);
613
+ lines.push(`### ${runtimeId} adapter (${bundle.adapterPathByRuntime[runtimeId]})`);
257
614
  lines.push("```text");
258
615
  lines.push(adapterText);
259
616
  lines.push("```");
260
617
  }
618
+ if (Array.isArray(bundle.referencePaths) && bundle.referencePaths.length > 0) {
619
+ lines.push("");
620
+ lines.push("### Available references");
621
+ for (const refPath of bundle.referencePaths) {
622
+ lines.push(`- ${refPath}`);
623
+ }
624
+ }
261
625
  lines.push("");
262
626
  return lines;
263
627
  }
@@ -268,32 +632,39 @@ function renderSkillPromptText(bundles, runtimeId) {
268
632
  }
269
633
  const lines = [
270
634
  "Active skill packs for this run:",
271
- ...bundles.map(
272
- (bundle) => `- ${bundle.id}${bundle.description ? `: ${bundle.description}` : ""}`,
273
- ),
635
+ ...bundles.map((bundle) => `- ${bundle.id}: ${bundle.description}`),
274
636
  "- Skills are additive guidance. Repository source, standing role prompts, shared summaries, and ownership boundaries remain authoritative.",
637
+ "- Use this catalog first. Open each bundle's manifest, SKILL.md, runtime adapter, and references only when needed for the current step.",
275
638
  "",
276
639
  ];
277
640
  for (const bundle of bundles) {
278
- lines.push(...renderBundlePrompt(bundle, runtimeId));
641
+ lines.push(...renderBundleCatalog(bundle, runtimeId));
642
+ }
643
+ return lines.join("\n").trim();
644
+ }
645
+
646
+ function renderExpandedSkillText(bundles, runtimeId) {
647
+ if (!Array.isArray(bundles) || bundles.length === 0) {
648
+ return "";
649
+ }
650
+ const lines = ["Full canonical skill payload for this run:", ""];
651
+ for (const bundle of bundles) {
652
+ lines.push(...renderBundleExpandedPrompt(bundle, runtimeId));
279
653
  }
280
654
  return lines.join("\n").trim();
281
655
  }
282
656
 
283
657
  function renderRuntimeOnlyText(bundles, runtimeId) {
284
- const sections = [];
658
+ if (!Array.isArray(bundles) || bundles.length === 0) {
659
+ return "";
660
+ }
661
+ const lines = [`Runtime skill catalog for ${runtimeId}:`];
285
662
  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("");
663
+ lines.push(
664
+ `- ${bundle.id}: adapter=${bundle.adapterPathByRuntime[runtimeId] || "none"} manifest=${bundle.manifestPath}`,
665
+ );
295
666
  }
296
- return sections.join("\n").trim();
667
+ return lines.join("\n").trim();
297
668
  }
298
669
 
299
670
  function defaultDeployEnvironmentKind(wave) {
@@ -301,7 +672,31 @@ function defaultDeployEnvironmentKind(wave) {
301
672
  if (environments.length === 0) {
302
673
  return null;
303
674
  }
304
- return environments.find((environment) => environment.isDefault)?.kind || environments[0]?.kind || null;
675
+ return (
676
+ environments.find((environment) => environment.isDefault)?.kind || environments[0]?.kind || null
677
+ );
678
+ }
679
+
680
+ function buildPromptHash({ ids, role, runtimeId, deployKind, promptText, bundles }) {
681
+ return hashText(
682
+ JSON.stringify({
683
+ ids,
684
+ role,
685
+ runtimeId,
686
+ deployKind,
687
+ promptText,
688
+ bundles: bundles.map((bundle) => ({
689
+ id: bundle.id,
690
+ bundleHash: bundle.bundleHash,
691
+ adapterPath: bundle.adapterPathByRuntime[runtimeId || "local"] || null,
692
+ activation: bundle.activation,
693
+ termination: bundle.termination,
694
+ permissions: bundle.permissions,
695
+ trust: bundle.trust,
696
+ referencePaths: bundle.referencePaths,
697
+ })),
698
+ }),
699
+ );
305
700
  }
306
701
 
307
702
  export function resolveSkillIdsForAgent(agent, wave, laneProfile) {
@@ -309,53 +704,106 @@ export function resolveSkillIdsForAgent(agent, wave, laneProfile) {
309
704
  const role = cleanText(agent?.executorResolved?.role).toLowerCase() || null;
310
705
  const runtimeId = cleanText(agent?.executorResolved?.id).toLowerCase() || null;
311
706
  const deployKind = cleanText(defaultDeployEnvironmentKind(wave)).toLowerCase() || null;
707
+ const configuredIds = uniqueStrings([
708
+ ...(skillsConfig.base || []),
709
+ ...(role ? skillsConfig.byRole?.[role] || [] : []),
710
+ ...(runtimeId ? skillsConfig.byRuntime?.[runtimeId] || [] : []),
711
+ ...(deployKind ? skillsConfig.byDeployKind?.[deployKind] || [] : []),
712
+ ]).map((skillId) => normalizeSkillId(skillId));
713
+ const explicitIds = normalizeSkillIdArray(agent?.skills, "agent.skills");
312
714
  return {
313
715
  role,
314
716
  runtimeId,
315
717
  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)),
718
+ configuredIds,
719
+ explicitIds,
720
+ ids: uniqueStrings([...configuredIds, ...explicitIds]).map((skillId) => normalizeSkillId(skillId)),
323
721
  };
324
722
  }
325
723
 
326
724
  export function resolveAgentSkills(agent, wave, options = {}) {
327
725
  const laneProfile = options.laneProfile || {};
328
726
  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");
727
+ const { configuredIds, explicitIds, role, runtimeId, deployKind } = resolveSkillIdsForAgent(
728
+ agent,
729
+ wave,
730
+ laneProfile,
731
+ );
732
+ const resolvedRuntimeId = runtimeId || "local";
733
+ const bundleCache = new Map();
734
+ const loadBundleCached = (skillId) => {
735
+ if (!bundleCache.has(skillId)) {
736
+ bundleCache.set(skillId, loadSkillBundle(skillId, { skillsDir: skillsConfig.dir }));
737
+ }
738
+ return bundleCache.get(skillId);
739
+ };
740
+ const activeConfiguredBundles = configuredIds
741
+ .map((skillId) => loadBundleCached(skillId))
742
+ .filter((bundle) =>
743
+ skillMatchesActivation(bundle, {
744
+ role,
745
+ runtimeId: resolvedRuntimeId,
746
+ deployKind,
747
+ }),
748
+ );
749
+ const explicitBundles = explicitIds.map((skillId) => loadBundleCached(skillId));
750
+ const bundles = [];
751
+ const seen = new Set();
752
+ for (const bundle of [...activeConfiguredBundles, ...explicitBundles]) {
753
+ if (seen.has(bundle.id)) {
754
+ continue;
755
+ }
756
+ seen.add(bundle.id);
757
+ bundles.push(bundle);
758
+ }
759
+ const promptText = renderSkillPromptText(bundles, resolvedRuntimeId);
760
+ const expandedPromptText = renderExpandedSkillText(bundles, resolvedRuntimeId);
761
+ const runtimeText = renderRuntimeOnlyText(bundles, resolvedRuntimeId);
333
762
  return {
334
763
  dir: skillsConfig.dir,
335
- ids,
764
+ ids: bundles.map((bundle) => bundle.id),
336
765
  role,
337
766
  runtime: runtimeId,
338
767
  deployKind,
339
768
  promptText,
340
- promptHash: hashText(promptText || JSON.stringify({ ids, role, runtimeId, deployKind })),
769
+ expandedPromptText,
770
+ promptHash: buildPromptHash({
771
+ ids: bundles.map((bundle) => bundle.id),
772
+ role,
773
+ runtimeId: resolvedRuntimeId,
774
+ deployKind,
775
+ promptText,
776
+ bundles,
777
+ }),
341
778
  runtimeText,
342
779
  bundles: bundles.map((bundle) => ({
343
780
  id: bundle.id,
344
781
  title: bundle.title,
345
782
  description: bundle.description,
783
+ version: bundle.version,
784
+ tags: bundle.tags,
785
+ activation: bundle.activation,
786
+ termination: bundle.termination,
787
+ permissions: bundle.permissions,
788
+ trust: bundle.trust,
789
+ evalCases: bundle.evalCases,
346
790
  bundlePath: bundle.bundlePath,
347
791
  manifestPath: bundle.manifestPath,
348
792
  skillPath: bundle.skillPath,
349
- adapterPath: bundle.adapterPathByRuntime[runtimeId] || null,
793
+ adapterPath: bundle.adapterPathByRuntime[resolvedRuntimeId] || null,
794
+ referencePaths: bundle.referencePaths,
350
795
  bundleHash: bundle.bundleHash,
351
796
  sourceFiles: bundle.sourceFiles.map((entry) => entry.path),
352
797
  })),
353
- codexAddDirs: uniqueStrings(bundles.map((bundle) => bundle.bundlePath)),
798
+ codexAddDirs: uniqueStrings(bundles.map((bundle) => bundle.bundlePath), { lowerCase: false }),
354
799
  opencodeFiles: uniqueStrings(
355
800
  bundles.flatMap((bundle) => [
801
+ bundle.manifestPath,
356
802
  bundle.skillPath,
357
- bundle.adapterPathByRuntime[runtimeId] || null,
803
+ bundle.adapterPathByRuntime[resolvedRuntimeId] || null,
804
+ ...bundle.referencePaths,
358
805
  ]),
806
+ { lowerCase: false },
359
807
  ),
360
808
  opencodeInstructions: promptText ? [promptText] : [],
361
809
  };
@@ -384,10 +832,18 @@ export function summarizeResolvedSkills(resolvedSkills) {
384
832
  id: bundle.id,
385
833
  title: bundle.title || null,
386
834
  description: bundle.description || null,
835
+ version: bundle.version || null,
836
+ tags: Array.isArray(bundle.tags) ? bundle.tags.slice() : [],
837
+ activation: bundle.activation || null,
838
+ termination: bundle.termination || null,
839
+ permissions: bundle.permissions || null,
840
+ trust: bundle.trust || null,
841
+ evalCases: Array.isArray(bundle.evalCases) ? bundle.evalCases.slice() : [],
387
842
  bundlePath: bundle.bundlePath,
388
843
  manifestPath: bundle.manifestPath,
389
844
  skillPath: bundle.skillPath,
390
845
  adapterPath: bundle.adapterPath || null,
846
+ referencePaths: Array.isArray(bundle.referencePaths) ? bundle.referencePaths.slice() : [],
391
847
  bundleHash: bundle.bundleHash || null,
392
848
  sourceFiles: Array.isArray(bundle.sourceFiles) ? bundle.sourceFiles.slice() : [],
393
849
  }))
@@ -401,17 +857,18 @@ export function writeResolvedSkillArtifacts(overlayDir, resolvedSkills) {
401
857
  return null;
402
858
  }
403
859
  const promptPath = path.join(overlayDir, "skills.resolved.md");
860
+ const expandedPromptPath = path.join(overlayDir, "skills.expanded.md");
404
861
  const metadataPath = path.join(overlayDir, "skills.metadata.json");
405
862
  const runtimePromptPath =
406
- resolvedSkills.runtime
407
- ? path.join(overlayDir, `${resolvedSkills.runtime}-skills.txt`)
408
- : null;
863
+ resolvedSkills.runtime ? path.join(overlayDir, `${resolvedSkills.runtime}-skills.txt`) : null;
409
864
  const artifacts = {
410
865
  promptPath: repoRelativePath(promptPath),
866
+ expandedPromptPath: repoRelativePath(expandedPromptPath),
411
867
  metadataPath: repoRelativePath(metadataPath),
412
868
  runtimePromptPath: runtimePromptPath ? repoRelativePath(runtimePromptPath) : null,
413
869
  };
414
870
  writeTextAtomic(promptPath, `${resolvedSkills.promptText}\n`);
871
+ writeTextAtomic(expandedPromptPath, `${resolvedSkills.expandedPromptText || resolvedSkills.promptText}\n`);
415
872
  if (runtimePromptPath) {
416
873
  writeTextAtomic(runtimePromptPath, `${resolvedSkills.runtimeText || resolvedSkills.promptText}\n`);
417
874
  }
@@ -425,24 +882,98 @@ export function writeResolvedSkillArtifacts(overlayDir, resolvedSkills) {
425
882
  return artifacts;
426
883
  }
427
884
 
428
- export function validateLaneSkillConfiguration(laneProfile) {
885
+ export function validateLaneSkillConfiguration(laneProfile, options = {}) {
429
886
  const skillsConfig = laneProfile?.skills || emptySkillsConfig();
430
- const referencedSkillIds = uniqueStrings([
887
+ const allowedDeployKinds = new Set([
888
+ ...SUPPORTED_SKILL_DEPLOY_KINDS,
889
+ ...(Array.isArray(options.allowedDeployKinds) ? options.allowedDeployKinds : []),
890
+ ]);
891
+ const allSkillIds = uniqueStrings([
892
+ ...listSkillBundleIds(skillsConfig.dir || DEFAULT_SKILLS_DIR),
431
893
  ...(skillsConfig.base || []),
432
894
  ...Object.values(skillsConfig.byRole || {}).flat(),
433
895
  ...Object.values(skillsConfig.byRuntime || {}).flat(),
434
896
  ...Object.values(skillsConfig.byDeployKind || {}).flat(),
435
897
  ]).map((skillId) => normalizeSkillId(skillId));
436
898
  const errors = [];
437
- for (const skillId of referencedSkillIds) {
899
+ const bundles = new Map();
900
+ const loadBundleSafe = (skillId) => {
901
+ if (bundles.has(skillId)) {
902
+ return bundles.get(skillId);
903
+ }
438
904
  try {
439
- loadSkillBundle(skillId, { skillsDir: skillsConfig.dir });
905
+ const bundle = loadSkillBundle(skillId, { skillsDir: skillsConfig.dir });
906
+ bundles.set(skillId, bundle);
907
+ return bundle;
440
908
  } catch (error) {
441
909
  errors.push(error instanceof Error ? error.message : String(error));
910
+ return null;
442
911
  }
912
+ };
913
+ for (const skillId of allSkillIds) {
914
+ loadBundleSafe(skillId);
915
+ }
916
+ for (const [role, skillIds] of Object.entries(skillsConfig.byRole || {})) {
917
+ for (const skillId of skillIds) {
918
+ const bundle = loadBundleSafe(skillId);
919
+ if (!bundle) {
920
+ continue;
921
+ }
922
+ if (bundle.activation.roles.length > 0 && !bundle.activation.roles.includes(role)) {
923
+ errors.push(
924
+ `Skill "${skillId}" is configured under skills.byRole.${role} but manifest activation.roles excludes "${role}"`,
925
+ );
926
+ }
927
+ }
928
+ }
929
+ for (const [runtimeId, skillIds] of Object.entries(skillsConfig.byRuntime || {})) {
930
+ for (const skillId of skillIds) {
931
+ const bundle = loadBundleSafe(skillId);
932
+ if (!bundle) {
933
+ continue;
934
+ }
935
+ if (
936
+ bundle.activation.runtimes.length > 0 &&
937
+ !bundle.activation.runtimes.includes(runtimeId)
938
+ ) {
939
+ errors.push(
940
+ `Skill "${skillId}" is configured under skills.byRuntime.${runtimeId} but manifest activation.runtimes excludes "${runtimeId}"`,
941
+ );
942
+ }
943
+ }
944
+ }
945
+ for (const [deployKind, skillIds] of Object.entries(skillsConfig.byDeployKind || {})) {
946
+ if (!allowedDeployKinds.has(deployKind)) {
947
+ errors.push(
948
+ `skills.byDeployKind.${deployKind} is not a supported selector key for this lane`,
949
+ );
950
+ continue;
951
+ }
952
+ for (const skillId of skillIds) {
953
+ const bundle = loadBundleSafe(skillId);
954
+ if (!bundle) {
955
+ continue;
956
+ }
957
+ if (
958
+ bundle.activation.deployKinds.length > 0 &&
959
+ !bundle.activation.deployKinds.includes(deployKind)
960
+ ) {
961
+ errors.push(
962
+ `Skill "${skillId}" is configured under skills.byDeployKind.${deployKind} but manifest activation.deployKinds excludes "${deployKind}"`,
963
+ );
964
+ }
965
+ }
966
+ }
967
+ let evaluatedCases = 0;
968
+ for (const bundle of bundles.values()) {
969
+ const evalResult = evaluateSkillBundleCases(bundle);
970
+ errors.push(...evalResult.errors);
971
+ evaluatedCases += evalResult.evaluatedCases;
443
972
  }
444
973
  return {
445
974
  ok: errors.length === 0,
446
975
  errors,
976
+ evaluatedBundles: bundles.size,
977
+ evaluatedCases,
447
978
  };
448
979
  }