@sweny-ai/core 0.1.181 → 0.1.182

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.
@@ -355,11 +355,55 @@ export declare function resolveAgentAccess(skillIds: readonly string[], skills:
355
355
  export type SandboxMode = "off" | "auto" | "strict";
356
356
  /**
357
357
  * Parse the sandbox mode from the explicit `mode` or `SWENY_SANDBOX`.
358
- * Accepts `off|false|0`, `auto`, `strict|on|true|1` (`on` is an alias for
358
+ * Accepts `off|false|0|no`, `auto`, `strict|on|true|1` (`on` is an alias for
359
359
  * `strict`). Unset or unknown (with a warning) uses the default: `auto` when
360
360
  * `CI` is truthy, `off` otherwise. An explicit value always wins.
361
361
  */
362
362
  export declare function resolveSandboxMode(env: Record<string, string | undefined>, mode?: string, logger?: Pick<Logger, "warn">): SandboxMode;
363
+ /**
364
+ * The fix-it hint for a sandbox start that failed on a user namespace. Ubuntu
365
+ * 23.10+ restricts unprivileged user namespaces (the sysctl), and Ubuntu 26.04
366
+ * also loads the `bwrap-userns-restrict` AppArmor profile, which denies every
367
+ * capability to bwrap's children, so the seccomp helper of
368
+ * @anthropic-ai/sandbox-runtime cannot nest its own user namespace
369
+ * (`apply-seccomp: write /proc/self/setgroups ... Permission denied`,
370
+ * anthropics/sandbox-runtime#429). Empty when `stderr` is not a namespace error.
371
+ */
372
+ export declare function usernsHint(stderr: string): string;
373
+ /**
374
+ * Where the host probes look for `bwrap` and `unshare`. Never PATH: a probe runs
375
+ * a real binary, and a directory an agent can write may be on PATH.
376
+ */
377
+ export declare const SYSTEM_BIN_DIRS: readonly ["/usr/bin", "/bin", "/usr/local/bin", "/usr/sbin"];
378
+ /** Absolute path of `name` in {@link SYSTEM_BIN_DIRS} (first match), never from PATH. */
379
+ export declare function systemBin(name: string, exists?: (p: string) => boolean): string | undefined;
380
+ export interface ProbeResult {
381
+ /** Exit status; null when the process could not start or was killed. */
382
+ status: number | null;
383
+ stdout: string;
384
+ stderr: string;
385
+ /** Spawn error (ENOENT, timeout). */
386
+ error?: Error;
387
+ }
388
+ /** Run a probe binary (an absolute path) and collect its result. */
389
+ export type ProbeRunner = (file: string, args: string[]) => ProbeResult;
390
+ /** One line of a probe's output for a warning: the first line, control characters masked, capped. */
391
+ export declare function probeLine(text: string): string;
392
+ /**
393
+ * Functional bubblewrap check: can it actually create a sandbox on this host?
394
+ * The same runs as the Action's `scripts/sandbox-userns.sh probe`: a plain
395
+ * `bwrap ... true`, then a user namespace nested inside bwrap, which the SDK
396
+ * sandbox's seccomp helper needs. On Ubuntu 26.04 the first passes and the
397
+ * second does not, so a plain probe would report a sandbox that then fails
398
+ * every command.
399
+ *
400
+ * `bwrap` and `unshare` come only from {@link SYSTEM_BIN_DIRS}, run by absolute
401
+ * path with {@link PROBE_ENV}, and at most one sanitized line of their stderr
402
+ * reaches the warning. The nested run is skipped (not failed) when there is no
403
+ * system `unshare` or it has no `--map-root-user` (busybox): an unusable probe
404
+ * tool is not a restricted host.
405
+ */
406
+ export declare function bwrapWorks(run?: ProbeRunner, findBin?: (name: string) => string | undefined): string | undefined;
363
407
  /**
364
408
  * Probe whether the SDK sandbox can start on this host. Returns a reason
365
409
  * string when it cannot, undefined when it looks supported.
package/dist/agent-env.js CHANGED
@@ -25,8 +25,8 @@
25
25
  * SWENY_SANDBOX / sandbox auto | strict | off (default: auto in CI, off locally)
26
26
  * SWENY_SANDBOX_ALLOWED_DOMAINS / sandbox-allowed-domains extra hosts for sandboxed commands
27
27
  */
28
- import { execFileSync } from "node:child_process";
29
- import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
28
+ import { spawnSync } from "node:child_process";
29
+ import { accessSync, constants as fsConstants, existsSync, mkdirSync, mkdtempSync, statSync, writeFileSync, } from "node:fs";
30
30
  import { homedir, tmpdir } from "node:os";
31
31
  import * as path from "node:path";
32
32
  import { runStateRoot } from "./journal.js";
@@ -872,13 +872,13 @@ export function resolveAgentAccess(skillIds, skills, grant = []) {
872
872
  }
873
873
  /**
874
874
  * Parse the sandbox mode from the explicit `mode` or `SWENY_SANDBOX`.
875
- * Accepts `off|false|0`, `auto`, `strict|on|true|1` (`on` is an alias for
875
+ * Accepts `off|false|0|no`, `auto`, `strict|on|true|1` (`on` is an alias for
876
876
  * `strict`). Unset or unknown (with a warning) uses the default: `auto` when
877
877
  * `CI` is truthy, `off` otherwise. An explicit value always wins.
878
878
  */
879
879
  export function resolveSandboxMode(env, mode, logger) {
880
880
  const raw = (mode ?? env.SWENY_SANDBOX ?? "").trim().toLowerCase();
881
- if (raw === "off" || raw === "false" || raw === "0")
881
+ if (raw === "off" || raw === "false" || raw === "0" || raw === "no")
882
882
  return "off";
883
883
  if (raw === "strict" || raw === "on" || raw === "true" || raw === "1")
884
884
  return "strict";
@@ -889,23 +889,105 @@ export function resolveSandboxMode(env, mode, logger) {
889
889
  logger?.warn(`SWENY_SANDBOX="${raw}" is not one of off|auto|strict; using ${fallback}`);
890
890
  return fallback;
891
891
  }
892
- /** Functional bubblewrap check: can it actually create a sandbox on this host? */
893
- function bwrapWorks() {
892
+ /**
893
+ * The fix-it hint for a sandbox start that failed on a user namespace. Ubuntu
894
+ * 23.10+ restricts unprivileged user namespaces (the sysctl), and Ubuntu 26.04
895
+ * also loads the `bwrap-userns-restrict` AppArmor profile, which denies every
896
+ * capability to bwrap's children, so the seccomp helper of
897
+ * @anthropic-ai/sandbox-runtime cannot nest its own user namespace
898
+ * (`apply-seccomp: write /proc/self/setgroups ... Permission denied`,
899
+ * anthropics/sandbox-runtime#429). Empty when `stderr` is not a namespace error.
900
+ */
901
+ export function usernsHint(stderr) {
902
+ return /user namespace|userns|uid map|uid_map|setgroups|setting up uid|Operation not permitted|Permission denied/i.test(stderr)
903
+ ? "; unprivileged user namespaces look restricted (Ubuntu 23.10+: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0; " +
904
+ "Ubuntu 26.04 also: sudo apparmor_parser -R /etc/apparmor.d/bwrap-userns-restrict)"
905
+ : "";
906
+ }
907
+ const BWRAP_PROBE = ["--ro-bind", "/", "/", "--dev", "/dev", "--proc", "/proc"];
908
+ /**
909
+ * Where the host probes look for `bwrap` and `unshare`. Never PATH: a probe runs
910
+ * a real binary, and a directory an agent can write may be on PATH.
911
+ */
912
+ export const SYSTEM_BIN_DIRS = ["/usr/bin", "/bin", "/usr/local/bin", "/usr/sbin"];
913
+ /** The whole environment a probe runs with: no parent variables, no secrets. */
914
+ const PROBE_ENV = { PATH: "/usr/bin:/bin" };
915
+ /** Longest probe stderr echoed into a warning. */
916
+ const PROBE_ECHO_MAX = 200;
917
+ function isExecutableFile(p) {
894
918
  try {
895
- execFileSync("bwrap", ["--ro-bind", "/", "/", "--dev", "/dev", "--proc", "/proc", "true"], {
896
- stdio: ["ignore", "ignore", "pipe"],
897
- timeout: 10_000,
898
- });
899
- return undefined;
919
+ if (!statSync(p).isFile())
920
+ return false;
921
+ accessSync(p, fsConstants.X_OK);
922
+ return true;
923
+ }
924
+ catch {
925
+ return false;
926
+ }
927
+ }
928
+ /** Absolute path of `name` in {@link SYSTEM_BIN_DIRS} (first match), never from PATH. */
929
+ export function systemBin(name, exists = isExecutableFile) {
930
+ for (const dir of SYSTEM_BIN_DIRS) {
931
+ const p = path.posix.join(dir, name);
932
+ if (exists(p))
933
+ return p;
900
934
  }
901
- catch (err) {
902
- const stderr = String(err.stderr ?? err.message ?? "").trim();
903
- const userns = /user namespace|uid map|setting up uid|Operation not permitted|Permission denied/i.test(stderr);
904
- return (`bwrap cannot create a sandbox (${stderr.split("\n")[0] || "unknown error"})` +
905
- (userns
906
- ? "; unprivileged user namespaces look restricted (Ubuntu 23.10+: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0)"
907
- : ""));
935
+ return undefined;
936
+ }
937
+ const runProbe = (file, args) => {
938
+ const r = spawnSync(file, args, {
939
+ env: PROBE_ENV,
940
+ stdio: ["ignore", "pipe", "pipe"],
941
+ encoding: "utf8",
942
+ timeout: 10_000,
943
+ maxBuffer: 64 * 1024,
944
+ });
945
+ return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "", error: r.error };
946
+ };
947
+ /** One line of a probe's output for a warning: the first line, control characters masked, capped. */
948
+ export function probeLine(text) {
949
+ const line = text.trim().split(/\r?\n/)[0] ?? "";
950
+ return line.replace(/[\u0000-\u001f\u007f-\u009f]/g, "?").slice(0, PROBE_ECHO_MAX) || "unknown error";
951
+ }
952
+ function probeEcho(r) {
953
+ return probeLine(r.stderr.trim() || r.error?.message || (r.status === null ? "killed" : `exit ${r.status}`));
954
+ }
955
+ /**
956
+ * Functional bubblewrap check: can it actually create a sandbox on this host?
957
+ * The same runs as the Action's `scripts/sandbox-userns.sh probe`: a plain
958
+ * `bwrap ... true`, then a user namespace nested inside bwrap, which the SDK
959
+ * sandbox's seccomp helper needs. On Ubuntu 26.04 the first passes and the
960
+ * second does not, so a plain probe would report a sandbox that then fails
961
+ * every command.
962
+ *
963
+ * `bwrap` and `unshare` come only from {@link SYSTEM_BIN_DIRS}, run by absolute
964
+ * path with {@link PROBE_ENV}, and at most one sanitized line of their stderr
965
+ * reaches the warning. The nested run is skipped (not failed) when there is no
966
+ * system `unshare` or it has no `--map-root-user` (busybox): an unusable probe
967
+ * tool is not a restricted host.
968
+ */
969
+ export function bwrapWorks(run = runProbe, findBin = (name) => systemBin(name)) {
970
+ const ok = (r) => r.status === 0 && !r.error;
971
+ const bwrap = findBin("bwrap");
972
+ if (!bwrap) {
973
+ const dirs = SYSTEM_BIN_DIRS.slice(0, -1).join(", ") + " or " + SYSTEM_BIN_DIRS[SYSTEM_BIN_DIRS.length - 1];
974
+ return `bwrap is not installed in ${dirs} (the sandbox probe does not search PATH)`;
975
+ }
976
+ const plain = run(bwrap, [...BWRAP_PROBE, "true"]);
977
+ if (!ok(plain))
978
+ return `bwrap cannot create a sandbox (${probeEcho(plain)})${usernsHint(plain.stderr)}`;
979
+ const unshare = findBin("unshare");
980
+ if (!unshare)
981
+ return undefined;
982
+ const help = run(unshare, ["--help"]);
983
+ if (!`${help.stdout}\n${help.stderr}`.includes("--map-root-user"))
984
+ return undefined;
985
+ const nested = run(bwrap, [...BWRAP_PROBE, unshare, "--user", "--map-root-user", "true"]);
986
+ if (!ok(nested)) {
987
+ return (`bwrap cannot nest a user namespace, which the sandbox's seccomp helper needs (${probeEcho(nested)})` +
988
+ usernsHint("user namespace"));
908
989
  }
990
+ return undefined;
909
991
  }
910
992
  /**
911
993
  * Probe whether the SDK sandbox can start on this host. Returns a reason
@@ -54,4 +54,4 @@ export type FileConfig = Record<string, string | string[] | Record<string, unkno
54
54
  */
55
55
  export declare function loadConfigFile(cwd?: string): FileConfig;
56
56
  /** Starter config written by `sweny new`. */
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";
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# log-file: ./logs/errors.json # required when observability-provider is \"file\"\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\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#\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";
@@ -221,13 +221,8 @@ export const STARTER_CONFIG = `# .sweny.yml — SWEny project configuration
221
221
  # pr-labels: agent,triage,needs-review
222
222
 
223
223
  # ── Paths ─────────────────────────────────────────────────────────────
224
- # service-map-path: .github/service-map.yml
225
224
  # log-file: ./logs/errors.json # required when observability-provider is "file"
226
225
 
227
- # ── Cache ─────────────────────────────────────────────────────────────
228
- # cache-dir: .sweny/cache
229
- # cache-ttl: 86400
230
-
231
226
  # ── Run reporting ─────────────────────────────────────────────────────
232
227
  # Opt-in and not currently available: token minting is not yet exposed, so
233
228
  # leaving this unset means no reporting request is ever made.
@@ -256,7 +251,6 @@ export const STARTER_CONFIG = `# .sweny.yml — SWEny project configuration
256
251
  # issue-tracker-provider: file
257
252
  # source-control-provider: file
258
253
  # notification-provider: file
259
- # output-dir: .sweny/output
260
254
 
261
255
  # ── Credentials (.env) ───────────────────────────────────────────────
262
256
  # Copy the relevant block into your .env file and fill in the values.
@@ -279,8 +273,6 @@ export const STARTER_CONFIG = `# .sweny.yml — SWEny project configuration
279
273
  #
280
274
  # Linear (issue tracker) — https://linear.app/settings/api
281
275
  # LINEAR_API_KEY=lin_api_...
282
- # LINEAR_TEAM_ID=... # Settings > Workspace > Teams > [team] > copy ID from URL
283
- # LINEAR_BUG_LABEL_ID=... # Settings > Labels > [label] > copy ID from URL
284
276
  #
285
277
  # Jira (issue tracker) — https://your-org.atlassian.net
286
278
  # JIRA_BASE_URL=https://your-org.atlassian.net
@@ -18,12 +18,6 @@ export interface CliConfig {
18
18
  observabilityCredentials: Record<string, Record<string, string>>;
19
19
  issueTrackerProvider: string;
20
20
  linearApiKey: string;
21
- linearTeamId: string;
22
- linearBugLabelId: string;
23
- linearTriageLabelId: string;
24
- linearStateBacklog: string;
25
- linearStateInProgress: string;
26
- linearStatePeerReview: string;
27
21
  timeRange: string;
28
22
  severityFocus: string;
29
23
  serviceFilter: string;
@@ -38,7 +32,6 @@ export interface CliConfig {
38
32
  noveltyMode: boolean;
39
33
  issueOverride: string;
40
34
  additionalInstructions: string;
41
- serviceMapPath: string;
42
35
  githubToken: string;
43
36
  botToken: string;
44
37
  cloudToken: string;
@@ -48,7 +41,6 @@ export interface CliConfig {
48
41
  jiraApiToken: string;
49
42
  gitlabToken: string;
50
43
  gitlabProjectId: string;
51
- gitlabBaseUrl: string;
52
44
  notificationProvider: string;
53
45
  notificationWebhookUrl: string;
54
46
  sendgridApiKey: string;
@@ -66,10 +58,6 @@ export interface CliConfig {
66
58
  */
67
59
  verbose: boolean;
68
60
  bell: boolean;
69
- cacheDir: string;
70
- cacheTtl: number;
71
- noCache: boolean;
72
- outputDir: string;
73
61
  mcpServers: Record<string, McpServerConfig>;
74
62
  workspaceTools: string[];
75
63
  rules: string[];
@@ -118,17 +106,10 @@ export declare function normalizeSwenyAuth(raw: unknown): "auto" | "api-key" | "
118
106
  * `0` and breaking downstream invariants.
119
107
  *
120
108
  * Why zero is rejected: every current caller (max-investigate-turns,
121
- * max-implement-turns, cache-ttl) uses the result as a positive count or
122
- * timeout. There is no "zero means disabled" semantics anywhere — a
123
- * cache-ttl of 0 wouldn't disable cache, it would expire entries
124
- * immediately, which is broken. If a future flag wants "0 = disabled",
125
- * use a sibling helper (parseNonNegativeInt) rather than weakening this
126
- * one's contract.
109
+ * max-implement-turns) uses the result as a positive turn count. There is
110
+ * no "zero means disabled" semantics anywhere. If a future flag wants
111
+ * "0 = disabled", use a sibling helper (parseNonNegativeInt) rather than
112
+ * weakening this one's contract.
127
113
  */
128
114
  export declare function parsePositiveInt(raw: unknown, fallback: number): number;
129
- /**
130
- * Returns non-fatal warnings about the configuration.
131
- * These are surfaced before the spinner starts but do not abort the run.
132
- */
133
- export declare function validateWarnings(config: Pick<CliConfig, "serviceMapPath">): string[];
134
115
  export declare function registerImplementCommand(program: Command): Command;
@@ -1,5 +1,4 @@
1
1
  import { execSync } from "node:child_process";
2
- import * as fs from "node:fs";
3
2
  import { hasCodexLogin } from "../agent-env.js";
4
3
  import { parseAcpAgent, unsupportedAgentError } from "../harness/agents.js";
5
4
  export function registerTriageCommand(program) {
@@ -18,8 +17,8 @@ export function registerTriageCommand(program) {
18
17
  .option("--severity-focus <focus>", "Severity level focus (default: errors)")
19
18
  .option("--service-filter <filter>", "Service filter pattern (default: *)")
20
19
  .option("--investigation-depth <depth>", "Investigation depth (default: standard)")
21
- .option("--max-investigate-turns <n>", "Max Claude turns for investigation (default: 50)")
22
- .option("--max-implement-turns <n>", "Max Claude turns for implementation (default: 30)")
20
+ .option("--max-investigate-turns <n>", "Max coding agent turns for investigation (default: 50)")
21
+ .option("--max-implement-turns <n>", "Max coding agent turns for implementation (default: 30)")
23
22
  .option("--base-branch <branch>", "Base branch for PRs (default: main)")
24
23
  .option("--pr-labels <labels>", "Comma-separated PR labels (default: agent,triage,needs-review)")
25
24
  .option("--dry-run", "Analyze only, do not create issues or PRs", false)
@@ -27,16 +26,9 @@ export function registerTriageCommand(program) {
27
26
  .option("--review-mode <mode>", "PR merge behavior: auto (merge when CI passes) | review (human approval, default)", "review")
28
27
  .option("--no-novelty-mode", "Disable novelty mode (allow +1 on existing issues)")
29
28
  .option("--issue-override <issue>", "Work on a specific existing issue")
30
- .option("--additional-instructions <text>", "Extra instructions for the Claude agent")
31
- .option("--service-map-path <path>", "Path to service map YAML (default: .github/service-map.yml)")
29
+ .option("--additional-instructions <text>", "Extra instructions for the coding agent")
32
30
  .option("--repository <owner/repo>", "Repository (auto-detected from git remote)")
33
- .option("--linear-team-id <id>", "Linear team ID")
34
- .option("--linear-bug-label-id <id>", "Linear bug label ID")
35
- .option("--linear-triage-label-id <id>", "Linear triage label ID")
36
31
  .option("--issue-labels <labels>", "Comma-separated extra labels applied to every agent-created issue (e.g. agent UUID for Linear, 'agent' for GitHub Issues)")
37
- .option("--linear-state-backlog <name>", "Linear backlog state name")
38
- .option("--linear-state-in-progress <name>", "Linear in-progress state name")
39
- .option("--linear-state-peer-review <name>", "Linear peer-review state name")
40
32
  .option("--log-file <path>", "Path to JSON log file (use with --observability-provider file)")
41
33
  .option("--dd-site <site>", "Datadog site (default: datadoghq.com)")
42
34
  .option("--sentry-org <org>", "Sentry organization slug")
@@ -56,15 +48,10 @@ export function registerTriageCommand(program) {
56
48
  .option("--axiom-org-id <id>", "Axiom org ID (required for multi-org Axiom accounts)")
57
49
  .option("--betterstack-source-id <id>", "Better Stack log source ID (used to auto-discover ClickHouse table)")
58
50
  .option("--betterstack-table-name <name>", 'Better Stack ClickHouse table name, e.g. "t273774.my_source"')
59
- .option("--gitlab-base-url <url>", "GitLab base URL (default: https://gitlab.com)")
60
51
  .option("--json", "Output results as JSON", false)
61
52
  .option("--stream", "Stream NDJSON events to stdout (for Studio / automation)", false)
62
53
  .option("--verbose", "Print each tool call's input and output inline (human-readable, truncated). Use --stream for full untruncated NDJSON.", false)
63
54
  .option("--bell", "Ring terminal bell on completion", false)
64
- .option("--cache-dir <path>", "Step cache directory (default: .sweny/cache)")
65
- .option("--cache-ttl <seconds>", "Cache TTL in seconds, 1 to 31536000 (default: 86400)")
66
- .option("--no-cache", "Disable step cache")
67
- .option("--output-dir <path>", "Output directory for file providers (default: .sweny/output)")
68
55
  .option("--workspace-tools <tools>", "Comma-separated workspace tool integrations to enable (slack, notion, pagerduty, monday)")
69
56
  .option("--offline", "Skip URL Sources and fail fast if any are referenced", false)
70
57
  .option("--file-root [dir]", "Sandbox file: Sources to this directory tree (defaults to the working directory when the flag is given without a value)");
@@ -106,7 +93,7 @@ export function parseCliInputs(options, fileConfig = {}) {
106
93
  anthropicApiKey: env.ANTHROPIC_API_KEY || "",
107
94
  anthropicAuthToken: env.ANTHROPIC_AUTH_TOKEN || "",
108
95
  claudeOauthToken: env.CLAUDE_CODE_OAUTH_TOKEN || "",
109
- // Non-secret config: env or .sweny config file (mirrors gitlabBaseUrl)
96
+ // Non-secret config: env or .sweny config file
110
97
  anthropicBaseUrl: env.ANTHROPIC_BASE_URL || f("anthropic-base-url") || "",
111
98
  swenyAuth: normalizeSwenyAuth(env.SWENY_AUTH || f("sweny-auth")),
112
99
  openaiApiKey: env.OPENAI_API_KEY || "",
@@ -116,12 +103,6 @@ export function parseCliInputs(options, fileConfig = {}) {
116
103
  observabilityCredentials: obsCreds,
117
104
  issueTrackerProvider: options.issueTrackerProvider || f("issue-tracker-provider") || "github-issues",
118
105
  linearApiKey: env.LINEAR_API_KEY || "",
119
- linearTeamId: options.linearTeamId || env.LINEAR_TEAM_ID || f("linear-team-id") || "",
120
- linearBugLabelId: options.linearBugLabelId || env.LINEAR_BUG_LABEL_ID || f("linear-bug-label-id") || "",
121
- linearTriageLabelId: options.linearTriageLabelId || env.LINEAR_TRIAGE_LABEL_ID || f("linear-triage-label-id") || "",
122
- linearStateBacklog: options.linearStateBacklog || env.LINEAR_STATE_BACKLOG || f("linear-state-backlog") || "",
123
- linearStateInProgress: options.linearStateInProgress || env.LINEAR_STATE_IN_PROGRESS || f("linear-state-in-progress") || "",
124
- linearStatePeerReview: options.linearStatePeerReview || env.LINEAR_STATE_PEER_REVIEW || f("linear-state-peer-review") || "",
125
106
  timeRange: options.timeRange || f("time-range") || "24h",
126
107
  severityFocus: options.severityFocus || f("severity-focus") || "errors",
127
108
  serviceFilter: options.serviceFilter || f("service-filter") || "*",
@@ -142,7 +123,6 @@ export function parseCliInputs(options, fileConfig = {}) {
142
123
  noveltyMode: options.noveltyMode !== false,
143
124
  issueOverride: options.issueOverride || "",
144
125
  additionalInstructions: options.additionalInstructions || "",
145
- serviceMapPath: options.serviceMapPath || f("service-map-path") || ".github/service-map.yml",
146
126
  githubToken: env.GITHUB_TOKEN || "",
147
127
  botToken: env.BOT_TOKEN || "",
148
128
  cloudToken: (() => {
@@ -160,7 +140,6 @@ export function parseCliInputs(options, fileConfig = {}) {
160
140
  jiraApiToken: env.JIRA_API_TOKEN || "",
161
141
  gitlabToken: env.GITLAB_TOKEN || "",
162
142
  gitlabProjectId: env.GITLAB_PROJECT_ID || "",
163
- gitlabBaseUrl: options.gitlabBaseUrl || env.GITLAB_BASE_URL || f("gitlab-base-url") || "https://gitlab.com",
164
143
  notificationProvider: options.notificationProvider || f("notification-provider") || "console",
165
144
  // Secrets: env only
166
145
  notificationWebhookUrl: env.NOTIFICATION_WEBHOOK_URL || "",
@@ -175,10 +154,6 @@ export function parseCliInputs(options, fileConfig = {}) {
175
154
  stream: Boolean(options.stream),
176
155
  verbose: Boolean(options.verbose),
177
156
  bell: Boolean(options.bell),
178
- cacheDir: options.cacheDir || env.SWENY_CACHE_DIR || f("cache-dir") || ".sweny/cache",
179
- cacheTtl: parsePositiveInt(options.cacheTtl ?? f("cache-ttl"), 86400),
180
- noCache: options.cache === false,
181
- outputDir: options.outputDir || env.SWENY_OUTPUT_DIR || f("output-dir") || ".sweny/output",
182
157
  mcpServers: parseMcpServers(env.SWENY_MCP_SERVERS || f("mcp-servers-json") || ""),
183
158
  workspaceTools: (options.workspaceTools || env.SWENY_WORKSPACE_TOOLS || f("workspace-tools") || "")
184
159
  .split(",")
@@ -379,8 +354,6 @@ export function validateInputs(config) {
379
354
  case "linear":
380
355
  if (!config.linearApiKey)
381
356
  errors.push("Missing: LINEAR_API_KEY - find API keys at https://linear.app/settings/api");
382
- if (!config.linearTeamId)
383
- errors.push("Missing: LINEAR_TEAM_ID - find it in Linear > Settings > Workspace > Teams > [your team] > copy the ID from the URL");
384
357
  break;
385
358
  case "jira":
386
359
  if (!config.jiraBaseUrl)
@@ -459,9 +432,6 @@ export function validateInputs(config) {
459
432
  // bounds check let junk through).
460
433
  validateIntegerBound(errors, "--max-investigate-turns", config.maxInvestigateTurns, 1, 500);
461
434
  validateIntegerBound(errors, "--max-implement-turns", config.maxImplementTurns, 1, 500);
462
- // cacheTtl is parsed with parsePositiveInt (junk → NaN); bound it so a
463
- // malformed --cache-ttl / cache-ttl: surfaces a field error instead of NaN.
464
- validateIntegerBound(errors, "--cache-ttl", config.cacheTtl, 1, 31_536_000);
465
435
  return errors;
466
436
  }
467
437
  function validateIntegerBound(errors, flag, value, min, max) {
@@ -498,12 +468,10 @@ export function normalizeSwenyAuth(raw) {
498
468
  * `0` and breaking downstream invariants.
499
469
  *
500
470
  * Why zero is rejected: every current caller (max-investigate-turns,
501
- * max-implement-turns, cache-ttl) uses the result as a positive count or
502
- * timeout. There is no "zero means disabled" semantics anywhere — a
503
- * cache-ttl of 0 wouldn't disable cache, it would expire entries
504
- * immediately, which is broken. If a future flag wants "0 = disabled",
505
- * use a sibling helper (parseNonNegativeInt) rather than weakening this
506
- * one's contract.
471
+ * max-implement-turns) uses the result as a positive turn count. There is
472
+ * no "zero means disabled" semantics anywhere. If a future flag wants
473
+ * "0 = disabled", use a sibling helper (parseNonNegativeInt) rather than
474
+ * weakening this one's contract.
507
475
  */
508
476
  export function parsePositiveInt(raw, fallback) {
509
477
  if (raw === undefined || raw === null || raw === "")
@@ -524,22 +492,6 @@ export function parsePositiveInt(raw, fallback) {
524
492
  const truncated = Math.trunc(parsed);
525
493
  return truncated >= 1 ? truncated : Number.NaN;
526
494
  }
527
- const DEFAULT_SERVICE_MAP_PATH = ".github/service-map.yml";
528
- /**
529
- * Returns non-fatal warnings about the configuration.
530
- * These are surfaced before the spinner starts but do not abort the run.
531
- */
532
- export function validateWarnings(config) {
533
- const warnings = [];
534
- // Warn when an explicitly configured serviceMapPath doesn't exist on disk.
535
- // We skip the default path to avoid noise for users who never set it up.
536
- if (config.serviceMapPath && config.serviceMapPath !== DEFAULT_SERVICE_MAP_PATH) {
537
- if (!fs.existsSync(config.serviceMapPath)) {
538
- warnings.push(`Service map file not found: "${config.serviceMapPath}". Cross-repo routing will be disabled.`);
539
- }
540
- }
541
- return warnings;
542
- }
543
495
  function parseObservabilityCredentials(provider, options, fileConfig = {}) {
544
496
  const env = process.env;
545
497
  const f = (key) => {
@@ -675,10 +627,6 @@ export function registerImplementCommand(program) {
675
627
  .option("--max-implement-turns <n>", "Max coding agent turns (default: 40)")
676
628
  .option("--base-branch <branch>", "Base branch for PRs (default: main)")
677
629
  .option("--repository <owner/repo>", "Repository (auto-detected from git remote)")
678
- .option("--linear-team-id <id>", "Linear team ID")
679
- .option("--linear-state-in-progress <name>", "Linear in-progress state name")
680
- .option("--linear-state-peer-review <name>", "Linear peer-review state name")
681
- .option("--output-dir <path>", "Output directory for file providers (default: .sweny/output)")
682
630
  .option("--workspace-tools <tools>", "Comma-separated workspace tool integrations to enable (slack, notion, pagerduty, monday)")
683
631
  .option("--review-mode <mode>", "PR merge behavior: auto (GitHub auto-merge when CI passes) | review (human approval, default)", "review")
684
632
  .option("--additional-instructions <text>", "Extra instructions for the coding agent")
package/dist/cli/main.js CHANGED
@@ -47,7 +47,7 @@ import { writeReceipt } from "./ticket.js";
47
47
  import { createNodeProgress, toolCallsText } from "./progress.js";
48
48
  import { applyNoColor, createPaint } from "./style.js";
49
49
  import { colorEnabled, glyphsFor, richOutput, spinnerFramesFor, supportsUnicode } from "./terminal.js";
50
- import { registerTriageCommand, registerImplementCommand, parseCliInputs, validateInputs, validateWarnings, parsePositiveInt, } from "./config.js";
50
+ import { registerTriageCommand, registerImplementCommand, parseCliInputs, validateInputs, parsePositiveInt, } from "./config.js";
51
51
  import { c, formatBanner, getStepDetails, formatStepLine, formatDagResultHuman, formatDagResultMarkdown, writeResultJson, formatValidationErrors, formatCrashError, formatCheckResults, } from "./output.js";
52
52
  import { checkProviderConnectivity, detectClaudeCodeLogin, discoverWorkflowSkillIds, validateCheckInputs, } from "./check.js";
53
53
  import { registerSetupCommand } from "./setup.js";
@@ -262,10 +262,6 @@ triageCmd.action(async (options) => {
262
262
  console.error(c.subtle("\n Run sweny triage --help for usage information.\n"));
263
263
  process.exit(1);
264
264
  }
265
- // Non-fatal warnings (e.g. missing service map file)
266
- for (const warning of validateWarnings(config)) {
267
- console.warn(chalk.yellow(` \u26A0 ${warning}`));
268
- }
269
265
  // Banner
270
266
  if (!config.json) {
271
267
  console.log(formatBanner(config, version));
@@ -572,10 +568,6 @@ implementCmd.action(async (issueId, options) => {
572
568
  maxImplementTurns: parsePositiveInt(options.maxImplementTurns ?? fileConfig["max-implement-turns"], 40),
573
569
  baseBranch: options.baseBranch || fileConfig["base-branch"] || "main",
574
570
  repository: options.repository || process.env.GITHUB_REPOSITORY || "",
575
- outputDir: options.outputDir ||
576
- process.env.SWENY_OUTPUT_DIR ||
577
- fileConfig["output-dir"] ||
578
- ".sweny/output",
579
571
  };
580
572
  // Validate (parity with triage): surface the curated "Missing: ..." errors
581
573
  // and reject malformed integer flags before any execution.
package/dist/cli/setup.js CHANGED
@@ -165,24 +165,17 @@ export async function setupLinear(apiKey, teamId) {
165
165
  const bugLabel = byName.get("bug") ?? (await ensure(BUG_LABEL));
166
166
  const bugId = bugLabel.id;
167
167
  // ── Print config snippet ─────────────────────────────────────────────────
168
- const triageId = workTypeIds["triage"] ?? byName.get("triage")?.id ?? "";
169
168
  console.log(`
170
169
  ${chalk.bold(" Done.")} ${created.length} created, ${skipped.length} already existed.
171
170
 
172
171
  ${chalk.bold(" Add these to your .sweny.yml:")}
173
172
 
174
173
  ${chalk.cyan(` issue-tracker-provider: linear
175
- linear-team-id: ${teamId}
176
- linear-triage-label-id: ${triageId}
177
- linear-bug-label-id: ${bugId}
178
174
  issue-labels: ${agentLabel.id}`)}
179
175
 
180
176
  ${chalk.bold(" Or as environment variables / GitHub Actions secrets:")}
181
177
 
182
- ${chalk.cyan(` LINEAR_TEAM_ID=${teamId}
183
- LINEAR_TRIAGE_LABEL_ID=${triageId}
184
- LINEAR_BUG_LABEL_ID=${bugId}
185
- SWENY_ISSUE_LABELS=${agentLabel.id}`)}
178
+ ${chalk.cyan(` SWENY_ISSUE_LABELS=${agentLabel.id}`)}
186
179
 
187
180
  ${chalk.dim(" Full label inventory:")}
188
181
  ${chalk.dim(` agent ${agentLabel.id}`)}
@@ -17,6 +17,7 @@ export function isSupportedAgent(id) {
17
17
  }
18
18
  /** The error for an agent id with no adapter. */
19
19
  export function unsupportedAgentError(id) {
20
- return (`Unsupported coding agent "${id}": supported agents are "claude" (headless Claude Code) ` +
21
- `and "codex" (Codex CLI), or "pi" (pi coding agent, experimental). Remove --agent / coding-agent-provider or set it to one of them.`);
20
+ return (`Unsupported coding agent "${id}": supported agents are "claude" (headless Claude Code), ` +
21
+ `"codex" (Codex CLI), "pi" (pi coding agent, experimental), or "acp:<command>" (any Agent Client Protocol agent, ` +
22
+ `experimental, for example "acp:opencode acp"). Remove --agent / coding-agent-provider or set it to one of them.`);
22
23
  }
@@ -35,7 +35,9 @@
35
35
  * socat for step 1: install it with `npm i -g @anthropic-ai/sandbox-runtime`, or
36
36
  * point `SWENY_SRT_PATH` at the binary. Linux also needs bubblewrap, socat and
37
37
  * ripgrep, and unprivileged user namespaces (Ubuntu 23.10+:
38
- * `sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0`).
38
+ * `sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0`; Ubuntu 26.04
39
+ * also `sudo apparmor_parser -R /etc/apparmor.d/bwrap-userns-restrict`, see
40
+ * {@link usernsHint}).
39
41
  *
40
42
  * Modes are the step 1 modes, unchanged (`SWENY_SANDBOX`, default `auto` in CI
41
43
  * and `off` locally): `off` never wraps; `auto` wraps when a wrapper is
@@ -35,7 +35,9 @@
35
35
  * socat for step 1: install it with `npm i -g @anthropic-ai/sandbox-runtime`, or
36
36
  * point `SWENY_SRT_PATH` at the binary. Linux also needs bubblewrap, socat and
37
37
  * ripgrep, and unprivileged user namespaces (Ubuntu 23.10+:
38
- * `sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0`).
38
+ * `sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0`; Ubuntu 26.04
39
+ * also `sudo apparmor_parser -R /etc/apparmor.d/bwrap-userns-restrict`, see
40
+ * {@link usernsHint}).
39
41
  *
40
42
  * Modes are the step 1 modes, unchanged (`SWENY_SANDBOX`, default `auto` in CI
41
43
  * and `off` locally): `off` never wraps; `auto` wraps when a wrapper is
@@ -47,7 +49,7 @@ import { existsSync, realpathSync } from "node:fs";
47
49
  import { lstat, mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
48
50
  import { homedir, tmpdir } from "node:os";
49
51
  import * as path from "node:path";
50
- import { DEFAULT_SANDBOX_DOMAINS, parseList, resolveSandboxMode } from "../agent-env.js";
52
+ import { DEFAULT_SANDBOX_DOMAINS, parseList, probeLine, resolveSandboxMode, usernsHint, } from "../agent-env.js";
51
53
  import { runStateRoot } from "../journal.js";
52
54
  import { policyGate } from "./policy.js";
53
55
  // ─── srt settings ────────────────────────────────────────────────
@@ -256,14 +258,8 @@ async function srtWorks(srtPath, env) {
256
258
  execFile(srtPath, ["--settings", settings, "--", "true"], { env: { PATH: env.PATH ?? "", HOME: dir }, timeout: 30_000 }, (err, _stdout, stderr) => {
257
259
  if (!err)
258
260
  return resolve(undefined);
259
- const first = String(stderr || err.message)
260
- .trim()
261
- .split("\n")[0] || "unknown error";
262
- const userns = /user namespace|uid map|setting up uid|Operation not permitted|Permission denied/i.test(String(stderr));
263
- resolve(`srt cannot sandbox on this host (${first})` +
264
- (userns
265
- ? "; unprivileged user namespaces look restricted (Ubuntu 23.10+: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0)"
266
- : ""));
261
+ const first = probeLine(String(stderr || err.message));
262
+ resolve(`srt cannot sandbox on this host (${first})${usernsHint(String(stderr))}`);
267
263
  });
268
264
  });
269
265
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.181",
3
+ "version": "0.1.182",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"