@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.
- package/dist/browser.d.ts +11 -5
- package/dist/browser.js +11 -11
- package/dist/claude.d.ts +20 -0
- package/dist/claude.js +139 -9
- package/dist/cli/config.d.ts +22 -0
- package/dist/cli/config.js +56 -9
- package/dist/cli/main.d.ts +1 -1
- package/dist/cli/main.js +56 -57
- package/dist/cli/marketplace.js +35 -9
- package/dist/cli/publish.d.ts +10 -0
- package/dist/cli/publish.js +22 -14
- package/dist/executor.js +104 -43
- package/dist/loader.d.ts +40 -0
- package/dist/loader.js +73 -0
- package/dist/mcp-catalog.d.ts +47 -0
- package/dist/mcp-catalog.js +304 -0
- package/dist/mcp.d.ts +6 -9
- package/dist/mcp.js +24 -314
- package/dist/requires.d.ts +7 -0
- package/dist/requires.js +34 -0
- package/dist/retry.d.ts +23 -0
- package/dist/retry.js +72 -0
- package/dist/schema.d.ts +667 -80
- package/dist/schema.js +144 -5
- package/dist/skills/custom-loader.d.ts +40 -0
- package/dist/skills/custom-loader.js +107 -32
- package/dist/skills/index.d.ts +13 -1
- package/dist/skills/index.js +12 -3
- package/dist/studio.js +4 -1
- package/dist/templates.d.ts +29 -8
- package/dist/templates.js +30 -20
- package/dist/testing.d.ts +7 -0
- package/dist/testing.js +7 -0
- package/dist/types.d.ts +76 -0
- package/dist/verify.d.ts +1 -0
- package/dist/verify.js +43 -5
- package/package.json +8 -4
- package/dist/__tests__/cascade.test.d.ts +0 -11
- package/dist/__tests__/cascade.test.js +0 -449
- package/dist/__tests__/claude.test.d.ts +0 -1
- package/dist/__tests__/claude.test.js +0 -427
- package/dist/__tests__/custom-loader.test.d.ts +0 -1
- package/dist/__tests__/custom-loader.test.js +0 -116
- package/dist/__tests__/executor.test.d.ts +0 -1
- package/dist/__tests__/executor.test.js +0 -736
- package/dist/__tests__/integration/datadog.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/datadog.integration.test.js +0 -23
- package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/e2e-workflow.integration.test.js +0 -75
- package/dist/__tests__/integration/github.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/github.integration.test.js +0 -37
- package/dist/__tests__/integration/harness.d.ts +0 -24
- package/dist/__tests__/integration/harness.js +0 -34
- package/dist/__tests__/integration/linear.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/linear.integration.test.js +0 -15
- package/dist/__tests__/integration/sentry.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/sentry.integration.test.js +0 -20
- package/dist/__tests__/integration/slack.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/slack.integration.test.js +0 -22
- package/dist/__tests__/integration/sources.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/sources.integration.test.js +0 -123
- package/dist/__tests__/publish.test.d.ts +0 -1
- package/dist/__tests__/publish.test.js +0 -180
- package/dist/__tests__/schema.test.d.ts +0 -1
- package/dist/__tests__/schema.test.js +0 -526
- package/dist/__tests__/skill-config-integration.test.d.ts +0 -1
- package/dist/__tests__/skill-config-integration.test.js +0 -204
- package/dist/__tests__/skills-index.test.d.ts +0 -1
- package/dist/__tests__/skills-index.test.js +0 -119
- package/dist/__tests__/skills.test.d.ts +0 -1
- package/dist/__tests__/skills.test.js +0 -565
- package/dist/__tests__/spec-conformance.test.d.ts +0 -18
- package/dist/__tests__/spec-conformance.test.js +0 -1074
- package/dist/__tests__/studio.test.d.ts +0 -1
- package/dist/__tests__/studio.test.js +0 -403
- package/dist/__tests__/templates.test.d.ts +0 -1
- package/dist/__tests__/templates.test.js +0 -49
- package/dist/__tests__/testing.test.d.ts +0 -1
- package/dist/__tests__/testing.test.js +0 -224
- package/dist/__tests__/verify.test.d.ts +0 -1
- package/dist/__tests__/verify.test.js +0 -517
- package/dist/__tests__/version-consistency.test.d.ts +0 -1
- package/dist/__tests__/version-consistency.test.js +0 -312
- package/dist/__tests__/workflow-builder.test.d.ts +0 -1
- package/dist/__tests__/workflow-builder.test.js +0 -110
- package/dist/__tests__/workflow-yaml.test.d.ts +0 -1
- package/dist/__tests__/workflow-yaml.test.js +0 -201
- package/dist/cli/__tests__/diagram.test.d.ts +0 -1
- package/dist/cli/__tests__/diagram.test.js +0 -225
- package/dist/cli/__tests__/report-cloud.test.d.ts +0 -1
- package/dist/cli/__tests__/report-cloud.test.js +0 -165
- package/dist/cli/__tests__/upgrade.test.d.ts +0 -1
- package/dist/cli/__tests__/upgrade.test.js +0 -317
- package/dist/cli/__tests__/version-check.test.d.ts +0 -1
- package/dist/cli/__tests__/version-check.test.js +0 -209
- package/dist/cli/config.test.d.ts +0 -1
- package/dist/cli/config.test.js +0 -200
- package/dist/cli/credentials.test.d.ts +0 -1
- package/dist/cli/credentials.test.js +0 -172
- package/dist/cli/e2e.test.d.ts +0 -1
- package/dist/cli/e2e.test.js +0 -690
- package/dist/cli/marketplace.test.d.ts +0 -1
- package/dist/cli/marketplace.test.js +0 -355
- package/dist/cli/new.test.d.ts +0 -1
- package/dist/cli/new.test.js +0 -1043
- package/dist/cli/output.test.d.ts +0 -1
- package/dist/cli/output.test.js +0 -180
- package/dist/cli/publish.test.d.ts +0 -1
- package/dist/cli/publish.test.js +0 -610
- package/dist/cli/renderer.test.d.ts +0 -1
- package/dist/cli/renderer.test.js +0 -302
- package/dist/executor.test.d.ts +0 -1
- package/dist/executor.test.js +0 -603
- package/dist/mcp.test.d.ts +0 -1
- package/dist/mcp.test.js +0 -735
- package/dist/mermaid.test.d.ts +0 -1
- package/dist/mermaid.test.js +0 -256
- package/dist/sources.test.d.ts +0 -1
- package/dist/sources.test.js +0 -265
- package/dist/sources.validation.test.d.ts +0 -12
- package/dist/sources.validation.test.js +0 -380
- package/dist/workflow-builder.test.d.ts +0 -1
- package/dist/workflow-builder.test.js +0 -117
package/dist/cli/marketplace.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as p from "@clack/prompts";
|
|
|
10
10
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
11
11
|
import { refineWorkflow } from "../workflow-builder.js";
|
|
12
12
|
import { DagRenderer } from "./renderer.js";
|
|
13
|
-
import {
|
|
13
|
+
import { validateParsed } from "../loader.js";
|
|
14
14
|
import { loadConfigFile } from "./config-file.js";
|
|
15
15
|
import { extractSkillsFromYaml, collectCredentialsForSkills, writeSwenyYmlIfMissing, appendMissingEnvKeys, writeWorkflowFile, } from "./new.js";
|
|
16
16
|
export const MARKETPLACE_REPO = "swenyai/workflows";
|
|
@@ -194,20 +194,36 @@ export async function installMarketplaceWorkflow(id, options) {
|
|
|
194
194
|
const overwrite = options.overwrite ?? false;
|
|
195
195
|
// 1. Fetch
|
|
196
196
|
const fetched = await fetchMarketplaceWorkflow(id);
|
|
197
|
-
// 2.
|
|
197
|
+
// 2. Validate fetched content BEFORE any local side effects (Fix #3).
|
|
198
|
+
// Previously, an invalid workflow could still get written to disk and
|
|
199
|
+
// .sweny.yml / .env polluted with keys inferred from broken content.
|
|
200
|
+
let parsed;
|
|
201
|
+
{
|
|
202
|
+
let yamlParsed;
|
|
203
|
+
try {
|
|
204
|
+
yamlParsed = parseYaml(fetched.yaml);
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
208
|
+
throw new Error(`Marketplace workflow "${id}" is not valid YAML: ${msg}`);
|
|
209
|
+
}
|
|
210
|
+
const validated = validateParsed(yamlParsed);
|
|
211
|
+
if (!validated.ok) {
|
|
212
|
+
throw new Error(`Marketplace workflow "${id}" failed validation:\n${validated.errors.map((e) => ` ${e.message}`).join("\n")}`);
|
|
213
|
+
}
|
|
214
|
+
parsed = validated.workflow;
|
|
215
|
+
}
|
|
216
|
+
// 3. Load existing config to detect mismatches
|
|
198
217
|
const fileConfig = loadConfigFile(cwd);
|
|
199
218
|
const workflowSkills = extractSkillsFromYaml(fetched.yaml);
|
|
200
219
|
const mismatches = computeProviderMismatch(workflowSkills, fileConfig, availableSkills);
|
|
201
|
-
//
|
|
220
|
+
// 4. Adapt if needed + possible. Schema was already validated above, so
|
|
221
|
+
// adaptation failure no longer hides a schema failure — it falls back to
|
|
222
|
+
// installing the validated-original.
|
|
202
223
|
let finalYaml = fetched.yaml;
|
|
203
224
|
let adapted = false;
|
|
204
225
|
if (mismatches.length > 0 && claude) {
|
|
205
226
|
try {
|
|
206
|
-
const parsed = workflowZ.parse(parseYaml(fetched.yaml));
|
|
207
|
-
const errs = validateWorkflow(parsed);
|
|
208
|
-
if (errs.length > 0) {
|
|
209
|
-
throw new Error(`Marketplace workflow has schema errors: ${errs.map((e) => e.message).join("; ")}`);
|
|
210
|
-
}
|
|
211
227
|
const refined = await adaptWorkflowInteractive(parsed, {
|
|
212
228
|
claude,
|
|
213
229
|
skills: availableSkills,
|
|
@@ -224,7 +240,17 @@ export async function installMarketplaceWorkflow(id, options) {
|
|
|
224
240
|
addedEnvKeys: 0,
|
|
225
241
|
};
|
|
226
242
|
}
|
|
227
|
-
|
|
243
|
+
// Re-validate the refined output before writing. refineWorkflow runs
|
|
244
|
+
// an LLM that produces a new Workflow object — it could drift away
|
|
245
|
+
// from the upfront-validated parse (renamed required fields, dropped
|
|
246
|
+
// edges). Reparsing the stringified YAML closes that loop so we
|
|
247
|
+
// never write content that hasn't been schema-checked.
|
|
248
|
+
const refinedYaml = stringifyYaml(refined, { indent: 2, lineWidth: 120 });
|
|
249
|
+
const revalidated = validateParsed(parseYaml(refinedYaml));
|
|
250
|
+
if (!revalidated.ok) {
|
|
251
|
+
throw new Error(`Adapted workflow failed re-validation:\n${revalidated.errors.map((e) => ` ${e.message}`).join("\n")}`);
|
|
252
|
+
}
|
|
253
|
+
finalYaml = refinedYaml;
|
|
228
254
|
adapted = true;
|
|
229
255
|
}
|
|
230
256
|
catch (err) {
|
package/dist/cli/publish.d.ts
CHANGED
|
@@ -32,4 +32,14 @@ export declare function validateSkillDir(dirPath: string): {
|
|
|
32
32
|
errors: string[];
|
|
33
33
|
};
|
|
34
34
|
export declare function runPublish(): Promise<PublishResult | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Recursively copy a skill source directory to its destination (Fix #17).
|
|
37
|
+
*
|
|
38
|
+
* Previously a flat `fs.readdirSync` + `fs.copyFileSync` loop: skills with
|
|
39
|
+
* nested directories (scripts/, references/, assets/) either silently
|
|
40
|
+
* shipped incomplete or — more often — threw EISDIR on the first subdir.
|
|
41
|
+
*
|
|
42
|
+
* Exported for tests; node:fs cp's recursive mode preserves structure.
|
|
43
|
+
*/
|
|
44
|
+
export declare function copySkillDir(sourceDir: string, destDir: string): void;
|
|
35
45
|
export declare function registerPublishCommand(program: Command): void;
|
package/dist/cli/publish.js
CHANGED
|
@@ -14,7 +14,7 @@ import chalk from "chalk";
|
|
|
14
14
|
import { parse as parseYaml } from "yaml";
|
|
15
15
|
import { parseWorkflow, validateWorkflow } from "../schema.js";
|
|
16
16
|
import { builtinSkills } from "../skills/index.js";
|
|
17
|
-
import {
|
|
17
|
+
import { discoverSkillsWithDiagnostics } from "../skills/custom-loader.js";
|
|
18
18
|
// ── Types ──────────────────────────────────────────────────────────────
|
|
19
19
|
const VALID_CATEGORIES = ["triage", "security", "devops", "code-review", "testing", "content", "ops"];
|
|
20
20
|
const VALID_SKILL_ID = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
@@ -35,8 +35,12 @@ export function validateWorkflowFile(filePath) {
|
|
|
35
35
|
}
|
|
36
36
|
try {
|
|
37
37
|
const workflow = parseWorkflow(parsed);
|
|
38
|
-
// Build known skill IDs from builtins + custom + inline workflow skills
|
|
39
|
-
|
|
38
|
+
// Build known skill IDs from builtins + custom + inline workflow skills.
|
|
39
|
+
// Surface skill-loader diagnostics (malformed frontmatter, invalid ids,
|
|
40
|
+
// duplicate overrides) so publishers don't ship broken dependencies.
|
|
41
|
+
const { skills: customSkills, warnings: skillWarnings } = discoverSkillsWithDiagnostics();
|
|
42
|
+
for (const w of skillWarnings)
|
|
43
|
+
warnings.push(w.message);
|
|
40
44
|
const allSkillIds = new Set([...builtinSkills.map((s) => s.id), ...customSkills.map((s) => s.id)]);
|
|
41
45
|
// Inline skills defined in the workflow's skills block (a Record<id, def>) are also valid
|
|
42
46
|
if (workflow.skills) {
|
|
@@ -343,12 +347,7 @@ async function openGitHubPR(type, id, sourcePath, meta) {
|
|
|
343
347
|
}
|
|
344
348
|
else {
|
|
345
349
|
const destDir = path.join(tmpDir, "skills", "community", id);
|
|
346
|
-
|
|
347
|
-
// Copy entire skill directory
|
|
348
|
-
const files = fs.readdirSync(sourcePath);
|
|
349
|
-
for (const file of files) {
|
|
350
|
-
fs.copyFileSync(path.join(sourcePath, file), path.join(destDir, file));
|
|
351
|
-
}
|
|
350
|
+
copySkillDir(sourcePath, destDir);
|
|
352
351
|
execFileSync("git", ["-C", tmpDir, "add", `skills/community/${id}`], { stdio: "pipe" });
|
|
353
352
|
}
|
|
354
353
|
s.message("Pushing...");
|
|
@@ -396,16 +395,25 @@ async function saveLocally(type, id, sourcePath) {
|
|
|
396
395
|
}
|
|
397
396
|
else {
|
|
398
397
|
const destDir = path.join(outputDir, id);
|
|
399
|
-
|
|
400
|
-
const files = fs.readdirSync(sourcePath);
|
|
401
|
-
for (const file of files) {
|
|
402
|
-
fs.copyFileSync(path.join(sourcePath, file), path.join(destDir, file));
|
|
403
|
-
}
|
|
398
|
+
copySkillDir(sourcePath, destDir);
|
|
404
399
|
p.log.success(`Saved to ${chalk.cyan(destDir)}`);
|
|
405
400
|
p.log.info(`Submit manually: copy to skills/community/ in a fork of swenyai/marketplace`);
|
|
406
401
|
return { type, id, name: id, outputPath: destDir };
|
|
407
402
|
}
|
|
408
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Recursively copy a skill source directory to its destination (Fix #17).
|
|
406
|
+
*
|
|
407
|
+
* Previously a flat `fs.readdirSync` + `fs.copyFileSync` loop: skills with
|
|
408
|
+
* nested directories (scripts/, references/, assets/) either silently
|
|
409
|
+
* shipped incomplete or — more often — threw EISDIR on the first subdir.
|
|
410
|
+
*
|
|
411
|
+
* Exported for tests; node:fs cp's recursive mode preserves structure.
|
|
412
|
+
*/
|
|
413
|
+
export function copySkillDir(sourceDir, destDir) {
|
|
414
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
415
|
+
fs.cpSync(sourceDir, destDir, { recursive: true });
|
|
416
|
+
}
|
|
409
417
|
// ── Command registration ───────────────────────────────────────────────
|
|
410
418
|
export function registerPublishCommand(program) {
|
|
411
419
|
program
|
package/dist/executor.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
import { consoleLogger } from "./types.js";
|
|
16
16
|
import { resolveSources } from "./source-resolver.js";
|
|
17
17
|
import { evaluateVerify } from "./verify.js";
|
|
18
|
+
import { evaluateRequires } from "./requires.js";
|
|
19
|
+
import { buildRetryPreamble } from "./retry.js";
|
|
18
20
|
/**
|
|
19
21
|
* Execute a workflow from entry to completion.
|
|
20
22
|
*
|
|
@@ -96,6 +98,33 @@ export async function execute(workflow, input, options) {
|
|
|
96
98
|
input,
|
|
97
99
|
...Object.fromEntries([...results.entries()].map(([k, v]) => [k, v.data])),
|
|
98
100
|
};
|
|
101
|
+
// Pre-condition gate: evaluate `requires` against the cross-node context
|
|
102
|
+
// BEFORE invoking the LLM. Failure either marks the node failed (on_fail
|
|
103
|
+
// default) or skipped (on_fail: "skip") and skips execution entirely.
|
|
104
|
+
const requiresError = evaluateRequires(node.requires, context);
|
|
105
|
+
if (requiresError) {
|
|
106
|
+
const onFail = node.requires?.on_fail ?? "fail";
|
|
107
|
+
const result = onFail === "skip"
|
|
108
|
+
? {
|
|
109
|
+
status: "skipped",
|
|
110
|
+
data: { skipped_reason: requiresError.replace(/^requires failed:/, "requires not met:") },
|
|
111
|
+
toolCalls: [],
|
|
112
|
+
}
|
|
113
|
+
: {
|
|
114
|
+
status: "failed",
|
|
115
|
+
data: { error: requiresError },
|
|
116
|
+
toolCalls: [],
|
|
117
|
+
};
|
|
118
|
+
results.set(currentId, result);
|
|
119
|
+
trace.steps.push({ node: currentId, status: result.status, iteration });
|
|
120
|
+
safeObserve(observer, { type: "node:exit", node: currentId, result }, logger);
|
|
121
|
+
logger.warn(` requires ${onFail === "skip" ? "skipped" : "failed"}: ${requiresError}`, { node: currentId });
|
|
122
|
+
// Apply normal routing rules (dry run gate + resolveNext).
|
|
123
|
+
// TODO: dedupe with requires path — see advanceFromNode helper below
|
|
124
|
+
const next = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace);
|
|
125
|
+
currentId = next;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
99
128
|
// Wrap tool handlers to emit events + inject context
|
|
100
129
|
const trackedTools = tools.map((t) => ({
|
|
101
130
|
...t,
|
|
@@ -111,56 +140,59 @@ export async function execute(workflow, input, options) {
|
|
|
111
140
|
const effectiveRules = assembleCascaded("rules", currentId, workflow.nodes[currentId], inputRules.length, (workflow.rules ?? []).length, resolvedSources);
|
|
112
141
|
const effectiveContext = assembleCascaded("context", currentId, workflow.nodes[currentId], inputContext.length, (workflow.context ?? []).length, resolvedSources);
|
|
113
142
|
const instruction = buildNodeInstruction(resolvedInstruction, effectiveRules, effectiveContext, input, skillInstructions);
|
|
114
|
-
// Run Claude on this node
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
143
|
+
// Run Claude on this node, with optional verify-failure retry loop.
|
|
144
|
+
let attempt = 0;
|
|
145
|
+
let result;
|
|
146
|
+
let currentInstruction = instruction;
|
|
147
|
+
const retry = node.retry;
|
|
148
|
+
while (true) {
|
|
149
|
+
result = await claude.run({
|
|
150
|
+
instruction: currentInstruction,
|
|
151
|
+
context,
|
|
152
|
+
tools: trackedTools,
|
|
153
|
+
outputSchema: node.output,
|
|
154
|
+
maxTurns: node.max_turns,
|
|
155
|
+
onProgress: (message) => {
|
|
156
|
+
safeObserve(observer, { type: "node:progress", node: currentId, message }, logger);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
// Retry only triggers on verify failure — bail on tool/API errors.
|
|
160
|
+
if (result.status !== "success")
|
|
161
|
+
break;
|
|
131
162
|
const verifyError = evaluateVerify(node.verify, result);
|
|
132
|
-
if (verifyError)
|
|
133
|
-
|
|
134
|
-
|
|
163
|
+
if (!verifyError)
|
|
164
|
+
break;
|
|
165
|
+
// Apply verify failure to the result.
|
|
166
|
+
result.status = "failed";
|
|
167
|
+
result.data = { ...result.data, error: verifyError };
|
|
168
|
+
if (!retry || attempt >= retry.max) {
|
|
135
169
|
logger.warn(` verify failed: ${verifyError}`, { node: currentId });
|
|
170
|
+
break;
|
|
136
171
|
}
|
|
172
|
+
// Build retry preamble + record attempt in trace.
|
|
173
|
+
trace.steps.push({ node: currentId, status: "failed", iteration, retryAttempt: attempt });
|
|
174
|
+
logger.warn(` verify failed (attempt ${attempt + 1}/${retry.max + 1}): ${verifyError}`, { node: currentId });
|
|
175
|
+
const preamble = await buildRetryPreamble({
|
|
176
|
+
retry,
|
|
177
|
+
verifyError,
|
|
178
|
+
toolCalls: result.toolCalls,
|
|
179
|
+
nodeInstruction: resolvedInstruction,
|
|
180
|
+
claude,
|
|
181
|
+
logger,
|
|
182
|
+
context,
|
|
183
|
+
});
|
|
184
|
+
currentInstruction = `${preamble}\n\n---\n\n${instruction}`;
|
|
185
|
+
safeObserve(observer, { type: "node:retry", node: currentId, attempt: attempt + 1, reason: verifyError, preamble }, logger);
|
|
186
|
+
attempt++;
|
|
137
187
|
}
|
|
138
188
|
results.set(currentId, result);
|
|
139
|
-
trace.steps.push(
|
|
189
|
+
trace.steps.push(attempt > 0
|
|
190
|
+
? { node: currentId, status: result.status, iteration, retryAttempt: attempt }
|
|
191
|
+
: { node: currentId, status: result.status, iteration });
|
|
140
192
|
safeObserve(observer, { type: "node:exit", node: currentId, result }, logger);
|
|
141
193
|
logger.info(` ✓ ${result.status}`, { node: currentId, toolCalls: result.toolCalls.length });
|
|
142
|
-
// Dry run
|
|
143
|
-
|
|
144
|
-
// conditional edges are action decisions (investigate→create_issue/skip).
|
|
145
|
-
// Enforced in the executor so it cannot be bypassed by LLM evaluation.
|
|
146
|
-
const isDryRun = input && typeof input === "object" && input.dryRun === true;
|
|
147
|
-
if (isDryRun) {
|
|
148
|
-
const outEdges = workflow.edges.filter((e) => e.from === currentId);
|
|
149
|
-
if (outEdges.some((e) => e.when)) {
|
|
150
|
-
safeObserve(observer, { type: "route", from: currentId, to: "(end)", reason: "dry run" }, logger);
|
|
151
|
-
currentId = null;
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
// Resolve next node via edge conditions
|
|
156
|
-
const prevId = currentId;
|
|
157
|
-
currentId = await resolveNext(workflow, currentId, results, input, claude, observer, edgeCounts, logger);
|
|
158
|
-
// Record the routing decision in the trace
|
|
159
|
-
if (currentId) {
|
|
160
|
-
const edgeKey = `${prevId}→${currentId}`;
|
|
161
|
-
const reason = workflow.edges.find((e) => e.from === prevId && e.to === currentId)?.when ?? "only path";
|
|
162
|
-
trace.edges.push({ from: prevId, to: currentId, reason });
|
|
163
|
-
}
|
|
194
|
+
// Dry run gate + routing — shared with requires path via advanceFromNode helper.
|
|
195
|
+
currentId = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace);
|
|
164
196
|
}
|
|
165
197
|
safeObserve(observer, {
|
|
166
198
|
type: "workflow:end",
|
|
@@ -341,6 +373,35 @@ function resolveConfig(skills, overrides) {
|
|
|
341
373
|
}
|
|
342
374
|
return config;
|
|
343
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Apply the dry-run gate and then resolve the next node via edge conditions.
|
|
378
|
+
*
|
|
379
|
+
* Used in both the normal execution path and the requires-failure path so
|
|
380
|
+
* the dry-run guard + resolveNext + trace-edge recording logic lives in one place.
|
|
381
|
+
*
|
|
382
|
+
* Returns the next node ID, or null when execution should stop.
|
|
383
|
+
*/
|
|
384
|
+
async function advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace) {
|
|
385
|
+
// Dry run hard gate — stop at the first conditional routing decision.
|
|
386
|
+
// Unconditional edges are analysis flow (prepare→gather→investigate);
|
|
387
|
+
// conditional edges are action decisions (investigate→create_issue/skip).
|
|
388
|
+
// Enforced in the executor so it cannot be bypassed by LLM evaluation.
|
|
389
|
+
const isDryRun = input && typeof input === "object" && input.dryRun === true;
|
|
390
|
+
if (isDryRun) {
|
|
391
|
+
const outEdges = workflow.edges.filter((e) => e.from === currentId);
|
|
392
|
+
if (outEdges.some((e) => e.when)) {
|
|
393
|
+
safeObserve(observer, { type: "route", from: currentId, to: "(end)", reason: "dry run" }, logger);
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const prevId = currentId;
|
|
398
|
+
const nextId = await resolveNext(workflow, currentId, results, input, claude, observer, edgeCounts, logger);
|
|
399
|
+
if (nextId) {
|
|
400
|
+
const reason = workflow.edges.find((e) => e.from === prevId && e.to === nextId)?.when ?? "only path";
|
|
401
|
+
trace.edges.push({ from: prevId, to: nextId, reason });
|
|
402
|
+
}
|
|
403
|
+
return nextId;
|
|
404
|
+
}
|
|
344
405
|
/**
|
|
345
406
|
* Resolve which edge to follow from the current node.
|
|
346
407
|
*
|
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow loader — read, parse, validate.
|
|
3
|
+
*
|
|
4
|
+
* The canonical path for CLI flows that read a workflow off disk. Composes
|
|
5
|
+
* file I/O + YAML/JSON parse + Zod schema parse + structural validation and
|
|
6
|
+
* returns a single tagged result so callers can render errors uniformly.
|
|
7
|
+
*
|
|
8
|
+
* Use this instead of calling `parseWorkflow` / `validateWorkflow` directly
|
|
9
|
+
* so the three user-facing surfaces (`sweny workflow run`, `sweny workflow
|
|
10
|
+
* validate`, `sweny publish`) agree on what "valid" means.
|
|
11
|
+
*/
|
|
12
|
+
import type { Workflow } from "./types.js";
|
|
13
|
+
import { type WorkflowError } from "./schema.js";
|
|
14
|
+
/** Structural/schema error. `code` is present only for structural errors. */
|
|
15
|
+
export interface LoaderError {
|
|
16
|
+
message: string;
|
|
17
|
+
code?: WorkflowError["code"] | "IO" | "PARSE" | "SCHEMA";
|
|
18
|
+
nodeId?: string;
|
|
19
|
+
}
|
|
20
|
+
export type LoaderResult = {
|
|
21
|
+
ok: true;
|
|
22
|
+
workflow: Workflow;
|
|
23
|
+
} | {
|
|
24
|
+
ok: false;
|
|
25
|
+
errors: LoaderError[];
|
|
26
|
+
};
|
|
27
|
+
export interface LoaderOptions {
|
|
28
|
+
/** Optional set of known skill IDs for UNKNOWN_SKILL validation. */
|
|
29
|
+
knownSkills?: Set<string>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Load a workflow YAML or JSON file, parse with the canonical Zod schema,
|
|
33
|
+
* and run structural validation. All three phases contribute to `errors`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadAndValidateWorkflow(filePath: string, options?: LoaderOptions): LoaderResult;
|
|
36
|
+
/**
|
|
37
|
+
* Validate an already-parsed workflow object (e.g. from a marketplace fetch
|
|
38
|
+
* or Studio import). Same schema + structural passes as the file loader.
|
|
39
|
+
*/
|
|
40
|
+
export declare function validateParsed(raw: unknown, options?: LoaderOptions): LoaderResult;
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow loader — read, parse, validate.
|
|
3
|
+
*
|
|
4
|
+
* The canonical path for CLI flows that read a workflow off disk. Composes
|
|
5
|
+
* file I/O + YAML/JSON parse + Zod schema parse + structural validation and
|
|
6
|
+
* returns a single tagged result so callers can render errors uniformly.
|
|
7
|
+
*
|
|
8
|
+
* Use this instead of calling `parseWorkflow` / `validateWorkflow` directly
|
|
9
|
+
* so the three user-facing surfaces (`sweny workflow run`, `sweny workflow
|
|
10
|
+
* validate`, `sweny publish`) agree on what "valid" means.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { parse as parseYaml } from "yaml";
|
|
15
|
+
import { ZodError } from "zod";
|
|
16
|
+
import { workflowZ, validateWorkflow } from "./schema.js";
|
|
17
|
+
/**
|
|
18
|
+
* Load a workflow YAML or JSON file, parse with the canonical Zod schema,
|
|
19
|
+
* and run structural validation. All three phases contribute to `errors`.
|
|
20
|
+
*/
|
|
21
|
+
export function loadAndValidateWorkflow(filePath, options = {}) {
|
|
22
|
+
let content;
|
|
23
|
+
try {
|
|
24
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
28
|
+
return { ok: false, errors: [{ message: `Could not read "${filePath}": ${msg}`, code: "IO" }] };
|
|
29
|
+
}
|
|
30
|
+
let raw;
|
|
31
|
+
try {
|
|
32
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
33
|
+
raw = ext === ".yaml" || ext === ".yml" ? parseYaml(content) : JSON.parse(content);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
37
|
+
return { ok: false, errors: [{ message: `Could not parse "${filePath}": ${msg}`, code: "PARSE" }] };
|
|
38
|
+
}
|
|
39
|
+
return validateParsed(raw, options);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Validate an already-parsed workflow object (e.g. from a marketplace fetch
|
|
43
|
+
* or Studio import). Same schema + structural passes as the file loader.
|
|
44
|
+
*/
|
|
45
|
+
export function validateParsed(raw, options = {}) {
|
|
46
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
47
|
+
const kind = raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw;
|
|
48
|
+
return { ok: false, errors: [{ message: `Expected a workflow object, got ${kind}`, code: "SCHEMA" }] };
|
|
49
|
+
}
|
|
50
|
+
let parsed;
|
|
51
|
+
try {
|
|
52
|
+
parsed = workflowZ.parse(raw);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
if (err instanceof ZodError) {
|
|
56
|
+
const errors = err.issues.map((iss) => ({
|
|
57
|
+
message: `${iss.path.length > 0 ? iss.path.join(".") + ": " : ""}${iss.message}`,
|
|
58
|
+
code: "SCHEMA",
|
|
59
|
+
}));
|
|
60
|
+
return { ok: false, errors };
|
|
61
|
+
}
|
|
62
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
63
|
+
return { ok: false, errors: [{ message: msg, code: "SCHEMA" }] };
|
|
64
|
+
}
|
|
65
|
+
const structural = validateWorkflow(parsed, options.knownSkills);
|
|
66
|
+
if (structural.length > 0) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
errors: structural.map((e) => ({ message: e.message, code: e.code, nodeId: e.nodeId })),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return { ok: true, workflow: parsed };
|
|
73
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Provider Catalog — single source of truth for provider/MCP wiring.
|
|
3
|
+
*
|
|
4
|
+
* Before this catalog existed, `buildSkillMcpServers` (skill-driven) and
|
|
5
|
+
* `buildAutoMcpServers` (flag-driven, legacy triage/implement) each encoded
|
|
6
|
+
* the same transport/auth/env for every provider. Drift was inevitable.
|
|
7
|
+
*
|
|
8
|
+
* Each entry declares:
|
|
9
|
+
* - which trigger paths (skill id, provider flag, workspace tool) wire it
|
|
10
|
+
* - how to build the McpServerConfig from credentials
|
|
11
|
+
* - whether the server uses stdio or http transport
|
|
12
|
+
* - if stdio: why the `npx -y` policy exception is acceptable (Fix #18)
|
|
13
|
+
*
|
|
14
|
+
* Adding a provider is a one-place edit. A module-load assertion and a
|
|
15
|
+
* test enforce that every stdio entry declares its exception reason.
|
|
16
|
+
*/
|
|
17
|
+
import type { McpServerConfig } from "./types.js";
|
|
18
|
+
export interface McpCatalogEntry {
|
|
19
|
+
/** Canonical server id. Used as the key in the returned `Record<string, McpServerConfig>`. */
|
|
20
|
+
id: string;
|
|
21
|
+
/** Which trigger paths wire this server. Most entries match both skill-driven and flag-driven paths. */
|
|
22
|
+
triggers: {
|
|
23
|
+
/** Skill IDs that should wire this server (skill-driven path). */
|
|
24
|
+
skill?: string[];
|
|
25
|
+
/** Matching values for `sourceControlProvider` (flag-driven path). */
|
|
26
|
+
sourceControl?: string[];
|
|
27
|
+
/** Matching values for `issueTrackerProvider` (flag-driven path). */
|
|
28
|
+
issueTracker?: string[];
|
|
29
|
+
/** Matching entries in `observabilityProviders` (flag-driven path). */
|
|
30
|
+
observability?: string[];
|
|
31
|
+
/** Matching entries in `workspaceTools` (flag-driven path). */
|
|
32
|
+
workspaceTool?: string[];
|
|
33
|
+
};
|
|
34
|
+
/** Build the MCP server config from creds. Returns undefined if required creds are missing. */
|
|
35
|
+
wire: (creds: Record<string, string>) => McpServerConfig | undefined;
|
|
36
|
+
/** Transport family. Informational + policy enforcement (Fix #18). */
|
|
37
|
+
transport: "http" | "stdio";
|
|
38
|
+
/**
|
|
39
|
+
* For stdio servers, why `npx -y` is allowed. Null forbids the exception.
|
|
40
|
+
* Http servers must set this to null (no exception needed).
|
|
41
|
+
*
|
|
42
|
+
* ARCHITECTURE.md documents the acceptable reasons (official first-party
|
|
43
|
+
* vendor server with no public HTTP endpoint, etc.).
|
|
44
|
+
*/
|
|
45
|
+
npxExceptionReason: string | null;
|
|
46
|
+
}
|
|
47
|
+
export declare const MCP_CATALOG: readonly McpCatalogEntry[];
|