@sweny-ai/core 0.1.123 → 0.1.124

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/agent-env.d.ts +171 -0
  2. package/dist/agent-env.js +393 -0
  3. package/dist/browser.d.ts +2 -2
  4. package/dist/browser.js +1 -1
  5. package/dist/claude.d.ts +61 -3
  6. package/dist/claude.js +187 -27
  7. package/dist/cli/check.d.ts +42 -1
  8. package/dist/cli/check.js +130 -8
  9. package/dist/cli/cloud-lifecycle.d.ts +24 -29
  10. package/dist/cli/cloud-lifecycle.js +187 -12
  11. package/dist/cli/cloud-report.js +45 -2
  12. package/dist/cli/config-file.d.ts +8 -1
  13. package/dist/cli/config-file.js +42 -1
  14. package/dist/cli/config.js +11 -14
  15. package/dist/cli/e2e.d.ts +22 -0
  16. package/dist/cli/e2e.js +31 -4
  17. package/dist/cli/main.d.ts +1 -0
  18. package/dist/cli/main.js +138 -46
  19. package/dist/cli/marketplace.d.ts +3 -1
  20. package/dist/cli/marketplace.js +2 -2
  21. package/dist/cli/new.d.ts +15 -0
  22. package/dist/cli/new.js +139 -65
  23. package/dist/cli/publish.js +9 -0
  24. package/dist/cli/run-output.d.ts +59 -0
  25. package/dist/cli/run-output.js +189 -0
  26. package/dist/cli/skill-env.d.ts +34 -0
  27. package/dist/cli/skill-env.js +64 -0
  28. package/dist/cli/templates.js +587 -0
  29. package/dist/cli/workflow-input.d.ts +41 -0
  30. package/dist/cli/workflow-input.js +56 -0
  31. package/dist/executor.d.ts +17 -1
  32. package/dist/executor.js +119 -9
  33. package/dist/index.d.ts +3 -3
  34. package/dist/index.js +2 -2
  35. package/dist/mermaid.d.ts +6 -0
  36. package/dist/mermaid.js +10 -5
  37. package/dist/schema.d.ts +218 -188
  38. package/dist/schema.js +24 -3
  39. package/dist/skills/betterstack.js +4 -0
  40. package/dist/skills/datadog.js +3 -0
  41. package/dist/skills/github.js +8 -0
  42. package/dist/skills/linear.js +8 -0
  43. package/dist/skills/notification.js +3 -0
  44. package/dist/skills/sentry.js +4 -0
  45. package/dist/skills/slack.js +2 -0
  46. package/dist/skills/supabase.js +9 -0
  47. package/dist/testing.d.ts +3 -2
  48. package/dist/testing.js +9 -3
  49. package/dist/types.d.ts +99 -2
  50. package/dist/types.js +17 -0
  51. package/dist/untrusted.d.ts +32 -0
  52. package/dist/untrusted.js +46 -0
  53. package/dist/workflows/browser.js +2 -2
  54. package/dist/workflows/implement.yml +2 -0
  55. package/dist/workflows/triage.yml +3 -1
  56. package/package.json +1 -1
package/dist/cli/check.js CHANGED
@@ -1,20 +1,33 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { parse as parseYaml } from "yaml";
6
+ import { validateInputs } from "./config.js";
7
+ import { findSkillEnvGaps } from "./skill-env.js";
8
+ import { configuredSkills } from "../skills/custom-loader.js";
1
9
  /**
2
10
  * Per-check network timeout. A hung connect must not make `sweny check` hang
3
11
  * indefinitely — the whole point of the command is fast diagnostics.
4
12
  */
5
13
  const CHECK_TIMEOUT_MS = 5000;
