patchwork-os 0.2.0-beta.2 → 0.2.0-beta.3
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.bridge.md +5 -5
- package/README.md +156 -12
- package/dist/activityLog.d.ts +6 -0
- package/dist/activityLog.js +8 -0
- package/dist/activityLog.js.map +1 -1
- package/dist/analyticsPrefs.d.ts +35 -2
- package/dist/analyticsPrefs.js +120 -21
- package/dist/analyticsPrefs.js.map +1 -1
- package/dist/analyticsSend.js +5 -1
- package/dist/analyticsSend.js.map +1 -1
- package/dist/bridge.d.ts +2 -0
- package/dist/bridge.js +111 -7
- package/dist/bridge.js.map +1 -1
- package/dist/bridgeLockDiscovery.d.ts +27 -1
- package/dist/bridgeLockDiscovery.js +37 -11
- package/dist/bridgeLockDiscovery.js.map +1 -1
- package/dist/commands/patchworkInit.d.ts +5 -0
- package/dist/commands/patchworkInit.js +86 -7
- package/dist/commands/patchworkInit.js.map +1 -1
- package/dist/commands/recipe.d.ts +51 -0
- package/dist/commands/recipe.js +353 -2
- package/dist/commands/recipe.js.map +1 -1
- package/dist/commands/recipeInstall.js +6 -3
- package/dist/commands/recipeInstall.js.map +1 -1
- package/dist/commands/task.js +2 -2
- package/dist/commands/task.js.map +1 -1
- package/dist/config.d.ts +9 -2
- package/dist/config.js +35 -17
- package/dist/config.js.map +1 -1
- package/dist/connectors/tokenStorage.js +46 -10
- package/dist/connectors/tokenStorage.js.map +1 -1
- package/dist/featureFlags.d.ts +76 -0
- package/dist/featureFlags.js +166 -2
- package/dist/featureFlags.js.map +1 -1
- package/dist/index.js +765 -69
- package/dist/index.js.map +1 -1
- package/dist/lockfile.js +4 -1
- package/dist/lockfile.js.map +1 -1
- package/dist/patchworkConfig.js +5 -0
- package/dist/patchworkConfig.js.map +1 -1
- package/dist/recipeOrchestration.js +35 -1
- package/dist/recipeOrchestration.js.map +1 -1
- package/dist/recipeRoutes.d.ts +36 -0
- package/dist/recipeRoutes.js +231 -32
- package/dist/recipeRoutes.js.map +1 -1
- package/dist/recipes/agentExecutor.d.ts +25 -5
- package/dist/recipes/agentExecutor.js.map +1 -1
- package/dist/recipes/chainedRunner.js +16 -2
- package/dist/recipes/chainedRunner.js.map +1 -1
- package/dist/recipes/connectorPreflight.d.ts +53 -0
- package/dist/recipes/connectorPreflight.js +79 -0
- package/dist/recipes/connectorPreflight.js.map +1 -0
- package/dist/recipes/githubInstallSource.d.ts +62 -0
- package/dist/recipes/githubInstallSource.js +125 -0
- package/dist/recipes/githubInstallSource.js.map +1 -0
- package/dist/recipes/haltCategory.d.ts +80 -0
- package/dist/recipes/haltCategory.js +125 -0
- package/dist/recipes/haltCategory.js.map +1 -0
- package/dist/recipes/idempotencyKey.d.ts +126 -0
- package/dist/recipes/idempotencyKey.js +298 -0
- package/dist/recipes/idempotencyKey.js.map +1 -0
- package/dist/recipes/judgeSummary.d.ts +50 -0
- package/dist/recipes/judgeSummary.js +47 -0
- package/dist/recipes/judgeSummary.js.map +1 -0
- package/dist/recipes/judgeVerdict.d.ts +48 -0
- package/dist/recipes/judgeVerdict.js +174 -0
- package/dist/recipes/judgeVerdict.js.map +1 -0
- package/dist/recipes/migrations/index.d.ts +9 -0
- package/dist/recipes/migrations/index.js +133 -0
- package/dist/recipes/migrations/index.js.map +1 -1
- package/dist/recipes/runBudget.d.ts +70 -0
- package/dist/recipes/runBudget.js +109 -0
- package/dist/recipes/runBudget.js.map +1 -0
- package/dist/recipes/scheduler.js +1 -1
- package/dist/recipes/scheduler.js.map +1 -1
- package/dist/recipes/schema.d.ts +30 -0
- package/dist/recipes/toolRegistry.js +19 -0
- package/dist/recipes/toolRegistry.js.map +1 -1
- package/dist/recipes/tools/http.d.ts +10 -0
- package/dist/recipes/tools/http.js +176 -0
- package/dist/recipes/tools/http.js.map +1 -0
- package/dist/recipes/tools/index.d.ts +1 -0
- package/dist/recipes/tools/index.js +1 -0
- package/dist/recipes/tools/index.js.map +1 -1
- package/dist/recipes/validation.js +1 -1
- package/dist/recipes/validation.js.map +1 -1
- package/dist/recipes/yamlRunner.d.ts +71 -7
- package/dist/recipes/yamlRunner.js +156 -22
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/runLog.d.ts +28 -0
- package/dist/runLog.js +5 -0
- package/dist/runLog.js.map +1 -1
- package/dist/server.d.ts +65 -0
- package/dist/server.js +302 -3
- package/dist/server.js.map +1 -1
- package/dist/streamableHttp.js +17 -6
- package/dist/streamableHttp.js.map +1 -1
- package/dist/tools/bridgeDoctor.js +6 -2
- package/dist/tools/bridgeDoctor.js.map +1 -1
- package/dist/tools/ccRoutines.d.ts +221 -0
- package/dist/tools/ccRoutines.js +264 -0
- package/dist/tools/ccRoutines.js.map +1 -0
- package/dist/tools/getCodeCoverage.js +7 -3
- package/dist/tools/getCodeCoverage.js.map +1 -1
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/recentTracesDigest.js +56 -11
- package/dist/tools/recentTracesDigest.js.map +1 -1
- package/dist/tools/testRunners/vitestJest.js +3 -1
- package/dist/tools/testRunners/vitestJest.js.map +1 -1
- package/dist/tools/utils.js +6 -3
- package/dist/tools/utils.js.map +1 -1
- package/package.json +17 -6
- package/scripts/postinstall.mjs +27 -0
- package/scripts/smoke/run-all.mjs +162 -0
- package/scripts/start-all.mjs +513 -0
- package/scripts/start-all.ps1 +209 -0
- package/scripts/start-all.sh +73 -17
- package/scripts/start-orchestrator.ps1 +158 -0
- package/scripts/start-remote.mjs +122 -0
- package/templates/automation-policies/recipe-authoring.json +1 -1
- package/templates/automation-policies/security-first.json +1 -1
- package/templates/automation-policies/strict-lint.json +1 -1
- package/templates/automation-policies/test-driven.json +1 -1
- package/templates/automation-policy.example.json +1 -1
- package/templates/co.patchwork-os.bridge.plist +1 -1
- package/templates/recipes/approval-queue-ui-test.yaml +1 -1
- package/templates/recipes/ctx-loop-test.yaml +1 -1
- package/templates/recipes/webhook/apple-watch-health-log.yaml +145 -0
- package/dist/commands/marketplace.d.ts +0 -16
- package/dist/commands/marketplace.js +0 -32
- package/dist/commands/marketplace.js.map +0 -1
- package/dist/recipes/legacyRecipeCompat.d.ts +0 -10
- package/dist/recipes/legacyRecipeCompat.js +0 -131
- package/dist/recipes/legacyRecipeCompat.js.map +0 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotency keys for write-tool calls.
|
|
3
|
+
*
|
|
4
|
+
* PR5a of the Val-inspired plan. Foundation for safe retry + safe resume.
|
|
5
|
+
*
|
|
6
|
+
* Two pieces:
|
|
7
|
+
*
|
|
8
|
+
* `deriveIdempotencyKey(toolId, params)`
|
|
9
|
+
* A stable, deterministic hash over `(toolId, canonicalised params)`.
|
|
10
|
+
* Canonicalisation = JSON.stringify with sorted keys, recursive — so
|
|
11
|
+
* `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` hash identically. Returns a
|
|
12
|
+
* hex SHA-256 prefix (first 16 chars; collisions vanishingly small
|
|
13
|
+
* within a single run scope).
|
|
14
|
+
*
|
|
15
|
+
* `WriteEffectLedger`
|
|
16
|
+
* Per-run in-memory map of key → cached output. The runner constructs
|
|
17
|
+
* one per recipe run and threads it through `StepDeps` / `ToolContext`.
|
|
18
|
+
* `toolRegistry.executeTool` checks the ledger before invoking write
|
|
19
|
+
* tools; if the key is present, returns the cached output instead of
|
|
20
|
+
* re-executing — preventing duplicate side effects when two parallel
|
|
21
|
+
* branches of a chained recipe both call the same write tool with the
|
|
22
|
+
* same params.
|
|
23
|
+
*
|
|
24
|
+
* Scope of this PR (deliberately narrow):
|
|
25
|
+
* - In-run dedup only (Map lives for one recipe run, discarded after).
|
|
26
|
+
* - Records only on successful execution; errors don't pollute the
|
|
27
|
+
* ledger, so retry-after-failure still re-executes (correct: if the
|
|
28
|
+
* tool errored, we can't assume the side effect happened).
|
|
29
|
+
* - No cross-run persistence — that's PR5b (disk-backed effect ledger).
|
|
30
|
+
* - No retry-time idempotency on partial-failure cases (Slack posted
|
|
31
|
+
* but HTTP timed out); that needs tool-side support and is a future
|
|
32
|
+
* PR.
|
|
33
|
+
*
|
|
34
|
+
* The protection this DOES provide today: a `parallel:` block (or a
|
|
35
|
+
* recipe that calls a write tool from two different chained steps with
|
|
36
|
+
* identical params) cannot duplicate the side effect. Concretely, this
|
|
37
|
+
* was a footgun that pre-dated PR5a: `chainedRunner.ts` schedules steps
|
|
38
|
+
* with dependency-graph parallelism; if two branches happen to call
|
|
39
|
+
* `slack.postMessage` with the same payload, the message went twice.
|
|
40
|
+
*/
|
|
41
|
+
import { createHash } from "node:crypto";
|
|
42
|
+
import { appendFileSync, lstatSync, mkdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
|
|
43
|
+
import path from "node:path";
|
|
44
|
+
/**
|
|
45
|
+
* Stable canonical-JSON serialiser. Recursively sorts object keys so two
|
|
46
|
+
* params records with the same shape but different key order produce the
|
|
47
|
+
* same string. Plain objects only — falls back to `JSON.stringify` for
|
|
48
|
+
* arrays / primitives / null.
|
|
49
|
+
*/
|
|
50
|
+
function canonicalise(value) {
|
|
51
|
+
if (value === null || typeof value !== "object") {
|
|
52
|
+
return JSON.stringify(value);
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
return `[${value.map(canonicalise).join(",")}]`;
|
|
56
|
+
}
|
|
57
|
+
const entries = Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
58
|
+
const body = entries
|
|
59
|
+
.map(([k, v]) => `${JSON.stringify(k)}:${canonicalise(v)}`)
|
|
60
|
+
.join(",");
|
|
61
|
+
return `{${body}}`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Derive a stable idempotency key for a write-tool invocation. 16 hex
|
|
65
|
+
* chars is 64 bits of entropy — far more than enough for in-run dedup
|
|
66
|
+
* (a single recipe with even 10⁵ steps has ~5×10⁻¹⁰ collision risk).
|
|
67
|
+
*/
|
|
68
|
+
export function deriveIdempotencyKey(toolId, params) {
|
|
69
|
+
const payload = `${toolId}|${canonicalise(params)}`;
|
|
70
|
+
return createHash("sha256").update(payload).digest("hex").slice(0, 16);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Compose a collision-safe scope key from `(recipeName, manualRunId)`.
|
|
74
|
+
*
|
|
75
|
+
* Naive `${recipeName}:${manualRunId}` is ambiguous: recipe `a:b` +
|
|
76
|
+
* attempt `c` and recipe `a` + attempt `b:c` both produce `a:b:c` and
|
|
77
|
+
* would share a ledger scope, letting one attempt read another's
|
|
78
|
+
* cached write-tool outputs. We hash both fields separately as a JSON
|
|
79
|
+
* array so the encoding is unambiguous regardless of either field's
|
|
80
|
+
* contents.
|
|
81
|
+
*
|
|
82
|
+
* Returned as a 32-hex-char SHA-256 prefix — long enough that
|
|
83
|
+
* collisions across a realistic ledger are effectively impossible
|
|
84
|
+
* (~2^128 birthday bound), short enough to scan in a JSONL row.
|
|
85
|
+
*/
|
|
86
|
+
export function deriveScopeKey(recipeName, manualRunId) {
|
|
87
|
+
const payload = JSON.stringify([recipeName, manualRunId]);
|
|
88
|
+
return createHash("sha256").update(payload).digest("hex").slice(0, 32);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* `manualRunId` charset validation — caller-supplied id from the CLI
|
|
92
|
+
* (`--attempt`), HTTP routes, or SDK. Rejects null bytes, control
|
|
93
|
+
* characters, path-traversal slugs (`/`, `\`, `..`), and anything
|
|
94
|
+
* longer than 64 chars. Returns the id verbatim when valid; throws
|
|
95
|
+
* with a descriptive message otherwise.
|
|
96
|
+
*
|
|
97
|
+
* Why strict: this string is hashed into the disk-ledger scope key,
|
|
98
|
+
* appended to `runs.jsonl` rows (capped audit-row size depends on it),
|
|
99
|
+
* and rendered into dashboard pills + CLI output. A 10 MB id would
|
|
100
|
+
* inflate every row past `MAX_PERSIST_BYTES` and erase audit during
|
|
101
|
+
* rotation; control characters break line-delimited persistence.
|
|
102
|
+
*/
|
|
103
|
+
const MANUAL_RUN_ID_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
104
|
+
export function assertValidManualRunId(id) {
|
|
105
|
+
if (typeof id !== "string" || !MANUAL_RUN_ID_PATTERN.test(id)) {
|
|
106
|
+
throw new Error(`manualRunId must match ${MANUAL_RUN_ID_PATTERN} (1-64 chars of [A-Za-z0-9_.-]); got: ${JSON.stringify(id).slice(0, 80)}`);
|
|
107
|
+
}
|
|
108
|
+
return id;
|
|
109
|
+
}
|
|
110
|
+
const LEDGER_FILENAME = "effect_ledger.jsonl";
|
|
111
|
+
const MAX_PERSIST_BYTES = 1024 * 1024; // 1 MB — same posture as runLog
|
|
112
|
+
const MAX_PERSIST_LINES = 10_000;
|
|
113
|
+
/**
|
|
114
|
+
* Validate a caller-supplied ledger directory before any filesystem IO.
|
|
115
|
+
*
|
|
116
|
+
* The dir argument flows from the CLI (`--ledger-dir`) and (if/when
|
|
117
|
+
* exposed) HTTP-runner inputs; we'd rather fail loudly than write JSONL
|
|
118
|
+
* to whatever path is handed in. Three checks:
|
|
119
|
+
*
|
|
120
|
+
* - **No null bytes** — would short-circuit C string handling in older
|
|
121
|
+
* libc paths and confuse logs / audit tooling.
|
|
122
|
+
* - **Absolute** — relative paths resolve against `process.cwd()`,
|
|
123
|
+
* which for recipe runs is the workspace; an `--ledger-dir foo`
|
|
124
|
+
* silently scattering ledger files under recipe sources is the
|
|
125
|
+
* wrong default. Caller can pass `path.resolve(...)` explicitly if
|
|
126
|
+
* they want relative resolution.
|
|
127
|
+
* - **Not a symlink** — if the directory already exists and is a
|
|
128
|
+
* symlink, an attacker who can write to the symlink's owning dir
|
|
129
|
+
* can swap the target and redirect appends. Rejecting up front
|
|
130
|
+
* means a fresh dir is created (via mkdirSync) or an existing real
|
|
131
|
+
* dir is used; symlink-replacement on the JSONL file itself is
|
|
132
|
+
* handled separately on each read in `loadExisting`.
|
|
133
|
+
*/
|
|
134
|
+
function assertSafeLedgerDir(dir) {
|
|
135
|
+
if (typeof dir !== "string" || dir.length === 0) {
|
|
136
|
+
throw new Error("ledgerDir must be a non-empty string");
|
|
137
|
+
}
|
|
138
|
+
if (dir.includes("\0")) {
|
|
139
|
+
throw new Error("ledgerDir must not contain null bytes");
|
|
140
|
+
}
|
|
141
|
+
if (!path.isAbsolute(dir)) {
|
|
142
|
+
throw new Error(`ledgerDir must be an absolute path; got: ${dir}`);
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const st = lstatSync(dir);
|
|
146
|
+
if (st.isSymbolicLink()) {
|
|
147
|
+
throw new Error(`ledgerDir must not be a symlink: ${dir}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
const code = err.code;
|
|
152
|
+
if (code === "ENOENT")
|
|
153
|
+
return; // dir will be created later — fine
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export class WriteEffectLedger {
|
|
158
|
+
cache = new Map();
|
|
159
|
+
disk;
|
|
160
|
+
file;
|
|
161
|
+
constructor(disk) {
|
|
162
|
+
if (disk) {
|
|
163
|
+
// Validate + normalise the directory before any IO. Rejects null
|
|
164
|
+
// bytes (would short-circuit C string handling in libc paths),
|
|
165
|
+
// requires absolute paths (relative paths resolve against cwd
|
|
166
|
+
// which is the recipe workspace — surprising and racy), and
|
|
167
|
+
// refuses symlinks (a symlink swap on the ledger directory after
|
|
168
|
+
// construction could redirect appends to an attacker-chosen
|
|
169
|
+
// path).
|
|
170
|
+
assertSafeLedgerDir(disk.dir);
|
|
171
|
+
}
|
|
172
|
+
this.disk = disk ?? null;
|
|
173
|
+
this.file = disk ? path.join(disk.dir, LEDGER_FILENAME) : null;
|
|
174
|
+
if (this.disk && this.file) {
|
|
175
|
+
try {
|
|
176
|
+
mkdirSync(this.disk.dir, { recursive: true, mode: 0o700 });
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
this.disk.logger?.warn?.(`[effect-ledger] could not create ${this.disk.dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
180
|
+
}
|
|
181
|
+
this.loadExisting();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
has(key) {
|
|
185
|
+
return this.cache.has(key);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Return the previously-cached output for `key`, or `undefined` if not
|
|
189
|
+
* recorded. `null` is a legitimate cached value (= the tool returned
|
|
190
|
+
* `null` originally), so callers must use `has()` to distinguish "not
|
|
191
|
+
* present" from "present and null".
|
|
192
|
+
*/
|
|
193
|
+
get(key) {
|
|
194
|
+
return this.cache.get(key);
|
|
195
|
+
}
|
|
196
|
+
record(key, output) {
|
|
197
|
+
this.cache.set(key, output);
|
|
198
|
+
if (this.disk && this.file) {
|
|
199
|
+
this.append({
|
|
200
|
+
scopeKey: this.disk.scopeKey,
|
|
201
|
+
idemKey: key,
|
|
202
|
+
output,
|
|
203
|
+
recordedAt: Date.now(),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/** Test-only inspection of the current key set. */
|
|
208
|
+
keys() {
|
|
209
|
+
return Array.from(this.cache.keys());
|
|
210
|
+
}
|
|
211
|
+
size() {
|
|
212
|
+
return this.cache.size;
|
|
213
|
+
}
|
|
214
|
+
loadExisting() {
|
|
215
|
+
if (!this.disk || !this.file)
|
|
216
|
+
return;
|
|
217
|
+
let raw;
|
|
218
|
+
try {
|
|
219
|
+
// `lstat` (not `stat`) so we see the symlink, not its target.
|
|
220
|
+
// A swapped symlink at `${dir}/effect_ledger.jsonl` would
|
|
221
|
+
// otherwise let an attacker substitute another file's contents
|
|
222
|
+
// as cached tool outputs.
|
|
223
|
+
const st = lstatSync(this.file);
|
|
224
|
+
if (st.isSymbolicLink()) {
|
|
225
|
+
this.disk.logger?.warn?.(`[effect-ledger] refusing to load ${this.file}: file is a symlink`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
raw = readFileSync(this.file, "utf-8");
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
const code = err.code;
|
|
232
|
+
if (code !== "ENOENT") {
|
|
233
|
+
this.disk.logger?.warn?.(`[effect-ledger] read failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
for (const line of raw.split("\n")) {
|
|
238
|
+
if (!line)
|
|
239
|
+
continue;
|
|
240
|
+
try {
|
|
241
|
+
const row = JSON.parse(line);
|
|
242
|
+
if (typeof row.scopeKey !== "string" ||
|
|
243
|
+
typeof row.idemKey !== "string") {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (row.scopeKey === this.disk.scopeKey) {
|
|
247
|
+
this.cache.set(row.idemKey, row.output ?? null);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
/* skip malformed row */
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
append(row) {
|
|
256
|
+
if (!this.disk || !this.file)
|
|
257
|
+
return;
|
|
258
|
+
try {
|
|
259
|
+
try {
|
|
260
|
+
const st = statSync(this.file);
|
|
261
|
+
if (st.size > MAX_PERSIST_BYTES)
|
|
262
|
+
this.rotate();
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
const code = err.code;
|
|
266
|
+
if (code !== "ENOENT")
|
|
267
|
+
throw err;
|
|
268
|
+
}
|
|
269
|
+
appendFileSync(this.file, `${JSON.stringify(row)}\n`, { mode: 0o600 });
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
this.disk.logger?.warn?.(`[effect-ledger] append failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Trim `effect_ledger.jsonl` to the most recent MAX_PERSIST_LINES.
|
|
277
|
+
* Best-effort — failure logs and the next append proceeds against the
|
|
278
|
+
* un-rotated file. Same pattern as RecipeRunLog / DecisionTraceLog.
|
|
279
|
+
*/
|
|
280
|
+
rotate() {
|
|
281
|
+
if (!this.file || !this.disk)
|
|
282
|
+
return;
|
|
283
|
+
try {
|
|
284
|
+
const raw = readFileSync(this.file, "utf-8");
|
|
285
|
+
let lines = raw.split("\n").filter((l) => l.trim());
|
|
286
|
+
if (lines.length > MAX_PERSIST_LINES) {
|
|
287
|
+
lines = lines.slice(-MAX_PERSIST_LINES);
|
|
288
|
+
}
|
|
289
|
+
writeFileSync(this.file, lines.length > 0 ? `${lines.join("\n")}\n` : "", {
|
|
290
|
+
mode: 0o600,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
this.disk.logger?.warn?.(`[effect-ledger] rotate failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=idempotencyKey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotencyKey.js","sourceRoot":"","sources":["../../src/recipes/idempotencyKey.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Judge-verdict aggregation — PR3b.
|
|
3
|
+
*
|
|
4
|
+
* Parallel to haltCategory.summariseHalts: walks a window of runs and
|
|
5
|
+
* counts judge-step verdicts (`approve` / `request_changes` /
|
|
6
|
+
* `unparseable`). Surfaces through:
|
|
7
|
+
*
|
|
8
|
+
* - `/metrics` as `bridge_recipe_judgments{verdict="..."}` gauge
|
|
9
|
+
* - (later) dashboard panel + session-start digest in PR3c
|
|
10
|
+
*
|
|
11
|
+
* Augment-only invariant (see judgeVerdict.ts): a `request_changes`
|
|
12
|
+
* verdict never appears as a HaltCategory and never causes
|
|
13
|
+
* `status: "error"`. This module is the *separate* channel that makes
|
|
14
|
+
* cold-eyes review visible without re-introducing gate semantics.
|
|
15
|
+
*/
|
|
16
|
+
import type { JudgeVerdict, JudgeVerdictKind } from "./judgeVerdict.js";
|
|
17
|
+
export interface JudgeSummary {
|
|
18
|
+
/** Total step results scanned that carry a `judgeVerdict`. */
|
|
19
|
+
total: number;
|
|
20
|
+
/** Per-verdict counts; verdicts with zero hits are omitted. */
|
|
21
|
+
byVerdict: Partial<Record<JudgeVerdictKind, number>>;
|
|
22
|
+
/** Most recent 5 verdicts (with first reason) for UI surfacing. */
|
|
23
|
+
recent: Array<{
|
|
24
|
+
verdict: JudgeVerdictKind;
|
|
25
|
+
firstReason?: string;
|
|
26
|
+
runSeq: number;
|
|
27
|
+
stepId: string;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
interface JudgeSummaryInputRun {
|
|
31
|
+
seq: number;
|
|
32
|
+
stepResults?: Array<{
|
|
33
|
+
id: string;
|
|
34
|
+
judgeVerdict?: JudgeVerdict;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Aggregate judge verdicts across a set of runs. Runs are expected to
|
|
39
|
+
* be sorted newest-first so `recent` reflects the most recent
|
|
40
|
+
* verdicts.
|
|
41
|
+
*/
|
|
42
|
+
export declare function summariseJudgments(runs: JudgeSummaryInputRun[]): JudgeSummary;
|
|
43
|
+
/**
|
|
44
|
+
* Format a `JudgeSummary` as Prometheus text-exposition lines for the
|
|
45
|
+
* `bridge_recipe_judgments{verdict="..."} N` gauge. Returns an empty
|
|
46
|
+
* array when the summary is empty (no HELP/TYPE block so Prom scrapers
|
|
47
|
+
* don't see an orphan declaration).
|
|
48
|
+
*/
|
|
49
|
+
export declare function judgeSummaryToPrometheus(summary: JudgeSummary): string[];
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregate judge verdicts across a set of runs. Runs are expected to
|
|
3
|
+
* be sorted newest-first so `recent` reflects the most recent
|
|
4
|
+
* verdicts.
|
|
5
|
+
*/
|
|
6
|
+
export function summariseJudgments(runs) {
|
|
7
|
+
const byVerdict = {};
|
|
8
|
+
const recent = [];
|
|
9
|
+
let total = 0;
|
|
10
|
+
for (const run of runs) {
|
|
11
|
+
for (const step of run.stepResults ?? []) {
|
|
12
|
+
const v = step.judgeVerdict;
|
|
13
|
+
if (!v)
|
|
14
|
+
continue;
|
|
15
|
+
total++;
|
|
16
|
+
byVerdict[v.verdict] = (byVerdict[v.verdict] ?? 0) + 1;
|
|
17
|
+
if (recent.length < 5) {
|
|
18
|
+
recent.push({
|
|
19
|
+
verdict: v.verdict,
|
|
20
|
+
...(v.reasons[0] !== undefined && { firstReason: v.reasons[0] }),
|
|
21
|
+
runSeq: run.seq,
|
|
22
|
+
stepId: step.id,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { total, byVerdict, recent };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Format a `JudgeSummary` as Prometheus text-exposition lines for the
|
|
31
|
+
* `bridge_recipe_judgments{verdict="..."} N` gauge. Returns an empty
|
|
32
|
+
* array when the summary is empty (no HELP/TYPE block so Prom scrapers
|
|
33
|
+
* don't see an orphan declaration).
|
|
34
|
+
*/
|
|
35
|
+
export function judgeSummaryToPrometheus(summary) {
|
|
36
|
+
if (summary.total === 0)
|
|
37
|
+
return [];
|
|
38
|
+
const lines = [
|
|
39
|
+
"# HELP bridge_recipe_judgments Recipe judge-step verdicts in the in-memory run-log window, by verdict",
|
|
40
|
+
"# TYPE bridge_recipe_judgments gauge",
|
|
41
|
+
];
|
|
42
|
+
for (const [verdict, count] of Object.entries(summary.byVerdict)) {
|
|
43
|
+
lines.push(`bridge_recipe_judgments{verdict="${verdict}"} ${count}`);
|
|
44
|
+
}
|
|
45
|
+
return lines;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=judgeSummary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"judgeSummary.js","sourceRoot":"","sources":["../../src/recipes/judgeSummary.ts"],"names":[],"mappings":"AAuCA;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAA4B;IAC7D,MAAM,SAAS,GAA8C,EAAE,CAAC;IAChE,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,WAAW,IAAI,EAAE,EAAE,CAAC;YACzC,MAAM,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC;YAC5B,IAAI,CAAC,CAAC;gBAAE,SAAS;YACjB,KAAK,EAAE,CAAC;YACR,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;YACvD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtB,MAAM,CAAC,IAAI,CAAC;oBACV,OAAO,EAAE,CAAC,CAAC,OAAO;oBAClB,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;oBAChE,MAAM,EAAE,GAAG,CAAC,GAAG;oBACf,MAAM,EAAE,IAAI,CAAC,EAAE;iBAChB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;AACtC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAqB;IAC5D,IAAI,OAAO,CAAC,KAAK,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACnC,MAAM,KAAK,GAAa;QACtB,uGAAuG;QACvG,sCAAsC;KACvC,CAAC;IACF,KAAK,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACjE,KAAK,CAAC,IAAI,CAAC,oCAAoC,OAAO,MAAM,KAAK,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Judge verdict — PR3a.
|
|
3
|
+
*
|
|
4
|
+
* Parses a free-form agent response into a structured `JudgeVerdict`.
|
|
5
|
+
* The judge prompt convention asks the model to end its response with
|
|
6
|
+
* a JSON object of the form:
|
|
7
|
+
*
|
|
8
|
+
* {"verdict": "approve" | "request_changes",
|
|
9
|
+
* "reasons": ["..."],
|
|
10
|
+
* "fixList": ["..."]}
|
|
11
|
+
*
|
|
12
|
+
* The parser walks back from the end of the string, finds the last
|
|
13
|
+
* JSON object, and validates its shape. On any failure we record the
|
|
14
|
+
* verdict as `unparseable` and keep the raw text — the runner *never*
|
|
15
|
+
* throws on a malformed judge response.
|
|
16
|
+
*
|
|
17
|
+
* **Augment-only invariant** — see the file-level comment in
|
|
18
|
+
* yamlRunner.ts. The verdict shape is intentionally separate from
|
|
19
|
+
* `StepResult.status`: a `request_changes` verdict produces
|
|
20
|
+
* `status: "ok"` with a stashed verdict, never `status: "error"`.
|
|
21
|
+
* That separation is what prevents the judge step from quietly
|
|
22
|
+
* becoming a gate.
|
|
23
|
+
*/
|
|
24
|
+
export type JudgeVerdictKind = "approve" | "request_changes" | "unparseable";
|
|
25
|
+
export interface JudgeVerdict {
|
|
26
|
+
verdict: JudgeVerdictKind;
|
|
27
|
+
/** Short bullet points; empty when unparseable. */
|
|
28
|
+
reasons: string[];
|
|
29
|
+
/** Optional fix-list when `verdict: "request_changes"`. */
|
|
30
|
+
fixList?: string[];
|
|
31
|
+
/** Original model text when parsing failed (or for audit). */
|
|
32
|
+
raw?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Append to the judge prompt to elicit the structured tail. Kept short
|
|
36
|
+
* so it doesn't crowd out the user-provided prompt body.
|
|
37
|
+
*/
|
|
38
|
+
export declare const JUDGE_PROMPT_SUFFIX = "\n\nYou are a cold-eyes reviewer. Respond with a brief assessment, then end\nwith a single JSON object on its own line:\n\n{\"verdict\": \"approve\" | \"request_changes\", \"reasons\": [\"...\"], \"fixList\": [\"...\"]}\n\nThe \"fixList\" is optional and only relevant when requesting changes.\nOutput only the JSON object as the final line.";
|
|
39
|
+
/**
|
|
40
|
+
* Build the artefact-injection block for a judge step that has a
|
|
41
|
+
* `reviews: <stepId>` reference. Returns an empty string when no
|
|
42
|
+
* artefact is available; the judge then sees the prompt as-is.
|
|
43
|
+
*/
|
|
44
|
+
export declare function buildJudgeArtefactBlock(artefact: unknown): string;
|
|
45
|
+
/**
|
|
46
|
+
* Parse an agent response into a `JudgeVerdict`. Never throws.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseJudgeVerdict(text: string): JudgeVerdict;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Judge verdict — PR3a.
|
|
3
|
+
*
|
|
4
|
+
* Parses a free-form agent response into a structured `JudgeVerdict`.
|
|
5
|
+
* The judge prompt convention asks the model to end its response with
|
|
6
|
+
* a JSON object of the form:
|
|
7
|
+
*
|
|
8
|
+
* {"verdict": "approve" | "request_changes",
|
|
9
|
+
* "reasons": ["..."],
|
|
10
|
+
* "fixList": ["..."]}
|
|
11
|
+
*
|
|
12
|
+
* The parser walks back from the end of the string, finds the last
|
|
13
|
+
* JSON object, and validates its shape. On any failure we record the
|
|
14
|
+
* verdict as `unparseable` and keep the raw text — the runner *never*
|
|
15
|
+
* throws on a malformed judge response.
|
|
16
|
+
*
|
|
17
|
+
* **Augment-only invariant** — see the file-level comment in
|
|
18
|
+
* yamlRunner.ts. The verdict shape is intentionally separate from
|
|
19
|
+
* `StepResult.status`: a `request_changes` verdict produces
|
|
20
|
+
* `status: "ok"` with a stashed verdict, never `status: "error"`.
|
|
21
|
+
* That separation is what prevents the judge step from quietly
|
|
22
|
+
* becoming a gate.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Append to the judge prompt to elicit the structured tail. Kept short
|
|
26
|
+
* so it doesn't crowd out the user-provided prompt body.
|
|
27
|
+
*/
|
|
28
|
+
export const JUDGE_PROMPT_SUFFIX = `
|
|
29
|
+
|
|
30
|
+
You are a cold-eyes reviewer. Respond with a brief assessment, then end
|
|
31
|
+
with a single JSON object on its own line:
|
|
32
|
+
|
|
33
|
+
{"verdict": "approve" | "request_changes", "reasons": ["..."], "fixList": ["..."]}
|
|
34
|
+
|
|
35
|
+
The "fixList" is optional and only relevant when requesting changes.
|
|
36
|
+
Output only the JSON object as the final line.`;
|
|
37
|
+
/**
|
|
38
|
+
* Build the artefact-injection block for a judge step that has a
|
|
39
|
+
* `reviews: <stepId>` reference. Returns an empty string when no
|
|
40
|
+
* artefact is available; the judge then sees the prompt as-is.
|
|
41
|
+
*/
|
|
42
|
+
export function buildJudgeArtefactBlock(artefact) {
|
|
43
|
+
if (artefact === undefined || artefact === null)
|
|
44
|
+
return "";
|
|
45
|
+
let body;
|
|
46
|
+
if (typeof artefact === "string") {
|
|
47
|
+
body = artefact;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
try {
|
|
51
|
+
body = JSON.stringify(artefact, null, 2);
|
|
52
|
+
// `JSON.stringify` returns `undefined` for functions / symbols /
|
|
53
|
+
// top-level BigInt — the artefact block becomes
|
|
54
|
+
// `<artefact>\nundefined\n</artefact>` which is misleading. Fall
|
|
55
|
+
// back to a marker so downstream readers can spot the gap.
|
|
56
|
+
if (body === undefined)
|
|
57
|
+
body = "[unserialisable artefact]";
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Circular references, BigInt inside the object graph, or any
|
|
61
|
+
// toJSON throwing. The judge step must never propagate this out
|
|
62
|
+
// of the prompt builder — augment-only invariant.
|
|
63
|
+
body = "[unserialisable artefact]";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return `\n\n<artefact>\n${body}\n</artefact>`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Walk `text` forward and emit `[start, endInclusive]` ranges for every
|
|
70
|
+
* balanced top-level `{...}` block, respecting JSON string syntax so a
|
|
71
|
+
* `}` inside a string doesn't offset the brace depth.
|
|
72
|
+
*
|
|
73
|
+
* The original implementation walked back from `lastIndexOf("}")` and
|
|
74
|
+
* counted braces literally. A judge response of the shape
|
|
75
|
+
* `Consider this snippet: { x: "} oops" }` would be miscounted — the
|
|
76
|
+
* `}` inside the string would close depth too early and the candidate
|
|
77
|
+
* slice would JSON.parse-fail, returning `unparseable` for an
|
|
78
|
+
* otherwise-legitimate verdict trailer.
|
|
79
|
+
*/
|
|
80
|
+
function findBalancedObjectRanges(text) {
|
|
81
|
+
const ranges = [];
|
|
82
|
+
let depth = 0;
|
|
83
|
+
let start = -1;
|
|
84
|
+
let inString = false;
|
|
85
|
+
let escaped = false;
|
|
86
|
+
for (let i = 0; i < text.length; i++) {
|
|
87
|
+
const ch = text[i];
|
|
88
|
+
if (inString) {
|
|
89
|
+
if (escaped) {
|
|
90
|
+
escaped = false;
|
|
91
|
+
}
|
|
92
|
+
else if (ch === "\\") {
|
|
93
|
+
escaped = true;
|
|
94
|
+
}
|
|
95
|
+
else if (ch === '"') {
|
|
96
|
+
inString = false;
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (ch === '"') {
|
|
101
|
+
inString = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (ch === "{") {
|
|
105
|
+
if (depth === 0)
|
|
106
|
+
start = i;
|
|
107
|
+
depth++;
|
|
108
|
+
}
|
|
109
|
+
else if (ch === "}") {
|
|
110
|
+
depth--;
|
|
111
|
+
if (depth === 0 && start !== -1) {
|
|
112
|
+
ranges.push([start, i]);
|
|
113
|
+
start = -1;
|
|
114
|
+
}
|
|
115
|
+
if (depth < 0) {
|
|
116
|
+
// Stray closing brace — reset so we don't underflow.
|
|
117
|
+
depth = 0;
|
|
118
|
+
start = -1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return ranges;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Parse an agent response into a `JudgeVerdict`. Never throws.
|
|
126
|
+
*/
|
|
127
|
+
export function parseJudgeVerdict(text) {
|
|
128
|
+
const trimmed = text.trim();
|
|
129
|
+
if (trimmed.length === 0) {
|
|
130
|
+
return {
|
|
131
|
+
verdict: "unparseable",
|
|
132
|
+
reasons: [],
|
|
133
|
+
raw: text,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
// Collect every balanced `{...}` range, then try them last-to-first
|
|
137
|
+
// so the JSON tail wins over an in-prose snippet earlier in the
|
|
138
|
+
// response.
|
|
139
|
+
const ranges = findBalancedObjectRanges(trimmed);
|
|
140
|
+
for (let i = ranges.length - 1; i >= 0; i--) {
|
|
141
|
+
const range = ranges[i];
|
|
142
|
+
if (!range)
|
|
143
|
+
continue;
|
|
144
|
+
const [s, e] = range;
|
|
145
|
+
const candidate = trimmed.slice(s, e + 1);
|
|
146
|
+
let parsed;
|
|
147
|
+
try {
|
|
148
|
+
parsed = JSON.parse(candidate);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (!parsed || typeof parsed !== "object")
|
|
154
|
+
continue;
|
|
155
|
+
const obj = parsed;
|
|
156
|
+
const verdictRaw = obj.verdict;
|
|
157
|
+
if (verdictRaw !== "approve" && verdictRaw !== "request_changes") {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const reasons = Array.isArray(obj.reasons)
|
|
161
|
+
? obj.reasons.filter((r) => typeof r === "string")
|
|
162
|
+
: [];
|
|
163
|
+
const fixList = Array.isArray(obj.fixList)
|
|
164
|
+
? obj.fixList.filter((r) => typeof r === "string")
|
|
165
|
+
: undefined;
|
|
166
|
+
return {
|
|
167
|
+
verdict: verdictRaw,
|
|
168
|
+
reasons,
|
|
169
|
+
...(fixList && fixList.length > 0 && { fixList }),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return { verdict: "unparseable", reasons: [], raw: text };
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=judgeVerdict.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"judgeVerdict.js","sourceRoot":"","sources":["../../src/recipes/judgeVerdict.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { WarnFn } from "./types.js";
|
|
2
2
|
export type { RecipeMigration, WarnFn } from "./types.js";
|
|
3
3
|
export { v1Migration } from "./v1.js";
|
|
4
|
+
/**
|
|
5
|
+
* Default deprecation-warning sink for the runtime/validation/fmt callers.
|
|
6
|
+
* Forwards to `console.warn` outside of tests so users see migration
|
|
7
|
+
* prompts in CLI output, but stays silent under vitest so the dozens of
|
|
8
|
+
* intentional legacy-shape regression fixtures don't flood stderr. Tests
|
|
9
|
+
* that need to assert warnings still pass their own `vi.fn()` directly.
|
|
10
|
+
*/
|
|
11
|
+
export declare const defaultDeprecationWarn: WarnFn;
|
|
4
12
|
/** apiVersion produced by the most recent migration. */
|
|
5
13
|
export declare const CURRENT_API_VERSION = "patchwork.sh/v1";
|
|
6
14
|
export interface MigrationResult {
|
|
@@ -22,3 +30,4 @@ export interface MigrationResult {
|
|
|
22
30
|
* through unchanged so downstream schema lint can flag it).
|
|
23
31
|
*/
|
|
24
32
|
export declare function migrateRecipeToCurrent(recipe: unknown, warn?: WarnFn): MigrationResult;
|
|
33
|
+
export declare function normalizeRecipeForRuntime(recipe: unknown, warn?: WarnFn): unknown;
|