@sweny-ai/core 0.1.89 → 0.1.91

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.
@@ -14,14 +14,7 @@
14
14
  * and `../skills/index.js`; this CLI is a thin authoring shell over them.
15
15
  */
16
16
  import { Command } from "commander";
17
- import { type SkillCategory } from "../types.js";
18
- declare const HARNESS_DIRS: {
19
- readonly claude: ".claude/skills";
20
- readonly sweny: ".sweny/skills";
21
- readonly agents: ".agents/skills";
22
- readonly gemini: ".gemini/skills";
23
- };
24
- type HarnessKey = keyof typeof HARNESS_DIRS;
17
+ import { type SkillCategory, type SkillHarnessKey } from "../types.js";
25
18
  /** Build the SKILL.md body for a fresh scaffold. Exported for tests. */
26
19
  export declare function renderSkillTemplate(opts: {
27
20
  id: string;
@@ -31,7 +24,7 @@ export declare function renderSkillTemplate(opts: {
31
24
  interface NewOptions {
32
25
  description?: string;
33
26
  category?: string;
34
- harness?: HarnessKey;
27
+ harness?: SkillHarnessKey;
35
28
  force?: boolean;
36
29
  }
37
30
  /**
package/dist/cli/skill.js CHANGED
@@ -18,15 +18,11 @@ import * as path from "node:path";
18
18
  import chalk from "chalk";
19
19
  import { builtinSkills } from "../skills/index.js";
20
20
  import { configuredSkillsWithDiagnostics, discoverSkillsWithDiagnostics } from "../skills/custom-loader.js";
21
- import { SKILL_CATEGORIES } from "../types.js";
22
- // Mirror of the loader's regex so authoring-side validation matches discovery.
23
- const VALID_SKILL_ID = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
24
- const HARNESS_DIRS = {
25
- claude: ".claude/skills",
26
- sweny: ".sweny/skills",
27
- agents: ".agents/skills",
28
- gemini: ".gemini/skills",
29
- };
21
+ import { SKILL_CATEGORIES, SKILL_HARNESSES, isValidSkillId, } from "../types.js";
22
+ // Lookup table: harness key -> directory. Built once from SKILL_HARNESSES so
23
+ // the CLI cannot drift from the loader's scan list.
24
+ const HARNESS_DIRS = Object.fromEntries(SKILL_HARNESSES.map((h) => [h.key, h.path]));
25
+ const HARNESS_KEYS = SKILL_HARNESSES.map((h) => h.key);
30
26
  /** Build the SKILL.md body for a fresh scaffold. Exported for tests. */
31
27
  export function renderSkillTemplate(opts) {
32
28
  const { id, description, category } = opts;
@@ -87,7 +83,7 @@ export function renderSkillTemplate(opts) {
87
83
  */
88
84
  export function runSkillNew(idArg, options, cwd = process.cwd()) {
89
85
  const id = idArg.toLowerCase();
90
- if (!VALID_SKILL_ID.test(id) || id.includes("--") || id.length > 64) {
86
+ if (!isValidSkillId(id)) {
91
87
  console.error(chalk.red(` Invalid skill id "${idArg}".\n Skill ids must be lowercase alphanumeric + hyphens (no consecutive hyphens), max 64 chars.`));
92
88
  process.exit(2);
93
89
  return;
@@ -102,7 +98,7 @@ export function runSkillNew(idArg, options, cwd = process.cwd()) {
102
98
  const harness = options.harness ?? "claude";
103
99
  const baseDir = HARNESS_DIRS[harness];
104
100
  if (!baseDir) {
105
- console.error(chalk.red(` Unknown harness "${harness}". Allowed: ${Object.keys(HARNESS_DIRS).join(", ")}`));
101
+ console.error(chalk.red(` Unknown harness "${harness}". Allowed: ${HARNESS_KEYS.join(", ")}`));
106
102
  process.exit(2);
107
103
  return;
108
104
  }
@@ -197,7 +193,7 @@ export function registerSkillCommand(program) {
197
193
  .description("Scaffold a new SKILL.md in .claude/skills/<id>/")
198
194
  .option("-d, --description <text>", "One-line description (required field in frontmatter)")
199
195
  .option("-c, --category <category>", `Skill category (${SKILL_CATEGORIES.join("|")})`, "general")
200
- .option("--harness <harness>", `Where to place the skill (${Object.keys(HARNESS_DIRS).join("|")})`, "claude")
196
+ .option("--harness <harness>", `Where to place the skill (${HARNESS_KEYS.join("|")})`, "claude")
201
197
  .option("--force", "Overwrite an existing SKILL.md")
202
198
  .action((id, options) => runSkillNew(id, options));
203
199
  skillCmd
package/dist/schema.d.ts CHANGED
@@ -42,7 +42,7 @@ export declare const toolZ: z.ZodObject<{
42
42
  description: string;
43
43
  input_schema: Record<string, unknown>;
44
44
  }>;
45
- export declare const skillCategoryZ: z.ZodEnum<["git", "observability", "tasks", "notification", "general"]>;
45
+ export declare const skillCategoryZ: z.ZodEnum<["general", "git", "tasks", "notification", "observability", "data"]>;
46
46
  export declare const mcpServerConfigZ: z.ZodEffects<z.ZodObject<{
47
47
  type: z.ZodOptional<z.ZodEnum<["stdio", "http"]>>;
48
48
  command: z.ZodOptional<z.ZodString>;
@@ -172,7 +172,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
172
172
  id: z.ZodString;
173
173
  name: z.ZodString;
174
174
  description: z.ZodString;
175
- category: z.ZodEnum<["git", "observability", "tasks", "notification", "general"]>;
175
+ category: z.ZodEnum<["general", "git", "tasks", "notification", "observability", "data"]>;
176
176
  config: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
177
177
  description: z.ZodString;
178
178
  required: z.ZodOptional<z.ZodBoolean>;
@@ -241,7 +241,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
241
241
  id: string;
242
242
  name: string;
243
243
  description: string;
244
- category: "general" | "git" | "tasks" | "notification" | "observability";
244
+ category: "general" | "git" | "tasks" | "notification" | "observability" | "data";
245
245
  config: Record<string, {
246
246
  description: string;
247
247
  required?: boolean | undefined;
@@ -266,7 +266,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
266
266
  id: string;
267
267
  name: string;
268
268
  description: string;
269
- category: "general" | "git" | "tasks" | "notification" | "observability";
269
+ category: "general" | "git" | "tasks" | "notification" | "observability" | "data";
270
270
  instruction?: string | undefined;
271
271
  mcp?: {
272
272
  url?: string | undefined;
@@ -291,7 +291,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
291
291
  id: string;
292
292
  name: string;
293
293
  description: string;
294
- category: "general" | "git" | "tasks" | "notification" | "observability";
294
+ category: "general" | "git" | "tasks" | "notification" | "observability" | "data";
295
295
  config: Record<string, {
296
296
  description: string;
297
297
  required?: boolean | undefined;
@@ -316,7 +316,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
316
316
  id: string;
317
317
  name: string;
318
318
  description: string;
319
- category: "general" | "git" | "tasks" | "notification" | "observability";
319
+ category: "general" | "git" | "tasks" | "notification" | "observability" | "data";
320
320
  instruction?: string | undefined;
321
321
  mcp?: {
322
322
  url?: string | undefined;
@@ -1375,10 +1375,25 @@ export declare const edgeZ: z.ZodObject<{
1375
1375
  when?: string | undefined;
1376
1376
  max_iterations?: number | undefined;
1377
1377
  }>;
1378
+ /**
1379
+ * Workflow type discriminator. Cloud uses this to route runs to a
1380
+ * type-specific renderer (Caught/Calibration/Missed for `pr_review`, flake
1381
+ * heatmap for `e2e_test`, source coverage delta for `monitor`, etc.).
1382
+ *
1383
+ * The field is optional on the workflow YAML; absence defaults to `generic`,
1384
+ * which gets a baseline renderer. Marketplace templates published in v1+
1385
+ * are required to declare it.
1386
+ *
1387
+ * Adding a new value here is a deliberate spec change: it requires a new
1388
+ * cloud renderer + metric schema. Don't extend casually.
1389
+ */
1390
+ export declare const workflowTypeZ: z.ZodEnum<["pr_review", "e2e_test", "content_generation", "monitor", "data_sync", "generic"]>;
1391
+ export type WorkflowType = z.infer<typeof workflowTypeZ>;
1378
1392
  export declare const workflowZ: z.ZodObject<{
1379
1393
  id: z.ZodString;
1380
1394
  name: z.ZodString;
1381
1395
  description: z.ZodDefault<z.ZodString>;
1396
+ workflow_type: z.ZodOptional<z.ZodEnum<["pr_review", "e2e_test", "content_generation", "monitor", "data_sync", "generic"]>>;
1382
1397
  nodes: z.ZodRecord<z.ZodString, z.ZodObject<{
1383
1398
  name: z.ZodString;
1384
1399
  instruction: z.ZodUnion<[z.ZodString, z.ZodObject<{
@@ -2250,6 +2265,7 @@ export declare const workflowZ: z.ZodObject<{
2250
2265
  type?: string | undefined;
2251
2266
  })[] | undefined;
2252
2267
  judge_model?: string | undefined;
2268
+ workflow_type?: "pr_review" | "e2e_test" | "content_generation" | "monitor" | "data_sync" | "generic" | undefined;
2253
2269
  judge_budget?: number | undefined;
2254
2270
  }, {
2255
2271
  id: string;
@@ -2381,6 +2397,7 @@ export declare const workflowZ: z.ZodObject<{
2381
2397
  type?: string | undefined;
2382
2398
  })[] | undefined;
2383
2399
  judge_model?: string | undefined;
2400
+ workflow_type?: "pr_review" | "e2e_test" | "content_generation" | "monitor" | "data_sync" | "generic" | undefined;
2384
2401
  judge_budget?: number | undefined;
2385
2402
  }>;
2386
2403
  /** Parse + validate a raw object as a Workflow. Throws on invalid input. */
@@ -2514,6 +2531,7 @@ export declare function parseWorkflow(raw: unknown): {
2514
2531
  type?: string | undefined;
2515
2532
  })[] | undefined;
2516
2533
  judge_model?: string | undefined;
2534
+ workflow_type?: "pr_review" | "e2e_test" | "content_generation" | "monitor" | "data_sync" | "generic" | undefined;
2517
2535
  judge_budget?: number | undefined;
2518
2536
  };
2519
2537
  export interface WorkflowError {
@@ -2767,6 +2785,11 @@ export declare const workflowJsonSchema: {
2767
2785
  };
2768
2786
  readonly description: "Background knowledge prepended to every node's instruction.";
2769
2787
  };
2788
+ readonly workflow_type: {
2789
+ readonly type: "string";
2790
+ readonly enum: readonly ["pr_review", "e2e_test", "content_generation", "monitor", "data_sync", "generic"];
2791
+ readonly description: "Workflow type discriminator. Cloud uses this to route runs to a type-specific renderer. Optional; absence defaults to 'generic'.";
2792
+ };
2770
2793
  readonly judge_model: {
2771
2794
  readonly type: "string";
2772
2795
  readonly minLength: 1;
@@ -3002,3 +3025,160 @@ export declare const workflowJsonSchema: {
3002
3025
  };
3003
3026
  };
3004
3027
  };
3028
+ /**
3029
+ * Skill JSON Schema, generated from runtime constants.
3030
+ *
3031
+ * Companion to {@link workflowJsonSchema}: published at
3032
+ * https://spec.sweny.ai/schemas/skill.json by the
3033
+ * `write-public-schema.mjs` build step. Validates the structural shape
3034
+ * of a Skill (id, name, description, category, config, plus optional
3035
+ * tools/instruction/mcp).
3036
+ *
3037
+ * Every enum and the `id` pattern + maxLength are imported from
3038
+ * `types.ts` so adding a category, harness, transport, or changing the
3039
+ * id rule updates the published schema with no manual sync.
3040
+ */
3041
+ export declare const skillJsonSchema: {
3042
+ readonly $schema: "https://json-schema.org/draft/2020-12/schema";
3043
+ readonly $id: "https://spec.sweny.ai/schemas/skill.json";
3044
+ readonly title: "SWEny Skill";
3045
+ readonly description: "A composable tool bundle that provides capabilities to workflow nodes.";
3046
+ readonly type: "object";
3047
+ readonly required: readonly ["id", "name", "description", "category", "config"];
3048
+ readonly anyOf: readonly [{
3049
+ readonly required: readonly ["tools"];
3050
+ }, {
3051
+ readonly required: readonly ["instruction"];
3052
+ }, {
3053
+ readonly required: readonly ["mcp"];
3054
+ }];
3055
+ readonly additionalProperties: false;
3056
+ readonly properties: {
3057
+ readonly id: {
3058
+ readonly type: "string";
3059
+ readonly minLength: 1;
3060
+ readonly maxLength: 64;
3061
+ readonly pattern: string;
3062
+ readonly description: "Unique skill identifier. Referenced by nodes' skills arrays. Lowercase kebab-case recommended.";
3063
+ };
3064
+ readonly name: {
3065
+ readonly type: "string";
3066
+ readonly minLength: 1;
3067
+ readonly description: "Human-readable skill name.";
3068
+ };
3069
+ readonly description: {
3070
+ readonly type: "string";
3071
+ readonly description: "What this skill provides.";
3072
+ };
3073
+ readonly category: {
3074
+ readonly type: "string";
3075
+ readonly enum: readonly ["general", "git", "tasks", "notification", "observability", "data"];
3076
+ readonly description: "Functional category.";
3077
+ };
3078
+ readonly config: {
3079
+ readonly type: "object";
3080
+ readonly description: "Configuration fields required by this skill.";
3081
+ readonly additionalProperties: {
3082
+ readonly $ref: "#/$defs/ConfigField";
3083
+ };
3084
+ };
3085
+ readonly tools: {
3086
+ readonly type: "array";
3087
+ readonly description: "Tools this skill provides to nodes.";
3088
+ readonly items: {
3089
+ readonly $ref: "#/$defs/Tool";
3090
+ };
3091
+ };
3092
+ readonly instruction: {
3093
+ readonly type: "string";
3094
+ readonly description: "Natural language expertise injected into the node prompt when this skill is referenced.";
3095
+ };
3096
+ readonly mcp: {
3097
+ readonly $ref: "#/$defs/McpServerConfig";
3098
+ readonly description: "External MCP server definition wired for nodes referencing this skill.";
3099
+ };
3100
+ };
3101
+ readonly $defs: {
3102
+ readonly ConfigField: {
3103
+ readonly type: "object";
3104
+ readonly required: readonly ["description"];
3105
+ readonly additionalProperties: false;
3106
+ readonly properties: {
3107
+ readonly description: {
3108
+ readonly type: "string";
3109
+ readonly description: "Human-readable description of this config field.";
3110
+ };
3111
+ readonly required: {
3112
+ readonly type: "boolean";
3113
+ readonly default: false;
3114
+ readonly description: "Whether this field must be provided for the skill to function.";
3115
+ };
3116
+ readonly env: {
3117
+ readonly type: "string";
3118
+ readonly description: "Default environment variable to read this value from.";
3119
+ };
3120
+ };
3121
+ };
3122
+ readonly Tool: {
3123
+ readonly type: "object";
3124
+ readonly required: readonly ["name", "description", "input_schema"];
3125
+ readonly additionalProperties: false;
3126
+ readonly properties: {
3127
+ readonly name: {
3128
+ readonly type: "string";
3129
+ readonly description: "Tool name. Must be unique within the skill.";
3130
+ };
3131
+ readonly description: {
3132
+ readonly type: "string";
3133
+ readonly description: "What this tool does. Provided to the AI model for tool selection.";
3134
+ };
3135
+ readonly input_schema: {
3136
+ readonly type: "object";
3137
+ readonly description: "JSON Schema defining the tool's input parameters.";
3138
+ };
3139
+ };
3140
+ };
3141
+ readonly McpServerConfig: {
3142
+ readonly type: "object";
3143
+ readonly description: "External MCP server definition.";
3144
+ readonly additionalProperties: false;
3145
+ readonly properties: {
3146
+ readonly type: {
3147
+ readonly type: "string";
3148
+ readonly enum: readonly ["stdio", "http"];
3149
+ readonly description: "Transport type. Inferred from presence of command (stdio) or url (http) when omitted.";
3150
+ };
3151
+ readonly command: {
3152
+ readonly type: "string";
3153
+ readonly description: "Spawn command (stdio transport).";
3154
+ };
3155
+ readonly args: {
3156
+ readonly type: "array";
3157
+ readonly items: {
3158
+ readonly type: "string";
3159
+ };
3160
+ readonly description: "Arguments for the command.";
3161
+ };
3162
+ readonly url: {
3163
+ readonly type: "string";
3164
+ readonly format: "uri";
3165
+ readonly description: "HTTP endpoint (HTTP transport).";
3166
+ };
3167
+ readonly headers: {
3168
+ readonly type: "object";
3169
+ readonly additionalProperties: {
3170
+ readonly type: "string";
3171
+ };
3172
+ readonly description: "HTTP headers (HTTP transport only).";
3173
+ };
3174
+ readonly env: {
3175
+ readonly type: "object";
3176
+ readonly additionalProperties: {
3177
+ readonly type: "string";
3178
+ };
3179
+ readonly description: "Environment variable names the server needs. Values are descriptions, not secrets.";
3180
+ };
3181
+ };
3182
+ };
3183
+ };
3184
+ };
package/dist/schema.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * parsing untrusted input (JSON/YAML import, Studio editor).
8
8
  */
9
9
  import { z } from "zod";
10
+ import { EVALUATOR_KINDS, EVAL_POLICIES, MCP_TRANSPORTS, REQUIRES_ON_FAIL, SKILL_CATEGORIES, SKILL_ID_MAX_LENGTH, SKILL_ID_PATTERN, } from "./types.js";
10
11
  import { sourceZ } from "./sources.js";
11
12
  export { sourceZ };
12
13
  // ─── Zod Schemas ─────────────────────────────────────────────────
@@ -26,10 +27,10 @@ export const toolZ = z.object({
26
27
  description: z.string(),
27
28
  input_schema: jsonSchemaZ,
28
29
  });
29
- export const skillCategoryZ = z.enum(["git", "observability", "tasks", "notification", "general"]);
30
+ export const skillCategoryZ = z.enum(SKILL_CATEGORIES);
30
31
  export const mcpServerConfigZ = z
31
32
  .object({
32
- type: z.enum(["stdio", "http"]).optional(),
33
+ type: z.enum(MCP_TRANSPORTS).optional(),
33
34
  command: z.string().optional(),
34
35
  args: z.array(z.string()).optional(),
35
36
  url: z.string().optional(),
@@ -90,8 +91,8 @@ export const outputMatchZ = z
90
91
  const operators = [m.equals !== undefined, m.in !== undefined, m.matches !== undefined];
91
92
  return operators.filter(Boolean).length === 1;
92
93
  }, { message: "output_matches entry must declare exactly one of: equals, in, matches" });
93
- export const evaluatorKindZ = z.enum(["value", "function", "judge"]);
94
- export const evalPolicyZ = z.enum(["all_pass", "any_pass", "weighted"]);
94
+ export const evaluatorKindZ = z.enum(EVALUATOR_KINDS);
95
+ export const evalPolicyZ = z.enum(EVAL_POLICIES);
95
96
  export const evaluatorRuleZ = z
96
97
  .object({
97
98
  any_tool_called: z.array(z.string().min(1)).min(1).optional(),
@@ -170,7 +171,7 @@ export const nodeRequiresZ = z
170
171
  .object({
171
172
  output_required: z.array(z.string().min(1)).min(1).optional(),
172
173
  output_matches: z.array(outputMatchZ).min(1).optional(),
173
- on_fail: z.enum(["fail", "skip"]).optional(),
174
+ on_fail: z.enum(REQUIRES_ON_FAIL).optional(),
174
175
  })
175
176
  .strict()
176
177
  .refine((r) => r.output_required !== undefined || r.output_matches !== undefined, {
@@ -208,6 +209,19 @@ export const edgeZ = z
208
209
  max_iterations: z.number().int().min(1).optional(),
209
210
  })
210
211
  .strict();
212
+ /**
213
+ * Workflow type discriminator. Cloud uses this to route runs to a
214
+ * type-specific renderer (Caught/Calibration/Missed for `pr_review`, flake
215
+ * heatmap for `e2e_test`, source coverage delta for `monitor`, etc.).
216
+ *
217
+ * The field is optional on the workflow YAML; absence defaults to `generic`,
218
+ * which gets a baseline renderer. Marketplace templates published in v1+
219
+ * are required to declare it.
220
+ *
221
+ * Adding a new value here is a deliberate spec change: it requires a new
222
+ * cloud renderer + metric schema. Don't extend casually.
223
+ */
224
+ export const workflowTypeZ = z.enum(["pr_review", "e2e_test", "content_generation", "monitor", "data_sync", "generic"]);
211
225
  // Fix #4 gap — workflowZ is intentionally NOT .strict(). The published
212
226
  // JSON Schema's `additionalProperties: false` only scopes the properties
213
227
  // block, but marketplace workflows routinely carry top-level metadata
@@ -218,6 +232,7 @@ export const workflowZ = z.object({
218
232
  id: z.string().min(1),
219
233
  name: z.string().min(1),
220
234
  description: z.string().default(""),
235
+ workflow_type: workflowTypeZ.optional(),
221
236
  nodes: z.record(nodeZ),
222
237
  edges: z.array(edgeZ),
223
238
  entry: z.string().min(1),
@@ -482,7 +497,7 @@ export const workflowJsonSchema = {
482
497
  minLength: 1,
483
498
  description: "Stable identifier for this evaluator. Used in EvalResult and retry preambles.",
484
499
  },
485
- kind: { type: "string", enum: ["value", "function", "judge"] },
500
+ kind: { type: "string", enum: [...EVALUATOR_KINDS] },
486
501
  rule: {
487
502
  type: "object",
488
503
  description: "Required for value and function kinds. Shape depends on kind.",
@@ -540,6 +555,11 @@ export const workflowJsonSchema = {
540
555
  items: { $ref: "#/$defs/Source" },
541
556
  description: "Background knowledge prepended to every node's instruction.",
542
557
  },
558
+ workflow_type: {
559
+ type: "string",
560
+ enum: ["pr_review", "e2e_test", "content_generation", "monitor", "data_sync", "generic"],
561
+ description: "Workflow type discriminator. Cloud uses this to route runs to a type-specific renderer. Optional; absence defaults to 'generic'.",
562
+ },
543
563
  judge_model: {
544
564
  type: "string",
545
565
  minLength: 1,
@@ -591,7 +611,7 @@ export const workflowJsonSchema = {
591
611
  },
592
612
  eval_policy: {
593
613
  type: "string",
594
- enum: ["all_pass", "any_pass", "weighted"],
614
+ enum: [...EVAL_POLICIES],
595
615
  default: "all_pass",
596
616
  description: "How evaluator results aggregate. v1 implements all_pass; the others are reserved.",
597
617
  },
@@ -618,7 +638,7 @@ export const workflowJsonSchema = {
618
638
  items: { $ref: "#/$defs/OutputMatch" },
619
639
  minItems: 1,
620
640
  },
621
- on_fail: { type: "string", enum: ["fail", "skip"] },
641
+ on_fail: { type: "string", enum: [...REQUIRES_ON_FAIL] },
622
642
  },
623
643
  },
624
644
  retry: {
@@ -689,7 +709,7 @@ export const workflowJsonSchema = {
689
709
  // Round 2: reject unknown keys to match Zod mcpServerConfigZ.strict().
690
710
  additionalProperties: false,
691
711
  properties: {
692
- type: { type: "string", enum: ["stdio", "http"] },
712
+ type: { type: "string", enum: [...MCP_TRANSPORTS] },
693
713
  command: { type: "string" },
694
714
  args: { type: "array", items: { type: "string" } },
695
715
  url: { type: "string" },
@@ -702,3 +722,144 @@ export const workflowJsonSchema = {
702
722
  },
703
723
  },
704
724
  };
725
+ /**
726
+ * Skill JSON Schema, generated from runtime constants.
727
+ *
728
+ * Companion to {@link workflowJsonSchema}: published at
729
+ * https://spec.sweny.ai/schemas/skill.json by the
730
+ * `write-public-schema.mjs` build step. Validates the structural shape
731
+ * of a Skill (id, name, description, category, config, plus optional
732
+ * tools/instruction/mcp).
733
+ *
734
+ * Every enum and the `id` pattern + maxLength are imported from
735
+ * `types.ts` so adding a category, harness, transport, or changing the
736
+ * id rule updates the published schema with no manual sync.
737
+ */
738
+ export const skillJsonSchema = {
739
+ $schema: "https://json-schema.org/draft/2020-12/schema",
740
+ $id: "https://spec.sweny.ai/schemas/skill.json",
741
+ title: "SWEny Skill",
742
+ description: "A composable tool bundle that provides capabilities to workflow nodes.",
743
+ type: "object",
744
+ required: ["id", "name", "description", "category", "config"],
745
+ anyOf: [{ required: ["tools"] }, { required: ["instruction"] }, { required: ["mcp"] }],
746
+ additionalProperties: false,
747
+ properties: {
748
+ id: {
749
+ type: "string",
750
+ minLength: 1,
751
+ maxLength: SKILL_ID_MAX_LENGTH,
752
+ pattern: SKILL_ID_PATTERN.source,
753
+ description: "Unique skill identifier. Referenced by nodes' skills arrays. Lowercase kebab-case recommended.",
754
+ },
755
+ name: {
756
+ type: "string",
757
+ minLength: 1,
758
+ description: "Human-readable skill name.",
759
+ },
760
+ description: {
761
+ type: "string",
762
+ description: "What this skill provides.",
763
+ },
764
+ category: {
765
+ type: "string",
766
+ enum: [...SKILL_CATEGORIES],
767
+ description: "Functional category.",
768
+ },
769
+ config: {
770
+ type: "object",
771
+ description: "Configuration fields required by this skill.",
772
+ additionalProperties: { $ref: "#/$defs/ConfigField" },
773
+ },
774
+ tools: {
775
+ type: "array",
776
+ description: "Tools this skill provides to nodes.",
777
+ items: { $ref: "#/$defs/Tool" },
778
+ },
779
+ instruction: {
780
+ type: "string",
781
+ description: "Natural language expertise injected into the node prompt when this skill is referenced.",
782
+ },
783
+ mcp: {
784
+ $ref: "#/$defs/McpServerConfig",
785
+ description: "External MCP server definition wired for nodes referencing this skill.",
786
+ },
787
+ },
788
+ $defs: {
789
+ ConfigField: {
790
+ type: "object",
791
+ required: ["description"],
792
+ additionalProperties: false,
793
+ properties: {
794
+ description: {
795
+ type: "string",
796
+ description: "Human-readable description of this config field.",
797
+ },
798
+ required: {
799
+ type: "boolean",
800
+ default: false,
801
+ description: "Whether this field must be provided for the skill to function.",
802
+ },
803
+ env: {
804
+ type: "string",
805
+ description: "Default environment variable to read this value from.",
806
+ },
807
+ },
808
+ },
809
+ Tool: {
810
+ type: "object",
811
+ required: ["name", "description", "input_schema"],
812
+ additionalProperties: false,
813
+ properties: {
814
+ name: {
815
+ type: "string",
816
+ description: "Tool name. Must be unique within the skill.",
817
+ },
818
+ description: {
819
+ type: "string",
820
+ description: "What this tool does. Provided to the AI model for tool selection.",
821
+ },
822
+ input_schema: {
823
+ type: "object",
824
+ description: "JSON Schema defining the tool's input parameters.",
825
+ },
826
+ },
827
+ },
828
+ McpServerConfig: {
829
+ type: "object",
830
+ description: "External MCP server definition.",
831
+ additionalProperties: false,
832
+ properties: {
833
+ type: {
834
+ type: "string",
835
+ enum: [...MCP_TRANSPORTS],
836
+ description: "Transport type. Inferred from presence of command (stdio) or url (http) when omitted.",
837
+ },
838
+ command: {
839
+ type: "string",
840
+ description: "Spawn command (stdio transport).",
841
+ },
842
+ args: {
843
+ type: "array",
844
+ items: { type: "string" },
845
+ description: "Arguments for the command.",
846
+ },
847
+ url: {
848
+ type: "string",
849
+ format: "uri",
850
+ description: "HTTP endpoint (HTTP transport).",
851
+ },
852
+ headers: {
853
+ type: "object",
854
+ additionalProperties: { type: "string" },
855
+ description: "HTTP headers (HTTP transport only).",
856
+ },
857
+ env: {
858
+ type: "object",
859
+ additionalProperties: { type: "string" },
860
+ description: "Environment variable names the server needs. Values are descriptions, not secrets.",
861
+ },
862
+ },
863
+ },
864
+ },
865
+ };
@@ -11,12 +11,15 @@
11
11
  import { readdirSync, readFileSync, existsSync } from "node:fs";
12
12
  import { join } from "node:path";
13
13
  import YAML from "yaml";
14
- import { SKILL_CATEGORIES } from "../types.js";
14
+ import { SKILL_CATEGORIES, SKILL_HARNESSES, isValidSkillId } from "../types.js";
15
15
  import { builtinSkills, isSkillConfigured } from "./index.js";
16
- /** Directories to scan, in ascending priority order. Last match wins. */
17
- const SKILL_DIRS = [".gemini/skills", ".agents/skills", ".claude/skills", ".sweny/skills"];
18
- /** Valid skill ID: lowercase alphanumeric + hyphens, no leading/trailing/consecutive hyphens, max 64 chars. */
19
- const VALID_SKILL_ID = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
16
+ /**
17
+ * Directories to scan, in ascending priority order. Last match wins.
18
+ *
19
+ * Derived from {@link SKILL_HARNESSES} so the loader and the CLI authoring
20
+ * side cannot disagree on what counts as a skill harness directory.
21
+ */
22
+ const SKILL_DIRS = SKILL_HARNESSES.map((h) => h.path);
20
23
  /**
21
24
  * Discover custom skills and surface diagnostics for broken ones.
22
25
  *
@@ -104,7 +107,7 @@ function parseSkillMd(content, path) {
104
107
  ? fm.category
105
108
  : "general";
106
109
  const id = String(fm.name);
107
- if (!VALID_SKILL_ID.test(id) || id.includes("--") || id.length > 64) {
110
+ if (!isValidSkillId(id)) {
108
111
  return {
109
112
  kind: "err",
110
113
  diagnostic: {