@sweny-ai/core 0.1.76 → 0.1.78

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 (123) hide show
  1. package/dist/browser.d.ts +11 -5
  2. package/dist/browser.js +11 -11
  3. package/dist/claude.d.ts +20 -0
  4. package/dist/claude.js +139 -9
  5. package/dist/cli/config.d.ts +22 -0
  6. package/dist/cli/config.js +56 -9
  7. package/dist/cli/main.d.ts +1 -1
  8. package/dist/cli/main.js +56 -57
  9. package/dist/cli/marketplace.js +35 -9
  10. package/dist/cli/publish.d.ts +10 -0
  11. package/dist/cli/publish.js +22 -14
  12. package/dist/executor.js +104 -43
  13. package/dist/loader.d.ts +40 -0
  14. package/dist/loader.js +73 -0
  15. package/dist/mcp-catalog.d.ts +47 -0
  16. package/dist/mcp-catalog.js +304 -0
  17. package/dist/mcp.d.ts +6 -9
  18. package/dist/mcp.js +24 -314
  19. package/dist/requires.d.ts +7 -0
  20. package/dist/requires.js +34 -0
  21. package/dist/retry.d.ts +23 -0
  22. package/dist/retry.js +72 -0
  23. package/dist/schema.d.ts +667 -80
  24. package/dist/schema.js +144 -5
  25. package/dist/skills/custom-loader.d.ts +40 -0
  26. package/dist/skills/custom-loader.js +107 -32
  27. package/dist/skills/index.d.ts +13 -1
  28. package/dist/skills/index.js +12 -3
  29. package/dist/studio.js +4 -1
  30. package/dist/templates.d.ts +29 -8
  31. package/dist/templates.js +30 -20
  32. package/dist/testing.d.ts +7 -0
  33. package/dist/testing.js +7 -0
  34. package/dist/types.d.ts +76 -0
  35. package/dist/verify.d.ts +1 -0
  36. package/dist/verify.js +43 -5
  37. package/package.json +8 -4
  38. package/dist/__tests__/cascade.test.d.ts +0 -11
  39. package/dist/__tests__/cascade.test.js +0 -449
  40. package/dist/__tests__/claude.test.d.ts +0 -1
  41. package/dist/__tests__/claude.test.js +0 -427
  42. package/dist/__tests__/custom-loader.test.d.ts +0 -1
  43. package/dist/__tests__/custom-loader.test.js +0 -116
  44. package/dist/__tests__/executor.test.d.ts +0 -1
  45. package/dist/__tests__/executor.test.js +0 -736
  46. package/dist/__tests__/integration/datadog.integration.test.d.ts +0 -1
  47. package/dist/__tests__/integration/datadog.integration.test.js +0 -23
  48. package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +0 -1
  49. package/dist/__tests__/integration/e2e-workflow.integration.test.js +0 -75
  50. package/dist/__tests__/integration/github.integration.test.d.ts +0 -1
  51. package/dist/__tests__/integration/github.integration.test.js +0 -37
  52. package/dist/__tests__/integration/harness.d.ts +0 -24
  53. package/dist/__tests__/integration/harness.js +0 -34
  54. package/dist/__tests__/integration/linear.integration.test.d.ts +0 -1
  55. package/dist/__tests__/integration/linear.integration.test.js +0 -15
  56. package/dist/__tests__/integration/sentry.integration.test.d.ts +0 -1
  57. package/dist/__tests__/integration/sentry.integration.test.js +0 -20
  58. package/dist/__tests__/integration/slack.integration.test.d.ts +0 -1
  59. package/dist/__tests__/integration/slack.integration.test.js +0 -22
  60. package/dist/__tests__/integration/sources.integration.test.d.ts +0 -1
  61. package/dist/__tests__/integration/sources.integration.test.js +0 -123
  62. package/dist/__tests__/publish.test.d.ts +0 -1
  63. package/dist/__tests__/publish.test.js +0 -180
  64. package/dist/__tests__/schema.test.d.ts +0 -1
  65. package/dist/__tests__/schema.test.js +0 -526
  66. package/dist/__tests__/skill-config-integration.test.d.ts +0 -1
  67. package/dist/__tests__/skill-config-integration.test.js +0 -204
  68. package/dist/__tests__/skills-index.test.d.ts +0 -1
  69. package/dist/__tests__/skills-index.test.js +0 -119
  70. package/dist/__tests__/skills.test.d.ts +0 -1
  71. package/dist/__tests__/skills.test.js +0 -565
  72. package/dist/__tests__/spec-conformance.test.d.ts +0 -18
  73. package/dist/__tests__/spec-conformance.test.js +0 -1074
  74. package/dist/__tests__/studio.test.d.ts +0 -1
  75. package/dist/__tests__/studio.test.js +0 -403
  76. package/dist/__tests__/templates.test.d.ts +0 -1
  77. package/dist/__tests__/templates.test.js +0 -49
  78. package/dist/__tests__/testing.test.d.ts +0 -1
  79. package/dist/__tests__/testing.test.js +0 -224
  80. package/dist/__tests__/verify.test.d.ts +0 -1
  81. package/dist/__tests__/verify.test.js +0 -517
  82. package/dist/__tests__/version-consistency.test.d.ts +0 -1
  83. package/dist/__tests__/version-consistency.test.js +0 -312
  84. package/dist/__tests__/workflow-builder.test.d.ts +0 -1
  85. package/dist/__tests__/workflow-builder.test.js +0 -110
  86. package/dist/__tests__/workflow-yaml.test.d.ts +0 -1
  87. package/dist/__tests__/workflow-yaml.test.js +0 -201
  88. package/dist/cli/__tests__/diagram.test.d.ts +0 -1
  89. package/dist/cli/__tests__/diagram.test.js +0 -225
  90. package/dist/cli/__tests__/report-cloud.test.d.ts +0 -1
  91. package/dist/cli/__tests__/report-cloud.test.js +0 -165
  92. package/dist/cli/__tests__/upgrade.test.d.ts +0 -1
  93. package/dist/cli/__tests__/upgrade.test.js +0 -317
  94. package/dist/cli/__tests__/version-check.test.d.ts +0 -1
  95. package/dist/cli/__tests__/version-check.test.js +0 -209
  96. package/dist/cli/config.test.d.ts +0 -1
  97. package/dist/cli/config.test.js +0 -200
  98. package/dist/cli/credentials.test.d.ts +0 -1
  99. package/dist/cli/credentials.test.js +0 -172
  100. package/dist/cli/e2e.test.d.ts +0 -1
  101. package/dist/cli/e2e.test.js +0 -690
  102. package/dist/cli/marketplace.test.d.ts +0 -1
  103. package/dist/cli/marketplace.test.js +0 -355
  104. package/dist/cli/new.test.d.ts +0 -1
  105. package/dist/cli/new.test.js +0 -1043
  106. package/dist/cli/output.test.d.ts +0 -1
  107. package/dist/cli/output.test.js +0 -180
  108. package/dist/cli/publish.test.d.ts +0 -1
  109. package/dist/cli/publish.test.js +0 -610
  110. package/dist/cli/renderer.test.d.ts +0 -1
  111. package/dist/cli/renderer.test.js +0 -302
  112. package/dist/executor.test.d.ts +0 -1
  113. package/dist/executor.test.js +0 -603
  114. package/dist/mcp.test.d.ts +0 -1
  115. package/dist/mcp.test.js +0 -735
  116. package/dist/mermaid.test.d.ts +0 -1
  117. package/dist/mermaid.test.js +0 -256
  118. package/dist/sources.test.d.ts +0 -1
  119. package/dist/sources.test.js +0 -265
  120. package/dist/sources.validation.test.d.ts +0 -12
  121. package/dist/sources.validation.test.js +0 -380
  122. package/dist/workflow-builder.test.d.ts +0 -1
  123. package/dist/workflow-builder.test.js +0 -117
