@sweny-ai/core 0.1.177 → 0.1.178

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.
@@ -24,6 +24,27 @@ export declare function loadDotenv(cwd?: string): void;
24
24
  * treats as on/off.
25
25
  */
26
26
  export declare function applyAgentFileConfig(fileConfig: FileConfig, env?: NodeJS.ProcessEnv): void;
27
+ /**
28
+ * Operator config for the decision model (#357), one trust domain per
29
+ * credential. `.sweny.yml` is repo content a pull request can change, so:
30
+ *
31
+ * - SWENY_DECIDER_URL (operator env) may point anywhere public; only then is
32
+ * SWENY_DECIDER_API_KEY sent, and only SWENY_DECIDER_ALLOW_PRIVATE=true
33
+ * (env) admits loopback or private addresses.
34
+ * - A `.sweny.yml` `decider: { url, model }` URL must be loopback (local
35
+ * Ollama) and never gets a key.
36
+ *
37
+ * `trusted(key)` reads an operator env value; it must not see values a
38
+ * committed `.env` supplied. `noDecider` (`--no-decider`) returns false.
39
+ * Undefined when no URL and model are configured.
40
+ */
41
+ export declare function operatorDeciderConfig(fileConfig: FileConfig, env: NodeJS.ProcessEnv, noDecider: boolean, trusted?: (key: string) => string | undefined): {
42
+ url: string;
43
+ model: string;
44
+ apiKey?: string;
45
+ allowPrivate?: boolean;
46
+ loopbackOnly?: boolean;
47
+ } | false | undefined;
27
48
  /** Parsed config file — flat strings for scalar fields, arrays for list fields, objects for nested blocks. */
28
49
  export type FileConfig = Record<string, string | string[] | Record<string, unknown>>;
29
50
  /**
@@ -33,4 +54,4 @@ export type FileConfig = Record<string, string | string[] | Record<string, unkno
33
54
  */
34
55
  export declare function loadConfigFile(cwd?: string): FileConfig;
35
56
  /** Starter config written by `sweny new`. */
36
- 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 | pi\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";
57
+ 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 | pi\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#\n# Decision model (optional) for nodes with route_by: decider. https://docs.sweny.ai/advanced/decision-models/\n# Local Ollama: set it here (a URL in this file must be loopback and never gets a key):\n# decider:\n# url: http://localhost:11434\n# model: nimble\n# Remote server: set it in the CI or shell environment only, never in this file or .env:\n# SWENY_DECIDER_URL=https://... SWENY_DECIDER_MODEL=... SWENY_DECIDER_API_KEY=...\n";
@@ -91,6 +91,40 @@ export function applyAgentFileConfig(fileConfig, env = process.env) {
91
91
  }
92
92
  }
93
93
  }