6
- /**
7
- * Run lightweight connectivity checks for all configured providers.
8
- * Uses raw fetch — does NOT import provider packages.
9
- */
10
- export async function checkProviderConnectivity(config) {
14
+ export async function checkProviderConnectivity(config, opts = {}) {
11
15
  const results = [];
16
+ const scope = opts.scope;
17
+ const inScope = (skillId) => !scope || scope.has(skillId);
12
18
  const githubChecked = { done: false }; // avoid double-checking same token
13
19
  // ── Coding agent ────────────────────────────────────────────────────────────
14
20
  if (config.codingAgentProvider === "claude") {
15
21
  const mode = resolveCheckAuthMode(config);
16
22
  const base = config.anthropicBaseUrl;
17
- if (mode === "none") {
23
+ if (mode === "none" && opts.claudeCodeLogin) {
24
+ results.push({
25
+ name: "Anthropic (claude agent)",
26
+ status: "ok",
27
+ detail: "Claude Code login detected",
28
+ });
29
+ }
30
+ else if (mode === "none") {
18
31
  results.push({ name: "Anthropic (claude agent)", status: "skip", detail: "No credential configured" });
19
32
  }
20
33
  else if (base) {
@@ -42,6 +55,8 @@ export async function checkProviderConnectivity(config) {
42
55
  }
43
56
  // ── Observability ────────────────────────────────────────────────────────────
44
57
  for (const provider of config.observabilityProviders) {
58
+ if (!inScope(provider))
59
+ continue;
45
60
  const creds = config.observabilityCredentials[provider] ?? {};
46
61
  if (provider === "datadog") {
47
62
  results.push(await checkDatadog(creds));
@@ -61,7 +76,10 @@ export async function checkProviderConnectivity(config) {
61
76
  }
62
77
  }
63
78
  // ── Issue tracker ────────────────────────────────────────────────────────────
64
- if (config.issueTrackerProvider === "linear") {
79
+ if (scope && !inScope(config.issueTrackerProvider === "github-issues" ? "github" : config.issueTrackerProvider)) {
80
+ // No workflow uses this tracker: nothing to check.
81
+ }
82
+ else if (config.issueTrackerProvider === "linear") {
65
83
  results.push(await checkLinear(config.linearApiKey));
66
84
  }
67
85
  else if (config.issueTrackerProvider === "github-issues") {
@@ -84,7 +102,10 @@ export async function checkProviderConnectivity(config) {
84
102
  });
85
103
  }
86
104
  // ── Source control ───────────────────────────────────────────────────────────
87
- if (config.sourceControlProvider === "github" && !githubChecked.done) {
105
+ if (scope && !inScope(config.sourceControlProvider)) {
106
+ // No workflow uses this source control: nothing to check.
107
+ }
108
+ else if (config.sourceControlProvider === "github" && !githubChecked.done) {
88
109
  const token = config.githubToken || config.botToken;
89
110
  results.push(await checkGitHub(token, "Source control (github)"));
90
111
  }
@@ -324,3 +345,104 @@ function networkErrorMessage(err) {
324
345
  }
325
346
  return msg;
326
347
  }
348
+ // ── Workflow-scoped checks ───────────────────────────────────────────────────
349
+ /**
350
+ * Detect a local Claude Code login the way `sweny workflow run` benefits from
351
+ * it: the spawned Claude Code agent finds its own login, so no env credential
352
+ * is needed. Checks `~/.claude/.credentials.json` (or CLAUDE_CONFIG_DIR) and,
353
+ * on macOS, the Keychain entry (attributes only, the secret is never read).
354
+ */
355
+ export function detectClaudeCodeLogin(opts = {}) {
356
+ const env = opts.env ?? process.env;
357
+ const home = opts.home ?? os.homedir();
358
+ const platform = opts.platform ?? process.platform;
359
+ const configDir = env.CLAUDE_CONFIG_DIR || path.join(home, ".claude");
360
+ try {
361
+ if (fs.existsSync(path.join(configDir, ".credentials.json")))
362
+ return true;
363
+ }
364
+ catch {
365
+ // fall through
366
+ }
367
+ if (platform === "darwin") {
368
+ const probe = opts.keychainHasLogin ??
369
+ (() => {
370
+ const r = spawnSync("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
371
+ stdio: "ignore",
372
+ timeout: 3000,
373
+ });
374
+ return r.status === 0;
375
+ });
376
+ try {
377
+ return probe();
378
+ }
379
+ catch {
380
+ return false;
381
+ }
382
+ }
383
+ return false;
384
+ }
385
+ /**
386
+ * Skill ids used by the workflows in `.sweny/workflows/`. `found` is false when
387
+ * there are no workflow files, in which case callers keep the legacy checks.
388
+ */
389
+ export function discoverWorkflowSkillIds(cwd) {
390
+ const dir = path.join(cwd, ".sweny", "workflows");
391
+ const skillIds = new Set();
392
+ let files;
393
+ try {
394
+ files = fs.readdirSync(dir).filter((f) => /\.(ya?ml|json)$/i.test(f));
395
+ }
396
+ catch {
397
+ return { found: false, skillIds };
398
+ }
399
+ let found = false;
400
+ for (const f of files) {
401
+ try {
402
+ const raw = parseYaml(fs.readFileSync(path.join(dir, f), "utf-8"));
403
+ if (!raw || typeof raw !== "object")
404
+ continue;
405
+ found = true;
406
+ for (const node of Object.values(raw.nodes ?? {})) {
407
+ if (Array.isArray(node?.skills)) {
408
+ for (const id of node.skills)
409
+ if (typeof id === "string")
410
+ skillIds.add(id);
411
+ }
412
+ }
413
+ }
414
+ catch {
415
+ // unparseable file: `workflow validate` reports it; check ignores it
416
+ }
417
+ }
418
+ return { found, skillIds };
419
+ }
420
+ const CLAUDE_AUTH_ERROR_PREFIX = "Missing: ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN";
421
+ /**
422
+ * Errors `sweny check` should report. With a workflow scope, only credentials
423
+ * those workflows need (plus agent auth) are required. A local Claude Code
424
+ * login satisfies agent auth.
425
+ */
426
+ export function validateCheckInputs(config, opts = {}) {
427
+ let errors;
428
+ if (opts.scope) {
429
+ // Agent auth only; repo / tracker / observability requirements come from skills below.
430
+ errors = validateInputs({
431
+ ...config,
432
+ issueTrackerProvider: "file",
433
+ sourceControlProvider: "file",
434
+ observabilityProviders: [],
435
+ });
436
+ const env = opts.env ?? process.env;
437
+ const available = new Set(configuredSkills(env, opts.cwd ?? process.cwd()).map((s) => s.id));
438
+ for (const gap of findSkillEnvGaps(opts.scope, available)) {
439
+ errors.push(`Missing: ${gap.missingEnv.join(", ")} (needed by skill "${gap.id}")`);
440
+ }
441
+ }
442
+ else {
443
+ errors = validateInputs(config);
444
+ }
445
+ if (opts.claudeCodeLogin)
446
+ errors = errors.filter((e) => !e.startsWith(CLAUDE_AUTH_ERROR_PREFIX));
447
+ return errors;
448
+ }
@@ -107,13 +107,25 @@ export declare function postNodeEvent(config: CloudReportConfig, runId: string,
107
107
  duration_ms?: number;
108
108
  data?: Record<string, unknown>;
109
109
  }): Promise<void>;
110
+ /**
111
+ * Aggregate SHAPE-ONLY telemetry off the executor's NodeResult map for the
112
+ * finish payload: token/cost totals, eval verdict counts, and tool-call
113
+ * NAME + STATUS counts. Never any prompt, response, tool input, or tool
114
+ * output. This is the substrate the paid cloud "brain" derives type-specific
115
+ * metrics from; the engine just ships the counts it has.
116
+ *
117
+ * Returns undefined when there is nothing to report (no usage, no evals, no
118
+ * tool calls) so the finish payload stays lean for trivial runs.
119
+ */
120
+ export declare function deriveTypeMetricSubstrate(results: Map<string, NodeResult>): Record<string, unknown> | undefined;
110
121
  /**
111
122
  * Derive a `metrics` blob from the executor's NodeResult map.
112
123
  *
113
- * For now this is intentionally minimal: total node count, failed-node
114
- * count, success rate. Per-workflow-type metric extraction (Caught /
115
- * Calibration / Missed for pr_review, flake rate for e2e_test, etc.) is
116
- * the renderer's job; the engine just ships what it has.
124
+ * Ships node counts + success rate, plus the shape-only type-metric
125
+ * substrate (token/cost totals, eval verdict counts, tool-call name+status
126
+ * counts) when present. Per-workflow-type metric extraction (Caught /
127
+ * Calibration / Missed for pr_review, flake rate for e2e_test, etc.) is the
128
+ * renderer's job; the engine just ships what it has.
117
129
  */
118
130
  export declare function deriveGenericMetrics(results: Map<string, NodeResult>, durationMs: number): {
119
131
  duration_ms: number;
@@ -151,33 +163,16 @@ export declare function beginCloudLifecycle(config: {
151
163
  * onto the cloud's accepted enum. Failure is silent — the workflow's
152
164
  * exit code already reflects the truth.
153
165
  */
154
- export declare function finishCloudLifecycle(config: {
155
- cloudToken?: string;
156
- }, handle: CloudLifecycleHandle | null, results: Map<string, NodeResult>, durationMs: number, status?: "success" | "failed" | "partial"): Promise<void>;
157
166
  /**
158
- * Build an Observer that ships per-node lifecycle events to cloud.
159
- *
160
- * Maps the engine's ExecutionEvent stream onto `POST /api/runs/:id/node`:
161
- * node:enter → { event: "enter", node }
162
- * node:exit → { event: "exit", node, status, duration_ms }
163
- * node:progress → { event: "progress", node, data: { message } }
164
- * node:retry → { event: "progress", node, data: { retry: true, attempt, reason } }
165
- * Other events (workflow:start, tool:*, route, workflow:end) are dropped.
166
- *
167
- * Returns undefined when the handle is null (no cloud session) or the
168
- * token is missing. The observer captures `node:enter` timestamps in a
169
- * closure-scoped Map so it can compute duration_ms on the matching exit
170
- * without depending on any external state.
171
- *
172
- * Hot-path safety contract:
173
- * 1. The observer never throws synchronously. The whole body is
174
- * wrapped in try/catch — any future ExecutionEvent shape change
175
- * that breaks a field access cannot crash the engine.
176
- * 2. The observer never awaits. postNodeEvent is fired with .catch
177
- * to swallow any rejection so node never sees an unhandledRejection.
178
- * 3. The Map is keyed by node id; collisions only happen if the same
179
- * node id runs concurrently, which the engine does not do today.
167
+ * Crash summary for the cloud: `error.name` plus a message capped at 200
168
+ * chars. Thrown messages can embed agent/LLM prose, file paths, or log
169
+ * excerpts (e.g. a wrapped SDK error), so the cloud only gets a short,
170
+ * bounded diagnostic, never the full text. Accepts an Error or a string.
180
171
  */
172
+ export declare function crashSummaryForCloud(err: unknown): string;
173
+ export declare function finishCloudLifecycle(config: {
174
+ cloudToken?: string;
175
+ }, handle: CloudLifecycleHandle | null, results: Map<string, NodeResult>, durationMs: number, status?: "success" | "failed" | "partial", error?: unknown): Promise<void>;
181
176
  export declare function createCloudStreamObserver(config: {
182
177
  cloudToken?: string;
183
178
  }, handle: CloudLifecycleHandle | null): Observer | undefined;
@@ -194,22 +194,102 @@ export async function postNodeEvent(config, runId, event) {
194
194
  // Silent — never block the workflow.
195
195
  }
196
196
  }
197
+ /**
198
+ * Aggregate SHAPE-ONLY telemetry off the executor's NodeResult map for the
199
+ * finish payload: token/cost totals, eval verdict counts, and tool-call
200
+ * NAME + STATUS counts. Never any prompt, response, tool input, or tool
201
+ * output. This is the substrate the paid cloud "brain" derives type-specific
202
+ * metrics from; the engine just ships the counts it has.
203
+ *
204
+ * Returns undefined when there is nothing to report (no usage, no evals, no
205
+ * tool calls) so the finish payload stays lean for trivial runs.
206
+ */
207
+ export function deriveTypeMetricSubstrate(results) {
208
+ let costUsd = 0;
209
+ let inputTokens = 0;
210
+ let outputTokens = 0;
211
+ let cacheReadTokens = 0;
212
+ let cacheCreationTokens = 0;
213
+ let numTurns = 0;
214
+ let sawUsage = false;
215
+ let evalsTotal = 0;
216
+ let evalsPassed = 0;
217
+ let evalsFailed = 0;
218
+ let toolCallsTotal = 0;
219
+ let toolCallsError = 0;
220
+ // NAME → count. Names only (e.g. "Bash", "create_pr"); never tool input
221
+ // or output, which can carry source, diffs, or customer data.
222
+ const toolCallsByName = {};
223
+ const add = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
224
+ for (const r of results.values()) {
225
+ if (r.usage) {
226
+ sawUsage = true;
227
+ costUsd += add(r.usage.costUsd);
228
+ inputTokens += add(r.usage.inputTokens);
229
+ outputTokens += add(r.usage.outputTokens);
230
+ cacheReadTokens += add(r.usage.cacheReadTokens);
231
+ cacheCreationTokens += add(r.usage.cacheCreationTokens);
232
+ numTurns += add(r.usage.numTurns);
233
+ }
234
+ if (r.evals && r.evals.length > 0) {
235
+ for (const e of r.evals) {
236
+ evalsTotal++;
237
+ if (e.pass)
238
+ evalsPassed++;
239
+ else
240
+ evalsFailed++;
241
+ }
242
+ }
243
+ for (const call of r.toolCalls ?? []) {
244
+ toolCallsTotal++;
245
+ if (call.status === "error")
246
+ toolCallsError++;
247
+ const name = call.tool || "unknown";
248
+ toolCallsByName[name] = (toolCallsByName[name] ?? 0) + 1;
249
+ }
250
+ }
251
+ const substrate = {};
252
+ if (sawUsage) {
253
+ substrate.cost = {
254
+ total_cost_usd: costUsd,
255
+ input_tokens: inputTokens,
256
+ output_tokens: outputTokens,
257
+ cache_read_tokens: cacheReadTokens,
258
+ cache_creation_tokens: cacheCreationTokens,
259
+ num_turns: numTurns,
260
+ };
261
+ }
262
+ if (evalsTotal > 0) {
263
+ substrate.evals = { total: evalsTotal, passed: evalsPassed, failed: evalsFailed };
264
+ }
265
+ if (toolCallsTotal > 0) {
266
+ substrate.tool_calls = {
267
+ total: toolCallsTotal,
268
+ error: toolCallsError,
269
+ by_name: toolCallsByName,
270
+ };
271
+ }
272
+ return Object.keys(substrate).length > 0 ? substrate : undefined;
273
+ }
197
274
  /**
198
275
  * Derive a `metrics` blob from the executor's NodeResult map.
199
276
  *
200
- * For now this is intentionally minimal: total node count, failed-node
201
- * count, success rate. Per-workflow-type metric extraction (Caught /
202
- * Calibration / Missed for pr_review, flake rate for e2e_test, etc.) is
203
- * the renderer's job; the engine just ships what it has.
277
+ * Ships node counts + success rate, plus the shape-only type-metric
278
+ * substrate (token/cost totals, eval verdict counts, tool-call name+status
279
+ * counts) when present. Per-workflow-type metric extraction (Caught /
280
+ * Calibration / Missed for pr_review, flake rate for e2e_test, etc.) is the
281
+ * renderer's job; the engine just ships what it has.
204
282
  */
205
283
  export function deriveGenericMetrics(results, durationMs) {
206
284
  const nodeCount = results.size;
207
285
  const failedNodes = [...results.values()].filter((r) => r.status === "failed").length;
286
+ const substrate = deriveTypeMetricSubstrate(results);
208
287
  return {
209
288
  duration_ms: durationMs,
210
289
  node_count: nodeCount,
211
290
  failed_nodes: failedNodes,
212
291
  success_rate: nodeCount > 0 ? (nodeCount - failedNodes) / nodeCount : 0,
292
+ ...(substrate ?? {}),
213
293
  };
214
294
  }
215
295
  /**
@@ -252,14 +332,36 @@ export async function beginCloudLifecycle(config, workflow, options) {
252
332
  * onto the cloud's accepted enum. Failure is silent — the workflow's
253
333
  * exit code already reflects the truth.
254
334
  */
255
- export async function finishCloudLifecycle(config, handle, results, durationMs, status = "success") {
335
+ /**
336
+ * Crash summary for the cloud: `error.name` plus a message capped at 200
337
+ * chars. Thrown messages can embed agent/LLM prose, file paths, or log
338
+ * excerpts (e.g. a wrapped SDK error), so the cloud only gets a short,
339
+ * bounded diagnostic, never the full text. Accepts an Error or a string.
340
+ */
341
+ export function crashSummaryForCloud(err) {
342
+ const name = err instanceof Error ? err.name || "Error" : "Error";
343
+ const raw = err instanceof Error ? err.message : typeof err === "string" ? err : "Unknown error";
344
+ const msg = raw.replace(/\s+/g, " ").trim().slice(0, 200);
345
+ return msg ? `${name}: ${msg}` : name;
346
+ }
347
+ // Handles already finished. A run gets exactly one terminal finish: a throw
348
+ // after the in-try finish must not send a second "failed" over a real result.
349
+ const finishedHandles = new WeakSet();
350
+ export async function finishCloudLifecycle(config, handle, results, durationMs, status = "success", error) {
256
351
  if (!handle || !config.cloudToken)
257
352
  return;
353
+ if (finishedHandles.has(handle))
354
+ return;
355
+ finishedHandles.add(handle);
258
356
  const reportConfig = { cloudToken: config.cloudToken };
259
357
  await finishRun(reportConfig, handle.runId, {
260
358
  status,
261
359
  duration_ms: durationMs,
262
360
  metrics: deriveGenericMetrics(results, durationMs),
361
+ // `error` is reduced to name + 200-char message (see crashSummaryForCloud).
362
+ // Only forwarded on the crash/throw path where the executor never
363
+ // returned a results map.
364
+ ...(error ? { error: crashSummaryForCloud(error) } : {}),
263
365
  });
264
366
  }
265
367
  /**
@@ -267,10 +369,18 @@ export async function finishCloudLifecycle(config, handle, results, durationMs,
267
369
  *
268
370
  * Maps the engine's ExecutionEvent stream onto `POST /api/runs/:id/node`:
269
371
  * node:enter → { event: "enter", node }
270
- * node:exit → { event: "exit", node, status, duration_ms }
271
- * node:progress → { event: "progress", node, data: { message } }
372
+ * node:exit → { event: "exit", node, status, duration_ms, data: { evals, tool_calls } }
373
+ * node:progress → { event: "progress", node, data: { progress: true } }
272
374
  * node:retry → { event: "progress", node, data: { retry: true, attempt, reason } }
273
- * Other events (workflow:start, tool:*, route, workflow:end) are dropped.
375
+ * node:warning → { event: "progress", node, data: { warning: true, reason, fields } }
376
+ * route → { event: "progress", node: from, data: { route: { to, reason } } }
377
+ * Other events (workflow:start, tool:*, workflow:end) are dropped.
378
+ *
379
+ * The node:exit `data` carries the SHAPE-ONLY type-metric substrate for the
380
+ * node: eval verdicts (name + pass, never reasoning prose) and tool-call
381
+ * NAME + STATUS counts (never tool input/output). This is the same substrate
382
+ * the finish payload aggregates; streaming it per-node lets the cloud render
383
+ * progress without waiting for finish.
274
384
  *
275
385
  * Returns undefined when the handle is null (no cloud session) or the
276
386
  * token is missing. The observer captures `node:enter` timestamps in a
@@ -286,6 +396,45 @@ export async function finishCloudLifecycle(config, handle, results, durationMs,
286
396
  * 3. The Map is keyed by node id; collisions only happen if the same
287
397
  * node id runs concurrently, which the engine does not do today.
288
398
  */
399
+ /**
400
+ * Build the SHAPE-ONLY per-node substrate for a `node:exit` event's `data`.
401
+ *
402
+ * Emits eval verdicts (name + kind + pass, and reasoning only when the
403
+ * evaluator's reasoning is itself a machine verdict, which it is NOT here, so
404
+ * reasoning is omitted), tool-call NAME + STATUS counts, and usage totals.
405
+ * NEVER emits tool input/output, response text, or eval reasoning prose.
406
+ * Returns undefined when the node carries none of it.
407
+ */
408
+ function summarizeNodeResult(result) {
409
+ const data = {};
410
+ if (result.evals && result.evals.length > 0) {
411
+ // name + kind + pass only. `reasoning` can quote agent output or bug
412
+ // detail, so it is deliberately dropped.
413
+ data.evals = result.evals.map((e) => ({ name: e.name, kind: e.kind, pass: e.pass }));
414
+ }
415
+ if (result.toolCalls && result.toolCalls.length > 0) {
416
+ let error = 0;
417
+ const byName = {};
418
+ for (const call of result.toolCalls) {
419
+ if (call.status === "error")
420
+ error++;
421
+ const name = call.tool || "unknown";
422
+ byName[name] = (byName[name] ?? 0) + 1;
423
+ }
424
+ data.tool_calls = { total: result.toolCalls.length, error, by_name: byName };
425
+ }
426
+ if (result.usage) {
427
+ data.usage = {
428
+ total_cost_usd: result.usage.costUsd,
429
+ input_tokens: result.usage.inputTokens,
430
+ output_tokens: result.usage.outputTokens,
431
+ cache_read_tokens: result.usage.cacheReadTokens,
432
+ cache_creation_tokens: result.usage.cacheCreationTokens,
433
+ num_turns: result.usage.numTurns,
434
+ };
435
+ }
436
+ return Object.keys(data).length > 0 ? data : undefined;
437
+ }
289
438
  export function createCloudStreamObserver(config, handle) {
290
439
  if (!handle || !config.cloudToken)
291
440
  return undefined;
@@ -312,11 +461,13 @@ export function createCloudStreamObserver(config, handle) {
312
461
  const enter = enterTimes.get(event.node);
313
462
  const duration_ms = typeof enter === "number" ? Date.now() - enter : undefined;
314
463
  enterTimes.delete(event.node);
464
+ const data = summarizeNodeResult(event.result);
315
465
  fire({
316
466
  event: "exit",
317
467
  node: event.node,
318
468
  status: event.result.status,
319
469
  duration_ms,
470
+ ...(data ? { data } : {}),
320
471
  });
321
472
  break;
322
473
  }
@@ -324,7 +475,9 @@ export function createCloudStreamObserver(config, handle) {
324
475
  fire({
325
476
  event: "progress",
326
477
  node: event.node,
327
- data: { message: event.message },
478
+ // PRIVACY: event.message is SDK tool_use_summary prose (model-written)
479
+ // or "tool (Ns)". Never ship it; the heartbeat alone is the signal.
480
+ data: { progress: true },
328
481
  });
329
482
  break;
330
483
  }
@@ -332,13 +485,35 @@ export function createCloudStreamObserver(config, handle) {
332
485
  fire({
333
486
  event: "progress",
334
487
  node: event.node,
335
- data: { retry: true, attempt: event.attempt, reason: event.reason },
488
+ // PRIVACY: `reason` can carry model/SDK error prose; omit it.
489
+ data: { retry: true, attempt: event.attempt },
490
+ });
491
+ break;
492
+ }
493
+ case "node:warning": {
494
+ // Shape-only: reason is a short summary and fields are declared
495
+ // property NAMES, never values. See the node:warning docs in types.ts.
496
+ fire({
497
+ event: "progress",
498
+ node: event.node,
499
+ data: { warning: true, reason: event.reason, fields: event.fields },
500
+ });
501
+ break;
502
+ }
503
+ case "route": {
504
+ // Routing decision. `reason` is LLM-written prose and is NEVER
505
+ // forwarded; only the node ids (author-written workflow structure).
506
+ // Attributed to the `from` node.
507
+ fire({
508
+ event: "progress",
509
+ node: event.from,
510
+ data: { route: { from: event.from, to: event.to } },
336
511
  });
337
512
  break;
338
513
  }
339
514
  default:
340
- // Drop workflow:start, sources:resolved, tool:*, route,
341
- // workflow:end. Cloud only models the per-node lifecycle today.
515
+ // Drop workflow:start, sources:resolved, tool:*, workflow:end.
516
+ // Cloud only models the per-node lifecycle today.
342
517
  break;
343
518
  }
344
519
  }
@@ -4,6 +4,32 @@ const _require = createRequire(import.meta.url);
4
4
  const { version } = _require("../../package.json");
5
5
  // ── Cloud reporting ────────────────────────────────────────────────
6
6
  const CLOUD_URL_DEFAULT = "https://cloud.sweny.ai";
7
+ /**
8
+ * Reduce a raw finding to its shape-only classification. Keeps severity,
9
+ * novel-vs-duplicate, fix complexity, and affected-service COUNT. Drops
10
+ * `title`, `root_cause`, `fix_approach`, and `duplicate_of` — the fields that
11
+ * would leak the customer's private bug description, the raw root-cause
12
+ * analysis, the proposed fix prose, or a linkable existing-issue id.
13
+ */
14
+ function shapeFinding(f) {
15
+ const shaped = {
16
+ severity: typeof f.severity === "string" ? f.severity : undefined,
17
+ is_duplicate: f.is_duplicate === true,
18
+ };
19
+ if (typeof f.fix_complexity === "string")
20
+ shaped.fix_complexity = f.fix_complexity;
21
+ // COUNT only. Service names are LLM-written strings; never ship them.
22
+ if (Array.isArray(f.affected_services)) {
23
+ shaped.affected_services_count = f.affected_services.filter((s) => typeof s === "string").length;
24
+ }
25
+ return shaped;
26
+ }
27
+ const RECOMMENDATIONS = ["implement", "escalate", "skip"];
28
+ /** Clamp free LLM text to a closed enum so no prose can ride in `recommendation`. */
29
+ function toRecommendationEnum(v) {
30
+ const t = typeof v === "string" ? v.trim().toLowerCase() : "";
31
+ return RECOMMENDATIONS.includes(t) ? t : "other";
32
+ }
7
33
  /**
8
34
  * Opt-in run reporting to SWEny Cloud.
9
35
  *
@@ -24,8 +50,22 @@ export async function reportToCloud(results, durationMs, config, workflow) {
24
50
  const investigateData = results.get("investigate")?.data;
25
51
  const createPrData = results.get("create_pr")?.data;
26
52
  const createIssueData = results.get("create_issue")?.data ?? results.get("create-issue")?.data;
27
- const findings = investigateData?.findings ?? [];
53
+ const rawFindings = investigateData?.findings ?? [];
54
+ // SHAPE-ONLY findings. The raw finding carries `title`, `root_cause`, and
55
+ // `fix_approach` — free LLM prose about the customer's private bug and the
56
+ // proposed fix. That MUST NEVER leave the host (product rule: show don't
57
+ // store code). We ship only the classification metadata: severity, novel vs
58
+ // duplicate, fix complexity, and affected-service count. Mirrors the
59
+ // `inputs_shape` precedent in cloud-lifecycle.ts (key shape, never values).
60
+ const findings = rawFindings.map(shapeFinding);
28
61
  const hasFailed = [...results.values()].some((r) => r.status === "failed");
62
+ // Severity histogram — counts only, derived from the shape-only findings.
63
+ const severityCounts = {};
64
+ for (const f of findings) {
65
+ if (f.severity)
66
+ severityCounts[f.severity] = (severityCounts[f.severity] ?? 0) + 1;
67
+ }
68
+ const duplicateCount = findings.filter((f) => f.is_duplicate).length;
29
69
  const nodes = [...results.entries()].map(([id, r]) => ({
30
70
  id,
31
71
  name: id,
@@ -42,8 +82,11 @@ export async function reportToCloud(results, durationMs, config, workflow) {
42
82
  status: hasFailed ? "failed" : "completed",
43
83
  workflow,
44
84
  duration_ms: durationMs,
45
- recommendation: investigateData?.recommendation,
85
+ recommendation: investigateData?.recommendation === undefined ? undefined : toRecommendationEnum(investigateData.recommendation),
46
86
  findings,
87
+ findings_count: findings.length,
88
+ duplicate_count: duplicateCount,
89
+ severity_counts: severityCounts,
47
90
  highest_severity: investigateData?.highest_severity,
48
91
  novel_count: investigateData?.novel_count,
49
92
  pr_url: createPrData?.prUrl,
@@ -3,6 +3,13 @@
3
3
  * Sets `process.env[KEY]` only if not already defined (real env vars win).
4
4
  */
5
5
  export declare function loadDotenv(cwd?: string): void;
6
+ /**
7
+ * Copy agent sandbox / env-passthrough settings from `.sweny.yml` into the
8
+ * environment. Real env vars win (same precedence as {@link loadDotenv}).
9
+ * YAML booleans arrive as "true"/"false", which the sandbox mode parser
10
+ * treats as on/off.
11
+ */
12
+ export declare function applyAgentFileConfig(fileConfig: FileConfig, env?: NodeJS.ProcessEnv): void;
6
13
  /** Parsed config file — flat strings for scalar fields, arrays for list fields, objects for nested blocks. */
7
14
  export type FileConfig = Record<string, string | string[] | Record<string, unknown>>;
8
15
  /**
@@ -12,4 +19,4 @@ export type FileConfig = Record<string, string | string[] | Record<string, unkno
12
19
  */
13
20
  export declare function loadConfigFile(cwd?: string): FileConfig;
14
21
  /** Starter config written by `sweny new`. */
15
- 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 | gemini\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 MCP servers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Extend the coding agent with additional tools via MCP.\n# Value is a JSON object \u2014 each key is a server name you choose.\n# See https://docs.sweny.ai/advanced/mcp-servers/ for a full catalog with copy-paste configs.\n#\n# Example: GitHub MCP server (query PRs, issues, CI run logs)\n# mcp-servers-json: '{\"github\":{\"type\":\"stdio\",\"command\":\"npx\",\"args\":[\"-y\",\"@modelcontextprotocol/server-github@latest\"],\"env\":{\"GITHUB_PERSONAL_ACCESS_TOKEN\":\"ghp_...\"}}}'\n\n# \u2500\u2500 Local-only quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Uncomment to run without any external services (just an LLM API key):\n# observability-provider: file\n# log-file: ./sample-errors.json\n# issue-tracker-provider: file\n# source-control-provider: file\n# notification-provider: file\n# output-dir: .sweny/output\n\n# \u2500\u2500 Credentials (.env) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Copy the relevant block into your .env file and fill in the values.\n#\n# Claude (coding agent) \u2014 https://console.anthropic.com/settings/api-keys\n# ANTHROPIC_API_KEY=sk-ant-...\n#\n# GitHub (source control + issue tracker)\n# GITHUB_TOKEN=ghp_... # https://github.com/settings/tokens (repo + issues scopes)\n#\n# Datadog (observability) \u2014 https://app.datadoghq.com/organization-settings\n# DD_API_KEY=... # Organization Settings > API Keys\n# DD_APP_KEY=... # Organization Settings > Application Keys\n# DD_SITE=datadoghq.com # or datadoghq.eu, us3.datadoghq.com, etc.\n#\n# Sentry (observability) \u2014 https://sentry.io/settings/auth-tokens/\n# SENTRY_AUTH_TOKEN=sntrys_...\n# SENTRY_ORG=your-org-slug # from sentry.io/organizations/<slug>/\n# SENTRY_PROJECT=your-project # Project Settings > General > Project Slug\n#\n# Linear (issue tracker) \u2014 https://linear.app/settings/api\n# LINEAR_API_KEY=lin_api_...\n# LINEAR_TEAM_ID=... # Settings > Workspace > Teams > [team] > copy ID from URL\n# LINEAR_BUG_LABEL_ID=... # Settings > Labels > [label] > copy ID from URL\n#\n# Jira (issue tracker) \u2014 https://your-org.atlassian.net\n# JIRA_BASE_URL=https://your-org.atlassian.net\n# JIRA_EMAIL=you@company.com # your Atlassian account email\n# JIRA_API_TOKEN=... # https://id.atlassian.com/manage-profile/security/api-tokens\n#\n# Vercel (observability) \u2014 https://vercel.com/account/tokens\n# VERCEL_TOKEN=...\n# VERCEL_PROJECT_ID=prj_... # Project Settings > General > Project ID\n# VERCEL_TEAM_ID=team_... # optional, for team-owned projects\n#\n# Supabase (observability) \u2014 https://supabase.com/dashboard/account/tokens\n# SUPABASE_MANAGEMENT_KEY=...\n# SUPABASE_PROJECT_REF=... # Project Settings > General > Reference ID\n#\n# Netlify (observability) \u2014 https://app.netlify.com/user/applications#personal-access-tokens\n# NETLIFY_TOKEN=...\n# NETLIFY_SITE_ID=... # Site Settings > General > Site ID\n#\n# Fly.io (observability) \u2014 https://fly.io/user/personal_access_tokens\n# FLY_TOKEN=...\n# FLY_APP_NAME=... # the name of your Fly.io application\n#\n# Render (observability) \u2014 https://dashboard.render.com/u/settings\n# RENDER_API_KEY=...\n# RENDER_SERVICE_ID=srv-... # from your service's Settings page\n#\n# Prometheus (observability) \u2014 self-hosted or Grafana Cloud\n# PROMETHEUS_URL=http://prometheus.internal:9090\n# PROMETHEUS_TOKEN=... # optional, for secured instances\n#\n# PagerDuty (observability) \u2014 https://your-account.pagerduty.com/api_keys\n# PAGERDUTY_API_KEY=...\n#\n# Honeycomb (observability) \u2014 https://docs.honeycomb.io/api/\n# HONEYCOMB_API_KEY=...\n# HONEYCOMB_DATASET=... # dataset name (e.g. production)\n#\n# Heroku (observability) \u2014 https://devcenter.heroku.com/articles/platform-api-reference\n# HEROKU_API_KEY=... # https://dashboard.heroku.com/account\n# HEROKU_APP_NAME=... # the name of your Heroku application\n#\n# OpsGenie (observability) \u2014 https://support.atlassian.com/opsgenie/docs/api-key-management/\n# OPSGENIE_API_KEY=...\n# OPSGENIE_REGION=us # or eu for EU-hosted accounts\n#\n# Slack (notifications) \u2014 https://api.slack.com/apps\n# NOTIFICATION_WEBHOOK_URL=https://hooks.slack.com/services/...\n# # or use a bot token: SLACK_BOT_TOKEN=xoxb-...\n";
22
+ export declare const STARTER_CONFIG = "# .sweny.yml \u2014 SWEny project configuration\n# Commit this file. Secrets (API keys, tokens) go in .env (gitignored).\n#\n# Every key matches a CLI flag: \"time-range: 4h\" is the same as \"--time-range 4h\".\n# CLI flags override this file; env vars override this file; this file overrides defaults.\n\n# \u2500\u2500 Providers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# observability-provider: datadog # datadog | sentry | cloudwatch | splunk | elastic | newrelic | loki | prometheus | pagerduty | heroku | opsgenie | vercel | supabase | netlify | fly | render | file\n# issue-tracker-provider: github-issues # github-issues | linear | jira\n# source-control-provider: github # github | gitlab\n# coding-agent-provider: claude # claude (the only supported agent)\n# notification-provider: console # console | slack | teams | discord | email | webhook\n\n# \u2500\u2500 Investigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# time-range: 24h\n# severity-focus: errors\n# service-filter: \"*\"\n# investigation-depth: standard # quick | standard | thorough\n\n# \u2500\u2500 PR / branch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# base-branch: main\n# pr-labels: agent,triage,needs-review\n\n# \u2500\u2500 Paths \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# service-map-path: .github/service-map.yml\n# log-file: ./logs/errors.json # required when observability-provider is \"file\"\n\n# \u2500\u2500 Cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# cache-dir: .sweny/cache\n# cache-ttl: 86400\n\n# \u2500\u2500 Run reporting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Opt-in and not currently available: token minting is not yet exposed, so\n# leaving this unset means no reporting request is ever made.\n# cloud-token: sweny_pk_...\n# Or set SWENY_CLOUD_TOKEN in your environment.\n\n# \u2500\u2500 Agent sandbox \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# See https://docs.sweny.ai/advanced/agent-sandbox/\n# sandbox: off # off (local default) | auto (CI default: sandbox if supported, else warn) | strict\n# sandbox-allowed-domains: [internal.example.com] # extra hosts agent commands may reach\n# env-scope: off # off (local default: full env) | on (CI default: allowlist)\n# env-passthrough: [NPM_TOKEN] # extra env vars the agent may see when scoped (\"*\" = all)\n\n# \u2500\u2500 MCP servers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Extend the coding agent with additional tools via MCP.\n# Value is a JSON object \u2014 each key is a server name you choose.\n# See https://docs.sweny.ai/advanced/mcp-servers/ for a full catalog with copy-paste configs.\n#\n# Example: GitHub MCP server (query PRs, issues, CI run logs)\n# mcp-servers-json: '{\"github\":{\"type\":\"stdio\",\"command\":\"npx\",\"args\":[\"-y\",\"@modelcontextprotocol/server-github@latest\"],\"env\":{\"GITHUB_PERSONAL_ACCESS_TOKEN\":\"ghp_...\"}}}'\n\n# \u2500\u2500 Local-only quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Uncomment to run without any external services (just an LLM API key):\n# observability-provider: file\n# log-file: ./sample-errors.json\n# issue-tracker-provider: file\n# source-control-provider: file\n# notification-provider: file\n# output-dir: .sweny/output\n\n# \u2500\u2500 Credentials (.env) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Copy the relevant block into your .env file and fill in the values.\n#\n# Claude (coding agent) \u2014 https://console.anthropic.com/settings/api-keys\n# ANTHROPIC_API_KEY=sk-ant-...\n#\n# GitHub (source control + issue tracker)\n# GITHUB_TOKEN=ghp_... # https://github.com/settings/tokens (repo + issues scopes)\n#\n# Datadog (observability) \u2014 https://app.datadoghq.com/organization-settings\n# DD_API_KEY=... # Organization Settings > API Keys\n# DD_APP_KEY=... # Organization Settings > Application Keys\n# DD_SITE=datadoghq.com # or datadoghq.eu, us3.datadoghq.com, etc.\n#\n# Sentry (observability) \u2014 https://sentry.io/settings/auth-tokens/\n# SENTRY_AUTH_TOKEN=sntrys_...\n# SENTRY_ORG=your-org-slug # from sentry.io/organizations/<slug>/\n# SENTRY_PROJECT=your-project # Project Settings > General > Project Slug\n#\n# Linear (issue tracker) \u2014 https://linear.app/settings/api\n# LINEAR_API_KEY=lin_api_...\n# LINEAR_TEAM_ID=... # Settings > Workspace > Teams > [team] > copy ID from URL\n# LINEAR_BUG_LABEL_ID=... # Settings > Labels > [label] > copy ID from URL\n#\n# Jira (issue tracker) \u2014 https://your-org.atlassian.net\n# JIRA_BASE_URL=https://your-org.atlassian.net\n# JIRA_EMAIL=you@company.com # your Atlassian account email\n# JIRA_API_TOKEN=... # https://id.atlassian.com/manage-profile/security/api-tokens\n#\n# Vercel (observability) \u2014 https://vercel.com/account/tokens\n# VERCEL_TOKEN=...\n# VERCEL_PROJECT_ID=prj_... # Project Settings > General > Project ID\n# VERCEL_TEAM_ID=team_... # optional, for team-owned projects\n#\n# Supabase (observability) \u2014 https://supabase.com/dashboard/account/tokens\n# SUPABASE_MANAGEMENT_KEY=...\n# SUPABASE_PROJECT_REF=... # Project Settings > General > Reference ID\n#\n# Netlify (observability) \u2014 https://app.netlify.com/user/applications#personal-access-tokens\n# NETLIFY_TOKEN=...\n# NETLIFY_SITE_ID=... # Site Settings > General > Site ID\n#\n# Fly.io (observability) \u2014 https://fly.io/user/personal_access_tokens\n# FLY_TOKEN=...\n# FLY_APP_NAME=... # the name of your Fly.io application\n#\n# Render (observability) \u2014 https://dashboard.render.com/u/settings\n# RENDER_API_KEY=...\n# RENDER_SERVICE_ID=srv-... # from your service's Settings page\n#\n# Prometheus (observability) \u2014 self-hosted or Grafana Cloud\n# PROMETHEUS_URL=http://prometheus.internal:9090\n# PROMETHEUS_TOKEN=... # optional, for secured instances\n#\n# PagerDuty (observability) \u2014 https://your-account.pagerduty.com/api_keys\n# PAGERDUTY_API_KEY=...\n#\n# Honeycomb (observability) \u2014 https://docs.honeycomb.io/api/\n# HONEYCOMB_API_KEY=...\n# HONEYCOMB_DATASET=... # dataset name (e.g. production)\n#\n# Heroku (observability) \u2014 https://devcenter.heroku.com/articles/platform-api-reference\n# HEROKU_API_KEY=... # https://dashboard.heroku.com/account\n# HEROKU_APP_NAME=... # the name of your Heroku application\n#\n# OpsGenie (observability) \u2014 https://support.atlassian.com/opsgenie/docs/api-key-management/\n# OPSGENIE_API_KEY=...\n# OPSGENIE_REGION=us # or eu for EU-hosted accounts\n#\n# Slack (notifications) \u2014 https://api.slack.com/apps\n# NOTIFICATION_WEBHOOK_URL=https://hooks.slack.com/services/...\n# # or use a bot token: SLACK_BOT_TOKEN=xoxb-...\n";