@sweny-ai/core 0.1.141 → 0.1.143
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/agent-env.d.ts +24 -0
- package/dist/agent-env.js +40 -7
- package/dist/cli/config-file.d.ts +1 -1
- package/dist/cli/config-file.js +1 -1
- package/dist/cli/config.d.ts +2 -0
- package/dist/cli/config.js +15 -7
- package/dist/cli/main.js +45 -15
- package/dist/cli/output.d.ts +2 -0
- package/dist/cli/output.js +11 -0
- package/dist/cli/run-history.d.ts +2 -0
- package/dist/cli/run-history.js +2 -0
- package/dist/cli/run-output.d.ts +13 -2
- package/dist/cli/run-output.js +23 -2
- package/dist/executor.js +30 -25
- package/dist/harness/abort.d.ts +17 -0
- package/dist/harness/abort.js +53 -0
- package/dist/harness/agents.d.ts +11 -0
- package/dist/harness/agents.js +15 -0
- package/dist/harness/capabilities.d.ts +23 -0
- package/dist/harness/capabilities.js +35 -0
- package/dist/harness/claude-code.d.ts +9 -40
- package/dist/harness/claude-code.js +21 -308
- package/dist/harness/codex.d.ts +175 -0
- package/dist/harness/codex.js +917 -0
- package/dist/harness/index.d.ts +11 -3
- package/dist/harness/index.js +8 -8
- package/dist/harness/parse.d.ts +49 -0
- package/dist/harness/parse.js +253 -0
- package/dist/harness/policy.d.ts +23 -1
- package/dist/harness/policy.js +73 -15
- package/dist/harness/prompts.d.ts +7 -1
- package/dist/harness/prompts.js +16 -0
- package/dist/harness/types.d.ts +13 -2
- package/dist/harness/types.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/schema.d.ts +12 -11
- package/dist/schema.js +7 -3
- package/dist/types.d.ts +8 -1
- package/package.json +1 -1
package/dist/agent-env.d.ts
CHANGED
|
@@ -42,9 +42,33 @@ export declare const AGENT_ENV_PREFIXES: readonly string[];
|
|
|
42
42
|
export declare const AGENT_AUTH_VARS: readonly string[];
|
|
43
43
|
/** Split a comma/whitespace separated list, dropping empties. */
|
|
44
44
|
export declare function parseList(v: string | undefined): string[];
|
|
45
|
+
/**
|
|
46
|
+
* Codex's own credentials and home (openai/codex rust-v0.159.2,
|
|
47
|
+
* login/src/auth/manager.rs): `codex exec` authenticates with `CODEX_API_KEY`
|
|
48
|
+
* (it does not read `OPENAI_API_KEY` for this; see {@link resolveCodexAuthEnv})
|
|
49
|
+
* or `CODEX_ACCESS_TOKEN`, else the login stored in `$CODEX_HOME/auth.json`.
|
|
50
|
+
*/
|
|
51
|
+
export declare const CODEX_AUTH_VARS: readonly string[];
|
|
52
|
+
/** Prefixes for a Codex run: locale only. `ANTHROPIC_*` / `CLAUDE_*` never reach Codex. */
|
|
53
|
+
export declare const CODEX_ENV_PREFIXES: readonly string[];
|
|
54
|
+
/**
|
|
55
|
+
* `codex exec` reads `CODEX_API_KEY`, not `OPENAI_API_KEY`. Copy the OpenAI
|
|
56
|
+
* key into `CODEX_API_KEY` when only the former is set, so either works.
|
|
57
|
+
* Pure: returns a copy.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveCodexAuthEnv(env: Record<string, string>): Record<string, string>;
|
|
60
|
+
/** A stored Codex login (`codex login`): `auth.json` under `CODEX_HOME`, default `~/.codex`. */
|
|
61
|
+
export declare function hasCodexLogin(env?: Record<string, string | undefined>, exists?: (p: string) => boolean): boolean;
|
|
45
62
|
export interface BuildAgentEnvOpts {
|
|
46
63
|
/** Per-call names to include (a node's declared skill env vars). */
|
|
47
64
|
extraVars?: readonly string[];
|
|
65
|
+
/**
|
|
66
|
+
* The harness's own credential names (Codex: {@link CODEX_AUTH_VARS}).
|
|
67
|
+
* Default: none beyond the Claude prefixes.
|
|
68
|
+
*/
|
|
69
|
+
authVars?: readonly string[];
|
|
70
|
+
/** Prefixes always kept. Default: {@link AGENT_ENV_PREFIXES} (Claude Code's). */
|
|
71
|
+
prefixes?: readonly string[];
|
|
48
72
|
/** Operator passthrough list (`env-passthrough`). `"*"` inherits everything. */
|
|
49
73
|
passthrough?: readonly string[];
|
|
50
74
|
logger?: Pick<Logger, "warn">;
|
package/dist/agent-env.js
CHANGED
|
@@ -138,6 +138,31 @@ export function parseList(v) {
|
|
|
138
138
|
.map((s) => s.trim())
|
|
139
139
|
.filter(Boolean);
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Codex's own credentials and home (openai/codex rust-v0.159.2,
|
|
143
|
+
* login/src/auth/manager.rs): `codex exec` authenticates with `CODEX_API_KEY`
|
|
144
|
+
* (it does not read `OPENAI_API_KEY` for this; see {@link resolveCodexAuthEnv})
|
|
145
|
+
* or `CODEX_ACCESS_TOKEN`, else the login stored in `$CODEX_HOME/auth.json`.
|
|
146
|
+
*/
|
|
147
|
+
export const CODEX_AUTH_VARS = ["CODEX_API_KEY", "CODEX_ACCESS_TOKEN", "CODEX_HOME"];
|
|
148
|
+
/** Prefixes for a Codex run: locale only. `ANTHROPIC_*` / `CLAUDE_*` never reach Codex. */
|
|
149
|
+
export const CODEX_ENV_PREFIXES = ["LC_"];
|
|
150
|
+
/**
|
|
151
|
+
* `codex exec` reads `CODEX_API_KEY`, not `OPENAI_API_KEY`. Copy the OpenAI
|
|
152
|
+
* key into `CODEX_API_KEY` when only the former is set, so either works.
|
|
153
|
+
* Pure: returns a copy.
|
|
154
|
+
*/
|
|
155
|
+
export function resolveCodexAuthEnv(env) {
|
|
156
|
+
const out = { ...env };
|
|
157
|
+
if (!out.CODEX_API_KEY && out.OPENAI_API_KEY)
|
|
158
|
+
out.CODEX_API_KEY = out.OPENAI_API_KEY;
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
/** A stored Codex login (`codex login`): `auth.json` under `CODEX_HOME`, default `~/.codex`. */
|
|
162
|
+
export function hasCodexLogin(env = process.env, exists = existsSync) {
|
|
163
|
+
const home = env.CODEX_HOME || path.join(homedir(), ".codex");
|
|
164
|
+
return exists(path.join(home, "auth.json"));
|
|
165
|
+
}
|
|
141
166
|
/**
|
|
142
167
|
* Whether the agent env is scoped to the allowlist. `SWENY_ENV_SCOPE` (or the
|
|
143
168
|
* explicit value) `on|true|1` / `off|false|0` wins everywhere; otherwise on
|
|
@@ -319,13 +344,21 @@ export function buildAgentEnv(source, opts = {}) {
|
|
|
319
344
|
opts.logger?.warn("[sweny] env-passthrough contains '*': the agent inherits the full environment, including every secret.");
|
|
320
345
|
return Object.fromEntries(Object.entries(source).filter((e) => e[1] != null));
|
|
321
346
|
}
|
|
322
|
-
const exact = new Set([
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
347
|
+
const exact = new Set([
|
|
348
|
+
...AGENT_ENV_ALLOWLIST,
|
|
349
|
+
...(opts.extraVars ?? []),
|
|
350
|
+
...(opts.authVars ?? []),
|
|
351
|
+
...passthrough,
|
|
352
|
+
]);
|
|
353
|
+
const prefixes = [...(opts.prefixes ?? AGENT_ENV_PREFIXES)];
|
|
354
|
+
// Bedrock / Vertex routing is a Claude Code setting; only its default prefixes carry it.
|
|
355
|
+
if (!opts.prefixes) {
|
|
356
|
+
if (truthy(source.CLAUDE_CODE_USE_BEDROCK))
|
|
357
|
+
prefixes.push(...BEDROCK_PREFIXES);
|
|
358
|
+
if (truthy(source.CLAUDE_CODE_USE_VERTEX))
|
|
359
|
+
for (const v of VERTEX_VARS)
|
|
360
|
+
exact.add(v);
|
|
361
|
+
}
|
|
329
362
|
const out = {};
|
|
330
363
|
for (const [k, v] of Object.entries(source)) {
|
|
331
364
|
if (v == null)
|
|
@@ -19,4 +19,4 @@ export type FileConfig = Record<string, string | string[] | Record<string, unkno
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function loadConfigFile(cwd?: string): FileConfig;
|
|
21
21
|
/** Starter config written by `sweny new`. */
|
|
22
|
-
export declare const STARTER_CONFIG = "# .sweny.yml \u2014 SWEny project configuration\n# Commit this file. Secrets (API keys, tokens) go in .env (gitignored).\n#\n# Every key matches a CLI flag: \"time-range: 4h\" is the same as \"--time-range 4h\".\n# CLI flags override this file; env vars override this file; this file overrides defaults.\n\n# \u2500\u2500 Providers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# observability-provider: datadog # datadog | sentry | cloudwatch | splunk | elastic | newrelic | loki | prometheus | pagerduty | heroku | opsgenie | vercel | supabase | netlify | fly | render | file\n# issue-tracker-provider: github-issues # github-issues | linear | jira\n# source-control-provider: github # github | gitlab\n# coding-agent-provider: claude # claude
|
|
22
|
+
export declare const STARTER_CONFIG = "# .sweny.yml \u2014 SWEny project configuration\n# Commit this file. Secrets (API keys, tokens) go in .env (gitignored).\n#\n# Every key matches a CLI flag: \"time-range: 4h\" is the same as \"--time-range 4h\".\n# CLI flags override this file; env vars override this file; this file overrides defaults.\n\n# \u2500\u2500 Providers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# observability-provider: datadog # datadog | sentry | cloudwatch | splunk | elastic | newrelic | loki | prometheus | pagerduty | heroku | opsgenie | vercel | supabase | netlify | fly | render | file\n# issue-tracker-provider: github-issues # github-issues | linear | jira\n# source-control-provider: github # github | gitlab\n# coding-agent-provider: claude # claude | codex\n# notification-provider: console # console | slack | teams | discord | email | webhook\n\n# \u2500\u2500 Investigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# time-range: 24h\n# severity-focus: errors\n# service-filter: \"*\"\n# investigation-depth: standard # quick | standard | thorough\n\n# \u2500\u2500 PR / branch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# base-branch: main\n# pr-labels: agent,triage,needs-review\n\n# \u2500\u2500 Paths \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# service-map-path: .github/service-map.yml\n# log-file: ./logs/errors.json # required when observability-provider is \"file\"\n\n# \u2500\u2500 Cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# cache-dir: .sweny/cache\n# cache-ttl: 86400\n\n# \u2500\u2500 Run reporting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Opt-in and not currently available: token minting is not yet exposed, so\n# leaving this unset means no reporting request is ever made.\n# cloud-token: sweny_pk_...\n# Or set SWENY_CLOUD_TOKEN in your environment.\n\n# \u2500\u2500 Agent sandbox \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# See https://docs.sweny.ai/advanced/agent-sandbox/\n# sandbox: off # off (local default) | auto (CI default: sandbox if supported, else warn) | strict\n# sandbox-allowed-domains: [internal.example.com] # extra hosts agent commands may reach\n# env-scope: off # off (local default: full env) | on (CI default: allowlist)\n# env-passthrough: [NPM_TOKEN] # extra env vars the agent may see when scoped (\"*\" = all)\n\n# \u2500\u2500 MCP servers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Extend the coding agent with additional tools via MCP.\n# Value is a JSON object \u2014 each key is a server name you choose.\n# See https://docs.sweny.ai/advanced/mcp-servers/ for a full catalog with copy-paste configs.\n#\n# Example: GitHub MCP server (query PRs, issues, CI run logs)\n# mcp-servers-json: '{\"github\":{\"type\":\"stdio\",\"command\":\"npx\",\"args\":[\"-y\",\"@modelcontextprotocol/server-github@latest\"],\"env\":{\"GITHUB_PERSONAL_ACCESS_TOKEN\":\"ghp_...\"}}}'\n\n# \u2500\u2500 Local-only quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Uncomment to run without any external services (just an LLM API key):\n# observability-provider: file\n# log-file: ./sample-errors.json\n# issue-tracker-provider: file\n# source-control-provider: file\n# notification-provider: file\n# output-dir: .sweny/output\n\n# \u2500\u2500 Credentials (.env) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Copy the relevant block into your .env file and fill in the values.\n#\n# Claude (coding agent) \u2014 https://console.anthropic.com/settings/api-keys\n# ANTHROPIC_API_KEY=sk-ant-...\n#\n# GitHub (source control + issue tracker)\n# GITHUB_TOKEN=ghp_... # https://github.com/settings/tokens (repo + issues scopes)\n#\n# Datadog (observability) \u2014 https://app.datadoghq.com/organization-settings\n# DD_API_KEY=... # Organization Settings > API Keys\n# DD_APP_KEY=... # Organization Settings > Application Keys\n# DD_SITE=datadoghq.com # or datadoghq.eu, us3.datadoghq.com, etc.\n#\n# Sentry (observability) \u2014 https://sentry.io/settings/auth-tokens/\n# SENTRY_AUTH_TOKEN=sntrys_...\n# SENTRY_ORG=your-org-slug # from sentry.io/organizations/<slug>/\n# SENTRY_PROJECT=your-project # Project Settings > General > Project Slug\n#\n# Linear (issue tracker) \u2014 https://linear.app/settings/api\n# LINEAR_API_KEY=lin_api_...\n# LINEAR_TEAM_ID=... # Settings > Workspace > Teams > [team] > copy ID from URL\n# LINEAR_BUG_LABEL_ID=... # Settings > Labels > [label] > copy ID from URL\n#\n# Jira (issue tracker) \u2014 https://your-org.atlassian.net\n# JIRA_BASE_URL=https://your-org.atlassian.net\n# JIRA_EMAIL=you@company.com # your Atlassian account email\n# JIRA_API_TOKEN=... # https://id.atlassian.com/manage-profile/security/api-tokens\n#\n# Vercel (observability) \u2014 https://vercel.com/account/tokens\n# VERCEL_TOKEN=...\n# VERCEL_PROJECT_ID=prj_... # Project Settings > General > Project ID\n# VERCEL_TEAM_ID=team_... # optional, for team-owned projects\n#\n# Supabase (observability) \u2014 https://supabase.com/dashboard/account/tokens\n# SUPABASE_MANAGEMENT_KEY=...\n# SUPABASE_PROJECT_REF=... # Project Settings > General > Reference ID\n#\n# Netlify (observability) \u2014 https://app.netlify.com/user/applications#personal-access-tokens\n# NETLIFY_TOKEN=...\n# NETLIFY_SITE_ID=... # Site Settings > General > Site ID\n#\n# Fly.io (observability) \u2014 https://fly.io/user/personal_access_tokens\n# FLY_TOKEN=...\n# FLY_APP_NAME=... # the name of your Fly.io application\n#\n# Render (observability) \u2014 https://dashboard.render.com/u/settings\n# RENDER_API_KEY=...\n# RENDER_SERVICE_ID=srv-... # from your service's Settings page\n#\n# Prometheus (observability) \u2014 self-hosted or Grafana Cloud\n# PROMETHEUS_URL=http://prometheus.internal:9090\n# PROMETHEUS_TOKEN=... # optional, for secured instances\n#\n# PagerDuty (observability) \u2014 https://your-account.pagerduty.com/api_keys\n# PAGERDUTY_API_KEY=...\n#\n# Honeycomb (observability) \u2014 https://docs.honeycomb.io/api/\n# HONEYCOMB_API_KEY=...\n# HONEYCOMB_DATASET=... # dataset name (e.g. production)\n#\n# Heroku (observability) \u2014 https://devcenter.heroku.com/articles/platform-api-reference\n# HEROKU_API_KEY=... # https://dashboard.heroku.com/account\n# HEROKU_APP_NAME=... # the name of your Heroku application\n#\n# OpsGenie (observability) \u2014 https://support.atlassian.com/opsgenie/docs/api-key-management/\n# OPSGENIE_API_KEY=...\n# OPSGENIE_REGION=us # or eu for EU-hosted accounts\n#\n# Slack (notifications) \u2014 https://api.slack.com/apps\n# NOTIFICATION_WEBHOOK_URL=https://hooks.slack.com/services/...\n# # or use a bot token: SLACK_BOT_TOKEN=xoxb-...\n";
|
package/dist/cli/config-file.js
CHANGED
|
@@ -148,7 +148,7 @@ export const STARTER_CONFIG = `# .sweny.yml — SWEny project configuration
|
|
|
148
148
|
# observability-provider: datadog # datadog | sentry | cloudwatch | splunk | elastic | newrelic | loki | prometheus | pagerduty | heroku | opsgenie | vercel | supabase | netlify | fly | render | file
|
|
149
149
|
# issue-tracker-provider: github-issues # github-issues | linear | jira
|
|
150
150
|
# source-control-provider: github # github | gitlab
|
|
151
|
-
# coding-agent-provider: claude # claude
|
|
151
|
+
# coding-agent-provider: claude # claude | codex
|
|
152
152
|
# notification-provider: console # console | slack | teams | discord | email | webhook
|
|
153
153
|
|
|
154
154
|
# ── Investigation ────────────────────────────────────────────────────
|
package/dist/cli/config.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface CliConfig {
|
|
|
11
11
|
/** Auth precedence mode: auto (default), api-key, or oauth. */
|
|
12
12
|
swenyAuth: "auto" | "api-key" | "oauth";
|
|
13
13
|
openaiApiKey: string;
|
|
14
|
+
/** Codex's own credential: CODEX_API_KEY, else CODEX_ACCESS_TOKEN. */
|
|
15
|
+
codexApiKey?: string;
|
|
14
16
|
geminiApiKey: string;
|
|
15
17
|
observabilityProviders: string[];
|
|
16
18
|
observabilityCredentials: Record<string, Record<string, string>>;
|
package/dist/cli/config.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
|
+
import { hasCodexLogin } from "../agent-env.js";
|
|
4
|
+
import { unsupportedAgentError } from "../harness/agents.js";
|
|
3
5
|
export function registerTriageCommand(program) {
|
|
4
6
|
// NOTE: File-eligible options omit Commander defaults so parseCliInputs() can
|
|
5
7
|
// distinguish "user passed flag" from "not passed" and fall through to .sweny.yml.
|
|
6
8
|
return program
|
|
7
9
|
.command("triage")
|
|
8
10
|
.description("Run the SWEny triage workflow")
|
|
9
|
-
.option("--agent <provider>", "Coding agent: claude (
|
|
11
|
+
.option("--agent <provider>", "Coding agent: claude (default) or codex")
|
|
10
12
|
.option("--coding-agent-provider <provider>", "Coding agent provider (alias for --agent)")
|
|
11
13
|
.option("--observability-provider <provider>", "Observability provider (default: none)")
|
|
12
14
|
.option("--issue-tracker-provider <provider>", "Issue tracker provider (default: github-issues)")
|
|
@@ -107,6 +109,7 @@ export function parseCliInputs(options, fileConfig = {}) {
|
|
|
107
109
|
anthropicBaseUrl: env.ANTHROPIC_BASE_URL || f("anthropic-base-url") || "",
|
|
108
110
|
swenyAuth: normalizeSwenyAuth(env.SWENY_AUTH || f("sweny-auth")),
|
|
109
111
|
openaiApiKey: env.OPENAI_API_KEY || "",
|
|
112
|
+
codexApiKey: env.CODEX_API_KEY || env.CODEX_ACCESS_TOKEN || "",
|
|
110
113
|
geminiApiKey: env.GEMINI_API_KEY || env.GOOGLE_API_KEY || "",
|
|
111
114
|
observabilityProviders: obsProviders,
|
|
112
115
|
observabilityCredentials: obsCreds,
|
|
@@ -228,12 +231,17 @@ export function validateInputs(config) {
|
|
|
228
231
|
errors.push("Missing: ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN — get a key at https://console.anthropic.com");
|
|
229
232
|
}
|
|
230
233
|
break;
|
|
234
|
+
case "codex":
|
|
235
|
+
// codex exec authenticates with CODEX_API_KEY (OPENAI_API_KEY is mapped
|
|
236
|
+
// to it) or a stored `codex login` under CODEX_HOME.
|
|
237
|
+
if (!config.openaiApiKey && !config.codexApiKey && !hasCodexLogin(process.env)) {
|
|
238
|
+
errors.push("Missing: CODEX_API_KEY or OPENAI_API_KEY, or a Codex login (`codex login`), for --agent codex");
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
231
241
|
default:
|
|
232
|
-
// Honest --agent (#330):
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
errors.push(`Unsupported coding agent "${config.codingAgentProvider}": the only supported agent is "claude" ` +
|
|
236
|
-
`(headless Claude Code). Remove --agent / coding-agent-provider or set it to claude.`);
|
|
242
|
+
// Honest --agent (#330): only agents with an adapter are accepted.
|
|
243
|
+
// Anything else would silently run a different agent under that name.
|
|
244
|
+
errors.push(unsupportedAgentError(config.codingAgentProvider));
|
|
237
245
|
}
|
|
238
246
|
// Repository required unless all providers are file-based
|
|
239
247
|
const allLocal = config.issueTrackerProvider === "file" && config.sourceControlProvider === "file";
|
|
@@ -650,7 +658,7 @@ export function registerImplementCommand(program) {
|
|
|
650
658
|
return program
|
|
651
659
|
.command("implement <issueId>")
|
|
652
660
|
.description("Implement a fix for a specific issue and open a PR")
|
|
653
|
-
.option("--agent <provider>", "Coding agent: claude (
|
|
661
|
+
.option("--agent <provider>", "Coding agent: claude (default) or codex")
|
|
654
662
|
.option("--coding-agent-provider <provider>", "Coding agent provider (alias for --agent)")
|
|
655
663
|
.option("--issue-tracker-provider <provider>", "Issue tracker (linear|jira|github-issues|file)")
|
|
656
664
|
.option("--source-control-provider <provider>", "Source control (github|gitlab|file)")
|
package/dist/cli/main.js
CHANGED
|
@@ -10,6 +10,8 @@ import { execute } from "../executor.js";
|
|
|
10
10
|
import { triageWorkflow, implementWorkflow, seedContentWorkflow } from "../workflows/index.js";
|
|
11
11
|
import { consoleLogger } from "../types.js";
|
|
12
12
|
import { createHarness } from "../harness/index.js";
|
|
13
|
+
import { isSupportedAgent, unsupportedAgentError } from "../harness/agents.js";
|
|
14
|
+
import { resolveHarnessPolicy } from "../harness/policy.js";
|
|
13
15
|
import { builtinSkills, createSkillMap, validateWorkflowSkills } from "../skills/index.js";
|
|
14
16
|
import { formatMissingSkillLines, skillEnvWarnings } from "./skill-env.js";
|
|
15
17
|
import { configuredSkills, configuredSkillsWithDiagnostics } from "../skills/custom-loader.js";
|
|
@@ -34,7 +36,7 @@ import { createVerboseToolObserver } from "./verbose-observer.js";
|
|
|
34
36
|
import { createRunLogger, renderReceiptLine, summarizeRun, writeStepSummary, WORKFLOW_RUN_DESCRIPTION, WORKFLOW_RUN_OPTIONS, } from "./run-output.js";
|
|
35
37
|
import { writeRunComment } from "./comment-output.js";
|
|
36
38
|
import { registerTriageCommand, registerImplementCommand, parseCliInputs, validateInputs, validateWarnings, parsePositiveInt, } from "./config.js";
|
|
37
|
-
import { c, formatBanner, getStepDetails, formatStepLine, formatDagResultHuman, formatDagResultMarkdown,
|
|
39
|
+
import { c, formatBanner, getStepDetails, formatStepLine, formatDagResultHuman, formatDagResultMarkdown, writeResultJson, formatValidationErrors, formatCrashError, formatCheckResults, } from "./output.js";
|
|
38
40
|
import { checkProviderConnectivity, detectClaudeCodeLogin, discoverWorkflowSkillIds, validateCheckInputs, } from "./check.js";
|
|
39
41
|
import { registerSetupCommand } from "./setup.js";
|
|
40
42
|
import { registerPublishCommand } from "./publish.js";
|
|
@@ -175,6 +177,28 @@ async function resolveRulesAndContext(config) {
|
|
|
175
177
|
context: contextResult.resolved,
|
|
176
178
|
};
|
|
177
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Build the harness for `--agent` (#331). A non-Claude agent is checked before
|
|
182
|
+
* any node runs, so a missing or too-old CLI fails fast with the fix.
|
|
183
|
+
*/
|
|
184
|
+
async function harnessFor(agent, opts, harnessPolicy) {
|
|
185
|
+
if (!isSupportedAgent(agent)) {
|
|
186
|
+
console.error(chalk.red(`\n ${unsupportedAgentError(agent)}\n`));
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
const harness = createHarness(agent, {
|
|
190
|
+
...opts,
|
|
191
|
+
policy: resolveHarnessPolicy(process.env, harnessPolicy, opts.logger),
|
|
192
|
+
});
|
|
193
|
+
if (agent !== "claude") {
|
|
194
|
+
const pre = await harness.preflight();
|
|
195
|
+
if (!pre.ok) {
|
|
196
|
+
console.error(chalk.red(`\n ${pre.reason}\n`));
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return harness;
|
|
201
|
+
}
|
|
178
202
|
// ── sweny triage ──────────────────────────────────────────────────────
|
|
179
203
|
const triageCmd = registerTriageCommand(program);
|
|
180
204
|
triageCmd.action(async (options) => {
|
|
@@ -203,7 +227,7 @@ triageCmd.action(async (options) => {
|
|
|
203
227
|
const skills = createSkillMap(triageSkillDiscovery.skills);
|
|
204
228
|
const mcpAutoConfig = buildMcpAutoConfig(config);
|
|
205
229
|
const mcpServers = buildAutoMcpServers(mcpAutoConfig);
|
|
206
|
-
const claude =
|
|
230
|
+
const claude = await harnessFor(config.codingAgentProvider, {
|
|
207
231
|
maxTurns: config.maxInvestigateTurns || 50,
|
|
208
232
|
cwd: process.cwd(),
|
|
209
233
|
logger: consoleLogger,
|
|
@@ -392,7 +416,7 @@ triageCmd.action(async (options) => {
|
|
|
392
416
|
const durationMs = Date.now() - runStart;
|
|
393
417
|
// Output
|
|
394
418
|
if (config.json) {
|
|
395
|
-
|
|
419
|
+
await writeResultJson(results);
|
|
396
420
|
}
|
|
397
421
|
else {
|
|
398
422
|
console.log(formatDagResultHuman(results, durationMs, config));
|
|
@@ -477,7 +501,7 @@ triageCmd.action(async (options) => {
|
|
|
477
501
|
}
|
|
478
502
|
});
|
|
479
503
|
// ── sweny implement ───────────────────────────────────────────────────
|
|
480
|
-
const implementCmd = registerImplementCommand(program);
|
|
504
|
+
const implementCmd = registerImplementCommand(program).option("--json", "Output result as JSON", false);
|
|
481
505
|
implementCmd.action(async (issueId, options) => {
|
|
482
506
|
const fileConfig = loadConfigFile();
|
|
483
507
|
// Build a minimal CliConfig for the implement command by merging CLI opts with env/file
|
|
@@ -513,7 +537,7 @@ implementCmd.action(async (issueId, options) => {
|
|
|
513
537
|
const skills = createSkillMap(implementSkillDiscovery.skills);
|
|
514
538
|
const mcpAutoConfig = buildMcpAutoConfig(config);
|
|
515
539
|
const mcpServers = buildAutoMcpServers(mcpAutoConfig);
|
|
516
|
-
const claude =
|
|
540
|
+
const claude = await harnessFor(config.codingAgentProvider, {
|
|
517
541
|
maxTurns: config.maxImplementTurns || 40,
|
|
518
542
|
cwd: process.cwd(),
|
|
519
543
|
logger: consoleLogger,
|
|
@@ -604,17 +628,22 @@ implementCmd.action(async (issueId, options) => {
|
|
|
604
628
|
catch {
|
|
605
629
|
// silent
|
|
606
630
|
}
|
|
631
|
+
if (config.json) {
|
|
632
|
+
await writeResultJson(results);
|
|
633
|
+
}
|
|
607
634
|
if (hasFailed) {
|
|
608
635
|
console.error(chalk.red(`\n Implement workflow failed\n`));
|
|
609
636
|
process.exit(1);
|
|
610
637
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
638
|
+
if (!config.json) {
|
|
639
|
+
const prResult = results.get("create_pr");
|
|
640
|
+
const prUrl = prResult?.data?.prUrl;
|
|
641
|
+
if (prUrl) {
|
|
642
|
+
console.log(chalk.green(`\n PR created: ${prUrl}\n`));
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
console.log(chalk.green(`\n Implement workflow completed\n`));
|
|
646
|
+
}
|
|
618
647
|
}
|
|
619
648
|
// Legacy /api/report back-compat — see triage path.
|
|
620
649
|
try {
|
|
@@ -756,14 +785,15 @@ export async function workflowRunAction(file, options) {
|
|
|
756
785
|
});
|
|
757
786
|
// Raw [info]/[debug] lines are verbose-only; warnings are rendered (#383).
|
|
758
787
|
const runLogger = createRunLogger({ verbose: Boolean(options.verbose), tty: isTTY });
|
|
759
|
-
|
|
788
|
+
// --agent picks the harness (#331): claude (default) or codex.
|
|
789
|
+
const claude = await harnessFor(config.codingAgentProvider, {
|
|
760
790
|
maxTurns: config.maxInvestigateTurns || 50,
|
|
761
791
|
cwd: process.cwd(),
|
|
762
792
|
logger: runLogger,
|
|
763
793
|
defaultMcpServers: mcpServers,
|
|
764
794
|
mcpServers: config.mcpServers,
|
|
765
795
|
model: workflow.model,
|
|
766
|
-
});
|
|
796
|
+
}, typeof options.harnessPolicy === "string" ? options.harnessPolicy : undefined);
|
|
767
797
|
// Track per-node entry time to compute elapsed on exit
|
|
768
798
|
const nodeEnterTimes = new Map();
|
|
769
799
|
const wfProgressObserver = isJson
|
|
@@ -956,7 +986,7 @@ export async function workflowRunAction(file, options) {
|
|
|
956
986
|
});
|
|
957
987
|
}
|
|
958
988
|
if (isJson) {
|
|
959
|
-
|
|
989
|
+
await writeResultJson(results);
|
|
960
990
|
process.exit(wfHasFailed ? 1 : 0);
|
|
961
991
|
return;
|
|
962
992
|
}
|
package/dist/cli/output.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export declare function extractCredentialHint(err: unknown): string | null;
|
|
|
24
24
|
export declare function formatCrashError(error: unknown): string;
|
|
25
25
|
export declare function formatCheckResults(results: CheckResult[]): string;
|
|
26
26
|
export declare function formatResultJson(results: Map<string, NodeResult>): string;
|
|
27
|
+
/** Drain terminal JSON before process.exit(), including when stdout is a pipe. */
|
|
28
|
+
export declare function writeResultJson(results: Map<string, NodeResult>): Promise<void>;
|
|
27
29
|
/**
|
|
28
30
|
* Format triage results as GitHub-flavored markdown for `$GITHUB_STEP_SUMMARY`.
|
|
29
31
|
*
|
package/dist/cli/output.js
CHANGED
|
@@ -405,6 +405,17 @@ export function formatCheckResults(results) {
|
|
|
405
405
|
export function formatResultJson(results) {
|
|
406
406
|
return JSON.stringify(Object.fromEntries(results), null, 2);
|
|
407
407
|
}
|
|
408
|
+
/** Drain terminal JSON before process.exit(), including when stdout is a pipe. */
|
|
409
|
+
export function writeResultJson(results) {
|
|
410
|
+
return new Promise((resolve, reject) => {
|
|
411
|
+
process.stdout.write(formatResultJson(results) + "\n", (error) => {
|
|
412
|
+
if (error)
|
|
413
|
+
reject(error);
|
|
414
|
+
else
|
|
415
|
+
resolve();
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
}
|
|
408
419
|
export function formatDagResultMarkdown(results, durationMs, config, options = {}) {
|
|
409
420
|
const duration = formatDuration(durationMs);
|
|
410
421
|
const lines = [];
|
|
@@ -53,6 +53,8 @@ export interface RunRecord {
|
|
|
53
53
|
id: string;
|
|
54
54
|
version: string;
|
|
55
55
|
};
|
|
56
|
+
/** Opinions the harness could not honor natively on some node, deduped. Absent when none. */
|
|
57
|
+
degraded?: string[];
|
|
56
58
|
}
|
|
57
59
|
/** sha256 (hex) of the workflow as canonical JSON: keys sorted, undefined dropped. */
|
|
58
60
|
export declare function hashWorkflow(workflow: Workflow): string;
|
package/dist/cli/run-history.js
CHANGED
|
@@ -92,6 +92,7 @@ export function buildRunRecord(i) {
|
|
|
92
92
|
};
|
|
93
93
|
});
|
|
94
94
|
const harness = [...i.results.values()].find((r) => r.harness)?.harness;
|
|
95
|
+
const degraded = [...new Set([...i.results.values()].flatMap((r) => r.degraded ?? []))];
|
|
95
96
|
return {
|
|
96
97
|
schema_version: RUN_HISTORY_SCHEMA_VERSION,
|
|
97
98
|
run_id: i.runId,
|
|
@@ -111,6 +112,7 @@ export function buildRunRecord(i) {
|
|
|
111
112
|
cost_usd: summary.costUsd ?? null,
|
|
112
113
|
},
|
|
113
114
|
...(harness ? { harness: { id: harness.id, version: harness.version } } : {}),
|
|
115
|
+
...(degraded.length > 0 ? { degraded } : {}),
|
|
114
116
|
};
|
|
115
117
|
}
|
|
116
118
|
// ── Write, prune, read ──────────────────────────────────────────
|
package/dist/cli/run-output.d.ts
CHANGED
|
@@ -18,7 +18,16 @@ export interface RunSummary {
|
|
|
18
18
|
tokens?: number;
|
|
19
19
|
/** Sum of SDK-reported cost. Undefined when the SDK reported none (never estimated). */
|
|
20
20
|
costUsd?: number;
|
|
21
|
+
/** Harness that ran the nodes, when a result was tagged. */
|
|
22
|
+
harness?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Opinions some node's harness could not honor natively, as short keys
|
|
25
|
+
* (`max_turns`, `egress allowlist`, `deny [write]`). Absent when none.
|
|
26
|
+
*/
|
|
27
|
+
degraded?: string[];
|
|
21
28
|
}
|
|
29
|
+
/** The short key of a `degraded` entry: the text before its first colon. */
|
|
30
|
+
export declare function degradedKey(entry: string): string;
|
|
22
31
|
/** Sum the per-node results into one run summary. */
|
|
23
32
|
export declare function summarizeRun(results: Map<string, NodeResult>, durationMs: number, crashed?: boolean): RunSummary;
|
|
24
33
|
export declare function formatReceiptDuration(ms: number): string;
|
|
@@ -26,8 +35,10 @@ export declare function formatTokenCount(n: number): string;
|
|
|
26
35
|
export declare function formatCost(usd: number): string;
|
|
27
36
|
/**
|
|
28
37
|
* One plain line: `✓ 3/3 nodes · 41 tool calls · 2m10s · 12k tokens · $0.18`.
|
|
29
|
-
* Token and cost segments are omitted when the SDK reported none.
|
|
30
|
-
*
|
|
38
|
+
* Token and cost segments are omitted when the SDK reported none. A harness
|
|
39
|
+
* other than Claude Code is named, and anything it could not honor natively
|
|
40
|
+
* is listed (`· codex · degraded: max_turns`). No ANSI; callers colorize only
|
|
41
|
+
* when writing to a TTY.
|
|
31
42
|
*/
|
|
32
43
|
export declare function formatReceipt(s: RunSummary): string;
|
|
33
44
|
/** Receipt for a terminal: colored only when `color` is true. */
|
package/dist/cli/run-output.js
CHANGED
|
@@ -9,6 +9,11 @@ import fs from "node:fs";
|
|
|
9
9
|
import chalk from "chalk";
|
|
10
10
|
import { consoleLogger } from "../types.js";
|
|
11
11
|
import { toMermaidBlock } from "../mermaid.js";
|
|
12
|
+
/** The short key of a `degraded` entry: the text before its first colon. */
|
|
13
|
+
export function degradedKey(entry) {
|
|
14
|
+
const i = entry.indexOf(":");
|
|
15
|
+
return (i === -1 ? entry : entry.slice(0, i)).trim();
|
|
16
|
+
}
|
|
12
17
|
/** Sum the per-node results into one run summary. */
|
|
13
18
|
export function summarizeRun(results, durationMs, crashed = false) {
|
|
14
19
|
let nodesOk = 0;
|
|
@@ -17,8 +22,13 @@ export function summarizeRun(results, durationMs, crashed = false) {
|
|
|
17
22
|
let toolCalls = 0;
|
|
18
23
|
let tokens;
|
|
19
24
|
let costUsd;
|
|
25
|
+
let harness;
|
|
26
|
+
const degraded = new Set();
|
|
20
27
|
const isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
21
28
|
for (const r of results.values()) {
|
|
29
|
+
harness ??= r.harness?.id;
|
|
30
|
+
for (const d of r.degraded ?? [])
|
|
31
|
+
degraded.add(degradedKey(d));
|
|
22
32
|
if (r.status === "success")
|
|
23
33
|
nodesOk++;
|
|
24
34
|
else if (r.status === "skipped")
|
|
@@ -45,6 +55,8 @@ export function summarizeRun(results, durationMs, crashed = false) {
|
|
|
45
55
|
durationMs,
|
|
46
56
|
...(tokens !== undefined ? { tokens } : {}),
|
|
47
57
|
...(costUsd !== undefined ? { costUsd } : {}),
|
|
58
|
+
...(harness !== undefined ? { harness } : {}),
|
|
59
|
+
...(degraded.size > 0 ? { degraded: [...degraded] } : {}),
|
|
48
60
|
};
|
|
49
61
|
}
|
|
50
62
|
export function formatReceiptDuration(ms) {
|
|
@@ -71,8 +83,10 @@ export function formatCost(usd) {
|
|
|
71
83
|
}
|
|
72
84
|
/**
|
|
73
85
|
* One plain line: `✓ 3/3 nodes · 41 tool calls · 2m10s · 12k tokens · $0.18`.
|
|
74
|
-
* Token and cost segments are omitted when the SDK reported none.
|
|
75
|
-
*
|
|
86
|
+
* Token and cost segments are omitted when the SDK reported none. A harness
|
|
87
|
+
* other than Claude Code is named, and anything it could not honor natively
|
|
88
|
+
* is listed (`· codex · degraded: max_turns`). No ANSI; callers colorize only
|
|
89
|
+
* when writing to a TTY.
|
|
76
90
|
*/
|
|
77
91
|
export function formatReceipt(s) {
|
|
78
92
|
const parts = [
|
|
@@ -82,6 +96,8 @@ export function formatReceipt(s) {
|
|
|
82
96
|
formatReceiptDuration(s.durationMs),
|
|
83
97
|
...(s.tokens !== undefined ? [`${formatTokenCount(s.tokens)} tokens`] : []),
|
|
84
98
|
...(s.costUsd !== undefined ? [formatCost(s.costUsd)] : []),
|
|
99
|
+
...(s.harness !== undefined && s.harness !== "claude-code" ? [s.harness] : []),
|
|
100
|
+
...(s.degraded && s.degraded.length > 0 ? [`degraded: ${s.degraded.join(", ")}`] : []),
|
|
85
101
|
];
|
|
86
102
|
return parts.join(" · ");
|
|
87
103
|
}
|
|
@@ -195,4 +211,9 @@ export const WORKFLOW_RUN_OPTIONS = [
|
|
|
195
211
|
"Write a PR-comment markdown (run receipt, status-colored DAG, per-node table; metadata only) to <path> so any CI can post it",
|
|
196
212
|
],
|
|
197
213
|
["--input <json>", "JSON string of input data to pass to the workflow"],
|
|
214
|
+
["--agent <id>", "Coding agent that runs the nodes: claude (default) or codex"],
|
|
215
|
+
[
|
|
216
|
+
"--harness-policy <mode>",
|
|
217
|
+
"strict: refuse a node whose policy the agent cannot enforce; warn: run it and report what was not enforced (default: strict under GitHub Actions, warn elsewhere; env SWENY_HARNESS_POLICY)",
|
|
218
|
+
],
|
|
198
219
|
];
|
package/dist/executor.js
CHANGED
|
@@ -23,6 +23,7 @@ import { validateWorkflow } from "./schema.js";
|
|
|
23
23
|
import { resolveAgentAccess } from "./agent-env.js";
|
|
24
24
|
import { fenceUntrusted } from "./untrusted.js";
|
|
25
25
|
import { asClaude } from "./harness/compat.js";
|
|
26
|
+
import { isToolClass } from "./harness/policy.js";
|
|
26
27
|
/**
|
|
27
28
|
* Default workflow-level step budget. Counts total node executions across the
|
|
28
29
|
* whole run (loop iterations included). Generous enough for legitimate deep
|
|
@@ -171,8 +172,8 @@ export async function execute(workflow, input, options) {
|
|
|
171
172
|
// Build context: input + all prior node results.
|
|
172
173
|
// Each prior node entry is the node's `data` augmented with `evals`
|
|
173
174
|
// (a Record<name, EvalResult> for downstream lookup like
|
|
174
|
-
// `priorNode.evals.tests_run_clean.pass`).
|
|
175
|
-
//
|
|
175
|
+
// `priorNode.evals.tests_run_clean.pass`). This namespace is reserved
|
|
176
|
+
// for runtime verdicts; an agent-provided `data.evals` never overrides it.
|
|
176
177
|
const context = {
|
|
177
178
|
input,
|
|
178
179
|
...Object.fromEntries([...results.entries()].map(([k, v]) => [k, buildPriorNodeContext(v)])),
|
|
@@ -240,6 +241,11 @@ export async function execute(workflow, input, options) {
|
|
|
240
241
|
// Per-node execution model: node.model ?? workflow.model. When undefined,
|
|
241
242
|
// claude.run falls back to its own client default (then Claude Code's).
|
|
242
243
|
const nodeModel = resolveExecutionModel(node, workflow);
|
|
244
|
+
// `tools.deny` entries that name a portable class (shell, write, edit, net,
|
|
245
|
+
// subagent) also deny that class of built-in agent tools; the harness
|
|
246
|
+
// compiles them or reports them in `degraded`.
|
|
247
|
+
const denyClasses = (node.tools?.deny ?? []).filter(isToolClass);
|
|
248
|
+
let degradedLogged = false;
|
|
243
249
|
while (true) {
|
|
244
250
|
result = await claude.run({
|
|
245
251
|
instruction: currentInstruction,
|
|
@@ -248,6 +254,7 @@ export async function execute(workflow, input, options) {
|
|
|
248
254
|
outputSchema: node.output,
|
|
249
255
|
maxTurns: node.max_turns,
|
|
250
256
|
disallowedTools: node.disallowed_tools,
|
|
257
|
+
...(denyClasses.length > 0 ? { deny: denyClasses } : {}),
|
|
251
258
|
model: nodeModel,
|
|
252
259
|
signal,
|
|
253
260
|
timeoutMs,
|
|
@@ -258,6 +265,12 @@ export async function execute(workflow, input, options) {
|
|
|
258
265
|
safeObserve(observer, { type: "node:progress", node: currentId, message }, logger);
|
|
259
266
|
},
|
|
260
267
|
});
|
|
268
|
+
// Opinions the harness could not honor natively: said once per node, never silently dropped.
|
|
269
|
+
if (!degradedLogged && result.degraded && result.degraded.length > 0) {
|
|
270
|
+
degradedLogged = true;
|
|
271
|
+
const who = result.harness?.id ?? "harness";
|
|
272
|
+
logger.warn(` ${who} could not honor natively: ${result.degraded.join("; ")}`, { node: currentId });
|
|
273
|
+
}
|
|
261
274
|
// Retry only triggers on eval failure. Bail on tool/API errors.
|
|
262
275
|
if (result.status !== "success") {
|
|
263
276
|
agentRunFailed = true;
|
|
@@ -355,7 +368,9 @@ export async function execute(workflow, input, options) {
|
|
|
355
368
|
// instead of failing outright. The original error stays in `data.error`
|
|
356
369
|
// and `data.fail_soft` marks the downgrade for downstream nodes and
|
|
357
370
|
// observers. Eval failures never take this path (agentRunFailed guards it).
|
|
358
|
-
|
|
371
|
+
// A strict-policy refusal (#331) is not an agent failure: fail_soft never softens it.
|
|
372
|
+
const refused = result.data?.refused === true;
|
|
373
|
+
if (agentRunFailed && result.status === "failed" && node.fail_soft === true && !refused) {
|
|
359
374
|
const failError = result.data?.error;
|
|
360
375
|
logger.warn(` fail_soft: node failed (${String(failError ?? "unknown error")}); continuing with partial output`, {
|
|
361
376
|
node: currentId,
|
|
@@ -488,23 +503,15 @@ function warnOnJudgeBudget(workflow, logger) {
|
|
|
488
503
|
*
|
|
489
504
|
* Spec contract (https://spec.sweny.ai/nodes/#evalresult-type): downstream
|
|
490
505
|
* nodes can read `priorNode.evals.<name>.pass` via the natural lookup path.
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
* output happens to include a literal `evals` field, that field wins (and
|
|
495
|
-
* the lookup degrades to "evals not available" for downstream readers).
|
|
506
|
+
* Preserve ordinary data fields, but reserve `evals` for runtime verdicts.
|
|
507
|
+
* Agent output must not forge a pass for downstream `requires` gates,
|
|
508
|
+
* including when no evaluator ran (the trusted namespace is then empty).
|
|
496
509
|
*/
|
|
497
510
|
function buildPriorNodeContext(result) {
|
|
498
511
|
const data = (result.data ?? {});
|
|
499
512
|
const evals = result.evals ?? [];
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const evalsByName = {};
|
|
503
|
-
for (const e of evals) {
|
|
504
|
-
evalsByName[e.name] = e;
|
|
505
|
-
}
|
|
506
|
-
// Spread evals first so a data-side `evals` key shadows it (back-compat).
|
|
507
|
-
return { evals: evalsByName, ...data };
|
|
513
|
+
const evalsByName = Object.fromEntries(evals.map((e) => [e.name, e]));
|
|
514
|
+
return { ...data, evals: evalsByName };
|
|
508
515
|
}
|
|
509
516
|
/**
|
|
510
517
|
* Build the prior-node entry shown to the LLM route evaluator.
|
|
@@ -564,14 +571,9 @@ function buildRouteEvalEntry(result, sourceNode) {
|
|
|
564
571
|
dataView = fullData;
|
|
565
572
|
}
|
|
566
573
|
const evals = result.evals ?? [];
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
for (const e of evals) {
|
|
571
|
-
evalsByName[e.name] = e;
|
|
572
|
-
}
|
|
573
|
-
// Spread evals first so a data-side `evals` key shadows it (back-compat).
|
|
574
|
-
return { view: { evals: evalsByName, ...dataView }, missing };
|
|
574
|
+
const evalsByName = Object.fromEntries(evals.map((e) => [e.name, e]));
|
|
575
|
+
// Schema projection never grants agent data authority over runtime verdicts.
|
|
576
|
+
return { view: { ...dataView, evals: evalsByName }, missing };
|
|
575
577
|
}
|
|
576
578
|
/**
|
|
577
579
|
* Return the declared output property names for a node, or null when the
|
|
@@ -762,7 +764,10 @@ function filterNodeTools(tools, filter, nodeId, logger) {
|
|
|
762
764
|
if (!filter)
|
|
763
765
|
return tools;
|
|
764
766
|
const resolvedNames = new Set(tools.map((t) => t.name));
|
|
765
|
-
|
|
767
|
+
// A deny entry that names a tool class (write, shell, ...) targets built-in
|
|
768
|
+
// agent tools too, so it is not a typo when no skill tool has that name.
|
|
769
|
+
const denyNames = (filter.deny ?? []).filter((n) => !isToolClass(n));
|
|
770
|
+
for (const name of [...(filter.allow ?? []), ...denyNames]) {
|
|
766
771
|
if (!resolvedNames.has(name)) {
|
|
767
772
|
logger.warn(` tools filter: '${name}' does not match any tool resolved for node '${nodeId}'`, {
|
|
768
773
|
node: nodeId,
|