@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.
- package/dist/agent-env.d.ts +171 -0
- package/dist/agent-env.js +393 -0
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +1 -1
- package/dist/claude.d.ts +61 -3
- package/dist/claude.js +187 -27
- package/dist/cli/check.d.ts +42 -1
- package/dist/cli/check.js +130 -8
- package/dist/cli/cloud-lifecycle.d.ts +24 -29
- package/dist/cli/cloud-lifecycle.js +187 -12
- package/dist/cli/cloud-report.js +45 -2
- package/dist/cli/config-file.d.ts +8 -1
- package/dist/cli/config-file.js +42 -1
- package/dist/cli/config.js +11 -14
- package/dist/cli/e2e.d.ts +22 -0
- package/dist/cli/e2e.js +31 -4
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +138 -46
- package/dist/cli/marketplace.d.ts +3 -1
- package/dist/cli/marketplace.js +2 -2
- package/dist/cli/new.d.ts +15 -0
- package/dist/cli/new.js +139 -65
- package/dist/cli/publish.js +9 -0
- package/dist/cli/run-output.d.ts +59 -0
- package/dist/cli/run-output.js +189 -0
- package/dist/cli/skill-env.d.ts +34 -0
- package/dist/cli/skill-env.js +64 -0
- package/dist/cli/templates.js +587 -0
- package/dist/cli/workflow-input.d.ts +41 -0
- package/dist/cli/workflow-input.js +56 -0
- package/dist/executor.d.ts +17 -1
- package/dist/executor.js +119 -9
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/mermaid.d.ts +6 -0
- package/dist/mermaid.js +10 -5
- package/dist/schema.d.ts +218 -188
- package/dist/schema.js +24 -3
- package/dist/skills/betterstack.js +4 -0
- package/dist/skills/datadog.js +3 -0
- package/dist/skills/github.js +8 -0
- package/dist/skills/linear.js +8 -0
- package/dist/skills/notification.js +3 -0
- package/dist/skills/sentry.js +4 -0
- package/dist/skills/slack.js +2 -0
- package/dist/skills/supabase.js +9 -0
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +9 -3
- package/dist/types.d.ts +99 -2
- package/dist/types.js +17 -0
- package/dist/untrusted.d.ts +32 -0
- package/dist/untrusted.js +46 -0
- package/dist/workflows/browser.js +2 -2
- package/dist/workflows/implement.yml +2 -0
- package/dist/workflows/triage.yml +3 -1
- 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 === "
|
|
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 (
|
|
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
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
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
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
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
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
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
|
-
|
|
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: {
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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:*,
|
|
341
|
-
//
|
|
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
|
}
|
package/dist/cli/cloud-report.js
CHANGED
|
@@ -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
|
|
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
|
|
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";
|