@sweny-ai/core 0.1.62 → 0.1.64
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.
- package/dist/__tests__/cascade.test.d.ts +11 -0
- package/dist/__tests__/cascade.test.js +449 -0
- package/dist/cli/main.js +4 -4
- package/dist/cli/marketplace.d.ts +103 -0
- package/dist/cli/marketplace.js +290 -0
- package/dist/cli/marketplace.test.d.ts +1 -0
- package/dist/cli/marketplace.test.js +355 -0
- package/dist/cli/new.d.ts +35 -2
- package/dist/cli/new.js +156 -51
- package/dist/cli/new.test.js +215 -2
- package/dist/executor.js +105 -26
- package/dist/schema.d.ts +724 -0
- package/dist/schema.js +51 -1
- package/dist/source-resolver.d.ts +22 -0
- package/dist/source-resolver.js +174 -0
- package/dist/sources.d.ts +9 -6
- package/dist/sources.js +13 -146
- package/dist/sources.test.js +2 -1
- package/dist/sources.validation.test.js +2 -1
- package/dist/templates.js +2 -1
- package/dist/types.d.ts +20 -0
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules / Context Cascade + max_turns Tests
|
|
3
|
+
*
|
|
4
|
+
* Covers the spec sections:
|
|
5
|
+
* - Workflow > Rules & Context
|
|
6
|
+
* - Workflow > Runtime Input (rules/context)
|
|
7
|
+
* - Nodes > Max Turns Semantics
|
|
8
|
+
* - Nodes > Rules & Context (cascade + `only` flag)
|
|
9
|
+
* - Nodes > Input Augmentation (assembly order)
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules / Context Cascade + max_turns Tests
|
|
3
|
+
*
|
|
4
|
+
* Covers the spec sections:
|
|
5
|
+
* - Workflow > Rules & Context
|
|
6
|
+
* - Workflow > Runtime Input (rules/context)
|
|
7
|
+
* - Nodes > Max Turns Semantics
|
|
8
|
+
* - Nodes > Rules & Context (cascade + `only` flag)
|
|
9
|
+
* - Nodes > Input Augmentation (assembly order)
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect } from "vitest";
|
|
12
|
+
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { randomBytes } from "node:crypto";
|
|
16
|
+
import { execute } from "../executor.js";
|
|
17
|
+
import { nodeZ, workflowZ, parseWorkflow, workflowJsonSchema } from "../schema.js";
|
|
18
|
+
import { createSkillMap } from "../skills/index.js";
|
|
19
|
+
// ─── Helpers ────────────────────────────────────────────────────
|
|
20
|
+
function freshDir(tag) {
|
|
21
|
+
const dir = path.join(tmpdir(), `sweny-cascade-${tag}-${Date.now()}-${randomBytes(4).toString("hex")}`);
|
|
22
|
+
rmSync(dir, { recursive: true, force: true });
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
return dir;
|
|
25
|
+
}
|
|
26
|
+
/** Minimal Claude that records every run() call for assertion. */
|
|
27
|
+
function recordingClaude(defaultData = {}) {
|
|
28
|
+
const runs = [];
|
|
29
|
+
const claude = {
|
|
30
|
+
async run(opts) {
|
|
31
|
+
runs.push({ instruction: opts.instruction, maxTurns: opts.maxTurns });
|
|
32
|
+
return { status: "success", data: defaultData, toolCalls: [] };
|
|
33
|
+
},
|
|
34
|
+
async evaluate(opts) {
|
|
35
|
+
return opts.choices[0].id;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
return { claude, runs };
|
|
39
|
+
}
|
|
40
|
+
function singleNodeWorkflow(overrides = {}, workflowOverrides = {}) {
|
|
41
|
+
return {
|
|
42
|
+
id: "t",
|
|
43
|
+
name: "T",
|
|
44
|
+
description: "",
|
|
45
|
+
entry: "a",
|
|
46
|
+
edges: [],
|
|
47
|
+
nodes: {
|
|
48
|
+
a: {
|
|
49
|
+
name: "A",
|
|
50
|
+
instruction: overrides.instruction ?? "Base instruction.",
|
|
51
|
+
skills: overrides.skills ?? [],
|
|
52
|
+
...overrides,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
...workflowOverrides,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// ─── max_turns ──────────────────────────────────────────────────
|
|
59
|
+
describe("spec: node.max_turns", () => {
|
|
60
|
+
it("passes max_turns to claude.run when set on the node", async () => {
|
|
61
|
+
const { claude, runs } = recordingClaude();
|
|
62
|
+
const w = singleNodeWorkflow({ instruction: "Do A.", max_turns: 7 });
|
|
63
|
+
await execute(w, {}, { skills: createSkillMap([]), claude });
|
|
64
|
+
expect(runs).toHaveLength(1);
|
|
65
|
+
expect(runs[0].maxTurns).toBe(7);
|
|
66
|
+
});
|
|
67
|
+
it("passes undefined when max_turns is absent (executor defers to Claude default)", async () => {
|
|
68
|
+
const { claude, runs } = recordingClaude();
|
|
69
|
+
const w = singleNodeWorkflow({ instruction: "Do A." });
|
|
70
|
+
await execute(w, {}, { skills: createSkillMap([]), claude });
|
|
71
|
+
expect(runs).toHaveLength(1);
|
|
72
|
+
expect(runs[0].maxTurns).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
it("Zod schema accepts positive integers and rejects 0 / negatives / floats", () => {
|
|
75
|
+
expect(() => nodeZ.parse({ name: "X", instruction: "Go.", max_turns: 5 })).not.toThrow();
|
|
76
|
+
expect(() => nodeZ.parse({ name: "X", instruction: "Go." })).not.toThrow();
|
|
77
|
+
expect(() => nodeZ.parse({ name: "X", instruction: "Go.", max_turns: 0 })).toThrow();
|
|
78
|
+
expect(() => nodeZ.parse({ name: "X", instruction: "Go.", max_turns: -3 })).toThrow();
|
|
79
|
+
expect(() => nodeZ.parse({ name: "X", instruction: "Go.", max_turns: 1.5 })).toThrow();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
// ─── Zod parsing for rules/context ──────────────────────────────
|
|
83
|
+
describe("spec: Zod parsing of rules/context", () => {
|
|
84
|
+
it("accepts top-level workflow rules/context as Source arrays", () => {
|
|
85
|
+
const w = parseWorkflow({
|
|
86
|
+
id: "wf",
|
|
87
|
+
name: "Wf",
|
|
88
|
+
entry: "a",
|
|
89
|
+
nodes: { a: { name: "A", instruction: "Go." } },
|
|
90
|
+
edges: [],
|
|
91
|
+
rules: ["Never break API.", { file: "./rules.md" }, { url: "https://x.test/r.md" }],
|
|
92
|
+
context: ["Prod is degraded."],
|
|
93
|
+
});
|
|
94
|
+
expect(w.rules).toHaveLength(3);
|
|
95
|
+
expect(w.context).toEqual(["Prod is degraded."]);
|
|
96
|
+
});
|
|
97
|
+
it("accepts per-node rules in additive (array) form", () => {
|
|
98
|
+
const node = nodeZ.parse({
|
|
99
|
+
name: "N",
|
|
100
|
+
instruction: "Go.",
|
|
101
|
+
rules: ["Rule 1", { inline: "Rule 2" }],
|
|
102
|
+
});
|
|
103
|
+
expect(Array.isArray(node.rules)).toBe(true);
|
|
104
|
+
expect(node.rules).toHaveLength(2);
|
|
105
|
+
});
|
|
106
|
+
it("accepts per-node rules in object form with `only`", () => {
|
|
107
|
+
const node = nodeZ.parse({
|
|
108
|
+
name: "N",
|
|
109
|
+
instruction: "Go.",
|
|
110
|
+
rules: { only: true, sources: ["Only this rule."] },
|
|
111
|
+
});
|
|
112
|
+
expect(node.rules).toEqual({ only: true, sources: ["Only this rule."] });
|
|
113
|
+
});
|
|
114
|
+
it("accepts object form without `only` (equivalent to additive)", () => {
|
|
115
|
+
const node = nodeZ.parse({
|
|
116
|
+
name: "N",
|
|
117
|
+
instruction: "Go.",
|
|
118
|
+
context: { sources: ["Background."] },
|
|
119
|
+
});
|
|
120
|
+
expect(node.context).toEqual({ sources: ["Background."] });
|
|
121
|
+
});
|
|
122
|
+
it("rejects object form without `sources`", () => {
|
|
123
|
+
expect(() => nodeZ.parse({ name: "N", instruction: "Go.", rules: { only: true } })).toThrow();
|
|
124
|
+
});
|
|
125
|
+
it("workflowZ round-trips rules/context through parse", () => {
|
|
126
|
+
const raw = {
|
|
127
|
+
id: "wf",
|
|
128
|
+
name: "Wf",
|
|
129
|
+
entry: "a",
|
|
130
|
+
nodes: {
|
|
131
|
+
a: {
|
|
132
|
+
name: "A",
|
|
133
|
+
instruction: "Go.",
|
|
134
|
+
rules: ["N-rule"],
|
|
135
|
+
context: { only: true, sources: ["N-ctx-only"] },
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
edges: [],
|
|
139
|
+
rules: ["W-rule"],
|
|
140
|
+
context: ["W-ctx"],
|
|
141
|
+
};
|
|
142
|
+
const w = workflowZ.parse(raw);
|
|
143
|
+
expect(w.rules).toEqual(["W-rule"]);
|
|
144
|
+
expect(w.context).toEqual(["W-ctx"]);
|
|
145
|
+
expect(w.nodes.a.rules).toEqual(["N-rule"]);
|
|
146
|
+
expect(w.nodes.a.context).toEqual({ only: true, sources: ["N-ctx-only"] });
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
// ─── Exported JSON Schema ───────────────────────────────────────
|
|
150
|
+
describe("spec: workflowJsonSchema exports rules/context", () => {
|
|
151
|
+
it("declares workflow-level rules/context as Source arrays", () => {
|
|
152
|
+
const props = workflowJsonSchema.properties;
|
|
153
|
+
expect(props.rules).toMatchObject({ type: "array" });
|
|
154
|
+
expect(props.context).toMatchObject({ type: "array" });
|
|
155
|
+
});
|
|
156
|
+
it("declares per-node rules/context using NodeSources $defs", () => {
|
|
157
|
+
const nodeProps = workflowJsonSchema.properties.nodes.additionalProperties.properties;
|
|
158
|
+
expect(nodeProps.rules).toMatchObject({ $ref: "#/$defs/NodeSources" });
|
|
159
|
+
expect(nodeProps.context).toMatchObject({ $ref: "#/$defs/NodeSources" });
|
|
160
|
+
expect(workflowJsonSchema.$defs.NodeSources).toBeDefined();
|
|
161
|
+
});
|
|
162
|
+
it("NodeSources accepts either array or object-with-sources", () => {
|
|
163
|
+
const ns = workflowJsonSchema.$defs.NodeSources;
|
|
164
|
+
expect(ns.oneOf).toHaveLength(2);
|
|
165
|
+
});
|
|
166
|
+
it("declares per-node max_turns as integer >= 1", () => {
|
|
167
|
+
const nodeProps = workflowJsonSchema.properties.nodes.additionalProperties.properties;
|
|
168
|
+
expect(nodeProps.max_turns).toMatchObject({ type: "integer", minimum: 1 });
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
// ─── Cascade resolution in executor ─────────────────────────────
|
|
172
|
+
describe("spec: rules/context cascade resolution", () => {
|
|
173
|
+
it("base instruction only when no rules/context anywhere", async () => {
|
|
174
|
+
const { claude, runs } = recordingClaude();
|
|
175
|
+
const w = singleNodeWorkflow({ instruction: "JUST_THE_BASE" });
|
|
176
|
+
await execute(w, {}, { skills: createSkillMap([]), claude });
|
|
177
|
+
expect(runs[0].instruction).toBe("JUST_THE_BASE");
|
|
178
|
+
expect(runs[0].instruction).not.toContain("## Rules");
|
|
179
|
+
expect(runs[0].instruction).not.toContain("## Background Context");
|
|
180
|
+
});
|
|
181
|
+
it("workflow-level rules are prepended with the canonical heading", async () => {
|
|
182
|
+
const { claude, runs } = recordingClaude();
|
|
183
|
+
const w = singleNodeWorkflow({ instruction: "Do A." }, { rules: ["Never break API."] });
|
|
184
|
+
await execute(w, {}, { skills: createSkillMap([]), claude });
|
|
185
|
+
expect(runs[0].instruction).toContain("## Rules — You MUST Follow These");
|
|
186
|
+
expect(runs[0].instruction).toContain("Never break API.");
|
|
187
|
+
expect(runs[0].instruction).toContain("Do A.");
|
|
188
|
+
});
|
|
189
|
+
it("workflow-level context is prepended with the canonical heading", async () => {
|
|
190
|
+
const { claude, runs } = recordingClaude();
|
|
191
|
+
const w = singleNodeWorkflow({ instruction: "Do A." }, { context: ["Prod is down."] });
|
|
192
|
+
await execute(w, {}, { skills: createSkillMap([]), claude });
|
|
193
|
+
expect(runs[0].instruction).toContain("## Background Context");
|
|
194
|
+
expect(runs[0].instruction).toContain("Prod is down.");
|
|
195
|
+
});
|
|
196
|
+
it("per-node rules (array form) are additive — both workflow and node rules appear", async () => {
|
|
197
|
+
const { claude, runs } = recordingClaude();
|
|
198
|
+
const w = {
|
|
199
|
+
id: "t",
|
|
200
|
+
name: "T",
|
|
201
|
+
description: "",
|
|
202
|
+
entry: "a",
|
|
203
|
+
edges: [],
|
|
204
|
+
rules: ["WORKFLOW_RULE"],
|
|
205
|
+
nodes: {
|
|
206
|
+
a: { name: "A", instruction: "base", skills: [], rules: ["NODE_RULE"] },
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
await execute(w, {}, { skills: createSkillMap([]), claude });
|
|
210
|
+
const instr = runs[0].instruction;
|
|
211
|
+
expect(instr).toContain("WORKFLOW_RULE");
|
|
212
|
+
expect(instr).toContain("NODE_RULE");
|
|
213
|
+
// Order: workflow rules appear before node rules
|
|
214
|
+
expect(instr.indexOf("WORKFLOW_RULE")).toBeLessThan(instr.indexOf("NODE_RULE"));
|
|
215
|
+
});
|
|
216
|
+
it("per-node rules with `only: true` blocks the cascade for rules", async () => {
|
|
217
|
+
const { claude, runs } = recordingClaude();
|
|
218
|
+
const w = {
|
|
219
|
+
id: "t",
|
|
220
|
+
name: "T",
|
|
221
|
+
description: "",
|
|
222
|
+
entry: "a",
|
|
223
|
+
edges: [],
|
|
224
|
+
rules: ["WORKFLOW_RULE"],
|
|
225
|
+
nodes: {
|
|
226
|
+
a: {
|
|
227
|
+
name: "A",
|
|
228
|
+
instruction: "base",
|
|
229
|
+
skills: [],
|
|
230
|
+
rules: { only: true, sources: ["OVERRIDE_RULE"] },
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
await execute(w, {}, { skills: createSkillMap([]), claude });
|
|
235
|
+
const instr = runs[0].instruction;
|
|
236
|
+
expect(instr).toContain("OVERRIDE_RULE");
|
|
237
|
+
expect(instr).not.toContain("WORKFLOW_RULE");
|
|
238
|
+
});
|
|
239
|
+
it("`only: true` on rules does NOT block context cascade", async () => {
|
|
240
|
+
const { claude, runs } = recordingClaude();
|
|
241
|
+
const w = {
|
|
242
|
+
id: "t",
|
|
243
|
+
name: "T",
|
|
244
|
+
description: "",
|
|
245
|
+
entry: "a",
|
|
246
|
+
edges: [],
|
|
247
|
+
rules: ["WORKFLOW_RULE"],
|
|
248
|
+
context: ["WORKFLOW_CTX"],
|
|
249
|
+
nodes: {
|
|
250
|
+
a: {
|
|
251
|
+
name: "A",
|
|
252
|
+
instruction: "base",
|
|
253
|
+
skills: [],
|
|
254
|
+
rules: { only: true, sources: ["NODE_RULE"] },
|
|
255
|
+
// No node-level context override → inherits normally
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
await execute(w, {}, { skills: createSkillMap([]), claude });
|
|
260
|
+
const instr = runs[0].instruction;
|
|
261
|
+
expect(instr).not.toContain("WORKFLOW_RULE");
|
|
262
|
+
expect(instr).toContain("NODE_RULE");
|
|
263
|
+
expect(instr).toContain("WORKFLOW_CTX"); // context still cascades
|
|
264
|
+
});
|
|
265
|
+
it("runtime input + workflow + node rules concatenate in order", async () => {
|
|
266
|
+
const { claude, runs } = recordingClaude();
|
|
267
|
+
const w = {
|
|
268
|
+
id: "t",
|
|
269
|
+
name: "T",
|
|
270
|
+
description: "",
|
|
271
|
+
entry: "a",
|
|
272
|
+
edges: [],
|
|
273
|
+
rules: ["WORKFLOW_RULE"],
|
|
274
|
+
nodes: {
|
|
275
|
+
a: { name: "A", instruction: "base", skills: [], rules: ["NODE_RULE"] },
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
await execute(w, { rules: ["INPUT_RULE"] }, { skills: createSkillMap([]), claude });
|
|
279
|
+
const instr = runs[0].instruction;
|
|
280
|
+
const inp = instr.indexOf("INPUT_RULE");
|
|
281
|
+
const wfl = instr.indexOf("WORKFLOW_RULE");
|
|
282
|
+
const nd = instr.indexOf("NODE_RULE");
|
|
283
|
+
expect(inp).toBeGreaterThanOrEqual(0);
|
|
284
|
+
expect(wfl).toBeGreaterThan(inp);
|
|
285
|
+
expect(nd).toBeGreaterThan(wfl);
|
|
286
|
+
});
|
|
287
|
+
it("accepts runtime input rules as a bare string (classified as inline Source)", async () => {
|
|
288
|
+
const { claude, runs } = recordingClaude();
|
|
289
|
+
const w = singleNodeWorkflow({ instruction: "base" });
|
|
290
|
+
await execute(w, { rules: "SINGLE_STRING_RULE" }, { skills: createSkillMap([]), claude });
|
|
291
|
+
expect(runs[0].instruction).toContain("SINGLE_STRING_RULE");
|
|
292
|
+
expect(runs[0].instruction).toContain("## Rules — You MUST Follow These");
|
|
293
|
+
});
|
|
294
|
+
it("accepts runtime input rules as a tagged object source", async () => {
|
|
295
|
+
const { claude, runs } = recordingClaude();
|
|
296
|
+
const w = singleNodeWorkflow({ instruction: "base" });
|
|
297
|
+
await execute(w, { rules: { inline: "TAGGED_RULE" } }, { skills: createSkillMap([]), claude });
|
|
298
|
+
expect(runs[0].instruction).toContain("TAGGED_RULE");
|
|
299
|
+
});
|
|
300
|
+
it("resolves file sources in workflow.rules and records them in trace.sources", async () => {
|
|
301
|
+
const dir = freshDir("file-rules");
|
|
302
|
+
writeFileSync(path.join(dir, "rules.md"), "RULES_FROM_FILE");
|
|
303
|
+
const { claude, runs } = recordingClaude();
|
|
304
|
+
const w = {
|
|
305
|
+
id: "t",
|
|
306
|
+
name: "T",
|
|
307
|
+
description: "",
|
|
308
|
+
entry: "a",
|
|
309
|
+
edges: [],
|
|
310
|
+
rules: [{ file: "./rules.md" }],
|
|
311
|
+
nodes: { a: { name: "A", instruction: "base", skills: [] } },
|
|
312
|
+
};
|
|
313
|
+
const result = await execute(w, {}, { skills: createSkillMap([]), claude, cwd: dir });
|
|
314
|
+
expect(runs[0].instruction).toContain("RULES_FROM_FILE");
|
|
315
|
+
expect(result.trace.sources["workflow.rules.0"]).toBeDefined();
|
|
316
|
+
expect(result.trace.sources["workflow.rules.0"].kind).toBe("file");
|
|
317
|
+
expect(result.trace.sources["workflow.rules.0"].hash).toMatch(/^[0-9a-f]{16}$/);
|
|
318
|
+
});
|
|
319
|
+
it("resolves node-level rules/context file sources and records them under correct trace keys", async () => {
|
|
320
|
+
const dir = freshDir("node-cascade");
|
|
321
|
+
writeFileSync(path.join(dir, "n-rules.md"), "N_RULES_FILE");
|
|
322
|
+
writeFileSync(path.join(dir, "n-ctx.md"), "N_CTX_FILE");
|
|
323
|
+
const { claude, runs } = recordingClaude();
|
|
324
|
+
const w = {
|
|
325
|
+
id: "t",
|
|
326
|
+
name: "T",
|
|
327
|
+
description: "",
|
|
328
|
+
entry: "a",
|
|
329
|
+
edges: [],
|
|
330
|
+
nodes: {
|
|
331
|
+
a: {
|
|
332
|
+
name: "A",
|
|
333
|
+
instruction: "base",
|
|
334
|
+
skills: [],
|
|
335
|
+
rules: [{ file: "./n-rules.md" }],
|
|
336
|
+
context: [{ file: "./n-ctx.md" }],
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
const result = await execute(w, {}, { skills: createSkillMap([]), claude, cwd: dir });
|
|
341
|
+
expect(runs[0].instruction).toContain("N_RULES_FILE");
|
|
342
|
+
expect(runs[0].instruction).toContain("N_CTX_FILE");
|
|
343
|
+
expect(result.trace.sources["nodes.a.rules.0"]).toBeDefined();
|
|
344
|
+
expect(result.trace.sources["nodes.a.context.0"]).toBeDefined();
|
|
345
|
+
expect(result.trace.sources["nodes.a.rules.0"].kind).toBe("file");
|
|
346
|
+
expect(result.trace.sources["nodes.a.context.0"].kind).toBe("file");
|
|
347
|
+
});
|
|
348
|
+
it("different nodes in the same workflow see different effective cascades", async () => {
|
|
349
|
+
const { claude, runs } = recordingClaude();
|
|
350
|
+
const w = {
|
|
351
|
+
id: "t",
|
|
352
|
+
name: "T",
|
|
353
|
+
description: "",
|
|
354
|
+
entry: "a",
|
|
355
|
+
edges: [{ from: "a", to: "b" }],
|
|
356
|
+
rules: ["WORKFLOW_RULE"],
|
|
357
|
+
nodes: {
|
|
358
|
+
a: {
|
|
359
|
+
name: "A",
|
|
360
|
+
instruction: "Node A base",
|
|
361
|
+
skills: [],
|
|
362
|
+
rules: { only: true, sources: ["A_ONLY"] },
|
|
363
|
+
},
|
|
364
|
+
b: {
|
|
365
|
+
name: "B",
|
|
366
|
+
instruction: "Node B base",
|
|
367
|
+
skills: [],
|
|
368
|
+
rules: ["B_ADDITIVE"],
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
await execute(w, {}, { skills: createSkillMap([]), claude });
|
|
373
|
+
const aInstr = runs.find((r) => r.instruction.includes("Node A base")).instruction;
|
|
374
|
+
const bInstr = runs.find((r) => r.instruction.includes("Node B base")).instruction;
|
|
375
|
+
expect(aInstr).toContain("A_ONLY");
|
|
376
|
+
expect(aInstr).not.toContain("WORKFLOW_RULE");
|
|
377
|
+
expect(bInstr).toContain("WORKFLOW_RULE");
|
|
378
|
+
expect(bInstr).toContain("B_ADDITIVE");
|
|
379
|
+
});
|
|
380
|
+
it("sections are separated by `---` in assembly order: rules → context → skill → base", async () => {
|
|
381
|
+
const { claude, runs } = recordingClaude();
|
|
382
|
+
const w = {
|
|
383
|
+
id: "t",
|
|
384
|
+
name: "T",
|
|
385
|
+
description: "",
|
|
386
|
+
entry: "a",
|
|
387
|
+
edges: [],
|
|
388
|
+
rules: ["R"],
|
|
389
|
+
context: ["C"],
|
|
390
|
+
nodes: {
|
|
391
|
+
a: { name: "A", instruction: "BASE", skills: ["rubric"] },
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
const rubric = {
|
|
395
|
+
id: "rubric",
|
|
396
|
+
name: "Rubric",
|
|
397
|
+
description: "",
|
|
398
|
+
category: "general",
|
|
399
|
+
config: {},
|
|
400
|
+
tools: [],
|
|
401
|
+
instruction: "SCORE_IT",
|
|
402
|
+
};
|
|
403
|
+
await execute(w, {}, { skills: createSkillMap([rubric]), claude });
|
|
404
|
+
const instr = runs[0].instruction;
|
|
405
|
+
const rIdx = instr.indexOf("## Rules — You MUST Follow These");
|
|
406
|
+
const cIdx = instr.indexOf("## Background Context");
|
|
407
|
+
const sIdx = instr.indexOf("## Skill: Rubric");
|
|
408
|
+
const bIdx = instr.indexOf("BASE");
|
|
409
|
+
expect(rIdx).toBeGreaterThanOrEqual(0);
|
|
410
|
+
expect(cIdx).toBeGreaterThan(rIdx);
|
|
411
|
+
expect(sIdx).toBeGreaterThan(cIdx);
|
|
412
|
+
expect(bIdx).toBeGreaterThan(sIdx);
|
|
413
|
+
});
|
|
414
|
+
it("empty arrays produce no rules/context sections", async () => {
|
|
415
|
+
const { claude, runs } = recordingClaude();
|
|
416
|
+
const w = {
|
|
417
|
+
id: "t",
|
|
418
|
+
name: "T",
|
|
419
|
+
description: "",
|
|
420
|
+
entry: "a",
|
|
421
|
+
edges: [],
|
|
422
|
+
rules: [],
|
|
423
|
+
context: [],
|
|
424
|
+
nodes: {
|
|
425
|
+
a: { name: "A", instruction: "BASE", skills: [], rules: [], context: [] },
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
await execute(w, {}, { skills: createSkillMap([]), claude });
|
|
429
|
+
expect(runs[0].instruction).toBe("BASE");
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
// ─── Legacy input.additionalContext compatibility ───────────────
|
|
433
|
+
describe("backward compat: input.additionalContext", () => {
|
|
434
|
+
it("still renders under the legacy heading when no rules/context are present", async () => {
|
|
435
|
+
const { claude, runs } = recordingClaude();
|
|
436
|
+
const w = singleNodeWorkflow({ instruction: "base" });
|
|
437
|
+
await execute(w, { additionalContext: "LEGACY_TEXT" }, { skills: createSkillMap([]), claude });
|
|
438
|
+
expect(runs[0].instruction).toContain("## Additional Context & Rules");
|
|
439
|
+
expect(runs[0].instruction).toContain("LEGACY_TEXT");
|
|
440
|
+
});
|
|
441
|
+
it("is suppressed once rules or context are present (rules/context take priority)", async () => {
|
|
442
|
+
const { claude, runs } = recordingClaude();
|
|
443
|
+
const w = singleNodeWorkflow({ instruction: "base" }, { rules: ["NEW_RULE"] });
|
|
444
|
+
await execute(w, { additionalContext: "LEGACY_TEXT" }, { skills: createSkillMap([]), claude });
|
|
445
|
+
expect(runs[0].instruction).toContain("NEW_RULE");
|
|
446
|
+
expect(runs[0].instruction).not.toContain("LEGACY_TEXT");
|
|
447
|
+
expect(runs[0].instruction).not.toContain("## Additional Context & Rules");
|
|
448
|
+
});
|
|
449
|
+
});
|
package/dist/cli/main.js
CHANGED
|
@@ -60,10 +60,10 @@ const program = new Command()
|
|
|
60
60
|
.version(version);
|
|
61
61
|
// ── sweny new ─────────────────────────────────────────────────────────
|
|
62
62
|
program
|
|
63
|
-
.command("new")
|
|
64
|
-
.description("Create a new workflow
|
|
65
|
-
.action(async () => {
|
|
66
|
-
await runNew();
|
|
63
|
+
.command("new [id]")
|
|
64
|
+
.description("Create a new workflow. With no id, opens the interactive picker. With an id, installs that workflow from the marketplace (swenyai/workflows).")
|
|
65
|
+
.action(async (id) => {
|
|
66
|
+
await runNew(id ? { marketplaceId: id } : undefined);
|
|
67
67
|
});
|
|
68
68
|
// ── sweny e2e ────────────────────────────────────────────────────────
|
|
69
69
|
const e2eCmd = program.command("e2e").description("End-to-end browser testing");
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marketplace install — fetch workflows from swenyai/workflows and
|
|
3
|
+
* adapt them to the user's .sweny.yml providers.
|
|
4
|
+
*
|
|
5
|
+
* Pure functions for fetch/mismatch/adapt; file writes delegate to
|
|
6
|
+
* helpers in ./new.ts.
|
|
7
|
+
*/
|
|
8
|
+
import type { Skill, SkillCategory, Workflow, Claude, Logger } from "../types.js";
|
|
9
|
+
import type { FileConfig } from "./config-file.js";
|
|
10
|
+
export declare const MARKETPLACE_REPO = "swenyai/workflows";
|
|
11
|
+
export declare const MARKETPLACE_RAW_BASE = "https://raw.githubusercontent.com/swenyai/workflows/main";
|
|
12
|
+
export interface MarketplaceEntry {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
skills: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface FetchError extends Error {
|
|
19
|
+
kind: "not-found" | "rate-limit" | "network" | "bad-yaml" | "unknown";
|
|
20
|
+
retryAfter?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface FetchedWorkflow {
|
|
23
|
+
id: string;
|
|
24
|
+
yaml: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function fetchMarketplaceWorkflow(id: string): Promise<FetchedWorkflow>;
|
|
27
|
+
export declare function fetchMarketplaceIndex(): Promise<MarketplaceEntry[]>;
|
|
28
|
+
export interface ProviderMismatch {
|
|
29
|
+
category: SkillCategory;
|
|
30
|
+
configKey: string;
|
|
31
|
+
workflowSkill: string;
|
|
32
|
+
userProvider: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function computeProviderMismatch(workflowSkills: string[], fileConfig: FileConfig, availableSkills: Skill[]): ProviderMismatch[];
|
|
35
|
+
export declare function buildAdaptPrompt(mismatches: ProviderMismatch[]): string;
|
|
36
|
+
export interface AdaptOptions {
|
|
37
|
+
claude: Claude;
|
|
38
|
+
skills: Skill[];
|
|
39
|
+
logger: Logger;
|
|
40
|
+
/** Optional pre-filled first refinement (e.g. buildAdaptPrompt output). */
|
|
41
|
+
initialRefinement?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Render the workflow DAG and run an accept/refine/cancel loop.
|
|
45
|
+
* Returns the accepted workflow or null on cancel.
|
|
46
|
+
*
|
|
47
|
+
* Applies `initialRefinement` (if provided) before the first render.
|
|
48
|
+
*/
|
|
49
|
+
export declare function adaptWorkflowInteractive(workflow: Workflow, options: AdaptOptions): Promise<Workflow | null>;
|
|
50
|
+
export interface InstallOptions {
|
|
51
|
+
cwd: string;
|
|
52
|
+
availableSkills: Skill[];
|
|
53
|
+
/** Claude client for optional LLM adaptation. If null, we install as-is with a warning on mismatch. */
|
|
54
|
+
claude: Claude | null;
|
|
55
|
+
logger: Logger;
|
|
56
|
+
/**
|
|
57
|
+
* Pre-computed source-control provider (e.g. from git-remote detection).
|
|
58
|
+
* When provided, takes precedence over the skill-based fallback inference.
|
|
59
|
+
*/
|
|
60
|
+
inferredSourceControl?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Pre-computed issue-tracker provider.
|
|
63
|
+
* When provided, takes precedence over the skill-based fallback inference.
|
|
64
|
+
*/
|
|
65
|
+
inferredIssueTracker?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Pre-computed observability provider (or null if none).
|
|
68
|
+
* When provided, takes precedence over the skill-based fallback inference.
|
|
69
|
+
*/
|
|
70
|
+
inferredObservability?: string | null;
|
|
71
|
+
/**
|
|
72
|
+
* Whether to overwrite an existing workflow file.
|
|
73
|
+
* Defaults to false. When false and the file already exists,
|
|
74
|
+
* `installMarketplaceWorkflow` returns `{ installed: false, alreadyExists: true }`.
|
|
75
|
+
*/
|
|
76
|
+
overwrite?: boolean;
|
|
77
|
+
}
|
|
78
|
+
export interface InstallResult {
|
|
79
|
+
installed: boolean;
|
|
80
|
+
adapted: boolean;
|
|
81
|
+
workflowPath: string;
|
|
82
|
+
mismatches: ProviderMismatch[];
|
|
83
|
+
addedEnvKeys: number;
|
|
84
|
+
/**
|
|
85
|
+
* True when `overwrite` was false and a workflow file already existed at the
|
|
86
|
+
* target path. The caller is responsible for prompting the user if desired.
|
|
87
|
+
*/
|
|
88
|
+
alreadyExists?: boolean;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Fetch a workflow from swenyai/workflows, adapt if provider mismatch is detected
|
|
92
|
+
* AND an agent is available, then write files idempotently.
|
|
93
|
+
*
|
|
94
|
+
* When `overwrite` is false (default) and the workflow file already exists,
|
|
95
|
+
* returns `{ installed: false, alreadyExists: true, workflowPath }` without
|
|
96
|
+
* touching the file. The caller (runNew) is responsible for prompting the user.
|
|
97
|
+
*
|
|
98
|
+
* Pre-computed provider fields (`inferredSourceControl`, `inferredIssueTracker`,
|
|
99
|
+
* `inferredObservability`) take precedence over skill-based inference when
|
|
100
|
+
* provided. Omit them to use the original skill-based fallback (backward-compat
|
|
101
|
+
* for direct callers that don't have git-remote information).
|
|
102
|
+
*/
|
|
103
|
+
export declare function installMarketplaceWorkflow(id: string, options: InstallOptions): Promise<InstallResult>;
|