@sweny-ai/core 0.1.133 → 0.1.135

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.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Bridges between the legacy `Claude` interface and `AgentHarness`.
3
+ * No agent SDK imports: the executor uses this.
4
+ */
5
+ import type { Claude } from "../types.js";
6
+ import type { AgentHarness } from "./types.js";
7
+ /**
8
+ * Wrap a legacy `Claude` object (`ExecuteOptions.claude`) as an `AgentHarness`.
9
+ * The legacy object's own `ask` / `evaluate` keep winning, so a custom or mock
10
+ * `Claude` behaves exactly as it did before the seam.
11
+ */
12
+ export declare function claudeCompat(claude: Claude): AgentHarness;
13
+ /**
14
+ * View a harness as the `Claude` shape the executor, retry and judge code
15
+ * consume: `run` goes to the harness, `ask` / `evaluate` are core prompts over
16
+ * `complete()`. A harness made by {@link claudeCompat} unwraps to the original.
17
+ */
18
+ export declare function asClaude(harness: AgentHarness): Claude;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Bridges between the legacy `Claude` interface and `AgentHarness`.
3
+ * No agent SDK imports: the executor uses this.
4
+ */
5
+ import { CLAUDE_CODE_CAPABILITIES } from "./capabilities.js";
6
+ import { ask, evaluate, legacyClaudeOf, registerLegacyClaude } from "./prompts.js";
7
+ function isHarness(x) {
8
+ const h = x;
9
+ return !!h && typeof h.complete === "function" && typeof h.preflight === "function" && !!h.capabilities;
10
+ }
11
+ /**
12
+ * Wrap a legacy `Claude` object (`ExecuteOptions.claude`) as an `AgentHarness`.
13
+ * The legacy object's own `ask` / `evaluate` keep winning, so a custom or mock
14
+ * `Claude` behaves exactly as it did before the seam.
15
+ */
16
+ export function claudeCompat(claude) {
17
+ if (isHarness(claude))
18
+ return claude;
19
+ const harness = {
20
+ id: "claude-code",
21
+ capabilities: CLAUDE_CODE_CAPABILITIES,
22
+ defaultJudgeModel: claude.defaultJudgeModel,
23
+ async preflight() {
24
+ return { ok: true, version: "legacy-compat" };
25
+ },
26
+ async run(req) {
27
+ const result = await claude.run(req);
28
+ return { ...result, harness: { id: "claude-code", version: "legacy-compat" }, degraded: [] };
29
+ },
30
+ async complete(req) {
31
+ // A legacy ask() with an empty context sends the instruction unchanged.
32
+ return claude.ask({
33
+ instruction: req.prompt,
34
+ context: {},
35
+ model: req.model,
36
+ timeoutMs: req.timeoutMs,
37
+ signal: req.signal,
38
+ });
39
+ },
40
+ };
41
+ registerLegacyClaude(harness, claude);
42
+ return harness;
43
+ }
44
+ /**
45
+ * View a harness as the `Claude` shape the executor, retry and judge code
46
+ * consume: `run` goes to the harness, `ask` / `evaluate` are core prompts over
47
+ * `complete()`. A harness made by {@link claudeCompat} unwraps to the original.
48
+ */
49
+ export function asClaude(harness) {
50
+ const legacy = legacyClaudeOf(harness);
51
+ if (legacy)
52
+ return legacy;
53
+ // A harness that carries its own ask/evaluate (ClaudeCodeHarness delegates to
54
+ // core; MockHarness scripts them) keeps them. Others get the core prompts.
55
+ const own = harness;
56
+ return {
57
+ defaultJudgeModel: harness.defaultJudgeModel,
58
+ run: (req) => harness.run(req),
59
+ evaluate: (opts) => (own.evaluate ? own.evaluate(opts) : evaluate(harness, opts)),
60
+ ask: (opts) => (own.ask ? own.ask(opts) : ask(harness, opts)),
61
+ };
62
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Harness seam entry point: the factory, the legacy-`Claude` bridge, and the
3
+ * public harness types. Every construction site goes through `createHarness`.
4
+ */
5
+ import { ClaudeCodeHarness, type ClaudeCodeHarnessOptions } from "./claude-code.js";
6
+ export { claudeCompat, asClaude } from "./compat.js";
7
+ export type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessId, HarnessInfo, HarnessRunRequest, HarnessRunResult, NodePolicy, PolicyGateResult, PolicyWrappers, ToolClass, } from "./types.js";
8
+ export { policyGate } from "./policy.js";
9
+ export { ask, evaluate, buildAskPrompt, buildEvaluatePrompt } from "./prompts.js";
10
+ export { ClaudeCodeHarness, CLAUDE_CODE_CAPABILITIES } from "./claude-code.js";
11
+ export type { ClaudeCodeHarnessOptions } from "./claude-code.js";
12
+ /**
13
+ * Build the harness for an agent id. `claude` is the historical `--agent` /
14
+ * `coding-agent-provider` value and means the same as `claude-code`.
15
+ * Unknown ids fail with the same error `sweny` has always given.
16
+ */
17
+ export declare function createHarness(id: string, opts?: ClaudeCodeHarnessOptions): ClaudeCodeHarness;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Harness seam entry point: the factory, the legacy-`Claude` bridge, and the
3
+ * public harness types. Every construction site goes through `createHarness`.
4
+ */
5
+ import { ClaudeCodeHarness } from "./claude-code.js";
6
+ export { claudeCompat, asClaude } from "./compat.js";
7
+ export { policyGate } from "./policy.js";
8
+ export { ask, evaluate, buildAskPrompt, buildEvaluatePrompt } from "./prompts.js";
9
+ export { ClaudeCodeHarness, CLAUDE_CODE_CAPABILITIES } from "./claude-code.js";
10
+ /**
11
+ * Build the harness for an agent id. `claude` is the historical `--agent` /
12
+ * `coding-agent-provider` value and means the same as `claude-code`.
13
+ * Unknown ids fail with the same error `sweny` has always given.
14
+ */
15
+ export function createHarness(id, opts = {}) {
16
+ switch (id) {
17
+ case "claude":
18
+ case "claude-code":
19
+ return new ClaudeCodeHarness(opts);
20
+ default:
21
+ throw new Error(`Unsupported coding agent "${id}": the only supported agent is "claude" ` +
22
+ `(headless Claude Code). Remove --agent / coding-agent-provider or set it to claude.`);
23
+ }
24
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * policyGate: the one place that decides "degrade" versus "refuse".
3
+ *
4
+ * Pure. Called before every harness run. For Claude Code every opinion is
5
+ * enforced natively, so the result is always `{ degraded: [] }`.
6
+ */
7
+ import type { HarnessCapabilities, NodePolicy, PolicyGateResult, PolicyWrappers } from "./types.js";
8
+ export declare function policyGate(caps: HarnessCapabilities, policy: NodePolicy, wrappers?: PolicyWrappers): PolicyGateResult;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * policyGate: the one place that decides "degrade" versus "refuse".
3
+ *
4
+ * Pure. Called before every harness run. For Claude Code every opinion is
5
+ * enforced natively, so the result is always `{ degraded: [] }`.
6
+ */
7
+ export function policyGate(caps, policy, wrappers = {}) {
8
+ const degraded = [];
9
+ if (policy.readOnly && caps.readOnly === "none" && !wrappers.readOnlyMount) {
10
+ degraded.push("read-only: harness cannot enforce it and no read-only mount is active");
11
+ }
12
+ if (policy.deny.length > 0) {
13
+ if (caps.builtinDeny === "none") {
14
+ degraded.push(`deny [${policy.deny.join(", ")}]: harness cannot deny built-in tools`);
15
+ }
16
+ else if (caps.builtinDeny === "shell-only") {
17
+ const unmappable = policy.deny.filter((c) => c !== "shell");
18
+ if (unmappable.length > 0) {
19
+ degraded.push(`deny [${unmappable.join(", ")}]: harness can only deny the shell tool`);
20
+ }
21
+ }
22
+ }
23
+ if ((policy.nativeDeny?.length ?? 0) > 0 && caps.builtinDeny === "none") {
24
+ degraded.push("disallowed_tools: harness cannot deny built-in tools");
25
+ }
26
+ if (policy.egress.length > 0 && !caps.sandbox.network && !wrappers.egress) {
27
+ degraded.push("egress allowlist: harness has no network sandbox and no egress wrapper is active");
28
+ }
29
+ if (degraded.length > 0 && policy.strict) {
30
+ return { degraded, refuse: `strict policy: ${degraded.join("; ")}` };
31
+ }
32
+ return { degraded };
33
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Harness-agnostic prompts: `ask()` and `evaluate()` build the prompt in core
3
+ * and run it over `harness.complete()`. The strings are byte-identical to what
4
+ * `ClaudeClient.ask` / `ClaudeClient.evaluate` built before the harness seam
5
+ * (#330); prompts.test.ts pins them against the old builders.
6
+ */
7
+ import type { Claude, Logger } from "../types.js";
8
+ import type { AgentHarness } from "./types.js";
9
+ /** @internal Registers the legacy object behind a compat harness. */
10
+ export declare function registerLegacyClaude(harness: AgentHarness, claude: Claude): void;
11
+ /** @internal The legacy `Claude` behind a compat harness, if any. */
12
+ export declare function legacyClaudeOf(harness: AgentHarness): Claude | undefined;
13
+ /** The `ask` prompt: the instruction, then the fenced context when there is any. */
14
+ export declare function buildAskPrompt(instruction: string, context: Record<string, unknown>): string;
15
+ /**
16
+ * Build the prompt used by `evaluate()` to pick a routing edge.
17
+ *
18
+ * Extracted as a pure function so the prompt body can be unit tested without
19
+ * spinning up an SDK query. The body is part of the routing contract: workflow
20
+ * authors and downstream maintainers rely on the model leaning on structured
21
+ * fields rather than prose narrative when picking an edge, especially in the
22
+ * fallback case where a source node did not declare an `output` schema (the
23
+ * executor's schema-strict filter only kicks in when a schema is present;
24
+ * see `buildRouteEvalEntry` in executor.ts).
25
+ *
26
+ * Any change to this prompt should keep:
27
+ * - The three "Evaluation rules" pointing the model at structured fields
28
+ * and away from prose narrative.
29
+ * - A terminal directive to return ONLY the choice ID.
30
+ */
31
+ export declare function buildEvaluatePrompt(question: string, context: Record<string, unknown>, choices: {
32
+ id: string;
33
+ description: string;
34
+ }[]): string;
35
+ /**
36
+ * Single-completion free-text query (retry reflection, judge scoring).
37
+ * A failed completion is the empty string, same as before the seam.
38
+ */
39
+ export declare function ask(harness: AgentHarness, opts: {
40
+ instruction: string;
41
+ context: Record<string, unknown>;
42
+ model?: string;
43
+ timeoutMs?: number;
44
+ signal?: AbortSignal;
45
+ }): Promise<string>;
46
+ /**
47
+ * Route evaluation: pick one of N choices. Fails closed: null when the
48
+ * completion failed or the answer names no valid choice.
49
+ */
50
+ export declare function evaluate(harness: AgentHarness, opts: {
51
+ question: string;
52
+ context: Record<string, unknown>;
53
+ choices: {
54
+ id: string;
55
+ description: string;
56
+ }[];
57
+ timeoutMs?: number;
58
+ signal?: AbortSignal;
59
+ }, logger?: Logger): Promise<string | null>;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Harness-agnostic prompts: `ask()` and `evaluate()` build the prompt in core
3
+ * and run it over `harness.complete()`. The strings are byte-identical to what
4
+ * `ClaudeClient.ask` / `ClaudeClient.evaluate` built before the harness seam
5
+ * (#330); prompts.test.ts pins them against the old builders.
6
+ */
7
+ import { consoleLogger } from "../types.js";
8
+ import { fenceUntrustedJson } from "../untrusted.js";
9
+ /** Legacy `Claude` objects wrapped by `claudeCompat()`, so their own ask/evaluate still win. */
10
+ const LEGACY = new WeakMap();
11
+ /** @internal Registers the legacy object behind a compat harness. */
12
+ export function registerLegacyClaude(harness, claude) {
13
+ LEGACY.set(harness, claude);
14
+ }
15
+ /** @internal The legacy `Claude` behind a compat harness, if any. */
16
+ export function legacyClaudeOf(harness) {
17
+ return LEGACY.get(harness);
18
+ }
19
+ /** The `ask` prompt: the instruction, then the fenced context when there is any. */
20
+ export function buildAskPrompt(instruction, context) {
21
+ return [instruction, Object.keys(context).length > 0 ? `\nContext:\n${fenceUntrustedJson(context, "context")}` : ""]
22
+ .filter(Boolean)
23
+ .join("\n");
24
+ }
25
+ /**
26
+ * Build the prompt used by `evaluate()` to pick a routing edge.
27
+ *
28
+ * Extracted as a pure function so the prompt body can be unit tested without
29
+ * spinning up an SDK query. The body is part of the routing contract: workflow
30
+ * authors and downstream maintainers rely on the model leaning on structured
31
+ * fields rather than prose narrative when picking an edge, especially in the
32
+ * fallback case where a source node did not declare an `output` schema (the
33
+ * executor's schema-strict filter only kicks in when a schema is present;
34
+ * see `buildRouteEvalEntry` in executor.ts).
35
+ *
36
+ * Any change to this prompt should keep:
37
+ * - The three "Evaluation rules" pointing the model at structured fields
38
+ * and away from prose narrative.
39
+ * - A terminal directive to return ONLY the choice ID.
40
+ */
41
+ export function buildEvaluatePrompt(question, context, choices) {
42
+ const choiceList = choices.map((c) => `- "${c.id}": ${c.description}`).join("\n");
43
+ return [
44
+ question,
45
+ `\nContext:\n${fenceUntrustedJson(context, "context")}`,
46
+ `\nChoices:\n${choiceList}`,
47
+ `\nEvaluation rules:`,
48
+ `1. Read each choice's condition literally and match against the structured fields in the context (e.g. status, counts, enum values, boolean flags).`,
49
+ `2. Ignore prose narrative fields ("summary", free-form rationale, conversational commentary). They are not the contract.`,
50
+ `3. When a field's value contradicts what a prose field claims, trust the field's value.`,
51
+ `4. A field whose value is explicitly null means the source node DECLARED that field but did NOT emit a value. Treat null as "unknown" and do NOT match it against any specific value (do not match "is 0", "is N", "is true", "is false", or "is undefined" against a null field). Prefer a default/fallback edge when the field needed for a decision is null.`,
52
+ `\nRespond with ONLY the choice ID, nothing else.`,
53
+ ].join("\n");
54
+ }
55
+ /**
56
+ * Single-completion free-text query (retry reflection, judge scoring).
57
+ * A failed completion is the empty string, same as before the seam.
58
+ */
59
+ export async function ask(harness, opts) {
60
+ const legacy = legacyClaudeOf(harness);
61
+ if (legacy)
62
+ return legacy.ask(opts);
63
+ const { instruction, context, model, timeoutMs, signal } = opts;
64
+ const response = await harness.complete({
65
+ prompt: buildAskPrompt(instruction, context),
66
+ model,
67
+ timeoutMs,
68
+ signal,
69
+ purpose: "ask",
70
+ });
71
+ return (response ?? "").trim();
72
+ }
73
+ /**
74
+ * Route evaluation: pick one of N choices. Fails closed: null when the
75
+ * completion failed or the answer names no valid choice.
76
+ */
77
+ export async function evaluate(harness, opts, logger) {
78
+ const legacy = legacyClaudeOf(harness);
79
+ if (legacy)
80
+ return legacy.evaluate(opts);
81
+ const log = logger ?? harness.logger ?? consoleLogger;
82
+ const { question, context, choices, timeoutMs, signal } = opts;
83
+ const response = await harness.complete({
84
+ prompt: buildEvaluatePrompt(question, context, choices),
85
+ timeoutMs,
86
+ signal,
87
+ purpose: "evaluate",
88
+ });
89
+ // The adapter already logged why; fail closed with no decision.
90
+ if (response === null)
91
+ return null;
92
+ const text = response.trim().replace(/^["']|["']$/g, "");
93
+ const validIds = choices.map((c) => c.id);
94
+ // Exact match
95
+ if (validIds.includes(text))
96
+ return text;
97
+ // Fuzzy: look for an ID embedded in the response
98
+ const match = validIds.find((id) => text.includes(id));
99
+ if (match)
100
+ return match;
101
+ // Fail closed. An unparseable answer is not a decision. Returning
102
+ // validIds[0] here is the fail-open bug: on a node with a single
103
+ // conditional out-edge, the "first choice" is always that edge, so a
104
+ // garbled model answer would always take it. Signal no-decision instead.
105
+ log.warn(`Could not parse route choice from: "${text.slice(0, 100)}". Failing closed (no route decision).`);
106
+ return null;
107
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * AgentHarness: the seam between the executor and whatever agent runs a node.
3
+ *
4
+ * Design: research/2026-09-30-sota/harness-design.md, section 1. Claude Code
5
+ * (`ClaudeCodeHarness`) is the only adapter today. `evaluate` and `ask` are
6
+ * not adapter methods: core builds the prompt and calls `complete()` (see
7
+ * prompts.ts), so every adapter gets routing and judging for free.
8
+ *
9
+ * Browser-safe: types only, no runtime imports.
10
+ */
11
+ import type { Claude, Logger, NodeResult } from "../types.js";
12
+ export type HarnessId = "claude-code" | "mock" | "codex" | "pi" | "hermes" | "gemini" | `acp:${string}`;
13
+ /** Portable tool vocabulary for node policy. */
14
+ export type ToolClass = "shell" | "write" | "edit" | "net" | "subagent";
15
+ export interface HarnessCapabilities {
16
+ /** native = the harness validates a JSON schema; prompt = sweny prompts, parses, validates. */
17
+ structuredOutput: "native" | "prompt";
18
+ /** full = built-in tool calls are visible with status; skill-only = only skill tools are traced. */
19
+ toolTrace: "full" | "skill-only";
20
+ builtinDeny: "by-name" | "by-class" | "shell-only" | "none";
21
+ mcp: {
22
+ inject: boolean;
23
+ exclusive: "native" | "home-isolation" | "none";
24
+ };
25
+ /** Native sandbox, before any sweny wrapper. */
26
+ sandbox: {
27
+ fs: boolean;
28
+ network: boolean;
29
+ };
30
+ readOnly: "native" | "tool-allowlist" | "none";
31
+ turnLimit: "native" | "watchdog" | "none";
32
+ usage: {
33
+ tokens: boolean;
34
+ costUsd: boolean;
35
+ live: boolean;
36
+ };
37
+ cancel: "signal" | "rpc" | "kill";
38
+ resume: boolean;
39
+ }
40
+ /** What a node is allowed to do, compiled per adapter by the harness. */
41
+ export interface NodePolicy {
42
+ /** Dry run: the node must not change anything. */
43
+ readOnly: boolean;
44
+ /** Portable deny list. */
45
+ deny: ToolClass[];
46
+ /** Legacy `disallowed_tools` passthrough, harness-native names. */
47
+ nativeDeny?: string[];
48
+ /** Allowed egress hosts. */
49
+ egress: string[];
50
+ /** Refuse instead of degrade when an opinion cannot be enforced. */
51
+ strict: boolean;
52
+ }
53
+ /** Process-level wrappers the host provides (OS sandbox, egress proxy, read-only mount). */
54
+ export interface PolicyWrappers {
55
+ sandbox?: boolean;
56
+ egress?: boolean;
57
+ readOnlyMount?: boolean;
58
+ }
59
+ export interface PolicyGateResult {
60
+ /** Every opinion this run could not honor natively. Empty for Claude Code. */
61
+ degraded: string[];
62
+ /** Set only in strict mode when at least one opinion cannot be honored. */
63
+ refuse?: string;
64
+ }
65
+ /** Which harness produced a result. Recorded on `NodeResult` and in run history. */
66
+ export interface HarnessInfo {
67
+ id: HarnessId;
68
+ version: string;
69
+ }
70
+ /** `Claude.run` request shape, kept verbatim so every existing caller still type-checks. */
71
+ export type ClaudeRunRequest = Parameters<Claude["run"]>[0];
72
+ export interface HarnessRunRequest extends ClaudeRunRequest {
73
+ /**
74
+ * Node policy. Optional: when absent the adapter derives it from the legacy
75
+ * `readOnly` / `disallowedTools` / `agentAccess` fields.
76
+ */
77
+ policy?: NodePolicy;
78
+ }
79
+ export interface HarnessRunResult extends NodeResult {
80
+ harness: HarnessInfo;
81
+ degraded: string[];
82
+ }
83
+ export interface HarnessCompleteRequest {
84
+ prompt: string;
85
+ model?: string;
86
+ timeoutMs?: number;
87
+ signal?: AbortSignal;
88
+ /**
89
+ * Which core call this is for. Adapters use it only to word their failure
90
+ * logs ("claude.ask: ..." vs "claude.evaluate: ..."); it never changes the query.
91
+ */
92
+ purpose?: "ask" | "evaluate";
93
+ }
94
+ export interface AgentHarness {
95
+ readonly id: HarnessId;
96
+ readonly capabilities: HarnessCapabilities;
97
+ /** Default model for judge evaluators when nothing else names one. */
98
+ readonly defaultJudgeModel?: string;
99
+ /** Logger the adapter reports to; core reuses it for routing parse warnings. */
100
+ readonly logger?: Logger;
101
+ preflight(): Promise<{
102
+ ok: true;
103
+ version: string;
104
+ } | {
105
+ ok: false;
106
+ reason: string;
107
+ }>;
108
+ run(req: HarnessRunRequest): Promise<HarnessRunResult>;
109
+ /** One completion, no tools, no MCP. null = failed (callers fail closed). */
110
+ complete(req: HarnessCompleteRequest): Promise<string | null>;
111
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * AgentHarness: the seam between the executor and whatever agent runs a node.
3
+ *
4
+ * Design: research/2026-09-30-sota/harness-design.md, section 1. Claude Code
5
+ * (`ClaudeCodeHarness`) is the only adapter today. `evaluate` and `ask` are
6
+ * not adapter methods: core builds the prompt and calls `complete()` (see
7
+ * prompts.ts), so every adapter gets routing and judging for free.
8
+ *
9
+ * Browser-safe: types only, no runtime imports.
10
+ */
11
+ export {};
package/dist/index.d.ts CHANGED
@@ -8,15 +8,15 @@
8
8
  *
9
9
  * @example
10
10
  * ```ts
11
- * import { execute, ClaudeClient, createSkillMap, github, sentry, slack } from '@sweny-ai/core'
11
+ * import { execute, createHarness, createSkillMap, github, sentry, slack } from '@sweny-ai/core'
12
12
  * import { triageWorkflow } from '@sweny-ai/core/workflows'
13
13
  *
14
14
  * const skills = createSkillMap([github, sentry, slack])
15
- * const claude = new ClaudeClient()
15
+ * const harness = createHarness("claude-code")
16
16
  *
17
17
  * const results = await execute(triageWorkflow, alertPayload, {
18
18
  * skills,
19
- * claude,
19
+ * harness,
20
20
  * observer: (event) => console.log(event),
21
21
  * })
22
22
  * ```
@@ -27,8 +27,10 @@ export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRA
27
27
  export { consoleLogger } from "./types.js";
28
28
  export { execute, RouteEvaluationError } from "./executor.js";
29
29
  export type { ExecuteOptions } from "./executor.js";
30
- export { ClaudeClient, resolveAuthEnv } from "./claude.js";
31
- export type { ClaudeClientOptions, SwenyAuthMode, ResolveAuthEnvOpts } from "./claude.js";
30
+ export { createHarness, claudeCompat, asClaude, policyGate, ClaudeCodeHarness } from "./harness/index.js";
31
+ export type { AgentHarness, HarnessCapabilities, HarnessId, HarnessInfo, HarnessRunRequest, HarnessRunResult, NodePolicy, PolicyGateResult, PolicyWrappers, ToolClass, ClaudeCodeHarnessOptions, } from "./harness/index.js";
32
+ export { ClaudeClient, resolveAuthEnv } from "./harness/claude-code.js";
33
+ export type { ClaudeClientOptions, SwenyAuthMode, ResolveAuthEnvOpts } from "./harness/claude-code.js";
32
34
  export { resolveExecutionModel } from "./model.js";
33
35
  export { github, linear, slack, sentry, datadog, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
34
36
  export type { SkillValidationResult } from "./skills/index.js";
package/dist/index.js CHANGED
@@ -8,15 +8,15 @@
8
8
  *
9
9
  * @example
10
10
  * ```ts
11
- * import { execute, ClaudeClient, createSkillMap, github, sentry, slack } from '@sweny-ai/core'
11
+ * import { execute, createHarness, createSkillMap, github, sentry, slack } from '@sweny-ai/core'
12
12
  * import { triageWorkflow } from '@sweny-ai/core/workflows'
13
13
  *
14
14
  * const skills = createSkillMap([github, sentry, slack])
15
- * const claude = new ClaudeClient()
15
+ * const harness = createHarness("claude-code")
16
16
  *
17
17
  * const results = await execute(triageWorkflow, alertPayload, {
18
18
  * skills,
19
- * claude,
19
+ * harness,
20
20
  * observer: (event) => console.log(event),
21
21
  * })
22
22
  * ```
@@ -27,8 +27,10 @@ export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRA
27
27
  export { consoleLogger } from "./types.js";
28
28
  // Executor
29
29
  export { execute, RouteEvaluationError } from "./executor.js";
30
- // Claude client
31
- export { ClaudeClient, resolveAuthEnv } from "./claude.js";
30
+ // Agent harness (the seam) and its Claude Code adapter
31
+ export { createHarness, claudeCompat, asClaude, policyGate, ClaudeCodeHarness } from "./harness/index.js";
32
+ // Claude client (back-compat: ClaudeClient is ClaudeCodeHarness, @deprecated)
33
+ export { ClaudeClient, resolveAuthEnv } from "./harness/claude-code.js";
32
34
  // Execution model resolution
33
35
  export { resolveExecutionModel } from "./model.js";
34
36
  // Skills
package/dist/testing.d.ts CHANGED
@@ -19,7 +19,8 @@
19
19
  * })
20
20
  * ```
21
21
  */
22
- import type { Claude, NodeResult, Tool, JSONSchema, Workflow } from "./types.js";
22
+ import type { Claude, Tool, JSONSchema, Workflow } from "./types.js";
23
+ import type { AgentHarness, HarnessCapabilities, HarnessRunResult } from "./harness/types.js";
23
24
  export interface MockNodeResponse {
24
25
  /** Tool calls to execute (optional — handlers will be called) */
25
26
  toolCalls?: {
@@ -31,7 +32,7 @@ export interface MockNodeResponse {
31
32
  /** Status (default: "success") */
32
33
  status?: "success" | "skipped" | "failed";
33
34
  }
34
- export interface MockClaudeOptions {
35
+ export interface MockHarnessOptions {
35
36
  /** Node ID → scripted response */
36
37
  responses: Record<string, MockNodeResponse>;
37
38
  /** Route decisions: "fromNode" → chosen target node ID */
@@ -49,21 +50,32 @@ export interface MockClaudeOptions {
49
50
  * routes map. With no scripted route (or an invalid one) it returns
50
51
  * `null`, mirroring the real client failing closed on a route-eval failure.
51
52
  */
52
- export declare class MockClaude implements Claude {
53
+ export declare class MockHarness implements Claude, AgentHarness {
54
+ readonly id: "mock";
55
+ readonly capabilities: HarnessCapabilities;
53
56
  private callOrder;
54
57
  private responses;
55
58
  private routes;
56
59
  private instructionMap;
57
60
  private askFn?;
58
- constructor(opts: MockClaudeOptions);
61
+ constructor(opts: MockHarnessOptions);
59
62
  /** Returns the order in which nodes were executed */
60
63
  get executedNodes(): string[];
64
+ preflight(): Promise<{
65
+ ok: true;
66
+ version: string;
67
+ }>;
68
+ /** One scripted completion: the `ask` script, or the empty string. */
69
+ complete(req: {
70
+ prompt: string;
71
+ }): Promise<string | null>;
61
72
  run(opts: {
62
73
  instruction: string;
63
74
  context: Record<string, unknown>;
64
75
  tools: Tool[];
65
76
  outputSchema?: JSONSchema;
66
- }): Promise<NodeResult>;
77
+ }): Promise<HarnessRunResult>;
78
+ private runScripted;
67
79
  evaluate(opts: {
68
80
  question: string;
69
81
  context: Record<string, unknown>;
@@ -85,6 +97,12 @@ export declare class MockClaude implements Claude {
85
97
  */
86
98
  private identifyNode;
87
99
  }
100
+ /** @deprecated Use {@link MockHarnessOptions}. */
101
+ export type MockClaudeOptions = MockHarnessOptions;
102
+ /** @deprecated Use {@link MockHarness}. */
103
+ export declare const MockClaude: typeof MockHarness;
104
+ /** @deprecated Use {@link MockHarness}. */
105
+ export type MockClaude = MockHarness;
88
106
  import type { Skill } from "./types.js";
89
107
  /**
90
108
  * Create a file-based skill for local testing.
package/dist/testing.js CHANGED
@@ -20,6 +20,19 @@
20
20
  * ```
21
21
  */
22
22
  import { consoleLogger } from "./types.js";
23
+ // ─── Mock harness ────────────────────────────────────────────────
24
+ const MOCK_CAPABILITIES = {
25
+ structuredOutput: "prompt",
26
+ toolTrace: "skill-only",
27
+ builtinDeny: "none",
28
+ mcp: { inject: false, exclusive: "none" },
29
+ sandbox: { fs: false, network: false },
30
+ readOnly: "none",
31
+ turnLimit: "none",
32
+ usage: { tokens: false, costUsd: false, live: false },
33
+ cancel: "signal",
34
+ resume: false,
35
+ };
23
36
  /**
24
37
  * A mock Claude client that follows a script.
25
38
  *
@@ -28,7 +41,9 @@ import { consoleLogger } from "./types.js";
28
41
  * routes map. With no scripted route (or an invalid one) it returns
29
42
  * `null`, mirroring the real client failing closed on a route-eval failure.
30
43
  */
31
- export class MockClaude {
44
+ export class MockHarness {
45
+ id = "mock";
46
+ capabilities = MOCK_CAPABILITIES;
32
47
  callOrder = [];
33
48
  responses;
34
49
  routes;
@@ -53,7 +68,18 @@ export class MockClaude {
53
68
  get executedNodes() {
54
69
  return [...this.callOrder];
55
70
  }
71
+ async preflight() {
72
+ return { ok: true, version: "mock" };
73
+ }
74
+ /** One scripted completion: the `ask` script, or the empty string. */
75
+ async complete(req) {
76
+ return this.ask({ instruction: req.prompt, context: {} });
77
+ }
56
78
  async run(opts) {
79
+ const result = await this.runScripted(opts);
80
+ return { ...result, harness: { id: "mock", version: "mock" }, degraded: [] };
81
+ }
82
+ async runScripted(opts) {
57
83
  // Identify which node this is by matching instruction text against responses
58
84
  // The executor wraps tools with event tracking, so we can find the node
59
85
  // by checking which response key's instruction appears
@@ -132,6 +158,8 @@ export class MockClaude {
132
158
  return unused[0] ?? `unknown-${this.callOrder.length}`;
133
159
  }
134
160
  }
161
+ /** @deprecated Use {@link MockHarness}. */
162
+ export const MockClaude = MockHarness;
135
163
  /**
136
164
  * Create a file-based skill for local testing.
137
165
  *