@sweny-ai/core 0.1.205 → 0.1.207
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/cli/run-history.js +77 -14
- package/dist/cli/runs.d.ts +7 -4
- package/dist/cli/runs.js +49 -23
- package/dist/harness/claude-code.js +31 -2
- package/package.json +1 -1
package/dist/cli/run-history.js
CHANGED
|
@@ -201,18 +201,80 @@ export function recordRun(record, cwd = process.cwd()) {
|
|
|
201
201
|
pruneRuns(cwd);
|
|
202
202
|
return true;
|
|
203
203
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
204
|
+
const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
205
|
+
const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
|
|
206
|
+
const numOrNull = (v) => (typeof v === "number" && Number.isFinite(v) ? v : null);
|
|
207
|
+
const RUNGS = new Set(["expr", "decider", "agent"]);
|
|
208
|
+
/**
|
|
209
|
+
* A typed, allow-listed copy of a stored record, or null when it is not a run record. History is
|
|
210
|
+
* agent-writable, so a record is never passed through: only the documented fields are copied, numbers
|
|
211
|
+
* are made finite, and malformed node, route, harness, degraded and spend entries are dropped. Strings
|
|
212
|
+
* are kept as read (terminal safety is the printer's job, see `printableRun`).
|
|
213
|
+
*/
|
|
214
|
+
function parseRecord(v) {
|
|
215
|
+
if (!isObj(v))
|
|
216
|
+
return null;
|
|
217
|
+
if (v.schema_version !== RUN_HISTORY_SCHEMA_VERSION ||
|
|
218
|
+
typeof v.run_id !== "string" ||
|
|
219
|
+
typeof v.workflow_id !== "string" ||
|
|
220
|
+
typeof v.started_at !== "string" ||
|
|
221
|
+
typeof v.status !== "string" ||
|
|
222
|
+
!Array.isArray(v.nodes) ||
|
|
223
|
+
!Array.isArray(v.routes) ||
|
|
224
|
+
!isObj(v.totals)) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
const t = v.totals;
|
|
228
|
+
const out = {
|
|
229
|
+
schema_version: RUN_HISTORY_SCHEMA_VERSION,
|
|
230
|
+
run_id: v.run_id,
|
|
231
|
+
workflow_id: v.workflow_id,
|
|
232
|
+
workflow_hash: typeof v.workflow_hash === "string" ? v.workflow_hash : "",
|
|
233
|
+
started_at: v.started_at,
|
|
234
|
+
duration_ms: num(v.duration_ms),
|
|
235
|
+
status: v.status,
|
|
236
|
+
nodes: v.nodes
|
|
237
|
+
.filter((n) => isObj(n) && typeof n.id === "string" && typeof n.status === "string")
|
|
238
|
+
.map((n) => ({
|
|
239
|
+
id: n.id,
|
|
240
|
+
status: n.status,
|
|
241
|
+
duration_ms: num(n.duration_ms),
|
|
242
|
+
tool_calls: num(n.tool_calls),
|
|
243
|
+
tokens: numOrNull(n.tokens),
|
|
244
|
+
cost_usd: numOrNull(n.cost_usd),
|
|
245
|
+
retries: num(n.retries),
|
|
246
|
+
})),
|
|
247
|
+
routes: v.routes
|
|
248
|
+
.filter((e) => isObj(e) && typeof e.from === "string" && typeof e.to === "string")
|
|
249
|
+
.map((e) => ({
|
|
250
|
+
from: e.from,
|
|
251
|
+
to: e.to,
|
|
252
|
+
...(typeof e.rung === "string" && RUNGS.has(e.rung) ? { rung: e.rung } : {}),
|
|
253
|
+
})),
|
|
254
|
+
totals: {
|
|
255
|
+
nodes_total: num(t.nodes_total),
|
|
256
|
+
nodes_ok: num(t.nodes_ok),
|
|
257
|
+
nodes_skipped: num(t.nodes_skipped),
|
|
258
|
+
tool_calls: num(t.tool_calls),
|
|
259
|
+
tokens: numOrNull(t.tokens),
|
|
260
|
+
cost_usd: numOrNull(t.cost_usd),
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
if (isObj(v.harness) && typeof v.harness.id === "string" && typeof v.harness.version === "string") {
|
|
264
|
+
out.harness = { id: v.harness.id, version: v.harness.version };
|
|
265
|
+
}
|
|
266
|
+
if (Array.isArray(v.degraded))
|
|
267
|
+
out.degraded = v.degraded.filter((d) => typeof d === "string");
|
|
268
|
+
if (isObj(v.spend)) {
|
|
269
|
+
const spend = {};
|
|
270
|
+
for (const k of SPEND_KINDS) {
|
|
271
|
+
const e = Object.hasOwn(v.spend, k) ? v.spend[k] : undefined;
|
|
272
|
+
if (isObj(e))
|
|
273
|
+
spend[k] = { tokens: num(e.tokens), cost_usd: num(e.cost_usd), calls: num(e.calls) };
|
|
274
|
+
}
|
|
275
|
+
out.spend = spend;
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
216
278
|
}
|
|
217
279
|
/**
|
|
218
280
|
* All readable records, newest first. Corrupt files and unknown schema versions
|
|
@@ -238,8 +300,9 @@ export function listRuns(cwd = process.cwd()) {
|
|
|
238
300
|
if (!st.isFile() || st.size > RECORD_READ_MAX_BYTES)
|
|
239
301
|
continue;
|
|
240
302
|
const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
241
|
-
|
|
242
|
-
|
|
303
|
+
const rec = parseRecord(parsed);
|
|
304
|
+
if (rec && RUN_ID_RE.test(rec.run_id) && f === `${rec.run_id}.json`)
|
|
305
|
+
out.push(rec);
|
|
243
306
|
}
|
|
244
307
|
catch {
|
|
245
308
|
// corrupt record: skip
|
package/dist/cli/runs.d.ts
CHANGED
|
@@ -10,12 +10,15 @@ import { type RunRecord } from "./run-history.js";
|
|
|
10
10
|
import { type AnswersComparison } from "./run-answers.js";
|
|
11
11
|
export declare function formatAge(startedAt: string, nowMs?: number): string;
|
|
12
12
|
/**
|
|
13
|
-
* A copy of a history record that is safe to print. `.sweny/runs/` is writable by an
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* A copy of a history record that is safe to print and to export. `.sweny/runs/` is writable by an
|
|
14
|
+
* agent, so the copy is built from an explicit allow-list of typed fields, never by spreading the
|
|
15
|
+
* record: unknown keys are dropped at every level, every string goes through {@link sanitizeTerminal}
|
|
16
|
+
* (control sequences stripped, length capped), every number is coerced finite, and malformed node and
|
|
17
|
+
* route entries are skipped. The table, the diff and every `--json` output use it.
|
|
17
18
|
*/
|
|
18
19
|
export declare function printableRun(r: RunRecord): RunRecord;
|
|
20
|
+
/** `sweny runs --json`: the validated records, never the raw ones. */
|
|
21
|
+
export declare function formatRunsJson(records: RunRecord[]): string;
|
|
19
22
|
export declare function formatRunsTable(records: RunRecord[], color: boolean, nowMs?: number, unicode?: boolean): string;
|
|
20
23
|
export interface RunDiffOptions {
|
|
21
24
|
/** Answer comparison for the pair; when set the diff leads with it and ends on its summary line. */
|
package/dist/cli/runs.js
CHANGED
|
@@ -51,23 +51,31 @@ function pad(s, w, right = false) {
|
|
|
51
51
|
// ── History records are untrusted ───────────────────────────────
|
|
52
52
|
const finite = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
|
|
53
53
|
const finiteOrNull = (v) => (typeof v === "number" && Number.isFinite(v) ? v : null);
|
|
54
|
+
const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
55
|
+
const RUNG = new Set(["expr", "decider", "agent"]);
|
|
54
56
|
/**
|
|
55
|
-
* A copy of a history record that is safe to print. `.sweny/runs/` is writable by an
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
57
|
+
* A copy of a history record that is safe to print and to export. `.sweny/runs/` is writable by an
|
|
58
|
+
* agent, so the copy is built from an explicit allow-list of typed fields, never by spreading the
|
|
59
|
+
* record: unknown keys are dropped at every level, every string goes through {@link sanitizeTerminal}
|
|
60
|
+
* (control sequences stripped, length capped), every number is coerced finite, and malformed node and
|
|
61
|
+
* route entries are skipped. The table, the diff and every `--json` output use it.
|
|
59
62
|
*/
|
|
60
63
|
export function printableRun(r) {
|
|
61
64
|
const t = (v, max = 64) => sanitizeTerminal(v, max);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
const rec = r;
|
|
66
|
+
const list = (v) => (Array.isArray(v) ? v : []);
|
|
67
|
+
const totals = isObj(rec.totals) ? rec.totals : {};
|
|
68
|
+
const out = {
|
|
69
|
+
schema_version: finite(rec.schema_version),
|
|
70
|
+
run_id: t(rec.run_id),
|
|
71
|
+
workflow_id: t(rec.workflow_id),
|
|
72
|
+
workflow_hash: t(rec.workflow_hash),
|
|
73
|
+
started_at: t(rec.started_at),
|
|
74
|
+
duration_ms: finite(rec.duration_ms),
|
|
75
|
+
status: t(rec.status, 16),
|
|
76
|
+
nodes: list(rec.nodes)
|
|
77
|
+
.filter(isObj)
|
|
78
|
+
.map((n) => ({
|
|
71
79
|
id: t(n.id),
|
|
72
80
|
status: t(n.status, 16),
|
|
73
81
|
duration_ms: finite(n.duration_ms),
|
|
@@ -76,17 +84,34 @@ export function printableRun(r) {
|
|
|
76
84
|
cost_usd: finiteOrNull(n.cost_usd),
|
|
77
85
|
retries: finite(n.retries),
|
|
78
86
|
})),
|
|
79
|
-
routes:
|
|
87
|
+
routes: list(rec.routes)
|
|
88
|
+
.filter((e) => isObj(e) && typeof e.from === "string" && typeof e.to === "string")
|
|
89
|
+
.map((e) => ({
|
|
90
|
+
from: t(e.from),
|
|
91
|
+
to: t(e.to),
|
|
92
|
+
...(typeof e.rung === "string" && RUNG.has(e.rung) ? { rung: e.rung } : {}),
|
|
93
|
+
})),
|
|
80
94
|
totals: {
|
|
81
|
-
nodes_total: finite(
|
|
82
|
-
nodes_ok: finite(
|
|
83
|
-
nodes_skipped: finite(
|
|
84
|
-
tool_calls: finite(
|
|
85
|
-
tokens: finiteOrNull(
|
|
86
|
-
cost_usd: finiteOrNull(
|
|
95
|
+
nodes_total: finite(totals.nodes_total),
|
|
96
|
+
nodes_ok: finite(totals.nodes_ok),
|
|
97
|
+
nodes_skipped: finite(totals.nodes_skipped),
|
|
98
|
+
tool_calls: finite(totals.tool_calls),
|
|
99
|
+
tokens: finiteOrNull(totals.tokens),
|
|
100
|
+
cost_usd: finiteOrNull(totals.cost_usd),
|
|
87
101
|
},
|
|
88
|
-
...(r.spend && typeof r.spend === "object" ? { spend: printableSpend(r.spend) } : {}),
|
|
89
102
|
};
|
|
103
|
+
if (isObj(rec.harness))
|
|
104
|
+
out.harness = { id: t(rec.harness.id), version: t(rec.harness.version) };
|
|
105
|
+
if (Array.isArray(rec.degraded)) {
|
|
106
|
+
out.degraded = rec.degraded.filter((d) => typeof d === "string").map((d) => t(d));
|
|
107
|
+
}
|
|
108
|
+
if (isObj(rec.spend))
|
|
109
|
+
out.spend = printableSpend(rec.spend);
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
/** `sweny runs --json`: the validated records, never the raw ones. */
|
|
113
|
+
export function formatRunsJson(records) {
|
|
114
|
+
return JSON.stringify(records.map(printableRun), null, 2) + "\n";
|
|
90
115
|
}
|
|
91
116
|
/** The record's split, or its totals as all node spend when it has none. */
|
|
92
117
|
function splitOf(r) {
|
|
@@ -432,7 +457,8 @@ export function runRunsDiff(refs, opts, io) {
|
|
|
432
457
|
const { a, b } = picked;
|
|
433
458
|
const answers = compareAnswers(readRunAnswers(a.run_id, io.cwd), readRunAnswers(b.run_id, io.cwd));
|
|
434
459
|
if (opts.json) {
|
|
435
|
-
|
|
460
|
+
const body = { schema_version: 1, a: printableRun(a), b: printableRun(b), answers };
|
|
461
|
+
io.stdout.write(JSON.stringify(body, null, 2) + "\n");
|
|
436
462
|
return 0;
|
|
437
463
|
}
|
|
438
464
|
io.stdout.write("\n" + formatRunDiff(a, b, colorEnabled(io.stdout, env), { answers, unicode: supportsUnicode(env) }) + "\n");
|
|
@@ -458,7 +484,7 @@ export function registerRunsCommand(program) {
|
|
|
458
484
|
runs = runs.filter((r) => r.workflow_id === opts.workflow);
|
|
459
485
|
runs = runs.slice(0, limit);
|
|
460
486
|
if (opts.json) {
|
|
461
|
-
process.stdout.write(
|
|
487
|
+
process.stdout.write(formatRunsJson(runs));
|
|
462
488
|
return;
|
|
463
489
|
}
|
|
464
490
|
process.stdout.write("\n" + formatRunsTable(runs, colorEnabled(process.stdout), Date.now(), supportsUnicode()) + "\n");
|
|
@@ -528,12 +528,19 @@ export class ClaudeCodeHarness {
|
|
|
528
528
|
// `sweny tool-bridge` stdio shim instead, so no in-process server is built.
|
|
529
529
|
const useBridge = this.toolBridge && tools.length > 0;
|
|
530
530
|
const sdkTools = useBridge ? [] : tools.map((t) => coreToolToSdkTool(t, this.defaultContext));
|
|
531
|
-
// Create in-process MCP server
|
|
531
|
+
// Create in-process MCP server. `alwaysLoad`: Claude Code defers every MCP
|
|
532
|
+
// server's tools behind ToolSearch by default and starts MCP servers
|
|
533
|
+
// without waiting, so a node's skill tools were not on the model's first
|
|
534
|
+
// turn and it could answer without them (dogfood run 42884113267: the
|
|
535
|
+
// pr-risk-review scope node called only StructuredOutput). These are the
|
|
536
|
+
// few tools the node was given to do its job; they load up front, and the
|
|
537
|
+
// query waits for the server before the first turn.
|
|
532
538
|
const mcpServer = useBridge
|
|
533
539
|
? undefined
|
|
534
540
|
: createSdkMcpServer({
|
|
535
541
|
name: "sweny-core",
|
|
536
542
|
tools: sdkTools,
|
|
543
|
+
alwaysLoad: true,
|
|
537
544
|
});
|
|
538
545
|
let bridge;
|
|
539
546
|
// Build prompt. Context (workflow input such as issues/alerts, plus prior
|
|
@@ -565,6 +572,14 @@ export class ClaudeCodeHarness {
|
|
|
565
572
|
const allMcpServers = mcpWithheld(!!readOnly, agentAccess)
|
|
566
573
|
? {}
|
|
567
574
|
: { ...this.defaultMcpServers, ...opts.mcpServers, ...this.mcpServers };
|
|
575
|
+
// A server the node's own skills declare (stdio or http) carries tools the
|
|
576
|
+
// node's checks require, so it loads up front like sweny-core below
|
|
577
|
+
// (Claude Code otherwise defers them behind ToolSearch). An explicit
|
|
578
|
+
// `alwaysLoad` in the server config wins.
|
|
579
|
+
for (const name of Object.keys(opts.mcpServers ?? {})) {
|
|
580
|
+
if (allMcpServers[name])
|
|
581
|
+
allMcpServers[name] = { alwaysLoad: true, ...allMcpServers[name] };
|
|
582
|
+
}
|
|
568
583
|
if (useBridge) {
|
|
569
584
|
// Only `tools` (already allow/deny and dry-run filtered by the
|
|
570
585
|
// executor) are exposed. Under readOnly it is still the only MCP
|
|
@@ -575,7 +590,8 @@ export class ClaudeCodeHarness {
|
|
|
575
590
|
logger: this.logger,
|
|
576
591
|
...(this.toolBridgeShim ? { shimCommand: this.toolBridgeShim } : {}),
|
|
577
592
|
});
|
|
578
|
-
|
|
593
|
+
// Same reason as the in-process server: the node's tools load up front.
|
|
594
|
+
allMcpServers["sweny-core"] = { ...bridge.mcpServer, alwaysLoad: true };
|
|
579
595
|
}
|
|
580
596
|
else if (sdkTools.length > 0) {
|
|
581
597
|
allMcpServers["sweny-core"] = mcpServer;
|
|
@@ -618,6 +634,19 @@ export class ClaudeCodeHarness {
|
|
|
618
634
|
},
|
|
619
635
|
});
|
|
620
636
|
for await (const message of stream) {
|
|
637
|
+
if (message.type === "system" && message.subtype === "init") {
|
|
638
|
+
// What the agent was actually given: the tool list (--verbose) and,
|
|
639
|
+
// loudly, a node tool server that did not come up.
|
|
640
|
+
const init = message;
|
|
641
|
+
if (Array.isArray(init.tools))
|
|
642
|
+
this.logger.debug(` agent tools: ${init.tools.join(", ")}`);
|
|
643
|
+
const core = Array.isArray(init.mcp_servers)
|
|
644
|
+
? init.mcp_servers.find((s) => s?.name === "sweny-core")
|
|
645
|
+
: undefined;
|
|
646
|
+
if (allMcpServers["sweny-core"] && core && core.status !== "connected") {
|
|
647
|
+
this.logger.warn(` sweny-core tool server is ${core.status} at start; the node's skill tools may be missing`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
621
650
|
if (message.type === "tool_progress") {
|
|
622
651
|
const tp = message;
|
|
623
652
|
if (tp.tool_name && typeof tp.elapsed_time_seconds === "number") {
|