@sweny-ai/core 0.1.166 → 0.1.167
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/main.js +9 -3
- package/dist/cli/try-fixture.json +35 -0
- package/dist/cli/try.d.ts +89 -0
- package/dist/cli/try.js +284 -0
- package/dist/cli/version-check.js +3 -0
- package/package.json +2 -2
package/dist/cli/main.js
CHANGED
|
@@ -34,6 +34,7 @@ import { nonInteractiveUsage, runNew } from "./new.js";
|
|
|
34
34
|
import { formatFinalMarkdown, formatFinalOutput, resolveFinalOutput, writeFinalOutput } from "./final-output.js";
|
|
35
35
|
import { buildRunRecord, createNodeTimer, historyDisabled, newRunId, recordRun } from "./run-history.js";
|
|
36
36
|
import { registerRunsCommand } from "./runs.js";
|
|
37
|
+
import { registerTryCommand } from "./try.js";
|
|
37
38
|
import { JournalLockedError, JournalMismatchError, RunJournal } from "../journal.js";
|
|
38
39
|
import { journalDisabled, prepareResume, RESUME_SHARED_RUN_FLAGS, WORKFLOW_RESUME_OPTIONS, } from "./resume.js";
|
|
39
40
|
import { runE2eRun, runWithWallClockBudget, DEFAULT_WORKFLOW_TIMEOUT_MS } from "./e2e.js";
|
|
@@ -76,9 +77,12 @@ function composeObservers(...observers) {
|
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
// Auto-load .env before Commander parses (so env vars are available for defaults)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// `sweny try` (#475) reads no .env and no config: it is a recording replay with no credentials.
|
|
81
|
+
if (process.argv[2] !== "try") {
|
|
82
|
+
loadDotenv();
|
|
83
|
+
// Agent sandbox / env-passthrough keys from .sweny.yml -> SWENY_* env (#360).
|
|
84
|
+
applyAgentFileConfig(loadConfigFile());
|
|
85
|
+
}
|
|
82
86
|
const program = new Command()
|
|
83
87
|
.name("sweny")
|
|
84
88
|
.description("Workflows for coding agents. One set of rules, a receipt for every run.")
|
|
@@ -98,6 +102,8 @@ program
|
|
|
98
102
|
}
|
|
99
103
|
await runNew({ marketplaceId: id, template: options.template, yes: options.yes });
|
|
100
104
|
});
|
|
105
|
+
// ── sweny try ─────────────────────────────────────────────────────────
|
|
106
|
+
registerTryCommand(program);
|
|
101
107
|
// ── sweny check ───────────────────────────────────────────────────────
|
|
102
108
|
/** validateInputs' generic agent-auth lines, replaced by the harness preflight reason (#339). */
|
|
103
109
|
const AGENT_AUTH_ERROR_PREFIXES = ["Missing: ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN", "Missing: CODEX_API_KEY"];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"illustrative": true,
|
|
4
|
+
"notice": "ILLUSTRATIVE SAMPLE DATA. This is not output from a real run and not about your code. The repo (acme-notes-api) is fictional; timings, tool-call counts, token usage and cost are plausible numbers for a small repo, written by hand. Replace this file with a recorded real run (metadata plus the sample answer only) and set illustrative to false.",
|
|
5
|
+
"workflow": "explain-repo",
|
|
6
|
+
"sample_repo": "acme-notes-api",
|
|
7
|
+
"harness": { "id": "claude-code", "version": "sample" },
|
|
8
|
+
"policy": { "envScope": true, "sandbox": "auto", "sandboxStarted": true },
|
|
9
|
+
"nodes": {
|
|
10
|
+
"survey": {
|
|
11
|
+
"duration_ms": 18400,
|
|
12
|
+
"tool_calls": { "Glob": 3, "Read": 7, "Grep": 2 },
|
|
13
|
+
"usage": { "inputTokens": 21300, "outputTokens": 1150, "costUsd": 0.09, "numTurns": 8 }
|
|
14
|
+
},
|
|
15
|
+
"explain": {
|
|
16
|
+
"duration_ms": 9100,
|
|
17
|
+
"tool_calls": {},
|
|
18
|
+
"usage": { "inputTokens": 6800, "outputTokens": 640, "costUsd": 0.04, "numTurns": 1 },
|
|
19
|
+
"output": {
|
|
20
|
+
"purpose": "A small REST API for storing and searching notes, built with Express and TypeScript on top of SQLite. Meant as the backend for a notes app.",
|
|
21
|
+
"how_to_run": ["npm install", "npm run build", "npm test", "npm start (serves on http://localhost:3000)"],
|
|
22
|
+
"key_files": [
|
|
23
|
+
"src/server.ts: the entry point, wires routes and the database",
|
|
24
|
+
"src/routes/notes.ts: the CRUD and search endpoints",
|
|
25
|
+
"src/db/schema.sql: the tables, applied on first start",
|
|
26
|
+
"test/notes.test.ts: the API tests, a good map of expected behavior"
|
|
27
|
+
],
|
|
28
|
+
"risks": [
|
|
29
|
+
"No auth on any route: fine locally, not safe to expose as is.",
|
|
30
|
+
"The README does not mention the PORT or DB_PATH environment variables that src/server.ts reads."
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sweny try`: a zero-credential demo (#475).
|
|
3
|
+
*
|
|
4
|
+
* Replays a recorded explain-repo run through the REAL executor, with a replay
|
|
5
|
+
* harness standing in for the agent, so the real progress line, answer block
|
|
6
|
+
* (#460), receipt, policy segment and PR-comment markdown (#396) are what a
|
|
7
|
+
* user sees. No agent, no model, no network, no env credentials, no files
|
|
8
|
+
* written (no `.sweny/runs`, no journal). The environment handed to the
|
|
9
|
+
* executor is empty by construction.
|
|
10
|
+
*
|
|
11
|
+
* The recording lives in `try-fixture.json` (metadata plus the sample answer
|
|
12
|
+
* only). Until a real run is recorded it is labelled illustrative.
|
|
13
|
+
*/
|
|
14
|
+
import type { Command } from "commander";
|
|
15
|
+
import type { AgentHarness } from "../harness/types.js";
|
|
16
|
+
import type { Workflow } from "../types.js";
|
|
17
|
+
export interface TryFixtureNode {
|
|
18
|
+
/** Recorded wall-clock time of the node. */
|
|
19
|
+
duration_ms: number;
|
|
20
|
+
/** Tool name to number of calls. Counts only: no inputs or outputs are recorded. */
|
|
21
|
+
tool_calls: Record<string, number>;
|
|
22
|
+
usage?: {
|
|
23
|
+
inputTokens?: number;
|
|
24
|
+
outputTokens?: number;
|
|
25
|
+
costUsd?: number;
|
|
26
|
+
numTurns?: number;
|
|
27
|
+
};
|
|
28
|
+
/** The node's declared output fields (the sample answer). */
|
|
29
|
+
output?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
export interface TryFixture {
|
|
32
|
+
schema_version: 1;
|
|
33
|
+
/** True until the owner replaces the file with a recording of a real run. */
|
|
34
|
+
illustrative: boolean;
|
|
35
|
+
notice: string;
|
|
36
|
+
/** Built-in template id the recording is of. */
|
|
37
|
+
workflow: string;
|
|
38
|
+
sample_repo: string;
|
|
39
|
+
harness: {
|
|
40
|
+
id: string;
|
|
41
|
+
version: string;
|
|
42
|
+
};
|
|
43
|
+
policy?: {
|
|
44
|
+
envScope: boolean;
|
|
45
|
+
sandbox: "off" | "auto" | "strict";
|
|
46
|
+
sandboxStarted: boolean;
|
|
47
|
+
};
|
|
48
|
+
/** Node id to recording, in execution order. */
|
|
49
|
+
nodes: Record<string, TryFixtureNode>;
|
|
50
|
+
}
|
|
51
|
+
/** Shape check with a message naming the first bad field. Throws on a malformed fixture. */
|
|
52
|
+
export declare function parseTryFixture(raw: unknown): TryFixture;
|
|
53
|
+
/** The shipped fixture: `try-fixture.json` next to this module (src/cli or dist/cli). */
|
|
54
|
+
export declare function loadTryFixture(file?: string): TryFixture;
|
|
55
|
+
/** The real explain-repo template, parsed and validated like any workflow file. */
|
|
56
|
+
export declare function loadTryWorkflow(templateId: string): Workflow;
|
|
57
|
+
/**
|
|
58
|
+
* An AgentHarness that plays the recording back: node N of the run gets
|
|
59
|
+
* recording N. It never spawns a process, reads env, or touches the network.
|
|
60
|
+
* `paceMs` is how long each node takes to replay (0 = instant); tool calls are
|
|
61
|
+
* reported through `onProgress` spread across that time so progress feels live.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createReplayHarness(fixture: TryFixture, opts: {
|
|
64
|
+
paceMs: number;
|
|
65
|
+
}): AgentHarness;
|
|
66
|
+
export declare const TRY_BANNER_TITLE = "Recorded demo";
|
|
67
|
+
/** The real commands, same as the README quickstart. */
|
|
68
|
+
export declare const TRY_NEXT_COMMANDS: readonly ["npm install -g @sweny-ai/core", "sweny new --template explain-repo --yes", "sweny workflow run .sweny/workflows/explain-repo.yml"];
|
|
69
|
+
/** Appended to `sweny try --help`. */
|
|
70
|
+
export declare const TRY_HELP_NOTE: string;
|
|
71
|
+
export declare function formatTryBanner(fixture: TryFixture): string;
|
|
72
|
+
export declare function formatTryNext(): string;
|
|
73
|
+
export interface TryOptions {
|
|
74
|
+
/** Replay instantly. */
|
|
75
|
+
fast?: boolean;
|
|
76
|
+
/** Milliseconds each node takes to replay. Default 1200. Ignored with `fast`. */
|
|
77
|
+
paceMs?: number;
|
|
78
|
+
/** Also write the PR-comment markdown here. */
|
|
79
|
+
commentFile?: string;
|
|
80
|
+
/** Output sink. Default stdout. */
|
|
81
|
+
write?: (s: string) => void;
|
|
82
|
+
/** Rewrite progress lines in place. Default: stdout is a TTY. */
|
|
83
|
+
tty?: boolean;
|
|
84
|
+
fixture?: TryFixture;
|
|
85
|
+
}
|
|
86
|
+
export declare const DEFAULT_TRY_PACE_MS = 1200;
|
|
87
|
+
/** Replay the recording. Returns the process exit code. */
|
|
88
|
+
export declare function runTry(opts?: TryOptions): Promise<number>;
|
|
89
|
+
export declare function registerTryCommand(program: Command): Command;
|
package/dist/cli/try.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sweny try`: a zero-credential demo (#475).
|
|
3
|
+
*
|
|
4
|
+
* Replays a recorded explain-repo run through the REAL executor, with a replay
|
|
5
|
+
* harness standing in for the agent, so the real progress line, answer block
|
|
6
|
+
* (#460), receipt, policy segment and PR-comment markdown (#396) are what a
|
|
7
|
+
* user sees. No agent, no model, no network, no env credentials, no files
|
|
8
|
+
* written (no `.sweny/runs`, no journal). The environment handed to the
|
|
9
|
+
* executor is empty by construction.
|
|
10
|
+
*
|
|
11
|
+
* The recording lives in `try-fixture.json` (metadata plus the sample answer
|
|
12
|
+
* only). Until a real run is recorded it is labelled illustrative.
|
|
13
|
+
*/
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import chalk from "chalk";
|
|
18
|
+
import { parse as parseYaml } from "yaml";
|
|
19
|
+
import { execute } from "../executor.js";
|
|
20
|
+
import { validateParsed } from "../loader.js";
|
|
21
|
+
import { WORKFLOW_TEMPLATES } from "./templates.js";
|
|
22
|
+
import { formatFinalOutput, resolveFinalOutput } from "./final-output.js";
|
|
23
|
+
import { formatRunComment } from "./comment-output.js";
|
|
24
|
+
import { formatReceiptDuration, renderReceiptLine, summarizeRun } from "./run-output.js";
|
|
25
|
+
import { c } from "./output.js";
|
|
26
|
+
const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
27
|
+
/** Shape check with a message naming the first bad field. Throws on a malformed fixture. */
|
|
28
|
+
export function parseTryFixture(raw) {
|
|
29
|
+
const bad = (what) => {
|
|
30
|
+
throw new Error(`try-fixture.json: ${what}`);
|
|
31
|
+
};
|
|
32
|
+
if (!isObj(raw))
|
|
33
|
+
return bad("expected an object");
|
|
34
|
+
if (raw.schema_version !== 1)
|
|
35
|
+
bad("schema_version must be 1");
|
|
36
|
+
for (const key of ["notice", "workflow", "sample_repo"]) {
|
|
37
|
+
if (typeof raw[key] !== "string" || raw[key] === "")
|
|
38
|
+
bad(`${key} must be a non-empty string`);
|
|
39
|
+
}
|
|
40
|
+
if (typeof raw.illustrative !== "boolean")
|
|
41
|
+
bad("illustrative must be a boolean");
|
|
42
|
+
if (!isObj(raw.harness) || typeof raw.harness.id !== "string" || typeof raw.harness.version !== "string") {
|
|
43
|
+
bad("harness must be { id, version }");
|
|
44
|
+
}
|
|
45
|
+
if (!isObj(raw.nodes) || Object.keys(raw.nodes).length === 0)
|
|
46
|
+
return bad("nodes must be a non-empty object");
|
|
47
|
+
for (const [id, n] of Object.entries(raw.nodes)) {
|
|
48
|
+
if (!isObj(n) || typeof n.duration_ms !== "number" || !isObj(n.tool_calls)) {
|
|
49
|
+
bad(`nodes.${id} needs duration_ms and tool_calls`);
|
|
50
|
+
}
|
|
51
|
+
for (const [tool, count] of Object.entries(n.tool_calls)) {
|
|
52
|
+
if (!Number.isInteger(count) || count < 0)
|
|
53
|
+
bad(`nodes.${id}.tool_calls.${tool} must be a count`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return raw;
|
|
57
|
+
}
|
|
58
|
+
/** The shipped fixture: `try-fixture.json` next to this module (src/cli or dist/cli). */
|
|
59
|
+
export function loadTryFixture(file) {
|
|
60
|
+
const p = file ?? fileURLToPath(new URL("./try-fixture.json", import.meta.url));
|
|
61
|
+
return parseTryFixture(JSON.parse(fs.readFileSync(p, "utf-8")));
|
|
62
|
+
}
|
|
63
|
+
/** The real explain-repo template, parsed and validated like any workflow file. */
|
|
64
|
+
export function loadTryWorkflow(templateId) {
|
|
65
|
+
const tpl = WORKFLOW_TEMPLATES.find((t) => t.id === templateId);
|
|
66
|
+
if (!tpl)
|
|
67
|
+
throw new Error(`try-fixture.json: unknown template "${templateId}"`);
|
|
68
|
+
const loaded = validateParsed(parseYaml(tpl.yaml));
|
|
69
|
+
if (!loaded.ok)
|
|
70
|
+
throw new Error(`template ${templateId} is invalid: ${loaded.errors.map((e) => e.message).join("; ")}`);
|
|
71
|
+
return loaded.workflow;
|
|
72
|
+
}
|
|
73
|
+
// ── Replay harness ──────────────────────────────────────────────
|
|
74
|
+
const REPLAY_CAPABILITIES = {
|
|
75
|
+
structuredOutput: "prompt",
|
|
76
|
+
toolTrace: "skill-only",
|
|
77
|
+
builtinDeny: "none",
|
|
78
|
+
mcp: { inject: false, exclusive: "none" },
|
|
79
|
+
sandbox: { fs: false, network: false },
|
|
80
|
+
readOnly: "none",
|
|
81
|
+
turnLimit: "none",
|
|
82
|
+
usage: { tokens: true, costUsd: true, live: false },
|
|
83
|
+
cancel: "signal",
|
|
84
|
+
resume: false,
|
|
85
|
+
};
|
|
86
|
+
function sleep(ms, signal) {
|
|
87
|
+
if (ms <= 0)
|
|
88
|
+
return Promise.resolve();
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
const t = setTimeout(resolve, ms);
|
|
91
|
+
signal?.addEventListener("abort", () => {
|
|
92
|
+
clearTimeout(t);
|
|
93
|
+
resolve();
|
|
94
|
+
}, { once: true });
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* An AgentHarness that plays the recording back: node N of the run gets
|
|
99
|
+
* recording N. It never spawns a process, reads env, or touches the network.
|
|
100
|
+
* `paceMs` is how long each node takes to replay (0 = instant); tool calls are
|
|
101
|
+
* reported through `onProgress` spread across that time so progress feels live.
|
|
102
|
+
*/
|
|
103
|
+
export function createReplayHarness(fixture, opts) {
|
|
104
|
+
const order = Object.keys(fixture.nodes);
|
|
105
|
+
let next = 0;
|
|
106
|
+
return {
|
|
107
|
+
id: "mock",
|
|
108
|
+
capabilities: REPLAY_CAPABILITIES,
|
|
109
|
+
async preflight() {
|
|
110
|
+
return { ok: true, version: "replay" };
|
|
111
|
+
},
|
|
112
|
+
// Routing and judging never run in a linear recorded workflow; fail closed if asked.
|
|
113
|
+
async complete() {
|
|
114
|
+
return null;
|
|
115
|
+
},
|
|
116
|
+
async run(req) {
|
|
117
|
+
const id = order[next++];
|
|
118
|
+
const rec = id ? fixture.nodes[id] : undefined;
|
|
119
|
+
const harness = { id: fixture.harness.id, version: fixture.harness.version };
|
|
120
|
+
if (!rec) {
|
|
121
|
+
return {
|
|
122
|
+
status: "failed",
|
|
123
|
+
data: { error: "recording has no more nodes" },
|
|
124
|
+
toolCalls: [],
|
|
125
|
+
harness,
|
|
126
|
+
degraded: [],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const toolCalls = [];
|
|
130
|
+
for (const [tool, count] of Object.entries(rec.tool_calls)) {
|
|
131
|
+
for (let i = 0; i < count; i++)
|
|
132
|
+
toolCalls.push({ tool, input: {} });
|
|
133
|
+
}
|
|
134
|
+
const step = toolCalls.length > 0 ? opts.paceMs / toolCalls.length : opts.paceMs;
|
|
135
|
+
for (const tc of toolCalls) {
|
|
136
|
+
await sleep(step, req.signal);
|
|
137
|
+
req.onProgress?.(tc.tool);
|
|
138
|
+
}
|
|
139
|
+
if (toolCalls.length === 0)
|
|
140
|
+
await sleep(step, req.signal);
|
|
141
|
+
return {
|
|
142
|
+
status: "success",
|
|
143
|
+
data: { ...(rec.output ?? {}) },
|
|
144
|
+
toolCalls,
|
|
145
|
+
...(rec.usage ? { usage: rec.usage } : {}),
|
|
146
|
+
...(fixture.policy ? { policy: fixture.policy } : {}),
|
|
147
|
+
harness,
|
|
148
|
+
degraded: [],
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// ── Output ──────────────────────────────────────────────────────
|
|
154
|
+
export const TRY_BANNER_TITLE = "Recorded demo";
|
|
155
|
+
/** The real commands, same as the README quickstart. */
|
|
156
|
+
export const TRY_NEXT_COMMANDS = [
|
|
157
|
+
"npm install -g @sweny-ai/core",
|
|
158
|
+
"sweny new --template explain-repo --yes",
|
|
159
|
+
"sweny workflow run .sweny/workflows/explain-repo.yml",
|
|
160
|
+
];
|
|
161
|
+
/** Appended to `sweny try --help`. */
|
|
162
|
+
export const TRY_HELP_NOTE = "The recording is ILLUSTRATIVE SAMPLE DATA (a fictional repo, hand-written timings and token counts), not output from a real run. " +
|
|
163
|
+
"Nothing runs: no agent, no network, no credentials read, no files written.";
|
|
164
|
+
export function formatTryBanner(fixture) {
|
|
165
|
+
const lines = [
|
|
166
|
+
`${c.brand("▲")} ${chalk.bold("sweny try")} ${chalk.bold.yellow(TRY_BANNER_TITLE)}, not a live run`,
|
|
167
|
+
chalk.dim(` A replay of an ${fixture.workflow} run on a sample repo (${fixture.sample_repo}).`),
|
|
168
|
+
chalk.dim(fixture.illustrative
|
|
169
|
+
? " The sample data is illustrative, not output from your code. No agent, no network, no credentials."
|
|
170
|
+
: " No agent, no network, no credentials."),
|
|
171
|
+
];
|
|
172
|
+
return lines.map((l, i) => (i === 0 ? ` ${l}` : l)).join("\n");
|
|
173
|
+
}
|
|
174
|
+
export function formatTryNext() {
|
|
175
|
+
return [
|
|
176
|
+
` ${chalk.bold("Run it for real")} on your own repo:`,
|
|
177
|
+
"",
|
|
178
|
+
...TRY_NEXT_COMMANDS.map((cmd) => ` ${cmd}`),
|
|
179
|
+
"",
|
|
180
|
+
chalk.dim(" Needs a Claude login (`claude` signed in) or ANTHROPIC_API_KEY. Your run ends with the same receipt."),
|
|
181
|
+
].join("\n");
|
|
182
|
+
}
|
|
183
|
+
export const DEFAULT_TRY_PACE_MS = 1200;
|
|
184
|
+
const silentLogger = { info() { }, warn() { }, error() { }, debug() { } };
|
|
185
|
+
/** Replay the recording. Returns the process exit code. */
|
|
186
|
+
export async function runTry(opts = {}) {
|
|
187
|
+
const write = opts.write ?? ((s) => void process.stdout.write(s));
|
|
188
|
+
const tty = opts.tty ?? Boolean(process.stdout.isTTY);
|
|
189
|
+
const paceMs = opts.fast ? 0 : Math.max(0, opts.paceMs ?? DEFAULT_TRY_PACE_MS);
|
|
190
|
+
const fixture = opts.fixture ?? loadTryFixture();
|
|
191
|
+
const workflow = loadTryWorkflow(fixture.workflow);
|
|
192
|
+
for (const id of Object.keys(fixture.nodes)) {
|
|
193
|
+
if (!workflow.nodes[id])
|
|
194
|
+
throw new Error(`try-fixture.json: node "${id}" is not in ${fixture.workflow}`);
|
|
195
|
+
}
|
|
196
|
+
write(`\n${formatTryBanner(fixture)}\n\n`);
|
|
197
|
+
const ticks = new Map();
|
|
198
|
+
const observer = (event) => {
|
|
199
|
+
switch (event.type) {
|
|
200
|
+
case "workflow:start":
|
|
201
|
+
write(` ${c.brand("▲")} ${chalk.bold(event.workflow)}\n\n`);
|
|
202
|
+
break;
|
|
203
|
+
case "node:enter":
|
|
204
|
+
ticks.set(event.node, 0);
|
|
205
|
+
if (tty)
|
|
206
|
+
write(` ${c.subtle("○")} ${chalk.dim(event.node)}…`);
|
|
207
|
+
break;
|
|
208
|
+
case "node:progress": {
|
|
209
|
+
const n = (ticks.get(event.node) ?? 0) + 1;
|
|
210
|
+
ticks.set(event.node, n);
|
|
211
|
+
if (tty) {
|
|
212
|
+
write(`\r\x1B[2K ${c.subtle("○")} ${chalk.dim(event.node)}… ${chalk.dim(`${n} tool ${n === 1 ? "call" : "calls"}`)}`);
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case "node:exit": {
|
|
217
|
+
const rec = fixture.nodes[event.node];
|
|
218
|
+
const ok = event.result.status === "success";
|
|
219
|
+
const icon = ok ? c.ok("✓") : c.fail("✗");
|
|
220
|
+
const calls = event.result.toolCalls.length;
|
|
221
|
+
const detail = [formatReceiptDuration(rec?.duration_ms ?? 0), ...(calls > 0 ? [`${calls} tool calls`] : [])];
|
|
222
|
+
write(`${tty ? "\r\x1B[2K" : ""} ${icon} ${event.node} ${chalk.dim(detail.join(" · "))}\n`);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const { results, trace } = await execute(workflow, {}, {
|
|
228
|
+
skills: new Map(),
|
|
229
|
+
harness: createReplayHarness(fixture, { paceMs }),
|
|
230
|
+
observer,
|
|
231
|
+
logger: silentLogger,
|
|
232
|
+
// Nothing for the executor to read: no credentials, no PATH, no config.
|
|
233
|
+
env: {},
|
|
234
|
+
offline: true,
|
|
235
|
+
harnessPolicy: "warn",
|
|
236
|
+
});
|
|
237
|
+
write("\n");
|
|
238
|
+
// The recording's timings, not the replay's wall clock.
|
|
239
|
+
const durationsMs = Object.fromEntries(Object.entries(fixture.nodes).map(([id, n]) => [id, n.duration_ms]));
|
|
240
|
+
const totalMs = Object.values(durationsMs).reduce((a, b) => a + b, 0);
|
|
241
|
+
const receipt = summarizeRun(results, totalMs, false, trace);
|
|
242
|
+
const answer = resolveFinalOutput(workflow, results);
|
|
243
|
+
if (answer)
|
|
244
|
+
write(`${formatFinalOutput(answer)}\n\n`);
|
|
245
|
+
write(` ${renderReceiptLine(receipt, tty)}\n\n`);
|
|
246
|
+
const comment = formatRunComment(workflow, results, receipt, { trace, durationsMs });
|
|
247
|
+
write(` ${chalk.dim("What CI posts on a pull request (sweny workflow run --comment-file):")}\n\n`);
|
|
248
|
+
write(`${comment
|
|
249
|
+
.split("\n")
|
|
250
|
+
.map((l) => (l === "" ? l : ` ${l}`))
|
|
251
|
+
.join("\n")}\n`);
|
|
252
|
+
if (opts.commentFile) {
|
|
253
|
+
try {
|
|
254
|
+
fs.mkdirSync(path.dirname(path.resolve(opts.commentFile)), { recursive: true });
|
|
255
|
+
fs.writeFileSync(opts.commentFile, comment);
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
process.stderr.write(` ⚠ could not write comment file: ${err instanceof Error ? err.message : err}\n`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
write(`${formatTryNext()}\n\n`);
|
|
262
|
+
return receipt.ok ? 0 : 1;
|
|
263
|
+
}
|
|
264
|
+
export function registerTryCommand(program) {
|
|
265
|
+
return program
|
|
266
|
+
.command("try")
|
|
267
|
+
.description("Replay a recorded demo run: no credentials, no network, no model calls (illustrative sample data)")
|
|
268
|
+
.option("--fast", "Replay instantly instead of pacing the progress")
|
|
269
|
+
.option("--pace <ms>", `Milliseconds each node takes to replay (default ${DEFAULT_TRY_PACE_MS})`)
|
|
270
|
+
.option("--comment-file <path>", "Also write the PR-comment markdown to <path>")
|
|
271
|
+
.addHelpText("after", `\n${TRY_HELP_NOTE}\n`)
|
|
272
|
+
.action(async (options) => {
|
|
273
|
+
let paceMs;
|
|
274
|
+
if (options.pace !== undefined) {
|
|
275
|
+
paceMs = Number(options.pace);
|
|
276
|
+
if (!Number.isFinite(paceMs) || paceMs < 0) {
|
|
277
|
+
console.error(chalk.red(`\n --pace must be a non-negative number of milliseconds, got "${options.pace}"\n`));
|
|
278
|
+
process.exitCode = 1;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
process.exitCode = await runTry({ fast: options.fast, paceMs, commentFile: options.commentFile });
|
|
283
|
+
});
|
|
284
|
+
}
|
|
@@ -38,6 +38,9 @@ export function shouldSkip(env, isTty, commandName) {
|
|
|
38
38
|
return true;
|
|
39
39
|
if (commandName === "upgrade" || commandName === "update")
|
|
40
40
|
return true;
|
|
41
|
+
// `sweny try` makes no network calls, the version check included.
|
|
42
|
+
if (commandName === "try")
|
|
43
|
+
return true;
|
|
41
44
|
return false;
|
|
42
45
|
}
|
|
43
46
|
export function readCache(cachePath) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sweny-ai/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.167",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sweny": "./dist/cli/main.js"
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"dist"
|
|
51
51
|
],
|
|
52
52
|
"scripts": {
|
|
53
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json && cp src/workflows/*.yml dist/workflows/ && node scripts/build-workflow-browser.mjs && node scripts/write-public-schema.mjs",
|
|
53
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && cp src/workflows/*.yml dist/workflows/ && cp src/cli/try-fixture.json dist/cli/ && node scripts/build-workflow-browser.mjs && node scripts/write-public-schema.mjs",
|
|
54
54
|
"typecheck": "tsc --noEmit",
|
|
55
55
|
"test": "vitest run --exclude 'src/__tests__/integration/**' --exclude 'dist/**'",
|
|
56
56
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|