@sweny-ai/core 0.1.169 → 0.1.171

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/agent-env.d.ts +2 -1
  2. package/dist/agent-env.js +9 -3
  3. package/dist/browser.d.ts +2 -0
  4. package/dist/browser.js +3 -0
  5. package/dist/budget.js +5 -1
  6. package/dist/cli/comment-output.d.ts +4 -2
  7. package/dist/cli/comment-output.js +16 -8
  8. package/dist/cli/main.js +42 -26
  9. package/dist/cli/new.js +3 -0
  10. package/dist/cli/output.d.ts +3 -2
  11. package/dist/cli/output.js +39 -37
  12. package/dist/cli/progress.d.ts +33 -0
  13. package/dist/cli/progress.js +69 -0
  14. package/dist/cli/renderer.d.ts +4 -0
  15. package/dist/cli/renderer.js +20 -13
  16. package/dist/cli/run-output.d.ts +13 -6
  17. package/dist/cli/run-output.js +36 -26
  18. package/dist/cli/runs.js +17 -11
  19. package/dist/cli/style.d.ts +26 -0
  20. package/dist/cli/style.js +48 -0
  21. package/dist/cli/terminal.d.ts +47 -0
  22. package/dist/cli/terminal.js +123 -0
  23. package/dist/cli/ticket.d.ts +89 -0
  24. package/dist/cli/ticket.js +218 -0
  25. package/dist/cli/try.d.ts +15 -8
  26. package/dist/cli/try.js +84 -65
  27. package/dist/executor.d.ts +8 -1
  28. package/dist/executor.js +21 -2
  29. package/dist/git-credentials.d.ts +106 -0
  30. package/dist/git-credentials.js +322 -0
  31. package/dist/harness/acp.js +3 -0
  32. package/dist/harness/claude-code.d.ts +7 -0
  33. package/dist/harness/claude-code.js +44 -3
  34. package/dist/harness/codex.js +3 -0
  35. package/dist/harness/pi.js +3 -0
  36. package/dist/harness/policy.d.ts +5 -0
  37. package/dist/harness/policy.js +14 -0
  38. package/dist/harness/sandbox-wrapper.d.ts +9 -2
  39. package/dist/harness/sandbox-wrapper.js +14 -4
  40. package/dist/harness/types.d.ts +10 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +2 -0
  43. package/dist/mermaid.d.ts +10 -5
  44. package/dist/mermaid.js +11 -13
  45. package/dist/safe-outputs.js +5 -0
  46. package/dist/skills/git-push.d.ts +62 -0
  47. package/dist/skills/git-push.js +132 -0
  48. package/dist/skills/github.d.ts +17 -0
  49. package/dist/skills/github.js +23 -0
  50. package/dist/testing.d.ts +16 -12
  51. package/dist/testing.js +45 -20
  52. package/dist/theme.d.ts +185 -0
  53. package/dist/theme.js +183 -0
  54. package/dist/types.d.ts +2 -0
  55. package/dist/workflows/browser.js +2 -2
  56. package/dist/workflows/implement.yml +2 -1
  57. package/dist/workflows/triage.yml +2 -1
  58. package/package.json +6 -1
