@sweny-ai/core 0.1.177 → 0.1.179
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 +5 -3
- package/dist/agent-env.js +12 -7
- package/dist/cli/config-file.d.ts +22 -1
- package/dist/cli/config-file.js +42 -0
- package/dist/cli/final-output.js +4 -1
- package/dist/cli/main.d.ts +2 -1
- package/dist/cli/main.js +7 -8
- package/dist/cli/resume.d.ts +10 -1
- package/dist/cli/resume.js +77 -7
- package/dist/cli/run-history.d.ts +3 -7
- package/dist/cli/run-history.js +1 -10
- package/dist/cli/run-output.d.ts +16 -10
- package/dist/cli/run-output.js +19 -13
- package/dist/cli/ticket.js +3 -2
- package/dist/decider.d.ts +176 -64
- package/dist/decider.js +475 -155
- package/dist/executor.d.ts +50 -7
- package/dist/executor.js +533 -93
- package/dist/harness/acp.js +2 -0
- package/dist/harness/claude-code.d.ts +18 -0
- package/dist/harness/claude-code.js +51 -6
- package/dist/harness/codex.js +2 -0
- package/dist/harness/pi.js +2 -0
- package/dist/harness/sandbox-wrapper.js +8 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/journal.d.ts +258 -74
- package/dist/journal.js +749 -208
- package/dist/schema.d.ts +189 -241
- package/dist/schema.js +56 -36
- package/dist/skills/github.js +39 -0
- package/dist/types.d.ts +55 -3
- package/dist/types.js +22 -0
- package/dist/when.d.ts +8 -1
- package/dist/when.js +8 -1
- package/dist/workflow-builder.js +3 -1
- package/dist/workflows/browser.js +2 -2
- package/dist/workflows/implement.yml +37 -5
- package/dist/workflows/triage.yml +40 -6
- package/package.json +1 -1
package/dist/agent-env.d.ts
CHANGED
|
@@ -373,9 +373,11 @@ export declare function checkSandboxSupport(platform?: NodeJS.Platform, pathEnv?
|
|
|
373
373
|
/**
|
|
374
374
|
* The SDK sandbox settings used when the sandbox is on.
|
|
375
375
|
*
|
|
376
|
-
* - `failIfUnavailable`: true
|
|
377
|
-
*
|
|
378
|
-
*
|
|
376
|
+
* - `failIfUnavailable`: true whenever settings are built, `auto` included:
|
|
377
|
+
* the preflight already passed, so a sandbox that then fails to start
|
|
378
|
+
* fails the node instead of silently running the agent unsandboxed (which
|
|
379
|
+
* the run would otherwise report as contained). `auto` on a host whose
|
|
380
|
+
* preflight fails builds no settings and runs unsandboxed, reported as such.
|
|
379
381
|
* - `allowUnsandboxedCommands: false`: the model cannot escape via the
|
|
380
382
|
* `dangerouslyDisableSandbox` parameter. Required, because nodes run under
|
|
381
383
|
* `bypassPermissions` (#365) which would otherwise auto-approve the escape.
|
package/dist/agent-env.js
CHANGED
|
@@ -29,7 +29,7 @@ import { execFileSync } from "node:child_process";
|
|
|
29
29
|
import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
30
30
|
import { homedir, tmpdir } from "node:os";
|
|
31
31
|
import * as path from "node:path";
|
|
32
|
-
import {
|
|
32
|
+
import { runStateRoot } from "./journal.js";
|
|
33
33
|
// ─── Scoped env ──────────────────────────────────────────────────
|
|
34
34
|
/**
|
|
35
35
|
* Exact variable names always passed to the agent subprocess.
|
|
@@ -938,9 +938,11 @@ function defaultProbe() {
|
|
|
938
938
|
/**
|
|
939
939
|
* The SDK sandbox settings used when the sandbox is on.
|
|
940
940
|
*
|
|
941
|
-
* - `failIfUnavailable`: true
|
|
942
|
-
*
|
|
943
|
-
*
|
|
941
|
+
* - `failIfUnavailable`: true whenever settings are built, `auto` included:
|
|
942
|
+
* the preflight already passed, so a sandbox that then fails to start
|
|
943
|
+
* fails the node instead of silently running the agent unsandboxed (which
|
|
944
|
+
* the run would otherwise report as contained). `auto` on a host whose
|
|
945
|
+
* preflight fails builds no settings and runs unsandboxed, reported as such.
|
|
944
946
|
* - `allowUnsandboxedCommands: false`: the model cannot escape via the
|
|
945
947
|
* `dangerouslyDisableSandbox` parameter. Required, because nodes run under
|
|
946
948
|
* `bypassPermissions` (#365) which would otherwise auto-approve the escape.
|
|
@@ -963,10 +965,12 @@ export function buildSandboxSettings(domains, opts = {}) {
|
|
|
963
965
|
envVars: AGENT_AUTH_VARS.map((name) => ({ name, mode: "deny" })),
|
|
964
966
|
files: [
|
|
965
967
|
{ path: path.join(opts.home ?? homedir(), ".claude", ".credentials.json"), mode: "deny" },
|
|
966
|
-
// Run
|
|
967
|
-
{ path:
|
|
968
|
+
// Run journals and their keys: an agent that could read a key could forge records.
|
|
969
|
+
{ path: runStateRoot(), mode: "deny" },
|
|
968
970
|
],
|
|
969
971
|
},
|
|
972
|
+
// Nor may it write there: cut a journal, delete a lock, or plant a run.
|
|
973
|
+
filesystem: { denyRead: [runStateRoot()], denyWrite: [runStateRoot()] },
|
|
970
974
|
};
|
|
971
975
|
}
|
|
972
976
|
/**
|
|
@@ -1003,7 +1007,8 @@ export function resolveAgentSandbox(opts) {
|
|
|
1003
1007
|
return {
|
|
1004
1008
|
mode,
|
|
1005
1009
|
settings: buildSandboxSettings([...DEFAULT_SANDBOX_DOMAINS, ...(opts.nodeDomains ?? []), ...extra], {
|
|
1006
|
-
|
|
1010
|
+
// Preflight passed: from here the sandbox is enforced or the node fails.
|
|
1011
|
+
failIfUnavailable: true,
|
|
1007
1012
|
}),
|
|
1008
1013
|
};
|
|
1009
1014
|
}
|
|
@@ -24,6 +24,27 @@ export declare function loadDotenv(cwd?: string): void;
|
|
|
24
24
|
* treats as on/off.
|
|
25
25
|
*/
|
|
26
26
|
export declare function applyAgentFileConfig(fileConfig: FileConfig, env?: NodeJS.ProcessEnv): void;
|
|
27
|
+
/**
|
|
28
|
+
* Operator config for the decision model (#357), one trust domain per
|
|
29
|
+
* credential. `.sweny.yml` is repo content a pull request can change, so:
|
|
30
|
+
*
|
|
31
|
+
* - SWENY_DECIDER_URL (operator env) may point anywhere public; only then is
|
|
32
|
+
* SWENY_DECIDER_API_KEY sent, and only SWENY_DECIDER_ALLOW_PRIVATE=true
|
|
33
|
+
* (env) admits loopback or private addresses.
|
|
34
|
+
* - A `.sweny.yml` `decider: { url, model }` URL must be loopback (local
|
|
35
|
+
* Ollama) and never gets a key.
|
|
36
|
+
*
|
|
37
|
+
* `trusted(key)` reads an operator env value; it must not see values a
|
|
38
|
+
* committed `.env` supplied. `noDecider` (`--no-decider`) returns false.
|
|
39
|
+
* Undefined when no URL and model are configured.
|
|
40
|
+
*/
|
|
41
|
+
export declare function operatorDeciderConfig(fileConfig: FileConfig, env: NodeJS.ProcessEnv, noDecider: boolean, trusted?: (key: string) => string | undefined): {
|
|
42
|
+
url: string;
|
|
43
|
+
model: string;
|
|
44
|
+
apiKey?: string;
|
|
45
|
+
allowPrivate?: boolean;
|
|
46
|
+
loopbackOnly?: boolean;
|
|
47
|
+
} | false | undefined;
|
|
27
48
|
/** Parsed config file — flat strings for scalar fields, arrays for list fields, objects for nested blocks. */
|
|
28
49
|
export type FileConfig = Record<string, string | string[] | Record<string, unknown>>;
|
|
29
50
|
/**
|
|
@@ -33,4 +54,4 @@ export type FileConfig = Record<string, string | string[] | Record<string, unkno
|
|
|
33
54
|
*/
|
|
34
55
|
export declare function loadConfigFile(cwd?: string): FileConfig;
|
|
35
56
|
/** Starter config written by `sweny new`. */
|
|
36
|
-
export declare const STARTER_CONFIG = "# .sweny.yml \u2014 SWEny project configuration\n# Commit this file. Secrets (API keys, tokens) go in .env (gitignored).\n#\n# Every key matches a CLI flag: \"time-range: 4h\" is the same as \"--time-range 4h\".\n# CLI flags override this file; env vars override this file; this file overrides defaults.\n\n# \u2500\u2500 Providers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# observability-provider: datadog # datadog | sentry | cloudwatch | splunk | elastic | newrelic | loki | prometheus | pagerduty | heroku | opsgenie | vercel | supabase | netlify | fly | render | file\n# issue-tracker-provider: github-issues # github-issues | linear | jira\n# source-control-provider: github # github | gitlab\n# coding-agent-provider: claude # claude | codex | pi\n# notification-provider: console # console | slack | teams | discord | email | webhook\n\n# \u2500\u2500 Investigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# time-range: 24h\n# severity-focus: errors\n# service-filter: \"*\"\n# investigation-depth: standard # quick | standard | thorough\n\n# \u2500\u2500 PR / branch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# base-branch: main\n# pr-labels: agent,triage,needs-review\n\n# \u2500\u2500 Paths \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# service-map-path: .github/service-map.yml\n# log-file: ./logs/errors.json # required when observability-provider is \"file\"\n\n# \u2500\u2500 Cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# cache-dir: .sweny/cache\n# cache-ttl: 86400\n\n# \u2500\u2500 Run reporting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Opt-in and not currently available: token minting is not yet exposed, so\n# leaving this unset means no reporting request is ever made.\n# cloud-token: sweny_pk_...\n# Or set SWENY_CLOUD_TOKEN in your environment.\n\n# \u2500\u2500 Agent sandbox \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# See https://docs.sweny.ai/advanced/agent-sandbox/\n# sandbox: off # off (local default) | auto (CI default: sandbox if supported, else warn) | strict\n# sandbox-allowed-domains: [internal.example.com] # extra hosts agent commands may reach\n# env-scope: off # off (local default: full env) | on (CI default: allowlist)\n# env-passthrough: [NPM_TOKEN] # extra env vars the agent may see when scoped (\"*\" = all)\n\n# \u2500\u2500 MCP servers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Extend the coding agent with additional tools via MCP.\n# Value is a JSON object \u2014 each key is a server name you choose.\n# See https://docs.sweny.ai/advanced/mcp-servers/ for a full catalog with copy-paste configs.\n#\n# Example: GitHub MCP server (query PRs, issues, CI run logs)\n# mcp-servers-json: '{\"github\":{\"type\":\"stdio\",\"command\":\"npx\",\"args\":[\"-y\",\"@modelcontextprotocol/server-github@latest\"],\"env\":{\"GITHUB_PERSONAL_ACCESS_TOKEN\":\"ghp_...\"}}}'\n\n# \u2500\u2500 Local-only quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Uncomment to run without any external services (just an LLM API key):\n# observability-provider: file\n# log-file: ./sample-errors.json\n# issue-tracker-provider: file\n# source-control-provider: file\n# notification-provider: file\n# output-dir: .sweny/output\n\n# \u2500\u2500 Credentials (.env) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Copy the relevant block into your .env file and fill in the values.\n#\n# Claude (coding agent) \u2014 https://console.anthropic.com/settings/api-keys\n# ANTHROPIC_API_KEY=sk-ant-...\n#\n# GitHub (source control + issue tracker)\n# GITHUB_TOKEN=ghp_... # https://github.com/settings/tokens (repo + issues scopes)\n#\n# Datadog (observability) \u2014 https://app.datadoghq.com/organization-settings\n# DD_API_KEY=... # Organization Settings > API Keys\n# DD_APP_KEY=... # Organization Settings > Application Keys\n# DD_SITE=datadoghq.com # or datadoghq.eu, us3.datadoghq.com, etc.\n#\n# Sentry (observability) \u2014 https://sentry.io/settings/auth-tokens/\n# SENTRY_AUTH_TOKEN=sntrys_...\n# SENTRY_ORG=your-org-slug # from sentry.io/organizations/<slug>/\n# SENTRY_PROJECT=your-project # Project Settings > General > Project Slug\n#\n# Linear (issue tracker) \u2014 https://linear.app/settings/api\n# LINEAR_API_KEY=lin_api_...\n# LINEAR_TEAM_ID=... # Settings > Workspace > Teams > [team] > copy ID from URL\n# LINEAR_BUG_LABEL_ID=... # Settings > Labels > [label] > copy ID from URL\n#\n# Jira (issue tracker) \u2014 https://your-org.atlassian.net\n# JIRA_BASE_URL=https://your-org.atlassian.net\n# JIRA_EMAIL=you@company.com # your Atlassian account email\n# JIRA_API_TOKEN=... # https://id.atlassian.com/manage-profile/security/api-tokens\n#\n# Vercel (observability) \u2014 https://vercel.com/account/tokens\n# VERCEL_TOKEN=...\n# VERCEL_PROJECT_ID=prj_... # Project Settings > General > Project ID\n# VERCEL_TEAM_ID=team_... # optional, for team-owned projects\n#\n# Supabase (observability) \u2014 https://supabase.com/dashboard/account/tokens\n# SUPABASE_MANAGEMENT_KEY=...\n# SUPABASE_PROJECT_REF=... # Project Settings > General > Reference ID\n#\n# Netlify (observability) \u2014 https://app.netlify.com/user/applications#personal-access-tokens\n# NETLIFY_TOKEN=...\n# NETLIFY_SITE_ID=... # Site Settings > General > Site ID\n#\n# Fly.io (observability) \u2014 https://fly.io/user/personal_access_tokens\n# FLY_TOKEN=...\n# FLY_APP_NAME=... # the name of your Fly.io application\n#\n# Render (observability) \u2014 https://dashboard.render.com/u/settings\n# RENDER_API_KEY=...\n# RENDER_SERVICE_ID=srv-... # from your service's Settings page\n#\n# Prometheus (observability) \u2014 self-hosted or Grafana Cloud\n# PROMETHEUS_URL=http://prometheus.internal:9090\n# PROMETHEUS_TOKEN=... # optional, for secured instances\n#\n# PagerDuty (observability) \u2014 https://your-account.pagerduty.com/api_keys\n# PAGERDUTY_API_KEY=...\n#\n# Honeycomb (observability) \u2014 https://docs.honeycomb.io/api/\n# HONEYCOMB_API_KEY=...\n# HONEYCOMB_DATASET=... # dataset name (e.g. production)\n#\n# Heroku (observability) \u2014 https://devcenter.heroku.com/articles/platform-api-reference\n# HEROKU_API_KEY=... # https://dashboard.heroku.com/account\n# HEROKU_APP_NAME=... # the name of your Heroku application\n#\n# OpsGenie (observability) \u2014 https://support.atlassian.com/opsgenie/docs/api-key-management/\n# OPSGENIE_API_KEY=...\n# OPSGENIE_REGION=us # or eu for EU-hosted accounts\n#\n# Slack (notifications) \u2014 https://api.slack.com/apps\n# NOTIFICATION_WEBHOOK_URL=https://hooks.slack.com/services/...\n# # or use a bot token: SLACK_BOT_TOKEN=xoxb-...\n";
|
|
57
|
+
export declare const STARTER_CONFIG = "# .sweny.yml \u2014 SWEny project configuration\n# Commit this file. Secrets (API keys, tokens) go in .env (gitignored).\n#\n# Every key matches a CLI flag: \"time-range: 4h\" is the same as \"--time-range 4h\".\n# CLI flags override this file; env vars override this file; this file overrides defaults.\n\n# \u2500\u2500 Providers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# observability-provider: datadog # datadog | sentry | cloudwatch | splunk | elastic | newrelic | loki | prometheus | pagerduty | heroku | opsgenie | vercel | supabase | netlify | fly | render | file\n# issue-tracker-provider: github-issues # github-issues | linear | jira\n# source-control-provider: github # github | gitlab\n# coding-agent-provider: claude # claude | codex | pi\n# notification-provider: console # console | slack | teams | discord | email | webhook\n\n# \u2500\u2500 Investigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# time-range: 24h\n# severity-focus: errors\n# service-filter: \"*\"\n# investigation-depth: standard # quick | standard | thorough\n\n# \u2500\u2500 PR / branch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# base-branch: main\n# pr-labels: agent,triage,needs-review\n\n# \u2500\u2500 Paths \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# service-map-path: .github/service-map.yml\n# log-file: ./logs/errors.json # required when observability-provider is \"file\"\n\n# \u2500\u2500 Cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# cache-dir: .sweny/cache\n# cache-ttl: 86400\n\n# \u2500\u2500 Run reporting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Opt-in and not currently available: token minting is not yet exposed, so\n# leaving this unset means no reporting request is ever made.\n# cloud-token: sweny_pk_...\n# Or set SWENY_CLOUD_TOKEN in your environment.\n\n# \u2500\u2500 Agent sandbox \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# See https://docs.sweny.ai/advanced/agent-sandbox/\n# sandbox: off # off (local default) | auto (CI default: sandbox if supported, else warn) | strict\n# sandbox-allowed-domains: [internal.example.com] # extra hosts agent commands may reach\n# env-scope: off # off (local default: full env) | on (CI default: allowlist)\n# env-passthrough: [NPM_TOKEN] # extra env vars the agent may see when scoped (\"*\" = all)\n\n# \u2500\u2500 MCP servers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Extend the coding agent with additional tools via MCP.\n# Value is a JSON object \u2014 each key is a server name you choose.\n# See https://docs.sweny.ai/advanced/mcp-servers/ for a full catalog with copy-paste configs.\n#\n# Example: GitHub MCP server (query PRs, issues, CI run logs)\n# mcp-servers-json: '{\"github\":{\"type\":\"stdio\",\"command\":\"npx\",\"args\":[\"-y\",\"@modelcontextprotocol/server-github@latest\"],\"env\":{\"GITHUB_PERSONAL_ACCESS_TOKEN\":\"ghp_...\"}}}'\n\n# \u2500\u2500 Local-only quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Uncomment to run without any external services (just an LLM API key):\n# observability-provider: file\n# log-file: ./sample-errors.json\n# issue-tracker-provider: file\n# source-control-provider: file\n# notification-provider: file\n# output-dir: .sweny/output\n\n# \u2500\u2500 Credentials (.env) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Copy the relevant block into your .env file and fill in the values.\n#\n# Claude (coding agent) \u2014 https://console.anthropic.com/settings/api-keys\n# ANTHROPIC_API_KEY=sk-ant-...\n#\n# GitHub (source control + issue tracker)\n# GITHUB_TOKEN=ghp_... # https://github.com/settings/tokens (repo + issues scopes)\n#\n# Datadog (observability) \u2014 https://app.datadoghq.com/organization-settings\n# DD_API_KEY=... # Organization Settings > API Keys\n# DD_APP_KEY=... # Organization Settings > Application Keys\n# DD_SITE=datadoghq.com # or datadoghq.eu, us3.datadoghq.com, etc.\n#\n# Sentry (observability) \u2014 https://sentry.io/settings/auth-tokens/\n# SENTRY_AUTH_TOKEN=sntrys_...\n# SENTRY_ORG=your-org-slug # from sentry.io/organizations/<slug>/\n# SENTRY_PROJECT=your-project # Project Settings > General > Project Slug\n#\n# Linear (issue tracker) \u2014 https://linear.app/settings/api\n# LINEAR_API_KEY=lin_api_...\n# LINEAR_TEAM_ID=... # Settings > Workspace > Teams > [team] > copy ID from URL\n# LINEAR_BUG_LABEL_ID=... # Settings > Labels > [label] > copy ID from URL\n#\n# Jira (issue tracker) \u2014 https://your-org.atlassian.net\n# JIRA_BASE_URL=https://your-org.atlassian.net\n# JIRA_EMAIL=you@company.com # your Atlassian account email\n# JIRA_API_TOKEN=... # https://id.atlassian.com/manage-profile/security/api-tokens\n#\n# Vercel (observability) \u2014 https://vercel.com/account/tokens\n# VERCEL_TOKEN=...\n# VERCEL_PROJECT_ID=prj_... # Project Settings > General > Project ID\n# VERCEL_TEAM_ID=team_... # optional, for team-owned projects\n#\n# Supabase (observability) \u2014 https://supabase.com/dashboard/account/tokens\n# SUPABASE_MANAGEMENT_KEY=...\n# SUPABASE_PROJECT_REF=... # Project Settings > General > Reference ID\n#\n# Netlify (observability) \u2014 https://app.netlify.com/user/applications#personal-access-tokens\n# NETLIFY_TOKEN=...\n# NETLIFY_SITE_ID=... # Site Settings > General > Site ID\n#\n# Fly.io (observability) \u2014 https://fly.io/user/personal_access_tokens\n# FLY_TOKEN=...\n# FLY_APP_NAME=... # the name of your Fly.io application\n#\n# Render (observability) \u2014 https://dashboard.render.com/u/settings\n# RENDER_API_KEY=...\n# RENDER_SERVICE_ID=srv-... # from your service's Settings page\n#\n# Prometheus (observability) \u2014 self-hosted or Grafana Cloud\n# PROMETHEUS_URL=http://prometheus.internal:9090\n# PROMETHEUS_TOKEN=... # optional, for secured instances\n#\n# PagerDuty (observability) \u2014 https://your-account.pagerduty.com/api_keys\n# PAGERDUTY_API_KEY=...\n#\n# Honeycomb (observability) \u2014 https://docs.honeycomb.io/api/\n# HONEYCOMB_API_KEY=...\n# HONEYCOMB_DATASET=... # dataset name (e.g. production)\n#\n# Heroku (observability) \u2014 https://devcenter.heroku.com/articles/platform-api-reference\n# HEROKU_API_KEY=... # https://dashboard.heroku.com/account\n# HEROKU_APP_NAME=... # the name of your Heroku application\n#\n# OpsGenie (observability) \u2014 https://support.atlassian.com/opsgenie/docs/api-key-management/\n# OPSGENIE_API_KEY=...\n# OPSGENIE_REGION=us # or eu for EU-hosted accounts\n#\n# Slack (notifications) \u2014 https://api.slack.com/apps\n# NOTIFICATION_WEBHOOK_URL=https://hooks.slack.com/services/...\n# # or use a bot token: SLACK_BOT_TOKEN=xoxb-...\n#\n# Decision model (optional) for nodes with route_by: decider. https://docs.sweny.ai/advanced/decision-models/\n# Local Ollama: set it here (a URL in this file must be loopback and never gets a key):\n# decider:\n# url: http://localhost:11434\n# model: nimble\n# Remote server: set it in the CI or shell environment only, never in this file or .env:\n# SWENY_DECIDER_URL=https://... SWENY_DECIDER_MODEL=... SWENY_DECIDER_API_KEY=...\n";
|
package/dist/cli/config-file.js
CHANGED
|
@@ -91,6 +91,40 @@ export function applyAgentFileConfig(fileConfig, env = process.env) {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Operator config for the decision model (#357), one trust domain per
|
|
96
|
+
* credential. `.sweny.yml` is repo content a pull request can change, so:
|
|
97
|
+
*
|
|
98
|
+
* - SWENY_DECIDER_URL (operator env) may point anywhere public; only then is
|
|
99
|
+
* SWENY_DECIDER_API_KEY sent, and only SWENY_DECIDER_ALLOW_PRIVATE=true
|
|
100
|
+
* (env) admits loopback or private addresses.
|
|
101
|
+
* - A `.sweny.yml` `decider: { url, model }` URL must be loopback (local
|
|
102
|
+
* Ollama) and never gets a key.
|
|
103
|
+
*
|
|
104
|
+
* `trusted(key)` reads an operator env value; it must not see values a
|
|
105
|
+
* committed `.env` supplied. `noDecider` (`--no-decider`) returns false.
|
|
106
|
+
* Undefined when no URL and model are configured.
|
|
107
|
+
*/
|
|
108
|
+
export function operatorDeciderConfig(fileConfig, env, noDecider, trusted = (key) => env[key]) {
|
|
109
|
+
if (noDecider)
|
|
110
|
+
return false;
|
|
111
|
+
const str = (v) => (typeof v === "string" && v.trim() ? v.trim() : undefined);
|
|
112
|
+
const model = str(trusted("SWENY_DECIDER_MODEL")) ?? str(env.SWENY_DECIDER_MODEL) ?? str(fileConfig["decider.model"]);
|
|
113
|
+
const envUrl = str(trusted("SWENY_DECIDER_URL"));
|
|
114
|
+
if (envUrl) {
|
|
115
|
+
if (!model)
|
|
116
|
+
return undefined;
|
|
117
|
+
const apiKey = str(trusted("SWENY_DECIDER_API_KEY"));
|
|
118
|
+
const allowPrivate = ["1", "true"].includes((str(trusted("SWENY_DECIDER_ALLOW_PRIVATE")) ?? "").toLowerCase());
|
|
119
|
+
return { url: envUrl, model, ...(apiKey ? { apiKey } : {}), ...(allowPrivate ? { allowPrivate } : {}) };
|
|
120
|
+
}
|
|
121
|
+
// Repo content: a URL a committed `.env` put in the environment (untrusted),
|
|
122
|
+
// else the `.sweny.yml` one. Loopback only, never a key.
|
|
123
|
+
const repoUrl = str(env.SWENY_DECIDER_URL) ?? str(fileConfig["decider.url"]);
|
|
124
|
+
if (!repoUrl || !model)
|
|
125
|
+
return undefined;
|
|
126
|
+
return { url: repoUrl, model, loopbackOnly: true };
|
|
127
|
+
}
|
|
94
128
|
/**
|
|
95
129
|
* Search upward from `cwd` for `.sweny.yml` and parse it.
|
|
96
130
|
* Scalar values are strings, list values (rules, context) are string arrays.
|
|
@@ -296,4 +330,12 @@ export const STARTER_CONFIG = `# .sweny.yml — SWEny project configuration
|
|
|
296
330
|
# Slack (notifications) — https://api.slack.com/apps
|
|
297
331
|
# NOTIFICATION_WEBHOOK_URL=https://hooks.slack.com/services/...
|
|
298
332
|
# # or use a bot token: SLACK_BOT_TOKEN=xoxb-...
|
|
333
|
+
#
|
|
334
|
+
# Decision model (optional) for nodes with route_by: decider. https://docs.sweny.ai/advanced/decision-models/
|
|
335
|
+
# Local Ollama: set it here (a URL in this file must be loopback and never gets a key):
|
|
336
|
+
# decider:
|
|
337
|
+
# url: http://localhost:11434
|
|
338
|
+
# model: nimble
|
|
339
|
+
# Remote server: set it in the CI or shell environment only, never in this file or .env:
|
|
340
|
+
# SWENY_DECIDER_URL=https://... SWENY_DECIDER_MODEL=... SWENY_DECIDER_API_KEY=...
|
|
299
341
|
`;
|
package/dist/cli/final-output.js
CHANGED
|
@@ -193,7 +193,10 @@ export function outputRelPath(runId) {
|
|
|
193
193
|
export function writeFinalOutput(runId, markdown, cwd = process.cwd()) {
|
|
194
194
|
try {
|
|
195
195
|
const rel = outputRelPath(runId);
|
|
196
|
-
|
|
196
|
+
const file = path.join(cwd, rel);
|
|
197
|
+
// Node output: keep the run dir out of commits (an agent's `git add -A` included).
|
|
198
|
+
writeFileNoFollow(path.join(path.dirname(file), ".gitignore"), "*\n", { root: cwd, mode: 0o644 });
|
|
199
|
+
writeFileNoFollow(file, markdown, { root: cwd, mode: 0o600 });
|
|
197
200
|
return rel;
|
|
198
201
|
}
|
|
199
202
|
catch {
|
package/dist/cli/main.d.ts
CHANGED
|
@@ -13,7 +13,8 @@ export declare function workflowRunAction(file: string | undefined, options: Rec
|
|
|
13
13
|
maxTokens?: string;
|
|
14
14
|
maxCost?: string;
|
|
15
15
|
yes?: boolean;
|
|
16
|
-
decider
|
|
16
|
+
/** False with `--no-decider`. */
|
|
17
|
+
decider?: boolean;
|
|
17
18
|
},
|
|
18
19
|
/** Set by `sweny workflow resume` (#363): the journal, run id and original input. */
|
|
19
20
|
resume?: ResumeContext): Promise<void>;
|
package/dist/cli/main.js
CHANGED
|
@@ -29,7 +29,7 @@ import { toMermaidBlock } from "../mermaid.js";
|
|
|
29
29
|
import { runWorkflowDiagram } from "./diagram.js";
|
|
30
30
|
import { DagRenderer } from "./renderer.js";
|
|
31
31
|
import * as readline from "node:readline";
|
|
32
|
-
import { loadDotenv, loadConfigFile, applyAgentFileConfig } from "./config-file.js";
|
|
32
|
+
import { loadDotenv, loadConfigFile, applyAgentFileConfig, operatorDeciderConfig } from "./config-file.js";
|
|
33
33
|
import { buildCredentialMap } from "./credentials.js";
|
|
34
34
|
import { nonInteractiveUsage, runNew } from "./new.js";
|
|
35
35
|
import { formatFinalMarkdown, formatFinalOutput, formatSavedOutputLine, resolveFinalOutput, shouldPrintFinalOutput, writeFinalOutput, } from "./final-output.js";
|
|
@@ -753,10 +753,6 @@ export function loadWorkflowFile(filePath, knownSkills) {
|
|
|
753
753
|
export async function workflowRunAction(file, options,
|
|
754
754
|
/** Set by `sweny workflow resume` (#363): the journal, run id and original input. */
|
|
755
755
|
resume) {
|
|
756
|
-
if (options.decider !== undefined && options.decider !== "off" && options.decider !== "shadow") {
|
|
757
|
-
console.error(chalk.red(`\n --decider must be off or shadow, got "${options.decider}"\n`));
|
|
758
|
-
process.exit(1);
|
|
759
|
-
}
|
|
760
756
|
// Reject junk --timeout/--max-steps up front (both paths) instead of
|
|
761
757
|
// silently falling back to a default.
|
|
762
758
|
let budget;
|
|
@@ -1053,7 +1049,9 @@ resume) {
|
|
|
1053
1049
|
fileRoot: config.fileRoot || undefined,
|
|
1054
1050
|
signal,
|
|
1055
1051
|
max_steps: wfMaxSteps,
|
|
1056
|
-
|
|
1052
|
+
// One trust domain per credential (#357): a URL, key or allowance a
|
|
1053
|
+
// committed .env introduced is repo content, read as untrusted.
|
|
1054
|
+
decider: operatorDeciderConfig(fileConfig, process.env, options.decider === false, (k) => trustedEnvValue(process.env, k)),
|
|
1057
1055
|
stageOutputs: options.stage === true,
|
|
1058
1056
|
journal,
|
|
1059
1057
|
...(spendBudget ? { budget: spendBudget } : {}),
|
|
@@ -1145,8 +1143,9 @@ resume) {
|
|
|
1145
1143
|
progress.stop();
|
|
1146
1144
|
console.error(formatCrashError(err));
|
|
1147
1145
|
runLogger.flush();
|
|
1146
|
+
const resumable = journal?.active === true;
|
|
1148
1147
|
journal?.end("crashed");
|
|
1149
|
-
if (
|
|
1148
|
+
if (resumable)
|
|
1150
1149
|
console.error(c.subtle(` resume with: sweny workflow resume ${runId}`));
|
|
1151
1150
|
recordHistory(nodeTimer.lastResults, undefined, true);
|
|
1152
1151
|
const crashSummary = summarizeRun(new Map(), Date.now() - runStart, true);
|
|
@@ -1257,7 +1256,7 @@ for (const [flags, description] of WORKFLOW_RUN_OPTIONS) {
|
|
|
1257
1256
|
}
|
|
1258
1257
|
workflowRunCmd.option("--no-history", "Do not record this run or save its output.md in .sweny/runs/ (or set `history: off` in .sweny.yml)");
|
|
1259
1258
|
workflowRunCmd.option("--show-output", "Print the final answer in CI too (off by default in CI: only the path to output.md is printed)");
|
|
1260
|
-
workflowRunCmd.option("--no-journal", "Do not write a run journal
|
|
1259
|
+
workflowRunCmd.option("--no-journal", "Do not write a run journal (kept in the sweny state dir; or set `journal: off` in .sweny.yml); the run cannot be resumed");
|
|
1261
1260
|
const workflowResumeCmd = workflowCmd
|
|
1262
1261
|
.command("resume <run-id>")
|
|
1263
1262
|
.description("Resume a killed or failed `workflow run` from its journal: finished nodes are replayed without calling the agent, " +
|
package/dist/cli/resume.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Lives outside main.ts so tests can import it (main.ts parses argv on import).
|
|
7
7
|
*/
|
|
8
8
|
import type { Logger, Workflow } from "../types.js";
|
|
9
|
-
import { RunJournal, type ResumePlan } from "../journal.js";
|
|
9
|
+
import { RunJournal, type LockHooks, type ResumePlan } from "../journal.js";
|
|
10
10
|
/** `sweny workflow resume` options (commander camelCases the flags). */
|
|
11
11
|
export interface ResumeOptions {
|
|
12
12
|
force?: boolean;
|
|
@@ -48,9 +48,18 @@ export interface PrepareResumeDeps {
|
|
|
48
48
|
swenyVersion?: string;
|
|
49
49
|
env?: Record<string, string | undefined>;
|
|
50
50
|
logger?: Logger;
|
|
51
|
+
/** Where run journals live (default: {@link runStateRoot} of `env`). */
|
|
52
|
+
stateRoot?: string;
|
|
53
|
+
/** Test seam for the run lock. */
|
|
54
|
+
lockHooks?: LockHooks;
|
|
51
55
|
}
|
|
52
56
|
/** One line per planned visit, for humans. */
|
|
53
57
|
export declare function formatResumePlan(plan: ResumePlan, mayRepeat: string[]): string[];
|
|
58
|
+
/**
|
|
59
|
+
* Check a run journal and build the resume. The run's lock is taken first,
|
|
60
|
+
* before anything is read or repaired, and is kept by the returned journal
|
|
61
|
+
* (released on every other outcome, `--plan` included).
|
|
62
|
+
*/
|
|
54
63
|
export declare function prepareResume(ref: string, opts: ResumeOptions, deps: PrepareResumeDeps): PrepareResumeResult;
|
|
55
64
|
/** Journal is on unless `--no-journal` (options.journal === false) or `.sweny.yml` has `journal: off`. */
|
|
56
65
|
export declare function journalDisabled(optionJournal: unknown, configJournal: unknown): boolean;
|
package/dist/cli/resume.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { validateRuntimeInput } from "../inputs.js";
|
|
10
|
-
import {
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { JournalKeyError, JournalLocationError, assertStateOutsideWorkspace, JournalLockedError, JournalRollbackError, JournalVersionError, RunJournal, buildResumePlan, canonicalHash, checkJournalAgainstWorkflow, findJournalRun, findLegacyJournal, journalFile, lockRun, mayRepeatWrites, readJournal, runStateRoot, workflowHashOf, } from "../journal.js";
|
|
11
12
|
import { parseInputFlag } from "./workflow-input.js";
|
|
12
13
|
/** Resume-only flags; the rest are shared with `workflow run`. */
|
|
13
14
|
export const WORKFLOW_RESUME_OPTIONS = [
|
|
@@ -34,6 +35,7 @@ export const RESUME_SHARED_RUN_FLAGS = [
|
|
|
34
35
|
"--input <json>",
|
|
35
36
|
"--agent <id>",
|
|
36
37
|
"--harness-policy <mode>",
|
|
38
|
+
"--no-decider",
|
|
37
39
|
];
|
|
38
40
|
/** One line per planned visit, for humans. */
|
|
39
41
|
export function formatResumePlan(plan, mayRepeat) {
|
|
@@ -68,23 +70,80 @@ export function formatResumePlan(plan, mayRepeat) {
|
|
|
68
70
|
lines.push(" every node finished; the resume only closes the run");
|
|
69
71
|
return lines;
|
|
70
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Check a run journal and build the resume. The run's lock is taken first,
|
|
75
|
+
* before anything is read or repaired, and is kept by the returned journal
|
|
76
|
+
* (released on every other outcome, `--plan` included).
|
|
77
|
+
*/
|
|
71
78
|
export function prepareResume(ref, opts, deps) {
|
|
72
79
|
const cwd = deps.cwd ?? process.cwd();
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
const root = deps.stateRoot ?? runStateRoot(deps.env ?? process.env);
|
|
81
|
+
// A state dir that resolves into the workspace is agent-writable: nothing in it can be trusted.
|
|
82
|
+
if (existsSync(root)) {
|
|
83
|
+
try {
|
|
84
|
+
assertStateOutsideWorkspace(root, cwd);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
if (err instanceof JournalLocationError)
|
|
88
|
+
return { ok: false, lines: [], error: err.message };
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const runId = findJournalRun(ref, cwd, root);
|
|
76
93
|
if (!runId) {
|
|
77
|
-
|
|
94
|
+
const legacy = findLegacyJournal(ref, cwd);
|
|
95
|
+
if (legacy) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
lines: [],
|
|
99
|
+
error: `run ${ref} has a journal in the workspace (${path.relative(cwd, legacy)}), written by an older sweny. ` +
|
|
100
|
+
`Journals now live in the sweny state dir, where agents cannot edit them; a workspace journal cannot ` +
|
|
101
|
+
`be resumed safely. Start a new run.`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
lines: [],
|
|
107
|
+
error: `no run journal matches "${ref}" for this workspace in ${root}. Runs started with --no-journal ` +
|
|
108
|
+
`(or by another user or state dir) cannot be resumed.`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
let lock;
|
|
112
|
+
try {
|
|
113
|
+
lock = lockRun(cwd, runId, root, deps.lockHooks);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
lines: [],
|
|
120
|
+
error: err instanceof JournalLockedError ? error : `cannot lock run ${runId}: ${error}`,
|
|
121
|
+
};
|
|
78
122
|
}
|
|
79
|
-
|
|
123
|
+
let result;
|
|
124
|
+
try {
|
|
125
|
+
result = prepareLocked(runId, journalFile(cwd, runId, root), lock, opts, deps, cwd, root);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
lock.release();
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
if (!(result.ok && "ctx" in result))
|
|
132
|
+
lock.release();
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
function prepareLocked(runId, file, lock, opts, deps, cwd, root) {
|
|
136
|
+
const lines = [];
|
|
137
|
+
const fail = (error) => ({ ok: false, error, lines });
|
|
80
138
|
let read;
|
|
81
139
|
try {
|
|
82
140
|
// --plan stays read-only: a torn tail is reported, not cut.
|
|
83
141
|
read = readJournal(file, { repair: !opts.plan });
|
|
84
142
|
}
|
|
85
143
|
catch (err) {
|
|
86
|
-
if (err instanceof JournalVersionError || err instanceof JournalKeyError)
|
|
144
|
+
if (err instanceof JournalVersionError || err instanceof JournalKeyError || err instanceof JournalRollbackError) {
|
|
87
145
|
return fail(err.message);
|
|
146
|
+
}
|
|
88
147
|
return fail(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
89
148
|
}
|
|
90
149
|
if (read.forgedAtLine !== undefined) {
|
|
@@ -100,6 +159,11 @@ export function prepareResume(ref, opts, deps) {
|
|
|
100
159
|
? `note: the journal ends in a torn record (${read.truncatedBytes} bytes); resuming drops it`
|
|
101
160
|
: `note: dropped a torn record at the end of the journal (${read.truncatedBytes} bytes)`);
|
|
102
161
|
}
|
|
162
|
+
if (read.restored) {
|
|
163
|
+
lines.push(opts.plan
|
|
164
|
+
? `note: the journal's last record was not appended before the crash; resuming restores it from the head`
|
|
165
|
+
: `note: restored the journal's last record from the head (the run stopped before appending it)`);
|
|
166
|
+
}
|
|
103
167
|
let plan;
|
|
104
168
|
try {
|
|
105
169
|
plan = buildResumePlan(read.records);
|
|
@@ -153,6 +217,10 @@ export function prepareResume(ref, opts, deps) {
|
|
|
153
217
|
}
|
|
154
218
|
const mayRepeat = mayRepeatWrites(plan, workflow, input);
|
|
155
219
|
lines.push(...formatResumePlan(plan, mayRepeat));
|
|
220
|
+
if (plan.unsandboxed.length > 0) {
|
|
221
|
+
lines.push(`warning: an earlier attempt ran ${plan.unsandboxed.join(", ")} without an enforced sandbox, so that agent ` +
|
|
222
|
+
`could have read the run key and edited this run's journaled spend and write records (journal_unsandboxed)`);
|
|
223
|
+
}
|
|
156
224
|
if (opts.plan)
|
|
157
225
|
return { ok: true, planOnly: true, lines };
|
|
158
226
|
if (mayRepeat.length > 0 && !opts.allowRepeatWrites) {
|
|
@@ -169,8 +237,10 @@ export function prepareResume(ref, opts, deps) {
|
|
|
169
237
|
swenyVersion: deps.swenyVersion,
|
|
170
238
|
env: deps.env,
|
|
171
239
|
logger: deps.logger,
|
|
240
|
+
stateRoot: root,
|
|
172
241
|
read,
|
|
173
242
|
plan,
|
|
243
|
+
lock,
|
|
174
244
|
force: opts.force === true,
|
|
175
245
|
allowRepeatWrites: opts.allowRepeatWrites === true,
|
|
176
246
|
});
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* builds on this format, so readers skip records with a version they do not
|
|
11
11
|
* know instead of guessing.
|
|
12
12
|
*/
|
|
13
|
-
import type { ExecutionEvent, ExecutionTrace, NodeResult, Workflow } from "../types.js";
|
|
13
|
+
import type { ExecutionEvent, ExecutionTrace, NodeResult, RouteRung, Workflow } from "../types.js";
|
|
14
14
|
export declare const RUN_HISTORY_SCHEMA_VERSION = 1;
|
|
15
15
|
export declare const RUN_HISTORY_DIR: string;
|
|
16
16
|
export declare const RUN_HISTORY_KEEP = 200;
|
|
@@ -43,9 +43,11 @@ export interface RunRecord {
|
|
|
43
43
|
duration_ms: number;
|
|
44
44
|
status: RunStatus;
|
|
45
45
|
nodes: RunNodeRecord[];
|
|
46
|
+
/** Edges taken. `rung` says who chose a conditional route: expr, decider or agent (#357). */
|
|
46
47
|
routes: Array<{
|
|
47
48
|
from: string;
|
|
48
49
|
to: string;
|
|
50
|
+
rung?: RouteRung;
|
|
49
51
|
}>;
|
|
50
52
|
totals: RunTotals;
|
|
51
53
|
/** Harness that ran the nodes (id + version). Absent for runs with no harness-tagged result. */
|
|
@@ -55,12 +57,6 @@ export interface RunRecord {
|
|
|
55
57
|
};
|
|
56
58
|
/** Opinions the harness could not honor natively on some node, deduped. Absent when none. */
|
|
57
59
|
degraded?: string[];
|
|
58
|
-
/** Shadow-mode decider agreement counts (#357). Absent when the decider was off. */
|
|
59
|
-
decider?: {
|
|
60
|
-
compared: number;
|
|
61
|
-
agreed: number;
|
|
62
|
-
fell_through: number;
|
|
63
|
-
};
|
|
64
60
|
}
|
|
65
61
|
/** sha256 (hex) of the workflow as canonical JSON: keys sorted, undefined dropped. */
|
|
66
62
|
export declare function hashWorkflow(workflow: Workflow): string;
|
package/dist/cli/run-history.js
CHANGED
|
@@ -103,7 +103,7 @@ export function buildRunRecord(i) {
|
|
|
103
103
|
duration_ms: Math.max(0, Math.round(i.durationMs)),
|
|
104
104
|
status: i.crashed ? "crashed" : summary.ok ? "success" : "failed",
|
|
105
105
|
nodes,
|
|
106
|
-
routes: (i.trace?.edges ?? []).map((e) => ({ from: e.from, to: e.to })),
|
|
106
|
+
routes: (i.trace?.edges ?? []).map((e) => ({ from: e.from, to: e.to, ...(e.rung ? { rung: e.rung } : {}) })),
|
|
107
107
|
totals: {
|
|
108
108
|
nodes_total: summary.nodesTotal,
|
|
109
109
|
nodes_ok: summary.nodesOk,
|
|
@@ -114,15 +114,6 @@ export function buildRunRecord(i) {
|
|
|
114
114
|
},
|
|
115
115
|
...(harness ? { harness: { id: harness.id, version: harness.version } } : {}),
|
|
116
116
|
...(degraded.length > 0 ? { degraded } : {}),
|
|
117
|
-
...(summary.decider
|
|
118
|
-
? {
|
|
119
|
-
decider: {
|
|
120
|
-
compared: summary.decider.compared,
|
|
121
|
-
agreed: summary.decider.agreed,
|
|
122
|
-
fell_through: summary.decider.fellThrough,
|
|
123
|
-
},
|
|
124
|
-
}
|
|
125
|
-
: {}),
|
|
126
117
|
};
|
|
127
118
|
}
|
|
128
119
|
// ── Write, prune, read ──────────────────────────────────────────
|
package/dist/cli/run-output.d.ts
CHANGED
|
@@ -40,15 +40,19 @@ export interface RunSummary {
|
|
|
40
40
|
spent: number;
|
|
41
41
|
};
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* Absent when
|
|
43
|
+
* Who decided the run's conditional routes (#357), counts only: a `when`
|
|
44
|
+
* expression, the decision model, or the agent. The decider and expression
|
|
45
|
+
* counts are routes with no agent call. Absent when no route was a decision.
|
|
46
46
|
*/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
routes?: RouteCounts;
|
|
48
|
+
/** Why the decider did not run for some or all routes (missing config, breaker, cap). */
|
|
49
|
+
deciderOff?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface RouteCounts {
|
|
52
|
+
total: number;
|
|
53
|
+
decider: number;
|
|
54
|
+
expr: number;
|
|
55
|
+
agent: number;
|
|
52
56
|
}
|
|
53
57
|
export interface RunPolicy {
|
|
54
58
|
/** Agent env narrowed to the allowlist. Absent when no node reported it. */
|
|
@@ -68,6 +72,8 @@ export interface RunPolicy {
|
|
|
68
72
|
export declare function degradedKey(entry: string): string;
|
|
69
73
|
/** Sum the per-node results into one run summary. */
|
|
70
74
|
export declare function summarizeRun(results: Map<string, NodeResult>, durationMs: number, crashed?: boolean, trace?: ExecutionTrace): RunSummary;
|
|
75
|
+
/** Conditional routes by rung. Unconditional edges decide nothing and are not counted. */
|
|
76
|
+
export declare function countRoutes(trace: ExecutionTrace): RouteCounts;
|
|
71
77
|
/**
|
|
72
78
|
* The policy facts the run knew, as short phrases: `env scoped`, `sandbox on`,
|
|
73
79
|
* `1 output staged`. No phrase for a fact no node reported.
|
|
@@ -85,8 +91,8 @@ export declare function formatCost(usd: number): string;
|
|
|
85
91
|
export declare function formatBudgetAmount(b: NonNullable<RunSummary["budget"]>): string;
|
|
86
92
|
/** `budget exceeded: tokens 52k of 50k (run, at node gather)`. */
|
|
87
93
|
export declare function formatBudgetOverrun(b: NonNullable<RunSummary["budget"]>): string;
|
|
88
|
-
/** `
|
|
89
|
-
export declare function
|
|
94
|
+
/** `5 (3 decider, 1 expr, 1 agent)`: zero rungs are left out. */
|
|
95
|
+
export declare function formatRouteCounts(r: RouteCounts): string;
|
|
90
96
|
/**
|
|
91
97
|
* One plain line: `✓ 3/3 nodes · 41 tool calls · 2m10s · 12k tokens · $0.18`.
|
|
92
98
|
* Token and cost segments are omitted when the SDK reported none. A harness
|
package/dist/cli/run-output.js
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { workspaceRoot, writeFileNoFollow } from "../safe-file.js";
|
|
9
9
|
import { consoleLogger } from "../types.js";
|
|
10
|
-
import { summarizeDecisions } from "../decider.js";
|
|
11
10
|
import { toMermaidBlock } from "../mermaid.js";
|
|
12
11
|
import { createPaint } from "./style.js";
|
|
13
12
|
import { colorEnabled, glyphs } from "./terminal.js";
|
|
@@ -84,12 +83,20 @@ export function summarizeRun(results, durationMs, crashed = false, trace) {
|
|
|
84
83
|
...(degraded.size > 0 ? { degraded: [...degraded] } : {}),
|
|
85
84
|
...(Object.keys(policy).length > 0 ? { policy } : {}),
|
|
86
85
|
...(budget ? { budget } : {}),
|
|
87
|
-
...(trace
|
|
86
|
+
...(trace && countRoutes(trace).total > 0 ? { routes: countRoutes(trace) } : {}),
|
|
87
|
+
...(trace?.deciderOff ? { deciderOff: trace.deciderOff } : {}),
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
/** Conditional routes by rung. Unconditional edges decide nothing and are not counted. */
|
|
91
|
+
export function countRoutes(trace) {
|
|
92
|
+
const c = { total: 0, decider: 0, expr: 0, agent: 0 };
|
|
93
|
+
for (const e of trace.edges) {
|
|
94
|
+
if (!e.rung)
|
|
95
|
+
continue;
|
|
96
|
+
c.total++;
|
|
97
|
+
c[e.rung]++;
|
|
98
|
+
}
|
|
99
|
+
return c;
|
|
93
100
|
}
|
|
94
101
|
/**
|
|
95
102
|
* The policy facts the run knew, as short phrases: `env scoped`, `sandbox on`,
|
|
@@ -157,9 +164,10 @@ export function formatBudgetAmount(b) {
|
|
|
157
164
|
export function formatBudgetOverrun(b) {
|
|
158
165
|
return `budget exceeded: ${formatBudgetAmount(b)} (${b.scope}, at node ${b.node})`;
|
|
159
166
|
}
|
|
160
|
-
/** `
|
|
161
|
-
export function
|
|
162
|
-
|
|
167
|
+
/** `5 (3 decider, 1 expr, 1 agent)`: zero rungs are left out. */
|
|
168
|
+
export function formatRouteCounts(r) {
|
|
169
|
+
const parts = ["decider", "expr", "agent"].filter((k) => r[k] > 0).map((k) => `${r[k]} ${k}`);
|
|
170
|
+
return parts.length > 0 ? `${r.total} (${parts.join(", ")})` : String(r.total);
|
|
163
171
|
}
|
|
164
172
|
/**
|
|
165
173
|
* One plain line: `✓ 3/3 nodes · 41 tool calls · 2m10s · 12k tokens · $0.18`.
|
|
@@ -180,7 +188,8 @@ export function formatReceipt(s) {
|
|
|
180
188
|
...(s.degraded && s.degraded.length > 0 ? [`degraded: ${s.degraded.join(", ")}`] : []),
|
|
181
189
|
...(formatPolicySegment(s.policy) ? [formatPolicySegment(s.policy)] : []),
|
|
182
190
|
...(s.budget ? [formatBudgetOverrun(s.budget)] : []),
|
|
183
|
-
...(s.
|
|
191
|
+
...(s.routes && s.routes.total > 0 ? [`routes ${formatRouteCounts(s.routes)}`] : []),
|
|
192
|
+
...(s.deciderOff ? [`decider off: ${s.deciderOff}`] : []),
|
|
184
193
|
];
|
|
185
194
|
return parts.join(" · ");
|
|
186
195
|
}
|
|
@@ -317,8 +326,5 @@ export const WORKFLOW_RUN_OPTIONS = [
|
|
|
317
326
|
"--harness-policy <mode>",
|
|
318
327
|
"strict: refuse a node whose policy the agent cannot enforce; warn: run it and report what was not enforced (default: strict under GitHub Actions, warn elsewhere; env SWENY_HARNESS_POLICY)",
|
|
319
328
|
],
|
|
320
|
-
[
|
|
321
|
-
"--decider <mode>",
|
|
322
|
-
"Decision model for route choices: off (default) or shadow. Shadow asks the workflow's decider.provider alongside the agent and reports agreement; the route is always the agent's. Needs decider.provider in the workflow (no default URL)",
|
|
323
|
-
],
|
|
329
|
+
["--no-decider", "Skip the decision model for this run: the agent decides the routes of route_by: decider nodes"],
|
|
324
330
|
];
|
package/dist/cli/ticket.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* METADATA ONLY, like the receipt line: counts, durations, tokens, cost,
|
|
7
7
|
* policy facts. Never prompts, node outputs, tool inputs, or model prose.
|
|
8
8
|
*/
|
|
9
|
-
import { formatBudgetAmount, formatCost,
|
|
9
|
+
import { formatBudgetAmount, formatCost, formatRouteCounts, formatReceiptDuration, formatTokenCount, policyParts, renderReceiptLine, } from "./run-output.js";
|
|
10
10
|
import { createPaint } from "./style.js";
|
|
11
11
|
import { colorEnabled, glyphsFor, richOutput, supportsUnicode, terminalColumns, ticketBoxFor } from "./terminal.js";
|
|
12
12
|
/** Widest ticket, in columns, frame included. */
|
|
@@ -69,7 +69,8 @@ function ticketFields(s) {
|
|
|
69
69
|
...(s.costUsd !== undefined ? [["cost", formatCost(s.costUsd)]] : []),
|
|
70
70
|
...(s.harness !== undefined && s.harness !== "claude-code" ? [["harness", s.harness]] : []),
|
|
71
71
|
...(s.degraded && s.degraded.length > 0 ? [["degraded", s.degraded.join(", ")]] : []),
|
|
72
|
-
...(s.
|
|
72
|
+
...(s.routes && s.routes.total > 0 ? [["routes", formatRouteCounts(s.routes)]] : []),
|
|
73
|
+
...(s.deciderOff ? [["decider off", s.deciderOff]] : []),
|
|
73
74
|
...(s.budget
|
|
74
75
|
? [
|
|
75
76
|
["budget", `${formatBudgetAmount(s.budget)} (${s.budget.scope})`],
|