@sweny-ai/core 0.1.142 → 0.1.144

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.
Files changed (45) hide show
  1. package/dist/agent-env.d.ts +24 -0
  2. package/dist/agent-env.js +40 -7
  3. package/dist/cli/config-file.d.ts +1 -1
  4. package/dist/cli/config-file.js +1 -1
  5. package/dist/cli/config.d.ts +2 -0
  6. package/dist/cli/config.js +15 -7
  7. package/dist/cli/main.js +30 -4
  8. package/dist/cli/run-history.d.ts +2 -0
  9. package/dist/cli/run-history.js +2 -0
  10. package/dist/cli/run-output.d.ts +13 -2
  11. package/dist/cli/run-output.js +27 -2
  12. package/dist/executor.d.ts +12 -0
  13. package/dist/executor.js +127 -7
  14. package/dist/harness/abort.d.ts +17 -0
  15. package/dist/harness/abort.js +53 -0
  16. package/dist/harness/agents.d.ts +11 -0
  17. package/dist/harness/agents.js +15 -0
  18. package/dist/harness/capabilities.d.ts +23 -0
  19. package/dist/harness/capabilities.js +35 -0
  20. package/dist/harness/claude-code.d.ts +20 -43
  21. package/dist/harness/claude-code.js +58 -312
  22. package/dist/harness/codex.d.ts +175 -0
  23. package/dist/harness/codex.js +921 -0
  24. package/dist/harness/index.d.ts +11 -3
  25. package/dist/harness/index.js +8 -8
  26. package/dist/harness/parse.d.ts +49 -0
  27. package/dist/harness/parse.js +253 -0
  28. package/dist/harness/policy.d.ts +23 -1
  29. package/dist/harness/policy.js +73 -15
  30. package/dist/harness/prompts.d.ts +7 -1
  31. package/dist/harness/prompts.js +16 -0
  32. package/dist/harness/types.d.ts +13 -2
  33. package/dist/harness/types.js +2 -2
  34. package/dist/index.d.ts +8 -4
  35. package/dist/index.js +6 -3
  36. package/dist/node-policy.d.ts +31 -0
  37. package/dist/node-policy.js +42 -0
  38. package/dist/safe-outputs.d.ts +103 -0
  39. package/dist/safe-outputs.js +460 -0
  40. package/dist/schema.d.ts +501 -12
  41. package/dist/schema.js +210 -4
  42. package/dist/skills/github.js +18 -0
  43. package/dist/types.d.ts +107 -1
  44. package/dist/types.js +29 -0
  45. package/package.json +1 -1
@@ -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([...AGENT_ENV_ALLOWLIST, ...(opts.extraVars ?? []), ...passthrough]);
323
- const prefixes = [...AGENT_ENV_PREFIXES];
324
- if (truthy(source.CLAUDE_CODE_USE_BEDROCK))
325
- prefixes.push(...BEDROCK_PREFIXES);
326
- if (truthy(source.CLAUDE_CODE_USE_VERTEX))
327
- for (const v of VERTEX_VARS)
328
- exact.add(v);
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 (the only supported agent)\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";
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";
@@ -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 (the only supported agent)
151
+ # coding-agent-provider: claude # claude | codex
152
152
  # notification-provider: console # console | slack | teams | discord | email | webhook
153
153
 
154
154
  # ── Investigation ────────────────────────────────────────────────────
