@sweny-ai/core 0.1.89 → 0.1.90

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.
@@ -15,9 +15,9 @@ import { parse as parseYaml } from "yaml";
15
15
  import { parseWorkflow, validateWorkflow } from "../schema.js";
16
16
  import { builtinSkills } from "../skills/index.js";
17
17
  import { discoverSkillsWithDiagnostics } from "../skills/custom-loader.js";
18
+ import { isValidSkillId } from "../types.js";
18
19
  // ── Types ──────────────────────────────────────────────────────────────
19
20
  const VALID_CATEGORIES = ["triage", "security", "devops", "code-review", "testing", "content", "ops"];
20
- const VALID_SKILL_ID = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
21
21
  // ── Validation helpers ─────────────────────────────────────────────────
22
22
  export function validateWorkflowFile(filePath) {
23
23
  const errors = [];
@@ -119,8 +119,8 @@ export function validateSkillDir(dirPath) {
119
119
  if (!id) {
120
120
  errors.push("Missing 'name' field in frontmatter");
121
121
  }
122
- else if (!VALID_SKILL_ID.test(id) || id.includes("--") || id.length > 64) {
123
- errors.push(`Invalid skill ID: "${id}" — must be lowercase, hyphens, no consecutive hyphens, max 64 chars`);
122
+ else if (!isValidSkillId(id)) {
123
+ errors.push(`Invalid skill ID: "${id}" (must be lowercase alphanumeric + hyphens, no consecutive hyphens, max 64 chars)`);
124
124
  }
125
125
  // Check directory name matches skill name
126
126
  const dirName = path.basename(dirPath);
@@ -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;
@@ -3002,3 +3002,160 @@ export declare const workflowJsonSchema: {
3002
3002
  };
3003
3003
  };
3004
3004
  };
3005
+ /**
3006
+ * Skill JSON Schema, generated from runtime constants.
3007
+ *
3008
+ * Companion to {@link workflowJsonSchema}: published at
3009
+ * https://spec.sweny.ai/schemas/skill.json by the
3010
+ * `write-public-schema.mjs` build step. Validates the structural shape
3011
+ * of a Skill (id, name, description, category, config, plus optional
3012
+ * tools/instruction/mcp).
3013
+ *
3014
+ * Every enum and the `id` pattern + maxLength are imported from
3015
+ * `types.ts` so adding a category, harness, transport, or changing the
3016
+ * id rule updates the published schema with no manual sync.
3017
+ */
3018
+ export declare const skillJsonSchema: {
3019
+ readonly $schema: "https://json-schema.org/draft/2020-12/schema";
3020
+ readonly $id: "https://spec.sweny.ai/schemas/skill.json";
3021
+ readonly title: "SWEny Skill";
3022
+ readonly description: "A composable tool bundle that provides capabilities to workflow nodes.";
3023
+ readonly type: "object";
3024
+ readonly required: readonly ["id", "name", "description", "category", "config"];
3025
+ readonly anyOf: readonly [{
3026
+ readonly required: readonly ["tools"];
3027
+ }, {
3028
+ readonly required: readonly ["instruction"];
3029
+ }, {
3030
+ readonly required: readonly ["mcp"];
3031
+ }];
3032
+ readonly additionalProperties: false;
3033
+ readonly properties: {
3034
+ readonly id: {
3035
+ readonly type: "string";
3036
+ readonly minLength: 1;
3037
+ readonly maxLength: 64;
3038
+ readonly pattern: string;
3039
+ readonly description: "Unique skill identifier. Referenced by nodes' skills arrays. Lowercase kebab-case recommended.";
3040
+ };
3041
+ readonly name: {
3042
+ readonly type: "string";
3043
+ readonly minLength: 1;
3044
+ readonly description: "Human-readable skill name.";
3045
+ };
3046
+ readonly description: {
3047
+ readonly type: "string";
3048
+ readonly description: "What this skill provides.";
3049
+ };
3050
+ readonly category: {
3051
+ readonly type: "string";
3052
+ readonly enum: readonly ["general", "git", "tasks", "notification", "observability", "data"];
3053
+ readonly description: "Functional category.";
3054
+ };
3055
+ readonly config: {
3056
+ readonly type: "object";
3057
+ readonly description: "Configuration fields required by this skill.";
3058
+ readonly additionalProperties: {
3059
+ readonly $ref: "#/$defs/ConfigField";
3060
+ };
3061
+ };
3062
+ readonly tools: {
3063
+ readonly type: "array";
3064
+ readonly description: "Tools this skill provides to nodes.";
3065
+ readonly items: {
3066
+ readonly $ref: "#/$defs/Tool";
3067
+ };
3068
+ };
3069
+ readonly instruction: {
3070
+ readonly type: "string";
3071
+ readonly description: "Natural language expertise injected into the node prompt when this skill is referenced.";
3072
+ };
3073
+ readonly mcp: {
3074
+ readonly $ref: "#/$defs/McpServerConfig";
3075
+ readonly description: "External MCP server definition wired for nodes referencing this skill.";
3076
+ };
3077
+ };
3078
+ readonly $defs: {
3079
+ readonly ConfigField: {
3080
+ readonly type: "object";
3081
+ readonly required: readonly ["description"];
3082
+ readonly additionalProperties: false;
3083
+ readonly properties: {
3084
+ readonly description: {
3085
+ readonly type: "string";
3086
+ readonly description: "Human-readable description of this config field.";
3087
+ };
3088
+ readonly required: {
3089
+ readonly type: "boolean";
3090
+ readonly default: false;
3091
+ readonly description: "Whether this field must be provided for the skill to function.";
3092
+ };
3093
+ readonly env: {
3094
+ readonly type: "string";
3095
+ readonly description: "Default environment variable to read this value from.";
3096
+ };
3097
+ };
3098
+ };
3099
+ readonly Tool: {
3100
+ readonly type: "object";
3101
+ readonly required: readonly ["name", "description", "input_schema"];
3102
+ readonly additionalProperties: false;
3103
+ readonly properties: {
3104
+ readonly name: {
3105
+ readonly type: "string";
3106
+ readonly description: "Tool name. Must be unique within the skill.";
3107
+ };
3108
+ readonly description: {
3109
+ readonly type: "string";
3110
+ readonly description: "What this tool does. Provided to the AI model for tool selection.";
3111
+ };
3112
+ readonly input_schema: {
3113
+ readonly type: "object";
3114
+ readonly description: "JSON Schema defining the tool's input parameters.";
3115
+ };
3116
+ };
3117
+ };
3118
+ readonly McpServerConfig: {
3119
+ readonly type: "object";
3120
+ readonly description: "External MCP server definition.";
3121
+ readonly additionalProperties: false;
3122
+ readonly properties: {
3123
+ readonly type: {
3124
+ readonly type: "string";
3125
+ readonly enum: readonly ["stdio", "http"];
3126
+ readonly description: "Transport type. Inferred from presence of command (stdio) or url (http) when omitted.";
3127
+ };
3128
+ readonly command: {
3129
+ readonly type: "string";
3130
+ readonly description: "Spawn command (stdio transport).";
3131
+ };
3132
+ readonly args: {
3133
+ readonly type: "array";
3134
+ readonly items: {
3135
+ readonly type: "string";
3136
+ };
3137
+ readonly description: "Arguments for the command.";
3138
+ };
3139
+ readonly url: {
3140
+ readonly type: "string";
3141
+ readonly format: "uri";
3142
+ readonly description: "HTTP endpoint (HTTP transport).";
3143
+ };
3144
+ readonly headers: {
3145
+ readonly type: "object";
3146
+ readonly additionalProperties: {
3147
+ readonly type: "string";
3148
+ };
3149
+ readonly description: "HTTP headers (HTTP transport only).";
3150
+ };
3151
+ readonly env: {
3152
+ readonly type: "object";
3153
+ readonly additionalProperties: {
3154
+ readonly type: "string";
3155
+ };
3156
+ readonly description: "Environment variable names the server needs. Values are descriptions, not secrets.";
3157
+ };
3158
+ };
3159
+ };
3160
+ };
3161
+ };
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, {
@@ -482,7 +483,7 @@ export const workflowJsonSchema = {
482
483
  minLength: 1,
483
484
  description: "Stable identifier for this evaluator. Used in EvalResult and retry preambles.",
484
485
  },
485
- kind: { type: "string", enum: ["value", "function", "judge"] },
486
+ kind: { type: "string", enum: [...EVALUATOR_KINDS] },
486
487
  rule: {
487
488
  type: "object",
488
489
  description: "Required for value and function kinds. Shape depends on kind.",
@@ -591,7 +592,7 @@ export const workflowJsonSchema = {
591
592
  },
592
593
  eval_policy: {
593
594
  type: "string",
594
- enum: ["all_pass", "any_pass", "weighted"],
595
+ enum: [...EVAL_POLICIES],
595
596
  default: "all_pass",
596
597
  description: "How evaluator results aggregate. v1 implements all_pass; the others are reserved.",
597
598
  },
@@ -618,7 +619,7 @@ export const workflowJsonSchema = {
618
619
  items: { $ref: "#/$defs/OutputMatch" },
619
620
  minItems: 1,
620
621
  },
621
- on_fail: { type: "string", enum: ["fail", "skip"] },
622
+ on_fail: { type: "string", enum: [...REQUIRES_ON_FAIL] },
622
623
  },
623
624
  },
624
625
  retry: {
@@ -689,7 +690,7 @@ export const workflowJsonSchema = {
689
690
  // Round 2: reject unknown keys to match Zod mcpServerConfigZ.strict().
690
691
  additionalProperties: false,
691
692
  properties: {
692
- type: { type: "string", enum: ["stdio", "http"] },
693
+ type: { type: "string", enum: [...MCP_TRANSPORTS] },
693
694
  command: { type: "string" },
694
695
  args: { type: "array", items: { type: "string" } },
695
696
  url: { type: "string" },
@@ -702,3 +703,144 @@ export const workflowJsonSchema = {
702
703
  },
703
704
  },
704
705
  };
706
+ /**
707
+ * Skill JSON Schema, generated from runtime constants.
708
+ *
709
+ * Companion to {@link workflowJsonSchema}: published at
710
+ * https://spec.sweny.ai/schemas/skill.json by the
711
+ * `write-public-schema.mjs` build step. Validates the structural shape
712
+ * of a Skill (id, name, description, category, config, plus optional
713
+ * tools/instruction/mcp).
714
+ *
715
+ * Every enum and the `id` pattern + maxLength are imported from
716
+ * `types.ts` so adding a category, harness, transport, or changing the
717
+ * id rule updates the published schema with no manual sync.
718
+ */
719
+ export const skillJsonSchema = {
720
+ $schema: "https://json-schema.org/draft/2020-12/schema",
721
+ $id: "https://spec.sweny.ai/schemas/skill.json",
722
+ title: "SWEny Skill",
723
+ description: "A composable tool bundle that provides capabilities to workflow nodes.",
724
+ type: "object",
725
+ required: ["id", "name", "description", "category", "config"],
726
+ anyOf: [{ required: ["tools"] }, { required: ["instruction"] }, { required: ["mcp"] }],
727
+ additionalProperties: false,
728
+ properties: {
729
+ id: {
730
+ type: "string",
731
+ minLength: 1,
732
+ maxLength: SKILL_ID_MAX_LENGTH,
733
+ pattern: SKILL_ID_PATTERN.source,
734
+ description: "Unique skill identifier. Referenced by nodes' skills arrays. Lowercase kebab-case recommended.",
735
+ },
736
+ name: {
737
+ type: "string",
738
+ minLength: 1,
739
+ description: "Human-readable skill name.",
740
+ },
741
+ description: {
742
+ type: "string",
743
+ description: "What this skill provides.",
744
+ },
745
+ category: {
746
+ type: "string",
747
+ enum: [...SKILL_CATEGORIES],
748
+ description: "Functional category.",
749
+ },
750
+ config: {
751
+ type: "object",
752
+ description: "Configuration fields required by this skill.",
753
+ additionalProperties: { $ref: "#/$defs/ConfigField" },
754
+ },
755
+ tools: {
756
+ type: "array",
757
+ description: "Tools this skill provides to nodes.",
758
+ items: { $ref: "#/$defs/Tool" },
759
+ },
760
+ instruction: {
761
+ type: "string",
762
+ description: "Natural language expertise injected into the node prompt when this skill is referenced.",
763
+ },
764
+ mcp: {
765
+ $ref: "#/$defs/McpServerConfig",
766
+ description: "External MCP server definition wired for nodes referencing this skill.",
767
+ },
768
+ },
769
+ $defs: {
770
+ ConfigField: {
771
+ type: "object",
772
+ required: ["description"],
773
+ additionalProperties: false,
774
+ properties: {
775
+ description: {
776
+ type: "string",
777
+ description: "Human-readable description of this config field.",
778
+ },
779
+ required: {
780
+ type: "boolean",
781
+ default: false,
782
+ description: "Whether this field must be provided for the skill to function.",
783
+ },
784
+ env: {
785
+ type: "string",
786
+ description: "Default environment variable to read this value from.",
787
+ },
788
+ },
789
+ },
790
+ Tool: {
791
+ type: "object",
792
+ required: ["name", "description", "input_schema"],
793
+ additionalProperties: false,
794
+ properties: {
795
+ name: {
796
+ type: "string",
797
+ description: "Tool name. Must be unique within the skill.",
798
+ },
799
+ description: {
800
+ type: "string",
801
+ description: "What this tool does. Provided to the AI model for tool selection.",
802
+ },
803
+ input_schema: {
804
+ type: "object",
805
+ description: "JSON Schema defining the tool's input parameters.",
806
+ },
807
+ },
808
+ },
809
+ McpServerConfig: {
810
+ type: "object",
811
+ description: "External MCP server definition.",
812
+ additionalProperties: false,
813
+ properties: {
814
+ type: {
815
+ type: "string",
816
+ enum: [...MCP_TRANSPORTS],
817
+ description: "Transport type. Inferred from presence of command (stdio) or url (http) when omitted.",
818
+ },
819
+ command: {
820
+ type: "string",
821
+ description: "Spawn command (stdio transport).",
822
+ },
823
+ args: {
824
+ type: "array",
825
+ items: { type: "string" },
826
+ description: "Arguments for the command.",
827
+ },
828
+ url: {
829
+ type: "string",
830
+ format: "uri",
831
+ description: "HTTP endpoint (HTTP transport).",
832
+ },
833
+ headers: {
834
+ type: "object",
835
+ additionalProperties: { type: "string" },
836
+ description: "HTTP headers (HTTP transport only).",
837
+ },
838
+ env: {
839
+ type: "object",
840
+ additionalProperties: { type: "string" },
841
+ description: "Environment variable names the server needs. Values are descriptions, not secrets.",
842
+ },
843
+ },
844
+ },
845
+ },
846
+ };
@@ -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: {
package/dist/types.d.ts CHANGED
@@ -32,6 +32,46 @@ export interface ConfigField {
32
32
  */
33
33
  export declare const SKILL_CATEGORIES: readonly ["general", "git", "tasks", "notification", "observability", "data"];
34
34
  export type SkillCategory = (typeof SKILL_CATEGORIES)[number];
35
+ /**
36
+ * Skill harness directories, listed in ascending priority order.
37
+ *
38
+ * Both the loader (which scans these directories last-wins for ID
39
+ * collisions) and the CLI (which writes scaffolds into one of them)
40
+ * must agree on this list. A divergence means scaffolded skills land
41
+ * somewhere the loader doesn't read, or the loader reads from a
42
+ * directory the CLI can't target.
43
+ *
44
+ * Order matters: the loader treats later entries as higher priority,
45
+ * so `.sweny/skills/` overrides `.claude/skills/` on a name collision.
46
+ */
47
+ export declare const SKILL_HARNESSES: readonly [{
48
+ readonly key: "gemini";
49
+ readonly path: ".gemini/skills";
50
+ }, {
51
+ readonly key: "agents";
52
+ readonly path: ".agents/skills";
53
+ }, {
54
+ readonly key: "claude";
55
+ readonly path: ".claude/skills";
56
+ }, {
57
+ readonly key: "sweny";
58
+ readonly path: ".sweny/skills";
59
+ }];
60
+ export type SkillHarnessKey = (typeof SKILL_HARNESSES)[number]["key"];
61
+ /**
62
+ * Skill ID validation rules, declared once for runtime + spec.
63
+ *
64
+ * The pattern excludes consecutive hyphens via a negative lookahead so a
65
+ * single regex is the whole rule. JSON Schema regex follows ECMA 262, which
66
+ * supports lookaheads, so the spec can embed this string directly.
67
+ *
68
+ * Length cap is enforced separately because JSON Schema's `maxLength` is
69
+ * the canonical place to express it (vs. cramming `{1,64}` into the regex).
70
+ */
71
+ export declare const SKILL_ID_PATTERN: RegExp;
72
+ export declare const SKILL_ID_MAX_LENGTH = 64;
73
+ /** True when `id` is a structurally valid skill ID under the rules above. */
74
+ export declare function isValidSkillId(id: string): boolean;
35
75
  /** A skill groups related tools with shared config requirements */
36
76
  export interface Skill {
37
77
  id: string;
@@ -77,9 +117,17 @@ export type NodeSources = _Source[] | {
77
117
  sources: _Source[];
78
118
  };
79
119
  /** Kind of evaluator. See {@link Evaluator}. */
80
- export type EvaluatorKind = "value" | "function" | "judge";
120
+ export declare const EVALUATOR_KINDS: readonly ["value", "function", "judge"];
121
+ export type EvaluatorKind = (typeof EVALUATOR_KINDS)[number];
81
122
  /** Aggregation policy for a node's evaluator results. v1 implements `all_pass`. */
82
- export type EvalPolicy = "all_pass" | "any_pass" | "weighted";
123
+ export declare const EVAL_POLICIES: readonly ["all_pass", "any_pass", "weighted"];
124
+ export type EvalPolicy = (typeof EVAL_POLICIES)[number];
125
+ /** Action when a `requires` precondition fails. */
126
+ export declare const REQUIRES_ON_FAIL: readonly ["fail", "skip"];
127
+ export type RequiresOnFail = (typeof REQUIRES_ON_FAIL)[number];
128
+ /** MCP server transport type. Inferred from command/url when omitted. */
129
+ export declare const MCP_TRANSPORTS: readonly ["stdio", "http"];
130
+ export type McpTransport = (typeof MCP_TRANSPORTS)[number];
83
131
  /**
84
132
  * The deterministic rule body for a `value` or `function` evaluator.
85
133
  *
package/dist/types.js CHANGED
@@ -7,6 +7,48 @@
7
7
  * only this line.
8
8
  */
9
9
  export const SKILL_CATEGORIES = ["general", "git", "tasks", "notification", "observability", "data"];
10
+ /**
11
+ * Skill harness directories, listed in ascending priority order.
12
+ *
13
+ * Both the loader (which scans these directories last-wins for ID
14
+ * collisions) and the CLI (which writes scaffolds into one of them)
15
+ * must agree on this list. A divergence means scaffolded skills land
16
+ * somewhere the loader doesn't read, or the loader reads from a
17
+ * directory the CLI can't target.
18
+ *
19
+ * Order matters: the loader treats later entries as higher priority,
20
+ * so `.sweny/skills/` overrides `.claude/skills/` on a name collision.
21
+ */
22
+ export const SKILL_HARNESSES = [
23
+ { key: "gemini", path: ".gemini/skills" },
24
+ { key: "agents", path: ".agents/skills" },
25
+ { key: "claude", path: ".claude/skills" },
26
+ { key: "sweny", path: ".sweny/skills" },
27
+ ];
28
+ /**
29
+ * Skill ID validation rules, declared once for runtime + spec.
30
+ *
31
+ * The pattern excludes consecutive hyphens via a negative lookahead so a
32
+ * single regex is the whole rule. JSON Schema regex follows ECMA 262, which
33
+ * supports lookaheads, so the spec can embed this string directly.
34
+ *
35
+ * Length cap is enforced separately because JSON Schema's `maxLength` is
36
+ * the canonical place to express it (vs. cramming `{1,64}` into the regex).
37
+ */
38
+ export const SKILL_ID_PATTERN = /^(?!.*--)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
39
+ export const SKILL_ID_MAX_LENGTH = 64;
40
+ /** True when `id` is a structurally valid skill ID under the rules above. */
41
+ export function isValidSkillId(id) {
42
+ return typeof id === "string" && id.length > 0 && id.length <= SKILL_ID_MAX_LENGTH && SKILL_ID_PATTERN.test(id);
43
+ }
44
+ /** Kind of evaluator. See {@link Evaluator}. */
45
+ export const EVALUATOR_KINDS = ["value", "function", "judge"];
46
+ /** Aggregation policy for a node's evaluator results. v1 implements `all_pass`. */
47
+ export const EVAL_POLICIES = ["all_pass", "any_pass", "weighted"];
48
+ /** Action when a `requires` precondition fails. */
49
+ export const REQUIRES_ON_FAIL = ["fail", "skip"];
50
+ /** MCP server transport type. Inferred from command/url when omitted. */
51
+ export const MCP_TRANSPORTS = ["stdio", "http"];
10
52
  export const consoleLogger = {
11
53
  info: (msg, data) => console.log(`[info] ${msg}`, data ?? ""),
12
54
  warn: (msg, data) => console.warn(`[warn] ${msg}`, data ?? ""),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.89",
3
+ "version": "0.1.90",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"
@@ -50,7 +50,7 @@
50
50
  "typecheck": "tsc --noEmit",
51
51
  "test": "vitest run --exclude 'src/__tests__/integration/**' --exclude 'dist/**'",
52
52
  "test:integration": "vitest run --config vitest.integration.config.ts",
53
- "check:schema-drift": "node scripts/write-public-schema.mjs && git diff --exit-code -- ../../spec/public/schemas/workflow.json"
53
+ "check:schema-drift": "node scripts/write-public-schema.mjs && git diff --exit-code -- ../../spec/public/schemas/workflow.json ../../spec/public/schemas/skill.json"
54
54
  },
55
55
  "dependencies": {
56
56
  "@anthropic-ai/claude-agent-sdk": "^0.2.114",