@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/browser.d.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Browser-safe entry point for @sweny-ai/core
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Exports only APIs that bundle cleanly in browser environments (Vite,
|
|
5
|
+
* webpack, Astro, etc.). Anything that pulls `node:fs`, `node:path`, or the
|
|
6
|
+
* Claude Code SDK lives in the Node entry (`@sweny-ai/core`).
|
|
7
|
+
*
|
|
8
|
+
* Not exported from here:
|
|
9
|
+
* - `ClaudeClient` — depends on `@anthropic-ai/claude-agent-sdk` (Node-only).
|
|
10
|
+
* - `execute` / `ExecuteOptions` — `executor.ts` → `source-resolver.ts`
|
|
11
|
+
* imports `node:fs/promises` for URL/file Source fetching. Import from
|
|
12
|
+
* the Node entry when you need to run a workflow.
|
|
13
|
+
* - `buildWorkflow` / `refineWorkflow` — depend on the Claude interface.
|
|
14
|
+
* - Testing helpers — `MockClaude`, `createFileSkill` use `node:fs`.
|
|
7
15
|
*/
|
|
8
16
|
export type { Skill, SkillCategory, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, } from "./types.js";
|
|
9
17
|
export { consoleLogger } from "./types.js";
|
|
10
|
-
export { execute } from "./executor.js";
|
|
11
|
-
export type { ExecuteOptions } from "./executor.js";
|
|
12
18
|
export { github, linear, slack, sentry, datadog, notification, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
|
|
13
19
|
export type { SkillValidationResult } from "./skills/index.js";
|
|
14
20
|
export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema } from "./schema.js";
|
package/dist/browser.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Browser-safe entry point for @sweny-ai/core
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Exports only APIs that bundle cleanly in browser environments (Vite,
|
|
5
|
+
* webpack, Astro, etc.). Anything that pulls `node:fs`, `node:path`, or the
|
|
6
|
+
* Claude Code SDK lives in the Node entry (`@sweny-ai/core`).
|
|
7
|
+
*
|
|
8
|
+
* Not exported from here:
|
|
9
|
+
* - `ClaudeClient` — depends on `@anthropic-ai/claude-agent-sdk` (Node-only).
|
|
10
|
+
* - `execute` / `ExecuteOptions` — `executor.ts` → `source-resolver.ts`
|
|
11
|
+
* imports `node:fs/promises` for URL/file Source fetching. Import from
|
|
12
|
+
* the Node entry when you need to run a workflow.
|
|
13
|
+
* - `buildWorkflow` / `refineWorkflow` — depend on the Claude interface.
|
|
14
|
+
* - Testing helpers — `MockClaude`, `createFileSkill` use `node:fs`.
|
|
7
15
|
*/
|
|
8
16
|
export { consoleLogger } from "./types.js";
|
|
9
|
-
// Executor
|
|
10
|
-
export { execute } from "./executor.js";
|
|
11
17
|
// Skills (browser-safe — no filesystem access, no `process.env` reads)
|
|
12
18
|
export { github, linear, slack, sentry, datadog, notification, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
|
|
13
19
|
// Schema & validation
|
|
14
20
|
export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema } from "./schema.js";
|
|
15
21
|
// Studio adapter
|
|
16
22
|
export { workflowToFlow, flowToWorkflow, applyExecutionEvent, exportAsTypescript, getSkillCatalog } from "./studio.js";
|
|
17
|
-
// Workflow builder — NOT included in browser entry.
|
|
18
|
-
// buildWorkflow/refineWorkflow depend on the Claude interface (Node-only).
|
|
19
|
-
// Import from "@sweny-ai/core" in Node environments.
|
|
20
|
-
// Testing utilities — NOT included in browser entry.
|
|
21
|
-
// MockClaude and createFileSkill depend on node:fs.
|
|
22
|
-
// Import from "@sweny-ai/core/testing" in Node environments.
|
package/dist/claude.d.ts
CHANGED
|
@@ -53,4 +53,24 @@ export declare class ClaudeClient implements Claude {
|
|
|
53
53
|
description: string;
|
|
54
54
|
}[];
|
|
55
55
|
}): Promise<string>;
|
|
56
|
+
ask(opts: {
|
|
57
|
+
instruction: string;
|
|
58
|
+
context: Record<string, unknown>;
|
|
59
|
+
}): Promise<string>;
|
|
56
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Best-effort recovery of typed output from a tool_result's `content`.
|
|
63
|
+
*
|
|
64
|
+
* The MCP protocol sends tool results as string content. Our in-process
|
|
65
|
+
* wrapper JSON-stringifies structured output before returning; external
|
|
66
|
+
* MCP servers generally do the same for JSON payloads. If the string
|
|
67
|
+
* looks like a JSON object or array and parses, return the parsed value
|
|
68
|
+
* so verify's output-path walks work against typed data.
|
|
69
|
+
*
|
|
70
|
+
* Raw strings are preserved verbatim. JSON-primitive strings (e.g. the
|
|
71
|
+
* literal four characters `"42"`) are intentionally NOT parsed — we
|
|
72
|
+
* cannot distinguish a tool that returned the number 42 (wrapper sends
|
|
73
|
+
* `"42"`) from a tool that returned the string "42" (wrapper also sends
|
|
74
|
+
* `"42"`). Preserving the string is safer than guessing.
|
|
75
|
+
*/
|
|
76
|
+
export declare function parseToolResultContent(content: unknown): unknown;
|
package/dist/claude.js
CHANGED
|
@@ -41,9 +41,28 @@ export class ClaudeClient {
|
|
|
41
41
|
}
|
|
42
42
|
async run(opts) {
|
|
43
43
|
const { instruction, context, tools, outputSchema, onProgress, maxTurns } = opts;
|
|
44
|
+
// Tool-call accounting (Fix #1).
|
|
45
|
+
//
|
|
46
|
+
// Stream-driven recording, correlated by tool_use_id:
|
|
47
|
+
// - `assistant` message with tool_use block → create a pending ToolCall
|
|
48
|
+
// and register it in `pendingByUseId` under its tool_use_id.
|
|
49
|
+
// - `user` message with tool_result block → look up by tool_use_id, set
|
|
50
|
+
// `status` from `is_error`, and recover the typed output by parsing
|
|
51
|
+
// the stringified `content` field (our wrapper JSON-stringifies it
|
|
52
|
+
// before returning; external MCP servers also return structured
|
|
53
|
+
// content).
|
|
54
|
+
//
|
|
55
|
+
// Keying by tool_use_id (not by tool name) avoids mis-pairing when
|
|
56
|
+
// Claude invokes the same tool in parallel — the self-review test
|
|
57
|
+
// `pairs parallel same-named in-process tool outputs correctly` shows
|
|
58
|
+
// why FIFO-by-name is wrong.
|
|
44
59
|
const toolCalls = [];
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
const pendingByUseId = new Map();
|
|
61
|
+
// Convert core tools to SDK MCP tools. The wrapper invokes the user's
|
|
62
|
+
// handler and returns `content` via the MCP transport. We do not push
|
|
63
|
+
// to toolCalls from the wrapper — the stream's tool_result is the sole
|
|
64
|
+
// signal of completion, which keeps pairing correct under parallelism.
|
|
65
|
+
const sdkTools = tools.map((t) => coreToolToSdkTool(t, this.defaultContext));
|
|
47
66
|
// Create in-process MCP server
|
|
48
67
|
const mcpServer = createSdkMcpServer({
|
|
49
68
|
name: "sweny-core",
|
|
@@ -96,19 +115,51 @@ export class ClaudeClient {
|
|
|
96
115
|
}
|
|
97
116
|
}
|
|
98
117
|
else if (message.type === "assistant") {
|
|
99
|
-
//
|
|
118
|
+
// Tool_use blocks start a ToolCall record. Status + output are
|
|
119
|
+
// filled in when the matching user tool_result arrives.
|
|
100
120
|
const am = message;
|
|
101
121
|
if (am.message?.content && Array.isArray(am.message.content)) {
|
|
102
122
|
for (const block of am.message.content) {
|
|
103
123
|
if (block.type === "tool_use") {
|
|
104
|
-
|
|
124
|
+
const call = {
|
|
105
125
|
tool: stripMcpPrefix(block.name ?? ""),
|
|
106
126
|
input: block.input,
|
|
107
|
-
}
|
|
127
|
+
};
|
|
128
|
+
toolCalls.push(call);
|
|
129
|
+
if (typeof block.id === "string")
|
|
130
|
+
pendingByUseId.set(block.id, call);
|
|
108
131
|
}
|
|
109
132
|
}
|
|
110
133
|
}
|
|
111
134
|
}
|
|
135
|
+
else if (message.type === "user") {
|
|
136
|
+
// Tool_result blocks close the loop. Pair to the pending ToolCall
|
|
137
|
+
// by tool_use_id — NOT by name, which would break parallel calls
|
|
138
|
+
// of the same tool (see regression test).
|
|
139
|
+
//
|
|
140
|
+
// Output handling is unified across in-process and external tools:
|
|
141
|
+
// our wrapper JSON-stringifies structured output before returning,
|
|
142
|
+
// and external MCP servers also send structured content as string.
|
|
143
|
+
// We parse the content back into a value so verify's output-path
|
|
144
|
+
// checks work against typed data.
|
|
145
|
+
const um = message;
|
|
146
|
+
if (um.message?.content && Array.isArray(um.message.content)) {
|
|
147
|
+
for (const block of um.message.content) {
|
|
148
|
+
if (block.type !== "tool_result" || typeof block.tool_use_id !== "string")
|
|
149
|
+
continue;
|
|
150
|
+
const call = pendingByUseId.get(block.tool_use_id);
|
|
151
|
+
if (!call)
|
|
152
|
+
continue;
|
|
153
|
+
// Delete after first match so a duplicate tool_result (SDK
|
|
154
|
+
// retry, malformed stream) cannot overwrite a completed call.
|
|
155
|
+
pendingByUseId.delete(block.tool_use_id);
|
|
156
|
+
const isError = block.is_error === true;
|
|
157
|
+
call.status = isError ? "error" : "success";
|
|
158
|
+
const parsed = parseToolResultContent(block.content);
|
|
159
|
+
call.output = isError ? { error: parsed } : parsed;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
112
163
|
else if (message.type === "result") {
|
|
113
164
|
const resultMsg = message;
|
|
114
165
|
if (resultMsg.subtype === "success" && "result" in resultMsg) {
|
|
@@ -202,26 +253,72 @@ export class ClaudeClient {
|
|
|
202
253
|
this.logger.warn(`Could not parse route choice from: "${text.slice(0, 100)}". Falling back to first choice.`);
|
|
203
254
|
return validIds[0];
|
|
204
255
|
}
|
|
256
|
+
async ask(opts) {
|
|
257
|
+
const { instruction, context } = opts;
|
|
258
|
+
const prompt = [
|
|
259
|
+
instruction,
|
|
260
|
+
Object.keys(context).length > 0 ? `\nContext:\n\`\`\`json\n${JSON.stringify(context, null, 2)}\n\`\`\`` : "",
|
|
261
|
+
]
|
|
262
|
+
.filter(Boolean)
|
|
263
|
+
.join("\n");
|
|
264
|
+
const env = this.buildEnv();
|
|
265
|
+
let response = "";
|
|
266
|
+
try {
|
|
267
|
+
const stream = query({
|
|
268
|
+
prompt,
|
|
269
|
+
options: {
|
|
270
|
+
maxTurns: 1,
|
|
271
|
+
cwd: this.cwd,
|
|
272
|
+
env,
|
|
273
|
+
permissionMode: "bypassPermissions",
|
|
274
|
+
allowDangerouslySkipPermissions: true,
|
|
275
|
+
stderr: (data) => this.logger.debug(`[claude-code] ${data}`),
|
|
276
|
+
...(this.model ? { model: this.model } : {}),
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
for await (const message of stream) {
|
|
280
|
+
if (message.type === "result") {
|
|
281
|
+
const resultMsg = message;
|
|
282
|
+
if (resultMsg.subtype === "success" && "result" in resultMsg) {
|
|
283
|
+
response = resultMsg.result;
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
this.logger.warn(`claude.ask: SDK returned non-success subtype "${resultMsg.subtype}" — returning empty string`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
this.logger.warn(`Ask query failed: ${err.message}`);
|
|
293
|
+
return "";
|
|
294
|
+
}
|
|
295
|
+
return response.trim();
|
|
296
|
+
}
|
|
205
297
|
}
|
|
206
298
|
// ─── Tool conversion ────────────────────────────────────────────
|
|
207
299
|
/**
|
|
208
300
|
* Convert a core Tool to an SDK MCP tool definition.
|
|
209
|
-
*
|
|
301
|
+
*
|
|
302
|
+
* The wrapper runs the user's handler and returns the MCP `CallToolResult`.
|
|
303
|
+
* Output bookkeeping happens on the stream side (see the tool_result
|
|
304
|
+
* handler in `run()`), keyed by `tool_use_id` — never from inside the
|
|
305
|
+
* wrapper. Keeping the wrapper stateless means parallel calls to the same
|
|
306
|
+
* tool cannot mis-pair outputs.
|
|
210
307
|
*/
|
|
211
|
-
function coreToolToSdkTool(coreTool, defaultCtx
|
|
308
|
+
function coreToolToSdkTool(coreTool, defaultCtx) {
|
|
212
309
|
const zodShape = jsonSchemaToZodShape(coreTool.input_schema);
|
|
213
310
|
return sdkTool(coreTool.name, coreTool.description, zodShape, async (args) => {
|
|
214
311
|
try {
|
|
215
312
|
// The executor wraps handlers to inject ToolContext.
|
|
216
313
|
// When used standalone, defaultCtx is the fallback.
|
|
217
314
|
const output = await coreTool.handler(args, defaultCtx);
|
|
218
|
-
|
|
315
|
+
// JSON-stringify structured output so the tool_result handler can
|
|
316
|
+
// recover it via parseToolResultContent. Strings pass through.
|
|
219
317
|
return {
|
|
220
318
|
content: [{ type: "text", text: typeof output === "string" ? output : JSON.stringify(output) }],
|
|
221
319
|
};
|
|
222
320
|
}
|
|
223
321
|
catch (err) {
|
|
224
|
-
toolCalls.push({ tool: coreTool.name, input: args, output: { error: err.message } });
|
|
225
322
|
return {
|
|
226
323
|
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
227
324
|
isError: true,
|
|
@@ -229,6 +326,39 @@ function coreToolToSdkTool(coreTool, defaultCtx, toolCalls) {
|
|
|
229
326
|
}
|
|
230
327
|
});
|
|
231
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Best-effort recovery of typed output from a tool_result's `content`.
|
|
331
|
+
*
|
|
332
|
+
* The MCP protocol sends tool results as string content. Our in-process
|
|
333
|
+
* wrapper JSON-stringifies structured output before returning; external
|
|
334
|
+
* MCP servers generally do the same for JSON payloads. If the string
|
|
335
|
+
* looks like a JSON object or array and parses, return the parsed value
|
|
336
|
+
* so verify's output-path walks work against typed data.
|
|
337
|
+
*
|
|
338
|
+
* Raw strings are preserved verbatim. JSON-primitive strings (e.g. the
|
|
339
|
+
* literal four characters `"42"`) are intentionally NOT parsed — we
|
|
340
|
+
* cannot distinguish a tool that returned the number 42 (wrapper sends
|
|
341
|
+
* `"42"`) from a tool that returned the string "42" (wrapper also sends
|
|
342
|
+
* `"42"`). Preserving the string is safer than guessing.
|
|
343
|
+
*/
|
|
344
|
+
export function parseToolResultContent(content) {
|
|
345
|
+
if (typeof content !== "string")
|
|
346
|
+
return content;
|
|
347
|
+
const trimmed = content.trim();
|
|
348
|
+
if (trimmed.length === 0)
|
|
349
|
+
return content;
|
|
350
|
+
const first = trimmed[0];
|
|
351
|
+
// Only objects and arrays are unambiguous to parse. Strings, numbers,
|
|
352
|
+
// booleans, and null would all corrupt or discard information.
|
|
353
|
+
if (first !== "{" && first !== "[")
|
|
354
|
+
return content;
|
|
355
|
+
try {
|
|
356
|
+
return JSON.parse(trimmed);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return content;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
232
362
|
// ─── JSON Schema → Zod conversion ───────────────────────────────
|
|
233
363
|
/**
|
|
234
364
|
* Convert a JSON Schema object to a Zod raw shape for the agent SDK.
|
package/dist/cli/config.d.ts
CHANGED
|
@@ -76,6 +76,28 @@ export declare function parseCliInputs(options: Record<string, unknown>, fileCon
|
|
|
76
76
|
*/
|
|
77
77
|
export declare const SUPPORTED_WORKSPACE_TOOLS: Set<string>;
|
|
78
78
|
export declare function validateInputs(config: CliConfig): string[];
|
|
79
|
+
/**
|
|
80
|
+
* Parse a value as a positive integer (>= 1).
|
|
81
|
+
*
|
|
82
|
+
* Returns:
|
|
83
|
+
* - the parsed integer when the input represents a positive finite one
|
|
84
|
+
* - the fallback when the input is null/undefined/empty (unset flag)
|
|
85
|
+
* - NaN for any malformed or non-positive input, so validateInputs can
|
|
86
|
+
* surface a field-specific error
|
|
87
|
+
*
|
|
88
|
+
* Name enforces the contract: non-positive values (0, -5) return NaN so
|
|
89
|
+
* validation rejects them loudly rather than quietly passing through as
|
|
90
|
+
* `0` and breaking downstream invariants.
|
|
91
|
+
*
|
|
92
|
+
* Why zero is rejected: every current caller (max-investigate-turns,
|
|
93
|
+
* max-implement-turns, cache-ttl) uses the result as a positive count or
|
|
94
|
+
* timeout. There is no "zero means disabled" semantics anywhere — a
|
|
95
|
+
* cache-ttl of 0 wouldn't disable cache, it would expire entries
|
|
96
|
+
* immediately, which is broken. If a future flag wants "0 = disabled",
|
|
97
|
+
* use a sibling helper (parseNonNegativeInt) rather than weakening this
|
|
98
|
+
* one's contract.
|
|
99
|
+
*/
|
|
100
|
+
export declare function parsePositiveInt(raw: unknown, fallback: number): number;
|
|
79
101
|
/**
|
|
80
102
|
* Returns non-fatal warnings about the configuration.
|
|
81
103
|
* These are surfaced before the spinner starts but do not abort the run.
|
package/dist/cli/config.js
CHANGED
|
@@ -116,8 +116,8 @@ export function parseCliInputs(options, fileConfig = {}) {
|
|
|
116
116
|
severityFocus: options.severityFocus || f("severity-focus") || "errors",
|
|
117
117
|
serviceFilter: options.serviceFilter || f("service-filter") || "*",
|
|
118
118
|
investigationDepth: options.investigationDepth || f("investigation-depth") || "standard",
|
|
119
|
-
maxInvestigateTurns:
|
|
120
|
-
maxImplementTurns:
|
|
119
|
+
maxInvestigateTurns: parsePositiveInt(options.maxInvestigateTurns ?? f("max-investigate-turns"), 50),
|
|
120
|
+
maxImplementTurns: parsePositiveInt(options.maxImplementTurns ?? f("max-implement-turns"), 30),
|
|
121
121
|
baseBranch: options.baseBranch || f("base-branch") || "main",
|
|
122
122
|
prLabels: (options.prLabels || f("pr-labels") || "agent,triage,needs-review")
|
|
123
123
|
.split(",")
|
|
@@ -165,7 +165,7 @@ export function parseCliInputs(options, fileConfig = {}) {
|
|
|
165
165
|
stream: Boolean(options.stream),
|
|
166
166
|
bell: Boolean(options.bell),
|
|
167
167
|
cacheDir: options.cacheDir || env.SWENY_CACHE_DIR || f("cache-dir") || ".sweny/cache",
|
|
168
|
-
cacheTtl:
|
|
168
|
+
cacheTtl: parsePositiveInt(options.cacheTtl ?? f("cache-ttl"), 86400),
|
|
169
169
|
noCache: options.cache === false,
|
|
170
170
|
outputDir: options.outputDir || env.SWENY_OUTPUT_DIR || f("output-dir") || ".sweny/output",
|
|
171
171
|
mcpServers: parseMcpServers(env.SWENY_MCP_SERVERS || f("mcp-servers-json") || ""),
|
|
@@ -424,14 +424,61 @@ export function validateInputs(config) {
|
|
|
424
424
|
if (!["auto", "review"].includes(config.reviewMode)) {
|
|
425
425
|
errors.push("--review-mode must be one of: auto, review");
|
|
426
426
|
}
|
|
427
|
-
// Integer bounds
|
|
428
|
-
|
|
429
|
-
|
|
427
|
+
// Integer bounds — reject NaN and Infinity first (parseInt can yield NaN
|
|
428
|
+
// silently and NaN compares false to both < min and > max, so the old
|
|
429
|
+
// bounds check let junk through).
|
|
430
|
+
validateIntegerBound(errors, "--max-investigate-turns", config.maxInvestigateTurns, 1, 500);
|
|
431
|
+
validateIntegerBound(errors, "--max-implement-turns", config.maxImplementTurns, 1, 500);
|
|
432
|
+
return errors;
|
|
433
|
+
}
|
|
434
|
+
function validateIntegerBound(errors, flag, value, min, max) {
|
|
435
|
+
if (!Number.isFinite(value)) {
|
|
436
|
+
errors.push(`${flag} must be a finite integer between ${min} and ${max} (got "${value}")`);
|
|
437
|
+
return;
|
|
430
438
|
}
|
|
431
|
-
if (
|
|
432
|
-
errors.push(
|
|
439
|
+
if (value < min || value > max) {
|
|
440
|
+
errors.push(`${flag} must be between ${min} and ${max}`);
|
|
433
441
|
}
|
|
434
|
-
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Parse a value as a positive integer (>= 1).
|
|
445
|
+
*
|
|
446
|
+
* Returns:
|
|
447
|
+
* - the parsed integer when the input represents a positive finite one
|
|
448
|
+
* - the fallback when the input is null/undefined/empty (unset flag)
|
|
449
|
+
* - NaN for any malformed or non-positive input, so validateInputs can
|
|
450
|
+
* surface a field-specific error
|
|
451
|
+
*
|
|
452
|
+
* Name enforces the contract: non-positive values (0, -5) return NaN so
|
|
453
|
+
* validation rejects them loudly rather than quietly passing through as
|
|
454
|
+
* `0` and breaking downstream invariants.
|
|
455
|
+
*
|
|
456
|
+
* Why zero is rejected: every current caller (max-investigate-turns,
|
|
457
|
+
* max-implement-turns, cache-ttl) uses the result as a positive count or
|
|
458
|
+
* timeout. There is no "zero means disabled" semantics anywhere — a
|
|
459
|
+
* cache-ttl of 0 wouldn't disable cache, it would expire entries
|
|
460
|
+
* immediately, which is broken. If a future flag wants "0 = disabled",
|
|
461
|
+
* use a sibling helper (parseNonNegativeInt) rather than weakening this
|
|
462
|
+
* one's contract.
|
|
463
|
+
*/
|
|
464
|
+
export function parsePositiveInt(raw, fallback) {
|
|
465
|
+
if (raw === undefined || raw === null || raw === "")
|
|
466
|
+
return fallback;
|
|
467
|
+
if (typeof raw === "number") {
|
|
468
|
+
if (!Number.isFinite(raw))
|
|
469
|
+
return Number.NaN;
|
|
470
|
+
const truncated = Math.trunc(raw);
|
|
471
|
+
return truncated >= 1 ? truncated : Number.NaN;
|
|
472
|
+
}
|
|
473
|
+
const asString = String(raw).trim();
|
|
474
|
+
if (asString.length === 0)
|
|
475
|
+
return fallback;
|
|
476
|
+
// Number() is stricter than parseInt — "5o" → NaN rather than 5.
|
|
477
|
+
const parsed = Number(asString);
|
|
478
|
+
if (!Number.isFinite(parsed))
|
|
479
|
+
return Number.NaN;
|
|
480
|
+
const truncated = Math.trunc(parsed);
|
|
481
|
+
return truncated >= 1 ? truncated : Number.NaN;
|
|
435
482
|
}
|
|
436
483
|
const DEFAULT_SERVICE_MAP_PATH = ".github/service-map.yml";
|
|
437
484
|
/**
|
package/dist/cli/main.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import type { Workflow } from "../types.js";
|
|
3
|
-
export declare function loadWorkflowFile(filePath: string): Workflow;
|
|
3
|
+
export declare function loadWorkflowFile(filePath: string, knownSkills?: Set<string>): Workflow;
|
|
4
4
|
export declare function workflowRunAction(file: string, options: Record<string, unknown> & {
|
|
5
5
|
json?: boolean;
|
|
6
6
|
stream?: boolean;
|
package/dist/cli/main.js
CHANGED
|
@@ -11,11 +11,11 @@ import { triageWorkflow, implementWorkflow, seedContentWorkflow } from "../workf
|
|
|
11
11
|
import { consoleLogger } from "../types.js";
|
|
12
12
|
import { ClaudeClient } from "../claude.js";
|
|
13
13
|
import { createSkillMap, validateWorkflowSkills } from "../skills/index.js";
|
|
14
|
-
import { configuredSkills } from "../skills/custom-loader.js";
|
|
14
|
+
import { configuredSkills, configuredSkillsWithDiagnostics } from "../skills/custom-loader.js";
|
|
15
15
|
import { buildAutoMcpServers, buildSkillMcpServers, buildProviderContext } from "../mcp.js";
|
|
16
16
|
import { loadAdditionalContext } from "../templates.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import { loadAndValidateWorkflow } from "../loader.js";
|
|
18
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
19
19
|
import { buildWorkflow, refineWorkflow } from "../workflow-builder.js";
|
|
20
20
|
import { toMermaidBlock } from "../mermaid.js";
|
|
21
21
|
import { runWorkflowDiagram } from "./diagram.js";
|
|
@@ -140,11 +140,20 @@ function buildProviderCtx(config, mcpServers) {
|
|
|
140
140
|
/**
|
|
141
141
|
* Resolve rules and context from config into structured workflow input fields.
|
|
142
142
|
* All source kinds (inline, file, URL) are resolved eagerly.
|
|
143
|
+
*
|
|
144
|
+
* `offline` and `fetchAuth` are threaded through so CLI-preloaded rules/context
|
|
145
|
+
* honor the same policy as Sources resolved later by the executor (Fix #16).
|
|
143
146
|
*/
|
|
144
147
|
async function resolveRulesAndContext(config) {
|
|
148
|
+
const loadOptions = {
|
|
149
|
+
cwd: process.cwd(),
|
|
150
|
+
offline: config.offline,
|
|
151
|
+
fetchAuth: config.fetchAuth,
|
|
152
|
+
env: process.env,
|
|
153
|
+
};
|
|
145
154
|
const [rulesResult, contextResult] = await Promise.all([
|
|
146
|
-
loadAdditionalContext(config.rules),
|
|
147
|
-
loadAdditionalContext(config.context),
|
|
155
|
+
loadAdditionalContext(config.rules, loadOptions),
|
|
156
|
+
loadAdditionalContext(config.context, loadOptions),
|
|
148
157
|
]);
|
|
149
158
|
return {
|
|
150
159
|
rules: rulesResult.resolved,
|
|
@@ -172,7 +181,11 @@ triageCmd.action(async (options) => {
|
|
|
172
181
|
console.log(formatBanner(config, version));
|
|
173
182
|
}
|
|
174
183
|
// ── Build skill map + MCP servers + Claude client ──────────
|
|
175
|
-
const
|
|
184
|
+
const triageSkillDiscovery = configuredSkillsWithDiagnostics(process.env, process.cwd());
|
|
185
|
+
for (const w of triageSkillDiscovery.warnings) {
|
|
186
|
+
console.error(chalk.yellow(` ⚠ ${w.message}`));
|
|
187
|
+
}
|
|
188
|
+
const skills = createSkillMap(triageSkillDiscovery.skills);
|
|
176
189
|
const mcpAutoConfig = buildMcpAutoConfig(config);
|
|
177
190
|
const mcpServers = buildAutoMcpServers(mcpAutoConfig);
|
|
178
191
|
const claude = new ClaudeClient({
|
|
@@ -423,7 +436,11 @@ implementCmd.action(async (issueId, options) => {
|
|
|
423
436
|
fileConfig["output-dir"] ||
|
|
424
437
|
".sweny/output",
|
|
425
438
|
};
|
|
426
|
-
const
|
|
439
|
+
const implementSkillDiscovery = configuredSkillsWithDiagnostics(process.env, process.cwd());
|
|
440
|
+
for (const w of implementSkillDiscovery.warnings) {
|
|
441
|
+
console.error(chalk.yellow(` ⚠ ${w.message}`));
|
|
442
|
+
}
|
|
443
|
+
const skills = createSkillMap(implementSkillDiscovery.skills);
|
|
427
444
|
const mcpAutoConfig = buildMcpAutoConfig(config);
|
|
428
445
|
const mcpServers = buildAutoMcpServers(mcpAutoConfig);
|
|
429
446
|
const claude = new ClaudeClient({
|
|
@@ -532,31 +549,36 @@ function promptUser(question) {
|
|
|
532
549
|
});
|
|
533
550
|
}
|
|
534
551
|
const workflowCmd = program.command("workflow").description("Manage and run workflow files");
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
return ext === ".yaml" || ext === ".yml" ? parseYaml(content) : JSON.parse(content);
|
|
540
|
-
}
|
|
541
|
-
export function loadWorkflowFile(filePath) {
|
|
542
|
-
const raw = parseWorkflowFileContent(filePath);
|
|
543
|
-
const errors = validateWorkflowSchema(raw);
|
|
544
|
-
if (errors.length > 0) {
|
|
545
|
-
throw new Error(`Invalid workflow file:\n${errors.map((e) => ` ${e.message}`).join("\n")}`);
|
|
552
|
+
export function loadWorkflowFile(filePath, knownSkills) {
|
|
553
|
+
const result = loadAndValidateWorkflow(filePath, { knownSkills });
|
|
554
|
+
if (!result.ok) {
|
|
555
|
+
throw new Error(`Invalid workflow file:\n${result.errors.map((e) => ` ${e.message}`).join("\n")}`);
|
|
546
556
|
}
|
|
547
|
-
return
|
|
557
|
+
return result.workflow;
|
|
548
558
|
}
|
|
549
559
|
export async function workflowRunAction(file, options) {
|
|
560
|
+
// Discover skills first so the loader can flag UNKNOWN_SKILL at parse
|
|
561
|
+
// time. validateWorkflowSkills below still runs for richer category /
|
|
562
|
+
// env-var diagnostics, but the loader's structural check fires earlier
|
|
563
|
+
// and gives users a clear "you typed `gtihub`" pointer before any other
|
|
564
|
+
// validation noise.
|
|
565
|
+
const earlySkillDiscovery = configuredSkillsWithDiagnostics(process.env, process.cwd());
|
|
566
|
+
for (const w of earlySkillDiscovery.warnings) {
|
|
567
|
+
console.error(chalk.yellow(` ⚠ ${w.message}`));
|
|
568
|
+
}
|
|
569
|
+
const knownSkillIds = new Set(earlySkillDiscovery.skills.map((s) => s.id));
|
|
550
570
|
let workflow;
|
|
551
571
|
try {
|
|
552
|
-
workflow = loadWorkflowFile(file);
|
|
572
|
+
workflow = loadWorkflowFile(file, knownSkillIds);
|
|
553
573
|
}
|
|
554
574
|
catch (err) {
|
|
555
575
|
console.error(chalk.red(` Error loading workflow file: ${err instanceof Error ? err.message : String(err)}`));
|
|
556
576
|
process.exit(1);
|
|
557
577
|
return;
|
|
558
578
|
}
|
|
559
|
-
|
|
579
|
+
// --list-nodes: lightweight static inspection, no execution. Used to be
|
|
580
|
+
// the behavior of --dry-run before Fix #6.
|
|
581
|
+
if (options.listNodes) {
|
|
560
582
|
console.log(chalk.green(` Workflow "${workflow.name}" is valid (${Object.keys(workflow.nodes).length} nodes)`));
|
|
561
583
|
for (const [id, node] of Object.entries(workflow.nodes)) {
|
|
562
584
|
console.log(chalk.dim(` ${id}: ${node.name}${node.skills.length ? ` skills=[${node.skills.join(",")}]` : ""}`));
|
|
@@ -568,11 +590,13 @@ export async function workflowRunAction(file, options) {
|
|
|
568
590
|
const config = parseCliInputs(options, fileConfig);
|
|
569
591
|
const isJson = Boolean(options.json);
|
|
570
592
|
const isTTY = !isJson && (process.stderr.isTTY ?? false);
|
|
571
|
-
|
|
593
|
+
// Reuse the discovery pulled before workflow load (above). Diagnostics
|
|
594
|
+
// already surfaced; just turn the skill list into a map for the executor.
|
|
595
|
+
const skills = createSkillMap(earlySkillDiscovery.skills);
|
|
572
596
|
// Hard-fail at startup if the workflow references skills that aren't available.
|
|
573
597
|
// Each node lists alternatives by category (e.g. [sentry, datadog, betterstack]);
|
|
574
598
|
// we require at least one configured skill per category.
|
|
575
|
-
const validation = validateWorkflowSkills(workflow, skills);
|
|
599
|
+
const validation = validateWorkflowSkills(workflow, skills, workflow.skills);
|
|
576
600
|
if (validation.errors.length > 0) {
|
|
577
601
|
console.error(chalk.red(`\n Workflow cannot run — missing required skills:\n`));
|
|
578
602
|
for (const err of validation.errors)
|
|
@@ -743,47 +767,21 @@ export function workflowExportAction(name) {
|
|
|
743
767
|
process.stdout.write(stringifyYaml(workflow, { indent: 2, lineWidth: 120 }));
|
|
744
768
|
}
|
|
745
769
|
export function workflowValidateAction(file, options) {
|
|
746
|
-
|
|
747
|
-
try {
|
|
748
|
-
raw = parseWorkflowFileContent(file);
|
|
749
|
-
}
|
|
750
|
-
catch (err) {
|
|
751
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
752
|
-
if (options.json) {
|
|
753
|
-
process.stderr.write(JSON.stringify({ valid: false, errors: [{ message }] }, null, 2) + "\n");
|
|
754
|
-
}
|
|
755
|
-
else {
|
|
756
|
-
console.error(chalk.red(` Cannot read "${file}": ${message}`));
|
|
757
|
-
}
|
|
758
|
-
process.exit(1);
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
762
|
-
const kind = raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw;
|
|
763
|
-
const message = `Expected a YAML/JSON object, got ${kind}`;
|
|
764
|
-
if (options.json) {
|
|
765
|
-
process.stderr.write(JSON.stringify({ valid: false, errors: [{ message }] }, null, 2) + "\n");
|
|
766
|
-
}
|
|
767
|
-
else {
|
|
768
|
-
console.error(chalk.red(` \u2717 ${file}: ${message}`));
|
|
769
|
-
}
|
|
770
|
-
process.exit(1);
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
const errors = validateWorkflowSchema(raw);
|
|
770
|
+
const result = loadAndValidateWorkflow(file);
|
|
774
771
|
if (options.json) {
|
|
775
|
-
|
|
772
|
+
const errs = result.ok ? [] : result.errors;
|
|
773
|
+
process.stdout.write(JSON.stringify({ valid: result.ok, errors: errs }, null, 2) + "\n");
|
|
776
774
|
}
|
|
777
|
-
else if (
|
|
775
|
+
else if (result.ok) {
|
|
778
776
|
console.log(chalk.green(` \u2713 ${file} is valid`));
|
|
779
777
|
}
|
|
780
778
|
else {
|
|
781
|
-
console.error(chalk.red(` \u2717 ${file} has ${errors.length} validation error${errors.length > 1 ? "s" : ""}:`));
|
|
782
|
-
for (const err of errors) {
|
|
779
|
+
console.error(chalk.red(` \u2717 ${file} has ${result.errors.length} validation error${result.errors.length > 1 ? "s" : ""}:`));
|
|
780
|
+
for (const err of result.errors) {
|
|
783
781
|
console.error(chalk.dim(` ${err.message}`));
|
|
784
782
|
}
|
|
785
783
|
}
|
|
786
|
-
process.exit(
|
|
784
|
+
process.exit(result.ok ? 0 : 1);
|
|
787
785
|
}
|
|
788
786
|
workflowCmd
|
|
789
787
|
.command("validate <file>")
|
|
@@ -793,7 +791,8 @@ workflowCmd
|
|
|
793
791
|
workflowCmd
|
|
794
792
|
.command("run <file>")
|
|
795
793
|
.description("Run a workflow from a YAML or JSON file")
|
|
796
|
-
.option("--dry-run", "
|
|
794
|
+
.option("--dry-run", "Execute until the first conditional routing decision, then stop (no side effects past that point). NOTE: behavior changed in this release — previously --dry-run only printed the node list. For that behavior use --list-nodes.")
|
|
795
|
+
.option("--list-nodes", "Validate, print nodes/skills, and exit without running. (Replaces the pre-Fix-#6 --dry-run behavior.)")
|
|
797
796
|
.option("--json", "Output result as JSON on stdout; suppress progress output")
|
|
798
797
|
.option("--stream", "Stream NDJSON events to stdout (for Studio / automation)")
|
|
799
798
|
.option("--mermaid", "Output a Mermaid diagram with execution state after run")
|