@sweny-ai/core 0.1.170 → 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 (55) 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 +20 -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/theme.d.ts +185 -0
  51. package/dist/theme.js +183 -0
  52. package/dist/workflows/browser.js +2 -2
  53. package/dist/workflows/implement.yml +2 -1
  54. package/dist/workflows/triage.yml +2 -1
  55. package/package.json +6 -1
@@ -232,7 +232,8 @@ export declare function noPushGitConfig(dir: string): Array<[string, string]>;
232
232
  * them. A deliberate agent on an unsandboxed run that finds a credential on
233
233
  * disk (an ssh key without a passphrase, a keychain entry, a token persisted
234
234
  * in `.git/config` by a checkout) can still push. Run sandboxed (the sandbox
235
- * hides credential files such as `~/.ssh`) and without push credentials in CI
235
+ * hides credential files such as `~/.ssh`, and a checkout's persisted token,
236
+ * see git-credentials.ts) and without push credentials in CI
236
237
  * for a guarantee; under a strict harness policy an unsandboxed staged write
237
238
  * node is refused (`stagedWrite` in policy.ts). `enabled` false returns `env`
238
239
  * unchanged. Pure apart from creating {@link noPushDir} once.
package/dist/agent-env.js CHANGED
@@ -646,7 +646,8 @@ export function noPushGitConfig(dir) {
646
646
  * them. A deliberate agent on an unsandboxed run that finds a credential on
647
647
  * disk (an ssh key without a passphrase, a keychain entry, a token persisted
648
648
  * in `.git/config` by a checkout) can still push. Run sandboxed (the sandbox
649
- * hides credential files such as `~/.ssh`) and without push credentials in CI
649
+ * hides credential files such as `~/.ssh`, and a checkout's persisted token,
650
+ * see git-credentials.ts) and without push credentials in CI
650
651
  * for a guarantee; under a strict harness policy an unsandboxed staged write
651
652
  * node is refused (`stagedWrite` in policy.ts). `enabled` false returns `env`
652
653
  * unchanged. Pure apart from creating {@link noPushDir} once.
@@ -670,10 +671,15 @@ export function withPushBlocked(env, enabled) {
670
671
  n++;
671
672
  }
672
673
  out.GIT_CONFIG_COUNT = String(n);
673
- const ownSsh = env.GIT_SSH_COMMAND ?? (env.GIT_SSH ? shQuote(env.GIT_SSH) : undefined);
674
+ const wrapper = shQuote(path.join(dir, "ssh"));
675
+ // Applied twice, GIT_SSH_COMMAND is already our wrapper: keep the operator's command it recorded,
676
+ // or the wrapper would exec itself forever on a fetch.
677
+ const ownSsh = env.GIT_SSH_COMMAND === wrapper
678
+ ? env.SWENY_NO_PUSH_SSH
679
+ : (env.GIT_SSH_COMMAND ?? (env.GIT_SSH ? shQuote(env.GIT_SSH) : undefined));
674
680
  if (ownSsh)
675
681
  out.SWENY_NO_PUSH_SSH = ownSsh;
676
- out.GIT_SSH_COMMAND = shQuote(path.join(dir, "ssh"));
682
+ out.GIT_SSH_COMMAND = wrapper;
677
683
  out.GIT_SSH_VARIANT = "ssh";
678
684
  out.GIT_ASKPASS = path.join(dir, "askpass");
679
685
  out.GIT_TERMINAL_PROMPT = "0";
