@sweny-ai/core 0.1.77 → 0.1.79

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 (119) hide show
  1. package/dist/browser.d.ts +11 -5
  2. package/dist/browser.js +11 -11
  3. package/dist/claude.d.ts +16 -0
  4. package/dist/claude.js +98 -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/loader.d.ts +40 -0
  13. package/dist/loader.js +73 -0
  14. package/dist/mcp-catalog.d.ts +47 -0
  15. package/dist/mcp-catalog.js +304 -0
  16. package/dist/mcp.d.ts +6 -9
  17. package/dist/mcp.js +24 -314
  18. package/dist/schema.d.ts +183 -151
  19. package/dist/schema.js +49 -7
  20. package/dist/skills/custom-loader.d.ts +40 -0
  21. package/dist/skills/custom-loader.js +107 -32
  22. package/dist/skills/index.d.ts +13 -1
  23. package/dist/skills/index.js +12 -3
  24. package/dist/studio.js +4 -1
  25. package/dist/templates.d.ts +29 -8
  26. package/dist/templates.js +30 -20
  27. package/dist/types.d.ts +12 -0
  28. package/dist/verify.js +96 -7
  29. package/package.json +8 -4
  30. package/dist/__tests__/cascade.test.d.ts +0 -11
  31. package/dist/__tests__/cascade.test.js +0 -452
  32. package/dist/__tests__/claude.test.d.ts +0 -1
  33. package/dist/__tests__/claude.test.js +0 -427
  34. package/dist/__tests__/custom-loader.test.d.ts +0 -1
  35. package/dist/__tests__/custom-loader.test.js +0 -116
  36. package/dist/__tests__/executor.test.d.ts +0 -1
  37. package/dist/__tests__/executor.test.js +0 -1635
  38. package/dist/__tests__/integration/datadog.integration.test.d.ts +0 -1
  39. package/dist/__tests__/integration/datadog.integration.test.js +0 -23
  40. package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +0 -1
  41. package/dist/__tests__/integration/e2e-workflow.integration.test.js +0 -75
  42. package/dist/__tests__/integration/github.integration.test.d.ts +0 -1
  43. package/dist/__tests__/integration/github.integration.test.js +0 -37
  44. package/dist/__tests__/integration/harness.d.ts +0 -24
  45. package/dist/__tests__/integration/harness.js +0 -34
  46. package/dist/__tests__/integration/linear.integration.test.d.ts +0 -1
  47. package/dist/__tests__/integration/linear.integration.test.js +0 -15
  48. package/dist/__tests__/integration/sentry.integration.test.d.ts +0 -1
  49. package/dist/__tests__/integration/sentry.integration.test.js +0 -20
  50. package/dist/__tests__/integration/slack.integration.test.d.ts +0 -1
  51. package/dist/__tests__/integration/slack.integration.test.js +0 -22
  52. package/dist/__tests__/integration/sources.integration.test.d.ts +0 -1
  53. package/dist/__tests__/integration/sources.integration.test.js +0 -123
  54. package/dist/__tests__/publish.test.d.ts +0 -1
  55. package/dist/__tests__/publish.test.js +0 -180
  56. package/dist/__tests__/requires.test.d.ts +0 -1
  57. package/dist/__tests__/requires.test.js +0 -82
  58. package/dist/__tests__/retry.test.d.ts +0 -1
  59. package/dist/__tests__/retry.test.js +0 -213
  60. package/dist/__tests__/schema.test.d.ts +0 -1
  61. package/dist/__tests__/schema.test.js +0 -600
  62. package/dist/__tests__/skill-config-integration.test.d.ts +0 -1
  63. package/dist/__tests__/skill-config-integration.test.js +0 -204
  64. package/dist/__tests__/skills-index.test.d.ts +0 -1
  65. package/dist/__tests__/skills-index.test.js +0 -119
  66. package/dist/__tests__/skills.test.d.ts +0 -1
  67. package/dist/__tests__/skills.test.js +0 -565
  68. package/dist/__tests__/spec-conformance.test.d.ts +0 -18
  69. package/dist/__tests__/spec-conformance.test.js +0 -1084
  70. package/dist/__tests__/studio.test.d.ts +0 -1
  71. package/dist/__tests__/studio.test.js +0 -403
  72. package/dist/__tests__/templates.test.d.ts +0 -1
  73. package/dist/__tests__/templates.test.js +0 -49
  74. package/dist/__tests__/testing.test.d.ts +0 -1
  75. package/dist/__tests__/testing.test.js +0 -242
  76. package/dist/__tests__/verify.test.d.ts +0 -1
  77. package/dist/__tests__/verify.test.js +0 -517
  78. package/dist/__tests__/version-consistency.test.d.ts +0 -1
  79. package/dist/__tests__/version-consistency.test.js +0 -312
  80. package/dist/__tests__/workflow-builder.test.d.ts +0 -1
  81. package/dist/__tests__/workflow-builder.test.js +0 -110
  82. package/dist/__tests__/workflow-yaml.test.d.ts +0 -1
  83. package/dist/__tests__/workflow-yaml.test.js +0 -201
  84. package/dist/cli/__tests__/diagram.test.d.ts +0 -1
  85. package/dist/cli/__tests__/diagram.test.js +0 -225
  86. package/dist/cli/__tests__/report-cloud.test.d.ts +0 -1
  87. package/dist/cli/__tests__/report-cloud.test.js +0 -165
  88. package/dist/cli/__tests__/upgrade.test.d.ts +0 -1
  89. package/dist/cli/__tests__/upgrade.test.js +0 -317
  90. package/dist/cli/__tests__/version-check.test.d.ts +0 -1
  91. package/dist/cli/__tests__/version-check.test.js +0 -209
  92. package/dist/cli/config.test.d.ts +0 -1
  93. package/dist/cli/config.test.js +0 -200
  94. package/dist/cli/credentials.test.d.ts +0 -1
  95. package/dist/cli/credentials.test.js +0 -172
  96. package/dist/cli/e2e.test.d.ts +0 -1
  97. package/dist/cli/e2e.test.js +0 -690
  98. package/dist/cli/marketplace.test.d.ts +0 -1
  99. package/dist/cli/marketplace.test.js +0 -355
  100. package/dist/cli/new.test.d.ts +0 -1
  101. package/dist/cli/new.test.js +0 -1043
  102. package/dist/cli/output.test.d.ts +0 -1
  103. package/dist/cli/output.test.js +0 -180
  104. package/dist/cli/publish.test.d.ts +0 -1
  105. package/dist/cli/publish.test.js +0 -610
  106. package/dist/cli/renderer.test.d.ts +0 -1
  107. package/dist/cli/renderer.test.js +0 -302
  108. package/dist/executor.test.d.ts +0 -1
  109. package/dist/executor.test.js +0 -618
  110. package/dist/mcp.test.d.ts +0 -1
  111. package/dist/mcp.test.js +0 -735
  112. package/dist/mermaid.test.d.ts +0 -1
  113. package/dist/mermaid.test.js +0 -256
  114. package/dist/sources.test.d.ts +0 -1
  115. package/dist/sources.test.js +0 -265
  116. package/dist/sources.validation.test.d.ts +0 -12
  117. package/dist/sources.validation.test.js +0 -380
  118. package/dist/workflow-builder.test.d.ts +0 -1
  119. package/dist/workflow-builder.test.js +0 -123
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 ||
@@ -107,16 +116,20 @@ export const nodeRequiresZ = z
107
116
  output_matches: z.array(outputMatchZ).min(1).optional(),
