@sweny-ai/core 0.1.170 → 0.1.172
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 +2 -1
- package/dist/agent-env.js +9 -3
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +3 -0
- package/dist/budget.js +5 -1
- package/dist/cli/check.js +12 -12
- package/dist/cli/comment-output.d.ts +4 -2
- package/dist/cli/comment-output.js +16 -8
- package/dist/cli/config.js +24 -24
- package/dist/cli/e2e.js +17 -17
- package/dist/cli/main.js +42 -26
- package/dist/cli/marketplace.js +3 -3
- package/dist/cli/new.js +11 -8
- package/dist/cli/output.d.ts +3 -2
- package/dist/cli/output.js +39 -37
- package/dist/cli/progress.d.ts +33 -0
- package/dist/cli/progress.js +69 -0
- package/dist/cli/publish.js +2 -2
- package/dist/cli/record.d.ts +66 -0
- package/dist/cli/record.js +344 -0
- package/dist/cli/renderer.d.ts +4 -0
- package/dist/cli/renderer.js +20 -13
- package/dist/cli/run-output.d.ts +13 -6
- package/dist/cli/run-output.js +36 -26
- package/dist/cli/runs.js +17 -11
- package/dist/cli/setup.js +7 -7
- package/dist/cli/style.d.ts +26 -0
- package/dist/cli/style.js +48 -0
- package/dist/cli/terminal.d.ts +47 -0
- package/dist/cli/terminal.js +123 -0
- package/dist/cli/ticket.d.ts +89 -0
- package/dist/cli/ticket.js +218 -0
- package/dist/cli/try.d.ts +19 -8
- package/dist/cli/try.js +97 -65
- package/dist/cli/upgrade.js +1 -1
- package/dist/executor.d.ts +8 -1
- package/dist/executor.js +20 -2
- package/dist/git-credentials.d.ts +106 -0
- package/dist/git-credentials.js +322 -0
- package/dist/harness/acp.js +3 -0
- package/dist/harness/claude-code.d.ts +7 -0
- package/dist/harness/claude-code.js +44 -3
- package/dist/harness/codex.js +3 -0
- package/dist/harness/pi.js +3 -0
- package/dist/harness/policy.d.ts +5 -0
- package/dist/harness/policy.js +14 -0
- package/dist/harness/sandbox-wrapper.d.ts +9 -2
- package/dist/harness/sandbox-wrapper.js +14 -4
- package/dist/harness/types.d.ts +10 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mermaid.d.ts +10 -5
- package/dist/mermaid.js +11 -13
- package/dist/safe-outputs.js +5 -0
- package/dist/skills/git-push.d.ts +62 -0
- package/dist/skills/git-push.js +132 -0
- package/dist/skills/github.d.ts +17 -0
- package/dist/skills/github.js +23 -0
- package/dist/theme.d.ts +185 -0
- package/dist/theme.js +183 -0
- package/dist/workflows/browser.js +2 -2
- package/dist/workflows/implement.yml +2 -1
- package/dist/workflows/triage.yml +2 -1
- package/package.json +6 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The receipt ticket: the run receipt as a small perforated card with a tear
|
|
3
|
+
* line and a policy stamp. Shown at the end of a run on a TTY, and at the top
|
|
4
|
+
* of the PR comment. CI logs, pipes and `--json` keep the plain receipt line.
|
|
5
|
+
*
|
|
6
|
+
* METADATA ONLY, like the receipt line: counts, durations, tokens, cost,
|
|
7
|
+
* policy facts. Never prompts, node outputs, tool inputs, or model prose.
|
|
8
|
+
*/
|
|
9
|
+
import { formatBudgetAmount, formatCost, formatDeciderCounts, formatReceiptDuration, formatTokenCount, policyParts, renderReceiptLine, } from "./run-output.js";
|
|
10
|
+
import { createPaint } from "./style.js";
|
|
11
|
+
import { colorEnabled, glyphsFor, richOutput, supportsUnicode, terminalColumns, ticketBoxFor } from "./terminal.js";
|
|
12
|
+
/** Widest ticket, in columns, frame included. */
|
|
13
|
+
export const TICKET_MAX_WIDTH = 48;
|
|
14
|
+
/** Narrowest ticket. Below this the fields stop being readable. */
|
|
15
|
+
export const TICKET_MIN_WIDTH = 32;
|
|
16
|
+
/** Left indent the CLI prints the ticket at. */
|
|
17
|
+
export const TICKET_INDENT = 2;
|
|
18
|
+
/** How long the stub sits empty before the stamp lands (the one animation frame). */
|
|
19
|
+
export const STAMP_DELAY_MS = 140;
|
|
20
|
+
const LABEL_WIDTH = 12;
|
|
21
|
+
/** `20260930-110000-3f9a2c` shows as `3f9a2c`; any other id as its first 8 characters. */
|
|
22
|
+
export function shortRunId(runId) {
|
|
23
|
+
const m = /-([0-9a-f]{6,})$/i.exec(runId);
|
|
24
|
+
return m ? m[1] : runId.slice(0, 8);
|
|
25
|
+
}
|
|
26
|
+
/** The ticket's status word. */
|
|
27
|
+
export function ticketStatus(data) {
|
|
28
|
+
if (data.crashed)
|
|
29
|
+
return "crashed";
|
|
30
|
+
if (data.summary.budget)
|
|
31
|
+
return "budget stop";
|
|
32
|
+
return data.summary.ok ? "passed" : "failed";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The policy stamp words, from what the run reported: `ENV SCOPED`,
|
|
36
|
+
* `SANDBOXED`. Empty when no node reported env scope or sandbox facts.
|
|
37
|
+
* `strong` is false when any fact is weaker than the default (an unscoped
|
|
38
|
+
* env, no sandbox), which the stamp shows in the warning color.
|
|
39
|
+
*/
|
|
40
|
+
export function stampWords(summary) {
|
|
41
|
+
const p = summary.policy;
|
|
42
|
+
const words = [];
|
|
43
|
+
let strong = true;
|
|
44
|
+
if (p?.envScope !== undefined) {
|
|
45
|
+
words.push(p.envScope ? "ENV SCOPED" : "ENV UNSCOPED");
|
|
46
|
+
strong &&= p.envScope;
|
|
47
|
+
}
|
|
48
|
+
if (p?.sandbox) {
|
|
49
|
+
const on = p.sandbox.mode !== "off" && p.sandbox.started;
|
|
50
|
+
words.push(on ? "SANDBOXED" : "UNSANDBOXED");
|
|
51
|
+
strong &&= on;
|
|
52
|
+
}
|
|
53
|
+
return { words, strong };
|
|
54
|
+
}
|
|
55
|
+
/** Field rows, label then value, metadata only. */
|
|
56
|
+
function ticketFields(s) {
|
|
57
|
+
const failed = Math.max(0, s.nodesTotal - s.nodesOk - s.nodesSkipped);
|
|
58
|
+
const nodes = [
|
|
59
|
+
`${s.nodesOk}/${s.nodesTotal}`,
|
|
60
|
+
...(failed > 0 ? [`${failed} failed`] : []),
|
|
61
|
+
...(s.nodesSkipped > 0 ? [`${s.nodesSkipped} skipped`] : []),
|
|
62
|
+
].join(", ");
|
|
63
|
+
const policy = policyParts(s.policy);
|
|
64
|
+
return [
|
|
65
|
+
["nodes", nodes],
|
|
66
|
+
["tool calls", String(s.toolCalls)],
|
|
67
|
+
["duration", formatReceiptDuration(s.durationMs)],
|
|
68
|
+
...(s.tokens !== undefined ? [["tokens", formatTokenCount(s.tokens)]] : []),
|
|
69
|
+
...(s.costUsd !== undefined ? [["cost", formatCost(s.costUsd)]] : []),
|
|
70
|
+
...(s.harness !== undefined && s.harness !== "claude-code" ? [["harness", s.harness]] : []),
|
|
71
|
+
...(s.degraded && s.degraded.length > 0 ? [["degraded", s.degraded.join(", ")]] : []),
|
|
72
|
+
...(s.decider ? [["decider", formatDeciderCounts(s.decider)]] : []),
|
|
73
|
+
...(s.budget
|
|
74
|
+
? [
|
|
75
|
+
["budget", `${formatBudgetAmount(s.budget)} (${s.budget.scope})`],
|
|
76
|
+
["stopped at", s.budget.node],
|
|
77
|
+
]
|
|
78
|
+
: []),
|
|
79
|
+
...(policy.length > 0 ? [["policy", policy.join(", ")]] : []),
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
/** Word-wrap to `width`, hard-breaking any word longer than the line. */
|
|
83
|
+
function wrap(text, width) {
|
|
84
|
+
const out = [];
|
|
85
|
+
let line = "";
|
|
86
|
+
for (let word of text.split(" ")) {
|
|
87
|
+
while (word.length > width) {
|
|
88
|
+
if (line) {
|
|
89
|
+
out.push(line);
|
|
90
|
+
line = "";
|
|
91
|
+
}
|
|
92
|
+
out.push(word.slice(0, width));
|
|
93
|
+
word = word.slice(width);
|
|
94
|
+
}
|
|
95
|
+
if (!line)
|
|
96
|
+
line = word;
|
|
97
|
+
else if (line.length + 1 + word.length <= width)
|
|
98
|
+
line += ` ${word}`;
|
|
99
|
+
else {
|
|
100
|
+
out.push(line);
|
|
101
|
+
line = word;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (line || out.length === 0)
|
|
105
|
+
out.push(line);
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
function clip(text, width, ellipsis) {
|
|
109
|
+
if (text.length <= width)
|
|
110
|
+
return text;
|
|
111
|
+
return text.slice(0, Math.max(0, width - ellipsis.length)) + ellipsis;
|
|
112
|
+
}
|
|
113
|
+
/** Ticket width for a terminal of `columns`: fits beside the indent, within the min and max. */
|
|
114
|
+
export function ticketWidth(columns) {
|
|
115
|
+
return Math.max(TICKET_MIN_WIDTH, Math.min(TICKET_MAX_WIDTH, columns - TICKET_INDENT));
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Render the ticket. Pure: same input, same rows. Every row is exactly the
|
|
119
|
+
* ticket width in visible columns.
|
|
120
|
+
*/
|
|
121
|
+
export function renderTicket(data, opts = {}) {
|
|
122
|
+
const unicode = opts.unicode ?? true;
|
|
123
|
+
const paint = opts.paint ?? createPaint(false);
|
|
124
|
+
const box = ticketBoxFor(unicode);
|
|
125
|
+
const g = glyphsFor(unicode);
|
|
126
|
+
const width = ticketWidth(opts.columns ?? 80);
|
|
127
|
+
const inner = width - 4;
|
|
128
|
+
const valueWidth = inner - LABEL_WIDTH;
|
|
129
|
+
const s = data.summary;
|
|
130
|
+
const frame = paint.muted;
|
|
131
|
+
const edge = (left, fill, right) => {
|
|
132
|
+
const run = fill.repeat(Math.ceil((width - 2) / fill.length)).slice(0, width - 2);
|
|
133
|
+
return frame(left + run + right);
|
|
134
|
+
};
|
|
135
|
+
/** One content row: `raw` sets the width, `painted` is what prints. */
|
|
136
|
+
const row = (raw, painted = raw) => `${frame(box.vertical)} ${painted}${" ".repeat(Math.max(0, inner - raw.length))} ${frame(box.vertical)}`;
|
|
137
|
+
const lines = [edge(box.topLeft, box.horizontal, box.topRight)];
|
|
138
|
+
// Status and run id.
|
|
139
|
+
const status = ticketStatus(data);
|
|
140
|
+
const good = status === "passed";
|
|
141
|
+
const tone = good ? paint.success : paint.error;
|
|
142
|
+
const left = `${good ? g.success : g.failure} ${status}`;
|
|
143
|
+
const right = data.runId ? `run ${shortRunId(data.runId)}` : "";
|
|
144
|
+
const gap = inner - left.length - right.length;
|
|
145
|
+
if (right && gap >= 2) {
|
|
146
|
+
lines.push(row(left + " ".repeat(gap) + right, paint.strong(tone(left)) + " ".repeat(gap) + paint.muted(right)));
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
lines.push(row(left, paint.strong(tone(left))));
|
|
150
|
+
}
|
|
151
|
+
const name = clip(data.workflow, inner, g.ellipsis);
|
|
152
|
+
lines.push(row(name));
|
|
153
|
+
lines.push(row(""));
|
|
154
|
+
// Fields, aligned: label column then value column; long values wrap under the value.
|
|
155
|
+
for (const [label, value] of ticketFields(s)) {
|
|
156
|
+
wrap(value, valueWidth).forEach((part, i) => {
|
|
157
|
+
const lab = (i === 0 ? label : "").padEnd(LABEL_WIDTH);
|
|
158
|
+
lines.push(row(lab + part, paint.muted(lab) + part));
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// Tear line and stub with the policy stamp.
|
|
162
|
+
let stampRow = -1;
|
|
163
|
+
const stamp = stampWords(s);
|
|
164
|
+
if (stamp.words.length > 0) {
|
|
165
|
+
lines.push(edge(box.tearLeft, box.tear, box.tearRight));
|
|
166
|
+
let text = `[ ${stamp.words.join(` ${g.sep} `)} ]`;
|
|
167
|
+
if (text.length > inner)
|
|
168
|
+
text = clip(stamp.words.join(` ${g.sep} `), inner, g.ellipsis);
|
|
169
|
+
const padL = Math.floor((inner - text.length) / 2);
|
|
170
|
+
const raw = " ".repeat(padL) + text;
|
|
171
|
+
stampRow = lines.length;
|
|
172
|
+
if (opts.stamp === false)
|
|
173
|
+
lines.push(row(""));
|
|
174
|
+
else
|
|
175
|
+
lines.push(row(raw, " ".repeat(padL) + paint.strong((stamp.strong ? paint.brand : paint.warning)(text))));
|
|
176
|
+
}
|
|
177
|
+
lines.push(edge(box.bottomLeft, box.horizontal, box.bottomRight));
|
|
178
|
+
return { lines, stampRow };
|
|
179
|
+
}
|
|
180
|
+
/** The ticket as plain text (no color, Unicode): what the PR comment carries. */
|
|
181
|
+
export function formatTicketText(data, columns = TICKET_MAX_WIDTH + TICKET_INDENT) {
|
|
182
|
+
return renderTicket(data, { columns }).lines.join("\n");
|
|
183
|
+
}
|
|
184
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
185
|
+
/**
|
|
186
|
+
* Print the receipt. A TTY (not CI) gets the ticket, its stamp landing one
|
|
187
|
+
* frame after the card; anything else gets the plain line. Both end with a
|
|
188
|
+
* blank line.
|
|
189
|
+
*/
|
|
190
|
+
export async function writeReceipt(summary, opts) {
|
|
191
|
+
const env = opts.env ?? process.env;
|
|
192
|
+
const { stream } = opts;
|
|
193
|
+
const color = opts.color ?? colorEnabled(stream, env);
|
|
194
|
+
const rich = opts.rich ?? richOutput(stream, env);
|
|
195
|
+
if (!rich) {
|
|
196
|
+
stream.write(` ${renderReceiptLine(summary, color)}\n\n`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const data = { summary, workflow: opts.workflow, runId: opts.runId, crashed: opts.crashed };
|
|
200
|
+
const base = {
|
|
201
|
+
columns: terminalColumns(stream, env),
|
|
202
|
+
unicode: opts.unicode ?? supportsUnicode(env),
|
|
203
|
+
paint: createPaint(color),
|
|
204
|
+
};
|
|
205
|
+
const pad = " ".repeat(TICKET_INDENT);
|
|
206
|
+
const full = renderTicket(data, base);
|
|
207
|
+
const animate = (opts.animate ?? true) && full.stampRow >= 0;
|
|
208
|
+
if (!animate) {
|
|
209
|
+
stream.write(full.lines.map((l) => pad + l).join("\n") + "\n\n");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const blank = renderTicket(data, { ...base, stamp: false });
|
|
213
|
+
stream.write(blank.lines.map((l) => pad + l).join("\n") + "\n");
|
|
214
|
+
await sleep(STAMP_DELAY_MS);
|
|
215
|
+
// Up to the stub row, redraw it with the stamp, back down below the ticket.
|
|
216
|
+
const up = full.lines.length - full.stampRow;
|
|
217
|
+
stream.write(`\x1B[${up}A\r\x1B[2K${pad}${full.lines[full.stampRow]}\x1B[${up}B\r\n`);
|
|
218
|
+
}
|
package/dist/cli/try.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* `sweny try`: a zero-credential demo (#475).
|
|
3
3
|
*
|
|
4
4
|
* Replays a recorded explain-repo run through the REAL executor, with a replay
|
|
5
|
-
* harness standing in for the agent, so the real progress
|
|
6
|
-
* (#460), receipt, policy
|
|
5
|
+
* harness standing in for the agent, so the real progress lines, answer block
|
|
6
|
+
* (#460), receipt ticket, policy stamp and PR comment (#396, #479) are what a
|
|
7
7
|
* user sees. No agent, no model, no network, no env credentials, no files
|
|
8
8
|
* written (no `.sweny/runs`, no journal). The environment handed to the
|
|
9
9
|
* executor is empty by construction.
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import type { Command } from "commander";
|
|
15
15
|
import type { AgentHarness } from "../harness/types.js";
|
|
16
16
|
import type { Workflow } from "../types.js";
|
|
17
|
+
import { type Paint } from "./style.js";
|
|
17
18
|
export interface TryFixtureNode {
|
|
18
19
|
/** Recorded wall-clock time of the node. */
|
|
19
20
|
duration_ms: number;
|
|
@@ -64,14 +65,16 @@ export declare function createReplayHarness(fixture: TryFixture, opts: {
|
|
|
64
65
|
paceMs: number;
|
|
65
66
|
}): AgentHarness;
|
|
66
67
|
export declare const TRY_BANNER_TITLE = "Recorded demo";
|
|
67
|
-
/** The
|
|
68
|
-
export declare const
|
|
68
|
+
/** The one command to run next: scaffolds the same workflow on the user's repo and says how to run it. */
|
|
69
|
+
export declare const TRY_NEXT_COMMAND = "npx @sweny-ai/core new --template explain-repo";
|
|
69
70
|
/** Appended to `sweny try --help`. */
|
|
70
71
|
export declare const TRY_HELP_NOTE: string;
|
|
71
|
-
|
|
72
|
-
export declare function
|
|
72
|
+
/** The tagline, then the demo notice as one dim line. */
|
|
73
|
+
export declare function formatTryBanner(fixture: TryFixture, paint?: Paint, unicode?: boolean): string;
|
|
74
|
+
/** The closing line: one command. */
|
|
75
|
+
export declare function formatTryNext(paint?: Paint): string;
|
|
73
76
|
export interface TryOptions {
|
|
74
|
-
/** Replay instantly. */
|
|
77
|
+
/** Replay instantly (and land the stamp without the one-frame pause). */
|
|
75
78
|
fast?: boolean;
|
|
76
79
|
/** Milliseconds each node takes to replay. Default 1200. Ignored with `fast`. */
|
|
77
80
|
paceMs?: number;
|
|
@@ -79,8 +82,16 @@ export interface TryOptions {
|
|
|
79
82
|
commentFile?: string;
|
|
80
83
|
/** Output sink. Default stdout. */
|
|
81
84
|
write?: (s: string) => void;
|
|
82
|
-
/**
|
|
85
|
+
/** Rich output: spinner, ticket, color. Default: stdout is a TTY. */
|
|
83
86
|
tty?: boolean;
|
|
87
|
+
/** Draw with Unicode. Default: what the terminal supports. */
|
|
88
|
+
unicode?: boolean;
|
|
89
|
+
/** Force ANSI color on or off. Default: on for a TTY that allows color. */
|
|
90
|
+
color?: boolean;
|
|
91
|
+
/** Force the receipt ticket (true) or the plain receipt line (false). Default: `tty`. */
|
|
92
|
+
rich?: boolean;
|
|
93
|
+
/** Terminal width. Default: stdout's. */
|
|
94
|
+
columns?: number;
|
|
84
95
|
fixture?: TryFixture;
|
|
85
96
|
}
|
|
86
97
|
export declare const DEFAULT_TRY_PACE_MS = 1200;
|
package/dist/cli/try.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* `sweny try`: a zero-credential demo (#475).
|
|
3
3
|
*
|
|
4
4
|
* Replays a recorded explain-repo run through the REAL executor, with a replay
|
|
5
|
-
* harness standing in for the agent, so the real progress
|
|
6
|
-
* (#460), receipt, policy
|
|
5
|
+
* harness standing in for the agent, so the real progress lines, answer block
|
|
6
|
+
* (#460), receipt ticket, policy stamp and PR comment (#396, #479) are what a
|
|
7
7
|
* user sees. No agent, no model, no network, no env credentials, no files
|
|
8
8
|
* written (no `.sweny/runs`, no journal). The environment handed to the
|
|
9
9
|
* executor is empty by construction.
|
|
@@ -14,15 +14,19 @@
|
|
|
14
14
|
import fs from "node:fs";
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
-
import chalk from "chalk";
|
|
18
17
|
import { parse as parseYaml } from "yaml";
|
|
19
18
|
import { execute } from "../executor.js";
|
|
20
19
|
import { validateParsed } from "../loader.js";
|
|
21
20
|
import { WORKFLOW_TEMPLATES } from "./templates.js";
|
|
22
21
|
import { formatFinalOutput, resolveFinalOutput } from "./final-output.js";
|
|
23
22
|
import { formatRunComment } from "./comment-output.js";
|
|
24
|
-
import { formatReceiptDuration,
|
|
25
|
-
import {
|
|
23
|
+
import { formatReceiptDuration, summarizeRun } from "./run-output.js";
|
|
24
|
+
import { DagRenderer } from "./renderer.js";
|
|
25
|
+
import { createNodeProgress, toolCallsText } from "./progress.js";
|
|
26
|
+
import { createPaint } from "./style.js";
|
|
27
|
+
import { colorEnabled, glyphsFor, supportsUnicode, terminalColumns } from "./terminal.js";
|
|
28
|
+
import { writeReceipt } from "./ticket.js";
|
|
29
|
+
import { SWENY_TAGLINE } from "../theme.js";
|
|
26
30
|
const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
27
31
|
/** Shape check with a message naming the first bad field. Throws on a malformed fixture. */
|
|
28
32
|
export function parseTryFixture(raw) {
|
|
@@ -152,34 +156,24 @@ export function createReplayHarness(fixture, opts) {
|
|
|
152
156
|
}
|
|
153
157
|
// ── Output ──────────────────────────────────────────────────────
|
|
154
158
|
export const TRY_BANNER_TITLE = "Recorded demo";
|
|
155
|
-
/** The
|
|
156
|
-
export const
|
|
157
|
-
"npm install -g @sweny-ai/core",
|
|
158
|
-
"sweny new --template explain-repo --yes",
|
|
159
|
-
"sweny workflow run .sweny/workflows/explain-repo.yml",
|
|
160
|
-
];
|
|
159
|
+
/** The one command to run next: scaffolds the same workflow on the user's repo and says how to run it. */
|
|
160
|
+
export const TRY_NEXT_COMMAND = "npx @sweny-ai/core new --template explain-repo";
|
|
161
161
|
/** Appended to `sweny try --help`. */
|
|
162
162
|
export const TRY_HELP_NOTE = "The recording is ILLUSTRATIVE SAMPLE DATA (a fictional repo, hand-written timings and token counts), not output from a real run. " +
|
|
163
163
|
"Nothing runs: no agent, no network, no credentials read, no files written.";
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
chalk.dim(fixture.illustrative
|
|
169
|
-
? " The sample data is illustrative, not output from your code. No agent, no network, no credentials."
|
|
170
|
-
: " No agent, no network, no credentials."),
|
|
171
|
-
];
|
|
172
|
-
return lines.map((l, i) => (i === 0 ? ` ${l}` : l)).join("\n");
|
|
173
|
-
}
|
|
174
|
-
export function formatTryNext() {
|
|
164
|
+
/** The tagline, then the demo notice as one dim line. */
|
|
165
|
+
export function formatTryBanner(fixture, paint = createPaint(false), unicode = true) {
|
|
166
|
+
const g = glyphsFor(unicode);
|
|
167
|
+
const data = fixture.illustrative ? ", illustrative data" : "";
|
|
175
168
|
return [
|
|
176
|
-
` ${
|
|
177
|
-
|
|
178
|
-
...TRY_NEXT_COMMANDS.map((cmd) => ` ${cmd}`),
|
|
179
|
-
"",
|
|
180
|
-
chalk.dim(" Needs a Claude login (`claude` signed in) or ANTHROPIC_API_KEY. Your run ends with the same receipt."),
|
|
169
|
+
` ${paint.brand(g.brand)} ${paint.strong("SWEny")} ${SWENY_TAGLINE}`,
|
|
170
|
+
paint.muted(` ${TRY_BANNER_TITLE} of ${fixture.workflow} on a sample repo (${fixture.sample_repo})${data}. No agent, network or credentials.`),
|
|
181
171
|
].join("\n");
|
|
182
172
|
}
|
|
173
|
+
/** The closing line: one command. */
|
|
174
|
+
export function formatTryNext(paint = createPaint(false)) {
|
|
175
|
+
return ` ${paint.muted("Run it on your repo:")} ${paint.strong(TRY_NEXT_COMMAND)}`;
|
|
176
|
+
}
|
|
183
177
|
export const DEFAULT_TRY_PACE_MS = 1200;
|
|
184
178
|
const silentLogger = { info() { }, warn() { }, error() { }, debug() { } };
|
|
185
179
|
/** Replay the recording. Returns the process exit code. */
|
|
@@ -193,72 +187,97 @@ export async function runTry(opts = {}) {
|
|
|
193
187
|
if (!workflow.nodes[id])
|
|
194
188
|
throw new Error(`try-fixture.json: node "${id}" is not in ${fixture.workflow}`);
|
|
195
189
|
}
|
|
196
|
-
|
|
197
|
-
const
|
|
190
|
+
const env = process.env;
|
|
191
|
+
const unicode = opts.unicode ?? supportsUnicode(env);
|
|
192
|
+
const color = opts.color ?? (tty && colorEnabled({ isTTY: true }, env));
|
|
193
|
+
const paint = createPaint(color);
|
|
194
|
+
const g = glyphsFor(unicode);
|
|
195
|
+
const columns = opts.columns ?? terminalColumns(process.stdout, env);
|
|
196
|
+
write(`\n${formatTryBanner(fixture, paint, unicode)}\n\n`);
|
|
197
|
+
const progress = createNodeProgress({
|
|
198
|
+
write,
|
|
199
|
+
live: tty,
|
|
200
|
+
unicode,
|
|
201
|
+
paint,
|
|
202
|
+
idWidth: Math.max(...Object.keys(fixture.nodes).map((id) => id.length)),
|
|
203
|
+
});
|
|
204
|
+
// The same events drive the DAG in the comment preview.
|
|
205
|
+
const dag = new DagRenderer(workflow, { legend: false, unicode });
|
|
198
206
|
const observer = (event) => {
|
|
207
|
+
dag.update(event);
|
|
199
208
|
switch (event.type) {
|
|
200
|
-
case "workflow:start":
|
|
201
|
-
write(` ${c.brand("▲")} ${chalk.bold(event.workflow)}\n\n`);
|
|
202
|
-
break;
|
|
203
209
|
case "node:enter":
|
|
204
|
-
|
|
205
|
-
if (tty)
|
|
206
|
-
write(` ${c.subtle("○")} ${chalk.dim(event.node)}…`);
|
|
210
|
+
progress.enter(event.node);
|
|
207
211
|
break;
|
|
208
|
-
case "node:progress":
|
|
209
|
-
|
|
210
|
-
ticks.set(event.node, n);
|
|
211
|
-
if (tty) {
|
|
212
|
-
write(`\r\x1B[2K ${c.subtle("○")} ${chalk.dim(event.node)}… ${chalk.dim(`${n} tool ${n === 1 ? "call" : "calls"}`)}`);
|
|
213
|
-
}
|
|
212
|
+
case "node:progress":
|
|
213
|
+
progress.tick(event.node);
|
|
214
214
|
break;
|
|
215
|
-
}
|
|
216
215
|
case "node:exit": {
|
|
217
216
|
const rec = fixture.nodes[event.node];
|
|
218
|
-
const ok = event.result.status === "success";
|
|
219
|
-
const icon = ok ? c.ok("✓") : c.fail("✗");
|
|
220
217
|
const calls = event.result.toolCalls.length;
|
|
221
|
-
|
|
222
|
-
|
|
218
|
+
// The recording's timing, not the replay's wall clock.
|
|
219
|
+
const detail = [formatReceiptDuration(rec?.duration_ms ?? 0), ...(calls > 0 ? [toolCallsText(calls)] : [])];
|
|
220
|
+
const status = event.result.status === "success" ? "success" : event.result.status === "failed" ? "failed" : "skipped";
|
|
221
|
+
progress.exit(event.node, status, detail.join(` ${g.sep} `));
|
|
223
222
|
break;
|
|
224
223
|
}
|
|
225
224
|
}
|
|
226
225
|
};
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
226
|
+
let executed;
|
|
227
|
+
try {
|
|
228
|
+
executed = await execute(workflow, {}, {
|
|
229
|
+
skills: new Map(),
|
|
230
|
+
harness: createReplayHarness(fixture, { paceMs }),
|
|
231
|
+
observer,
|
|
232
|
+
logger: silentLogger,
|
|
233
|
+
// Nothing for the executor to read: no credentials, no PATH, no config.
|
|
234
|
+
env: {},
|
|
235
|
+
offline: true,
|
|
236
|
+
harnessPolicy: "warn",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
progress.stop();
|
|
241
|
+
}
|
|
242
|
+
const { results, trace } = executed;
|
|
237
243
|
write("\n");
|
|
238
|
-
// The recording's timings, not the replay's wall clock.
|
|
239
244
|
const durationsMs = Object.fromEntries(Object.entries(fixture.nodes).map(([id, n]) => [id, n.duration_ms]));
|
|
240
245
|
const totalMs = Object.values(durationsMs).reduce((a, b) => a + b, 0);
|
|
241
246
|
const receipt = summarizeRun(results, totalMs, false, trace);
|
|
242
247
|
const answer = resolveFinalOutput(workflow, results);
|
|
243
248
|
if (answer)
|
|
244
249
|
write(`${formatFinalOutput(answer)}\n\n`);
|
|
245
|
-
|
|
250
|
+
// Compact preview of the PR comment: its heading and DAG here, its ticket (the receipt) below.
|
|
246
251
|
const comment = formatRunComment(workflow, results, receipt, { trace, durationsMs });
|
|
247
|
-
write(` ${
|
|
248
|
-
|
|
252
|
+
write(` ${paint.muted("On a pull request, CI posts this heading, this DAG and the receipt below:")}\n\n`);
|
|
253
|
+
const tone = receipt.ok ? paint.success : paint.error;
|
|
254
|
+
write(` ${tone(receipt.ok ? g.success : g.failure)} ${paint.strong(workflow.name)}\n\n`);
|
|
255
|
+
write(`${dag
|
|
256
|
+
.renderToString()
|
|
249
257
|
.split("\n")
|
|
250
|
-
.
|
|
251
|
-
.
|
|
258
|
+
.slice(2)
|
|
259
|
+
.map((l) => (l === "" ? l : ` ${l}`))
|
|
260
|
+
.join("\n")}\n\n`);
|
|
261
|
+
await writeReceipt(receipt, {
|
|
262
|
+
workflow: workflow.id,
|
|
263
|
+
runId: "demo",
|
|
264
|
+
stream: { write, isTTY: tty, columns },
|
|
265
|
+
env,
|
|
266
|
+
rich: opts.rich ?? tty,
|
|
267
|
+
color,
|
|
268
|
+
unicode,
|
|
269
|
+
animate: !opts.fast,
|
|
270
|
+
});
|
|
252
271
|
if (opts.commentFile) {
|
|
253
272
|
try {
|
|
254
273
|
fs.mkdirSync(path.dirname(path.resolve(opts.commentFile)), { recursive: true });
|
|
255
274
|
fs.writeFileSync(opts.commentFile, comment);
|
|
256
275
|
}
|
|
257
276
|
catch (err) {
|
|
258
|
-
process.stderr.write(`
|
|
277
|
+
process.stderr.write(` ${g.warning} could not write comment file: ${err instanceof Error ? err.message : err}\n`);
|
|
259
278
|
}
|
|
260
279
|
}
|
|
261
|
-
write(`${formatTryNext()}\n\n`);
|
|
280
|
+
write(`${formatTryNext(paint)}\n\n`);
|
|
262
281
|
return receipt.ok ? 0 : 1;
|
|
263
282
|
}
|
|
264
283
|
export function registerTryCommand(program) {
|
|
@@ -268,13 +287,26 @@ export function registerTryCommand(program) {
|
|
|
268
287
|
.option("--fast", "Replay instantly instead of pacing the progress")
|
|
269
288
|
.option("--pace <ms>", `Milliseconds each node takes to replay (default ${DEFAULT_TRY_PACE_MS})`)
|
|
270
289
|
.option("--comment-file <path>", "Also write the PR-comment markdown to <path>")
|
|
290
|
+
.option("--record <file.svg>", "Write the demo as an animated, self-contained SVG terminal recording to <file.svg>")
|
|
271
291
|
.addHelpText("after", `\n${TRY_HELP_NOTE}\n`)
|
|
272
292
|
.action(async (options) => {
|
|
293
|
+
if (options.record) {
|
|
294
|
+
try {
|
|
295
|
+
const { recordTrySvgToFile } = await import("./record.js");
|
|
296
|
+
const bytes = await recordTrySvgToFile(options.record);
|
|
297
|
+
process.stdout.write(` wrote ${options.record} (${bytes} bytes)\n`);
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
console.error(`\n could not record the demo: ${err instanceof Error ? err.message : err}\n`);
|
|
301
|
+
process.exitCode = 1;
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
273
305
|
let paceMs;
|
|
274
306
|
if (options.pace !== undefined) {
|
|
275
307
|
paceMs = Number(options.pace);
|
|
276
308
|
if (!Number.isFinite(paceMs) || paceMs < 0) {
|
|
277
|
-
console.error(
|
|
309
|
+
console.error(createPaint(colorEnabled(process.stderr)).error(`\n --pace must be a non-negative number of milliseconds, got "${options.pace}"\n`));
|
|
278
310
|
process.exitCode = 1;
|
|
279
311
|
return;
|
|
280
312
|
}
|
package/dist/cli/upgrade.js
CHANGED
|
@@ -154,7 +154,7 @@ export async function runUpgrade(options, deps) {
|
|
|
154
154
|
latest = await deps.fetchLatestVersion(tag);
|
|
155
155
|
}
|
|
156
156
|
catch (err) {
|
|
157
|
-
stderr.write(chalk.red(` Error: couldn't reach the npm registry
|
|
157
|
+
stderr.write(chalk.red(` Error: couldn't reach the npm registry - ${err instanceof Error ? err.message : String(err)}`) +
|
|
158
158
|
"\n");
|
|
159
159
|
stderr.write(chalk.dim(" Check your network connection and try again.") + "\n");
|
|
160
160
|
exit(1);
|
package/dist/executor.d.ts
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*
|
|
13
13
|
* This replaces ~8k lines of engine + recipe step code.
|
|
14
14
|
*/
|
|
15
|
-
import type { Workflow, Skill, Tool, Claude, Observer, Logger, ExecutionResult } from "./types.js";
|
|
15
|
+
import type { Workflow, Skill, Tool, Claude, Observer, NodeResult, Logger, ExecutionResult } from "./types.js";
|
|
16
|
+
import "./skills/git-push.js";
|
|
16
17
|
import type { HarnessPolicyMode } from "./harness/policy.js";
|
|
17
18
|
import type { AgentHarness } from "./harness/types.js";
|
|
18
19
|
import type { DeciderMode } from "./decider.js";
|
|
@@ -138,6 +139,12 @@ export declare class RouteEvaluationError extends Error {
|
|
|
138
139
|
readonly node: string;
|
|
139
140
|
constructor(node: string, message: string);
|
|
140
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* The bounded prompt context for one node: `input` plus its dependencies' entries.
|
|
144
|
+
* Exported for the property tests only; not part of the package API (index.ts does not re-export it).
|
|
145
|
+
* @internal
|
|
146
|
+
*/
|
|
147
|
+
export declare function buildBoundedContext(workflow: Workflow, nodeId: string, results: Map<string, NodeResult>, input: unknown, instruction: string): Record<string, unknown>;
|
|
141
148
|
/**
|
|
142
149
|
* Dry-run tool gate (#380). Only an explicit `access: "read"` passes; a tool
|
|
143
150
|
* with no `access` is treated as a write so new tools fail safe.
|
package/dist/executor.js
CHANGED
|
@@ -22,6 +22,9 @@ import { resolveExecutionModel } from "./model.js";
|
|
|
22
22
|
import { buildToolAliases } from "./skills/index.js";
|
|
23
23
|
import { validateWorkflow } from "./schema.js";
|
|
24
24
|
import { grantedAgentEnv, resolveAgentAccess } from "./agent-env.js";
|
|
25
|
+
import { gitCredentialWarning, scanGitCredentials } from "./git-credentials.js";
|
|
26
|
+
// #473: arms the sweny-side branch push in `github_create_pr` (Node only; skills/ stays browser-safe).
|
|
27
|
+
import "./skills/git-push.js";
|
|
25
28
|
import { fenceUntrusted } from "./untrusted.js";
|
|
26
29
|
import { asClaude } from "./harness/compat.js";
|
|
27
30
|
import { budgetGate, isToolClass, policyGate, resolveHarnessPolicy } from "./harness/policy.js";
|
|
@@ -89,6 +92,17 @@ export async function execute(workflow, input, options) {
|
|
|
89
92
|
// `budget` and the caller's (`--max-tokens`, `--max-cost`).
|
|
90
93
|
const budgetGuard = new BudgetGuard(minLimits(toLimits(workflow.budget), toLimits(options.budget)));
|
|
91
94
|
const harnessPolicy = options.harnessPolicy ?? resolveHarnessPolicy(runEnv);
|
|
95
|
+
// #473: a checkout that persisted a git credential (actions/checkout's
|
|
96
|
+
// default). Each harness masks it for read-only and staged nodes, or reports
|
|
97
|
+
// the node degraded; this says so once per run. Paths and keys, never values.
|
|
98
|
+
try {
|
|
99
|
+
const credentialWarning = gitCredentialWarning(scanGitCredentials(process.cwd(), { env: runEnv }));
|
|
100
|
+
if (credentialWarning)
|
|
101
|
+
logger.warn(credentialWarning);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// A scan failure never stops a run; each node scans again before it starts.
|
|
105
|
+
}
|
|
92
106
|
// Decision model (#357): shadow only. Null (the default) means no HTTP at all.
|
|
93
107
|
const shadow = createShadowDecider(workflow.decider, options.decider, runEnv, (m) => logger.warn(m));
|
|
94
108
|
if (shadow)
|
|
@@ -911,8 +925,12 @@ function contextDependencies(workflow, nodeId, instruction = "") {
|
|
|
911
925
|
}
|
|
912
926
|
return deps;
|
|
913
927
|
}
|
|
914
|
-
/**
|
|
915
|
-
|
|
928
|
+
/**
|
|
929
|
+
* The bounded prompt context for one node: `input` plus its dependencies' entries.
|
|
930
|
+
* Exported for the property tests only; not part of the package API (index.ts does not re-export it).
|
|
931
|
+
* @internal
|
|
932
|
+
*/
|
|
933
|
+
export function buildBoundedContext(workflow, nodeId, results, input, instruction) {
|
|
916
934
|
const deps = contextDependencies(workflow, nodeId, instruction);
|
|
917
935
|
return {
|
|
918
936
|
input,
|