@@ -0,0 +1,123 @@
1
+ /**
2
+ * What the terminal can show: color, Unicode, width, live redraws.
3
+ *
4
+ * Every check reads the env and stream passed in, so specs can pin each
5
+ * case. Defaults read the real process.
6
+ */
7
+ import { GLYPHS, SPINNER_FRAMES, TICKET_BOX } from "../theme.js";
8
+ /** The narrowest layout SWEny designs for. Narrower terminals still work, they just wrap. */
9
+ export const MIN_COLUMNS = 40;
10
+ /**
11
+ * True when the terminal can draw box-drawing characters and the glyph set.
12
+ *
13
+ * `SWENY_ASCII=1` forces ASCII; `SWENY_UNICODE=1` forces Unicode. Otherwise a
14
+ * locale that names a charset decides (`LC_ALL`, then `LC_CTYPE`, then
15
+ * `LANG`): UTF-8 is Unicode, `C`/`POSIX`/anything else is ASCII. The Linux
16
+ * console (`TERM=linux`) is ASCII. With no locale set, POSIX terminals are
17
+ * assumed Unicode; Windows is Unicode only in terminals known to support it.
18
+ */
19
+ export function supportsUnicode(env = process.env, platform = process.platform) {
20
+ if (truthy(env.SWENY_ASCII))
21
+ return false;
22
+ if (truthy(env.SWENY_UNICODE))
23
+ return true;
24
+ if (env.TERM === "linux")
25
+ return false;
26
+ const locale = env.LC_ALL || env.LC_CTYPE || env.LANG;
27
+ if (locale)
28
+ return /utf-?8/i.test(locale);
29
+ if (platform !== "win32")
30
+ return true;
31
+ return Boolean(env.WT_SESSION ||
32
+ env.TERMINUS_SUBLIME ||
33
+ env.ConEmuTask === "{cmd::Cmder}" ||
34
+ env.TERM_PROGRAM === "vscode" ||
35
+ env.TERM === "xterm-256color" ||
36
+ env.TERM === "alacritty" ||
37
+ env.TERMINAL_EMULATOR === "JetBrains-JediTerm");
38
+ }
39
+ /**
40
+ * True when output to `stream` should carry ANSI color. `NO_COLOR` (any
41
+ * non-empty value) always wins; then `FORCE_COLOR` (anything but `0`); then a
42
+ * TTY that is not `TERM=dumb`.
43
+ */
44
+ export function colorEnabled(stream, env = process.env) {
45
+ if (env.NO_COLOR)
46
+ return false;
47
+ if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== "")
48
+ return env.FORCE_COLOR !== "0";
49
+ if (env.TERM === "dumb")
50
+ return false;
51
+ return Boolean(stream?.isTTY);
52
+ }
53
+ /**
54
+ * True when `stream` gets the rich view (the ticket, redraws, a spinner):
55
+ * a TTY, not CI, not dumb. Everything else gets plain lines.
56
+ */
57
+ export function richOutput(stream, env = process.env) {
58
+ return Boolean(stream?.isTTY) && !truthy(env.CI) && env.TERM !== "dumb";
59
+ }
60
+ /** Usable width of `stream`: its column count, else `COLUMNS`, else 80. */
61
+ export function terminalColumns(stream, env = process.env) {
62
+ const fromStream = stream?.columns;
63
+ if (typeof fromStream === "number" && fromStream > 0)
64
+ return fromStream;
65
+ const fromEnv = Number(env.COLUMNS);
66
+ if (Number.isInteger(fromEnv) && fromEnv > 0)
67
+ return fromEnv;
68
+ return 80;
69
+ }
70
+ export function glyphsFor(unicode) {
71
+ return unicode ? GLYPHS.unicode : GLYPHS.ascii;
72
+ }
73
+ export function ticketBoxFor(unicode) {
74
+ return unicode ? TICKET_BOX.unicode : TICKET_BOX.ascii;
75
+ }
76
+ export function spinnerFramesFor(unicode) {
77
+ return unicode ? SPINNER_FRAMES.unicode : SPINNER_FRAMES.ascii;
78
+ }
79
+ /** The glyphs for the current process. */
80
+ export function glyphs(env = process.env) {
81
+ return glyphsFor(supportsUnicode(env));
82
+ }
83
+ function truthy(v) {
84
+ return v !== undefined && v !== "" && v !== "0" && v.toLowerCase() !== "false";
85
+ }
86
+ /**
87
+ * Box-drawing and glyph fallbacks for text drawn with Unicode (the DAG
88
+ * renderer). Each maps to a single ASCII column so alignment holds.
89
+ */
90
+ const ASCII_FALLBACK = {
91
+ "┌": "+",
92
+ "┐": "+",
93
+ "└": "+",
94
+ "┘": "+",
95
+ "├": "+",
96
+ "┤": "+",
97
+ "┬": "+",
98
+ "┴": "+",
99
+ "┼": "+",
100
+ "╭": "+",
101
+ "╮": "+",
102
+ "╰": "+",
103
+ "╯": "+",
104
+ "─": "-",
105
+ "┄": "-",
106
+ "╌": "-",
107
+ "│": "|",
108
+ "┆": ":",
109
+ "▼": "v",
110
+ "▶": ">",
111
+ [GLYPHS.unicode.success]: GLYPHS.ascii.success,
112
+ [GLYPHS.unicode.failure]: GLYPHS.ascii.failure,
113
+ [GLYPHS.unicode.skipped]: GLYPHS.ascii.skipped,
114
+ [GLYPHS.unicode.pending]: GLYPHS.ascii.pending,
115
+ [GLYPHS.unicode.running]: GLYPHS.ascii.running,
116
+ [GLYPHS.unicode.brand]: GLYPHS.ascii.brand,
117
+ [GLYPHS.unicode.sep]: GLYPHS.ascii.sep,
118
+ };
119
+ const ASCII_FALLBACK_RE = new RegExp(`[${Object.keys(ASCII_FALLBACK).join("")}]`, "g");
120
+ /** Replace single-column Unicode drawing characters with their ASCII stand-ins. */
121
+ export function toAsciiDrawing(text) {
122
+ return text.replace(ASCII_FALLBACK_RE, (ch) => ASCII_FALLBACK[ch] ?? ch);
123
+ }
@@ -0,0 +1,89 @@
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 type { RunSummary } from "./run-output.js";
10
+ import { type Paint } from "./style.js";
11
+ /** Widest ticket, in columns, frame included. */
12
+ export declare const TICKET_MAX_WIDTH = 48;
13
+ /** Narrowest ticket. Below this the fields stop being readable. */
14
+ export declare const TICKET_MIN_WIDTH = 32;
15
+ /** Left indent the CLI prints the ticket at. */
16
+ export declare const TICKET_INDENT = 2;
17
+ /** How long the stub sits empty before the stamp lands (the one animation frame). */
18
+ export declare const STAMP_DELAY_MS = 140;
19
+ export interface TicketData {
20
+ summary: RunSummary;
21
+ /** Workflow id, shown under the status. */
22
+ workflow: string;
23
+ /** Full run id; the ticket shows its short hash. */
24
+ runId?: string;
25
+ /** The run crashed before finishing (not a node failure). */
26
+ crashed?: boolean;
27
+ }
28
+ export interface TicketRenderOptions {
29
+ /** Columns available, indent included. Default 80. The ticket fits in `columns - 2`. */
30
+ columns?: number;
31
+ /** Box drawing and glyphs in Unicode (default) or ASCII. */
32
+ unicode?: boolean;
33
+ /** Painters. Default: no color. */
34
+ paint?: Paint;
35
+ /** Draw the policy stamp. False leaves the stub empty: the frame before the stamp lands. Default true. */
36
+ stamp?: boolean;
37
+ }
38
+ export interface RenderedTicket {
39
+ /** The ticket, one string per row, no indent. */
40
+ lines: string[];
41
+ /** Index of the stamp row in `lines`, or -1 when the run reported no policy facts. */
42
+ stampRow: number;
43
+ }
44
+ /** `20260930-110000-3f9a2c` shows as `3f9a2c`; any other id as its first 8 characters. */
45
+ export declare function shortRunId(runId: string): string;
46
+ /** The ticket's status word. */
47
+ export declare function ticketStatus(data: TicketData): "passed" | "failed" | "budget stop" | "crashed";
48
+ /**
49
+ * The policy stamp words, from what the run reported: `ENV SCOPED`,
50
+ * `SANDBOXED`. Empty when no node reported env scope or sandbox facts.
51
+ * `strong` is false when any fact is weaker than the default (an unscoped
52
+ * env, no sandbox), which the stamp shows in the warning color.
53
+ */
54
+ export declare function stampWords(summary: RunSummary): {
55
+ words: string[];
56
+ strong: boolean;
57
+ };
58
+ /** Ticket width for a terminal of `columns`: fits beside the indent, within the min and max. */
59
+ export declare function ticketWidth(columns: number): number;
60
+ /**
61
+ * Render the ticket. Pure: same input, same rows. Every row is exactly the
62
+ * ticket width in visible columns.
63
+ */
64
+ export declare function renderTicket(data: TicketData, opts?: TicketRenderOptions): RenderedTicket;
65
+ /** The ticket as plain text (no color, Unicode): what the PR comment carries. */
66
+ export declare function formatTicketText(data: TicketData, columns?: number): string;
67
+ export interface ReceiptStream {
68
+ write(s: string): unknown;
69
+ isTTY?: boolean;
70
+ columns?: number;
71
+ }
72
+ export interface WriteReceiptOptions extends Omit<TicketData, "summary"> {
73
+ stream: ReceiptStream;
74
+ env?: Record<string, string | undefined>;
75
+ /** Land the stamp one frame after the ticket. Default true on a rich stream. */
76
+ animate?: boolean;
77
+ /** Override the rich (ticket) or plain (one line) choice. Default: `richOutput(stream, env)`. */
78
+ rich?: boolean;
79
+ /** Override color. Default: `colorEnabled(stream, env)`. */
80
+ color?: boolean;
81
+ /** Override Unicode. Default: `supportsUnicode(env)`. */
82
+ unicode?: boolean;
83
+ }
84
+ /**
85
+ * Print the receipt. A TTY (not CI) gets the ticket, its stamp landing one
86
+ * frame after the card; anything else gets the plain line. Both end with a
87
+ * blank line.
88
+ */
89
+ export declare function writeReceipt(summary: RunSummary, opts: WriteReceiptOptions): Promise<void>;
@@ -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 line, answer block
6
- * (#460), receipt, policy segment and PR-comment markdown (#396) are what a
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 real commands, same as the README quickstart. */
68
- export declare const TRY_NEXT_COMMANDS: readonly ["npm install -g @sweny-ai/core", "sweny new --template explain-repo --yes", "sweny workflow run .sweny/workflows/explain-repo.yml"];
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
- export declare function formatTryBanner(fixture: TryFixture): string;
72
- export declare function formatTryNext(): string;
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,12 @@ export interface TryOptions {
79
82
  commentFile?: string;
80
83
  /** Output sink. Default stdout. */
81
84
  write?: (s: string) => void;
82
- /** Rewrite progress lines in place. Default: stdout is a TTY. */
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
+ /** Terminal width. Default: stdout's. */
90
+ columns?: number;
84
91
  fixture?: TryFixture;
85
92
  }
86
93
  export declare const DEFAULT_TRY_PACE_MS = 1200;