108
117
  on_fail: z.enum(["fail", "skip"]).optional(),
109
118
  })
119
+ .strict()
110
120
  .refine((r) => r.output_required !== undefined || r.output_matches !== undefined, {
111
121
  message: "requires must declare at least one check (output_required or output_matches)",
112
122
  });
113
123
  const retryInstructionAutoZ = z.object({ auto: z.literal(true) }).strict();
114
124
  const retryInstructionReflectZ = z.object({ reflect: z.string().min(1) }).strict();
115
- export const nodeRetryZ = z.object({
125
+ export const nodeRetryZ = z
126
+ .object({
116
127
  max: z.number().int().min(1),
117
128
  instruction: z.union([z.string().min(1), retryInstructionAutoZ, retryInstructionReflectZ]).optional(),
118
- });
119
- export const nodeZ = z.object({
129
+ })
130
+ .strict();
131
+ export const nodeZ = z
132
+ .object({
120
133
  name: z.string().min(1),
121
134
  instruction: sourceZ,
122
135
  skills: z.array(z.string()).default([]),
@@ -127,13 +140,22 @@ export const nodeZ = z.object({
127
140
  verify: nodeVerifyZ.optional(),
128
141
  requires: nodeRequiresZ.optional(),
129
142
  retry: nodeRetryZ.optional(),
130
- });
131
- export const edgeZ = z.object({
143
+ })
144
+ .strict();
145
+ export const edgeZ = z
146
+ .object({
132
147
  from: z.string().min(1),
133
148
  to: z.string().min(1),
134
149
  when: z.string().optional(),
135
150
  max_iterations: z.number().int().min(1).optional(),
136
- });
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.
137
159
  export const workflowZ = z.object({
138
160
  id: z.string().min(1),
139
161
  name: z.string().min(1),
@@ -328,7 +350,10 @@ export const workflowJsonSchema = {
328
350
  description: "A declarative YAML format for AI agent orchestration as a directed graph with natural language routing.",
329
351
  type: "object",
330
352
  required: ["id", "name", "nodes", "edges", "entry"],
331
- 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,
332
357
  $defs: {
333
358
  Source: sourceJsonSchema,
334
359
  NodeSources: {
@@ -396,6 +421,8 @@ export const workflowJsonSchema = {
396
421
  type: "object",
397
422
  description: "Machine-checked post-conditions evaluated after the LLM finishes.",
398
423
  additionalProperties: false,
424
+ // Fix #4: at least one check must be declared.
425
+ minProperties: 1,
399
426
  properties: {
400
427
  any_tool_called: { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 },
401
428
  all_tools_called: { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 },
@@ -413,6 +440,8 @@ export const workflowJsonSchema = {
413
440
  in: { type: "array" },
414
441
  matches: { type: "string", minLength: 1 },
415
442
  },
443
+ // Fix #4: exactly one operator must be declared.
444
+ oneOf: [{ required: ["equals"] }, { required: ["in"] }, { required: ["matches"] }],
416
445
  },
417
446
  minItems: 1,
418
447
  },
@@ -422,6 +451,9 @@ export const workflowJsonSchema = {
422
451
  type: "object",
423
452
  description: "Pre-condition checks evaluated before the LLM runs.",
424
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"] }],
425
457
  properties: {
426
458
  output_required: {
427
459
  type: "array",
@@ -440,6 +472,8 @@ export const workflowJsonSchema = {
440
472
  in: { type: "array" },
441
473
  matches: { type: "string", minLength: 1 },
442
474
  },
475
+ // Fix #4: exactly one operator must be declared.
476
+ oneOf: [{ required: ["equals"] }, { required: ["in"] }, { required: ["matches"] }],
443
477
  },
444
478
  minItems: 1,
445
479
  },
@@ -498,6 +532,10 @@ export const workflowJsonSchema = {
498
532
  description: "Inline skill definitions scoped to this workflow",
499
533
  additionalProperties: {
500
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,
501
539
  properties: {
502
540
  name: { type: "string" },
503
541
  description: { type: "string" },
@@ -505,6 +543,10 @@ export const workflowJsonSchema = {
505
543
  mcp: {
506
544
  type: "object",
507
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,
508
550
  properties: {
509
551
  type: { type: "string", enum: ["stdio", "http"] },
510
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
  }>;