@sweny-ai/core 0.1.171 → 0.1.173

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/new.js CHANGED
@@ -328,7 +328,7 @@ export function inferObservability(skills) {
328
328
  */
329
329
  export function buildSwenyYml(sourceControl, observability, issueTracker) {
330
330
  const lines = [];
331
- lines.push("# .sweny.yml — SWEny project configuration");
331
+ lines.push("# .sweny.yml - SWEny project configuration");
332
332
  lines.push("# Secrets (API keys, tokens) go in .env (gitignored).");
333
333
  lines.push("# Docs: https://docs.sweny.ai/cli");
334
334
  lines.push("");
@@ -345,7 +345,7 @@ export function buildSwenyYml(sourceControl, observability, issueTracker) {
345
345
  */
346
346
  export function buildEnvTemplate(credentials) {
347
347
  const lines = [];
348
- lines.push("# .env — SWEny credentials (DO NOT COMMIT)");
348
+ lines.push("# .env - SWEny credentials (DO NOT COMMIT)");
349
349
  lines.push("# Fill in each value, then run: sweny check");
350
350
  lines.push("");
351
351
  for (const cred of credentials) {
@@ -746,7 +746,7 @@ export async function runNew(options) {
746
746
  summaryLines.push("");
747
747
  }
748
748
  if (hasExistingConfig) {
749
- summaryLines.push(chalk.dim("(.sweny.yml already exists — keeping your existing config)"));
749
+ summaryLines.push(chalk.dim("(.sweny.yml already exists - keeping your existing config)"));
750
750
  summaryLines.push("");
751
751
  }
752
752
  summaryLines.push(chalk.bold(hasExistingConfig ? "Workflow uses:" : "Inferred from workflow:"));
@@ -773,7 +773,7 @@ export async function runNew(options) {
773
773
  p.log.success("Created .sweny.yml");
774
774
  }
775
775
  else {
776
- p.log.info(".sweny.yml already exists — keeping existing config");
776
+ p.log.info(".sweny.yml already exists - keeping existing config");
777
777
  }
778
778
  // 2. Append missing .env keys
779
779
  const envExistedBefore = fs.existsSync(path.join(cwd, ".env"));
@@ -786,7 +786,7 @@ export async function runNew(options) {
786
786
  p.log.success(`Appended ${addedCount} new key(s) to .env`);
787
787
  }
788
788
  else {
789
- p.log.info(".env already contains all required keys — skipped");
789
+ p.log.info(".env already contains all required keys - skipped");
790
790
  }
791
791
  // The pack's Actions trigger (#474): never overwritten, skipped with --no-ci.
792
792
  const installTrigger = () => {
@@ -815,7 +815,7 @@ export async function runNew(options) {
815
815
  if (p.isCancel(overwrite))
816
816
  cancel();
817
817
  if (!overwrite) {
818
- p.log.info("Workflow file preserved — no changes");
818
+ p.log.info("Workflow file preserved - no changes");
819
819
  p.outro("Done.");
820
820
  return;
821
821
  }
@@ -903,8 +903,8 @@ async function runCustomWorkflowBuilder(skills) {
903
903
  const action = await p.select({
904
904
  message: "Looks good?",
905
905
  options: [
906
- { value: "accept", label: "Yes — use this workflow" },
907
- { value: "refine", label: "Refine — describe what to change" },
906
+ { value: "accept", label: "Yes - use this workflow" },
907
+ { value: "refine", label: "Refine - describe what to change" },
908
908
  { value: "cancel", label: "Cancel" },
909
909
  ],
910
910
  });
@@ -216,7 +216,7 @@ async function publishWorkflow() {
216
216
  p.log.error(`Validation failed:\n${validation.errors.map((e) => ` - ${e}`).join("\n")}`);
217
217
  return null;
218
218
  }
219
- p.log.success(`Validated: ${chalk.cyan(validation.name)} — ${validation.nodeCount} nodes, ${validation.edgeCount} edges`);
219
+ p.log.success(`Validated: ${chalk.cyan(validation.name)} - ${validation.nodeCount} nodes, ${validation.edgeCount} edges`);
220
220
  if (validation.warnings.length > 0) {
221
221
  for (const warn of validation.warnings) {
222
222
  p.log.warn(warn);
@@ -312,7 +312,7 @@ async function selectOutputMode(type, id, sourcePath, meta) {
312
312
  const hasGh = ghAvailable();
313
313
  const options = [
314
314
  ...(hasGh
315
- ? [{ value: "pr", label: "Open GitHub PR", hint: "recommended — forks marketplace repo and opens a PR" }]
315
+ ? [{ value: "pr", label: "Open GitHub PR", hint: "recommended - forks marketplace repo and opens a PR" }]
316
316
  : []),
317
317
  { value: "save", label: "Save submission file", hint: "saves to ./sweny-publish/" },
318
318
  ];
@@ -0,0 +1,66 @@
1
+ /**
2
+ * `sweny try --record <file.svg>`: the demo as an animated terminal SVG.
3
+ *
4
+ * Runs the real `sweny try` replay (instant, no TTY) with color forced on,
5
+ * captures the ANSI text, and lays it out as a slate terminal window whose
6
+ * lines appear on a CSS keyframe timeline: the command, a spinner per node
7
+ * until it settles, the answer, the receipt ticket with its stamp
8
+ * landing last. Then it loops.
9
+ *
10
+ * Self-contained by construction: inline CSS only, a monospace font stack, no
11
+ * external fonts, images, links or scripts. Deterministic: no clock, no ids,
12
+ * no randomness; the same fixture and theme always yield the same bytes, so
13
+ * CI can regenerate the file and fail when the committed copy is stale.
14
+ * `prefers-reduced-motion` shows the finished frame with no animation.
15
+ */
16
+ import { type TryFixture } from "./try.js";
17
+ /** Terminal width the demo is rendered at (the ticket and DAG are designed for 80). */
18
+ export declare const RECORD_COLUMNS = 80;
19
+ /** Lines longer than this wrap in the window, like a terminal would. */
20
+ export declare const RECORD_WRAP = 96;
21
+ /** Width of one terminal column in the window, in px. Every segment is placed at column * CELL_W. */
22
+ export declare const CELL_W = 7.85;
23
+ /** Left edge of column 0. */
24
+ export declare const TEXT_X = 24;
25
+ export interface Style {
26
+ fg?: string;
27
+ bold?: boolean;
28
+ dim?: boolean;
29
+ underline?: boolean;
30
+ }
31
+ export interface Cell {
32
+ ch: string;
33
+ style: Style;
34
+ }
35
+ /** One line of ANSI text as styled cells. Cursor and erase sequences are dropped. */
36
+ export declare function parseAnsiLine(line: string): Cell[];
37
+ /** Soft-wrap a row of cells at `max` columns, on spaces, continuation indented two past the first. */
38
+ export declare function wrapCells(cells: Cell[], max: number): Cell[][];
39
+ export interface Row {
40
+ cells: Cell[];
41
+ /** Vertical slot. A spinner shares the slot of the line that replaces it. */
42
+ slot: number;
43
+ /** Seconds at which the row appears. */
44
+ from: number;
45
+ /** Seconds at which it disappears again (spinners). Absent: stays until the loop restarts. */
46
+ to?: number;
47
+ spinner?: boolean;
48
+ }
49
+ /** Lay captured output lines on the timeline. Pure. */
50
+ export declare function buildTimeline(lines: string[], fixture: TryFixture): {
51
+ rows: Row[];
52
+ slots: number;
53
+ total: number;
54
+ };
55
+ /** Render the timeline as a self-contained animated SVG. Pure and deterministic. */
56
+ export declare function renderRecordingSvg(lines: string[], fixture: TryFixture): string;
57
+ /**
58
+ * The PR-comment preview (heading plus DAG) sits between the answer and the
59
+ * ticket. The recording drops it to stay a README-sized hero: tagline, nodes,
60
+ * answer, ticket.
61
+ */
62
+ export declare function dropCommentPreview(lines: string[]): string[];
63
+ /** Run the demo and return the SVG text. */
64
+ export declare function recordTrySvg(fixture?: TryFixture): Promise<string>;
65
+ /** Record to `file`, creating its directory. Returns the byte size written. */
66
+ export declare function recordTrySvgToFile(file: string, fixture?: TryFixture): Promise<number>;
@@ -0,0 +1,344 @@
1
+ /**
2
+ * `sweny try --record <file.svg>`: the demo as an animated terminal SVG.
3
+ *
4
+ * Runs the real `sweny try` replay (instant, no TTY) with color forced on,
5
+ * captures the ANSI text, and lays it out as a slate terminal window whose
6
+ * lines appear on a CSS keyframe timeline: the command, a spinner per node
7
+ * until it settles, the answer, the receipt ticket with its stamp
8
+ * landing last. Then it loops.
9
+ *
10
+ * Self-contained by construction: inline CSS only, a monospace font stack, no
11
+ * external fonts, images, links or scripts. Deterministic: no clock, no ids,
12
+ * no randomness; the same fixture and theme always yield the same bytes, so
13
+ * CI can regenerate the file and fail when the committed copy is stale.
14
+ * `prefers-reduced-motion` shows the finished frame with no animation.
15
+ */
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import chalk from "chalk";
19
+ import { GLYPHS, PALETTE, ROLE_COLORS, SPINNER_FRAMES, SURFACE_COLORS } from "../theme.js";
20
+ import { loadTryFixture, runTry } from "./try.js";
21
+ /** Terminal width the demo is rendered at (the ticket and DAG are designed for 80). */
22
+ export const RECORD_COLUMNS = 80;
23
+ /** Lines longer than this wrap in the window, like a terminal would. */
24
+ export const RECORD_WRAP = 96;
25
+ const FONT_SIZE = 13;
26
+ /** Width of one terminal column in the window, in px. Every segment is placed at column * CELL_W. */
27
+ export const CELL_W = 7.85;
28
+ /** Left edge of column 0. */
29
+ export const TEXT_X = 24;
30
+ const NBSP = "\u00A0";
31
+ const LINE_H = 19;
32
+ const PAD_X = TEXT_X;
33
+ const BAR_H = 38;
34
+ const PAD_TOP = 18;
35
+ const PAD_BOTTOM = 22;
36
+ const WINDOW_W = Math.ceil(RECORD_WRAP * CELL_W + PAD_X * 2);
37
+ const FONT_STACK = "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace";
38
+ const SECONDS_PER_LINE = 0.06;
39
+ const SECONDS_PER_BLANK = 0.25;
40
+ const SECONDS_STAMP_DELAY = 0.5;
41
+ const SECONDS_HOLD = 5;
42
+ const BASIC_COLORS = {
43
+ 30: PALETTE.slate900,
44
+ 31: ROLE_COLORS.error,
45
+ 32: ROLE_COLORS.success,
46
+ 33: ROLE_COLORS.warning,
47
+ 34: ROLE_COLORS.brand,
48
+ 35: PALETTE.blue300,
49
+ 36: ROLE_COLORS.info,
50
+ 37: SURFACE_COLORS.dark.text,
51
+ 90: ROLE_COLORS.muted,
52
+ 91: ROLE_COLORS.error,
53
+ 92: ROLE_COLORS.success,
54
+ 93: ROLE_COLORS.warning,
55
+ 94: ROLE_COLORS.info,
56
+ 95: PALETTE.blue300,
57
+ 96: ROLE_COLORS.info,
58
+ 97: PALETTE.white,
59
+ };
60
+ const hex = (n) => n.toString(16).padStart(2, "0");
61
+ /** Apply one SGR parameter list to a style. */
62
+ function applySgr(style, params) {
63
+ const next = { ...style };
64
+ for (let i = 0; i < params.length; i++) {
65
+ const p = params[i];
66
+ if (p === 0)
67
+ return {};
68
+ else if (p === 1)
69
+ next.bold = true;
70
+ else if (p === 2)
71
+ next.dim = true;
72
+ else if (p === 22) {
73
+ delete next.bold;
74
+ delete next.dim;
75
+ }
76
+ else if (p === 4)
77
+ next.underline = true;
78
+ else if (p === 24)
79
+ delete next.underline;
80
+ else if (p === 39)
81
+ delete next.fg;
82
+ else if (p === 38 && params[i + 1] === 2) {
83
+ next.fg = `#${hex(params[i + 2] ?? 0)}${hex(params[i + 3] ?? 0)}${hex(params[i + 4] ?? 0)}`;
84
+ i += 4;
85
+ }
86
+ else if (p === 38 && params[i + 1] === 5) {
87
+ next.fg = SURFACE_COLORS.dark.text;
88
+ i += 2;
89
+ }
90
+ else if (BASIC_COLORS[p])
91
+ next.fg = BASIC_COLORS[p];
92
+ }
93
+ return next;
94
+ }
95
+ /** One line of ANSI text as styled cells. Cursor and erase sequences are dropped. */
96
+ export function parseAnsiLine(line) {
97
+ const cells = [];
98
+ let style = {};
99
+ const re = /\x1B\[([0-9;]*)([A-Za-z])/g;
100
+ let last = 0;
101
+ const push = (text) => {
102
+ for (const ch of text.replace(/\r/g, ""))
103
+ cells.push({ ch, style });
104
+ };
105
+ for (let m = re.exec(line); m; m = re.exec(line)) {
106
+ push(line.slice(last, m.index));
107
+ if (m[2] === "m")
108
+ style = applySgr(style, m[1] === "" ? [0] : m[1].split(";").map(Number));
109
+ last = m.index + m[0].length;
110
+ }
111
+ push(line.slice(last));
112
+ return cells;
113
+ }
114
+ const sameStyle = (a, b) => a.fg === b.fg && !!a.bold === !!b.bold && !!a.dim === !!b.dim && !!a.underline === !!b.underline;
115
+ /** Soft-wrap a row of cells at `max` columns, on spaces, continuation indented two past the first. */
116
+ export function wrapCells(cells, max) {
117
+ const rows = [];
118
+ const indent = cells.findIndex((c) => c.ch !== " ");
119
+ const pad = " ".repeat(Math.max(0, indent) + 2);
120
+ let rest = cells;
121
+ while (rest.length > max) {
122
+ let cut = -1;
123
+ for (let i = max; i > pad.length; i--) {
124
+ if (rest[i].ch === " ") {
125
+ cut = i;
126
+ break;
127
+ }
128
+ }
129
+ if (cut < 0)
130
+ cut = max;
131
+ rows.push(rest.slice(0, cut));
132
+ let tail = rest.slice(cut);
133
+ while (tail.length > 0 && tail[0].ch === " ")
134
+ tail = tail.slice(1);
135
+ rest = [...[...pad].map((ch) => ({ ch, style: {} })), ...tail];
136
+ }
137
+ rows.push(rest);
138
+ return rows;
139
+ }
140
+ const plainText = (cells) => cells.map((c) => c.ch).join("");
141
+ function textCells(text, style = {}) {
142
+ return [...text].map((ch) => ({ ch, style }));
143
+ }
144
+ /** Lay captured output lines on the timeline. Pure. */
145
+ export function buildTimeline(lines, fixture) {
146
+ const rows = [];
147
+ const nodeIds = Object.keys(fixture.nodes);
148
+ const settledPrefix = (id) => ` ${GLYPHS.unicode.success} ${id}`;
149
+ const spinnerGlyph = SPINNER_FRAMES.unicode[3];
150
+ let t = 0.5;
151
+ let slot = 0;
152
+ // The command being typed.
153
+ rows.push({
154
+ cells: [...textCells("$ ", { fg: ROLE_COLORS.muted }), ...textCells("sweny try", { bold: true })],
155
+ slot: slot++,
156
+ from: t,
157
+ });
158
+ t += 0.9;
159
+ for (const line of lines) {
160
+ const cells = parseAnsiLine(line);
161
+ const text = plainText(cells);
162
+ const id = nodeIds.find((n) => text.startsWith(settledPrefix(n)));
163
+ if (id) {
164
+ const seconds = 0.8 + Math.min(1.6, fixture.nodes[id].duration_ms / 12000);
165
+ rows.push({
166
+ cells: [...textCells(" "), ...textCells(spinnerGlyph, { fg: ROLE_COLORS.brand }), ...textCells(` ${id}`)],
167
+ slot,
168
+ from: t,
169
+ to: t + seconds,
170
+ spinner: true,
171
+ });
172
+ t += seconds;
173
+ }
174
+ else if (/\[ (ENV|SANDBOXED|UNSANDBOXED)\b/.test(text)) {
175
+ // The policy stamp lands a beat after the card.
176
+ t += SECONDS_STAMP_DELAY;
177
+ }
178
+ for (const part of wrapCells(cells, RECORD_WRAP)) {
179
+ rows.push({ cells: part, slot, from: t });
180
+ slot++;
181
+ }
182
+ t += text.trim() === "" ? SECONDS_PER_BLANK : SECONDS_PER_LINE;
183
+ }
184
+ return { rows, slots: slot, total: t + SECONDS_HOLD };
185
+ }
186
+ // ── SVG ─────────────────────────────────────────────────────────
187
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
188
+ const pct = (seconds, total) => ((seconds / total) * 100).toFixed(2);
189
+ /** Render the timeline as a self-contained animated SVG. Pure and deterministic. */
190
+ export function renderRecordingSvg(lines, fixture) {
191
+ const { rows, slots, total } = buildTimeline(lines, fixture);
192
+ const height = BAR_H + PAD_TOP + slots * LINE_H + PAD_BOTTOM;
193
+ const dark = SURFACE_COLORS.dark;
194
+ // Color classes by first use, so the CSS is small and the order is stable.
195
+ const colorClass = new Map();
196
+ const classFor = (style) => {
197
+ const names = [];
198
+ if (style.fg) {
199
+ let c = colorClass.get(style.fg);
200
+ if (!c) {
201
+ c = `c${colorClass.size}`;
202
+ colorClass.set(style.fg, c);
203
+ }
204
+ names.push(c);
205
+ }
206
+ if (style.bold)
207
+ names.push("b");
208
+ if (style.dim)
209
+ names.push("d");
210
+ if (style.underline)
211
+ names.push("u");
212
+ return names.join(" ");
213
+ };
214
+ // Keyframes by (from, to), numbered by first use.
215
+ const frames = new Map();
216
+ const frameFor = (from, to) => {
217
+ const key = `${pct(from, total)}:${to === undefined ? "" : pct(to, total)}`;
218
+ let name = frames.get(key);
219
+ if (!name) {
220
+ name = `k${frames.size}`;
221
+ frames.set(key, name);
222
+ }
223
+ return name;
224
+ };
225
+ const body = [];
226
+ for (const row of rows) {
227
+ const y = BAR_H + PAD_TOP + row.slot * LINE_H + FONT_SIZE;
228
+ const spans = [];
229
+ let i = 0;
230
+ while (i < row.cells.length) {
231
+ let j = i + 1;
232
+ while (j < row.cells.length && sameStyle(row.cells[i].style, row.cells[j].style))
233
+ j++;
234
+ const raw = row.cells
235
+ .slice(i, j)
236
+ .map((c) => c.ch)
237
+ .join("");
238
+ // Layout must not depend on whitespace handling (rsvg, proxies and some viewers collapse
239
+ // spaces): each segment sits at an explicit x for its column, edge spaces become offset
240
+ // or nothing, and inner spaces are non-breaking.
241
+ const lead = raw.length - raw.trimStart().length;
242
+ const body_ = raw.trim();
243
+ if (body_ !== "") {
244
+ const x = (TEXT_X + (i + lead) * CELL_W).toFixed(2);
245
+ const text = esc(body_).replace(/ /g, NBSP);
246
+ const cls = classFor(row.cells[i].style);
247
+ spans.push(`<tspan x="${x}"${cls ? ` class="${cls}"` : ""}>${text}</tspan>`);
248
+ }
249
+ i = j;
250
+ }
251
+ const cls = `r ${frameFor(row.from, row.to)}${row.spinner ? " sp" : ""}`;
252
+ body.push(`<text class="${cls}" x="${TEXT_X}" y="${y}" xml:space="preserve">${spans.join("")}</text>`);
253
+ }
254
+ const css = [
255
+ `text{font-family:${FONT_STACK};font-size:${FONT_SIZE}px;fill:${dark.text};white-space:pre}`,
256
+ // No-animation default is the FINAL frame: lines visible, spinner frames hidden. Keyframes drive the replay.
257
+ `.r{opacity:1}`,
258
+ `.sp{opacity:0}`,
259
+ `.b{font-weight:700}`,
260
+ `.d{fill-opacity:.65}`,
261
+ `.u{text-decoration:underline}`,
262
+ `.t{fill:${dark.textSecondary};font-size:12px}`,
263
+ ];
264
+ for (const [fg, name] of colorClass)
265
+ css.push(`.${name}{fill:${fg}}`);
266
+ for (const [key, name] of frames) {
267
+ const [from, to] = key.split(":");
268
+ const a = Number(from);
269
+ const keyframe = to === ""
270
+ ? `0%,${from}%{opacity:0}${(a + 0.01).toFixed(2)}%,98%{opacity:1}100%{opacity:0}`
271
+ : `0%,${from}%{opacity:0}${(a + 0.01).toFixed(2)}%,${to}%{opacity:1}${(Number(to) + 0.01).toFixed(2)}%,100%{opacity:0}`;
272
+ css.push(`@keyframes ${name}{${keyframe}}`);
273
+ css.push(`.${name}{animation:${name} ${total.toFixed(2)}s linear infinite}`);
274
+ }
275
+ css.push(`@media (prefers-reduced-motion:reduce){.r{animation:none!important}.sp{display:none}}`);
276
+ const label = "Recorded demo of sweny try: two nodes run, then the answer and the receipt ticket.";
277
+ return [
278
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${WINDOW_W} ${height}" width="${WINDOW_W}" height="${height}" xml:space="preserve" role="img" aria-label="${esc(label)}">`,
279
+ `<title>sweny try</title>`,
280
+ `<style>${css.join("")}</style>`,
281
+ `<rect x="0.5" y="0.5" width="${WINDOW_W - 1}" height="${height - 1}" rx="10" fill="${dark.background}" stroke="${dark.border}"/>`,
282
+ `<path d="M0.5 ${BAR_H}V10.5A10 10 0 0 1 10.5 0.5H${WINDOW_W - 10.5}A10 10 0 0 1 ${WINDOW_W - 0.5} 10.5V${BAR_H}Z" fill="${PALETTE.slate900}" stroke="${dark.border}"/>`,
283
+ `<circle cx="20" cy="19" r="5.5" fill="${ROLE_COLORS.error}"/>`,
284
+ `<circle cx="40" cy="19" r="5.5" fill="${ROLE_COLORS.warning}"/>`,
285
+ `<circle cx="60" cy="19" r="5.5" fill="${ROLE_COLORS.success}"/>`,
286
+ `<text class="t" x="${WINDOW_W / 2}" y="23" text-anchor="middle">sweny try</text>`,
287
+ ...body,
288
+ `</svg>`,
289
+ ``,
290
+ ].join("\n");
291
+ }
292
+ // ── Entry points ────────────────────────────────────────────────
293
+ const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
294
+ /**
295
+ * The PR-comment preview (heading plus DAG) sits between the answer and the
296
+ * ticket. The recording drops it to stay a README-sized hero: tagline, nodes,
297
+ * answer, ticket.
298
+ */
299
+ export function dropCommentPreview(lines) {
300
+ const from = lines.findIndex((l) => stripAnsi(l).includes("On a pull request, CI posts"));
301
+ const to = lines.findIndex((l, i) => i > from && stripAnsi(l).startsWith(" \u256D"));
302
+ if (from < 0 || to < 0)
303
+ return lines;
304
+ return [...lines.slice(0, from), ...lines.slice(to)];
305
+ }
306
+ /** Run the demo and return the SVG text. */
307
+ export async function recordTrySvg(fixture = loadTryFixture()) {
308
+ let out = "";
309
+ // The DAG renderer paints through the default chalk; pin it so the capture carries color on any host.
310
+ const prevLevel = chalk.level;
311
+ chalk.level = 3;
312
+ try {
313
+ const code = await runTry({
314
+ fast: true,
315
+ tty: false,
316
+ color: true,
317
+ rich: true,
318
+ unicode: true,
319
+ columns: RECORD_COLUMNS,
320
+ fixture,
321
+ write: (s) => {
322
+ out += s;
323
+ },
324
+ });
325
+ if (code !== 0)
326
+ throw new Error(`the demo run exited ${code}`);
327
+ }
328
+ finally {
329
+ chalk.level = prevLevel;
330
+ }
331
+ const lines = out.split("\n");
332
+ while (lines.length > 0 && lines[0].trim() === "")
333
+ lines.shift();
334
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "")
335
+ lines.pop();
336
+ return renderRecordingSvg(dropCommentPreview(lines), fixture);
337
+ }
338
+ /** Record to `file`, creating its directory. Returns the byte size written. */
339
+ export async function recordTrySvgToFile(file, fixture) {
340
+ const svg = await recordTrySvg(fixture);
341
+ fs.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
342
+ fs.writeFileSync(file, svg);
343
+ return Buffer.byteLength(svg);
344
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import path from "node:path";
9
9
  import { validateRuntimeInput } from "../inputs.js";
10
- import { JOURNAL_DIR, JOURNAL_FILE, JournalLockedError, JournalVersionError, RunJournal, buildResumePlan, canonicalHash, findJournalRun, journalDir, mayRepeatWrites, readJournal, workflowHashOf, } from "../journal.js";
10
+ import { JOURNAL_DIR, JOURNAL_FILE, JournalKeyError, JournalLockedError, JournalVersionError, RunJournal, buildResumePlan, canonicalHash, checkJournalAgainstWorkflow, findJournalRun, journalDir, mayRepeatWrites, readJournal, workflowHashOf, } from "../journal.js";
11
11
  import { parseInputFlag } from "./workflow-input.js";
12
12
  /** Resume-only flags; the rest are shared with `workflow run`. */
13
13
  export const WORKFLOW_RESUME_OPTIONS = [
@@ -83,10 +83,14 @@ export function prepareResume(ref, opts, deps) {
83
83
  read = readJournal(file, { repair: !opts.plan });
84
84
  }
85
85
  catch (err) {
86
- if (err instanceof JournalVersionError)
86
+ if (err instanceof JournalVersionError || err instanceof JournalKeyError)
87
87
  return fail(err.message);
88
88
  return fail(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
89
89
  }
90
+ if (read.forgedAtLine !== undefined) {
91
+ return fail(`run journal ${file} line ${read.forgedAtLine} fails authentication: it was edited or written by something ` +
92
+ `other than this run. Refusing to resume. Start a new run.`);
93
+ }
90
94
  if (read.corruptAtLine !== undefined) {
91
95
  return fail(`run journal ${file} is damaged at line ${read.corruptAtLine}, with valid records after it. ` +
92
96
  `That is not a torn last write, so it cannot be repaired safely. Start a new run.`);
@@ -122,6 +126,10 @@ export function prepareResume(ref, opts, deps) {
122
126
  }
123
127
  lines.push(`warning: --force: workflow ${workflowFile} changed since the run started`);
124
128
  }
129
+ // Replayed control flow must exist in the workflow (not overridable by --force).
130
+ const flaw = checkJournalAgainstWorkflow(read.records, workflow);
131
+ if (flaw)
132
+ return fail(`cannot resume run ${runId}: ${flaw}. Start a new run.`);
125
133
  let input;
126
134
  if (opts.input !== undefined) {
127
135
  const parsed = parseInputFlag(opts.input);
package/dist/cli/setup.js CHANGED
@@ -12,17 +12,17 @@ const SIGNAL_LABELS = [
12
12
  {
13
13
  name: "agent-needs-input",
14
14
  color: "CA8A04",
15
- description: "Agent hit a decision point — needs human clarification before it can proceed",
15
+ description: "Agent hit a decision point - needs human clarification before it can proceed",
16
16
  },
17
17
  {
18
18
  name: "agent-error",
19
19
  color: "B91C1C",
20
- description: "Unexpected technical failure during agent execution — needs human investigation",
20
+ description: "Unexpected technical failure during agent execution - needs human investigation",
21
21
  },
22
22
  {
23
23
  name: "human-only",
24
24
  color: "6B7280",
25
- description: "Guard rail — automation must not touch this issue or PR",
25
+ description: "Guard rail - automation must not touch this issue or PR",
26
26
  },
27
27
  {
28
28
  name: "needs-review",
@@ -52,7 +52,7 @@ const WORK_TYPE_LABELS = [
52
52
  {
53
53
  name: "research",
54
54
  color: "D97706",
55
- description: "Spike, investigation, or report by the agent — exploratory work without a direct code change",
55
+ description: "Spike, investigation, or report by the agent - exploratory work without a direct code change",
56
56
  isWorkType: true,
57
57
  },
58
58
  {
@@ -64,7 +64,7 @@ const WORK_TYPE_LABELS = [
64
64
  {
65
65
  name: "spec",
66
66
  color: "BE185D",
67
- description: "Spec generation — agent converted non-technical input into a structured spec",
67
+ description: "Spec generation - agent converted non-technical input into a structured spec",
68
68
  isWorkType: true,
69
69
  },
70
70
  {
@@ -77,7 +77,7 @@ const WORK_TYPE_LABELS = [
77
77
  const AGENT_PARENT = {
78
78
  name: "agent",
79
79
  color: "7C3AED",
80
- description: "Parent group — marks all autonomous agent work",
80
+ description: "Parent group - marks all autonomous agent work",
81
81
  };
82
82
  const BUG_LABEL = {
83
83
  name: "bug",
@@ -254,7 +254,7 @@ ${chalk.cyan(` source-control-provider: github
254
254
  pr-labels: agent,triage,needs-review
255
255
  issue-labels: agent`)}
256
256
 
257
- ${chalk.dim(" GitHub labels are referenced by name — no UUIDs needed.")}
257
+ ${chalk.dim(" GitHub labels are referenced by name - no UUIDs needed.")}
258
258
  `);
259
259
  }
260
260
  // ── Command registration ───────────────────────────────────────────────────
package/dist/cli/try.d.ts CHANGED
@@ -86,6 +86,10 @@ export interface TryOptions {
86
86
  tty?: boolean;
87
87
  /** Draw with Unicode. Default: what the terminal supports. */
88
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;
89
93
  /** Terminal width. Default: stdout's. */
90
94
  columns?: number;
91
95
  fixture?: TryFixture;
package/dist/cli/try.js CHANGED
@@ -189,7 +189,7 @@ export async function runTry(opts = {}) {
189
189
  }
190
190
  const env = process.env;
191
191
  const unicode = opts.unicode ?? supportsUnicode(env);
192
- const color = tty && colorEnabled({ isTTY: true }, env);
192
+ const color = opts.color ?? (tty && colorEnabled({ isTTY: true }, env));
193
193
  const paint = createPaint(color);
194
194
  const g = glyphsFor(unicode);
195
195
  const columns = opts.columns ?? terminalColumns(process.stdout, env);
@@ -263,7 +263,7 @@ export async function runTry(opts = {}) {
263
263
  runId: "demo",
264
264
  stream: { write, isTTY: tty, columns },
265
265
  env,
266
- rich: tty,
266
+ rich: opts.rich ?? tty,
267
267
  color,
268
268
  unicode,
269
269
  animate: !opts.fast,
@@ -287,8 +287,21 @@ export function registerTryCommand(program) {
287
287
  .option("--fast", "Replay instantly instead of pacing the progress")
288
288
  .option("--pace <ms>", `Milliseconds each node takes to replay (default ${DEFAULT_TRY_PACE_MS})`)
289
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>")
290
291
  .addHelpText("after", `\n${TRY_HELP_NOTE}\n`)
291
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
+ }
292
305
  let paceMs;
293
306
  if (options.pace !== undefined) {
294
307
  paceMs = Number(options.pace);
@@ -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 — ${err instanceof Error ? err.message : String(err)}`) +
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);