@sweny-ai/core 0.1.185 → 0.1.186
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/comment-output.d.ts +4 -1
- package/dist/cli/comment-output.js +3 -1
- package/dist/cli/main.js +17 -1
- package/dist/cli/run-answers.d.ts +162 -0
- package/dist/cli/run-answers.js +451 -0
- package/dist/cli/run-history.d.ts +10 -2
- package/dist/cli/run-history.js +17 -5
- package/dist/cli/run-output.d.ts +2 -2
- package/dist/cli/run-output.js +5 -3
- package/dist/cli/runs.d.ts +42 -3
- package/dist/cli/runs.js +262 -45
- package/dist/cli/terminal.d.ts +9 -0
- package/dist/cli/terminal.js +22 -0
- package/package.json +1 -1
|
@@ -4,7 +4,8 @@
|
|
|
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,
|
|
7
|
-
* tool inputs, node outputs, errors, or model prose.
|
|
7
|
+
* tool inputs, node outputs, errors, or model prose. The optional "since last
|
|
8
|
+
* run" line carries counts of added and resolved answers, never their text.
|
|
8
9
|
*/
|
|
9
10
|
import type { ExecutionTrace, NodeResult, Workflow } from "../types.js";
|
|
10
11
|
import { type RunSummary } from "./run-output.js";
|
|
@@ -15,6 +16,8 @@ export interface RunCommentOptions {
|
|
|
15
16
|
durationsMs?: Record<string, number>;
|
|
16
17
|
/** Run id; the ticket shows its short hash. */
|
|
17
18
|
runId?: string;
|
|
19
|
+
/** Pre-rendered "since last run" markdown line (run-answers.ts formatSinceLastMarkdown): counts only. */
|
|
20
|
+
sinceLast?: string | null;
|
|
18
21
|
}
|
|
19
22
|
/** Hidden marker the poster searches for to update the comment in place. */
|
|
20
23
|
export declare function runCommentMarker(workflowId: string): string;
|
|
@@ -4,7 +4,8 @@
|
|
|
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,
|
|
7
|
-
* tool inputs, node outputs, errors, or model prose.
|
|
7
|
+
* tool inputs, node outputs, errors, or model prose. The optional "since last
|
|
8
|
+
* run" line carries counts of added and resolved answers, never their text.
|
|
8
9
|
*/
|
|
9
10
|
import { workspaceRoot, writeFileNoFollow } from "../safe-file.js";
|
|
10
11
|
import { toMermaidBlock } from "../mermaid.js";
|
|
@@ -40,6 +41,7 @@ export function formatRunComment(workflow, results, summary, opts = {}) {
|
|
|
40
41
|
"",
|
|
41
42
|
ticketBlock(workflow, summary, opts.runId),
|
|
42
43
|
"",
|
|
44
|
+
...(opts.sinceLast ? [opts.sinceLast, ""] : []),
|
|
43
45
|
toMermaidBlock(workflow, { state, trace: opts.trace }),
|
|
44
46
|
"",
|
|
45
47
|
"<details>",
|
package/dist/cli/main.js
CHANGED
|
@@ -35,6 +35,7 @@ import { nonInteractiveUsage, runNew } from "./new.js";
|
|
|
35
35
|
import { formatFinalMarkdown, formatFinalOutput, formatSavedOutputLine, resolveFinalOutput, shouldPrintFinalOutput, writeFinalOutput, } from "./final-output.js";
|
|
36
36
|
import { buildRunRecord, createNodeTimer, historyDisabled, newRunId, recordRun } from "./run-history.js";
|
|
37
37
|
import { registerRunsCommand } from "./runs.js";
|
|
38
|
+
import { formatSinceLast, formatSinceLastMarkdown, recordRunAnswers } from "./run-answers.js";
|
|
38
39
|
import { registerTryCommand } from "./try.js";
|
|
39
40
|
import { JournalLockedError, JournalMismatchError, RunJournal, runSecretValues } from "../journal.js";
|
|
40
41
|
import { journalDisabled, prepareResume, RESUME_SHARED_RUN_FLAGS, WORKFLOW_RESUME_OPTIONS, } from "./resume.js";
|
|
@@ -1053,6 +1054,16 @@ resume) {
|
|
|
1053
1054
|
const wfHasFailed = [...results.values()].some((r) => r.status === "failed");
|
|
1054
1055
|
journal?.end(wfHasFailed ? "failed" : "success");
|
|
1055
1056
|
recordHistory(results, trace, false);
|
|
1057
|
+
// What the run answered (declared output fields, redacted), beside the history record, then a
|
|
1058
|
+
// comparison with the previous run of this workflow for the "since last run" line.
|
|
1059
|
+
const sinceLast = historyDisabled(options.history, fileConfig["history"])
|
|
1060
|
+
? null
|
|
1061
|
+
: recordRunAnswers({
|
|
1062
|
+
runId,
|
|
1063
|
+
workflow,
|
|
1064
|
+
results,
|
|
1065
|
+
secrets: runSecretValues([...skills.values(), ...builtinSkills], process.env),
|
|
1066
|
+
});
|
|
1056
1067
|
// Close the cloud lifecycle session BEFORE the JSON early-exit so
|
|
1057
1068
|
// every workflow run reports a terminal status, regardless of how
|
|
1058
1069
|
// the CLI returns (json, mermaid, or normal stdout).
|
|
@@ -1069,6 +1080,7 @@ resume) {
|
|
|
1069
1080
|
trace,
|
|
1070
1081
|
durationsMs: Object.fromEntries(nodeTimer.durations),
|
|
1071
1082
|
runId,
|
|
1083
|
+
sinceLast: sinceLast ? formatSinceLastMarkdown(sinceLast) : null,
|
|
1072
1084
|
});
|
|
1073
1085
|
}
|
|
1074
1086
|
if (isJson) {
|
|
@@ -1094,7 +1106,7 @@ resume) {
|
|
|
1094
1106
|
// Run receipt (metadata only) + optional $GITHUB_STEP_SUMMARY.
|
|
1095
1107
|
runLogger.flush();
|
|
1096
1108
|
const receipt = summarizeRun(results, wfDurationMs, false, trace);
|
|
1097
|
-
writeStepSummary(workflow, results, receipt, trace);
|
|
1109
|
+
writeStepSummary(workflow, results, receipt, trace, process.env, sinceLast ? formatSinceLastMarkdown(sinceLast) : null);
|
|
1098
1110
|
// The answer, above the receipt: the terminal node's result, or the failed node's error.
|
|
1099
1111
|
// Also saved to .sweny/runs/<id>/output.md (local only; --no-history skips the file).
|
|
1100
1112
|
// Redacted with the journal's redactor; in CI printed only with --show-output.
|
|
@@ -1117,12 +1129,16 @@ resume) {
|
|
|
1117
1129
|
// The receipt: a ticket on a TTY, one plain line in CI and pipes.
|
|
1118
1130
|
if (wfHasFailed) {
|
|
1119
1131
|
await writeReceipt(receipt, { workflow: workflow.id, runId, stream: process.stderr });
|
|
1132
|
+
if (sinceLast)
|
|
1133
|
+
console.error(createPaint(colorEnabled(process.stderr)).muted(` ${formatSinceLast(sinceLast)}`));
|
|
1120
1134
|
if (journal?.active)
|
|
1121
1135
|
console.error(c.subtle(` resume with: sweny workflow resume ${runId}\n`));
|
|
1122
1136
|
process.exit(1);
|
|
1123
1137
|
return;
|
|
1124
1138
|
}
|
|
1125
1139
|
await writeReceipt(receipt, { workflow: workflow.id, runId, stream: process.stdout });
|
|
1140
|
+
if (sinceLast)
|
|
1141
|
+
console.log(createPaint(colorEnabled(process.stdout)).muted(` ${formatSinceLast(sinceLast)}`));
|
|
1126
1142
|
process.exit(0);
|
|
1127
1143
|
}
|
|
1128
1144
|
catch (err) {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a run answered, kept so the next run can say what changed.
|
|
3
|
+
*
|
|
4
|
+
* `sweny workflow run` saves `.sweny/runs/<run-id>/answers.json` beside the
|
|
5
|
+
* output.md: each successful node's DECLARED output fields (the node's
|
|
6
|
+
* `output` schema is the contract; with no schema, only a string `summary`).
|
|
7
|
+
* Local only, mode 0600, redacted with the run journal's redactor before
|
|
8
|
+
* anything is stored. The run record itself (run-history.ts) stays metadata
|
|
9
|
+
* only; answers live in their own file so that contract is untouched.
|
|
10
|
+
*
|
|
11
|
+
* Everything under `.sweny/runs/` is writable by an agent, so it is untrusted:
|
|
12
|
+
* ids are validated against the generator's pattern before any path is built,
|
|
13
|
+
* the embedded id must match the one asked for, and the PUBLIC line (PR
|
|
14
|
+
* comment, step summary) carries only the current workflow's own id and
|
|
15
|
+
* integer counts, never text read from history.
|
|
16
|
+
*
|
|
17
|
+
* `compareAnswers` is the diff behind `sweny runs diff` and the "since last
|
|
18
|
+
* run" line. Identity rules (what counts as "the same finding") are explicit
|
|
19
|
+
* and tested; see {@link itemIdentity}.
|
|
20
|
+
*/
|
|
21
|
+
import type { NodeResult, Workflow } from "../types.js";
|
|
22
|
+
export declare const RUN_ANSWERS_SCHEMA_VERSION = 1;
|
|
23
|
+
/** Strings longer than this are cut (with an ellipsis) before they are stored. */
|
|
24
|
+
export declare const ANSWER_STRING_MAX = 2000;
|
|
25
|
+
export interface RunAnswers {
|
|
26
|
+
schema_version: number;
|
|
27
|
+
run_id: string;
|
|
28
|
+
workflow_id: string;
|
|
29
|
+
/** node id -> declared output fields (redacted). Only nodes that succeeded and produced a field. */
|
|
30
|
+
nodes: Record<string, Record<string, unknown>>;
|
|
31
|
+
/**
|
|
32
|
+
* Strings longer than {@link ANSWER_STRING_MAX} are cut in `nodes` (display only). This mirrors
|
|
33
|
+
* `nodes` and holds the sha256 of each cut string's FULL text, so a change past the cut still
|
|
34
|
+
* compares as changed. Absent when nothing was cut.
|
|
35
|
+
*/
|
|
36
|
+
digests?: Record<string, Record<string, unknown>>;
|
|
37
|
+
}
|
|
38
|
+
export interface BuildRunAnswersInput {
|
|
39
|
+
runId: string;
|
|
40
|
+
workflow: Workflow;
|
|
41
|
+
results: Map<string, NodeResult>;
|
|
42
|
+
/** Secret values to redact (the same set the journal and output.md use). */
|
|
43
|
+
secrets?: string[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Declared fields of each successful node, deep, redacted (values and keys). A failed or skipped
|
|
47
|
+
* node records nothing, so the diff never reads a failure as "everything resolved".
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildRunAnswers(i: BuildRunAnswersInput): RunAnswers;
|
|
50
|
+
/** Relative path: `.sweny/runs/<id>/answers.json`. */
|
|
51
|
+
export declare function answersRelPath(runId: string): string;
|
|
52
|
+
/** Write answers.json (mode 0600; the run dir ignores itself in git). Returns the relative path, or null. Never throws. */
|
|
53
|
+
export declare function writeRunAnswers(answers: RunAnswers, cwd?: string): string | null;
|
|
54
|
+
/**
|
|
55
|
+
* Read a run's answers. Null when the id is not a generated run id (checked before any path is
|
|
56
|
+
* built), the file is missing, oversized, reached through a link, corrupt, an unknown schema
|
|
57
|
+
* version, or carries a different run id than the one asked for.
|
|
58
|
+
*/
|
|
59
|
+
export declare function readRunAnswers(runId: string, cwd?: string): RunAnswers | null;
|
|
60
|
+
/** Fields that name an item, in priority order. */
|
|
61
|
+
export declare const IDENTITY_KEYS: readonly ["id", "key", "fingerprint", "title", "name"];
|
|
62
|
+
/**
|
|
63
|
+
* "Is this the same finding as that one?" The rule, in order:
|
|
64
|
+
* 1. An object item is identified by the first of `id`, `key`, `fingerprint`,
|
|
65
|
+
* `title`, `name` that holds a non-empty string or a number, normalized
|
|
66
|
+
* (trimmed, lowercased, whitespace collapsed).
|
|
67
|
+
* 2. An object with none of those is identified by its whole content (a
|
|
68
|
+
* short hash of its canonical JSON): any edit reads as resolved + added.
|
|
69
|
+
* 3. A plain string, number or boolean item is identified by its normalized value.
|
|
70
|
+
* Two items of one list with the same identity pair up in order.
|
|
71
|
+
*/
|
|
72
|
+
export declare function itemIdentity(item: unknown): string;
|
|
73
|
+
export interface DiffCounts {
|
|
74
|
+
added: number;
|
|
75
|
+
resolved: number;
|
|
76
|
+
changed: number;
|
|
77
|
+
unchanged: number;
|
|
78
|
+
}
|
|
79
|
+
export interface DiffItem {
|
|
80
|
+
/** Normalized identity (see {@link itemIdentity}). */
|
|
81
|
+
id: string;
|
|
82
|
+
/** Human label: the raw identity value, else the first string field. */
|
|
83
|
+
label: string;
|
|
84
|
+
value: unknown;
|
|
85
|
+
}
|
|
86
|
+
export interface FieldChange {
|
|
87
|
+
field: string;
|
|
88
|
+
/** Absent when the field is new on this item. */
|
|
89
|
+
before?: unknown;
|
|
90
|
+
/** Absent when the field was dropped from this item. */
|
|
91
|
+
after?: unknown;
|
|
92
|
+
}
|
|
93
|
+
export interface ChangedItem {
|
|
94
|
+
id: string;
|
|
95
|
+
/** Label from the newer run. */
|
|
96
|
+
label: string;
|
|
97
|
+
fields: FieldChange[];
|
|
98
|
+
}
|
|
99
|
+
export interface CollectionEntry {
|
|
100
|
+
node: string;
|
|
101
|
+
field: string;
|
|
102
|
+
kind: "collection";
|
|
103
|
+
added: DiffItem[];
|
|
104
|
+
resolved: DiffItem[];
|
|
105
|
+
changed: ChangedItem[];
|
|
106
|
+
unchanged: number;
|
|
107
|
+
}
|
|
108
|
+
export interface ValueEntry {
|
|
109
|
+
node: string;
|
|
110
|
+
field: string;
|
|
111
|
+
kind: "value";
|
|
112
|
+
status: "added" | "resolved" | "changed" | "unchanged";
|
|
113
|
+
before?: unknown;
|
|
114
|
+
after?: unknown;
|
|
115
|
+
}
|
|
116
|
+
export type AnswerEntry = CollectionEntry | ValueEntry;
|
|
117
|
+
export interface AnswerDiff {
|
|
118
|
+
available: true;
|
|
119
|
+
counts: DiffCounts;
|
|
120
|
+
entries: AnswerEntry[];
|
|
121
|
+
/** Nodes with answers in only one run: not compared, never counted as added or resolved. */
|
|
122
|
+
not_compared: Array<{
|
|
123
|
+
node: string;
|
|
124
|
+
only_in: "a" | "b";
|
|
125
|
+
}>;
|
|
126
|
+
}
|
|
127
|
+
export type AnswersComparison = AnswerDiff | {
|
|
128
|
+
available: false;
|
|
129
|
+
reason: "different_workflows";
|
|
130
|
+
} | {
|
|
131
|
+
available: false;
|
|
132
|
+
reason: "missing_answers";
|
|
133
|
+
missing: Array<"a" | "b">;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Compare an older run's answers (a) with a newer one's (b), node by node and
|
|
137
|
+
* field by field. A list field (all objects, or all scalars) is diffed item by
|
|
138
|
+
* item using {@link itemIdentity}; any other field is one value. Cut strings
|
|
139
|
+
* compare by the digest of their full text.
|
|
140
|
+
*/
|
|
141
|
+
export declare function compareAnswers(a: RunAnswers | null, b: RunAnswers | null): AnswersComparison;
|
|
142
|
+
export interface SinceLast {
|
|
143
|
+
/** The CURRENT run's own workflow id, from the loaded workflow, never from history. */
|
|
144
|
+
workflow_id: string;
|
|
145
|
+
/** The run this one was compared with. */
|
|
146
|
+
previous_run_id: string;
|
|
147
|
+
counts: DiffCounts;
|
|
148
|
+
/** Nodes with answers in only one of the two runs (failed, skipped or no output): not compared. */
|
|
149
|
+
not_compared: number;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Compare a recorded run with the run of the same workflow just before it. `workflowId` comes from
|
|
153
|
+
* the caller's loaded workflow; history only picks WHICH earlier run, never what is printed.
|
|
154
|
+
* Null when there is no earlier run, either run has no answers, or anything fails.
|
|
155
|
+
*/
|
|
156
|
+
export declare function sinceLastRun(runId: string, workflowId: string, cwd?: string): SinceLast | null;
|
|
157
|
+
/** Save this run's answers, then compare with the previous run. Never throws. */
|
|
158
|
+
export declare function recordRunAnswers(i: BuildRunAnswersInput, cwd?: string): SinceLast | null;
|
|
159
|
+
/** Plain text, one line: for the terminal and CI logs. */
|
|
160
|
+
export declare function formatSinceLast(s: SinceLast): string;
|
|
161
|
+
/** Markdown, one line: for the step summary and the PR comment. */
|
|
162
|
+
export declare function formatSinceLastMarkdown(s: SinceLast): string;
|