package/dist/schema.js CHANGED
@@ -36,6 +36,7 @@ export const mcpServerConfigZ = z
36
36
  headers: z.record(z.string()).optional(),
37
37
  env: z.record(z.string()).optional(),
38
38
  })
39
+ .strict()
39
40
  .refine((c) => c.command || c.url, {
40
41
  message: "MCP server must have either command (stdio) or url (HTTP)",
41
42
  });
@@ -46,6 +47,7 @@ export const skillDefinitionZ = z
46
47
  instruction: z.string().optional(),
47
48
  mcp: mcpServerConfigZ.optional(),
48
49
  })
50
+ .strict()
49
51
  .refine((s) => s.instruction || s.mcp, {
50
52
  message: "Inline skill must provide instruction, mcp, or both",
51
53
  });
@@ -82,6 +84,7 @@ export const outputMatchZ = z
82
84
  in: z.array(z.unknown()).optional(),
83
85
  matches: z.string().min(1).optional(),
84
86
  })
87
+ .strict()
85
88
  .refine((m) => {
86
89
  const operators = [m.equals !== undefined, m.in !== undefined, m.matches !== undefined];
87
90
  return operators.filter(Boolean).length === 1;
@@ -94,6 +97,12 @@ export const nodeVerifyZ = z
94
97
  output_required: z.array(z.string().min(1)).min(1).optional(),
95
98
  output_matches: z.array(outputMatchZ).min(1).optional(),
96
99
  })