94
+ /**
95
+ * Operator config for the decision model (#357), one trust domain per
96
+ * credential. `.sweny.yml` is repo content a pull request can change, so:
97
+ *
98
+ * - SWENY_DECIDER_URL (operator env) may point anywhere public; only then is
99
+ * SWENY_DECIDER_API_KEY sent, and only SWENY_DECIDER_ALLOW_PRIVATE=true
100
+ * (env) admits loopback or private addresses.
101
+ * - A `.sweny.yml` `decider: { url, model }` URL must be loopback (local
102
+ * Ollama) and never gets a key.
103
+ *
104
+ * `trusted(key)` reads an operator env value; it must not see values a
105
+ * committed `.env` supplied. `noDecider` (`--no-decider`) returns false.
106
+ * Undefined when no URL and model are configured.
107
+ */
108
+ export function operatorDeciderConfig(fileConfig, env, noDecider, trusted = (key) => env[key]) {
109
+ if (noDecider)
110
+ return false;
111
+ const str = (v) => (typeof v === "string" && v.trim() ? v.trim() : undefined);
112
+ const model = str(trusted("SWENY_DECIDER_MODEL")) ?? str(env.SWENY_DECIDER_MODEL) ?? str(fileConfig["decider.model"]);
113
+ const envUrl = str(trusted("SWENY_DECIDER_URL"));
114
+ if (envUrl) {
115
+ if (!model)
116
+ return undefined;
117
+ const apiKey = str(trusted("SWENY_DECIDER_API_KEY"));
118
+ const allowPrivate = ["1", "true"].includes((str(trusted("SWENY_DECIDER_ALLOW_PRIVATE")) ?? "").toLowerCase());
119
+ return { url: envUrl, model, ...(apiKey ? { apiKey } : {}), ...(allowPrivate ? { allowPrivate } : {}) };
120
+ }
121
+ // Repo content: a URL a committed `.env` put in the environment (untrusted),
122
+ // else the `.sweny.yml` one. Loopback only, never a key.
123
+ const repoUrl = str(env.SWENY_DECIDER_URL) ?? str(fileConfig["decider.url"]);
124
+ if (!repoUrl || !model)
125
+ return undefined;
126
+ return { url: repoUrl, model, loopbackOnly: true };
127
+ }
94
128
  /**
95
129
  * Search upward from `cwd` for `.sweny.yml` and parse it.
96
130
  * Scalar values are strings, list values (rules, context) are string arrays.
@@ -296,4 +330,12 @@ export const STARTER_CONFIG = `# .sweny.yml — SWEny project configuration
296
330
  # Slack (notifications) — https://api.slack.com/apps
297
331
  # NOTIFICATION_WEBHOOK_URL=https://hooks.slack.com/services/...
298
332
  # # or use a bot token: SLACK_BOT_TOKEN=xoxb-...
333
+ #
334
+ # Decision model (optional) for nodes with route_by: decider. https://docs.sweny.ai/advanced/decision-models/
335
+ # Local Ollama: set it here (a URL in this file must be loopback and never gets a key):
336
+ # decider:
337
+ # url: http://localhost:11434
338
+ # model: nimble
339
+ # Remote server: set it in the CI or shell environment only, never in this file or .env:
340
+ # SWENY_DECIDER_URL=https://... SWENY_DECIDER_MODEL=... SWENY_DECIDER_API_KEY=...
299
341
  `;
@@ -13,7 +13,8 @@ export declare function workflowRunAction(file: string | undefined, options: Rec
13
13
  maxTokens?: string;
14
14
  maxCost?: string;
15
15
  yes?: boolean;
16
- decider?: string;
16
+ /** False with `--no-decider`. */
17
+ decider?: boolean;
17
18
  },
18
19
  /** Set by `sweny workflow resume` (#363): the journal, run id and original input. */
19
20
  resume?: ResumeContext): Promise<void>;
package/dist/cli/main.js CHANGED
@@ -29,7 +29,7 @@ import { toMermaidBlock } from "../mermaid.js";
29
29
  import { runWorkflowDiagram } from "./diagram.js";
30
30
  import { DagRenderer } from "./renderer.js";
31
31
  import * as readline from "node:readline";
32
- import { loadDotenv, loadConfigFile, applyAgentFileConfig } from "./config-file.js";
32
+ import { loadDotenv, loadConfigFile, applyAgentFileConfig, operatorDeciderConfig } from "./config-file.js";
33
33
  import { buildCredentialMap } from "./credentials.js";
34
34
  import { nonInteractiveUsage, runNew } from "./new.js";
35
35
  import { formatFinalMarkdown, formatFinalOutput, formatSavedOutputLine, resolveFinalOutput, shouldPrintFinalOutput, writeFinalOutput, } from "./final-output.js";
