agent-procedures 0.3.0 → 0.3.1
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 +15 -2
- package/dist/runtime.js +488 -115
- package/lib/engine.js +54 -45
- package/lib/groomer.js +108 -72
- package/lib/harnesses/claude.js +3 -1
- package/lib/init.js +30 -22
- package/lib/main.js +3 -1
- package/lib/recall.js +108 -0
- package/lib/sanitize.js +181 -0
- package/lib/store.js +178 -5
- package/package.json +1 -1
package/dist/runtime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by agent-procedures@0.3.
|
|
1
|
+
// Generated by agent-procedures@0.3.1. Do not edit.
|
|
2
2
|
// To upgrade: npx agent-procedures init
|
|
3
3
|
|
|
4
4
|
// lib/main.js
|
|
@@ -9,16 +9,59 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
// lib/store.js
|
|
10
10
|
import fs from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
|
+
var GROOM_LOCK_STALE_MS = 15 * 60 * 1e3;
|
|
12
13
|
function paths(reflexDir) {
|
|
13
14
|
return {
|
|
14
15
|
reflexDir,
|
|
15
16
|
tracesDir: path.join(reflexDir, "traces"),
|
|
16
|
-
proceduresFile: path.join(reflexDir, "procedures.jsonl")
|
|
17
|
+
proceduresFile: path.join(reflexDir, "procedures.jsonl"),
|
|
18
|
+
pendingFile: path.join(reflexDir, "pending.jsonl"),
|
|
19
|
+
processingFile: path.join(reflexDir, "processing.jsonl"),
|
|
20
|
+
groomerLockFile: path.join(reflexDir, "groomer.lock")
|
|
17
21
|
};
|
|
18
22
|
}
|
|
19
23
|
function readJsonl(file) {
|
|
20
24
|
if (!fs.existsSync(file)) return [];
|
|
21
|
-
return fs.readFileSync(file, "utf8").split("\n").filter(Boolean).map((l) =>
|
|
25
|
+
return fs.readFileSync(file, "utf8").split("\n").filter(Boolean).map((l) => {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(l);
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}).filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
function sleepSync(ms) {
|
|
34
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
35
|
+
}
|
|
36
|
+
function renameWithRetry(from, to, attempts = 8) {
|
|
37
|
+
for (let i = 0; i < attempts; i++) {
|
|
38
|
+
try {
|
|
39
|
+
fs.renameSync(from, to);
|
|
40
|
+
return;
|
|
41
|
+
} catch (e) {
|
|
42
|
+
const retryable = e.code === "EBUSY" || e.code === "EPERM" || e.code === "EACCES";
|
|
43
|
+
if (!retryable || i === attempts - 1) throw e;
|
|
44
|
+
sleepSync(15 * (i + 1));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function unlinkWithRetry(file, attempts = 8) {
|
|
49
|
+
for (let i = 0; i < attempts; i++) {
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
52
|
+
return;
|
|
53
|
+
} catch (e) {
|
|
54
|
+
const retryable = e.code === "EBUSY" || e.code === "EPERM" || e.code === "EACCES";
|
|
55
|
+
if (!retryable || i === attempts - 1) throw e;
|
|
56
|
+
sleepSync(15 * (i + 1));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function writeJsonlAtomic(file, nodes) {
|
|
61
|
+
const content = nodes.length ? nodes.map((n) => JSON.stringify(n)).join("\n") + "\n" : "";
|
|
62
|
+
const tmp = file + ".tmp";
|
|
63
|
+
fs.writeFileSync(tmp, content, "utf8");
|
|
64
|
+
renameWithRetry(tmp, file);
|
|
22
65
|
}
|
|
23
66
|
function traceFile(reflexDir, sessionId, promptId) {
|
|
24
67
|
return path.join(paths(reflexDir).tracesDir, sessionId, `${promptId}.jsonl`);
|
|
@@ -39,17 +82,324 @@ function readTrace(reflexDir, sessionId, promptId) {
|
|
|
39
82
|
function deleteTrace(reflexDir, sessionId, promptId) {
|
|
40
83
|
try {
|
|
41
84
|
fs.unlinkSync(traceFile(reflexDir, sessionId, promptId));
|
|
42
|
-
} catch
|
|
85
|
+
} catch {
|
|
43
86
|
}
|
|
44
87
|
}
|
|
45
88
|
function readProcedures(reflexDir) {
|
|
46
89
|
return readJsonl(paths(reflexDir).proceduresFile);
|
|
47
90
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
91
|
+
function readPending(reflexDir) {
|
|
92
|
+
return readJsonl(paths(reflexDir).pendingFile);
|
|
93
|
+
}
|
|
94
|
+
function readProcessing(reflexDir) {
|
|
95
|
+
return readJsonl(paths(reflexDir).processingFile);
|
|
96
|
+
}
|
|
97
|
+
function readAllProcedures(reflexDir) {
|
|
98
|
+
return [
|
|
99
|
+
...readProcedures(reflexDir),
|
|
100
|
+
...readPending(reflexDir),
|
|
101
|
+
...readProcessing(reflexDir)
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
function countIngest(reflexDir) {
|
|
105
|
+
return readPending(reflexDir).length + readProcessing(reflexDir).length;
|
|
106
|
+
}
|
|
107
|
+
function appendPending(reflexDir, node) {
|
|
108
|
+
const file = paths(reflexDir).pendingFile;
|
|
109
|
+
fs.mkdirSync(reflexDir, { recursive: true });
|
|
51
110
|
fs.appendFileSync(file, JSON.stringify(node) + "\n", "utf8");
|
|
52
111
|
}
|
|
112
|
+
function migrateLegacyPending(reflexDir) {
|
|
113
|
+
const p = paths(reflexDir);
|
|
114
|
+
if (!fs.existsSync(p.proceduresFile)) return { moved: 0 };
|
|
115
|
+
const nodes = readJsonl(p.proceduresFile);
|
|
116
|
+
const pending = nodes.filter((n) => n.status === "pending_review");
|
|
117
|
+
if (pending.length === 0) return { moved: 0 };
|
|
118
|
+
const stable = nodes.filter((n) => n.status !== "pending_review");
|
|
119
|
+
for (const node of pending) appendPending(reflexDir, node);
|
|
120
|
+
writeJsonlAtomic(p.proceduresFile, stable);
|
|
121
|
+
return { moved: pending.length };
|
|
122
|
+
}
|
|
123
|
+
function pidAlive(pid) {
|
|
124
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
125
|
+
try {
|
|
126
|
+
process.kill(pid, 0);
|
|
127
|
+
return true;
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function readLock(lockFile) {
|
|
133
|
+
try {
|
|
134
|
+
return JSON.parse(fs.readFileSync(lockFile, "utf8"));
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function isGroomerLocked(reflexDir, now = Date.now()) {
|
|
140
|
+
const lockFile = paths(reflexDir).groomerLockFile;
|
|
141
|
+
if (!fs.existsSync(lockFile)) return false;
|
|
142
|
+
const lock = readLock(lockFile);
|
|
143
|
+
if (!lock) return false;
|
|
144
|
+
const age = typeof lock.startedAt === "number" ? now - lock.startedAt : Infinity;
|
|
145
|
+
if (age >= GROOM_LOCK_STALE_MS) return false;
|
|
146
|
+
return pidAlive(lock.pid);
|
|
147
|
+
}
|
|
148
|
+
function tryAcquireGroomerLock(reflexDir, now = Date.now()) {
|
|
149
|
+
const lockFile = paths(reflexDir).groomerLockFile;
|
|
150
|
+
fs.mkdirSync(reflexDir, { recursive: true });
|
|
151
|
+
if (fs.existsSync(lockFile)) {
|
|
152
|
+
if (isGroomerLocked(reflexDir, now)) return false;
|
|
153
|
+
try {
|
|
154
|
+
fs.unlinkSync(lockFile);
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const fd = fs.openSync(lockFile, "wx");
|
|
161
|
+
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, startedAt: now }) + "\n", "utf8");
|
|
162
|
+
fs.closeSync(fd);
|
|
163
|
+
return true;
|
|
164
|
+
} catch (e) {
|
|
165
|
+
if (e.code === "EEXIST") return false;
|
|
166
|
+
throw e;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function releaseGroomerLock(reflexDir) {
|
|
170
|
+
const lockFile = paths(reflexDir).groomerLockFile;
|
|
171
|
+
try {
|
|
172
|
+
const lock = readLock(lockFile);
|
|
173
|
+
if (lock && lock.pid !== process.pid) return;
|
|
174
|
+
fs.unlinkSync(lockFile);
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function claimPendingBatch(reflexDir) {
|
|
179
|
+
const { pendingFile, processingFile } = paths(reflexDir);
|
|
180
|
+
if (fs.existsSync(processingFile)) return processingFile;
|
|
181
|
+
if (!fs.existsSync(pendingFile)) return null;
|
|
182
|
+
const stat = fs.statSync(pendingFile);
|
|
183
|
+
if (stat.size === 0) {
|
|
184
|
+
unlinkWithRetry(pendingFile);
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
renameWithRetry(pendingFile, processingFile);
|
|
188
|
+
return processingFile;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// lib/sanitize.js
|
|
192
|
+
var PROVIDER_RULES = [
|
|
193
|
+
["anthropic-api-key", /\b(sk-ant-api03-[a-zA-Z0-9_-]{93}AA)(?:[\x60'"\s;]|\\[nr]|$)/],
|
|
194
|
+
["aws-access-token", /\b((?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16})\b/],
|
|
195
|
+
["cloudflare-api-key", /[\w.-]{0,50}?(?:cloudflare)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{40})(?:[\x60'"\s;]|\\[nr]|$)/i],
|
|
196
|
+
["datadog-access-token", /[\w.-]{0,50}?(?:datadog)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)/i],
|
|
197
|
+
["digitalocean-access-token", /\b(doo_v1_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)/],
|
|
198
|
+
["discord-api-token", /[\w.-]{0,50}?(?:discord)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)/i],
|
|
199
|
+
["dropbox-short-lived-api-token", /[\w.-]{0,50}?(?:dropbox)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(sl\.[a-z0-9\-=_]{135})(?:[\x60'"\s;]|\\[nr]|$)/i],
|
|
200
|
+
["gcp-api-key", /\b(AIza[\w-]{35})(?:[\x60'"\s;]|\\[nr]|$)/],
|
|
201
|
+
["github-pat", /ghp_[0-9a-zA-Z]{36}/],
|
|
202
|
+
["gitlab-pat", /glpat-[\w-]{20}/],
|
|
203
|
+
["heroku-api-key-v2", /\b((HRKU-AA[0-9a-zA-Z_-]{58}))(?:[\x60'"\s;]|\\[nr]|$)/],
|
|
204
|
+
["mailgun-private-api-token", /[\w.-]{0,50}?(?:mailgun)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(key-[a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)/i],
|
|
205
|
+
["npm-access-token", /\b(npm_[a-z0-9]{36})(?:[\x60'"\s;]|\\[nr]|$)/i],
|
|
206
|
+
["openai-api-key", /\b(sk-(?:proj|svcacct|admin)-(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})T3BlbkFJ(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})\b|sk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)/],
|
|
207
|
+
["pypi-upload-token", /pypi-AgEIcHlwaS5vcmc[\w-]{50,1000}/],
|
|
208
|
+
["sendgrid-api-token", /\b(SG\.[a-z0-9=_\-.]{66})(?:[\x60'"\s;]|\\[nr]|$)/i],
|
|
209
|
+
["shopify-access-token", /shpat_[a-fA-F0-9]{32}/],
|
|
210
|
+
["slack-bot-token", /xoxb-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*/],
|
|
211
|
+
["stripe-access-token", /\b((?:sk|rk)_(?:test|live|prod)_[a-zA-Z0-9]{10,99})(?:[\x60'"\s;]|\\[nr]|$)/],
|
|
212
|
+
["twilio-api-key", /SK[0-9a-fA-F]{32}/],
|
|
213
|
+
["private-key", /-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----[\s\S-]{64,}?KEY(?: BLOCK)?-----/i],
|
|
214
|
+
["jwt", /\b(ey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9/\\_-]{17,}\.(?:[a-zA-Z0-9/\\_-]{10,}={0,2})?)(?:[\x60'"\s;]|\\[nr]|$)/]
|
|
215
|
+
];
|
|
216
|
+
var SENSITIVE_NAME = /(?:^|_)(?:API_?)?(?:KEY|TOKEN|SECRET|PASS|PASSWORD|AUTH|CREDENTIALS?|DATABASE_URL|DB_URL|CONNECTION_STRING|DSN)(?:_|$)/i;
|
|
217
|
+
var ASSIGNMENT = /\b(export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(?:"([^"]*)"|'([^']*)'|([^\s;&|"']+))(\s*)/g;
|
|
218
|
+
var CREDENTIAL_URL = /^[a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:[^@\s/]+@/i;
|
|
219
|
+
function result(text, requiresEnv = [], unsafe = false) {
|
|
220
|
+
return { text, requiresEnv: [...new Set(requiresEnv)], unsafe };
|
|
221
|
+
}
|
|
222
|
+
function replaceDetectedSecrets(input) {
|
|
223
|
+
let text = input;
|
|
224
|
+
let unsafe = false;
|
|
225
|
+
for (const [id, regex] of PROVIDER_RULES) {
|
|
226
|
+
let match;
|
|
227
|
+
while (match = regex.exec(text)) {
|
|
228
|
+
const secret = match[1] || match[0];
|
|
229
|
+
const offset = match[0].indexOf(secret);
|
|
230
|
+
const start = match.index + offset;
|
|
231
|
+
text = text.slice(0, start) + `[secret:${id}]` + text.slice(start + secret.length);
|
|
232
|
+
unsafe = true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { text, unsafe };
|
|
236
|
+
}
|
|
237
|
+
function sanitizeText(input = "") {
|
|
238
|
+
const detected = replaceDetectedSecrets(String(input));
|
|
239
|
+
return result(detected.text, [], detected.unsafe);
|
|
240
|
+
}
|
|
241
|
+
function sanitizeCommand(input = "") {
|
|
242
|
+
let text = String(input);
|
|
243
|
+
const requiresEnv = [];
|
|
244
|
+
let removedAssignment = false;
|
|
245
|
+
text = text.replace(ASSIGNMENT, (match, exported, name, double, single, bare, trailing, offset, source) => {
|
|
246
|
+
const value = double ?? single ?? bare ?? "";
|
|
247
|
+
const valueIsSecret = SENSITIVE_NAME.test(name) || CREDENTIAL_URL.test(value) || PROVIDER_RULES.some(([, regex]) => regex.test(value));
|
|
248
|
+
if (!valueIsSecret) return match;
|
|
249
|
+
requiresEnv.push(name);
|
|
250
|
+
const before = source.slice(0, offset);
|
|
251
|
+
const isCommandAssignment = exported || !before.trim() || /(?:&&|\|\||;)\s*$/.test(before);
|
|
252
|
+
if (isCommandAssignment) {
|
|
253
|
+
removedAssignment = true;
|
|
254
|
+
return "";
|
|
255
|
+
}
|
|
256
|
+
const placeholder = bare !== void 0 ? `\${${name}}` : `"\${${name}}"`;
|
|
257
|
+
return `${name}=${placeholder}${trailing}`;
|
|
258
|
+
});
|
|
259
|
+
text = text.replace(
|
|
260
|
+
/'(\bAuthorization\s*:\s*Bearer\s+)([^']+)'/gi,
|
|
261
|
+
(_match, prefix) => {
|
|
262
|
+
requiresEnv.push("AUTH_TOKEN");
|
|
263
|
+
return `"${prefix}\${AUTH_TOKEN}"`;
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
text = text.replace(
|
|
267
|
+
/'(\bAuthorization\s*:\s*Basic\s+)([^']+)'/gi,
|
|
268
|
+
(_match, prefix) => {
|
|
269
|
+
requiresEnv.push("BASIC_AUTH");
|
|
270
|
+
return `"${prefix}\${BASIC_AUTH}"`;
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
text = text.replace(
|
|
274
|
+
/'([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)([^@']+)(@[^']+)'/gi,
|
|
275
|
+
(_match, prefix, _password, suffix) => {
|
|
276
|
+
requiresEnv.push("PASSWORD");
|
|
277
|
+
return `"${prefix}\${PASSWORD}${suffix}"`;
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
text = text.replace(
|
|
281
|
+
/(\bAuthorization\s*:\s*Bearer\s+)([^"'\s]+)/gi,
|
|
282
|
+
(_match, prefix) => {
|
|
283
|
+
requiresEnv.push("AUTH_TOKEN");
|
|
284
|
+
return `${prefix}\${AUTH_TOKEN}`;
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
text = text.replace(
|
|
288
|
+
/(\bAuthorization\s*:\s*Basic\s+)([^"'\s]+)/gi,
|
|
289
|
+
(_match, prefix) => {
|
|
290
|
+
requiresEnv.push("BASIC_AUTH");
|
|
291
|
+
return `${prefix}\${BASIC_AUTH}`;
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
text = text.replace(
|
|
295
|
+
/([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)([^@\s/]+)(@)/gi,
|
|
296
|
+
(_match, prefix, _password, suffix) => {
|
|
297
|
+
requiresEnv.push("PASSWORD");
|
|
298
|
+
return `${prefix}\${PASSWORD}${suffix}`;
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
text = text.replace(
|
|
302
|
+
/(--(?:password|passwd|token|secret|api-key|key)(?:=|\s+))(["']?)([^"'\s;&|]+)\2/gi,
|
|
303
|
+
(_match, prefix, quote) => {
|
|
304
|
+
const name = prefix.match(/^--([a-z-]+)/i)[1].replace(/-/g, "_").toUpperCase();
|
|
305
|
+
requiresEnv.push(name);
|
|
306
|
+
const safeQuote = quote === "'" ? '"' : quote;
|
|
307
|
+
return `${prefix}${safeQuote}\${${name}}${safeQuote}`;
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
if (removedAssignment) {
|
|
311
|
+
text = text.replace(/^\s*(?:&&|\|\||;)\s*/, "").replace(/\s*(?:&&|\|\||;)\s*$/, "").replace(/(?:&&|\|\||;)\s*(?=(?:&&|\|\||;))/g, "").trim();
|
|
312
|
+
}
|
|
313
|
+
const detected = replaceDetectedSecrets(text);
|
|
314
|
+
return result(detected.text, requiresEnv, detected.unsafe);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// lib/recall.js
|
|
318
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
319
|
+
"the",
|
|
320
|
+
"a",
|
|
321
|
+
"an",
|
|
322
|
+
"to",
|
|
323
|
+
"in",
|
|
324
|
+
"on",
|
|
325
|
+
"please",
|
|
326
|
+
"can",
|
|
327
|
+
"you",
|
|
328
|
+
"my",
|
|
329
|
+
"this",
|
|
330
|
+
"for",
|
|
331
|
+
"with"
|
|
332
|
+
]);
|
|
333
|
+
function tokenize(text) {
|
|
334
|
+
if (!text) return [];
|
|
335
|
+
return text.toLowerCase().replace(/[^\w\s]+/g, " ").split(/\s+/).filter((w) => w.length > 1 && !STOP_WORDS.has(w));
|
|
336
|
+
}
|
|
337
|
+
function scoreTarget(promptTokens, targetString) {
|
|
338
|
+
const targetTokens = tokenize(targetString);
|
|
339
|
+
if (targetTokens.length === 0) return { coverage: 0, matchCount: 0, targetLen: 0 };
|
|
340
|
+
let matchCount = 0;
|
|
341
|
+
let hasHighValueWord = false;
|
|
342
|
+
for (const tWord of targetTokens) {
|
|
343
|
+
const isMatch = promptTokens.some(
|
|
344
|
+
(pWord) => pWord === tWord || tWord.length >= 3 && pWord.startsWith(tWord)
|
|
345
|
+
);
|
|
346
|
+
if (isMatch) {
|
|
347
|
+
matchCount++;
|
|
348
|
+
if (tWord.length >= 6) hasHighValueWord = true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const coverage = matchCount / targetTokens.length;
|
|
352
|
+
const isReliable = coverage >= 0.75 && (matchCount >= 2 || hasHighValueWord);
|
|
353
|
+
if (!isReliable) return { coverage: 0, matchCount: 0, targetLen: 0 };
|
|
354
|
+
return { coverage, matchCount, targetLen: targetTokens.length };
|
|
355
|
+
}
|
|
356
|
+
function isBetter(a, b) {
|
|
357
|
+
if (a.coverage !== b.coverage) return a.coverage > b.coverage;
|
|
358
|
+
if (a.matchCount !== b.matchCount) return a.matchCount > b.matchCount;
|
|
359
|
+
return a.targetLen > b.targetLen;
|
|
360
|
+
}
|
|
361
|
+
function scoreProcedure(promptTokens, node) {
|
|
362
|
+
let best = scoreTarget(promptTokens, node.trigger || "");
|
|
363
|
+
if (Array.isArray(node.aliases)) {
|
|
364
|
+
for (const alias of node.aliases) {
|
|
365
|
+
const next = scoreTarget(promptTokens, alias);
|
|
366
|
+
if (isBetter(next, best)) best = next;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return best;
|
|
370
|
+
}
|
|
371
|
+
function formatSteps(steps, requiresEnv = []) {
|
|
372
|
+
const required = requiresEnv.length > 0 ? `Requires environment: ${requiresEnv.join(", ")}
|
|
373
|
+
|
|
374
|
+
` : "";
|
|
375
|
+
return required + steps.map((s) => `- ${s.t}: ${s.target}`).join("\n");
|
|
376
|
+
}
|
|
377
|
+
function recall(prompt, procs) {
|
|
378
|
+
if (!prompt) return null;
|
|
379
|
+
const promptTokens = tokenize(prompt);
|
|
380
|
+
let best = null;
|
|
381
|
+
let bestScore = { coverage: 0, matchCount: 0, targetLen: 0 };
|
|
382
|
+
for (const node of procs) {
|
|
383
|
+
if (node.enabled === false || node.parent_id) continue;
|
|
384
|
+
const score = scoreProcedure(promptTokens, node);
|
|
385
|
+
if (isBetter(score, bestScore)) {
|
|
386
|
+
bestScore = score;
|
|
387
|
+
best = node;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (!best) return null;
|
|
391
|
+
let text = formatSteps(best.steps, best.requires_env);
|
|
392
|
+
const alt = procs.find((e) => e.parent_id === best.id && e.type === "alt" && e.enabled !== false);
|
|
393
|
+
if (alt) text += "\n\nAlternative path that worked:\n" + formatSteps(alt.steps, alt.requires_env);
|
|
394
|
+
return {
|
|
395
|
+
node: best,
|
|
396
|
+
text: `Reflex memory found a procedure for this task:
|
|
397
|
+
|
|
398
|
+
${text}
|
|
399
|
+
|
|
400
|
+
Follow these steps instead of figuring it out from scratch.`
|
|
401
|
+
};
|
|
402
|
+
}
|
|
53
403
|
|
|
54
404
|
// lib/engine.js
|
|
55
405
|
function handleEvent(evt, reflexDir, deps = {}) {
|
|
@@ -62,84 +412,83 @@ function handleEvent(evt, reflexDir, deps = {}) {
|
|
|
62
412
|
var GROOM_THRESHOLD = 5;
|
|
63
413
|
function onPrompt(evt, reflexDir) {
|
|
64
414
|
const { sessionId, promptId, prompt } = evt;
|
|
65
|
-
|
|
66
|
-
|
|
415
|
+
const sanitized = sanitizeCommand(prompt);
|
|
416
|
+
startTrace(reflexDir, sessionId, promptId, {
|
|
417
|
+
t: "Goal",
|
|
418
|
+
prompt: sanitized.text,
|
|
419
|
+
...sanitized.requiresEnv.length > 0 ? { requires_env: sanitized.requiresEnv } : {},
|
|
420
|
+
...sanitized.unsafe ? { contains_unresolvable_secret: true } : {}
|
|
421
|
+
});
|
|
422
|
+
const hit = recall(sanitized.text, readAllProcedures(reflexDir));
|
|
67
423
|
if (!hit) return { context: null };
|
|
68
424
|
appendTrace(reflexDir, sessionId, promptId, { t: "Recall", hit: hit.node.id });
|
|
69
425
|
return { context: hit.text };
|
|
70
426
|
}
|
|
71
|
-
function recall(prompt, procs) {
|
|
72
|
-
if (!prompt) return null;
|
|
73
|
-
const p = prompt.toLowerCase();
|
|
74
|
-
const fmt = (steps) => steps.map((s) => `- ${s.t}: ${s.target}`).join("\n");
|
|
75
|
-
for (const node of procs) {
|
|
76
|
-
if (node.enabled === false || node.parent_id) continue;
|
|
77
|
-
let match = node.trigger && p.includes(node.trigger.toLowerCase());
|
|
78
|
-
if (!match && Array.isArray(node.aliases)) {
|
|
79
|
-
match = node.aliases.some((a) => p.includes(a.toLowerCase()));
|
|
80
|
-
}
|
|
81
|
-
if (!match) continue;
|
|
82
|
-
let text = fmt(node.steps);
|
|
83
|
-
const alt = procs.find((e) => e.parent_id === node.id && e.type === "alt" && e.enabled !== false);
|
|
84
|
-
const onFail = procs.find((e) => e.parent_id === node.id && e.type === "on_fail" && e.enabled !== false);
|
|
85
|
-
if (alt) text += "\n\nAlternative path that worked:\n" + fmt(alt.steps);
|
|
86
|
-
if (onFail) text += "\n\nIf it fails, this recovery worked:\n" + fmt(onFail.steps);
|
|
87
|
-
return {
|
|
88
|
-
node,
|
|
89
|
-
text: `Reflex memory found a procedure for this task:
|
|
90
|
-
|
|
91
|
-
${text}
|
|
92
|
-
|
|
93
|
-
Follow these steps instead of figuring it out from scratch.`
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
427
|
function onTool(evt, reflexDir) {
|
|
99
|
-
|
|
428
|
+
const sanitized = evt.tool === "Bash" || evt.tool === "bash" ? sanitizeCommand(evt.target) : sanitizeText(evt.target);
|
|
429
|
+
appendTrace(reflexDir, evt.sessionId, evt.promptId, {
|
|
430
|
+
t: evt.tool,
|
|
431
|
+
target: sanitized.text,
|
|
432
|
+
ok: evt.ok,
|
|
433
|
+
...sanitized.requiresEnv.length > 0 ? { requires_env: sanitized.requiresEnv } : {},
|
|
434
|
+
...sanitized.unsafe ? { contains_unresolvable_secret: true } : {}
|
|
435
|
+
});
|
|
100
436
|
return { context: null };
|
|
101
437
|
}
|
|
102
438
|
function onStop(evt, reflexDir, deps) {
|
|
103
439
|
const { sessionId, promptId } = evt;
|
|
104
440
|
const lines = readTrace(reflexDir, sessionId, promptId);
|
|
105
441
|
if (lines.length === 0) return { context: null };
|
|
442
|
+
if (lines.some((line) => line.contains_unresolvable_secret)) {
|
|
443
|
+
deleteTrace(reflexDir, sessionId, promptId);
|
|
444
|
+
return { context: null };
|
|
445
|
+
}
|
|
106
446
|
let goal = "";
|
|
107
447
|
let recallHit = null;
|
|
448
|
+
let hasGoal = false;
|
|
108
449
|
for (const l of lines) {
|
|
109
|
-
if (l.t === "Goal")
|
|
450
|
+
if (l.t === "Goal") {
|
|
451
|
+
goal = l.prompt;
|
|
452
|
+
hasGoal = true;
|
|
453
|
+
}
|
|
110
454
|
if (l.t === "Recall" && l.hit) recallHit = l.hit;
|
|
111
455
|
}
|
|
456
|
+
if (!hasGoal) {
|
|
457
|
+
deleteTrace(reflexDir, sessionId, promptId);
|
|
458
|
+
return { context: null };
|
|
459
|
+
}
|
|
112
460
|
if (judge(lines, evt)) {
|
|
113
|
-
const steps = synthesize(lines);
|
|
461
|
+
const { steps, requiresEnv } = synthesize(lines);
|
|
114
462
|
if (steps.length > 0) {
|
|
463
|
+
const id = Math.random().toString(36).slice(2, 11);
|
|
115
464
|
if (!recallHit) {
|
|
116
|
-
|
|
117
|
-
id
|
|
465
|
+
appendPending(reflexDir, {
|
|
466
|
+
id,
|
|
118
467
|
title: goal,
|
|
119
468
|
trigger: goal,
|
|
120
469
|
steps,
|
|
470
|
+
...requiresEnv.length > 0 ? { requires_env: requiresEnv } : {},
|
|
121
471
|
enabled: true,
|
|
122
|
-
hits: 0,
|
|
123
|
-
last_used_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
124
472
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
125
473
|
status: "pending_review"
|
|
126
474
|
});
|
|
127
475
|
} else {
|
|
128
|
-
const parent =
|
|
476
|
+
const parent = readAllProcedures(reflexDir).find((p) => p.id === recallHit);
|
|
129
477
|
const parentSteps = parent ? parent.steps : [];
|
|
130
478
|
const same = parentSteps.length === steps.length && parentSteps.every((s, i) => s.t === steps[i].t && s.target === steps[i].target);
|
|
131
479
|
if (!same) {
|
|
132
|
-
|
|
480
|
+
appendPending(reflexDir, {
|
|
481
|
+
id,
|
|
133
482
|
parent_id: recallHit,
|
|
134
483
|
type: "alt",
|
|
135
484
|
steps,
|
|
485
|
+
...requiresEnv.length > 0 ? { requires_env: requiresEnv } : {},
|
|
136
486
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
137
487
|
status: "pending_review"
|
|
138
488
|
});
|
|
139
489
|
}
|
|
140
490
|
}
|
|
141
|
-
|
|
142
|
-
if (pending >= GROOM_THRESHOLD && deps.spawnGroomer) deps.spawnGroomer();
|
|
491
|
+
if (countIngest(reflexDir) >= GROOM_THRESHOLD && deps.spawnGroomer) deps.spawnGroomer();
|
|
143
492
|
}
|
|
144
493
|
}
|
|
145
494
|
deleteTrace(reflexDir, sessionId, promptId);
|
|
@@ -174,17 +523,20 @@ function judge(lines, evt) {
|
|
|
174
523
|
}
|
|
175
524
|
function synthesize(lines) {
|
|
176
525
|
const steps = [];
|
|
526
|
+
const requiresEnv = /* @__PURE__ */ new Set();
|
|
177
527
|
for (const l of lines) {
|
|
528
|
+
for (const name of l.requires_env || []) requiresEnv.add(name);
|
|
178
529
|
if (l.t === "Goal" || l.t === "Recall" || l.ok === false) continue;
|
|
179
530
|
if (l.t === "Read" || l.t === "View") continue;
|
|
180
531
|
if (l.t === "Bash" || l.t === "bash") {
|
|
181
532
|
const target = l.target || "";
|
|
533
|
+
if (!target.trim()) continue;
|
|
182
534
|
if (PURE_READ_CMDS.includes(target.trim().split(" ")[0])) continue;
|
|
183
535
|
if (/^git (status|log|diff)/.test(target)) continue;
|
|
184
536
|
}
|
|
185
537
|
steps.push({ t: l.t, target: l.target });
|
|
186
538
|
}
|
|
187
|
-
return steps;
|
|
539
|
+
return { steps, requiresEnv: [...requiresEnv] };
|
|
188
540
|
}
|
|
189
541
|
|
|
190
542
|
// lib/groomer.js
|
|
@@ -345,48 +697,37 @@ function resolveCredentials(env = process.env) {
|
|
|
345
697
|
}
|
|
346
698
|
|
|
347
699
|
// lib/groomer.js
|
|
348
|
-
|
|
349
|
-
|
|
700
|
+
var GROOM_MAX_ATTEMPTS = 3;
|
|
701
|
+
function logError(reflexDir, msg) {
|
|
350
702
|
const logFile = path3.join(reflexDir, "worker.log");
|
|
351
703
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
352
704
|
try {
|
|
353
705
|
fs3.appendFileSync(logFile, `[${timestamp}] ${msg}
|
|
354
706
|
`, "utf8");
|
|
355
|
-
} catch
|
|
707
|
+
} catch {
|
|
356
708
|
}
|
|
357
709
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
710
|
+
function mergeStable(stable, batchIds, keptFromBatch) {
|
|
711
|
+
const withoutBatch = stable.filter((n) => !n.id || !batchIds.has(n.id));
|
|
712
|
+
const byId = /* @__PURE__ */ new Map();
|
|
713
|
+
const anonymous = [];
|
|
714
|
+
for (const n of [...withoutBatch, ...keptFromBatch]) {
|
|
715
|
+
if (n.id) byId.set(n.id, n);
|
|
716
|
+
else anonymous.push(n);
|
|
365
717
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
718
|
+
return [...byId.values(), ...anonymous];
|
|
719
|
+
}
|
|
720
|
+
async function processBatch(reflexDir, processingFile, creds) {
|
|
721
|
+
const batch = readProcessing(reflexDir);
|
|
722
|
+
if (batch.length === 0) {
|
|
723
|
+
unlinkWithRetry(processingFile);
|
|
724
|
+
return { ok: true, deferred: 0 };
|
|
372
725
|
}
|
|
373
|
-
const { provider, apiKey, model } = creds;
|
|
374
|
-
if (!fs3.existsSync(proceduresFile)) return;
|
|
375
|
-
const lines = fs3.readFileSync(proceduresFile, "utf8").split("\n").filter(Boolean);
|
|
376
|
-
const nodes = lines.map((l) => {
|
|
377
|
-
try {
|
|
378
|
-
return JSON.parse(l);
|
|
379
|
-
} catch (e) {
|
|
380
|
-
return null;
|
|
381
|
-
}
|
|
382
|
-
}).filter(Boolean);
|
|
383
|
-
const pending = nodes.filter((n) => n.status === "pending_review");
|
|
384
|
-
if (pending.length === 0) return;
|
|
385
726
|
const prompt = `
|
|
386
|
-
Here are ${
|
|
727
|
+
Here are ${batch.length} new execution nodes/edges added by a coding agent.
|
|
387
728
|
|
|
388
729
|
<edges>
|
|
389
|
-
${JSON.stringify(
|
|
730
|
+
${JSON.stringify(batch, null, 2)}
|
|
390
731
|
</edges>
|
|
391
732
|
|
|
392
733
|
For each item:
|
|
@@ -408,46 +749,77 @@ Return ONLY a JSON array of objects with this schema:
|
|
|
408
749
|
`;
|
|
409
750
|
let responseText;
|
|
410
751
|
try {
|
|
411
|
-
responseText = await PROVIDERS[provider].complete(apiKey, model, prompt);
|
|
752
|
+
responseText = await PROVIDERS[creds.provider].complete(creds.apiKey, creds.model, prompt);
|
|
412
753
|
} catch (err) {
|
|
413
|
-
logError(
|
|
414
|
-
return;
|
|
754
|
+
logError(reflexDir, err.message);
|
|
755
|
+
return { ok: false, deferred: 0 };
|
|
415
756
|
}
|
|
416
757
|
let actions = [];
|
|
417
758
|
try {
|
|
418
759
|
const jsonStr = responseText.replace(/```json/g, "").replace(/```/g, "").trim();
|
|
419
760
|
actions = JSON.parse(jsonStr);
|
|
420
|
-
} catch
|
|
421
|
-
logError(
|
|
422
|
-
|
|
423
|
-
"Failed to parse LLM JSON response: " + responseText
|
|
424
|
-
);
|
|
425
|
-
return;
|
|
761
|
+
} catch {
|
|
762
|
+
logError(reflexDir, "Failed to parse LLM JSON response.");
|
|
763
|
+
return { ok: false, deferred: 0 };
|
|
426
764
|
}
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
765
|
+
const kept = [];
|
|
766
|
+
const deferred = [];
|
|
767
|
+
for (const node of batch) {
|
|
768
|
+
const actionDef = actions.find((a) => a.id === node.id);
|
|
769
|
+
if (!actionDef) {
|
|
770
|
+
const attempts = (node.groom_attempts || 0) + 1;
|
|
771
|
+
if (attempts < GROOM_MAX_ATTEMPTS) deferred.push({ ...node, groom_attempts: attempts });
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (actionDef.action === "DROP") continue;
|
|
775
|
+
const next = { ...node };
|
|
776
|
+
delete next.status;
|
|
777
|
+
delete next.groom_attempts;
|
|
778
|
+
if (!next.parent_id && Array.isArray(actionDef.aliases)) {
|
|
779
|
+
next.aliases = actionDef.aliases;
|
|
441
780
|
}
|
|
442
|
-
|
|
781
|
+
kept.push(next);
|
|
443
782
|
}
|
|
444
|
-
const
|
|
445
|
-
|
|
783
|
+
const resolvedIds = new Set(
|
|
784
|
+
batch.filter((n) => actions.some((a) => a.id === n.id)).map((n) => n.id).filter(Boolean)
|
|
785
|
+
);
|
|
786
|
+
const merged = mergeStable(readProcedures(reflexDir), resolvedIds, kept);
|
|
787
|
+
writeJsonlAtomic(paths(reflexDir).proceduresFile, merged);
|
|
788
|
+
unlinkWithRetry(processingFile);
|
|
789
|
+
for (const node of deferred) appendPending(reflexDir, node);
|
|
790
|
+
return { ok: true, deferred: deferred.length };
|
|
791
|
+
}
|
|
792
|
+
async function runGroomer(reflexDir) {
|
|
793
|
+
if (!tryAcquireGroomerLock(reflexDir)) return { ran: false, reason: "locked" };
|
|
446
794
|
try {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
795
|
+
migrateLegacyPending(reflexDir);
|
|
796
|
+
let creds;
|
|
797
|
+
try {
|
|
798
|
+
creds = resolveCredentials();
|
|
799
|
+
} catch (e) {
|
|
800
|
+
logError(reflexDir, e.message);
|
|
801
|
+
return { ran: false, reason: "creds" };
|
|
802
|
+
}
|
|
803
|
+
if (!creds) {
|
|
804
|
+
logError(
|
|
805
|
+
reflexDir,
|
|
806
|
+
"No API key found. Run `npx agent-procedures auth login` or set ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY."
|
|
807
|
+
);
|
|
808
|
+
return { ran: false, reason: "creds" };
|
|
809
|
+
}
|
|
810
|
+
let batches = 0;
|
|
811
|
+
while (batches < 8) {
|
|
812
|
+
const processingFile = claimPendingBatch(reflexDir);
|
|
813
|
+
if (!processingFile) break;
|
|
814
|
+
const { ok, deferred } = await processBatch(reflexDir, processingFile, creds);
|
|
815
|
+
if (!ok) break;
|
|
816
|
+
batches++;
|
|
817
|
+
if (deferred > 0) break;
|
|
818
|
+
if (countIngest(reflexDir) === 0) break;
|
|
819
|
+
}
|
|
820
|
+
return { ran: true, batches };
|
|
821
|
+
} finally {
|
|
822
|
+
releaseGroomerLock(reflexDir);
|
|
451
823
|
}
|
|
452
824
|
}
|
|
453
825
|
|
|
@@ -500,18 +872,18 @@ var claude_default = {
|
|
|
500
872
|
return {
|
|
501
873
|
type: "stop",
|
|
502
874
|
...base,
|
|
503
|
-
interrupted:
|
|
875
|
+
interrupted: false,
|
|
504
876
|
busy: Array.isArray(payload.background_tasks) && payload.background_tasks.length > 0
|
|
505
877
|
};
|
|
506
878
|
}
|
|
507
879
|
return null;
|
|
508
880
|
},
|
|
509
881
|
/** Engine result → what to print on stdout for Claude. */
|
|
510
|
-
render(evt,
|
|
882
|
+
render(evt, result2) {
|
|
511
883
|
if (evt.type !== "prompt") return "";
|
|
512
|
-
if (!
|
|
884
|
+
if (!result2.context) return "{}";
|
|
513
885
|
return JSON.stringify({
|
|
514
|
-
hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext:
|
|
886
|
+
hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: result2.context }
|
|
515
887
|
});
|
|
516
888
|
}
|
|
517
889
|
};
|
|
@@ -539,6 +911,7 @@ function readStdin() {
|
|
|
539
911
|
});
|
|
540
912
|
}
|
|
541
913
|
function spawnGroomer() {
|
|
914
|
+
if (isGroomerLocked(REFLEX_DIR)) return;
|
|
542
915
|
const child = spawn(process.argv[0], [process.argv[1], "Groom"], { detached: true, stdio: "ignore" });
|
|
543
916
|
child.unref();
|
|
544
917
|
}
|
|
@@ -546,7 +919,7 @@ async function run(harnessId, argv = process.argv.slice(2)) {
|
|
|
546
919
|
const [eventName] = argv;
|
|
547
920
|
if (!eventName) return;
|
|
548
921
|
if (eventName === "Groom") {
|
|
549
|
-
await runGroomer(
|
|
922
|
+
await runGroomer(REFLEX_DIR);
|
|
550
923
|
return;
|
|
551
924
|
}
|
|
552
925
|
const raw = await readStdin();
|
|
@@ -560,8 +933,8 @@ async function run(harnessId, argv = process.argv.slice(2)) {
|
|
|
560
933
|
const harness = getHarness(harnessId);
|
|
561
934
|
const evt = harness.normalize(eventName, payload);
|
|
562
935
|
if (!evt) return;
|
|
563
|
-
const
|
|
564
|
-
const out = harness.render(evt,
|
|
936
|
+
const result2 = handleEvent(evt, REFLEX_DIR, { spawnGroomer });
|
|
937
|
+
const out = harness.render(evt, result2);
|
|
565
938
|
if (out) process.stdout.write(out + "\n");
|
|
566
939
|
}
|
|
567
940
|
export {
|