100
+ // Fix #4 gap: the exported JSON Schema sets additionalProperties: false.
101
+ // Without .strict() here Zod would silently drop unknown keys and then
102
+ // the refine would run on the stripped object, producing a different
103
+ // verdict than ajv (which rejects the unknown key directly). Keep them
104
+ // in sync.
105
+ .strict()
97
106
  .refine((v) => v.any_tool_called !== undefined ||
98
107
  v.all_tools_called !== undefined ||
99
108
  v.no_tool_called !== undefined ||
@@ -101,7 +110,26 @@ export const nodeVerifyZ = z
101
110
  v.output_matches !== undefined, {
102
111
  message: "verify must declare at least one check (any_tool_called, all_tools_called, no_tool_called, output_required, or output_matches)",
103
112
  });
104
- export const nodeZ = z.object({
113
+ export const nodeRequiresZ = z
114
+ .object({
115
+ output_required: z.array(z.string().min(1)).min(1).optional(),
116
+ output_matches: z.array(outputMatchZ).min(1).optional(),
117
+ on_fail: z.enum(["fail", "skip"]).optional(),
118
+ })
119
+ .strict()
120
+ .refine((r) => r.output_required !== undefined || r.output_matches !== undefined, {
121
+ message: "requires must declare at least one check (output_required or output_matches)",
122
+ });
123
+ const retryInstructionAutoZ = z.object({ auto: z.literal(true) }).strict();
124
+ const retryInstructionReflectZ = z.object({ reflect: z.string().min(1) }).strict();
125
+ export const nodeRetryZ = z
126
+ .object({
127
+ max: z.number().int().min(1),
128
+ instruction: z.union([z.string().min(1), retryInstructionAutoZ, retryInstructionReflectZ]).optional(),
129
+ })
130
+ .strict();
131
+ export const nodeZ = z
132
+ .object({
105
133
  name: z.string().min(1),
106
134
  instruction: sourceZ,
107
135
  skills: z.array(z.string()).default([]),
@@ -110,13 +138,24 @@ export const nodeZ = z.object({
110
138
  rules: nodeSourcesZ.optional(),
111
139
  context: nodeSourcesZ.optional(),
112
140
  verify: nodeVerifyZ.optional(),
113
- });
114
- export const edgeZ = z.object({
141
+ requires: nodeRequiresZ.optional(),
142
+ retry: nodeRetryZ.optional(),
143
+ })
144
+ .strict();
145
+ export const edgeZ = z
146
+ .object({
115
147
  from: z.string().min(1),
116
148
  to: z.string().min(1),
117
149
  when: z.string().optional(),
118
150
  max_iterations: z.number().int().min(1).optional(),
119
- });
151
+ })
152
+ .strict();
153
+ // Fix #4 gap — workflowZ is intentionally NOT .strict(). The published
154
+ // JSON Schema's `additionalProperties: false` only scopes the properties
155
+ // block, but marketplace workflows routinely carry top-level metadata
156
+ // (author, category, tags) handled by publish.ts. Striping rather than
157
+ // rejecting them preserves compatibility with the existing marketplace
158
+ // contract while the inner objects (nodes, edges, skills) stay strict.
120
159
  export const workflowZ = z.object({
121
160
  id: z.string().min(1),
122
161
  name: z.string().min(1),
@@ -311,7 +350,10 @@ export const workflowJsonSchema = {
311
350
  description: "A declarative YAML format for AI agent orchestration as a directed graph with natural language routing.",
312
351
  type: "object",
313
352
  required: ["id", "name", "nodes", "edges", "entry"],
314
- additionalProperties: false,
353
+ // Top-level is intentionally open: marketplace workflows carry extra
354
+ // metadata (author, category, tags) that publish.ts reads. Inner
355
+ // objects (nodes, edges, skills, verify, requires, etc.) are strict.
356
+ additionalProperties: true,
315
357
  $defs: {
316
358
  Source: sourceJsonSchema,
317
359
  NodeSources: {
@@ -375,6 +417,95 @@ export const workflowJsonSchema = {
375
417
  $ref: "#/$defs/NodeSources",
376
418
  description: "Per-node context. Additive by default; set { only: true } to block cascade.",
377
419
  },
420
+ verify: {
421
+ type: "object",
422
+ description: "Machine-checked post-conditions evaluated after the LLM finishes.",
423
+ additionalProperties: false,
424
+ // Fix #4: at least one check must be declared.
425
+ minProperties: 1,
426
+ properties: {
427
+ any_tool_called: { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 },
428
+ all_tools_called: { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 },
429
+ no_tool_called: { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 },
430
+ output_required: { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 },
431
+ output_matches: {
432
+ type: "array",
433
+ items: {
434
+ type: "object",
435
+ required: ["path"],
436
+ additionalProperties: false,
437
+ properties: {
438
+ path: { type: "string", minLength: 1 },
439
+ equals: {},
440
+ in: { type: "array" },
441
+ matches: { type: "string", minLength: 1 },
442
+ },
443
+ // Fix #4: exactly one operator must be declared.
444
+ oneOf: [{ required: ["equals"] }, { required: ["in"] }, { required: ["matches"] }],
445
+ },
446
+ minItems: 1,
447
+ },
448
+ },
449
+ },
450
+ requires: {
451
+ type: "object",
452
+ description: "Pre-condition checks evaluated before the LLM runs.",
453
+ additionalProperties: false,
454
+ // Fix #4: at least one of output_required / output_matches must be declared.
455
+ // on_fail alone is not sufficient — it only tags how a failing check behaves.
456
+ anyOf: [{ required: ["output_required"] }, { required: ["output_matches"] }],
457
+ properties: {
458
+ output_required: {
459
+ type: "array",
460
+ items: { type: "string", minLength: 1 },
461
+ minItems: 1,
462
+ },
463
+ output_matches: {
464
+ type: "array",
465
+ items: {
466
+ type: "object",
467
+ required: ["path"],
468
+ additionalProperties: false,
469
+ properties: {
470
+ path: { type: "string", minLength: 1 },
471
+ equals: {},
472
+ in: { type: "array" },
473
+ matches: { type: "string", minLength: 1 },
474
+ },
475
+ // Fix #4: exactly one operator must be declared.
476
+ oneOf: [{ required: ["equals"] }, { required: ["in"] }, { required: ["matches"] }],
477
+ },
478
+ minItems: 1,
479
+ },
480
+ on_fail: { type: "string", enum: ["fail", "skip"] },
481
+ },
482
+ },
483
+ retry: {
484
+ type: "object",
485
+ description: "Node-local retry on verify failure.",
486
+ required: ["max"],
487
+ additionalProperties: false,
488
+ properties: {
489
+ max: { type: "integer", minimum: 1 },
490
+ instruction: {
491
+ oneOf: [
492
+ { type: "string", minLength: 1 },
493
+ {
494
+ type: "object",
495
+ required: ["auto"],
496
+ additionalProperties: false,
497
+ properties: { auto: { const: true } },
498
+ },
499
+ {
500
+ type: "object",
501
+ required: ["reflect"],
502
+ additionalProperties: false,
503
+ properties: { reflect: { type: "string", minLength: 1 } },
504
+ },
505
+ ],
506
+ },
507
+ },
508
+ },
378
509
  },
379
510
  },
380
511
  },
@@ -401,6 +532,10 @@ export const workflowJsonSchema = {
401
532
  description: "Inline skill definitions scoped to this workflow",
402
533
  additionalProperties: {
403
534
  type: "object",
535
+ // Fix #4: an inline skill must provide instruction, mcp, or both.
536
+ anyOf: [{ required: ["instruction"] }, { required: ["mcp"] }],
537
+ // Round 2: reject unknown keys to match Zod skillDefinitionZ.strict().
538
+ additionalProperties: false,
404
539
  properties: {
405
540
  name: { type: "string" },
406
541
  description: { type: "string" },
@@ -408,6 +543,10 @@ export const workflowJsonSchema = {
408
543
  mcp: {
409
544
  type: "object",
410
545
  description: "External MCP server definition",
546
+ // Fix #4: an MCP server must declare command (stdio) or url (http).
547
+ anyOf: [{ required: ["command"] }, { required: ["url"] }],
548
+ // Round 2: reject unknown keys to match Zod mcpServerConfigZ.strict().
549
+ additionalProperties: false,
411
550
  properties: {
412
551
  type: { type: "string", enum: ["stdio", "http"] },
413
552
  command: { type: "string" },
@@ -9,8 +9,33 @@
9
9
  * .gemini/skills/ < .agents/skills/ < .claude/skills/ < .sweny/skills/
10
10
  */
11
11
  import type { Skill } from "../types.js";
12
+ /**
13
+ * Diagnostic emitted when a SKILL.md file is skipped or overridden during
14
+ * discovery. CLI flows can render these as yellow warnings; library consumers
15
+ * can inspect `kind` to decide whether to surface, ignore, or fail.
16
+ */
17
+ export interface SkillDiagnostic {
18
+ kind: "invalid-frontmatter" | "missing-name" | "invalid-id" | "unreadable-file" | "duplicate-id";
19
+ path: string;
20
+ message: string;
21
+ }
22
+ export interface SkillDiscoveryResult {
23
+ skills: Skill[];
24
+ warnings: SkillDiagnostic[];
25
+ }
26
+ /**
27
+ * Discover custom skills and surface diagnostics for broken ones.
28
+ *
29
+ * Prefer this over `discoverSkills` for CLI flows that can surface warnings
30
+ * to users. `discoverSkills` stays as a backward-compatible wrapper for
31
+ * callers that only want the happy-path skill list.
32
+ */
33
+ export declare function discoverSkillsWithDiagnostics(cwd?: string): SkillDiscoveryResult;
12
34
  /**
13
35
  * Discover custom skills from all known harness directories.
36
+ *
37
+ * Backwards-compatible wrapper that drops diagnostics. Use
38
+ * `discoverSkillsWithDiagnostics` when you want to surface warnings.
14
39
  */
15
40
  export declare function discoverSkills(cwd?: string): Skill[];
16
41
  /**
@@ -18,7 +43,22 @@ export declare function discoverSkills(cwd?: string): Skill[];
18
43
  * @deprecated Use `discoverSkills` for multi-directory support.
19
44
  */
20
45
  export declare function loadCustomSkills(cwd?: string): Skill[];
46
+ /**
47
+ * From all builtins + custom repo skills, return only the skills that are usable
48
+ * alongside any custom-skill loader diagnostics (warnings for malformed or
49
+ * duplicate SKILL.md files).
50
+ *
51
+ * Most CLI flows should prefer this over `configuredSkills` so they can
52
+ * surface warnings to the user.
53
+ */
54
+ export declare function configuredSkillsWithDiagnostics(env?: Record<string, string | undefined>, cwd?: string): {
55
+ skills: Skill[];
56
+ warnings: SkillDiagnostic[];
57
+ };
21
58
  /**
22
59
  * From all builtins + custom repo skills, return only the skills that are usable.
60
+ *
61
+ * Backwards-compatible wrapper that drops diagnostics. Prefer
62
+ * `configuredSkillsWithDiagnostics` in CLI flows so broken skill files surface.
23
63
  */
24
64
  export declare function configuredSkills(env?: Record<string, string | undefined>, cwd?: string): Skill[];
@@ -17,10 +17,15 @@ const SKILL_DIRS = [".gemini/skills", ".agents/skills", ".claude/skills", ".swen
17
17
  /** Valid skill ID: lowercase alphanumeric + hyphens, no leading/trailing/consecutive hyphens, max 64 chars. */
18
18
  const VALID_SKILL_ID = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
19
19
  /**
20
- * Discover custom skills from all known harness directories.
20
+ * Discover custom skills and surface diagnostics for broken ones.
21
+ *
22
+ * Prefer this over `discoverSkills` for CLI flows that can surface warnings
23
+ * to users. `discoverSkills` stays as a backward-compatible wrapper for
24
+ * callers that only want the happy-path skill list.
21
25
  */
22
- export function discoverSkills(cwd = process.cwd()) {
26
+ export function discoverSkillsWithDiagnostics(cwd = process.cwd()) {
23
27
  const skillMap = new Map();
28
+ const warnings = [];
24
29
  for (const relDir of SKILL_DIRS) {
25
30
  const skillsDir = join(cwd, relDir);
26
31
  if (!existsSync(skillsDir))
@@ -38,32 +43,73 @@ export function discoverSkills(cwd = process.cwd()) {
38
43
  const skillFile = join(skillsDir, dirName, "SKILL.md");
39
44
  if (!existsSync(skillFile))
40
45
  continue;
46
+ let content;
41
47
  try {
42
- const content = readFileSync(skillFile, "utf-8");
43
- const parsed = parseSkillMd(content);
44
- if (!parsed)
45
- continue;
46
- skillMap.set(parsed.id, parsed);
48
+ content = readFileSync(skillFile, "utf-8");
47
49
  }
48
- catch {
49
- // Skip malformed skill files
50
+ catch (err) {
51
+ const msg = err instanceof Error ? err.message : "unknown error";
52
+ warnings.push({
53
+ kind: "unreadable-file",
54
+ path: skillFile,
55
+ message: `Could not read ${skillFile}: ${msg}`,
56
+ });
57
+ continue;
58
+ }
59
+ const parsed = parseSkillMd(content, skillFile);
60
+ if (parsed.kind === "ok") {
61
+ if (skillMap.has(parsed.skill.id)) {
62
+ warnings.push({
63
+ kind: "duplicate-id",
64
+ path: skillFile,
65
+ message: `Skill "${parsed.skill.id}" overrides an earlier definition (last-one-wins)`,
66
+ });
67
+ }
68
+ skillMap.set(parsed.skill.id, parsed.skill);
69
+ }
70
+ else {
71
+ warnings.push(parsed.diagnostic);
50
72
  }
51
73
  }
52
74
  }
53
- return [...skillMap.values()];
75
+ return { skills: [...skillMap.values()], warnings };
54
76
  }
55
77
  /**
56
- * Parse a SKILL.md file into a Skill object.
78
+ * Discover custom skills from all known harness directories.
79
+ *
80
+ * Backwards-compatible wrapper that drops diagnostics. Use
81
+ * `discoverSkillsWithDiagnostics` when you want to surface warnings.
57
82
  */
58
- function parseSkillMd(content) {
83
+ export function discoverSkills(cwd = process.cwd()) {
84
+ return discoverSkillsWithDiagnostics(cwd).skills;
85
+ }
86
+ function parseSkillMd(content, path) {
59
87
  const raw = parseFrontmatter(content);
60
- if (!raw?.name)
61
- return null;
88
+ if (raw === "invalid") {
89
+ return {
90
+ kind: "err",
91
+ diagnostic: { kind: "invalid-frontmatter", path, message: `Invalid YAML frontmatter in ${path}` },
92
+ };
93
+ }
94
+ if (!raw?.name) {
95
+ return {
96
+ kind: "err",
97
+ diagnostic: { kind: "missing-name", path, message: `SKILL.md at ${path} has no 'name' field in frontmatter` },
98
+ };
99
+ }
62
100
  // Narrow the loosely-typed YAML output into the fields we expect
63
101
  const fm = raw;
64
102
  const id = String(fm.name);
65
- if (!VALID_SKILL_ID.test(id) || id.includes("--") || id.length > 64)
66
- return null;
103
+ if (!VALID_SKILL_ID.test(id) || id.includes("--") || id.length > 64) {
104
+ return {
105
+ kind: "err",
106
+ diagnostic: {
107
+ kind: "invalid-id",
108
+ path,
109
+ message: `Skill id "${id}" is invalid (lowercase alphanumeric + hyphens only, no consecutive hyphens, max 64 chars)`,
110
+ },
111
+ };
112
+ }
67
113
  const instruction = extractBody(content);
68
114
  let mcp;
69
115
  if (fm.mcp && typeof fm.mcp === "object") {
@@ -94,26 +140,42 @@ function parseSkillMd(content) {
94
140
  }
95
141
  }
96
142
  return {
97
- id,
98
- name: id,
99
- description: typeof fm.description === "string" ? fm.description : `Custom skill: ${id}`,
100
- category: "general",
101
- config,
102
- tools: [],
103
- instruction: instruction || undefined,
104
- mcp,
143
+ kind: "ok",
144
+ skill: {
145
+ id,
146
+ name: id,
147
+ description: typeof fm.description === "string" ? fm.description : `Custom skill: ${id}`,
148
+ category: "general",
149
+ config,
150
+ tools: [],
151
+ instruction: instruction || undefined,
152
+ mcp,
153
+ },
105
154
  };
106
155
  }
107
- /** Extract YAML frontmatter from a markdown file (between --- delimiters). */
156
+ /**
157
+ * Extract and parse YAML frontmatter.
158
+ *
159
+ * Returns:
160
+ * - parsed object when frontmatter is present and valid
161
+ * - `null` when no frontmatter delimiters found (treat as no-op skip)
162
+ * - `"invalid"` when delimiters are present but the YAML body is malformed
163
+ * (distinct from "missing" so diagnostics can differentiate).
164
+ */
108
165
  function parseFrontmatter(content) {
109
166
  const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
110
167
  if (!match)
111
168
  return null;
112
169
  try {
113
- return YAML.parse(match[1]);
170
+ const parsed = YAML.parse(match[1]);
171
+ if (parsed === null || parsed === undefined)
172
+ return null;
173
+ if (typeof parsed !== "object")
174
+ return "invalid";
175
+ return parsed;
114
176
  }
115
177
  catch {
116
- return null;
178
+ return "invalid";
117
179
  }
118
180
  }
119
181
  /** Extract the markdown body (everything after closing ---). */
@@ -131,18 +193,22 @@ export function loadCustomSkills(cwd = process.cwd()) {
131
193
  return discoverSkills(cwd);
132
194
  }
133
195
  /**
134
- * From all builtins + custom repo skills, return only the skills that are usable.
196
+ * From all builtins + custom repo skills, return only the skills that are usable
197
+ * alongside any custom-skill loader diagnostics (warnings for malformed or
198
+ * duplicate SKILL.md files).
199
+ *
200
+ * Most CLI flows should prefer this over `configuredSkills` so they can
201
+ * surface warnings to the user.
135
202
  */
136
- export function configuredSkills(env = process.env, cwd) {
203
+ export function configuredSkillsWithDiagnostics(env = process.env, cwd) {
137
204
  const configured = builtinSkills.filter((s) => isSkillConfigured(s, env));
138
- const custom = discoverSkills(cwd);
205
+ const { skills: custom, warnings } = discoverSkillsWithDiagnostics(cwd);
139
206
  // Custom skills can override built-in IDs. When overriding, merge:
140
207
  // keep the built-in's tools and config, add the custom instruction/mcp.
141
208
  const configuredMap = new Map(configured.map((s) => [s.id, s]));
142
209
  for (const skill of custom) {
143
210
  const existing = configuredMap.get(skill.id);
144
211
  if (existing) {
145
- // Merge: built-in tools + config, custom instruction/mcp + extra config
146
212
  configuredMap.set(skill.id, {
147
213
  ...existing,
148
214
  config: { ...existing.config, ...skill.config },
@@ -154,5 +220,14 @@ export function configuredSkills(env = process.env, cwd) {
154
220
  configuredMap.set(skill.id, skill);
155
221
  }
156
222
  }
157
- return [...configuredMap.values()];
223
+ return { skills: [...configuredMap.values()], warnings };
224
+ }
225
+ /**
226
+ * From all builtins + custom repo skills, return only the skills that are usable.
227
+ *
228
+ * Backwards-compatible wrapper that drops diagnostics. Prefer
229
+ * `configuredSkillsWithDiagnostics` in CLI flows so broken skill files surface.
230
+ */
231
+ export function configuredSkills(env = process.env, cwd) {
232
+ return configuredSkillsWithDiagnostics(env, cwd).skills;
158
233
  }
@@ -69,8 +69,20 @@ export interface SkillValidationResult {
69
69
  /** Nodes where notification category has zero configured skills (non-fatal) */
70
70
  warnings: string[];
71
71
  }
72
+ /**
73
+ * Minimal shape we need off an inline skill definition. Matches
74
+ * `SkillDefinition` from types.ts (kept loose here to avoid a cross-import
75
+ * that drags the full type graph into skill-index consumers).
76
+ */
77
+ type InlineSkillDef = {
78
+ name?: string;
79
+ description?: string;
80
+ instruction?: string;
81
+ mcp?: unknown;
82
+ category?: SkillCategory;
83
+ };
72
84
  export declare function validateWorkflowSkills(workflow: {
73
85
  nodes: Record<string, {
74
86
  skills: string[];
75
87
  }>;
76
- }, available: Map<string, Skill>): SkillValidationResult;
88
+ }, available: Map<string, Skill>, inlineSkills?: Record<string, InlineSkillDef>): SkillValidationResult;
@@ -69,13 +69,18 @@ export function isSkillConfigured(skill, env = {}) {
69
69
  export function configuredBuiltinSkills(env = {}) {
70
70
  return builtinSkills.filter((s) => isSkillConfigured(s, env));
71
71
  }
72
- export function validateWorkflowSkills(workflow, available) {
72
+ export function validateWorkflowSkills(workflow, available, inlineSkills) {
73
73
  const configured = [];
74
74
  const configuredIds = new Set();
75
75
  const missing = [];
76
76
  const missingIds = new Set();
77
77
  const errors = [];
78
78
  const warnings = [];
79
+ // Inline workflow.skills are first-class at runtime (the executor merges
80
+ // them into the skill map via mergeInlineSkills). Treat them as present
81
+ // for validation so spec-valid workflows don't fail CLI pre-flight.
82
+ const inlineIds = new Set(inlineSkills ? Object.keys(inlineSkills) : []);
83
+ const inlineCategory = (id) => inlineSkills?.[id]?.category ?? "general";
79
84
  // Collect all referenced skill IDs
80
85
  const allReferencedIds = new Set();
81
86
  for (const node of Object.values(workflow.nodes)) {
@@ -91,6 +96,10 @@ export function validateWorkflowSkills(workflow, available) {
91
96
  configuredIds.add(id);
92
97
  }
93
98
  }
99
+ else if (inlineIds.has(id)) {
100
+ // Inline skills need no env vars and are auto-configured by the executor.
101
+ continue;
102
+ }
94
103
  else {
95
104
  if (!missingIds.has(id)) {
96
105
  // Try to find it in builtins to get its category and required env vars
@@ -114,14 +123,14 @@ export function validateWorkflowSkills(workflow, available) {
114
123
  for (const id of node.skills) {
115
124
  const skill = available.get(id);
116
125
  const builtin = builtinSkills.find((s) => s.id === id);
117
- const cat = skill?.category ?? builtin?.category ?? "unknown";
126
+ const cat = skill?.category ?? builtin?.category ?? (inlineIds.has(id) ? inlineCategory(id) : "unknown");
118
127
  if (!categoriesNeeded.has(cat))
119
128
  categoriesNeeded.set(cat, []);
120
129
  categoriesNeeded.get(cat).push(id);
121
130
  }
122
131
  // Check each category has at least one configured skill
123
132
  for (const [cat, skillIds] of categoriesNeeded) {
124
- const hasAny = skillIds.some((id) => available.has(id));
133
+ const hasAny = skillIds.some((id) => available.has(id) || inlineIds.has(id));
125
134
  if (!hasAny) {
126
135
  const msg = `Node "${nodeId}" has no configured ${cat} providers (needs one of: ${skillIds.join(", ")})`;
127
136
  if (cat === "notification") {
package/dist/studio.js CHANGED
@@ -123,8 +123,11 @@ export function applyExecutionEvent(event, nodeDataMap) {
123
123
  case "node:exit": {
124
124
  const data = nodeDataMap.get(event.node);
125
125
  if (data) {
126
+ // NodeResult.status is already one of "success" | "failed" | "skipped";
127
+ // pass it through so Studio can render each distinctly. Previously
128
+ // collapsed skipped → failed (Fix #7).
126
129
  data.exec = {
127
- status: event.result.status === "success" ? "success" : "failed",
130
+ status: event.result.status,
128
131
  result: event.result,
129
132
  };
130
133
  }
@@ -1,13 +1,15 @@
1
1
  /**
2
- * Template loading for issues and PRs.
2
+ * Template + rules/context loading for the CLI.
3
3
  *
4
- * Templates can come from:
4
+ * Templates and additional-context documents can come from:
5
5
  * 1. Local file path (relative to repo root)
6
6
  * 2. URL (fetched at runtime)
7
- * 3. Built-in defaults
7
+ * 3. Built-in defaults / inline text
8
8
  *
9
- * Delegates to sources.ts for actual resolution; this module provides
10
- * backwards-compatible wrappers with fallback semantics.
9
+ * Delegates to source-resolver.ts for the actual fetch. Fix #16: both
10
+ * `loadTemplate` and `loadAdditionalContext` now honor `offline` and
11
+ * `fetchAuth` the same way the executor's per-node Sources do, so CLI-level
12
+ * rules/context stay consistent with everything the executor touches later.
11
13
  */
12
14
  export declare const DEFAULT_ISSUE_TEMPLATE = "## Summary\n<!-- One-line description of the issue -->\n\n## Root Cause\n<!-- What caused this issue -->\n\n## Impact\n- **Severity**: <!-- critical / high / medium / low -->\n- **Affected Services**: <!-- list -->\n- **User Impact**: <!-- description -->\n\n## Steps to Reproduce\n1. ...\n\n## Recommended Fix\n<!-- Proposed solution -->\n\n## Related\n- Commits: <!-- relevant commits -->\n- PRs: <!-- related PRs -->\n";
13
15
  export declare const DEFAULT_PR_TEMPLATE = "## Summary\n<!-- What does this PR do? -->\n\n## Root Cause\n<!-- What caused the issue this fixes? -->\n\n## Changes\n<!-- Bullet list of changes -->\n\n## Testing\n- [ ] Tested locally\n- [ ] No breaking changes\n\n## Related Issues\nFixes #\n";
@@ -15,25 +17,44 @@ export interface Templates {
15
17
  issueTemplate: string;
16
18
  prTemplate: string;
17
19
  }
20
+ /**
21
+ * Options shared by `loadTemplate` and `loadAdditionalContext`.
22
+ *
23
+ * `offline` and `fetchAuth` mirror the same knobs on the executor's
24
+ * `ExecuteOptions`, so CLI-preloaded rules/context resolve under the same
25
+ * policy as Sources resolved later by the executor.
26
+ */
27
+ export interface SourceLoadOptions {
28
+ cwd?: string;
29
+ offline?: boolean;
30
+ /** Host → env-var-name map for URL Source authentication (Bearer). */
31
+ fetchAuth?: Record<string, string>;
32
+ /** Environment used to resolve `fetchAuth` env-var names. Defaults to process.env. */
33
+ env?: NodeJS.ProcessEnv;
34
+ }
18
35
  /**
19
36
  * Load a template from a file path or URL.
20
37
  * Returns the default if source is empty or loading fails.
38
+ *
39
+ * Accepts the legacy `cwd?: string` signature for backward compat.
21
40
  */
22
- export declare function loadTemplate(source: string | undefined, fallback: string, cwd?: string): Promise<string>;
41
+ export declare function loadTemplate(source: string | undefined, fallback: string, options?: SourceLoadOptions | string): Promise<string>;
23
42
  /**
24
43
  * Resolve issue and PR templates from config.
25
44
  */
26
45
  export declare function resolveTemplates(config: {
27
46
  issueTemplate?: string;
28
47
  prTemplate?: string;
29
- }, cwd?: string): Promise<Templates>;
48
+ }, options?: SourceLoadOptions | string): Promise<Templates>;
30
49
  /**
31
50
  * Load additional context documents (local files, URLs, or inline text).
32
51
  * Each source is resolved eagerly (including URLs) and wrapped with a header.
33
52
  * Returns { resolved, urls } — resolved text for all sources; urls is kept
34
53
  * for API compat but will be empty since URLs now resolve eagerly.
54
+ *
55
+ * Accepts the legacy `cwd?: string` signature for backward compat.
35
56
  */
36
- export declare function loadAdditionalContext(sources: string[], cwd?: string): Promise<{
57
+ export declare function loadAdditionalContext(sources: string[], options?: SourceLoadOptions | string): Promise<{
37
58
  resolved: string;
38
59
  urls: string[];
39
60
  }>;