@sweny-ai/core 0.1.193 → 0.1.195

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/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @sweny-ai/core
2
+
3
+ **Workflows for coding agents. One set of rules, a receipt for every run.**
4
+
5
+ SWEny runs a DAG of coding-agent steps from a YAML file, on your laptop or in CI. The agent does the work. SWEny decides what it can touch, checks what it hands back, and records what it did.
6
+
7
+ ## Start
8
+
9
+ ```bash
10
+ npx @sweny-ai/core new # pick a named workflow, e.g. explain-repo (no skill credentials)
11
+ npx @sweny-ai/core check # preflight: is your agent logged in?
12
+ npx @sweny-ai/core workflow run .sweny/workflows/explain-repo.yml
13
+ ```
14
+
15
+ Install once with `npm install -g @sweny-ai/core` and the binary is `sweny`. Want to watch it first? `sweny try` replays a recorded demo with no login and no network (sample data, not a real run).
16
+
17
+ ## Credentials
18
+
19
+ Requires Node 20+ and a login or API key for the agent that runs your nodes. Claude Code is the default; pick another with `--agent`.
20
+
21
+ | Agent | Credentials it needs |
22
+ |-------|----------------------|
23
+ | Claude Code (default) | `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`), or `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN`, or a Claude Code login (`claude`, then `/login`), or Bedrock / Vertex routing. |
24
+ | Codex (`--agent codex`) | `CODEX_API_KEY`, `OPENAI_API_KEY` or `CODEX_ACCESS_TOKEN`, or a `codex login`. |
25
+ | pi (`--agent pi`) | The API key of the model provider pi calls (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, ...). |
26
+ | ACP agent (`--agent "acp:<command>"`) | Whatever that agent needs; pass its key var through `SWENY_ENV_PASSTHROUGH`. |
27
+
28
+ For Claude Code and Codex, `sweny check` and `sweny workflow run` stop with the fix when no credential or login is found, before any node runs. Skill credentials (`GITHUB_TOKEN` and so on) are separate and only needed by workflows that use those skills.
29
+
30
+ ## What you get
31
+
32
+ Every run ends with a receipt: nodes, tool calls, duration, tokens, cost, and the policy the agent ran under. An example:
33
+
34
+ ```text
35
+ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
36
+ ┆ ✓ passed run 3f9a2c ┆
37
+ ┆ explain-repo ┆
38
+ ┆ ┆
39
+ ┆ nodes 2/2 ┆
40
+ ┆ tool calls 12 ┆
41
+ ┆ duration 28s ┆
42
+ ┆ tokens 30k ┆
43
+ ┆ cost $0.13 ┆
44
+ ┆ policy env scoped, sandbox on ┆
45
+ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
46
+ ┆ [ ENV SCOPED · SANDBOXED ] ┆
47
+ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
48
+ ```
49
+
50
+ Then `sweny runs` lists your history, and `sweny runs diff` shows what changed in the answers between two runs. The [GitHub Action](https://docs.sweny.ai/action/) posts the same receipt on a PR, and recurring [packs](https://docs.sweny.ai/workflows/packs/) (weekly digest, dependency drift, PR risk review) run it on a schedule.
51
+
52
+ ## Links
53
+
54
+ - [Quick start](https://docs.sweny.ai/getting-started/quick-start/)
55
+ - [CLI reference](https://docs.sweny.ai/cli/commands/)
56
+ - [Agents and policies](https://docs.sweny.ai/getting-started/agents-and-policies/)
57
+ - [Source and issues](https://github.com/swenyai/sweny)
package/dist/browser.d.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * - `buildWorkflow` / `refineWorkflow` — depend on the Claude interface.
14
14
  * - Testing helpers — `MockClaude`, `createFileSkill` use `node:fs`.
15
15
  */
16
- export type { Skill, SkillCategory, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, EdgeWhen, WhenExpression, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, } from "./types.js";
16
+ export type { Skill, SkillCategory, Tool, ToolContext, ToolRunContext, ConfigField, JSONSchema, Workflow, Node, Edge, EdgeWhen, WhenExpression, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, } from "./types.js";
17
17
  export { consoleLogger } from "./types.js";
18
18
  export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
19
19
  export { github, linear, slack, sentry, datadog, betterstack, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
@@ -1,7 +1,9 @@
1
1
  /**
2
- * PR billboard: the markdown a CI runner posts as one sticky comment on a
3
- * pull request. Leads with the receipt ticket, then the run's DAG. Pure and runner-agnostic; the CLI writes it with
4
- * `--comment-file`, and any runner (the GitHub Action, GitLab, a script) posts it.
2
+ * PR billboard. Quiet by default (#536): `--comment-file` now writes an empty
3
+ * file, so the Action's poster (scripts/post-run-comment.sh skips an empty
4
+ * file) adds nothing to the PR. The receipt and DAG go to the job summary, and
5
+ * findings go in the workflow's one updating comment (pr-comment.ts). The
6
+ * formatters below remain for the offline demo (`sweny try`).
5
7
  *
6
8
  * METADATA ONLY. Node names, statuses, counts, and durations. Never prompts,
7
9
  * tool inputs, node outputs, errors, or model prose. The optional "since last
@@ -9,6 +11,7 @@
9
11
  */
10
12
  import type { ExecutionTrace, NodeResult, Workflow } from "../types.js";
11
13
  import { type RunSummary } from "./run-output.js";
14
+ import { prCommentMarker } from "../pr-comment.js";
12
15
  export declare const RUN_COMMENT_FOOTER = "Run with [SWEny](https://github.com/swenyai/sweny): `npx @sweny-ai/core new`";
13
16
  export interface RunCommentOptions {
14
17
  trace?: ExecutionTrace;
@@ -19,8 +22,8 @@ export interface RunCommentOptions {
19
22
  /** Pre-rendered "since last run" markdown line (run-answers.ts formatSinceLastMarkdown): counts only. */
20
23
  sinceLast?: string | null;
21
24
  }
22
- /** Hidden marker the poster searches for to update the comment in place. */
23
- export declare function runCommentMarker(workflowId: string): string;
25
+ /** Hidden marker the poster searches for to update the comment in place (the findings comment's marker too). */
26
+ export declare const runCommentMarker: typeof prCommentMarker;
24
27
  export declare function formatRunComment(workflow: Workflow, results: Map<string, NodeResult>, summary: RunSummary, opts?: RunCommentOptions): string;
25
28
  /**
26
29
  * Minimal comment for a run that crashed before producing node results: no DAG
@@ -28,7 +31,8 @@ export declare function formatRunComment(workflow: Workflow, results: Map<string
28
31
  * success comment for the same workflow.
29
32
  */
30
33
  export declare function formatCrashComment(workflow: Workflow, summary: RunSummary, runId?: string): string;
31
- /** Write the comment markdown to `file`. Never throws; returns true only when written. */
32
- export declare function writeRunComment(file: string, workflow: Workflow, results: Map<string, NodeResult>, summary: RunSummary, opts?: RunCommentOptions & {
33
- crashed?: boolean;
34
- }): boolean;
34
+ /**
35
+ * Write an empty comment file: no metadata-only comment on the PR. Truncates a
36
+ * file an earlier step left there. Never throws; returns true only when written.
37
+ */
38
+ export declare function writeRunComment(file: string): boolean;
@@ -1,27 +1,27 @@
1
1
  /**
2
- * PR billboard: the markdown a CI runner posts as one sticky comment on a
3
- * pull request. Leads with the receipt ticket, then the run's DAG. Pure and runner-agnostic; the CLI writes it with
4
- * `--comment-file`, and any runner (the GitHub Action, GitLab, a script) posts it.
2
+ * PR billboard. Quiet by default (#536): `--comment-file` now writes an empty
3
+ * file, so the Action's poster (scripts/post-run-comment.sh skips an empty
4
+ * file) adds nothing to the PR. The receipt and DAG go to the job summary, and
5
+ * findings go in the workflow's one updating comment (pr-comment.ts). The
6
+ * formatters below remain for the offline demo (`sweny try`).
5
7
  *
6
8
  * METADATA ONLY. Node names, statuses, counts, and durations. Never prompts,
7
9
  * tool inputs, node outputs, errors, or model prose. The optional "since last
8
10
  * run" line carries counts of added and resolved answers, never their text.
9
11
  */
10
- import { workspaceRoot, writeFileNoFollow } from "../safe-file.js";
11
12
  import { toMermaidBlock } from "../mermaid.js";
12
13
  import { formatReceiptDuration, nodeStates } from "./run-output.js";
13
14
  import { formatTicketText } from "./ticket.js";
14
15
  import { glyphs } from "./terminal.js";
16
+ import { workspaceRoot, writeFileNoFollow } from "../safe-file.js";
17
+ import { prCommentMarker } from "../pr-comment.js";
15
18
  export const RUN_COMMENT_FOOTER = "Run with [SWEny](https://github.com/swenyai/sweny): `npx @sweny-ai/core new`";
16
19
  /** The receipt ticket as a fenced block, the same card a TTY run ends on. */
17
20
  function ticketBlock(workflow, summary, runId, crashed) {
18
21
  return ["```text", formatTicketText({ summary, workflow: workflow.id, runId, crashed }), "```"].join("\n");
19
22
  }
20
- /** Hidden marker the poster searches for to update the comment in place. */
21
- export function runCommentMarker(workflowId) {
22
- const safe = workflowId.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/-{2,}/g, "-") || "workflow";
23
- return `<!-- sweny-run-comment:${safe} -->`;
24
- }
23
+ /** Hidden marker the poster searches for to update the comment in place (the findings comment's marker too). */
24
+ export const runCommentMarker = prCommentMarker;
25
25
  /** Table-cell-safe text: no pipes, newlines, or backticks. */
26
26
  function cell(text) {
27
27
  return text.replace(/\s+/g, " ").replace(/\|/g, "\\|").replace(/`/g, "'").trim();
@@ -75,13 +75,14 @@ export function formatCrashComment(workflow, summary, runId) {
75
75
  "",
76
76
  ].join("\n");
77
77
  }
78
- /** Write the comment markdown to `file`. Never throws; returns true only when written. */
79
- export function writeRunComment(file, workflow, results, summary, opts = {}) {
78
+ /**
79
+ * Write an empty comment file: no metadata-only comment on the PR. Truncates a
80
+ * file an earlier step left there. Never throws; returns true only when written.
81
+ */
82
+ export function writeRunComment(file) {
80
83
  try {
81
84
  // The file may sit in the agent-writable workspace: no link on the path is followed.
82
- writeFileNoFollow(file, opts.crashed
83
- ? formatCrashComment(workflow, summary, opts.runId)
84
- : formatRunComment(workflow, results, summary, opts), { ...workspaceRoot(file), mode: 0o644, dirMode: 0o755 });
85
+ writeFileNoFollow(file, "", { ...workspaceRoot(file), mode: 0o644, dirMode: 0o755 });
85
86
  return true;
86
87
  }
87
88
  catch (err) {
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The "what next" note after a workspace's first successful real run (#534).
3
+ *
4
+ * `sweny try` ends with one command to run next; a real run used to end in
5
+ * silence, at the exact moment a user has just seen value. This prints one
6
+ * short block (history, compare, schedule) once per workspace.
7
+ *
8
+ * Once means once per workspace for this user: the marker lives in sweny's
9
+ * per-user state dir (`<state>/first-success/<workspace hash>`), never in the
10
+ * repo, so it is not committed and an agent in the workspace cannot reset it.
11
+ * The marker is claimed atomically (`wx`), so two runs finishing together
12
+ * print the note once.
13
+ *
14
+ * Silent when: CI, a failed run, `--json`, `--stream`, `--dry-run`, or the
15
+ * marker already exists. A marker that cannot be written means no note: a
16
+ * note that might repeat is worse than none. Every other output (a TTY, a
17
+ * pipe, NO_COLOR) gets the same words; only the paint and glyphs change, and
18
+ * the last line is always plain text.
19
+ */
20
+ import { type Paint } from "./style.js";
21
+ type Env = Record<string, string | undefined>;
22
+ /** How to schedule a workflow you wrote: the Action's cron and dispatch triggers. */
23
+ export declare const SCHEDULING_DOCS_URL = "https://docs.sweny.ai/action/scheduling/";
24
+ /** The two command steps, in order. Commands are real subcommands (a spec asserts it). */
25
+ export declare const WHAT_NEXT_STEPS: ReadonlyArray<{
26
+ command: string;
27
+ what: string;
28
+ }>;
29
+ /** The third step, and the plain closing line. */
30
+ export declare const WHAT_NEXT_SCHEDULE = "Run it on a schedule: https://docs.sweny.ai/action/scheduling/";
31
+ /** CI: the `CI` convention, or GitHub Actions itself. */
32
+ export declare function inCi(env?: Env): boolean;
33
+ /**
34
+ * The note: a heading, one aligned row per step, then a plain closing line
35
+ * that says how to schedule it. Same words with or without color or Unicode.
36
+ */
37
+ export declare function formatWhatNext(paint?: Paint, unicode?: boolean): string;
38
+ /** `<state>/first-success/<workspace hash>`: one empty file per workspace. */
39
+ export declare function firstSuccessMarker(cwd: string, env?: Env): string;
40
+ /**
41
+ * Claim the workspace's marker. True only for the caller that created it.
42
+ * False when it already exists or cannot be written (read-only home, no
43
+ * state dir): the note then stays silent rather than risk repeating.
44
+ */
45
+ export declare function claimFirstSuccess(cwd: string, env?: Env): boolean;
46
+ export interface WhatNextOptions {
47
+ /** Where the note goes: the run's stdout. */
48
+ stream: {
49
+ write(s: string): unknown;
50
+ isTTY?: boolean;
51
+ };
52
+ /** The run succeeded (no failed node, not crashed). Anything else prints nothing. */
53
+ ok: boolean;
54
+ /** `--json`, `--stream` or `--dry-run`: stdout is data, or nothing real ran. */
55
+ quiet?: boolean;
56
+ env?: Env;
57
+ cwd?: string;
58
+ /** Override color. Default: `colorEnabled(stream, env)`. */
59
+ color?: boolean;
60
+ /** Override Unicode. Default: `supportsUnicode(env)`. */
61
+ unicode?: boolean;
62
+ }
63
+ /** Print the note if this is the workspace's first successful real run. Returns whether it printed. */
64
+ export declare function writeWhatNext(opts: WhatNextOptions): boolean;
65
+ export {};
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The "what next" note after a workspace's first successful real run (#534).
3
+ *
4
+ * `sweny try` ends with one command to run next; a real run used to end in
5
+ * silence, at the exact moment a user has just seen value. This prints one
6
+ * short block (history, compare, schedule) once per workspace.
7
+ *
8
+ * Once means once per workspace for this user: the marker lives in sweny's
9
+ * per-user state dir (`<state>/first-success/<workspace hash>`), never in the
10
+ * repo, so it is not committed and an agent in the workspace cannot reset it.
11
+ * The marker is claimed atomically (`wx`), so two runs finishing together
12
+ * print the note once.
13
+ *
14
+ * Silent when: CI, a failed run, `--json`, `--stream`, `--dry-run`, or the
15
+ * marker already exists. A marker that cannot be written means no note: a
16
+ * note that might repeat is worse than none. Every other output (a TTY, a
17
+ * pipe, NO_COLOR) gets the same words; only the paint and glyphs change, and
18
+ * the last line is always plain text.
19
+ */
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import { swenyStateDir, workspaceScope } from "../journal.js";
23
+ import { createPaint } from "./style.js";
24
+ import { colorEnabled, glyphsFor, supportsUnicode } from "./terminal.js";
25
+ /** How to schedule a workflow you wrote: the Action's cron and dispatch triggers. */
26
+ export const SCHEDULING_DOCS_URL = "https://docs.sweny.ai/action/scheduling/";
27
+ /** The two command steps, in order. Commands are real subcommands (a spec asserts it). */
28
+ export const WHAT_NEXT_STEPS = [
29
+ { command: "sweny runs", what: "see this run and the ones before it" },
30
+ { command: "sweny runs diff", what: "run it again, then see what changed in the answers" },
31
+ ];
32
+ /** The third step, and the plain closing line. */
33
+ export const WHAT_NEXT_SCHEDULE = `Run it on a schedule: ${SCHEDULING_DOCS_URL}`;
34
+ function truthy(v) {
35
+ return v !== undefined && v !== "" && v !== "0" && v.toLowerCase() !== "false";
36
+ }
37
+ /** CI: the `CI` convention, or GitHub Actions itself. */
38
+ export function inCi(env = process.env) {
39
+ return truthy(env.CI) || env.GITHUB_ACTIONS === "true";
40
+ }
41
+ /**
42
+ * The note: a heading, one aligned row per step, then a plain closing line
43
+ * that says how to schedule it. Same words with or without color or Unicode.
44
+ */
45
+ export function formatWhatNext(paint = createPaint(false), unicode = true) {
46
+ const g = glyphsFor(unicode);
47
+ const width = Math.max(...WHAT_NEXT_STEPS.map((s) => s.command.length));
48
+ const rows = WHAT_NEXT_STEPS.map((s) => ` ${paint.strong(s.command.padEnd(width))} ${paint.muted(s.what)}`);
49
+ return [` ${paint.brand(g.brand)} ${paint.strong("Next")}`, ...rows, ` ${WHAT_NEXT_SCHEDULE}`].join("\n");
50
+ }
51
+ /** `<state>/first-success/<workspace hash>`: one empty file per workspace. */
52
+ export function firstSuccessMarker(cwd, env = process.env) {
53
+ return path.join(swenyStateDir(env), "first-success", workspaceScope(cwd));
54
+ }
55
+ /**
56
+ * Claim the workspace's marker. True only for the caller that created it.
57
+ * False when it already exists or cannot be written (read-only home, no
58
+ * state dir): the note then stays silent rather than risk repeating.
59
+ */
60
+ export function claimFirstSuccess(cwd, env = process.env) {
61
+ const file = firstSuccessMarker(cwd, env);
62
+ try {
63
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
64
+ fs.closeSync(fs.openSync(file, "wx", 0o600));
65
+ return true;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ /** Print the note if this is the workspace's first successful real run. Returns whether it printed. */
72
+ export function writeWhatNext(opts) {
73
+ const env = opts.env ?? process.env;
74
+ if (!opts.ok || opts.quiet || inCi(env))
75
+ return false;
76
+ if (!claimFirstSuccess(opts.cwd ?? process.cwd(), env))
77
+ return false;
78
+ const paint = createPaint(opts.color ?? colorEnabled(opts.stream, env));
79
+ opts.stream.write(`${formatWhatNext(paint, opts.unicode ?? supportsUnicode(env))}\n\n`);
80
+ return true;
81
+ }
package/dist/cli/main.js CHANGED
@@ -45,6 +45,7 @@ import { createStreamObserver } from "./stream-observer.js";
45
45
  import { createRunLogger, summarizeRun, writeStepSummary, WORKFLOW_RUN_DESCRIPTION, WORKFLOW_RUN_OPTIONS, } from "./run-output.js";
46
46
  import { writeRunComment } from "./comment-output.js";
47
47
  import { writeReceipt } from "./ticket.js";
48
+ import { writeWhatNext } from "./first-success.js";
48
49
  import { createNodeProgress, toolCallsText } from "./progress.js";
49
50
  import { applyNoColor, createPaint } from "./style.js";
50
51
  import { colorEnabled, glyphsFor, richOutput, spinnerFramesFor, supportsUnicode } from "./terminal.js";
@@ -1029,6 +1030,8 @@ resume) {
1029
1030
  // --max-steps overrides the executor's own DEFAULT_MAX_STEPS when set.
1030
1031
  const wfTimeoutMs = budget.timeoutMs;
1031
1032
  const wfMaxSteps = budget.maxSteps;
1033
+ // The job summary is written once: by the finished run, else by the crash path.
1034
+ let stepSummaryWritten = false;
1032
1035
  try {
1033
1036
  const { results, trace } = await runWithWallClockBudget((signal) => execute(workflow, workflowInput, {
1034
1037
  skills,
@@ -1073,16 +1076,12 @@ resume) {
1073
1076
  catch {
1074
1077
  // silent
1075
1078
  }
1076
- // PR billboard markdown (metadata only). Written before any early exit so
1077
- // --json runs and failed runs still get a comment.
1078
- if (options.commentFile) {
1079
- writeRunComment(options.commentFile, workflow, results, summarizeRun(results, wfDurationMs, false, trace), {
1080
- trace,
1081
- durationsMs: Object.fromEntries(nodeTimer.durations),
1082
- runId,
1083
- sinceLast: sinceLast ? formatSinceLastMarkdown(sinceLast) : null,
1084
- });
1085
- }
1079
+ // Receipt metadata belongs in the job summary, including --json and failed runs.
1080
+ stepSummaryWritten = true;
1081
+ writeStepSummary(workflow, results, summarizeRun(results, wfDurationMs, false, trace), trace, process.env, sinceLast ? formatSinceLastMarkdown(sinceLast) : null);
1082
+ // Quiet by default (#536): an empty comment file, so no metadata-only PR comment.
1083
+ if (options.commentFile)
1084
+ writeRunComment(options.commentFile);
1086
1085
  if (isJson) {
1087
1086
  await writeResultJson(results, outputSecrets(skills));
1088
1087
  process.exit(wfHasFailed ? 1 : 0);
@@ -1103,10 +1102,9 @@ resume) {
1103
1102
  catch {
1104
1103
  // silent
1105
1104
  }
1106
- // Run receipt (metadata only) + optional $GITHUB_STEP_SUMMARY.
1105
+ // Run receipt (metadata only). The job summary was written before early exits.
1107
1106
  runLogger.flush();
1108
1107
  const receipt = summarizeRun(results, wfDurationMs, false, trace);
1109
- writeStepSummary(workflow, results, receipt, trace, process.env, sinceLast ? formatSinceLastMarkdown(sinceLast) : null);
1110
1108
  // The answer, above the receipt: the terminal node's result, or the failed node's error.
1111
1109
  // Also saved to .sweny/runs/<id>/output.md (local only; --no-history skips the file).
1112
1110
  // Redacted with the journal's redactor; in CI printed only with --show-output.
@@ -1139,6 +1137,12 @@ resume) {
1139
1137
  await writeReceipt(receipt, { workflow: workflow.id, runId, stream: process.stdout });
1140
1138
  if (sinceLast)
1141
1139
  console.log(createPaint(colorEnabled(process.stdout)).muted(` ${formatSinceLast(sinceLast)}`));
1140
+ // First successful real run in this workspace: one "what next" note (#534).
1141
+ writeWhatNext({
1142
+ stream: process.stdout,
1143
+ ok: true,
1144
+ quiet: Boolean(options.stream) || config.dryRun,
1145
+ });
1142
1146
  process.exit(0);
1143
1147
  }
1144
1148
  catch (err) {
@@ -1158,10 +1162,11 @@ resume) {
1158
1162
  recordHistory(nodeTimer.lastResults, undefined, true);
1159
1163
  const crashSummary = summarizeRun(new Map(), Date.now() - runStart, true);
1160
1164
  await writeReceipt(crashSummary, { workflow: workflow.id, runId, crashed: true, stream: process.stderr });
1161
- // A crash must not leave a stale success comment behind.
1162
- if (options.commentFile) {
1163
- writeRunComment(options.commentFile, workflow, new Map(), crashSummary, { crashed: true, runId });
1164
- }
1165
+ // A crash records its receipt in the job summary; the PR gets no status comment.
1166
+ if (!stepSummaryWritten)
1167
+ writeStepSummary(workflow, new Map(), crashSummary);
1168
+ if (options.commentFile)
1169
+ writeRunComment(options.commentFile);
1165
1170
  // Finalize the cloud run as failed (covers thrown errors, incl.
1166
1171
  // RouteEvaluationError). Without this a crashed workflow run stays
1167
1172
  // "running" in cloud forever.
package/dist/cli/new.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { type WorkflowTemplate } from "./templates.js";
11
11
  import { type Skill } from "../types.js";
12
+ import { SCHEDULING_DOCS_URL } from "./first-success.js";
12
13
  export interface Credential {
13
14
  key: string;
14
15
  hint?: string;
@@ -171,3 +172,11 @@ export declare function runNew(options?: {
171
172
  /** Internal: the id came from the marketplace picker, skip the built-in shortcut. */
172
173
  forceMarketplace?: boolean;
173
174
  }): Promise<void>;
175
+ export { SCHEDULING_DOCS_URL };
176
+ /**
177
+ * The closing line (#534). A pack already shows its own trigger step, so it
178
+ * closes as before. Any other workflow that was written (a starter or a
179
+ * described one) says how to schedule THIS workflow; it never names another
180
+ * pack, because no template metadata says which pack would suit it.
181
+ */
182
+ export declare function outroMessage(hasExistingConfig: boolean, offerSchedule: boolean): string;
package/dist/cli/new.js CHANGED
@@ -20,6 +20,7 @@ import { configuredSkills } from "../skills/custom-loader.js";
20
20
  import { builtinSkills } from "../skills/index.js";
21
21
  import { DagRenderer } from "./renderer.js";
22
22
  import { runE2eInit } from "./e2e.js";
23
+ import { SCHEDULING_DOCS_URL } from "./first-success.js";
23
24
  // ── Skill → Credential extras ─────────────────────────────────────────
24
25
  /**
25
26
  * Supplementary metadata (URLs, hints, defaults) for env vars.
@@ -853,7 +854,18 @@ export async function runNew(options) {
853
854
  steps.push(`${stepNum++}. Create your first workflow:`, ` ${cli} new`);
854
855
  }
855
856
  p.note(steps.join("\n"), "Next steps");
856
- p.outro(hasExistingConfig ? "Workflow added!" : "You're all set!");
857
+ p.outro(outroMessage(hasExistingConfig, Boolean(template) && !template?.pack));
858
+ }
859
+ export { SCHEDULING_DOCS_URL };
860
+ /**
861
+ * The closing line (#534). A pack already shows its own trigger step, so it
862
+ * closes as before. Any other workflow that was written (a starter or a
863
+ * described one) says how to schedule THIS workflow; it never names another
864
+ * pack, because no template metadata says which pack would suit it.
865
+ */
866
+ export function outroMessage(hasExistingConfig, offerSchedule) {
867
+ const done = hasExistingConfig ? "Workflow added!" : "You're all set!";
868
+ return offerSchedule ? `${done} Run it on a schedule: ${SCHEDULING_DOCS_URL}` : done;
857
869
  }
858
870
  // ── Custom workflow branch ────────────────────────────────────────────
859
871
  /**
package/dist/cli/packs.js CHANGED
@@ -1000,8 +1000,7 @@ nodes:
1000
1000
  (type comment). It can only land on context.input.pr_number; sweny
1001
1001
  posts it after this step.
1002
1002
 
1003
- Start the body with the line: <!-- sweny-pr-risk -->
1004
- Then: "Risk: LEVEL" with the size, then the reasons as short bullets
1003
+ Start the body with "Risk: LEVEL" and the size, then the reasons as short bullets
1005
1004
  (each naming its evidence), then the focus pointers, then a last line:
1006
1005
  "Read-only scope review. No code was changed."
1007
1006
  Keep it under 15 lines. No emoji walls, no praise.
@@ -1053,7 +1052,7 @@ on:
1053
1052
 
1054
1053
  permissions:
1055
1054
  contents: read
1056
- pull-requests: write # the review comment and the run billboard
1055
+ pull-requests: write # the one review comment, updated in place
1057
1056
 
1058
1057
  concurrency:
1059
1058
  group: sweny-pr-risk-\${{ github.event.pull_request.number }}
@@ -55,7 +55,7 @@ export declare function buildTimeline(lines: string[], fixture: TryFixture): {
55
55
  /** Render the timeline as a self-contained animated SVG. Pure and deterministic. */
56
56
  export declare function renderRecordingSvg(lines: string[], fixture: TryFixture): string;
57
57
  /**
58
- * The PR-comment preview (heading plus DAG) sits between the answer and the
58
+ * The job summary preview (heading plus DAG) sits between the answer and the
59
59
  * ticket. The recording drops it to stay a README-sized hero: tagline, nodes,
60
60
  * answer, ticket.
61
61
  */
@@ -17,7 +17,7 @@ import fs from "node:fs";
17
17
  import path from "node:path";
18
18
  import chalk from "chalk";
19
19
  import { GLYPHS, PALETTE, ROLE_COLORS, SPINNER_FRAMES, SURFACE_COLORS } from "../theme.js";
20
- import { loadTryFixture, runTry } from "./try.js";
20
+ import { loadTryFixture, runTry, SUMMARY_PREVIEW_LEAD } from "./try.js";
21
21
  /** Terminal width the demo is rendered at (the ticket and DAG are designed for 80). */
22
22
  export const RECORD_COLUMNS = 80;
23
23
  /** Lines longer than this wrap in the window, like a terminal would. */
@@ -292,12 +292,12 @@ export function renderRecordingSvg(lines, fixture) {
292
292
  // ── Entry points ────────────────────────────────────────────────
293
293
  const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
294
294
  /**
295
- * The PR-comment preview (heading plus DAG) sits between the answer and the
295
+ * The job summary preview (heading plus DAG) sits between the answer and the
296
296
  * ticket. The recording drops it to stay a README-sized hero: tagline, nodes,
297
297
  * answer, ticket.
298
298
  */
299
299
  export function dropCommentPreview(lines) {
300
- const from = lines.findIndex((l) => stripAnsi(l).includes("On a pull request, CI posts"));
300
+ const from = lines.findIndex((l) => stripAnsi(l).includes(SUMMARY_PREVIEW_LEAD));
301
301
  const to = lines.findIndex((l, i) => i > from && stripAnsi(l).startsWith(" \u256D"));
302
302
  if (from < 0 || to < 0)
303
303
  return lines;
@@ -320,7 +320,7 @@ export const WORKFLOW_RUN_OPTIONS = [
320
320
  ["--mermaid", "Output a Mermaid diagram with execution state after run"],
321
321
  [
322
322
  "--comment-file <path>",
323
- "Write a PR-comment markdown (run receipt, status-colored DAG, per-node table; metadata only) to <path> so any CI can post it",
323
+ "Write an empty PR-comment file at <path> (kept for the Action; the receipt and DAG go to the job summary)",
324
324
  ],
325
325
  ["--input <json>", "JSON string of input data to pass to the workflow"],
326
326
  ["--agent <id>", "Coding agent that runs the nodes: claude (default), codex, or pi (experimental)"],
package/dist/cli/try.d.ts CHANGED
@@ -95,6 +95,8 @@ export interface TryOptions {
95
95
  fixture?: TryFixture;
96
96
  }
97
97
  export declare const DEFAULT_TRY_PACE_MS = 1200;
98
+ /** The line that opens the job summary preview (record.ts cuts the preview from here). */
99
+ export declare const SUMMARY_PREVIEW_LEAD = "In CI, the job summary gets this heading, this DAG and the receipt below:";
98
100
  /** Replay the recording. Returns the process exit code. */
99
101
  export declare function runTry(opts?: TryOptions): Promise<number>;
100
102
  export declare function registerTryCommand(program: Command): Command;
package/dist/cli/try.js CHANGED
@@ -176,6 +176,8 @@ export function formatTryNext(paint = createPaint(false)) {
176
176
  }
177
177
  export const DEFAULT_TRY_PACE_MS = 1200;
178
178
  const silentLogger = { info() { }, warn() { }, error() { }, debug() { } };
179
+ /** The line that opens the job summary preview (record.ts cuts the preview from here). */
180
+ export const SUMMARY_PREVIEW_LEAD = "In CI, the job summary gets this heading, this DAG and the receipt below:";
179
181
  /** Replay the recording. Returns the process exit code. */
180
182
  export async function runTry(opts = {}) {
181
183
  const write = opts.write ?? ((s) => void process.stdout.write(s));
@@ -247,9 +249,9 @@ export async function runTry(opts = {}) {
247
249
  const answer = resolveFinalOutput(workflow, results);
248
250
  if (answer)
249
251
  write(`${formatFinalOutput(answer)}\n\n`);
250
- // Compact preview of the PR comment: its heading and DAG here, its ticket (the receipt) below.
252
+ // Compact preview of CI's job summary: heading and DAG here, receipt below.
251
253
  const comment = formatRunComment(workflow, results, receipt, { trace, durationsMs });
252
- write(` ${paint.muted("On a pull request, CI posts this heading, this DAG and the receipt below:")}\n\n`);
254
+ write(` ${paint.muted(SUMMARY_PREVIEW_LEAD)}\n\n`);
253
255
  const tone = receipt.ok ? paint.success : paint.error;
254
256
  write(` ${tone(receipt.ok ? g.success : g.failure)} ${paint.strong(workflow.name)}\n\n`);
255
257
  write(`${dag
package/dist/executor.js CHANGED
@@ -26,6 +26,7 @@ import { gitCredentialWarning, scanGitCredentials } from "./git-credentials.js";
26
26
  // #473: the sweny-side branch push in `github_create_pr` (Node only; skills/ stays browser-safe).
27
27
  import { bindBranchPusher } from "./skills/git-push.js";
28
28
  import { trustedEnvValue } from "./startup-env.js";
29
+ import { toolRunContext } from "./run-context.js";
29
30
  import { fenceUntrusted, fenceUntrustedJson } from "./untrusted.js";
30
31
  import { asClaude } from "./harness/compat.js";
31
32
  import { budgetGate, isToolClass, policyGate, resolveHarnessPolicy } from "./harness/policy.js";
@@ -213,6 +214,8 @@ async function executeRun(workflow, input, options, results, trace, at) {
213
214
  const pushBranch = bindBranchPusher(resolveCwd, runEnv);
214
215
  // GHES: the REST base for the github skill, never a value the workspace .env introduced.
215
216
  const githubApiUrl = trustedEnvValue(runEnv, "GITHUB_API_URL") || undefined;
217
+ // The run's identity and job-summary writer, the same for every tool, from the operator's env.
218
+ const runToolContext = toolRunContext(workflow.id, runEnv);
216
219
  const resolvedSources = await resolveSources(sourceMap, {
217
220
  cwd: resolveCwd,
218
221
  env: options.env ?? process.env,
@@ -421,6 +424,7 @@ async function executeRun(workflow, input, options, results, trace, at) {
421
424
  staged: stageOutputs,
422
425
  pushBranch,
423
426
  ...(githubApiUrl ? { githubApiUrl } : {}),
427
+ ...runToolContext,
424
428
  };
425
429
  const output = await t.handler(toolInput, toolCtx);
426
430
  safeObserve(observer, { type: "tool:result", node: currentId, tool: t.name, output }, logger);
@@ -832,6 +836,7 @@ async function executeRun(workflow, input, options, results, trace, at) {
832
836
  cwd: resolveCwd,
833
837
  pushBranch,
834
838
  ...(githubApiUrl ? { githubApiUrl } : {}),
839
+ ...runToolContext,
835
840
  state: writeState,
836
841
  logger,
837
842
  input: runInput,
package/dist/index.d.ts CHANGED
@@ -21,7 +21,7 @@
21
21
  * })
22
22
  * ```
23
23
  */
24
- export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, EdgeWhen, WhenExpression, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, NodeAccess, NodePermissions, NodePermissionsSpec, SafeOutputType, SafeOutputDeclaration, SafeOutputsPolicy, SafeOutputReceipt, SafeOutputPin, AuthorAssociation, } from "./types.js";
24
+ export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ToolRunContext, ConfigField, JSONSchema, Workflow, Node, Edge, EdgeWhen, WhenExpression, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, NodeAccess, NodePermissions, NodePermissionsSpec, SafeOutputType, SafeOutputDeclaration, SafeOutputsPolicy, SafeOutputReceipt, SafeOutputPin, AuthorAssociation, } from "./types.js";
25
25
  export { WORKFLOW_INPUT_TYPES } from "./types.js";
26
26
  export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, NODE_ACCESS, TOOL_CLASSES, SAFE_OUTPUT_TYPES, SAFE_OUTPUT_APPLIERS, AUTHOR_ASSOCIATIONS, } from "./types.js";
27
27
  export { consoleLogger } from "./types.js";
package/dist/journal.js CHANGED
@@ -1411,7 +1411,7 @@ async function probeProvider(toolName, args, key, skill, ctx, since) {
1411
1411
  return { state: "unknown", reason: err instanceof Error ? err.message : String(err) };
1412
1412
  }
1413
1413
  }
1414
- /** The ids a write produced, nothing else (refs and URLs for receipts). */
1414
+ /** The ids a write produced and a skip reason, nothing else (refs, URLs and skips for receipts). */
1415
1415
  function slimOutput(o, depth = 0) {
1416
1416
  if (!o || typeof o !== "object" || Array.isArray(o) || depth > 3)
1417
1417
  return null;
@@ -1425,7 +1425,7 @@ function slimOutput(o, depth = 0) {
1425
1425
  out.transient = true;
1426
1426
  }
1427
1427
  for (const [k, v] of Object.entries(o)) {
1428
- if (["number", "identifier", "id", "html_url", "url", "success", "reused"].includes(k)) {
1428
+ if (["number", "identifier", "id", "html_url", "url", "success", "reused", "skipped"].includes(k)) {
1429
1429
  if (typeof v === "string" || typeof v === "number" || typeof v === "boolean")
1430
1430
  out[k] = v;
1431
1431
  }