@@ -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>>;
@@ -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 (the only supported agent)")
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): SWEny runs headless Claude Code only. Nothing
233
- // dispatches on other values, so accepting them would silently run
234
- // Claude under a different name.
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 (the only supported agent)")
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";
@@ -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 = createHarness("claude-code", {
230
+ const claude = await harnessFor(config.codingAgentProvider, {
207
231
  maxTurns: config.maxInvestigateTurns || 50,
208
232
  cwd: process.cwd(),
209
233
  logger: consoleLogger,
@@ -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 = createHarness("claude-code", {
540
+ const claude = await harnessFor(config.codingAgentProvider, {
517
541
  maxTurns: config.maxImplementTurns || 40,
518
542
  cwd: process.cwd(),
519
543
  logger: consoleLogger,
@@ -761,14 +785,15 @@ export async function workflowRunAction(file, options) {
761
785
  });
762
786
  // Raw [info]/[debug] lines are verbose-only; warnings are rendered (#383).
763
787
  const runLogger = createRunLogger({ verbose: Boolean(options.verbose), tty: isTTY });
764
- const claude = createHarness("claude-code", {
788
+ // --agent picks the harness (#331): claude (default) or codex.
789
+ const claude = await harnessFor(config.codingAgentProvider, {
765
790
  maxTurns: config.maxInvestigateTurns || 50,
766
791
  cwd: process.cwd(),
767
792
  logger: runLogger,
768
793
  defaultMcpServers: mcpServers,
769
794
  mcpServers: config.mcpServers,
770
795
  model: workflow.model,
771
- });
796
+ }, typeof options.harnessPolicy === "string" ? options.harnessPolicy : undefined);
772
797
  // Track per-node entry time to compute elapsed on exit
773
798
  const nodeEnterTimes = new Map();
774
799
  const wfProgressObserver = isJson
@@ -939,6 +964,7 @@ export async function workflowRunAction(file, options) {
939
964
  fileRoot: config.fileRoot || undefined,
940
965
  signal,
941
966
  max_steps: wfMaxSteps,
967
+ stageOutputs: options.stage === true,
942
968
  }), wfTimeoutMs, `Workflow ${workflow.name}`);
943
969
  const wfDurationMs = Date.now() - runStart;
944
970
  const wfHasFailed = [...results.values()].some((r) => r.status === "failed");
@@ -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;
@@ -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 ──────────────────────────────────────────
@@ -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. No ANSI;
30
- * callers colorize only when writing to a TTY.
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. */
@@ -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. No ANSI;
75
- * callers colorize only when writing to a TTY.
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
  }
@@ -182,6 +198,10 @@ export const WORKFLOW_RUN_OPTIONS = [
182
198
  "--dry-run",
183
199
  "Run with read-only tools only: write tools, external MCP servers, and shell/file-edit tools are withheld, so nothing is created, posted, or sent. Stops at the first conditional edge. To inspect nodes without running, use --list-nodes.",
184
200
  ],
201
+ [
202
+ "--stage",
203
+ "Run normally, but preview every safe output (nodes with outputs:) instead of writing it: print what would be written and write nothing",
204
+ ],
185
205
  ["--list-nodes", "Validate, print nodes and skills, and exit without running"],
186
206
  ["--json", "Output result as JSON on stdout; suppress progress output"],
187
207
  ["--stream", "Stream NDJSON events to stdout (for Studio / automation)"],
@@ -195,4 +215,9 @@ export const WORKFLOW_RUN_OPTIONS = [
195
215
  "Write a PR-comment markdown (run receipt, status-colored DAG, per-node table; metadata only) to <path> so any CI can post it",
196
216
  ],
197
217
  ["--input <json>", "JSON string of input data to pass to the workflow"],
218
+ ["--agent <id>", "Coding agent that runs the nodes: claude (default) or codex"],
219
+ [
220
+ "--harness-policy <mode>",
221
+ "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)",
222
+ ],
198
223
  ];
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import type { Workflow, Skill, Tool, Claude, Observer, Logger, ExecutionResult } from "./types.js";
16
16
  import type { AgentHarness } from "./harness/types.js";
17
+ import { type ActorInfo } from "./safe-outputs.js";
17
18
  export interface ExecuteOptions {
18
19
  /** Registered skills (id → Skill) */
19
20
  skills: Map<string, Skill>;
@@ -73,6 +74,17 @@ export interface ExecuteOptions {
73
74
  * whole-run budget. Default: no timeout (back-compat).
74
75
  */
75
76
  timeoutMs?: number;
77
+ /**
78
+ * Safe outputs (#365): preview every write and apply none (CLI `--stage`).
79
+ * A dry run always stages. Default: false.
80
+ */
81
+ stageOutputs?: boolean;
82
+ /**
83
+ * Who triggered the run, for `safe_outputs.trusted_actors` /
84
+ * `trusted_associations`. Default: `GITHUB_ACTOR` and the author
85
+ * association in the `GITHUB_EVENT_PATH` payload.
86
+ */
87
+ actor?: ActorInfo;
76
88
  }
77
89
  /**
78
90
  * Default workflow-level step budget. Counts total node executions across the