@@ -753,10 +753,6 @@ export function loadWorkflowFile(filePath, knownSkills) {
753
753
  export async function workflowRunAction(file, options,
754
754
  /** Set by `sweny workflow resume` (#363): the journal, run id and original input. */
755
755
  resume) {
756
- if (options.decider !== undefined && options.decider !== "off" && options.decider !== "shadow") {
757
- console.error(chalk.red(`\n --decider must be off or shadow, got "${options.decider}"\n`));
758
- process.exit(1);
759
- }
760
756
  // Reject junk --timeout/--max-steps up front (both paths) instead of
761
757
  // silently falling back to a default.
762
758
  let budget;
@@ -1053,7 +1049,9 @@ resume) {
1053
1049
  fileRoot: config.fileRoot || undefined,
1054
1050
  signal,
1055
1051
  max_steps: wfMaxSteps,
1056
- ...(options.decider ? { decider: options.decider } : {}),
1052
+ // One trust domain per credential (#357): a URL, key or allowance a
1053
+ // committed .env introduced is repo content, read as untrusted.
1054
+ decider: operatorDeciderConfig(fileConfig, process.env, options.decider === false, (k) => trustedEnvValue(process.env, k)),
1057
1055
  stageOutputs: options.stage === true,
1058
1056
  journal,
1059
1057
  ...(spendBudget ? { budget: spendBudget } : {}),
@@ -34,6 +34,7 @@ export const RESUME_SHARED_RUN_FLAGS = [
34
34
  "--input <json>",
35
35
  "--agent <id>",
36
36
  "--harness-policy <mode>",
37
+ "--no-decider",
37
38
  ];
38
39
  /** One line per planned visit, for humans. */
39
40
  export function formatResumePlan(plan, mayRepeat) {
@@ -10,7 +10,7 @@
10
10
  * builds on this format, so readers skip records with a version they do not
11
11
  * know instead of guessing.
12
12
  */
13
- import type { ExecutionEvent, ExecutionTrace, NodeResult, Workflow } from "../types.js";
13
+ import type { ExecutionEvent, ExecutionTrace, NodeResult, RouteRung, Workflow } from "../types.js";
14
14
  export declare const RUN_HISTORY_SCHEMA_VERSION = 1;
15
15
  export declare const RUN_HISTORY_DIR: string;
16
16
  export declare const RUN_HISTORY_KEEP = 200;
@@ -43,9 +43,11 @@ export interface RunRecord {
43
43
  duration_ms: number;
44
44
  status: RunStatus;
45
45
  nodes: RunNodeRecord[];
46
+ /** Edges taken. `rung` says who chose a conditional route: expr, decider or agent (#357). */
46
47
  routes: Array<{
47
48
  from: string;
48
49
  to: string;
50
+ rung?: RouteRung;
49
51
  }>;
50
52
  totals: RunTotals;
51
53
  /** Harness that ran the nodes (id + version). Absent for runs with no harness-tagged result. */
@@ -55,12 +57,6 @@ export interface RunRecord {
55
57
  };
56
58
  /** Opinions the harness could not honor natively on some node, deduped. Absent when none. */
57
59
  degraded?: string[];
58
- /** Shadow-mode decider agreement counts (#357). Absent when the decider was off. */
59
- decider?: {
60
- compared: number;
61
- agreed: number;
62
- fell_through: number;
63
- };
64
60
  }
65
61
  /** sha256 (hex) of the workflow as canonical JSON: keys sorted, undefined dropped. */
66
62
  export declare function hashWorkflow(workflow: Workflow): string;
@@ -103,7 +103,7 @@ export function buildRunRecord(i) {
103
103
  duration_ms: Math.max(0, Math.round(i.durationMs)),
104
104
  status: i.crashed ? "crashed" : summary.ok ? "success" : "failed",
105
105
  nodes,
106
- routes: (i.trace?.edges ?? []).map((e) => ({ from: e.from, to: e.to })),
106
+ routes: (i.trace?.edges ?? []).map((e) => ({ from: e.from, to: e.to, ...(e.rung ? { rung: e.rung } : {}) })),
107
107
  totals: {
108
108
  nodes_total: summary.nodesTotal,
109
109
  nodes_ok: summary.nodesOk,
@@ -114,15 +114,6 @@ export function buildRunRecord(i) {
114
114
  },
115
115
  ...(harness ? { harness: { id: harness.id, version: harness.version } } : {}),
116
116
  ...(degraded.length > 0 ? { degraded } : {}),
117
- ...(summary.decider
118
- ? {
119
- decider: {
120
- compared: summary.decider.compared,
121
- agreed: summary.decider.agreed,
122
- fell_through: summary.decider.fellThrough,
123
- },
124
- }
125
- : {}),
126
117
  };
127
118
  }
128
119
  // ── Write, prune, read ──────────────────────────────────────────
@@ -40,15 +40,19 @@ export interface RunSummary {
40
40
  spent: number;
41
41
  };
42
42
  /**
43
- * Shadow-mode decider agreement (#357), counts only. `compared` decisions
44
- * passed every gate; `agreed` matched the agent; the rest fell through.
45
- * Absent when the decider was off or never consulted.
43
+ * Who decided the run's conditional routes (#357), counts only: a `when`
44
+ * expression, the decision model, or the agent. The decider and expression
45
+ * counts are routes with no agent call. Absent when no route was a decision.
46
46
  */
47
- decider?: {
48
- compared: number;
49
- agreed: number;
50
- fellThrough: number;
51
- };
47
+ routes?: RouteCounts;
48
+ /** Why the decider did not run for some or all routes (missing config, breaker, cap). */
49
+ deciderOff?: string;
50
+ }
51
+ export interface RouteCounts {
52
+ total: number;
53
+ decider: number;
54
+ expr: number;
55
+ agent: number;
52
56
  }
53
57
  export interface RunPolicy {
54
58
  /** Agent env narrowed to the allowlist. Absent when no node reported it. */
@@ -68,6 +72,8 @@ export interface RunPolicy {
68
72
  export declare function degradedKey(entry: string): string;
69
73
  /** Sum the per-node results into one run summary. */
70
74
  export declare function summarizeRun(results: Map<string, NodeResult>, durationMs: number, crashed?: boolean, trace?: ExecutionTrace): RunSummary;
75
+ /** Conditional routes by rung. Unconditional edges decide nothing and are not counted. */
76
+ export declare function countRoutes(trace: ExecutionTrace): RouteCounts;
71
77
  /**
72
78
  * The policy facts the run knew, as short phrases: `env scoped`, `sandbox on`,
73
79
  * `1 output staged`. No phrase for a fact no node reported.
@@ -85,8 +91,8 @@ export declare function formatCost(usd: number): string;
85
91
  export declare function formatBudgetAmount(b: NonNullable<RunSummary["budget"]>): string;
86
92
  /** `budget exceeded: tokens 52k of 50k (run, at node gather)`. */
87
93
  export declare function formatBudgetOverrun(b: NonNullable<RunSummary["budget"]>): string;
88
- /** `agreed 4/5`, or `fell through 2/2` when no decision passed the gates. */
89
- export declare function formatDeciderCounts(d: NonNullable<RunSummary["decider"]>): string;
94
+ /** `5 (3 decider, 1 expr, 1 agent)`: zero rungs are left out. */
95
+ export declare function formatRouteCounts(r: RouteCounts): string;
90
96
  /**
91
97
  * One plain line: `✓ 3/3 nodes · 41 tool calls · 2m10s · 12k tokens · $0.18`.
92
98
  * Token and cost segments are omitted when the SDK reported none. A harness
@@ -7,7 +7,6 @@
7
7
  */
8
8
  import { workspaceRoot, writeFileNoFollow } from "../safe-file.js";
9
9
  import { consoleLogger } from "../types.js";
10
- import { summarizeDecisions } from "../decider.js";
11
10
  import { toMermaidBlock } from "../mermaid.js";
12
11
  import { createPaint } from "./style.js";
13
12
  import { colorEnabled, glyphs } from "./terminal.js";
@@ -84,12 +83,20 @@ export function summarizeRun(results, durationMs, crashed = false, trace) {
84
83
  ...(degraded.size > 0 ? { degraded: [...degraded] } : {}),
85
84
  ...(Object.keys(policy).length > 0 ? { policy } : {}),
86
85
  ...(budget ? { budget } : {}),
87
- ...(trace?.decisions && trace.decisions.length > 0 ? { decider: decisionCounts(trace) } : {}),
86
+ ...(trace && countRoutes(trace).total > 0 ? { routes: countRoutes(trace) } : {}),
87
+ ...(trace?.deciderOff ? { deciderOff: trace.deciderOff } : {}),
88
88
  };
89
89
  }
90
- function decisionCounts(trace) {
91
- const d = summarizeDecisions(trace.decisions ?? []);
92
- return { compared: d.compared, agreed: d.agreed, fellThrough: d.fell_through };
90
+ /** Conditional routes by rung. Unconditional edges decide nothing and are not counted. */
91
+ export function countRoutes(trace) {
92
+ const c = { total: 0, decider: 0, expr: 0, agent: 0 };
93
+ for (const e of trace.edges) {
94
+ if (!e.rung)
95
+ continue;
96
+ c.total++;
97
+ c[e.rung]++;
98
+ }
99
+ return c;
93
100
  }
94
101
  /**
95
102
  * The policy facts the run knew, as short phrases: `env scoped`, `sandbox on`,
@@ -157,9 +164,10 @@ export function formatBudgetAmount(b) {
157
164
  export function formatBudgetOverrun(b) {
158
165
  return `budget exceeded: ${formatBudgetAmount(b)} (${b.scope}, at node ${b.node})`;
159
166
  }
160
- /** `agreed 4/5`, or `fell through 2/2` when no decision passed the gates. */
161
- export function formatDeciderCounts(d) {
162
- return d.compared > 0 ? `agreed ${d.agreed}/${d.compared}` : `fell through ${d.fellThrough}/${d.fellThrough}`;
167
+ /** `5 (3 decider, 1 expr, 1 agent)`: zero rungs are left out. */
168
+ export function formatRouteCounts(r) {
169
+ const parts = ["decider", "expr", "agent"].filter((k) => r[k] > 0).map((k) => `${r[k]} ${k}`);
170
+ return parts.length > 0 ? `${r.total} (${parts.join(", ")})` : String(r.total);
163
171
  }
164
172
  /**
165
173
  * One plain line: `✓ 3/3 nodes · 41 tool calls · 2m10s · 12k tokens · $0.18`.
@@ -180,7 +188,8 @@ export function formatReceipt(s) {
180
188
  ...(s.degraded && s.degraded.length > 0 ? [`degraded: ${s.degraded.join(", ")}`] : []),
181
189
  ...(formatPolicySegment(s.policy) ? [formatPolicySegment(s.policy)] : []),
182
190
  ...(s.budget ? [formatBudgetOverrun(s.budget)] : []),
183
- ...(s.decider ? [`decider ${formatDeciderCounts(s.decider)}`] : []),
191
+ ...(s.routes && s.routes.total > 0 ? [`routes ${formatRouteCounts(s.routes)}`] : []),
192
+ ...(s.deciderOff ? [`decider off: ${s.deciderOff}`] : []),
184
193
  ];
185
194
  return parts.join(" · ");
186
195
  }
@@ -317,8 +326,5 @@ export const WORKFLOW_RUN_OPTIONS = [
317
326
  "--harness-policy <mode>",
318
327
  "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)",
319
328
  ],
320
- [
321
- "--decider <mode>",
322
- "Decision model for route choices: off (default) or shadow. Shadow asks the workflow's decider.provider alongside the agent and reports agreement; the route is always the agent's. Needs decider.provider in the workflow (no default URL)",
323
- ],
329
+ ["--no-decider", "Skip the decision model for this run: the agent decides the routes of route_by: decider nodes"],
324
330
  ];
@@ -6,7 +6,7 @@
6
6
  * METADATA ONLY, like the receipt line: counts, durations, tokens, cost,
7
7
  * policy facts. Never prompts, node outputs, tool inputs, or model prose.
8
8
  */
9
- import { formatBudgetAmount, formatCost, formatDeciderCounts, formatReceiptDuration, formatTokenCount, policyParts, renderReceiptLine, } from "./run-output.js";
9
+ import { formatBudgetAmount, formatCost, formatRouteCounts, formatReceiptDuration, formatTokenCount, policyParts, renderReceiptLine, } from "./run-output.js";
10
10
  import { createPaint } from "./style.js";
11
11
  import { colorEnabled, glyphsFor, richOutput, supportsUnicode, terminalColumns, ticketBoxFor } from "./terminal.js";
12
12
  /** Widest ticket, in columns, frame included. */
@@ -69,7 +69,8 @@ function ticketFields(s) {
69
69
  ...(s.costUsd !== undefined ? [["cost", formatCost(s.costUsd)]] : []),
70
70
  ...(s.harness !== undefined && s.harness !== "claude-code" ? [["harness", s.harness]] : []),
71
71
  ...(s.degraded && s.degraded.length > 0 ? [["degraded", s.degraded.join(", ")]] : []),
72
- ...(s.decider ? [["decider", formatDeciderCounts(s.decider)]] : []),
72
+ ...(s.routes && s.routes.total > 0 ? [["routes", formatRouteCounts(s.routes)]] : []),
73
+ ...(s.deciderOff ? [["decider off", s.deciderOff]] : []),
73
74
  ...(s.budget
74
75
  ? [
75
76
  ["budget", `${formatBudgetAmount(s.budget)} (${s.budget.scope})`],