@sweny-ai/core 0.1.49 → 0.1.50

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.
@@ -28,6 +28,17 @@ const linearWorkflow = {
28
28
  { from: "b", to: "c" },
29
29
  ],
30
30
  };
31
+ /** Same shape as linearWorkflow but with no skill requirements — for tests that
32
+ * don't exercise tool usage and just need a 3-node DAG. */
33
+ const skilllessWorkflow = {
34
+ ...linearWorkflow,
35
+ id: "linear-no-skills",
36
+ nodes: {
37
+ a: { name: "A", instruction: "Do A", skills: [] },
38
+ b: { name: "B", instruction: "Do B", skills: [] },
39
+ c: { name: "C", instruction: "Do C", skills: [] },
40
+ },
41
+ };
31
42
  const branchingWorkflow = {
32
43
  id: "branching",
33
44
  name: "Branching",
@@ -154,7 +165,7 @@ describe("executor", () => {
154
165
  c: { data: {} },
155
166
  },
156
167
  });
157
- const { results } = await execute(linearWorkflow, {}, {
168
+ const { results } = await execute(skilllessWorkflow, {}, {
158
169
  skills: createSkillMap([]),
159
170
  claude,
160
171
  observer: () => {
@@ -246,13 +257,13 @@ describe("executor", () => {
246
257
  const claude = new MockClaude({
247
258
  responses: { a: { data: {} }, b: { data: {} }, c: { data: {} } },
248
259
  });
249
- // Should throw without config
250
- await expect(execute(linearWorkflow, {}, {
260
+ // Should throw without config (uses skilllessWorkflow — no skill requirements on nodes)
261
+ await expect(execute(skilllessWorkflow, {}, {
251
262
  skills: createSkillMap([skillWithConfig]),
252
263
  claude,
253
264
  })).rejects.toThrow("Missing required config");
254
265
  // Should succeed with override
255
- const { results } = await execute(linearWorkflow, {}, {
266
+ const { results } = await execute(skilllessWorkflow, {}, {
256
267
  skills: createSkillMap([skillWithConfig]),
257
268
  claude,
258
269
  config: { MY_KEY: "provided" },
@@ -267,7 +278,7 @@ describe("executor", () => {
267
278
  c: { data: {} },
268
279
  },
269
280
  });
270
- const { results } = await execute(linearWorkflow, {}, {
281
+ const { results } = await execute(skilllessWorkflow, {}, {
271
282
  skills: createSkillMap([]),
272
283
  claude,
273
284
  config: {},
@@ -368,6 +379,56 @@ describe("executor", () => {
368
379
  expect(aCount).toBe(2);
369
380
  expect(results.has("end")).toBe(true);
370
381
  });
382
+ it("throws when a node with required skills is reached but none are configured", async () => {
383
+ const workflowWithSkills = {
384
+ id: "needs-linear",
385
+ name: "Needs Linear",
386
+ description: "A→B where B needs linear skill",
387
+ entry: "a",
388
+ nodes: {
389
+ a: { name: "A", instruction: "Do A", skills: [] },
390
+ b: { name: "B", instruction: "Create a Linear issue", skills: ["linear"] },
391
+ },
392
+ edges: [{ from: "a", to: "b" }],
393
+ };
394
+ const claude = new MockClaude({
395
+ responses: {
396
+ a: { data: { done: true } },
397
+ b: { data: {} },
398
+ },
399
+ });
400
+ // No linear skill in the map — should throw when node B is reached
401
+ await expect(execute(workflowWithSkills, {}, { skills: createSkillMap([]), claude, config: {} })).rejects.toThrow('Node "b" requires skills [linear] but none are configured');
402
+ });
403
+ it("does not throw for unreachable nodes with missing skills", async () => {
404
+ const workflowWithUnreachable = {
405
+ id: "unreachable-skill",
406
+ name: "Unreachable Skill",
407
+ description: "A→C, B needs linear but is never reached",
408
+ entry: "a",
409
+ nodes: {
410
+ a: { name: "A", instruction: "Do A", skills: [] },
411
+ b: { name: "B", instruction: "Create issue", skills: ["linear"] },
412
+ c: { name: "C", instruction: "Do C", skills: [] },
413
+ },
414
+ edges: [
415
+ { from: "a", to: "c" },
416
+ { from: "a", to: "b", when: "needs escalation" },
417
+ ],
418
+ };
419
+ const claude = new MockClaude({
420
+ responses: {
421
+ a: { data: {} },
422
+ c: { data: {} },
423
+ },
424
+ routes: { a: "c" },
425
+ });
426
+ // Node B is never reached, so missing linear skill should not cause a throw
427
+ const { results } = await execute(workflowWithUnreachable, {}, { skills: createSkillMap([]), claude, config: {} });
428
+ expect(results.has("a")).toBe(true);
429
+ expect(results.has("c")).toBe(true);
430
+ expect(results.has("b")).toBe(false);
431
+ });
371
432
  it("validates route choice falls back on invalid Claude response", async () => {
372
433
  // Create a claude mock that returns an invalid route
373
434
  const badRouteClaude = {
package/dist/executor.js CHANGED
@@ -41,6 +41,14 @@ export async function execute(workflow, input, options) {
41
41
  logger.info(`→ ${node.name}`, { node: currentId });
42
42
  // Gather tools from the node's skills
43
43
  const tools = resolveTools(node.skills, skills);
44
+ // Runtime guard: if this node declares skills but none resolved, the node
45
+ // cannot do its job (e.g. "create a Linear issue" with no linear skill).
46
+ // The startup validate() warns about this possibility, but only throw when
47
+ // the node is actually reached — unreachable nodes with missing skills are fine.
48
+ if (node.skills.length > 0 && tools.length === 0) {
49
+ throw new Error(`Node "${currentId}" requires skills [${node.skills.join(", ")}] but none are configured. ` +
50
+ `Set the required environment variables and try again.`);
51
+ }
44
52
  // Build context: input + all prior node results
45
53
  const context = {
46
54
  input,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.49",
3
+ "version": "0.1.50",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"