package/dist/browser.d.ts CHANGED
@@ -24,3 +24,5 @@ export { isWhenExpression, whenLabel, parseExpression, evaluateExpression, check
24
24
  export type { ExprNode, ExpressionScope, ExpressionResult } from "./when.js";
25
25
  export { workflowToFlow, flowToWorkflow, applyExecutionEvent, exportAsTypescript, getSkillCatalog } from "./studio.js";
26
26
  export type { FlowNode, SkillNodeData, FlowEdge } from "./studio.js";
27
+ export { SWENY_TAGLINE, PALETTE, ROLE_COLORS, SURFACE_COLORS, GLYPHS, SPINNER_FRAMES, SPINNER_INTERVAL_MS, TICKET_BOX, MERMAID_CLASS_DEFS, MERMAID_EDGE_STYLES, } from "./theme.js";
28
+ export type { ColorRole, GlyphKey, GlyphSet, TicketBox, DiagramNodeStatus } from "./theme.js";
package/dist/browser.js CHANGED
@@ -27,3 +27,6 @@ export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workf
27
27
  export { isWhenExpression, whenLabel, parseExpression, evaluateExpression, checkExpression, ExpressionSyntaxError, } from "./when.js";
28
28
  // Studio adapter
29
29
  export { workflowToFlow, flowToWorkflow, applyExecutionEvent, exportAsTypescript, getSkillCatalog } from "./studio.js";
30
+ // Visual tokens (#479): palette, color roles, glyphs, diagram classDefs. No
31
+ // imports, no chalk: Studio and the docs site share them with the CLI.
32
+ export { SWENY_TAGLINE, PALETTE, ROLE_COLORS, SURFACE_COLORS, GLYPHS, SPINNER_FRAMES, SPINNER_INTERVAL_MS, TICKET_BOX, MERMAID_CLASS_DEFS, MERMAID_EDGE_STYLES, } from "./theme.js";
package/dist/budget.js CHANGED
@@ -38,7 +38,11 @@ export function hasLimits(l) {
38
38
  export function spendOf(u) {
39
39
  if (!u)
40
40
  return {};
41
- const tokens = isNum(u.inputTokens) || isNum(u.outputTokens) ? (u.inputTokens ?? 0) + (u.outputTokens ?? 0) : undefined;
41
+ // Count only finite fields: a NaN beside a real count must not turn the sum into NaN,
42
+ // which no limit compares as crossed (the run would pass its budget without stopping).
43
+ const inTokens = isNum(u.inputTokens) ? u.inputTokens : undefined;
44
+ const outTokens = isNum(u.outputTokens) ? u.outputTokens : undefined;
45
+ const tokens = inTokens !== undefined || outTokens !== undefined ? (inTokens ?? 0) + (outTokens ?? 0) : undefined;
42
46
  return { ...(tokens !== undefined ? { tokens } : {}), ...(isNum(u.costUsd) ? { costUsd: u.costUsd } : {}) };
43
47
  }
44
48
  function over(spent, limit, strictlyOver) {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * PR billboard: the markdown a CI runner posts as one sticky comment on a
3
- * pull request. Pure and runner-agnostic; the CLI writes it with
3
+ * pull request. Leads with the receipt ticket, then the run's DAG. Pure and runner-agnostic; the CLI writes it with
4
4
  * `--comment-file`, and any runner (the GitHub Action, GitLab, a script) posts it.
5
5
  *
6
6
  * METADATA ONLY. Node names, statuses, counts, and durations. Never prompts,
@@ -13,6 +13,8 @@ export interface RunCommentOptions {
13
13
  trace?: ExecutionTrace;
14
14
  /** Per-node wall-clock duration in ms, keyed by node id. Missing entries render as "-". */
15
15
  durationsMs?: Record<string, number>;
16
+ /** Run id; the ticket shows its short hash. */
17
+ runId?: string;
16
18
  }
17
19
  /** Hidden marker the poster searches for to update the comment in place. */
18
20
  export declare function runCommentMarker(workflowId: string): string;
@@ -22,7 +24,7 @@ export declare function formatRunComment(workflow: Workflow, results: Map<string
22
24
  * and no error text (thrown messages can embed model prose). Replaces any stale
23
25
  * success comment for the same workflow.
24
26
  */
25
- export declare function formatCrashComment(workflow: Workflow, summary: RunSummary): string;
27
+ export declare function formatCrashComment(workflow: Workflow, summary: RunSummary, runId?: string): string;
26
28
  /** Write the comment markdown to `file`. Never throws; returns true only when written. */
27
29
  export declare function writeRunComment(file: string, workflow: Workflow, results: Map<string, NodeResult>, summary: RunSummary, opts?: RunCommentOptions & {
28
30
  crashed?: boolean;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * PR billboard: the markdown a CI runner posts as one sticky comment on a
3
- * pull request. Pure and runner-agnostic; the CLI writes it with
3
+ * pull request. Leads with the receipt ticket, then the run's DAG. Pure and runner-agnostic; the CLI writes it with
4
4
  * `--comment-file`, and any runner (the GitHub Action, GitLab, a script) posts it.
5
5
  *
6
6
  * METADATA ONLY. Node names, statuses, counts, and durations. Never prompts,
@@ -9,8 +9,14 @@
9
9
  import fs from "node:fs";
10
10
  import path from "node:path";
11
11
  import { toMermaidBlock } from "../mermaid.js";
12
- import { STEP_SUMMARY_CLASS_DEFS, formatReceipt, formatReceiptDuration, nodeStates, } from "./run-output.js";
12
+ import { formatReceiptDuration, nodeStates } from "./run-output.js";
13
+ import { formatTicketText } from "./ticket.js";
14
+ import { glyphs } from "./terminal.js";
13
15
  export const RUN_COMMENT_FOOTER = "Run with [SWEny](https://github.com/swenyai/sweny): `npx @sweny-ai/core new`";
16
+ /** The receipt ticket as a fenced block, the same card a TTY run ends on. */
17
+ function ticketBlock(workflow, summary, runId, crashed) {
18
+ return ["```text", formatTicketText({ summary, workflow: workflow.id, runId, crashed }), "```"].join("\n");
19
+ }
14
20
  /** Hidden marker the poster searches for to update the comment in place. */
15
21
  export function runCommentMarker(workflowId) {
16
22
  const safe = workflowId.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/-{2,}/g, "-") || "workflow";
@@ -33,9 +39,9 @@ export function formatRunComment(workflow, results, summary, opts = {}) {
33
39
  runCommentMarker(workflow.id),
34
40
  `## ${summary.ok ? "✅" : "❌"} ${cell(workflow.name)}`,
35
41
  "",
36
- `\`${formatReceipt(summary)}\``,
42
+ ticketBlock(workflow, summary, opts.runId),
37
43
  "",
38
- toMermaidBlock(workflow, { state, trace: opts.trace, classDefs: STEP_SUMMARY_CLASS_DEFS }),
44
+ toMermaidBlock(workflow, { state, trace: opts.trace }),
39
45
  "",
40
46
  "<details>",
41
47
  "<summary>Nodes</summary>",
@@ -55,12 +61,12 @@ export function formatRunComment(workflow, results, summary, opts = {}) {
55
61
  * and no error text (thrown messages can embed model prose). Replaces any stale
56
62
  * success comment for the same workflow.
57
63
  */
58
- export function formatCrashComment(workflow, summary) {
64
+ export function formatCrashComment(workflow, summary, runId) {
59
65
  return [
60
66
  runCommentMarker(workflow.id),
61
67
  `## ❌ ${cell(workflow.name)}`,
62
68
  "",
63
- `\`${formatReceipt(summary)} · crashed\``,
69
+ ticketBlock(workflow, summary, runId, true),
64
70
  "",
65
71
  "The run stopped before finishing. See the job log for details.",
66
72
  "",
@@ -72,11 +78,13 @@ export function formatCrashComment(workflow, summary) {
72
78
  export function writeRunComment(file, workflow, results, summary, opts = {}) {
73
79
  try {
74
80
  fs.mkdirSync(path.dirname(file), { recursive: true });
75
- fs.writeFileSync(file, opts.crashed ? formatCrashComment(workflow, summary) : formatRunComment(workflow, results, summary, opts));
81
+ fs.writeFileSync(file, opts.crashed
82
+ ? formatCrashComment(workflow, summary, opts.runId)
83
+ : formatRunComment(workflow, results, summary, opts));
76
84
  return true;
77
85
  }
78
86
  catch (err) {
79
- process.stderr.write(` ⚠ could not write comment file: ${err instanceof Error ? err.message : err}\n`);
87
+ process.stderr.write(` ${glyphs().warning} could not write comment file: ${err instanceof Error ? err.message : err}\n`);
80
88
  return false;
81
89
  }
82
90
  }
package/dist/cli/main.js CHANGED
@@ -39,8 +39,12 @@ import { JournalLockedError, JournalMismatchError, RunJournal } from "../journal
39
39
  import { journalDisabled, prepareResume, RESUME_SHARED_RUN_FLAGS, WORKFLOW_RESUME_OPTIONS, } from "./resume.js";
40
40
  import { runE2eRun, runWithWallClockBudget, DEFAULT_WORKFLOW_TIMEOUT_MS } from "./e2e.js";
41
41
  import { createVerboseToolObserver } from "./verbose-observer.js";
42
- import { createRunLogger, renderReceiptLine, summarizeRun, writeStepSummary, WORKFLOW_RUN_DESCRIPTION, WORKFLOW_RUN_OPTIONS, } from "./run-output.js";
42
+ import { createRunLogger, summarizeRun, writeStepSummary, WORKFLOW_RUN_DESCRIPTION, WORKFLOW_RUN_OPTIONS, } from "./run-output.js";
43
43
  import { writeRunComment } from "./comment-output.js";
44
+ import { writeReceipt } from "./ticket.js";
45
+ import { createNodeProgress, toolCallsText } from "./progress.js";
46
+ import { applyNoColor, createPaint } from "./style.js";
47
+ import { colorEnabled, glyphsFor, richOutput, spinnerFramesFor, supportsUnicode } from "./terminal.js";
44
48
  import { registerTriageCommand, registerImplementCommand, parseCliInputs, validateInputs, validateWarnings, parsePositiveInt, } from "./config.js";
45
49
  import { c, formatBanner, getStepDetails, formatStepLine, formatDagResultHuman, formatDagResultMarkdown, writeResultJson, formatValidationErrors, formatCrashError, formatCheckResults, } from "./output.js";
46
50
  import { checkProviderConnectivity, detectClaudeCodeLogin, discoverWorkflowSkillIds, validateCheckInputs, } from "./check.js";
@@ -83,6 +87,8 @@ if (process.argv[2] !== "try") {
83
87
  // Agent sandbox / env-passthrough keys from .sweny.yml -> SWENY_* env (#360).
84
88
  applyAgentFileConfig(loadConfigFile());
85
89
  }
90
+ // NO_COLOR (https://no-color.org) turns off every color, including direct chalk calls.
91
+ applyNoColor();
86
92
  const program = new Command()
87
93
  .name("sweny")
88
94
  .description("Workflows for coding agents. One set of rules, a receipt for every run.")
@@ -275,7 +281,7 @@ triageCmd.action(async (options) => {
275
281
  mcpServers: config.mcpServers,
276
282
  });
277
283
  // ── Progress display state ─────────────────────────────────
278
- const FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
284
+ const FRAMES = spinnerFramesFor(supportsUnicode());
279
285
  const isTTY = !config.json && (process.stderr.isTTY ?? false);
280
286
  const MAX_ACTIVITY = 3;
281
287
  let spinnerInterval;
@@ -297,7 +303,7 @@ triageCmd.action(async (options) => {
297
303
  /** Render the multi-line progress block (spinner + activity lines). */
298
304
  function renderProgressBlock() {
299
305
  const cols = process.stderr.columns || 80;
300
- const frame = chalk.cyan(FRAMES[frameIdx++ % FRAMES.length]);
306
+ const frame = c.brand(FRAMES[frameIdx++ % FRAMES.length]);
301
307
  const counter = c.subtle(`[${stepIndex}/${totalNodes}]`);
302
308
  const elapsed = c.subtle(formatElapsed(Date.now() - stepStart));
303
309
  const headerLine = ` ${frame} ${counter} ${stepLabel} ${elapsed}`;
@@ -594,7 +600,7 @@ implementCmd.action(async (issueId, options) => {
594
600
  const implProgressObserver = (event) => {
595
601
  switch (event.type) {
596
602
  case "workflow:start":
597
- process.stderr.write(`\n \u25B2 ${chalk.bold(event.workflow)}\n\n`);
603
+ process.stderr.write(`\n ${c.brand("\u25B2")} ${chalk.bold(event.workflow)}\n\n`);
598
604
  break;
599
605
  case "node:enter":
600
606
  process.stderr.write(` ${c.subtle("\u25CB")} ${chalk.dim(event.node)}\u2026\n`);
@@ -852,32 +858,40 @@ resume) {
852
858
  }, typeof options.harnessPolicy === "string" ? options.harnessPolicy : undefined);
853
859
  // Track per-node entry time to compute elapsed on exit
854
860
  const nodeEnterTimes = new Map();
861
+ // Node progress (#479): a spinner on the running node when stderr is live,
862
+ // then one settled line per node. Verbose and stream output interleave with
863
+ // it, so those runs get plain lines.
864
+ const unicode = supportsUnicode();
865
+ const g = glyphsFor(unicode);
866
+ const progressPaint = createPaint(!isJson && colorEnabled(process.stderr));
867
+ const progress = createNodeProgress({
868
+ write: (s) => void process.stderr.write(s),
869
+ live: !isJson && richOutput(process.stderr) && !options.verbose && !options.stream,
870
+ unicode,
871
+ paint: progressPaint,
872
+ announce: true,
873
+ });
855
874
  const wfProgressObserver = isJson
856
875
  ? undefined
857
876
  : (event) => {
858
877
  switch (event.type) {
859
878
  case "workflow:start":
860
- process.stderr.write(`\n \u25B2 ${chalk.bold(event.workflow)}\n\n`);
879
+ process.stderr.write(`\n ${progressPaint.brand(g.brand)} ${progressPaint.strong(event.workflow)}\n\n`);
861
880
  break;
862
881
  case "node:enter":
863
882
  nodeEnterTimes.set(event.node, Date.now());
864
- process.stderr.write(` ${c.subtle("\u25CB")} ${chalk.dim(event.node)}\u2026\n`);
883
+ progress.enter(event.node);
884
+ break;
885
+ case "node:progress":
886
+ progress.tick(event.node);
865
887
  break;
866
888
  case "node:exit": {
867
- const icon = event.result.status === "success"
868
- ? c.ok("\u2713")
869
- : event.result.status === "skipped"
870
- ? c.subtle("\u2212")
871
- : c.fail("\u2717");
872
889
  const enterTime = nodeEnterTimes.get(event.node) ?? Date.now();
873
890
  const elapsedMs = Date.now() - enterTime;
874
- const elapsed = chalk.dim(elapsedMs < 1000 ? `${elapsedMs}ms` : `${Math.round(elapsedMs / 100) / 10}s`);
875
- if (isTTY) {
876
- process.stderr.write(`\x1B[1A\x1B[2K ${icon} ${event.node} ${elapsed}\n`);
877
- }
878
- else {
879
- process.stderr.write(` ${icon} ${event.node} ${elapsed}\n`);
880
- }
891
+ const elapsed = elapsedMs < 1000 ? `${elapsedMs}ms` : `${Math.round(elapsedMs / 100) / 10}s`;
892
+ const calls = event.result.toolCalls.length;
893
+ const detail = [elapsed, ...(calls > 0 ? [toolCallsText(calls)] : [])].join(` ${g.sep} `);
894
+ progress.exit(event.node, event.result.status, detail);
881
895
  runLogger.flush();
882
896
  break;
883
897
  }
@@ -1059,6 +1073,7 @@ resume) {
1059
1073
  writeRunComment(options.commentFile, workflow, results, summarizeRun(results, wfDurationMs, false, trace), {
1060
1074
  trace,
1061
1075
  durationsMs: Object.fromEntries(nodeTimer.durations),
1076
+ runId,
1062
1077
  });
1063
1078
  }
1064
1079
  if (isJson) {
@@ -1098,14 +1113,15 @@ resume) {
1098
1113
  else
1099
1114
  console.log(`${block}\n`);
1100
1115
  }
1116
+ // The receipt: a ticket on a TTY, one plain line in CI and pipes.
1101
1117
  if (wfHasFailed) {
1102
- console.error(` ${renderReceiptLine(receipt, isTTY)}\n`);
1118
+ await writeReceipt(receipt, { workflow: workflow.id, runId, stream: process.stderr });
1103
1119
  if (journal?.active)
1104
1120
  console.error(c.subtle(` resume with: sweny workflow resume ${runId}\n`));
1105
1121
  process.exit(1);
1106
1122
  return;
1107
1123
  }
1108
- console.log(` ${renderReceiptLine(receipt, isTTY)}\n`);
1124
+ await writeReceipt(receipt, { workflow: workflow.id, runId, stream: process.stdout });
1109
1125
  process.exit(0);
1110
1126
  }
1111
1127
  catch (err) {
@@ -1115,18 +1131,18 @@ resume) {
1115
1131
  process.exit(1);
1116
1132
  return;
1117
1133
  }
1134
+ progress.stop();
1118
1135
  console.error(formatCrashError(err));
1119
1136
  runLogger.flush();
1120
1137
  journal?.end("crashed");
1121
1138
  if (journal)
1122
1139
  console.error(c.subtle(` resume with: sweny workflow resume ${runId}`));
1123
1140
  recordHistory(nodeTimer.lastResults, undefined, true);
1124
- console.error(` ${renderReceiptLine(summarizeRun(new Map(), Date.now() - runStart, true), isTTY)}\n`);
1141
+ const crashSummary = summarizeRun(new Map(), Date.now() - runStart, true);
1142
+ await writeReceipt(crashSummary, { workflow: workflow.id, runId, crashed: true, stream: process.stderr });
1125
1143
  // A crash must not leave a stale success comment behind.
1126
1144
  if (options.commentFile) {
1127
- writeRunComment(options.commentFile, workflow, new Map(), summarizeRun(new Map(), Date.now() - runStart, true), {
1128
- crashed: true,
1129
- });
1145
+ writeRunComment(options.commentFile, workflow, new Map(), crashSummary, { crashed: true, runId });
1130
1146
  }
1131
1147
  // Finalize the cloud run as failed (covers thrown errors, incl.
1132
1148
  // RouteEvaluationError). Without this a crashed workflow run stays
@@ -1272,7 +1288,7 @@ workflowCmd
1272
1288
  .command("diagram <file>")
1273
1289
  .description("Render a workflow as a Mermaid diagram (raw .mmd by default; .md output auto-fences)")
1274
1290
  .option("--direction <dir>", "Graph direction: TB (top-bottom) or LR (left-right)", "TB")
1275
- .option("--title <title>", "Inject a title header (off by default — raw Mermaid has no title)")
1291
+ .option("--title <title>", "Inject a title header (off by default: raw Mermaid has no title)")
1276
1292
  .option("--block", "Wrap in ```mermaid fenced code block (forces fencing in any output)")
1277
1293
  .option("--no-block", "Force raw Mermaid even when writing to a .md file")
1278
1294
  .option("-o, --output <path>", "Write to a file instead of stdout (.mmd/.mermaid raw; .md fenced)")
@@ -1483,7 +1499,7 @@ program
1483
1499
  shell: process.platform === "win32",
1484
1500
  });
1485
1501
  if (res.error) {
1486
- process.stderr.write(chalk.red(` Error: couldn't run ${cmd} — ${res.error.message}`) + "\n");
1502
+ process.stderr.write(chalk.red(` Error: couldn't run ${cmd}: ${res.error.message}`) + "\n");
1487
1503
  return 127;
1488
1504
  }
1489
1505
  return typeof res.status === "number" ? res.status : 1;
package/dist/cli/new.js CHANGED
@@ -385,6 +385,9 @@ export function buildActionWorkflow(credentials, cronExpression) {
385
385
  lines.push(" runs-on: ubuntu-latest");
386
386
  lines.push(" steps:");
387
387
  lines.push(" - uses: actions/checkout@v4");
388
+ // #473: no token on disk for the agents to read; sweny pushes the PR branch itself.
389
+ lines.push(" with:");
390
+ lines.push(" persist-credentials: false");
388
391
  lines.push(" - uses: swenyai/sweny@v5");
389
392
  lines.push(" with:");
390
393
  lines.push(" workflow: triage");
@@ -9,6 +9,7 @@ export declare const c: {
9
9
  report: import("chalk").ChalkInstance;
10
10
  ok: import("chalk").ChalkInstance;
11
11
  fail: import("chalk").ChalkInstance;
12
+ warn: import("chalk").ChalkInstance;
12
13
  subtle: import("chalk").ChalkInstance;
13
14
  link: import("chalk").ChalkInstance;
14
15
  };
@@ -43,9 +44,9 @@ export declare function writeResultJson(results: Map<string, NodeResult>): Promi
43
44
  * - Node execution details (collapsible) with tool-call summaries
44
45
  */
45
46
  export interface FormatMarkdownOptions {
46
- /** Workflow definition — required for the Mermaid diagram. */
47
+ /** Workflow definition: required for the Mermaid diagram. */
47
48
  workflow?: Workflow;
48
- /** Execution trace from `execute()` — colors the diagram and drives the path/routing sections. */
49
+ /** Execution trace from `execute()`: colors the diagram and drives the path/routing sections. */
49
50
  trace?: ExecutionTrace;
50
51
  }
51
52
  export declare function formatDagResultMarkdown(results: Map<string, NodeResult>, durationMs: number, config?: CliConfig, options?: FormatMarkdownOptions): string;
@@ -1,16 +1,18 @@
1
1
  import chalk from "chalk";
2
2
  import { toMermaidBlock } from "../mermaid.js";
3
- // ── Color palette ───────────────────────────────────────────────
3
+ import { PALETTE, ROLE_COLORS } from "../theme.js";
4
+ // ── Color palette (tokens from theme.ts; one color per meaning) ─
4
5
  export const c = {
5
- brand: chalk.hex("#FF6B2B"),
6
- brandDim: chalk.hex("#CC5522"),
7
- learn: chalk.hex("#60A5FA"),
8
- act: chalk.hex("#F59E0B"),
9
- report: chalk.hex("#A78BFA"),
10
- ok: chalk.hex("#34D399"),
11
- fail: chalk.hex("#F87171"),
12
- subtle: chalk.hex("#6B7280"),
13
- link: chalk.hex("#60A5FA").underline,
6
+ brand: chalk.hex(ROLE_COLORS.brand),
7
+ brandDim: chalk.hex(ROLE_COLORS.brandDeep),
8
+ learn: chalk.hex(ROLE_COLORS.info),
9
+ act: chalk.hex(ROLE_COLORS.warning),
10
+ report: chalk.hex(PALETTE.slate400),
11
+ ok: chalk.hex(ROLE_COLORS.success),
12
+ fail: chalk.hex(ROLE_COLORS.error),
13
+ warn: chalk.hex(ROLE_COLORS.warning),
14
+ subtle: chalk.hex(ROLE_COLORS.muted),
15
+ link: chalk.hex(ROLE_COLORS.info).underline,
14
16
  };
15
17
  export function phaseColor(phase) {
16
18
  return phase === "learn" ? c.learn : phase === "act" ? c.act : phase === "report" ? c.report : chalk.white;
@@ -84,7 +86,7 @@ export function formatBanner(config, version) {
84
86
  const title = `${c.brand("\u25B2")} ${chalk.bold(c.brand("SWEny"))} ${chalk.bold("Triage")}`;
85
87
  const ver = c.subtle(`v${version}`);
86
88
  const titlePad = BOX_WIDTH - 4 - visLen(title) - visLen(ver);
87
- const mode = config.dryRun ? chalk.hex("#F59E0B")("dry run") : c.ok("live");
89
+ const mode = config.dryRun ? c.warn("dry run") : c.ok("live");
88
90
  const header = [title + " ".repeat(Math.max(1, titlePad)) + ver];
89
91
  const rows = [
90
92
  `${c.subtle("Repository")}${" ".repeat(6)}${chalk.white(config.repository)}`,
@@ -134,7 +136,7 @@ export function getStepDetails(name, data) {
134
136
  case "novelty-gate": {
135
137
  const action = data.action;
136
138
  if (action === "dry-run")
137
- details.push("Dry run — analysis only");
139
+ details.push("Dry run: analysis only");
138
140
  else if (action === "skip")
139
141
  details.push("No novel issues found");
140
142
  else if (action === "+1")
@@ -182,7 +184,7 @@ export function getStepDetails(name, data) {
182
184
  export function formatStepLine(icon, counter, name, elapsed, reason) {
183
185
  const label = `${counter} ${name}`;
184
186
  const pad = Math.max(1, 40 - visLen(label));
185
- const suffix = reason ? c.subtle(` \u2014 ${reason}`) : "";
187
+ const suffix = reason ? c.subtle(` ${reason}`) : "";
186
188
  return ` ${icon} ${c.subtle(counter)} ${name}${" ".repeat(pad)}${c.subtle(elapsed)}${suffix}`;
187
189
  }
188
190
  // ── DAG result summary ──────────────────────────────────────────
@@ -202,7 +204,7 @@ export function formatDagResultHuman(results, durationMs, config) {
202
204
  if (createPrResult?.data?.prUrl) {
203
205
  return formatDagSuccessResult(results, duration);
204
206
  }
205
- // Dry run — show findings summary, no side effects taken
207
+ // Dry run: show findings summary, no side effects taken
206
208
  if (config?.dryRun) {
207
209
  return formatDagDryRunResult(results, duration);
208
210
  }
@@ -283,7 +285,7 @@ function formatDagDryRunResult(results, duration) {
283
285
  if (rec)
284
286
  body.push(`${c.subtle("Next")}${" ".repeat(6)}${String(rec)}`);
285
287
  body.push("");
286
- body.push(c.subtle("No side effects — dry run mode"));
288
+ body.push(c.subtle("No side effects: dry run mode"));
287
289
  return ["", boxTop(), ...boxSection(header), boxDivider(), ...boxSection(body), boxBottom(), ""].join("\n");
288
290
  }
289
291
  function formatDagFailureResult(nodeId, result, duration) {
@@ -342,19 +344,19 @@ export function formatValidationErrors(errors) {
342
344
  export function extractCredentialHint(err) {
343
345
  const msg = err instanceof Error ? err.message : String(err);
344
346
  if (/401|unauthorized|authentication/i.test(msg) && /anthropic/i.test(msg)) {
345
- return "Check your ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN — get a key at https://console.anthropic.com";
347
+ return "Check your ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN. Get a key at https://console.anthropic.com";
346
348
  }
347
349
  if (/401|403|unauthorized/i.test(msg) && /datadog/i.test(msg)) {
348
- return "Check your DD_API_KEY and DD_APP_KEY — find them at https://app.datadoghq.com/organization-settings/api-keys";
350
+ return "Check your DD_API_KEY and DD_APP_KEY. Find them at https://app.datadoghq.com/organization-settings/api-keys";
349
351
  }
350
352
  if (/401|403|unauthorized/i.test(msg) && /linear/i.test(msg)) {
351
- return "Check your LINEAR_API_KEY — find it at https://linear.app/settings/api";
353
+ return "Check your LINEAR_API_KEY. Find it at https://linear.app/settings/api";
352
354
  }
353
355
  if (/401|403|unauthorized/i.test(msg) && /github/i.test(msg)) {
354
- return "Check your GITHUB_TOKEN — create a Personal Access Token at https://github.com/settings/tokens";
356
+ return "Check your GITHUB_TOKEN. Create a Personal Access Token at https://github.com/settings/tokens";
355
357
  }
356
358
  if (/ENOTFOUND|ETIMEDOUT|network/i.test(msg)) {
357
- return "Network error — check your internet connection and provider endpoint URL.";
359
+ return "Network error: check your internet connection and provider endpoint URL.";
358
360
  }
359
361
  return null;
360
362
  }
@@ -380,14 +382,14 @@ export function formatCheckResults(results) {
380
382
  const body = results.map((r) => {
381
383
  const icon = r.status === "ok" ? c.ok("\u2713") : r.status === "fail" ? c.fail("\u2717") : c.subtle("\u2212");
382
384
  const name = chalk.white(r.name);
383
- const detail = r.status === "ok" ? c.subtle(r.detail) : r.status === "fail" ? chalk.red(r.detail) : c.subtle(r.detail);
385
+ const detail = r.status === "ok" ? c.subtle(r.detail) : r.status === "fail" ? c.fail(r.detail) : c.subtle(r.detail);
384
386
  return `${icon} ${name}\n ${detail}`;
385
387
  });
386
388
  const hasFailure = results.some((r) => r.status === "fail");
387
389
  const summary = hasFailure
388
- ? c.fail("One or more checks failed — fix the issues above before running sweny triage.")
390
+ ? c.fail("One or more checks failed. Fix the issues above before running sweny triage.")
389
391
  : results.every((r) => r.status === "skip")
390
- ? c.subtle("All providers set to file mode — no network checks performed.")
392
+ ? c.subtle("All providers set to file mode: no network checks performed.")
391
393
  : c.ok("All checks passed.");
392
394
  return [
393
395
  "",
@@ -442,19 +444,19 @@ export function formatDagResultMarkdown(results, durationMs, config, options = {
442
444
  lines.push(`## ❌ SWEny Triage Failed`);
443
445
  }
444
446
  else if (prData?.prUrl) {
445
- lines.push(`## ✅ SWEny Triage — PR opened`);
447
+ lines.push(`## ✅ SWEny Triage: PR opened`);
446
448
  }
447
449
  else if (issueData?.issueIdentifier || issueData?.issueUrl) {
448
- lines.push(`## ✅ SWEny Triage — Issue created`);
450
+ lines.push(`## ✅ SWEny Triage: Issue created`);
449
451
  }
450
452
  else if (isDryRun) {
451
- lines.push(`## 🔍 SWEny Triage — Dry run complete`);
453
+ lines.push(`## 🔍 SWEny Triage: Dry run complete`);
452
454
  }
453
455
  else if (novelCount === 0) {
454
- lines.push(`## ✅ SWEny Triage — No new incidents`);
456
+ lines.push(`## ✅ SWEny Triage: No new incidents`);
455
457
  }
456
458
  else {
457
- lines.push(`## ℹ️ SWEny Triage — No action taken`);
459
+ lines.push(`## ℹ️ SWEny Triage: No action taken`);
458
460
  }
459
461
  lines.push("");
460
462
  // ── Workflow diagram (Mermaid) ────────────────────────────────
@@ -532,9 +534,9 @@ export function formatDagResultMarkdown(results, durationMs, config, options = {
532
534
  lines.push("| --- | --- | --- | --- | --- |");
533
535
  for (let i = 0; i < findings.length; i++) {
534
536
  const f = findings[i];
535
- const sev = String(f.severity ?? "—");
536
- const title = String(f.title ?? "—").replace(/\|/g, "\\|");
537
- const complexity = String(f.fix_complexity ?? "—");
537
+ const sev = String(f.severity ?? "-");
538
+ const title = String(f.title ?? "-").replace(/\|/g, "\\|");
539
+ const complexity = String(f.fix_complexity ?? "-");
538
540
  const status = f.is_duplicate ? `dup of ${f.duplicate_of ?? "existing"}` : "novel";
539
541
  lines.push(`| ${i + 1} | ${sev} | ${title} | ${complexity} | ${status} |`);
540
542
  }
@@ -549,7 +551,7 @@ export function formatDagResultMarkdown(results, durationMs, config, options = {
549
551
  const title = issueData.issueTitle ? String(issueData.issueTitle) : "";
550
552
  const url = issueData.issueUrl ? String(issueData.issueUrl) : "";
551
553
  const link = url ? `[${id || url}](${url})` : id;
552
- lines.push(`- **Issue created:** ${link}${title ? ` — ${title}` : ""}`);
554
+ lines.push(`- **Issue created:** ${link}${title ? `: ${title}` : ""}`);
553
555
  }
554
556
  if (prData?.prUrl) {
555
557
  const num = prData.prNumber ? `#${String(prData.prNumber)}` : "";
@@ -565,7 +567,7 @@ export function formatDagResultMarkdown(results, durationMs, config, options = {
565
567
  lines.push("");
566
568
  }
567
569
  if (isDryRun) {
568
- lines.push("_Dry run mode — no side effects were taken._");
570
+ lines.push("_Dry run mode: no side effects were taken._");
569
571
  lines.push("");
570
572
  }
571
573
  // ── Node execution details (collapsible) ──────────────────────
@@ -578,17 +580,17 @@ function appendNodeDetails(lines, results) {
578
580
  lines.push("<details><summary>Node execution details</summary>");
579
581
  lines.push("");
580
582
  for (const [nodeId, result] of results) {
581
- const icon = result.status === "success" ? "✓" : result.status === "failed" ? "✗" : "—";
583
+ const icon = result.status === "success" ? "✓" : result.status === "failed" ? "✗" : "−";
582
584
  const toolCount = result.toolCalls?.length ?? 0;
583
- lines.push(`#### ${icon} \`${nodeId}\` — ${toolCount} tool call${toolCount === 1 ? "" : "s"}`);
585
+ lines.push(`#### ${icon} \`${nodeId}\` · ${toolCount} tool call${toolCount === 1 ? "" : "s"}`);
584
586
  lines.push("");
585
587
  if (toolCount > 0 && result.toolCalls) {
586
588
  lines.push("| Tool | Input (truncated) |");
587
589
  lines.push("| --- | --- |");
588
590
  for (const tc of result.toolCalls.slice(0, 20)) {
589
- const name = tc.tool ?? "—";
591
+ const name = tc.tool ?? "-";
590
592
  const input = summarizeToolInput(tc.input).replace(/\|/g, "\\|");
591
- lines.push(`| \`${name}\` | ${input || "—"} |`);
593
+ lines.push(`| \`${name}\` | ${input || "-"} |`);
592
594
  }
593
595
  if (toolCount > 20) {
594
596
  lines.push(`| … | _${toolCount - 20} more tool calls_ |`);
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Node progress lines for `sweny workflow run` and `sweny try`: a spinner on
3
+ * the running node when the output is live, then one settled line per node.
4
+ *
5
+ * ⠙ survey 7 tool calls (live, redrawn in place)
6
+ * ✓ survey 18s · 12 tool calls (settled)
7
+ *
8
+ * Not live (pipes, CI, --verbose): no redraws, no spinner, one line per event.
9
+ */
10
+ import type { Paint } from "./style.js";
11
+ export interface NodeProgressOptions {
12
+ write: (s: string) => void;
13
+ /** Redraw the running node in place with a spinner. */
14
+ live: boolean;
15
+ unicode: boolean;
16
+ paint: Paint;
17
+ /** Pad node ids to this width so the details line up. */
18
+ idWidth?: number;
19
+ /** When not live, print a `○ node…` line as each node starts. Default false. */
20
+ announce?: boolean;
21
+ }
22
+ export type SettledStatus = "success" | "failed" | "skipped";
23
+ export interface NodeProgress {
24
+ enter(node: string): void;
25
+ /** One unit of work (a tool call) on the running node. */
26
+ tick(node: string): void;
27
+ /** Settle the node: its glyph, its id, and a muted detail (`18s · 12 tool calls`). */
28
+ exit(node: string, status: SettledStatus, detail?: string): void;
29
+ /** Stop any spinner without settling (a crash). Clears the live line. */
30
+ stop(): void;
31
+ }
32
+ export declare function toolCallsText(n: number): string;
33
+ export declare function createNodeProgress(o: NodeProgressOptions): NodeProgress;