@sweny-ai/core 0.1.142 → 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 +29 -4
- 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 +20 -2
- 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";
|
|
@@ -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,
|
|
@@ -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,
|
|
@@ -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
|
-
|
|
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
|
|
@@ -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
|
|
@@ -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,
|
|
@@ -749,7 +764,10 @@ function filterNodeTools(tools, filter, nodeId, logger) {
|
|
|
749
764
|
if (!filter)
|
|
750
765
|
return tools;
|
|
751
766
|
const resolvedNames = new Set(tools.map((t) => t.name));
|
|
752
|
-
|
|
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]) {
|
|
753
771
|
if (!resolvedNames.has(name)) {
|
|
754
772
|
logger.warn(` tools filter: '${name}' does not match any tool resolved for node '${nodeId}'`, {
|
|
755
773
|
node: nodeId,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeout + caller-signal wiring shared by every adapter. No agent SDK imports.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Wire an optional timeout + caller signal onto a single AbortController.
|
|
6
|
+
*
|
|
7
|
+
* Returns undefined when neither a timeout nor a signal is supplied so the
|
|
8
|
+
* default code path (no abortController, no timer) is byte-for-byte unchanged.
|
|
9
|
+
*
|
|
10
|
+
* `reason()` reports whether the abort came from the timeout timer or an
|
|
11
|
+
* external signal, so callers can log a distinct timeout message.
|
|
12
|
+
*/
|
|
13
|
+
export declare function makeAbort(timeoutMs?: number, signal?: AbortSignal): {
|
|
14
|
+
controller: AbortController;
|
|
15
|
+
clear: () => void;
|
|
16
|
+
reason: () => "timeout" | "signal" | undefined;
|
|
17
|
+
} | undefined;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeout + caller-signal wiring shared by every adapter. No agent SDK imports.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Wire an optional timeout + caller signal onto a single AbortController.
|
|
6
|
+
*
|
|
7
|
+
* Returns undefined when neither a timeout nor a signal is supplied so the
|
|
8
|
+
* default code path (no abortController, no timer) is byte-for-byte unchanged.
|
|
9
|
+
*
|
|
10
|
+
* `reason()` reports whether the abort came from the timeout timer or an
|
|
11
|
+
* external signal, so callers can log a distinct timeout message.
|
|
12
|
+
*/
|
|
13
|
+
export function makeAbort(timeoutMs, signal) {
|
|
14
|
+
if (!timeoutMs && !signal)
|
|
15
|
+
return undefined;
|
|
16
|
+
const controller = new AbortController();
|
|
17
|
+
let reason;
|
|
18
|
+
let timer;
|
|
19
|
+
const onSignalAbort = () => {
|
|
20
|
+
if (controller.signal.aborted)
|
|
21
|
+
return;
|
|
22
|
+
reason = "signal";
|
|
23
|
+
controller.abort();
|
|
24
|
+
};
|
|
25
|
+
if (signal) {
|
|
26
|
+
if (signal.aborted) {
|
|
27
|
+
reason = "signal";
|
|
28
|
+
controller.abort();
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
signal.addEventListener("abort", onSignalAbort, { once: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (timeoutMs && timeoutMs > 0 && !controller.signal.aborted) {
|
|
35
|
+
timer = setTimeout(() => {
|
|
36
|
+
if (controller.signal.aborted)
|
|
37
|
+
return;
|
|
38
|
+
reason = "timeout";
|
|
39
|
+
controller.abort();
|
|
40
|
+
}, timeoutMs);
|
|
41
|
+
// Don't keep the event loop alive just for the abort timer.
|
|
42
|
+
timer?.unref?.();
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
controller,
|
|
46
|
+
reason: () => reason,
|
|
47
|
+
clear: () => {
|
|
48
|
+
if (timer)
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
signal?.removeEventListener("abort", onSignalAbort);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent ids `--agent` / `coding-agent-provider` accept. Each one has an
|
|
3
|
+
* adapter and a green contract suite; anything else is refused, so sweny never
|
|
4
|
+
* runs one agent under another's name. No runtime imports (the CLI config and
|
|
5
|
+
* the harness factory share it).
|
|
6
|
+
*/
|
|
7
|
+
export declare const SUPPORTED_AGENTS: readonly ["claude", "codex"];
|
|
8
|
+
export type SupportedAgent = (typeof SUPPORTED_AGENTS)[number];
|
|
9
|
+
export declare function isSupportedAgent(id: string): id is SupportedAgent;
|
|
10
|
+
/** The error for an agent id with no adapter. */
|
|
11
|
+
export declare function unsupportedAgentError(id: string): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent ids `--agent` / `coding-agent-provider` accept. Each one has an
|
|
3
|
+
* adapter and a green contract suite; anything else is refused, so sweny never
|
|
4
|
+
* runs one agent under another's name. No runtime imports (the CLI config and
|
|
5
|
+
* the harness factory share it).
|
|
6
|
+
*/
|
|
7
|
+
export const SUPPORTED_AGENTS = ["claude", "codex"];
|
|
8
|
+
export function isSupportedAgent(id) {
|
|
9
|
+
return SUPPORTED_AGENTS.includes(id);
|
|
10
|
+
}
|
|
11
|
+
/** The error for an agent id with no adapter. */
|
|
12
|
+
export function unsupportedAgentError(id) {
|
|
13
|
+
return (`Unsupported coding agent "${id}": supported agents are "claude" (headless Claude Code) ` +
|
|
14
|
+
`and "codex" (Codex CLI). Remove --agent / coding-agent-provider or set it to one of them.`);
|
|
15
|
+
}
|
|
@@ -5,3 +5,26 @@
|
|
|
5
5
|
import type { HarnessCapabilities } from "./types.js";
|
|
6
6
|
/** Claude Code enforces every sweny opinion natively, so `policyGate` never degrades or refuses it. */
|
|
7
7
|
export declare const CLAUDE_CODE_CAPABILITIES: HarnessCapabilities;
|
|
8
|
+
/**
|
|
9
|
+
* Codex (`codex exec --json`), declared from the openai/codex source at
|
|
10
|
+
* rust-v0.159.2 and probed by the contract suite against the scripted fake:
|
|
11
|
+
* - structuredOutput native: `--output-schema <file>` (exec/src/cli.rs).
|
|
12
|
+
* - toolTrace full: `item.*` events for command_execution, file_change,
|
|
13
|
+
* mcp_tool_call and web_search, each with an id and a status
|
|
14
|
+
* (exec/src/exec_events.rs).
|
|
15
|
+
* - builtinDeny shell-only, plus net and subagent: `features.shell_tool=false`
|
|
16
|
+
* removes every shell tool (core/src/tools/spec_plan.rs add_shell_tools),
|
|
17
|
+
* `web_search="disabled"` removes web search, and `features.multi_agent=false`
|
|
18
|
+
* with `agents.enabled=false` removes subagents. apply_patch has no switch,
|
|
19
|
+
* so write and edit cannot be denied as classes.
|
|
20
|
+
* - mcp exclusive native: `--ignore-user-config` drops `$CODEX_HOME/config.toml`,
|
|
21
|
+
* and project `.codex/config.toml` layers stay disabled for a project that
|
|
22
|
+
* is not trusted in that ignored file (config/src/loader/mod.rs).
|
|
23
|
+
* - sandbox fs only: `--sandbox read-only|workspace-write`. Network is one
|
|
24
|
+
* on/off switch (`sandbox_workspace_write.network_access`), not a host list.
|
|
25
|
+
* - readOnly native: `--sandbox read-only` plus the shell, web search and
|
|
26
|
+
* subagent switches above.
|
|
27
|
+
* - turnLimit watchdog: codex exec has no turn limit; sweny counts tool calls.
|
|
28
|
+
* - usage tokens only: `turn.completed.usage`; Codex reports no cost.
|
|
29
|
+
*/
|
|
30
|
+
export declare const CODEX_CAPABILITIES: HarnessCapabilities;
|